【Rust自学】10.4. trait Pt.2:trait作为参数和返回类型、trait bound

📅 2026/7/23 18:51:03
【Rust自学】10.4. trait Pt.2:trait作为参数和返回类型、trait bound
10.4 trait Pt.2trait作为参数和返回类型、trait bound10.4.1. 把trait作为参数继续以 10.3 trait Pt.1trait的定义、约束与实现 中的内容为例pub trait Summary { fn summarize(self) - String; } pub struct NewsArticle { pub headline: String, pub location: String, pub author: String, pub content: String, } impl Summary for NewsArticle { fn summarize(self) - String { format!({}, by {} ({}), self.headline, self.author, self.location) } } pub struct Tweet { pub username: String, pub content: String, pub reply: bool, pub retweet: bool, } impl Summary for Tweet { fn summarize(self) - String { format!({}: {}, self.username, self.content) } }如果我们新定义一个函数notify它以NewsArticle和Tweet这两种类型作为参数并打印Breaking news!后面跟上在参数上调用Summary中summarize方法的返回值就会遇到一个问题这个函数接收的是两个不同的结构体类型。怎样才能让参数同时适用于这两种类型呢细想一下这两个结构体有什么共同点没错——它们都实现了Summarytrait。Rust 为这种情况提供了解决方案pub fn notify(item: impl Summary) { println!(Breaking news! {}, item.summarize()); }只要把参数类型写成impl 某个trait即可。因为这两个结构体都实现了Summarytrait所以写成impl Summary。又因为这个函数不需要数据的所有权所以写成引用impl Summary。如果还有其他数据类型也实现了Summary同样可以作为参数传入。impl trait语法适用于简单情况。对于更复杂的情况通常使用 trait bound 语法。同样的代码用 trait bound 来写pub fn notifyT: Summary(item: T) { println!(Breaking news! {}, item.summarize()); }这两种写法是等价的。不过在有两个参数时这两种写法的差别会更明显。假设我要设计一个新的notify1函数。它接收两个参数Breaking news!后面的内容是分别在每个参数上调用summarize的返回值。trait bound 写法pub fn notify1T: Summary(item1: T, item2: T) { println!(Breaking news! {} {}, item1.summarize(), item2.summarize()); }impl trait写法pub fn notify1(item1: impl Summary, item2: impl Summary) { println!(Breaking news! {} {}, item1.summarize(), item2.summarize()); }显然这两种写法并不等价。用 trait bound 时item1和item2必须是同一个具体类型都是T。用impl Trait时只要各自都实现了Summaryitem1和item2可以是不同类型例如一个是NewsArticle另一个是Tweet。当你需要两个参数共用同一类型时用 trait bound当允许不同类型、且签名仍然简单时用impl Trait即可。在简单情况下impl Trait相当于带有 trait bound 的匿名泛型写起来更省事。对于更复杂的签名——例如多个参数必须是同一类型或约束很多——具名的 trait bound或where子句通常更清晰。那么如果notify函数需要其参数同时实现Displaytrait 和Summarytrait 呢换句话说两个或更多 trait bound 该怎么写例如pub fn notify_with_displayT: Summary std::fmt::Display(item: T) { println!(Breaking news! {}, item); }使用连接各个 trait bound。还有一点因为Display不在预导入模块中写它时需要写出完整路径。也可以先在代码开头引入Display像这样use std::fmt::Display。然后就可以在 trait bound 中直接写Displayuse std::fmt::Display; pub fn notify_with_displayT: Summary Display(item: T) { println!(Breaking news! {}, item); }别忘了impl trait也是语法糖在这种语法糖中同样用连接 trait bounduse std::fmt::Display; pub fn notify_with_display(item: (impl Summary Display)) { println!(Breaking news! {}, item); }这种写法有一个缺点如果 trait bound 太多大量约束信息会降低函数签名的可读性。为了解决这个问题Rust 提供了一种替代语法在函数签名之后使用where子句来写 trait bound。下面是多个 trait bound 的普通写法use std::fmt::Display; use std::fmt::Debug; pub fn special_notifyT: Summary Display, U: Summary Debug(item1: T, item2: U) { println!(Breaking news! {} and {}, item1.summarize(), item2.summarize()); }同样的代码用where子句重写use std::fmt::Display; use std::fmt::Debug; pub fn special_notifyT, U(item1: T, item2: U) where T: Summary Display, U: Summary Debug, { println!(Breaking news! {} and {}, item1.summarize(), item2.summarize()); }这种语法与 C# 很相似。10.4.2. 把trait作为返回类型和把 trait 作为参数一样把 trait 作为返回值也可以使用impl trait。例如fn returns_summarizable() - impl Summary { Tweet { username: String::from(horse_ebooks), content: String::from( of course, as you probably already know, people, ), reply: false, retweet: false, } }这种语法有一个缺点如果返回类型实现了某个 trait那么必须保证这个函数/方法所有可能的返回值都只能是同一种类型。这是因为impl形式在工作方式上有一些限制所以 Rust 并非在所有情况下都支持它。但 Rust 支持动态派发之后会讲。例如fn returns_summarizable(flag:bool) - impl Summary { if flag { Tweet { username: String::from(horse_ebooks), content: String::from( of course, as you probably already know, people, ), reply: false, retweet: false, } } else { NewsArticle { headline: String::from(Penguins win the Stanley Cup Championship!), location: String::from(Pittsburgh, PA, USA), author: String::from(Iceburgh, Scotland), content: String::from( The Pittsburgh Penguins once again are the best \ hockey team in the NHL., ), } } }根据flag的值可能有两种返回类型Tweet和NewsArticle。这时编译器会报错error[E0308]: if and else have incompatible types -- src/lib.rs:42:9 | 32 | / if flag { 33 | | / Tweet { 34 | | | username: String::from(horse_ebooks), 35 | | | content: String::from( 36 | | | of course, as you probably already know, people, ... | | 39 | | | retweet: false, 40 | | | } | | |_________- expected because of this 41 | | } else { 42 | | / NewsArticle { 43 | | | headline: String::from(Penguins win the Stanley Cup Championship!), 44 | | | location: String::from(Pittsburgh, PA, USA), 45 | | | author: String::from(Iceburgh, Scotland), ... | | 49 | | | ), 50 | | | } | | |_________^ expected Tweet, found NewsArticle 51 | | } | |_______- if and else have incompatible types | help: you could change the return type to be a boxed trait object | 31 - fn returns_summarizable(flag:bool) - impl Summary { 31 fn returns_summarizable(flag:bool) - Boxdyn Summary { | help: if you change the return type to expect trait objects, box the returned expressions | 33 ~ Box::new(Tweet { 34 | username: String::from(horse_ebooks), ... 39 | retweet: false, 40 ~ }) 41 | } else { 42 ~ Box::new(NewsArticle { 43 | headline: String::from(Penguins win the Stanley Cup Championship!), ... 49 | ), 50 ~ }) |报错信息说的是if和else的返回类型不兼容也就是它们不是同一种类型。使用trait bounds的实例还记得在 10.2. 泛型 中提到的比大小代码吗我把它粘在这里fn largestT(list: [T]) - T{ let mut largest list[0]; for item in list{ if item largest{ largest item; } } largest }当时出现的错误我也粘在这里error[E0369]: binary operation cannot be applied to type T -- src/main.rs:4:17 | 4 | if item largest{ | ---- ^ ------- T | | | T | help: consider restricting type parameter T with trait PartialOrd | 1 | fn largestT: std::cmp::PartialOrd(list: [T]) - T{ | 现在学了 trait 之后对这段代码及其报错信息的理解是不是又不一样了先从报错信息开始分析。错误说比较运算符不能应用于类型T。下面的help行说考虑限制类型参数T再往下给出了具体做法在T后面加上std::cmp::PartialOrd在 trait bound 中只需要写PartialOrd因为它在预导入模块中所以不需要写完整路径。这实际上就是用于比较的 trait。试试按照提示修改fn largestT: PartialOrd(list: [T]) - T{ let mut largest list[0]; for item in list{ if item largest{ largest item; } } largest }仍然会报错error[E0508]: cannot move out of type [T], a non-copy slice -- src/main.rs:2:23 | 2 | let mut largest list[0]; | ^^^^^^^ | | | cannot move out of here | move occurs because list[_] has type T, which does not implement the Copy trait | help: if T implemented Clone, you could clone the value -- src/main.rs:1:12 | 1 | fn largestT: PartialOrd(list: [T]) - T{ | ^ consider constraining this type parameter with Clone 2 | let mut largest list[0]; | ------- you could clone this value help: consider borrowing here | 2 | let mut largest list[0]; | error[E0507]: cannot move out of a shared reference -- src/main.rs:3:18 | 3 | for item in list{ | ---- ^^^^ | | | data moved here because item has type T, which does not implement the Copy trait | help: consider removing the borrow | 3 - for item in list{ 3 for item in list{ |但这次错误不同了无法从list中移出元素因为list中的T没有实现Copytrait。下面的help说如果T实现了Clonetrait可以考虑克隆该值。再下面还有一个help建议使用借用。根据以上信息有三种解决方案- 为泛型类型添加Copytrait- 使用克隆也就是为泛型类型添加Clonetrait- 使用借用该选择哪个方案呢这取决于你的需求。我想让这个函数处理数字和字符的集合。由于数字和字符都存储在栈上它们都实现了Copytrait所以只要给泛型类型加上Copy就够了fn largestT: PartialOrd Copy(list: [T]) - T{ let mut largest list[0]; for item in list{ if item largest{ largest item; } } largest } fn main() { let number_list vec![34, 50, 25, 100, 65]; let result largest(number_list); println!(The largest number is {}, result); let char_list vec![y, m, a, q]; let result largest(char_list); println!(The largest char is {}, result); }输出The largest number is 100 The largest char is y如果我想让这个函数比较String集合呢由于String存储在堆上它没有实现Copytrait所以给泛型类型加上Copy的思路行不通。那就试试克隆也就是给泛型类型加上Clonetraitfn largestT: PartialOrd Clone(list: [T]) - T{ let mut largest list[0].clone(); for item in list.iter() { if item largest{ largest item; } } largest } fn main() { let string_list vec![String::from(dev1ce), String::from(Zywoo)]; let result largest(string_list); println!(The largest string is {}, result); }输出error[E0507]: cannot move out of a shared reference -- src/main.rs:3:18 | 3 | for item in list.iter() { | ---- ^^^^^^^^^^^ | | | data moved here because item has type T, which does not implement the Copy trait | help: consider removing the borrow | 3 - for item in list.iter() { 3 for item in list.iter() { |错误说无法移动数据因为这种写法要求实现Copy而String做不到。该怎么办呢那就不要移动数据不要使用模式匹配。去掉item前面的这样item就从T变成了不可变引用T。然后在比较时使用解引用运算符*把T解引用为T再与largest比较下面的代码就是这种做法或者在largest前面加使其变成T。总之被比较的两个值必须类型一致fn largestT: PartialOrd Clone(list: [T]) - T{ let mut largest list[0].clone(); for item in list.iter() { if *item largest{ largest item.clone(); } } largest } fn main() { let string_list vec![String::from(dev1ce), String::from(Zywoo)]; let result largest(string_list); println!(The largest string is {}, result); }记住T没有实现Copytrait所以给largest赋值时需要使用clone方法。输出The largest string is dev1ce之所以这样写是因为返回值是T。如果把返回值改成T就不再需要克隆了fn largestT: PartialOrd(list: [T]) - T{ let mut largest list[0]; for item in list.iter() { if item largest{ largest item; } } largest } fn main() { let string_list vec![String::from(dev1ce), String::from(Zywoo)]; let result largest(string_list); println!(The largest string is {}, result); }但要记住初始化largest时必须把它设为T所以需要在list[0]前面加使其成为引用。另外比较时两边应是同一种值这里item和largest都是T因此可以直接写item largest。10.4.3. 使用trait bound有条件地实现方法如果在带有泛型类型参数的impl块上使用 trait bound就可以有条件地为实现了特定 trait 的类型实现方法。例如use std::fmt::Display; struct PairT { x: T, y: T, } implT PairT { fn new(x: T, y: T) - Self { Self { x, y } } } implT: Display PartialOrd PairT { fn cmp_display(self) { if self.x self.y { println!(The largest member is x {}, self.x); } else { println!(The largest member is y {}, self.y); } } }无论T的具体类型是什么new函数都会存在于Pair上。但只有当T同时实现了Display和PartialOrd时才会有cmp_display方法。也可以为实现了另一个 trait 的任意类型有条件地实现某个 trait。为所有满足某个 trait bound 的类型实现一个 trait叫做覆盖实现blanket implementation。以标准库中的to_string函数为例implT: Display ToString for T { // ...... }这意味着对所有满足Displaytrait 的类型都实现了ToString这就是覆盖实现任何实现了Display的类型都可以调用ToString上的方法。以整数为例let s 3.to_string();之所以能这样做是因为i32实现了Displaytrait所以可以调用ToString上的to_string方法。