※ 小説です
※ 読むとRustや所有権・参照とちょっとだけ仲良くなれるかもしれません
※ まとめやメッセージの類は最後のあとがきに書いてあります(読んで)
プロローグ
放課後のチャイムを合図に親友のCSSちゃんが現れた。
CSS「TSちゃん放課後ヒマでしょ?駅前にできたECサイトのデザイン見ていかない?」
TypeScript「気になる!…けどごめんね、今日は美化委員会の仕事があるんだ。」
CSS「えー?今日委員会だったっけ?あの偉そうな堅物とすることなんてある?」
TS「偉そうなって…(苦笑)、うーん、まぁ、ちょっとね。埋め合わせに今度新しいライブラリ紹介するから!」
CSS「しょうがないなぁ」
魅力的な誘いを断ってしまった私は、足早に隣のクラスの彼女の元へと向かった。
TS「ふふふ…今日はとっておきのネタがあるからね…」
美化委員会の仕事があるというのは、半分本当で半分嘘である。
委員会ではソースコードを綺麗に書く活動を行っている。その一環で、2人一組でペアプロ・コードレビューを行い、可読性の高いソースコードを書く練習が課せられているのだ。
だけど、、、
Rust「…?」
教室のドアでキョトンとした仏頂面に鉢合う。私のペアであるRustちゃんは、すでにコートを着込み下校の構えをしていた。
TS「帰ろうとするの早くない?!」
Rust「…もう放課後ですから…」
TS「でも先週の委員会活動、Rustちゃんサボったよね…?」
Rust「…活動内容が曖昧だったので…」
Rustちゃんは目を合わせず答えてくる。彼女はすごい人見知りらしく、私とペアを組むことになってからずっと寡黙であった。
TS「だから改めて今日代わりの活動をしようって約束したはずだけど…まぁいいや。とりあえずRustちゃんとお話したいなって思ってさ」
今日はそんな彼女と仲良くなろうと、私なりに「話題」を持ってきたのだった。
ディープコピーとシャローコピー
TS「この前面白い記事見つけたんだよ!」
TS「私つい最近まで、ディープコピーをするのに、構造体を文字列化する必要があったんだよね…」
Rust「?…そう、だったんですね…?」
歯切れの悪い回答が返ってくる…言葉だけでは伝わっていないのかもしれない。
TS「ディープコピーというのはあれだよ!プログラミング初見殺しあるある!!こんなやつ!」カタカタ…
const arr: number[] = [1, 2, 3];
const arr2: number[] = arr;
arr2[0] = 10;
console.log(arr);
// 出力: [ 10, 2, 3 ]
TS「arrをarr2にコピーしたと思ってたのにarr2の中身をいじったらarrも変更されちゃうってネタ!実は参照がコピーされていただけってのがオチで…」ホワイトボードカキカキ…
TS「じゃあスプレッド構文でコピーすればいいじゃん、って発想になって、さっきの例だと確かに上手く行くんだけど」
const arr: number[] = [1, 2, 3];
const arr2: number[] = [...arr];
arr2[0] = 10;
console.log(arr);
// 出力: [ 1, 2, 3 ]
console.log(arr2);
// 出力: [ 10, 2, 3 ]
TS「2重配列になると結局また同じ問題が再発しちゃう!」
const arr: number[][] = [[1, 2, 3], [4, 5, 6]];
const arr2: number[][] = [...arr];
arr2[1] = [7, 8, 9];
console.log(arr);
// 出力: [ [ 1, 2, 3 ], [ 4, 5, 6 ] ] // 予想通り
console.log(arr2);
// 出力: [ [ 1, 2, 3 ], [ 7, 8, 9 ] ]
arr2[0][0] = 10;
console.log(arr);
// 出力: [ [ 10, 2, 3 ], [ 4, 5, 6 ] ] // ?!?!
TS「これは浅いコピー…つまりシャローコピーだから起きちゃう事故なんだ。シャローコピーを回避するため、以前は配列や構造体を最深部まで深いコピー、いわゆるディープコピーをするのにJSON.stringifyを使ってたんだけど、ついにstructuredCloneでそれが可能に…」
Rustちゃんの表情を見てハタと止まった。本気で不思議そうな顔が貼りついていた。
Rust「…」
TS「あの、Rustちゃん…?」
Rust「…最初のconstのところから何を言っているのかわからないわ…」
……
やってしまった…
…….
また、やってしまった…相手のことを全く気にせずずっとしゃべり続けてしまった…
…私は昔…JSだった時から突っ走ってしまう癖があった。そして計算を間違えたりnullとundefinedを混同したりundefinedを変数のように扱ったりなどの数々のおっちょこちょいな性格もあって、周りからはドジっ子扱いされていた。それが嫌で陽キャを演じていた私だったが…
TS「やっぱり私は陰キャ言語相手のことを考えられないコミュ障…UDPでしか会話できない…」?グルグルグルグル?
CSS「…心配になって付いてきてしまったけど、またTSちゃん鬱モードに入っちゃってる…Rustちゃん何したのさ?」
Rust「いえ、私は何も…」
CSS「何もしてないって?TSちゃんを無視したりだって嫌がらせよ?」
Rust「説明していただいたソースコードが読めない、とは言いました。」
CSS「ソースコードって…これかな」カチカチッ
const arr: number[] = [1, 2, 3];
const arr2: number[] = arr;
arr2[0] = 10;
console.log(arr);
// 出力: [ 10, 2, 3 ]
CSS「中身はよくわからないけど、大した行数はないじゃない?Rustちゃんなら平気で読める量では?」
Rust「いえ、茶化したりしているわけではなく、本当にconstのところからわからないんです。」
TypeScriptのconstとRustのlet
TS「constと書くのにも関わらず、中身を変更可能だからかな…?たまに指摘されます…」
CSS「あ、TSちゃん復活した」
TS「今回の話に関わるので解説するね。constが保証するのは変数に格納された値が以降で変更されないこと」ホワイトボードカキカキ…
TS「配列の場合、constは変数に入っている参照の再代入は防いでくれるけど、参照先の値は変更できちゃうんだよね。」
CSS「Rustちゃんの場合はこれをどう解釈するの?」
Rust「constはコンパイル前に値が定まる定数を表します1。TSさんにとってのconstに一番近い宣言はlet val: &mut Tでしょうか?ここでvalは変数名、Tは格納する変数の(参照先の)型です。」
let mut _arr: Vec<usize> = vec![1, 2, 3];
let arr: &mut [usize] = &mut _arr; // このarrはconst arrに相当
CSS「(Rustちゃんも他人のこと言えないぐらい意味のわからないコード書いてない…??)」
Rust「おそらくですが私にとってのletによる宣言がTSさんのconstとほぼ同じ意味です。しかし私の場合は、&mutをつけて可変参照を取らないと、ホワイトボードに描かれた赤矢印のような可変は得られません。」
CSS「(そして急に饒舌になったなぁ…)」
TS「うーん…もしかしてRustちゃんにとってのletは私にとってはconst arr: readonly number[]かもしれない…」
CSS「(TSちゃんまで呪文を…アワワ)」
CSS「えっと、、ごめんよくわからなくなってきたのだけど(大声)、要はTSちゃんとRustちゃんでは書き方が違うから伝わらないっていう、それだけじゃない?」
Rust「いえ、constの意味を教えていただいたので読める気がしてきました…しかし、ホワイトボードで議論した内容と私の直感的な理解が一致しないです。」
CSS「(全くわからないと言いつつ結構ちゃんと聞いていたのでは…?)」
TS「うーん…じゃあ試しに、元のコードを私がRustで書いてみるね」
Rustの所有権とCloneトレイト
~ 調べながら書くこと数十分後 ~
TS「配列に[T; n]みたいなのとVecがあって迷ったけど、私に合わせて、可変長なVecにしたよ2!」
[T; n]は静的配列で、Vecはヒープにメモリを確保する可変長配列です。以降、本小説において「配列」は可変長配列Vecを指すこととします。
配列とスライス – Rust By Example 日本語版
ベクタ型 – Rust By Example 日本語版
fn main() {
let arr: Vec<usize> = vec![1, 2, 3];
let arr2: Vec<usize> = arr;
arr2[0] = 10;
println!("{:?}", arr);
}
TS「できた!けど、ありゃりゃ…コンパイルエラーだ…?…長い!!」
$ rustc main.rs
error[E0596]: cannot borrow `arr2` as mutable, as it is not declared as mutable
--> main.rs:5:5
|
5 | arr2[0] = 10;
| ^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
3 | let mut arr2: Vec<usize> = arr;
| +++
error[E0382]: borrow of moved value: `arr`
--> main.rs:7:22
|
2 | let arr: Vec<usize> = vec![1, 2, 3];
| --- move occurs because `arr` has type `Vec<usize>`, which does not implement the `Copy` trait
3 | let arr2: Vec<usize> = arr;
| --- value moved here
...
7 | println!("{:?}", arr);
| ^^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
|
3 | let arr2: Vec<usize> = arr.clone();
| ++++++++
error: aborting due to 2 previous errors
Some errors have detailed explanations: E0382, E0596.
For more information about an error, try `rustc --explain E0382`.
TS「エラー自体は2つみたいだね…えっと一つ目は」
error[E0596]: cannot borrow `arr2` as mutable, as it is not declared as mutable
--> main.rs:5:5
|
5 | arr2[0] = 10;
| ^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
3 | let mut arr2: Vec<usize> = arr;
| +++
TS「cannot borrow arr2 as mutable, as it is not declared as mutable…和訳すると、『arr2は可変で宣言されていないため、arr2を可変で借用できません。』」
TS「そしてhelpに、consider changing this to be mutable…これも和訳すると、『これを可変になるように変えることを検討してください』…?」
Rust「先ほどTSさんがちらりとreadonlyというキーワードを口にしていたと思うのですが、letはデフォルトでreadonlyが付与されていると考えるとわかりやすいです。mutは、このreadonlyを外す操作に近いかと。」
TS「じゃあarr2にmutを付ければ解決しそうだね」
fn main() {
let arr: Vec<usize> = vec![1, 2, 3];
- let arr2: Vec<usize> = arr;
+ let mut arr2: Vec<usize> = arr;
arr2[0] = 10; // arr2は可変なのでおk
println!("{:?}", arr);
}
TS「でも2つ目のエラーは解決しなかったみたい」
$ rustc main.rs
error[E0382]: borrow of moved value: `arr`
--> main.rs:7:22
|
2 | let arr: Vec<usize> = vec![1, 2, 3];
| --- move occurs because `arr` has type `Vec<usize>`, which does not implement the `Copy` trait
3 | let mut arr2: Vec<usize> = arr;
| --- value moved here
...
7 | println!("{:?}", arr);
| ^^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
|
3 | let mut arr2: Vec<usize> = arr.clone();
| ++++++++
error: aborting due to previous error
For more information about this error, try `rustc --explain E0382`.
TS「borrow of moved value: arr…『移動されたarrの値を借用した』?」
Rust「…(“これ”を説明する時がまた来てしまった…)」
Rust「…arr2に、arrが持っていたvec![1, 2, 3]という値の所有権が移動(ムーブ)したため、arrは使用できない、という意味です。」ホワイトボードカキカキ…
Rust「使用できないのにも関わらず、println!で参照しているのでエラーになっています。借用という言葉は私には参照と同義です。」
TS「参照がコピーされるわけじゃないんだね…あ、さっきと同じくエラー文に解決策も書いてる」
help: consider cloning the value if the performance cost is acceptable
|
3 | let mut arr2: Vec<usize> = arr.clone();
| ++++++++
Rust「(あれ?拒絶されなかった…)」
Rustちゃんは何か不思議な表情をしていたが、私はそのまま続けた。
TS「えっと、consider cloning the value if the performance cost is acceptable…『パフォーマンスコスト的に許せるなら値のクローンを作ることを検討してください』…?」
TS「cloneというメソッドを呼べばいいんだね!このメソッドはVec固有の物?」
Rust「いえ、Cloneトレイトを実装した型であればどれもが持ち合わせています。」
TS「トレイト?」
Rust「TSさんで言うinterfaceに該当するもの3です。ただしトレイトにはフィールドを指定する機能はありません。」
-
- クローン – Rust By Example 日本語版
- Clone in std::clone – Rust
pub trait Clone: Sized {
fn clone(&self) -> Self;
// 自動実装される`clone_from`は省略
}
interface Clone<T> {
clone(): T;
}
Rust「名前から察しが付くと思いますが、cloneメソッドを使うと元の値のクローンが作られます。元の変数の所有権は奪いません。」
TS「…ということはもしかして私が2つ目に書いたスプレッド構文を使うコードと同じ結果になるのでは…?」
CSS「とりあえず動かしてみればわかるんじゃない?」
TS「そうだね、ついでにarr2の出力も加えてみるよ」
fn main() {
let arr: Vec<usize> = vec![1, 2, 3];
- let mut arr2: Vec<usize> = arr;
+ let mut arr2: Vec<usize> = arr.clone();
arr2[0] = 10;
- println!("{:?}", arr);
+ println!("arr = {:?}", arr);
+ println!("arr2 = {:?}", arr2);
}
TS「コンパイルは通った!、、実行結果は…」
$ rustc main.rs
$ ./main
arr = [1, 2, 3]
arr2 = [10, 2, 3]
TS「やっぱりスプレッド構文と同じだ!!」
Rust「結局初見殺しあるあるの正体はわかりませんでしたね…」
TS「でもまだシャローコピーである可能性が残っているかも」
CSS「じゃあ同じ感じで3つ目のコードをRustで書いてみたら?」
TS「がんばる!」カタカタ…
fn main() {
let arr: Vec<Vec<usize>> = vec![vec![1, 2, 3], vec![4, 5, 6]];
let mut arr2: Vec<Vec<usize>> = arr.clone();
arr2[1] = vec![7, 8, 9];
println!("arr = {:?}", arr);
println!("arr2 = {:?}", arr2);
arr2[0][0] = 10;
println!("arr = {:?}", arr);
println!("arr2 = {:?}", arr2);
}
TS「今回はエラーなく一発でコンパイル通ったみたい」
$ rustc main.rs
$ ./main
arr = [[1, 2, 3], [4, 5, 6]]
arr2 = [[1, 2, 3], [7, 8, 9]]
arr = [[1, 2, 3], [4, 5, 6]]
arr2 = [[10, 2, 3], [7, 8, 9]]
TS「デ ィ ー プ コ ピ ー さ れ て る !」
Rust「cloneメソッドは構造体や配列の要素に対し再帰的に呼び出されるので、Vecの要素になっているVecについても中身がクローンされます。そのため、所有権譲渡も発生せずコンパイルが通っていたわけですね4。」
TS「最初から全部ディープコピーされるなんて…もしかしてRustちゃんにはシャローコピー初見殺しあるあるが伝わらない…?」
そんな…せっかく話題を共有できると思ったのに…
CSS「でもTypeScriptだけじゃなくPythonとか多くの言語ができるらしいことが、Rustちゃんにはできないってことなんだね?意外な一面を見たかも」ニヤニヤ
Rust「できなくはないはずですよ」
TS「…ほえ?」
Rustちゃんは、ムッとした表情を貼り付けながら
Rust「だから、TSさんがホワイトボードに描いたような、参照だけを複製するようなコードも、書けなくはないです。」
そう言ってキーボードの前に座った。
Rustの不変参照と可変参照
- 参考元: 参照と借用 – The Rust Programming Language 日本語版
(読者の方へ: ここからちょっと長いので頑張ってください)
不変参照
Rust「その前に、私にとっての参照とはどういうものかを解説したほうが良さそうですね。基本的には、不変参照と可変参照の二種類があります。」
TS「可変参照についてはさっき少し触れていたような?」
Rust「そうですね。しかし説明の順番のため、まずは不変参照についてです。」カタカタ…
// vecの型&Vec<Vec<usize>>は、vecがVec<Vec<usize>>型の値の不変参照であることを表す
fn print_vec(vec: &Vec<Vec<usize>>) {
for i in vec {
for j in i {
print!("{} ", j);
}
println!("");
}
}
fn main() {
let arr: Vec<Vec<usize>> = vec![vec![1, 2, 3], vec![4, 5, 6]];
let arr_ref: &Vec<Vec<usize>> = &arr; // &をつけることで参照を取れる
print_vec(arr_ref);
// 関数に渡す場合は直接こう書く方が普通
print_vec(&arr);
}
Rust「&arrのように変数の前に&を付けることで、arrという変数が持つ値への不変参照を取れます。名前の通り、不変参照に対して変数の中身を変更する操作は不可能です5。」
fn main() {
let arr: Vec<Vec<usize>> = vec![vec![1, 2, 3], vec![4, 5, 6]];
let arr_ref: &Vec<Vec<usize>> = &arr;
arr_ref[0][0] = 10; // コンパイルエラー。不変参照では変更操作ができない
}
CSS「&で参照ってC言語っぽい…?うーん…C言語先生の授業のトラウマが…」
TS「&を使っているせいで難しく見えてるだけかも、さっき話した通り私にも参照の考えはあるし。ところで、C言語のポインタには不変・可変の区分はなかったよね?不変というだけでかなり強い制限な気がするけど、どんな場面で使われるの?」
Rust「不変参照はあるデータに依存して分岐したり、例にあるprint_vec関数のようにデータそのものを出力する場合に使われることが多いです。」
TS「覗くだけなら可変である必要はないもんね。」
CSS「でもできるなら参照を使いたくないような…参照じゃなきゃだめ?」
TS「使わないで書いてみると…こんな感じかな…」
fn print_vec(vec: Vec<Vec<usize>>) {
for i in vec {
for j in i {
print!("{} ", j);
}
println!("");
}
}
fn main() {
let arr: Vec<Vec<usize>> = vec![vec![1, 2, 3], vec![4, 5, 6]];
print_vec(arr);
}
TS「特にコンパイルエラーも起きなかったけど…」
CSS「大丈夫そうじゃん」
Rust「もう一回print_vecしてみてくれますか?」
TS「…?何か変わるの…?」
fn main() {
let arr: Vec<Vec<usize>> = vec![vec![1, 2, 3], vec![4, 5, 6]];
print_vec(arr);
+ print_vec(arr);
}
TS「コンパイルエラーになった!なんで?!」
$ rustc main.rs
error[E0382]: use of moved value: `arr`
--> main.rs:15:15
|
11 | let arr: Vec<Vec<usize>> = vec![vec![1, 2, 3], vec![4, 5, 6]];
| --- move occurs because `arr` has type `Vec<Vec<usize>>`, which does not implement the `Copy` trait
12 |
13 | print_vec(arr);
| --- value moved here
14 |
15 | print_vec(arr);
| ^^^ value used here after move
|
note: consider changing this parameter type in function `print_vec` to borrow instead if owning the value isn't necessary
--> main.rs:1:19
|
1 | fn print_vec(vec: Vec<Vec<usize>>) {
| --------- ^^^^^^^^^^^^^^^ this parameter takes ownership of the value
| |
| in this function
help: consider cloning the value if the performance cost is acceptable
|
13 | print_vec(arr.clone());
| ++++++++
error: aborting due to previous error
For more information about this error, try `rustc --explain E0382`.
TS「use of moved value: arr…『すでに移された値arrを使ってる』…?」
Rust「変数間だけではなく関数の引数に変数が与えられる場合も引数への所 有 権 譲 渡が行われるため、エラーになっています。譲渡されたことが『移った』と表現されています。」ホワイトボードカキカキカキカキカキ
そこまで大した内容ではないはずなのに、強い口調で話しながらRustちゃんは熱心に図を描き殴っていた。出会った時のRustちゃんからは全く想像できないその姿に、戸惑いながらも。
TS「しょゆうけんのじょうと、っていうのは、さっきから点線で描いてるやつだね…つまり一回目のprint_vecでarrから所有権が奪われるから、二回目の呼び出し箇所でエラーになるのかな?」
こう私が答えると、どこか安心したように彼女は続けた。
Rust「そうです。配列の出力にはarrの所有権は不要ですから、本体ではなく、所有権譲渡なしで使える参照だけあれば十分です。それがprint_vecの引数vecの型が参照型&Vec<Vec>である理由でした。」
TS「不変参照が必要というよりは、不変参照で十分ってことだね。helpに書いているみたいにcloneを使ってもエラーは消えるのだろうけど、クローンするまでもないという感じ…?」
Rust「はい、cloneを呼び出す場合、メモリ使用量は倍に増えます。一方、不変参照なら余計なメモリアロケーションを回避できます。arr本体の代わりに不変参照としてリソースを”借りて”いるので、参照のことを借用とも言います。」
CSS「cloneで思い出したけど、さっきまでこんな難しい話なかったような…よくよく思い返してみれば、所有権でコンパイルエラーになって、参照の話が回避されていきなりディープコピーのコードにたどり着いていたよね…アレ?もしかして所有権のおかげで難しい参照の話を回避できていた…?」
TS「私のコードは言うなれば『無断借用』状態になっていたけど、Rustちゃんの場合は所有権のおかげでリソースの位置や借用関係がはっきりしているよね。『あげられない・貸せない時はクローン』ってことで、参照記号(&)なしではディープコピーのようなコードしか書けなかったのかも。」
Rust「言われてみれば、最初にTSさんのコードを直感的に理解できなかったのは、どこに借用…参照が生じているのかわからなかったからですね。」
CSS「なんかもろもろの解決の糸口が見えてきたね」
どうして関数の引数も所有権を奪うのでしょうか?詳しくは書きませんがヒントを。この仕組みやclone、そして後述する可変参照を応用すれば「値渡し」と「参照渡し」を区別してどちらも記述することが可能ですよね。つまり都合が良いからです。
具体例
Rust
#[derive(Clone, Copy, Debug)]
struct Hoge {
val: usize,
}
fn value_watashi(mut hoge: Hoge) {
hoge.val = 100;
println!(“value_watashi: {:?}”, hoge);
}
fn mutref_watashi(hoge: &mut Hoge) {
hoge.val = 100;
println!(“mutref_watashi: {:?}”, hoge);
}
fn main() {
let mut hoge = Hoge { val: 0 };
value_watashi(hoge);
println!(“main: {:?}”, hoge);
mutref_watashi(&mut hoge);
println!(“main: {:?}”, hoge);
}
関数の引数も所有権を奪うというよりは、「変数が評価される際は所有権譲渡が起こるかも」と考えた方が自然な捉え方かもしれません。おまけのCopyトレイトの節でも解説していますが、デフォルトで所有権譲渡が行われる仕様のため「余計なメモリアロケーションを避ける」ことができています。
可変参照
Rust「次は可変参照についてです。先ほどの例がコンパイルエラーにならないように書き直してみました。」
fn main() {
let mut arr: Vec<Vec<usize>> = vec![vec![1, 2, 3], vec![4, 5, 6]];
let arr_mref: &mut Vec<Vec<usize>> = &mut arr;
arr_mref[0][0] = 10;
}
Rust「変数の可変参照を取るには、元の変数が可変である必要があります。そのため、let mut arrとしています。記法として違うのは&ではなく&mutとしている点ですね。」
TS「可変参照はどんな時に使われるの?」
Rust「主に関数呼び出しで引数に副作用が生じる(または生じさせたい)場合に使います。関数については、例えば以下に書くソートとかですね6。他は、競技プログラミング等で再帰関数で使われることが多いです。」カタカタ…
// vecの型&mut Vec<usize>は、vecがVec<usize>型の値の可変参照であることを表す
fn sort_vec(vec: &mut Vec<usize>) {
// select sort
for i in 0..vec.len() {
let mut min = i;
for j in i..vec.len() {
if vec[j] < vec[min] {
min = j;
}
}
vec.swap(i, min);
}
}
fn main() {
let mut arr: Vec<usize> = vec![3, 1, 4, 1, 5, 9, 2];
sort_vec(&mut arr); // &mut で変数の可変参照が取れる
println!("{:?}", arr); // [1, 1, 2, 3, 4, 5, 9]
}
Rust「例えばPythonにあるsortedみたいに、引数で配列を受け取り返り値で返す方法も考えられますが、それだけでは不便です7。」カタカタ…
fn sorted(mut vec: Vec<usize>) -> Vec<usize> {
// select sort
for i in 0..vec.len() {
// omit details
}
vec
}
fn main() {
let arr: Vec<usize> = vec![3, 1, 4, 1, 5, 9, 2];
let arr = sorted(arr);
println!("{:?}", arr);
}
Rust「関数の返り値にするほどではない、しかし副作用を起こすために参照渡しを行いたい。そんな時に使うと効果的です。」
CSS「(Rustちゃん、自分の話ができて楽しくなってきてるのかな、饒舌さがマックスに…)」
TS「可変参照&mutがあれば初見殺しあるあるを再現できそうだね!」
Rust「いえ、残念ながらそうでもないんです。」
参照に関する制約
Rust「ここまでの説明に加え、&と&mutには以下に示す制約があります。この制約のため、あるあるを実現できません。」
-
- ある変数への参照は、不変参照か可変参照の、どちらか片方の種類の参照のみしか同時に存在できない
-
- 不変参照は同時に複数存在することができる
-
- 可変参照は一つしか存在できない
- 参照は変数のライフタイムが有効な範囲でのみ存在できる(後述)
CSS「不変か可変かだけで頭が痛いのになんか多いな…?なぜこんなに制約が…?」
Rust「最後に示したライフタイムの話は一旦後にするとして、最初の3つについて解説します。何かわかりやすい例え…そうですね…」カチカチッ
TS「これは…会計兼予算編成委員会のスプレッドシート…?しかも共同編集中みたいだけど」
CSS「なんでRustちゃんにアクセス権が…?」
Rust「私は会計兼予算編成委員会も兼任していますから。」
TS「へー、ということはRustちゃんは美化兼会計兼予算編成委員なんだね」
CSS「Rustちゃん予算編成委員もサボってるん?サボるくせに兼任してるん??そしてそのスプレッドシートは私たちに見せて大丈夫なのか…?!」
ツッコミが追い付いていないCSSちゃんなのであった。それにしてもRustちゃん、結構度胸あるなぁ
一方その頃、会計兼予算編成委員会では…
Haskell「どの部活動もChatGPTがどうのとかNovel AIがどうのとかでサブスク入りすぎですわ!とてもじゃないけど承認できないわ。」
Elixir「そうですね。特に委員会を中心に予算を減らしましょう。」
Haskell「では私が編集するので範囲にロックを掛けますわ」
Rust「あ、変化がありましたよ」
TS「風紀委員会が網掛けになったね。この範囲にロックが掛けられて編集ができなくなってる…?あ、値が変わった」
Haskell「風紀委員の方たちにChatGPTなんて不要ですわ!9000円でよろしくてよ」
Elixir「さすがに反発が起きるのでは…」
Haskell「それもそうですわね。では去年と同じで90000円ということで」
Elm「(お姉さまが委員長になってからこの委員会不安しかない件について)」
CSS「さすがに9000円は何かの間違いでしょ。編集中なんじゃない?」
Rust「ということは、これを不変として捉えるのは避けたほうがいいですね。具体的には、網掛けがある間はこの範囲をコピペするのは避けるべきです。」
TS「あっ!1番の『不変参照か可変参照の、どちらか片方の種類の参照のみしか同時に存在できない』だね!」
CSS「可変参照でHaskell委員長?が編集中だから、不変参照は取れないってことか」
Haskell「ついでに美化委員会の奴らの予算も減らしてやりましょ。おーっほっほっほ」
Elm「(お姉さまいつからこんなキャラになったんだっけ…)」
TS「あー-!!なんかついでに私たちの予算まで減らされているんだけど!!!Rustちゃん直せない…?」
Rust「ロックが掛けられているので無理ですね。それにもしロックが掛からないものとして、同時に編集したら仁義なき編集合戦の無限ループになります…」
CSS「あっ、これが3番の『可変参照は一つしか存在できない』ってことか。確かに同時に複数の場所から変更されると混乱しちゃうかも」
TS「ちょっと…誰か…予算減額を…止めて…」
Haskell「今日はこんなところですわね。お疲れ様でしたわ。」
Elixir「お疲れさまでしたー」
Elm「(非承認のままだし色々雑すぎだろ。てかロック掛ける意味ってあったんか?)」
CSS「網掛けが消えて人影も消えた…」
TS「えっと、もう可変ではなくなったのだろうし不変と捉えてコピペできるね、2番の『不変参照は同時に複数存在することができる』かな…?」
Rust「例えとして無理がありそうですがそんなところですね。」
CSS「制約の理由はなんとなくわかったけど、それがどうして初見殺しあるあるが実現できないことにつながるの…?」
Rust「主に1番の『不変参照か可変参照の、どちらか片方の種類の参照のみしか同時に存在できない』が効いてきます。実際に書いてみればわかるかなと。」
TS「やってみる!」
「あるある」を可変参照で書いてみる
fn main() {
let mut arr: Vec<Vec<usize>> = vec![vec![1, 2, 3], vec![4, 5, 6]];
let arr2: &mut Vec<Vec<usize>> = &mut arr;
arr2[1] = vec![7, 8, 9];
println!("arr = {:?}", arr);
println!("arr2 = {:?}", arr2);
}
TS「案の定エラーが出たね…」
error[E0502]: cannot borrow `arr` as immutable because it is also borrowed as mutable
--> main.rs:26:28
|
22 | let arr2: &mut Vec<Vec<usize>> = &mut arr;
| -------- mutable borrow occurs here
...
26 | println!("arr = {:?}", arr);
| ^^^ immutable borrow occurs here
27 | println!("arr2 = {:?}", arr2);
| ---- mutable borrow later used here
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
error: aborting due to previous error
For more information about this error, try `rustc --explain E0502`.
Rust「ソースコードからはわかりにくいですが、println!関数(ホントはマクロだけどとりあえず関数と言っておこう…)は変数の不変参照を取るようにできています。」
TS「そっか、arr2で可変参照借用中だから、arrの不変参照が取れなくてエラーになってるんだね…」
Rust「arrの出力を最後に持っていけば、arr2がもう使われないため、可変参照は自動で無効化されるのでコンパイルが通ります。」カタカタ…
fn main() {
let mut arr: Vec<Vec<usize>> = vec![vec![1, 2, 3], vec![4, 5, 6]];
let arr2: &mut Vec<Vec<usize>> = &mut arr;
arr2[1] = vec![7, 8, 9];
// println!("arr = {:?}", arr); // ここでの出力は諦める
println!("arr2 = {:?}", arr2);
arr2[0][0] = 10;
// println!("arr = {:?}", arr); // ここでの出力は諦める
println!("arr2 = {:?}", arr2);
// ここでarr2が自動的に無効化される
println!("arr = {:?}", arr);
}
$ rustc main.rs
$ ./main
arr2 = [[1, 2, 3], [7, 8, 9]]
arr2 = [[10, 2, 3], [7, 8, 9]]
arr = [[10, 2, 3], [7, 8, 9]]
CSS「あれ?実行結果がTSちゃんのシャローコピーと全然違くない…?」
Rust「説明を簡単にするためにarr2はarr自体の可変参照としたのが原因ですね。図にするとこんな感じです。」ホワイトボードカキカキ…
CSS「元の図はこっちだね。」
TS「うーんと…えーっと…あーんと…あっ、『参照の配列』が必要でそれぞれの配列自体の所有権はarrとarr2のそれぞれにある感じ…?でも難しそうだよぉ」
Rust「さすがにこれは私が書きますね。こんな感じでしょうか」カタカタ…
fn main() {
let mut arr: Vec<Vec<usize>> = vec![vec![1, 2, 3], vec![4, 5, 6]];
let mut arr2: Vec<&mut Vec<usize>> = arr.iter_mut().collect();
let mut new_vec = vec![7, 8, 9];
arr2[1] = &mut new_vec;
// println!("arr = {:?}", arr); // ここでの出力は諦める
println!("arr2 = {:?}", arr2);
arr2[0][0] = 10;
// println!("arr = {:?}", arr); // ここでの出力は諦める
println!("arr2 = {:?}", arr2);
// ここでarr2が自動的に無効化される
println!("arr = {:?}", arr);
}
Rust「最終的なarrとarr2の状態は希望と一致しているはずです。」
$ rustc main.rs
$ ./main
arr2 = [[1, 2, 3], [7, 8, 9]]
arr2 = [[10, 2, 3], [7, 8, 9]]
arr = [[10, 2, 3], [4, 5, 6]]
TS「大きく変わったのは
let mut arr2: Vec<&mut Vec<usize>> = arr.iter_mut().collect();
let mut new_vec = vec![7, 8, 9];
arr2[1] = &mut new_vec;
の部分だね」
Rust「はい。Vec<&mut Vec>に注目すると、arr2が『参照の配列』になっていることがわかります。iter_mutは『要素(ここではVec型の値が要素)それぞれへの可変参照のイテレータを得る』メソッドで、collectは『イテレータから新しい配列(など)を得る』メソッドです。組み合わせることで、『参照の配列』が得られます。」
CSS「わざわざnew_vecという変数を設けているのはなぜ?」
Rust「4番の『参照は変数のライフタイムが有効な範囲でのみ存在できる』の影響です。&mut vec![7, 8 ,9]としてしまうと、vec![7, 8, 9]を所有する変数が存在しないことになってしまいます。」
TS「&mutはあくまでも借用だもんね。貸してくれる相手…参照先がいなければそもそも借りられないわけだ」
Rust「はい、ですから、何かしらの変数にvec![7, 8, 9]を束縛する必要があったので、new_vecに束縛していました。」
TS「…ちょっと待って…?arrとarr2の型って一致していないとおかしくない…?今って図にするとこういう状態では…?」ホワイトボードカキカキ…
CSS「それに結局途中のarrの状態を出力できてないよね。」
Rust「気づいちゃいましたか…これが『まだあるあるを実現できない』と言った理由です。正確に表現するには、もう少し細工が必要です。」
参照の制約を超えたい: Rc と RefCell
RefCell
Rust「最後のコードで問題となるのは、3番の『可変参照は一つしか存在できない』です。arrとarr2の型を一致させ対等にするには、両方ともVec<&mut Vec>型にしなければいけませんが、Vecへの可変参照はそれぞれについて一つしか取れないので、arrとarr2は同時に存在することができません。」
TS「確かに…元はと言えば、arrとarr2がどちらからもお互い共通の参照先を変更できてしまうのが初見殺しあるあるの問題点だもんね。そういうコードはそもそも書けないのか。」
Rust「その無理を押し通すため、もう少し柔軟な”可変参照っぽいもの”を使います。『可変参照中』かどうかは、通常コンパイル前に調べられるのですが、この借用チェックをランタイム時に行う、RefCell型がそれです。」カタカタ…
- RefCellと内部可変性パターン – The Rust Programming Language 日本語版
use std::cell::{RefCell, RefMut, BorrowMutError};
fn main() {
let x: RefCell<i32> = RefCell::new(5);
let y: &RefCell<i32> = &x;
let z: &RefCell<i32> = &x;
{
let x_mr: &mut i32 = &mut *y.borrow_mut();
*x_mr += 1;
}
println!("x = {:?}, y = {:?}, z = {:?}", x, y, z);
*z.borrow_mut() += 1;
println!("x = {:?}, y = {:?}, z = {:?}", x, y, z);
// 可変参照が複数ある場合に借用できないことを確認
{
let x_mr: &mut i32 = &mut *y.borrow_mut();
let Ok(x_mr_2): Result<RefMut<i32>, BorrowMutError> = z.try_borrow_mut() else {
println!("x is already borrowed");
return;
};
println!("x_mr = {:?}, x_mr_2 = {:?}", x_mr, x_mr_2);
}
}
$ rustc main.rs
$ ./main
x = RefCell { value: 6 }, y = RefCell { value: 6 }, z = RefCell { value: 6 }
x = RefCell { value: 7 }, y = RefCell { value: 7 }, z = RefCell { value: 7 }
x is already borrowed
Rust「RefCellは可変参照そのものではないのですが、『ランタイム時、一時的に可変を得る』ことができる構造体です。」
TS「yやzはxへの可変参照じゃなくて、『可変参照を一時的に得られるRefCellへの不変参照』だから共存できるわけだね。」
Rust「はい。そしてランタイム時にて可変参照を得る際に可変参照が複数ないかを確認します。すでに存在する場合はパニックになります。」
CSS「Rustちゃんがヒステリーを起こすってこと…?」
Rust「パニックは、皆さんで言う実行時エラーのことです…」
TS「どういうものかはわかったけど、各場所で何をしているのかがよくわからないかも」
Rust「9行目の&mut *y.borrow_mut()が実際に可変参照を得る場所です。*記号は『参照外し』と言って、&Tや&mut T、RefMut等8からT型の値を得る操作です。その後、改めて&mutを付けることで可変参照を得られます。9行目では、RefMutから&mut i32を得ています。」
CSS「参照外しの瞬間は所有権とやらは奪われないの?」
Rust「ややこしい部分ですが、型解決のために参照なしの型に一旦戻るだけで、所有権譲渡は発生しません9。」
TS「10行目、インクリメントの*x_mr += 1;では参照外しを行ってi32型を得ているけど、x_mrから他の変数に所有権が移ったりすることはなさそうだもんね。14行目の*z.borrow_mut() += 1;は9行目と10行目を一気に行っている感じ?」
Rust「そうなります。z.borrow_mut()はRefMutを返します。*を付けてi32型として扱い、インクリメントしています。」
CSS「ところでさっきから気になっているんだけど{と}の中括弧で囲んでインデントしている箇所があるのはなんなの…?」
Rust「スコープを定めるために使用しています。x_mrのスコープを狭め、ライフタイムを狭めることで}より外では可変参照が無効になるようにしています10。ライフタイムについてはまた触れるのでその時解説しますね。」
TS「無効にならない場合、ランタイム時での借用チェックに失敗しそうだね。最後あたりではあえて可変参照の重複借用を試みているみたいだけど、z.try_borrow_mut()というのは…?」
Rust「可変参照として借用中のものであるかを確かめながら、可変参照を得るためのメソッドです。パニックせずにResult型にすることで、可変参照がすでに存在する場合には分岐して対応しています。」
TS「Result型、冷静に対応できてクールだな…!」
CSS「とりあえずRefCellについてはわかった気がする…多分…で、これを結局どう使うのさ?」
Rust「こんな感じで…可変借用したい内側のVecだけRefCell<Vec>にすれば…」カタカタ…
use std::cell::RefCell;
fn main() {
let original_arr: Vec<RefCell<Vec<usize>>> = vec![
RefCell::new(vec![1, 2, 3]),
RefCell::new(vec![4, 5, 6])
];
let new_vec: RefCell<Vec<usize>> = RefCell::new(vec![7, 8, 9]);
let arr: Vec<&RefCell<Vec<usize>>> = original_arr.iter().collect();
let mut arr2: Vec<&RefCell<Vec<usize>>> = original_arr.iter().collect();
arr2[1] = &new_vec;
println!("arr = {:?}", arr);
println!("arr2 = {:?}", arr2);
(arr2[0].borrow_mut())[0] = 10; // この時点で他に可変参照はない
println!("arr = {:?}", arr);
println!("arr2 = {:?}", arr2);
}
$ rustc main.rs
$ ./main
arr = [RefCell { value: [1, 2, 3] }, RefCell { value: [4, 5, 6] }]
arr2 = [RefCell { value: [1, 2, 3] }, RefCell { value: [7, 8, 9] }]
arr = [RefCell { value: [10, 2, 3] }, RefCell { value: [4, 5, 6] }]
arr2 = [RefCell { value: [10, 2, 3] }, RefCell { value: [7, 8, 9] }]
TS「おー!arrとarr2の型がVec<&RefCell<Vec>>で一致しているし、」
CSS「arrのprintln!出力も可能になっている上に、」
Rust「初見殺しあるあるの実行結果を忠実に再現できていますね。」
TS「シャローコピーによるバグを再現できてここまで感動したのは初めてだよ…」
Rust「しかしまだ”本家”とは少し構造が違います。図にするとこうなっています。」ホワイトボードカキカキ…
CSS「…?いやほとんど同じでしょ」
TS「Rustちゃん、こだわるなぁ…」
ライフタイム制約
- ライフタイムで参照を検証する – The Rust Programming Language 日本語版
Rust「ポイントはどこから借用しているかです。説明を先延ばしにしていたライフタイムが関わる話ですが、arrとarr2は、貸し出し主であるoriginal_arrとnew_vecが生きている間しか存在できません。具体的には、次のようなコードが書けません。」カタカタ…
use std::cell::RefCell;
fn main() {
let arr: Vec<&RefCell<Vec<usize>>>;
let mut arr2: Vec<&RefCell<Vec<usize>>>;
{
let original_arr: Vec<RefCell<Vec<usize>>> = vec![
RefCell::new(vec![1, 2, 3]),
RefCell::new(vec![4, 5, 6])
];
let new_vec: RefCell<Vec<usize>> = RefCell::new(vec![7, 8, 9]);
arr = original_arr.iter().collect();
arr2 = original_arr.iter().collect();
arr2[1] = &new_vec;
println!("arr = {:?}", arr);
println!("arr2 = {:?}", arr2);
(arr2[0].borrow_mut())[0] = 10;
} // original_arrとnew_vecはここでドロップ
println!("arr = {:?}", arr); // 貸し出し主不在なので不正な参照になりエラー
println!("arr2 = {:?}", arr2);
}
$ rustc main.rs
error[E0597]: `original_arr` does not live long enough
--> main.rs:14:15
|
14 | arr = original_arr.iter().collect();
| ^^^^^^^^^^^^^^^^^^^ borrowed value does not live long enough
...
23 | }
| - `original_arr` dropped here while still borrowed
24 |
25 | println!("arr = {:?}", arr);
| --- borrow later used here
error[E0597]: `new_vec` does not live long enough
--> main.rs:17:19
|
17 | arr2[1] = &new_vec;
| ^^^^^^^^ borrowed value does not live long enough
...
23 | }
| - `new_vec` dropped here while still borrowed
...
26 | println!("arr2 = {:?}", arr2);
| ---- borrow later used here
error: aborting due to 2 previous errors
For more information about this error, try `rustc --explain E0597`.
Rust「先ほども少しだけ登場しましたが、中括弧{と}はその変数を使えるスコープを定め、さらにリソースのライフタイム(=生存期間)も限定します。リソースの生存期間が本家のarrとarr2とは異なっているので、本家とはまだ違うと言いました。」
CSS「こだわりが最早ワガママレベル…(目的は達成できてるしもう良くないか…?続けるの…?って思った読者はあとがきに行っちゃっていいと思うよ…)」
TS「うーんと、この場合…original_arrとnew_vecがスコープ外で使えないのは私と一緒だね。ライフタイムというのが…イマイチピンと来ない…貸し出し主本人に会えないのに貸し続けてもらうわけにはいかない、みたいな?」
Rust「ここは例え話ではなく私とTSさんの”違い”を明らかにした方がわかりやすいかと。」
違い…?Rustちゃんは何ともなしに言ったけど、何故か身体が強張ってしまった。
CSS「いやここまでずっと違いしか見てこなかったじゃんwww」
Rust「ああいえ、すみません。TSさんとの違いというよりは私独特の性質なんですが」
CSS「…ふざけて言ってる…?それとも天然で言っているのか…?」
TS「結局”違い”とは…?」
食い気味に聞いてしまった
Rust「私…ガベージコレクタを持っていないんです。」
TS「使わなくなったメモリ領域を自動でお掃除してくれるあの機能…?」
Rust「そうです。無いって言うと不正確かもしれませんが…私自身が自分でメモリを片付けています。」
TS「えぇー?!その神業ができるのってC言語先生やC++先生ぐらいだと思ってた…」
Rust「姉の…C++の話は苦手なので避けていただけると…」
CSS「そもそも姉妹なの?!全く気付かなかった…」
Rust「義理ですがね。それに姉のは神業でもなんでもなく姉がズボラなだけです。」
TS「先生相手に結構ズタボロに言うね…ところで、じゃあどうやって”お掃除”してるの?」
Rust「基本的には、リソースの所有権を持つ変数がスコープ終端まで着いたら、そのリソースは片付けています。リソースが生成されてからこの片付けられるまでの期間を、ライフタイムと呼んでいます。4番目の制約として紹介していた通り、リソースへの参照もライフタイムより長くは生きられません。」カタカタ…
fn main() {
let x: i32 = 1;
let mut p: &i32 = &x;
println!("x = {}, p = {}", x, p);
{
// ここで宣言しているyはスコープ外からは参照できない
let y: i32 = 2; // #### yのライフタイムはここから
p = &y; // yが生きているので&yをpに代入できる
println!("x = {}, y = {}, p = {}", x, y, p); // xはここからでも参照可能
} // ここでyを片付ける。#### yのライフタイムはここまで。&yも消失
// pの中身である&yは無効な参照になっているのでここでの出力はコンパイルエラー
// println!("x = {}, p = {}", x, p);
p = &x; // xはまだ生きている
println!("x = {}, p = {}", x, p);
} // ここで全てのリソースが解放される
CSS「スコープの終端って中括弧の終わり(})のことか。さっきからリソース操作のために{や}が登場していたんだな。」
TS「そして所有権を気にしているのは片付ける時に困るからだったんだね…なるほど…」
所有権とライフタイムは、Rustちゃん流の片付け術、といったところなのだろう。
Rust「TSさんのコードではおそらく、配列への参照が残っている限りarrやarr2の参照先は片付けられずに残っているはずです。同様の仕組みを提供する打って付けの型があるので、それを使って書き直せば完成になります。」
TS「”マジ”でこだわってますね…そこまでリソースについて考えたことないや」
CSS「(“マジ”でこだわってますね…?)」
Rc
Rust「こだわり実現のため、スコープの終端ではなく、すべての参照がなくなった時にリソースを解放するスマートポインタ Rcを使います。RcはReference Counter、和訳すると参照カウンタの略です。RcはDerefトレイトを実装しており不変参照のように扱えます。」カタカタ…
-
- Rcは、参照カウント方式のスマートポインタ – The Rust Programming Language 日本語版
- Derefトレイトでスマートポインタを普通の参照のように扱う – The Rust Programming Language 日本語版
#[derive(Debug)]
struct Hoge;
impl Hoge {
fn use_ref(&self) {
println!("called use_ref: {:?}", self);
}
}
impl Drop for Hoge {
fn drop(&mut self) {
println!("Hoge is dropped");
}
}
use std::rc::Rc;
fn main() {
println!("Program start");
{
let joge: Rc<Hoge>;
{
let ioge: Rc<Hoge>;
{
// 初めてのリソース確保
let hoge: Rc<Hoge> = Rc::new(Hoge); // カウント1
ioge = Rc::clone(&hoge); // ++カウント == 2
println!("{:?} hoge is dropping.", hoge); // --カウント == 1
}
// Rc::clone(&hoge)と同じ
joge = ioge.clone(); // ++カウント == 2
println!("{:?} ioge is dropping.", ioge); // --カウント == 1
}
joge.use_ref(); // RcはDerefトレイトを実装しているのでRc<Hoge>は&Hogeとして使える
println!("{:?} joge is dropping.", joge); // --カウント == 0
} // ここでHogeがdropされる
println!("Program end");
}
Program start
Hoge hoge is dropping.
Hoge ioge is dropping.
called use_ref: Hoge
Hoge joge is dropping.
Hoge is dropped
Program end
Rust「上記の例では、私がリソースを破棄(drop)する際の挙動を実装できるDropトレイトを利用して、参照がすべて消滅した際にリソースが片付けられていることを確認しています。」ホワイトボードカキカキ…
Rust「hoge、ioge、jogeの型がすべてRcである点がポイントです。Rcによるリソース管理では『貸し出し主』と『借用』のような上下関係、貸借関係はなく、参照自体がリソース管理の鍵となっています。」
CSS「貸借関係がない…さっきで言うと貸し出し主のoriginal_arrやnew_vecが要らなくなるわけだ。」
Rust「はい。登場する変数は対等にリソースを参照しています。このように別な見方として、しばしば所有権を複数で共有するために使われるスマートポインタ、だと紹介されます。」
TS「挙動については、図を見た感じ、Rc::clone関数11で参照だけコピーしているみたい!これって、初見殺しあるあるで元々実現したかったシャローコピーの挙動だね。」
Rust「実はこだわりたかった理由は『参照だけコピー』という雰囲気を出したいからというのもありました。ではいよいよ、Rcを使ってあるあるを実現してみますね」カタカタ…
TS「いよいよだね!緊張する…」ドキドキ…
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let arr: Vec<Rc<RefCell<Vec<usize>>>> = vec![
Rc::new(RefCell::new(vec![1, 2, 3])),
Rc::new(RefCell::new(vec![4, 5, 6])),
];
// ↓cloneメソッドが再帰的に呼ばれる。Rcのcloneは参照だけコピー
let mut arr2: Vec<Rc<RefCell<Vec<usize>>>> = arr.clone();
arr2[1] = Rc::new(RefCell::new(vec![7, 8, 9]));
println!("arr = {:?}", arr);
println!("arr2 = {:?}", arr2);
(arr2[0].borrow_mut())[0] = 10;
println!("arr = {:?}", arr);
println!("arr2 = {:?}", arr2);
}
$ rustc main.rs
$ ./main
arr = [RefCell { value: [1, 2, 3] }, RefCell { value: [4, 5, 6] }]
arr2 = [RefCell { value: [1, 2, 3] }, RefCell { value: [7, 8, 9] }]
arr = [RefCell { value: [10, 2, 3] }, RefCell { value: [4, 5, 6] }]
arr2 = [RefCell { value: [10, 2, 3] }, RefCell { value: [7, 8, 9] }]
CSS「実行結果はちゃんと初見殺しあるあるとおんなじだ」
TS「変数が減った分、さっきよりすっきりしたね!」
Rust「リソースのライフタイムはarrとarr2両方が消えるまでになっています。図に描き起こすと…」ホワイトボードカキカキ…
TS「無事、初見殺しあるあるを描き写した感じになったね!」
エピローグ
もうすでに外は真っ暗で、いつもの委員会活動ならとっくに家に帰っているような時刻になっていた。
TS「しっかしシャローコピーの実装だけにこんなに時間がかかるとは思わなかったよ…」
Rust「TSさんが伝えたかった処理をソースコードとして書けて、私にとってもとても勉強になりました。」
CSS「なんで私まで…なんてね。Rustちゃんのことよくわかった気がするし、2人が仲良くなれたみたいで良かった良かった。」
TS「なんか気恥ずかしいな…ハハハ…それにしても最後に書いたコードも大分読むのが大変そうな見た目だったよね。いつもあんななの?」
Rust「いえ、『あんな変なこと』は普段しないので…RcやRefCellが必要になることは滅多にないですね。」
TS「へん…(涙)」
CSS「あー!またTSちゃんを落ち込ませて!Rustちゃん…そういうとこだぞ…」
Rustちゃんこそ変なところで堅いし、何考えているかよくわからないし、でも実はすごいところがあったりして…そんな秘密を知ることができて、今日はとても楽しい一日でした!
~ fin ~
あとがき
本小説は、前回出した記事中の「TypeScriptとRustがペアプロする物語が書きたい!」という思いをこらえきれず具現化したものになります。
小説スタイルで技術的な話をするのは、結構難しい部分が多かったです。
-
- 話の文脈を断ち切りどうしても伝えたいことを書く、というわけにいかない
-
- 目次やまとめが入れにくい
-
- 最後まで読んでもらわないと何を伝えたかったかが伝わらない
-
- そもそも伝えたいことをストレートに伝えるのが難しい
- 推敲が難しすぎる
…というわけでこの位置になってしまったことを後悔しつつ、記事で伝えたかったことを書きます。
伝えたかったこと
Rustは所有権やスマートポインタのおかげでシャローコピーのような複雑な構造は記述が目立つようにできており、良くない構造が書きにくくなっている。
所有権やスマートポインタといった仕組みは最適化のために科せられた必要悪ではなく、変な設計をしないための積極的な防御策であり、味方につけるととても心強い
「良くない構造が書きにくい」というのは、裏を返すと「書きやすい構造は良い構造である」ということで、そこを前半の「Rustのcloneはデフォルトでディープコピーである」のところで表現したつもりでした。
後半のRustちゃんがムッっとなって可変参照やスマートポインタを使って無理やりシャローコピーを実現したパートでは、本当に伝えたかったのは上述の通り「Rustでは下手な構造はそもそも書きにくくなっている」ということだったのですが、変に「Rustはやっぱり難しい言語…」という印象をつけてしまったかもしれず、ちょっとだけ後悔しています。ドクシャサマドウカサイゴノアトガキマデヨンデクダサイマスヨウニ…
伝えたいメッセージが少ない記事ならばやめ太郎さんのように対話型で書くのは効果的かもしれませんが、小説スタイルで書かれた記事があんまりない理由は今回身に染みてわかりました(苦笑)。次回記事はまた普通の書き方をしていると思います。多分。
ここまで読んでくださり誠にありがとうございました!!
2023/07/19 追記
RustではなくReactが題材ですが、続きを書きました!読んでいただけたら幸いです。
おまけ Copyトレイト
- Copy in std::marker – Rust
本来はCloneトレイトにて一緒に説明するべきトレイトだったのですが、尺の都合上?説明できなかったためおまけに掲載させていただきます。
Copyトレイトは簡単に説明すると、「本来は所有権譲渡が行われる場所・書き方で、クローンしないとエラーになる場所について、自動で.clone()が付いていたことにしてくれるマーカートレイト」です。
例を示します。所有権譲渡のため以下はエラーでした。
#[derive(Debug, Clone)]
struct Hoge {
v: Vec<usize>
}
fn main() {
let hoge = Hoge { v: vec![1, 2, 3] };
let hoge2 = hoge; // 所有権譲渡のためエラー!
println!("{:?}", hoge);
println!("{:?}", hoge2);
}
Cloneトレイトの節では複製により所有権譲渡が発生しないというcloneメソッドを用いた解決方法を提示しました。(#[derive(Clone)]と書くことで構造体にCloneトレイトが自動実装されています。)
#[derive(Debug, Clone)]
struct Hoge {
v: Vec<usize>
}
fn main() {
let hoge = Hoge { v: vec![1, 2, 3] };
- let hoge2 = hoge;
+ let hoge2 = hoge.clone();
println!("{:?}", hoge);
println!("{:?}", hoge2);
}
しかし、使用するメモリ領域が小さく、いちいちcloneを呼ぶ方が手間だと感じるような構造体もたまにはあるでしょう。Copyトレイトはそんなときに使えます。
#[derive(Debug, Clone, Copy)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 1, y: 2 };
let p2 = p; // 裏ではp.clone()となっている。
println!("{:?}", p);
println!("{:?}", p2);
}
注目してほしいのは#[derive(Debug, Clone, Copy)]のCopyです。#[derive(Copy)]とすることでこのCopyトレイトが実装され、.clone()を省略しても所有権譲渡の場面では勝手に複製されるようになります。
ところで何故CloneトレイトとCopyトレイトが分かれているのでしょうか?Cloneトレイトだけでは自動でクローンしてくれないのはなぜなのでしょう?これはリソースの複製というコストがかかる操作がどこで行われているかが、ソースコードを読めばすぐ分かるようにするためです。Copyトレイトをあえて実装しないことで、cloneメソッドを明記しなければならず、そこがコストがかかる箇所だとすぐわかるようになっています。
fn main() {
let s = String::from("hoge");
// 参照を取るだけ。軽そう
let v1 = (0..10).map(|_| &s).collect::<Vec<_>>();
// .clone()が呼ばれてる?!重そう!!!
let v2 = (0..10).map(|_| s.clone()).collect::<Vec<_>>();
}
この考えに則れば、Copyトレイト付与を乱用するのは避けるべきということがわかるでしょう。
以下、CloneトレイトとCopyトレイト付与のために求められる構造体の条件です。
Cloneトレイト
構造体が持つすべてのフィールドの型がCloneトレイトを実装していること
Copyトレイト
構造体自体がCloneトレイトを実装していること
構造体が持つすべてのフィールドの型がCloneトレイトとCopyトレイトを共に実装していること
よく「プリミティブ型は所有権譲渡が発生しないけど構造型や文字列型、ベクタ型は所有権譲渡が発生する」といった解説を目にしますが、これは事実としては合っていますがあまり的を射ていません。「所有権譲渡が発生しないとされるプリミティブ型」の公式ドキュメントに行ってみると、Copyトレイトが実装されているのを確認できるはずです。すなわち「Copyトレイトが実装された型は勝手に複製され所有権譲渡が発生しない」と説明したほうがより一般的な解説になっています。
参考・引用
-
- 変数宣言: letとconst | TypeScript入門『サバイバルTypeScript』
-
- オブジェクト型のreadonlyプロパティ (readonly property) | TypeScript入門『サバイバルTypeScript』
- 参照と借用 – The Rust Programming Language 日本語版
実行環境
$ ts-node --version
v10.9.1
$ npm ls -g typescript
/usr/local/lib
├─┬ ts-node@10.9.1
│ └── typescript@5.0.4
└── typescript@5.0.4
$ cargo --version
cargo 1.68.2 (6feb7c9cf 2023-03-26)
以降で詳しく解説しますが、配列型にはCloneトレイト及びCopyトレイトの両方が実装されていて解説の上で不都合だったのでVecを選んだ経緯もあります。 ↩
TypeScriptではかなり色々な使われ方をしているキーワードのようですね。今回のはclassにimplementsする使い方をするinterfaceです。 ↩
裏を返すとCloneトレイトが実装されていない型がVecの要素になっていた場合、つまり、Cloneを実装しないTに対するVecについては、Cloneが実装されずcloneメソッドを呼び出すことはできません。ジェネリクスを上手く指定することでこのような実装が可能です。 ↩
不変性は基本的には再帰的に成り立ちますが、フィールドのどこかに可変参照を内包するような構造体については、その不変参照からも可変参照を通して”中身を変えられる”ことに気を付けてください。 ↩
スライスに存在するsortメソッドの車輪再開発です。 ↩
Pythonの場合は元の配列に一切手が加わりません。Rustの場合はどっちとも言えず、cloneを使って複製したものを渡す場合は値渡しのため元の配列に影響がありませんが、そのまま渡す場合はそもそも所有権が奪われます。 ↩
正確には、DerefトレイトやDerefMutトレイトを実装している型からTを取り出す操作になります。Derefトレイトでスマートポインタを普通の参照のように扱う – The Rust Programming Language 日本語版 ↩
参照外しで所有権譲渡のようなことが発生しそうなシーンでは、Copyトレイトを実装した型でないとコンパイルエラーになると思います。Copyトレイトが実装されている場合は、Clone::clone(&self)が呼ばれているため、結局所有権自体は不要です。Copyトレイトについて詳しくはおまけ Copyトレイトを見てください。 ↩
この辺も正確にどのように参照が評価されているかを考えるのが難しい部分のため、解説に誤りがある可能性があります。やっぱライフタイムって難しいですね… ↩
本当はRc型に実装されたCloneトレイト由来のメソッドcloneで、コード中にも書いたようにRc型の変数についてhoge.clone()のように呼び出すことも可能です。一般的なcloneメソッドではリソースのクローンが行われるコストがかかる一方、こちらは参照のみクローンされコストがかかりません。それを区別するため、慣例的に関連関数のように呼び出す文化があるそうです。その文化に合わせました。 ↩