Trait Object: 子类型多态,运行时通过vtable动态分发
子类型多态: 动态分派
在运行期决定, 第一个参数是&self
pub trait Formatter { fn format(&self, input: &mut String) -> bool; } struct MarkdownFormatter; impl Formatter for MarkdownFormatter { fn format(&self, input: &mut String) -> bool { input.push_str("\nformatted with Markdown formatter"); true } } struct RustFormatter; impl Formatter for RustFormatter { fn format(&self, input: &mut String) -> bool { input.push_str("\nformatted with Rust formatter"); true } } struct HtmlFormatter; impl Formatter for HtmlFormatter { fn format(&self, input: &mut String) -> bool { input.push_str("\nformatted with HTML formatter"); true } } pub fn format(input: &mut String, formatters: Vec<&dyn Formatter>) { for formatter in formatters { formatter.format(input); } } fn main() { let mut text = "Hello world!".to_string(); let html: &dyn Formatter = &HtmlFormatter; let rust: &dyn Formatter = &RustFormatter; let formatters = vec![html, rust]; format(&mut text, formatters); println!("text: {}", text); }
要有一种手段,告诉编译器,此处需要并且仅需要任何实现了 Formatter 接口的数据类型。
在 Rust 里,这种类型叫 Trait Object,表现为 &dyn Trait 或者 Box。
- 这里结构体只是声明了一下,并不关注其包含什么字段
实现机理:ptr+vtable
Trait Object的底层逻辑就是胖指针
vtable是一张静态表
vtable会为每个类型的每个trait实现一张表
use std::fmt::{Debug, Display}; use std::mem::transmute; fn main() { let s = String::from("hello world!"); let s1 = String::from("goodbye world!"); // Display / Debug trait object for s let w1: &dyn Display = &s; let w2: &dyn Debug = &s; // Display / Debug trait object for s1 let w3: &dyn Display = &s1; let w4: &dyn Debug = &s1; // 强行把 triat object 转换成两个地址 (usize, usize) // 这是不安全的,所以是 unsafe let (addr1, vtable1): (usize, usize) = unsafe { transmute(w1) }; let (addr2, vtable2): (usize, usize) = unsafe { transmute(w2) }; let (addr3, vtable3): (usize, usize) = unsafe { transmute(w3) }; let (addr4, vtable4): (usize, usize) = unsafe { transmute(w4) }; // s 和 s1 在栈上的地址,以及 main 在 TEXT 段的地址 println!( "s: {:p}, s1: {:p}, main(): {:p}", &s, &s1, main as *const () ); // trait object(s / Display) 的 ptr 地址和 vtable 地址 println!("addr1: 0x{:x}, vtable1: 0x{:x}", addr1, vtable1); // trait object(s / Debug) 的 ptr 地址和 vtable 地址 println!("addr2: 0x{:x}, vtable2: 0x{:x}", addr2, vtable2); // trait object(s1 / Display) 的 ptr 地址和 vtable 地址 println!("addr3: 0x{:x}, vtable3: 0x{:x}", addr3, vtable3); // trait object(s1 / Display) 的 ptr 地址和 vtable 地址 println!("addr4: 0x{:x}, vtable4: 0x{:x}", addr4, vtable4); // 指向同一个数据的 trait object 其 ptr 地址相同 assert_eq!(addr1, addr2); assert_eq!(addr3, addr4); // 指向同一种类型的同一个 trait 的 vtable 地址相同 // 这里都是 String + Display assert_eq!(vtable1, vtable3); // 这里都是 String + Debug assert_eq!(vtable2, vtable4); }
对象安全
那什么样的 trait 不是对象安全的呢?
- 如果 trait 所有的方法,返回值是 Self 或者携带泛型参数,那么这个 trait 就不能产生 trait object。
- 不允许返回 Self,是因为 trait object 在产生时,原来的类型会被抹去,所以 Self 究竟是谁不知道。
- 比如 Clone trait 只有一个方法 clone(),返回 Self,所以它就不能产生 trait object。
- 不允许携带泛型参数,是因为 Rust 里带泛型的类型在编译时会做单态化,而 trait object 是运行时的产物,两者不能兼容。
- 比如 Fromtrait,因为整个 trait 带了泛型,每个方法也自然包含泛型,就不能产生 trait object。如果一个 trait 只有部分方法返回 Self 或者使用了泛型参数,那么这部分方法在 trait object 中不能调用。