Rust 所有権とは何か

この記事では Rust 初心者向けに Rust の所有権の考え方を説明します。とはいっても、私自身 The Rust Programming Language の一部を読んで、Rust のコードを 1,000 行ほど書いた程度の理解度です。そのわりに「Rust の世界観を風船で表現しよう!」というヘンな切り口での説明を試みてます。間違ってたらすみません。書きたい衝動を抑えられなかった...。ってことで始めましょう。

なぜか関数が1度しか実行できない...

Rust はヘンな言語です。いろんな言語に触れたことのあるプログラマであっても、Rust を触り始めると知らない概念がたくさん。まさに未知との遭遇です。

手始めに hello というメッセージを2度表示するプログラムをこんな感じに書いてみました。

fn f(t: String) {
    println!("{}", t);
}

fn main() {
    let s = String::from("hello");
    f(s);
    f(s);
}

文字列 s を引数に与えて関数 f を2度実行するだけです。

...しかし、残念ながら以下のようなコンパイルエラーが発生します。

$ rustc main.rs 
error[E0382]: use of moved value: `s`
 --> main.rs:8:7
...

ちなみに関数 f を1度だけ実行するプログラムならコンパイルエラーにはなりません。なにか Rust には他の言語とは違う秘密がありそうです。
本記事では、このプログラムの例を通じて Rust の "所有権" を理解しましょう。

ようこそ風船の世界へ!

Rust の気持ちを理解するため、とある世界を考えてみましょう。

この世界の "住人" は変数です。そして、"風船" が値に対応します。たとえば、以下の代入文では、右辺の String::from("hello") によって得られる値が左辺の変数 s に代入されます。

let s = String::from("hello");

これを以下のように図示することにします。

f:id:t-keita:20210315034410p:plain

住人 "s" が値に対応する風船を持っているだけです。ちなみに風船にはヘリウムガスが入っていて、住人が手を放すと空へ飛んでいってしまいます。本記事では "値" としてスタック領域ではなくヒープ領域に格納されるもののみを扱うものとしています。

変数がスコープに所属するのと同様に、住人は特定の範囲に住んでいます。たとえば、以下のプログラムではスコープ 'a 中に変数 s が存在します。

{// 'a
   let s = String::from("hello"); 
}

これを図示すると以下になります。住人は 'a と書かれた高台に住んでいます。

f:id:t-keita:20210315021124p:plain

そして、プログラムの実行が進むにつれて、この世界では住人が移動します。たとえば以下のプログラムを考えます。プログラムの実行に対応する時刻 t1 と t2 をソースコードコメントとして記載しています。

...
{// 'a
   let s = String::from("hello"); 
   // time = t1
   ...
}
// time = t2

このプログラムの実行を図示すると以下になります。

f:id:t-keita:20210315021041p:plain

左の図が時刻 t1 で、右の図が時刻 t2 に対応します。時刻 t1 では住人が風船をしっかり持っています。しかし、時刻 t2 では住人が高台 'a から落ちてしまいました。住人はあまり丈夫ではありません。悲しいことに住人は息絶え、風船は空の彼方へ消え去ります。これは、変数がスコープの外に出たとき、その変数が参照していたメモリが自動的に解放されることを示しています。

一見すると悲しい世界ですが、実はこの仕組みによって世界は "クリーン" に保たれています。風船にはヘリウムガスが入っているので、住人が息絶えたタイミングで風船は空の彼方に消えてゆくのでした。では、この風船が水風船だったらどうでしょう?住人が息絶えると水風船は地面に落ち、ゴミとしてその場に残り続けます。そしてプログラムの実行が進むつれて世界がゴミだらけになってゆきます。そのためゴミを定期的に掃除したくなるのですが、これはまさに Java などに見られるガーベジコレクションです。

他方、住人の生死問わず不要になったタイミングで水風船を捨てるように管理するとしましょう。これはいわゆるメモリの解放であり C/C++ などの基本戦略です。しかし、不要になったタイミングで水風船を捨てるのを忘れ、メモリリークとして問題化してきたわけです。

このような背景から、Rust では変数が生存区間から出るとき、その変数が参照している値を自動的に解放する仕組みが採用されています。ガーベジコレクションも明示的なメモリの解放も不要なわけです。その結果、Rust は圧倒的な実行性能を保ちつつ、メモリ安全性を担保するといったシステムプログラミングに最適な強みを持つわけです。

所有権とムーブ

住人が息絶えるタイミングで風船も消え去る。こんな世界では代入文がもつ意味すらひと味違います。代入は風船の受け渡しになります。

以下の代入文を考えてみましょう。変数 s の値を変数 t に代入するだけです。

let s = String::from("hello");
let t = s;

これは以下のように図示できます。

f:id:t-keita:20210314125852p:plain

左の図が代入前で、右の図が代入後です。ここで重要なのは、住人 s は風船を手放してしまっていることです。つまり、変数 s はもはや値に関する情報を持っていません。

Rust ではこの状況を「変数 s が持っていた値の 所有権(ownership) が変数 tムーブ(move)された」という言い方をします。この "所有権" と "ムーブ" こそが Rust の重要な概念のひとつです。この世界では、"所有権" は住人が風船を持っていることに相当し、"ムーブ" は風船の受け渡しに相当します。

ここまで来ると、以下のプログラムがコンパイルエラーになるのも納得できるはずです。

let s = String::from("hello");
let t = s;
println!("{}", s); // compile error!

代入(ムーブ)によって住人 s が風船を手放した後に、住人 s がもつ風船を使おうとしている状況を思い浮かべられたらオッケーです。このような状況が発生することは、実際にプログラムを実行するまでもなく Rust コンパイラによって検出されます。

実際、以下のようなコンパイルエラーが発生します。

$ rustc main.rs 
error[E0382]: borrow of moved value: `s`
 --> main.rs:4:20
  |
2 |     let s = String::from("hello");
  |         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
3 |     let t = s;
  |             - value moved here
4 |     println!("{}", s);
  |                    ^ value borrowed here after move

最後のメッセージ "value borrowed here after move" は、ムーブした後に変数を使おうとしてるからダメだよ、と教えてくれています。("borrow" という文言は今は気にしなくて大丈夫です。あとで説明します。)

異なるスコープ間での代入はシンプルです。以下のプログラムでは、内側のスコープに属する変数へ所有権をムーブしています。

{// 'b
   let s = String::from("hello");
   {// 'a
      let t = s;
   }
}

図は以下のような感じでしょうか。

f:id:t-keita:20210316000829p:plain

この世界では変数のスコープには高台が対応するのでした。住人 s が風船を手放すと風船は宙へ浮き上がり、住人 t がキャッチします。これで無事 "上の階" に住む住人に風船をパスできました。

"下の階" に住む住人へ風船をパスすることでもできます。このプログラムを考えてみます。

{// 'b
   let t;
   {// 'a
      let s = String::from("hello");
      t = s;
   }
}

これを図示すると以下になります。

f:id:t-keita:20210314135446p:plain

上の階に住む住人 s が糸を垂らし、それを下の階の住人 t がキャッチします。住人 s が風船から手を放すと風船の受け渡しができます。ちなみに、プログラムの実行時にはスコープ 'a を抜けるタイミングで上の階の住人 sは息絶えます。しかし、風船は下の階の住人 t に引き継がれたので、風船が空へ消えてゆくことはありません。

関数呼び出し

この世界では、関数は文字通り雲の上の存在です。具体的に見ていきましょう。

まずは以下の関数 f を考えてみます。

fn f(t: String) {
    println!("{}", t);
}

これを図示すると以下のようになります。

f:id:t-keita:20210315024145p:plain

関数そのものは雲の上に存在します。関数の仮引数は住人として表現され、実引数となる風船を今か今かと待ち構えています。では実際に、この関数を実行してみましょう。

以下のようなプログラムで関数 f を実行します。

fn f(t: String) {
    println!("{}", t);
}

fn main() {
    let s = String::from("hello");
    f(s);
}

これを図示すると以下になります。

f:id:t-keita:20210315025530p:plain

左の図は関数 f の実行前の状態で住人 s が風船を持っています。真ん中の図は関数 f を実行するために引数として風船の受け渡しを行った様子です。先ほどの ”上の階" に風船を渡すのと同じように、下の住人 s が風船を手放し、それを住人 t がキャッチする流れです。右の図は関数の実行が終了した直後の様子です。プログラム実行にともなって住人は移動するのでした。雲の上に住む住人も例外ではありません。住人 s は息絶え、風船は空の彼方へ消えてゆきます。これが Rust における関数実行の流れです。つまり、引数として与えた値は、関数の仮引数へムーブされたのち、関数の終了時に消え去ります。

なぜ関数が1度しか実行できなかったのか?

ここでようやく冒頭のプログラムの例「なぜか関数が1度しか実行できない...」に戻ってきます。冒頭のプログラムを以下に再掲します。

fn f(t: String) {
    println!("{}", t);
}

fn main() {
    let s = String::from("hello");
    f(s);
    f(s); // compile error!
}

2度目の関数呼び出し f(s); がなぜエラーになるのかもうお分かりですね。この時点で住人 s が風船を持っていないのです。持ってない風船は渡せません。それがコンパイラによって検出されるため、コンパイルエラーとして指摘されるわけです。

では、関数を2度実行するにはどうすればよいでしょうか?一番素朴なやり方は、関数に渡された風船を、関数の実行が終了した後に呼び出し元に返すやり方です。以下はその例です。

fn f(t: String) -> String {
    println!("{}", t);
    return t;
}

fn main() {
    let mut s = String::from("hello");
    s = f(s);
    f(s);
}

さきほどのプログラムからの修正点は2点あります。1つ目は、関数 f の戻り値として引数に受け取った変数をそのまま返すようにした点です。2つ目は、関数 main 内の変数 s をミュータブルに変更し、関数 f の実行結果を変数 s に代入するようにした点です。これで無事関数 f を2度実行できます。

これに対応する図は以下のようになります。

f:id:t-keita:20210315031950p:plain

左の図は関数の引数として風船を受け取った状態です。真ん中の図では、住人 t が雲の上から糸を垂らすことで関数の戻り値を住人 s に返しています。右の図は関数 f の実行後の様子です。住人 t が息絶えるものの、住人 s が風船を持っているため風船が消えることはありません。住人 s が風船を持っているため、この後もう一度関数 f を実行することができるわけです。めでたしめでたし。

...しかしながら、関数を定義するときに、引数を戻り値として返すのはあまりに面倒ですし、プログラムの複雑化を招きます。なにかもっと良い方法がありそうです。そこで登場するのが Rust の重要な概念である "所有権の借用" です。

以降では借用について説明したのち、冒頭の例を借用を使っていい感じに書き直してみます。

所有権の借用(borrowing)

住人と住人の間で風船のやり取りをしてきましたが、関数呼び出しのときは一時的に風船を借りた後に返すという一連の流れが生まれるのでした。この "借りた後に返す" こそが Rust において "所有権の借用(borrowing)" という概念です。借り方は2パターンあります。1つは不変参照(immutable reference)であり、もう1つは可変参照(mutable reference)です。具体的に見ていきましょう。

不変参照(immutable reference)

まずは不変参照から見ていきましょう。

let s = String::from("hello");
let t = &s;

このプログラムでは変数 s&s という形にして変数 t に代入を行っています。これによって、変数 t は変数 s の値を参照することのみ許されます。変数 s の値を更新することは許されていません。このとき値の所有権は変数 t へムーブされません。

ここで、不変参照型の変数に対応する新たな住人を登場させます。以下の図をご覧ください。

f:id:t-keita:20210315034255p:plain

右の住人t が新たな住人です。この住人は風船を見ることだけが許されています。風船を掴むことはできません。この住人は雲の上に載ったときによい仕事をしてくれます。その様子を見てみましょう。

冒頭のプログラムを、不変参照を用いて以下のように書き換えてみます。

fn f(t: &String) {
    println!("{}", t);
}

fn main() {
    let s = String::from("hello");
    f(&s);
    f(&s);
}

元のプログラムからの変更点は、関数 f の引数を不変参照にしただけです。このプログラムを実行すると期待通りに hello というメッセージを2度表示してくれます。プログラムとしてもスッキリしていい感じです。

関数 f の実行時に対応する図は以下の通りです。

f:id:t-keita:20210316005116p:plain

関数 f の呼び出し時、地上にいる住人 s は雲の上に住む住人 t に風船を見せます(左の図)。あくまで見せるだけであって風船を渡すわけではありません。住人 t は風船から値を読み取れるので、それを使って関数の処理を行います(真ん中の図)。そして、関数の実行が終了すると住人 t は雲の上から落ちて息絶えます。しかし、風船は地上の住人 s がずっと持っていたので空の彼方へ消えてゆくことはありません。そのため、住人 s はその後も風船を他の住人に渡したり見せたりすることができます。このように、参照を使うことで引数に所有権をムーブすることなく関数を実行できました。

可変参照(mutable reference)

上記の不変参照の例では関数 f が引数の値を参照できれば十分でした。一方で、引数の値を更新する必要がある場合、不変参照では不十分です。そこで登場するのが可変参照です。

以下に可変参照を用いたプログラムの例を示します。

let mut s = String::from("hello");
let t = &mut s;

この代入文では変数 s の値を &mut s として変数 t に代入しています。その結果、変数 t は変数 s が持つ値を更新することが許されます。もちろん参照することも許されます。ただし所有権はムーブされません。あくまで一時的に更新する権利を貸し出しているだけです。

可変参照型の変数に対応する新たな住人を登場させます。以下の図をご覧ください。

f:id:t-keita:20210316011353p:plain

右の住人 t が新たな住人です。この住人は風船大好きで、風船本体をしっかり抱えて持つのが特徴です。かわいいヤツです。

あまり面白みはないですが、可変参照を用いた関数実行の例を見ておきましょう。

fn f(t: &mut String) {
    t.push('!');
    println!("{}", t);
}

fn main() {
    let mut s = String::from("hello");
    f(&mut s);
    f(&mut s);
}

関数の引数を &mut とすることで可変参照として所有権を借用しています。関数 f では引数の文字列に ! を付加したうえでそれを表示します。そのため、この関数を実行すると hello!hello!! が続けて表示されます。

これに対応する図を書くと、上記の不変参照の図のうち不変参照の住人を可変参照の住人に置き換えたものになります。あえて載せるほとではないのでここでは省略します。

Rust のルール

ここまで、所有権とその借用として不変参照と可変参照を紹介してきました。実は、これらを使うにはいくつかの決まりが存在します。プログラムを書くときはその決まりを満たすように書かなければなりません。さもなければ Rust コンパイラによってエラーとして指摘されてしまいます。しかしご心配なく。対応する風船の世界を考えることで、それらのルールに従っているかどうか直感的にイメージできます。すでに道具は揃っているのです。

所有権(ownership)のルール

まず、所有権について以下の決まりがあります。Rust プログラマは常にこれを意識してプログラムを書く必要があります。

所有権規則

(規則1) Rustの各値は、所有者と呼ばれる変数と対応している。

(規則2) いかなる時も所有者は一つである。

(規則3) 所有者がスコープから外れたら、値は破棄される。

所有権とは? - The Rust Programming Language 日本語版 から一部改変)

(規則1)は、すべての風船は住人によって持たれていることに対応します。住人に持たれていない風船は空の彼方に消えてゆくのでした。

(規則2)は、風船の所有者は常に1人であることに対応します。風船のヒモは1本しかないので、複数の住人で風船を持つことはできません。

(規則3)は、住人が足場から落ちたとき、息絶えて風船が空の彼方に消えてゆくことに対応します。

いかがでしょうか。すべて風船の世界では当たり前に思えるのではないのでしょうか。

参照(reference)のルール

次に参照を扱う上での決まりを見てゆきます。これはちょっとややこしいです。

参照の規則

(規則4) 任意のタイミングで、一つの可変参照か不変な参照いくつでものどちらかを行える。

(規則5) 参照は常に有効でなければならない。

参照と借用 - The Rust Programming Language 日本語版から一部改変)

(規則4)について、許されるパターンを以下の図を用いて説明します。

f:id:t-keita:20210316015848p:plain

左の図は、風船1つにつき、可変参照の住人が1人でも存在するときは他の可変参照や不変参照が許されないことを示しています。可変参照の住人は風船を抱え込んで隠してしまうので、他の住人が参照を行うことは許されません。右の図は風船1つに対して複数の不変参照の住人が存在できることを示しています。不変参照の住人は風船を見るだけです。みんなで仲良く風船を見ることができます。

(規則4) を破るようなケースを見ることでより理解が深まるかもしれません。以下のコードは(規則4)を満たしません。

let mut s = String::from("hello");
let t = &mut s;
let u = &s; // compile error!
println!("{}", t);

これを図に書くと以下のようになります。

f:id:t-keita:20210316020937p:plain

可変参照の住人 t が抱え込んでいる風船を不変参照の住人 u が見ることは許されません。可変参照の住人は独占欲が強く、他の住人に風船を見られることすら嫌うのです。

実際、可変参照と不変参照を同時に作ると危険なケースがあります。たとえば、以下の関数 push_all に同じ変数の可変参照と不変参照を与えたとき、不正なメモリアクセスが発生する可能性があります。

fn push_all(from: &Vec<i32>, to: &mut Vec<i32>) {
    for i in from.iter() {
        to.push(*i);
    }
}

詳しくは このページ を参照ください。

(規則5) 「参照は常に有効でなければならない。」は、参照の住人が存在している間は所有権を持つ住人は生きていなければならないことを示します。当たり前に満たされる話に聞こえるかもしれないので、以下に満たさない例を示します。このプログラムはコンパイルエラーになります。

fn f() -> &String { // compile error!
    let s = String::from("hello");
    return &s;
}

fn main() {
    let t = f();
    println!("{}", t);
}

関数 f で不変参照 &s を返していますが、その値の所有権を持つ変数 s は関数 f の呼び出し後に消え去ります。そのため、呼び出し元の変数 t は解放済みのメモリを参照してしまうわけです。

対応する図を以下に示します。

f:id:t-keita:20210316023559p:plain

地上の住人 t が雲の上の風船を見ようとしたとき、雲の上の住人 s は息絶えてしまいます。その結果、地上の住人 t は空の彼方へ消えゆく風船を見続けることになってしまいます。あまりにも住人 t が可哀想ですね。

これは、いわゆる ダングリングポインタ を防ぐための規則です。これが Rust コンパイラによって検出されるので、実行時の安全が担保されます。C/C++ のような言語だと、実行時にプログラムが落ちるしかなかったわけです。

ちなみに、関数の戻り値が参照になっていても、この規則を満たすようなケースは存在します。例えば、戻り値がその関数の引数の参照になっているケースや、'static なライフタイムを持つ参照になっているケースです。これについては次回の「ライフタイム」編で書きたいと思います。

いかがでしょうか。いろんな決まりがありましたが、どれも風船の世界を考えることで、何がまずいのか直感的にイメージできたのではないでしょうか?

さいごに

この記事では、風船と住人からなる世界を通して Rust の気持ちを可視化してみました。我ながら(私の理解している範囲においては)よい説明になっていると思います。Rust の学習曲線が険しいということはよく聞くので、とくに初心者の理解の助けになれば幸いです。

この記事の続きとして、近いうちにライフタイム編を書きたいと思います。ただ、たくさんの絵を書くはしんどい...。