aimdevel’s blog

勉強したことを書きます

rust初心者がmoveではまった

rustでツールを作成する際にはまった箇所をメモしておく。
以下のドキュメントを参照しながら自分の理解をまとめる。

The Rust Programming Language 日本語版 - The Rust Programming Language 日本語版

何にはまったのか?

結論から言うと、値の所有権を理解していなかったためコンパイルに失敗した。
自分がコンパイルエラーを出したコードは以下のような構造になっていた。

struct Sample{
    name: String,
    num: u32,
}

fn main() {
    let mut samples: Vec<Sample> = Vec::new();
    for i in 1..4{
        samples.push(Sample{
            name: String::from("sample"),
            num: i,
        });
    }

    for sample in samples{
        println!("sample num:{}", sample.num);
    }

    println!("sample[2] name:{}, num:{}", samples[2].name, samples[2].num);

}

コンパイルエラーが以下。

   Compiling move-sample v0.1.0 (/root/git/move-sample)
error[E0382]: borrow of moved value: `samples`
  --> src/main.rs:19:43
   |
7  |     let mut samples: Vec<Sample> = Vec::new();
   |         ----------- move occurs because `samples` has type `Vec<Sample>`, which does not implement the `Copy` trait
...
15 |     for sample in samples{
   |                   ------- `samples` moved due to this implicit call to `.into_iter()`
...
19 |     println!("sample[2] name:{}, num:{}", samples[2].name, samples[2].num);
   |                                           ^^^^^^^ value borrowed here after move
   |
note: `into_iter` takes ownership of the receiver `self`, which moves `samples`
  --> /rustc/eb26296b556cef10fb713a38f3d16b9886080f26/library/core/src/iter/traits/collect.rs:271:18
help: consider iterating over a slice of the `Vec<Sample>`'s content to avoid moving into the `for` loop
   |
15 |     for sample in &samples{
   |                   +

これはエラーメッセージの通り、「move occurs because samples has type Vec<Sample>, which does not implement the Copy trait」であり、解決手段は「for sample in &samples{」なのだが、これがどういうことなのか初心者なりに調べてみた。

値の所有権

rustの値には所有権という概念が存在する。ドキュメントによると以下のルールである。

所有権規則

  • Rustの各値は、所有者と呼ばれる変数と対応している。
  • いかなる時も所有者は一つである。
  • 所有者がスコープから外れたら、値は破棄される。

例えば、以下のように書くと値「1」の所有者は「num」ということになる。

let num:u32 = 1;

値のcopyとmove

以下のように書くと変数xの値が変数yにコピーされ、yとxどちらを参照しても同じ値が入っているというのは、どの言語でもほとんど共通の処理だと思う。

x = 1;
y = x;

上記はrustでも同様の意味になる。これをcopyという。
だが、rustでは同じ意味にならないことがある。それが以下の例だ。

let s1 = String::from("test");
let s2 = s1;

この場合は、s1の値はs2にコピーされない。所有者s1が持っていた値は所有者s2のものとなる。これをmoveという。
値の所有者は一つというルールがあるので、この処理の後にs1は値を所有していない。そのため、これ以降の処理でs1を参照しようとするとコンパイルエラーになる。
また所有者の変更は代入のように記述した場合だけではない。関数に値を渡したときやforループでinto_iter()で値を渡した時にも所有者の変更が行われる。

moveかcopyか

=で値のやり取りをする場合でも数値を扱う場合はcopyで、文字列を扱う場合はmoveと、意味合いが異なっているが、それは扱う値の型に依存している。ドキュメント曰く以下の型はcopyを使用できる。

  • あらゆる整数型。u32など。
  • 論理値型であるbool。trueとfalseという値がある。
  • あらゆる浮動小数点型、f64など。
  • 文字型であるchar。
  • タプル。ただ、Copyの型だけを含む場合。例えば、(i32, i32)はCopyだが、 (i32, String)は違う。

今回の失敗

今回のコンパイルエラーの原因は、copyできない値をforで処理しようとしたことだ。
以下のコードでは、暗黙的にinto_iter()が呼び出され、samplesの各要素の参照ではなく値を返す処理が行われてしまう。

for sample in samples{

そのため以下で参照しようと際に 値を所有していないのでコンパイルエラーになった。

println!("sample[2] name:{}, num:{}", samples[2].name, samples[2].num);

修正

これの修正はコンパイルエラーにも記述されているが、各要素の参照を返すように記述すればよい。

for sample in &samples{

まとめ

コンパイルエラーを通して、rustのmoveとcopyの概念を理解した()。

この記事を書いた後で気が付いたが、rustの所有権に関する記事はQiitaなどにいくつもあるようだ。
それだけ所有権の話はrustを使う上で重要という事なのだろう。