mem::{take(_), replace(_)} による値を所有したままの列挙値の変換

説明

A { name: String, x: u8 }B { name: String } という(少なくとも) 2つのバリアントを持つ &mut MyEnum があるとします。MyEnum::A をその x が 0 の場合に B に変更したいとします。このとき MyEnum::B に手を入れたくないとします。

name をクローンすることなく、これを実行できます。

#![allow(unused)]
fn main() {
use std::mem;

enum MyEnum {
    A { name: String, x: u8 },
    B { name: String },
}

fn a_to_b(e: &mut MyEnum) {
    if let MyEnum::A { name, x: 0 } = e {
        // これは `name` を取り出し、その代わりに空文字列を置きます。
        // (空文字列はメモリが割り当てが発生しないことに注意)
        // そして、新しい列挙型のバリアントを作成します( `*e` に代入します)。
        *e = MyEnum::B {
            name: mem::take(name),
        }
    }
}
}

より多くのバリアントでも同様です:

#![allow(unused)]
fn main() {
use std::mem;

enum MultiVariateEnum {
    A { name: String },
    B { name: String },
    C,
    D,
}

fn swizzle(e: &mut MultiVariateEnum) {
    use MultiVariateEnum::*;
    *e = match e {
        // 所有権ルールでは `name` を値として取り出すことはできず、        // 値を置き換えない限り、可変参照から値を取り出すことはできない:
        A { name } => B {
            name: mem::take(name),
        },
        B { name } => A {
            name: mem::take(name),
        },
        C => D,
        D => C,
    }
}
}

動機形成

列挙型を扱うとき、ある列挙型の値を別のバリアントに置き換えたいことがあります。通常これは、借用チェッカーを満足させるために、二段階に分けて行われます。第一段階では、既存の値を参照し、その一部を見て次の処理を決めます。第二段階では、(上の例のように) 条件付きで値を変更することができます。

借用チェッカーは列挙型の name を取り出すことを許可しません (そこに 何か がなければならないため)。もちろん、 name.clone() して、そのクローンを MyEnum::B に入れることはできますが、それは 借用チェッカーを満足させるためのClone アンチパターンの実例になってしまいます。いずれにせよ、 可変借用のみで ‘e‘ を変更することで余分なアロケーションを避けることができます。

mem::take は、値をそのデフォルト値に置き換えるとともに、元の値を返します。これにより値を入れ替える形で取り出せます。Stringの場合、デフォルト値は空の String であり、アロケートの必要がありません。結果として、元の name所有した値として 取得できます。そしてこれを別の列挙型にラップすることができます。

NOTE: mem::replace は非常に似ていますが、値を何に置き換えるかを指定できます。例示の mem::takemem::replace(name, String::new()) に相当します。

ただし Option を使用していてその値を None に置き換えたい状況では、 Optiontake() メソッドを使用することがより短くイディオム的な代替手段であること注意してください。

長所

見て!アロケーション無し!インディ・ジョーンズのような気分も味わえるかも。

短所

少しくどい表現になります。何度も間違えると借用チェッカーが憎くなるかもしれません。コンパイラはダブルストアの最適化に失敗するかもしれず、そうすると unsafe な言語で行うのと比べてパフォーマンスが低下する結果となります。

さらに、扱う型は Default trait を実装している必要があります。しかしながら、もし実装していない場合は、代わりに mem::replace を使用することができます。

議論

このパターンが注目されるのはRustだけです。GCのある言語では、デフォルトで値への参照を取ります(そしてGCは参照を追跡します)。Cのような低レベル言語では、単にポインタをエイリアスして後から修正します。

しかしRustでは、これを行うにはもう少し工夫が必要です。所有された値には所有者が1人しかいないので、それを取り出すには、何かを戻す必要があります。インディ・ジョーンズのように、アーティファクトを砂袋に置き換えるのです。

See also

これは 借用チェッカーを満足させるためのClone アンチパターンを特定のケースで取り除くものです。

Last change: 2024-07-09, commit: 317c88e