泛型、特征(trait),生命周期

一、泛型

泛型用于简化、方便代码复用。
与C++的模板函数,模板类相似。除了语法上有些不同,没什么特别的。

泛型结构

struct Point {
    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(item1: &T, item2: &T) {

两种方式各有优劣。
第一种方式可能更简洁
第二种方式可以限定两个参数必须是同一种类型,而不仅仅是实现了Summary接口就行。

5、指定多个trait限制

pub fn notify(item: &(impl Summary + Display)) {
pub fn notify(item: &T) {

多个trait用+连接即可。

where从句

fn some_function(t: &T, u: &U) -> i32 {
fn some_function(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(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 {
    x: T,
    y: T,
}
impl Pair {
    fn new(x: T, y: T) -> Self {
        Self { x, y }
    }
}

impl Pair {
    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 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 or y

一般人都会不适应的,看看编译器的解释。

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
    }
}

区区一个比较字符串的函数,都这么复杂。。。

总结

泛化类型参数消除了重复,特征、特征限定保证类型泛化的情况下,类型的行为仍然满足代码正确运行的需求。
另外,生命期检查在编译期进行,对运行性能无影响。

你可能感兴趣的:(Rust学习笔记,rust)