2.4. API设计原则之不意外性(unsurprising) Pt.4:“人体工程学”的trait实现、包装类型(Wrapper Types)、Borrow trait
2.4.1. “人体工程学”的trait实现
Rust不会自动为实现某一trait的类型的引用提供对应的实现。
比如说Bar实现了Trait,但不能将&Bar传递给fn foo<T: Trait>(t: T)。因为为Bar实现Trait并不会自动为&Bar实现Trait。
看代码例:
trait Trait { fn name(&self) -> &'static str; } struct Bar; impl Trait for Bar { fn name(&self) -> &'static str { "Bar" } } fn foo<T: Trait>(t: T) { println!("{}", t.name()); } fn main() { let bar = Bar; foo(bar); // OK let bar_ref = &Bar; foo(bar_ref); // 报错[E0277]:the trait bound `&Bar: Trait` is not satisfied }而用户看到某个trait的方法只接收&self(而不接收self或&mut self)时,仍然可能惊讶于&Bar不能满足T: Trait,不符合不意外(unsurprising)原则。
为了解决这一问题,我们需要在定义新的trait时,(通常)在方法签名允许的情况下(典型是&self/&mut self方法)为下列提供相应的全局实现:
-&T where T: Trait + ?Sized
-&mut T where T: Trait + ?Sized
-Box<T> where T: Trait + ?Sized
接着上面的代码例,为了不让foo(bar_ref);报错,我们需要手动提供&T的Trait实现:
impl<T: Trait + ?Sized> Trait for &T { fn name(&self) -> &'static str { (**self).name() } }注意:如果 trait 方法按值接收self(消耗所有权),一般无法写一个把调用转发给T的impl Trait for &T,因为共享引用不能把T移出去。
对于迭代器来说,如果某个类型可以迭代,那么它的引用也应该添加相应的trait实现。也就是说:对于任何可迭代的类型,考虑为&MyType和&mut MyType实现IntoIterator。这样在循环中我们就可以直接使用借用的实例,符合用户预期。
看个代码例:
struct MyCollection { items: Vec<i32>, } // 为 MyCollection 实现 IntoIterator impl IntoIterator for MyCollection { type Item = i32; type IntoIter = std::vec::IntoIter<Self::Item>; fn into_iter(self) -> Self::IntoIter { self.items.into_iter() } } // 为 &MyCollection 实现 IntoIterator impl<'a> IntoIterator for &'a MyCollection { type Item = &'a i32; type IntoIter = std::slice::Iter<'a, i32>; fn into_iter(self) -> Self::IntoIter { self.items.iter() } } // 为 &mut MyCollection 实现 IntoIterator impl<'a> IntoIterator for &'a mut MyCollection { type Item = &'a mut i32; type IntoIter = std::slice::IterMut<'a, i32>; fn into_iter(self) -> Self::IntoIter { self.items.iter_mut() } } fn main() { let mut collection = MyCollection { items: vec![1, 2, 3] }; // 使用所有权迭代 for item in collection { println!("Owned: {}", item); } let collection = MyCollection { items: vec![4, 5, 6] }; // 使用不可变借用迭代 for item in &collection { println!("Borrowed: {}", item); } let mut collection = MyCollection { items: vec![7, 8, 9] }; // 使用可变借用迭代 for item in &mut collection { *item *= 2; } // 确保修改生效 for item in &collection { println!("Modified: {}", item); } }2.4.2. 包装类型(Wrapper Types)
Rust没有面向对象传统意义上的继承,但是Deref和AsRef提供了类似继承的东西。
比如说你有一个类型为T的值,并满足Deref<Target = U>,那就可以在T类型值上直接调用类型U的方法。
看代码例:
use std::ops::Deref; // 定义一个包装类型 Wrapper,它内部存储一个 String struct Wrapper(String); // 实现 Deref,使得 Wrapper 的 Deref 目标是 String impl Deref for Wrapper { type Target = String; fn deref(&self) -> &Self::Target { &self.0 } } fn main() { let my_wrapper = Wrapper(String::from("Hello, Rust!")); // 由于 Wrapper 实现了 Deref<Target = String>, // 这里可以直接调用 String 的方法,而不需要手动解引用 let len = my_wrapper.len(); let uppercased = my_wrapper.to_uppercase(); println!("Length: {}", len); println!("Uppercased: {}", uppercased); }输出:
Length: 12 Uppercased: HELLO, RUST!如果你提供了相对透明的类型(例如Arc<T>),那么实现Deref允许你的包装类型在使用点运算符时自动解引用为内部类型,从而可以直接调用内部类型的方法。
如果访问内部类型不需要任何复杂或潜在的低效逻辑,应考虑实现AsRef,这样用户就可以轻松将&WrapperType作为&InnerType使用。
对于大多是包装类型,还应在可能的情况下为包装类型实现From<InnerType>,并为内部类型实现From<Wrapper>(这样会免费得到Into),以便用户可以轻松地添加或移除包装。
看代码例:
use std::ops::Deref; use std::sync::Arc; // 定义一个包装类型 struct Wrapper(Arc<String>); // 实现 Deref 以允许透明地访问内部 String impl Deref for Wrapper { type Target = String; fn deref(&self) -> &Self::Target { &self.0 } } // 实现 AsRef<String>,允许用户获取 `&String` impl AsRef<String> for Wrapper { fn as_ref(&self) -> &String { &self.0 } } // 实现 From<String> 以便用户轻松创建 Wrapper impl From<String> for Wrapper { fn from(s: String) -> Self { Wrapper(Arc::new(s)) } } // 实现 From<Wrapper> for String,允许用户将 Wrapper 转回 String(克隆字符串) impl From<Wrapper> for String { fn from(w: Wrapper) -> Self { (*w).clone() // Deref 使 Wrapper 可以当作 String 使用 } } fn main() { let wrapped = Wrapper::from("Hello, Rust!".to_string()); // 由于实现了 Deref,我们可以直接调用 String 的方法 println!("Length: {}", wrapped.len()); println!("Uppercased: {}", wrapped.to_uppercase()); // 使用 AsRef,可以获取 &String 引用 let str_ref: &String = wrapped.as_ref(); println!("AsRef: {}", str_ref); // 通过 Into 获取 String(克隆) let original: String = wrapped.into(); println!("Converted back: {}", original); }输出:
Length: 12 Uppercased: HELLO, RUST! AsRef: Hello, Rust! Converted back: Hello, Rust!2.4.3.Borrowtrait
Borrowtrait与Deref和AsRef有些类似,不过它针对的是更为狭窄的使用情况,并且进行了定制。
Borrowtrait允许调用者提供统一类型的多个本质上相同的变体中的任意一个,这些变体叫做Equivalent。
注意:Borrowtrait仅适用于当你的类型本质上与另一个类型等价时(Equivalent)。也就是说Borrow适用于“等价”的情况,而AsRef和Deref适用于“充当”的情况。
比如说对于一个HashSet<String>,Borrow允许调用者提供&str或&String。
看一个代码例:
use std::collections::HashSet; fn main() { let mut set: HashSet<String> = HashSet::new(); set.insert("hello".to_string()); set.insert("world".to_string()); // 直接使用 &str 进行查找,而不需要创建 String。 // 这能工作是因为标准库已经提供了 `impl Borrow<str> for String`。 let exists = set.contains("hello"); let not_exists = set.contains("rust"); println!("Contains 'hello': {}", exists); println!("Contains 'rust': {}", not_exists); }输出:
Contains 'hello': true Contains 'rust': false与AsRef的对比
当然使用AsRef也是可以实现上面的代码效果的:
use std::collections::HashSet; // 泛型函数,接受任何 `AsRef<str>` 的类型,如 `&str` 和 `&String` fn contains<S: AsRef<str>>(set: &HashSet<String>, value: S) -> bool { set.contains(value.as_ref()) // `AsRef<str>` 使 `value` 转换为 `&str` } fn main() { let mut set: HashSet<String> = HashSet::new(); set.insert("hello".to_string()); set.insert("world".to_string()); // 直接使用 &str 进行查找 let exists = contains(&set, "hello"); // 也可以使用 &String 进行查找 let string_value = "world".to_string(); let exists_string = contains(&set, &string_value); println!("Contains 'hello': {}", exists); println!("Contains 'world': {}", exists_string); }使用AsRef能达到同样的效果,但是如果没有Borrow的额外要求,这种实现对哈希表查找是不安全的,因为Borrow要求借用形式的Hash、Eq和Ord必须与拥有类型保持一致。
潜在的问题是:AsRef<U>甚至不会在文档层面要求源类型与U之间的Hash、Eq和Ord保持一致。
比如说:
use std::collections::HashSet; #[derive(Hash, Eq, PartialEq)] struct CustomType { value: String, } // 实现 AsRef<str>,但这本身并不能让 HashSet 用 &str 查找合法 impl AsRef<str> for CustomType { fn as_ref(&self) -> &str { &self.value } } fn main() { let mut set: HashSet<CustomType> = HashSet::new(); set.insert(CustomType { value: "hello".to_string() }); // 这里不会通过编译(error[E0308]):`contains` 依赖 `Borrow` 而不是 `AsRef`, // 因此没有 `CustomType: Borrow<str>` 时 `&str` 无法匹配 let exists = set.contains("hello"); println!("Exists: {}", exists); }contains的类型是按Borrow写的,所以AsRef<str>并不参与- 即便你写了借助
AsRef再查找的辅助函数,类型系统也不会强制CustomType的Hash/Eq与str一致
相比之下,Borrow<U>才是HashMap/HashSet查找使用的 trait,其文档要求类型与借用形式之间的Hash、Eq和Ord保持一致(编译器不会证明这一点;实现者必须遵守):
use std::collections::HashSet; use std::borrow::Borrow; #[derive(Hash, Eq, PartialEq)] struct CustomType { value: String, } // `Borrow<str>` 才让 `contains("hello")` 成为可能,并且你必须让 Hash/Eq 与 `str` 对齐 impl Borrow<str> for CustomType { fn borrow(&self) -> &str { &self.value } } fn main() { let mut set: HashSet<CustomType> = HashSet::new(); set.insert(CustomType { value: "hello".to_string() }); let exists = set.contains("hello"); // 在 Hash/Eq 与 str 一致时,查找是安全的 println!("Exists: {}", exists); }其余特性
Borrowtrait还为Borrow<T>、&T和&mut T提供了通用实现。这使得在trait约束中使用它来给接收给定类型的拥有值或引用值非常方便。
Rust 标准库中为所有 T 提供了以下Borrow<T>的通用实现,这些实现意味着:
- 类型T本身可以Borrow<T>,即T可以直接作为Borrow<T>的参数
-&T也可以Borrow<T>,这允许我们用一个不可变引用来满足Borrow<T>的约束
-&mut T也可以Borrow<T>,这允许我们用一个可变引用来满足Borrow<T>的约束
假设我们有一个find_item函数,它在HashMap<K, V>中查找某个键:
use std::borrow::Borrow; use std::collections::HashMap; use std::hash::Hash; fn find_item<'a, K, V, Q>(map: &'a HashMap<K, V>, key: &Q) -> Option<&'a V> where K: Eq + Hash + Borrow<Q>, // 关键点:K 可以借用为 Q Q: ?Sized + Eq + Hash, { map.get(key) } fn main() { let mut map: HashMap<String, i32> = HashMap::new(); map.insert("hello".to_string(), 42); // 由于 `String: Borrow<str>`,我们可以用 &str 直接查找 HashMap<String, i32> let value = find_item(&map, "hello"); println!("Value: {:?}", value); // Output: Value: Some(42) }这里的Borrow<T>提供的便利有这些:
-String可以作为str的Borrow<T>实现,因此HashMap<String, i32>允许用&str作为键进行查找
-find_item(&map, "hello")直接传入&str,而不需要转换成String
-find_item(&map, &"hello".to_string())也可以工作,因为&String也满足Borrow<str>