Rust
0
どのような問題がありますか?

この記事は最終更新日から1年以上が経過しています。

投稿日

更新日

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

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

新規登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
新規登録ログイン
hiyoko3m

コメント

この記事にコメントはありません。
あなたもコメントしてみませんか :)
新規登録
すでにアカウントを持っている方はログイン
記事投稿イベント開催中
Go強化月間~開発する上で知っておくべき知見を共有しよう~
~
エンジニア夏休み企画!~自由研究や読書感想文を発表しよう~
~
0
どのような問題がありますか?
新規登録して、Qiitaをもっと便利に使ってみませんか

この機能を利用するにはログインする必要があります。ログインするとさらに下記の機能が使えます。

  1. ユーザーやタグのフォロー機能であなたにマッチした記事をお届け
  2. ストック機能で便利な情報を後から効率的に読み返せる
新規登録ログイン
ストックするカテゴリー