[ Rust笔记 十三] 内存安全-借用检查

两个概念:

  1. Alias的意思是“别名”​。如果一个变量可以通过多种Path来访问,那它们就可以互相看作alias。Alias意味着“共享”​,我们可以通过多个入口访问同一块内存。
  2. Mutation的意思是“改变”​。如果我们通过某个变量修改了一块内存,就是发生了mutation。Mutation意味着拥有“修改”权限,我们可以写入数据。

共享不可变,可变不共享

编译错误示例

通过不同的Path访问同一块内存p, *p1, * p2,所以它们存在“共享”​。而且它们都只有只读的权限,所以它们存在“共享”​,不存在“可变”​。因此它一定是安全的。

fn main() {//正常
    let i = 0;
    let p1 = & i;
    let p2 = & i;
    println! ("{} {} {}", i, p1, p2);
}

在存在只读借用的情况下,变量绑定i和p1已经互为alias,它们之间存在“共享”​,因此必须避免“可变”​。这段代码违反了“共享不可变”的原则。

fn main() {//报错
    let mut i = 0;
    let p1 = & i;
    i = 1;
}

因为这段代码中不存在“共享”​。在可变借用存在的时候,编译器认为原来的变量绑定i已经被冻结(frozen)​,不可通过i读写变量。此时有且仅有p1这一个入口可以读写变量。

fn main() {//正常
    let mut i = 0;
    let p1 = &mut i;
    *p1 = 1;
}

因为p1、p2都是可变借用,它们都指向了同一个变量,而且都有修改权限,这是Rust不允许的情况,因此这段代码无法编译通过。

 fn main() {//报错
    let mut i = 0;
    let p1 = &mut i;
    let p2 = &mut i;
}

&mut型借用也经常被称为“独占指针”, &型借用也经常被称为“共享指针”​。

Read More

[ Rust笔记 十二] 内存安全-借用和生命周期

生命周期

先看一个示例

1
2
3
4
5
6
7
8
fn main() {
let v = vec! [1,2,3,4,5]; // --> v 的生命周期开始
{
let center = v[2]; // --> center 的生命周期开始
println! ("{}", center);
} // <-- center 的生命周期结束
println! ("{:? }", v);
} // <-- v 的生命周期结束

借用

变量对其管理的内存拥有所有权。这个所有权不仅可以被转移(move)​,还可以被借用(borrow)。

借用指针的语法使用

  1. &符号只读借用
  2. &mut可读写借用

借用指针与普通指针的内部数据是一模一样的,唯一的区别是语义层面上的。它的作用是告诉编译器,它对指向的这块内存区域没有所有权。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
let mut var = 0_i32;
{
let p1 = &mut var; // p1 指针本身不能被重新绑定,我们可以通过p1改变变量var的值
*p1 = 1;
}
{
let temp = 2_i32;
let mut p2 = &var; // 我们不能通过p2改变变量var的值,但p2指针本身指向的位置可
以被改变
p2 = &temp;
}
{
let mut temp = 3_i32;
let mut p3 = &mut var; // 我们既可以通过p3改变变量var的值,而且p3指针本身指向
的位置也可以改变
*p3 = 3;
p3 = &mut temp;
}
}

借用指针在编译后,实际上就是一个普通的指针,它的意义只能在编译阶段的静态检查中体现。

借用规则

  1. 借用指针不能比它指向的变量存在的时间更长。
  2. &mut型借用只能指向本身具有mut修饰的变量,对于只读变量,不可以有&mut型借用。
  3. &mut型借用指针存在的时候,被借用的变量本身会处于“冻结”状态。
  4. 如果只有&型借用指针,那么能同时存在多个;如果存在&mut型借用指针,那么只能存在一个;如果同时有其他的&或者&mut型借用指针存在,那么会出现编译错误。

Read More

[ Rust笔记 十一] 内存安全-所有权和移动语义

所有权

“所有权”代表着以下意义:

  1. 每个值在Rust中都有一个变量来管理它,这个变量就是这个值、这块内存的所有者;

  2. 每个值在一个时间点上只有一个管理者;

  3. 当变量所在的作用域结束的时候,变量以及它代表的值将会被销毁。

移动语义

一个变量可以把它拥有的值转移给另外一个变量,称为“所有权转移”​。赋值语句、函数调用、函数返回等,都有可能导致所有权转移。

1
2
3
4
5
6
7
8
9
10
11
fn create() -> String {
let s = String::from("hello");
return s; // 所有权转移,从函数内部移动到外部
}
fn consume(s: String) { // 所有权转移,从函数外部移动到内部
println! ("{}", s);
}
fn main() {
let s = create();
consume(s);
}

所有权转移的步骤分解如下。

  1. main函数调用create函数。

  2. 在调用create函数的时候创建了字符串,在栈上和堆上都分配有内存。局部变量s是这些内存的所有者。

  3. create函数返回的时候,需要将局部变量s移动到函数外面,这个过程就是简单地按字节复制memcpy。

  4. 同理,在调用consume函数的时候,需要将main函数中的局部变量转移到consume函数,这个过程也是简单地按字节复制memcpy。

  5. 当consume函数结束的时候,它并没有把内部的局部变量再转移出来,这种情况下,consume内部局部变量的生命周期就该结束了。这个局部变量s生命周期结束的时候,会自动释放它所拥有的内存,因此字符串也就被释放了。

Rust中所有权转移的重要特点是,它是所有类型的默认语义。这是许多读者一开始不习惯的地方。这里再重复一遍,请大家牢牢记住,Rust中的变量绑定操作,默认是move语义,执行了新的变量绑定后,原来的变量就不能再被使用!一定要记住!

Read More

[ Rust笔记 九] 基础-宏

示例

两种方式实现自定义宏:

  1. 通过标准库提供的macro_rules!宏实现

  2. 通过提供编译器扩展来实现

编译器扩展只能在不稳定版本中使用。它的API正在重新设计中,还没有正式定稿,这就是所谓的macro 2.0。

实现这样一个宏定义

1
let counts = hashmap! ['A' => 0, 'C' => 0, 'G' => 0, 'T' => 0];

实现了hashmap! {‘A’ => ‘1’}

1
2
3
4
5
6
macro_rules! hashmap {
($key: expr => $val: expr) => {
// 暂时空着
()
}
}

我们希望这个宏扩展开后的类型是HashMap,而且进行了合理的初始化,那么我们可以使用“语句块”的方式来实现:

1
2
3
4
5
6
7
8
9
macro_rules! hashmap {
($key: expr => $val: expr) => {
{
let mut map = ::std::collections::HashMap::new();
map.insert($key, $val);
map
}
}
}

现在我们希望在宏里面,可以支持重复多个这样的语法元素。我们可以使用+模式和*模式来完成。类似正则表达式的概念,+代表一个或者多个重复,*代表零个或者多个重复。因此,我们需要把需要重复的部分用括号括起来,并加上逗号分隔符:

1
2
3
4
5
6
7
macro_rules! hashmap {
($( $key: expr => $val: expr ), *) => {{
let mut map = ::std::collections::HashMap::new();
map.insert($key, $val);
map
}}
}

最后,我们在语法扩展的部分也使用*符号,将输入部分扩展为多条insert语句。最终的结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
macro_rules! hashmap {
($( $key: expr => $val: expr ), *) => {{
let mut map = ::std::collections::HashMap::new();
$( map.insert($key, $val); )*
map
}}
}
fn main() {
let counts = hashmap! ['A' => 0, 'C' => 0, 'G' => 0, 'T' => 0];
println! ("{:? }", counts);
}

宏1.1

对于一些简单的宏,这种“示例型”​(by example)的方式足够使用了。但是更复杂的逻辑则需要通过更复杂的方式来实现,这就是所谓的“过程宏”​(procedural macro)​。它是直接用Rust语言写出来的,相当于一个编译器插件。但是编译器插件的最大问题是,它依赖于编译器的内部实现方式。一旦编译器内部有所变化,那么对应的宏就有可能出现编译错误,需要修改。因此,Rust中的“宏”一直难以稳定。

[ Rust笔记 七] 基础-模式解构(Pattern Destructure)

简介

1
2
let tuple = (1_i32, false, 3f32);
let (head, center, tail) = tuple;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct T1 (i32, char);
struct T2 {
item1: T1,
item2: bool,
}

fn main()
{
let x = T2 {
item1: T1(0, 'A'),
item2: false,
};
let T2 {
item1: T1(value1, value2),
item2: value3,
} = x;
println!("{} {} {}", value1, value2, value3);
}

match

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
enum Direction {
East, West, South, North
}
fn print(x: Direction)
{
match x {
Direction::East => {
println!("East");
}
Direction::West => {
println!("West");
}
Direction::South => {
println!("South");
}
Direction::North => {
println!("North");
}
}
}
fn main() {
let x = Direction::East;
print(x);
}

Read More

[ Rust笔记 六] 基础-数组和字符串

数组

简介

数组本身所容纳的元素个数也必须是编译期确定的,执行阶段不可变。数组类型的表示方式为[T; n]​。其中T代表元素类型;n代表元素个数。

1
2
3
4
5
6
fn main() {
// 定长数组
let xs: [i32; 5] = [1, 2, 3, 4, 5];
// 所有的元素,如果初始化为同样的数据,可以使用如下语法
let ys: [i32; 500] = [0; 500];
}

在Rust中,对于两个数组类型,只有元素类型和元素个数都完全相同,这两个数组才是同类型的。

1
2
3
4
5
6
fn main() {
let mut xs: [i32; 5] = [1, 2, 3, 4, 5];
let ys: [i32; 5] = [6, 7, 8, 9, 10];
xs = ys; //同类型,正常赋值
println!("new array {:? }", xs);
}

把数组xs作为参数传给一个函数,这个数组并不会退化成一个指针。而是会将这个数组完整复制进这个函数。函数体内对数组的改动不会影响到外面的数组。

内置方法

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let v1 = [1, 2, 3];
let v2 = [1, 2, 4];
println!("{:? }", v1 < v2 );//比较大小
}

fn main() {
let v = [0_i32; 10];
for i in &v {//遍历切片
println!("{:? }", i);
}
}

数组本身没有实现IntoIterator trait,但是数组切片是实现了的。所以我们可以直接在for in循环中使用数组切片,而不能直接使用数组本身。

多维数组

1
2
3
4
5
6
fn main() {
let v : [[i32; 2]; 3] = [[0, 0], [0, 0], [0, 0]];
for i in &v {
println!("{:? }", i);
}
}

数组切片

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
fn mut_array(a : &mut [i32]) {
a[2] = 5;
}
println!("size of &[i32; 3] : {:? }", std::mem::size_of::<&[i32; 3]>());
println!("size of &[i32] : {:? }", std::mem::size_of::<&[i32]>());
let mut v : [i32; 3] = [1,2,3];
{
let s : &mut [i32; 3] = &mut v;
mut_array(s);
}
println!("{:? }", v);
}

DST和胖指针

Slice与普通的指针是不同的,它有一个非常形象的名字:胖指针(fat pointer)​。与这个概念相对应的概念是“动态大小类型”​(Dynamic Sized Type, DST)​。所谓的DST指的是编译阶段无法确定占用空间大小的类型。为了安全性,指向DST的指针一般是胖指针。

Read More

[ Rust笔记 五] 基础-trait

成员方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
trait Shape {
fn area(&self) -> f64;//关联函数,参数被称为接收者
}

trait T {
fn method1(self: Self);
fn method2(self: &Self);
fn method3(self: &mut Self);
}
// 上下两种写法是完全一样的
trait T {
fn method1(self);
fn method2(&self);
fn method3(&mut self);
}

具有receiver参数的函数,我们称为“方法”​(method)​,可以通过变量实例使用小数点来调用。
没有receiver参数的函数,我们称为“静态函数”​(static function)​,可以通过类型加双冒号::的方式来调用。

Rust中Self(大写S)和self(小写s)都是关键字,大写S的是类型名,小写s的是变量名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Circle {
radius: f64,
}
impl Shape for Circle {
// Self 类型就是 Circle
// self 的类型是 &Self,即 &Circle
fn area(&self) -> f64 {
// 访问成员变量,需要用 self.radius
std::f64::consts::PI * self.radius * self.radius
}
}
fn main() {
let c = Circle { radius : 2f64};
// 第一个参数名字是 self,可以使用小数点语法调用
println!("The area is {}", c.area());
}

另一种写法“内在方法”

1
2
3
impl Circle {
fn get_radius(&self) -> f64 { self.radius }
}

trait中可以包含方法的默认实现。如果这个方法在trait中已经有了方法体,那么在针对具体类型实现的时候,就可以选择不用重写。

Read More

[ Rust笔记 四] 基础-函数

简介

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
   fn add1(t : (i32, i32)) -> i32 { //模式解构
t.0 + t.1
}

fn add2((x, y) : (i32, i32)) -> i32 {//和上述写法一样
x + y
}


fn main() {
// 先让 func 指向 add1
let mut func = add1;
// 再重新赋值,让 func 指向 add2
func = add2;
}

会报错,类型不一致。每个函数都有自己得类型。

1
2
3
4
// 写法一,用 as 类型转换
let mut func = add1 as fn((i32, i32))->i32;
// 写法二,用显式类型标记
let mut func : fn((i32, i32))->i32 = add1;

Rust的函数体内也允许定义其他item,包括静态变量、常量、函数、trait、类型、模块等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn test_inner() {
static INNER_STATIC: i64 = 42;
// 函数内部定义的函数
fn internal_incr(x: i64) -> i64 {
x + 1
}
struct InnerTemp(i64);
impl InnerTemp {
fn incr(&mut self) {
self.0 = internal_incr(self.0);
}
}
// 函数体,执行语句
let mut t = InnerTemp(INNER_STATIC);
t.incr();
println!("{}", t.0);
}

发散函数

1
2
3
4
5
6
7
8
9
10
11
12
fn diverges() -> ! {
panic! ("This function never returns! ");
}

let x : i32 = diverges();
let y : String = diverges();

let p = if x {
panic!("error");
} else {
100
};

可以被转换成任意类型

Read More

[ Rust笔记 三] 基础-复合数据类型

tuple

1
2
3
4
5
6
7
8
9
10
11
   let a = (1i32, false);        // 元组中包含两个元素,第一个是i32类型,第二个是bool类型
let b = ("a", (1i32, 2i32)); // 元组中包含两个元素,第二个元素本身也是元组,它又包含了两个元素
let a = (0, ); // a是一个元组,它有一个元素
let b = (0); // b是一个括号表达式,它是i32类型

let p = (1i32, 2i32);
let (a, b) = p;

let x = p.0;
let y = p.1;
println!("{} {} {} {}", a, b, x, y);

元组内部也可以一个元素都没有。这个类型单独有一个名字,叫unit(单元类型)​:

1
let empty : () = ();

unit类型是Rust中最简单的类型之一,也是占用空间最小的类型之一。空元组和空结构体struct Foo;一样,都是占用0内存空间。

1
2
3
4
5
fn main() {
println!("size of i8 {}" , std::mem::size_of::<i8>());
println!("size of char {}" , std::mem::size_of::<char>());
println!("size of '()' {}" , std::mem::size_of::<()>());
}

struct

1
2
3
4
struct Point {
x: i32,
y: i32,
}

初始化,类json语法初始化

1
2
3
4
fn main() {
let p = Point { x: 0, y: 0};
println!("Point is at {} {}", p.x, p.y);
}

使用局部变量初始化

1
2
3
4
5
6
7
8
fn main() {
// 刚好局部变量名字和结构体成员名字一致
let x = 10;
let y = 20;
// 下面是简略写法,等同于 Point { x: x, y: y },同名字的相对应
let p = Point { x, y };
println!("Point is at {} {}", p.x, p.y);
}

使用语法糖赋值

1
2
3
4
5
6
7
8
9
10
11
12
struct Point3d {
x: i32,
y: i32,
z: i32,
}
fn default() -> Point3d {
Point3d { x: 0, y: 0, z: 0 }
}
// 可以使用default()函数初始化其他的元素
// ..expr 这样的语法,只能放在初始化表达式中,所有成员的最后最多只能有一个
let origin = Point3d { x: 5, ..default()};
let point = Point3d { z: 1, x: 2, ..origin };

Read More

[ Rust笔记 二] 基础-基本类型

bool

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  fn main() {
let x = true;
let y: bool = !x; // 取反运算
let z = x && y; // 逻辑与,带短路功能
println!("{}", z);
let z = x || y; // 逻辑或,带短路功能
println!("{}", z);
let z = x & y; // 按位与,不带短路功能
println!("{}", z);
let z = x | y; // 按位或,不带短路功能
println!("{}", z);
let z = x ^ y; // 按位异或,不带短路功能
println!("{}", z);
}

char

1
2
3
4
5
6
7
8
9
10
11
12
13
   unicode
都是4个字节
let love = '❤'; // 可以直接嵌入任何unicode字符
转义符
let c1 = '\n'; // 换行符
let c2 = '\x7f'; // 8 bit字符变量
let c3 = '\u{7FFF}'; // unicode字符

都是1个字节
let x :u8 = 1;
let y :u8 = b'A';
let s :&[u8;5] = b"hello";
let r :&[u8;14] = br#"hello \n world"#;

整数类型

i8 u8, i16 u16 … i128, u128, isize, usize

1
2
3
4
5
6
7
8
9
let var1 : i32 = 32;      // 十进制表示
let var2 : i32 = 0xFF; // 以0x开头代表十六进制表示
let var3 : i32 = 0o55; // 以0o开头代表八进制表示
let var4 : i32 = 0b1001; // 以0b开头代表二进制表示
let var6 = 123usize; // i6变量是usize类型
let var7 = 0x_ff_u8; // i7变量是u8类型
let var8 = 32; // 不写类型,默认为 i32 类型

let var5 = 0x_1234_ABCD; // 使用下划线分割数字,不影响语义,但是极大地提升了阅读体验。

(pointer size)isize和usize占用字节数和32位或者64位系统有关系

浮点类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
   let f1 = 123.0f64;         // type f64
let f2 = 0.1f64; // type f64
let f3 = 0.1f32; // type f32
let f4 = 12E+99_f64; // type f64 科学计数法
let f5 : f64 = 2.; // type f64


enum FpCategory {
Nan,
Infinite,
Zero,
Subnormal,
Normal,
}

Read More