3.5 Rust Generic Types, Traits, and Lifetimes
Every programming language has tools for effectively handling the duplication of concepts. In Rust, one such tool is generics. Generics are abstract stand-ins for concrete types or other properties.
泛型的作用:降低代码冗余
- Identify duplicate code.
- Extract the duplicate code into the body of the function and specify the inputs and return values of that code in the function signature.
- Update the two instances of duplicated code to call the function instead.
Traits: Defining Shared Behavior
A trait tells the Rust compiler about functionality a particular type has and can share with other types. We can use traits to define shared behavior in an abstract way. We can use trait bounds to specify that a generic can be any type that has certain behavior.
Note: Traits are similar to a feature often called interfaces in other languages, although with some differences.
#![allow(unused)] pub trait Person { fn food(&self) -> String; fn eat(&self) -> String { format!("(eat {}...)", self.food()) } } pub struct Teacher { pub name: String, } impl Person for Teacher { fn food(&self) -> String { format!("{}", "面包") } } pub struct Student { pub username: String, pub age: i8, } impl Person for Student { fn food(&self) -> String { format!("{}", "水果") } } pub fn test(){ let tch = Teacher { name: String::from("a01"), }; println!("teacher: {}", tch.eat()); let st = Student { username: String::from("Penguins win the Stanley Cup Championship!"), age: 12, }; println!("student: {}", st.eat()); }
mod tra; fn main() { println!("----------------"); tra::tra1::test(); }
输出
----------------
teacher: (eat 面包...)
student: (eat 水果...)
Traits as Parameters
Now that you know how to define and implement traits, we can explore how to use traits to define functions that accept many different types.
pub fn notify_eat(item: &impl Person) { println!("notify: {}", item.eat()); } pub fn test2(){ let tch = Teacher { name: String::from("a01"), }; notify_eat(&tch); let st = Student { username: String::from("Penguins win the Stanley Cup Championship!"), age: 12, }; notify_eat(&st); }
----------------
notify: (eat 面包...)
notify: (eat 水果...)
Trait Bound Syntax
pub fn notify<T: Person>(item: &T) { println!("notify: {}", item.eat()); } pub fn test3(){ let tch = Teacher { name: String::from("a01"), }; notify(&tch); let st = Student { username: String::from("hong yun"), age: 12, }; notify(&st); }
----------------
notify: (eat 面包...)
notify: (eat 水果...)
Returning Types that Implement Traits
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) } } 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, } }
泛型的复制与比较
#![allow(unused)] fn largest<T: PartialOrd + Copy>(list: &[T]) -> T { let mut largest = list[0]; for &item in list { if item > largest { largest = item; } } largest } pub fn test() { 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); }
If we don’t want to restrict the largest
function to the types that implement the Copy
trait, we could specify that T
has the trait bound Clone
instead of Copy
. Then we could clone each value in the slice when we want the largest
function to have ownership. Using the clone
function means we’re potentially making more heap allocations in the case of types that own heap data like String
, and heap allocations can be slow if we’re working with large amounts of data.
Validating References with Lifetimes
说生命周期就绕不开rust的引用与借用,可先阅读一遍下面的文章,回顾一下引用与借用的概念,再继续阅读本文章
2.5 References & Borrowing
One detail we didn’t discuss in the “References and Borrowing” section in Chapter 4 is that every reference in Rust has a lifetime, which is the scope for which that reference is valid. Most of the time, lifetimes are implicit and inferred, just like most of the time, types are inferred. We must annotate types when multiple types are possible. In a similar way, we must annotate lifetimes when the lifetimes of references could be related in a few different ways. Rust requires us to annotate the relationships using generic lifetime parameters to ensure the actual references used at runtime will definitely be valid.
Preventing Dangling References with Lifetimes
fn main() { { let r; { let x = 5; r = &x; } println!("r: {}", r); } }
Note: The examples declare variables without giving them an initial value, so the variable name exists in the outer scope. At first glance, this might appear to be in conflict with Rust’s having no null values. However, if we try to use a variable before giving it a value, we’ll get a compile-time error, which shows that Rust indeed does not allow null values.
The outer scope declares a variable named r
with no initial value, and the inner scope declares a variable named x
with the initial value of 5. Inside the inner scope, we attempt to set the value of r
as a reference to x
. Then the inner scope ends, and we attempt to print the value in r
. This code won’t compile because the value r
is referring to has gone out of scope before we try to use it. Here is the error message:
$ cargo run Compiling chapter10 v0.1.0 (file:///projects/chapter10) error[E0597]: `x` does not live long enough --> src/main.rs:7:17 | 7 | r = &x; | ^^ borrowed value does not live long enough 8 | } | - `x` dropped here while still borrowed 9 | 10 | println!("r: {}", r); | - borrow later used here error: aborting due to previous error For more information about this error, try `rustc --explain E0597`. error: could not compile `chapter10`. To learn more, run the command again with --verbose.
The variable x
doesn’t “live long enough.” The reason is that x
will be out of scope when the inner scope ends on line 7. But r
is still valid for the outer scope; because its scope is larger, we say that it “lives longer.” If Rust allowed this code to work, r
would be referencing memory that was deallocated when x
went out of scope, and anything we tried to do with r
wouldn’t work correctly. So how does Rust determine that this code is invalid? It uses a borrow checker.