< 返回博客

Jason5Lee 发表于 2019-04-18 22:04

Tags:trait,polymorphism,object

Rust基于特质(trait)的多态机制是我认为很巧妙的设计。Rust用同一套机制解决编译期和运行期的多态,比C++那种模板和面向对象几乎分离的设计要简洁。然而,Rust的运行时多态的灵活性略低,有些特质不能创建特质对象(trait object),也不能自定义哪些方法在特质对象中是动态分派的。这个缺陷在我编写Rust递归函数库 RecurFn 中比较明显。针对这个缺陷,我研究出了一个work around,我称它为自定义特质对象(customized trait object).

举个例子,假设我们有这样的特质定义

trait Foo {
    fn foo(&self, f: impl Fn() -> ());
    fn foo_twice(&self, f: impl Fn() -> () + Copy) {
        self.foo(f);
        self.foo(f)
    }
}

其中,foo方法是多态的行为,而foo_twice本应是函数,放在特质内是为了方便以方法的形式调用,实现不应当重写。

特质Foo不能直接作为特质对象,因为它的两个方法都接收了一个impl Fn() -> ()参数。但我们可以做一些转化使得它可以成为特质对象:对于foo中的impl Fn() -> (),可以通过动态的&Fn() -> ()代替,而且对于foo_twice方法我们可以让它静态分派。

这样,我们可以得到Foo特质的一个“动态化”版本。

trait DynFoo {
    fn foo(&self, f: &Fn() -> ());
    // 由于我们希望foo_twice是静态分派的,这里不出现。
}

这个特质的特质对象dyn DynFoo就是自定义特质对象。现在我们希望dyn DynFoo能当作一个Foo的特质对象使用。也就是说,我们需要实现:

  1. 对于一个Foo的实现的指针(包括裸指针、借用、Box等),它能转化成对应的dyn DynFoo的指针。
  2. dyn DynFoo实现了Foo.

对于1,我们可以通过给所有Foo的实现实现DynFoo做到。

impl<F: Foo> DynFoo for F {
    fn foo(&self, f: &Fn() -> ()) {
        self.foo(f)
    }
}

对于2,我们可以通过给dyn DynFoo实现Foo做到。

impl Foo for DynFoo {
    fn foo(&self, f: impl Fn() -> ()) {
        self.foo(&f)
    }
}

这样,如下代码就能通过编译。

let foo: &DynFoo = &FooImpl::new(); // 此处FooImpl::new()返回一个实现Foo特质的值。
foo.foo(&|| {});
foo.foo_twice(&|| {});

问题似乎已经解决了。然而,Rust有一个迷之设定:在impl Foo for DynFoo中,DynFoo的默认生命周期为'static,且DynFoo不能带SendSync。也就是说,如下代码

fn test_complex<'a>() {
    let a: Box<DynFoo + Send + 'a> = Box::new(FooImpl::new());
    a.foo_twice(&|| {});
}

是编译不过的。

为了解决这个问题,我们需要将上面的实现代码复制四份,并分别将实现的开头修改成impl<'a> Foo for DynFoo + 'a, impl<'a> Foo for DynFoo + 'a + Send, impl<'a> Foo for DynFoo + 'a + Sync, impl<'a> Foo for DynFoo + 'a + Send + Sync。四个实现的其余代码完全一样。

为了消除这个代码重复,目前我能想到的方法是用宏。

macro_rules! impl_dyn_with_markers {
    ($($marker:ident),*) => {
        impl<'a> Foo for DynFoo + 'a$( + $marker)* {
            fn foo(&self, f: impl Fn() -> ()) {
                self.foo(&f)
            }
        }
    };
}
impl_dyn_with_markers! {}
impl_dyn_with_markers! {Send}
impl_dyn_with_markers! {Sync}
impl_dyn_with_markers! {Send, Sync}

这样就能一定程度上减少重复代码。但是用宏有个缺点:使用IDEA的Rust插件重命名方法时,宏内的代码不会被重命名。

这就是我针对Rust特质对象灵活度不足提出的一种解决方案。如果你有更好的方法,欢迎在评论中提出。

本文的示例代码放在这里,这个方法的实际应用案例可参考 RecurFn.

评论区

写评论
laizy 2019-05-20 17:58

写得相当不错,关于最后一点:“DynFoo的默认生命周期为'static,且DynFoo不能带Send或Sync”,这段能否深入分析下rust为啥这么干?或者有啥链接没?

Mike Tang 2019-04-19 01:04

先mark,明天去北京的火车上读。:)

1 共 2 条评论, 1 页