Rust 语言学习笔记(三)

引用与解除引用

觉得还是有必要继续深入学习一下 Rust 再练手,毕竟仍然看到 & 和 * 符号还有些恍惚,大概就是 C/C++ 里的取地址和取值操作吧,实际上也确实类似。只是叫法略有不同, 还有就是在 C/C++ 多用了指针的概念。


在 C/C++ 中, &:称作 Address-of Operator, 在 Rust 中称作 Reference Operator, 而 * 在 C/C++ 和 Rust 中都叫做 Dereference Operator。以前学 C/C++ 经常被一系列的 &, * 打晕了头,如今参考了它们的英文名称立刻变得清晰了起来。

就像当初看汇编各种寻址方式弄得头都大了,其实也就是依照约定。

回顾一段 C++ 的代码,hello.cpp
 1#include <iostream>
 2using namespace std;
 3
 4int main()
 5{
 6    int a = 42;
 7    int* r = &a;     // 比写成 int *r 好理解,int* 直接理解成类型是指向 int 的指针
 8    int* x = r + 4;  // r 中存了一个地址,指针偏移
 9    int** y = &x;
10    cout << "r: " << r << ", *r: " << *r << ", *&*&a: " << *&*&a << endl; // *& 是一对逆操作
11    cout << "*x: " << *x << ", **y: " << **y << endl;
12    return 0;
13}

可用 g++ 编译
$ g++ hello.cpp -o hello $ ./hello
r: 0x7ff7bbd06228, *r: 42, *&*&a: 42 *x: 301843487, **y: 301843487
在 C/C++ 中声明指针个人偏向于 * 号紧挨类型,int* 整体作为类型,而写成 int *r 中的 *r 就有些不明所以了。类似的声明数组倾向用 int[] data 而不 int data[]

&:取地址(引用), *:解除引用,获得地址中存储的值

回到 Rust 的 Reference(&) 和 Dereference(*), 在 Rust 中直接用地址偏移来获得内存中的数值是不安全的操作,所以不在上面的代码中演示。
1let a = 42;
2let r:&i32 = &a;
3let y = &r; // 自动推断的 &&i32
4println!("r: {}, *r: {}, *&*&a: {}, **y: {}", r, *r, *&*&a, **y);

在 Rust 声明引用时,类型可自动推断或手动明确指定,由于变量类型总是在 : 号后面,所以不会有 C/C++ 那种 * 号前后移造成的理解混乱。这也证明了 variable_name: type 要比 type variable_name 变量声明方式优越,现代语言如 Scala, Swift, Kotlin 等多用前一种方式,连 Python 的 type hint 也是这种方式。

&* 的意义与 C/C++ 中的意义是一样的,&: 取得引用,*: 解除引用,获得引用所指向的值,在 Rust 中也有引用的引用,类似于 C/C++ 的指针的指针。

在 C/C++ 中相关的有 引用, 地址, 和 指针 三个概念,容易把人搞糊涂,而 Rust 中也有 引用指针 的概念。

回到 Rust 的字面字符串
1let s = "abc"; // s 的类型被推断为 &str

由 "abc" 本质上是一个字符数组,所以 Rust 推断 s 的类型为 &str

在 Rust 中数组变量本身并非首个元素的地址,同样必须用 & 来获得引用,如
1let arr = [11, 22, 33];
2let arr_ref = &arr;
3println!("arr[0]: {}", unsafe { *(arr_ref as *const i32) });    // 偏移地址
4println!("arr[1]: {}", unsafe { *(arr_ref as *const i32).offset(1) });
5for item in &arr {  // 这里遍历的是引用,也可以用  for item int arr { println!("{}", item); } 遍历值
6    println!("{}", *item);
7}

执行后输出
arr[0]: 11 arr[1]: 22 11 22 33

函数的显式生命周期注解

让人头大的 Rust 函数定义方式要来了,即带有显式生命周期(lifetime) 注解的函数定义,比如我们定义一个 add 函数
1fn add(i: &i32, j: &i32) -> i32 {
2    *i + *j
3}

上面其实是省略了生命周期注解,等效的定义如下
1fn add<'a, 'b>(i: &'a i32, j: &'b i32) -> i32 {
2    *i + *j
3}

<'a, 'b> 定义了两个生命周期变量,参数 i 是一个具有生命周期 a 的 i32 类型变量,参数 j 是一个具有生命周期 b 的 i32 类型变量。到这里还是无法知道这个生命周期指的是什么。

定义函数时多数时候都不用显式的声明生命周期,编译器能自动推断,但对某些函数定义无法推断出生命周期
1fn add(i: &i32, j: &i32) -> &i32 {
2    let res = *i + *j;
3    &res
4}

编译会出错
 1error[E0106]: missing lifetime specifier
 2  --> src/main.rs:24:29
 3   |
 424 | fn add(i: &i32, j: &i32) -> &i32 {
 5   |           ----     ----     ^ expected named lifetime parameter
 6   |
 7   = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `i` or `j`
 8help: consider introducing a named lifetime parameter
 9   |
1024 | fn add<'a>(i: &'a i32, j: &'a i32) -> &'a i32 {
11   |       ++++     ++          ++          ++

因为函数中返回一个引用,从函数返回后获得的引用还要保持有效,所返回值必须是 lifetime 的,函数应声明为
1fn add<'a>(i: &'a i32, j: &'a i32) -> &'a i32 {

仍然是无法编译
1error[E0515]: cannot return reference to local variable `res`
2  --> src/main.rs:19:5
3   |
419 |     &res
5   |     ^^^^ returns a reference to data owned by the current function

这就是 Rust 想要的安全,只得把返回值类型改为 i32 才行。在 C/C++ 函数中返回一个指针就必须由调用者负责不用时 delete 它,否则造成内存泄露。

泛型函数

Rust 声明泛型函数的方式与 Java 类似, 只是泛类型移到了方法名后面,如
1fn add<T>(i: T, j: T) -> T {
2  i + j    // 会转换为 i.add(j)
3}

显然这个函数是无法通过编译的,因为 <T> 代表的是所有类型,不是所有类型都支持 add 操作。<T> 必须指定类型或 Trait 为上界。Scala 也用 Trait 这个概念,Trait 像是一个接口,协议或合约,但更像是一个可被多重继承的抽象类。
1use std::ops::Add;
2
3fn main() {
4   println!("{}, {}", add(10, 20), add(1.1, 2.2))
5}
6
7fn add<T: Add<Output=T>>(i: T, j: T) -> T {
8    i + j
9}

add 即可处理 i32, 又能处理 f64。Rust 是不会自动转型的,所以声明两个 f64 相加的函数,不能接受两个 i32 值。

命令行参数

Rust 标准库获取命令行输入参数用  std::env:args
1use std::env::args;
2
3let args1: Args = args();
4let args2: Vec<String> = args1.collection();
5println!("{:?}", args2);

cargo run 的话传入参数的方式是
$ cargo run -- aa bb
["target/debug/hello", "aa", "bb"]
第一个参数像  bash  一样是命令本身的路径

Rust 标准库处理命令行参数的功能太弱了,可用第三方的 Crate - clap, 见官方的用法 https://docs.rs/clap/latest/clap/#example
$ cargo add clap --features derive
 1use clap::Parser;
 2
 3/// Simple program to greet a person
 4#[derive(Parser, Debug)]
 5#[command(author, version, about, long_about = None)]
 6struct Args {
 7    /// Name of the person to greet
 8    #[arg(short, long)]
 9    name: String,
10
11    /// Show verbose information
12    #[arg(short, long)]
13    verbose: bool,
14
15    /// Number of times to greet
16    #[arg(short, long, default_value_t = 1)]
17    count: u8,
18}
19
20fn main() {
21    let args = Args::parse();
22
23    for _ in 0..args.count {
24        println!("Hello {}!", args.name)
25    }
26}

查看命令帮助
 1target/debug/hello --help
 2Simple program to greet a person
 3
 4Usage: hello [OPTIONS] --name <NAME>
 5
 6Options:
 7  -n, --name <NAME>    Name of the person to greet
 8  -v, --verbose        Show verbose information
 9  -c, --count <COUNT>  Number of times to greet [default: 1]
10  -h, --help           Print help
11  -V, --version        Print version

用 Rust 的属性和文档注释来声明输入参数,也可以用 Arg 来构建输入参数规则。

函数指针

Rust 也是函数式的,可声明高阶函数,即函数的参数或返回值可为一个函数,这就要知道函数类型怎么表示,也就函数指针类型
1let f1: fn() = ...;
2let f2: fn(i32) = ...;
3let f3: fn(i32, f64) -> f64 = ...;
4
5fn math(op: fn(i32, i32) -> i32, x: i32, y: i32) -> i32 { ... };
6
7type MathOp = fn(i32, i32) -> i32;
8fn math1(op: MathOp, y: i32) -> i32 { ... }

闭包

有了函数和函数指针的概念,闭包也就不难理解了,它是一个匿名函数,所以定义一个闭包变量也就是函数。只是闭包格式上不同, 用 | 隔开参数,返回类型能根据 {} 中的返回值进行推断。闭包能能够捕获外部变量,函数不也可以吗!

下面是一系列的相关例子
 1fn main() {
 2    let add_one_v1 = |x: u32| -> u32 { x + 1 }; // 推断为 fn(u32) -> u32
 3    let add_one_v2 = |x: u32| { x + 1 };  // 可省略返回值,同样推断为 fn(u32) -> u32
 4    let add_one_v3 = |x | {x + 1};   // 没有类型,所以推断为 fn(?) -> ?, 泛型函数
 5    let add_one_v4 = |x| x + 1;     // 省略了大括号,推断为 fn(?) -> ?
 6
 7    foo(|x| x + 1, 100);
 8}
 9
10fn foo(op: fn(u32) -> u32, a: u32) -> u32 {
11    op(a)
12}

迭代器

Iterator trait 的两个重要方法, iter 返回一个迭代器,next 返回下一个元素 Some(x), 如果到达末尾则是 None。

迭代器的消费器,有 sum, any, collect 等,这些相当于 Java Stream 的 Terminal Operations

还有类似于 Java Stream Intermediate Operations 的,map, filter, take, rev 等,把它们串起来就是
 1let v = [1, 2, 3, 4, 5];
 2
 3// result 的类型不能省略,当前 Rust 1.75 还无法完整推断出来
 4// 也可写成 let result: Vec<_> = ..., Rust 可推断 Vec<_> 为 Vec<i32>
 5let result: Vec<i32> = v.iter()
 6    .filter(|&x| *x > 1)
 7    .map(|&x| x * 2)
 8    .rev()  // 反向迭代
 9    .take(2)// 取前两个元素
10    .collect();
11println!("{:?}", result);  // [4, 6]

Rust 中声明全局变量, static 是 lifetime
1static mut ERROR_1: i32 = 0;
2const ERROR_2:i32 = 0;

约定名称用全大写

Rust 抛出异常用了与 Go 语言一样的关键字,只是 Rust 用的是宏
1panic!("something wrong here");

在 Rust 用了 unsafe { ... } 就意味着可能有 C 那样不安全的代码。 访问静态可变变量需放在 unsafe {} 中

let 声明的所谓的 Immutable 变量内部也可能会变,read-only references(borrows), read-write references(mutable borrows)。

Rust 偏向于用 Result 类型返回值表达成功(Ok) 与出错(Err) 两种状态。下面要专门看下 Rust 怎么处理 Result。 永久链接 https://yanbin.blog/rust-language-learning-3/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。