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

变量
变量命名
需要遵循Rust命名规范
变量绑定
在其他语言中我们可以这么给变量a赋值:
1 | std::string a = "hello world"; |
而在Rust中,我们这样写:
1 | let a = "hello world"; |
我们称这个操作为变量绑定(bind
)。称之为绑定而不是赋值的原因,涉及到Rust最核心的原则——所有权。简单来讲,任何内存对象都有一个明确的所有者,该所有者负责管理该对象的生命周期
变量可变性
Rust变量默认是不可变的,一旦为变量绑定值,就不能再修改这个变量:
1 | fn main() |
会报错:
1 | error[E0384]: cannot assign twice to immutable variable `x` |
这是因为我们想为不可变的变量x再次赋值。
若想创建一个可变变量,在变量名前加一个mut
即可:
1 | fn main() |
忽略变量
如果创建了一个变量却从未使用,Rust编译器通常会给出一个警告,因为这可能意味着程序中存在潜在的错误或冗余代码。这种警告有助于提升代码的质量和效率。
如果想告诉编译器不要警告未使用的变量,可以在变量名称前加上下划线:
1 | fn main() |
会有以下警告:
1 | warning: unused variable: `y` |
变量解构
let
表达式不仅仅用于变量的绑定,还能进行复杂变量的解构:从一个相对复杂的变量中,匹配出该变量的一部分内容:
1 | fn main() |
在Rust 1.59版本后,我们可以在赋值语句的左式中使用元组、切片和结构体模式:
1 | struct Struct |
详情见模式匹配
常量
与不可变变量一样,常量(constants
)是绑定到名称并且不允许更改的值,但是常量和变量之间存在一些区别。
- 常量不允许使用
mut
关键字。常量不仅仅默认不可变,而且自始至终不可变,因为常量在编译完成后,就已经确定它的值 - 常量使用
const
关键字而不是let
关键字来声明,并且必须对值的类型进行标注(annotate
)
常量可以在任何作用域中声明,包括全局作用域。常量在程序整个运行期间有效,在声明它们的作用域内有效。对于需要在多处代码共享一个不可变的值时非常有用。在实际使用中,最好将程序中用到的硬编码值都声明为常量,对于代码后续的维护有莫大的帮助。如果将来需要更改硬编码的值,你也只需要在代码中更改一处即可。
下面是一个常量声明的两个例子
1 | const MAX_POINTS: u32 = 100_000; |
- ①常量名为
MAX_POINTS
,值设置为100000。(Rust常量的命名约定是全部字母都使用大写,并使用下划线分隔单词;对数字字面量可插入下划线以提高可读性) - ②常量名为
THREE_HOURS_IN_SECONDS
,它的值被设置为60(一分钟内的秒数)乘60(一小时内的分钟数)乘3(我们想在这个程序中计算的小时数)的结果。编译器能够在编译时计算一组有限的操作,这让我们可以选择以一种更容易理解和验证的方式写出这个值,而不是将这个常数直接设置为10800。
变量遮蔽
Rust允许声明一个与之前变量同名的新变量,在后面声明的变量会遮蔽(shadow
)掉前面声明的,这意味着当使用变量名时,编译期将看到后面的变量,直到这个后面的变量被遮蔽或出作用域,如下所示:
1 | fn main() |
这个程序首先将数值5绑定到x
,然后通过重复使用let x =
来创建一个新的变量x
,并取原来的值加上1,所以x
的值变成了6。然后在用花括号创建的内部作用域中,第三个let
语句同样创建一个新的变量x
并遮蔽前面的x
,取之前的值并乘上2,得到的x
最终值为 12。当出花括号作用域后,内部遮蔽结束,x
的值还是6
当运行这个程序时,它将输出如下内容:
1 | Compiling world_hello v0.1.0 (/setup/rust-code/world_hello) |
注意,遮蔽不同于将变量标记为mut
,虽然他们都涉及到变量的修改:
- 当使用
mut
关键字声明一个变量时,将标记这个变量为可变的,这意味着可以在同一个内存地址上修改这个变量的值。由于没有发生变量的重新声明或内存重新分配,性能开销较低。 - 当使用变量遮蔽时,尽管变量名相同,但实际上是创建了一个全新的变量(所以我们可以改变值的类型)。这个新的变量将有自己的内存分配,与原始变量完全独立。每次遮蔽都可能涉及到内存的重新分配(取决于遮蔽的变量类型和上下文)(作为使用者可以这么理解,实际上编译器可能出于优化目的在物理上重用相同的内存位置),这可能会比简单修改现有变量的值有更多的性能开销。
示例:
遮蔽过程中,spaces
变量的类型从&str
更改为usize
1 | // 字符串类型 |
当然如果你将上述代码修改为:
1 | let mut spaces = " "; |
显然是不对的,因为Rust对类型的要求很严格,不允许将usize
赋值给&str
类型变量。
数据类型
Rust是一门静态类型语言,也就是说编译器必须在编译期知道所有变量的类型。编译器通常可以根据值和使用方式自动推导出变量的类型。但是在某些情况下,它却无法推导出变量类型,需要手动去给予一个类型标注:
1 | let guess = "42".parse().expect("Not a number!"); |
这段代码的目的是将字符串”42”进行解析,而编译器在这里无法推导出我们想要的类型:整数?浮点数?字符串?因此编译器会报错:
1 | error[E0284]: type annotations needed |
因此我们需要提供给编译器更多的信息,例如给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)。
此外,isize
和usize
类型取决于程序运行的计算机的体系结构,在表中表示为“arch”:如果使用64位体系结构,则为64位;如果使用32位体系结构,则为32位。
可以使用下表所示的任何形式来书写整形字面量。
数值字面量 | 示例 |
---|---|
字节(仅限于u8) | b'A' |
二进制 | 0b1111_0000 |
八进制 | 0o77 |
十进制 | 98_222 |
十六进制 | 0xff |
请注意,可以是多个数字类型的数字字面量允许使用类型后缀(如57u8
)来指定类型。数字字面量也可以使用_
作为可视分隔符,使数字更易于阅读。例如1_000,等价于1000。
Rust的整形默认使用i32
。isize
或usize
的主要应用场景是用作集合的索引。
整形溢出(
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中浮点类型有两种基本类型:f32
和f64
,大小分别为32位和64位。默认浮点类型是f64
,因为在现代CPU上,它的速度与f32
几乎相同,但精度更高。所有的浮点类型都是有符号的。
下面是一个演示浮点数的示例:
1 | fn main() |
浮点数根据IEEE-754标准实现。f32
是单精度浮点型,f64
为双精度浮点型。
浮点数陷阱
布尔型(The Boolean Type)
与大多数其他编程语言一样,Rust中的布尔类型有两个可能的值:true和false。布尔值的大小是一个字节。
1 | fn main() { |
使用布尔类型的场景主要在于流程控制,例如上述代码的中的if表达式就是其中之一。
字符型(The Character Type)
Rust的char类型是该语言最原始的字母类型,下面的代码展示了几个颇具异域风情的字符:
1 | fn main() |
注意,我们使用单引号指定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 | fn main() |
变量tup
绑定到一个元组,类型是(i32, f64, u8)
,值是(500, 6.4, 1)
。可以使用模式匹配(pattern matching
)来解构(destructuring
) 元组值,从元组中获取单个值:
1 | fn main() |
上述代码创建了一个元组,然后将其绑定到变tup
上,接着使用let (x, y, z) = tup
将tup
分解成三个独立的变量x,y和z,这叫做解构
也可以直接访问元组元素,方法是使用句号.
,后面跟着要访问的值的索引:
1 | fn main() |
上述代码创建了一个元组x
,然后使用索引访问元组中的每个元素。与大多数编程语言一样,元组中的第一个索引是0。
单元(Unit)
没有任何值的元组有一个特殊的名称,叫做单元(unit
)。这个值及其对应的类型都写作()
,代表一个空值或一个空返回类型。如果表达式不返回任何其他值,则它们隐式返回单元值。
数组类型(The Array Type)
与元组不同,数组的每个元素必须具有相同的类型。Rust中的数组具有固定长度。
创建数组
我们将数组中的值写成方括号内的逗号分隔的列表:
1 | fn main() |
当希望数据分配在栈上,而不是堆上时;或者确保总是有固定数量的元素时,数组非常有用。例如,如果在程序中使用月份的名称,你可能会使用数组而不是向量(vector
),因为它总是包含12个固定的元素:
1 | let months = ["January", "February", "March", "April", "May", "June", "July", |
可以显式指定数组类型:方括号内部写上每个元素的类型、一个分号,然后是数组中元素的数量,如下所示:
1 | let a: [i32; 5] = [1, 2, 3, 4, 5]; |
也可以通过指定初始值、后面加上分号,然后在方括号中写出数组长度的方式,初始化一个每个元素都是相同值的数组,如下所示:
1 | let a = [3; 5]; |
访问数组元素
数组是一块已知的大小固定的单一内存块,可以在栈上分配。你可以使用索引来访问数组中的元素,像这样:
1 | fn main() |
访问越界:使用超出数组范围的索引值访问数组元素
1 | use std::io; |
上述代码可以成功编译,但在运行时,如果输入一个超出数组范围的数字,如6,则会报错:
1 | thread 'main' panicked at src/main.rs:27:19: |
当尝试使用索引访问元素时,Rust在运行时会检查指定的索引是否小于数组长度。如果索引大于或等于长度,将会panic。
这种就是Rust的安全特性之一。在很多系统编程语言中,并不会检查数组越界问题,你会访问到无效的内存地址获取到一个意料之外的值,最终导致在程序逻辑上出现大问题,而且这种问题会非常难以检查。
参考文章
- 标题: 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 进行许可。