Progrust Library.

// book chapter

動かして学ぶRustプログラミング問題集

第7章 所有権とムーブ.

ここからは、Rust最大の特徴である所有権に入ります。
値がいつ破棄されるのか、なぜ代入しただけで元の変数が使えなくなるのかを、8問で身につけます。

第6章までに何度か「なぜか後から使えなくなる」という現象を保留にしてきました。ベクタをfor式にそのまま渡すと後で使えない、String同士を+でつなぐと左辺が使えない——その答えがすべてこの章にあります。

所有権はガベージコレクション(GC)を持たないRustが、メモリの解放をコンパイル時にすべて決めるための仕組みです。他の言語にはない考え方なので最初は戸惑いますが、ルール自体は3つだけです。エラーメッセージを読みながら1問ずつ進めていけば、自然と体に入ります。

進め方は第6章までと同じです。各問題の冒頭に関連する辞書へのリンクを挙げているので、まずはリンク先で必要な知識を確認してから取り組んでください。

01 - スコープと値の寿命

所有権変数に関する問題です。
次のコードはコンパイルエラー(E0425)になります。println!で値が出力できるように、コードの位置を1箇所だけ動かして修正してください。

期待する出力
レシート: 弁当 500円
「Playgroundで開く」をクリックして修正・実行してください
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!が使う変数を変えて修正してください。

期待する出力
注文内容: コーヒー ×2
「Playgroundで開く」をクリックして修正・実行してください
fn 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つでなければならないため、orderconfirmedが同じ文字列を同時に所有することはできません。そこでRustは所有権をorderからconfirmedへ移動させ、移動元のorderを使えない状態にします。この移動をムーブ(move)と呼びます。

エラーメッセージもborrow of moved value: 'order'(E0382)と、「ムーブ済みの値を使おうとしている」と教えてくれます。

ムーブは特別な操作を書かなくても、値の受け渡しのたびに自動で起こります。

場面 移動先
変数への代入 let b = a; b
関数の引数への受け渡し f(a) 関数の引数
関数からの戻り値 let b = f(); b

なぜこんな仕組みになっているかというと、問題01の「所有者がスコープを抜けたら解放する」というルールと組み合わせるためです。もしorderconfirmedの両方が同じ文字列を所有できてしまうと、スコープの終わりで同じメモリを2回解放してしまいます。移動元を無効にしておけば、解放されるのは1回だけだと保証できます。

03 - Copy型はムーブしない

ムーブ整数型に関する問題です。
整数を別の変数に代入し、代入元と代入先の両方を出力してください。問題02のStringと結果を見比べてみてください。

期待する出力
元の値: 150
コピー先: 150
「Playgroundで開く」をクリックして修正・実行してください
fn 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型(ムーブしない)
StringVec<T> 整数型・浮動小数点型・boolchar
&str(文字列スライス)・上記だけからなるタプル

見分け方の目安は「後始末が必要かどうか」です。整数は決まった大きさの箱に値が収まっているだけなので、まるごと複製しても誰も困りませんし、破棄するときも特別な後始末は要りません。一方StringVec<T>は実行時に確保したメモリを解放する責任を負っているため、所有者を1つに保つ必要があります。

第5章で整数の配列をfor式にそのまま渡しても後から使えたのに、第6章でベクタでは&が必要だったのは、この違いによるものです。

04 - 関数に渡すとムーブする

ムーブ関数所有権に関する問題です。
次のコードはコンパイルエラー(E0382)になります。2行の順番を入れ替えるだけで修正できます。どちらを先に書けばよいか考えてください。

期待する出力
控え: コーヒー ×2
注文内容: コーヒー ×2
「Playgroundで開く」をクリックして修正・実行してください
fn 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
控え: コーヒー ×2
「Playgroundで開く」をクリックして修正・実行してください
fn 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);のように別名でも構いません。

所有権の動きを追うと、次のように一周して戻ってきています。

引数として渡す

戻り値として返す

main の order

print_order の order

main の order(束縛し直し)

ただしこの書き方は、値を1つ受け渡すたびに戻り値の面倒を見なければならず、関数が複数の引数を取るとすぐに煩雑になります。第8章の参照を学べば、こうした受け渡しはほとんど不要になります。

06 - クローンで複製する

クローンムーブに関する問題です。
問題04と同じコードです。今度は順番を変えずにprint_orderへ渡す値に手を加えて修正してください。

期待する出力
注文内容: コーヒー ×2
控え: コーヒー ×2
「Playgroundで開く」をクリックして修正・実行してください
fn 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()と自分で書く
コスト 無視できるほど小さい 型による(大きくなりうる)
対象の型 整数型など StringVec<T>など

Stringのクローンでは、文字列データそのものをまるごと複製します。長い文字列や要素の多いベクタでは、それなりの時間とメモリを使う操作です。

07 - クローンは独立した複製

クローン文字列型に関する問題です。
文字列をクローンし、複製のほうだけに追記してください。原本が影響を受けないことを確認します。

期待する出力
原本: 買い物メモ
複製: 買い物メモ / 牛乳
「Playgroundで開く」をクリックして修正・実行してください
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 / 2
「Playgroundで開く」をクリックして修正・実行してください
fn 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は整数なのでムーブは起きません。何も書かなくてもcountdoubledの両方が最後まで使えます。

let backup = order.clone();(クローン)
let backup = &order;のような書き方では「追記する前の内容」は残せません。参照は値そのものを持たないので、原本が書き換われば見える内容も変わってしまうからです(正確にはこの書き方は第8章で学ぶ借用規則にも触れてコンパイルが通りません)。追記前の姿を独立した値として保存したいので、ここはクローンが正解です。

let order = shout(order);(ムーブと戻り値)
shoutStringを引数に取るので、渡した時点で所有権が移ります。戻り値で返ってくる所有権をlet order = ...で受け取り直すことで、その後のprintln!でも使えるようになります。ここをshout(order);とだけ書くと、次の行でE0382になります。

これで第7章は終わりです。所有権・ムーブ・クローンという、Rustのメモリ管理の骨格が一通りそろいました。

ただし現時点の道具では、関数に値を見せるたびに「戻り値で返してもらう」か「複製を作る」かのどちらかを選ばなければならず、どう考えても不便です。実際のRustコードではそのどちらでもない第3の方法——所有権を渡さずに値を貸す——が主役になります。次の章のテーマである参照と借用です。