// book chapter
動かして学ぶRustプログラミング問題集第7章 所有権とムーブ.
ここからは、Rust最大の特徴である所有権に入ります。
値がいつ破棄されるのか、なぜ代入しただけで元の変数が使えなくなるのかを、8問で身につけます。
第6章までに何度か「なぜか後から使えなくなる」という現象を保留にしてきました。ベクタをfor式にそのまま渡すと後で使えない、String同士を+でつなぐと左辺が使えない——その答えがすべてこの章にあります。
所有権はガベージコレクション(GC)を持たないRustが、メモリの解放をコンパイル時にすべて決めるための仕組みです。他の言語にはない考え方なので最初は戸惑いますが、ルール自体は3つだけです。エラーメッセージを読みながら1問ずつ進めていけば、自然と体に入ります。
進め方は第6章までと同じです。各問題の冒頭に関連する辞書へのリンクを挙げているので、まずはリンク先で必要な知識を確認してから取り組んでください。
01 - スコープと値の寿命
所有権と変数に関する問題です。
次のコードはコンパイルエラー(E0425)になります。println!で値が出力できるように、コードの位置を1箇所だけ動かして修正してください。
レシート: 弁当 500円fn main() {
{
let receipt = String::from("レシート: 弁当 500円");
}
println!("{}", receipt);
}Playgroundで開く解答例と解説
fn main() {
{
let receipt = String::from("レシート: 弁当 500円");
println!("{}", receipt); // 波括弧の中へ移動した
}
println!("{}", receipt);
}Playgroundで開く波括弧{}で囲まれた範囲をスコープと呼びます。変数が使えるのは、宣言されたスコープの中だけです。receiptは内側の波括弧の中で宣言されているので、その外側には存在しません。エラーメッセージのcannot find value 'receipt' in this scope(E0425)は、まさに「このスコープにreceiptという値は見つからない」と言っています。
スコープはこれまでもfn main() { ... }やfor i in 0..3 { ... }という形でずっと使ってきました。ここで新しいのは、スコープを抜けるときに値が破棄されるという点です。
Rustでは、すべての値がただ1つの所有者(owner)を持ちます。上のコードでは変数receiptが文字列の所有者です。そして所有者がスコープを抜けると、その値は自動的に破棄され、メモリが解放されます。
fn main() {
{
let receipt = String::from("レシート: 弁当 500円");
println!("{}", receipt);
} // ここで所有者receiptがスコープを抜け、文字列は自動で破棄される
println!("スコープを抜けた後");
}Playgroundで開くStringは実行時に必要なだけメモリを確保する型でした。そのメモリはいつか誰かが解放しなければなりませんが、Rustには解放を代行するGCがありません。かわりに「所有者がスコープを抜けた地点で解放する」というルールを置くことで、コンパイル時に解放する場所を決めてしまうのがRustのやり方です。
02 - ムーブ
ムーブと所有権に関する問題です。
次のコードはコンパイルエラー(E0382)になります。エラーメッセージを読み、println!が使う変数を変えて修正してください。
注文内容: コーヒー ×2fn main() {
let order = String::from("コーヒー ×2");
let confirmed = order;
println!("注文内容: {}", order);
}Playgroundで開く解答例と解説
fn main() {
let order = String::from("コーヒー ×2");
let confirmed = order; // 所有権がorderからconfirmedへ移動した
println!("注文内容: {}", order);
println!("注文内容: {}", confirmed);
}Playgroundで開くlet confirmed = order;は、一見すると値をコピーしているように見えます。しかし所有者は常にただ1つでなければならないため、orderとconfirmedが同じ文字列を同時に所有することはできません。そこでRustは所有権をorderからconfirmedへ移動させ、移動元のorderを使えない状態にします。この移動をムーブ(move)と呼びます。
エラーメッセージもborrow of moved value: 'order'(E0382)と、「ムーブ済みの値を使おうとしている」と教えてくれます。
ムーブは特別な操作を書かなくても、値の受け渡しのたびに自動で起こります。
| 場面 | 例 | 移動先 |
|---|---|---|
| 変数への代入 | let b = a; |
b |
| 関数の引数への受け渡し | f(a) |
関数の引数 |
| 関数からの戻り値 | let b = f(); |
b |
なぜこんな仕組みになっているかというと、問題01の「所有者がスコープを抜けたら解放する」というルールと組み合わせるためです。もしorderとconfirmedの両方が同じ文字列を所有できてしまうと、スコープの終わりで同じメモリを2回解放してしまいます。移動元を無効にしておけば、解放されるのは1回だけだと保証できます。
03 - Copy型はムーブしない
ムーブと整数型に関する問題です。
整数を別の変数に代入し、代入元と代入先の両方を出力してください。問題02のStringと結果を見比べてみてください。
元の値: 150
コピー先: 150fn main() {
let price = 150;
// priceの値をcopiedに代入せよ
println!("元の値: {}", price);
println!("コピー先: {}", copied);
}Playgroundで開く解答例と解説
fn main() {
let price = 150;
// priceの値をcopiedに代入せよ
let copied = price;
println!("元の値: {}", price); // ムーブしていないのでpriceも使える
println!("コピー先: {}", copied);
}Playgroundで開く問題02とまったく同じlet 変数 = 変数;という書き方なのに、今回は代入元のpriceもそのまま使えます。整数のような型ではムーブが起きず、値そのものが複製されるからです。
このような型をCopy型と呼びます。これまで学んだ型では次のように分かれます。
| ムーブする型 | Copy型(ムーブしない) |
|---|---|
String・Vec<T> |
整数型・浮動小数点型・bool・char |
&str(文字列スライス)・上記だけからなるタプル |
見分け方の目安は「後始末が必要かどうか」です。整数は決まった大きさの箱に値が収まっているだけなので、まるごと複製しても誰も困りませんし、破棄するときも特別な後始末は要りません。一方StringやVec<T>は実行時に確保したメモリを解放する責任を負っているため、所有者を1つに保つ必要があります。
第5章で整数の配列をfor式にそのまま渡しても後から使えたのに、第6章でベクタでは&が必要だったのは、この違いによるものです。
04 - 関数に渡すとムーブする
ムーブ・関数・所有権に関する問題です。
次のコードはコンパイルエラー(E0382)になります。2行の順番を入れ替えるだけで修正できます。どちらを先に書けばよいか考えてください。
控え: コーヒー ×2
注文内容: コーヒー ×2fn print_order(order: String) {
println!("注文内容: {}", order);
}
fn main() {
let order = String::from("コーヒー ×2");
print_order(order);
println!("控え: {}", order);
}Playgroundで開く解答例と解説
fn print_order(order: String) {
println!("注文内容: {}", order);
}
fn main() {
let order = String::from("コーヒー ×2");
print_order(order);
println!("控え: {}", order); // ムーブする前に使う
print_order(order); // ここで所有権が関数へ移動する
}Playgroundで開く関数に値を渡すときにも、変数への代入とまったく同じようにムーブが起きます。print_order(order)と書いた瞬間に所有権は関数の引数orderへ移り、呼び出し元のorderは使えなくなります。そして関数を抜けるときに引数がスコープを抜けるので、文字列はそこで破棄されます。
つまりこのコードは「注文内容を渡したら、その注文票ごと持っていかれて手元には何も残らない」という状態です。修正版のようにムーブより前であれば元の変数は問題なく使えますが、これでは「渡した後にもう一度使う」ことはできません。
実際のプログラムでは「関数に値を見せたいだけで、渡してしまいたいわけではない」ほうが圧倒的に多くなります。その解決策が次の3つです。
| 方法 | 扱う問題 |
|---|---|
| 関数が戻り値で所有権を返す | 問題05 |
| 複製を作って渡す | 問題06 |
| 所有権を渡さずに「貸す」 | 第8章 |
05 - 戻り値で所有権を返す
ムーブと関数に関する問題です。
print_orderは受け取った値を戻り値として返すようになっています。呼び出した後もorderを使い続けられるように、呼び出しの1行を書いてください。
注文内容: コーヒー ×2
控え: コーヒー ×2fn print_order(order: String) -> String {
println!("注文内容: {}", order);
order
}
fn main() {
let order = String::from("コーヒー ×2");
// print_orderにorderを渡し、返ってきた値をorderに束縛し直せ
println!("控え: {}", order);
}Playgroundで開く解答例と解説
fn print_order(order: String) -> String {
println!("注文内容: {}", order);
order // 末尾の式なので、受け取った所有権をそのまま返す
}
fn main() {
let order = String::from("コーヒー ×2");
// print_orderにorderを渡し、返ってきた値をorderに束縛し直せ
let order = print_order(order);
println!("控え: {}", order);
}Playgroundで開く戻り値でもムーブは起きます。関数の中でorderという末尾の式を書くと、引数として受け取った所有権が呼び出し元へ返ります。呼び出し元はそれをlet order = ...で受け取り直すことで、また使えるようになります。
同じ名前orderで束縛し直しているのは、第1章で学んだシャドーイングです。もちろんlet returned = print_order(order);のように別名でも構いません。
所有権の動きを追うと、次のように一周して戻ってきています。
ただしこの書き方は、値を1つ受け渡すたびに戻り値の面倒を見なければならず、関数が複数の引数を取るとすぐに煩雑になります。第8章の参照を学べば、こうした受け渡しはほとんど不要になります。
06 - クローンで複製する
クローンとムーブに関する問題です。
問題04と同じコードです。今度は順番を変えずに、print_orderへ渡す値に手を加えて修正してください。
注文内容: コーヒー ×2
控え: コーヒー ×2fn print_order(order: String) {
println!("注文内容: {}", order);
}
fn main() {
let order = String::from("コーヒー ×2");
print_order(order);
println!("控え: {}", order);
}Playgroundで開く解答例と解説
fn print_order(order: String) {
println!("注文内容: {}", order);
}
fn main() {
let order = String::from("コーヒー ×2");
print_order(order);
print_order(order.clone()); // 複製を作って渡す
println!("控え: {}", order); // 原本の所有権は残っている
}Playgroundで開く.clone()は、値の独立した複製を作るメソッドです。関数へムーブしていくのは複製のほうなので、原本を持つorderはそのまま使い続けられます。
ムーブが「注文票を渡してしまう」ことだとすれば、クローンは「コピーを取って渡す」ことにあたります。
Copy型との違いは、明示的に書くかどうかです。
Copy |
Clone(.clone()) |
|
|---|---|---|
| 起き方 | 代入や受け渡しで自動的に | .clone()と自分で書く |
| コスト | 無視できるほど小さい | 型による(大きくなりうる) |
| 対象の型 | 整数型など | String・Vec<T>など |
Stringのクローンでは、文字列データそのものをまるごと複製します。長い文字列や要素の多いベクタでは、それなりの時間とメモリを使う操作です。
07 - クローンは独立した複製
クローンと文字列型に関する問題です。
文字列をクローンし、複製のほうだけに追記してください。原本が影響を受けないことを確認します。
原本: 買い物メモ
複製: 買い物メモ / 牛乳fn main() {
let original = String::from("買い物メモ");
// originalをクローンしてcopyに束縛せよ
copy.push_str(" / 牛乳");
println!("原本: {}", original);
println!("複製: {}", copy);
}Playgroundで開く解答例と解説
fn main() {
let original = String::from("買い物メモ");
// originalをクローンしてcopyに束縛せよ
let mut copy = original.clone();
copy.push_str(" / 牛乳");
println!("原本: {}", original);
println!("複製: {}", copy);
}Playgroundで開くStringのクローンで作られる2つの値は完全に独立しています。それぞれが自分専用のメモリに文字列を持っているので、片方に追記しても、もう片方はまったく影響を受けません。
mutが必要なのはcopyだけです。originalは最後まで読むだけなのでmutは要りません。第1章で学んだ「変数はデフォルトで不変」というルールは、クローンした値にもそのまま当てはまります。
これは当たり前のことのようですが、他の多くの言語では代入しただけでは中身が共有され、片方を変更するともう片方も変わってしまう(参照の共有)というつまずきがよく起こります。Rustでは.clone()と書いてあれば独立した複製、書いていなければムーブかCopyと、コードを見ただけで区別できます。
08 - 応用: 所有権の行方を追う
この章の総復習として、ムーブ・クローン・所有権・関数を組み合わせた問題です。
2箇所の空欄を埋めて、期待する出力にしてください。backupには追記する前の内容を残しておく必要があります。
コーヒー ×2!!
原本: コーヒー ×2
控え: コーヒー
個数: 2 / 2fn shout(text: String) -> String {
println!("{}!!", text);
text
}
fn main() {
let count = 2;
let doubled = count;
let mut order = String::from("コーヒー");
// 現在のorderの内容を控えとしてbackupに残せ
order.push_str(" ×2");
// shoutにorderを渡し、呼び出した後もorderを使い続けられるようにせよ
println!("原本: {}", order);
println!("控え: {}", backup);
println!("個数: {} / {}", count, doubled);
}Playgroundで開く解答例と解説
fn shout(text: String) -> String {
println!("{}!!", text);
text
}
fn main() {
let count = 2;
let doubled = count; // 整数はCopy型なのでムーブしない
let mut order = String::from("コーヒー");
// 現在のorderの内容を控えとしてbackupに残せ
let backup = order.clone();
order.push_str(" ×2");
// shoutにorderを渡し、呼び出した後もorderを使い続けられるようにせよ
let order = shout(order);
println!("原本: {}", order);
println!("控え: {}", backup);
println!("個数: {} / {}", count, doubled);
}Playgroundで開くこの章で扱った3つの振る舞いが1つのコードに同居しています。それぞれ確認していきましょう。
let doubled = count;(Copy)
countは整数なのでムーブは起きません。何も書かなくてもcountとdoubledの両方が最後まで使えます。
let backup = order.clone();(クローン)
let backup = ℴのような書き方では「追記する前の内容」は残せません。参照は値そのものを持たないので、原本が書き換われば見える内容も変わってしまうからです(正確にはこの書き方は第8章で学ぶ借用規則にも触れてコンパイルが通りません)。追記前の姿を独立した値として保存したいので、ここはクローンが正解です。
let order = shout(order);(ムーブと戻り値)
shoutはStringを引数に取るので、渡した時点で所有権が移ります。戻り値で返ってくる所有権をlet order = ...で受け取り直すことで、その後のprintln!でも使えるようになります。ここをshout(order);とだけ書くと、次の行でE0382になります。
これで第7章は終わりです。所有権・ムーブ・クローンという、Rustのメモリ管理の骨格が一通りそろいました。
ただし現時点の道具では、関数に値を見せるたびに「戻り値で返してもらう」か「複製を作る」かのどちらかを選ばなければならず、どう考えても不便です。実際のRustコードではそのどちらでもない第3の方法——所有権を渡さずに値を貸す——が主役になります。次の章のテーマである参照と借用です。