Rust 语言基础学习

yang - 2023-04-06

rust notes

NOTE: 有的人可能开了 rustlings 的 lsp 功能,但是仍然没有智能提示/补全功能,这可能是因为 rust-analyzer 插件更新后和当前的 rustlings 不兼容导致的,可以将 rust-analyzer 插件的版本回退,暂时解决此问题:

很多概念看一遍记不住,在学和练的过程中经常要回头看之前的章节才能想起来。

Rust 的文档和教程中的代码片段都可以在线运行,每一个都可以运行看一下结果,加深印象和理解。

2023-03-27 类型系统、流程控制、模式匹配等基本概念

Rust 中使用所有权机制和可变性对变量的操作进行限制,这是它的一大特点。使用时要经常注意变量的所有权转移问题,以及变量在一个作用域内只能有一个可变引用多个不可变引用

Rust 中使用 match 进行匹配实现多分支的情况。match 的匹配需要列出所有匹配的情况,如匹配一个 u8 类型的数字可能就要列出 0~255 个情况。不过我们可以使用 _ 进行通配,直接匹配所有没有列出的情形。(注意只在必要的时候使用,不然可能因为忘了处理特殊的值而出现逻辑 BUG。)

match 还可以使用 guard 形式(不知道怎么翻译,看守?)进行条件约束的匹配,如下:

match value {
    x if x < 0 => println!("Negative number"),
    x if x == 0 => println!("Zero number"),
    x => println!("Positive number"),
}

2023-03-29 集合类型 HashMap

HashMap 练习主要用到了 entry 方法获取对应的值的访问入口 (Entry),再通过 Entry 的 or_insert 方法进行无键时的 KV 插入。使用 Entry 的 and_modify 方法可以进行有键时的值更新。

Rust 的函数很多可以链式调用,但使用时要注意各种方法的返回类型,必要的时候要调整调用顺序。

在做 rustlings 练习时用到了 entry 方法,练习中 HashMap 的键是一个 String 变量,传入到 entry 方法时会转移它的所有权,再链式调用 or_insert 的时候里面用到这个键就因为所有权被转移而报错,所以我使用了 clone 方法。(用引用形式(变成了字符串切片)会报类型不匹配的错误)不知道是不是我写的有问题,有没有更好的方法?

2023-03-30 泛型、枚举和 Options, Result

模式匹配的时候可以使用 ref 修饰解构的变量,表示希望对这个变量进行引用而不转移所有权。这种方式和 & 不同,& 表示的是要匹配一个引用类型变量。

例如下面这段代码,如果 Some(ref p) 改成了 Some(p) 会导致 Option 变量 y 中的值所有权被转移到 p,如果改成了 Some(&p) 类型又匹配不上。

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let y: Option<Point> = Some(Point { x: 100, y: 200 });

    match y {
        Some(ref p) => println!("Co-ordinates are {},{} ", &p.x, &p.y),
        _ => println!("no match"),
    }
    y; // Fix without deleting this line.
}

2023-03-31 错误处理

相关链接: Result 类 Option 类

Rust 的错误处理具有自己的特色。主要使用 panic!(不可恢复错误) 和 Result (可恢复错误)进行处理。

panic! 使程序直接结束并输出崩溃信息,可以在遇到程序无法自己进行处理的错误或者不能继续正常运行时(如系统异常或者无法获取到后续运行需要的资源等情况)使用。

Result<T, E> 是一个泛型的枚举类型,函数正常处理时可解构出结果,处理失败时可解构出错误信息。Result 的 unwrap 方法可以简单粗暴的解包 Ok 的结果,但是当其是 Err 时也简单粗暴地 panic 了。如果能确定得到的 Result 一定是 Ok,那就能愉快地用 unwrap。类似的还有 expect 方法,区别只是你可以自定义 panic 的信息。

Result<T, E> 和 Option 很相似,只不过 Option 是用于有没有值的包装(None 的情况不会包含额外的数据),Result<T, E> 是对函数执行情况的包装(Ok(T) 成功时的结果/Err(E) 失败时的错误信息)。

有时候我们只想在本函数内完成正常的功能,不想直接处理错误,而是将错误时的情况传递给更上层的函数。这时就可以在匹配到 Err 的时候给它 return 出去(这个函数的返回值类型也得是个 Result 类型)。不过 Rust 给我们提供了一个更方便的符号 ? 就相当于一次对 Result 或 Option 的模式匹配,当结果是 Ok / Some 时解构出其中的值,当结果是 Err / None 时,会将其返回出去。 如下面的函数,当 open 和 read_to_string 都获得了正常的结果时会运行到最后返回 Ok(s),如果前面出现 Err 就会直接将错误返回。

use std::fs;
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)?;
	// 上面两行也可以直接像这样进行链式调用,效果相同
	// File::open("hello.txt")?.read_to_string(&mut s)?;
    Ok(s)
	// 或者上面几行用下面这行标准库的函数直接完成
	// fs::read_to_string("hello.txt")
}

注意 ? 匹配出 Ok 或 Some 是直接解构出其中的值的,所以这个值是不能作为返回 Resut / Option 类型的函数的返回值的,而是要再包装一下才行。如下面的这个函数就会报错

fn first(arr: &[i32]) -> Option<&i32> {
    arr.get(0)?
    // 需要写成下面这样才行
    // let v = arr.get(0)?;
    // Some(v)
}

2023-04-01 泛型练习、特征、生命周期和自动化测试

泛型

使用泛型需要提前声明,结构体、枚举等的泛型声明在其名字后面,为泛型实现方法时,还要在 impl 后面声明泛型类型。

struct Point<T, U> {
    x: T,
    y: U,
}
impl<T, U> Point<T, U> {
    fn x(&self) -> &T {
        &self.x
    }
	fn y(&self) -> &U {
        &self.y
    }
    fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}
impl<T> Point<T, T> {
	fn foo(&self) {
        println!("x: {} and y: {} have same type.", self.x, self.y);
    }
}
impl Point<i32, String> {
	fn bar(&self) {
		println!("x: {} is an integer, and y: {} is a string.", self.x, self.y);
	}
}

上面的 mixup<V, W> 是泛型类型 Point<T, U> 的泛型方法,要在方法名后面声明用于 other 参数的泛型类型。 像上面的示例一样,也可以为特定的类型组合实现特定的方法。当类型匹配时就可以调用对应的方法。

另外有一个 const 泛型,可以将值作为泛型声明,根据值的不同也会生成不同的类型/函数。声明的方式是:

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

const 泛型声明时要指定类型。上面这个例子里如果不使用泛型方法是没法传递数组进去的,因为 3 个元素和 2 个元素的数组是不同的类型。

特征

Rust 中的特征 (Trait) 可以为不同的类型实现相同的行为,或者为已有的类型实现拓展的方法。使用 trait 关键字声明一个特征,它内部的方法可以写实现(默认实现),也可以不写实现。如下:

trait MyTrait {
    fn foo(&self) {
        println!("A default function.")
    }
    fn bar(&self) -> &str;
}
struct MyStruct {
    info: String,
}
// 为类型实现特征,和为类型实现方法很相似,但是多了特征名和 for
impl MyTrait for MyStruct {
    fn bar(&self) -> &str {
        return &self.info;
    }
}
// 限定函数参数和返回值是实现了特征的类型
fn zap(v: &impl MyTrait) -> impl MyTrait {
// 也可以写成这样的形式
// fn zap<T: MyTrait>(v: &T) -> impl MyTrait {
// 或这样的形式
// fn zap<T>(v: &T) -> impl MyTrait
//     where T: MyTrait {
    v.foo();
    MyStruct {
        info: "foo bar zap".into()
    }
}

像上面的例子一样,特征也可以用来指定函数或返回值是实现了某些特征的类型,而不关系具体的类型是什么。

生命周期

生命周期是一种特殊的泛型标注,用来提示编译器标注到的引用的生命周期长度。其使用方式如下:

// 函数参数和返回值的生命周期标注(参数和返回值的生命周期可以不同,用不同的名字标注就行)
// 注意,和泛型声明一样,生命周期也要事先声明(函数名、结构名和 impl 后面<'lifetime>)
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
// 结构体内引用字段的生命周期标注
struct ImportantExcerpt<'a> {
    part: &'a str,
}
// 实现方法时也要标注出生命周期(和泛型声明一样)
impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}
// 可以用这样的形式表示 'a 被 'b 引用,即 'a 生命周期大于等于 'b
// 类似于泛型的特征约束
impl<'a: 'b, 'b> ImportantExcerpt<'a> {
    fn announce_and_return_part(&'a self, announcement: &'b str) -> &'b 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,
    };
}

生命周期标注并不会改变引用的生命周期,只是提示编译器程序员对这些引用预期的生命周期长度(例如 longest 函数中,预期的生命周期是 x, y 和返回值三者中最短的生命周期长度(三者标注的生命周期都是 'a))。这样编译器就可以通过标注检查出来字段/返回值正确的生命周期,并再不满足要求时报错提示。

自动化测试

运行 cargo test 启动自动化测试,下面是一个简单的自动化测试文件的部分内容

#[cfg(test)]
mod tests {
    #[test] // 测试函数注解,表示紧跟着的是一个测试函数
    fn it_works() {
        let result = 2 + 2;
        assert_eq!(result, 4); // 函数运行成功,测试通过
    }
}

2023-04-02 函数式语言功能:闭包和迭代器

闭包

闭包的声明格式:| param_list | expression,即用一对 | 包裹参数列表,紧跟着闭包的包体表达式

let mut x = 5;
let add_y = |y: i32| -> i32 {
    x + y
};
// 上面的闭包可以直接写成下面这样的形式
// let add_y = |y| x + y;
// 默认的形式对变量的捕获是不可变的,如果是可变的捕获需要下面这样
// let mut add_y = |y| {x += y; x};
// 如果闭包捕获变量时同时也要转移所有权,则是下面这样
// let mut add_y = move |y| {x += y; x};
println!("Result is: {}", add_y(4));

和函数的参数必须标注类型不同,闭包的参数类型标注可以省略,只有在闭包没有被使用的情况下才强制要求标注参数类型。 使用闭包作为结构体成员时,也需要标注闭包的特征(限定这个闭包成员的形式)

struct Cacher<T, U>
where
    T: Fn(U) -> U,
{
    query: T,
    value: Option<U>,
}
impl<T> Cacher<T>
where
    T: Fn(U) -> U,
{
    fn new(query: T) -> Cacher<T> {
        Cacher {
            query,
            value: None,
        }
    }
    // 先查询缓存值 `self.value`,若不存在,则调用 `query` 加载
    fn value(&mut self, arg: U) -> U {
        match self.value {
            Some(v) => v,
            None => {
                let v = (self.query)(arg);
                self.value = Some(v);
                v
            }
        }
    }
}

上面 Fn(U) -> U 表示闭包的特征标注。根据闭包捕获变量的形式不同有三种不同的标注:

像特征一样,闭包也可以用来标注返回值,用于约束返回值实现了闭包特征。并且同样是使用和特征约束相同的形式(impl Fn(T) -> R 这种,对应到特征就是 impl TraitName 的形式)

迭代器

实现了 Iterator 特征的类型可以使用迭代器的相关方法:

实现了 IntoIterator 特征的类型可以转换为迭代器:

2023-04-04 智能指针

智能指针

2023-04-05 多线程、宏

多线程

yang
yang

Be curious about the world.