文章目录
- 1 基本类型
- 1.1 数值类型
- 1.1.1 Rust 中的内置的整数类型:
- 1.1.2 浮点类型
- 1.1.3 数学运算
- 1.1.4 位运算
- 1.1.5 序列(Range)
- 1.2 字符、布尔、单元类型
- 1.3 语句和表达式
- 1.4 函数
- 2 所有权与借用
- 2.1 栈(Stack)与堆(Heap)
- 2.2 所有权原则
- 2.2.1 转移所有权
- 2.2.2 克隆(深拷贝)
- 2.2.3 拷贝(浅拷贝)
- 2.2.4 函数传值与返回
- 2.3 引用与借用
- 可变引用
- 悬垂引用(Dangling References)
- 3 复合类型
- 3.1 字符串与切片
- 3.1.1 切片(slice)
- 3.1.2 字符串操作
- 追加(push)
- 插入(insert)
- 替换(replace)
- 删除(delete)
- 连接(concatenate)
- 遍历字符串
- 3.2 元组
- 用模式匹配解构元组
- 用 `.` 来访问元组
- 3.3 结构体
- 3.3.1 结构体语法
- 定义结构体
- 创建结构体实例
- 访问结构体字段
- 简化结构体创建
- 3.3.2 元组结构体(Tuple Struct)
- 3.3.3 单元结构体(Unit-like Struct)
- 3.3.4 使用 `#[derive(Debug)]` 来打印结构体的信息
- 3.4 枚举
- 3.4.1 枚举的语法
- 3.4.2 Opention枚举处理空值
- 3.5 数组
- 3.5.1 创建数组
- 3.5.2 数组越界访问
- 3.5.3 数组切片
1 基本类型
示例, 在rust中,变量是默认不可变的。要使变量可变,要用mut
修饰
// Rust 程序入口函数,跟其它语言一样,都是 main,该函数目前无返回值
fn main() {
// 使用let来声明变量,进行绑定,a是不可变的
// 此处没有指定a的类型,编译器会默认根据a的值为a推断类型:i32,有符号32位整数
// 语句的末尾必须以分号结尾
let a = 10;
// 主动指定b的类型为i32
let b: i32 = 20;
// 这里有两点值得注意:
// 1. 可以在数值中带上类型:30i32表示数值是30,类型是i32
// 2. c是可变的,mut是mutable的缩写
let mut c = 30i32;
// 还能在数值和类型中间添加一个下划线,让可读性更好
let d = 30_i32;
// 跟其它语言一样,可以使用一个函数的返回值来作为另一个函数的参数
let e = add(add(a, b), add(c, d));
// println!是宏调用,看起来像是函数但是它返回的是宏定义的代码块
// 该函数将指定的格式化字符串输出到标准输出中(控制台)
// {}是占位符,在具体执行过程中,会把e的值代入进来
println!("( a + b ) + ( c + d ) = {}", e);
}
// 定义一个函数,输入两个i32类型的32位有符号整数,返回它们的和
fn add(i: i32, j: i32) -> i32 {
// 返回相加值,这里可以省略return
i + j
}
1.1 数值类型
Rust 每个值都有其确切的数据类型,总的来说可以分为两类:基本类型和复合类型。 基本类型意味着它们往往是一个最小化原子类型,无法解构为其它类型(一般意义上来说),由以下组成:
- 数值类型: 有符号整数 (i8, i16, i32, i64, isize)、 无符号整数 (u8, u16, u32, u64, usize) 、浮点数 (f32, f64)、以及有理数、复数
- 字符串:字符串字面量和字符串切片 &str
- 布尔类型: true和false
- 字符类型: 表示单个 Unicode 字符,存储为 4 个字节
- 单元类型: 即 () ,其唯一的值也是 ()
1.1.1 Rust 中的内置的整数类型:
长度 | 有符号类型 | 无符号类型 |
---|---|---|
8 位 | i8 | u8 |
16 位 | i16 | u16 |
32 位 | i32 | u32 |
64 位 | i64 | u64 |
128 位 | i128 | u128 |
视架构而定 | isize | usize |
每个有符号类型规定的数字范围是 − ( 2 n − 1 ) -(2^{n-1}) −(2n−1)~ 2 n − 1 − 1 2^{n-1}-1 2n−1−1,其中 n 是该定义形式的位长度。因此 i8 可存储数字范围是 -(27) ~ 27 - 1,即 -128 ~ 127。无符号类型可以存储的数字范围是 0 ~ 2n - 1,所以 u8 能够存储的数字为 0 ~ 28 - 1,即 0 ~ 255。
此外,isize
和 usize
类型取决于程序运行的计算机 CPU 类型: 若 CPU 是 32 位的,则这两个类型是 32 位的,同理,若 CPU 是 64 位,那么它们则是 64 位。
整形字面量可以用下表的形式书写:
数字字面量 | 示例 |
---|---|
十进制 | 98_222 |
十六进制 | 0xff |
八进制 | 0o77 |
二进制 | 0b1111_0000 |
字节 (仅限于 u8) | b’A’ |
1.1.2 浮点类型
浮点类型数字 是带有小数点的数字,在 Rust 中浮点类型数字也有两种基本类型: f32
和 f64
,分别为 32 位和 64 位大小。默认浮点类型是f64
,在现代的 CPU 中它的速度与 f32
几乎相同,但精度更高。
当需要使用浮点数时,需遵守以下准则:
- 避免在浮点数上测试相等性
- 当结果在数学上可能存在未定义时,需要格外的小心
例如:
fn main() {
let abc: (f32, f32, f32) = (0.1, 0.2, 0.3);
let xyz: (f64, f64, f64) = (0.1, 0.2, 0.3);
println!("abc (f32)");
println!(" 0.1 + 0.2: {:x}", (abc.0 + abc.1).to_bits());
println!(" 0.3: {:x}", (abc.2).to_bits());
println!();
println!("xyz (f64)");
println!(" 0.1 + 0.2: {:x}", (xyz.0 + xyz.1).to_bits());
println!(" 0.3: {:x}", (xyz.2).to_bits());
println!();
assert!(abc.0 + abc.1 == abc.2);
assert!(xyz.0 + xyz.1 == xyz.2);
}
运行该程序,输出如下:
abc (f32)
0.1 + 0.2: 3e99999a
0.3: 3e99999a
xyz (f64)
0.1 + 0.2: 3fd3333333333334
0.3: 3fd3333333333333
thread 'main' panicked at 'assertion failed: xyz.0 + xyz.1 == xyz.2',
➥ch2-add-floats.rs.rs:14:5
note: run with `RUST_BACKTRACE=1` environment variable to display
➥a backtrace
对于数学上未定义的结果,例如对负数取平方根-42.1.sqrt()
,会产生一个特殊的结果:Rust 的浮点数类型使用 NaN
(not a number)来处理这些情况。可以使用 is_nan()
等方法,可以用来判断一个数值是否是 NaN
fn main() {
let x = (-42.0_f32).sqrt();
if x.is_nan() {
println!("未定义的数学行为")
}
}
1.1.3 数学运算
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]);
}
1.1.4 位运算
Rust的运算基本上和其他语言一样
运算符 | 说明 |
---|---|
& 位与 | 相同位置均为1时则为1,否则为0 |
| 位或 | 相同位置只要有1时则为1,否则为0 |
^ 异或 | 相同位置不相同则为1,相同则为0 |
! 位非 | 把位中的0和1相互取反,即0置为1,1置为0 |
<< 左移 | 所有位向左移动指定位数,右位补0 |
>> 右移 | 所有位向右移动指定位数,带符号移动(正数补0,负数补1) |
1.1.5 序列(Range)
Rust 提供了一个非常简洁的方式,用来生成连续的数值,例如 1..5
,生成从 1 到 4 的连续数字,不包含 5 ;1..=5
,生成从 1 到 5 的连续数字,包含 5.
序列只允许用于数字或字符类型
1.2 字符、布尔、单元类型
- Rust 的字符不仅仅是 ASCII,所有的 Unicode 值都可以作为 Rust 字符,包括单个的中文、日文、韩文、emoji 表情符号等等,都是合法的字符类型。由于 Unicode 都是 4 个字节编码,因此字符类型也是占用 4 个字节:
fn main() {
let x = '中';
println!("字符'中'占用了{}字节的内存大小",std::mem::size_of_val(&x)); //4
let your_character:char='8'; // What's your favorite character?
// Try a letter, try a number, try a special character, try a character
// from a different language than your own, try an emoji!
if your_character.is_alphabetic() {
println!("Alphabetical!");
} else if your_character.is_numeric() {
println!("Numerical!");
} else {
println!("Neither alphabetic nor numeric!");
}
}
-
Rust 中的布尔类型有两个可能的值:
true
和false
,布尔值占用内存的大小为 1 个字节 -
单元类型就是
()
。main 函数就返回这个单元类型()
,不能说 main 函数无返回值,因为没有返回值的函数在 Rust 中是有单独的定义的:发散函数( diverge function ),顾名思义,无法收敛的函数。
例如常见的println!()
的返回值也是单元类型()
。再比如,可以用()
作为 map 的值,表示我们不关注具体的值,只关注key
。 可以作为一个值用来占位,但是完全不占用任何内存。
1.3 语句和表达式
Rust 的函数体是由一系列语句组成,最后由一个表达式来返回值,例如:
fn add_with_extra(x: i32, y: i32) -> i32 {
let x = x + 1; // 语句
let y = y + 5; // 语句
x + y // 表达式
}
语句会执行一些操作但是不会返回一个值,而表达式会在求值后返回一个值,因此在上述函数体的三行代码中,前两行是语句,最后一行是表达式。
对于 Rust 语言而言,这种基于语句(statement)和表达式(expression)的方式是非常重要的,你需要能明确的区分这两个概念。基于表达式是函数式语言的重要特征,表达式总要返回值。
调用一个函数是表达式,因为会返回一个值,调用宏也是表达式,用花括号包裹最终返回一个值的语句块也是表达式,总之,能返回值,它就是表达式:
fn main() {
let y = {
let x = 3;
x + 1
};
println!("The value of y is: {}", y);
}
上面使用一个语句块表达式将值赋给 y 变量。表达式不能加分号!
表达式如果不返回任何值,会隐式地返回一个 () 。
fn main() {
assert_eq!(ret_unit_type(), ())
}
fn ret_unit_type() {
let x = 1;
// if 语句块也是一个表达式,因此可以用于赋值,也可以直接返回
// 类似三元运算符,在Rust里我们可以这样写
let y = if x % 2 == 1 {
"odd"
} else {
"even"
};
// 或者写成一行
let z = if x % 2 == 1 { "odd" } else { "even" };
}
if 是最基本的控制流语句,使用方法:
pub fn foo_if_fizz(fizzish: &str) -> &str {
if fizzish == "fizz" {
"foo"
} else if fizzish=="fuzz" {
"bar"
}else {
"baz"
}
}
1.4 函数
函数要点:
- 函数名和变量名使用蛇形命名法(snake case),例如
fn add_two() -> {}
- 函数的位置可以随便放,Rust 不关心我们在哪里定义了函数,只要有定义即可
- 每个函数参数都需要标注类型
Rust 是强类型语言,因此需要你为每一个函数参数都标识出它的具体类型。
特殊返回类型 - 单元类型 ()
,是一个零长度的元组。它没啥作用,但是可以用来表达一个函数没有返回值:
- 函数没有返回值,那么返回一个
()
- 通过
;
结尾的表达式返回一个()
//显示返回()
fn clear(text: &mut String) -> () {
*text = String::from("");
}
当用!
作函数返回类型的时候,表示该函数永不返回( diverge function ),特别的,这种语法往往用做会导致程序崩溃的函数。
函数示例:
fn main() {
let original_price = 51;
println!("Your sale price is {}", sale_price(original_price));
}
fn sale_price(price: i32) ->i32 {
if is_even(price) {
price - 10
} else {
price - 3
}
}
fn is_even(num: i32) -> bool {
num % 2 == 0
}
2 所有权与借用
所有的程序都必须和计算机内存打交道,如何从内存中申请空间来存放程序的运行内容,如何在不需要的时候释放这些空间,成了重中之重,也是所有编程语言设计的难点之一。在计算机语言不断演变过程中,出现了三种流派:
- 垃圾回收机制(GC),在程序运行时不断寻找不再使用的内存,典型代表:Java、Go
- 手动管理内存的分配和释放, 在程序中,通过函数调用的方式来申请和释放内存,典型代表:C++
- 通过所有权来管理内存,编译器在编译时会根据一系列规则进行检查
其中 Rust 选择了第三种,最妙的是,这种检查只发生在编译期,因此对于程序运行期,不会有任何性能上的损失。
2.1 栈(Stack)与堆(Heap)
堆和栈是编程语言最核心的数据结构,核心目标就是为程序在运行时提供可供使用的内存空间。
栈按照顺序存储值并以相反顺序取出值,这也被称作后进先出。
与栈不同,对于大小未知或者可能变化的数据,我们需要将它存储在堆上。当向堆上放入数据时,需要请求一定大小的内存空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的指针, 该过程被称为在堆上分配内存,有时简称为 “分配”(allocating)。接着,该指针会被推入栈中,因为指针的大小是已知且固定的,在后续使用过程中,你将通过栈中的指针,来获取数据在堆上的实际内存位置,进而访问该数据。
因此,入栈比在堆上分配内存要快,因为入栈时操作系统无需分配新的空间,只需要将新数据放入栈顶即可;栈数据往往可以直接存储在 CPU 高速缓存中,而堆数据只能存储在内存中。访问堆上的数据比访问栈上的数据慢,因为必须先访问栈再通过栈上的指针来访问内存。
2.2 所有权原则
Rust中关于所有权的规则:
- Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
- 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
- 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)
2.2.1 转移所有权
String 类型是一个复杂类型,由存储在栈中的堆指针、字符串长度、字符串容量共同组成,其中堆指针是最重要的,它指向了真实存储字符串内容的堆内存,容量是堆内存分配空间的大小,长度是目前已经使用的大小。总之 String 类型指向了一个堆上的空间,这里存储着它的真实数据,例如:
let s1 = String::from("hello");
let s2 = s1;
注意,当变量离开作用域后,Rust 会自动调用 drop
函数并清理变量的堆内存。不过由于两个 String 变量指向了同一位置。这就有了一个问题:当 s1 和 s2 离开作用域,它们都会尝试释放相同的内存。这是一个叫做 二次释放(double free) 的错误,也是之前提到过的内存安全性 BUG 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。
Rust 这样解决该问题:当 s1 赋予 s2 后,Rust 认为 s1 不再有效,因此也无需在 s1 离开作用域后 drop
任何东西,这就是把所有权从 s1 转移给了 s2,s1 在被赋予 s2 后就马上失效了。拷贝指针、长度和容量而不拷贝数据听起来就像浅拷贝,但是又因为 Rust 同时使第一个变量 s1 无效了,因此这个操作被称为 移动(move),而不是浅拷贝。上面的例子可以解读为 s1 被移动到了 s2 中
Rust 永远也不会自动创建数据的 “深拷贝”。因此,任何自动的复制都不是深拷贝,可以被认为对运行时性能影响较小。
2.2.2 克隆(深拷贝)
如果我们确实需要深度复制 String 中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 clone
的方法。但是对于执行较为频繁的代码,使用 clone
会极大的降低程序性能。
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
}
2.2.3 拷贝(浅拷贝)
Rust 有一个叫做 Copy
的特征,可以用在类似整型这样在栈中存储的类型。如果一个类型拥有 Copy
特征,一个旧的变量在被赋值给其他变量后仍然可用。
那么什么类型是可 Copy 的呢?可以查看给定类型的文档来确认,不过作为一个通用的规则: 任何基本类型的组合可以 Copy ,不需要分配内存或某种形式资源的类型是可以 Copy 的。如下是一些 Copy 的类型:
- 所有整数类型,比如
u32
。 - 布尔类型,
bool
,它的值是 true 和 false。 - 所有浮点数类型,比如
f64
。 - 字符类型,
char
。 - 元组,当且仅当其包含的类型也都是 Copy 的时候。比如,
(i32, i32)
是 Copy 的,但(i32, String)
就不是。 - 不可变引用
&T
,例如转移所有权中的最后一个例子,但是注意: 可变引用&mut T
是不可以 Copy的
2.2.4 函数传值与返回
将值传递给函数,一样会发生 移动 或者 复制,就跟 let
语句一样,下面的代码展示了所有权、作用域的规则:
fn main() {
let s1 = gives_ownership(); // gives_ownership 将返回值
// 移给 s1
let s2 = String::from("hello"); // s2 进入作用域
let s3 = takes_and_gives_back(s2); // s2 被移动到
// takes_and_gives_back 中,
// 它也将返回值移给 s3
} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
// 所以什么也不会发生。s1 移出作用域并被丢弃
fn gives_ownership() -> String { // gives_ownership 将返回值移动给
// 调用它的函数
let some_string = String::from("hello"); // some_string 进入作用域.
some_string // 返回 some_string 并移出给调用的函数
}
// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
a_string // 返回 a_string 并移出给调用的函数
}
2.3 引用与借用
Rust 通过获取变量的引用,称之为借用(borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来,当使用完毕后,也必须要物归原主。
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);//错误,不同类型的比较
assert_eq!(5, *y);//使用 *y 来解出引用所指向的值(也就是解引用)。一旦解引用了 y,就可以访问 y 所指向的整型值并可以与 5 做比较。
}
可变引用
首先,声明 s 是可变类型,其次创建一个可变的引用 &mut s
和接受可变引用参数 some_string: &mut String
的函数。
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
同一作用域,特定数据只能有一个可变引用,这种限制的好处就是使 Rust 在编译期就避免数据竞争,数据竞争可由以下行为造成:
- 两个或更多的指针同时访问同一数据
- 至少有一个指针被用来写入数据
- 没有同步数据访问的机制
可以通过手动限制变量的作用域:
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用
let r2 = &mut s;
可变引用与不可变引用不能同时存在,比如
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 大问题
println!("{}, {}, and {}", r1, r2, r3);
其实这个也很好理解,正在借用不可变引用的用户,肯定不希望他借用的东西,被另外一个人莫名其妙改变了。多个不可变借用被允许是因为没有人会去试图修改数据,每个人都只读这一份数据而不做修改,因此不用担心数据被污染。
注意,引用的作用域 s 从创建开始,一直持续到它最后一次使用的地方,这个跟变量的作用域有所不同,变量的作用域从创建持续到某一个花括号 }
悬垂引用(Dangling References)
悬垂引用也叫做悬垂指针,意思为指针指向某个值后,这个值被释放掉了,而指针仍然存在,其指向的内存可能不存在任何值或已被其它变量重新使用。在 Rust 中编译器可以确保引用永远也不会变成悬垂状态:当你获取数据的引用后,编译器可以确保数据不会在引用结束前被释放,要想释放数据,必须先停止其引用的使用。 例如:
fn dangle() -> &String { // dangle 返回一个字符串的引用
let s = String::from("hello"); // s 是一个新字符串
&s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃。其内存被释放。
// 危险!
因为 s 是在 dangle 函数内创建的,当 dangle 的代码执行完毕后,s 将被释放, 但是此时我们又尝试去返回它的引用。这意味着这个引用会指向一个无效的 String,这可不对!
其中一个很好的解决方法是直接返回 String:
fn no_dangle() -> String {
let s = String::from("hello");
s
}
这样就没有任何错误了,最终 String 的 所有权被转移给外面的调用者。
3 复合类型
3.1 字符串与切片
3.1.1 切片(slice)
对于字符串而言,切片就是对 String 类型中某一部分的引用
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
这就是创建切片的语法,使用方括号包括的一个序列:[开始索引…终止索引],其中开始索引是切片中第一个元素的索引位置,而终止索引是最后一个元素后面的索引位置,也就是这是一个 右半开区间
。在切片数据结构内部会保存开始的位置和切片的长度,其中长度是通过 终止索引
- 开始索引
的方式计算得来的。
对于 let world = &s[6..11];
来说,world
是一个切片,该切片的指针指向 s
的第 7 个字节(索引从 0 开始, 6 是第 7 个字节),且该切片的长度是 5
个字节。
截取完整的String切片:
let s = String::from("hello");
let len = s.len();
let slice = &s[0..len];
let slice = &s[..];
字符串切片的类型标识是 &str
,在对字符串使用切片语法时需要格外小心,切片的索引必须落在字符之间的边界位置,也就是 UTF-8 字符的边界,例如中文在 UTF-8 中占用三个字节,下面的代码就会崩溃:
let s = "中国人";
let a = &s[0..2];
println!("{}",a);
因为我们只取 s
字符串的前两个字节,但是本例中每个汉字占用三个字节,因此没有落在边界处,也就是连 中
字都取不完整,此时程序会直接崩溃退出,如果改成 &s[0..3]
,则可以正常通过编译。
因为切片是对集合的部分引用,因此不仅仅字符串有切片,其它集合类型也有,例如数组:
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);
该数组切片的类型是 &[i32]
,数组切片和字符串切片的工作方式是一样的,例如持有一个引用指向原始数组的某个元素和长度。
字符串是由字符组成的连续集合,Rust 中的字符是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是在字符串中不一样,字符串是 UTF-8 编码,也就是字符串中的字符所占的字节数是变化的(1 - 4),这样有助于大幅降低字符串所占用的内存空间。Rust 在语言级别,只有一种字符串类型: str
,它通常是以引用类型出现 &str
,也就是上文提到的字符串切片。但在标准库中String
则是一个可增长、可改变且具有所有权的 UTF-8 编码字符串,当 Rust 用户提到字符串时,往往指的就是 String
类型和 &str
字符串切片类型,这两个类型都是 UTF-8 编码。
String
与&str
类型之间的转换:
fn main() {
let s = String::from("hello,world!");
//等同于 let s="hello,world!".to_string()
say_hello(&s); //String转&str
say_hello(&s[..]);
say_hello(s.as_str());
}
fn say_hello(s: &str) {
println!("{}",s);
}
3.1.2 字符串操作
追加(push)
在字符串尾部可以使用 push()
方法追加字符 char
,也可以使用 push_str()
方法追加字符串字面量。这两个方法都是在原有的字符串上追加,并不会返回新的字符串。由于字符串追加操作要修改原来的字符串,则该字符串必须是可变的,即字符串变量必须由 mut
关键字修饰。示例代码如下:
fn main() {
let mut s = String::from("Hello ");
s.push('r'); //Hello r
s.push_str("ust!"); //Hello rust!
}
插入(insert)
可以使用 insert()
方法插入单个字符 char
,也可以使用 insert_str()
方法插入字符串字面量,与 push()
方法不同,这俩方法需要传入两个参数,第一个参数是字符(串)插入位置的索引,第二个参数是要插入的字符(串),索引从 0 开始计数,如果越界则会发生错误。由于字符串插入操作要修改原来的字符串,则该字符串必须是可变的,即字符串变量必须由 mut
关键字修饰。示例代码如下:
fn main() {
let mut s = String::from("Hello rust!");
s.insert(5, ','); //Hello, rust!
s.insert_str(6, " I like"); //Hello, I like rust!
}
替换(replace)
1、replace
该方法可适用于 String
和 &str
类型。replace()
方法接收两个参数,第一个参数是要被替换的字符串,第二个参数是新的字符串。该方法会替换所有匹配到的字符串。该方法是返回一个新的字符串,而不是操作原来的字符串。示例代码如下:
fn main() {
let string_replace = String::from("I like rust. Learning rust is my favorite!");
let new_string_replace = string_replace.replace("rust", "RUST");
dbg!(new_string_replace);
}
代码运行结果:
new_string_replace = "I like RUST. Learning RUST is my favorite!"
2、replacen
replacen
方法可适用于 String
和 &str
类型。replacen()
方法接收三个参数,前两个参数与 replace()
方法一样,第三个参数则表示替换的个数。该方法是返回一个新的字符串,而不是操作原来的字符串。
示例代码如下:
fn main() {
let string_replace = "I like rust. Learning rust is my favorite!";
let new_string_replacen = string_replace.replacen("rust", "RUST", 1);
dbg!(new_string_replacen);
}
代码运行结果:
new_string_replacen = "I like RUST. Learning rust is my favorite!"
3、replace_range
该方法仅适用于 String
类型。replace_range
接收两个参数,第一个参数是要替换字符串的范围(Range),第二个参数是新的字符串。该方法是直接操作原来的字符串,不会返回新的字符串。该方法需要使用 mut
关键字修饰。
示例代码如下:
fn main() {
let mut string_replace_range = String::from("I like rust!");
string_replace_range.replace_range(7..8, "R");
dbg!(string_replace_range);
}
代码运行结果:
string_replace_range = "I like Rust!"
删除(delete)
1、 pop
—— 删除并返回字符串的最后一个字符
该方法是直接操作原来的字符串。但是存在返回值,其返回值是一个 Option
类型,如果字符串为空,则返回 None
。 示例代码如下:
fn main() {
let mut string_pop = String::from("rust pop 中文!");
let p1 = string_pop.pop();
let p2 = string_pop.pop();
dbg!(p1);
dbg!(p2);
dbg!(string_pop);
}
代码运行结果:
p1 = Some(
'!',
)
p2 = Some(
'文',
)
string_pop = "rust pop 中"
2、 remove
—— 删除并返回字符串中指定位置的字符
该方法是直接操作原来的字符串。但是存在返回值,其返回值是删除位置的字符串,只接收一个参数,表示该字符起始索引位置。remove()
方法是按照字节来处理字符串的,如果参数所给的位置不是合法的字符边界,则会发生错误。
示例代码如下:
fn main() {
let mut string_remove = String::from("测试remove方法");
println!(
"string_remove 占 {} 个字节",
std::mem::size_of_val(string_remove.as_str())
);
// 删除第一个汉字
string_remove.remove(0);
// 下面代码会发生错误
// string_remove.remove(1);
// 直接删除第二个汉字
// string_remove.remove(3);
dbg!(string_remove);
}
代码运行结果:
string_remove 占 18 个字节
string_remove = "试remove方法"
3、truncate
—— 删除字符串中从指定位置开始到结尾的全部字符
该方法是直接操作原来的字符串。无返回值。该方法 truncate()
方法是按照字节来处理字符串的,如果参数所给的位置不是合法的字符边界,则会发生错误。
示例代码如下:
fn main() {
let mut string_truncate = String::from("测试truncate");
string_truncate.truncate(3);
dbg!(string_truncate);
}
代码运行结果:
string_truncate = "测"
4、clear
—— 清空字符串
该方法是直接操作原来的字符串。调用后,删除字符串中的所有字符,相当于 truncate()
方法参数为 0 的时候。
示例代码如下:
fn main() {
let mut string_clear = String::from("string clear");
string_clear.clear();
dbg!(string_clear);
}
代码运行结果:
string_clear = ""
连接(concatenate)
1、使用 +
或者 +=
连接字符串
使用 +
或者 +=
连接字符串,要求右边的参数必须为字符串的切片引用(Slice)类型。其实当调用 +
的操作符时,相当于调用了 std::string
标准库中的 add()
方法,这里 add()
方法的第二个参数是一个引用的类型。因此我们在使用 +
, 必须传递切片引用类型。不能直接传递 String
类型。+
和 +=
都是返回一个新的字符串。所以变量声明可以不需要 mut
关键字修饰。
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
// String = String + &str + &str + &str + &str
let s = s1 + "-" + &s2 + "-" + &s3;
String + &str
返回一个 String
,然后再继续跟一个 &str
进行 +
操作,返回一个 String
类型,不断循环,最终生成一个 s
,也是 String
类型。
s1
这个变量通过调用 add()
方法后,所有权被转移到 add()
方法里面, add()
方法调用后就被释放了,同时 s1
也被释放了。再使用 s1
就会发生错误。
2、使用 format!
连接字符串
format!
这种方式适用于 String
和 &str
。format!
的用法与 print!
的用法类似,详见格式化输出。
示例代码如下:
fn main() {
let s1 = "hello";
let s2 = String::from("rust");
let s = format!("{} {}!", s1, s2);
println!("{}", s);
}
代码运行结果:
hello rust!
遍历字符串
1、按字符遍历,如果你想要以 Unicode 字符的方式遍历字符串,最好的办法是使用 chars
方法,例如:
for c in "中国人".chars() {
println!("{}", c);
}
输出如下
中
国
人
2、按字节遍历,返回字符串的底层字节数组表现形式:
for b in "中国人".bytes() {
println!("{}", b);
}
输出如下:
228
184
173
229
155
189
228
186
186
3.2 元组
元组是由多种类型组合到一起形成的,因此它是复合类型,元组的长度是固定的,元组中元素的顺序也是固定的。
可以通过以下语法创建一个元组:
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
变量 tup
被绑定了一个元组值 (500, 6.4, 1)
,该元组的类型是 (i32, f64, u8)
用模式匹配解构元组
用同样的形式把一个复杂对象中的值匹配出来。
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {}", y);
}
用 .
来访问元组
模式匹配可以让我们一次性把元组中的值全部或者部分获取出来,如果只想要访问某个特定元素,那模式匹配就略显繁琐,对此,Rust 提供了 .
的访问方式,元组的索引从 0 开始:
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;
}
可以通过使用元组使函数返回多个值:
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("The length of '{}' is {}.", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() 返回字符串的长度
(s, length)
}
3.3 结构体
3.3.1 结构体语法
定义结构体
一个结构体由几部分组成:
- 通过关键字
struct
定义 - 一个清晰明确的结构体
名称
- 几个有名字的结构体
字段
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
该结构体名称是 User
,拥有 4 个字段,且每个字段都有对应的字段名及类型声明,例如 username
代表了用户名,是一个可变的 String
类型。
创建结构体实例
创建 User
结构体的实例:
let mut user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
有几点值得注意:
- 初始化实例时,每个字段都需要进行初始化
- 初始化时的字段顺序不需要和结构体定义时的顺序一致
访问结构体字段
必须要将结构体实例声明为可变的,才能修改其中的字段,Rust 不支持将某个结构体某个字段标记为可变。
user1.email = String::from("anotheremail@example.com");
简化结构体创建
当函数参数和结构体字段同名时,可以直接使用缩略的方式进行初始化
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
根据已有的结构体实例,创建新的结构体实例,例如根据已有的 user1
实例来构建 user2
,Rust 为我们提供了 结构体更新语法
:
let user2 = User {
email: String::from("another@example.com"),
..user1
};
因为 user2
仅仅在 email
上与 user1
不同,因此我们只需要对 email
进行赋值,剩下的通过结构体更新语法 ..user1
即可完成。
..
语法表明凡是我们没有显式声明的字段,全部从 user1
中自动获取。需要注意的是 ..user1
必须在结构体的尾部使用。
结构体更新语法跟赋值语句
=
非常相像,因此在上面代码中,user1
的部分字段所有权被转移到user2
中:username
字段发生了所有权转移,作为结果,user1
无法再被使用。值得注意的是:username
所有权被转移给了user2
,导致了user1
无法再被使用,但是并不代表user1
内部的其它字段不能被继续使用:
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};
println!("{}", user1.active);
// 下面这行会报错
println!("{:?}", user1);
即,把结构体中具有所有权的字段转移出去后,将无法再访问该字段,但是可以正常访问其它的字段。
3.3.2 元组结构体(Tuple Struct)
结构体必须要有名称,但是结构体的字段可以没有名称,这种结构体长得很像元组,因此被称为元组结构体,例如:
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
元组结构体在希望有一个整体名称,但是又不关心里面字段的名称时将非常有用。例如上面的 Point
元组结构体,众所周知 3D 点是 (x, y, z)
形式的坐标点,因此我们无需再为内部的字段逐一命名为:x
, y
, z
。
3.3.3 单元结构体(Unit-like Struct)
单元结构体没有任何字段和属性,但是好在,它还挺有用。如果你定义一个类型,但是不关心该类型的内容, 只关心它的行为时,就可以使用 单元结构体
:
struct AlwaysEqual;
let subject = AlwaysEqual;
// 我们不关心 AlwaysEqual 的字段数据,只关心它的行为,因此将它声明为单元结构体,然后再为它实现某个特征
impl SomeTrait for AlwaysEqual {
}
如果想在结构体中使用一个引用,就必须加上生命周期。
3.3.4 使用 #[derive(Debug)]
来打印结构体的信息
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {:?}", rect1);
}
此时运行程序,就不再有错误,输出如下:
$ cargo run
rect1 is Rectangle { width: 30, height: 50 }
当结构体较大时,我们可能希望能够有更好的输出表现,此时可以使用 {:#?}
来替代 {:?}
,输出如下:
rect1 is Rectangle {
width: 30,
height: 50,
}
还有一个简单的输出 debug 信息的方法,那就是使用 dbg!
宏,它会拿走表达式的所有权,然后打印出相应的文件名、行号等 debug 信息,当然还有我们需要的表达式的求值结果。除此之外,它最终还会把表达式值的所有权返回!
dbg!` 输出到标准错误输出 `stderr`,而 `println!` 输出到标准输出 `stdout
下面的例子中清晰的展示了 dbg!
如何在打印出信息的同时,还把表达式的值赋给了 width
:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};
dbg!(&rect1);
}
最终的 debug 输出如下:
$ cargo run
[src/main.rs:10] 30 * scale = 60
[src/main.rs:14] &rect1 = Rectangle {
width: 60,
height: 50,
}
3.4 枚举
3.4.1 枚举的语法
枚举(enum 或 enumeration)允许通过列举可能的成员来定义一个枚举类型,枚举类型是一个类型,它会包含所有可能的枚举成员, 而枚举值是该类型中的具体某个成员的实例。 如:
enum PokerSuit {
Clubs,
Spades,
Diamonds,
Hearts,
}
结构体实现带值的枚举成员:
enum PokerSuit {
Clubs,
Spades,
Diamonds,
Hearts,
}
struct PokerCard {
suit: PokerSuit,
value: u8
}
fn main() {
let c1 = PokerCard {
suit: PokerSuit::Clubs,
value: 1,
};
let c2 = PokerCard {
suit: PokerSuit::Diamonds,
value: 12,
};
}
直接将数据信息关联到枚举成员上,可省去近一半的代码,例如:
enum PokerCard {
Clubs(u8),
Spades(u8),
Diamonds(char),
Hearts(char),
}
fn main() {
let c1 = PokerCard::Spades(5);
let c2 = PokerCard::Diamonds('A');
}
任何类型的数据都可以放入枚举成员中: 例如字符串、数值、结构体甚至另一个枚举。以下代码:
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {
let m1 = Message::Quit;
let m2 = Message::Move{x:1,y:1};
let m3 = Message::ChangeColor(255,255,0);
}
该枚举类型代表一条消息,它包含四个不同的成员:
Quit
没有任何关联数据Move
包含一个匿名结构体Write
包含一个String
字符串ChangeColor
包含三个i32
3.4.2 Opention枚举处理空值
Option
枚举包含两个成员,一个成员表示含有值:Some(T)
, 另一个表示没有值:None
,定义如下:
enum Option<T> {
Some(T),
None,
}
其中 T
是泛型参数,Some(T)
表示该枚举成员的数据类型是 T
,换句话说,Some
可以包含任何类型的数据。Option<T>
和 T
(这里 T
可以是任何类型)是不同的类型。
为了使用 Option<T>
值,需要编写处理每个成员的代码。match
表达式就是这么一个处理枚举的控制流结构:它会根据枚举的成员运行不同的代码,这些代码可以使用匹配到的值中的数据。
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
plus_one
通过 match
来处理不同 Option
的情况。
3.5 数组
在 Rust 中,最常用的数组有两种,第一种是速度很快但是长度固定的 array
,第二种是可动态增长的但是有性能损耗的 Vector
。这两个数组的关系跟 &str
与 String
的关系很像,前者是长度固定的字符串切片,后者是可动态增长的字符串。其实,在 Rust 中无论是 String
还是 Vector
,它们都是 Rust 的高级类型:集合类型。数组的具体定义很简单:将多个类型相同的元素依次组合在一起,就是一个数组。数组声明格式为:[类型; 长度]。结合上面的内容,可以得出数组的三要素:
- 长度固定
- 元素必须有相同的类型
- 依次线性排列
这里说的数组是 Rust 的基本类型,是固定长度的,这点与其他编程语言不同,其它编程语言的数组往往是可变长度的,与 Rust 中的动态数组
Vector
类似
3.5.1 创建数组
fn main() {
//let a = [1, 2, 3, 4, 5]; 或者
let a: [i32; 5] = [1, 2, 3, 4, 5]; //包含5个i32类型的数
let b = [3; 5]; //数组b中包含5个3
}
3.5.2 数组越界访问
下面是一个接收用户的控制台输入,然后将其作为索引访问数组元素的例子:
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 {} is: {}",
index, element
);
}
使用 cargo run
来运行代码,因为数组只有 5 个元素,如果我们试图输入 5
去访问第 6 个元素,则会访问到不存在的数组元素,最终程序会崩溃退出:
Please enter an array index.
5
thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 5', src/main.rs:19:19
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
这就是数组访问越界,访问了数组中不存在的元素,导致 Rust 运行时错误。
当你尝试使用索引访问元素时,Rust 将检查你指定的索引是否小于数组长度。如果索引大于或等于数组长度,Rust 会出现 *panic*。这种检查只能在运行时进行,比如在上面这种情况下,编译器无法在编译期知道用户运行代码时将输入什么值,这种就是 Rust 的安全特性之一。
数组元素为非基础类型时,应调用std::array::from_fn
let array: [String; 8] = core::array::from_fn(|i| String::from("rust is good!"));
println!("{:#?}", array);
3.5.3 数组切片
切片允许你引用集合中的部分连续片段,而不是整个集合,对于数组也是,数组切片允许我们引用数组的一部分:
let a: [i32; 5] = [1, 2, 3, 4, 5];
let slice: &[i32] = &a[1..3];
assert_eq!(slice, &[2, 3]);
上面的数组切片 slice
的类型是&[i32]
,与之对比,数组的类型是[i32;5]
,简单总结下切片的特点:
- 切片的长度可以与数组不同,并不是固定的,而是取决于你使用时指定的起始和结束位置
- 创建切片的代价非常小,因为切片只是针对底层数组的一个引用
- 切片类型[T]拥有不固定的大小,而切片引用类型&[T]则具有固定的大小,因为 Rust 很多时候都需要固定大小数据类型,因此&[T]更有用,
&str
字符串切片也同理
数组综合示例:
fn main() {
// 编译器自动推导出one的类型
let one = [1, 2, 3];
// 显式类型标注
let two: [u8; 3] = [1, 2, 3];
let blank1 = [0; 3];
let blank2: [u8; 3] = [0; 3];
// arrays是一个二维数组,其中每一个元素都是一个数组,元素类型是[u8; 3]
let arrays: [[u8; 3]; 4] = [one, two, blank1, blank2];
// 借用arrays的元素用作循环中
for a in &arrays {
print!("{:?}: ", a);
// 将a变成一个迭代器,用于循环
// 你也可以直接用for n in a {}来进行循环
for n in a.iter() {
print!("\t{} + 10 = {}", n, n+10);
}
let mut sum = 0;
// 0..a.len,是一个 Rust 的语法糖,其实就等于一个数组,元素是从0,1,2一直增加到到a.len-1
for i in 0..a.len() {
sum += a[i];
}
println!("\t({:?} = {})", a, sum);
}
}
运行结果:
[1, 2, 3]: 1 + 10 = 11 2 + 10 = 12 3 + 10 = 13 ([1, 2, 3] = 6)
[1, 2, 3]: 1 + 10 = 11 2 + 10 = 12 3 + 10 = 13 ([1, 2, 3] = 6)
[0, 0, 0]: 0 + 10 = 10 0 + 10 = 10 0 + 10 = 10 ([0, 0, 0] = 0)
[0, 0, 0]: 0 + 10 = 10 0 + 10 = 10 0 + 10 = 10 ([0, 0, 0] = 0)
数组虽然很简单,但是其实还是存在几个要注意的点:
- 数组类型容易跟数组切片混淆,[T;n]描述了一个数组的类型,而[T]描述了切片的类型, 因为切片是运行期的数据结构,它的长度无法在编译期得知,因此不能用[T;n]的形式去描述
[u8; 3]
和[u8; 4]
是不同的类型,数组的长度也是类型的一部分- 在实际开发中,使用最多的是数组切片[T],我们往往通过引用的方式去使用
&[T]
,因为后者有固定的类型大小