開発

[binllion] Rustの所有権を理解する

// ratatui

        // パネルを描画
        frame.render_widget(&contents, main_panel);
        frame.render_widget(&contents, sub_panel_0);
        frame.render_widget(&contents, sub_panel_1);Code language: JavaScript (javascript)

ところで、同じcontentsを複数描画するために前回のコードではrender_widget()を呼び出す際に渡す引数を&contentsとしていました。

このように変数に&がつくと変数そのものではなく変数の参照を関数に渡すこととなります。

仮にcontents変数そのもの関数に渡してしまうと2回目の関数呼び出しが失敗してしまいます。

失敗してしまう原因はRustの所有権に起因しています。

この所有権の考え方がRustの学習コストを高くしている一因とも言われておりますが、Rustを使う際には避けては通れないものです。

まずは変数の動作をサンプルを通して説明し、その後革新的所有権を論じている国民的漫画風エピソードを通じてRustの所有権の説明を試みてみたいと思います。

変数と参照

fn main() {
    let foo = String::from("Hello!");
    let bar = String::from("World!");

    println!("{:p} {:} ", &foo, foo);
    println!("{:p} {:} ", &bar, bar);
}

// 出力例
// 0x7ffd0a11c7e8 Hello! 
// 0x7ffd0a11c800 World! Code language: JavaScript (javascript)

Playground

細かい話を抜きにして大まかな変数の仕組みを説明します。

概ねプログラミング言語に於いては、変数は値そのものを格納している訳ではありません。値はメモリ上の何処かに格納され、変数はそのメモリの格納先を知っているだけです。

例えば、変数fooへ Hello! という文字列を束縛するということは、変数fooが示すメモリ上に Hello! が格納される、ということになります。変数fooの値を読み取るということはメモリ格納先の値を読み取るということになります。

Rustでは、変数fooに&(アンパサンド)をつけるとメモリの格納先のアドレス自体を示します。つまり値自体ではなく、値が存在する格納場所の情報になります。

出力例を見ると変数fooのメモリ上のアドレスは 0x7ffd0a11c7e8 でそこに格納されているものは Hello! となります。

このアドレスを参照値と呼ぶこととします。

変数は参照値が示す格納先の値をやり取りしている訳なので参照値がわかれば変数と同じようなことができる、と考えられます。

参照値を元に値を表示するprint_something()を作って動作させてみましょう。参照値を持つ変数に*(アスタリスク)をつけると参照値が示すアドレスから値を取るという意味になります(参照外しと呼ばれます)。

print_something()は変数fooと同じ出力結果となりました。

fn print_something(baz: &String) {
    println!("{:p} {:} ", baz, *baz);
}

fn main() {
    let foo = String::from("Hello!");
    let bar = String::from("World!");

    print_something(&foo);

    println!("{:p} {:} ", &bar, bar);
    println!("{:p} {:} ", &foo, foo);
}

// 出力例
// 0x7fffa1d52568 Hello! 
// 0x7fffa1d52580 World! 
// 0x7fffa1d52568 Hello!Code language: JavaScript (javascript)

Playground

なぜこのような面倒なことが必要なのでしょうか!?

試しにprint_something(&foo)の&を取り除いて実行してみましょう。エラーとなるはずです。

これはprint_something()を呼び出した際にfooの所有権がこの関数内に移動したからです。

Rustでは所有権というものがあります。基本的な規則は次のとおりです。

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

https://doc.rust-jp.rs/book-ja/ch04-01-what-is-ownership.html#%E6%89%80%E6%9C%89%E6%A8%A9%E8%A6%8F%E5%89%87

この所有権を元のままに保持するためにrender_widget()では参照値を渡しています。

つまり関数に変数を引数として渡すと変数の所有権がそちらに移動してしまい、後続の処理で同じ変数を使う場合に所有権がないためにエラーとなるのでそれを防ぐために参照値を渡すようにしているのです。

他言語では変数のコピー処理に相当する書き方がRustでは所有自体を移動する行為になり、以後使えなくなるということです(例外あり、後述)。

所有権の例

ざっくりと言ってしまうとRustの所有権とは現実世界と同じでAさんの所有しているものはAさんのものでありBさんのものではない、ということです。

Bさんに何かしら処理をお願いする場合はBさんに所有権を渡す必要がある、と。Aさんが再び所有権を取り戻すにはBさんが処理を終えた後、所有権を返すように約束しておかなくてはなりません。

ただし、処理のたびに所有権を渡しているとその都度所有権のやり取りの処理も必要になるので、一時的な所有権を渡す仕組みとしてRustでは参照という仕組みで他者に所有物に対して変更などを行うこともできるようになっています。

ここからは所有権を簡潔に理解できるようにエピソードを通じて1説明を試みます。

さらに深い理解には線形論理を学ぶと良いかもしれません。

(不変)参照

S夫

どうだ すごいだろ!ラジコンマニアのいとこがつくったドローンさ!

重さ50gしかない超小型なのに最高じそく80kmも出るんだ!アクロバット飛行だってお手のもの、宙返りだったできるんだぞ!

AIせいぎょで障害物を自動検知してしょうとつ回避してくれるスグレモノさ!


N太

ワー

スゴい!

ぼくにもそうじゅうさせてよ!


S夫

N太は壊すからだめ

こんなさいしんドローンを見ることができただけでもしあわせだろ!

そんなにそうじゅうしたければ買ったら?

きみにはむりだろうけど


N太がS夫のドローン(変数)をそうじゅう(変数の内容を変更)したければ、S夫からラジコンのそうじゅう権(所有権)をゲットするしかありません。

しかし、不変変数であるけど壊される恐れがあるのでS夫はラジコンをそうじゅうさせません(所有権を与えてくれません)。ただしN太はラジコンを見ること(参照値をゲット)だけはできます。所有権は移動していません。

Rustコード的に表すとこんな感じでしょう。

fn main() {
    let s夫 = String::from("すごいドローン");
    let n太 = &s夫;

    println!("{:} ", s夫);   // s夫は所有権を持っている
    println!("{:} ", *n太);  // n太は値を見ることはできる
}
Code language: JavaScript (javascript)

Playground

可変参照

しZちゃん

きれいな包装ね

すてき!宝石箱みたい!!


S夫

めったにてにはいらない高名なパティシエがオーナーの有名店の高級チョコレートさ。パパがヨーロッパみやげで買ってきたんだ。

みためだけでなく味もいいんだ。つかっているのは厳選されたカカオ豆だけ。その豆から抽出したカカオバターしかだせない濃厚な味わい。それでいてなめらかな口どけなんだ。

きょうはきみたちにこのチョコレートをわけてあげるよ

ぼくはもうたべあきたからね


N太

いいの!?


しZちゃん

ほんと!?

うれしいわ!!


S夫

家にはまだ食べきれないくらいたくさんあるんだ

おすそ分けとおもってね

まあ、しょみんの君たちにこのじょうひんな味がわかるとはとてもおもえないけど


しZちゃん

ありがとう S夫さん


N太

やったー

さっきからよだれがとまらなくてしようがなかったんだ!

いっただきま~す


S夫

わるいなN太

この&mutはひとりようなんだ


・任意のタイミングで、「一つの可変参照」か「不変な参照いくつでも」のどちらかを行える。
・参照は常に有効でなければならない。

https://doc.rust-jp.rs/book-ja/ch04-02-references-and-borrowing.html#%E5%8F%82%E7%85%A7%E3%81%AE%E8%A6%8F%E5%89%87

Rustでは参照に関しても所有権と同様に規則があります。

すなわち、変数と同様に参照にも可変と不変の参照があります。変数と同じように可変にする場合は&mutとします。

しかし、同時に変更されると値が壊れるおそれがありますので可変参照は同時に一つだけ許可されます。可変参照が機能している間は不変参照は使えません。

つまり、S夫はチョコレートをたべられてもいい(変数の値が変更されても良い可変変数)ので、しZちゃんにチョコレートをわけてあげました(所有権は与えないが値を変更可能な可変参照)。

しZちゃんが食べている間(可変参照が有効な間)は他の人は手出しできません。不変参照ですら許されません。N太はなにもできません2。この場合も所有権は移動していません。

fn main() {
    let mut s夫 = String::from("有名店の高級チョコレート");
    let しzちゃん = &mut s夫;
    // let n太 = &mut s夫; // 可変参照は2つ同時に作れないのでエラー
    // let n太 = &s夫; // 可変参照と不変参照は2つ同時に作れないのでエラー

    // println!("{:} ", s夫); // 可変参照と不変参照はどちらか1つしか同時に作れないのでエラー
    *しzちゃん = "はんぶんなくなった有名店の高級チョコレート".to_string();
    println!("{:} ", *しzちゃん);

    // println!("{:} ", *n太);  // 参照が作れなかったので実行できない

    s夫 = "のこりもなくなった有名店の高級チョコレート".to_string();
    println!("{:} ", s夫);
}
Code language: JavaScript (javascript)

Playground

所有権の移動

S夫

まだ未発表の狩人x猟人の最新話さ!

パパが社長をしている会社のグループに出版会社があってね

コネで試し刷りをわけてもらったんだ

ボツになったげんこうに先生のサインも書いてもらったんだ

せかいにひとつしかないきちょうなものだぞ!

それにしてもまさかこんな展開になるなんて、やっぱり先生はてんさいだな

しかし きみたちがうらやましいよ

いつ再開するのかわからない連載を楽しみにずっとワクワクしていられるのだから


N太

く~うらやましい!


Gアン

おう S夫!いいものもってるな

みせてみろよ!


S夫

G、Gアン!!

こ、これはきちょうな未発表のげんこうなんだよ

ちゃんとかえしてくれるよね?


Gアン

おう

読み飽きたらかえしてやるよ

じゃあな!


N太

ざんねん あれはもうかえってこないね

他人事ながらどうじょうするよ


S夫

そんな~


S夫が持っていた試し刷りの原稿はジャイアニズムの発動によって所有権がGアンに移動してしまいました。

一度所有権が移動してしまうと返却してもらわない限りはもう原稿を読むことはできません(所有権がないので変数の値を読み込むことは不可)。

fn main() {
    let s夫 = String::from("未発表のげんこう");
    let gアン = s夫; // 所有権の移動

    println!("{:} ", gアン);
    // println!("{:} ", s夫);  //所有権が移動しているのでエラー
}
Code language: JavaScript (javascript)

Playground

他のプログラミング言語の経験があると、代入しただけでなんでエラーとなるのかわからなくなるかもしれません。

しかし思い出してください。所有権の規則にある「いかなる時も所有者は一つである。」ということを。

例えばJavaScriptでは下記のようなコードで変数の値を共有できてしまい書き換えすら可能です。

Rustではこのようなことが起きないように所有権という考え方が導入されていることを意識していく必要があります。

const s = {prop: "未発表のげんこう"};
const g = s;

console.log(s);
console.log(g);

g.prop = "どこかでなくした";

console.log(s);
console.log(g);
Code language: JavaScript (javascript)

Playground

冒頭のケースを再確認する

一番初めに提示したrender_widget()の引数を参照値として渡していた理由がわかったでしょうか。

簡潔なサンプルコードを書いて再度確認してみます。

シーケンス図で追ってみるとわかりやすいでしょう。

fn print_ref_value(bar: &String) {
    println!("{:p} {:} ", bar, *bar);
}

fn print_value(bar: String) {
    println!("{:p} {:} ", &bar, bar);
}

fn main() {
    let foo = String::from("Hello!");

    println!("{:p} {:} ", &foo, foo);

    println!("{}", "print_ref_value");
    print_ref_value(&foo);
    print_ref_value(&foo);
    
    println!("{}", "print_value");
    print_value(foo);
    // print_value(foo);  //所有権が移動しているので2回目は呼べない
}Code language: JavaScript (javascript)

Playground

シーケンス図の点線が参照値の移動3、実線が変数の所有権の移動です。

関数の引数も変数の一種です。所有権の移動が起きてしまいます。

関数のスコープが終わると変数barのライフタイムが終了し破棄されます。

参照値は破棄されても実体が残っていますので問題ありませんが、print_value()のようにbarに所有権が移動した変数の値はスコープ外となったときに破棄されてしまい再呼び出しができません。

これを避けるには関数側で移動してきた所有権を終了時にfooに戻してあげればよいですが、参照値を渡す方法がスマートでしょう。

Copyトレイト

実はprint_value()を複数回呼び出してもエラーとならないケースがあります。

プリミティブ値がそれにあたります。正確にはCopyトレイトを実装している型になります。

Copyトレイトを実装していると所有権の移動が発生せず値はコピーされます。

これはプリミティブ値はメモリ占有サイズが小さく、コピーしても同じ消費量のためで現実面からの設計となっているようです。

fn print_ref_value(bar: &usize) {
    println!("{:p} {:} ", bar, *bar);
}

fn print_value(bar: usize) {
    println!("{:p} {:} ", &bar, bar);
}

fn main() {
    // let foo = String::from("Hello!");
    let foo = 123;

    println!("{:p} {:} ", &foo, foo);

    println!("{}", "print_ref_value");
    print_ref_value(&foo);
    print_ref_value(&foo);
    
    println!("{}", "print_value");
    print_value(foo);
    print_value(foo);  //プリミティブ値はコピーされるので2回目もOK
}Code language: JavaScript (javascript)

Playground


  1. BGMとしてこちらをどうぞ。 ↩︎
  2. 実は順番を工夫すればN太も食べることができます。別途まとめます。 ↩︎
  3. この図は正確さに欠けるかもしれません。スコープによる破棄なのかNLLによる破棄なのか理解できていません。 ↩︎

GitHubにコードをアップロードしています。0.1.0バージョン

コードのコメントに書かれているfirst_stepなどをcargoコマンドに渡すと実行できます。

# Example
$ cargo run --example first_stepCode language: Bash (bash)
管理人

Recent Posts

情報セキュリティマネジメント試験取得への道

スキルアップを図るべく情報セキ…

2か月 ago

ファイナンシャルプランナー3級試験取得への道

スキルアップを図るべくファイナ…

2か月 ago

[rust] New Type Patternを使ってみる

DDDの考えを取り入れることで…

5か月 ago

RustでDDDの要素を取り入れてみる

前回SOLID原則というものを…

5か月 ago

期間限定!書籍無料キャンペーン2025

「mdBookではじめるKin…

5か月 ago