You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

12 KiB

特征对象

在上一节中有一段代码无法通过编译:

fn returns_summarizable(switch: bool) -> impl Summary {
    if switch {
        Post {
           // ...
        }
    } else {
        Weibo {
            // ...
        }
    }
}

其中PostWeibo都实现了Summary特征,因此上面的函数试图通过返回impl Summary来返回这两个类型,但是编译器无情的报错了,原因是 impl Trait的返回值类型并不支持多种不同的类型返回,那如果我们想返回多种类型,该怎么办?

再来考虑一个问题: 现在在做一款游戏需要将多个对象渲染在屏幕上这些对象拥有不同的类型存储在列表中渲染的时候循环该列表顺序渲染每个对象即可在Rust中该怎么实现

聪明的同学可能已经能想到一个办法,利用枚举:

#[derive(Debug)]
enum UiObject {
    Button,
    SelectBox,
}

fn main() {
    let objects = [
        UiObject::Button,
        UiObject::SelectBox
    ];

    for o in objects {
        draw(o)
    }
}

fn draw(o: UiObject) {
    println!("{:?}",o);
}

Bingo这个确实是一个办法但是问题来了如果你的对象集合并不能明确知道呢或者别人想要实现一个UI组件呢是不是还要修改你的代码增加一个枚举成员

总之在编写这个UI库时我们无法知道所有的UI对象类型只知道的是

  • UI对象的类型不同
  • 需要一个统一的类型来处理这些对象,无论是作为函数参数还是作为列表中的一员
  • 需要对每一个对象调用draw方法

在拥有继承的语言中,可以定义一个名为 Component 的类,该类上有一个 draw 方法。其他的类比如 ButtonImageSelectBox 会从 Component 派生并因此继承 draw 方法。它们各自都可以覆盖 draw 方法来定义自己的行为,但是框架会把所有这些类型当作是 Component 的实例,并在其上调用 draw。不过 Rust 并没有继承,我们得另寻出路。

特征对象定义

为了解决上面的所有问题Rust引入了一个概念 - 特征对象。

在介绍特征对象之前先来为之前的UI组件定义一个特征:

pub trait Draw {
    fn draw(&self);
}

只要组件实现了Draw特征,就可以调用draw方法来进行渲染。假设有一个ButtonSelectBox组件实现了Draw特征:

pub struct Button {
    pub width: u32,
    pub height: u32,
    pub label: String,
}

impl Draw for Button {
    fn draw(&self) {
        // 绘制按钮的代码
    }
}

struct SelectBox {
    width: u32,
    height: u32,
    options: Vec<String>,
}

impl Draw for SelectBox {
    fn draw(&self) {
        // 绘制SelectBox的代码
    }
}

此时还需要一个动态数组来存储这些UI对象

pub struct Screen {
    pub components: Vec<?>,
}

注意到上面代码中的?吗?它的意思是:我们应该填入什么类型,可以说就之前学过的内容里,你找不到哪个类型可以填入这里,但是因为ButtonSelectBox都实现了Draw特征,那我们是不是可以把Draw特征的对象作为类型,填入到数组中呢?答案是肯定的。

特征对象指向实现了Draw特征的类型的实例,也就是指向了Button或者SelectBox的实例,这种映射关系是存储在一张表中,可以在运行时通过特征对象找到具体调用的类型方法。

可以通过&引用或者Box<T>智能指针的方式来创建特征对象:

trait Draw { fn draw(&self) -> String; }

impl Draw for u8 { fn draw(&self) -> String { format!("u8: {}", *self) } }
impl Draw for f64 { fn draw(&self) -> String { format!("f64: {}", *self) } }

fn draw1(x: Box<dyn Draw>) {
    x.draw();
}

fn draw2(x: &dyn Draw) {
    x.draw();
}

fn main() {
    let x = 1.1f64;
    // do_something(&x);
    let y = 8u8;

    draw1(Box::new(x));
    draw1(Box::new(y));
    draw2(&x);
    draw2(&y);
}

上面代码,有几个非常重要的点:

  • draw1函数的参数是Box<dyn Draw>形式的特征对象,该特征对象是通过Box::new(x)的方式创建的
  • draw2函数的参数是&dyn Draw形式的特征对象,该特征对象是通过&x的方式创建的
  • dyn关键字只用在特征对象的类型声明上,在创建时无需使用dyn

因此,可以使用特征对象来代表泛型或具体的类型。

继续来完善之前的UI组件代码,首先来实现Screen:

pub struct Screen {
    pub components: Vec<Box<dyn Draw>>,
}

其中存储了一个动态数组,里面元素的类型是Draw特征对象: Box<dyn Draw>, 任何实现了Draw特征的类型,都可以存放其中。

再来为Screen定义run方法用于将列表中的UI组件渲染在屏幕上

impl Screen {
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}

至此,我们就完成了之前的目标:在列表中存储多种不同类型的实例,然后将它们使用同一个方法逐一渲染在屏幕上!

再来看看,如果通过泛型实现,会如何:

pub struct Screen<T: Draw> {
    pub components: Vec<T>,
}

impl<T> Screen<T>
    where T: Draw {
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}

上面的Screen的列表中,存储了类型为T的元素,然后在Screen中使用特征约束让T实现了Draw特征,进而可以调用draw方法。

这限制了Screen 实例必须拥有一个全是 Button 类型或者全是 SelectBox 类型的组件列表。如果只需要同质(相同类型)集合,倾向于使用泛型和 特征约束,因为实现更清晰,且性能更好(特征对象,需要在运行时从vtable动态查找需要调用的方法).

现在来运行渲染下咱们精心设计的UI组件列表

fn main() {
    let screen = Screen {
        components: vec![
            Box::new(SelectBox {
                width: 75,
                height: 10,
                options: vec![
                    String::from("Yes"),
                    String::from("Maybe"),
                    String::from("No")
                ],
            }),
            Box::new(Button {
                width: 50,
                height: 10,
                label: String::from("OK"),
            }),
        ],
    };

    screen.run();
}

上面使用Box::new(T)的方式来创建了两个Box<dyn Draw>特征对象如果在未来还需要增加一个UI组件那么让该组件实现Draw特征,则可以很轻松的将其渲染在屏幕上,甚至用户可以引入我们的库作为三方库,然后在自己的库中为自己的类型实现Draw特征,然后进行渲染。

在动态类型语言中,有一个很重要的概念: 鸭子类型duck typing,简单来说,就是只关心值长啥样,而不关心它实际是什么。当一个东西走起来像鸭子,叫起来像鸭子,那么它就是一只鸭子,就算它实际上是一个奥特曼,也不重要,我们就当它是鸭子。

在上例中,Screenrun时, 并不需要知道各个组件的具体类型是什么。它并不检查组件到底是 Button还是SelectBox 的实例,只要它实现了Draw特征,就能通过Box::new包装成Box<dyn Draw>特征对象,然后被渲染在屏幕上。

使用特征对象和 Rust 类型系统来进行类似鸭子类型操作的优势是无需在运行时检查一个值是否实现了特定方法或者担心在调用时因为值没有实现方法而产生错误。如果值没有实现特征对象所需的特征, 则 Rust根本就不会编译这些代码

fn main() {
    let screen = Screen {
        components: vec![
            Box::new(String::from("Hi")),
        ],
    };

    screen.run();
}

因为String类型没有实现Draw特征,编译器直接就会报错,不会让上述代码运行。如果想要String类型被渲染在屏幕上,那么只需要为其实现Draw特征即可,非常容易。

&和dyn的区别

前文提到,&dyn都可以用于特征对象,因此在功能上&dyn几无区别,唯一的区别就是:&减少了一次指针调用。

因为dyn是一个宽指针(fat pointer), 它内部保存一个指针指向vtable,然后通过vtable查询到具体的函数指针,最后进行调用.

所以,如果你在乎性能,又想使用特征对象简化代码,可以优先考虑&

特征对象的动态分发

回一下泛型章节我们提到过的,泛型是在编译期完成处理的:编译器会为每一个泛型参数对应的具体类型生成一份代码,这种方式是静态分发(static dispatch),因为是在编译期完成的,对于运行期性能完全没有任何影响。

与静态分发相对应的是动态分发(dynamic dispatch),在这种情况下,直到运行时,才能确定需要调用什么方法。

当使用特赠对象时Rust 必须使用动态分发。编译器无法知晓所有可能用于特征对象代码的类型所以它也不知道应该调用哪个类型的哪个方法实现。为此Rust 在运行时使用特征对象中的指针来知晓需要调用哪个方法。动态分发也阻止编译器有选择的内联方法代码,这会相应的禁用一些优化。

Self与self

在Rust中有两个self,一个指代当前的实例对象,一个指代特征或者方法类型的别名:

trait Draw {
    fn draw(&self) ->  Self;
}

#[derive(Clone)]
struct Button;
impl Draw for Button {
    fn draw(&self) -> Self {
        return self.clone()
    }
}

fn main() {
    let button = Button;
    let newb = button.draw();
}

上述代码中,self指代的就是当前的实例对象,也就是button.draw()中的button实例,Self则指代的是Button类型.

当理解了selfSelf的区别后,我们再来看看何为对象安全。

特征对象的限制

不是所有特征都能拥有特征对象,只有对象安全的特征才行。当一个特征的所有方法都有如下属性时,该对象才是安全的:

  • 方法的返回类型不能是Self
  • 方法没有任何泛型参数

对象安全对于特征对象是必须的,因为一旦有了特征对象,就不再知道实现该特征的具体类型是什么了。如果特征方法返回具体的Self类型,但是特征对象忘记了其真正的类型,那这个Self就非常尴尬,因为没人知道它是谁了。同理对于泛型类型参数来说,当使用特征时其会放入具体的类型参数:此具体类型变成了实现该特征的类型的一部分。当使用特征对象时其具体类型被抹去了,故而无从得知放入泛型参数类型到底是什么。

标准库中的 Clone特征就符合对象安全的要求:

pub trait Clone {
    fn clone(&self) -> Self;
}

因为它的其中一个方法,返回了Self类型,因此它是对象不安全的。

String类型实现了 Clone特征, String 实例上调用 clone 方法时会得到一个 String 实例。类似的,当调用 Vec<T> 实例的 clone 方法会得到一个 Vec<T> 实例。clone 的签名需要知道什么类型会代替 Self,因为这是它的返回值。

如果违反了对象安全的规则,编译器会提示你。例如,如果尝试使用之前的Screen结构体来存放实现了 Clone特征的类型:

pub struct Screen {
    pub components: Vec<Box<dyn Clone>>,
}

将会得到如下错误:

error[E0038]: the trait `std::clone::Clone` cannot be made into an object
 --> src/lib.rs:2:5
  |
2 |     pub components: Vec<Box<dyn Clone>>,
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::clone::Clone`
  cannot be made into an object
  |
  = note: the trait cannot require that `Self : Sized`

这意味着不能以这种方式使用此特征作为特征对象。