trait 与 trait对象
特征定义与实现的位置(孤儿规则) 如果你想要为类型 A 实现特征 T,那么 A 或者 T 至少有一个是在当前作用域中定义的!
特征对象指向实现了 Draw 特征的类型的实例,可以通过 &引用或者 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) } } // 若 T 实现了 Draw 特征, 则调用该函数时传入的 Box<T> 可以被隐式转换成函数参数签名中的 Box<dyn Draw> fn draw1(x: Box<dyn Draw>) { // 由于实现了 Deref 特征,Box 智能指针会自动解引用为它所包裹的值,然后调用该值对应的类型上定义的 `draw` 方法 x.draw(); } fn draw2(x: &dyn Draw) { x.draw(); } fn main() { let x = 1.1f64; // do_something(&x); let y = 8u8; // x 和 y 的类型 T 都实现了 `Draw` 特征,因为 Box<T> 可以在函数调用时隐式地被转换为特征对象 Box<dyn Draw> // 基于 x 的值创建一个 Box<f64> 类型的智能指针,指针指向的数据被放置在了堆上 draw1(Box::new(x)); // 基于 y 的值创建一个 Box<u8> 类型的智能指针 draw1(Box::new(y)); draw2(&x); draw2(&y); }
上面代码,有几个非常重要的点:
- draw1 函数的参数是 Box
形式的特征对象,该特征对象是通过 Box::new(x) 的方式创建的 - draw2 函数的参数是 &dyn Draw 形式的特征对象,该特征对象是通过 &x 的方式创建的
- dyn 关键字只用在特征对象的类型声明上,在创建时无需使用 dyn
注意 dyn 不能单独作为特征对象的定义
#![allow(unused)] fn main() { fn draw2(x: dyn Draw) { x.draw(); } }
原因是特征对象可以是任意实现了某个特征的类型,编译器在编译期不知道该类型的大小,不同的类型大小是不同的。而 &dyn 和 Box<dyn> 在编译期都是已知大小,所以可以用作特征对象的定义。
特征对象的动态分发
- 特征对象大小不固定:这是因为,实现这个特征的具体类型不固定管,因此特征没有固定大小。
- 几乎总是使用特征对象的引用方式,如
&dyn Draw、Box<dyn Draw>- 虽然特征对象没有固定大小,但它的引用类型的大小是固定的,它由两个指针组成(ptr 和 vptr),因此占用两个指针大小
- 一个指针 ptr 指向实现了特征的具体类型的实例
- 另一个指针 vptr 指向一个虚表 vtable,vtable 中保存了类型实例对于可以调用的实现于特征的方法。当调用方法时,直接从 vtable 中找到方法并调用。之所以要使用一个 vtable 来保存各实例的方法,是因为实现了特征的类型有多种,这些类型拥有的方法各不相同,当将这些类型的实例都当作特征来使用时(此时,它们全都看作是特征类型的实例, 而不再是具体类型的实例),有必要区分这些实例各自有哪些方法可调用
特征对象的限制
不是所有特征都能拥有特征对象,只有对象安全的特征才行。当一个特征的所有方法都有如下属性时,它的对象才是安全的:
- 方法的返回类型不能是 Self
- 方法没有任何泛型参数
对象安全对于特征对象是必须的,因为一旦有了特征对象,就不再需要知道实现该特征的具体类型是什么了。如果特征方法返回了具体的 Self 类型,但是特征对象忘记了其真正的类型,那这个 Self 就非常尴尬,因为没人知道它是谁了。但是对于泛型类型参数来说,当使用特征时其会放入具体的类型参数:此具体类型变成了实现该特征的类型的一部分。而当使用特征对象时其具体类型被抹去了,故而无从得知放入泛型参数类型到底是什么
完全限定语法
#![allow(unused)] fn main() { <Type as Trait>::function(receiver_if_method, next_arg, ...); }
特征定义中的特征约束
#![allow(unused)] fn main() { use std::fmt::Display; trait OutlinePrint: Display { fn outline_print(&self) { let output = self.to_string(); let len = output.len(); println!("{}", "*".repeat(len + 4)); println!("*{}*", " ".repeat(len + 2)); println!("* {} *", output); println!("*{}*", " ".repeat(len + 2)); println!("{}", "*".repeat(len + 4)); } } }
如果你想要实现 OutlinePrint 特征,首先你需要实现 Display 特征。
在外部类型上实现外部特征(newtype)
绕过孤儿规则,使用newtype 模式,简而言之:就是为一个元组结构体创建新类型。该元组结构体封装有一个字段,该字段就是希望实现特征的具体类型。
use std::fmt; struct Wrapper(Vec<String>); impl fmt::Display for Wrapper { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[{}]", self.0.join(", ")) } } fn main() { let w = Wrapper(vec![String::from("hello"), String::from("world")]); println!("w = {}", w); }