落とし穴: すぐに dyn Trait に飛びついてしまうこと

// Copyright 2025 Google LLC
// SPDX-License-Identifier: Apache-2.0

use std::any::Any;

pub trait AddDyn: Any {
    fn add_dyn(&self, rhs: &dyn AddDyn) -> Box<dyn AddDyn>;
}

impl AddDyn for i32 {
    fn add_dyn(&self, rhs: &dyn AddDyn) -> Box<dyn AddDyn> {
        if let Some(downcast) = (rhs as &dyn Any).downcast_ref::<Self>() {
            Box::new(self + downcast)
        } else {
            Box::new(*self)
        }
    }
}

fn main() {
    let i: &dyn AddDyn = &42;
    let j: &dyn AddDyn = &64;
    let k: Box<dyn AddDyn> = i.add_dyn(j);
    dbg!((k.as_ref() as &dyn Any).is::<i32>());
    dbg!((k.as_ref() as &dyn Any).downcast_ref::<i32>());
}
  • OOP のバックグラウンドから来ると、この動的ディスパッチという 道具にできるだけ早く頼ろうとするのは理解できます。

  • これは推奨されるやり方ではありません。トレイトオブジェクトを使うと、 開発者とコンパイラの双方が持っている型に関する知識を、柔軟性と 引き換えに手放す状況になります。

  • 上の例はこれを極端なところまで押し進めています。数値の加算が 動的ディスパッチのプロセスに縛られていたら、ほとんど何もできなく なってしまうでしょう。

    しかし、動的ディスパッチは多くのプログラミング言語ではしばしば 隠されています。ここではそれがより明示的になっています。

    AddDyni32 実装では、まず rhs 引数を i32 と同じ型へ ダウンキャストできるか試す必要があり、そうでない場合は黙って 失敗します。

    次に、新しい値をヒープに確保する必要があります。これを動的 ディスパッチの世界にとどめておくなら、それが必要になるからです。

    2 つの値を足し合わせたあと、それを見たい場合は、ここまでの操作で 課されたトレイト境界のため、そのままでは出力できないので、再び 出力可能な “実際の” 型へダウンキャストしなければなりません。

  • クラスに質問してみましょう: なぜ main に Display 境界を追加する だけでは、そのまま表示できないのでしょうか?

    答え: add_dyn が返すのは dyn AddDyn だけなので、引数の型から 戻り値の型に至るまでの間に、その型が何を実装しているかという情報を 失ってしまいます。入力が Display を実装していても、戻り値の型は そうではありません。

  • この結果、パフォーマンスが低く、理解もしにくいコードになります