【Rust中级教程】2.4. API设计原则之不意外性(unsurprising) Pt.4:“人体工程学”的trait实现、包装类型(Wrapper Types)、`Borrow` trait

【Rust中级教程】2.4. API设计原则之不意外性(unsurprising) Pt.4:“人体工程学”的trait实现、包装类型(Wrapper Types)、`Borrow` trait
2.4. API设计原则之不意外性(unsurprising) Pt.4“人体工程学”的trait实现、包装类型(Wrapper Types)、Borrow trait2.4.1. “人体工程学”的trait实现Rust不会自动为实现某一trait的类型的引用提供对应的实现。比如说Bar实现了Trait但不能将Bar传递给fn fooT: 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 fooT: 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-BoxT where T: Trait ?Sized接着上面的代码例为了不让foo(bar_ref);报错我们需要手动提供T的Trait实现implT: 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: Veci32, } // 为 MyCollection 实现 IntoIterator impl IntoIterator for MyCollection { type Item i32; type IntoIter std::vec::IntoIterSelf::Item; fn into_iter(self) - Self::IntoIter { self.items.into_iter() } } // 为 MyCollection 实现 IntoIterator impla IntoIterator for a MyCollection { type Item a i32; type IntoIter std::slice::Itera, i32; fn into_iter(self) - Self::IntoIter { self.items.iter() } } // 为 mut MyCollection 实现 IntoIterator impla IntoIterator for a mut MyCollection { type Item a mut i32; type IntoIter std::slice::IterMuta, 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的值并满足DerefTarget 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 实现了 DerefTarget String // 这里可以直接调用 String 的方法而不需要手动解引用 let len my_wrapper.len(); let uppercased my_wrapper.to_uppercase(); println!(Length: {}, len); println!(Uppercased: {}, uppercased); }输出Length: 12 Uppercased: HELLO, RUST!如果你提供了相对透明的类型例如ArcT那么实现Deref允许你的包装类型在使用点运算符时自动解引用为内部类型从而可以直接调用内部类型的方法。如果访问内部类型不需要任何复杂或潜在的低效逻辑应考虑实现AsRef这样用户就可以轻松将WrapperType作为InnerType使用。对于大多是包装类型还应在可能的情况下为包装类型实现FromInnerType并为内部类型实现FromWrapper这样会免费得到Into以便用户可以轻松地添加或移除包装。看代码例use std::ops::Deref; use std::sync::Arc; // 定义一个包装类型 struct Wrapper(ArcString); // 实现 Deref 以允许透明地访问内部 String impl Deref for Wrapper { type Target String; fn deref(self) - Self::Target { self.0 } } // 实现 AsRefString允许用户获取 String impl AsRefString for Wrapper { fn as_ref(self) - String { self.0 } } // 实现 FromString 以便用户轻松创建 Wrapper impl FromString for Wrapper { fn from(s: String) - Self { Wrapper(Arc::new(s)) } } // 实现 FromWrapper for String允许用户将 Wrapper 转回 String克隆字符串 impl FromWrapper 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.BorrowtraitBorrowtrait与Deref和AsRef有些类似不过它针对的是更为狭窄的使用情况并且进行了定制。Borrowtrait允许调用者提供统一类型的多个本质上相同的变体中的任意一个这些变体叫做Equivalent。注意Borrowtrait仅适用于当你的类型本质上与另一个类型等价时(Equivalent)。也就是说Borrow适用于“等价”的情况而AsRef和Deref适用于“充当”的情况。比如说对于一个HashSetStringBorrow允许调用者提供str或String。看一个代码例use std::collections::HashSet; fn main() { let mut set: HashSetString HashSet::new(); set.insert(hello.to_string()); set.insert(world.to_string()); // 直接使用 str 进行查找而不需要创建 String。 // 这能工作是因为标准库已经提供了 impl Borrowstr 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; // 泛型函数接受任何 AsRefstr 的类型如 str 和 String fn containsS: AsRefstr(set: HashSetString, value: S) - bool { set.contains(value.as_ref()) // AsRefstr 使 value 转换为 str } fn main() { let mut set: HashSetString 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必须与拥有类型保持一致。潜在的问题是AsRefU甚至不会在文档层面要求源类型与U之间的Hash、Eq和Ord保持一致。比如说use std::collections::HashSet; #[derive(Hash, Eq, PartialEq)] struct CustomType { value: String, } // 实现 AsRefstr但这本身并不能让 HashSet 用 str 查找合法 impl AsRefstr for CustomType { fn as_ref(self) - str { self.value } } fn main() { let mut set: HashSetCustomType HashSet::new(); set.insert(CustomType { value: hello.to_string() }); // 这里不会通过编译error[E0308]contains 依赖 Borrow 而不是 AsRef // 因此没有 CustomType: Borrowstr 时 str 无法匹配 let exists set.contains(hello); println!(Exists: {}, exists); }contains的类型是按Borrow写的所以AsRefstr并不参与即便你写了借助AsRef再查找的辅助函数类型系统也不会强制CustomType的Hash/Eq与str一致相比之下BorrowU才是HashMap/HashSet查找使用的 trait其文档要求类型与借用形式之间的Hash、Eq和Ord保持一致编译器不会证明这一点实现者必须遵守use std::collections::HashSet; use std::borrow::Borrow; #[derive(Hash, Eq, PartialEq)] struct CustomType { value: String, } // Borrowstr 才让 contains(hello) 成为可能并且你必须让 Hash/Eq 与 str 对齐 impl Borrowstr for CustomType { fn borrow(self) - str { self.value } } fn main() { let mut set: HashSetCustomType HashSet::new(); set.insert(CustomType { value: hello.to_string() }); let exists set.contains(hello); // 在 Hash/Eq 与 str 一致时查找是安全的 println!(Exists: {}, exists); }其余特性Borrowtrait还为BorrowT、T和mut T提供了通用实现。这使得在trait约束中使用它来给接收给定类型的拥有值或引用值非常方便。Rust 标准库中为所有 T 提供了以下BorrowT的通用实现这些实现意味着- 类型T本身可以BorrowT即T可以直接作为BorrowT的参数-T也可以BorrowT这允许我们用一个不可变引用来满足BorrowT的约束-mut T也可以BorrowT这允许我们用一个可变引用来满足BorrowT的约束假设我们有一个find_item函数它在HashMapK, V中查找某个键use std::borrow::Borrow; use std::collections::HashMap; use std::hash::Hash; fn find_itema, K, V, Q(map: a HashMapK, V, key: Q) - Optiona V where K: Eq Hash BorrowQ, // 关键点K 可以借用为 Q Q: ?Sized Eq Hash, { map.get(key) } fn main() { let mut map: HashMapString, i32 HashMap::new(); map.insert(hello.to_string(), 42); // 由于 String: Borrowstr我们可以用 str 直接查找 HashMapString, i32 let value find_item(map, hello); println!(Value: {:?}, value); // Output: Value: Some(42) }这里的BorrowT提供的便利有这些-String可以作为str的BorrowT实现因此HashMapString, i32允许用str作为键进行查找-find_item(map, hello)直接传入str而不需要转换成String-find_item(map, hello.to_string())也可以工作因为String也满足Borrowstr