跳至主要內容

Rust圣经小记

ruleeeer原创RustRust大约 26 分钟约 7892 字

最近对 Rust 有点兴趣,于是读了下<<Rust 圣经>>这本书,这里记录了一些基础语法作为笔记.

函数

当用 ! 作函数返回类型的时候,表示该函数永不返回(diverge function),特别的,这种语法往往用做会导致程序崩溃的函数:

fn dead_end() -> ! {
  panic!("你已经到了穷途末路,崩溃吧!");
}

所有权系统

引用类型

Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)

报错,因为 x 的所有权被转移到了y,转移后x已不可用

fn main() {
    let x = String::from("123");
    let y = x;
    println!("{}" , x)
}

正常运行,因为基础数据类型的copy属性

fn main() {
    let x = 2;
    let y = x;
    println!("{}" , x)
}

Copy 类型

copy 类型的数据不会有所有权转移的操作,因为直接分配在栈上,复制非常快,以下类型都是 copy 类型

  1. 所有整数类型,比如 u32
  2. 布尔类型,bool,它的值是 true 和 false
  3. 所有浮点数类型,比如 f64
  4. 字符类型,char
  5. 元组,当且仅当其包含的类型也都是 Copy 的时候。比如,(i32, i32) 是 Copy 的,但 (i32, String) 就不是
  6. 不可变引用 &T ,例如转移所有权中的最后一个例子,但是注意: 可变引用 &mut T 是不可以 Copy 的

借用(&)

引用规定了一块内存区域只能又一个指针持有,这导致一些函数必须要返回数据否则就会导致所有权转移,为此特意推出借用 使用借用时不会导致所有权的转移

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

如果想要修改数据,必须声明为可变类型 (mut)/(&mut) 同一作用域可变引用同时只能存在一个 可变引用与不可变引用不能同时存在

fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

复合类型

切片

let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];

在对字符串使用切片语法时需要格外小心,切片的索引必须落在字符之间的边界位置,也就是 UTF-8 字符的边界,例如中文在 UTF-8 中占用三个字节,下面的代码就会崩溃:


let s = "中国人";
let a = &s[0..2];
println!("{}",a);

因为我们只取 s 字符串的前两个字节,但是本例中每个汉字占用三个字节,因此没有落在边界处,也就是连 中 字都取不完整,此时程序会直接崩溃退出,如果改成 &s[0..3],则可以正常通过编译。 因此,当你需要对字符串做切片索引操作时,需要格外小心这一点

结构体

当结构体中的字段名称和变量名称一致时,可以直接声明

fn build_user(email: String, username: String) -> User {
    User {
    // 实际上应该是 emial:email
        email,
        username,
        active: true,
        sign_in_count: 1,
    }
}

当需要从另一个结构体中复制相同的字段时,使用..语法可以直接讲其余字段复制过去,当需要注意的是该种写法会导致所有权转移,也就是 User.username 已经不再可用,当其他字段依然能使用,因为是 copy 类型,会直接复制一份,String 类型在堆上,无法复制

let user1 = {
    email:String::from("11"),
    ... user2
}

结构体必须要有名称,但是结构体的字段可以没有名称,这种结构体长得很像元组,因此被称为元组结构体,例如:

    struct Color(i32, i32, i32);
    struct Point(i32, i32, i32);
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);

枚举

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {
    let m1 = Message::Quit;
    let m2 = Message::Move{x:1,y:1};
    let m3 = Message::ChangeColor(255,255,0);
}

该枚举类型代表一条消息,它包含四个不同的成员:

Quit 没有任何关联数据 Move 包含一个匿名结构体 Write 包含一个 String 字符串 ChangeColor 包含三个 i32 当然,我们也可以用结构体的方式来定义这些消息:


#![allow(unused)]
fn main() {
struct QuitMessage; // 单元结构体
struct MoveMessage {
    x: i32,
    y: i32,
}
struct WriteMessage(String); // 元组结构体
struct ChangeColorMessage(i32, i32, i32); // 元组结构体
}


Optional 是rust中用来替代null的特殊枚举


enum Option<T> {
    Some(T),
    None,
}

数组

在日常开发中,使用最广的数据结构之一就是数组,在 Rust 中,最常用的数组有两种,第一种是速度很快但是长度固定的 array,第二种是可动态增长的但是有性能损耗的 Vector,更多的情况下,我们称 array 为数组,Vector 为动态数组。

不知道你们发现没,这两个数组的关系跟 &str 与 String 的关系很像,前者是长度固定的字符串切片,后者是可动态增长的字符串。其实,在 Rust 中无论是 String 还是 Vector,它们都是 Rust 的高级类型:集合类型,在后面章节会有详细介绍。

// i32表示类型,5表示长度
let a: [i32; 5] = [1, 2, 3, 4, 5];
// 等同于 let a = [3, 3, 3, 3, 3];
let a = [3; 5];

但是如果数组元素为非基本类型,这样就会出现问题

// 编译报错,因为不知道如何复制String类型
let str_array = [String::from("1");5]
// 使用下面这种写法
let array: [String; 8] = core::array::from_fn(|i| String::from("rust is good!"));

}

解构语法

解构语法主要用于快速赋值变量的场景,例如对数组,切片,元组等数据类型

// 元组
let (a,b,c) = tunlp;

// 数组
let [a,b,c] = arr;

// 切片,注意切片因为无法在编译期确定长度,所以需要使用if let语法判断是否满足长度要求
let &[a,b,c]  = slice {
	pringln!("{}" , a);
} else {
	println!("{}" , "ERR , slice is not three length");
}

调试

比较常见的调试就是使用#[derive(Debug)]dbg()了,注意dbg()函数会拿走所有权,而且println!输出到stdoutdbg()输出到stderr

fn main() {
    let user = User{
        username:String::from("123"),
        email:String::from("456"),
        active:true
    };
    println!("{:#?}" , user);
    dbg!(user);
    ();
}


#[derive(Debug)]
struct User {
    username: String,
    email: String,
    active: bool,
}

流程控制

for

fn main() {
    for i in 1..=5 {
        println!("{}", i);
    }
}

以上代码循环输出一个从 1 到 5 的序列,简单粗暴,核心就在于 for 和 in 的联动,语义表达如下:

如果想在循环中获取元素的索引:

fn main() {
    let a = [4, 3, 2, 1];
    // `.iter()` 方法把 `a` 数组变成一个迭代器
    for (i, v) in a.iter().enumerate() {
        println!("第{}个元素是{}", i + 1, v);
    }
}

如果我们想用 for 循环控制某个过程执行 10 次,但是又不想单独声明一个变量来控制这个流程,该怎么写?

for _ in 0..10 {
  // ...
}

while

fn main() {
    let mut n = 0;

    while n <= 5  {
        println!("{}!", n);

        n = n + 1;
    }

    println!("我出来了!");
}

loop

对于循环而言,loop 循环毋庸置疑,是适用面最高的,它可以适用于所有循环场景(虽然能用,但是在很多场景下, for 和 while 才是最优选择),因为 loop 就是一个简单的无限循环,你可以在内部实现逻辑通过 break 关键字来控制循环何时结束。

使用 loop 循环一定要打起精神,否则你会写出下面的跑满你一个 CPU 核心的疯子代码:

fn main() {
    loop {
        println!("again!");
    }
}

这里有几点值得注意:

break 可以单独使用,也可以带一个返回值,有些类似 return loop 是一个表达式,因此可以返回一个值

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {}", result);
}

迭代器

实际上 rust 的 for..in 语法并不是使用索引访问元素的,使用的是迭代器,当使用数组时,会直接将数组转化为可迭代类型,然后通过迭代器访问

into_iter, iter, iter_mut

在之前的代码中,我们统一使用了 into_iter 的方式将数组转化为迭代器,除此之外,还有 iteriter_mut,聪明的读者应该大概能猜到这三者的区别:

  • into_iter 会夺走所有权
  • iter 是借用
  • iter_mut 是可变借用
fn main() {
    let values = vec![1, 2, 3];

    for v in values.into_iter() {
        println!("{}", v)
    }

    // 下面的代码将报错,因为 values 的所有权在上面 `for` 循环中已经被转移走
    // println!("{:?}",values);

    let values = vec![1, 2, 3];
    let values_iter = values.iter();

    // 不会报错,因为 values_iter 只是借用了 values 中的元素
    println!("{:?}", values);

    let mut values = vec![1, 2, 3];
    // 对 values 中的元素进行可变借用
    let mut values_iter_mut = values.iter_mut();

    // 取出第一个元素,并修改为0
    if let Some(v) = values_iter_mut.next() {
        *v = 0;
    }

    // 输出[0, 2, 3]
    println!("{:?}", values);
}

  • .iter() 方法实现的迭代器,调用 next 方法返回的类型是 Some(&T)
  • .iter_mut() 方法实现的迭代器,调用 next 方法返回的类型是 Some(&mut T),因此在 if let Some(v) = values_iter_mut.next() 中,v 的类型是 &mut i32,最终我们可以通过 *v = 0 的方式修改其值

Iterator 和 IntoIterator 的区别

这两个其实还蛮容易搞混的,但我们只需要记住,Iterator 就是迭代器特征,只有实现了它才能称为迭代器,才能调用 next

IntoIterator 强调的是某一个类型如果实现了该特征,它可以通过 into_iteriter 等方法变成一个迭代器。

模式匹配

在 Rust 中,模式匹配最常用的就是 matchif let,本章节将对两者及相关的概念进行详尽介绍。

match

先来看一个关于 match 的简单例子:

enum Direction {
    East,
    West,
    North,
    South,
}

fn main() {
    let dire = Direction::South;
    match dire {
        Direction::East => println!("East"),
        Direction::North | Direction::South => {
            println!("South or North");
        },
        _ => println!("West"),
    };
}

这里我们想去匹配 dire 对应的枚举类型,因此在 match 中用三个匹配分支来完全覆盖枚举变量 Direction 的所有成员类型,有以下几点值得注意:

match 的匹配必须要穷举出所有可能,因此这里用 _ 来代表未列出的所有可能性 match 的每一个分支都必须是一个表达式,且所有分支的表达式最终返回值的类型必须相同 X | Y,类似逻辑运算符 或,代表该分支可以匹配 X 也可以匹配 Y,只要满足一个即可 其实 match 跟其他语言中的 switch 非常像,_ 类似于 switch 中的 default。

另外注意,match 本身是一个表达式,可以用来赋值

enum IpAddr {
   Ipv4,
   Ipv6
}

fn main() {
    let ip1 = IpAddr::Ipv6;
    let ip_str = match ip1 {
        IpAddr::Ipv4 => "127.0.0.1",
        _ => "::1",
    };

    println!("{}", ip_str);
}
enum Action {
    Say(String),
    MoveTo(i32, i32),
    ChangeColorRGB(u16, u16, u16),
}

fn main() {
    let actions = [
        Action::Say("Hello Rust".to_string()),
        Action::MoveTo(1,2),
        Action::ChangeColorRGB(255,255,0),
    ];
    for action in actions {
        match action {
            Action::Say(s) => {
                println!("{}", s);
            },
            Action::MoveTo(x, y) => {
                println!("point from (0, 0) move to ({}, {})", x, y);
            },
            Action::ChangeColorRGB(r, g, _) => {
                println!("change color into '(r:{}, g:{}, b:0)', 'b' has been ignored",
                    r, g,
                );
            }
        }
    }
}

if let

当你只要匹配一个条件,且忽略其他条件时就用 if let ,否则都用 match。

  let v = Some(3u8);
    match v {
        Some(3) => println!("three"),
        _ => (),
    }

等同与

if let Some(3) = v {
    println!("three");
}

matches!宏

enum MyEnum {
    Foo,
    Bar
}

fn main() {
    let v = vec![MyEnum::Foo,MyEnum::Bar,MyEnum::Foo];
}

v.iter().filter(|x| x == MyEnum::Foo);

以上语法会报错,因为 x 无法直接与枚举类型比较,当然可以直接使用 matches,但是语法会过于复杂,好在有更简便的方式

v.iter().filter(|x| matches!(x , MyEnum::Foo));

还有更多实用的例子


#![allow(unused)]
fn main() {
let foo = 'f';
assert!(matches!(foo, 'A'..='Z' | 'a'..='z'));

let bar = Some(4);
assert!(matches!(bar, Some(x) if x > 2));
}

变量覆盖

无论是 match 还是 if let,他们都可以在模式匹配时覆盖掉老的值,绑定新的值:

fn main() {
   let age = Some(30);
   println!("在匹配前,age是{:?}",age);
   if let Some(age) = age {
       println!("匹配出来的age是{}",age);
   }

   println!("在匹配后,age是{:?}",age);
}

运行结果为

在匹配前,age是Some(30)
匹配出来的age是30
在匹配后,age是Some(30)

matches 也是如此

fn main() {
   let age = Some(30);
   println!("在匹配前,age是{:?}",age);
   match age {
       Some(age) =>  println!("匹配出来的age是{}",age),
       _ => ()
   }
   println!("在匹配后,age是{:?}",age);
}

方法

Rust 中结构数据和行为(方法)是分离的,这和 java 中的类概念有些不同 关联函数使用::引用,例如String::from("1")


#![allow(unused)]
fn main() {
struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}

impl Circle {
    // new是Circle的关联函数,因为它的第一个参数不是self,且new并不是关键字
    // 这种方法往往用于初始化当前结构体的实例
    fn new(x: f64, y: f64, radius: f64) -> Circle {
        Circle {
            x: x,
            y: y,
            radius: radius,
        }
    }

    // Circle的方法,&self表示借用当前的Circle结构体
    fn area(&self) -> f64 {
        std::f64::consts::PI * (self.radius * self.radius)
    }
}
}

像上文中的 new()实际上就是一个关联函数,他处于 impl Circle 中,但是第一个参数不是 self(&self,mut)等,这类函数称为关联函数,关联函数的调用方式需要使用::,例如 String::from("123")

泛型和特征

泛型

rust 中不仅有类型泛型,还有值泛型,例如下面 T 就是类型泛型,而 N 是值泛型,它的声明方式和常规泛型不同

fn display_array<T: std::fmt::Debug, const N: usize>(arr: [T; N]) {
    println!("{:?}", arr);
}
fn main() {
    let arr: [i32; 3] = [1, 2, 3];
    display_array(arr);

    let arr: [i32; 2] = [1, 2];
    display_array(arr);
}

特征

特征的概念其实就是接口的概念,表示某一组固定的行为

pub trait Summary {
    fn summarize(&self) -> String;
}

#![allow(unused)]
fn main() {
pub trait Summary {
    fn summarize(&self) -> String;
}
pub struct Post {
    pub title: String, // 标题
    pub author: String, // 作者
    pub content: String, // 内容
}

impl Summary for Post {
    fn summarize(&self) -> String {
        format!("文章{}, 作者是{}", self.title, self.author)
    }
}

pub struct Weibo {
    pub username: String,
    pub content: String
}

impl Summary for Weibo {
    fn summarize(&self) -> String {
        format!("{}发表了微博{}", self.username, self.content)
    }
}
}

注意特征的定义是有特殊的作用域的

如果你想要为类型 A 实现特征 T ,那么 A 或者 T 至少有一个是在当前作用域中定义的 例如我们可以为上面的 Post 类型实现标准库中的 Display 特征,这是因为 Post 类型定义在当前的作用域中。同时,我们也可以在当前包中为 String 类型实现 Summary 特征,因为 Summary 定义在当前作用域中。

但是你无法在当前作用域中,为 String 类型实现 Display 特征,因为它们俩都定义在标准库中,其定义所在的位置都不在当前作用域,跟你半毛钱关系都没有,看看就行了。

该规则被称为孤儿规则,可以确保其它人编写的代码不会破坏你的代码,也确保了你不会莫名其妙就破坏了风马牛不相及的代码。

特征作为函数参数


#![allow(unused)]
fn main() {
pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}
}

特征约束


#![allow(unused)]
fn main() {
pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}
}

如以下代码只能表示两个实现了特征的对象,但是无法保证二者一致


#![allow(unused)]
fn main() {
pub fn notify(item1: &impl Summary, item2: &impl Summary) {}
}

如果需要保持一致需要使用下面的语法


#![allow(unused)]
fn main() {
pub fn notify<T: Summary>(item1: &T, item2: &T) {}
}

当需要保持多个约束时,使用


#![allow(unused)]
fn main() {
pub fn notify(item: &(impl Summary + Display)) {}
}


#![allow(unused)]
fn main() {
pub fn notify<T: Summary + Display>(item: &T) {}
}

使用关联类型简化泛型


#![allow(unused)]
fn main() {
pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        // --snip--
    }
}

fn main() {
    let c = Counter{..}
    c.next()
}

在上述代码中,我们为 Counter 类型实现了 Iterator 特征,变量 c 是特征 Iterator 的实例,也是 next 方法的调用者。 结合之前的黑体内容可以得出:对于 next 方法而言,Self 是调用者 c 的具体类型: Counter,而 Self::ItemCounter 中定义的 Item 类型: u32

为什么不使用泛型?

使用泛型可能会得到如下代码


#![allow(unused)]
fn main() {
trait Container<A,B> {
    fn contains(&self,a: A,b: B) -> bool;
}

fn difference<A,B,C>(container: &C) -> i32
  where
    C : Container<A,B> {...}
}

而使用关联类型就方便很多


#![allow(unused)]
fn main() {
trait Container{
    type A;
    type B;
    fn contains(&self, a: &Self::A, b: &Self::B) -> bool;
}

fn difference<C: Container>(container: &C) {}
}

调用同名方法


#![allow(unused)]
fn main() {
trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}
}

如下使用时,优先调用结构体上定义的方法

fn main() {
    let person = Human;
    person.fly();
}

如果需要调用特征的方法,需要手动指定特征

fn main() {
    let person = Human;
    Pilot::fly(&person); // 调用Pilot特征上的方法
    Wizard::fly(&person); // 调用Wizard特征上的方法
    person.fly(); // 调用Human类型自身的方法
}

生命周期

一个简单的比较字符串长度的函数

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

一旦执行,就会出现以下问题

error[E0106]: missing lifetime specifier
 --> src/main.rs:9:33
  |
9 | fn longest(x: &str, y: &str) -> &str {
  |               ----     ----     ^ expected named lifetime parameter // 参数需要一个生命周期
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is
  borrowed from `x` or `y`
  = 帮助: 该函数的返回值是一个引用类型,但是函数签名无法说明,该引用是借用自 `x` 还是 `y`
help: consider introducing a named lifetime parameter // 考虑引入一个生命周期
  |
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ^^^^    ^^^^^^^     ^^^^^^^     ^^^

其实主要是编译器无法知道该函数的返回值到底引用 x还是 y ,因为编译器需要知道这些,来确保函数调用后的引用生命周期分析。

在先简单记住,标记的生命周期只是为了取悦编译器,让编译器不要难为我们,记住了吗?没记住,再回头看一遍,这对未来你遇到生命周期问题时会有很大的帮助!

例如一个变量,只能活一个花括号,那么就算你给它标注一个活全局的生命周期,它还是会在前面的花括号结束处被释放掉,并不会真的全局存活。


&i32        // 一个引用
&'a i32     // 具有显式生命周期的引用
&'a mut i32 // 具有显式生命周期的可变引用

一个生命周期标注,它自身并不具有什么意义,因为生命周期的作用就是告诉编译器多个引用之间的关系。例如,有一个函数,它的第一个参数 first 是一个指向 i32 类型的引用,具有生命周期 'a,该函数还有另一个参数 second,它也是指向 i32 类型的引用,并且同样具有生命周期 'a。此处生命周期标注仅仅说明,这两个参数 firstsecond 至少活得和'a 一样久,至于到底活多久或者哪个活得更久,抱歉我们都无法得知:

fn useless<'a>(first: &'a i32, second: &'a i32) {}

需要注意的点如下:

  • 和泛型一样,使用生命周期参数,需要先声明 <'a>
  • xy 和返回值至少活得和 'a 一样久(因为返回值要么是 x,要么是 y)

该函数签名表明对于某些生命周期 'a,函数的两个参数都至少跟 'a 活得一样久,同时函数的返回引用也至少跟 'a 活得一样久。实际上,这意味着返回值的生命周期与参数生命周期中的较小值一致:虽然两个参数的生命周期都是标注了 'a,但是实际上这两个参数的真实生命周期可能是不一样的(生命周期 'a不代表生命周期等于 '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);
}

上述代码依然会报错,因为 result 必须活到 println 位置,result 的生命周期是 a,同时参数 stirng1 和 string2 的生命周期一样是 a,这是矛盾的,string2 根本活不到 pringln 处,所以编译报错

函数的返回值如果是一个引用类型,那么它的生命周期只会来源于:

  • 函数参数的生命周期
  • 函数体中某个新建引用的生命周期

若是后者情况,就是典型的悬垂引用场景:


#![allow(unused)]
fn main() {
fn longest<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    result.as_str()
}
}

主要问题就在于,result 在函数结束后就被释放,但是在函数结束后,对 result 的引用依然在继续。在这种情况下,没有办法指定合适的生命周期来让编译通过,因此我们也就在 Rust 中避免了悬垂引用。

那遇到这种情况该怎么办?最好的办法就是返回内部字符串的所有权,然后把字符串的所有权转移给调用者:

fn longest<'a>(_x: &str, _y: &str) -> String {
    String::from("really long string")
}

fn main() {
   let s = longest("not", "important");
}

至此,可以对生命周期进行下总结:生命周期语法用来将函数的多个引用参数和返回值的作用域关联到一起,一旦关联到一起后,Rust 就拥有充分的信息来确保我们的操作是内存安全的。

结构体生命周期

struct ImportantExcerpt<'a> {
    part: &'a str,
}

该生命周期标注说明,结构体 ImportantExcerpt 所引用的字符串 str 必须比该结构体活得更久。

下面的示例掩饰了错误的用法,结构体中的借用 first_sentence 的生命周期比结构体更小,所以编译报错

#[derive(Debug)]
struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    let i;
    {
        let novel = String::from("Call me Ishmael. Some years ago...");
        let first_sentence = novel.split('.').next().expect("Could not find a '.'");
        i = ImportantExcerpt {
            part: first_sentence,
        };
    }
    println!("{:?}",i);
}

生命周期消除

并不是所有的函数携带借用类型时都需要标注生命周期,例如一下场景

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[..]
}

这是因为函数标注的生命周期只存在两种场景

  • 从参数获取
  • 从函数体内部新创建的变量获取

如果是后者就会被拒绝编译,前者只有一个参数且返回的情况下可以保证返回值的生命周期和参数的生命周期时一致的,所以无需特别声明

函数或者方法中,参数的生命周期被称为 输入生命周期,返回值的生命周期被称为 输出生命周期

  1. 每一个引用参数都会获得独自的生命周期

    例如一个引用参数的函数就有一个生命周期标注: fn foo<'a>(x: &'a i32),两个引用参数的有两个生命周期标注:fn foo<'a, 'b>(x: &'a i32, y: &'b i32), 依此类推。

  2. 若只有一个输入生命周期(函数参数中只有一个引用类型),那么该生命周期会被赋给所有的输出生命周期,也就是所有返回值的生命周期都等于该输入生命周期

    例如函数 fn foo(x: &i32) -> &i32x 参数的生命周期会被自动赋给返回值 &i32,因此该函数等同于 fn foo<'a>(x: &'a i32) -> &'a i32

  3. 若存在多个输入生命周期,且其中一个是 &self&mut self,则 &self 的生命周期被赋给所有的输出生命周期

    拥有 &self 形式的参数,说明该函数是一个 方法,该规则让方法的使用便利度大幅提升。

生命周期约束

impl<'a: 'b, 'b> ImportantExcerpt<'a> {
    fn announce_and_return_part(&'a self, announcement: &'b str) -> &'b str {
        println!("Attention please: {}", announcement);
        self.part
    }
}
  • 'a: 'b,是生命周期约束语法,跟泛型约束非常相似,用于说明 'a 必须比 'b 活得久
  • 可以把 'a'b 都在同一个地方声明(如上),或者分开声明但通过 where 'a: 'b 约束生命周期关系,如下:
impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part<'b>(&'a self, announcement: &'b str) -> &'b str
    where
        'a: 'b,
    {
        println!("Attention please: {}", announcement);
        self.part
    }
}

总之,实现方法比想象中简单:加一个约束,就能暗示编译器,尽管引用吧,反正我想引用的内容比我活得久,爱咋咋地,我怎么都不会引用到无效的内容!

静态生命周期

let s: &'static str = "我没啥优点,就是活得久,嘿嘿";
  • 生命周期 'static 意味着能和程序活得一样久,例如字符串字面量和特征对象
  • 实在遇到解决不了的生命周期标注问题,可以尝试 T: 'static,有时候它会给你奇迹

错误处理

最常见的错误处理就是 panic!了,但是不可能所有的都用 panic!,因为有些错误是可以被容忍的,程序遇到panic时会直接错误退出,对于可容忍的错误尽量使用


#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    // 打开文件,f是`Result<文件句柄,io::Error>`
    let f = File::open("hello.txt");

    let mut f = match f {
        // 打开文件成功,将file句柄赋值给f
        Ok(file) => file,
        // 打开文件失败,将错误返回(向上传播)
        Err(e) => return Err(e),
    };
    // 创建动态字符串s
    let mut s = String::new();
    // 从f文件句柄读取数据并写入s中
    match f.read_to_string(&mut s) {
        // 读取成功,返回Ok封装的字符串
        Ok(_) => Ok(s),
        // 将错误向上传播
        Err(e) => Err(e),
    }
}
}

上述是完整的错误处理流程, 但是稍嫌啰嗦,可以使用更简便的?宏来搞定 在 Rust 中,?  宏是一个用于简化错误处理的语法糖。它可以用于在函数中快速返回错误,而不需要显式地编写错误处理代码。

当在函数中调用一个可能会返回错误的函数时,可以在函数调用后加上  ?,这将自动将错误传播到调用函数的调用者。如果函数返回一个  Result  类型的错误,那么  ?  宏将自动将错误返回给调用者,而不需要显式地编写错误处理代码。


#![allow(unused)]
fn main() {
use std::fs::File;
use std::io;
use std::io::Read;

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

在精简一些


#![allow(unused)]
fn main() {
use std::fs::File;
use std::io;
use std::io::Read;

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

闭包

闭包对内存的影响

当闭包从环境中捕获一个值时,会分配内存去存储这些值。对于有些场景来说,这种额外的内存分配会成为一种负担。与之相比,函数就不会去捕获这些环境值,因此定义和使用函数不会拥有这种内存负担。

三种闭包函数

FnOnec

  1. FnOnce,该类型的闭包会拿走被捕获变量的所有权。Once 顾名思义,说明该闭包只能运行一次:
fn fn_once<F>(func: F)
where
// 这里面有一个很重要的提示,因为 F 没有实现 Copy 特征,所以会报错,那么我们添加一个约束,试试实现了 Copy 的闭包:
    F: FnOnce(usize) -> bool+Copy,
{
    println!("{}", func(3));
    println!("{}", func(4));
}

fn main() {
    let x = vec![1, 2, 3];
    fn_once(|z|{z == x.len()})
}

如果你想强制闭包取得捕获变量的所有权,可以在参数列表前添加 move 关键字,这种用法通常用于闭包的生命周期大于捕获变量的生命周期时,例如将闭包返回或移入其他线程。

use std::thread;
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
    println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();

FnMut

可变借用

fn main() {
    let mut s = String::new();

    let mut update_string =  |str| s.push_str(str);
    update_string("hello");

    println!("{:?}",s);
}

Fn

不可变借用

fn main() {
    let s = "hello, ".to_string();
    let update_string =  |str| println!("{},{}",s,str);
    exec(update_string);
    println!("{:?}",s);
}

fn exec<'a, F: Fn(String) -> ()>(f: F)  {
    f("world".to_string())
}

智能指针

Box<T>

Box<T>类型的作用是将内容分配在堆上而非栈上,主要是用来解决循环引用,无限嵌套类型的问题,因为rust需要准确的直到数据所占的大小,但是无限嵌套类型无法准确的得知大小,所以可以将其分配在堆上,在栈中仅保存固定大小的指针即可,如下就是一个循环链表的结构体.

struct CircularLinkedList<T> {
    head: Option<Box<Node<T>>>,
}

struct Node<T> {
    data: T,
    next: Option<Box<Node<T>>>,
}

Cow<'a,T>

Cow类型其实是(Copy On Write)的缩写,表示在需要修改时复制数据,其他情况不会存在复制情况的发生,主要是为了提高系统的性能 例如下面的用于替换String中敏感词的函数,原本的方法中remove_sensitive_word_old()无论是否需要修改,都会进行一次复制操作,但是新函数remove_senstive_word()只有在需要修改的情况下才会进行复制,无休改将会直接返回引用数据.

fn remove_sensitive_word<'a>(words: &'a str) -> Cow<'a, str> {
     if words.contains(SENSITIVE_WORD) {
         Cow::Owned(words.replace(SENSITIVE_WORD, ""))
     } else {
         Cow::Borrowed(words)
    }
}

fn remove_sensitive_word_old(words: &str) -> String {
    if words.contains(SENSITIVE_WORD) {
        words.replace(SENSITIVE_WORD, "")
           } else {
        words.to_owned()
    }
}

RC<T>

RC类型其实就是引用计数器,每有一个引用就会讲引用计数器+1,每减少一个引用就会讲引用计数器-1,如果引用计数器为 0,该部分数据会自动销毁.

为什么不直接使用引用而需要使用 RC 智能指针?

在 Rust 中,引用是一种非常有用的类型,它允许我们在不拥有值的情况下对其进行操作。但是,引用有一个限制,即它们必须在其生命周期内保持有效。这意味着如果我们想要在多个地方共享同一个值,我们需要确保每个引用都在其生命周期内保持有效。这可能会变得非常困难,特别是在复杂的代码中。

相比之下,Rc  类型允许我们在多个地方共享同一个值,而不必担心生命周期的问题。Rc  使用引用计数来跟踪值的使用情况,并在没有任何引用时自动释放值。这使得在 Rust 中使用  Rc  变得非常方便和安全

下面展示一个使用 RC 实现家庭成员共享的例子

use std::rc::Rc;

struct Father {
    name: String,
}

struct Mother {
    name: String,
}

struct Child {
    name: String,
    father: Rc<Father>,
    mother: Rc<Mother>,
}

fn main() {
    let father = Rc::new(Father { name: String::from("张三") });
    let mother = Rc::new(Mother { name: String::from("李四") });

    let child1 = Child {
        name: String::from("小明"),
        father: Rc::clone(&father),
        mother: Rc::clone(&mother),
    };

    let child2 = Child {
        name: String::from("小红"),
        father: Rc::clone(&father),
        mother: Rc::clone(&mother),
    };

    println!("{} 的父亲是 {},母亲是 {}", child1.name, child1.father.name, child1.mother.name);
    println!("{} 的父亲是 {},母亲是 {}", child2.name, child2.father.name, child2.mother.name);
}

上次编辑于:
贡献者: ruleeeer