🌴

Rustの落とし穴の話

2022/05/07に公開1

なぜRustは面倒なのか 同じ構造体の参照

これが全てをものがっている。所有権と型にまつわるエラーが面倒なのである。それも思ってもみなかったところでエラーが出る。


pub fn function(a:&mut [Vec<u8>;3],b:&Vec<u8>) {

}

pub fn main() {
    let mut a:[Vec<u8>;3] = [vec![],vec![],vec![]];
    let mut b = &a[0];
    function(&mut a,&b);
}

cannot borrow `a` as mutable because it is also borrowed as immutable

mutable borrow occurs here

なぜこうなるのか?

functionに変数aをミュータブル(書き換え可能)で引き渡しているのだが、bがaの一部をイミュータブル(書き換え不可)として所有しているので不整合でエラーになる。しかも'staticでは回避出来ないのである。constで持てるケースならconstを使った方が良いだろう。これは極端な例だが、structで変数を管理しているとこの手のエラーが発生しやすい。

回避法

上のケースではbをclone()すれば回避可能である。基本的にはイミュータブルの方をcloneするのが回避策になる。問題はcloneが実装できないstructの場合である。その場合、rcなどでラップする必要が出てくる。しかし、状況に応じてclone()が出てきたり、出てこなかったりするため、一見分かりにくいソースが発生する。

    let mut a:[Vec<u8>;3] = [vec![],vec![],vec![]];
    let mut b = &a[0].clone();
    function(&mut a,&b);

ケース2 同じ構造体の参照2

このケースは別のエラーが発生する。

pub fn function(b:&mut Vec<u8>,c:&Vec<u8>) {

}

pub fn main() {
    let mut a:[Vec<u8>;3] = [vec![],vec![],vec![]];
    let mut b = &mut a[0];
    let c = &a[0].clone();
    function(&mut b,&c);
}
cannot borrow `a[_]` as immutable because it is also borrowed as mutable

immutable borrow occurs here

回避法

rustで同じ構造体をいじる場合は、原則的にイミュータブルを先に処理する必要がある。例はVec型だけど。

pub fn function(b:&mut Vec<u8>,c:&Vec<u8>) {

}

pub fn main() {
    let mut a:[Vec<u8>;3] = [vec![],vec![],vec![]];
    let c = &a[0].clone();
    let mut b = &mut a[0];
    function(&mut b,&c);
}

ミュータブルがぶつかった場合は頭が痛い。かなり遠回りの実装を余儀なくされる場合がある。

ケース3 借用の型変換

借用すると型変換が面倒である。

    let a = 3;
    let b = &a;
    let c = b as f32;

回避法

これはbが&{interger}型なので問題が起きている。そのためinteger型に変換してやる必要があり、*を付ける必要がある。これはエディタが書き直してくれる。

    let a = 3;
    let b = &a;
    let c = *b as f32;

ケース4 Boxで括らないといけない

enumやsturctを再帰するとエラーになる。

pub enum Test{
    Test(Test),
    None
};

recursive type `Test` has infinite size

recursive type has infinite size

回避策

Boxで括る必要がある。

pub enum Test{
    Test(Box<Test>),
    None
};

Box::new()だらけに……。

ケース5 ライフタイムにまつわる問題

マルチスレッドコーディングをするときに起きやすいのだが、これは非常に頭が痛い。

pub fn function() -> &[u8] {
    &[0,1,2,3]
}
missing lifetime specifier

expected named lifetime parameter

help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime: `&'static `

回避策

この場合、&[u8]ではなくVec<u8>を使うか、引数として&mut [u8]をわたす。これは[u8]のライフタイムが関数内で、関数外まで持ち出せないからである。そのためヒープに領域を取り、ポインタを返す必要がある。そのためVec型を使う訳である。Box型で回避出来る場合もある(ただし、あまり多く無い気がする。)しかしtraitやファンクションで同じ問題がおきた場合、限りなく回避処理が面倒で、この問題はライフタイムの問題が発生しやすいマルチスレッドで生じやすい。

pub fn function() -> Vec<u8> {
    [0,1,2,3].to_vec()
}
pub fn function(array: &mut [u8]) {
    if array.len() >= 4 {
        for i in 0..4.min(array.len()) {
            array[i] = i as u8;
        }
    }
}

それでもRustは便利

このように処理順でエラーが出たり出なかったりする言語は、簡易エディタでコードを書いていた時代なら採用できない言語である。デバッグ以前にコンパイルを通すまでに時間がかかりすぎるのである。しかもその大半が原因不明なのである。

今時のエディタはコンパイル前にエラーが分かるし、ある程度、修正点を指摘してくれるのでそのコスト増を上回るメリットを享受できる訳である。

それも、ここ1年でかなり向上しており採用するには良いタイミングだとは思う。

Discussion

lucidfrontier45lucidfrontier45

ケース1と2は二重可変参照が問題なのでcloneするよりかは関数のAPIか渡すデータの持ち方を直す方が本質的ではないでしょうか?例えば関数の引数は本来はaの方だけで足りるはずです。