0
0

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 3 years have passed since last update.

Mutable referenceからは(partial) moveできない

Last updated at Posted at 2021-02-18

Rust bookのChapter 17に、StateをなぜOptionに入れるかの理由が書いてある。なんとなく分かった気分になっていたが、後になって理解が甘かったことに気付いたのでメモ。

所有しているstructからはpartial moveできる

Rust by exampleにあるように、structの持っている変数の一部をmoveすることができる。その後で元のstructを使おうとすると所有権のエラーが出る(Copy traitが実装されている場合を除く)。
ただし、moveされていない変数はそのまま使うことができる。

#[derive(Debug)]
struct Test {
    str1: String,
    str2: String,
}

fn main() {
    let t = Test {
        str1: String::from("str1"),
        str2: String::from("str2"),
    };

    println!("{:?}", t); // Test { str1: "str1", str2: "str2" }

    let s = Test {
        str1: String::from("new_str1"),
        str2: t.str2,
    };

    println!("{:?}", s); // Test { str1: "new_str1", str2: "str2" }
    // println!("{:?}", t); // <-- error!
    println!("{}", t.str1); // str1
}

Moveされた変数に新しく値をbindingしなおすこともできる。

fn main() {
    let mut t = Test { .. };
    // -- snip --
    t.str2 = String::from("new_str2");

    println!("{:?}", s); // Test { str1: "new_str1", str2: "str2" }
    println!("{:?}", t); // Test { str1: "str1", str2: "new_str2" }
    println!("{}", t.str1); // str1
}

Mutable referenceからは(partial) moveできない

一方、mutable referenceからはこのようなことはできない(できたら所有権のルールが壊れるわけで)。

fn main() {
    let mut t = Test {
        str1: String::from("str1"),
        str2: String::from("str2"),
    };
    let t_ref_mut = &mut t;

    println!("{:?}", t_ref_mut); // Test { str1: "str1", str2: "str2" }

    let s = Test {
        str1: String::from("new_str1"),
        str2: t_ref_mut.str2, // <-- error!
    };

    t.str2 = String::from("new_str2");
}

もっとダイレクトに

    let s = *t_ref_mut;

とするのも、もちろん上手くいかない。
(なお、ここでmutable referenceを使う必要はなく、単にreferenceでも同じ話なのだが、次の節との整合性でmutableにしている。)

この例ではStringにはClone traitが実装されているので、一回別の値に退避させておくことはできる。

    let tmp = t_ref_mut.str2.clone();

    let s = Test {
        str1: String::from("new_str1"),
        str2: tmp,
    };

    t.str2 = String::from("new_str2");

    println!("{:?} {:?}", t, s);

ただ、Cloneが実装されていない型の変数を持つようなstructだと、Clone traitが実装できなかったりする。その場合にはこの方法は使えない。

Optionを使って実現する

Optionで囲うことで、Cloneが実装できないstructに対してもpartial moveっぽいものが実現できる。
つまり、変数に対してOption::take()を呼び出すことで、structのmutable referenceから所有権付きで値を取り出すことができる。

#[derive(Debug)]
struct Test {
    str1: Option<String>,
    str2: Option<String>,
}

fn make_s(t_ref_mut: &mut Test) -> Test {
    let tmp = t_ref_mut.str2.take();

    Test {
        str1: Some(String::from("new_str1")),
        str2: tmp,
    }
}

fn main() {
    let mut t = Test {
        str1: Some(String::from("str1")),
        str2: Some(String::from("str2")),
    };

    println!("{:?}", t); // Test { str1: Some("str1"), str2: Some("str2") }

    let s = make_s(&mut t);
    t.str2 = Some(String::from("new_str2"));

    println!("{:?}", s); // Test { str1: Some("new_str1"), str2: Some("str2") }
    println!("{:?}", t); // Test { str1: Some("str1"), str2: Some("new_str2") }
}

関数にする意味はここでは無いが、同様にすればstructのmethodの実装に使うことができる。

Optionを使わずにできないの?

ここで、なぜOptionを使えばこんなことができるのかが疑問になる。Optionでできるなら元のStringのままでもできそうな気もしてくる。

調べてみると、どうやらOptionそのものに本質的な意味があるわけではないようだ。
RustのドキュメントでOption::takeの実装を見ると、mem::takeというものが呼び出されている。これはDefault traitが実装されている型に対して動く。

StringはDefault traitが実装されているので、先程までのコードは次のように書くことができる。

use std::mem;

#[derive(Debug)]
struct Test {
    str1: String,
    str2: String,
}

fn make_s(t_ref_mut: &mut Test) -> Test {
    let tmp = mem::take(&mut t_ref_mut.str2);

    Test {
        str1: String::from("new_str1"),
        str2: tmp,
    }
}

fn main() {
    let mut t = Test {
        str1: String::from("str1"),
        str2: String::from("str2"),
    };

    let s = make_s(&mut t);

    // --snip--
}

当然、OptionにもDefault traitが実装されている(default()はNoneを返す)。
Defaultが実装されている型なら直接mem::takeを呼べるし、Defaultが実装されていない型でもOptionで囲ってやることでOption::takeが使えるということである。

ところで、mem::takeはどう実現されているのだろう?
内部的には(いくつかの関数を経た後で)ptr::swap_nonoverlapping_oneというunsafeな関数が呼び出され、raw pointerを使っていろいろしているらしい。
多分、safeなRustでtake的な動作を(Cloneなどが実装されていない型に対しても)実現するのは難しいんだろうなーという気がする。


追記:知っていないと中々思いつかないrustのイディオムではtakeイディオムと呼ばれています。

0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?