読者です 読者をやめる 読者になる 読者になる

ひだまりソケットは壊れない

ソフトウェア開発に関する話を書きます。 最近は主に Android アプリ、Windows アプリ (UWP アプリ)、Java 関係です。

まじめなことを書くつもりでやっています。 適当なことは 「一角獣は夜に啼く」 に書いています。

Rust 入門 #2 : 所有権システムと可変性

Rust

Rust 入門してる #1」 に続き、ぼちぼちと Rust を学んでます。 今日は所有権システムと可変性についてのメモです。 4 月 14 日に Rust 1.8 がリリースされたので、バージョン 1.8 のドキュメントを読んでます。 *1

所有権システムについては下記の 3 ページを読みました。

可変性については次のページ。

基本的にはそんなに難しいことは書かれてないですが、後で説明される内容が出てきたり説明があまりうまくなかったりで理解が難しい感じがしますね。 まあざっくりと理解して読み進めていけば良さそうです。

所有権システム (Ownership system)

  • Rust の最も独特で感動的な機能であり、Rust 開発者はこれに精通すべきとのこと。
  • メモリ安全がこの機能の大きな目的である。
  • コンセプトは次の 3 つ。
    • 所有権 (ownership)
    • 借用 (borrowing) とそれに関連する機能である 「参照 (reference)」
    • 生存期間 (lifetimes)

Meta

  • Rust は安全性と速度に焦点を当てている。
  • 所有権システムはゼロコストの抽象化の最たる例で、コンパイル時に解析することで実行時にコストがかからないようになっている。
  • 一方で、このシステムには学習曲線というコストがかかる。 実際の実装と開発者のメンタルモデルが違っていて、開発者が正しいと思っているコードがコンパイラに弾かれるということが最初のうちは多いかもしれない。 経験を積むことで徐々にそれは減っていく。

所有権 (Ownership)

  • 変数束縛 (variable binding) には、それらの結合先の 「所有権を持っている」 かどうかを表すプロパティがある。
  • 束縛がスコープを抜けると、Rust は束縛されているリソースを解放するであろう。
fn foo() {
    let v = vec![1, 2, 3];
}

上の例だと、新しい vector がスタック上に作られ、各要素のためのスペースはヒープ上に生成される。 v がスコープから外れると、Rust は vector に関するリソースを解放する (ヒープに割り当てられたメモリも)。

Move semantics
  • 任意のリソースに対して所有権を持つ束縛は、プログラム中に 1 つしか存在できない?
  • あるリソースに対して所有権を持つ束縛を別の束縛に割り当てた場合、所有権は新しい束縛に移り、古い束縛経由でリソースにアクセスしようとしてもコンパイルエラーが発生する。
fn foo() {
    let v = vec![1, 2, 3];
    let v2 = v; // 所有権が v から v2 に移る。
    // v 経由で vector の操作をしようとしてもコンパイルエラーになる。
}

上は別の変数束縛への割り当ての例だが、関数の引数として渡す場合もやはり所有権は移る。

Copy
  • 上で見たように、ある束縛の値を別の束縛に割り当てると普通は所有権が移る。 そのため、古い束縛を使おうとするとコンパイルエラーになる。
  • 所有権が移らないような型を定義する場合は、Copy トレイトを実装することになるらしい。
    • プリミティブ型は Copy トレイトを実装している。

参照と借用 (References and Borrowing)

単純に引数で所有権を受け取る関数を書いた場合、呼び出し元に所有権を返すために戻り値で所有権を返す必要がある。 それはめっちゃ面倒なので、参照を受け取るようにして回避する。

参照と借用

参照はリソースを所有するのではなく、所有権を借りる (借用)。 スコープを抜けたときにはリソースを解放するのではなく所有権を返す。

2 つの参照

2 種類の参照がある。

  • (通常の) 参照
    • &T
  • ミュータブルな参照
    • &mut T

通常の参照とミュータブルな参照は同時に存在はできない。 通常の参照は同時に複数個存在できるが、ミュータブルな参照は 1 つのみしか存在できない。

参照を使う際には、前置の単項演算子 * を使ってデリファレンスすればよい。 ただし、自動でのデリファレンス機能もあるっぽくて、* を使って明示的にデリファレンスしなくてもそのまま使える状況が多いっぽい。 (まだよくわかってない。)

生存期間 (Lifetimes)

  • 生存期間とは、参照が有効であるスコープを記述するもの。
  • 全ての参照は生存期間を持つが、一般的には明示しなくても良い。 (省略可能。)
  • 特別な生存期間として 「static」 がある。 これはプログラム全体を表す生存期間である。
  • 生存期間を指定する場合、参照を表す型の & の後ろに 'lifetime を付ける。
    • 例えば fn calc(val: &'static i32) { ... } という感じ。
  • ジェネリックパラメータとして生存期間を宣言することもできる。
    • 例えば fn calc<'a>(val: &'a i32) { ... } という感じ。
生存期間の省略 (lifetime elision)
  • Rust ではシグネチャでの型推論は基本的に禁止されているが、関数のシグネチャにおいては 「生存期間の省略」 が適用される。
  • 関数の引数に関連する生存期間を入力生存期間 (input lifetime)、関数の戻り値に関連する生存期間を出力生存期間 (output lifetime) という。
  • 「生存期間の省略」 のルールは次のとおり。
    • 関数の引数のうち、省略された生存期間は全て異なる生存期間のパラメータを持つ。
    • 入力生存期間が 1 つのみの場合 (省略されているかどうかに関わらない)、返り値に関連する生存期間のうち省略されているものは全て、入力生存期間と同じ生存期間となる。
    • 入力生存期間が複数ある場合でも、引数の 1 つが &self&mut self の場合は、返り値に関連する生存期間のうち省略されているものは全て、self の生存期間と同じ生存期間となる。 (これはまだよくわかってない。)
    • 上記以外は全てエラーとなる。

可変性 (Mutability)

  • Rust で 「イミュータブル」 というのは、必ずしも 「変更できない」 ことを意味するわけではない。
  • 通常の参照と mut 参照が Rust における不可変性の基礎となっている。
  • すなわち、「イミュータブル」 であれば、複数の参照が存在しても安全であるということである。

*1:前回は 1.7 のドキュメントを読んでました。 基本的には変わりないはずです。