改进rust代码的35种具体方法-类型(十二)-通用方法,而不是重复造轮子

上一遍文章


在改进rust代码的35种具体方法-类型(二)描述了使用特征来封装类型系统中的行为,作为相关方法的集合,并观察到有两种方法可以使用特征:作为通用的特质边界,或作为特质对象。本项目探讨了这两种可能性之间的权衡。

Rust的泛型大致等同于C++的模板:它们允许程序员编写适用于某些任意类型T的代码,并在编译时生成通用代码的特定用途——这个过程在Rust中被称为单态化,在C++中被称为模板实例化。与C++不同,Rust以通用的特征边界的形式,在类型系统中显式编码对T类型的期望。

相比之下,特征对象是脂肪指针(熟悉引用和指针类型),它将指向底层具体项的指针与指向vtable的指针相结合,而vtable又包含所有特征实现方法的函数指针。

这些基本事实已经允许对两种可能性进行一些直接比较:

  • 泛型可能会导致更大的代码大小,因为编译器会为使用的每种类型T生成代码generic::<T>(T: &T)的新副本;一个traitobj(t: &dyn t)方法只需要一个实例。
  • 从泛型调用特征方法通常比从使用特征对象的代码中调用略快,因为后者需要执行两次取消引用来找到代码的位置(特征对象到vtable,vtable到实现位置)。
  • 泛型的编译时间可能会更长,因为编译器正在构建更多的代码,链接器有更多的工作要做,以折叠重复项。

在大多数情况下,这些都不是显著的差异;只有当您测量了影响并发现它具有真正的影响(速度瓶颈或有问题的占用增加)时,您才应该将与优化相关的问题作为主要决策驱动因素。

更显著的区别是,通用特征边界可用于有条件地提供方法,这取决于类型参数是否实现多个特征。

trait Drawable {
    fn bounds(&self) -> Bounds;
}
    struct Container<T>(T);

    impl<T: Drawable> Container<T> {
        // The `area` method is available for all `Drawable` containers.
        fn area(&self) -> i64 {
            let bounds = self.0.bounds();
            (bounds.bottom_right.x - bounds.top_left.x)
                * (bounds.bottom_right.y - bounds.top_left.y)
        }
    }

    impl<T: Drawable + Debug> Container<T> {
        // The `show` method is only available if `Debug` is also implemented.
        fn show(&self) {
            println!("{:?} has bounds {:?}", self.0, self.0.bounds());
        }
    }
    let square = Container(Square::new(1, 2, 2)); // Square is not Debug
    let circle = Container(Circle::new(3, 4, 1)); // Circle is Debug

    println!("area(square) = {}", square.area());
    println!("area(circle) = {}", circle.area());
    circle.show();
    // The following line would not compile.
    // square.show();

特征对象只对单个特征的实现vtable进行编码,因此做等效的事情要笨拙得多。例如,可以为show()案例定义组合DebugDrawable特征,以及一些转换操作((六)-了解类型转换),以使生活更轻松。然而,如果存在不同特征的多个不同组合,很明显,这种方法的组合迅速变得笨重。

在改进rust代码的35种具体方法-类型(二)描述了使用特征边界来限制通用函数可接受的类型参数。性状边界也可以应用于性状定义本身:

trait Shape: Drawable {
    fn render_in(&self, bounds: Bounds);
    fn render(&self) {
        self.render_in(overlap(SCREEN_BOUNDS, self.bounds()));
    }
}

在本例中,render()方法的默认实现使用特征绑定,依赖于Drawablebounds()方法的可用性。

来自面向对象语言的程序员经常混淆trait边界和继承,错误地认为这样的trait边界意味着Shape是可绘制的。事实并非如此:这两种类型之间的关系最好表示为Shape also-implements Drawable。

在幕后,具有特征边界的特征的特征对象

    let square = Square::new(1, 2, 2);
    let draw: &dyn Drawable = &square;
    let shape: &dyn Shape = &square;

有一个组合vtable 包含顶级特征的方法,以及所有特征边界的方法:

这意味着无法从Shapeto “向上转换” Drawable,因为(纯)Drawablevtable 无法在运行时恢复。相关特征对象之间无法进行转换,这又意味着没有利斯科夫换人

Shape用不同的词重复相同的点,接受特征对象的方法

  • 可以使用来自的方法Drawable(因为Shapealso-implements Drawable,并且因为相关的函数指针存在于Shapevtable中)
  • 无法将特征对象传递给另一个需要Drawable特征对象的方法(因为Shapeis-not Drawable,并且Drawablevtable 不可用)。

相反,接受实现项目的通用方法Shape

  • 可以使用以下方法Drawable
  • 可以将项传递给另一个具有Drawable特征绑定的泛型方法,因为特征绑定在编译时被单态化以使用Drawable具体类型的方法。

对特征对象的另一个限制是要求对象安全性:只有符合以下两条规则的特征才能用作特征对象。

  • 该特征的方法不能是通用的。
  • 该特征的方法不得返回包含以下内容的类型Self

第一个限制很容易理解:泛型方法f实际上是无限的方法集,可能包含f::<i16>f::<i32>f::<i64>f::<u8>, … 另一方面,特征对象的 vtable 很大程度上是函数指针的有限集合,因此不可能将无限夸脱装入有限品脱壶中。

第二个限制有点微妙,但往往是在实践中更经常遇到的限制——强加的特征Copy或者Clone 特征界限立即属于此规则。要了解为什么它被禁止,请考虑手中有一个特征对象的代码;如果该代码调用(比如说)会发生什么let y = x.clone()?调用代码需要y在堆栈上保留足够的空间,但它不知道 的大小,y因为Self它是任意类型。结果,返回提到的类型1 Self导致非对象安全的特征。

第二个限制有一个例外。如果返回某些相关类型的方法带有对其类型的显式限制,Self则不会影响对象安全性Self大小在编译时已知:Self: Sized.此特征绑定意味着该方法无论如何都不能与特征对象一起使用,因为特征对象的大小显式未知(!Sized),因此该方法与对象安全无关。

到目前为止,综合因素的影响,我们建议选择泛型而不是特征对象,但在某些情况下,特征对象是完成这项工作的正确工具。

第一个是实际的考虑:如果生成的代码大小或编译时间是一个问题,那么特征对象会执行得更好(如本项开头所述)。

导致特征对象的一个​​更理论的方面是它们从根本上涉及类型擦除:有关具体类型的信息在转换为特征对象时丢失。这可能是一个缺点,但它也可能很有用,因为它允许异构对象的集合——因为代码 依赖于特征的方法,它可以调用和组合不同(具体)的方法键入的项目。

传统的OO 渲染形状列表的示例就是这样的一个例子:相同的 render()方法可以用于同一循环中的正方形、圆形、椭圆形和星形。

    let shapes: Vec<&dyn Shape> = vec![&square, &circle];
    for shape in shapes {
        shape.render()
    }

特征对象的一个​​更隐蔽的潜在优势是当可用类型在编译时未知时;如果新代码在运行时动态加载(例如通过dlopen(3)),那么在新代码中实现特征的项目只能通过特征对象调用,因为没有源代码可以单态化。


1:目前对返回方法的限制Self包括Box<Self>可以安全存储在堆栈上的类型;这个限制将来可能会放宽

下一篇文章

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值