// ratatui
// パネルを描画
frame.render_widget(&contents, main_panel);
frame.render_widget(&contents, sub_panel_0);
frame.render_widget(&contents, sub_panel_1);
ところで、同じ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!
細かい話を抜きにして大まかな変数の仕組みを説明します。
概ねプログラミング言語に於いては、変数は値そのものを格納している訳ではありません。値はメモリ上の何処かに格納され、変数はそのメモリの格納先を知っているだけです。
例えば、変数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!
なぜこのような面倒なことが必要なのでしょうか!?
試しに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()では参照値を渡しています。
所有権の例
ここからは所有権を簡潔に理解できるようにエピソードを通じて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);
println!("{:} ", *n); // 見ることはできる
}
可変参照
し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つ同時に作れないのでエラー
// println!("{:} ", s); // 可変参照と通常の参照はどちらか1つしか同時に作れないのでエラー
*z = "はんぶんなくなった有名店の高級チョコレート".to_string();
println!("{:} ", *z);
// println!("{:} ", *n);
println!("{:} ", s);
}
所有権の移動
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); //所有権が移動しているのでエラー
}
他のプログラミング言語の経験があると、代入しただけでなんでエラーとなるのかわからなくなるかもしれません。
しかし思い出してください。所有権の規則にある「いかなる時も所有者は一つである。」ということを。
例えばJavaScriptでは下記のようなコードで変数の値を共有できてしまい書き換えすら可能です。
Rustではこのようなことが起きないように所有権という考え方が導入されていることを意識していく必要があります。
const s = {prop: "未発表のげんこう"};
const g = s;
console.log(s);
console.log(g);
g.prop = "どこかでなくした";
console.log(s);
console.log(g);
冒頭のケースを再確認する
一番初めに提示した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回目は呼べない
}
シーケンス図の点線が参照値の移動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
}
- BGMとしてこちらをどうぞ。 ↩︎
- 実は順番を工夫すればN太も食べることができます。別途まとめます。 ↩︎
- この図は正確さに欠けるかもしれません。スコープによる破棄なのかNLLによる破棄なのか理解できていません。 ↩︎
TOC
GitHubにコードをアップロードしています。
コードのコメントに書かれているfirst_stepなどをcargoコマンドに渡すと実行できます。
# Example
$ cargo run --examples first_step