mem::{take(_), replace(_)} で変更された列挙型の所有値を保持する

説明

&mut MyEnum があり、(少なくとも)2つのバリアント、 A { name: String, x: u8 }B { name: String } を持つとします。ここで、 x がゼロの場合は MyEnum::AB に変更し、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 {
        // This takes out our `name` and puts in an empty String instead
        // (note that empty strings don't allocate).
        // Then, construct the new enum variant (which will
        // be assigned to `*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 {
        // Ownership rules do not allow taking `name` by value, but we cannot
        // take the value out of a mutable reference, unless we replace it:
        A { name } => B {
            name: mem::take(name),
        },
        B { name } => A {
            name: mem::take(name),
        },
        C => D,
        D => C,
    }
}
}

動機

列挙型を扱う際、列挙型の値を別のバリアントに変更したい場合があります。 これは通常、借用チェッカーを満足させるために2つのフェーズで行われます。最初のフェーズでは、既存の値を観察し、 その部分を見て次に何をすべきかを決定します。2番目のフェーズでは、 条件付きで値を変更する可能性があります(上記の例のように)。

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

mem::take は、値を交換して、デフォルト値に置き換え、 以前の値を返すことを可能にします。String の場合、デフォルト値は空の String であり、アロケーションを必要としません。結果として、元の name所有値として 取得します。これを別の列挙型でラップできます。

注: mem::replace は非常に似ていますが、値を何に置き換えるかを指定できます。 mem::take 行と同等のものは mem::replace(name, String::new()) です。

ただし、Option を使用していて、その値を None に置き換えたい場合、Optiontake() メソッドは、より短く、よりイディオマティックな 代替手段を提供します。

利点

見てください、アロケーションなしです!また、これを行っている間、インディ・ジョーンズのように感じるかもしれません。

欠点

これは少し冗長になります。繰り返し間違えると、借用チェッカーを嫌いになるでしょう。 コンパイラは二重ストアを最適化できない場合があり、アンセーフな言語で行うことと比較して パフォーマンスが低下する可能性があります。

さらに、取得する型は Default トレイトを実装する必要があります。ただし、作業している型が これを実装していない場合は、代わりに mem::replace を使用できます。

議論

このパターンは Rust においてのみ興味深いものです。GC のある言語では、デフォルトで 値への参照を取得し(GC が参照を追跡します)、C のような他の低レベル言語では、単に ポインタをエイリアスして、後で修正します。

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

関連項目

これは、特定のケースにおいて 借用チェッカーを満足させるためのクローン アンチパターンを取り除きます。