[Rust 入門] #03 所有権 — ムーブ、Copy、clone を実際に動かして確かめる

[Rust 入門] #03 所有権 — ムーブ、Copy、clone を実際に動かして確かめる

はじめに

第2回の最後に、こう書きました。

let s = String::from("hello"); のあと、s を、別の変数に渡すと、何が起きるのか

今回は、その答えにあたる、所有権です。Rust が、GC なしでメモリの安全さを守る仕組みで、Java には無い考え方です。

所有権とは

所有権には、3つの決まりがあります。

  1. 値には、持ち主(所有者)が、1つだけ決まっている
  2. 持ち主は、同時に、1つだけ
  3. 持ち主がスコープ({ } の範囲)を出ると、値は捨てられる(drop)

Java との違いを並べると、こうなります。

観点JavaRust
値を片付けるのはGCスコープの終わりで、自動で
いつ片付くかGC が決めるコードの書き方で、決まっている
b = a と書くと同じ物を、2つの変数が指す値が b に移り、a は使えなくなる
同じ物を、2つの名前から書き換えることできるふつうの書き方では、できない(コンパイルエラー)

表の下の2行を、このあと、1つずつ確かめます。

スコープと drop

「スコープを出ると、値が捨てられる」ことを、目で見られるようにします。捨てられるときに、文字を出す入れ物を作ります。

struct Noisy(&'static str);

impl Drop for Noisy {
    fn drop(&mut self) {
        println!("drop: {}", self.0);
    }
}

fn main() {
    let _a = Noisy("a");
    {
        let _b = Noisy("b");
        println!("内側のブロックの終わり");
    }
    let _c = Noisy("c");
    println!("main の終わり");
}

struct は第5回、impl Drop for は第7回で扱います。ここでは、「捨てられるときに drop: と出す入れ物」とだけ読んでください。

内側のブロックの終わり
drop: b
main の終わり
drop: c
drop: a
出力起きていること
drop: b内側の { } を出た時点で、_b が捨てられます
drop: c → drop: amain の終わりで、作った順の逆に、捨てられます

捨てる処理を、free のように書く必要は、ありません。スコープの終わりで、コンパイラが差し込みます。

文字列の型

第2回で、&str と String の違いを、「第3回で扱う」としていました。ここで回収します。

let lit: &str = "hello";
let mut s: String = String::from("hello");
s.push_str(", world");
println!("{lit} / {s}");
println!(
    "&str={} String={} Vec<i32>={}",
    std::mem::size_of::<&str>(),
    std::mem::size_of::<String>(),
    std::mem::size_of::<Vec<i32>>()
);
hello / hello, world
&str=16 String=24 Vec<i32>=24
型内容変更
&str文字列の一部を、見ているだけ。"hello" のような文字列リテラルは、この型ですできない
String文字列を、自分で持っている。長さを変えられますmut なら、できる

大きさは、&str が 16 バイト、String が 24 バイトです。どちらも、文字列の中身そのものではなく、中身を指す情報の大きさです。String の 24 は、8 バイト × 3 で、位置・長さ・容量の3つ分、と説明されます。Vec<i32> も 24 バイトで、同じ作りです。

この「指す情報だけが動く」ことが、次のムーブの話につながります。

String を変更するには、mut が要ります。mut を付け忘れると、こうなります。

let s = String::from("hello");
s.push_str(", world");
error[E0596]: cannot borrow `s` as mutable, as it is not declared as mutable
 --> mutarg_err.rs:3:5
  |
3 |     s.push_str(", world");
  |     ^ cannot borrow as mutable
  |
help: consider changing this to be mutable
  |
2 |     let mut s = String::from("hello");
  |         +++

第2回の let mut と、同じ話です。

ムーブ

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    println!("{s1}, {s2}");
}
error[E0382]: borrow of moved value: `s1`
 --> mv_err.rs:4:16
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |     println!("{s1}, {s2}");
  |                ^^ value borrowed here after move
  |
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let s2 = s1.clone();
  |                ++++++++

読むところは、次のとおりです。

  • value moved here: 3行目で、値が s2 に移りました(ムーブ)
  • value borrowed here after move: 移ったあとの s1 を、4行目で使おうとしています
  • help: 複製したいなら、.clone() がある、と教えています

let s2 = s1; は、s1 の持ち主の立場を、s2 に譲る操作です。 譲ったあとの s1 は、使えません。

なぜ使えなくするのか

s1 と s2 が、どちらも持ち主のままだと、スコープの終わりで、同じ値を2回捨てることになります。2回目は、すでに無い物を捨てるので、危険です。持ち主を1人にする決まりは、これを防ぎます。

中身はコピーされない

ムーブのとき、文字列の中身は、コピーされているでしょうか。メモリ上の位置を比べて、確かめました。

let s1 = String::from("hello");
let s2 = s1.clone();
println!("clone: 同じ場所? {}", s1.as_ptr() == s2.as_ptr());

let s3 = String::from("hello");
let p3 = s3.as_ptr();
let s4 = s3;
println!("move: 同じ場所? {}", p3 == s4.as_ptr());
clone: 同じ場所? false
move: 同じ場所? true

as_ptr() は、文字列の中身が置かれている位置を返します。

操作中身の位置起きていること
.clone()別の場所中身を、複製した
ムーブ(let s4 = s3;)同じ場所中身は、そのまま。持ち主だけが移った

ムーブは、24 バイトの「指す情報」を渡すだけで、中身は、動きません。重い物を渡しても、渡す側の手間は、変わりません。

Java と並べる

Java で、同じ形のコードを書いて、動かしました。

StringBuilder a = new StringBuilder("hello");
StringBuilder b = a;
b.append(", world");
System.out.println(a);
System.out.println(a == b);
hello, world
true

Java では、b = a のあとも、a は使えます。2つの変数が、同じ物を指していて、b で変えた結果が、a にも見えます。

Rust では、let b = a; のあと、a は使えません。同じ物を、2つの名前から変えてしまう状況が、ふつうの書き方では、そもそも作れません。

Copy

整数は、どうでしょうか。

let a = 5;
let b = a;
println!("{a} {b}");

let p = (1, 2.5);
let q = p;
println!("{:?} {:?}", p, q);
5 5
(1, 2.5) (1, 2.5)

エラーになりません。整数のような小さくて単純な型は、代入のときに、複製されます。これが、Copy 型です。

確かめた型は、次のとおりです。

型代入のあと、元の変数
整数、小数、bool、char使える(Copy)
&str使える(Copy)
タプル (i32, f64)使える(Copy)
配列 [i32; 3]使える(Copy)
String使えない(ムーブ)
Vec<String>使えない(ムーブ)
タプル (i32, String)使えない(ムーブ)
配列 [String; 2]使えない(ムーブ)

タプルと配列は、中身がすべて Copy 型のときだけ、Copy になります。1つでも String が入ると、全体がムーブになります。

let t = (1, String::from("x"));
let u = t;
println!("{:?} {:?}", t, u);
error[E0382]: borrow of moved value: `t`
 --> tup_err.rs:4:27
  |
2 |     let t = (1, String::from("x"));
  |         - move occurs because `t` has type `(i32, String)`, which does not implement the `Copy` trait
3 |     let u = t;
  |             - value moved here

エラーの中の「does not implement the Copy trait」は、「Copy の性質を持っていない」という意味です。トレイトは、第7回で扱います。

Java でも、int の代入は、複製です。

int x = 5;
int y = x;
y++;
System.out.println(x + " " + y);
5 6

clone

ムーブせずに、両方を使いたいときは、.clone() で、複製します。

let s1 = String::from("hello");
let s2 = s1.clone();
println!("{s1} {s2}");
hello hello

中身が複製されるので、前の節の確認のとおり、別の場所に、新しい中身ができます。文字列が長いほど、手間は増えます。エラーメッセージの help に、「if the performance cost is acceptable(手間が許せるなら)」と添えられているのは、そのためです。

関数に渡す

関数に渡すときも、代入と、同じ決まりです。

fn takes(s: String) {
    println!("takes: {s}");
}

fn gives() -> String {
    String::from("from gives")
}

fn takes_and_gives_back(s: String) -> String {
    println!("pass: {s}");
    s
}

fn copy_it(n: i32) {
    println!("copy_it: {n}");
}

fn main() {
    let a = String::from("hello");
    takes(a);

    let b = gives();
    println!("{b}");
    let c = takes_and_gives_back(b);
    println!("{c}");

    let n = 5;
    copy_it(n);
    println!("{n}");
}
takes: hello
from gives
pass: from gives
from gives
copy_it: 5
5
呼び出し所有権
takes(a)a の所有権が、関数に移る。関数の終わりで、捨てられる
gives()関数の中で作った値の所有権が、戻り値として、呼び出し側に移る
takes_and_gives_back(b)渡して、戻り値で、受け取り直す
copy_it(n)i32 は Copy なので、複製が渡る。n は、そのまま使える

takes_and_gives_back の中で、println!("pass: {s}"); のあとでも、s を返せています。println! は、引数を借りるだけで、所有権を取りません。

takes(a) のあとで、a を使うと、エラーです。

let a = String::from("hello");
takes(a);
println!("{a}");
error[E0382]: borrow of moved value: `a`
 --> fn_err.rs:8:16
  |
6 |     let a = String::from("hello");
  |         - move occurs because `a` has type `String`, which does not implement the `Copy` trait
7 |     takes(a);
  |           - value moved here
8 |     println!("{a}");
  |                ^ value borrowed here after move
  |
note: consider changing this parameter type in function `takes` to borrow instead if owning the value isn't necessary
 --> fn_err.rs:1:13
  |
1 | fn takes(s: String) {
  |    -----    ^^^^^^ this parameter takes ownership of the value
  |    |
  |    in this function
help: consider cloning the value if the performance cost is acceptable
  |
7 |     takes(a.clone());
  |            ++++++++

note が、「所有権が要らないなら、借りる型にしてはどうか」と言っています。これが、次回の内容(借用と参照)です。

for ループでも動く

for に Vec を渡すときも、ムーブが起きます。

let v = vec![String::from("a"), String::from("b")];
for s in v {
    println!("{s}");
}
println!("{}", v.len());
error[E0382]: borrow of moved value: `v`
 --> for_err.rs:6:20
  |
2 |     let v = vec![String::from("a"), String::from("b")];
  |         - move occurs because `v` has type `Vec<String>`, which does not implement the `Copy` trait
3 |     for s in v {
  |              - `v` moved due to this implicit call to `.into_iter()`
...
6 |     println!("{}", v.len());
  |                    ^ value borrowed here after move
  |
help: consider iterating over a slice of the `Vec<String>`'s content to avoid moving into the `for` loop
  |
3 |     for s in &v {
  |              +

for s in v は、v の中身を、1つずつ取り出して、v ごと使い切ります。help が、&v と書けば、ムーブしない、と教えています。これも、次回です。

代入と drop

値が捨てられるタイミングを、もう一度、目で見ます。

fn take(n: Noisy) {
    println!("take の中: {}", n.0);
}

fn main() {
    let mut x = Noisy("old");
    println!("代入の前: {}", x.0);
    x = Noisy("new");
    println!("代入の後");

    let y = Noisy("y");
    let z = y;
    println!("ムーブの後: {}", z.0);

    take(z);
    println!("take から戻った後");
    println!("main の終わり: {}", x.0);
}

(Noisy と impl Drop は、前の節と同じです)

代入の前: old
drop: old
代入の後
ムーブの後: y
take の中: y
drop: y
take から戻った後
main の終わり: new
drop: new
出力起きていること
drop: oldx = Noisy("new"); の代入の瞬間に、古い値が捨てられます
drop: ytake(z) の中で、所有権が関数に移り、関数の終わりで捨てられます。main に戻ってからではありません
drop: newmain の終わりで、x が捨てられます

let z = y; のムーブでは、drop は出ていません。持ち主が変わっただけで、値は、捨てられていません。

サンプル: 買い物リスト

所有権を、「渡して、返してもらう」形で、使い切るサンプルです。

cargo new shop
cd shop

src/main.rs を、次のようにします。

fn add_item(mut list: Vec<String>, item: &str) -> Vec<String> {
    list.push(String::from(item));
    list
}

fn print_and_return(list: Vec<String>) -> Vec<String> {
    println!("{:?}({} 品目)", list, list.len());
    list
}

fn consume(list: Vec<String>) {
    println!("片付けます: {} 品目", list.len());
}

fn main() {
    let list = Vec::new();
    let list = add_item(list, "りんご");
    let list = add_item(list, "みかん");
    let list = print_and_return(list);
    let list = add_item(list, "ぶどう");
    consume(list);
}
cargo run
["りんご", "みかん"](2 品目)
片付けます: 3 品目
関数所有権の動き
add_itemリストを受け取り、品目を足して、返す
print_and_returnリストを受け取り、表示して、返す
consumeリストを受け取り、返さない。関数の終わりで、捨てられる

let list = ... を何度も書いているのは、第2回のシャドーイングです。返ってきた所有権を、同じ名前で受けています。list.len() は、中身を借りるだけで、所有権を取りません。Vec::new() の中身の型は、add_item に渡すことで、Vec<String> と決まります。

item: &str は、品目の文字列を借りて、String::from(item) で、自分の String を作っています。

consume(list); のあとで、list を使うと、エラーです。

consume(list);
println!("{}", list.len());
error[E0382]: borrow of moved value: `list`
  --> shop_err.rs:22:20
   |
20 |     let list = add_item(list, "ぶどう");
   |         ---- move occurs because `list` has type `Vec<String>`, which does not implement the `Copy` trait
21 |     consume(list);
   |             ---- value moved here
22 |     println!("{}", list.len());
   |                    ^^^^ value borrowed here after move

このサンプルの書き方は、動きますが、使うたびに、渡して、受け取り直す必要があります。次回の「借用」は、この手間をなくす仕組みです。

cargo clippy と cargo fmt --check は、指摘なしで通りました。

歴史

年できごと
1994Bjarne Stroustrup が、著書『The Design and Evolution of C++』で、RAII(「スコープを出たら、自動で後始末する」C++ の書き方)を紹介する。名付け親も、Stroustrup
2010Rust の所有権の仕組みが、すでに入っていた(Graydon Hoare の話として、MIT Technology Review が伝えている)
2013GC がほとんど使われなくなり、言語から取り除かれる。所有権が中心になる

第1回の年表にあった「2013年に、GC が、言語から取り除かれた」と、同じ出来事です。

RAII は、「持ち主が消えるときに、後始末も一緒に行う」という考え方で、今回の「スコープを出ると、drop が呼ばれる」動きと、同じ形をしています。Rust は、これに、「持ち主は1人だけ」という決まりを足して、コンパイル時に、誤りを見つける形にした、と見ることができます。

また、「持ち主は1人だけで、譲ったら使えない」という決まりから、Rust の型は、アフィン型(各値を、高々1回しか使えない型)と呼ばれます。

動作確認

確認できたこと

  • 上のコードと出力は、すべて、Linux(Rust 1.97.0、Cargo 1.97.0)で、実際に実行した結果です
  • Java の比較(StringBuilder の代入、int の代入)は、同じ環境の Java で、実際に実行した結果です
  • drop のタイミング(スコープの終わり、作った順の逆、代入の瞬間、関数の終わり)
  • ムーブでは、as_ptr() が同じ値になり、clone では、違う値になること
  • 型の大きさ(&str = 16、String = 24、Vec<i32> = 24。64 ビットの環境)
  • Copy になる型(整数・小数・bool・char・&str・Copy 型だけのタプルと配列)と、ムーブになる型(String・Vec<String>・String を含むタプルと配列)
  • 6つのエラー(E0382 を5種類の形、E0596)のメッセージ
  • 買い物リストのサンプルが、cargo run で動くこと。cargo clippy と cargo fmt --check が、指摘なしで通ること

歴史の出典

  • RAII を Stroustrup が名付けたこと、C++ の考え方であること、1994年の著書で紹介されたことは、cardinalpeak.com の記事(RAII Introduction)の記述です。この1つの出典だけで、他の資料とは、突き合わせていません
  • 所有権が2010年には入っていたこと、2013年に GC が取り除かれたこと、Rust の型がアフィン型と呼ばれることは、Wikipedia「Rust (programming language)」の記述です。2010年の件は、同ページが引く MIT Technology Review の記事の内容で、元の記事までは、確認していません
  • Wikipedia のページには、Rust の所有権が、アフィン型の研究から作られたとは、書かれていません。この記事も、そうは書いていません

確認できていないこと

  • String の 24 バイトが、位置・長さ・容量の3つ分であること(大きさの数字は測りましたが、内訳は、一般的な説明です)
  • 「同じ値を2回捨てる危険を防ぐため」という、ムーブの理由。これは、言語の設計の説明で、この記事のコードでは、確かめていません
  • 「RAII と Rust の所有権は、同じ形をしている」という、歴史の節の説明。これは筆者の解釈で、出典では確認していません
  • Windows と macOS での実行結果。出力の内容は、同じになるはずですが、実行はしていません
  • drop の順序の細かい決まり(構造体のフィールドの順など)。この記事では、触れていません

次回

次回は、借用と参照です。今回のエラーで、何度も出てきた「借りる」を、& と &mut の使い方から、見ていきます。買い物リストのサンプルを、渡して返すのをやめた形に、書き直します。

連載の目次(予定)

入門編(8回)

  1. Rust とは / 環境構築 / Hello World
  2. 変数・型・関数(Java との対比)
  3. 所有権(ムーブ)(今回)
  4. 借用と参照
  5. 構造体・enum・match
  6. Result / Option とエラー処理
  7. トレイトとジェネリクス(Java の interface との違い)
  8. ライフタイム入門と、総まとめの CLI(ファイル一覧ツール)