参考资料:
Rust语言圣经(Rust Course)
The Rustonomicon
The Rust Reference

变量

所有权:

  • 变量可以是某个值的所有者,两个变量不能同时(同时指在生命周期重叠的地方)是同一个值的所有者,但可以通过赋值将所有权转移到别的变量(叫做移动),此时原变量失去了所有权;
  • 拥有某个值的所有权的变量在生命周期结束的时候会回收这个值;
  • 实现了 Copy triat 的类型无需所有权转移,在赋值时会进行 数据拷贝
  • 复合类型的所有权跟字段的所有权是独立的,某些字段的所有权被移动后(叫做部分移动partial move),这个复合类型的其他字段还是能使用的,数组不支持部分移动。
  • 当变量部分移动所有权后,不能将这个变量的所有权移动给别的变量,也不能创建引用;

引用:

  • 变量还可以是某个值的不可变引用或可变引用,通过它的所有者可以创建出它的引用,创建引用和使用引用(使用包含该引用的字段的struct也算)叫做借用
  • 当变量被移动或部分移动所有权后不能再通过这个变量创建引用;
  • 某个值可以同时有多个不可变引用,而可变引用同时最多只能有一个,并且不能同时存在同一个值的不可变引用和可变引用;可以通过&*将可变引用转换为不可变引用,此时可变引用在这个不可变引用的生命周期内无法使用。
  • 在引用的生命周期内不能对这个值的所有权进行移动;
  • 引用的生命周期必须在所有者的生命周期的范围内;
  • 当对引用进行解引用时(使用星号前缀)不能获取到值的所有权,只能进行值的拷贝(需要该类型实现 Copy),同样不能通过复合类型的引用获取字段的所有权。
  • 结构体中的引用字段必须加上生命周期

生命周期:

  • 每个变量都有它的生命周期(对于拥有所有权的变量:声明处~最后出现的地方,对于引用变量:如果是字符串字面值那么是static,否则需要根据生命周期标注判断(在一些情况下编译器会推断出生命周期标注)),这里的生命周期标注只是对变量的实际生命周期之间的关系的描述,rust编译器通过这些标注所推断出来的生命周期不完全等于程序运行时的实际生命周期(变量作用域),它的范围可能比实际的生命周期大。

  • 生命周期省略规则:https://doc.rust-lang.org/nomicon/lifetime-elision.html#lifetime-elision

  • 通过引用创建新引用时,新引用的生命周期不能大于原引用的生命周期。

  • 函数只有一个引用参数,那出参引用的生命周期不能超过这个参数的生命周期

  • 可以通过在&后面添加生命周期名称来指定变量的生命周期之间的关系

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 这里代表返回值的生命周期在参数的生命周期的重叠部分内
    fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
        if x.len() > y.len() {
            x
        } else {
            y
        }
    }
    
    // 这里代表某个S值的所有者的生命周期在x,y的重叠部分内
    struct S<'a> {
        x: &'a str,
        y: &'a str
    }
  • rust的生命周期判别系统不够聪明的情况:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    #[derive(Debug)]
    struct Foo;
    
    impl Foo {
        // 这里rust要求出参的生命周期在入参生命周期内,所以即使入参的实际生命周期是这个函数体内,但是rust会认为入参的生命周期跟出参一致
        fn mutate_and_share(&mut self) -> &Self { &*self }
        fn share(&self) {}
    }
    
    fn main() {
        let mut foo = Foo;
        let loan = foo.mutate_and_share();
        foo.share();
        println!("{:?}", loan);
    }
    
    fn get_default<'m, K, V>(map: &'m mut HashMap<K, V>, key: K) -> &'m mut V
    where
        K: Clone + Eq + Hash,
        V: Default,
    {
        // 这里对map进行了mut 借用, 返回了一个Option<&mut V>,同样这里map的mut借用需要大于返回值的生命周期。
        match map.get_mut(&key) {
            Some(value) => value,
            None => {
                // 这里面Option<&mut V> 已经不在生命周期了,但是rust仍然认为它还存活,所以map的生命周期也被扩展到这里
                // 从而判断出出现多次mut借用
                map.insert(key.clone(), V::default());
                map.get_mut(&key).unwrap()
            }
        }
    }
  • 无界(unbound)生命周期,这个引用是凭空产生的,所以这里的'a可以是任意生命周期大小,也可以是'static

    1
    2
    3
    4
    5
    fn f<'a, T>(x: *const T) -> &'a T {
        unsafe {
            &*x
        }
    }
  • 生命周期约束, <'a: 'b, 'b>,中的'a: 'b代表'a 包含 'b<'a, T: 'a>代表T的生命周期包含'a

    1
    2
    3
    4
    5
    6
    7
    // 这种形式编译器可以自动推断,不用手动标注
    struct Ref<'a, T: 'a> {
        r: &'a T
    }
    struct Ref<'a, T> {
        r: &'a T
    }
  • 另外说一句,在同一个函数内的局部变量,根据括号的匹配规则,两个局部变量的生命周期要么是完全分离的,要么是其中一个完全包含另一个,不存在交错的情况。(暂时举不出交错的情况)

mut:

  • 值的所有者是不可变的(类似 Java 的 final),声明变量时使用mut前缀可以声明可变所有者,struct 字段的可变性与struct所有者本身的可变性一致。
  • 可变引用只能通过可变所有者生成

Trait

the ! in a trait means “does not implement”

Deref:

1
2
3
4
pub trait Deref {
    type Target: ?Sized;
    fn deref(&self) -> &Self::Target;
}
  • 如果 S 实现了 Dereflet s = S {},当执行 *s 时将执行 *(s.deref()),这里的替换不会递归进行只会替换一层。

错误处理

panic!:

  • 使用panic!会触发panic,此时可以触发进行栈展开等善后工作再终止(默认)或直接终止当前线程,在toml中配置panic = 'abort'开启直接终止。
  • 当调用 panic! 宏时,它会
    • 格式化 panic 信息,然后使用该信息作为参数,调用 std::panic::panic_any() 函数
    • panic_any 会检查应用是否使用了 panic hook,如果使用了,该 hook 函数就会被调用(hook 是一个钩子函数,是外部代码设置的,用于在 panic 触发时,执行外部代码所需的功能)
    • 当 hook 函数返回后,当前的线程就开始进行栈展开:从 panic_any 开始,如果寄存器或者栈因为某些原因信息错乱了,那很可能该展开会发生异常,最终线程会直接停止,展开也无法继续进行
    • 展开的过程是一帧一帧的去回溯整个栈,每个帧的数据都会随之被丢弃,但是在展开过程中,你可能会遇到被用户标记为 catching 的帧(通过 std::panic::catch_unwind() 函数标记),此时用户提供的 catch 函数会被调用,展开也随之停止:当然,如果 catch 选择在内部调用 std::panic::resume_unwind() 函数,则展开还会继续。
    • 还有一种情况,在展开过程中,如果展开本身 panic 了,那展开线程会终止,展开也随之停止。

一旦线程展开被终止或者完成,最终的输出结果是取决于哪个线程 panic:对于 main 线程,操作系统提供的终止功能 core::intrinsics::abort() 会被调用,最终结束当前的 panic 进程;如果是其它子线程,那么子线程就会简单的终止,同时信息会在稍后通过 std::thread::join() 进行收集。

?、.?:

使用?.?要求函数返回类型为Option或者Result,下面都用Result演示。

另外,main函数的返回值可以是std::process::Termination的任意实现,!Result都实现了,所以如果需要在main中使用?,那么可以修改main的返回类型.

可以通过?省略match, a?会翻译成

1
2
3
4
match a {
    Ok(v) => Ok(v),
    Err(e) => Err(e)
}

可以通过?.进行链式调用, a?.b()?.c()? 会翻译成

1
2
3
4
5
6
7
8
9
10
match a {
    Ok(v) => match v.b() {
        Ok(v) => match v.c() {
            Ok(v) => Ok(v)
            Err(e) => Err(e)    
        }
        Err(e) => Err(e)
    },
    Err(e) => Err(e)
}

当函数返回Result<_, B>,并且impl From<A> for B,那么Ok(a?)会翻译成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// a类型是Result<_, A>, 结果是
let b: Result<_, B> = match a {
    Ok(v) => Ok(v),
    Err(e) => Err(e.into())
}

// 完整例子

struct A {
    v: i32,
}

struct B {
    v: i32
}

impl From<A> for B {
    fn from(value: A) -> B {
        B {v: value.v}
    }
}

fn test() -> Result<(), B> {
    let a: Result<(), A> = Err(A {v: 1});
    let b: Result<(), B> = Ok(a?);
    // let b: Result<(), B> = match a {
    //     Ok(v) => Ok(v.into()),
    //     Err(e) => Err(e.into())
    // };
    b
}

包管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.
├── Cargo.toml
├── Cargo.lock
├── src
   ├── main.rs
   ├── lib.rs
   └── bin
       └── main1.rs
       └── main2.rs
├── tests
   └── some_integration_tests.rs
├── benches
   └── simple_bench.rs
└── examples
    └── simple_example.rs
  • 唯一库包:src/lib.rs,crate 名称与 package 同名
  • 默认二进制包:src/main.rs,编译后生成的可执行文件与 Package 同名
  • 其余二进制包:src/bin/main1.rs 和 src/bin/main2.rs,它们会分别生成一个文件同名的二进制可执行文件
  • 集成测试文件:tests 目录下
  • 基准性能测试 benchmark 文件:benches 目录下
  • 项目示例:examples 目录下

package:

  • 理解为一个项目,一个Cargo.toml表示一个package,可以通过这个文件定义package名称

crate:

  • 二进制包可以有多个,lib包只能有一个,二进制包不能被引用
  • 引用lib时使用lib的crate名称,引用当前crate时使用”crate”作为名称

module:

可以有层次结构:

1
2
3
4
5
mod <moduleName1> {
    mod <moduleName2> {
        
    }
}

可以放在别的文件:

1
2
3
4
5
6
7
8
9
10
11
// src/lib.rs
// 将会加载src/m.rs 或者 src/m/mod.rs(旧版本的用法)的内容作为一个mod m
mod m;

// src/m/mod.rs,旧版本的用法
// 将会加载src/m/m1.rs作为mod m::m1
mod m1;

// src/m.rs
// 将会加载src/m/m1.rs作为mod m::m1
mod m1;

引用一个module

  • 绝对路径:<crateName>::<moduleName1>::<moduleName2>
  • 相对路径: 假设当前在root下,那引用moduleName2使用[self::]<moduleName1>::<moduleName2>,假设当前在moduleName1模块,那引用moduleName2就可以直接moduleName2。如果要访问父模块那么使用super
  • 例子:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    pub mod A {
        fn a() {
            crate::A::B::C::c();
            self::B::C::c();
            B::C::c();
        }
        pub mod B {
            pub fn b() {
                super::a()
            }
            pub mod C {
                pub fn c() {
                    super::super::a()
                }
            }
        }
    }

可见性:

  • 默认情况下,模块、函数、类型、常量都是私有的(私有代表这个元素只能在本模块中使用)。
  • 父模块无法访问子模块中的私有项,但是子模块可以访问父模块、父父..模块的私有项。
  • 父模块可以访问私有直接元素:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    mod a {
        mod b {
            pub fn b() {}
            mod c {
                pub fn c() {}
            }
        }
    
        fn g() {
            b::b(); // 可以访问
            b::c::c(); // 不能访问c模块
        }
    }
  • 将结构体/模块设置为 pub,它的内部元素依然是私有的;而将枚举设置为 pub,它的所有字段将对外可见
  • 使用pub时可以限制可见性:
    • pub 意味着可见性无任何限制
    • pub(crate) 表示在当前包可见
    • pub(self) 在当前模块可见
    • pub(super) 在父模块可见
    • pub(in ) 表示在某个路径代表的模块中可见,其中 path 必须是父模块或者祖先模块

use:

  • use 可以使用别名use xxx as yyy
  • use 可以引入当前模块并pub导出pub use xxx。例如一个模块a::b::c中定义了函数f,如果模块x中使用use a::b::c,那么c仅在x中可用,即别的模块引入x后不能能通过x::c访问到x所导入的c,使用pub use将能实现这个效果。
  • use a::{self, a1, a2}:相当于引入use a; use a::a1; use a::a2
  • use a::*: 会引入所有元素, 重名时本地同名的优先级更高