Rust 基础-变量和数据类型(第一章)

1. 变量

  • 变量 : 声明时用 let 关键字,默认不可变,但可再增加 mut 声明成可变
  • 常量 : 声明时用 const 关键字,且必须标注类型,可以声明在任何作用域里。只能绑定常量表达式。规范是全部大写,且用下划线连接多个单词。

变量特点:

  • 变量绑定和所有权:使用 变量绑定 来代替 变量赋值,是因为 Rust 中,变量是会有所有权交换的
  • 变量不可变:默认情况下变量是不可变的,一旦为它绑定值,就不能再修改
  • 变量忽略:当创建变量而未使用时,Rust 会提出警告,可以用下划线作为变量名的开头来取消警告
  • 变量解构:可以使用 let 来进行复杂变量解构,只取出一部分内容,并可以使用元组、切片、结构体给解构式赋值
  • 变量遮蔽:可以使用相同名字声明新变量,此时会将之前的声明覆盖掉
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
// 变量不可变:如下,cargo run 会报错,cannot assign twice to immutable variable y
let y = 5;
y = 6;
println!("The value of y is: {}", y);

// 变量解构:t 不可变,f 可变
let (t, mut f): (bool,bool) = (true, false);
println!("t = {:?}, f = {:?}", t, f);
f = true;
assert_eq!(t, f);

// 变量解构:在赋值语句的左式中使用元组、切片和结构体模式
let (a, b, c, d, e);
(a, b) = (1, 2);
// _ 代表匹配一个值,但不关心具体的值,使用 _ 忽略
[c, .., d, _] = [1, 2, 3, 4, 5];
Struct { e, .. } = Struct { e: 5 };
assert_eq!([1, 2, 1, 4, 5], [a, b, c, d, e]);

// 变量遮蔽:和 mut 变量不同,let 生成同名的新变量,且涉及内存对象的重新分配
let x = 5;
// 在main函数的作用域内对之前的x进行遮蔽
let x = x + 1;// x=6
{
// 在当前的花括号作用域内,对之前的x进行遮蔽
let x = x * 2;
println!("The value of x in the inner scope is: {}", x);// x=12
}
println!("The value of x is: {}", x);// x=6

2. 所有权和借用

通过所有权来管理内存,编译器在编译时会根据一系列规则进行检查,因此对于程序运行期,不会有任何性能上的损失。

所有权原则:

  • Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
  • 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
  • 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)

堆和栈:

  • 栈:后进先出,所有数据都必须占用已知且固定大小的内存空间
  • 堆:存储大小未知或者可能变化的数据,当堆上放入数据时,需要请求一定大小的内存空间,并返回一个表示该位置地址的指针,,该指针会被推入栈中

1. 转移所有权

对于基本数据类型,是通过自动拷贝的方式来赋值的,都被存在栈中。

1
2
let x = 5;
let y = x;

复杂类型,由存储在栈中的 堆指针、字符串长度、字符串容量 组成,为了提高性能,拷贝时只会字面量本身(浅拷贝),如下方式,就会违背 “一个值只允许有一个所有者” 的原则,可能导致 “二次释放” 问题,因此 Rust 中,解决方法是:当 s1 被赋予 s2 后,所有权从 s1 转移到 s2,s1 马上失效。

1
2
3
4
// 转移所有权:s1 会变成无效引用,使用时会报错
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);

2. 引用与借用

仅通过转移所有权的方式获取一个值,会让程序变复杂,可以通过获取变量的引用简化操作,也叫做借用。常规引用是一个指针类型,指向了对象存储的内存地址。

1
2
3
4
5
6
7
fn main() {
let x = 5;
let y = &x;// y 是 x 的一个引用

assert_eq!(5, x);
assert_eq!(5, *y);// *y 解引用,才能使用值
}

借用规则:

  • 同一时刻,只能拥有一个可变引用, 或者任意多个不可变引用
  • 引用必须总是有效的
1
2
3
4
5
6
7
8
9
10
11
12
13
// 不可变引用:引用指向的值默认不可变
fn main() {
let s = String::from("hello");
change(&s);
}

fn change(some_string: &String) {// 报错,some_string 不可变
some_string.push_str(", world");
}

fn change(some_string: &mut String) {// 使用 &mut 创建可变引用
some_string.push_str(", world");
}

1. 可变引用与不可变引用不能同时存在

同一作用域,特定数据只能有一个可变引用,但不可变的可以有多个,即借用规则第一条。

1
2
3
4
5
6
7
let mut s = String::from("hello");

let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 大问题

println!("{}, {}, and {}", r1, r2, r3);

2. NLL(Non-Lexical Lifetimes)

它是一个 Rust 编译器优化行为,用于找到某个引用在作用域结束前就不再被使用的代码位置。

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let mut s = String::from("hello");

let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// 新编译器中,r1,r2作用域在这里结束

let r3 = &mut s;
println!("{}", r3);
} // 老编译器中,r1、r2、r3作用域在这里结束
// 新编译器中,r3作用域在这里结束

3. 悬垂引用(Dangling References)

悬垂引用也叫做悬垂指针,意为指针指向的值被释放了,而指针仍然存在,在 Rust 中编译器可以确保引用永远也不会变成悬垂状态。

3. 数据类型

作为静态语言,编译时需要知道其类型,当可能得类型比较多时(如字符串转数字),不指定类型就会编译报错。

1. 标量类型

标量(scalar)类型表示单个值,由以下组成:

  • 整数类型:有无符号分为 iu,长度为 8,16,32,64,128bit,特殊的有 isizeusize ,共有 12 种了类型
    • i32 为默认类型
    • isizeusize,根据系统架构决定,如 32 位计算机长度为 32bit
    • 可以使用如下任意形式来编写整型的字面量,如使用类型后缀来指定类型(57u8),使用 _ 作为可视分隔符(1_000)
    • 对于整型溢出,当使用调试模式时,会抛出 panic,发布模式下则会直接环绕,如 u8 时,256变0,257变1
  • 浮点类型:分为2种,f32 单精度32位,f64 双精度64位,
    • f64 为默认类型
  • 布尔类型:符号为 bool,占1个字节,分为 2 种,truefalse
  • 字符类型:符号为 char,占4个字节,字面量使用单引号,表示一个 Unicode 标量值
数字字面量 示例
十进制 98_222
十六进制 0xff
八进制 0o77
二进制 0b1111_0000
字节 b’A’

2. 数字运算

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
fn main() {
// 编译器会进行自动推导,给予twenty i32的类型
let twenty = 20;
// 类型标注
let twenty_one: i32 = 21;
// 通过类型后缀的方式进行类型标注:22是i32类型
let twenty_two = 22i32;

// 只有同样类型,才能运算
let addition = twenty + twenty_one + twenty_two;
println!("{} + {} + {} = {}", twenty, twenty_one, twenty_two, addition);

// 对于较长的数字,可以用_进行分割,提升可读性
let one_million: i64 = 1_000_000;
println!("{}", one_million.pow(2));

// 定义一个f32数组,其中42.0会自动被推导为f32类型
let forty_twos = [
42.0,
42f32,
42.0_f32,
];

// 打印数组中第一个值,并控制小数位为2位
println!("{:.2}", forty_twos[0]);
}

3. 位运算

位运算符除了 ! 之外都可以加上 = 进行赋值,因为 ! = 要用来判断不等于

运算符 说明
& 位与 相同位置均为1时则为1,否则为0
\| 位或 相同位置只要有1时则为1,否则为0
^ 异或 相同位置不相同则为1,相同则为0
! 位非 把位中的0和1相互取反,即0置为1,1置为0
<< 左移 所有位向左移动指定位数,右位补0
>> 右移 所有位向右移动指定位数,带符号移动(正数补0,负数补1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fn main() {
// 二进制为00000010
let a:i32 = 2;
// 二进制为00000011
let b:i32 = 3;

println!("(a & b) value is {}", a & b);

println!("(a | b) value is {}", a | b);

println!("(a ^ b) value is {}", a ^ b);

println!("(!b) value is {} ", !b);

println!("(a << b) value is {}", a << b);

println!("(a >> b) value is {}", a >> b);

let mut a = a;
// 赋值
a <<= b;
println!("(a << b) value is {}", a);
}

2. 复合类型

1. Tuple 元组

可以存放多种数据类型,且长度是固定的,一旦声明就无法改变。

1
2
3
4
5
6
// 创建
let tup = (1,2,3,4.1)
// 访问: 使用访问下标方式
let s = tup.0;
// 结构: 使用解构方式
let (x,y,z) = tup;
  • 没有任何值的元组 () 是一种特殊的类型,被称为单元类型(unit type),它只有一个值 (),该值被称为单元值(unit value)。
  • 如果表达式不返回任何其他值,就隐式地返回单元值

2. 数组

只能存放单一数据类型,且长度是固定的,存在栈内存中。当下标溢出时,编译通过,但运行时报错。

1
2
3
4
5
6
7
8
9
10
// 创建: 可以显示声明类型或者省略
let a = [1,2,3]; // rust推断为[i32;3]
let a: [u8; 3] = [1, 2, 3]; // [类型;长度]
// 对于相同内容的,可以简写
let a = [3;5]; // 相当于 let a = [3,3,3,3,3]
// 访问: 使用访问下标方式
let x = a[0];
let y = a[1];
// 访问: 使用解构方式
let [x, y, ..] = a; // .. 符号表示,此时只解构出第一和第二个元素