この記事はRust Advent Calendar 2021 カレンダー2の10日目の記事です。
trait objectとobject safety
みなさんtrait objectを使っていますか?
もちろんパフォーマンスのことなどを考えると、できるだけenum dispatchなどを使う方がいいのですが、利便性やapiの都合、パフォーマンスがいらない場合などではまあ便利だとおもいます。
あるtraitをtrait objectとして使う場合、traitがobject safetyという規則に従ったものでないといけません。この中に
- traitのメソッドがジェネリックなパラメータを持ってはいけない
- traitがSelf:Sizedを要求してはいけない
というのがあります。
trait ObjectSafe1{
// object safe なメソッド
fn safe_method_1(&self, value : i32) -> i32;
}
//object safeなメソッドだけなのでobject safeなtrait
trait ObjectSafe2<T>{
// object safe なメソッド
fn safe_method_2(&self, t : T) -> T;
}
//object safeでないメソッドがあるのでobject safeでないtrait
trait NotObjectSafe{
// object safe でないメソッド
fn not_safe_method_3<T>(&self, t : T) -> T;
// 実はここにwhere Self : Sizedとつければobject safeにはなるが、
// dynはSizedではないのでdynした時点でメソッドとして使えなくなる。
}
//sized を要求するのでobject safeでない
trait NotObjectSafe2 : Sized{
fn method_4(self, value : i32) -> i32;
}
struct S;
impl ObjectSafe1 for S {
fn safe_method_1(&self, value : i32) -> i32 { /*省略*/ }
}
impl ObjectSafe2<i32> for S {
fn safe_method_2(&self, t : i32) -> i32 { /*省略*/ }
}
impl NotObjectSafe1 for S {
fn not_safe_method_3<T>(&self, t : T) -> T { /*省略*/ }
}
impl NotObjectSafe2 for S {
fn method_4(self, value : i32) -> i32 { /*省略*/ }
}
fn test(){
let x : Box<dyn ObjectSafe1> = Box::new(S);
x.safe_method_1(1);
let x : Box<dyn ObjectSafe2<i32>> = Box::new(S);
x.safe_method_2(2);
//let x : Box<dyn NotObjectSafe1> = Box::new(S); compile error
// let x : Box<dyn NotObjectSafe2> = Box::new(S);
}
以上のようにNotObjectSafe
というトレイトはtrait objectにしようとするとcompile errorとなります。
このような例としてserdeのSerialize
、Serializer
があります。
// serde::Serialize
pub trait Serialize {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer;
}
// serde::Serializer
pub trait Serializer: Sized {
/*省略*/
}
とうぜんこれもobject safeでないためtrait object にはできません。
erased_serde
そんなserde::Serialize
に対してトレイトオブジェクト化できるようにしたのがerased_serdeです。
ではどうやってobject safetyを回避したのでしょうか。
その方法は単純で、object safeなtraitを別に用意し、そのtrait object(の参照)に対して元のtraitをimplしてやることによって達成してます。
まずobject safetyなmethodを定義します。
// erased_serde::Serialize
pub trait Serialize {
fn erased_serialize(&self, v: &mut dyn Serializer) -> Result<Ok, Error>;
}
// erased_serde::Serializer
pub trait Serializer { /*省略*/ }
そしてそれらのtrait objectにたいして元のtraitをimplします。
//実際にはともに +Send, +Sync, +Send+Syncに対しても定義している
impl<'a> serde::Serializer for &'a mut dyn erased_serde::Serializer { /*省略*/ }
impl<'erased> serde::Serialize for dyn Serialize + 'erased { /*省略*/ }
//serde::Serialzieをerased_serde::Serialzieでつかえるようにwrapする関数
impl dyn Serializer {
pub fn erase<S : serde::Serialzie>(serializer: S) -> Serializer<S>
}
これによってtraitを少しwrapしてやることによいってtrait objectとして使えるようにしています。
よって似たようにobject safeでないtraitでも少しの工夫でtrait objectにできます。
参考文献
object safetyの定義
https://github.com/rust-lang/rfcs/blob/master/text/0255-object-safety.md
asyncなiteratorをboxed dynで使う方法についての英語記事、
これのmacroがあったらいいなという提案をしている。
https://smallcultfollowing.com/babysteps//blog/2021/10/15/dyn-async-traits-part-6/