Rust - A CPP Programmer's Perspective

鉴于贵司大作tikv、tidb、tiflash在Rust、Go和C++之间横跳,因此学习Rust被提上了日程。

本文简称叫Rust: ACPPPP,它主要是用来讨论Rust在一些方面和C++的异同,而不是介绍这一门语言。所以文章是话题形式的,会有很多穿插,例如在讨论所有权时,会直接讲结构体。

Cargo

包管理

C++并没有什么包管理,如果我们想要引用什么东西,代码声明一下,然后确保链接器能够看到定义就行。并且因为模板的引入,很多都是头文件,直接include就行。

crate内

src/main.rs和src/lib.rs被称为crate根。

跨crate

跨crate访问,需要使用Cargo.toml中定义的crate别名。

Cargo.toml解读

  1. [dependencies]
    依赖的第三方package

  2. [dev-dependencies]
    只有tests/examples/benchmarks依赖的第三方package

  3. [features]
    用来支持条件编译,例如

    1
    2
    3
    4
    #[cfg(feature = "xxx")]
    if cfg!(feature = "yyy") {

    }
  4. [lib]

  5. [[test]]
    两个中括号说明是表数组,我们可以这样写

    1
    2
    3
    4
    5
    6
    [[test]]
    path = ""
    name = ""
    [[test]]
    path = ""
    name = ""

所有权、生命周期

Rust哪些操作是需要unsafe包裹的呢?

  1. *mut T解引用
    注意,取引用是safe的
  2. 访问全局的static对象

这也对应了Rust的两个机制,所有权(禁止裸指针)和并发安全。

为了检验是否初步理解Rust所有权,可以尝试自己实现一个双向链表。

绑定和可变性

let和let mut

let x = y表示把y这个值bound/assign到变量x上,因为let是immutable的,所以就不能修改变量x,也就是再次给它赋值(assign)了。如果需要能re-bound或者re-assign,就需要let mut x = y这种形式。

对结构体而言,如果它是immutable的,那么它的所有成员也都是immutable的。在C++中,可以声明类中的某个成员是mutable的,这样即使在const类中也可以修改它,但Rust不允许这样。

由此还派生出了&mut&两种引用。可以可变或者不可变地借用let mut绑定的值,但只能不可变地借用let绑定的值。

Pattern Matching

下面的语句都在尝试定义一个&mut {interger}类型的a,但第三条语句是编译不过的。原因是它触发了Rust里面的pattern matching。

1
2
3
4
5
let ref mut a = 5;
let a = &mut 5;
let &mut a = 5;
// 下面这个语句肯定编译不过,但我们可以从错误中得到a的实际类型
let _: () = a;

我们会很熟悉对enum类型,诸如OptionResult,进行Pattern Matching的做法。下面介绍一些不一样的,例如可以Pattern Match一个struct。

1
2
3
4
5
6
7
8
9
10
11
struct Point {
x: i32,
y: i32,
}

fn main() {
let p = Point { x: 0, y: 7 };
let Point { x: a, y: b } = p;
assert_eq!(0, a);
assert_eq!(7, b);
}

通过@,可以在Pattern Match的时候同时指定期待的值,并将该值保存到局部变量中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Point {
x: i32,
y: i32,
}


fn main() {
let p = Point {x: 1, y: 2};
match p {
Point {
x: xv @ 1,
y: yv @ 2,
} => println!("matched x {:?} y {:?}", xv, yv),
_ => println!("no match"),
}
}

如何在pattern matching的时候不move,而是borrow呢?如下所示,g是一个owned值,而不是一个mutable borrow的值。解决方案就是直接match v.intention_mut()

1
2
3
4
5
6
7
let intention = v.intention_mut();
match intention {
vehicle::Intention::Die => {
},
vehicle::Intention::Goto(g) => {
},
}

Variable shadow

在Rust中有如下称为Variable shadow的做法。一个问题油然而生,既然可以直接let mut,为什么还需要如下的做法呢?

1
2
let x = 1;
let x = 2;

其实shadow的含义是这个变量的生命周期没变,只是我们无法通过从前的名字访问它了,而let mut在重新assign之后,原来的value就会被析构掉。进一步举个例子,给出下面这个程序,它的输出是啥?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct S {
x: i32
}

impl Drop for S{
fn drop(&mut self) {
println!("drop {}", self.x)
}
}

fn main() {
{
let a = S { x: 1 };
let a = S { x: 2 };
}
{
let mut a = S { x: 1 };
a = S { x: 2 };
}
}

结论如下

1
2
3
4
drop 2
drop 1
drop 1
drop 2

为什么呢?对于第一种情况,a被rebound了,但是S {x: 1}只是被shadow了,并没有立即析构。但对于第二种情况,在rebound的时候,S { x: 1 }就被析构了。

移动和借用

可以把所有对值的使用方式归纳为三种:复制、移动和引用(或者称为指针):

  1. 复制的缺点是浪费空间和时间。
  2. 移动的缺点是很多变量的地址会变,这个FFI带来很多麻烦,需要用Box/Pin将一些东西分配到堆上的固定地址,并且传出裸指针。
  3. 引用的缺点是存在NULL,为了避免NULL,又要引入生命周期注解等机制。此外,即使在有了移动语义后,多线程之间依然可以通过引用来访问同一个值,产生并发问题。

Rust中的移动可能伴随着内存地址的变化。很显然,一个对象从A方法通过调用被移动到B方法中,那么肯定出于不同的栈帧中,它的地址肯定会变化,所以要提防这个。而C++中移动更类似金蝉脱壳,将老对象中的东西拆出来用来构建新对象。

引用

引用和借用是什么关系呢?创建一个引用的行为称为借用,在借用过程中,是不可以访问owned值的,否则出现use of borrowed xxx错误。

在C++中,引用必须在定义时就绑定,并且,无论它是可变引用T&还是不可变引用const T&,都不能重新绑定。这很难受,并且std::reference_wrapper也不是什么时候都可以用的。Rust中这些都不是问题,例如下面的代码就可以正常运行。

1
2
3
let mut a: &i32;
a = &1;
a = &2;

只能有一个可变借用,或多个不可变借用

考虑下面的Race Condition:

  1. 多个指针访问同一块数据
  2. 至少一个指针被用来修改数据
  3. 没有同步机制

Rust解决方案是只能同时有一个可变借用,或者多个不可变借用。问题来了,如果Owner在写,有一个可变引用在写,或者有一个不可变引用在读呢?
对于对象的成员函数的调用,这种情况是不存在的。如下所示,成员函数需要&self或者&mut self

1
2
3
4
5
6
let mut x = "123".to_string();
let y = &mut x;

x.push_str("456");

println!("y = {}", y);

那么对于primitive types呢?运行下面的代码,发现出现错误提示”use of borrowed aaa“,这也就是说在借用期间,是无法访问owned value的,毕竟被借走了嘛。

1
2
3
4
let mut aaa: i32 = 1;
let bbb = &mut aaa;
aaa += 1;
println!("bbb {:?}", *bbb);

注意,下面的代码给人一种”可以同时使用借用和owned的值的错觉“,但并不是这样。因为change_aaaaaa的借用在调用完成之后就结束了,后面aaa = 2的时候就没有其他借用情况了。

1
2
3
4
5
6
7
8
9
10
fn change_aaa(bbb: &mut i32){
*bbb = 2;
}

fn main() {
let mut aaa: i32 = 1;
// TODO 是否可以想个办法异步执行
change_aaa(&mut aaa)
aaa = 2;
}

移动的demo

std::mem::drop函数用来析构T的对象,这是对移动的应用。在调用drop函数时,_x的所有权会被移入。当然,如果实现了Copy,那么drop就无效了

1
pub fn drop<T>(_x: T) { }

借用的demo

当一个函数接受引用作为参数时,需要显式借用,这一点和C++不一样。

1
2
3
4
5
6
7
fn fn_takes_ref(i: &int) {
println!("{}", i);
}
// Error
fn_takes_ref(1);
// Ok
fn_takes_ref(&1);

Clone和Copy

Copy

Rust有一个叫做std::marker::Copy的特殊trait,其中不带有任何方法,所以基本可以视作是给编译器提供的一个marker。如果一个类型实现了Copy trait,在赋值的时候使用复制语义而不是移动语义。

Rust不允许自身或其任何部分实现了Drop trait的类型使用Copy trait。这听起来很奇怪,但如果我说Copy trait的实现就是bitwise的Copy,就合理了。所以可以近似理解为Copy只适用于C++中的trivial的对象。

Clone

对于非trivial对象,又想复制怎么办呢?一个方法是实现Clone trait。可以理解为是C++中的拷贝构造函数。

容易想到,如果仅仅实现深复制,那么实际上就是递归调用所有field的.clone()而已,这其实等价于下面的代码

1
2
3
#[derive(Clone)]
struct S {
}

但注意,编译器在要求实现Copy后,Clone的含义也必须代表bitwise memcpy。因此我们通常会通过#[derive(Copy,Clone)]来支持自动生成Copy特性。

所有权相关设施

介绍Borrow(Mut)/AsRef(Mut)/ToOwned等基础的实现。

Borrow和AsRef的区别是什么?

可以看到AsRef和Borrow两个trait的定义不能说非常相似,也可以说是一模一样了,那为什么会分成两个呢?

1
2
3
4
5
6
7
pub trait AsRef<T: ?Sized> {
fn as_ref(&self) -> &T;
}

pub trait Borrow<Borrowed: ?Sized> {
fn borrow(&self) -> &Borrowed;
}

显然这个疑问是普遍的,通常的说法是Borrow更严格,目的是借用;AsRef支持的类型更广,目的是类型转换。但说实话,还是一头雾水。这篇文章讲解了个例子,概括如下:

  1. HashMap存储(K, V)对,并且可以通过提供的&K查找对应的&mut V。因为按K查,按&K取,所以需要保证这两个的行为是一致的。
    【Q】为什么HashMap要按照&K取呢?
  2. 于此同时,我们可以实现一个CaseInsensitiveString结构,它可以看做是忽略大小写比较的一个String。
  3. 问题来了,我们有impl Borrow<str> for String,那么是否可以实现impl Borrow<str> for CaseInsensitiveString呢?
    答案是不可以的,这样会破坏HashMap的一致性。
    但这就够了么?难道CaseInsensitiveString不可以转换成&str么?当然可以,所以有AsRef。

为什么不能从&mut调用Clone?

从下面的实现可以看到,标准库没有为&mut提供Clone,原因是会产生指向同一个位置的两个&mut

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
impl<T: ?Sized> Clone for *const T {
fn clone(&self) -> Self {
*self
}
}

impl<T: ?Sized> Clone for *mut T {
fn clone(&self) -> Self {
*self
}
}

impl<T: ?Sized> Clone for &T {
fn clone(&self) -> Self {
*self
}
}

impl<T: ?Sized> !Clone for &mut T {}

下面的代码中,如果clone了&mut MyStruct2,会出现多个指向同一个地址的&'a mut MyStruct

1
2
3
4
5
6
7
8
9
#[derive(Clone)]
struct MyStruct {
val: usize,
}

#[derive(Clone)]
struct MyStruct2<'a> {
struct_reference: &'a mut MyStruct
}

但需要注意,clone的目标不是&T而是T。上面例子为什么会失败,原因是在Clone MyStruct的时候递归地需要Clone &'a mut MyStruct导致的。但如果直接对一个&mut T调用Clone就不会出现编译问题,如下所示

1
2
3
4
5
6
7
#[derive(Clone)]
struct DoClone{
x: i32
}
let mut dc = DoClone{x:1};
let mdc = &mut dc;
mdc.clone();

ToOwned和Clone的区别是什么?

1
2
3
4
5
6
pub trait ToOwned {
type Owned: Borrow<Self>;
fn to_owned(&self) -> Self::Owned;

fn clone_into(&self, target: &mut Self::Owned) { ... }
}

下面这个例子很经典,"123"是一个&str类型,对它调用clone,还会得到一个&str类型。但调用to_owned则会得到一个String类型。

1
2
let become_str = "123".clone();
let become_String = "123".to_owned();

指针和智能指针

C++中,为了突破栈上分配的限制会在堆上分配对象,Rust中为了避免移动,有更进一步的往堆上创建对象的需求。C++不会对指针进行资源管理,后面标准库也只是断断续续支持了一些智能指针,但Rust希望做得更周到一点。

在Rust中有下面的指针:

  1. *mut T/*const T
    这是C的裸指针
  2. Box
  3. Pin
  4. Rc
  5. Arc
    原子引用计数
  6. Ref
  7. RefCell
  8. Cow

trait Deref/DerefMut

Deref是deref操作符*的 trait,比如*v。它的作用是:

  1. 对于实现了Copy对象,获得其拷贝
  2. 对于没有实现Copy的对象,获得其所有权

如下所示,一个智能指针对象U比如Box,如果它实现了U: Deref<Target=T>,那么Deref能够从它获得一个&T。实现上,我们从一个&Box<T>解两次引用,获得T,再返回&T。抽象一点来说,在实现了Deref后,能将&U变成&T,换种说法*x的效果就是*Deref::deref(&x)。这么做的好处是将所有奇怪的对智能指针的引用都转成&T

1
2
3
4
5
6
7
8
9
10
11
12
13
impl<T: ?Sized> Deref for &T {
type Target = T;
fn deref(&self) -> &T {
*self
}
}

impl<T: ?Sized, A: Allocator> Deref for Box<T, A> {
type Target = T;
fn deref(&self) -> &T {
&**self
}
}

RefCell

RefCell是类似于Box的指针,但相比引用和Box类型在编译期检查借用,RefCell在运行期检查。具体来说,RefCell在运行期检查:

  1. 在任意时刻只能获得一个&mut或任意个&
  2. 引用指向的对象是存在的

容易发现RefCell能够帮助用户获得多个&mut,从而实现内部可变性

Pin

【建议在学习Pin之前,了解Deref和DerefMut】
我们知道,一个async fn会产生一个自引用结构AsyncFuture,因此它不能被移动。让一个对象不能被移动的第一步是将它分配到堆上,但Box还不够,因为如下所示,std::mem::swap能够移动Box中的对象。如果我心怀歹意,那其实是可以移动Box中的对象的。

1
2
3
4
let mut rb = Box::new(TestNUnpin{b: "b".to_owned()});
let mut rb2 = Box::new(TestNUnpin{b: "a".to_owned()});
std::mem::swap(rb.as_mut(), rb2.as_mut());
println!("{} {}", rb.b, rb2.b); // Should be `a b`

另一方面,很多FFI的实现也需要保证地址是不变的,综上于是就有了Pin。

Pin分析了下,诸如std::mem::swap之流为什么能移动,原因是它们都能获得&mut T。所以只要限制可变借用,就可以在把对象Pin在堆上。限制获得可变引用简单啊,不实现AsMut就行。

以上的容易理解,但为什么会有Unpin!Unpin呢?原因是需要给类型分类,讨论在Pin之前和之后类型的行为。这肯定难以理解,所以不妨先看看Pin是如何创建的,再回过来看。

在下面的代码中,对一个实现了trait Unpin的类型Target,可以直接通过Pin::new产生一个Pin<P>对象。但如果说是一个!Unpin的对象,反而需要通过Box::pin或者pin_utils::pin_mut!来pin住。

1
2
3
4
5
6
7
8
impl<P: Deref<Target: Unpin>> Pin<P> {
pub const fn new(pointer: P) -> Pin<P> {
unsafe { Pin::new_unchecked(pointer) }
}
}
pub const unsafe fn new_unchecked(pointer: P) -> Pin<P> {
Pin { pointer }
}

如下代码展示了这一点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct TestUnpin {
a: String,
}
struct TestNUnpin {
b: String,
}
impl !Unpin for TestNUnpin {}


fn main() {
let rp = std::pin::Pin::new(&mut TestUnpin{a: "a".to_owned()});
// let rnp = std::pin::Pin::new(&mut TestNUnpin{b: "b".to_owned()});
let rnb = Box::pin(TestNUnpin{b: "b".to_owned()});
}

容易产生几个疑问:

  1. 为什么可以直接Pin::new一个Unpin对象?
    因为对于Unpin,Pin不做任何保证。

  2. 为什么不能直接Pin::new一个!Unpin的对象?
    因为这是不安全的,所以需要用Pin::new_unchecked来创建。
    另外,Pin需要保证自己维护的指针不会再被移动了,**即使在自己销毁之后,也是不能被移动的**,但这个很难在编译期判定。

  3. 如果对一个Unpin做了Pin::new,能获得里面的&mut么?
    可以通过Pin::get_mut获得

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let mut p = TestUnpin{a: "b".to_owned()};
    let mut p2 = TestUnpin{a: "b".to_owned()};
    let mut rp = unsafe {
    std::pin::Pin::new_unchecked(&mut p)
    };
    let mut rp2 = unsafe {
    std::pin::Pin::new_unchecked(&mut p2)
    };
    std::mem::swap(Pin::get_mut(rp), Pin::get_mut(rp2));
    println!("{} {}", p.a, p2.a); // Should be `a b`

    但如果类型是!Unpin,则不行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let mut np = TestNUnpin{b: "b".to_owned()};
    let mut np2 = TestNUnpin{b: "a".to_owned()};
    let mut rnp = unsafe {
    std::pin::Pin::new_unchecked(&mut np)
    };
    let mut rnp2 = unsafe {
    std::pin::Pin::new_unchecked(&mut np2)
    };
    std::mem::swap(Pin::get_mut(rnp), Pin::get_mut(rnp2));
    println!("{} {}", rnp.b, rnp2.b); // Should be `a b`
  4. 为什么Box::pin可以Pin住!Unpin?
    Box::pin的实现是传入一个&mut T,然后创建一个Box<&mut T>并立马Pin住它。
    在Pin之前,无法移动T,这是因为只能同时有一个可变借用&mut T
    在Pin之后,无法移动T,这是因为Box被实现为owned且unique的。可以参考下面的代码

    1
    2
    3
    4
    5
    6
    let mut t = TestNUnpin{b: "b".to_owned()};
    let mt = &mut t;
    let b = Box::pin(&mut t);
    let mut t2 = TestNUnpin{b: "a".to_owned()};
    std::mem::swap(mt, &mut t2);
    println!("{} {}", t.b, t2.b);

现在回答为什么要有Unpin和!Unpin的问题。对于Unpin类型,它实际上是给Pin做了一个担保,告诉Pin即使我这个类型被移动了也没事,所以Pin对它的作用就是屏蔽了&mut的获取渠道。对于!Unpin和PhantomPinned类型,它们是真的不能被移动的,这不仅要借助Pin,这些类型自己也要提供一个合适的接口,从它们来创建Pin。

这里另外提一句,!Unpin这样的称为negative bounds,Rust对它的支持还没有稳定下来。

闭包和函数

Fn/FnMut/FnOnce

Rust对a(b,c,d)这样的调用搞了个有点像Haskell中的$的东西,目的是为了重载“对函数的调用”

1
2
3
Fn::call(&a, (b, c, d))
FnMut::call_mut(&mut a, (b, c, d))
FnOnce::call_once(a, (b, c, d))

FnOnce会获取自由变量的所有权,并且只能调用一次,调用完会把自己释放掉。
FnMut会可变借用自由变量。
Fn会不可变借用自由变量。
FnMutFn都可以调用多次。

可以用下面的代码确定某个函数具体实现了哪个trait,实现了的trait能够通过编译。

1
2
3
4
fn is_fn <A, R>(_x: fn(A) -> R) {}
fn is_Fn <A, R, F: Fn(A) -> R> (_x: &F) {}
fn is_FnMut <A, R, F: FnMut(A) -> R> (_x: &F) {}
fn is_FnOnce <A, R, F: FnOnce(A) -> R> (_x: &F) {}

查看代码,发现三者具有继承关系Fn : FnMut : FnOnce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pub trait FnOnce<Args> {
/// The returned type after the call operator is used.
#[lang = "fn_once_output"]
#[stable(feature = "fn_once_output", since = "1.12.0")]
type Output;

/// Performs the call operation.
#[unstable(feature = "fn_traits", issue = "29625")]
extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}

pub trait FnMut<Args>: FnOnce<Args> {
/// Performs the call operation.
#[unstable(feature = "fn_traits", issue = "29625")]
extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}

pub trait Fn<Args>: FnMut<Args> {
/// Performs the call operation.
#[unstable(feature = "fn_traits", issue = "29625")]
extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}

为什么是这样的继承关系呢这篇回答给出了解释。

确实可以让FnOnce、FnMut和FnOnce做7种自由组合,但其中只有三种traits是有意义的:

  1. Fn/FnMut/FnOnce
  2. FnMut/FnOnce
  3. FnOnce

这是因为,如果传入&self可以解决的问题,传入&mut self也可以解决。传入&mut self可以解决的问题,传入self也可以解决。但反之就不一定成立。

所以self是大哥级的人物,动用了伤害很大,它能够解决一切的问题,所以他是最base的trait,而不是最derive的trait。

闭包对三个trait的实现

  1. 所有的闭包都实现了FnOnce
  2. 如果闭包只移出了所有权,则只实现FnOnce
  3. 如果闭包没移出所捕获变量的所有权,并修改了变量,则实现FnMut
  4. 如果闭包没移出所捕获变量的所有权,且没有修改变量,则实现Fn

move关键字不会改变闭包具体实现的trait,而只影响变量的捕获方式,我们将在下节讨论。

捕获

上面的章节中介绍了闭包可能实现的三个trait,这个章节说明闭包如何捕获环境中的变量。

C++中捕获的问题

在C++中,返回一个捕获了Local变量的闭包,是有安全问题的,见get_f()
对于类中的方法,如果捕获了this指针(即使是[=])并传出,在对象析构之后也是有问题的,见print_this_proxy()

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
33
34
#include <iostream>
#include <functional>
struct S {
int x_ = 0;
S(int x) : x_(x) {}
~S() {
printf("Bye\n");
}
std::function<void()> print_this_proxy() {
return [=](){
printf("x_ %d", x_);
};
}
};

std::function<void()> get_f() {
S s(1);
auto f = [&](){
printf("S %d\n", s.x_);
};
return f;
}

int main(){
auto f = get_f();
f(); // Not safe

std::function<void()> proxy;
{
S s(2);
proxy = s.print_this_proxy();
}
proxy(); // Not safe
}

Rust的捕获

Rust的捕获相比C++使人比较困惑。首先它没有地方指定捕获哪些变量;另外,还有个move关键字;最后还会加上复制和移动语义。

1
2
3
4
5
|| 42;
|x| x + 1;
|x:i32| x + 1;
|x:i32| -> i32 { x + 1 };
move |x:i32| -> i32 { x + 1 };

闭包按照什么方式捕获,取决于我们打算如何使用捕获后的变量。

我们不妨看一个例子,首先定义下面的结构。get_numberinc_numberdestructor分别需要传入不可变引用,可变引用以及值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct MyStruct {
text: &'static str,
number: u32,
}
impl MyStruct {
fn new (text: &'static str, number: u32) -> MyStruct {
MyStruct {
text: text,
number: number,
}
}
fn get_number (&self) -> u32 {
self.number
}
fn inc_number (&mut self) {
self.number += 1;
}
fn destructor (self) {
println!("Destructing {}", self.text);
}
}

下面代码展示了类似fn的情况,这里fn并没有捕获任何自由变量,因此下面的代码可以正常编译和运行。

1
2
3
4
5
6
7
8
9
10
let obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
let closure1 = |x: &MyStruct| x.get_number() + 3;
assert_eq!(closure1(&obj1), 18);
assert_eq!(closure1(&obj2), 13);

is_fn(closure1);
is_Fn(&closure1);
is_FnMut(&closure1);
is_FnOnce(&closure1);

下面的代码展示了Fn的情况,这里closure2捕获了obj1的引用。后面的代码进行验证,仍然可以obj1.get_number()来不可变借用,但需要可变引用的obj1.inc_number()就不能通过编译了。

1
2
3
4
5
6
7
8
9
let obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
// obj1 is borrowed by the closure immutably.
let closure2 = |x: &MyStruct| x.get_number() + obj1.get_number();
assert_eq!(closure2(&obj2), 25);
// We can borrow obj1 again immutably...
assert_eq!(obj1.get_number(), 15);
// But we can't borrow it mutably.
// obj1.inc_number(); // ERROR

事实上,闭包类似语法糖,相当于把需要捕获的上下文封装到一个Context里面传给真正的执行单元。例如我们可以改写closure2,得到一个自由函数func2。它接受一个Context对象,里面封装了一个不可变引用,并且其生命周期等于Context的生命周期。

1
2
3
4
5
6
7
8
9
10
11
12
struct Context<'a>(&'a MyStruct);
let obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
let ctx = Context(&obj1);
fn func2 (context: &Context, x: &MyStruct) -> u32 {
x.get_number() + context.0.get_number()
}
assert_eq!(func2(&ctx, &obj2), 25);
// We can borrow obj1 again immutably...
assert_eq!(obj1.get_number(), 15);
// But we can't borrow it mutably.
// obj1.inc_number(); // ERROR

其实细心观察,就可以提出反对意见。上面的case中不能obj1.inc_number()原因是我们没有let mut obj1 = ...,如果加上去就能正常编译了。这不就是同时Immutable和Mutable Borrow了么?其实我们在最后加一行,再调用一次func2就能报错了。看起来Rust还蛮智能的,func2虽然可变借用,但后续没有用到了,所以就不影响obj1.get_number()

1
2
3
4
...
assert_eq!(func2(&ctx, &obj2), 25);
assert_eq!(obj1.get_number(), 15);
assert_eq!(func2(&ctx, &obj2), 26);

下面的代码展示了FnMut的情况。现在闭包里就直接是可变借用了。在闭包之外,我们既不能可变借用,也不能不变借用,否则都无法编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
let mut obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
// obj1 is borrowed by the closure mutably.
let mut closure3 = |x: &MyStruct| {
obj1.inc_number();
x.get_number() + obj1.get_number()
};
assert_eq!(closure3(&obj2), 26);
assert_eq!(closure3(&obj2), 27);
assert_eq!(closure3(&obj2), 28);
// We can't borrow obj1 mutably or immutably
// assert_eq!(obj1.get_number(), 18); // ERROR
// obj1.inc_number(); // ERROR

下面的代码展示了FnOnce的情况

1
2
3
4
5
6
7
let obj1 = MyStruct::new("Hello", 15);
let obj2 = MyStruct::new("More Text", 10);
// obj1 is owned by the closure
let closure4 = |x: &MyStruct| {
obj1.destructor();
x.get_number()
};

尝试用四个函数检查下,发现上面三个trait的检查都无法通过编译,也就说明closure4没有实现上面三个trait。

1
2
3
4
5
6
// Does not compile:
// is_fn(closure4);
// is_Fn(&closure4);
// is_FnMut(&closure4);
// Compiles successfully:
is_FnOnce(&closure4);

可以发现,闭包捕获变量按照&T -> &mut T -> T的顺序,和Fn -> FnMut -> FnOnce的继承关系如出一辙。也就是先派小弟尝试捕获,小弟解决不了,再请老大出山的思路。

当然,可以通过move关键字,强行请老大出山。

闭包能否被多个线程使用?

https://stackoverflow.com/questions/36211389/can-a-rust-closure-be-used-by-multiple-threads

并发与异步

Send和Sync

trait Send表示该类型的实例可以在线程之间移动。大多数的Rust类型都是Send的,另一些则不可以。比如引用计数智能指针Rc<T>只能在线程内部使用,它就不能被实现为Send的;此外裸指针也不是Send的。由Send类型组成的新类型也是Send的。

trait Sync表示多个线程中拥有该类型实例的引用。换句话说,对于任意类型T,如果&T是Send的,那么T就是Sync的。

不妨看几个Demo:

  1. 如果T是Send和Sync的,那么Arc<T>是Send和Sync的

    1
    2
    unsafe impl<T: ?Sized + Sync + Send> Send for Arc<T> {}
    unsafe impl<T: ?Sized + Sync + Send> Sync for Arc<T> {}

    初看这很奇怪,难道不是为了并发安全才用的Arc么?为什么反过来Arc还需要一个并发安全的类型呢?其实Arc相对Rc只是保证了引用计数这一块功能是并发安全的,所以如果类型不是并发安全的,通常需要配合RwLock和Mutex等使用。

  2. 为各类引用实现Send
    &T需要T是Sync的,这个对应了上面的定义。
    &mut T需要T是Send的

    1
    2
    unsafe impl<T: Sync + ?Sized> Send for &T {}
    unsafe impl<T: Send + ?Sized> Send for &mut T {}
  3. 各类指针都不是Send的

    1
    2
    impl<T: ?Sized> !Send for *const T {}
    impl<T: ?Sized> !Send for *mut T {}
  4. Future的所有权可能在各个线程之间移动,那为什么Future不是Send的呢

并发安全

多线程

因为Rust目前不支持可变参数包,所以只能通过spawn闭包的形式创建线程

如果子线程panic了,其他线程是没影响的,除非:

  1. 某个线程join了panic的线程,此时会得到一个包含Err的Result,如果直接unwrap则会panic
  2. 如果线程在获得锁后panic,这种现象称为poison
    此时,再次尝试mutex.lock()会得到PoisonError,并且mutex.is_poisoned()会返回true。

异步

async和await

下面展示了两种async的写法,一种是async函数plus_one,另一种是async块plus_two。对async函数,编译器在实现时也会最终转成async块的形式,并且会改写函数签名。
所以,可以理解为async函数最终是返回了一个impl Future<Output = ...>类型,impl Future表示这个类型实现了trait Future,这应该就是impl Trait这个特性。Output是我们期望这个Future在Ready后实际返回的类型,比如在这里就是i32。

1
2
3
4
5
6
7
8
9
10
11
async fn initial() -> i32 {
1
}
fn plus_one() -> impl Future<Output = i32> {
async {
initial().await + 1
}
}
async fn plus_two() -> i32 {
initial().await + 2
}

plus_one_resplus_two_res都是Future,可以通过block_on获取结果。

1
2
3
4
5
6
fn main() {
let plus_one_res = plus_one();
let plus_two_res = plus_two();
println!("{}", futures::executor::block_on(plus_one_res));
println!("{}", futures::executor::block_on(plus_two_res));
}

我们也可以通过join同时await多个Future。

1
2
3
4
5
futures::executor::block_on(async {
let j = futures::future::join(plus_one_res, plus_two_res).await;
println!("{}", j.0);
println!("{}", j.1);
});

trait Future

这里指的是std::future::Future,因为在早前还有futures::future::Future,它是一个“社区版”的时候,后来trait Future被整合到了标准库中,剩余部分整合到了pub trait FutureExt: Future,其中包含了map/then等操作。当然对于trait Future还有其他的扩展,例如async-std。但回过头,先来看看最基础的trait Future

1
2
3
4
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

poll函数返回的Poll是个enum,包含Ready(T)Pending两个状态。但它并不只有忙等,如果在一次poll后返回的是Pending,那就会注册cx.waker这个回调,在Future后调用进行通知。

Pin<&mut Self>实际是个指针,它是为了解决自引用结构的问题。

impl Future

impl了trait Future的类型有很多,例如f.map生成的Map,f.then生成的Then这些组合子都是Future。

例如,下面代码为Map类型impl Future。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pub struct Map<I, F> {
// Used for `SplitWhitespace` and `SplitAsciiWhitespace` `as_str` methods
pub(crate) iter: I,
f: F,
}

// in futures, src/future/map.rs
impl<U, A, F> Future for Map<A, F>
where A: Future,
F: FnOnce(A::Item) -> U,
{
type Item = U;
type Error = A::Error;

fn poll(&mut self) -> Poll<U, A::Error> {
let e = match self.future.poll() {
Ok(Async::NotReady) => return Ok(Async::NotReady),
Ok(Async::Ready(e)) => Ok(e),
Err(e) => Err(e),
};
e.map(self.f.take().expect("cannot poll Map twice"))
.map(Async::Ready)
}
}

这里e.map().map()比较独特,前一个map是把self.f应用到e里面的东西,并且清空self.f,让它成为一次性的调用。后一个是把将map的结果包在Async::Ready里面。

async的生命周期

async实现

我们知道,因为在async实现中会产生自引用结构,所以需要用Pin,那什么是自引用结构?为什么async中会存在这种结构呢?
首先得从async的实现讲起

普通情况

看下面代码,f1和f2两个await之间是串行的,那么编译器如何生成f.await的代码呢?

1
2
3
4
5
let f = async move {
f1.await;
f2.await;
}
f.await;

如果是用Then的方式,那么就通过回调实现,但这里Rust使用了状态机的方式,即编译器会生成类似下面的代码AsyncFuture实际上

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
struct AsyncFuture {
fut_one: FutOne,
fut_two: FutTwo,
state: State,
}

enum State {
AwaitingFutOne,
AwaitingFutTwo,
Done,
}

impl Future for AsyncFuture {
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
loop {
match self.state {
State::AwaitingFutOne => match self.fut_one.poll(..) {
Poll::Ready(()) => self.state = State::AwaitingFutTwo,
Poll::Pending => return Poll::Pending,
}
State::AwaitingFutTwo => match self.fut_two.poll(..) {
Poll::Ready(()) => self.state = State::Done,
Poll::Pending => return Poll::Pending,
}
State::Done => return Poll::Ready(()),
}
}
}
}

为什么async实现会涉及自引用结构?

在之前,已经讨论过编译async的普通情况。考虑下面的代码,应该如何编译呢?

1
2
3
4
5
6
async {
let mut x = [0; 128];
let read_into_buf_fut = read_into_buf(&mut x);
read_into_buf_fut.await;
println!("{:?}", x);
}

因为在await时可能发生线程切换,所以我们需要将x也转移到生成的AsyncFuture中,那么read_into_buf就会产生一个指向x的引用。如果在一个结构中,某个字段是指向另一个字段的引用,这就是一个自引用结构。

1
2
3
4
5
6
7
8
struct ReadIntoBuf<'a> {
buf: &'a mut [u8], // points to `x` below
}

struct AsyncFuture {
x: [u8; 128],
read_into_buf_fut: ReadIntoBuf<'what_lifetime?>,
}

自引用结构

自引用结构如下,b是一个指向a的引用

1
2
3
4
struct Test<'a> {
a: String,
b: &'a String,
}

但很遗憾,Rust现在不支持自引用结构,导致下面的代码会报错

1
2
3
4
fn main() {
let a = String::from("Hello");
let _test = Test { a, b: &a };
}

作为workaround,又得用裸指针。但这玩意有个问题,它的地址是绝对的。当Test被移动了,b指向的地址并不会变化。这就好比反过来的刻舟求剑,我们希望b是一个刻在船(Test)上的地址,但实际上它是个GPS坐标。

1
2
3
4
struct Test {
a: String,
b: *const String,
}

then用法

在没有async和await时,可以使用then系列的用法。

类型系统

数组和切片

数组的签名是[T;N],和C++一样,数组类型中包含了它的大小,是编译期常量。数组是否是Copy/Clone取决于其内部的类型,但如果使用[x, N]创建数组,则x对应的类型必须是Copy的。数组引用&[T;N]可以转换为切片引用&[T]

与之对应的是切片&[T]&mut [T]

Sized、!Sized、?Sized和DST

Dynamically sized type(DST),即动态大小类型,表示在编译阶段无法确定大小的类型

如果一个类型在编译期是已知Size,并且Size固定不变的,那么它会自动实现trait Sized。
但有些类型是无法在编译期确定大小的,例如str的大小是未知的(所以我们一般通过&str来访问它),一个trait的大小也是未知的。

如果一个类型的大小是未知的,那么它的使用会有限制,例如我们不能Vec<T>,而只能将T放到Box里面,做成Vec<Box<T>>

胖指针

胖指针指的是指向DST的引用或者指针

1
2
3
4
5
size_of::<&u32>()      = 8
size_of::<&[u32; 2]>() = 8
size_of::<&[u32]>() = 16
size_of::<&[u32]>() = 16
size_of::<&mut [u32]>() = 16

特别强调,指针也是胖的。考虑到C++允许直接使用delete析构POD数组,这也是和C++部分一致的。

1
2
size_of::<*const [u32]>() = 16
size_of::<*mut [u32]>() = 16

可以看到,&[u32]具有两倍大小,原因是其中还储存了一份长度,如下所示

1
2
3
4
struct SliceRef { 
ptr: *const u32,
len: usize,
}

不能直接把变量绑定到一个DST上,因为编译器无法计算出如何分配内存。例如我们经常见到&str,但基本见不到str

除了切片,trait object也是DST,它还包含了一个vptr,将在后面讨论。

1
2
3
4
struct TraitObjectRef {
data_ptr: *const (),
vptr: *const (),
}

Rust中的字符串

Rust中的字符串是很好的比较数组和切片的工具。和C++一样,Rust有两种字符串:

  1. str
    str是Rust的原生字符串类型。因为是DST,所以通常以&str出现。
  2. String
    String类型可以随时修改其长度和内容。

str

&str相关方法实现在str.rs的impl str中。通过.as_ptr()将其转换为一个*const u8指针,通过.len()获得其长度。

字符串字面量的类型是&'static str

&str&[u8]可以互相转换。

String

ZST

在C++中,会接触到这样的类型

1
2
3
4
5
6
struct ZST {
};

sizeof(ZST)

&ZST() != &ZST()

在Rust中

ZST实例的地址是什么呢?

never类型和!

诸如returnbreakcontinuepanic!()loop 没有返回值的,或者说返回值类型是never!,对应到类型理论中就是Bottom类型
never类型可以转换为其他任何类型,所以在诸如match中才能下入如下代码而不会产生类型错误

1
None => panic!

如下的发散函数也没有返回值,因此也具有never类型

1
2
3
4
5
6
fn foo() -> u32
{
let x: ! = {
return 123;
}
}

类型推导

通过turbofish可以辅助推导,下面列出一些例子

1
2
3
4
5
6
7
8
9
x.parse::<i32>()
[
AdminCmdType::CompactLog,
AdminCmdType::ComputeHash,
AdminCmdType::VerifyHash,
]
.iter()
.cloned()
.collect::<std::collections::HashSet<AdminCmdType>>()

trait

trait类似于Haskell中的typeclass。

trait和adhoc多态

见笔记

关联类型

关联类型(associated types)是一个将类型占位符(也就是下面的type Output)与trait相关联的方式。

考虑如果某个类型impl了trait Add,那么它可以接受一个RHS类型的右操作数,并返回Output类型的结果。

1
2
3
4
5
6
7
8
pub trait Add<RHS, Output> {
fn my_add(self, rhs: RHS) -> Output
}
impl Add<u32, u32> for u32 {
fn my_add(self, ths: u32) -> u32 {
self + rhs
}
}

但考虑到trait Add可以接受的RHS可能是多种(例如对String而言可以接受String&str),但返回的Output类型是确定的,所以可以将Output类型从由用户指定改为由实现方指定。此时就可以定义一个关联类型type Output

1
2
3
4
pub trait Add<RHS = Self> {
type Output;
fn add(self, rhs: RHS) -> Self::Output
}

trait的继承

struct不能继承,但是trait可以继承。

这里涉及到泛型约束的问题,例如我们impl的是两个Father的交集还是并集呢?

1
2
3
trait Son: Father1 + Father2 {
}
impl <T: Father1 + Father2> Son for T {}

在这里Father1和Father2是取的交集,也就是说对所有实现了Father1和Father2的T实现Son。

孤儿规则(Orphan Rule)

泛型约束

例如我们实现sum函数,它只能接受泛型参数T是实现了trait Add的。可以这样写

1
fn sum<T: Add<T, Output=T>>

因为使用了关联参数,所以还可以简写成这样

1
fn sum<T: Add<Output=T>>

如果要写的比较多,可以把里面的东西拿出来,用where来写

静态分发和动态分发

静态分发和动态分发是对trait而言的。
下面是静态分发,为fly_static::<Pig>fly_static::<Duck>生成独立的代码。这类似于C++里面的模板实例化。

1
2
3
fn fly_static<T: Fly>(S: T) -> bool{

}

下面是动态分发,在运行期查找fly_dyn(&Fly)对应类型的方法,例如实际传入的是&Duck还是&Pig,是不一样的。这类似C++里面的动态绑定,是有运行时开销的。

1
2
3
fn fly_dyn(S: &Fly) -> bool {

}

问题来了,这里的&Fly是啥呢?实际上这是后面讨论的trait对象。

trait作为存在类型(Existential Type)

存在类型,又被称为无法直接实例化,它的每个实例是具体类型的实例。
对于存在类型,编译期无法知道其功能和Size,目前Rust使用trait object和impl Trait处理存在类型。

trait object

如下所示,fly_dyn中的&Fly参数就是一个trait object。

1
2
3
fn fly_dyn(S: &Fly) -> bool {

}
1
2
3
4
5
6
7
#[repr(C)]
#[derive(Copy, Clone)]
#[allow(missing_debug_implementations)]
pub struct TraitObject {
pub data: *mut (),
pub vtable: *mut (),
}

vtable中包含了对象的析构函数、大小、对齐、方法(也就是虚函数指针)等信息。

只有对象安全的trait才可以作为trait object来使用:

  1. 该trait的Self不能被限定为Sized
  2. 该trait的所有方法必须是对象安全的
    1. 方法受Self: Sized约束
    2. 不包含任何泛型参数
    3. 第一个参数必须为Self类型,或者可以解引用为Self的类型
    4. Self不能出现在出第一个参数之外的地方,包括返回值中

impl Trait

在目前的版本中,不能在trait中返回impl Trait,也就是下面的代码无法编译。只能使用Trait Object。

1
2
3
4
5
pub trait Vehicle {
fn next(&self) -> impl Vehicle;
}

// `impl Trait` not allowed outside of function and inherent method return types

标准库

连接

默认情况下Rust编译时会link标准库,通过添加no_std属性可以关闭这个行为。

字符串

字符串相关的结构之间的转换

包括strString&[u8]Vec<u8>

Option

如何处理Option呢?

  1. unwrap+if
  2. match,并处理Some(e)None
  3. unwrap_or
  4. map组合子,and_then组合子
  5. ?
    得到Result<T, NoneError>

Result

如何处理Result呢?

  1. try!
  2. ?

测试

相比C++的各种测试库,Cargo直接整合了cargo test。

Demo

线程安全的双向链表

Reference

  1. Rust编程之道 by 张汉东
  2. https://learnku.com/docs/rust-async-std/translation-notes/7132
    异步rust学习
  3. https://huangjj27.github.io/async-book/01_getting_started/03_state_of_async_rust.html
    同样是异步教程
  4. https://huangjj27.github.io/async-book/02_execution/02_future.html
    对Future实现的讲解
  5. https://kangxiaoning.github.io/post/2021/04/writing-an-os-in-rust-01/
    这个是用Rust写操作系统的教程,这一节讲的是如何移除标准库
  6. https://www.cnblogs.com/praying/p/14179397.html
    future的实现,不关注async相关,包含各种组合子
  7. https://cloud.tencent.com/developer/article/1628311
    对pin的讲解
  8. https://folyd.com/blog/rust-pin-unpin/
    对pin的讲解
  9. https://doc.rust-lang.org/std/pin/
    pin的官方文档
  10. https://www.zhihu.com/question/470049587
    AsRef/Borrow/Deref的讲解
  11. https://dengjianping.github.io/2019/03/05/%E8%B0%88%E4%B8%80%E8%B0%88Fn,-FnMut,-FnOnce%E7%9A%84%E5%8C%BA%E5%88%AB.html
    Fn FnOnce FnMut的区别
  12. https://zhuanlan.zhihu.com/p/341815515
    对闭包的论述
  13. https://medium.com/swlh/understanding-closures-in-rust-21f286ed1759
    对闭包的说明
  14. https://stackoverflow.com/questions/59593989/what-will-happen-in-rust-if-create-mutable-variable-and-mutable-reference-and-ch
    Owner和&mut是否可以同时修改?