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イディオムと呼ばれています。