2.6. API设计原则之灵活性(flexible) Pt.2对象安全(Object Safety)、对象安全与API设计、trait的泛型方法与API设计2.6.1. 对象安全(Object Safety)在定义trait时它是否是对象安全的也是契约未写明的一部分。对象安全Object Safety是Rust中与Trait 对象Trait Object相关的一个概念它决定了某个 Trait 是否可以被动态分发dynamic dispatch即能否用作dyn Trait形式的Trait对象。对象安全的 Trait 需要满足以下条件基于 RFC 255所有的 supertrait 也必须是对象安全的如果某个trait继承了其他traitsupertrait详见 【Rust自学】19.2.5. 使用supertrait来要求trait附带其它trait的功能那么这些supertrait也必须是对象安全的。不能要求SizedTrait 不能有Sized(详见 【Rust自学】19.5.4. 动态大小和和Sizedtrait)作为supertrait即不能包含Self: Sized限制因为 Trait 对象的大小在编译期未知。不能有任何关联常量Associated Constants。不能有任何带有泛型的关联类型Associated Types。所有的关联函数methods必须符合以下规则之一-可分发函数Dispatchable functions不能有任何类型参数但生命周期参数是允许的。必须是方法并且Self只能出现在接收器receiver的位置例如selfmut selfBoxSelfRcSelfArcSelfPinP其中P是上述类型之一不能要求Self: Sized否则会限制 Trait 只能用于已知大小的类型破坏对象安全。显式不可分发函数Non-dispatchable functions允许Self作为方法的返回值但这些函数必须要求Self: Sized这样它们在 Trait 对象上就无法被调用只能用于具体类型。如果上面的内容你记不住你就记住对象安全(object safety)描述一个trait是否能被安全的包装成trait对象(trait object)即可对象安全的作用如果某个trait是对象安全的也就是满足上述的所有条件那么我们就可以使用dyn Trait将实现该trait的不同类型视为单一通用类型。如果不是对象安全的编译器会禁止你使用dyn Trait。对象安全与API设计在设计API时建议把trait写成是对象安全的即使会稍微降低使用的便利度因为它提供了新的使用方式和灵活性。看个例子假设我们有一个Animaltrait它有两个方法name和speak。name方法返回str表示动物的名字。speak方法打印动物声音的拟声词没有返回值。我们有两个结构体Dog和Cat都要实现这个trait。trait Animal { fn name(self) - str; fn speak(self); } struct Dog { name: String, } impl Animal for Dog { fn name(self) - str { self.name } fn speak(self) { println!(Woof!); } } struct Cat { name: String, } impl Animal for Cat { fn name(self) - str { self.name } fn speak(self) { println!(Meow!); } } fn main() { let dog Dog { name: String::from(George) }; let cat Cat { name: String::from(Hamilton) }; let animals: Vecdyn Animal vec![dog, cat]; for animal in animals { println!(The name of this animal is {}, animal.name()); animal.speak(); } }Animaltrait是对象安全(object-safe)的因为它没有返回Self类型或是使用泛型参数所以我们可以用它来创建一个trait对象let animals: Vecdyn Animal vec![dog, cat];这个Vector的类型就相当于一个trait输出:The name of this animal is George Woof! The name of this animal is Hamilton Meow!接下来我们小改一下之前的代码例我们给Animaltrait添加一个新的方法clone它返回一个Self类型trait Animal { fn name(self) - str; fn speak(self); fn clone(self) - Self; } struct Dog { name: String, } impl Animal for Dog { fn name(self) - str { self.name } fn speak(self) { println!(Woof!); } fn clone(self) - Self { todo!() } } struct Cat { name: String, } impl Animal for Cat { fn name(self) - str { self.name } fn speak(self) { println!(Meow!); } fn clone(self) - Self { todo!() } } fn main() { let dog Dog { name: String::from(George) }; let cat Cat { name: String::from(Hamilton) }; let animals: Vecdyn Animal vec![dog, cat]; for animal in animals { println!(The name of this animal is {}, animal.name()); animal.speak(); } }输出error[E0038]: the trait Animal is not dyn compatible -- src/main.rs:47:27 | 47 | let animals: Vecdyn Animal vec![dog, cat]; | ^^^^^^ Animal is not dyn compatible | note: for a trait to be dyn compatible it needs to allow building a vtable for more information, visit https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility -- src/main.rs:4:24 | 1 | trait Animal { | ------ this trait is not dyn compatible... ... 4 | fn clone(self) - Self; | ^^^^ ...because method clone references the Self type in its return type help: consider moving clone to another trait help: the following types implement Animal: Dog Cat consider defining an enum where each variant holds one of these types, implementing Animal for this new enum and using it instead添加了clone方法之后Animal就不再是对象安全的了因为clone方法违反了规则——返回类型不能是Self因为dyn Trait需要通过指针调用而Self代表具体实现类型在编译时无法确定具体大小。比如说fn main() { let dog Dog {name: Ver.to_string()}; let dog2 dog.clone(); // 这里没问题因为 Self Dog let animal: Boxdyn Animal Box::new(Dog { name: Ver.to_string() }); let animal2 animal.clone(); // 编译错误,编译时无法确定具体大小 }那如果我想保留Animal的对象安全同时保留clone方法该怎么写呢回到本文第一小节看显式不可分发函数Non-dispatchable functions允许Self作为方法的返回值但这些函数必须要求Self: Sized这样它们在 Trait 对象上就无法被调用只能用于具体类型。根据它的要求我们这么改代码trait Animal { fn name(self) - str; fn speak(self); fn clone(self) - Self where Self: Sized; } // ...其余代码不变输出The name of this animal is George Woof! The name of this animal is Hamilton Meow!这时候就不会报错了。这么写需要注意的是clone方法就只能在具体的类型下调用否则会报错fn main() { let dog Dog { name: String::from(George) }; let cat Cat { name: String::from(Hamilton) }; let animals: Vecdyn Animal vec![dog, cat]; for animal in animals { println!(The name of this animal is {}, animal.name()); animal.speak(); animal.clone(); // 会报错因为aniaml是dyn Animal而不是具体的类型 } }输出error: the clone method cannot be invoked on a trait object -- src/main.rs:54:16 | 6 | Self: Sized; | ----- this has a Sized requirement ... 54 | animal.clone(); // 会报错因为aniaml是dyn Animal而不是具体的类型 | ^^^^^由于在写trait的方法时写到了clone的返回值实现了Sizedtrait而dyn Animal是不清楚具体类型大小的所以不能调用。当然在具体类型上是肯定可以的fn main() { let dog Dog { name: String::from(George) }; let dog_clone dog.clone(); // 能够通过编译 }trait的泛型方法与API设计把泛型参数放到trait上如果trait必须有泛型方法那么考虑把泛型参数放到trait上。看个例子use std::collections::HashSet; use std::hash::Hash; trait ContainerT { fn contains(self, item: T) - bool; } implT ContainerT for VecT where T: PartialEq, { fn contains(self, item: T) - bool { self.iter().any(|x| x item) } } implT ContainerT for HashSetT where T: Eq Hash, { fn contains(self, item: T) - bool { HashSet::contains(self, item) } } fn main() { // 创建VecT和HashSetT的实例 let vec_container: Boxdyn Containeri32 Box::new(vec![1, 2, 3]); let hashset_container: Boxdyn Containeri32 Box::new(vec![4, 5, 6].into_iter().collect::HashSet_()); // 调用contains方法 println!(Vector contains 2: {}, vec_container.contains(2)); println!(HashSet contains 4: {}, hashset_container.contains(4)); }有一个trait叫Container它有一个方法叫做contains这个contains方法的实现肯定会需要泛型参数。但是为了实现对象安全我们不能在方法上添加类型参数。所以我们把泛型参数移到trait上而不是在trait的方法上也就是ContainerT其中T是泛型参数。这样我们就可以为不同的容器类型实现Containertrait每个实现都有自己特定的元素类型例如代码例中我们为VecT和HashSetT实现了Containertrait输出Vector contains 2: true HashSet contains 4: true使用动态分发不仅可以这么写另一个选择是考虑这个泛型参数是否可以使用动态分发来保证trait的对象安全。看个例子假设我们有一个 Foo trait其中包含一个泛型方法 bar它接受一个泛型参数 Ttrait Foo { fn barT(self, x: T); }这个 trait不是对象安全的因为对象安全要求trait的方法不含泛型参数。原因是泛型方法依赖单态化monomorphizationRust 需要在编译时确定T的具体类型并为不同的T生成不同的代码而dyn Foo允许运行时动态分发编译器无法为dyn Foo预先生成所有可能T的代码。但有一个方法可以“曲线救国”——把泛型参数换成动态分发的表述比如说这样trait Foo { fn bar(self, x: dyn Debug); }这样bar方法可以通过动态分发vtable调用来调用x的Debug方法而不需要在编译时知道具体类型从而保持Foo的对象安全性。看例子trait Foo { fn barT(self, x: T); // 泛型方法导致 trait 不是对象安全的 } struct MyStruct; impl Foo for MyStruct { fn barT(self, x: T) { println!(Received a value!); } } fn main() { let obj MyStruct; let obj_ref: dyn Foo obj; // 编译错误the trait Foo is not dyn compatible obj_ref.bar(42); // 这里无法调用因为 T 需要在编译时确定 }这么写肯定不行所以要换成动态分发的写法use std::fmt::Debug; trait Foo { fn bar(self, x: dyn Debug); // 这里用 trait 对象替代泛型使其对象安全 } struct MyStruct; impl Foo for MyStruct { fn bar(self, x: dyn Debug) { println!(Received a value: {:?}, x); } } fn main() { let obj MyStruct; let obj_ref: dyn Foo obj; // 现在可以作为 trait 对象 obj_ref.bar(42); // 输出Received a value: 42 obj_ref.bar(Hello); // 输出Received a value: Hello }实现对象安全的代价为了实现对象安全我们需要做出多大的牺牲呢- 考虑你的trait会被用户怎么使用如果用户会想把它当作trait对象那就尽力实现对象安全