変更後の enum 内に所有値を保持するための mem::{take(_), replace(_)}
説明
&mut MyEnum があり、これに(少なくとも)2 つのバリアント、
A { name: String, x: u8 } と B { name: String } があるとします。ここで、
MyEnum::B はそのままにしつつ、x がゼロの場合に MyEnum::A を B に変更したいとします。
これは name を clone せずに実現できます。
例
#![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` を取り出し、代わりに空の String を入れます
// (空文字列はアロケーションしないことに注意してください)。
// その後、新しい enum バリアントを構築します(これは
// `*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,
}
}
}
動機
enum を扱うとき、enum の値をその場で、場合によっては別のバリアントへ変更したいことがあります。これは通常、借用チェッカーを満足させるために 2 つのフェーズで行われます。最初のフェーズでは、既存の値を観察し、その部分を見て次に何をするかを決定します。2 番目のフェーズでは、条件に応じて値を変更できます(上の例のように)。
借用チェッカーは、enum から name を取り出すことを許可しません(なぜなら、何か がそこになければならないからです)。もちろん、name を .clone() して、その clone を MyEnum::B に入れることもできますが、それは
借用チェッカーを満足させるための Clone
アンチパターンの一例になります。いずれにせよ、可変借用だけで e を変更することで、余分なアロケーションを避けることができます。
mem::take を使うと、値をそのデフォルト値で置き換え、以前の値を返すことで、値を入れ替えることができます。String の場合、デフォルト値は空の String であり、これはアロケーションを必要としません。その結果、元の name を所有値として取得できます。その後、これを別の enum でラップできます。
注: mem::replace は非常によく似ていますが、値を何で置き換えるかを指定できます。上記の mem::take の行と等価なものは、mem::replace(name, String::new()) になります。
ただし、Option を使用していて、その値を None に置き換えたい場合は、Option の take() メソッドがより短く、より慣用的な代替手段を提供します。
利点
見てください、アロケーションなしです!また、これを行っている間、インディ・ジョーンズのような気分になれるかもしれません。
欠点
これは少し冗長になります。何度も間違えると、借用チェッカーが嫌いになるでしょう。コンパイラーが二重のストアを最適化で取り除けない可能性があり、その結果、unsafe な言語で行う場合と比べてパフォーマンスが低下することがあります。
さらに、取り出そうとしている型は
Default トレイトを実装している必要があります。ただし、扱っている型がこれを実装していない場合は、代わりに mem::replace を使用できます。
議論
このパターンが関心の対象となるのは Rust だけです。GC 付き言語では、デフォルトで値への参照を取得するでしょう(そして GC が参照を追跡します)。また、C のような他の低レベル言語では、単にポインターに別名を付けて、後で物事を修正するでしょう。
しかし Rust では、これを行うためにもう少し作業が必要です。所有値は 1 つの所有者しか持てないため、それを取り出すには、何かを代わりに戻す必要があります。ちょうどインディ・ジョーンズが、秘宝を砂袋と置き換えるようにです。
関連項目
これは、特定の場合に 借用チェッカーを満足させるための Clone アンチパターンを取り除きます。