一、泛型
泛型用于简化、方便代码复用。
与C++的模板函数,模板类相似。除了语法上有些不同,没什么特别的。
泛型结构
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
}
二、特征(trait)
trait类似其它语言中的interface(比如go中的interface,也类似于C++中的抽象类),但是也并不完全相同,先这么理解,后面再深入。
用法
pub trait Summary {
fn summarize(&self) -> String;
}
go的示例
type Reader interface {
Read(p []byte) (n int, err error)
}
Rust示例
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)
}
}
与go
相比,需要显式指明实现哪(impl ... for ...
),go
是隐式的,只要某个类实现了interface
中的所有方法,就可以无缝使用。
限制:只有trait(Summary
)或者type(NewsArticle
)中至少一个是本crate本地定义的,才可以为该type实现该trait。也叫孤儿规则,可以避免多个crate为同一个type实现同一个trait。假定没有这个规则,crate C定义了trait t,struct s,那么crate C1可以为s实现trait t, crate C2也可以。那么,s就有了两个不同且冲突的实现。
1、默认实现
trait也可以有默认实现,等同于C++的虚函数(而非纯虚函数,虚函数可以有默认实现)。
go的interface是没有默认实现的吗?
2、不同点
主要是语法和具体使用规则上的,慢慢深入了解,实质差不多。
3、作为参数
pub trait Summary {
fn summarize(&self) -> String;
}
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
Summary
作为参数后,notify就可以支持所有实现了该trait的类型。语法上,需要明确加上关键字impl
。
这样其实就实现了多态,而且是运行期的。
4、trait限定(bound)语法
pub fn notify(item1: &impl Summary, item2: &impl Summary) {
pub fn notify<T: Summary>(item1: &T, item2: &T) {
两种方式各有优劣。
第一种方式可能更简洁
第二种方式可以限定两个参数必须是同一种类型,而不仅仅是实现了Summary
接口就行。
5、指定多个trait限制
pub fn notify(item: &(impl Summary + Display)) {
pub fn notify<T: Summary + Display>(item: &T) {
多个trait用+
连接即可。
where
从句
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
fn some_function<T, U>(t: &T, u: &U) -> i32
where T: Display + Clone,
U: Clone + Debug
{
语法糖,让阅读更清晰。
6、trait作为返回值
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,
}
}
这个也是顺其自然的规则,就像C++返回抽象基类。
例子
fn largest<T: 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);
}
根据trait限定为泛型实现不同的方法
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
impl<T: Display + PartialOrd> Pair<T> {
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实现了Display + PartialOrd
trait,才会实现fn cmp_display
。
impl<T: Display> ToString for T {
// --snip--
}
只有当类型T实现了Display trait,才会实现ToString trait(blanket implementation覆盖实现)。
trait和trait 限定既减少重复代码,又提高了灵活性。编译器在编译时就会进行trait bound的检查,这样运行期就无需检查某个类型是否实现了某个方法,提高了运行效率。而C++的虚函数,是运行期检查,模板也是编译期检查。
三、生命期验证
主要还是为了解决内存、资源泄漏的问题,有了生命期,才能确定何时可以释放哪些内存、资源。类比现实,相当于上帝得知道每个人的死亡日期,才能到日子派天使把人带走。如果人没有寿命,那上帝只能随机派天使随机带人了,那世界显然就很混乱。
1、生命期(寿命)防止悬空(dangling)引用
例子
fn main() {
{
let r;
{
let x = 5;
r = &x;
}
println!("r: {}", r);
}
}
r引用了x,r却在x死亡后使用x,显然不行。
2、借用检查器
检查所有的借用是否有效。
前提是必须在x的寿命之后再用了r,如果没用r,是没有关系的。
3、函数中的泛型生命期
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
问题
= help: this function’s return type contains a borrowed value, but the signature does not say whether it is borrowed from
x
ory
一般人都会不适应的,看看编译器的解释。
help: consider introducing a named lifetime parameter
意思是增加一个命名的生命周期参数,改成这种
fn longest(x: &'a str, y: &'a str) -> &'a str {
意思是,返回的引用生命期和入参x,y一样,这样就不会出现x,y的生命期短于返回值,导致引用已销毁对象的情况了。类比现实(可能不大恰当),假定longest是一个函数,从关张赵选一个出来单挑,然后诸葛亮可以调这个函数,那打孟获的时候,如果调这个函数,却返回了关张(关张此前已经挂了),显然是不合理的。所以,对函数的入参(关、张、赵),必须标注他们三的寿命,返回的人的寿命就可以在编译期确定是否合理。其实,这个是rust编译器的问题,因为没法很好的推断出所有参数的生命周期,所以才需要程序员手动指定。
4、生命期注解
&i32 // a reference
&'a i32 // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime
吊诡的语法,其中的'a
就是所谓的生命期注解,它就表示了变量的生命期。假定变量是个人,这个人生命起止1900-2000
,'a
就代表了1900-2000
。因为它可以作为泛型的参数,所以也叫generic lifetime parameter(泛型生命期参数)。结合上面关张赵的例子思考。
生命期注解相同,表明参数的生命期相同。
5、函数签名中的生命期注解
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
生命期注解是'a
,泛型指的是<>
,所以<'a>
就是泛型生命期参数。这表明返回值的生命期和入参相同。
但这里只是注解,并不改变变量的实际生命期。如果注解与实际不符合,还是报错的。还是关张赵的例子,注解关张活得比赵长,但编译器实际检查发现不是,还是一样报错。
比如
fn main() {
let string1 = String::from("long string is long"); // 赵云
let result; // 打孟获的大将
{
let string2 = String::from("xyz"); // 关羽
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {}", result);
}
// 谎称关羽(x)、赵云(y)、打孟获的大将(未知)活得一样长
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
string2的生命期短于result,报错
| ^^^^^^^^^^^^^^^^ borrowed value does not live long enough
这种与实际不符的写法,一样无法逃脱编译器的检查,因为打孟获的大将显然要活到关羽死之后的。
6、思考生命期
fn main() {
let string1 = String::from("abcd");
let string2 = "efghijklmnopqrstuvwxyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}
返回值一定 为x,生命期与y完全无关,因此y不需要注解。编译器要做的事情还是相当多的!!!
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}
fn longest<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("really long string");
result.as_str()
}
返回值虽然有生命期注解,但是没有和入参关联,一样导致编译错误。
error[E0515]: cannot return reference to local variable
result
生命期语法用于关联入参和返回值的生命期,关联后,rust就可以保证内存安全,并阻止非法的虚悬指针、非法内存使用。联想关张赵的例子,就是保证选的大将,还活着。
7、结构体定义中注解
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
}
结构体可能有引用,那么就需要生命期注解了。上面指,结构体对象的生命期不能长于它的引用成员变量。
8、生命期简化(省略、偷懒)
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let my_string = String::from("hello world");
// first_word works on slices of `String`s
let word = first_word(&my_string[..]);
let my_string_literal = "hello world";
// first_word works on slices of string literals
let word = first_word(&my_string_literal[..]);
// Because string literals *are* string slices already,
// this works too, without the slice syntax!
let word = first_word(my_string_literal);
}
不需要生命期注解,在rust1.0之前,需要这样
fn first_word<'a>(s: &'a str) -> &'a str {
但后来发现特定条件下借用检查器可以发现某些问题,不需要显式的注解,称作生命期偷懒(省略)。
编译器自动推导引用生命期的规则。
- 每个参数单独一个生命期
- 如果只有一个入参,那入参的生命期就是所有出参的生命期
- 如果有多个入参生命期,但是有一个入参是&self或者&mut self,那么self的生命期就所有出参的。这让成员方法更简洁。
成功推导的例子
fn first_word(s: &str) -> &str {
等价于
fn first_word<'a>(s: &'a str) -> &'a str {
无法成功简化省略的例子
fn longest(x: &str, y: &str) -> &str {
等价于
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
因为有两个入参,而且不少成员方法,而是函数,因此无法推导出参生命期。rust管成员函数叫方法,管普通函数叫函数。
9、方法中的生命期注解
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
}
返回值的生命期和self相同。
10、静态生命期
#![allow(unused)]
fn main() {
let s: &'static str = "I have a static lifetime.";
}
静态生命期,与程序生命期相同,存储于程序的二进制文件中。与C++中的static变量类似。
字符串常量(享元模式);单例中的懒汉模式(早早的构建好实例)。
11、综合运用泛化类型参数、特征限定、生命期
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest_with_an_announcement(
string1.as_str(),
string2,
"Today is someone's birthday!",
);
println!("The longest string is {}", result);
}
use std::fmt::Display;
fn longest_with_an_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display,
{
println!("Announcement! {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}
区区一个比较字符串的函数,都这么复杂。。。
总结
泛化类型参数消除了重复,特征、特征限定保证类型泛化的情况下,类型的行为仍然满足代码正确运行的需求。
另外,生命期检查在编译期进行,对运行性能无影响。