『The Rust Programming Language』勉強会#4

クラスメソッド 福岡オフィスで iOS アプリケーションエンジニアとして働いている田辺です。先週に続き、社内で Rust の勉強会が行われたのでそのレポートを記事にします。

前回までの記事。

前回に引き続きRust の言語仕様について解説する章なので勉強会で話題になったところや個人的に気になったところのみを扱います。

所有権を理解する

所有権完全に初見です。誤りや理解にずれがあればご指摘ください。

  • ある値の所有権を持っている変数を所有者と呼ぶ
  • 変数はひとつの値しか持ていないため、変数が所有権を持っている値はただひとつ
  • 値の所有権はひとつしかないので、常に所有者となる変数もひとつ
  • 所有者がスコープから外れたら値が破棄される

データの保存領域

  • スタティック領域
  • スタック
  • ヒープ

Swift では変数を定義するとヒープ領域に必要な領域が確保され、定数はスタックに領域が確保されます。また、Swift のコンパイル時の最適化(AllocBoxToStack)でメモリプロモーションが行われ一部のオブジェクトがヒープからスタックへプロモートされたりします。

Rust はスタックの高速性を活かすため、基本的にはローカル変数などの値をスタックにおきます。長く保持する必要がある場合はBox<T>型を使ってヒープ領域を確保、そしてそのアドレスを持ったポインタをスタックに確保します。

ヒープに確保することでスコープを気にせず値を保持できますが、スタックへ同時に確保しているポインタが開放された時にヒープからも開放する必要があります。

String 型を通して所有権を理解する

  • プログラムにハードコードされる文字列リテラルは不変
  • コードを書く際にすべての文字列値が判明するわけではない
    • String 型を使用する

  • String 型の値は可変にできる

メモリと確保

  • 文字列リテラルの場合、中身はコンパイル時に判明しているので、テキストは最終的なバイナリファイルに直接ハードコードされる
  • コンパイル時にサイズが不明だったり、実行時にサイズが可変なテキスト片用にメモリをバイナリに確保しておくことは不可能なので String 型を使う
  • String 型はコンパイル時には不明な量のメモリをヒープに確保して内容を保持する
    • 使用後はメモリを返還する必要がある
  • Rust ではメモリを所有している変数がスコープを抜けるとメモリを自動的に返還する
  • 変数がスコープを抜ける際に呼ばれる関数がdrop

上記でスコープを抜けるとdropが呼ばれて変数が使っていたメモリを自動的に返還すると書きましたが、そうなると s1 と s2 が変数を抜けると同じメモリを開放しようとしてしまいます。メモリ安全性を保証するために、 Rust は確保したメモリをコピーする代わりに s1 を有効でないとします。

無効になった参照は使用できなくなります。これがムーブです。

上記のコードの s1 を println!に渡すと次のようなエラーを返します。

所有権の移動

所有権が移動(ムーブ)しているのでmove_ownership関数に string を渡した後に、println!へ渡すとエラーになります。

i32 は Copy というトレイトを実装しているので代入後も変数が使用できます。

関数の引数で所有権をムーブできるのと同様戻り値でも所有権を移動できます。この要領で所有権をもらっては返してを毎回実装するのは面倒です。関数に値だけを使わせて所有権は渡したくない時に参照と借用というしくみを使います。

参照と借用

String 型の値を加工してかつ加工前の値を使用したい場合、参照と借用を用いないなら関数に使用する際移動した所有権を戻さないといけません。

参照と借用のしくみを使い、オブジェクトへの参照のみを関数に渡したい時、&を使います。

可変の参照

参照を借用しているに過ぎないので参照はデフォルトでは不変です。しかし変数が必要に応じて可変にできるのと同様に参照も可変にできます。

この可変な参照には特定のスコープで特定のデータに対してひとつしか可変な参照を持ていないという制約があります。

この制約に反するコードを書いてみるとsecond mutable borrow occurs hereと警告され、コンパイルエラーになります。

さらに不変な参照をしている間は可変な参照をすることはできないので、先述の制約と合わせて随分と使いづらそうだなと思いましたが、未然にバグの可能性を潰せる仕組みでもありデバッグのしにくいデータ競合を避けられるのは良いことに感じました。

宙に浮いた参照

無効なメモリ領域を指すポインタ(ダングリング参照)について扱っていました。 自分が主に触れているSwiftはARCという仕組みを使ってメモリ管理を行っています。レファレンスカウンタの増減を行いメモリ管理を行ってくれる仕組みで基本的にはメモリ管理をあまり意識せずにコードを書いています。変数宣言時に循環参照を避けるためにweakとつける時など全く意識しないというわけではありません。

他にはGC(ガベージコレクタ)という仕組みを使ってメモリ管理を行っている言語が多いです。触れたことがある言語だとRubyやLispなどもそうですね。

Rustは所有権の仕組み(コンパイル時にメモリ安全性を確保する)を使ってGCによるオーバーヘッドを避けつつ手動管理による非安全性を排除したメモリ管理の仕組みを行っています。

変数はスコープを抜けるとdropされてメモリが消されてしまうので、参照をreturnしてしまうその参照は無効なメモリ領域を指してしまいます。Rustはこれをコンパイル時に警告してコンパイルを失敗させます。

最後に

所有権はRustの重要な言語仕様なので勉強会やブログを書きながら疑問を潰しつつ腹落ちさせて次の章に入ることことができました。

次は構造体について扱う章に入ります。

また社内向けですが、この社内勉強会ではプライベートリポジトリで進捗やメモの管理をしているので途中から参加できます。興味のある社内の方は参加してみてください。