文章目录
- 前言
- 1、输出格式规范
- 一、占位符相关
- (一)`{}`与`{:?}`
- 二、参数替换方式
- (一)位置参数
- (二)具名参数
- 三、格式化参数
- (一)宽度
- (二)对齐
- (三)精度
- (四)进制
- (五)指数
- (六)指针地址
- (七)转义
- 四. 调试
- 2、变量绑定与解构
- 手动设置变量的可变性
- (一)变量声明
- (二)变量解析
- (三)变量和常量之间的差异
- (四) 变量遮蔽
- 3、基本类型
- 一、数据类型概述
- 二、标量类型
- (一)整型
- (二)浮点型
- (三)布尔型
- (四)字符类型
- 三、复合类型
- (一)元组类型
- (二)数组类型
- 四、代码示例
- (一)无类型注解错误示例
- (二)整型溢出示例
- (三)数值运算示例
- (四)元组操作示例
- (五)数组操作示例
- 五、位运算
- 六、字符类型(char)、布尔类型(bool)、单元类型(())
- 七、序列
- 八、语句和表达式、函数
- 4、所有权,引用和借用
- 栈(Stack)与堆(Heap)
- 深拷贝和浅拷贝
- 一、所有权(Ownership)
- 二、引用和借用
- 三、生命周期
- 四、Slice 类型
- 1.创建slice
- 2.从向量创建 Slice
- 3. Slice 的操作
- 4.Slice 的方法
- 5.iter() 和 iter_mut() 方法
- 5、复合类型
- 一、字符串类型
- 二、元组
- 三、数组(Arrays)
- 四、结构体
- 五、 枚举
- 6、流程控制
- 一、分支控制(if语句)
- 二、循环控制
- (一)`for`循环
- (二)`while`循环
- (三)`loop`循环
- 7、模式匹配
- match和if let总结
- 一、match
- 二、if let
- 三、matches!宏
- 四、变量遮蔽
- 解构Option
- 一、Option枚举介绍
- 二、匹配Option<T>
- 模式适用场景
- (一)`match`分支
- (二)`if let`分支
- (三)`while let`条件循环
- (四)`for`循环
- (五)`let`语句
- (六)函数参数
- (七)`let-else`(Rust 1.65新增)
- 全模式匹配总结
- 一、匹配字面值
- 二、匹配命名变量
- 三、单分支多模式
- 四、通过序列`..=`匹配值的范围
- 五、解构并分解值
- (一)解构结构体
- (二)解构枚举
- (三)解构嵌套的结构体和枚举
- (四)解构结构体和元组
- (五)解构数组
- 六、忽略模式中的值
- (一)使用`_`忽略整个值
- (二)使用嵌套的`_`忽略部分值
- (三)使用下划线开头忽略未使用的变量
- (四)用`..`忽略剩余值
- 七、匹配守卫提供的额外条件
- 八、@绑定
- (一)、@前绑定后解构(Rust 1.56新增)
- (二)、@新特性(Rust 1.53 新增)
- 8、方法Method
- 一、方法定义
- 二、self、&self 和 &mut self
- 三、方法名与结构体字段名相同
- 四、`->`运算符
- 五、带有多个参数的方法
- 六、关联函数
- 七、多个`impl`定义
- 八、为枚举实现方法
- 9、泛型总结
- 一、泛型概念
- 二、泛型详解
- 三、结构体中使用泛型
- 四、枚举中使用泛型
- 五、方法中使用泛型
- 六、const泛型
- 七、const fn
- 八、泛型性能
- 10、Trait总结
- 一、Trait概念
- 二、定义Trait
- 三、为类型实现Trait
- 四、默认实现
- 五、Trait作为参数
- 六、返回实现了trait的类型
- 七、使用trait bound有条件地实现方法
- 11、生命周期总结
- 一、生命周期概念
- 二、悬垂引用与借用检查器
- (一)悬垂引用示例
- (二)借用检查器
- 三、函数中的生命周期
- (一)生命周期标注需求
- (二)生命周期标注语法
- (三)函数签名中的生命周期标注
- (四)深入理解生命周期标注
- 四、结构体中的生命周期
- (一)标注语法
- (二)示例
- 五、生命周期省略
- (一)原因
- (二)省略规则
- 六、方法中的生命周期
- (一)语法
- (二)示例
- 七、静态生命周期
- 12、集合类型总结
- 一、`Vec<T>`(向量)
- (一)概念
- (二)操作
- 二、`String`(字符串)
- (一)概念
- (二)操作
- 三、`HashMap<K, V>`(哈希表)
- 一、概念
- 二、操作
- (一)新建
- (二)访问
- (三)所有权
- (四)更新
- (五)哈希函数
- 13、错误处理总结
- 一、`Result<T, E>`
- (一)概念
- (二)使用
- 二、`panic!`与不可恢复错误
- (一)、`backtrace`栈展开
- (二)、`panic`时的两种终止方式
- (三)、线程`panic`后程序是否会终止
- (四)、何时该使用`panic!`
- (五)、`panic`原理剖析
- 14、包管理和模块
- 一、包(Crate)和项目(Package)
- (一) 包(Crate)
- (二) 项目(Package)
- 二、模块(Module)
- (一)创建嵌套模块
- (二)模块树
- (三)父子模块
- (四)用路径引用模块
- (五)受限的可见性
- (一)限制可见性语法
- (六)使用`super`引用模块
- (七)使用`self`引用模块
- (八)结构体和枚举的可见性
- (九)模块与文件分离
- 三、使用 use 及受限可见性
- (一)引入模块或函数简化调用
- (二)引入模块还是函数的选择
- 四、处理同名引用
- (一)使用模块区分
- (二)`as`别名引用
- 五、引入项再导出
- 六、使用第三方包
- 七、简化引入方式
- (一)使用`{}`简化
- (二)使用`*`引入模块下所有项
- 15、注释与文档总结
- 一、注释种类
- 二、常用文档标题
- 三、查看文档
- 四、文档测试
- 五、文档注释中的代码跳转
- 六、文档搜索别名
- 七、综合例子
- 总结
前言
这个笔记基于《The Rust Programming Language, 2nd Edition》 这本书为基础的记录学习笔记。
Rust 程序设计语言的本质实际在于 赋能(empowerment):无论你现在编写的是何种代码,Rust 能让你在更为广泛的编程领域走得更远,写出自信。(这一点并不显而易见)
举例来说,那些“系统层面”的工作涉及内存管理、数据表示和并发等底层细节。从传统角度来看,这是一个神秘的编程领域,只为浸润多年的极少数人所触及,也只有他们能避开那些臭名昭著的陷阱。即使谨慎的实践者,亦唯恐代码出现漏洞、崩溃或损坏。
Rust 破除了这些障碍:它消除了旧的陷阱,并提供了伴你一路同行的友好、精良的工具。想要 “深入” 底层控制的程序员可以使用 Rust,无需时刻担心出现崩溃或安全漏洞,也无需因为工具链不靠谱而被迫去了解其中的细节。更妙的是,语言设计本身会自然而然地引导你编写出可靠的代码,并且运行速度和内存使用上都十分高效。
已经在从事编写底层代码的程序员可以使用 Rust 来提升信心。例如,在 Rust 中引入并行是相对低风险的操作,因为编译器会替你找到经典的错误。同时你可以自信地采取更加激进的优化,而不会意外引入崩溃或漏洞。
但 Rust 并不局限于底层系统编程。它表达力强、写起来舒适,让人能够轻松地编写出命令行应用、网络服务器等各种类型的代码——在本书中就有这两者的简单示例。使用 Rust 能让你把在一个领域中学习的技能延伸到另一个领域:你可以通过编写网页应用来学习 Rust,接着将同样的技能应用到你的 Raspberry Pi(树莓派)上。
本书全面介绍了 Rust 为用户赋予的能力。其内容平易近人,致力于帮助你提升 Rust 的知识,并且提升你作为程序员整体的理解与自信。欢迎你加入 Rust 社区,让我们准备深入学习 Rust 吧!
—— Nicholas Matsakis 和 Aaron Turon
1、输出格式规范
打印操作由 std::fmt 里面所定义的一系列宏来处理,包括:
format!
:将格式化文本写到字符串。 宏旨在使那些使用 C 的 printf/fprintf 函数或 Python 的 str.format 函数的用户熟悉。
format!("Hello"); // => "Hello"
format!("Hello, {}!", "world"); // => "Hello, world!"
format!("The number is {}", 1); // => "The number is 1"
format!("{:?}", (3, 4)); // => "(3, 4)"
format!("{value}", value=4); // => "4"
let people = "Rustaceans";
format!("Hello {people}!"); // => "Hello Rustaceans!"
format!("{} {}", 1, 2); // => "1 2"
format!("{:04}", 42); // => 带前导零的 "0042"
format!("{:#?}", (100, 200)); // => "(// 100,// 200, )"
print!:与 format! 类似,但将文本输出到控制台(io::stdout)。
println!: 与 print! 类似,但输出结果追加一个换行符。
print!("Hello, world!");
// 输出后会自动换行
println!("Nice to meet you.");
eprint!:与 print! 类似,但将文本输出到标准错误(io::stderr)。
eprintln!:与 eprint! 类似,但输出结果追加一个换行符。
eprint!("Error: file not found.");
// 输出后会自动换行
eprintln!("Please check the file path and try again.");
write! 不会在输出末尾添加换行符。
writeln! 会在输出末尾自动添加换行符。
示例 1:基本用法
use std::io::Write;
fn main() {let name = "Alice";let age = 30;// 使用 write!let mut stdout = std::io::stdout();write!(stdout, "Name: {}", name).unwrap();write!(stdout, ", Age: {}", age).unwrap();println!(""); // 手动添加换行符// 使用 writeln!writeln!(stdout, "Name: {}", name).unwrap();writeln!(stdout, "Age: {}", age).unwrap();
}
示例 2:文件输出
use std::fs::File;
use std::io::{Write, Result};fn write_to_file() -> Result<()> {let file = File::create("output.txt")?;let mut writer = std::io::BufWriter::new(file);// 使用 write!write!(writer, "Name: Alice").unwrap();write!(writer, ", Age: 30").unwrap(); // 不会自动换行// 使用 writeln!writeln!(writer, "Name: Alice").unwrap();writeln!(writer, "Age: 30").unwrap(); // 会自动换行Ok(())
}
fn main() {if let Err(e) = write_to_file() {eprintln!("Error writing to file: {}", e);}
}
一、占位符相关
(一){}
与{:?}
- 适用类型
{}
适用于实现std::fmt::Display
特征的类型,用于友好展示;{:?}
适用于实现std::fmt::Debug
特征的类型,用于调试。
Debug
特征- 多数Rust类型实现或支持派生
Debug
特征,结构体需派生该特征后才能用{:?}
输出。
- 多数Rust类型实现或支持派生
Display
特征- 实现
Display
特征的类型较少,对于不支持的类型:- 可考虑
{:#?}
,它比{:?}
输出更优美。 - 可为自定义类型实现
Display
特征,实现fmt
方法来定义格式化方式。 - 可使用
newtype
为外部类型实现Display
特征。
- 可考虑
- 实现
二、参数替换方式
(一)位置参数
- 如
{1}
表示用第二个参数替换占位符(索引从0开始)。
fn main() {println!("{}{}", 1, 2); // =>"12"println!("{1}{0}", 1, 2); // =>"21"// => Alice, this is Bob. Bob, this is Aliceprintln!("{0}, this is {1}. {1}, this is {0}", "Alice", "Bob");println!("{1}{}{0}{}", 1, 2); // => 2112
}
(二)具名参数
- 可为参数指定名称,如
{argument}
,
fn main() {println!("{argument}", argument = "test"); // => "test"println!("{name} {}", 1, name = 2); // => "2 1"println!("{a} {c} {b}", a = "a", b = 'b', c = 3); // => "a 3 b"
}
- 带名称的参数须放在不带名称参数后面。
println!("{abc} {1}", abc = "def", 2);//报错,positional arguments must be before named arguments
三、格式化参数
(一)宽度
-
字符串填充
- 默认用空格填充,左对齐,可指定宽度,如
{:5}
。
- 默认用空格填充,左对齐,可指定宽度,如
-
数字填充
- 默认用空格填充,右对齐,可显式输出正号,用0填充等,如
{:05}
。
println!("Hello {:5}!", 5);// 宽度是5 => Hello 5! println!("Hello {:+}!", 5);// 显式的输出正号 => Hello +5!println!("Hello {:05}!", 5); // 宽度5,使用0进行填充 => Hello 00005!println!("Hello {:05}!", -5); // 负号也要占用一位宽度 => Hello -0005!
- 默认用空格填充,右对齐,可显式输出正号,用0填充等,如
(二)对齐
-
有左对齐
{:<5}
、右对齐{:>5}
、居中对齐{:^5}
,还可指定符号填充,如{:&<5}
。println!("Hello {:<5}!", "x"); // 左对齐 => Hello x !println!("Hello {:>5}!", "x"); // 右对齐 => Hello x!println!("Hello {:^5}!", "x"); // 居中对齐 => Hello x !// 对齐并使用指定符号填充 => Hello x&&&&!// 指定符号填充的前提条件是必须有对齐字符println!("Hello {:&<5}!", "x");
(三)精度
-
可控制浮点数精度或字符串长度,如
{:.2}
保留小数点后两位,{:.3}
保留字符串前三个字符。fn main() {let v = 3.1415926;// Display => 3.14println!("{:.2}", v);// Debug => 3.14println!("{:.2?}", v); }
(四)进制
-
有
#b
(二进制)、#o
(八进制)、#x
(小写十六进制)、#X
(大写十六进制)、x
(不带前缀的小写十六进制)。println!("{:#b}!", 27);// 二进制 => 0b11011!println!("{:#o}!", 27); // 八进制 => 0o33!println!("{}!", 27); // 十进制 => 27!println!("{:#x}!", 27); // 小写十六进制 => 0x1b!println!("{:#X}!", 27); // 大写十六进制 => 0x1B!println!("{:x}!", 27); // 不带前缀的十六进制 => 1b!println!("{:#010b}!", 27); // 使用0填充二进制,宽度为10 => 0b00011011!
(五)指数
- 如
{:2e}
,{:2E}
。println!("{:2e}", 1000000000); // => 1e9
(六)指针地址
- 如
{:p}
输出指针地址。println!("{:p}", v.as_ptr()) // => 0x600002324050
(七)转义
- 输出
{
和}
需转义为{{
和}}
,"
转义为\"
。
四. 调试
使用 Rust 的内置调试工具
编译检查
cargo check
运行测试
cargo test
查看测试结果
cargo test -- --show-output
在 Rust 中,fmt::Debug
和 fmt::Display
特质用于不同的目的。fmt::Debug
主要用于调试输出,而 fmt::Display
用于用户友好的输出。
- fmt::Debug特质
用于调试输出,通常用于内部状态的展示,它的输出应该尽可能代表内部状态,在大多数情况下,可以用#[derive(Debug)]
自动生成 fmt::Debug 的实现
use std::fmt;
#[derive(Debug)]
struct Person{name:String,age:u32,
}
impl fmt::Debug for Person{fn fmt(&self,f:&mut fmt::Formatter<'_>) -> fmt::Result {wrint!(f,"Person {{ name: {}, age: {} }}", self.name, self.age);}
}
fn main(){let person = Person{name: String::from("Alice"),age: 30,};println!("Debug output: {:?}", person);
}
- fmt::Display 特质
用于用户友好的输出。它的输出通常更加简洁和易于阅读。fmt::Display 特质需要手动实现。
use std::fmt;struct Person{name:String,age:u32,
}
impl fmt::Display for Person{fn fmt(&self, f:&mut fmt::Formatter<'_>)-> fmt::Rusult{write!(f, "Name: {}, Age: {}", self.name, self.age)}
}
fn main(){let person = Person {name: String::from("Alice"),age:30,};println!("Display output: {}", person);
}
2、变量绑定与解构
在其它语言中,我们用 var a = "hello world"
的方式给 a 赋值,也就是把等式右边的 “hello world” 字符串赋值给变量 a ,而在 Rust 中,我们这样写:let a = "hello world"
,同时给这个过程起了另一个名字:变量绑定。
手动设置变量的可变性
优点
- 支持声明可变的变量为编程提供了灵活性,只支持声明不可变的变量( 例如函数式语言 )为编程提供了安全性,而 Rust 比较野,选择了两者我都要,既要灵活性又要安全性。
- 运行性能上的提升,因为 将本身无需改变的变量声明为不可变在运行期间会避免一些多余的runtime检查。
(一)变量声明
在Rust中,变量使用let
关键字进行声明。与一些其他编程语言不同的是,Rust的变量默认是不可变的。例如:
let x = 5;
这里的x
被绑定到值5
,并且不能被重新赋值。如果您需要一个可变变量,可以使用mut
关键字:
let mut y = 10;
y = 15; // 合法,因为y是可变的
选择可变还是不可变,更多的还是取决于你的使用场景,例如不可变可以带来安全性,但是丧失了灵活性和性能(如果你要改变,就要重新创建一个新的变量,这里涉及到内存对象的再分配)。而可变变量最大的好处就是使用上的灵活性和性能上的提升。
(二)变量解析
- 元组解构
let tuple = (1, 2, 3);// 解构元组
let (a, b, c) = tuple;println!("a: {}", a); // 输出: a: 1
println!("b: {}", b); // 输出: b: 2
println!("c: {}", c); // 输出: c: 3
- 枚举解构
enum Message {Text(String),Number(u32),
}let message = Message::Text(String::from("Hello"));// 解构枚举
match message {Message::Text(text) => println!("Text: {}", text), // 输出: Text: HelloMessage::Number(num) => println!("Number: {}", num),
}
- 结构体解构
通过解构来访问其各个字段。
struct Point {x: i32,y: i32,
}let point = Point { x: 10, y: 20 };// 解构结构体
let Point { x, y } = point;println!("x: {}", x); // 输出: x: 10
println!("y: {}", y); // 输出: y: 20
- 复合解构
同时解构多个复合类型,例如元组和结构体的组合。
struct Circle {center: (i32, i32),radius: i32,
}let circle = Circle {center: (10, 20),radius: 5,
};// 解构结构体中的元组
let Circle { center: (cx, cy), radius } = circle;println!("cx: {}", cx); // 输出: cx: 10
println!("cy: {}", cy); // 输出: cy: 20
println!("radius: {}", radius); // 输出: radius: 5
- 解构与模式匹配
可以结合模式匹配来解构复合类型,并进行条件判断。
struct Rectangle{width:i32,height:i32,
}
let rectangle = Rectangle { width:10,height:20};
// 解构结构体并进行条件判断
match rectangle {Rectangle { width,height} if width == height => println!("Square with side length: {}", width);Rectangle { width, height } => println!("Rectangle with width: {} and height: {}", width, height),
}
(三)变量和常量之间的差异
变量和常量之间存在一些差异:
- 常量不允许使用
mut
。常量不仅仅默认不可变,而且自始至终不可变,因为常量在编译后,已经确定它的值。 - 常量使用
const
关键字而不是let
关键字来声明,并且值的类型必须标注。
const MAX_POINTS: u32 = 100_000;
常量在任意作用域内声明,包括全局作用域,在声明对的作用域内,常量的作用域内,常量在程序运行的整个过程中都有效。对于需要在多处代码共享一个不可以变的值。
(四) 变量遮蔽
在一个作用域内重新声明一个同名变量,从而遮蔽之前的变量,这种机制在某些情况下有用,特别是在需要改变变量值或类型的情况下。下面详细介绍变量遮蔽的概念及其应用场景。
变量遮蔽的基本概念
- 遮蔽前后的变量
- 在一个作用域内,新的变量声明会“遮蔽”之前的同名变量。
- 遮蔽后的变量在当前作用域内有效,直到作用域结束。
类型变化
- 遮蔽后的变量可以有不同的类型。
示例 1:简单遮蔽
fn main() {let x = 5;println!("x: {}", x); // 输出: x: 5let x = "hello";println!("x: {}", x); // 输出: x: hello
}
在这个示例中,x 最初是一个整数类型 5,然后被重新声明为字符串 “hello”,从而遮蔽了之前的 x。
示例 2:循环中的遮蔽
代码如下(示例):
fn main() {let mut x = 5;println!("x: {}", x); // 输出: x: 5while x > 0 {println!("x: {}", x);// 遮蔽 xlet x = x - 1;}println!("x: {}", x); // 输出: x: 0
}
在这个示例中,x 在循环内部被遮蔽,每次迭代都会重新声明一个新的 x。
示例 3:函数参数遮蔽
fn main() {let x = 10;increment(x);println!("x: {}", x); // 输出: x: 10let x = increment(10);println!("x: {}", x); // 输出: x: 11
}fn increment(y: i32) -> i32 {let y = y + 1;y
}
Rust 允许声明相同的变量名,在后面声明的变量会遮蔽掉前面声明的,如下所示:
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);
}
The value of x in the inner scope is: 12
The value of x is: 6
第二个let 生成了完全不同的新变量,两个变量只是恰好拥有同样的名称,涉及一次内存对象的再分配,而mut
声明的变量,可以修改同一个内存地址上的值,并不会发生内存对象的再分配,性能要更好。
在被遮蔽后,无法再访问到之前的同名变量),就可以重复的使用变量名字,而不用绞尽脑汁去想更多的名字。
3、基本类型
一、数据类型概述
Rust是静态类型语言,编译时必须知道所有变量类型,编译器可根据值及其使用方式推断类型,必要时需添加类型注解。数据类型分为标量类型和复合类型。
二、标量类型
(一)整型
- 类型介绍
Rust 中的整数类型分为有符号整数和无符号整数。
有符号整数类型
类型 | 占用字节数 | 范围 |
---|---|---|
isize | 机器字长 | -isize_MAX 到 isize_MAX |
i8 | 1 | -128 到 127 |
i16 | 2 | -32768 到 32767 |
i32 | 4 | -2^31 到 2^31 - 1 |
i64 | 8 | -2^63 到 2^63 - 1 |
i128 | 16 | -2^127 到 2^127 - 1 |
无符号整数类型
类型 | 占用字节数 | 范围 |
---|---|---|
usize | 机器字长 | 0 到 usize_MAX |
u8 | 1 | 0 到 255 |
u16 | 2 | 0 到 65535 |
u32 | 4 | 0 到 4294967295 |
u64 | 8 | 0 到 18446744073709551615 |
u128 | 16 | 0 到 340282366920938463463374607431768211455 |
- 整型溢出
- 在
debug
模式编译时,整型溢出会使程序panic
;在release
模式构建时,会进行二进制补码wrapping
操作,如u8
类型的256
会变成0
。 - 可使用
wrapping_*
、checked_*
、overflowing_*
、saturating_*
等方法处理溢出。
- 在
- wrapping_* 方法
wrapping_*
方法在所有模式下都按照补码循环溢出规则处理整数运算。这意味着当发生溢出时,结果会“循环”回到有效范围内。
- checked_* 方法
checked_* 方法在发生溢出时返回 None 值,否则返回正常的运算结果。
- overflowing_* 方法
overflowing_* 方法返回一个元组,其中第一个元素是运算结果,第二个元素是一个布尔值,指示是否发生了溢出。
- saturating_* 方法
saturating_* 方法确保计算后的结果不超过目标类型的最大值或低于最小值。如果发生溢出,则结果被截断为最大值或最小值。
fn main() {let a: u32 = u32::MAX;let b: u32 = 1;// Wrapping methodslet sum_wrapping = a.wrapping_add(b);let diff_wrapping = a.wrapping_sub(b);let product_wrapping = a.wrapping_mul(2);// Checked methodslet sum_checked = a.checked_add(b);let diff_checked = a.checked_sub(b);let product_checked = a.checked_mul(2);// Overflowing methodslet (sum_overflowing, did_overflow_add) = a.overflowing_add(b);let (diff_overflowing, did_overflow_sub) = a.overflowing_sub(b);let (product_overflowing, did_overflow_mul) = a.overflowing_mul(2);// Saturating methodslet sum_saturating = a.saturating_add(b);let diff_saturating = a.saturating_sub(b);let product_saturating = a.saturating_mul(2);println!("Wrapping add: {}", sum_wrapping); // 0 (溢出后循环回 0)println!("Wrapping sub: {}", diff_wrapping); // 4294967294 (溢出后循环回 4294967294)println!("Wrapping mul: {}", product_wrapping); // 0 (溢出后循环回 0)println!("Checked add: {:?}", sum_checked); // None (溢出)println!("Checked sub: {:?}", diff_checked); // Some(4294967294) (正常结果)println!("Checked mul: {:?}", product_checked); // None (溢出)println!("Overflowing add: ({}, {})", sum_overflowing, did_overflow_add); // (0, true) (溢出)println!("Overflowing sub: ({}, {})", diff_overflowing, did_overflow_sub); // (4294967294, false) (未溢出)println!("Overflowing mul: ({}, {})", product_overflowing, did_overflow_mul); // (0, true) (溢出)println!("Saturating add: {}", sum_saturating); // 4294967295 (最大值)println!("Saturating sub: {}", diff_saturating); // 4294967294 (正常结果)println!("Saturating mul: {}", product_saturating); // 4294967295 (最大值)
}
(二)浮点型
Rust 中的浮点数类型有两种:
类型 | 占用字节数 | 精度 |
---|---|---|
f32 | 4 | 单精度 (约 6-9 位有效数字) |
f64 | 8 | 双精度 (约 15 位有效数字) |
- 类型介绍
- Rust有
f32
和f64
两种浮点数类型,分别占32位和64位,默认是f64
,采用IEEE - 754
标准表示,f32
是单精度,f64
是双精度,所有浮点型都是有符号的。
- Rust有
- 数值运算
- 支持加法、减法、乘法、除法和取余运算,整数除法会向零舍入。
(三)布尔型
- 布尔类型用
bool
表示,有true
和false
两个值,主要用于条件表达式。
(四)字符类型
- 用
char
表示,大小为四个字节,代表Unicode标量值,可表示带变音符号的字母、中文、日文、韩文等字符以及emoji等,用单引号声明字面量,范围是从U + 0000
到U + D7FF
和U + E000
到U + 10FFFF
。
三、复合类型
(一)元组类型
- 创建与访问
- 用圆括号内逗号分隔的值列表创建,如
let tup: (i32, f64, u8) = (500, 6.4, 1)
。 - 可通过模式匹配解构获取单个值,如
let (x, y, z) = tup
;也可使用点号后跟索引访问,如let five_hundred = x.0
。
- 用圆括号内逗号分隔的值列表创建,如
- 单元元组
- 不带任何值的元组叫单元元组,写作
()
,表示空值或空的返回类型。
- 不带任何值的元组叫单元元组,写作
(二)数组类型
- 创建与访问
- 在方括号内用逗号分隔值创建,如
let a = [1, 2, 3, 4, 5]
。 - 类型可写成在方括号中包含每个元素的类型,后跟分号,再后跟数组元素数量,如
let a: [i32; 5] = [1, 2, 3, 4, 5]
,也可创建每个元素都为相同值的数组,如let a = [3; 5]
。 - 通过索引访问元素,如
let first = a[0]
。
- 在方括号内用逗号分隔值创建,如
- 无效访问处理
- 访问数组结尾之后的元素会导致运行时错误,程序会
panic
,因为Rust会检查索引是否小于数组长度。
- 访问数组结尾之后的元素会导致运行时错误,程序会
四、代码示例
(一)无类型注解错误示例
$ cargo buildCompiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0284]: type annotations needed--> src/main.rs:2:9|
2 | 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|
2 | let guess: /* Type */ = "42".parse().expect("Not a number!");| ++++++++++++
(二)整型溢出示例
// debug模式下溢出
fn main() {let a: u8 = 256;
}
// release模式下溢出处理
fn main() {let a: u8 = 256;let b = a.wrapping_add(20);println!("{}", b); // 19
}
(三)数值运算示例
fn main() {// additionlet sum = 5 + 10;// subtractionlet difference = 95.5 - 4.3;// multiplicationlet product = 4 * 30;// divisionlet quotient = 56.7 / 32.2;let truncated = -5 / 3; // 结果为 -1// remainderlet remainder = 43 % 5;
}
(四)元组操作示例
fn main() {let tup: (i32, f64, u8) = (500, 6.4, 1);let (x, y, z) = tup;println!("The value of y is: {y}");
}
fn main() {let x: (i32, f64, u8) = (500, 6.4, 1);let five_hundred = x.0;let six_point_f4 = x.1;let one = x.2;
}
(五)数组操作示例
fn main() {let a = [1, 2, 3, 4, 5];let first = a[0];let second = a[1];
}
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}");
}
五、位运算
常见的位运算符
- 按位与(&)
- 按位或(|)
- 按位异或(^)
- 按位取反(~)
- 左移(<<)
- 右移(>>)
fn main() {// 定义两个整数let a = 5; // 二进制表示为 0101let b = 3; // 二进制表示为 0011// 按位与let and_result = a & b;println!("a & b = {}", and_result); // 输出 1 (二进制 0001)// 按位或let or_result = a | b;println!("a | b = {}", or_result); // 输出 7 (二进制 0111)// 按位异或let xor_result = a ^ b;println!("a ^ b = {}", xor_result); // 输出 6 (二进制 0110)// 按位取反let not_a = !a;println!("!a = {}", not_a); // 输出 -6 (二进制 11111010 补码表示)// 左移let left_shift = a << 2;println!("a << 2 = {}", left_shift); // 输出 20 (二进制 10100)// 右移let right_shift = a >> 2;println!("a >> 2 = {}", right_shift); // 输出 1 (二进制 0001)
}
六、字符类型(char)、布尔类型(bool)、单元类型(())
- 定义与示例
Rust 中的字符类型涵盖所有 Unicode 值,占用 4 个字节(32 位),包括单个中文、日文、韩文、emoji 表情符号等。
fn main() {// 定义字符let c1 = 'A'; // 大写字母 Alet c2 = 'a'; // 小写字母 alet c3 = '1'; // 数字 1let c4 = ' '; // 空格let c5 = '😊'; // 表情符号// 输出字符println!("c1 = {}", c1); // 输出 Aprintln!("c2 = {}", c2); // 输出 aprintln!("c3 = {}", c3); // 输出 1println!("c4 = {}", c4); // 输出 空格println!("c5 = {}", c5); // 输出 😊
}
- 布尔类型(bool)
布尔类型(bool)用于表示真(true)或假(false)。在 Rust 中,布尔类型非常常用,尤其是在条件判断和逻辑运算中。占用 1 个字节内存
fn main() {// 定义布尔变量let is_raining = true;let is_sunny = false;// 使用布尔变量if is_raining {println!("It's raining!");} else {println!("It's not raining.");}if is_sunny {println!("It's sunny!");} else {println!("It's not sunny.");}// 布尔运算let result_and = is_raining && is_sunny;let result_or = is_raining || is_sunny;let result_not = !is_raining;println!("is_raining && is_sunny = {}", result_and); // 输出 falseprintln!("is_raining || is_sunny = {}", result_or); // 输出 trueprintln!("!is_raining = {}", result_not); // 输出 false
}
- 单元类型(())
单元类型(())是一种特殊的类型,表示没有值。它通常用于没有返回值的函数,或者表示一个空元组。
fn main(){let emopty_value:() = ();greet();let result = add(1,2);println!("result = {:?}", result); // 输出 ()// 单元类型作为元组的一部分let tuple_with_unit = (1, "hello", ());println!("tuple_with_unit = {:?}", tuple_with_unit); // 输出 (1, "hello", ())
}
// 无返回值的函数
fn greet() {println!("Hello, world!");
}
// 返回单元类型的函数
fn add(a: i32, b: i32) -> () {let sum = a + b;println!("The sum is: {}", sum);
}
七、序列
序列(Range)通常用于表示一个连续的区间。在 Rust 中,可以使用 ..
、..=
、...
等语法来表示序列。
常见的序列表示
- a…b:表示从a到b(不包括b)的范围
- a…=b:表示从 a 到 b(包括 b)的范围。
- …b:表示从 0 到 b(不包括 b)的范围。
- a…:表示从 a 到无穷大(通常用于迭代)。
fn main() {// 从 1 到 5(不包括 5)for i in 1..5 {println!("{}", i); // 输出 1, 2, 3, 4}// 从 1 到 5(包括 5)for i in 1..=5 {println!("{}", i); // 输出 1, 2, 3, 4, 5}// 从 0 到 5(不包括 5)for i in ..5 {println!("{}", i); // 输出 0, 1, 2, 3, 4}// 从 5 到无穷大(通常用于迭代)for i in 5.. {println!("{}", i); // 输出 5, 6, 7, ...break; // 通常需要 break 来终止循环}
}
八、语句和表达式、函数
- 语句(Statements)
- 赋值语句:
let x = 5;
- 控制流语句:
- if 语句
- loop 语句
- while 语句
- for 语句
fn main() {// 赋值语句let x = 5;// 控制流语句if x > 0 {println!("x is positive");} else {println!("x is non-positive");}loop {println!("Inside the loop");break; // 终止循环}while x > 0 {println!("x = {}", x);x -= 1;}for i in 0..5 {println!("i = {}", i);}
}
- 表达式(Expressions)
算术表达式:5 + 3
逻辑表达式:5 > 0
函数调用表达式:add(5, 3)
匹配表达式:match number { ... }
fn main() {// 算术表达式let sum = 5 + 3;println!("sum = {}", sum);// 逻辑表达式let is_positive = 5 > 0;println!("is_positive = {}", is_positive);// 函数调用表达式let result = add(5, 3);println!("result = {}", result);// 匹配表达式let number = 5;let message = match number{1 => "one",2 => "two",3 | 4 => "three or four",_ => "other",};println!("message = {}", message);
}
// 定义一个简单的加法函数
fn add(a: i32, b: i32) -> i32 {a + b
}
- 函数(Functions)
定义函数:fn add(a: i32, b: i32) -> i32 { ... }
调用函数:let result = add(5, 3);
无返回值的函数:fn greet() { ... }
fn main() {// 调用函数let result = add(5, 3);println!("result = {}", result);// 无返回值的函数greet();
}
// 定义一个加法函数
fn add(a: i32, b: i32) -> i32 {a + b
}
// 定义一个无返回值的函数
fn greet() {println!("Hello, world!");
}
4、所有权,引用和借用
它们确保了内存安全性和资源管理的高效性。理解这些概念对于编写安全且高效的 Rust 代码至关重要。在以往,内存安全几乎都是通过 GC 的方式实现,但是 GC 会引来性能、内存占用以及 Stop the world 等问题,在高性能场景和系统编程上是不可接受的。
如何从内存中申请空间来存放程序的运行内容,出现了三种流派:
- 垃圾回收机制(GC),在程序运行时不断寻找不再使用的内存,典型代表:Java,Go,
- 手动管理内存的分配和释放,在程序中,通过函数调用的方式来申请和释放内存,代表:C++
- 通过所有权来管理内存,编译器在编译时会根据一系列规则进行检查
第三种只发生在编译期,因此对于程序运行期,不会任何性能上的损失。
栈(Stack)与堆(Heap)
- 栈 (Stack)
后进先出 (LIFO)。
数据大小已知且固定。
分配和释放速度快。
适合局部变量、函数参数等。
fn main() {let x = 5; // 局部变量,存储在栈上println!("x = {}", x);let s = String::from("hello"); // 字符串,存储在堆上,栈上存储指针println!("s = {}", s);
}
- 堆 (Heap)
存储大小未知或可能变化的数据。
动态分配内存。
分配和释放速度较慢。
适合动态数组、字符串等。
fn main() {let s = String::from("hello"); // 字符串,存储在堆上println!("s = {}", s);let v = vec![1, 2, 3]; // 动态数组,存储在堆上println!("v = {:?}", v);
}
深拷贝和浅拷贝
- 浅拷贝 (Shallow Copy)
只复制引用或指针。
数据共享同一个内存区域。
适用于简单类型或不可变数据。
fn main() {let x = 5;let y = x; // 浅拷贝:x 和 y 共享相同的值println!("x = {}, y = {}", x, y); // 输出 "x = 5, y = 5"// 对于引用类型let s1 = String::from("hello");let s2 = &s1; // 浅拷贝:s1 和 s2 共享相同的内存区域println!("s1 = {}, s2 = {}", s1, s2); // 输出 "s1 = hello, s2 = hello"
}
- 深拷贝 (Deep Copy)
创建新的内存区域。
复制原始数据的内容。
适用于复杂数据结构或可变数据。
use std::clone::Clone;
fn main() {let s1 = String::from("hello");let s2 = s1.clone(); // 深拷贝:创建新的内存区域并复制内容println!("s1 = {}, s2 = {}", s1, s2); // 输出 "s1 = hello, s2 = hello"// 对于 Vec 类型let v1 = vec![1, 2, 3];let v2 = v1.clone(); // 深拷贝:创建新的内存区域并复制内容println!("v1 = {:?}, v2 = {:?}", v1, v2); // 输出 "v1 = [1, 2, 3], v2 = [1, 2, 3]"
}
3. Rust 中的深拷贝
在 Rust 中,深拷贝通常通过实现Clone trait
来完成。Clone trait 提供了一个 clone 方法,用于创建一个新的实例。
use std::clone::Clone;#[derive(Clone)]
struct Person{name:String,age:u32,
}
impl Person{fn new(name:&str,age:u32) -> Self{Person{name:String::from(name),age,}}
}
fn main(){let p1 = Person::new("Alice",32);let p2 = p1.clone();println!("p1 = {{ name: {}, age: {} }}, p2 = {{ name: {}, age: {} }}",p1.name, p1.age, p2.name, p2.age);// 输出 "p1 = { name: Alice, age: 30 }, p2 = { name: Alice, age: 30 }"
}
一、所有权(Ownership)
所有权是指 Rust 中数据的所有权机制。每个值都有一个所有者,并且每个值只能有一个所有者。当所有者离开作用域时,该值会被自动释放。
特点:
- 每个值都有一个所有者
- 只有当所有者离开作用域时,值才会被释放
- 所有权可以通过移动(move)传递给新的所有者
fn main() {// 创建一个字符串let s = String::from("hello");// 移动所有权let t = s; // s 的所有权移动到 t,s 不再有效println!("t = {}", t); // 输出 "t = hello"// println!("s = {}", s); // 编译错误:s 已经移动给 t
}
// 另一个示例
fn main() {let mut s1 = String::from("hello");let s2 = s1.clone(); // 浅拷贝println!("s1 = {}", s1); // 输出 "s1 = hello"println!("s2 = {}", s2); // 输出 "s2 = hello"// s1 和 s2 都有效
}
二、引用和借用
内存安全和所有权模型的核心组成部分,引用允许你在不拥有数据的情况下访问数据,从而避免了不必要的数据复制。
借用是指在Rust种使用引用的过程,Rust的借用规则确保了内存安全性和线程安全性
规则
- 借用不可变数据:
- 可以同时存在多个不可变引用。
- 不允许存在可变引用。
- 借用可变数据:
- 每次只能存在一个可变引用。
- 不允许存在不可变引用。
fn main() {// 不可变借用let s1 = String::from("hello");let len = calculate_length(&s1);println!("data = {:?}", len); // data = 5// 可变借用let mut s = String::from("hello");change(&mut s);println!("data = {:?}", len); // data = hello world
}
fn calculate_length(s: &String) -> usize { // s 是 String 的引用s.len()
} // 这里,s 离开了作用域。但因为它并不拥有引用值的所有权,// 所以什么也不会发生
fn change(some_string: &mut String) {some_string.push_str(", world");
}
- 不可变引用 (&T)
不可变引用允许你读取数据,但不能修改数据。多个不可变引用可以同时存在。
fn main(){let x = 5;let y = &x;println!("x = {},y = {}",x,*y);// 输出 "x = 5, y = 5"// 多个不可变引用let z = &x;println!("x = {}, y = {}, z = {}", x, *y, *z); // 输出 "x = 5, y = 5, z = 5"
}
- 可变引用 (&mut T)
可变引用允许你读取和修改数据,但每次只能存在一个可变引用。
fn main() {let mut x = 5;let y = &mut x; // 可变引用*y = 10; // 修改 x 的值println!("x = {}, y = {}", x, *y); // 输出 "x = 10, y = 10"// 可变引用不能与其他引用共存// let z = &x; // 编译错误:不可变引用与可变引用冲突
}
- 避免悬挂引用
一个引用指向了一个已经释放或者不再有效的内存位置。这会导致程序崩溃或者未定义行为。
通过生命周期和所有权机制来确保引用的有效性。
fn main() {let reference_to_nothing = dangle();
}
fn dangle() -> &String { // dangle 返回一个字符串的引用let s = String::from("hello"); // s 是一个新字符串&s // 返回字符串 s 的引用//直接返回s 即可
} // 这里 s 离开作用域并被丢弃。其内存被释放。// 危险!
❌注意:借用会造成的问题—数据竞争
类似于竞态条件,它可由这三个行为造成:
- 两个或更多指针同时访问同一数据。
- 至少有一个指针被用来写入数据。
- 没有同步数据访问的机制。
let mut s = String::from("hello");let r1 = &mut s;let r2 = &mut s;println!("{}, {}", r1, r2);// 报错
/*
error[E0499]: cannot borrow `s` as mutable more than once at a time--> src/main.rs:2:18|
2 | let r1 = &mut s;| -- first mutable borrow occurs here
3 | let r2 = &mut s;| ^ second mutable borrow occurs here
4 | println!("{}, {}", r1, r2);| - first borrow later used here
For more information about this error, try `rustc --explain E0499`.
*/
三、生命周期
用来描述引用的有效范围的概念,Rust使用生命周期来确保引用不会超过其作用域。
语法:
- 使用
'a
表示生命周期 - 在引用类型中指定生命周期
fn longest<'a>(x:&'a str, y:&'a str) -> &'a str{if x.len() > y.len(){x }else{y}
}fn main(){let string1 = String::from("long string is long");let result = longest(string1.as_str(), "xyz"); // 'xyz' 的生命周期不足以覆盖整个函数println!("The longest string is {}", result);
}
四、Slice 类型
slice类型通常表示&[T]
或&mut[T]
,其中T是元素类型,slice
类型有两部分组成:
- 指向数组的指针
- 数组的长度
1.创建slice
fn main() {let arr = [1,2,3,4,5];let slice = &arr[1..3];// 创建一个 slice,包含 arr[1] 和 arr[2]println!("Slice: {:?}", slice); // 输出 "Slice: [2, 3]"
}
2.从向量创建 Slice
fn main() {let vec = vec![1,2,3,4,5];let slice = &vec[1..3]; // 创建一个 slice,包含 vec[1] 和 vec[2]println!("Slice: {:?}", slice); // 输出 "Slice: [2, 3]"
}
3. Slice 的操作
fn main() {let arr = [1,2,3,4,5];let slice = &arr[1..3];println!("First element: {}", slice[0]); // 输出 "First element: 2"println!("Second element: {}", slice[1]); // 输出 "Second element: 3"for item in slice.iter(){println!("Item: {}",item);}
}
修改 Slice
fn main() {let mut arr = [1, 2, 3, 4, 5];let mut slice = &mut arr[1..3];slice[0] = 10;slice[1] = 20;println!("Array: {:?}", arr); // 输出 "Array: [1, 10, 20, 4, 5]"
}
4.Slice 的方法
len()
fn main() {let arr = [1, 2, 3, 4, 5];let slice = &arr[1..3];println!("Length: {}", slice.len()); // 输出 "Length: 2"
}
is_empty()
fn main() {let arr = [1, 2, 3, 4, 5];let empty_slice:&[] = [];let non_empty_slice = &arr[1..3];println!("Empty slice is empty: {}", empty_slice.is_empty()); // 输出 "Empty slice is empty: true"println!("Non-empty slice is empty: {}", non_empty_slice.is_empty()); // 输出 "Non-empty slice is empty: false"
}
5.iter() 和 iter_mut() 方法
fn main() {let arr = [1, 2, 3, 4, 5];let slice = &arr[1..3];for item in arr.iter(){println!("Item: {}", item);}let mut arr = [1,2,3,4,5];let mut slice =&mut[1..3];for item int slice_mut(){*item *=2;}println!("Array: {:?}", arr); // 输出 "Array: [1, 4, 6, 4, 5]"
}
5、复合类型
一、字符串类型
&str
(字符串切片)
表示一个不可变的字符串片段
- 特点:
- 不可变
- 不拥有数据,只引用数据
- 用于函数参数和返回值
let s = "hello"; // 字面量形式
String
表示一个可变的字符串,可以动态增长和缩小。
- 特点:
- 可变
- 拥有数据
- 支持拼接、插入、删除操作。
let s = String::new(); // 创建空字符串
let s = String::from("hello"); // 从字面量创建字符串
Vec<u8>
(字节数组)
表示一个可变的字节数组,可以用来存储二进制数据或编码不明确的文本。
- 特点:
- 可变
- 不支持直接的字符串操作
Vec<char>
(字符数组)
表示一个可变的字符串数组,每个元素都是一个Unicode字符
- 特点:
- 可变。
- 适用于需要逐个字符操作的情况。
let v:Vec<char> = "hello".chars().collect();
常见操作
- 创建和初始化
let empty_string = String::new();// 创建空字符串:
let s = String::from("hello");// 从字面量创建字符串
- 拼接字符串
使用 + 操作符:
let s1 = String::from("hello");
let s2 = String::from("world");
let s3 = s1 + " " + &s2;
println!("{}", s3); // 输出 "hello world"
使用 format! 宏:
let s1 = String::from("hello");
let s2 = String::from("world");
let s3 = format!("{} {}", s1, s2);
println!("{}", s3); // 输出 "hello world"
- 修改字符串
追加字符串:
let mut s = String::from("hello");
s.push_str(", world!");
println!("{}", s); // 输出 "hello, world!"
插入字符:
let mut s = String::from("hello");
s.insert(0, 'H');
println!("{}", s); // 输出 "Hello"
删除字符:
let mut s = String::from("hello");
s.pop(); // 删除最后一个字符
println!("{}", s); // 输出 "hell"
- 字符迭代
遍历字符:
let s = String::from("hello");
for c in s.chars() {println!("{}", c);
}
- 字节迭代
遍历字节:
let s = String::from("hello");
for b in s.bytes() {println!("{}", b);
}
创建和使用 String
fn main() {let mut s = String::from("hello");s.push_str(", world!");println!("{}", s); // 输出 "hello, world!"
}
二、元组
元组是一种固定大小的数据结构,可以包含不同类型的元素。
fn main() {let t: (i32, f64, String) = (1, 3.14, "hello".to_string());// 访问元组中的元素println!("First element: {}", t.0); // 输出: First element: 1println!("Second element: {}", t.1); // 输出: Second element: 3.14println!("Third element: {}", t.2); // 输出: Third element: hello
}
三、数组(Arrays)
数组是一种固定大小的数据结构,所有元素必须具有相同的类型。
fn main() {let arr: [i32; 5] = [1, 2, 3, 4, 5];// 访问数组中的元素println!("First element: {}", arr[0]); // 输出: First element: 1println!("Second element: {}", arr[1]); // 输出: Second element: 2println!("Last element: {}", arr[4]); // 输出: Last element: 5
}
四、结构体
结构体是一种用户定义的数据结构,可以包含不同类型的字段。
struct Person{name:String,age:u32,
}
fn main() {let person = Person {name: "Alice".to_string(),age: 30,};// 访问结构体中的字段println!("Name: {}", person.name); // 输出: Name: Aliceprintln!("Age: {}", person.age); // 输出: Age: 30
}
使用字段初始化简写语法
因为 email 字段与 email 参数有着相同的名称,则只需编写 email
而不是 email: email
。
fn build_user(email: String, username: String) -> User {User {active: true,username,email,sign_in_count: 1,}
}
访问结构体字段
let mut user1 = User {email: String::from("someone@example.com"),username: String::from("someusername123"),active: true,sign_in_count: 1,};user1.email = String::from("anotheremail@example.com");
结构体更新语法
根据已有的 user1 实例来构建 user2:
let user2 = User {email:String::from("another@example.com"),..user1
}
因为 user2 仅仅在 email 上与 user1 不同,因此我们只需要对 email 进行赋值,剩下的通过结构体更新语法 …user1 即可完成。
因为你在创建 user2 时重复使用了 user1 中的 username 字段,而 String 类型是拥有所有权的,这意味着当 user1 的 username 被移动到 user2 后,user1 就不再拥有这个 String 了。
解决方案:
- 使用 .clone() 方法: 如果你想保留 user1 和 user2 都有各自的 username,可以使用 .clone() 方法来复制 String。
let user2 = User {active: user1.active,username: user1.username.clone(),email: String::from("another@example.com"),sign_in_count: user1.sign_in_count,
};
- 使用 Rc 或 Arc 来共享数据: 如果你不希望每次复制字符串,可以使用引用计数类型如 Rc 或线程安全版本 Arc 来共享数据。
use std::rc::Rc;
#[derive(Debug)]
struct User {active: bool,username:Rc<String>,email:String,
}
fn main() {let username = Rc::new(String::from("someusername123"));let user1 = User {email: String::from("someone@example.com"),username:Rc::clone(&username);active: true,sign_in_count: 1,};let user2 = User {active: user1.active,username: Rc::clone(&username),email: String::from("another@example.com"),sign_in_count: user1.sign_in_count,};println!("{}", user1.active);println!("{:?}", user1);
}
❌结构体数据的所有权
如果你想在结构体中使用一个引用,就必须加上生命周期,否则就会报错。
struct User {//struct User<'a> {username: &str,email: &str,//&'asign_in_count: u64,active: bool,
}
fn main() {let user1 = User {email: "someone@example.com",username: "someusername123",active: true,sign_in_count: 1,};
}
五、 枚举
枚举是一种定义一组命名的常量的数据类型,可以包含不同的变体
enum Color {Red,Green,Blue,
}
fn main() {let color = Color::Green;match color {Color::Red => println!("Red"),Color::Green => println!("Green"), // 输出: GreenColor::Blue => println!("Blue"),}
}
包含数据的枚举
枚举的变体可以包含不同类型的数据
enum IpAddr {V4(String),V6(String),
}
fn main() {let home = IpAddr::V4(String::from("127.0.0.1"));let loopback = IpAddr::V6(String::from("::1"));match home {IpAddr::V4(ip) => println!("IPv4 address: {}", ip), // 输出: IPv4 address: 127.0.0.1IpAddr::V6(ip) => println!("IPv6 address: {}", ip),}match loopback {IpAddr::V4(ip) => println!("IPv4 address: {}", ip),IpAddr::V6(ip) => println!("IPv6 address: {}", ip), // 输出: IPv6 address: ::1}
}
枚举的方法
可以在枚举上定义方法,使其具有行为
enum Message {Quit,Move { x: i32, y: i32 },Write(String),ChangeColor(i32, i32, i32),
}impl Message {fn call(&self) {match self {Message::Quit => println!("The Quit variant has no data to print."),Message::Move { x, y } => println!("Move in the x direction {} and in the y direction {}", x, y),Message::Write(text) => println!("Text message: {}", text),Message::ChangeColor(r, g, b) => println!("Change the color to red {}, green {}, and blue {}", r, g, b),}}
}
fn main() {let m = Message::Write(String::from("hello"));m.call(); // 输出: Text message: hello
}
枚举的用途
错误处理:通过定义一个包含多种错误类型的枚举,可以统一处理不同的错误情况。
enum Result<T, E> {Ok(T),Err(E),
}
fn divide(a: i32, b: i32) -> Result<i32, &'static str> {if b == 0 {Err("Cannot divide by zero")} else {Ok(a / b)}
}
fn main() {let result = divide(10, 2);match result {Ok(value) => println!("Result: {}", value), // 输出: Result: 5Err(e) => println!("Error: {}", e),}let result = divide(10, 0);match result {Ok(value) => println!("Result: {}", value),Err(e) => println!("Error: {}", e), // 输出: Error: Cannot divide by zero}
}
6、流程控制
一、分支控制(if语句)
- 基本使用
if else
根据条件执行不同代码分支。例如:
fn main() {let condition = true;let number = if condition {5} else {6};println!("The value of number is: {}", number);
}
if
语句块是表达式,可用于赋值,但每个分支返回类型需一致。
else if
处理多重条件- 可与
if
、else
组合实现复杂条件分支判断。例如:
- 可与
fn main() {let n = 6;if n % 4 == 0 {println!("number is divisible by 4");} else if n % 3 == 0 {println!("number is divisible by 3");} else if n % 2 == 0 {println!("number is divisible by 2");} else {println!("number is not divisible by 4, 3, or 2");}
- 程序按自上至下顺序执行分支判断,第一个匹配的分支会被执行。
二、循环控制
(一)for
循环
- 基本使用
- 例如循环输出1到5:
fn main() {for i in 1..=5 {println!("{}", i);}
}
- 语法是
for 元素 in 集合
,常用集合的引用形式,否则所有权可能转移。对于实现copy
特征的数组,直接循环不会转移所有权。 - 可使用
mut
关键字在循环中修改元素,也可使用enumerate
方法获取元素索引。例如:
fn main() {let a = [4, 3, 2, 1];for (i, v) in a.iter().enumerate() {println!("第{}个元素是{}", i + 1, v);}
}
- 与其他循环方式对比
- 性能:直接迭代集合元素的方式比循环索引然后通过索引下标访问集合的方式性能更好,因为索引访问会因边界检查导致性能损耗。
- 安全:直接迭代集合元素更安全,索引访问是非连续的,可能产生脏数据,而直接迭代是连续访问且数据不会因所有权限制而变化。
continue
和break
continue
可跳过当次循环,break
可跳出整个循环。例如:
fn main() {for i in 1..4 {if i == 2 {continue;}println!("{}", i);}
}
fn main() {for i in 1..4 {if i == 2 {break;}println!("{}", i);}
}
(二)while
循环
- 基本使用
- 当条件为
true
时循环,条件为false
时跳出。例如:
- 当条件为
fn main() {let mut n = 0;while n <= 5 {println!("{}!", n);n = n + 1;}println!("我出来了!");
}
- 与
for
循环对比- 可以实现
for
循环的功能,但更容易出错且性能更差,因为需要通过索引访问数组且编译器会增加运行时代码进行条件检查。例如:
- 可以实现
fn main() {let a = [10, 20, 30, 4、40, 50];let mut index = 0;while index < 5 {println!("the value is: {}", a[index]);index = index + 1;}
}
(三)loop
循环
- 基本使用
- 是无条件循环,适用于所有循环场景,但在很多场景下
for
和while
是更好选择。例如:
- 是无条件循环,适用于所有循环场景,但在很多场景下
fn main() {loop {println!("again!");}
}
- 与
break
配合使用- 必不可少的是
break
关键字,可让循环在满足条件时跳出,break
可单独使用也可带返回值,loop
是表达式可返回值。例如:
- 必不可少的是
fn main() {let mut counter = 0;let result = loop {counter += 1;if counter == 10 {break counter * 2;}};println!("The result is {}", result);
}
7、模式匹配
match和if let总结
一、match
- 基本使用
- 匹配枚举类型示例
enum Direction {East,West,North,South,
}
fn main() {let dire = Direction::South;match dire {Direction::East => println!("East"),Direction::North | Direction::South => {println!("South or North");},_ => println!("West"),};
}
- 通用形式
match target {模式1 => 表达式1,模式2 => {语句1;语句2;表达式2},_ => 表达式3
}
- 将模式与目标值进行匹配,根据匹配的模式执行对应代码。
- 注意事项
- 匹配必须穷举出所有可能,可用
_
代表未列出的所有可能性。 - 每个分支必须是一个表达式,且所有分支表达式最终返回值的类型必须相同。
- 可使用
X | Y
形式匹配X
或Y
。
- 匹配必须穷举出所有可能,可用
- 使用match表达式赋值
- 例如:
enum IpAddr {Ipv4,Ipv6
}
fn main() {let ip1 = IpAddr::Ipv6;let ip_str = match ip1 {IpAddr::Ipv4 => "127.0.0.1",_ => "::1",};println!("{}", ip_str);
}
- 模式绑定
- 从模式中取出绑定的值。例如:
enum Coin {Penny,Nickel,Dime,Quarter(UsState), // 25美分硬币
}
fn value_in_cents(coin: Coin) -> u8 {match coin {Coin::Penny => 1,Coin::Nickel => 5,Coin::Dime => 10,Coin::Quarter(state) => {println!("State quarter from {:?}!", state);25},}
}
- 穷尽匹配
- 必须处理所有情况,否则报错。例如:
enum Direction {East,West,North,South,
}
fn main() {let dire = Direction::South;match dire {Direction::East => println!("East"),Direction::North | Direction::South => {println!("South or North");},};
}
_
通配符- 当不想列出所有值时使用,放置于其他分支后,匹配所有遗漏的值。例如:
let some_u8_value = 0u8;
match some_u8_value {1 => println!("one"),3 => println!("three"),5 => println!("five"),7 => println!("seven"),_ => (),
}
- 也可用变量承载其他情况。
二、if let
- 基本使用
- 当只关心一个模式的值,忽略其他值时使用。例如:
let v = Some(3u8);
if let Some(3) = v {println!("three");
}
- 与match的区别
- 当只要匹配一个条件且忽略其他条件时用
if let
,否则用match
。
- 当只要匹配一个条件且忽略其他条件时用
三、matches!宏
- 基本使用
- 将表达式跟模式进行匹配,返回匹配结果
true
或false
。例如:
- 将表达式跟模式进行匹配,返回匹配结果
enum MyEnum {Foo,Bar
}
fn main() {let v = vec![MyEnum::Foo,MyEnum::Bar,MyEnum::Foo];v.iter().filter(|x| matches!(x, MyEnum::Foo));
}
- 更多示例
let foo = 'f';
assert!(matches!(foo, 'A'..='Z' | 'a'..='Z'));let bar = Some(4);
assert!(matches!(bar, Some(x) if x > 2));
四、变量遮蔽
- if let中的变量遮蔽
- 例如:
fn main() {let age = Some(30);println!("在匹配前,age是{:?}",age);if let Some(age) = age {println!("匹配出来的age是{}",age);}println!("在匹配后,age是{:?}",age);
}
if let
中=
右边的变量被左边新变量遮蔽,遮蔽持续到if let
语句块结束。
- match中的变量遮蔽
- 例如:
fn main() {let age = Some(30);println!("在匹配前,age是{:?}",age);match age {Some(age) => println!("匹配出来的age是{}",age),_ => ()}println!("在匹配后,age是{:?}",age);
}
- 最好不要使用同名变量,避免难以理解。
解构Option
一、Option枚举介绍
- 定义
- 定义如下:
enum Option<T> {None,Some(T),
}
- 用于解决Rust中变量是否有值的问题,一个变量要么有值(
Some(T)
),要么为空(None
)。
- 使用注意
Option
,Some
,None
都包含在prelude
中,可直接通过名称使用,无需Option::Some
这种形式,但要记住它们是Option
的枚举成员。
二、匹配Option
- plus_one函数示例
- 函数定义:
fn plus_one(x: Option<i32>) -> Option<i32> {match x {None => None,Some(x) => Some(x + 1),}
}
- 函数接受
Option<i32>
类型参数,返回Option<i32>
类型值。
- 匹配过程分析
- 当传入
Some(5)
时:- 首先匹配
None
分支,不匹配,继续下一个分支。 Some(5)
与Some(x)
匹配,x
绑定值为5
,执行Some(x + 1)
,返回Some(6)
。
- 首先匹配
- 当传入
None
时:- 匹配
None
分支,返回None
,其他分支不再比较。
- 匹配
- 当传入
模式适用场景
- 组成内容
- 由字面值、解构的数组、枚举、结构体或元组、变量、通配符、占位符组合而成。
- 适用场景
- 常与
match
表达式联用,也用于if let
、while let
、for
循环、let
语句、函数参数等地方。
- 常与
(一)match
分支
- 基本形式
match VALUE {PATTERN => EXPRESSION,PATTERN => EXPRESSION,PATTERN => EXPRESSION,
}
- 使用通配符
_
- 用于匹配剩余所有情况,因为
match
匹配是穷尽式的。
- 用于匹配剩余所有情况,因为
match VALUE {.._ => EXPRESSION,
}
(二)if let
分支
- 基本形式
if let PATTERN = SOME_VALUE {}
- 特点
- 用于匹配一个模式,忽略剩下所有模式。
(三)while let
条件循环
- 基本形式及示例
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);
while let Some(top) = stack.pop() {println!("{}", top);
}
- 原理
- 只要模式匹配就一直进行
while
循环,如pop
方法返回Some
就循环,返回None
则停止。
- 只要模式匹配就一直进行
(四)for
循环
- 基本形式及示例
let v = vec!['a', 'b', 'c'];
for (index, value) in v.iter().enumerate() {println!("{} is at index {}", value, index);
}
- 原理
- 使用
enumerate
方法产生迭代器,每次迭代返回(索引,值)
形式元组,用(index,value)
匹配。
- 使用
(五)let
语句
- 基本形式及示例
let PATTERN = EXPRESSION;
let x = 5;
let (x, y, z) = (1, 2, 3);
- 原理
- 也是一种模式匹配,变量名是一种模式,将匹配的值绑定到变量上。要求两边类型必须相同,对于元组元素个数也是类型一部分。
(六)函数参数
- 基本形式及示例
fn foo(x: i32) {// 代码
}
fn print_coordinates(&(x, y): &(i32, i32)) {println!("Current location: ({}, {})", x, y);
}
- 原理
- 函数参数是模式,可在参数中匹配元组。
(七)let-else
(Rust 1.65新增)
- 基本形式及示例
use std::str::FromStr;
fn get_count_item(s: &str) -> (u64, &str) {let mut it = s.split(' ');let (Some(count_str), Some(item)) = (it.next(), it.next()) else {panic!("Can't segment count item pair: '{s}'");};let Ok(count) = u64::from_str(count_str) else {panic!("Can't parse integer: '{count_str}'");};(count, item)
}
- 特点
- 可使
let
变为可驳模式,用else
分支处理模式不匹配情况,else
分支须用发散代码块处理。 - 解包成功时创建的变量具有更广作用域,与
if let
相比,let-else
写法里的变量可在let
之外使用。
- 可使
全模式匹配总结
一、匹配字面值
let x = 1;
match x {1 => println!("one"),2 => println!("two"),3 => println!("three"),_ => println!("anything"),
}
- 根据值匹配特定字面值,若
x
为1则打印one
。
二、匹配命名变量
let x = Some(5);
let y = 10;
match x {Some(50) => println!("Got 50"),Some(y) => println!("Matched, y = {:?}", y),_ => println!("Default case, x = {:?}", x),
}
println!("at the end: x = {:?}, y = {:?}", x, y);
- 匹配过程中可能出现变量遮蔽,新变量
y
在match
作用域内,匹配Some
中的值(此处为x
中的值5)。若x
为None
,匹配_
分支时x
为外部未遮蔽的None
。
三、单分支多模式
let x = 1;
match x {1|2 => println!("one or two"),3 => println!("three"),_ => println!("anything"),
}
- 使用
|
语法匹配多个模式,x
为1或2时打印one or two
。
四、通过序列..=
匹配值的范围
let x = 5;
match x {1..=5 => println!("one through five"),_ => println!("something else"),
}
..=
语法用于匹配闭区间序列内的值,适用于数字或字符类型。如x
为1 - 5时匹配第一个分支。
五、解构并分解值
(一)解构结构体
struct Point{ x:i32, y:i32, }
let p = Point { x:0, y:7};
let Point { x: a, y: b } = p;
// 或简写为
let Point { x, y } = p;
- 可以用
let
解构结构体,创建变量匹配结构体字段,变量名可与字段名不一致,也可使用字面值作为结构体模式一部分进行解构。
(二)解构枚举
enum Message{Quit,Move { x:i32, y:i32},Write(String),ChangeColor(i32,i32,i32),
}
let msg = Message::ChangeColor(0,160,255);
match msg {Message::Quit => {println!("The Quit variant has no data to destructure.") },Message::Move { x, y } => {println!("Move in the x direction {} and in the y direction {}", x, y ); }Message::Write(text) =>println!("Text message: {}", text),Message::ChangeColor(r, g, b) => {println!("Change the color to red {}, green {}, and blue {}", r, g, b ) }
}
- 用
match
解构枚举,模式要与枚举值类型相同,对于无数据的枚举成员(如Message::Quit
)只能匹配字面值,其他成员用同类型模式匹配出对应值。
(三)解构嵌套的结构体和枚举
enum Color{ Rgb(i32,i32,i32), Hsv(i32,i32,i32), }
enum Message{ Quit, Move { x:i32, y:i32}, Write(String), ChangeColor(Color), }
let msg = Message::ChangeColor(Color::Hsv(0,160,255));
match msg {Message::ChangeColor(Color::Rgb(r, g, b)) => {println!("Change the color to red {}, green {}, and blue {}", r, g, b ) }Message::ChangeColor(Color::Hsv(h, s, v)) => {println!("Change the color to hue {}, saturation {}, and value {}", h, s, v ) }_ => ()
}
match
可匹配嵌套项,如上述代码匹配Message::ChangeColor
枚举成员及内部的Color
枚举成员。
(四)解构结构体和元组
struct Point{ x:i32, y:i32, }
let ((feet, inches), Point {x, y}) = ((3,10), Point { x:3, y: -10});
- 可混合、匹配和嵌套解构模式,分解复杂类型得到感兴趣的值。
(五)解构数组
// 定长数组
let arr: [u16;2] = [114,514];
let [x, y] = arr;
// 不定长数组
let arr: & [u16] = & [114,514];
if let [x,..] = arr {assert_eq!(x, &114); }
if let & [.., y] = arr {assert_eq!(y,514); }
let arr: & [u16] = & [];
assert!(matches!(arr, [..]));
assert!(!matches!(arr, [x,..]));
- 定长数组可直接解构,不定长数组可使用
if let
解构部分元素。
六、忽略模式中的值
(一)使用_
忽略整个值
fn foo(_:i32, y:i32) {println!("This code only uses the y parameter: {}", y); }
- 可用于函数参数中忽略值,如忽略第一个参数
3
。
(二)使用嵌套的_
忽略部分值
let mut setting_value =Some(5);
let new_setting_value =Some(10);
match (setting_value, new_setting_value) {(Some(_),Some(_)) => {println!("Can't overwrite an existing customized value"); }_ => { setting_value = new_setting_value; }
}
- 在模式内部使用
_
忽略部分值,如上述代码忽略元组中Some
内的值。
(三)使用下划线开头忽略未使用的变量
let _x = 5;
let y = 10;
- 以下划线开头命名变量可忽略未使用变量的警告,
_x
会绑定值,_
则不会。
(四)用..
忽略剩余值
struct Point{ x:i32, y:i32, z:i32, }
let origin = Point { x:0, y:0, z:0};
match origin {Point { x,.. } =>println!("x is {}", x),
}
- 使用
..
忽略模式中剩余未显式匹配的值部分,如上述代码忽略Point
结构体除x
外的字段。
七、匹配守卫提供的额外条件
let num =Some(4);
match num {Some(x)ifx <5=>println!("less than five: {}", x),Some(x) =>println!("{}", x),None=> (),
}
- 匹配守卫是位于
match
分支模式后的if
条件,可使用模式中创建的变量,为分支模式提供更进一步匹配条件。
八、@绑定
enum Message{ Hello { id:i32}, }
let msg = Message::Hello { id:5};
match msg {Message::Hello { id: id_variable @3..=7} => {println!("Found an id in range: {}", id_variable) },Message::Hello { id:10..=12} => {println!("Found an id in another range") },Message::Hello { id } => {println!("Found some other id: {}", id) },
}
@
运算符允许为一个字段绑定另外一个变量,如上述代码将id
值绑定到id_variable
并测试是否在3..=7
范围内。
(一)、@前绑定后解构(Rust 1.56新增)
#[derive(Debug)]
struct Point{ x:i32, y:i32, }
let p @ Point {x: px, y: py } = Point {x:10, y:23};
println!("x: {}, y: {}", px, py);
println!("{:?}", p);
- 使用
@
可在绑定新变量同时对目标进行解构。
(二)、@新特性(Rust 1.53 新增)
fn main(){match 1 {num @ (1|2) {println!("{}",num);}}
}
8、方法Method
一、方法定义
- 基本概念
- Rust中方法往往和结构体、枚举、特征一起使用。使用
impl
来定义方法。
- Rust中方法往往和结构体、枚举、特征一起使用。使用
- 示例
- 例如定义
Circle
结构体及相关方法:
- 例如定义
struct Circle {x: f64,y: f64,radius: f64,
}impl Circle {fn new(x:f64,y:f64,radius:f64)->Circle {Circle {x:x,y:y,radius:radius,}}fn area(&self) -> f64{std::f64::const::PI * (self.radius * self.radius)}
}
- 以及
Rectangle
结构体及方法:
#[derive(Debug)]
struct Rectangle {width: u32,height: u32,
}
impl Rectangle {fn area(&self) -> u32 {self.width * self.height}
}
二、self、&self 和 &mut self
- 含义
- 在
impl
块内,Self
指代被实现方法的结构体类型,self
指代此类型的实例。&self
是self: &Self
的简写。
- 在
- 使用场景
- 当不想获取所有权且只需读取结构体数据时使用
&self
,如Rectangle
的area
方法。若要在方法中改变结构体,使用&mut self
。使用self
获取所有权较少见,常用于对象转换。
- 当不想获取所有权且只需读取结构体数据时使用
- self
含义:表示方法接收者的所有权将被转移。
使用场景:当方法需要消耗接收者的资源时使用。
struct MyStruct {value: i32,
}
impl MyStruct {fn consume(self) -> i32 {self.value}
}
fn main() {let s = MyStruct { value: 42 };let value = s.consume(); // s 的所有权被转移,之后不能再使用 sprintln!("Value: {}", value);
}
consum
方法消耗s
的所有权,因此在调用consume
之后,s
不能再被使用
- &self
含义:表示方法接收者是一个不可变引用。
使用场景:当方法需要读取接收者的数据但不修改它时使用。
struct Mystruct {value: i32,
}
impl Mystruct {fn get_value(&self) ->i32 {self.value}
}
fn main(){let s = Mystruct {value: 42};let value = s.get_value();println!("Value: {}", value);println!("Value again: {}", s.get_value());
}
get_value
方法只读取 s
的数据,因此s
的所有权没有转移,可以在方法调用后继续使用 s
。
- &mut self
含义:表示方法接收者是一个可变引用。
使用场景:当方法需要修改接收者的数据时使用。
struct MyStruct {value: i32,
}
impl MyStruct {fn set_value(&mut self, new_value: i32) {self.value = new_value;}
}
fn main() {let mut s = MyStruct { value: 42 };s.set_value(100); // s 的所有权没有转移,但 s 是可变的println!("New value: {}", s.value);
}
三、方法名与结构体字段名相同
- 示例
- 如
Rectangle
结构体的width
方法:
- 如
impl Rectangle {fn width(&self) -> bool {self.width > 0}
}
- 用途
- 适用于实现
getter
访问器,当从模块外部访问结构体时,字段默认私有,可通过公开方法获取字段值。
- 适用于实现
四、->
运算符
- 与其他语言对比
- 在C/C++中,有
.<
和->
运算符分别用于对象和对象指针调用方法。
- 在C/C++中,有
- Rust中的情况
- Rust没有
->
等效运算符,有自动引用和解引用功能。当调用方法object.something()
时,Rust会自动为object
添加&
、&mut
或*
使其与方法签名匹配。
- Rust没有
p1.distance(&p2);
(&p1).distance(&p2);
五、带有多个参数的方法
- 示例
- 如
Rectangle
结构体的can_hold
方法:
- 如
impl Rectangle {fn area(&self) -> u32 {self.width * self.height}fn can_hold(&self, other: &Rectangle) -> bool {self.width > other.width && self.height > other.height}
}
六、关联函数
- 定义
- 定义在
impl
中且没有self
的函数,与结构体紧密关联但不是方法,如Rectangle
的new
函数。
- 定义在
- 调用方式
- 用
::
调用,位于结构体命名空间中,如let sq = Rectangle::new(3, 3);
。
- 用
七、多个impl
定义
- 作用
- 提供更多灵活性和代码组织性,可将相关方法组织在同一
impl
块中。
- 提供更多灵活性和代码组织性,可将相关方法组织在同一
- 示例
- 如为
Rectangle
结构体定义两个impl
块分别包含area
和can_hold
方法。
- 如为
八、为枚举实现方法
- 示例
- 为
Message
枚举实现call
方法:
- 为
enum Message {Quit,Move { x: i32, y: i32 },Write(String),ChangeColor(i32, i32, i32),
}
impl Message {fn call(&self) {// 在这里定义方法体}
}
fn main(){let m = Message::Write(String::from("hello"));m.call();
}
9、泛型总结
一、泛型概念
- 作用
- 用同一功能函数处理不同类型数据,减少代码臃肿,是一种多态。
- 示例
- 如不使用泛型需为不同类型写多个加法函数,使用泛型可简化:
fn add<T>(a:T, b:T) -> T {a + b
}
二、泛型详解
- 泛型参数声明与使用
- 需在使用前声明,如
largest<T>
函数:
- 需在使用前声明,如
fn largest<T>(list: &[T]) -> T {
}
- 泛型函数可能因类型无对应操作而报错,需添加类型限制,如
add
函数需std::ops::Add<Output = T>
限制,largest
函数需std::cmp::PartialOrd
限制。
- 显式指定泛型类型参数
- 编译器无法推断时需显式指定,如
create_and_print
函数:
- 编译器无法推断时需显式指定,如
use std::fmt::Display;
fn create_and_print<T>() where T: From<i32> + Display {let a: T = 100.into();println!("a is: {}", a);
}
三、结构体中使用泛型
- 定义与使用
- 结构体字段类型可用泛型定义,如
Point<T>
结构体:
- 结构体字段类型可用泛型定义,如
struct Point<T> {x: T,y: T,
}
- 注意提前声明泛型参数,且
x
和y
类型需相同,否则报错,若想不同需用不同泛型参数,如Point<T,U>
。
四、枚举中使用泛型
Option<T>
枚举- 拥有泛型
T
,Some(T)
存放类型为T
的值,用于函数返回值表示有值或无值。
- 拥有泛型
Result<T,E>
枚举- 用于函数返回值,关注值的正确性,正常返回
Ok(T)
,异常返回Err(E)
。
- 用于函数返回值,关注值的正确性,正常返回
五、方法中使用泛型
- 在结构体方法中使用泛型
- 需提前声明,如
Point<T>
结构体的x
方法:
- 需提前声明,如
impl<T> Point<T> {fn x(&self) -> &T {&self.x}
}
- 结构体和方法可分别定义泛型参数,如
Point<T,U>
结构体的mixup
方法定义V,W
参数。
- 为具体泛型类型实现方法
- 可针对特定类型定义方法,如
Point<f32>
的distance_from_origin
方法,其他Point<T>
(T
不是f32
)无此方法。
- 可针对特定类型定义方法,如
六、const泛型
- 定义与使用
- 针对值的泛型,如
display_array
函数可处理不同长度数组:
- 针对值的泛型,如
fn display_array<T: std::fmt::Debug, const N: usize>(arr: [T; N]) {println!("{:?}", arr);
}
- 语法为
const N: usize
,N
基于的值类型是usize
。
- const泛型表达式
- 可用于限制函数参数内存大小等,如
something
函数:
- 可用于限制函数参数内存大小等,如
// 目前只能在nightly版本下使用
#![allow(incomplete_features)]
#![feature(generic_const_exprs)]
fn something<T>(val: T)
whereAssert<{ core::mem::ristmasAssert<{ core::mem::size_of::<T>() < 768 }>: IsTrue,
{//
}
- 结合const fn与const泛型
- 可实现更灵活高效代码,如创建固定大小缓冲区结构,
compute_buffer_size
常量函数计算缓冲区大小,传递给Buffer
结构体。
- 可实现更灵活高效代码,如创建固定大小缓冲区结构,
七、const fn
- 基本用法
- 函数声明前加
const
关键字,如
- 函数声明前加
const fn add(a: usize, b: usize) -> usize {a + b}
- 限制
- 在编译期执行,需确保能安全求值,不可将随机数生成器写成
const fn
,不建议使数组长度
和Enum判别式
依赖于浮点计算。
- 在编译期执行,需确保能安全求值,不可将随机数生成器写成
八、泛型性能
- 零成本抽象
- Rust中泛型是零成本抽象,使用时无需担心性能问题。
- 编译期单态化
- Rust在编译期为泛型对应的多个类型生成各自代码,通过单态化保证效率,会损失编译速度和增大生成文件大小。如
Option<T>
会展开为Option_i32
和Option_f64
等。
- Rust在编译期为泛型对应的多个类型生成各自代码,通过单态化保证效率,会损失编译速度和增大生成文件大小。如
10、Trait总结
一、Trait概念
- 定义
- 定义了某个特定类型拥有可能与其他类型共享的功能,类似于接口。
- 用途
- 以抽象方式定义共同行为,可使用trait bounds指定泛型为拥有特定行为的类型。
二、定义Trait
- 语法
- 使用
trait
关键字声明,如
- 使用
pub trait Summary{fn summarize(&self)->String;
}
- 声明为
pub
可被其他crate使用。方法签名后跟分号,实现类型需提供方法体,编译器确保方法一致。Trait体可有多行方法签名且都以分号结尾。
三、为类型实现Trait
- 语法
impl Summary for NewsArticle {fn summarize(&self)->String{format!("{},by {}({})",self.headline,self.author,self.location)}
}
- 类似于实现常规方法,
impl
关键字后需提供trait名称和类型名称,在impl
块中使用 trait定义中的方法签名并编写函数体实现行为。
示例:
pub trait Summary{fn summarize(&self) -> String;
}
pub struct Post{pub title:String,pub author:String,pub content:String,
}
impl Summary for Post{fn summarize(&self) -> String{format!("文章{}, 作者是{}", self.title, self.author)}
}
pub struct Weibo {pub username: String,pub content: String
}
impl Summary for Weibo {fn summarize(&self) -> String {format!("{}发表了微博{}", self.username, self.content)}
}
// 实现特征的语法为结构体,枚举实现方法,如:impl Summary for Post,读作为Post类型实现Summary特征,然后在impl的花括号中实现该特征的具体方法。
fn main() {let post = Post{title: "Rust语言简介".to_string(),author: "Sunface".to_string(), content: "Rust棒极了!".to_string()};let weibo = Weibo{username: "sunface".to_string(),content: "好像微博没Tweet好用".to_string()};println!("{}",post.summarize());println!("{}",weibo.summarize());
}
- 限制(孤儿规则)
- 只有trait或类型至少有一个属于当前crate时
// 在当前 crate 中定义的类型
struct MyType;
// 在当前 crate 中定义的 trait
trait MyTrait {fn my_method(&self);
}
// 为 MyType 实现 MyTrait,这是合法的
impl MyTrait for MyType {fn my_method(&self) {println!("MyType's implementation of MyTrait");}
}
- 不合法的实现
假设我们有一个外部库 external_lib,其中定义了一个 ExternalType 和一个 ExternalTrait:
// external_lib.rs
pub struct ExternalType;
pub trait ExternalTrait {fn external_method(&self);
}
在crate中,尝试为ExternalType实现ExternalTrait不合法,因为ExternalType和ExternalTrait都不是在当前crate中定义的
extern crate external_lib;
// 尝试为外部类型实现外部 trait,这是不合法的impl external_lib::ExternalTrait for
external_lib::ExternalType {fn external_method(&self) {println!("Implementation of ExternalTrait for ExternalType");}
}
//解决方案:创建一个新的类型,该类型包装外部类型,并实现外部
// struct MyWrapper(external_lib::ExternalType);
impl external_lib::ExternalTrait for MyWrapper {}
四、默认实现
- 定义与使用
- 可为Trait中的方法提供默认行为,如
pub trait Summary {fn summarize(&self) -> String {String::from("(Read more...)")}}
。
- 可为Trait中的方法提供默认行为,如
pub trait Summary{// 方法签名fn summarize(&self) -> String{String::from("(Read more...)")}
}
- 类型可选择保留或重载默认行为,如
impl Summary for NewsArticle {}
可使用默认实现。
impl Summary for Post {}
impl Summary for Weibo {fn summarize(&self) -> String {format!("{}发表了微博{}", self.username, self.content)}
}
println!("{}",post.summarize());
println!("{}",weibo.summarize());> 输出:
(Read more...)
sunface发表了微博好像微博没Tweet好用
- 优势
- 默认实现可调用相同Trait中的其他方法,例如可定义
Summary
trait使summarize
方法默认调用summarize_author
方法,只需实现summarize_author
即可使用Summary
trait的summarize
功能。
- 默认实现可调用相同Trait中的其他方法,例如可定义
五、Trait作为参数
impl Trait
语法
pub fn notify(item: &impl Summary) {println!("Breaking news! {}", item.summarize());
}
- 参数支持任何实现指定Trait的类型,可以实现了Summary特征的类型作为该函数的参数,同时在函数体内,还可以调用该特征的方法。
- Trait Bound语法
- 完整形式如
pub fn notify<T: Summary>(item: &T) {println!("Breaking news! {}", item.summarize());
}
T:Summary
被称为特征约束
trait bound与泛型参数声明在一起。impl Trait
适用于短小例子,trait bound适用于复杂场景。
pub fn notify(item1: &impl Summary,item2: &impl Summary){}
//特征约束合并相同类型
pub fn notify<T: Summary>(item1: &T,item2: &T){}
- 通过
+
指定多重trait bound- 如
pub fn notify<T: Summary + Display>(item: &T) {}
,可使参数同时满足多个trait要求。
- 如
pub fn notify(item: &(impl Summary + Display)) {}
- 通过
where
简化trait bound
函数签名变得复杂后:
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {}
在以上形式的改进通过where
:
fn some_function<T, U>(t: &T, u: &U) -> i32 where T: Display + Clone, U: Clone + Debug {}
可使函数签名更简洁。
需要确保两个泛型参数 T 和 U 分别实现了不同的 trait。
use std::fmt::Display;
fn combine_and_print<T, U>(t: T, u: U)
whereT: Display,U: Clone,
{println!("T: {}", t);let cloned_u = u.clone();println!("Cloned U: {:?}", cloned_u);
}
fn main() {let x = 42;let y = "hello";combine_and_print(x, y);
}
where
约束确保了 T
必须实现了Display
trait,而U
必须实现了 Clone
trait。
- 有关联类型的 trait
trait MyTrait {type Output; // 关联类型fn process(&self, input: i32) -> Self::Output; // 输出类型为关联类型
}
在这个 trait 中,Self::Output 表示一个与 Self 相关的类型。具体实现时,你需要为 Output 指定一个实际的类型。
类型实现的trait
示例:
struct MyStruct;
impl MyTrait for MyStruct {type Output = i32; // 指定关联类型为 i32fn process(&self, input: i32) -> Self::Output {input * 2}
}struct AnotherStruct;
impl MyTrait for AnotherStruct {type Output = String; // 指定关联类型为 Stringfn process(&self, input: i32) -> Self::Output {format!("Processed value: {}", input)}
}
fn main() {let s = MyStruct;let result1 = s.process(10);println!("Result 1: {}", result1); // 输出: Result 1: 20let a = AnotherStruct;let result2 = a.process(10);println!("Result 2: {}", result2); // 输出: Result 2: Processed value: 10
}
六、返回实现了trait的类型
impl Trait
语法
简洁性:避免在函数签名中写出冗长的类型名称。
抽象性:隐藏具体的实现细节,只暴露接口。
灵活性:可以在不同情况下返回不同类型,只要这些类型实现了相同的 trait
fn returns_summarizable() -> impl Summary {...}
指定函数返回某个实现了Summary
trait的类型,但不确定具体类型。
- 适用于返回类型复杂如闭包和迭代器的情况,但只能返回单一类型。
示例 1:基本用法
fn create_iterator()-> impl Iterator<Item = i32>{vec![1,2,3,4,5].into_iter()
}
fn main(){let iter = create_iterator();for item in iter {println!("{}", item);}
}
create_iterator函数实现了Iterator trait的迭代器,具体的迭代器类型是std::vec::IntoIter<i32>
,但我们不需要在函数签名中显式地指定它。
七、使用trait bound有条件地实现方法
- 在泛型上有条件地实现方法
- 如
impl<T: Display + PartialOrd> Pair<T> {fn cmp_display(&self) {...}}
,只有满足条件的Pair<T>
才实现cmp_display
方法。 - 首先定义了一个泛型结构体Pair,它有两个类型为T的字段x和y。
- 为Pair实现了一个new方法,这个方法对于任何T类型都可以使用,因为它没有任何特征约束。
- 如
use std::fmt::Display;
struct Pair<T> {x: T,y: T,
}
impl<T> Pair<T> {fn new(x: T, y: T) -> Self {Self { x, y }}
}
impl<T: Display + PartialOrd> Pair<T> {fn cmp_display(&self) {if self.x >= self.y {println!("The largest member is x = {}", self.x);} else {println!("The largest member is y = {}", self.y);}}
}
- 对类型有条件地实现trait(blanket implementations)
- 如标准库为实现
Display
trait的类型实现ToString
trait,可通过to_string
方法转换类型。
- 如标准库为实现
impl<T> ToString for T
whereT: Display + ?Sized,
{fn to_string(&self)-> String{format!("{}",self);}
}
- 使用 to_string 方法
由于标准库已经为所有实现了 Display trait 的类型提供了 ToString trait 的实现,你可以在这些类型上调用 to_string 方法,将它们转换为 String。
use std::fmt::Display;
struct Point {x: i32,y: i32,
}
impl fmt::Display for Point {fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {write!(f, "({}, {})", self.x, self.y)}
}
fn main() {let p = Point { x: 1, y: 2 };let s: String = p.to_string();println!("Point as String: {}", s); // 输出: Point as String: (1, 2)let num = 42;let s: String = num.to_string();println!("Number as String: {}", s); // 输出: Number as String: 42
}
- 自定义 blanket implementation
use std::fmt::Display;
trait MyTrait {fn my_method(&self) -> String;
}
// 有条件地实现 MyTrait
impl<T> MyTrait for T
whereT: Display,
{fn my_method(&self) -> String {format!("Value: {}", self)}
}
struct Point {x: i32,y: i32,
}
impl fmt::Display for Point {fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {write!(f, "({}, {})", self.x, self.y)}
}
fn main() {let p = Point { x: 1, y: 2 };let s: String = p.my_method();println!("Point as String: {}", s); // 输出: Point as String: Value: (1, 2)let num = 42;let s: String = num.my_method();println!("Number as String: {}", s); // 输出: Number as String: Value: 42
}
在这个例子中,我们为所有实现了 Display trait 的类型有条件地实现了 MyTrait,并在 my_method 方法中使用 format! 宏将 self 转换为字符串。
11、生命周期总结
一、生命周期概念
- 定义
- 生命周期是引用有效的范围。Rust 编译器通过生命周期注解来确保引用在其生命周期内始终有效。生命周期注解不会影响程序的运行时行为,仅在编译时进行检查。
- 作用
- 避免悬垂引用,即程序引用非预期引用的数据。
二、悬垂引用与借用检查器
(一)悬垂引用示例
生命周期的主要作用是避免悬垂引用,它会导致程序引用了本不该引用的数据:
{let r; // ---------+-- 'a{ // |let x = 5; // -+-- 'b |r = &x; // | |} // -+ |println!("r: {}", r); // |
} // ---------+
- 外部作用域变量
r
引用内部作用域变量x
,x
在内部作用域结束时被释放,r
成为悬垂引用,导致编译错误。
(二)借用检查器
{let x = 5; // ----------+-- 'blet r = &x; // --+-- 'a |println!("r: {}", r); // | |// --+ |
} // ----------+
- 编译器中的借用检查器比较作用域,确保借用有效。如上述示例中,
r
生命周期标记为'a
,x
为'b
,'b
小于'a
,引用无效,编译被拒绝。
三、函数中的生命周期
(一)生命周期标注需求
fn main() {let string1 = String::from("abcd");let string2 = "xyz";let result = longest(string1.as_str(), string2);println!("The longest string is {result}");
}fn longest(x: &str, y: &str) -> &str {if x.len() > y.len() {x} else {y}
}
- 对于返回两个字符串slice中较长者的
longest
函数,编译器无法确定返回值引用x
还是y
,需要标注生命周期。
(二)生命周期标注语法
- 以
'
开头,名称通常为小写字母,如'a
。位于引用的&
之后,用空格分隔。例如:
&i32 // 引用
&'a i32 // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用
(三)函数签名中的生命周期标注
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {if x.len() > y.len() {x} else {y}
}
- 声明生命周期参数
< 'a>
,x
、y
和返回值至少活得和'a
一样久,返回值生命周期与参数生命周期中的较小值一致。
(四)深入理解生命周期标注
- 函数返回值引用类型的生命周期来源于函数参数或函数体中新建引用。
- 若来源于新建引用且在函数结束后引用依然存在则是悬垂引用,应返回内部字符串所有权。
若是后者情况,就是典型的悬垂引用场景:
fn longest<'a>(x: &str, y: &str) -> &'a str {let result = String::from("really long string");result.as_str()
}
显然函数的返回值和参数x,y没有任何关系,而是引用了函数体内创建的字符串,主要是,result在函数结束后就被释放,但是在函数结束后,对result的引用依然在继续,这种情况下,没有办法指定合适的生命周期来让编译通过,因此造成悬挂引用。
fn longest<'a>(_x: &str, _y: &str) -> String {String::from("really long string")
}
fn main() {let s = longest("not", "important");
}
新创建的引用将字符串的所有权转移给调用者。
四、结构体中的生命周期
(一)标注语法
struct ImportantExcerpt<'a> {part: &'a str,
}
- 结构体中有引用类型字段时需标注生命周期,语法与泛型参数语法相似,需声明
< 'a>
。
(二)示例
fn main() {let novel = String::from("Call me Ishmael. Some years ago...");let first_sentence = novel.split('.').next().unwrap();let i = ImportantExcerpt {part: first_sentence,};
}
- 结构体
ImportantExcerpt<'a>
的part
字段为&'a str
,要求引用的字符串生命周期大于等于结构体生命周期。
五、生命周期省略
(一)原因
- 早期Rust要求所有引用必须有明确生命周期,后来因程序员常重复编写相同生命周期注解,编译器将一些模式编码进自身,符合这些模式的情况可省略生命周期注解。
(二)省略规则
函数或者方法中,参数的生命周期被称为 输入生命周期,返回值的生命周期被称为 输出生命周期
- 输入生命周期
- 编译器为每一个引用参数分配一个生命周期参数,如
fn foo<'a>(x: &'a i32)
,fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
。
- 编译器为每一个引用参数分配一个生命周期参数,如
- 输出生命周期
- 若只有一个输入生命周期参数,它被赋予所有输出生命周期参数,如
fn foo<'a>(x: &'a i32) -> &'a i32
。
- 若只有一个输入生命周期参数,它被赋予所有输出生命周期参数,如
fn first_word<'a>(s: &'a str) -> &'a str { // 编译器自动为返回值添加生命周期
- 若方法有多个输入生命周期参数且其中一个是
&self
或&mut self
,所有输出生命周期参数被赋予self
的生命周期。
impl String{fn split_whitespace(&self)->Vec<&str>{self.split_whitespace().collect()}
}
六、方法中的生命周期
(一)语法
- 为带有生命周期的结构体实现方法时,
impl
中需使用结构体完整名称包括< 'a>
,方法签名中往往不需标注生命周期(得益于生命周期省略规则)。
(二)示例
struct ImportantExcerpt<'a> {part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {fn level(&self) -> i32 {3}
}
impl<'a> ImportantExcerpt<'a> {fn announce_and_return_part(&self, announcement: &str) -> &str {println!("Attention please: {announcement}");self.part}
}
- 这里有两个输入生命周期,应用第一条规则给予
&self
和announcement
各自生命周期,因&self
存在,返回值被赋予&self
生命周期
多个生命周期参数
如果方法有多个输入引用参数,且这些参数的生命周期不同,而且显示标注生命周期。
struct Pair<'a,'b>{first:&'a str,second:&'b str,
}
impl<'a, 'b> Pair<'a, 'b>{fn compare_lengths(&self) -> bool {self.first.len() > self.second.len()}fn longer(&self) -> &str{if self.first.len() > self.second.len() {self.first}else{self.second}}
}
fn main(){let first = "hello";let second = "World";let p =Pair{first,second};println!("Are the lengths of the strings different? {}", p.compare_lengths());println!("The longer string is: {}", p.longer());
}
- Pair 结构体包含两个引用 first 和 second,分别标注为
'a
和'b
。 - compare_lengths 方法没有返回引用,因此不需要生命周期注解。
- longer 方法返回一个引用,返回值的生命周期需要与 self 的生命周期相同。由于 self 包含两个不同的生命周期参数,返回值的生命周期需要与最长的那个生命周期相同。
七、静态生命周期
- 定义与示例
'static
生命周期能存活于整个程序期间,所有字符串字面值都拥有'static
生命周期,如
let s: &'static str = "I have a static lifetime.";
- 使用注意
- 考虑引用是否真的在整个程序生命周期里有效,以及是否希望它存在这么久,避免在错误的情况下使用
'static
来解决生命周期问题。
- 考虑引用是否真的在整个程序生命周期里有效,以及是否希望它存在这么久,避免在错误的情况下使用
12、集合类型总结
一、Vec<T>
(向量)
(一)概念
- 定义
Vec<T>
是一个可以在一个单独的数据结构中储存多于一个相同类型值的类型,这些值在内存中彼此相邻排列。
(二)操作
- 新建
- 可以使用
Vec::new
函数创建一个新的空Vec<T>
,需要指定类型注解(当未插入值时),如let v: Vec<i32> = Vec::new();
。 - 也可使用
vec!
宏根据提供的值创建并推断类型,如let v = vec![1, 2, 3];
。
- 可以使用
- 更新
- 使用
push
方法向Vec<T>
增加值,需要将其声明为可变,如let mut v = Vec::new(); v.push(5);
。
- 使用
- 读取
- 可以通过索引或
get
方法引用Vec<T>
中储存的值。 - 索引语法
&v[index]
会得到一个索引位置元素的引用,索引从0开始;get
方法v.get(index)
会得到一个可以用于match
的Option<&T>
。 - 当索引超出范围时,
[]
方法会导致panic
,get
方法会返回None
。
- 可以通过索引或
- 遍历
- 可以使用
for
循环遍历Vec<T>
的元素。对于不可变Vec<T>
,使用for i in &v {...}
获取不可变引用; - 对于可变
Vec<T>
,使用for i in &mut v {...}
获取可变引用并可修改元素。
- 可以使用
- 使用枚举储存多种类型
- 当需要在
Vec<T>
中储存不同类型值时,可以定义一个枚举,其成员存放不同类型的值,然后创建储存枚举值的Vec<T>
,如
- 当需要在
enum SpreadsheetCell {Int(i32),Float(f64),Text(String),}let row = vec![SpreadsheetCell::Int(3),SpreadsheetCell::Text(String::from("blue")),SpreadsheetCell::Float(10.12),];
二、String
(字符串)
(一)概念
- 定义
String
是由标准库提供的一种可增长、可变、可拥有、UTF - 8编码的字符串类型,与字符串slice&str
相关但不同。
(二)操作
- 新建
- 可以使用
String::new
函数创建一个空的String
,如let mut s = String::new();
。 - 也可使用
to_string
方法从实现了Display
trait的类型(如字符串字面值)创建String
,如let s = "initial contents".to_string();
,还可使用String::from
函数,如let s = String::from("initial contents");
。
- 可以使用
- 更新
- 使用
push_str
方法附加字符串slice,如let mut s = String::from("foo"); s.push_str("bar");
。 - 使用
push
方法附加一个单独的字符,如let mut s = String::from("lo"); s.push('l');
。 - 可以使用
+
运算符或format!
宏拼接String
值。+
运算符使用add
函数,会获取第一个字符串的所有权并使用第二个字符串的引用,如let s1 = String::from("Hello, "); let s2 = String::from("world!"); let s3 = s1 + &s2;
。format!
宏更适合复杂的字符串链接,如let s1 = String::from("tic"); let s2 = String::from("tac"); let s3 = String::from("toe"); let s = format!("{s1}-{s2}-{s3}");
。
- 使用
- 索引
- Rust的
String
不支持索引操作,因为字符串的内部表现是Vec<u8>
的封装,字节索引并不总是对应一个有效的Unicode标量值,且索引操作预期的常数时间性能无法保证。 - 可以使用
[]
和一个range来创建含特定字节的字符串slice,如let s = &hello[0..4];
,但要注意字节边界问题。
- Rust的
- 遍历
- 可以使用
chars
方法遍历字符串的Unicode标量值,如for c in "Зд".chars() {...}
。 - 也可使用
bytes
方法遍历字符串的原始字节,如for b in "Зд".bytes() {...}
。
- 可以使用
三、HashMap<K, V>
(哈希表)
一、概念
- 定义
HashMap<K, V>
类型储存键类型K
对应值类型V
的映射,通过哈希函数实现映射,在很多编程语言中有不同名字。
- 用途
- 用于通过键寻找数据,而非像
vector
那样通过索引。
- 用于通过键寻找数据,而非像
二、操作
(一)新建
- 语法
- 使用
new
创建空HashMap
,再用insert
增加元素,如:
- 使用
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
- 注意事项
- 需先
use
标准库中的HashMap
,它相对vector
和字符串使用频率较低,支持也较少,如无内建构建宏。 - 键和值类型必须相同,数据储存在堆上。
- 需先
(二)访问
- 语法
- 通过
get
方法并提供键获取值,返回Option<&V>
,如:
- 通过
let team_name = String::from("Blue");
let score = scores.get(&team_name).copied().unwrap_or(0);
- 遍历
- 使用
for
循环遍历键值对,如:
- 使用
for (key, value) in &scores {println!("{key}: {value}");
}
(三)所有权
- 对于实现
Copy
trait的类型- 值可以拷贝进
HashMap
。
- 值可以拷贝进
- 对于拥有所有权的值(如
String
)- 值将被移动,
HashMap
成为所有者,如插入field_name
和field_value
后不能再使用它们。插入值的引用时,引用指向的值在HashMap
有效时也必须有效。
- 值将被移动,
(四)更新
- 覆盖一个值
- 用相同键插入不同值会替换旧值,如:
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);
// 结果为{"Blue": 25}
- 只在键没有对应值时插入键值对
- 使用
entry
方法,如:
- 使用
scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);
// 结果为{"Yellow": 50, "Blue": 10}
entry
返回Entry
枚举,or_insert
在键对应值存在时返回可变引用,不存在时插入并返回新值的可变引用。
- 根据旧值更新一个值
- 如统计文本中单词出现次数,使用
entry
获取键值对的可变引用并更新,如:
- 如统计文本中单词出现次数,使用
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {let count = map.entry(word).or_insert(0);*count += 1;
}
// 结果可能为{"world": 2, "hello": 1, "wonderful": 1}
(五)哈希函数
- 默认哈希函数
HashMap
默认使用SipHash
哈希函数,可抵御拒绝服务攻击,但不是最快算法。
- 更换哈希函数
- 可指定实现
BuildHasher
trait的类型作为hasher
来切换哈希函数,也可使用crates.io
上的相关库。
- 可指定实现
13、错误处理总结
一、Result<T, E>
(一)概念
- 定义
Result<T, E>
是一个枚举类型,用于处理可恢复的错误。T
代表成功时的值类型,存放于Ok(T)
;E
代表错误时的值类型,存放于Err(E)
。
(二)使用
- 获取返回类型
- 可查询文档、使用IDE插件查看,或故意标记错误类型让编译器提示。例如
File::open
返回Result<std::fs::File, std::io::Error>
。
- 可查询文档、使用IDE插件查看,或故意标记错误类型让编译器提示。例如
- 处理返回的错误
- 可使用
match
匹配Result
类型:
- 可使用
let f = File::open("hello.txt");
let f = match f {Ok(file) => file,Err(error) => {panic!("Problem opening the file: {:?}", error)},
};
- 也可对错误进一步匹配处理,如区分
ErrorKind::NotFound
进行文件创建操作。
- 失败就
panic
:unwrap
和expect
- 在不需要处理错误的场景,
unwrap
和expect
可简化操作。如果成功,取出Ok(T)
中的值;如果失败,直接panic
。expect
可自定义错误提示信息。
- 在不需要处理错误的场景,
- 传播错误
- 函数可将
io::Error
等错误往上传播,调用者再处理。例如:
- 函数可将
fn read_username_from_file() -> Result<String, io::Error> {let mut f = File::open("hello.txt");let mut f = match f {Ok(file) => file,Err(e) => return Err(e),};let mut s = String::new();match f.read_to_string(&mut s) {Ok(_) => Ok(s),Err(e) => Err(e),}
}
?
宏可简化错误传播,功能与match
类似,还可自动进行类型提升(转换),支持链式调用,如:
fn read_username_from_file() -> Result<String, io::Error> {let mut s = String::新();File::open("hello.txt")?.read_to_string(&mut s)?;Ok(s)
}
?
用于Option
的返回?
也可用于Option
传播,Option
通过?
返回None
,如:
fn first(arr: &[i32]) -> Option<&i32> {let v = arr.get(0)?;Some(v)
}
二、panic!
与不可恢复错误
- 适用场景
- 当错误严重影响程序运行且无法恢复时使用,如系统启动阶段文件读取失败。
- 触发方式
- 被动触发:如数组访问越界等错误会自动触发
panic
,给出详细报错信息。 - 主动调用:使用
panic!
宏主动抛出异常,需是不可恢复的错误,如panic!("crash and burn");
。
- 被动触发:如数组访问越界等错误会自动触发
(一)、backtrace
栈展开
- 获取方式
- 在Linux/macOS等UNIX系统使用
RUST_BACKTRACE=1 cargo run
,在Windows系统(PowerShell)使用$env:RUST_BACKTRACE=1 ; cargo run
。
- 在Linux/macOS等UNIX系统使用
- 信息内容
- 包含函数调用顺序(逆序排列),如数组越界访问的栈展开信息可显示从
rust_begin_unwind
到main
函数的调用过程。需开启debug
标志(cargo run
或cargo build
默认开启),栈展开信息在不同操作系统或Rust版本上可能不同。
- 包含函数调用顺序(逆序排列),如数组越界访问的栈展开信息可显示从
(二)、panic
时的两种终止方式
- 栈展开
- 默认方式,回溯栈上数据和函数调用,给出报错和栈调用信息,便于问题复盘。
- 直接终止
- 不清理数据直接退出程序,善后工作交与操作系统。可在
Cargo.toml
中配置[profile.release] panic = 'abort'
实现在release
模式下panic
直接终止。
- 不清理数据直接退出程序,善后工作交与操作系统。可在
(三)、线程panic
后程序是否会终止
main
线程main
线程panic
则程序终止。
- 子线程
- 子线程
panic
则该线程终止,不影响main
线程。
- 子线程
(四)、何时该使用panic!
- 示例、原型、测试场景
- 为快速搭建代码,可使用
unwrap
、expect
等方法触发panic
简化处理,后续可全局搜索替换。
- 为快速搭建代码,可使用
- 确切知道程序正确时
- 如解析已知正确的字符串为IP地址,可使用
unwrap
等方法,因为知道不会panic
。
- 如解析已知正确的字符串为IP地址,可使用
- 可能导致全局有害状态时
- 包括非预期错误、后续代码受显著影响、内存安全问题等情况。当错误预期可处理时(如解析器接收格式错误数据),应返回错误;当启动流程错误影响后续代码运行或内存安全受影响(如数组越界)时,应使用
panic
。
- 包括非预期错误、后续代码受显著影响、内存安全问题等情况。当错误预期可处理时(如解析器接收格式错误数据),应返回错误;当启动流程错误影响后续代码运行或内存安全受影响(如数组越界)时,应使用
(五)、panic
原理剖析
- 触发
panic!
宏时的操作- 格式化
panic
信息,调用std::panic::panic_any()
函数。 panic_any
检查是否使用panic hook
,若使用则调用hook
函数。
- 格式化
- 栈展开过程
hook
函数返回后开始栈展开,从panic_any
开始,一帧一帧回溯栈,每个帧数据可能被丢弃。- 若遇到被标记为
catching
的帧(通过std::panic::catch_unwind()
标记),调用用户提供的catch
函数,展开可能停止,若catch
函数内部调用std::panic::resume_unwind()
则展开继续。若展开本身panic
,展开线程终止。
- 最终输出结果
- 取决于
panic
的线程,main
线程panic
时调用core::intrinsics::abort()
结束panic
进程;子线程panic
则简单终止,信息稍后通过std::thread::join()
收集。
- 取决于
14、包管理和模块
一、包(Crate)和项目(Package)
(一) 包(Crate)
- 定义
- 是一个由多个模块组成的树形结构,是独立的可编译单元,可生成可执行文件或库。
- 示例
- 如
rand
包提供随机数生成功能,通过use rand;
引入后可使用rand::XXX
。
- 如
(二) 项目(Package)
- 定义
- 包含独立的
Cargo.toml
文件以及一个或多个包,只能包含一个库类型的包,但可包含多个二进制可执行类型的包。
- 包含独立的
- 示例
- 二进制
Package
:cargo new my-project
创建,src/main.rs
是二进制包的根文件,包名与Package
相同,可通过cargo run
运行。 - 库
Package
:cargo new my-lib --lib
创建,src/lib.rs
是库包的根文件,不能独立运行。
- 二进制
典型的 Package 结构
├── Cargo.toml
├── Cargo.lock
├── src
│ ├── main.rs
│ ├── lib.rs
│ └── bin
│ └── main1.rs
│ └── main2.rs
├── tests
│ └── some_integration_tests.rs
├── benches
│ └── simple_bench.rs
└── examples└── simple_example.rs
- 唯一库包:
src/lib.rs
- 其余二进制包:
src/bin/main1.rs
和src/bin/main2.rs
,它们会分别生成一个文件同名的二进制可执行文件 - 集成测试文件:
tests
目录下 - 基准性能测试 benchmark 文件:
benches
目录下 - 项目示例:
examples
目录下
二、模块(Module)
(一)创建嵌套模块
- 示例
mod front_of_house {mod hosting {fn add_to_waitlist() {}fn seat_at_table() {}}mod serving {fn take_order() {}fn serve_order() {}fn take_payment() {}}
}
- 使用
mod
关键字创建模块,模块可嵌套,可定义各种Rust类型。
(二)模块树
- 结构
src/main.rs
和src/lib.rs
被称为包根(crate root),其内容形成模块crate
位于模块树根部。如:
crate└── front_of_house├── hosting│ ├── add_to_waitlist│ └── seat_at_table└── serving|── take_order├── serve_order└── take_payment
(三)父子模块
- 关系
- 若模块
A
包含模块B
,A
是B
的父模块,B
是A
的父模块。如front_of_house
是hosting
和serving
的父模块。
- 若模块
src/
├── main.rs
└── kitchen/├── mod.rs└── dishes/└── mod.rs
main.rs 内容:
mod kitchen;
fn main() {kitchen::dishes::make_salad();
}
kitchen/mod.rs 内容:
pub mod dishes;
kitchen/dishes/mod.rs 内容:
pub fn make_salad() {println!("Making a salad!");
}
(四)用路径引用模块
- 绝对路径
- 从包根开始,以
包名
或crate
开头,如crate::front_of_house::hosting::add_to_waitlist
。
- 从包根开始,以
- 相对路径
- 从当前模块开始,以
self
、super
或当前模块标识符开头,如front_of_house::hosting::add_to_waitlist
。 - 选择绝对路径还是相对路径需考虑代码挪动时路径修改量,优先使用绝对路径(定义地方变动少)。
- 从当前模块开始,以
(五)受限的可见性
(一)限制可见性语法
pub(crate)
- 表示在当前包可见,如
pub(crate) item;
。
- 表示在当前包可见,如
pub(in <path>)
- 通过绝对路径限制在包内某个模块内可见,如
pub(in crate::a) mod c {...}
限制模块c
在a
模块内可见。
- 通过绝对路径限制在包内某个模块内可见,如
- 其他
pub
无限制可见;pub(self)
在当前模块可见;pub(super)
在父
- 默认情况
- Rust默认所有类型私有化,父模块无法访问子模块私有项,子模块可访问父模块等的私有项。
pub
关键字- 用于控制模块和模块中项的可见性。如
pub mod hosting {pub fn add_to_waitlist() {}}
使hosting
模块和add_to_waitlist
函数可见。
- 用于控制模块和模块中项的可见性。如
mod front_of_house {mod hosting {fn add_to_waitlist() {}}
}
pub fn eat_at_restaurant() {// 绝对路径crate::front_of_house::hosting::add_to_waitlist();// 报错// 相对路径front_of_house::hosting::add_to_waitlist();
}
hosting 模块是私有的,无法在包根进行访问,那么为何 front_of_house 模块就可以访问?
答:因为它和 eat_at_restaurant 同属于一个包根作用域内,同一个模块内的代码自然不存在私有化问题。
修改后:
mod front_of_house {pub mod hosting {pub fn add_to_waitlist() {}}
}
/*--- snip ----*/
mod kitchen {pub fn serve() {println!("Serving from the kitchen!");}pub(in crate) fn prepare() {println!("Preparing in the kitchen!");}fn cook() {println!("Cooking in the kitchen!");}
}
fn main() {kitchen::serve(); // 正确:`serve` 是公有的kitchen::prepare(); // 正确:`prepare` 在当前 crate 内公有// kitchen::cook(); // 错误:`cook` 是私有的
}
(六)使用super
引用模块
super 代表的是父模块为开始的引用方式,非常类似于文件系统中的..
语法:../a/b
文件名:src/lib.rs
- 语法
- 以父模块为开始引用,类似文件系统
..
语法,如super::serve_order
可在子模块中调用父模块函数。
- 以父模块为开始引用,类似文件系统
(七)使用self
引用模块
- 语法
- 引用自身模块中的项,如
self::back_of_house::cook_order()
。
- 引用自身模块中的项,如
fn serve_order() {self::back_of_house::cook_order()
}
mod back_of_house {fn fix_incorrect_order() {cook_order();crate::serve_order();}pub fn cook_order() {}
}
(八)结构体和枚举的可见性
- 结构体
- 设置为
pub
时,所有字段依然私有。
- 设置为
- 枚举
- 设置为
pub
时,所有成员字段对外可见。
- 设置为
(九)模块与文件分离
- 分离过程
- 如将
front_of_house
模块分离到src/front_of_house.rs
文件,
- 如将
pub mod hosting {pub fn add_to_waitlist() {}
}
src/lib.rs
中保留mod front_of_house;
和pub use crate::front_of_house::hosting;
。
mod front_of_house;
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant(){hosting::add_to_waitlist();
}
三、使用 use 及受限可见性
(一)引入模块或函数简化调用
- 绝对路径引入模块
- 如
use crate::front_of_house::hosting;
,可简化hosting
模块中函数的调用,后续可使用hosting::add_to_waitlist
。
- 如
- 相对路径引入函数
- 如
use front_of_house::hosting::add_to_waitlist;
,可直接引入函数进一步简化调用。
- 如
(二)引入模块还是函数的选择
- 引入模块的情况
- 需要引入同一个模块的多个函数或作用域中存在同名函数时,引入模块更好,如引入
HashMap
结构体时直接引入模块use std::collections::HashMap;
。
- 需要引入同一个模块的多个函数或作用域中存在同名函数时,引入模块更好,如引入
- 建议的引用方式
- 优先使用最细粒度(引入函数、结构体等)的引用方式,有麻烦时再考虑引入模块。
四、处理同名引用
(一)使用模块区分
- 如
use std::fmt; use std::io;
,通过模块::Result
的方式调用具体的Result
来避免同名冲突。
use std::fmt;
use std::io;
fn function1() -> fmt::Result {// --snip--
}
fn function2() -> io::Result<()> {// --snip--
}
(二)as
别名引用
- 如
use std::fmt::Result;
、use std::io::Result as IoResult;
,使用as
关键字赋予引入项新名称解决冲突。
五、引入项再导出
- 语法
- 使用
pub use
,如pub use crate::front_of_house::hosting;
,可将引入的模块设置为对外可见,用于隐藏内部实现细节或组织代码。
- 使用
六、使用第三方包
- 引入步骤
- 修改
Cargo.toml
文件添加依赖,如rand = "0.8.3"
,然后使用use
引入包中的模块,如use rand::Rng;
。
- 修改
七、简化引入方式
(一)使用{}
简化
- 引入多个同模块项
- 如
use std::collections::{HashMap,BTreeMap,HashSet};
,可一起引入多个同模块下的项。
- 如
- 同时引入模块和项
- 如
use std::io::{self, Write};
,可简化同时引入模块和模块中的项。
- 如
(二)使用*
引入模块下所有项
- 示例
- 如
use std::collections::*;
,可引入模块下所有公共项,但要注意可能的名称冲突。
- 如
15、注释与文档总结
一、注释种类
- 代码注释
- 用于说明代码功能,供项目内协作开发者阅读。
- 有行注释
//
和块注释/*...*/
两种形式。行注释可放在代码行上方或后方,超出一行需在新行开头加//
;块注释用于注释内容较多时。
- 文档注释
- 支持Markdown,用于介绍项目描述、公共API等,供想了解项目的人阅读。
- 有文档行注释
///
和文档块注释/**...*/
两种形式。文档注释需位于lib
类型包中(如src/lib.rs
),被注释对象需pub
对外可见。
/// `add_one` 将指定值加1
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {x + 1
}/* `add_two` 将指定值加2
` ` `
let arg = 5;
let answer = my_crate::add_two(arg);assert_eq!(7, answer);
` ` `
*/
pub fn add_two(x: i32) -> i32 {x + 2
}
- 包和模块注释
- 用于说明包和模块功能,需添加在包、模块最上方。
- 有行注释
//!
和块注释/*!...*/
两种形式。
二、常用文档标题
Panics
- 描述函数可能出现的异常状况。
Errors
- 描述可能出现的错误及导致错误的情况。
Safety
- 若函数使用
unsafe
代码,说明使用条件。
- 若函数使用
三、查看文档
- 运行
cargo doc
可生成HTML文件并放入target/doc
目录下;运行cargo doc --open
可在生成文档后自动在浏览器中打开网页。
四、文档测试
- 测试用例编写
- 可在文档注释中写单元测试用例,如
add_one
函数注释中的示例代码可作为测试用例运行(cargo test
)。
- 可在文档注释中写单元测试用例,如
- 处理
panic
的测试用例- 若测试用例会
panic
,可添加should_panic
让测试通过。
- 若测试用例会
- 保留测试隐藏文档
- 可使用
#
隐藏不想让用户看到的内容,但不影响测试运行。
- 可使用
五、文档注释中的代码跳转
- 跳转到标准库
- 如
add_one
函数返回Option
类型,Option
可链接到标准库中的Option
枚举,可通过IDE快捷键(macOS:Command + 鼠标左键
;Windows:CTRL + 鼠标左键
)或在文档中直接点击链接跳转。
- 如
- 跳转到指定项
- 可通过完整路径跳转到自己代码或其他库的指定项,如
crate::MySpecialFormatter
可跳转到lib.rs
中定义的结构体。
- 可通过完整路径跳转到自己代码或其他库的指定项,如
- 同名项跳转
- 可使用标示类型的方式跳转,如
struct@Foo
、fn@Foo
、macro@foo!
。
- 可使用标示类型的方式跳转,如
六、文档搜索别名
- 可为自己的类型定义别名,如
#[doc(alias = "x")]
,当别名命中时搜索结果会放第一位。
七、综合例子
- 项目结构
- 包含二进制可执行包(
src/main.rs
)和lib
包(src/lib.rs
)。
- 包含二进制可执行包(
- 库包内容
- 在
src/lib.rs
中定义子模块kinds
和utils
,并通过pub use
再导出PrimaryColor
、SecondaryColor
和mix
。
- 在
- 二进制包内容
- 在
src/main.rs
中引入库包中的模块项并在main
函数中使用,通过cargo run
运行可实现调色并打印输出。
- 在
总结
如果你认真将里面的例子理解透彻,基本可以达到入门级别❤️