36
32

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

【Rust】構造体からフィールドの値を引っ剥がすメソッド

Last updated at Posted at 2018-03-21

大抵の言語なら、こんなことは何も考えずにできるのですが、Rustでは安直にコードを書くと、borrow checkerさんに怒られます。
頻出パターンなのですが、解決法を探してもなかなか見つからなかったので、自分なりの解法をここに残そうと思います。
※Rustのバージョンは1.24.1 (stable)です。

#構造体から所有権を持っているフィールドを引っ剥がす
フィールドから所有権を引っ剥がした後は、なにか代わりの値を入れなければならないので、swap処理を実装します。
が、とりあえず安直に書くとこんな感じで怒られます。

#[derive(Debug)]
pub struct MyStruct<T> {
    value: T
}

// NG
impl<T> MyStruct<T> {
    pub fn swap(&mut self, newvalue: T) -> T {
        let tmpvalue = self.value; // ←ここでコンパイルエラー
        self.value = newvalue;
        return tmpvalue;
    }
}
terminal
error[E0507]: cannot move out of borrowed content

Rustでプログラムを書いているとよく出会うエラーです。
##エラーの回避方法
selfは借用ですので、勝手に所有権を渡して、元の構造体を破壊することはできません。上記のコードは最終的にはself.valueは新しい値で埋まりますが、処理途中でself.valueの所有権がなくなり、不正な状態となります。
では、参照でどうにかしよう…とすると、また怒られます。

// NG
impl<T> MyStruct<T> {
    pub fn swap(&mut self, newvalue: T) -> T {
        let ref tmpvalue = self.value;
        self.value = newvalue; // ←ここでコンパイルエラー
        return *tmpvalue; // ちなみにこれも借用しているオブジェクトの所有権を渡しているため、ダメ
    }
}
terminal
error[E0506]: cannot assign to `self.value` because it is borrowed

self.valueは貸与中なので、潰しちゃダメと言われます。
そんなこんなでいろいろ試すも、合法な方法はなかなか見つかりません。
では、どのようにすれば可能なのでしょうか。

複製する

引っ剥がしたいフィールドがCopyトレイトやCloneトレイトを実装している場合は、複製することでエラーを回避できます。

// OK
impl<T: Clone> MyStruct<T> {
    pub fn swap(&mut self, newvalue: T) -> T {
        let tmpvalue = self.value.clone(); // 値が複製される。
        self.value = newvalue;
        return tmpvalue;
    }
}

ただし、複製の場合は(実装次第ですが)中身も完全に複製されます。例えばVec<T>は中身の要素まで全て複製されます。あまり効率がよろしくありません。使える型も制約されます。

selfの所有権を放棄させる

メソッドがselfの所有権を奪えば、処理途中でself.valueの所有権がなくなっても、すぐに戻してあげれば問題なく動作します。

// OK
impl<T> MyStruct<T> {
    pub fn swap(mut self, newvalue: T) -> (Self, T) {
        let tmpvalue = self.value; // 構造体の所有権を持っているため、問題ない
        self.value = newvalue; // ただし、フィールドをmoveした場合は、次に使うまでに所有権を埋めなければならない
        return (self, tmpvalue);
    }
}

Cloneのような完全複製は行われないのですが、メソッドの使い方が少し変わってしまうのが残念なところ。

std::mem::swapを使う

一時的に発生する不正な状態をBorrow Checkerさんに見せなければ、エラーを回避できます。

// OK
impl<T> MyStruct<T> {
    pub fn swap(&mut self, newvalue: T) -> T {
        let mut tmpvalue = newvalue;
        std::mem::swap(&mut self.value, &mut tmpvalue); // self.valueとtmpvalueを入れ替える
        return tmpvalue;
    }
}

self.valueの所有権がなくなる瞬間は、std::mem::swap関数の中に隠蔽されているので、外から見れば合法です。では、std::mem::swap関数の中はどうなっているのかと言うと、std::mem::swapのソースを見ると、unsafeコードとなっています。
&mut selfからフィールドを複製せずに所有権を引っ剥がすには、unsafeコードを書かなければならないのです。std::mem::swap関数は、swap処理のunsafeな部分を担ってくれるので、外から見ればunsafeを意識しなくて済みます。

この方法だと、コピーも最小限で済むし、selfの所有権も元のまま保持させられるので、この方法を使うといいでしょう。

cloneswapの違い

本当に最小限のコピーで済んでいるのか検証のため、イケないコードを書きます。

検証コード
fn main() {
    let vsource = vec![1,2,3];
    let mut mystruct = MyStruct{ value: vec![4,5,6]};

    // mystruct.valueとスライスを共有するVecを作成する
    let p = mystruct.value.as_mut_ptr();
    let len = mystruct.value.len();
    let cap = mystruct.value.capacity();
    let v = unsafe {Vec::from_raw_parts(p, len, cap)};

    println!("vsource: {:?}", vsource);
    println!("mystruct: {:?}", mystruct);
    println!("v: {:?}", v);
    // mystruct.valueを引っ剥がす
    println!("let mut vstruct = mystruct.swap(vsource)");
    let mut vstruct = mystruct.swap(vsource);
    println!("vstruct: {:?}", vstruct);
    println!("mystruct: {:?}", mystruct);
    println!("v: {:?}", v);
    // 引っ剥がしたVecの要素を編集
    println!("vstruct[2] = 0");
    vstruct[2] = 0;
    println!("vstruct: {:?}", vstruct);
    println!("v: {:?}", v);
}

vswap前に作成したvec![4,5,6]のスライスを持っているので、このvの変化に着目します。
では、cloneバージョンとswapバージョンでそれぞれ実行してみます。

clone
vsource: [1, 2, 3]
mystruct: MyStruct { value: [4, 5, 6] }
v: [4, 5, 6]
let mut vstruct = mystruct.swap(vsource)
vstruct: [4, 5, 6]
mystruct: MyStruct { value: [1, 2, 3] }
v: [4, 5, 6]
vstruct[2] = 0
vstruct: [4, 5, 0]
v: [4, 5, 6]
swap
vsource: [1, 2, 3]
mystruct: MyStruct { value: [4, 5, 6] }
v: [4, 5, 6]
let mut vstruct = mystruct.swap(vsource)
vstruct: [4, 5, 6]
mystruct: MyStruct { value: [1, 2, 3] }
v: [4, 5, 6]
vstruct[2] = 0
vstruct: [4, 5, 0]
v: [4, 5, 0]

cloneバージョンでは、内部で持っているポインタが指す先も複製しているので、vstructを変更してもvの値は全く変化しません。
対して、swapバージョンは、vstructのスライスへの変更によってvも変更されます。std::mem::swapでは、内部で持っているポインタが指す先は複製されていないことがわかります。

(追記)std::mem::replaceを使う

コメントで記事を紹介していただきました。
Rustの身代わりパターン - 簡潔なQ
その記事ではstd::mem::replaceを使ってますね。
replace関数は目にしたことはあったのですが、戻り値が上書き前の値だということを見逃してました。
つまり、前述のstd::mem::swapを使ったメソッドは、以下のようにしても問題なく動作します。

// OK
impl<T> MyStruct<T> {
    pub fn swap(&mut self, newvalue: T) -> T {
        // let mut tmpvalue = newvalue;
        // std::mem::swap(&mut self.value, &mut tmpvalue);
        // return tmpvalue;
        std::mem::replace(&mut self.value, newvalue)
    }
}

1行で済みますね。どちらを使うかは用途次第ですが、動作的にはほぼ同じです。

#構造体からイミュータブルな参照のフィールドを引っ剥がす

フィールドがイミュータブルな参照となっている場合は、先程の安直な方法で実装できます。

#[derive(Debug)]
pub struct MyStruct<'a, T: 'a> {
    value: &'a T
}

// OK
impl<'a, T> MyStruct<'a, T> {
    pub fn swap(&mut self, newvalue: &'a T) -> &'a T {
        let tmpvalue = self.value;
        self.value = newvalue;
        return tmpvalue;
    }
}

イミュータブルな参照は、複数持つことが許されているので、tmpvalue = self.valueは合法です。
また、std::mem::swapを使う方法も可能です。

// OK
impl<'a, T> MyStruct<'a, T> {
    pub fn swap(&mut self, newvalue: &'a T) -> &'a T {
        let mut tmpvalue = newvalue;
        std::mem::swap(&mut self.value, &mut tmpvalue);
        return tmpvalue;
    }
}

(追記)std::mem::replaceを使う

// OK
impl<'a, T> MyStruct<'a, T> {
    pub fn swap(&mut self, newvalue: &'a T) -> &'a T {
        std::mem::replace(&mut self.value, newvalue)
    }
}

#構造体からミュータブルな参照のフィールドを引っ剥がす

フィールドがミュータブルな参照となっている場合は、先程の安直な方法では実装できません。

pub struct MyStruct<'a, T: 'a> {
    value: &'a mut T
}

// NG
impl<'a, T> MyStruct<'a, T> {
    pub fn swap(&mut self, newvalue: &'a mut T) -> &'a mut T {
        let tmpvalue = self.value; // ←ここでコンパイルエラー
        self.value = newvalue;
        return tmpvalue;
    }
}
terminal
error[E0507]: cannot move out of borrowed content

ミュータブルな参照は、唯一つしか存在できないため、tmpvalue = self.valueは違法です。
これも、std::mem::swapを使う方法で解決可能です。

// OK
impl<'a, T> MyStruct<'a, T> {
    pub fn swap(&mut self, newvalue: &'a mut T) -> &'a mut T {
        let mut tmpvalue = newvalue;
        std::mem::swap(&mut self.value, &mut tmpvalue);
        return tmpvalue;
    }
}

(追記)std::mem::replaceを使う

// OK
impl<'a, T> MyStruct<'a, T> {
    pub fn swap(&mut self, newvalue: &'a mut T) -> &'a mut T {
        std::mem::replace(&mut self.value, newvalue)
    }
}

結論

構造体からフィールドの値を引っ剥がすメソッドは、std::mem::swapを使えば実装可能。

#[derive(Debug)]
pub struct MyStruct<T> {
    value: T
}

// OK
impl<T> MyStruct<T> {
    pub fn swap(&mut self, newvalue: T) -> T {
        let mut tmpvalue = newvalue;
        std::mem::swap(&mut self.value, &mut tmpvalue); // self.valueとtmpvalueを入れ替える
        return tmpvalue;
    }
}

(追記)std::mem::replaceでもよりシンプルに実装可能。

// OK
impl<T> MyStruct<T> {
    pub fn swap(&mut self, newvalue: T) -> T {
        std::mem::replace(&mut self.value, newvalue)
    }
}
36
32
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
36
32

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?