Rust学习笔记(中)

news2025/1/19 20:34:22

前言

笔记的内容主要参考与《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也能清理

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

中北大学软件学院javaweb实验三JSP+JDBC综合实训(一)__数据库记录的增加、查询

目录 1.实验名称2.实验目的3.实验内容4.实验原理或流程图5.实验过程或源代码&#xff08;一&#xff09;编程实现用户的登录与注册功能【步骤1】建立数据库db_news2024和用户表(笔者使用的数据库软件是navicat)【步骤2】实现用户注册登录功能(与上一实验报告不同的是&#xff0…

LeetCode2215找出两数组的不同

题目描述 给你两个下标从 0 开始的整数数组 nums1 和 nums2 &#xff0c;请你返回一个长度为 2 的列表 answer &#xff0c;其中&#xff1a;answer[0] 是 nums1 中所有 不 存在于 nums2 中的 不同 整数组成的列表。answer[1] 是 nums2 中所有 不 存在于 nums1 中的 不同 整数组…

Kafka基础架构详解

Kafka基础架构 Kafka概述 1. Producer&#xff08;生产者&#xff09;&#xff1a; 生产者是向 Kafka broker 发送消息的客户端。它负责将消息发布到指定的主题&#xff08;Topic&#xff09;&#xff0c;并可以选择将消息发送到特定的分区&#xff08;Partition&#xff09…

vwmare虚拟机迁移磁盘方法

前言 欢迎来到我的博客 个人主页:北岭敲键盘的荒漠猫-CSDN博客 本文整理 虚拟机迁移磁盘的方法 简单方便快上手 当前目标 当前迁移文件: 当前位置&#xff1a; 目的地: e盘虚拟机文件夹 迁移到当前目录。 实际操作 先打开虚拟机的设置&#xff0c;找到这个虚拟机当前的位置…

手机微信备份:防止数据丢失的明智之举

我们通过微信聊天、支付、购物等方式与他人进行交流和互动&#xff0c;而这些聊天记录和文件也成为了我们重要的数据资源。为了防止数据丢失给我们带来的不便和损失&#xff0c;手机微信备份成为了一项非常重要的任务。本文将为您介绍如何有效地备份手机微信数据&#xff0c;确…

windows和 Linux 下通过 QProcess 打开ssh 和vnc

文章目录 SSHSSH验证启动SSH一、口令登录二、公钥登录通过Qprocess 启动ssh VNC Viewer简介通过QProcess启动vncViewer SSH Secure Shell(SSH) 是由 IETF(The Internet Engineering Task Force) 制定的建立在应用层基础上的**安全网络协议**。它是专为远程登录会话(**甚至可以…

centos7安装zabbix-server

zabbixan-server安装 环境安装zabbix安装zabbix配置apachezabbix-UI前端配置修改zabbix为中文语言 环境 准备&#xff1a; centos7系统、mysql数据库/MariaDB数据库 mysql数据库可参照&#xff1a;https://blog.csdn.net/weixin_61367575/article/details/138774428?spm1001.…

网站设计模板简单又好看

在互联网时代&#xff0c;每个企业都需要拥有一个好看又具有吸引力的网站。一个简单却又好看的网站设计模板可以为企业带来许多好处。本文将探讨一些如何设计一个简单又好看的网站模板的技巧。 首先&#xff0c;一个好的网站设计模板应该具备简洁明了的布局。简单的布局能够使用…

有哪些值得买的开放式耳机推荐?2024年开放式运动耳机选购指南

开放式耳机因其独特设计&#xff0c;能在一定程度上保护听力。相较于传统封闭式耳机&#xff0c;开放式设计允许周围环境声音自然流入耳内&#xff0c;降低了耳内共振和声压&#xff0c;减少了耳道的不适感&#xff0c;从而减轻了对听力的潜在损害。对于追求音质与听力保护并重…

项目经理之路:裁员与内卷下的生存策略

作为一名项目经理&#xff0c;身处这个充满挑战与机遇的行业中&#xff0c;今年所面临的裁员潮和内卷化趋势无疑给我的工作带来了前所未有的压力。然而&#xff0c;正是这些压力和挑战&#xff0c;让我们更加深刻地思考了在这个快速变化的时代中&#xff0c;我们项目经理应该如…

【SolidWorks】在零件表面写字、改大小、旋转字的方法

博主在使用SolidWorks建模过程中需要在零件表面写字&#xff0c;并且改变字的大小&#xff0c;必要的时候还要旋转字体&#xff0c;这里就将写字、改字大小、旋转字的方法分享给大家。 1、准备工作。选择要写字的面&#xff0c;并新建草图&#xff0c;在草图模式下编辑。 2、写…

以大开放促进大开发 | 陕西粮农集团携手开源网安引领新时代西部大开发

​5月13日&#xff0c;开源网安与陕西粮农集团成功签署战略合作协议。双方将在网络安全保障体系建设及人才培养领域展开深度合作&#xff0c;共同筑牢陕西省数字经济建设安全屏障。陕西省粮农信息技术有限公司总经理解玮峰、陕西省粮农信息技术有限公司安全事业部负责人马德君、…

银河麒麟V10桌面版分区分析

前言&#xff1a;本文只讨论gpt分区uefi引导形式 &#xff0c;了解分区方案的目的是方便恢复&#xff0c;还原&#xff0c;扩容等&#xff0c;普通用户使用无需了解这些细节。 先回顾分析windows和ubuntu默认分区用做对比 1、windows11默认分区 win11分区&#xff0c;如上图&am…

Linux - make与makefile

文章目录 什么是make和makefile如何使用依赖关系 和 依赖方法伪目标 写个程序-进度条换行和回车的区别 什么是make和makefile make是一个命令 makefile是一个文件 这就是make和makefile的本质 make和 ll , pwd ,su 一样都是命令 makefile和 test &#xff0c; test.c 一样都是…

内存卡惊现0字节!数据丢失怎么办?

在日常使用电子设备的过程中&#xff0c;有时我们会遇到一个令人困惑的问题——内存卡突然变成了0字节。这意味着原本存储在内存卡中的数据似乎在一夜之间消失得无影无踪&#xff0c;给用户带来极大的困扰。本文将详细解析内存卡0字节现象&#xff0c;探究其原因&#xff0c;并…

prompt工程策略(一:使用 CO-STAR 框架来搭建 prompt 的结构)

原文&#xff1a;我是如何赢得GPT-4提示工程大赛冠军的 为了让 LLM 给出最优响应&#xff0c;为 prompt 设置有效的结构至关重要。CO-STAR 框架是一种可以方便用于设计 prompt 结构的模板。该模板考虑了会影响 LLM 响应的有效性和相关性的方方面面&#xff0c;从而有助于得到更…

波卡 2024 一季度报告:XCM 创下历史新高,JAM 链将引领 Polkadot 2.0 新风向

作者&#xff1a;Nicholas Garcia&#xff5c;Messari 研究分析师 编译&#xff1a;OneBlock 原文&#xff1a;https://messari.io/report/state-of-polkadot-q1-2024 近期&#xff0c;Messari Crypto 发布了 Polkadot 2024 年 Q1 状况的数据报告。OneBlock 为你梳理了本篇报…

[自动化]pyautogui的使用

目录 环境 包的版本 前置知识 鼠标控制函数 屏幕与鼠标位置 size() position() OnScreen() 鼠标移动 moveTo() move() 鼠标拖动 dragTo() drag() mouseDown()按下鼠标 mouseUp()松开鼠标 鼠标滚动 scroll() 键盘控制函数 write() press() keyDown()和keyU…

用友系统-U9企业版存在任意文件上传漏洞(新接口)-漏洞挖掘

0x01 产品简介 用友系统-U9 cloud是基于最新应用平台&#xff0c;全新系统架构及开发技术&#xff0c;适配安可要求&#xff0c;融合生态发展&#xff0c;开放标准数据服务接口&#xff0c;并与用友系统-U9 cloud系列产品无缝融合应用&#xff0c;打造场景化、数据化工作驱动的…

简单易懂的 Postman 在线测试方法

在现代软件开发的生命周期中&#xff0c;API 测试扮演着一个不可或缺的角色。Postman 是一个普遍被使用的工具&#xff0c;它可以帮助开发者快速有效地进行接口测试。以下内容是关于如何使用 Postman 的详细指南。 下载并安装 Postman 最初的步骤是安装 Postman 到你的电脑。…