Rust基础学习(一)——变量和数据类型

paw5zx Lv4

变量

变量命名

需要遵循Rust命名规范

变量绑定

在其他语言中我们可以这么给变量a赋值:

1
std::string a = "hello world";

而在Rust中,我们这样写:

1
let a = "hello world";

我们称这个操作为变量绑定(bind)。称之为绑定而不是赋值的原因,涉及到Rust最核心的原则——所有权。简单来讲,任何内存对象都有一个明确的所有者,该所有者负责管理该对象的生命周期

变量可变性

Rust变量默认是不可变的,一旦为变量绑定值,就不能再修改这个变量:

1
2
3
4
5
6
7
fn main()
{
let x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is: {}", x);
}

会报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:5:5
|
3 | let x = 5;
| - first assignment to `x`
4 | println!("The value of x is: {}", x);
5 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
|
help: consider making this binding mutable
|
3 | let mut x = 5;
| +++

这是因为我们想为不可变的变量x再次赋值。

若想创建一个可变变量,在变量名前加一个mut即可:

1
2
3
4
5
6
7
fn main()
{
let mut x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is: {}", x);
}

忽略变量

如果创建了一个变量却从未使用,Rust编译器通常会给出一个警告,因为这可能意味着程序中存在潜在的错误或冗余代码。这种警告有助于提升代码的质量和效率。

如果想告诉编译器不要警告未使用的变量,可以在变量名称前加上下划线:

1
2
3
4
5
6
fn main()
{
// 变量x和y都只有声明,没有使用。
let _x = 5;
let y = 10;
}

会有以下警告:

1
2
3
4
5
6
7
warning: unused variable: `y`
--> src/main.rs:4:9
|
4 | let y = 10;
| ^ help: if this is intentional, prefix it with an underscore: `_y`
|
= note: `#[warn(unused_variables)]` on by default

变量解构

let表达式不仅仅用于变量的绑定,还能进行复杂变量的解构:从一个相对复杂的变量中,匹配出该变量的一部分内容:

1
2
3
4
5
6
7
8
9
10
fn main()
{
// a = true,不可变; b = false,可变
let (a, mut b): (bool,bool) = (true, false);

println!("a = {:?}, b = {:?}", a, b);

b = true;
assert_eq!(a, b);
}

在Rust 1.59版本后,我们可以在赋值语句的左式中使用元组、切片和结构体模式:

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

fn main()
{
// 声明变量
let (a, b, c, d, e);
// 元组解构
(a, b) = (1, 2);
// 数组解构,..表示忽略中间的值;_是一个占位符,用于匹配数组最后一个元素5,但不将其存储到任何变量中
// c被赋值为第一个元素1,d赋值为倒数第二个元素4
[c, .., d, _] = [1, 2, 3, 4, 5];
// 结构体解构
Struct { e, .. } = Struct { e: 5 };

assert_eq!([1, 2, 1, 4, 5], [a, b, c, d, e]);
}

详情见模式匹配

常量

与不可变变量一样,常量(constants)是绑定到名称并且不允许更改的值,但是常量和变量之间存在一些区别。

  • 常量不允许使用mut关键字。常量不仅仅默认不可变,而且自始至终不可变,因为常量在编译完成后,就已经确定它的值
  • 常量使用const关键字而不是let关键字来声明,并且必须对值的类型进行标注(annotate)

常量可以在任何作用域中声明,包括全局作用域。常量在程序整个运行期间有效,在声明它们的作用域内有效。对于需要在多处代码共享一个不可变的值时非常有用。在实际使用中,最好将程序中用到的硬编码值都声明为常量,对于代码后续的维护有莫大的帮助。如果将来需要更改硬编码的值,你也只需要在代码中更改一处即可。

下面是一个常量声明的两个例子

1
2
const MAX_POINTS: u32 = 100_000;
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
  • ①常量名为MAX_POINTS,值设置为100000。(Rust常量的命名约定是全部字母都使用大写,并使用下划线分隔单词;对数字字面量可插入下划线以提高可读性)
  • ②常量名为THREE_HOURS_IN_SECONDS,它的值被设置为60(一分钟内的秒数)乘60(一小时内的分钟数)乘3(我们想在这个程序中计算的小时数)的结果。编译器能够在编译时计算一组有限的操作,这让我们可以选择以一种更容易理解和验证的方式写出这个值,而不是将这个常数直接设置为10800。

变量遮蔽

Rust允许声明一个与之前变量同名的新变量,在后面声明的变量会遮蔽(shadow)掉前面声明的,这意味着当使用变量名时,编译期将看到后面的变量,直到这个后面的变量被遮蔽或出作用域,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
fn main()
{
let x = 5;
// 在main函数的作用域内对之前的x进行遮蔽
let x = x + 1;
{
// 在当前的花括号作用域内,对之前的x进行遮蔽
let x = x * 2;
println!("The value of x in the inner scope is: {}", x);
}
println!("The value of x is: {}", x);
}

这个程序首先将数值5绑定到x,然后通过重复使用let x =来创建一个新的变量x,并取原来的值加上1,所以x的值变成了6。然后在用花括号创建的内部作用域中,第三个let语句同样创建一个新的变量x并遮蔽前面的x,取之前的值并乘上2,得到的x最终值为 12。当出花括号作用域后,内部遮蔽结束,x的值还是6

当运行这个程序时,它将输出如下内容:

1
2
3
4
5
   Compiling world_hello v0.1.0 (/setup/rust-code/world_hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.35s
Running `target/debug/world_hello`
The value of x in the inner scope is: 12
The value of x is: 6

注意,遮蔽不同于将变量标记为mut,虽然他们都涉及到变量的修改:

  • 当使用mut关键字声明一个变量时,将标记这个变量为可变的,这意味着可以在同一个内存地址上修改这个变量的值。由于没有发生变量的重新声明或内存重新分配,性能开销较低。
  • 当使用变量遮蔽时,尽管变量名相同,但实际上是创建了一个全新的变量(所以我们可以改变值的类型)。这个新的变量将有自己的内存分配,与原始变量完全独立。每次遮蔽都可能涉及到内存的重新分配(取决于遮蔽的变量类型和上下文)(作为使用者可以这么理解,实际上编译器可能出于优化目的在物理上重用相同的内存位置),这可能会比简单修改现有变量的值有更多的性能开销。

示例:
遮蔽过程中,spaces变量的类型从&str更改为usize

1
2
3
4
// 字符串类型
let spaces = " ";
// usize数值类型
let spaces = spaces.len();

当然如果你将上述代码修改为:

1
2
let mut spaces = "   ";
spaces = spaces.len();

显然是不对的,因为Rust对类型的要求很严格,不允许将usize赋值给&str类型变量。

数据类型

Rust是一门静态类型语言,也就是说编译器必须在编译期知道所有变量的类型。编译器通常可以根据值和使用方式自动推导出变量的类型。但是在某些情况下,它却无法推导出变量类型,需要手动去给予一个类型标注:

1
let guess = "42".parse().expect("Not a number!");

这段代码的目的是将字符串”42”进行解析,而编译器在这里无法推导出我们想要的类型:整数?浮点数?字符串?因此编译器会报错:

1
2
3
4
5
6
7
8
9
10
11
error[E0284]: type annotations needed
--> src/main.rs:3:9
|
3 | let guess = "42".parse().expect("Not a number!");
| ^^^^^ ----- type must be known at this point
|
= note: cannot satisfy `<_ as FromStr>::Err == _`
help: consider giving `guess` an explicit type
|
3 | let guess: /* Type */ = "42".parse().expect("Not a number!");
| ++++++++++++

因此我们需要提供给编译器更多的信息,例如给guess变量一个显式的类型标注:

1
let guess: u32 = "42".parse().expect("Not a number!");

标量类型(Scalar Types)

标量类型指的是那些能够表示单一数据值的基本类型。Rust有四种主要的标量类型:整数、浮点数、布尔数和字符。

整形(Integer Types)

整数是没有小数部分的数字。u32类型,表示无符号32位整数(u是英文单词unsigned的首字母,与之相反的是i,是英文单词integer的首字母,代表有符号类型)。下表显示了Rust中的内置的整数类型:

长度 有符号类型 无符号类型
8-bit i8 u8
16-bit i16 u8
32-bit i32 u8
64-bit i64 u8
128-bit i128 u8
视架构而定 isize usize

有符号数使用二进制补码表示方式存储,每个有符号类型存储的数字范围是-(2n-1) ~ (2n-1 - 1),无符号类型可以存储的数字范围是0 ~ (2n - 1)。例如i8可以存储的数字范围是-(27) ~ (27 - 1),u8是0 ~ (28 - 1)。

此外,isizeusize类型取决于程序运行的计算机的体系结构,在表中表示为“arch”:如果使用64位体系结构,则为64位;如果使用32位体系结构,则为32位。

可以使用下表所示的任何形式来书写整形字面量。

数值字面量 示例
字节(仅限于u8) b'A'
二进制 0b1111_0000
八进制 0o77
十进制 98_222
十六进制 0xff

请注意,可以是多个数字类型的数字字面量允许使用类型后缀(如57u8)来指定类型。数字字面量也可以使用_作为可视分隔符,使数字更易于阅读。例如1_000,等价于1000。

Rust的整形默认使用i32isizeusize的主要应用场景是用作集合的索引。

整形溢出(Integer Overflow)

假设有一个u8类型的变量,它可以存放的值为0到255。如果尝试将变量更改为该范围以外的值,如256,则会发生整型溢出。这可能导致以下两种行为之一:

  • 对于在Debug模式下编译的可执行文件,在运行时Rust会进行整形溢出检查,若存在这个问题,则程序在运行时panic(崩溃,Rust使用这个术语来表明程序因错误而退出)
  • 对于在Release模式下编译的可执行文件,在运行时Rust不会进行整形溢出的检查。如果发生溢出,会进行二进制补码回绕(two's complement wrapping),即大于该类型所能容纳的最大值的值将“绕到”(被补码转换成)该类型所能支持的对应数字的最小值。例如尝试对一个u8类型的变量赋值256,运行时Rust会自动将这个值回绕至0,257回绕至1,以此类推。这样,虽然程序继续运行,不会出现中断,但变量的值可能并不是期望的值。依赖这种默认行为的代码都应该被认为是错误的代码。
    1
    2
    3
    4
    5
    6
    7
    fn main() 
    {
    let mut num: u8 = 255; // 设置初始值为255
    println!("初始值: {}", num);
    num = num + 1; // 直接加1,这里将会在编译时通过,但在运行时panic(如果在debug模式下);release模式下打印0
    println!("尝试加1后的值: {}", num);
    }

要显式处理可能的溢出,可以使用标准库针对基本数字类型提供的这些方法族:

  • 使用wrapping_*方法在所有模式下都按二进制补码回绕处理,例如wrapping_add
    1
    2
    3
    4
    5
    6
    fn main()
    {
    let a: u8 = 255;
    let b = a.wrapping_add(1); // 结果会回绕到0
    println!("wrapping_add: {}", b); // 输出: wrapping_add: 0
    }
  • 使用checked_*方法检查是否溢出,溢出则返回None
    1
    2
    3
    4
    5
    6
    7
    8
    9
    fn main()
    {
    let a: u8 = 255;
    match a.checked_add(1)
    {
    Some(value) => println!("checked_add: {}", value),
    None => println!("checked_add: overflow occurred"), // 溢出时处理
    }
    }
  • 使用overflowing_*方法返回值和一个表示是否溢出的布尔值。
    1
    2
    3
    4
    5
    6
    7
    fn main()
    {
    let a: u8 = 255;
    let (value, overflowed) = a.overflowing_add(1);
    println!("overflowing_add: {}, overflowed: {}", value, overflowed);
    // 输出: overflowing_add: 0, overflowed: true
    }
  • 使用saturating_*方法使超出类型表示范围的值”饱和”至类型最小值或最大值,而不是回绕。
    1
    2
    3
    4
    5
    6
    fn main()
    {
    let a: u8 = 255;
    let b = a.saturating_add(23); // 结果会饱和在255
    println!("saturating_add: {}", b); // 输出: saturating_add: 255
    }

浮点型(Floating-Point Types)

浮点类型数字是带有小数点的数字,在Rust中浮点类型有两种基本类型:f32f64,大小分别为32位和64位。默认浮点类型是f64,因为在现代CPU上,它的速度与f32几乎相同,但精度更高。所有的浮点类型都是有符号的。

下面是一个演示浮点数的示例:

1
2
3
4
5
fn main()
{
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}

浮点数根据IEEE-754标准实现。f32是单精度浮点型,f64为双精度浮点型。

浮点数陷阱

待补充

布尔型(The Boolean Type)

与大多数其他编程语言一样,Rust中的布尔类型有两个可能的值:true和false。布尔值的大小是一个字节。

1
2
3
4
5
6
7
8
9
fn main() {
let t = true;

let f: bool = false; // 使用类型标注,显式指定f的类型

if f {
println!("这是段毫无意义的代码");
}
}

使用布尔类型的场景主要在于流程控制,例如上述代码的中的if表达式就是其中之一。

字符型(The Character Type)

Rust的char类型是该语言最原始的字母类型,下面的代码展示了几个颇具异域风情的字符:

1
2
3
4
5
6
7
fn main()
{
let c = 'z';
let z = 'ℤ';
let g = '国';
let heart_eyed_cat = '😻';
}

注意,我们使用单引号指定char字面量,这与使用双引号的字符串字面量不同。Rust的char类型大小为4字节,表示一个Unicode标量值(因为Unicode都是4个字节编码,因此字符类型也是占用4个字节),这意味着它可以表示的不仅仅是ASCII。带重音的字母;中文、日文、韩文字符;表情符号以及零宽度空格都是Rust中有效的char值。Unicode标量值的范围从U+0000到U+D7FF和U+E000到U+10FFFF(包括边界)。

复合类型(Compound Types)

复合类型可以将多个值分组到一个类型中。Rust有两种基本复合类型:元组(tuple)和数组(array)。

元组类型(The Tuple Type)

元组是一个具有固定长度(声明后长度不可变)的复合类型,它用于存储不必具有相同类型的若干值

可以通过在括号内编写逗号分隔的值列表来创建元组。元组中的每个位置都有一个类型,不同值的类型可以不同:

1
2
3
4
fn main()
{
let tup: (i32, f64, u8) = (500, 6.4, 1);
}

变量tup绑定到一个元组,类型是(i32, f64, u8),值是(500, 6.4, 1)。可以使用模式匹配(pattern matching)来解构(destructuring) 元组值,从元组中获取单个值:

1
2
3
4
5
6
7
fn main()
{
let tup = (500, 6.4, 1);
let (x, y, z) = tup;

println!("The value of y is: {y}");
}

上述代码创建了一个元组,然后将其绑定到变tup上,接着使用let (x, y, z) = tuptup分解成三个独立的变量x,y和z,这叫做解构

也可以直接访问元组元素,方法是使用句号.,后面跟着要访问的值的索引:

1
2
3
4
5
6
7
8
fn main()
{
let x: (i32, f64, u8) = (500, 6.4, 1);

let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
}

上述代码创建了一个元组x,然后使用索引访问元组中的每个元素。与大多数编程语言一样,元组中的第一个索引是0。

单元(Unit)

没有任何值的元组有一个特殊的名称,叫做单元(unit)。这个值及其对应的类型都写作(),代表一个空值或一个空返回类型。如果表达式不返回任何其他值,则它们隐式返回单元值。

数组类型(The Array Type)

与元组不同,数组的每个元素必须具有相同的类型。Rust中的数组具有固定长度。

创建数组

我们将数组中的值写成方括号内的逗号分隔的列表:

1
2
3
4
fn main()
{
let a = [1, 2, 3, 4, 5];
}

当希望数据分配在栈上,而不是堆上时;或者确保总是有固定数量的元素时,数组非常有用。例如,如果在程序中使用月份的名称,你可能会使用数组而不是向量(vector),因为它总是包含12个固定的元素:

1
2
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];

可以显式指定数组类型:方括号内部写上每个元素的类型、一个分号,然后是数组中元素的数量,如下所示:

1
let a: [i32; 5] = [1, 2, 3, 4, 5];

也可以通过指定初始值、后面加上分号,然后在方括号中写出数组长度的方式,初始化一个每个元素都是相同值的数组,如下所示:

1
2
3
let a = [3; 5];
// 等价于
let a = [3, 3, 3, 3, 3];

访问数组元素

数组是一块已知的大小固定的单一内存块,可以在栈上分配。你可以使用索引来访问数组中的元素,像这样:

1
2
3
4
5
6
7
fn main()
{
let a = [1, 2, 3, 4, 5];

let first = a[0]; // 1
let second = a[1]; // 2
}

访问越界:使用超出数组范围的索引值访问数组元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::io;

fn main()
{
let a = [1, 2, 3, 4, 5];

println!("Please enter an array index.");

let mut index = String::new();

io::stdin()
.read_line(&mut index)
.expect("Failed to read line");

let index: usize = index
.trim()
.parse()
.expect("Index entered was not a number");

let element = a[index];

println!("The value of the element at index {index} is: {element}");
}

上述代码可以成功编译,但在运行时,如果输入一个超出数组范围的数字,如6,则会报错:

1
2
3
thread 'main' panicked at src/main.rs:27:19:
index out of bounds: the len is 5 but the index is 6
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

当尝试使用索引访问元素时,Rust在运行时会检查指定的索引是否小于数组长度。如果索引大于或等于长度,将会panic。

这种就是Rust的安全特性之一。在很多系统编程语言中,并不会检查数组越界问题,你会访问到无效的内存地址获取到一个意料之外的值,最终导致在程序逻辑上出现大问题,而且这种问题会非常难以检查。

参考文章

1.《Rust语言圣经》
2.《The Rust Programming Language》

  • 标题: Rust基础学习(一)——变量和数据类型
  • 作者: paw5zx
  • 创建于 : 2025-01-15 15:56:40
  • 更新于 : 2025-02-09 13:55:34
  • 链接: https://paw5zx.github.io/rust-common-concepts-1/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论