Edited at

Rust の std::any::Any トレイトを用いて安全に動的型付けっぽいことをやる

More than 1 year has passed since last update.


概要

Rust の std::any モジュールには Any という名前のトレイトがあります(ここで boost::any を使ったことのある C++er は何かを察する)。Any トレイトはほとんどあらゆる型に実装されるため、すなわちほとんどあらゆる型の参照を &Any で受けることができます。

Any は “A type to emulate dynamic typing” (動的型付けをエミュレートする型)と公式リファレンスで説明されています。これを用いることで「あらゆる型の参照が入る変数を定義する」「実行時の型によって処理を分岐させる」などといった動的型付けっぽいプログラミングが可能となります。しかもダウンキャスト(Any から指定の型への変換1)の結果は Option 型で返ってくるため、パターンマッチを使うことで分岐処理を型安全に、かつ少ない記述量で書くことができます。

あらかじめ入る型の候補が分かっている場合には値持ちの enum を使う方が筋がよさそうではあるけれど2&Any も一つの手段として覚えておくといざという時に役に立ちそうですね。


解説

(以下のコード例の動作は Wandbox の Rust HEAD 1.15.0-dev (5f128ed10 2016-12-06) で確認しています。実際のコードはこちら

&Any 型のミュータブルな変数 a に色々な型の参照を突っ込む例です。is::<T> メソッドで &Any が指定の型 T の値を参照しているかどうかを判定できます。

use std::any::Any;

#[derive(Debug)] enum E { H, He, Li, Be }
struct S { x: u8, y: u8, z: u16 }

fn main() {
let v1 = 0xc0ffee_u32;
let v2 = E::He;
let v3 = S { x: 0xde, y: 0xad, z: 0xbeef };
let v4 = "hoge";

let mut a: &Any;

a = &v1;
assert!(a.is::<u32>());
a = &v2;
assert!(a.is::<E>());
a = &v3;
assert!(a.is::<S>());
a = &v4;
assert!(a.is::<&str>());
}

downcast_ref::<T> メソッドを使うと指定の型へのダウンキャストができます。結果は Option<&T> 型で返ってくるため、if let を使うと型による処理の分岐をいー感じに書けます。

fn print_any(a: &Any) {

if let Some(v) = a.downcast_ref::<u32>() {
println!("u32 {:x}", v);
} else if let Some(v) = a.downcast_ref::<E>() {
println!("enum E {:?}", v);
} else if let Some(v) = a.downcast_ref::<S>() {
println!("struct S {:x} {:x} {:x}", v.x, v.y, v.z);
} else {
println!("else!");
}
}

fn main() {
print_any(& 0xc0ffee_u32); // => u32 c0ffee
print_any(& E::He); // => enum E He
print_any(& S { x: 0xde, y: 0xad, z: 0xbeef }); // => struct S de ad beef
print_any(& "hoge"); // => else!
}


制限


'static な参照を含む型には使えない

'static な参照を含む型は Any トレイトを実装しないため、その参照を &Any で受けることはできません(コンパイルエラーになります)。

struct T<'a> { x: &'a i32 }

fn main() {
let answer = 42;
let v = T { x: &answer };
let mut a: &Any;
// a = &v; // Compile Error!
// (note: borrowed value must be valid for the static lifetime...)
}

以下のように 'static な参照であればコンパイルが通ります。

static ANSWER: i32 = 42;

fn main() {
let v = T { x: &ANSWER };
let mut a: &Any;
a = &v;
assert!(a.is::<T>());
}


トレイトへのダウンキャストはできない

ダウンキャストの変換先は具象型に限定され、トレイトへの変換はできません。つまり a.downcast_ref::<Display>() とか a.downcast_ref::<Clone>() とか a.downcast_ref::<Eq>() とかはムリ。


その他の機能


downcast_mut メソッド

ミュータブルな参照にダウンキャストする downcast_mut メソッドも用意されています。

let mut answer = 0;

{
let a: &mut Any;
a = &mut answer;
if let Some(x) = a.downcast_mut::<i32>() {
*x = 42;
}
}
assert_eq!(answer, 42);


Box<Any>

&Any の代わりに Box<Any> を使うこともできます。こちらの場合中身の値はヒープ領域上に配置されます。

Box<Any> には downcast<T> メソッドが用意されています。戻り値は Result<Box<T>, Box<Any>> です。

let mut a: Box<Any>;

a = Box::new(42);
if let Ok(answer) = a.downcast::<i32>() {
assert_eq!(*answer, 42);
}


TypeId::of::<T> 関数

std::any::TypeId::of::<T> 関数で各具象型について一意に定まる TypeId を得ることができます。

use std::any::TypeId;

println!("{:?}", TypeId::of::<u8>()); // => TypeId { t: 7100172564015177708 }
println!("{:?}", TypeId::of::<&str>()); // => TypeId { t: 3020856465566936866 }

TypeId は opaque(不透明)なオブジェクトであり、内部の具体的な値に依存したコードを書いてはいけません(デバッグプリントすると中身見えちゃうけど)。Eq, Hash トレイトを実装しているので TypeId 同士を比較したり TypeIdHashMap のキーとして使ったりすることなどは可能です。


get_type_id メソッド (unstable)

Any トレイトの get_type_id メソッドを用いてそのオブジェクトの TypeId を得ることができます。ただし unstable です。そのうち関連関数(静的メソッド)に置き換えられるらしい。

#![feature(get_type_id)]

fn main() {
use std::any::Any;
println!("{:?}", (&0u8 as &Any).get_type_id()); // => TypeId { t: 7100172564015177708 }
println!("{:?}", (&"" as &Any).get_type_id()); // => TypeId { t: 3020856465566936866 }
}


内部動作

'static な型 TAny トレイトを以下のように実装します。TypeId::of::<T> 関数は内部でコンパイラ組み込みの std::intrinsics::type_id::<T> 関数を呼びます。

impl<T: 'static + ?Sized> Any for T {

#[unstable(feature = "get_type_id",
reason = "this method will likely be replaced by an associated static",
issue = "27745")]
fn get_type_id(&self) -> TypeId { TypeId::of::<T>() }
}

downcast_ref の実装は「self.is::<T>()(すなわち self.get_type_id() == TypeId::of::<T>())が真なら &T にキャストして Some に包んで返す」というシンプルな実装です。

impl Any {

...
pub fn downcast_ref<T: Any>(&self) -> Option<&T> {
if self.is::<T>() {
unsafe { Some(&*(self as *const Any as *const T)) }
} else {
None
}
}
...
}


まとめ

Rust でいわゆる実行時型情報 (RTTI, RunTime Type Identification) を扱う方法でした。Rust は色々と厳格な言語であるという印象が強いけど、こういうゆるふわっぽいこともできるよ(しかも型安全に!)、ということで紹介しました。





  1. ダウンキャストは他言語では「基底クラスから派生クラスへの型変換」の意。C++ だと dynamic_cast。Rust の場合クラスの継承は機能として存在しないので、ここでは「抽象型(トレイト)から具象型への変換」を指す。 



  2. boost::any より boost::variant の方が基本的に扱いやすいのと同様。