項目19:リフレクションを避けよう
https://effective-rust.com/reflection.html
LT;DR
Rust はリフレクションをサポートしていない
std::any::Any トレイトを用いると、トレイトを実装している型や具象型への ダウンキャスト は可能
しかし、その型がどのトレイトを実装しているかを知ることは不可能である
他言語でリフレクションを用いて実現している機能は、トレイトや マクロ を用いることで対応可能
hr.icon
Rust はリフレクションをサポートしていない
言語によっては リフレクション をサポートしているが、Rust はサポートしていない
そのため、「リフレクションを避けよう」というアドバイスに従うのは簡単である
しかし、他の言語でリフレクションを用いて解決していた問題の多くは、Rust で別の機能を用いて解決することが可能
たとえば std::any モジュールには、類似の機能がいくつかある
warning.icon ただし制約が多いため、他の代替手段が無い場合のみ使うこと
std::any モジュール
type_name を用いると、型名を取得することができる
code:rs
fn tname<T: ?Sized>(_v: &T) -> &'static str {
std::any::type_name::<T>()
}
fn main() {
let x = 42u32;
let y = vec!3, 4, 2;
println!("x: {} = {}", tname(&x), x); // x: u32 = 42
println!("y: {} = {:?}", tname(&y), y); // y: alloc::vec::Vec<i32> = 3, 4, 2
}
ただし、type_name は コンパイル 時の情報しかアクセスできない
したがって、トレイトオブジェクト の場合は型を取得できない
code:rs
let square = Square::new(1, 2, 1);
let draw: &dyn Draw = &square;
println!("draw: {}", tname(&draw)); // draw: &dyn playground::Draw
上記の場合、具象型 Square はその型が実装しているトレイトオブジェクト dyn Draw に 自動型変換 される
この変換により、単純なポインタからファットポインタが作られる
https://scrapbox.io/files/677696231ad24a2fee61867e.png
そのため、type_name の結果は診断のみに適しており、「結果をパースしてはいけない」
ドキュメントにも best-effort のヘルパ関数に過ぎず、内容が変更される可能性や一意でない可能性があることが明記されている
The exact contents and format of the string returned are not specified, other than being a best-effort description of the type.
...
The returned string must not be considered to be a unique identifier of a type as multiple types may map to the same type name. Similarly, there is no guarantee that all parts of a type will appear in the returned string: for example, lifetime specifiers are currently not included. In addition, the output may change between versions of the compiler.
もしグローバルに一意な型識別子が必要な場合、TypeId を使うと良い
code:rs
use std::any::TypeId;
fn type_id<T: 'static + ?Sized>(_v: &T) -> TypeId {
TypeId::of::<T>()
}
println!("x has {:?}", type_id(&x)); // x has TypeId(0x...)
println!("y has {:?}", type_id(&y)); // y has TypeId(0x...)
しかし、通常は TypeId を直接使うのではなく、Any トレイトを用いた方が良い
∵ 標準ライブラリが Any のインスタンスに対して他の機能を提供している
(項目19:リフレクションを避けよう#6777e9b375d04f000001aa42)
std::any::Any トレイト
Any は自分で実装することもできるが、ほぼすべての型 T に対する ブランケット実装 が提供されている
code:rs
impl<T: 'static + ?Sized> Any for T {
fn type_id(&self) -> TypeId {
TypeId::of::<T>()
}
}
ただし、ブランケット実装では 生存期間制約(T: 'static)が指定されているため、T がこの制約に違反している場合はこの実装は利用できない
なぜこの制約が必要か?
生存期間 は 型シグネチャ に含まれるが、実行時の型識別(TypeId)には含まれておらず、異なる生存期間で同じ値になる
これにより、意図しない型の混同が発生する可能性 がある
これを防ぐために、Any トレイトでは T に対して生存期間制約を課している
Any の トレイトオブジェクト の vtable には、typeId メソッドの 1 つしかない
項目8:参照型とポインタ型に慣れよう#67775e8275d04f0000104165
code:rs
let x_any: Box<dyn Any> = Box::new(42u64);
let y_any: Box<dyn Any> = Box::new(Square::new(3, 4, 3));
https://scrapbox.io/files/6777f449b934e1cec1b64449.png
dyn Any にはいくつかのジェネリクスメソッドが存在する
is::<T>(): あるトレイトオブジェクトの型が、別の型 T と等しいか
downcast_ref::<T>(): トレイトオブジェクトの型が T と等しいならば、具象型 T への参照を返す
downcast_mut::<T>(): トレイトオブジェクトの型が T と等しいならば、具象型 T への可変参照を返す
以上を踏まえると、プログラマは 明示的に &dyn Any を生成し、その中にコンパイル時の型情報を保存する 必要があり、ダウンキャスト は &dyn Any を生成しないと行うことができない
そのため、Any はリフレクションの完全な代替とはなり得ない ことが分かる
Rust ではコンパイル時と実行時の関連付けられる型が異なる場合がある
そのため、Any を用いればコンパイル時の情報は取れるが、実行時にそのがどのトレイトを実装しているか調べたり、トレイトオブジェクトを生成するために必要な vtable にアクセスできない
コンパイル時と実行時の関連付けられる型が異なるケース
トレイトオブジェクト
項目19:リフレクションを避けよう#6777e3e875d04f000001aa1e
トレイト境界
トレイトオブジェクト同様、トレイト境界 も is-a ではなく also-implements の関係にある
項目12:ジェネリクスとトレイトオブジェクトのトレードオフを理解しよう#6776949d75d04f00002197ae
理由
code:rs
trait Draw: Debug {
fn bounds(&self) -> Bounds;
}
trait Shape: Draw {
fn render_in(&self, bounds: Bounds);
fn render(&self) {
self.render_in(overlap(SCREEN_BOUNDS, self.bounds());
}
}
code:rs
let square = Square::new(1, 2, 2);
let draw: &dyn Draw = &square;
let shape: &dyn Shape = &square;
上記のコードを実行すると、内部状態は以下のようになる
https://scrapbox.io/files/67792d056e1817393701f4d5.png
これを踏まえると、impl Draw for Square への vtable を取り戻す方法が無いため、dyn Shape トレイトオブジェクトに対して dyn Draw トレイトオブジェクトを作る直接的な方法は無いことが分かる
warning.icon Square::bounds メソッドのアドレスは理論的には取り出せるはずなので、今後変わる可能性はある
リフレクションの代替案
他の言語(e.g. Java)でも推奨されるように、リフレクションを避けてトレイト(他の言語では インタフェース)を利用したほうが良い
「リフレクションよりもインタフェースを使おう」
特定の振る舞いが必要な場合、それをトレイトとして定義するのが良い
もし振る舞いが具体的なメソッド群では表現できない場合、マーカトレイト を用いると良い
このほうが、たとえばクラス名を見て特定の プレフィクス
トレイトオブジェクトを用いると、リンク 時には存在しないコードも、実行時に動的にロードして利用可能
リンク時に存在しないコード
dlopen(3) のように、実行時に外部コードを動的にロードする
他の言語ではリフレクションを用いて、同じライブラリの異なるバージョンを同時にロードすることがあるが、Rust では Cargo で実現できるため不要
項目25:依存グラフを管理しよう
マクロ(特に derive マクロ)を使えば、コンパイル時に型情報を利用したコードを生成することが可能
これにより、効率的かつ型安全に実現可能
項目28:分別をもってマクロを使おう
将来の Rust でのアップキャスト
1.76 に登場して、1.81 現在でもリリースされていないが、トレイトアップキャスト を用いると、以下が実現可能になる
U が T の スーパートレイト(trait T: U {...})であれば、トレイトオブジェクト dyn T をトレイトオブジェクト dyn U に変換できる
1.86 でリリースされました 👏
これにより、リスコフの置換原則 でいう is-a の関係に近づくことができる
ただし、この項目で説明した論点に代わりはない
#Rust #Effective_Rust_―_Rustコードを改善し、エコシステムを最大限に活用するための35項目