Rust 之四 运算符、标量、元组、数组、字符串、结构体、枚举

发布于:2025-04-09 ⋅ 阅读:(47) ⋅ 点赞:(0)

概述

  Rust 的基本语法对于从事底层 C/C++ 开发的人来说多少有些难以理解,虽然官方有详细的文档来介绍,不过内容是相当的多,看起来也费劲。本文通过将每个知识点简化为 一个 DEMO + 每种特性各用一句话描述的形式来简化学习过程,提高学习效率。

运算符

  编程语言中的运算符是用于对数据执行特定操作的符号或关键字。涵盖了从基本的算术运算到复杂的位运算、逻辑运算、比较运算等。Rust 中运算符按照优先级整理如下所示:

优先级 运算符/操作 结合性 示例 说明
1 ()(调用)、[](索引)、.(成员访问) 左结合 func(), arr[0], struct.field 函数调用、数组索引、结构体字段访问
2 单目运算符:-(负)、!(非)、*(解引用)、&/&mut(引用) 右结合 -x, !flag, *ptr, &data 单目运算优先级高于算术运算符
3 as(类型转换) 左结合 x as f32 显式类型转换
4 *(乘)、/(除)、%(取余) 左结合 a * b + c(a * b) + c 乘除取余优先级相同,高于加减
5 +(加)、-(减) 左结合 a + b - c(a + b) - c 加减优先级相同
6 <<(左移)、>>(右移) 左结合 a << 2 + 3a << (2 + 3) 位移运算符优先级低于算术运算
7 &(位与) 左结合 a & b == ca & (b == c) 位与优先级低于比较运算符
8 ^(位异或) 左结合 a ^ b & ca ^ (b & c) 位异或优先级低于位与
9 |(位或) 左结合 a | b ^ ca | (b ^ c) 位或优先级低于位异或
10 ==!=<><=>= 无结合性 a < b == c(a < b) == c 比较运算符优先级相同
11 &&(逻辑与) 左结合 a && b || c(a && b) || c 逻辑与优先级高于逻辑或
12 ||(逻辑或) 左结合 a || b && ca || (b && c) 逻辑或优先级最低
13 ....=(范围) 无结合性 1..n+11..(n+1) 范围运算符优先级低于算术运算
14 =+=-= 等赋值运算符 右结合 a = b = 0a = (b = 0) 赋值运算符优先级最低

算术运算符

  算术运算符包含 +(加)、-(减)、*(乘)、/(除)、%(取余)这 6 个运算符。其中,整数除法实际是向零取整(如 -5 / 3 = -1),浮点数除法为数学除法;%(取余)操作的余数的符号与被除数符号相同(如 -5 % 3 = -2)。

逻辑运算符

  逻辑运算符包含 &&(逻辑与)、||(逻辑或)、!(逻辑非)这三个运算符。Rust 严格要求逻辑运算符的操作数为布尔类型,且不支持隐式类型转换。

  &&|| 会跳过不必要的计算(短路求值,Short-Circuiting),提升效率并避免潜在错误。例如,a && b 中若 a 为 false,则直接返回 false,不计算 b;a || b 中若 a 为 true,则直接返回 true,不计算 b。

位运算符

  位运算符用于对整数类型的二进制位进行操作,主要包含 &(位与)、|(位或)、^(异或)、!(位非)、<<(左移)、>>(右移)这 6 个运算符。

  1. !(位非)作用于整数而 !(逻辑非)作用于逻辑表达式。

  2. 无符号整数的右移时左侧补 0(逻辑右移);有符号整数的右移时左侧补符号位(算术右移)

  3. 左移等价于乘以 2n,右移等价于除以 2n

赋值运算符

  赋值运算符用于将值赋给变量,主要包含 = 这一个简单运算符以及 +=-=*=/=%=<<=>>=&=|=^= 这 10 个复合赋值运算符。

比较运算符

  比较运算符用于比较两个值,并返回一个布尔值 (truefalse),主要包含 ==!=<><=>= 这 6 个运算符。比较的双方类型必须一致,且不支持隐式转换(例如,无法直接比较 i32u32 这两种类型)。

范围运算符

  .. 为左闭右开(如 1..5 包含 1, 2, 3, 4,但是不含 5)的范围区间,而 ..= 为闭区间(如 1..=5 包含 1, 2, 3, 4, 5)。后文 Rust 之五 语句和表达式、作用域、所有权、引用与切片 详细介绍!

引用运算符

  Rust 使用借用(&)和可变借用(&mut)来进行引用操作:& 创建不可变引用,&mut 创建可变引用。后文 Rust 之五 语句和表达式、作用域、所有权、引用与切片 详细介绍!

数据类型

  在编程语言中,数据类型定义了变量、常量、数值可以存储的数据的类型和范围。每种数据类型规定了该数据如何存储、如何操作以及占用多少内存空间。编程语言通常提供多种数据类型,以提升代码可读性与维护性!
在这里插入图片描述
  Rust 中有标量(scalar)和复合(compound)、字符串、自定义等数据类型。在 Rust 中,每一个变量、常量、数值都属于某一个数据类型(data type),这告诉 Rust 它被指定为何种数据,以便明确数据处理方式。

  1. Rust 是 静态类型(statically typed)语言,也就是说在编译时就必须知道所有变量的类型。
  2. 在某些没有歧义的情况下,Rust 会自动推断数据的类型,因此,有时候数据类型可以省略。

标量类型

  标量(scalar)类型代表一个单独的值。Rust 有整型、浮点型、布尔类型和字符类型这 4 种基本的标量类型。

整数类型

  整数类型(integer)简称整型,用来表示整数,整数是一个没有小数部分的数字,并且可以是有符号或无符号的。因此,Rust 中使用 integer 和 unsigned 的首字母 i / u + 整数占用的 BIT 位数 分别来表示有符号整数类型 / 无符号整数类型。各种类型如下表所示:

有符号整型 无符号整型 长度
i8 u8 8-bit
i16 u16 16-bit
i32 u32 32-bit
i64 u64 64-bit
i128 u128 128-bit
isize usize 由 Rust 根据系统架构自行决定
  1. 有符号数的范围为 -(2n-1) 到 2n-1 - 1,无符号数的范围为 0 到 2n-1,其中 n 为整数占用的 BIT 位数

  2. 有符号数以补码形式(two’s complement representation) 存储

整型字面值

  整型字面值就是整数数字在 Rust 源代码中的写法,Rust 中提供了多种不同的写法(比与我们数学中的写法要多一些),不同的写法主要是为了阅读起来方便,如下所示:

字面值形式 例子
Decimal (十进制) 98_222
Hex (十六进制) 0xff
Octal (八进制) 0o77
Binary (二进制) 0b1111_0000
Byte (单字节字符)(仅限于u8) b’A’
上限 类型::MAX,例如 i32:MAX(2147483647);u8::MAX(255)
下限 类型::MIN,例如 u32:MIN(0);i32::MIN(-2147483648)
  1. 整数数字默认是 i32 类型。也就是当在 Rust 源码中写一个整数数字,例如,200,它默认就是 i32 类型

  2. 整型字面值可以在后面添加上具体的类型,此时编译器就可以帮助我们检查类型是否匹配。例如 123u8456u320xffi32,这样就不在是默认的 i32 类型了!

  3. 数字较多时,可以使用下划线分隔。 以 4 个数字为一组,这样最符合人类阅读,但 Rust 语法则没有此要求,我们可以加上任意数量的下划。例如 let z = 0b1111_______10______1_1;

类型转换

  Rust 要求不同的数据类型之间必须显式使用 as 关键字进行类型转换,不允许隐式转换!

let x = 100i32 as u8;       // i32 转换为 u8
let y = 255u8 as i8;        // 255 转换为 i8 会溢出为 -1(二进制补码)
运算

  整型可以进行 +-*/%(加、减、乘、除(取整)、取余)这 5 种算术运算,也可以进行&(位与)、|(位或)、^(异或)、!(位非)、<<(左移)、>>(右移)这 6 种位运算。

let a: i32 = 10;
let b: i32 = 4;
let c: i32 = 2;
let sum = a + b;    // 加法
let diff = a - b;   // 减法
let prod = a * b;   // 乘法
let div = a / b;    // 除法(对于整数类型,除法操作会自动舍去小数部分,向零取整)
let rem = a % b;    // 取余
let res = (a + b) * c - b / c; // 结果是 26,因为括号优先
println!("Sum: {sum} Difference: {diff} Product: {prod} Division: {div} Remainder: {rem} res: {res}");
let x: u8 = 0b1010; // 10
let y: u8 = 0b1100; // 12
println!("&: {:08b}", x & y); // 与运算,00001000 (8)
println!("|: {:08b}", x | y); // 或运算,00001110 (14)
println!("!: {:08b}", !x);    // 非运算,11110101 (245)
println!("^: {:08b}", x ^ y); // 异或运算,00000110 (6)
println!("&^|!: {:08b}", x & (x ^ y) | !y);
  1. Rust 中的取反运算符 ! 作为逻辑非运算符时,作用于整个表达式,而非单个数字的按位反转;作为按位取反使用 ! 时,作用于整型数值。
比较

  整型可以直接进行 ==(等于)、!=(不等于)、<(小于)、>(大于)、<=(小于等于)、>=(大于等于) 这 6 种逻辑比较运算。逻辑比较运算符用于比较两个值,结果是 true 或 false 。

let x = 10;
let y = 5;
println!("x == y: {}", x == y);  // false
println!("x != y: {}", x != y);  // true
println!("x < y: {}", x < y);    // false
println!("x > y: {}", x > y);    // true
println!("x <= y: {}", x <= y);  // false
println!("x >= y: {}", x >= y);  // true
println!("x > y || x == y: {}", x >= y || x == y);  // true
println!("x < y && x != y: {}", x >= y && x == y);  // false

  可以将多个比较运算符结合起来使用,通常和逻辑运算符 &&|| 一起构成复杂的条件判断。&&(与):如果左右两边的条件都为 true,则结果为 true。||(或):如果左右两边的条件有一个为 true,则结果为 true。

示例

  完整示例源码可以通过 git clone git@gitee.com:itexp/data_scalar_integer.git 下载。

浮点型

  浮点(float)型用来表示浮点数(floating-point numbers),浮点数是带小数点的数字,所有的浮点数都是有符号的。因此,Rust 中使用 float 的首字母 f + 浮点数占用的 BIT 位数 来表示不同的浮点数类型。各种类型如下表所示:

类型 长度
f32 32-bit
f64 64-bit
  1. 浮点数采用 IEEE-754 标准表示。f32 是单精度浮点数,f64 是双精度浮点数。
浮点型字面值

  浮点型字面值就是浮点数数字在 Rust 源代码中的写法,Rust 中只提供了一种写法(与我们数学中的写法基本一致),如下所示:

字面值形式 例子
正常浮点数 12.34
正无穷 f64::INFINITYf32::INFINITY
负无穷 f64::NEG_INFINITYf32::NEG_INFINITY
非数值(Not a Number) f64::NANf32::NAN
上限 f64::MAXf32::MAX
下限 f64::MINf32::MIN
  1. 浮点数数字默认是 f64 类型。也就是当在 Rust 源码中写一个浮点数数字,例如,1.1,它默认就是 f64 类型

  2. 浮点型字面值可以在后面添加上具体的类型,此时编译器就可以帮助我们检查类型是否匹配。例如 123.123f32456.456f64,这样就不在是默认的 f64 类型了!

类型转换
  1. 浮点数与整型之间必须显式使用 as 关键字进行转换
    let x = 3.14 as i32;  // 3(直接截断小数部分)
    let y = 5i32 as f64;  // 5.0
    
  2. f32f64 之间也必须显式使用 as 关键字进行显式转换
    let z = 1.0f32 as f64; // f32 转 f64(精度保留)
    let w = 1.0f64 as f32; // f64 转 f32(可能丢失精度)
    
运算

  浮点型数据除了可以进行基本的 +-*/%(加、减、乘、除、取余)这 5 种算术运算,但是,不能进行按位运算。Rust 还提供了一些标准数学函数用于浮点数基本运算。

let a: f64 = 10.5;
let b: f64 = 4.2;
let sum = a + b;        // 加法
let difference = a - b; // 减法
let product = a * b;    // 乘法
let quotient = a / b;   // 除法
let remainder = a % b;  // 取余
println!("a + b = {sum} a - b = {difference} a * b = {product} a / b = {quotient} a % b = {}", remainder);
let a: f64 = 10.0;
let b: f64 = 0.0;
let result = a / b;
println!("10.0 / 0.0 = {}", result);  // 输出 "inf"
let a: f64 = 0.0;
let b: f64 = 0.0;
let result = a / b;
println!("0.0 / 0.0 = {}", result);  // 输出 "NaN"
let x = 2.0f64;
let sqrt = x.sqrt();        // 平方根(1.4142...)
let sin = x.sin();          // 正弦函数
let pow = x.powi(3);        // 整数次幂(8.0)
let log = x.log(10.0);      // 对数(log10(2.0))
let abs = x.abs();          // 绝对值
println!("sqrt = {sqrt} sin = {sin} pow = {pow} log = {log} abs = {abs}");
  1. Rust 提供了标准的取余运算符 %,它可以对浮点数进行取余操作。取余操作会返回浮点数的余数部分。
比较

  浮点数直接进行逻辑比较运算(==!=<><=>=)时,可能会因为浮点数的精度问题可能导致不精确的结果。Rust 提供了一些方法来处理浮点数的比较。

  1. 正常比较 abs(a - b) < 1e-6
  2. is_nan():检查浮点数是否是 NaN。
  3. is_infinite():检查浮点数是否是无穷大(正无穷或负无穷)。
示例源码

  完整示例源码可以通过 git clone git@gitee.com:itexp/data_scalar_float.git 下载。

布尔型

  布尔型就是只有真和假两种状态的一种类型,Rust 中使用 bool 来表示真和假两种状态的类型。这点与其他一些语言基本类似,例如,C 语言最新标准中也定义了 bool 类型。

类型 长度
bool 8-bit
  1. 布尔类型的大小为 1 字节

  2. 使用布尔值的主要场景是条件表达式

布尔型字面值

  布尔型字面值就是真和假这两种状态在 Rust 源代码中的写法,分别用 truefalse 来表示。这点与一些底层语言不同,例如,C 语言中 0 表示假,非 0 都是真!

字面值形式 例子
true
false
类型转换

  在 Rust 中,布尔值不能直接与数字值(例如 0 或 1)进行转换,需要显示使用 as 进行类型转换!

// let a = true + 1;  // 错误:不能将布尔值与数字相加
let t = true as u8;  // 1
let f = false as u8; // 0
let result = t & f;  // 0(等价于 1 & 0)
运算

  布尔类型支持逻辑与 (AND) &&、逻辑或 (OR) ||、逻辑非 (NOT) ! 这 3 种逻辑运算。但是,不能直接进行算术运算和按位运算。

比较

  布尔类型是所有逻辑比较运算的结果。且不能直接与数字进行比较!

示例源码

  完整示例源码可以通过 git clone git@gitee.com:itexp/data_scalar_bool.git 下载。

字符型

  在 Rust 中的字符类型是用于表示单个 Unicode 标量值的类型,使用 char 来表示。Rust 的字符类型能够表示任何有效的 Unicode 标量值(范围 U+0000U+10FFFF,排除代理项对 U+D800-U+DFFF),这包括各种语言中的字符,包括中文、表情符号、拉丁字母等。

类型 长度
char 32-bit
  1. 字符型实际是占用 4 个字节

  2. 在 Rust 中,字符型是不可变的

字符型字面值

  字符型字面值就是字符在 Rust 源代码中的写法,使用单引号包裹字符 '字符' 来声明字符字面值。Rust 中的字符实际上代表一个 Unicode 标量值,意味着它可以比 ASCII 表示更多内容。

字面值形式 例子
普通字符 ‘A’
转义字符 ‘\n’
Unicode 码点 ‘\u{1F600}’
最大值 ‘\u{10FFFF}’(Unicode 标量值的上限)
最小值 ‘\0’(Unicode 的 U+0000)
类型转换

  字符型可以使用 as 关键字转换为对应 Unicode 码点的整数(u32),整数则需通过 std::char::from_u32() 安全转换(返回 Option<char>)为字符型。

let c = 'A';
let code = c as u32;  // 65
println!("Hello, world! code = {code}");
let code = 65; 
let c = std::char::from_u32(code).unwrap(); // 'A'
// let invalid = std::char::from_u32(0xD800);  // None(无效 Unicode 代理区)
println!("Hello, world! c = {c}");
运算

  字符型本身不能直接进行算术运算、按位运算等数学操作。Rust 为字符类型提供多种方法处理字符

  • 判断字符类别
    'a'.is_alphabetic();  // 是否为字母
    '3'.is_numeric();     // 是否为数字
    ' '.is_whitespace();  // 是否为空白符
    
  • 大小写转换
    'A'.to_lowercase();   // 返回迭代器(可能多个字符,如德语 ß)
    'ä'.to_uppercase();   // 转大写 'Ä'
    
  • 转换为数字
    '7'.to_digit(10);     // Some(7),按进制解析
    
比较

  字符型可以进行逻辑比较运算(==!=<><=>=),Rust 实际上会根据字符的 Unicode 值进行比较。

let char1: char = 'a';
let char2: char = 'b';
let char3: char = 'a';
// 比较字符是否相等
println!("{}", char1 == char3); // true,因为 'a' 和 'a' 相等
println!("{}", char1 == char2); // false,因为 'a' 和 'b' 不相等
// 比较字符的大小(基于 Unicode 值)
println!("{}", char1 < char2); // true,因为 'a' 的 Unicode 值小于 'b'
// 其他比较操作
println!("{}", char1 <= char2); // true
println!("{}", char1 != char3); // false    
示例源码

  完整示例源码可以通过 git clone git@gitee.com:itexp/data_scalar_char.git 下载。

复合类型

  复合类型(Compound types)可以将多个值组合成一个类型。Rust 有元组(Tuple)和数组(Array)这两个原生的复合类型。

元组

  元组(Tuple)是一个将多个不同或相同类型的值组合进一个复合类型的主要方式。使用包含在圆括号中的逗号分隔的值列表 (元素1, 元素2, ..., 元素n) 来创建一个元组。元组中的每一个位置都有一个类型,而且这些不同值的类型可以相同,也可以不同。

let x: (i32, f64, u8) = (500, 6.4, 1);  // 正常定义拥有不同类型的值的元组
println!("Hello, world! x.0 = {}, x.1 = {}, x.1 = {}", x.0, x.1, x.2);
let x = (500, 6.4, 1);                  // 可以简写,自动推导类型
println!("Hello, world! x.0 = {}, x.1 = {}, x.2 = {}", x.0, x.1, x.2);
let a: (i32, &str) = (10, "test");
let b: (i64, String) = (10, "test".to_string());    // a 和 b 是不同类型
println!("Hello, world! a.0 = {}, b.1 = {}", a.0, b.1);
let rect = (30, 50);                    // 具有相同类型的值的元组
println!("Hello, world! rect.0 = {}, rect.1 = {}", rect.0, rect.1);
  1. 元组长度固定,一旦声明,其长度不会增大或缩小,在定义时必须让 Rust 可以明确知道元组中元素的类型和个数(个数可以为 0)

    • Rust 可以自动推导出元素类型和个数,例如 let x = (500, 6.4, 1);
    • 可以显示指定元素类型和个数,例如 let x: (i32, f64, u8) = (500, 6.4, 1);
  2. 元组可以嵌套

    let y = ((1, 2.71, 3), (1, 2u16), 33);  // 嵌套元组
    println!("Hello, world! y.0.0 = {}, y.0.1 = {}, y.0.2 = {}", y.0.0, y.0.1, y.0.2);
    
  3. 可以使用 元组名 + 点号(.)+ 从 0 开始的值的位置索引 来直接访问元组中的元素,如果有多级嵌套,就用多级 点号(.)+ 从 0 开始的值的位置索引,例如 x.1.2

  4. println! 中可以使用 {:?}{:#?} 直接打印元组

单元素元组

  单元素元组就是只包含一个元素的元组,在定义时,类型或值后面的逗号是必须的,以区分普通括号。

let y = (1,);                           // 单元素元组, 逗号是必须的,因为(1)会被当做数学运算中的括号,也就是 1,而不是元组
println!("Hello, world! y.0 = {}", y.0);
let y: (i32,) = (2,);                   // 单元素元组, 逗号是必须的,因为(1)会被当做数学运算中的括号,也就是 1,而不是元组
println!("Hello, world! y.0 = {}", y.0);
单元元组

  单元元组(Unit Tuple)是指的不带任何元素的元组,也称为空元组单元类型,用 () 来表示。() 表示空值或空的返回类型。如果表达式不返回任何其他值,则会隐式返回单元元组。

let z = ();
println!("{:?}", z);                    // 打印元组
  1. 单元元组是 Rust 函数的默认返回类型
可变元组

  默认元组是不可变的,在定义时指定 mut 关键字可以让元组种内容可被修改。但是需要注意,新值的数据类型必须与原数据的数据类型是一致的!

let mut rect = (30, 50.50);                    // 具有相同类型的值的元组
println!("Hello, world! rect.0 = {}, rect.1 = {}", rect.0, rect.1);
rect.0 = 60;
rect.1 = 70.70;
println!("Hello, world! rect.0 = {}, rect.1 = {}", rect.0, rect.1);
元组解构

  可以使用模式匹配(pattern matching)来解构(destructure)元组值。并且,在解构时可以选择使用 _(下划线)来不忽略元组中的某些元素。

let x: (i32, f64, u8) = (500, 6.4, 1);  // 正常定义拥有不同类型的值的元组
let (a, b, c) = x;                      // 解构
println!("Hello, world! a = {}, b = {}, c = {}", a, b, c);
let (a, _, c) = x;                      // 忽略第二个元素 (3.14)
println!("Hello, world! a = {}, c = {}", a, c);
元组赋值

  相同类型(元素及个数)的元组之间可以直接进行赋值。如果元组中的元素类型(如 i32f64&str 等)实现了 Copy 特性,赋值会复制元素值;如果元素类型(如 StringVec)没有实现 Copy 特性,赋值时会发生所有权转移,原元组就不能再使用。

let tuple1 = (42, 3.14);  // 元组中的元素类型是 Copy
let tuple2 = tuple1;      // tuple1 会被复制
println!("{:?}", tuple1);  // 可以正常使用 tuple1
println!("{:?}", tuple2);  // 也可以正常使用 tuple2
let tuple1 = (String::from("hello"), 3.14);
let tuple2 = tuple1;  // tuple1 的所有权转移给 tuple2
// println!("{:?}", tuple1);  // 编译错误,tuple1 的所有权已经转移,不能再使用
println!("{:?}", tuple2);  // 可以使用 tuple2
let tuple1 = (String::from("hello"), 3.14);
let tuple2 = (tuple1.0.clone(), tuple1.1);  // 克隆元组中的 String 元素
println!("{:?}", tuple1);  // tuple1 可以继续使用
println!("{:?}", tuple2);  // tuple2 是独立的副本
  1. 若元组元素实现了 Clone,可以通过 clone() 创建深拷贝
元组比较

  相同类型(元素及个数)的元组的元素如果实现了 PartialEq 这个 Trait 或 PartialOrd 这个 Trait,那么他们之间就可以直接进行比较。==!= 要求元素实现 PartialEq;<><=>= 要求元素实现 PartialOrd。

let tup1 = (1, 2, 3, 4, 5);
let tup2 = (1, 2, 3, 4, 5);
let tup3 = (5, 4, 3, 2, 1);
println!("tup1 == tup2: {}", tup1 == tup2); // true
println!("tup2 < tup3: {}", tup2 < tup3);   // true(tup2[0]=1 < tup3[0]=5)
  1. 比较是逐个元素进行对比,只要出现一个符合条件的就认为成立
示例源码

  完整示例源码可以通过 git clone git@gitee.com:itexp/data_compound_tuple.git 下载。

数组

  数组(Array)是一个将多个相同类型的值组合进一个复合类型的主要方式。使用包含在方括号中的逗号分隔的值列表 [元素1, 元素2, ..., 元素n] 来创建一个数组。由于数组中的所有元素类型相同,因此 Rust 提供了多种定义格式。

let x = [1, 2, 3];              // 自动推导出类型和个数
// let x: [i32, i32, i32] = [1, 2, 3];// 【错误】不支持显示给出所有类型
println!("Hello, world! x[0] = {}, x[1] = {}, x[2] = {}", x[0], x[1], x[2]);
let x: [i32; 3] = [1, 2, 3];    // 类型 + 个数
println!("Hello, world! x[0] = {}, x[1] = {}, x[2] = {}", x[0], x[1], x[2]);
let x = [1; 3];                 // 初值 + 个数,相当于 [1, 1, 1]
println!("Hello, world! x[0] = {}, x[1] = {}, x[2] = {}", x[0], x[1], x[2]);

  数组是可以在栈 (stack) 上分配的已知固定大小的单个内存块。可以使用索引来访问数组的元素,索引操作中使用一个无效的值时导致 运行时 错误。当尝试用索引访问一个元素时,Rust 会检查指定的索引是否小于数组的长度。如果索引超出了数组长度,Rust 会 panic。

  1. 数组的长度是固定的,在定义时必须让 Rust 可以明确知道数组中元素的类型和个数(个数至少为 1)

    • Rust 可以自动推导出元素类型和个数,但是,数组类型标注不允许枚举元素类型,例如 let x: [i32, i32, i32] = [1, 2, 3]; 不合法,必须用 [T; N] 格式!
    • 支持通过 : [T; N] 语法来指定变量类型和元素个数,例如,let a: [i32; 5] = [1, 2, 3, 4, 5];
    • 支持通过 [value; N] 语法快速初始化所有元素为相同值,例如,let a = [3; 5];
  2. 可以使用 数组名[从 0 开始的值的位置索引] 来直接访问数组中的元素,如果是嵌套多级就用多次 [从 0 开始的值的位置索引],例如 a[0][1][2]

  3. println! 中可以使用 {:?}{:#?} 直接打印数组

  4. 可以使用 .len() 方法来获取数组的元素个数,返回的是数组的大小

单元素数组

  单元素数组即只有一个元素的数组,值后面的分号可以省略

let x = [1,];                   // 单元素数组,值后面逗号是可以省略的
println!("Hello, world! x[0] = {}", x[0]);
let x: [i32; 1] = [1,];         // 单元素数组,值后面逗号是可以省略的
println!("Hello, world! x[0] = {}", x[0]);
多维数组

  数组可以嵌套,形成多维数组

let x = [[1, 2], [3, 4], [5, 6]]; // 二维数组
println!("Hello, world! x[0][0] = {}, x[0][1] = {}, x[1][0] = {}, x[1][1] = {}, x[2][0] = {}, x[2][1] = {}", 
        x[0][0], x[0][1], x[1][0], x[1][1], x[2][0], x[2][1]);
let arr: [[[i32; 2]; 2]; 2] = [
    [
        [1, 2],
        [3, 4],
    ],
    [
        [5, 6],
        [7, 8],
    ],
];                              // 三维数组
println!("Hello, world! arr[0][0][0] = {}, arr[0][0][1] = {}, arr[0][1][0] = {}, arr[0][1][1] = {}, arr[1][0][0] = {}, arr[1][0][1] = {}, arr[1][1][0] = {}, arr[1][1][1] = {}", 
        arr[0][0][0], arr[0][0][1], arr[0][1][0], arr[0][1][1], arr[1][0][0], arr[1][0][1], arr[1][1][0], arr[1][1][1]);
可变数组

  默认数组是不可变的,在定义时指定 mut 关键字可以让数组种内容可被修改。但是需要注意,新值的数据类型必须与原数据的数据类型是一致的!

let mut x = [1, 2, 3];
println!("Hello, world! x[0] = {}, x[1] = {}, x[2] = {}", x[0], x[1], x[2]);
x[0] = 4;
x[1] = 5;
x[2] = 6;
println!("Hello, world! x[0] = {}, x[1] = {}, x[2] = {}", x[0], x[1], x[2]);
数组赋值

  相同类型(元素及个数)的数值之间可以直接赋值。若数组包含未实现 Copy trait 的类型(如 StringVec),直接赋值会导致 所有权转移,原数组失效;若所有元素实现 Copy trait(如 i32, f64),直接赋值会 复制整个数组,原数组仍有效。

let arr1 = [String::from("Rust"), String::from("C++")];
let arr2 = arr1; // 所有权转移给 arr2
// println!("{:?}", arr1); // 错误!arr1 已失效
println!("{:?}", arr2);
let nums = [1, 2, 3];
let nums_copy = nums; // 复制 nums 到 nums_copy
println!("原数组: {:?}, 复制后数组: {:?}", nums, nums_copy); // 均有效
let arr1 = [String::from("Rust"), String::from("C++")];
let arr2 = arr1.clone(); // 深拷贝每个元素
println!("arr1: {:?}, arr2: {:?}", arr1, arr2); // 均有效
  1. 若数组元素实现 Clone trait,可通过 clone() 方法深拷贝
数组比较

  相同类型(元素及个数)的数组的元素如果实现了 PartialEq 这个 Trait 或 PartialOrd 这个 Trait,那么他们之间就可以直接进行比较。==!= 要求元素实现 PartialEq;<><=>= 要求元素实现 PartialOrd。

let arr1 = [1, 2, 3, 4, 5];
let arr2 = [1, 2, 3, 4, 5];
let arr3 = [5, 4, 3, 2, 1];
println!("arr1 == arr2: {}", arr1 == arr2); // true
println!("arr2 < arr3: {}", arr2 < arr3);   // true(arr2[0]=1 < arr3[0]=5)
  1. 比较是逐个元素进行对比,只要出现一个符合条件的就认为成立
示例源码

  完整示例源码可以通过 git clone git@gitee.com:itexp/data_compound_array.git 下载。

字符串类型

  在 Rust 中,字符串类型实际上分为 String 类型 和 &str 类型。如果考虑到操作系统,还有 OsStringOsStr 类型,与 C 语言交互时,还有 CStringCStr 类型,在此不做学习。

String 类型

  String 类型是一个在堆内存中分配的、可变的、UTF-8 编码的字符串类型,它是一个拥有所有权的类型,可以动态地增长和修改。同样,String 类型可以根据是否有 mut 关键字分为不可变字符串和可变字符串。

fn main() {
    let s1 = String::from("Hello");
    println!("s1: {}", s1);
    let mut s3 = String::new(); // 空字符串
    s3.push_str("Hello");
    println!("s3: {}", s3);
}

&str 类型

  &str 类型(就和 u32 一样是一种类型)则通常表示一个不可变的字符串切片,指向一些字符串数据的引用!字符串字面值就是 &str 类型。 后文我们将详细学习切片类型,这里只需要了解 &str 是一种切片类型!

fn main() {
    let s = "字符串字面值";         // 字符串字面值默认就是 &str 类型
    println!("s: {}", s);
    let s: &str = "字符串字面值";   // 显式指定类型 &str
    println!("s: {}", s);
}

String 和 &str 之间的转换

  由于 String&str 之间有着紧密的关系,Rust 提供了一些方法来方便它们之间的转换:&str 类型可以通过 to_string() 方法返回 String 类型;而 String 类型则可以通过 as_str() 方法或者直接 & 引用来返回 &str 类型。

fn main() {
    let s = "字符串字面值";     // 字符串字面值默认就是 &str 类型
    let s1 = s.to_string();     // &str -> String
    println!("s1: {}", s1);
    let string_s = String::from("Hello, world!");
    let str_s: &str = &string_s;  // String -> &str
    println!("{}", str_s);  // 输出: Hello, world!
}

示例源码

  完整示例源码可以通过 git clone git@gitee.com:itexp/data_string.git 下载即可

自定义类型

  在 Rust 中,自定义数据类型是通过结构体(struct)、枚举(enum)、和联合体(union)来定义的。这些数据类型允许我们创建复杂的数据结构和实现特定的功能。

结构体

  结构体(struct)是编程语言中一种非常常见的数据结构,用于将不同类型的数据组合在一起形成一个整体。它的主要作用是将多个相关的数据项(通常是不同类型)存储在一个单一的实体中,使得它们能够一起处理和传递。

  在 Rust 中,结构体的定义需要使用 struct 关键字并为整个结构体提供一个名字,然后,在大括号中定义每一部分数据的名字和类型,名字和类型的组合称为字段(field)。并且,整个定义的最后({} 后面)没有分号,注意对比单元结构体!

/* =============================== 结构体定义 ================================ */
// 需要使用 struct 关键字并为整个结构体提供一个名字
// 定义的最后没有分号
struct UserInfo { // 结构体定义
    height: u8,
    weight: u8,
}

struct User<'a> { // 结构体定义
    active: bool,
    username: String,
    email: String,
    ids: &'a str,
    sign_in_count: u64,
    info: UserInfo, // 结构体嵌套
}

fn main() {
    /* ============================ 结构体实例化 ============================= */
    let user1 = User {      // 通过为每个字段指定具体值来创建这个结构体的实例
        active: true,
        username: String::from("user1"),
        email: String::from("email1"),
        ids: "id1",
        sign_in_count: 1,
        info: UserInfo { // 结构体嵌套
            height: 180,
            weight: 70,
        },
    };
    print!("user1.acive = {}, user1.username = {}, user1.email = {}, user1.sign_in_count = {}, user1.info.height = {}, user1.info.weight = {}\r\n", 
    user1.active, user1.username, user1.email, user1.sign_in_count, user1.info.height, user1.info.weight);
}

  定义了结构体后,为了使用它,需要通过为每个字段指定具体值来创建这个结构体的实例。创建一个实例需要以结构体的名字开头,接着在大括号中使用 字段名: 字段值 键值对的形式提供字段,其中 key 是字段的名字,value 是需要存储在字段中的数据值。

  1. 结构体也可以嵌套其他结构体

  2. 实例化时字段的顺序不需要和它们在结构体中声明的顺序一致

  3. 可以使用 结构体实例名.字段名 语法从结构体中获取某个特定的值。

  4. 结构体的定义中若包含引用字段(如 &str&[T]&T 等),必须显式声明生命周期参数 'a 关于这一点,我们后续再详细学习!

结构体更新语法

  可以通过结构体更新语法(struct update syntax)实现使用旧实例的大部分值来改变新实例的部分值来创建一个新的结构体实例,而 .. 语法则可以指定剩余未显式设置值的字段应有与给定实例对应字段相同的值。此外,结构体实例之间可以直接进行赋值。

/* =============================== 结构体定义 ================================ */
// 需要使用 struct 关键字并为整个结构体提供一个名字
// 定义的最后没有分号
struct UserInfo { // 结构体定义
    height: u8,
    weight: u8,
}

struct User<'a> { // 结构体定义
    active: bool,
    username: String,
    email: String,
    ids: &'a str,
    sign_in_count: u64,
    info: UserInfo, // 结构体嵌套
}

fn main() {
    let mut user2 = User {      // 通过添加 mut 关键字来创建一个可变的结构体实例
        active: true,
        username: String::from("user2"),
        email: String::from("email1"),
        ids: "id2",
        sign_in_count: 1,
        info: UserInfo { // 结构体嵌套
            height: 180,
            weight: 70,
        },
    };
    user2.email = String::from("email.com");    // 修改结构体实例的字段值
    print!("user2.acive = {}, user2.username = {}, user2.email = {}, user2.sign_in_count = {}, user2.info.height = {}, user2.info.weight = {}\r\n", 
    user2.active, user2.username, user2.email, user2.sign_in_count, user2.info.height, user2.info.weight);
    /* =========================== 结构体更新语法 ============================ */
    let user3 = User {  // 通过结构体更新语法(struct update syntax)来用旧结构体实例初始化新结构体实例
        active: true,
        username: user2.username,
        email: user2.email,
        ids: user2.ids,
        sign_in_count: user2.sign_in_count,
        info: user2.info,   // 结构体嵌套赋值,此后 user2.info 就不能再使用了
    };
    print!("user3.acive = {}, user3.username = {}, user3.email = {}, user3.sign_in_count = {}, user3.info.height = {}, user3.info.weight = {}\r\n", 
    user3.active, user3.username, user3.email, user3.sign_in_count, user3.info.height, user3.info.weight);
    print!("user2.acive = {}, user2.sign_in_count = {}, user2.ids = {}\r\n", 
    user2.active,  user2.sign_in_count, user2.ids);// 不能再使用 user2 中已经赋值给 user3 的 username 和 email 及 info 字段了
    let user4 = User {
        active: true,
        ..user3         // 使用 .. 语法指定剩余未显式设置值的字段应有与给定实例对应字段相同的值
    };
    print!("user4.acive = {}, user4.username = {}, user4.email = {}, user4.sign_in_count = {}\r\n", 
    user4.active, user4.username, user4.email, user4.sign_in_count);
    print!("user3.acive = {}, user3.sign_in_count = {}\r\n", 
    user3.active, user3.sign_in_count); // 结构更新语法就像带有 = 的赋值,它实际上移动了数据,致使原来的字段不能再被使用
    let user5 = user4;  // 更特殊的情况,user4 彻底没法用了,因为 user4 的所有权已经转移给 user5
    print!("user5.acive = {}, user5.username = {}, user5.email = {}, user5.sign_in_count = {}\r\n", 
    user5.active, user5.username, user5.email, user5.sign_in_count);
}

  如果结构体实例中的元素类型(如 i32f64&str 等)实现了 Copy 特性,赋值会复制元素值;如果元素类型(如 StringVec)没有实现 Copy 特性,赋值时会发生所有权转移,原结构体实例中的字段就不能再使用。

可变结构体

  默认的结构体实例是不可变的,也就无法更改结构体中字段的值。如果要支持修改结构体中的字段,则必须在整个实例前加 mut 关键字使整个实例可变,Rust 并不允许只将结构体中的某个字段标记为可变。

/* =============================== 结构体定义 ================================ */
// 需要使用 struct 关键字并为整个结构体提供一个名字
// 定义的最后没有分号
struct UserInfo { // 结构体定义
    height: u8,
    weight: u8,
}

struct User<'a> { // 结构体定义
    active: bool,
    username: String,
    email: String,
    ids: &'a str,
    sign_in_count: u64,
    info: UserInfo, // 结构体嵌套
}

fn main() {
    /* =========================== 可变结构体实例 ============================ */
    let mut user2 = User {      // 通过添加 mut 关键字来创建一个可变的结构体实例
        active: true,
        username: String::from("user2"),
        email: String::from("email1"),
        ids: "id2",
        sign_in_count: 1,
        info: UserInfo { // 结构体嵌套
            height: 180,
            weight: 70,
        },
    };
    user2.email = String::from("email.com");    // 修改结构体实例的字段值
    print!("user2.acive = {}, user2.username = {}, user2.email = {}, user2.sign_in_count = {}, user2.info.height = {}, user2.info.weight = {}\r\n", 
    user2.active, user2.username, user2.email, user2.sign_in_count, user2.info.height, user2.info.weight);
}
结构体解构

  Rust 支持结构体的解构,可以将结构体的字段分配给局部变量。并且支持仅列出自己需要的字段名然后使用 .. 语法(必须放到最后面)忽略自己不需要的字段这种方式。

/* =============================== 结构体定义 ================================ */
// 需要使用 struct 关键字并为整个结构体提供一个名字
// 定义的最后没有分号
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    let User {active, email, ..} = user5; // 解构结构体实例,其中 .. 表示忽略其余字段
    println!("active: {}, email: {}", active, email);
}
元组结构体

  元组结构体(tuple structs)有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型。要定义元组结构体,struct 关键字和结构体名开头并后跟元组中的类型,并且最后以分号结尾

/* ============================= 元组结构体定义 ============================== */
// 以 struct 关键字和结构体名开头并后跟元组中的类型来定义元组结构体
struct Color(u32, u32, u32);
struct User(String, &'static str, f32, i32, bool);   // 元组结构体的字段可以是不同的类型

fn main() {
    /* ========================== 元组结构体实例化 =========================== */
    // 虽然元组结构体有相同的字段类型,但它们是不同的类型
    let black = Color(1, 2, 3);
    // 虽然元组结构体没有具名字段,但它们仍然可以通过点号和索引访问元组的字段
    println!("black.0: {}, black.1: {}, black.2: {}", black.0, black.1, black.2);
    let user1 = User(String::from("zhangsan"), "zhangsan", 1.0, 2, true);
    println!("user1.0: {}, user1.1: {}, user1.2: {}, user1.3: {}, user1.4: {}",
             user1.0, user1.1, user1.2, user1.3, user1.4);
    /* ========================== 可变元组结构体实例 ========================= */
    let mut user2 = User(String::from("lisi"), "lisi", 1.0, 2, true);
    user2.0 = String::from("wangwu"); // 修改元组结构体的字段
    user2.1 = "wangwu";     // 修改元组结构体的字段
    user2.2 = 2.0;          // 修改元组结构体的字段
    user2.3 = 3;            // 修改元组结构体的字段
    user2.4 = false;        // 修改元组结构体的字段
    println!("user2.0: {}, user2.1: {}, user2.2: {}, user2.3: {}, user2.4: {}",
             user2.0, user2.1, user2.2, user2.3, user2.4);
    /* =========================== 结构体更新语法 ============================ */
    let user3 = User(user2.0, user2.1, 1.0, 2, true); // 此后,user2.0 无法继续使用,因为它已经被 user3.0 占用了
    println!("user3.0: {}, user3.1: {}, user3.2: {}, user3.3: {}, user3.4: {}",
             user3.0, user3.1, user3.2, user3.3, user3.4);
    let user4 = user3;  // 此后,user3 无法继续使用,因为它已经被 user4 占用了
    println!("user4.0: {}, user4.1: {}, user4.2: {}, user4.3: {}, user4.4: {}",
             user4.0, user4.1, user4.2, user4.3, user4.4);
    /* ========================== 元组结构体解构 ============================= */
    let Color(r, g, b) = black;
    println!("r: {}, g: {}, b: {}", r, g, b);
    let User(name, _, _, _, _) = user1; // 这里的 _ 表示忽略掉的字段
    println!("name: {}", name);
    let  Color(g, ..) = black;  // 这里是按照顺序来解构的,g 实际就是 black.0
    println!("g: {}", g);
}
  1. 元组结构体不能使用 .. 语法来更新实例

  2. 元组结构体没有具名字段,但它们仍然可以通过点号和索引访问元组的字段

  3. 元组结构体解构操作是严格按照顺序来的,因为它没有具名字段,只能通过位置索引

类单元结构体

  类单元结构体(unit-like structs)是一个没有任何字段的结构体,它们类似于 (),即元组中提到的 unit 类型。要定义类单元结构体,只需要使用 struct 关键字,接着是我们想要的名称,最后加一个分号。不需要花括号或圆括号!

// 以 struct 关键字和结构体名开头并后跟分号来定义类单元结构体
struct Unit; // 不带有任何字段,一般用于 trait

fn main() {
    let unit = Unit{};	// 实例化
}
  1. 编译后不占用内存空间。无数据,仅作为类型标识,不存储任何信息。
示例源码
  1. 通用定义部分的源码:git clone git@gitee.com:itexp/data_struct_normal.git
  2. 元组结构体相关源码:git clone git@gitee.com:itexp/data_struct_tuple.git
  3. 单元机构提相关源码:git clone git@gitee.com:itexp/data_struct_unit.git

枚举

  在 Rust 中,枚举(Enum) 是一种强大的数据类型,允许你定义一组可能的变体(Variants,就是指的不同的枚举值),每个变体可以关联不同类型和数量的数据。枚举通过 enum 关键字定义,其中的成员可以是无数据、具名数据或元组数据。

/* =============================== 结构体定义 ================================ */
struct MsgSize { // 结构体定义
    height: u8,
    width: u8,
}

/* =============================== 枚举定义 ================================ */
enum MsgType {
    V4,
    V6,
}

enum Msg<'a> {
    Quit,                       // 可以是无数据类型的普通值
    Move { x: i32, y: i32 },    // 可以是具名字段(结构体形式)
    Write(String),              // 单个数据(元组形式)
    Apps(u8, u8, u8),           // 多个数据(元组形式)
    Text(&'a str),              // 引用外部字符串
    Type(MsgType),              // 直接使用枚举类型
    Size(MsgSize),              // 直接使用结构体类型
}

fn main() {
    /* ============================= 枚举实例化 ============================== */
    let quit = Msg::Quit;
    let mov = Msg::Move { x: 10, y: 20 };
    let write = Msg::Write("Hello".to_string());
    let app = Msg::Apps(255, 0, 0);
    let text = Msg::Text("Hello");
    let type_msg = Msg::Type(MsgType::V4);
    let size = Msg::Size(MsgSize { height: 10, width: 20 });
}
  1. 可以嵌套其他枚举或结构体

  2. 若枚举中包含引用类型,必须显式标注生命周期,也就是 'a 这个语法。关于这一点,我们后续再详细学习!

模式匹配

  模式匹配(match)允许在处理枚举值时,针对不同的变体执行不同的代码。而枚举和模式匹配 是紧密结合的核心特性

match size {
    Msg::Quit => println!("Quit"),
    Msg::Move { x, y } => println!("Move to x: {}, y: {}", x, y),
    Msg::Write(text) => println!("Write: {}", text),
    Msg::Apps(r, g, b) => println!("Apps color: R: {}, G: {}, B: {}", r, g, b),
    Msg::Text(text) => println!("Text: {}", text),
    Msg::Type(msg_type) => match msg_type {
        MsgType::V4 => println!("MsgType: V4"),
        MsgType::V6 => println!("MsgType: V6"),
    },
    Msg::Size(size) => println!("Size: height: {}, width: {}", size.height, size.width),
}

  if let 是一种简洁的模式匹配语法,专门用于处理只关心 单个枚举变体 或 特定模式 的场景。它结合了 if 的条件判断和 let 的模式绑定能力,能够简化代码,避免冗长的 match 表达式。

if let Msg::Quit = quit {
    println!("Quit");
} else if let Msg::Move { x, y } = mov {
    println!("Move to x: {}, y: {}", x, y);
} else if let Msg::Write(text) = write {
    println!("Write: {}", text);
} else if let Msg::Apps(r, g, b) = app {
    println!("Apps color: R: {}, G: {}, B: {}", r, g, b);
} else if let Msg::Text(text) = text {
    println!("Text: {}", text);
} else if let Msg::Type(msg_type) = type_v6 {
    match msg_type {
        MsgType::V4 => println!("MsgType: V4"),
        MsgType::V6 => println!("MsgType: V6"),
    }
}
示例源码

  完整示例源码可以通过 git clone git@gitee.com:itexp/data_enum.git 下载即可

参考

  1. https://kaisery.github.io/trpl-zh-cn/
  2. https://www.cnblogs.com/traditional/p/17767848.html

网站公告

今日签到

点亮在社区的每一天
去签到