APC 技術ブログ

株式会社エーピーコミュニケーションズの技術ブログです。

株式会社 エーピーコミュニケーションズの技術ブログです。

Pythonコードで考えるRustの所有権

はじめに

こんにちは、コンテナソリューション事業部の髙井です。

Rustの所有権について、とっかかりやすいPythonから考えてみる記事です。

最近だとPythonは基本情報技術者試験にもチョイスされるようになりましたね。

本記事は以下の記事の補足記事になっています。

techblog.ap-com.co.jp

よくある参照のバグ

まずは、リストaを変数bに代入する例を見てみましょう。

a = [1, 2, 3]
b = a
b[0] = 5
print(a) #=> [5, 2, 3]

このように、baを代入すると、bを書き換えたときにaの内容も書き換わっているということが生じます。
これは、リストを格納する変数は「参照」であり、abが同じ実データを参照していることが理由です。意図して書く場合は良いとして、多くの場合、意図に反して書き換えてしまいバグの温床になります。

こうした状況はRustでは生じません。Pythonと同じように実装した以下のようなコードはそもそもコンパイルエラーになるからです。

fn main() {
    let a = vec![1, 2, 3];
    let mut b = a;
    b[0] = 5;
    println!("{:?}", a);
}

let mut b = a;のようにabに束縛(Rustでは代入ではなく束縛といいます)した時点で、aの所有権がbに移るため、それ以降aにアクセスすることはできません。 このようにして意図せぬ書き換えが起きる状況をコンパイル段階で防いでくれます。

少し補足をしておくと、PythonのListに対応するのがRustだとVecです。そのVecを生成するのがvec!の記述になります(マクロといいます)。

また、Rustだと変数はデフォルトで書き換え不可のため、書き換えを行うbについてはlet mut b = a;のように明示的な修飾子mutを付与して宣言する必要があります。安全で好ましい仕様です。

整数の場合(Python)

一方で、リストではなく1など整数の場合は、話が変わります。再びPythonを例にとると、

a = 1
b = a
b = 5
print(a) #=> 1

のように整数の場合は、bにつられてaが書き換わるようなことがありません。

これは、変数が「参照」ではなく「値」だからです。というのはよくある勘違いで、厳密に言うとPythonではすべてがオブジェクトであるためintが入っている変数も「参照」です。

しかし、Pythonのintはイミュータブル(書き換え不可)のため、b = 5のように違う値を代入した時点で「別のint」への参照になります。

これを確認するために、オブジェクトの識別子を確認できる組み込み関数であるid()を用いてみましょう。

a = [1, 1, 1]
b = a
print(id(a), id(b), id(a)==id(b))
#=> 140218622681344 140218622681344 True
b[0] = 5
print(id(a), id(b), id(a)==id(b))
#=> 140218622681344 140218622681344 True

a = 1
b = a
print(id(a), id(b), id(a)==id(b))
#=> 140218624606448 140218624606448 True
b = 5
print(id(a), id(b), id(a)==id(b))
#=> 140218624606448 140218624606576 False

リストの一部を書き換える場合、書き換え後も参照先は元のリストのままです。一方で、整数を代入した場合は、書き換え後にbの参照先が元のaとは違うものになっていることがわかります。

整数の場合(Rust)

Rustでも同じように書けます。

fn main() {
    let a = 1;
    let mut _b = a;
    _b = 5;
    println!("{}", a);
}

こちらはコンパイルエラーにならず問題なく1が出力されます。ちょっとまて!所有権はどうした!となりますよね。

これはCopyトレイトがなせる業です。Copyトレイトをもつ型は、束縛の際に元の値がコピーされ所有権の移転は起こりません(新しい所有権が生まれます)。

したがって、Python的なノリで書いても所有権で怒られることはありません。Rustではプリミティブ型にはCopyトレイトが実装されています。

スタック領域とヒープ領域

なら全部の型にCopyトレイトを付ければいいじゃないか、という話にもなりません。

Pythonだと意識することはないかもしれませんが、プログラム上のデータを格納する場所にはスタック領域とヒープ領域の2種類があります。

所有権が重要になるのは主にヒープ領域に格納されるデータです。PythonにはGC(ガベージコレクタ)があるのでヒープ領域のデータはどこからも参照されなくなった段階で自動で解放されます。

一方でGCは実行時にメモリを管理するプロセスが発生するため性能面の課題があります。Rustはガベージコレクタを使用せず、所有権システムによりメモリ解放タイミングの静的解析を可能にしています。

さて、スタックとヒープについてもう少し考えてみましょう。

整数のようにコンパイル時にサイズを確定できる固定長のデータは、スタックに積むことが出来ます。スコープに入るときスタックにpushして抜ける際にpopすればよいです。つまりメモリ管理は容易です。

一方でコンパイル時にサイズを確定できないVec等の不定長のデータはヒープ領域に格納する必要があります。そして、そのヒープを指し示す参照をスタックに積みます(参照はメモリアドレス等で表せるため固定長です)。

すると、ヒープ上のデータをいつまで保持しておくべきかの問題が発生します。これをRustでは所有権、PythonではGCによって管理しているというわけです。

詳しく知りたい方は

公式ドキュメントに書いてあります。

doc.rust-jp.rs

おわりに

Rustは安全で素晴らしい言語です。使いましょう。

本記事の投稿者: 髙井 比文
AKSをメインにしたインフラとアプリの領域際をご支援することが多いです。Azureは11冠です。
Hifumi Takai - Credly