オンスタックの動的ディスパッチ

説明

複数の値を動的にディスパッチすることは可能ですが、そうするためには、異なる型のオブジェクトに束縛するため複数の変数を宣言する必要があります。ライフタイムを必要に応じて延長するために、遅延条件付き初期化を使うことができます:

use std::io;
use std::fs;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let arg = "-";

// これらは `readable` よりも長く生きなければならないため、最初に宣言する:
let (mut stdin_read, mut file_read);

// 動的ディスパッチを得るためには、型を記述する必要がある。
let readable: &mut dyn io::Read = if arg == "-" {
    stdin_read = io::stdin();
    &mut stdin_read
} else {
    file_read = fs::File::open(arg)?;
    &mut file_read
};

// ここで `readable` から読み込む。

Ok(())
}

動機形成

Rustはデフォルトでコードを単相化します。これは、使用する型毎にコードのコピーが生成され、別個に最適化されることを意味します。これはホットパスにおける非常に高速なコードを実現しますが、パフォーマンスが重要でない場所においてコードが肥大化することにもなります。その結果コンパイル時間やキャッシュの使用量が犠牲になります。

幸いなことに、Rustでは動的ディスパッチが使えますが、明示的に要求する必要があります。

長所

ヒープ上に何もアロケートする必要がありません。後で使わないものを初期化する必要もなく、 FileStdin の両方で動作するように、続くコード全体を単相化する必要もありません。

短所

このコードは、 Box ベースのバージョンよりも多くの変動部があります:

// 動的なディスパッチには、型を指定する必要がある。
let readable: Box<dyn io::Read> = if arg == "-" {
    Box::new(io::stdin())
} else {
    Box::new(fs::File::open(arg)?)
};
// ここで `readable` から読み込む。

議論

Rustの初心者は通常、Rustはすべての変数を 使用前に 初期化する必要があることを学びます。そのため、 未使用 変数が初期化されていなくともよいという事実を見落としがちです。Rust は、これが問題なく動作するよう相当な努力を払って保証しており、スコープの最後には初期化済みの値のみがdropされます。

この例は、Rust が私たちに課しているすべての制約を満たしています:

  • すべての変数は、使用する(この場合は借用される)前に初期化されます
  • 各変数は単一の型の値のみを保持します。この例では、 stdin_readStdin 型、file_readFile 型、readable&mut dyn Read 型です
  • 借用された各値は、それから借用されたすべての参照よりも長生きします

See also

  • デストラクタでのファイナライズRAIIガードは、ライフタイムの厳密な制御から恩恵を得ます。
  • 条件により中身が決まる参照 (可変のもの含む) の Option<&T> などに対しては、次のようにします。 Option<T> を直接初期化し、その [.as_ref()] メソッドを使用することでOptionにくるまれた参照を取得できます。
Last change: 2024-07-09, commit: 317c88e