Rust 学习笔记:基本语法和数据类型
Rust 学习笔记:基本语法和数据类型
本篇文章介绍 Rust 的变量、基本类型。
Rust 语言有一组关键字,这些关键字只保留给该语言使用。请记住,不能将这些词用作变量或函数的名称。
变量和可变性
默认情况下,变量是不可变的。一旦值绑定到名称,就不能更改该值。
let number = 5;
打开 RustRover,编写一小段很简单的代码:
fn main() {
let x = 5;
println!("The value of x is {}", x);
x = 6;
println!("The value of x is {}", x);
}
当然会报错:
收到错误消息,无法为不可变变量 x 分配两次,因为程序试图为不可变变量 x 分配第二个值。
虽然变量在默认情况下是不可变的,但是可以通过在变量名前面加上 mut 来使它们可变:
let mut another_number = 10;
修改代码:
fn main() {
let mut x = 5;
println!("The value of x is {}", x);
x = 6;
println!("The value of x is {}", x);
}
成功运行:
常量
与不可变变量一样,常量是绑定到名称并且不允许更改的值,但是常量和变量之间存在一些区别。
首先,不允许对常量使用 mut。常量不仅在默认情况下是不可变的——它们总是不可变的。使用 const 关键字而不是 let 关键字来声明常量,并且必须对值的类型进行注释。
常量可以在任何作用域中声明,包括全局作用域。常量在编译时确定,在程序运行的整个时间内有效,在声明它们的作用域内有效。
最后一个区别是常量只能设置为常量表达式,而不是只能在运行时计算的值的结果。
一个常量的例子:
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
Rust 对常量的命名约定是在单词之间使用所有大写字母和下划线,仅可以使用常量表达式赋值。
变量遮蔽(Shadowing)
Rust 允许声明一个与之前变量同名的新变量。
let number = 1;
let number = 2;
在同一个作用域内,第一个变量被第二个遮蔽。
示例程序:
fn main() {
let x = 5;
let x = x + 1;
{
let x = x * 2;
println!("The value of x in the inner scope is: {x}");
}
println!("The value of x is: {x}");
}
运行结果:
这个程序首先将 x 绑定到一个值 5。然后它通过重复 let x = 来创建一个新的变量 x,取原来的值加 1,所以 x 的值是 6。然后,在用花括号创建的内部作用域中,第三个 let 语句也隐藏 x 并创建一个新变量,将前一个值乘以 2,使 x 的值为 12。当该范围结束时,内部遮蔽结束,x 返回为 6。
遮蔽不同于将变量标记为 mut,因为如果不使用 let 关键字而不小心尝试对该变量重新赋值,将会得到编译时错误。通过使用 let,我们可以对一个值执行一些转换,但在这些转换完成后,变量是不可变的。
mut 和遮蔽之间的另一个区别是,因为当我们再次使用 let 关键字时,我们实际上是在创建一个新变量,所以我们可以改变值的类型,但重用相同的名称。
例如,假设我们的程序要求用户通过输入空格字符来显示他们希望在某些文本之间有多少空格,然后我们希望将该输入存储为数字:
let spaces = " ";
let spaces = spaces.len();
第一个 spaces 变量是字符串类型,第二个 spaces 变量是数字类型。因此,遮蔽使我们不必提出不同的名称,例如 spaces_str 和 spaces_num;相反,我们可以重用更简单的 spaces。
然而,如果我们尝试使用 mut,如下所示,我们将得到一个编译时错误:
let mut spaces = " ";
spaces = spaces.len();
错误提示我们不允许改变变量的类型。
数据类型
Rust 中的每个值都有一个特定的数据类型,分为两种:
- 标量类型:表示一个单一的值。
- 复合类型:可以将多个值组合在一个类型。
请记住,Rust 是一种静态类型语言,这意味着它必须在编译时知道所有变量的类型。编译器通常可以根据值和使用方式推断出我们想要使用的类型。在可能存在多种类型的情况下,必须添加类型注释,如下所示:
fn main() {
let guess: u32 = "42".parse().expect("Not a number!");
println!("{}", guess);
}
如果不添加 :u32类型注释,Rust 将报错。
这意味着编译器需要从我们这里获得更多信息来知道我们想要使用哪种类型。
标量数据(Scalar Types)
标量类型表示单个值。
Rust 有四种主要的标量类型:整数、浮点数、布尔数和字符。
整数类型(Integer Types)
整数是没有小数部分的数字。
注意几点:
- 整数类型默认为 i32。
- isize 和 usize 类型取决于程序运行的计算机的体系结构。如果使用 64 位操作系统,则为 64 位;如果使用 32 位操作系统,则为 32 位。
- 可以是多个数字类型的数字字面值允许使用类型后缀(如57u8)来指定类型。
- 数字字面量也可以使用 _ 作为可视分隔符,使数字更易于阅读,例如 1_000,它将具有与指定 1000 相同的值。
整形字面值(Integer Literals):
假设您有一个类型为 u8 的变量,它可以保存 0 到 255 之间的值。如果尝试将变量更改为该范围之外的值,例如 256,则会发生整数溢出,这可能导致两种行为之一:
- 当在 debug 模式下编译时,Rust 包括整数溢出检查,如果发生这种行为,则会导致程序在运行时报错。
- 当在 release 模式下编译时,Rust 不包括整数溢出的检查,如果发生溢出,Rust 执行补码包装。简而言之,大于该类型所能容纳的最大值的值“绕到”该类型所能容纳的最小值。在 u8 的情况下,值 256 变为 0,值 257 变为 1,以此类推。
要显式处理溢出的可能性,您可以使用标准库为基本数字类型提供的这些方法族:
-
使用 wrapping_* 方法在所有模式下进行包装,例如 wrapping_add。
-
如果 checked_* 方法溢出,则返回 None 值。
-
使用 overflowing_* 方法返回值和一个布尔值,指示是否存在溢出。
-
使用 saturating_* 方法使值的最小值或最大值饱和。
浮点数类型(Floating-Point Types)
浮点数是带有小数点的数字。Rust 为浮点数提供了两种基本类型:f32 和 f64,大小分别为 32 位和 64 位,默认类型是 f64。所有浮点类型都是有符号的。
示例:
fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}
浮点数按照 IEEE-754 标准表示。
操作符
Rust支持所有数字类型所需的基本数学运算:加、减、乘、除和余。
fn main() {
// addition
let sum = 5 + 10;
// subtraction
let difference = 95.5 - 4.3;
// multiplication
let product = 4 * 30;
// division
let quotient = 56.7 / 32.2;
let truncated = -5 / 3; // Results in -1
// remainder
let remainder = 43 % 5;
}
整数除法向零截断到最接近的整数。
布尔类型(Boolean Type)
与大多数其他编程语言一样,Rust 中的布尔类型有两个可能的值:true 和 false。布尔值的大小是 1 个字节。Rust 中的布尔类型是用 bool 指定的。
fn main() {
let t = true;
let f: bool = false; // with explicit type annotation
}
字符类型(Character Type)
Rust 的 char 类型大小为 4 个字节,表示一个 Unicode 标量值,这意味着它可以表示的不仅仅是 ASCII,中文、日文、韩文、emoji 和零宽度空格在 Rust 中都是有效的字符值。
fn main() {
let c = 'z';
let z: char = 'ℤ'; // with explicit type annotation
let heart_eyed_cat = '😻';
}
复合数据(Compound Types)
复合类型可以将多个值分组到一个类型中。Rust 有两种基本复合类型:元组和数组。
元组(Tuple)
元组是一种将许多具有不同类型的值分组为一个复合类型的通用方法。元组具有固定的长度:一旦声明,它们的大小就不能增加或缩小。
通过在圆括号内写入以逗号分隔的值列表来创建元组。元组中的每个位置都有一个类型,元组中不同值的类型不必相同。
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
变量 tup 绑定到整个元组,因为元组被视为单个复合元素。为了从元组中获取单个值,我们可以使用模式匹配来解构元组值,如下所示:
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {y}");
}
该程序首先创建一个元组并将其绑定到变量 tup。然后,它使用带有 let 的模式将 tuple 转换为三个独立的变量,x, y 和 z。这称为解构,因为它将单个元组分解为三个部分。最后,程序输出 y 的值,即 6.4。
也可以直接访问元组元素,方法是使用句号(.),后面跟着要访问的值的索引。例如:
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。此值及其对应的类型都是 write_(),并表示空值或空返回类型。表达式如果不返回任何其他值,则隐式返回 unit。
数组(Array)
拥有多个值的集合的另一种方法是使用数组。与元组不同,数组的每个元素必须具有相同的类型。
与其他语言中的数组不同,Rust 中的数组具有固定长度。
我们将数组中的值写成方括号内的逗号分隔列表:
fn main() {
let a = [1, 2, 3, 4, 5];
}
数组不像 vector 类型那样灵活。vector 是标准库提供的一种类似的集合类型,允许在大小上增长或缩小。
但是,当知道元素的数量不需要更改时,数组更有用。例如,如果你在程序中使用月份的名称,你可能会使用 array 而不是 vector,因为你知道它总是包含 12 个元素:
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
用方括号写一个数组的类型,加上每个元素的类型,一个分号,然后是数组中元素的数量,如下所示:
let a: [i32; 5] = [1, 2, 3, 4, 5];
这里,i32 是每个元素的类型。分号后面的数字 5 表示数组包含 5 个元素。
你也可以通过指定初始值,后面跟着分号,然后在方括号中指定数组的长度来初始化数组,使每个元素包含相同的值,如下所示:
let a = [3; 5];
名为 a 的数组将包含 5 个元素,初始值都设置为 3。
Rust 程序在索引操作中使用无效值时导致运行时错误,程序退出时显示一条错误消息。在许多低级语言中,没有进行这种检查,当提供不正确的索引时,可能会访问无效的内存。Rust 通过立即退出而不是允许内存访问并继续来保护程序员免受此类错误的侵害。