024号文書

主にプログラミング

PythonistaがRustはじめました#005 -- 所有権

Rustの最初の難関っぽい所有権についてまとめてみます。

参考までに、Rustの学習曲線として以下のグラフがあります。 ライフタイムは所有権に関係していると思っています。

https://keens.github.io/slide/rustnokoremadetokorekara/

100時間はROMってろ!って感じですね。

それでは、公式ドキュメントを読みながら理解していきましょう。 https://doc.rust-jp.rs/book/second-edition/ch04-01-what-is-ownership.html

所有権規則

以下の3つのルールが基礎を成すようです。

  • ルール1: Rustの各値は、所有者と呼ばれる変数と対応している。
  • ルール2: いかなる時も所有者は一つである。
  • ルール3: 所有者がスコープから外れたら、値は破棄される。

上記の規則により、Rustでは ガベージコレクション 機能はないようです。

この所有権規則はプログラマにどのような制約を課すのでしょうか? NG例が分かりやすいですね。

let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s1);

大多数のプログラミング言語では、上記のようなコードは問題なく hello, world! を出力するでしょう。 しかし、Rustでは上記コードをコンパイルすると、以下のコンパイルエラーが出ます。

error[E0382]: use of moved value: `s1`
              (ムーブされた値の使用: `s1`)
 --> src/main.rs:5:28
  |
3 |     let s2 = s1;
  |         -- value moved here
4 |
5 |     println!("{}, world!", s1);
  |                            ^^ value used here after move
  |                               (ムーブ後にここで使用されています)
  |
  = note: move occurs because `s1` has type `std::string::String`, which does
  not implement the `Copy` trait
    (注釈: ムーブが起きたのは、`s1`が`std::string::String`という
    `Copy`トレイトを実装していない型だからです)

これは、所有権のルール1, 2に反するためです。 Rustではヒープ領域に存在する "hello" というデータに対して、それを所有する変数という概念があります(ルール1)。 そして、"hello" の所有権を持つ変数はs2のみです(ルール2)。 let s2 = s1; で "hello" の所有権は変数 s1 から s2 に移ります。 その結果、s1の "hello" の所有権は放棄されるので、s1を通して "hello" にアクセスはできなくなってしまいます。

この所有権のルールは、関数の引数渡しにおいても成り立ちます。

fn main() {
    let s = String::from("hello");  // sがスコープに入る

    takes_ownership(s);             // sの値が関数にムーブされ...
                                    // ... ここではもう有効ではない
}

fn takes_ownership(some_string: String) {
    println!("{}", some_string);
} 

上記のサンプルコードにおいて、 takes_ownership に s が渡されていますが、これにより、 "hello" の所有権は take_ownership の some_string に移ります。 したがって、 take_ownership 呼び出し後、 s を通して "hello" にアクセスできなくなります。

借用

所有権ルールに関するサンプルコードを見ると分かる通り、所有権ルールは非常に強い制約です。 とくに関数の引数渡しで一々所有権の譲渡が行われるのはしんどすぎます。 変更するなら譲渡の手続きはやむを得ないとしても、参照させるだけならもうちょっと簡略化できないものかと思ってしまいます。 この簡略化手続きが借用になります。

以下のサンプルコードが分かりやすいです。

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    // '{}'の長さは、{}です
    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

ポイントは引数に&をつけること。 ABC116-BのHashSet.containsの引数につけた謎&は参照渡しの&だったという訳です! 上記コードは、sに対して変更を加える訳ではないので、所有権ではなく参照権だけもらえればいいや、という気持ちを表現しているのです。 HashSetのcontainsも引数が集合に属すかどうかを判定するのに使うだけなので、参照権だけで十分ですよね? したがって、所有権を渡すというオーバーキルを避け、&をつけて参照権だけ渡すことになっているのです。

ちなみに、mutをつけることで、可変な参照権を渡すことができます。ただし、可変な参照権は一つしか渡せない、既に不変な参照権を渡している場合、可変な参照権は渡せない、などの制約があります(トランザクション分離レベルっぽいですねぇ)。

ほとんど、公式ドキュメントの引用になってしまいましたが、ひとまず以上です。 なお、ここまでの話ではABC098-Aのコンパイルエラーが error[E0597]: borrowed value does not live long enough 説明できていないと思います。 これは、ライフタイムの話をまとめる際に解説しようと思っています。