スタック上の動的ディスパッチ
説明
複数の値に対して動的ディスパッチできますが、そのためには、型の異なるオブジェクトを束縛する複数の変数を宣言する必要があります。必要に応じてライフタイムを延ばすには、以下に示すように遅延条件付き初期化を使用できます。
例
use std::io;
use std::fs;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let arg = "-";
// 動的ディスパッチを得るには、型を記述する必要があります。
let readable: &mut dyn io::Read = if arg == "-" {
&mut io::stdin()
} else {
&mut fs::File::open(arg)?
};
// ここで `readable` から読み取ります。
Ok(())
}
動機
Rust はデフォルトでコードを単相化します。これは、そのコードが使用される型ごとにコードのコピーが生成され、それぞれ独立して最適化されることを意味します。これにより、ホットパスでは非常に高速なコードが可能になりますが、性能が本質的に重要ではない箇所ではコードが肥大化し、コンパイル時間とキャッシュ使用量のコストがかかります。
幸い、Rust では動的ディスパッチを使用できますが、それを明示的に要求する必要があります。
利点
ヒープ上に何も割り当てる必要がありません。また、後で使用しないものを初期化する必要もなく、続くコード全体を File と Stdin の両方で動作するように単相化する必要もありません。
欠点
Rust 1.79.0 より前では、このコードには遅延初期化を伴う 2 つの let 束縛が必要であり、Box ベースのバージョンよりも可動部分が多くなっていました。
// 動的ディスパッチのために型を注釈する必要は依然としてあります。
let readable: Box<dyn io::Read> = if arg == "-" {
Box::new(io::stdin())
} else {
Box::new(fs::File::open(arg)?)
};
// ここで `readable` から読み取ります。
幸い、この欠点は現在なくなりました。やった!
議論
Rust 1.79.0 以降、コンパイラは & または &mut 内の一時値のライフタイムを、関数のスコープ内で可能な限り自動的に延長します。
これは、内容を何らかの let 束縛に配置することを心配せずに、ここで単純に &mut 値を使用できることを意味します(そのような束縛は遅延初期化のために必要だったもので、その変更以前に使用されていた解決策でした)。
それでも各値には場所があり(たとえその場所が一時的なものであっても)、コンパイラは各値のサイズを把握しており、借用された各値は、そこから借用されたすべての参照よりも長く存続します。
関連項目
- デストラクタでのファイナライズ と RAII ガード は、ライフタイムを厳密に制御することで恩恵を受けられます。
- (可変)参照の条件付きで埋められる
Option<&T>については、Option<T>を直接初期化し、その.as_ref()メソッドを使用してオプションの参照を取得できます。