Rust学习笔记(中)

前言

笔记的内容主要参考与《Rust 程序设计语言》,一些也参考了《通过例子学 Rust》和《Rust语言圣经》。

Rust学习笔记分为上中下,其它两个地址在Rust学习笔记(上)和Rust学习笔记(下)。

错误处理

panic! 与不可恢复的错误

当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据(也可以不清理数据就退出程序),然后接着退出。

panic! 和和其他语言不一样的地方,像下面的代码,这种情况下其他像 C 这样语言会尝试直接提供所要求的值,即便这可能不是你期望的:你会得到任何对应 vector 中这个元素的内存位置的值,甚至是这些内存并不属于 vector 的情况。这被称为 缓冲区溢出(buffer overread),并可能会导致安全漏洞,比如攻击者可以像这样操作索引来读取储存在数组后面不被允许的数据。为了使程序远离这类漏洞,如果尝试读取一个索引不存在的元素,Rust 会停止执行并拒绝继续。

fn main() {
    let v = vec![1, 2, 3];
    v[99];
}

遇到错误 Rust 还可以使用 backtrace ,得到一个详细的错误,通过 RUST_BACKTRACE=1 cargo run 启用。

Result 与可恢复的错误

大部分错误并没有严重到需要程序完全停止执行。有时,一个函数会因为一个容易理解并做出反应的原因失败。例如,如果因为打开一个并不存在的文件而失败,此时我们可能想要创建这个文件,而不是终止进程。

enum Result<T, E> {
    Ok(T),
    Err(E),
}

现在你需要知道的就是 T 代表成功时返回的 Ok 成员中的数据的类型,而 E 代表失败时返回的 Err 成员中的错误的类型。

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => {
            panic!("Problem opening the file: {:?}", error)
        },
    };
}


// 还可以匹配不同的错误
use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Problem creating the file: {:?}", e),
            },
            other_error => panic!("Problem opening the file: {:?}", other_error),
        },
    };
}
unwrap 和 expect

match 能够胜任它的工作,不过它可能有点冗长并且不总是能很好的表明其意图。Result<T, E> 类型定义了很多辅助方法来处理各种情况。其中之一叫做 unwrap,它的实现就类似于示例 9-4 中的 match 语句。如果 Result 值是成员 Okunwrap 会返回 Ok 中的值。如果 Result 是成员 Errunwrap 会为我们调用 panic!。这里是一个实践 unwrap 的例子:

use std::fs::File;

fn main() {
    let f = File::open("hello.txt").unwrap();
}

还有另一个类似于 unwrap 的方法叫做 expect,不过它允许自定义错误。

use std::fs::File;

fn main() {
    let f = File::open("hello.txt").expect("Failed to open hello.txt");
}
传播错误

Result<String, io::Error>,这意味着函数返回一个 Result<T, E> 类型的值,其中泛型参数 T 的具体类型是 String,而 E 的具体类型是 io::Error。如果这个函数没有出任何错误成功返回,函数的调用者会收到一个包含 StringOk 值 。如果函数遇到任何错误,函数的调用者会收到一个 Err 值,它储存了一个包含更多这个问题相关信息的 io::Error 实例。

fn read_username_from_file() -> Result<String, io::Error> {
    let f = File::open("hello.txt");
    let mut f = match f {
        Ok(file) => file,
        Err(e) => return Err(e),
    };
    let mut s = String::new();
    match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        Err(e) => Err(e),
    }
}
?简写
// 等同于上面代码,如果是ok会继续执行,Err的话结束程序
fn read_username_from_file() -> Result<String, io::Error> {
    // 文章中提到,?会自动匹配你想要的返回错我类型
    let mut f = File::open("hello.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}

// 更简洁的方式
fn read_username_from_file() -> Result<String, io::Error> {
    let mut s = String::new();
    File::open("hello.txt")?.read_to_string(&mut s)?;
    Ok(s)
}
main函数返回错误

main 函数是特殊的,其必须返回什么类型是有限制的。main 函数的一个有效的返回值是 (),同时出于方便,另一个有效的返回值是 Result<T, E>,如下所示。Box<dyn Error> 被称为 “trait 对象”(“trait object”),目前可以理解 Box<dyn Error> 为使用 ?main 允许返回的 “任何类型的错误”。

use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
    let f = File::open("hello.txt")?;

    Ok(())
}

panic还是不panic

该如何决定何时应该 panic! 以及何时应该返回 Result 呢?如果代码 panic,就没有恢复的可能。选择返回 Result 值的话,就将选择权交给了调用者,而不是代替他们做出决定。调用者可能会选择以符合他们场景的方式尝试恢复,或者也可能干脆就认为 Err 是不可恢复的,所以他们也可能会调用 panic! 并将可恢复的错误变成了不可恢复的错误。因此返回 Result 是定义可能会失败的函数的一个好的默认选择。

有一些情况 panic 比返回 Result 更为合适,不过他们并不常见。

  1. 示例、代码原型和测试都非常适合 panic
  2. 当我们比编译器知道更多的情况
  3. 错误处理指导原则(有害状态并不包含预期会偶尔发生的错误;在此之后代码的运行依赖于不处于这种有害状态;当没有可行的手段来将有害状态信息编码进所使用的类型中的情况)
  4. 创建自定义类型进行有效性验证

泛型、trait与生命周期

泛型

在函数定义中使用泛型

我们可以使用泛型为像函数签名或结构体这样的项创建定义,这样它们就可以用于多种不同的具体数据类型。如下图,两个只在名称和签名中类型有所不同的函数,可以利用泛型优化它们。

fn largest_i32(list: &[i32]) -> i32 {
    let mut largest = list[0];
    for &item in list.iter() {
        if item > largest {
            largest = item;
        }
    }
    largest
}

fn largest_char(list: &[char]) -> char {
    let mut largest = list[0];
    for &item in list.iter() {
        if item > largest {
            largest = item;
        }
    }
    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];
    let result = largest_i32(&number_list);
    println!("The largest number is {}", result);
    let char_list = vec!['y', 'm', 'a', 'q'];
    let result = largest_char(&char_list);
    println!("The largest char is {}", result);
}
fn largest<T>(list: &[T]) -> T {
    let mut largest = list[0];
    for &item in list.iter() {
        // 这里会报错
        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);
}

选择 T 是因为 Rust 的习惯是让变量名尽量短,通常就只有一个字母,同时 Rust 类型命名规范是骆驼命名法(CamelCase)。T 作为 “type” 的缩写是大部分 Rust 程序员的首选。

那里会报错是因为,注释中提到了 std::cmp::PartialOrd,这是一个 trait,这个错误表明 largest 的函数体不能适用于 T 的所有可能的类型。因为在函数体需要比较 T 类型的值,不过它只能用于我们知道如何排序的类型。为了开启比较功能,标准库中定义的 std::cmp::PartialOrd trait 可以实现类型的比较功能。

结构体定义中的泛型

这个定义表明结构体 Point<T> 对于一些类型 T 是泛型的,而且字段 xy 都是相同类型的,无论它具体是何类型。

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

不同类型的

struct Point<T, U> {
    x: T,
    y: U,
}

fn main() {
    let both_integer = Point { x: 5, y: 10 };
    let both_float = Point { x: 1.0, y: 4.0 };
    let integer_and_float = Point { x: 5, y: 4.0 };
}
枚举定义中的泛型
enum Option<T> {
    Some(T),
    None,
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}
方法定义中的泛型

impl 之后声明泛型 T ,这样 Rust 就知道 Point 的尖括号中的类型是泛型而不是具体类型。(不用纠结这里了,为什么 impl 后要加 T,就按它的理解)

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };
    println!("p.x = {}", p.x());
}
泛型代码的性能

Rust 实现了泛型,使得使用泛型类型参数的代码相比使用具体类型并没有任何速度上的损失。Rust 通过在编译时进行泛型代码的单态化(monomorphization)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。

trait 定义共享的行为

一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合。

为类实现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)
    }
}

let tweet = Tweet {
    username: String::from("horse_ebooks"),
    content: String::from("of course, as you probably already know, people"),
    reply: false,
    retweet: false,
};

println!("1 new tweet: {}", tweet.summarize());

如果这个 lib.rs 是对应 aggregator crate 的,而别人想要利用我们 crate 的功能为其自己的库作用域中的结构体实现 Summary trait。首先他们需要将 trait 引入作用域。这可以通过指定 use aggregator::Summary; 实现,这样就可以为其类型实现 Summary trait 了。Summary 还必须是公有 trait 使得其他 crate 可以实现它,

实现 trait 时需要注意的一个限制是,只有当 trait 或者要实现 trait 的类型位于 crate 的本地作用域时,才能为该类型实现 trait。例如,可以为 aggregator crate 的自定义类型 Tweet 实现如标准库中的 Display trait,这是因为 Tweet 类型位于 aggregator crate 本地的作用域中。类似地,也可以在 aggregator crate 中为 Vec<T> 实现 Summary,这是因为 Summary trait 位于 aggregator crate 本地作用域中。

但是不能为外部类型实现外部 trait。例如,不能在 aggregator crate 中为 Vec<T> 实现 Display trait。这是因为 DisplayVec<T> 都定义于标准库中,它们并不位于 aggregator crate 本地作用域中。这个限制是被称为 相干性coherence) 的程序属性的一部分,或者更具体的说是 孤儿规则orphan rule),其得名于不存在父类型。这条规则确保了其他人编写的代码不会破坏你代码,反之亦然。没有这条规则的话,两个 crate 可以分别对相同类型实现相同的 trait,而 Rust 将无从得知应该使用哪一个实现。

(这段代码没有违反孤儿规则(orphan rule),因为至少有一方(trait 或类型)是在本地 crate 中定义的。孤儿规则防止你为不在你的 crate 中定义的类型实现不在你的 crate 中定义的 trait。在这个例子中,CommandError是在你的 crate 中定义的,而fmt::Display trait 是标准库提供的。这样的实现是被允许的,因为它满足了孤儿规则的条件之一:要实现的类型(CommandError)是本地定义的。)

默认实现
pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
  	// 可以有多个
  	fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

// 指定一个空的impl,也可只实现个别trait
impl Summary for NewsArticle {}

fn main() {
    let article = NewsArticle {
        headline: String::from("Penguins win the Stanley Cup Championship!"),
        location: String::from("Pittsburgh, PA, USA"),
        author: String::from("Iceburgh"),
        content: String::from("The Pittsburgh Penguins once again are the best
    hockey team in the NHL."),
    };

    println!("New article available! {}", article.summarize());
}
trait作为参数

可以将传递 NewsArticleTweet 的实例来调用 notify

外面可以直接用,为什么要套个函数?方便代码重用,这样限制了只有实现trait的类才能用。

pub fn notify(item: impl Summary) {
    println!("Breaking news! {}", item.summarize());
}
Trait Bound 语法

上面的代码可以变成这样。

pub fn notify<T: Summary>(item: T) {
    println!("Breaking news! {}", item.summarize());
}

pub fn notify(item1: impl Summary, item2: impl Summary) {
pub fn notify<T: Summary>(item1: T, item2: T) {
通过 + 指定多个 trait bound
pub fn notify(item: impl Summary + Display) {
pub fn notify<T: Summary + Display>(item: T) {
通过 where 简化 trait bound
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
{
返回实现了 trait 的类型

通过使用 impl Summary 作为返回值类型,在不确定其具体的类型的情况下。

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

// 这样无法运行,不能返回两种
fn returns_summarizable(switch: bool) -> impl Summary {
    if switch {
        NewsArticle {
            headline: String::from("Penguins win the Stanley Cup Championship!"),
            location: String::from("Pittsburgh, PA, USA"),
            author: String::from("Iceburgh"),
            content: String::from("The Pittsburgh Penguins once again are the best
            hockey team in the NHL."),
        }
    } else {
        Tweet {
            username: String::from("horse_ebooks"),
            content: String::from("of course, as you probably already know, people"),
            reply: false,
            retweet: false,
        }
    }
}
使用 trait bounds 来修复 largest 函数
// 过滤掉没有PartialOrd和Copy trait的T
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut largest = list[0];

    for &item in list.iter() {
        if item > largest {
            largest = item;
        }
    }

    largest
}

使用 trait bound 有条件地实现方法

更高级的那 impl 控制

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

也可以对任何实现了特定 trait 的类型有条件地实现 trait。对任何满足特定 trait bound 的类型实现 trait 被称为 blanket implementations,他们被广泛的用于 Rust 标准库中。例如,标准库为任何实现了 Display trait 的类型实现了 ToString trait。这个 impl 块看起来像这样:

impl<T: Display> ToString for T {
    // --snip--
}

生命周期

生命周期避免了悬垂引用(指向已经被释放或无效内存的引用)。

下图为借用检查器

{
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {}", r); //          |
}                         // ---------+

// 正确的例子

#![allow(unused_variables)]
{
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {}", r); //   |       |
                          // --+       |
}                         // ----------+
函数的生命周期

下面这个会报错,因为编译器不知道到底返回的是x还是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(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

借用检查器自身同样也无法确定,因为它不知道 xy 的生命周期是如何与返回值的生命周期相关联的。

要解决这个问题需要用到生命周期注解,生命周期注解并不改变任何引用的生命周期的长短,它用于描述多个引用生命周期相互的关系

// 这里我们想要告诉Rust关于参数中的引用和返回值之间的限制是他们都必须拥有相同的生命周期
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

// 就算是这样也必须加注解,不过可以只加一个
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    x
}

当具体的引用被传递给 longest 时,被 'a 所替代的具体生命周期是 x 的作用域与 y 的作用域相重叠的那一部分。换一种说法就是泛型生命周期 'a 的具体生命周期等同于 xy 的生命周期中较小的那一个。因为我们用相同的生命周期参数 'a 标注了返回的引用值,所以返回的引用值就能保证在 xy 中较短的那个生命周期结束之前保持有效。

另一个问题,当从函数返回一个引用,返回值的生命周期参数需要与一个参数的生命周期参数相匹配。如果返回的引用 没有 指向任何一个参数,那么唯一的可能就是它指向一个函数内部创建的值,它将会是一个悬垂引用,因为它将会在函数结束时离开作用域。像下面的代码会报错。

这是因为返回值的生命周期与参数完全没有关联。 resultlongest 函数的结尾将离开作用域并被清理,而我们尝试从函数返回一个 result 的引用。解决方案是返回一个有所有权的数据类型而不是一个引用,这样函数调用者就需要负责清理这个值了。

fn longest<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    result.as_str()
}
结构体中的生命周期

比如下面的结构体,用到了&str,就需要加声明周期。为什么?因为&str是一个引用,也就是这个struct拿不到它的所有权,所以很有肯能,在这个struct使用的过程中,&str失效了,造成错误。所有必须让这个结构体的生命周期和&str一样。

struct ImportantExcerpt<'a> {
    part: &'a str,
}
声明周期的省略(lifetime elision)

编译器采用三条规则来判断引用何时不需要明确的注解。

第一条,每一个是引用的参数都有它自己的生命周期参数。

第二条,如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数。

第三条,如果方法有多个输入生命周期参数并且其中一个参数是 &self 或 &mut self,说明是个对象的方法, 那么所有输出生命周期参数被赋予 self 的生命周期。为什么这么规定呢?我觉得记住就行,就是这么设计的,那难道声明周期高于这个对象吗?

fn first_word(s: &str) -> &str
// 根据第一条规则变为
fn first_word<'a>(s: &'a 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
// 根据第二条规则不成立
方法中的声明周期
struct Book<'a> {
    name: &'a str,
}
impl<'a> Book<'a> {
    // 更具第三规则省略声明周期的标注
    fn get_prefix(&self) -> &str {
        &self.name[..3]
    }
}
静态生命周期

(这块我有些困惑,暂时不思考了,有机会读一读关于String和&str的文章)

'static,其生命周期能够存活于整个程序期间。所有的字符串字面值都拥有 'static 生命周期。这是因为字符串的文本被直接储存在程序的二进制文件中而这个文件总是可用的,因此所有的字符串字面值都是 'static 的。

let s: &'static str = "I have a static lifetime.";
// 这两句是等效的
let s: &str = "I have a static lifetime.";

为什么?既然所有的字符串字面值都拥有 'static 生命周期,那么下面的代码为什么报错?

fn main() {
    {
        let s: &'static str = "hello world";
    }
    println!("s={}", s);
}

这里 str 的 lifetime 确实是 'static,但是它被 scope 所限制,也就是“变小”了。

还有个疑问,既然 &str 默认'static,呢么这里为什么编译不通过呢?为什么必须加 'static

fn get_static_message() -> &str {
    "Hello, I have a static lifetime."
}

具体原因不懂,我觉得可能是 'static 是一个上限,是可能被别的值所影响的,所以还是得标出来。

至于为什么 &str 都是 'static,因为它直接存储在二进制文件内,而不是在运行时动态地存储在堆或栈上。那为什么直接存储在二进制文件内?因为这样可以减少运行时的内存使用,而且字符串字面值是不变的等一些原因吧。

另外,需要注意的,只有引用有声明周期,像下面的例子,都是直接把值 copy 出去,不存在什么声明周期。

fn main() {
    let r;
    {
        let x = 5;
        r = x;
    }
    println!("r: {}", r);
}

fn get_static_message() -> i32 {
		1
}
结合泛型的生命周期
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
    }
}

迭代器与闭包

闭包

闭包(closures)是可以保存进变量或作为参数传递给其他函数的匿名函数,并且可以使用在其所在的作用域的值

// simulated_expensive_calculation(intensity)这个函数是非常耗时的,在下面三个地方出现
// 我们其实只需要它的运行结果,所以第一种方式就是把它提出来,赋给变量
// 但这样带来一个问题是有些地方并不执行,比如else的if,也必须执行
// 所以第二种方式就是用闭包,它只在调用它时运行
// 但是使用了这种,在第一个if里它仍然会执行两次
// 解决办法为可以搞个变量在第一个if里接一下值(那直接在函数呢个办法里,在第一个if接一下不就行了?呵呵,例子不好),另一种用srtuct(后面会写)
fn generate_workout(intensity: u32, random_number: u32) {
    let expensive_closure = |num| {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    };

    if intensity < 25 {
        println!(
            "Today, do {} pushups!",
            // simulated_expensive_calculation(intensity)
            expensive_closure(intensity)
        );
        println!(
            "Next, do {} situps!",
            // simulated_expensive_calculation(intensity)
            expensive_closure(intensity)
        );
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                // // simulated_expensive_calculation(intensity)
                expensive_closure(intensity)
            );
        }
    }
}
闭包的类型

闭包不要求像 fn 函数那样在参数和返回值上注明类型。函数中需要类型注解是因为他们是暴露给用户的显式接口的一部分,如果不定义用户无法使用。但是闭包并不用于这样暴露给外面,只供自己使用。当然,你也可以标出来。

像下面,会报错,这是因为每个闭包都有自己的唯一类型,不能像下面呢样。

let example_closure = |x| x;

let s = example_closure(String::from("hello"));
let n = example_closure(5);
存储闭包的结果

可以创建一个存放闭包和调用闭包结果的结构体。该结构体只会在需要结果时执行闭包,并会缓存结果值,这样余下的代码就不必再负责保存结果并可以复用该值。这种模式被称 memoization 或 lazy evaluation。

struct Cacher<T>
    where T: Fn(u32) -> u32
{
    calculation: T,
    value: Option<u32>,
}


impl<T> Cacher<T>
    where T: Fn(u32) -> u32
{
    fn new(calculation: T) -> Cacher<T> {
        Cacher {
            // 省略的写法
            calculation,
            value: None,
        }
    }

    fn value(&mut self, arg: u32) -> u32 {
        match self.value {
            // 当然这里的逻辑,当arg改变时,还是返回原来的v。可以自行设定逻辑
            Some(v) => v,
            None => {
                let v = (self.calculation)(arg);
                self.value = Some(v);
                v
            },
        }
    }
}

上面的目标将变为:

fn generate_workout(intensity: u32, random_number: u32) {
    let mut expensive_result = Cacher::new(|num| {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    });

    if intensity < 25 {
        println!(
            "Today, do {} pushups!",
            expensive_result.value(intensity)
        );
        println!(
            "Next, do {} situps!",
            expensive_result.value(intensity)
        );
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                expensive_result.value(intensity)
            );
        }
    }
}
捕获环境变量

像这样,可以直接拿到 x 的值。

fn main() {
    let x = 4;

    let equal_to_x = |z| z == x;

    let y = 4;

    assert!(equal_to_x(y));
}

闭包可以通过三种方式捕获其环境,他们直接对应函数的三种获取参数的方式:获取所有权,可变借用和不可变借用。这三种捕获值的方式被编码为如下三个 Fn trait:

  • FnOnce 消费从周围作用域捕获的变量,闭包周围的作用域被称为其环境,environment。为了消费捕获到的变量,闭包必须获取其所有权并在定义闭包时将其移动进闭包。其名称的 Once 部分代表了闭包不能多次获取相同变量的所有权的事实,所以它只能被调用一次。
  • FnMut 获取可变的借用值所以可以改变其环境
  • Fn 从其环境获取不可变的借用值

这个要在where里写,如果只是在函数里用,Rust会自动判断。

另外也可以用 move 将所有权移到闭包里。

let equal_to_x = move |z| z == x;

迭代器

迭代器(iterator)负责遍历序列中的每一项,像下面 iter() 会返回一个迭代器,然后遍历。直接遍历也行,因为 for 循环会帮你调用迭代器 。

let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
for val in v1_iter {
    println!("Got: {}", val);
}

迭代器会实现了一个叫做 Iterator 的定义于标准库的 trait,里面都会有一个 next 方法。比如下面这样,要注意的是声明的迭代器需要为 mut,在迭代器上调用 next 方法改变了迭代器中用来记录序列位置的状态。

#[test]
fn iterator_demonstration() {
    let v1 = vec![1, 2, 3];
    let mut v1_iter = v1.iter();
    assert_eq!(v1_iter.next(), Some(&1));
    assert_eq!(v1_iter.next(), Some(&2));
    assert_eq!(v1_iter.next(), Some(&3));
    assert_eq!(v1_iter.next(), None);
}

另外iter 方法生成一个不可变引用的迭代器。如果我们需要一个获取 v1 所有权并返回拥有所有权的迭代器,则可以调用 into_iter。类似的,如果我们希望迭代可变引用,则可以调用 iter_mut

消费迭代器的方法

这些调用 next 方法的方法被称为 消费适配器(consuming adaptors),因为调用他们会消耗迭代器。一个消费适配器的例子是 sum 方法。这个方法获取迭代器的所有权并反复调用 next 来遍历迭代器,因而会消费迭代器。调用 sum 之后不再允许使用 v1_iter 因为调用 sum 时它会获取迭代器的所有权。

fn iterator_sum() {
    let v1 = vec![1, 2, 3];
    let v1_iter = v1.iter();
    let total: i32 = v1_iter.sum();
    assert_eq!(total, 6);
}
迭代器适配器

迭代器适配器(iterator adaptors),他们允许我们将当前迭代器变为不同类型的迭代器,还可以链式调用多个迭代器适配器。

map:将迭代器中的每个元素转换为另一种形式或值。

let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();

filter:用于从迭代器中筛选出满足某个条件的元素。

shoes.into_iter().filter(|s| s.size == shoe_size).collect()
自定义迭代器

大概就是下面这样,重点就是实现 next 方法。

struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = u32;
		// 只会从 1 数到 5 的迭代器
    fn next(&mut self) -> Option<Self::Item> {
        self.count += 1;
        if self.count < 6 {
            Some(self.count)
        } else {
            None
        }
    }
}


fn calling_next_directly() {
    let mut counter = Counter::new();
    assert_eq!(counter.next(), Some(1));
    assert_eq!(counter.next(), Some(2));
    assert_eq!(counter.next(), Some(3));
    assert_eq!(counter.next(), Some(4));
    assert_eq!(counter.next(), Some(5));
    assert_eq!(counter.next(), None);
}

智能指针

智能指针是一类数据结构,他们的表现类似指针,但是也拥有额外的元数据和功能。而且引用是一类只借用数据的指针;相反,在大部分情况下,智能指针拥有他们指向的数据,比如String,Vec。智能指针通常使用结构体实现,区别于常规结构体的显著特性在于其实现了 DerefDrop trait。Deref trait 允许智能指针结构体实例表现的像引用一样,这样就可以编写既用于引用、又用于智能指针的代码。Drop trait 允许我们自定义当智能指针离开作用域时运行的代码。

Box

Box<T>允许你将一个值放在堆上而不是栈上,留在栈上的则是指向堆数据的指针。除了数据被储存在堆上而不是栈上之外,box 没有性能损失。不过也没有很多额外的功能。用于像是编译时未知大小,而又想要在需要确切大小的上下文中使用这个类型值的时候;当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候(普通呢些i32都实现了copy trait);当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候。

它的一个应用像是可以存储递归。如果正常写的话Rust会因为不知道这个变量的大小而报错。对于 Box<T>,因为它是一个指针,我们总是知道它需要多少空间,指针的大小并不会根据其指向的数据量而改变。意味着不同于直接储存一个值,我们将间接的储存一个指向值的指针。

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1,
        Box::new(Cons(2,
            Box::new(Cons(3,
                Box::new(Nil))))));
}

Deref trait

Box<T> 类型是一个智能指针,因为它实现了 Deref trait,它允许 Box<T> 值被当作引用对待。当 Box<T> 值离开作用域时,由于 Box<T> 类型 Drop trait 的实现,box 所指向的堆数据也会被清除。

以下是一个简单的解引用:

fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

fn main() {
    let x = 5;
    let y = Box::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
自定义智能指针

实现 Deref trait 允许我们重载 解引用运算符dereference operator*(与乘法运算符或通配符相区别)。通过这种方式实现 Deref trait 的智能指针可以被当作常规引用来对待,可以编写操作引用的代码并用于智能指针。

use std::ops::Deref;

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

// 为什么这么写,不用想呢么多了
impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

实现之后,像下面解引用代码就可以运行了,实际输入*y,运行逻辑为*(y.deref())(外面还有个*是因为deref里是&self.0)

fn main() {
    let x = 5;
    let y = MyBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
解引用强制多态

它是 Rust 在函数或方法传参上的一种便利。其将实现了 Deref 的类型的引用转换为原始类型通过 Deref 所能够转换的类型的引用。当这种特定类型的引用作为实参传递给和形参类型不同的函数或方法时,解引用强制多态将自动发生。这时会有一系列的 deref 方法被调用,把我们提供的类型转换成了参数所需的类型。

比如下面这个例子,MyBox<T> 上实现了 Deref trait,Rust 可以通过 deref 调用将 &MyBox<String> 变为 &String。标准库中提供了 String 上的 Deref 实现,其会返回字符串 slice,这可以在 Deref 的 API 文档中看到。Rust 再次调用 deref&String 变为 &str,这就符合 hello 函数的定义了。

解引用强制多态(deref coercions)的加入使得 Rust 程序员编写函数和方法调用时无需增加过多显式使用 &* 的引用和解引用。

fn hello(name: &str) {
    println!("Hello, {}!", name);
}
fn main() {
  let m = MyBox::new(String::from("Rust"));
  hello(&m);
  // 如果没有deref coercions
  hello(&(*m)[..]);
}

如果是要处理可变引用,会用到DerefMut,具体不说了。

Drop trait

其允许我们在值要离开作用域时执行一些代码,像是这样

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

然而,有时你可能需要提早清理某个值。一个例子是当使用智能指针管理锁时,你可能希望强制运行 drop 方法来释放锁以便作用域中的其他代码可以获取锁。Rust 并不允许我们主动调用 Drop trait 的 drop 方法;当我们希望在作用域结束之前就强制释放变量的话,我们应该使用的是由标准库提供的 std::mem::drop。类似这样:

let c = CustomSmartPointer { data: String::from("some data") };
drop(c);

Rc<T>

Rust 有一个叫做 Rc<T> 的类型。其名称为 引用计数reference counting)的缩写。引用计数意味着记录一个值引用的数量来知晓这个值是否仍在被使用。如果某个值有零个引用,就代表没有任何有效引用并可以被清理。Rc<T> 只能用于单线程场景;第十六章并发会涉及到如何在多线程程序中进行引用计数。

Rc<T> 用于当我们希望在堆上分配一些内存供程序的多个部分读取,而且无法在编译时确定程序的哪一部分会最后结束使用它的时候。

如何使用它共享数据呢,想要b和c共享5、10是难以完成的,看下面的代码

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let a = Cons(5,
        Box::new(Cons(10,
            Box::new(Nil))));
    let b = Cons(3, Box::new(a));
    let c = Cons(4, Box::new(a));
}

可以改变 Cons 的定义来存放一个引用,不过接着必须指定生命周期参数。通过指定生命周期参数,表明列表中的每一个元素都至少与列表本身存在的一样久。

也可以修改 List 的定义为使用 Rc<T> 代替 Box<T>。当创建 b 时,不同于获取 a 的所有权,这里会克隆 a 所包含的 Rc,这会将引用计数从 1 增加到 2 并允许 ab 共享 Rc 中数据的所有权。创建 c 时也会克隆 a,这会将引用计数从 2 增加为 3。每次调用 Rc::cloneRc 中数据的引用计数都会增加,直到有零个引用之前其数据都不会被清理。当 c 离开作用域时,计数减1。

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(4, Rc::clone(&a));
}

当然也可以使用 a.clone() 而不是 Rc::clone(&a)Rc::clone 的实现并不像大部分类型的 clone 实现那样对所有数据进行深拷贝。Rc::clone 只会增加引用计数,这并不会花费多少时间。深拷贝可能会花费很长时间。

查看数量是可以调用Rc::strong_count(&a)获得

RefCell<T>

内部可变性Interior mutability)允许你即使在有不可变引用时也可以改变数据,这通常是借用规则所不允许的。为了改变数据,该模式在数据结构中使用 unsafe 代码来模糊 Rust 通常的可变性和借用规则。

为什么用它呢(用到再说吧)?因为一些分析是不可能的,如果 Rust 编译器不能通过所有权规则编译,它可能会拒绝一个正确的程序;从这种角度考虑它是保守的。RefCell<T> 正是用于当你确信代码遵守借用规则,而编译器不能理解和确定的时候。类似于 Rc<T>RefCell<T> 只能用于单线程场景。在需要绕过Rust静态借用规则(编译时借用检查)的情况,允许在运行时进行动态借用检查。这样的设计允许在特定条件下安全地进行内部可变性

  • Rc<T> 允许相同数据有多个所有者;Box<T>RefCell<T> 有单一所有者。
  • Box<T> 允许在编译时执行不可变或可变借用检查;Rc<T>仅允许在编译时执行不可变借用检查;RefCell<T> 允许在运行时执行不可变或可变借用检查。
  • 因为 RefCell<T> 允许在运行时执行可变借用检查,所以我们可以在即便 RefCell<T> 自身是不可变的情况下修改其内部的值。
#[derive(Debug)]
enum List {
    Cons(Rc<RefCell<i32>>, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    let value = Rc::new(RefCell::new(5));

    let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));

    let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a));
    let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a));

    *value.borrow_mut() += 10;

    println!("a after = {:?}", a);
    println!("b after = {:?}", b);
    println!("c after = {:?}", c);
}

引用循环与内存泄漏

可以理解为,比如a依赖b,b依赖a,在作用域结束时,Rust要先确定删谁,这种情况,谁都删不了。

可以用Rc::downgrade,代替Rc::clone。后者strong_count不为0就不能清理,前者weak_count不为0也能清理

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/615699.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

百问C语言第1问——彻底弄懂define用法

系列文章目录 玩转指针专栏 趣味c程序专栏 一.c语言关系操作符练习题(新手必会) 一.c语言常见概念(超全) 一.趣味c程序—关机程序&#xff08;整蛊同学版) 二.趣味c程序—猜数字游戏&#xff08;含干货知识点 三.趣味c程序—打印图形&#xff08;1&#xff09;&#xff08;含干…

前端笔记-day05

文章目录 01-结构伪类选择器02-结构伪类选择器-公式用法03-伪元素选择器04-盒子模型-组成05-盒子模型-边框线06-盒子模型-单方向边框线07-盒子模型-内边距08-盒子模型-padding多值写法09-盒子模型-尺寸计算10-盒子模型-版心居中11-清除默认样式12-元素溢出overflow13-外边距合并…

Java | Leetcode Java题解之第83题删除排序链表中的重复元素

题目&#xff1a; 题解&#xff1a; class Solution {public ListNode deleteDuplicates(ListNode head) {if (head null) {return head;}ListNode cur head;while (cur.next ! null) {if (cur.val cur.next.val) {cur.next cur.next.next;} else {cur cur.next;}}return…

E - Yet Another Sigma Problem(ABC字典树)

思路&#xff1a;我们可以发现两个字符串的最长公共前缀就是字典树中的最近公共祖先。然而这道题&#xff0c;比如说某个结点是x个字符串的前缀&#xff0c;那么当前结点对答案的贡献为x * (x - 1) / 2&#xff0c;就是x中任选两个字符串组合&#xff0c;因为在这之前&#xff…

Linux提权--本地环境变量文件配合 SUID

免责声明:本文仅做技术交流与学习... 目录 背景: 前提条件: 演示: 实战中如何操作? 探针发现: 背景: 环境变量提权--------> 背景&#xff1a; 管理员编译了程序&#xff0c;给予了程序管理员运行的方案, 攻击通过对程序的运行调试反编译等得到了程序的运行大概逻辑, …

软考-软件工程

软件工程概述 软件工程指的是应用计算机科学、数学及管理科学等原理&#xff0c;以工程化的原则和方法来解决软件 问题的工程&#xff0c;目的是提高软件生产率、提高软件质量、降低软件成本。 概述&#xff1a; 软件开发模型&#xff1a;指导软件开发的体系 需求分析确定软件…

使用Remix部署智能合约到币安链(Remix的操作介绍 币安链合约的部署) 点赞收藏哦

大家好&#xff0c;我是程序员大猩猩呀。 据我所知&#xff0c;很多人进入币圈之后&#xff0c;想要通过炒币一夜暴富&#xff01;另一部分人呢他们希望自己能创建一个项目&#xff0c;然后发行自己的数字货币然后暴富。 不管是什么方式吧&#xff0c;只要不违法&#xff0c;…

原创未发表!24年新算法SBOA优化TVFEMD实现分解+四种熵值+频谱图+参数变化图+相关系数图!

声明&#xff1a;文章是从本人公众号中复制而来&#xff0c;因此&#xff0c;想最新最快了解各类智能优化算法及其改进的朋友&#xff0c;可关注我的公众号&#xff1a;强盛机器学习&#xff0c;不定期会有很多免费代码分享~ 目录 数据介绍 优化流程 创新点 使用TVFEMD的创…

【详细介绍下Visual Studio】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

树莓派、ubuntu低版本python3安装库

如果遇到树莓派中自带低版本python3&#xff0c;又不想额外去安装python3时&#xff0c;可能会遇到版本过低&#xff0c;无法安装库的情况&#xff0c;以下用我实际情况举例解决方案。 本次遇到的问题是树莓派低版本中&#xff0c;python3为3.7.3&#xff0c;需要安装numpy&am…

数据结构与算法学习笔记三---循环队列的表示和实现(C语言)

目录 前言 1.为啥要使用循环队列 2.队列的顺序表示和实现 1.定义 2.初始化 3.销毁 4.清空 5.空队列 6.队列长度 7.获取队头 8.入队 9.出队 10.遍历队列 11.完整代码 前言 本篇博客介绍栈和队列的表示和实现。 1.为啥要使用循环队列 上篇文章中我们知道了顺序队列…

【数据结构】静态链表

静态链表 1.静态链表的结构设计&#xff1a; typedef struct SNode {int data; // 数据int next; //后继指针&#xff08;下标&#xff09; }SNode, SLinkList[MAXSIZE];2.静态链表的结构示意图 0&#xff1a;有效数据链的头结点 1&#xff1a;空闲数据链的头结点 3.静态链表…

重生奇迹mu再生宝石怎么用有什么用

重生奇迹mu再生宝石有2个用处&#xff1a; 1、在玛雅哥布林处给380装备加PVP属性4追4以上的380级装备,守护宝石一颗,再生宝石一颗,成功得到PVP装备,失败宝石消失,装备无变化&#xff1b; 2、给非套装点强化属性用法跟祝福,灵魂,生命一样直接往装备上敲,成功得到随机强化属性一…

【linux软件基础知识】如何使用 run_list 字段将任务放入就绪队列中

在给定的代码片段中,struct task_struct 表示内核中任务或进程的进程控制块 (PCB)。 run_list 字段的类型为 struct list_head,这表明它是链表实现的一部分。 run_list字段在Linux内核中常用来表示任务在调度队列中的位置,例如就绪队列或各种优先级队列。 init_task是一个…

深入理解Java并发:Future与CompletableFuture详解

知识背景&#xff1a; 在工作过程中有用到CompletableFuture&#xff0c;之前接触不多&#xff0c;特此下来学习一下&#xff0c;与大家一起分享&#xff01; 总体介绍&#xff1a; 在多线程编程中&#xff0c;异步计算是一种常见的需求。其中Future和CompletableFuture是处…

SVN 合并到 Git 时有文件大于 100 M 被限制 Push

如果有文件大小大于 100M&#xff0c;GitHub 是会被限制推送到仓库中的&#xff0c;大概率情况会显示下面的错误&#xff1a; remote: Resolving deltas: 100% (3601/3601), done. remote: error: Trace: aea1f450da6f2ef7bfce457c715d0fbb9b0f6d428fdca80233aff34b601ff59b re…

飞书API(8):MySQL 入库定制版本

一、引入 通用版能解决百分之八九十的任务&#xff0c;剩下的部分任务需要进行定制。 先说明通用版本和定制版本有什么不同&#xff0c;通用版本就是只管大的数据类型&#xff0c;将数据处理为对应的类型入库&#xff0c;而定制版本会考虑局部列的数据类型&#xff0c;。举个…

SpringCloud 2023.0.1

本文介绍如何使用 springboot3及cloud2023 进行微服务模块化开发 采用父-module 模块开发 父工程 demo-java pom.xml <!--配置 springboot的依赖的版本号, 方便 module 进行继承--><dependencyManagement><dependencies><!--增加 springboot的依赖--&g…

XXE-lab靶场搭建

源码下载地址 https://github.com/c0ny1/xxe-lab1.php_xxe 直接放在php web页面下即可运行。 2.java_xxe java_xxe是serlvet项目&#xff0c;直接导入eclipse当中即可部署运行。 3.python_xxe: 安装好Flask模块python xxe.py 4.Csharp_xxe 直接导入VS中运行 phpstudy…

第100+7步 ChatGPT文献复现:ARIMA-GRNN预测出血热

基于WIN10的64位系统演示 一、写在前面 这一次&#xff0c;我们来解读ARIMA-GRNN组合模型文章&#xff0c;也是老文章了&#xff1a; 《PLoS One》杂志的2015年一篇题目为《Comparison of Two Hybrid Models for Forecasting the Incidence of Hemorrhagic Fever with Renal…