Rustでベクターの要素から指定したindexの値を取得しようとしてハマった話

cannot move out of a shared reference

Rust初心者です。上記のエラーでハマりました。

vectorの要素のうち指定したindexの値だけを取り出したい

Goでいうと以下のような処理です。

func get() Hoge {  
    vector := []Hoge{{name: "first"}}  
    return vector[0]  
}

type Hoge struct {  
    name string  
}

まあ普通にできます。Rustでこれと似たようなことをするとき、最初に以下のようなコード書きました。

fn get() -> Result<Hoge, String> {  
    let vector: Vec<Hoge> = vec![Hoge {  
        name: String::from("first"),  
    }];
    
    if let Some(&first) = vector.get(0) {  
        return Ok(first);  
    }
    
    Err("error".to_string())  
}

struct Hoge {  
    name: String,  
}

これはコンパイルエラーになります。

error[E0507]: cannot move out of a shared reference  
  --> src/main.rs:6:27  
  |  
6 |     if let Some(&first) = vector.get(0) {  
  |                  -----    ^^^^^^^^^^^^^  
  |                  |  
  |                  data moved here  
  |                  move occurs because `first` has type `Hoge`, which does not implement the `Copy` trait

調べた結果、原因は vector から0番目の要素の所有権を奪おうとしていることのようです。vector.get(0) は(存在すれば)vectorの0番目の要素の参照を返しますが、let Some(&first) = vector.get(0)で所有権を奪ってfirst を生成しようとしているのでエラーになるようです。

std::mem::replace を使う

vector[0]の値を別の値と入れ替えることはできます。

fn get() -> Result<Hoge, String> {  
    let mut vector: Vec<Hoge> = vec![Hoge {  
        name: String::from("first"),  
    }];
    
    let first = std::mem::replace(  
        &mut vector[0],  
        Hoge {  
            name: String::from(""),  
        },  
    )
    
    return Ok(first);  
}

vector[0] を取り出し、代わりに新しい構造体をセットしています。これでコンパイルが通りました。

個人的には、元の配列の要素をSome でくくり、replace 時にNone で置き換えたほうが良いかなと思いました。理由は以下です。

  • 代わりの要素を生成するのが面倒
  • 要素のstructが複雑な場合は無駄にコストがかかる
  • 2回目以降のアクセスでNone が返ってくるので参照済みかの判定ができる
fn get() -> Result<Hoge, String> {  
    let mut vector = vec![Some(Hoge {  
        name: String::from("first"),  
    })];
    
    if let Some(first) = std::mem::replace(&mut vector[0], None) {  
        return Ok(first);  
    }
    
    Err("error".to_string())  
}

ちなみに、最初のエラーメッセージに書いてありますが、要素の型にCopy トレイトが実装されていれば、vector.get(0)の方法でもできるらしいです。取り出すというより、元の値をコピーするみたいです。

fn get() -> Result<Hoge, String> {  
    let vector: Vec<i64> = vec![1];
    
    if let Some(&first) = vector.get(0) {  
        return Ok(Hoge {  
            name: first.to_string(),  
        });  
    }
    
    Err("error".to_string())  
}

Rustでは他の言語で普通にできることができなくで躓きがちです。難しい…

参考文献