rust语言
2025-02-02 08:43:10 1 举报
AI智能生成
rust学习框架
作者其他创作
大纲/内容
注释和文档
注释的种类
代码注释、文档注释、包和模块注释
代码注释
用于说明某一块代码的功能,读者往往是同一个项目的协作开发者
行注释 //
块注释 /* ... */
文档注释
支持 Markdown,对项目描述、公共 API 等用户关心的功能进行介绍,同时还能提供示例代码,目标读者往往是想要了解你项目的人
文档行注释 ///
注:
1. 文档注释需要位于 lib 类型的包中,例如 src/lib.rs 中
2. 文档注释可以使用 markdown语法!例如 # Examples 的标题,以及代码块高亮
3. 被注释的对象需要使用 pub 对外可见,记住:文档注释是给用户看的,内部实现细节不应该被暴露出去
1. 文档注释需要位于 lib 类型的包中,例如 src/lib.rs 中
2. 文档注释可以使用 markdown语法!例如 # Examples 的标题,以及代码块高亮
3. 被注释的对象需要使用 pub 对外可见,记住:文档注释是给用户看的,内部实现细节不应该被暴露出去
文档块注释 /** ... */
查看文档cargo doc
使用 cargo doc --open 命令,可以在生成文档后,自动在浏览器中打开网页
常用文档标题
Panics:函数可能会出现的异常状况,这样调用函数的人就可以提前规避
Errors:描述可能出现的错误及什么情况会导致错误,有助于调用者针对不同的错误采取不同的处理方式
Safety:如果函数使用 unsafe 代码,那么调用者就需要注意一些使用条件,以确保 unsafe 代码块的正常工作
Errors:描述可能出现的错误及什么情况会导致错误,有助于调用者针对不同的错误采取不同的处理方式
Safety:如果函数使用 unsafe 代码,那么调用者就需要注意一些使用条件,以确保 unsafe 代码块的正常工作
包和模块注释
严格来说这也是文档注释中的一种,它主要用于说明当前包和模块的功能,方便用户迅速了解一个项目
这些注释要添加到包、模块的最上方!
这些注释要添加到包、模块的最上方!
行注释 //!
块注释 /*! ... */
文档测试
Rust 允许我们在文档注释中写单元测试用例!
使用 cargo test 运行测试
使用 cargo test 运行测试
/// `add_one` 将指定值加1
///
/// # Examples11
///
/// ```
/// let arg = 5;
/// let answer = world_hello::compute::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}
///
/// # Examples11
///
/// ```
/// let arg = 5;
/// let answer = world_hello::compute::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}
需要注意的是,你可能需要使用类如 world_hello::compute::add_one(arg) 的完整路径来调用函数,因为测试是在另外一个独立的线程中运行的
造成 panic 的文档测试
如果想要通过这种测试,可以添加 should_panic:
保留测试,隐藏文档
保留文档测试的功能,但是又要将某些测试用例的内容从文档中隐藏起来
使用 # 将不想让用户看到的内容隐藏起来,但是又不影响测试用例的运行,最终用户将只能看到那行没有隐藏的 let res = world_hello::compute::try_div(10, 0)?;
使用 # 将不想让用户看到的内容隐藏起来,但是又不影响测试用例的运行,最终用户将只能看到那行没有隐藏的 let res = world_hello::compute::try_div(10, 0)?;
/// ```
/// # // 使用#开头的行会在文档中被隐藏起来,但是依然会在文档测试中运行
/// # fn try_main() -> Result<(), String> {
/// let res = world_hello::compute::try_div(10, 0)?;
/// # Ok(()) // returning from try_main
/// # }
/// # fn main() {
/// # try_main().unwrap();
/// #
/// # }
/// ```
pub fn try_div(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("Divide-by-zero"))
} else {
Ok(a / b)
}
}
/// # // 使用#开头的行会在文档中被隐藏起来,但是依然会在文档测试中运行
/// # fn try_main() -> Result<(), String> {
/// let res = world_hello::compute::try_div(10, 0)?;
/// # Ok(()) // returning from try_main
/// # }
/// # fn main() {
/// # try_main().unwrap();
/// #
/// # }
/// ```
pub fn try_div(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("Divide-by-zero"))
} else {
Ok(a / b)
}
}
文档注释中的代码跳转
跳转到标准库
/// `add_one` 返回一个[`Option`]类型
pub fn add_one(x: i32) -> Option<i32> {
Some(x + 1)
}
pub fn add_one(x: i32) -> Option<i32> {
Some(x + 1)
}
此处的 [Option] 就是一个链接,指向了标准库中的 Option 枚举类型,有两种方式可以进行跳转:
在 IDE 中,使用 Command + 鼠标左键(macOS),CTRL + 鼠标左键(Windows)
在文档中直接点击链接
在 IDE 中,使用 Command + 鼠标左键(macOS),CTRL + 鼠标左键(Windows)
在文档中直接点击链接
路径的方式跳转
use std::sync::mpsc::Receiver;
/// [`Receiver<T>`] [`std::future`].
///
/// [`std::future::Future`] [`Self::recv()`].
pub struct AsyncReceiver<T> {
sender: Receiver<T>,
}
impl<T> AsyncReceiver<T> {
pub async fn recv() -> T {
unimplemented!()
}
}
/// [`Receiver<T>`] [`std::future`].
///
/// [`std::future::Future`] [`Self::recv()`].
pub struct AsyncReceiver<T> {
sender: Receiver<T>,
}
impl<T> AsyncReceiver<T> {
pub async fn recv() -> T {
unimplemented!()
}
}
使用完整路径跳转到指定项
指定具体的路径跳转到自己代码或者其它库的指定项
pub mod a {
/// `add_one` 返回一个[`Option`]类型
/// 跳转到[`crate::MySpecialFormatter`]
pub fn add_one(x: i32) -> Option<i32> {
Some(x + 1)
}
}
pub struct MySpecialFormatter;
/// `add_one` 返回一个[`Option`]类型
/// 跳转到[`crate::MySpecialFormatter`]
pub fn add_one(x: i32) -> Option<i32> {
Some(x + 1)
}
}
pub struct MySpecialFormatter;
同名项的跳转
/// 跳转到结构体 [`Foo`](struct@Foo)
pub struct Bar;
/// 跳转到同名函数 [`Foo`](fn@Foo)
pub struct Foo {}
/// 跳转到同名宏 [`foo!`]
pub fn Foo() {}
#[macro_export]
macro_rules! foo {
() => {}
}
pub struct Bar;
/// 跳转到同名函数 [`Foo`](fn@Foo)
pub struct Foo {}
/// 跳转到同名宏 [`foo!`]
pub fn Foo() {}
#[macro_export]
macro_rules! foo {
() => {}
}
文档搜索别名
为自己的类型定义几个别名,以实现更好的搜索展现
#[doc(alias = "x")]
#[doc(alias = "big")]
pub struct BigX;
#[doc(alias("y", "big"))]
pub struct BigY;
#[doc(alias = "big")]
pub struct BigX;
#[doc(alias("y", "big"))]
pub struct BigY;
Rust集合与字符串
向量Vec<T>
--相当于List
--相当于List
向量(Vector)是一个存放多值的单数据结构,该结构将相同类型的值线性的存放在内存中。
向量是线性表,在 Rust 中的表示是 Vec<T>。
let vector: Vec<i32> = Vec::new(); // 创建类型为 i32 的空向量
let vector = vec![1, 2, 4, 8]; // 通过数组创建向量
let vector = vec![1, 2, 4, 8]; // 通过数组创建向量
fn main() {
let mut vector = vec![1, 2, 4, 8];
vector.push(16);
vector.push(32);
vector.push(64);
println!("{:?}", vector);
}
let mut vector = vec![1, 2, 4, 8];
vector.push(16);
vector.push(32);
vector.push(64);
println!("{:?}", vector);
}
//append 方法用于将一个向量拼接到另一个向量的尾部:
fn main() {
let mut v1: Vec<i32> = vec![1, 2, 4, 8];
let mut v2: Vec<i32> = vec![16, 32, 64];
v1.append(&mut v2);
println!("{:?}", v1);
}
fn main() {
let mut v1: Vec<i32> = vec![1, 2, 4, 8];
let mut v2: Vec<i32> = vec![16, 32, 64];
v1.append(&mut v2);
println!("{:?}", v1);
}
//get 方法用于取出向量中的值
fn main() {
let mut v = vec![1, 2, 4, 8];
println!("{}", match v.get(0) {
Some(value) => value.to_string(),
None => "None".to_string()
});
}
//因为向量的长度无法从逻辑上推断,get 方法无法保证一定取到值,所以 get 方法的返回值是 Option 枚举类,有可能为空。
//保证取值的下标不会超出向量下标取值范围,你也可以使用数组取值语法:
fn main() {
let v = vec![1, 2, 4, 8];
println!("{}", v[1]);
}
fn main() {
let mut v = vec![1, 2, 4, 8];
println!("{}", match v.get(0) {
Some(value) => value.to_string(),
None => "None".to_string()
});
}
//因为向量的长度无法从逻辑上推断,get 方法无法保证一定取到值,所以 get 方法的返回值是 Option 枚举类,有可能为空。
//保证取值的下标不会超出向量下标取值范围,你也可以使用数组取值语法:
fn main() {
let v = vec![1, 2, 4, 8];
println!("{}", v[1]);
}
动态数组Vector
创建动态数组
let v: Vec<i32> = Vec::new();
如果预先知道要存储的元素个数,可以使用 Vec::with_capacity(capacity) 创建动态数组,这样可以避免因为插入大量新数据导致频繁的内存分配和拷贝,提升性能
let v = vec![1, 2, 3];
使用宏 vec! 来创建数组
更新Vector
向数组尾部添加元素
let mut v = Vec::new();
v.push(1);
v.push(1);
Vector与其元素共存亡
跟结构体一样,Vector 类型在超出作用域范围后,会被自动删除
当 Vector 被删除后,它内部存储的所有内容也会随之被删除。目前来看,这种解决方案简单直白,但是当 Vector 中的元素被引用后,事情可能会没那么简单。
当 Vector 被删除后,它内部存储的所有内容也会随之被删除。目前来看,这种解决方案简单直白,但是当 Vector 中的元素被引用后,事情可能会没那么简单。
{
let v = vec![1, 2, 3];
// ...
} // <- v超出作用域并在此处被删除
let v = vec![1, 2, 3];
// ...
} // <- v超出作用域并在此处被删除
从Vector中读取元素
通过下标索引访问。
使用 get 方法。
使用 get 方法。
//两种取值方式
//1. &v[2]借用第三个元素
//2. v.get(2) 访问第三个元素,返回Option<&T>
let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];
println!("第三个元素是 {}", third);
match v.get(2) {
Some(third) => println!("第三个元素是 {third}"),
None => println!("去你的第三个元素,根本没有!"),
}
//1. &v[2]借用第三个元素
//2. v.get(2) 访问第三个元素,返回Option<&T>
let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];
println!("第三个元素是 {}", third);
match v.get(2) {
Some(third) => println!("第三个元素是 {third}"),
None => println!("去你的第三个元素,根本没有!"),
}
集合类型的索引下标都是从 0 开始,&v[2] 表示借用 v 中的第三个元素,最终会获得该元素的引用。而 v.get(2) 也是访问第三个元素,但是有所不同的是,它返回了 Option<&T>,因此还需要额外的 match 来匹配解构出具体的值。
下标索引与.get的区别
数组越界问题
两种访问方式,各有优缺点
两种访问方式,各有优缺点
let v = vec![1, 2, 3, 4, 5];
let does_not_exist = &v[100];
let does_not_exist = v.get(100);
let does_not_exist = &v[100];
let does_not_exist = v.get(100);
同时借用多个数组元素
不可变借用和可变借用生命周期不能有交集
注:数组的大小是可变的,当旧数组的大小不够用时,Rust 会重新分配一块更大的内存空间,然后把旧数组拷贝过来。这种情况下,之前的引用显然会指向一块无效的内存,这非常 rusty —— 对用户进行严格的教育。
注:数组的大小是可变的,当旧数组的大小不够用时,Rust 会重新分配一块更大的内存空间,然后把旧数组拷贝过来。这种情况下,之前的引用显然会指向一块无效的内存,这非常 rusty —— 对用户进行严格的教育。
//不可变借用与可变借用不能有相同的生命周期
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0]; //不可变借用
v.push(6); //可变借用
println!("The first element is: {first}"); //在这里不能使用不可变引用first,因为和可变引用相交生命周期了
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0]; //不可变借用
v.push(6); //可变借用
println!("The first element is: {first}"); //在这里不能使用不可变引用first,因为和可变引用相交生命周期了
迭代遍历Vector中的元素
使用迭代遍历数组
注:比用下标方式更高效(下标访问会触发数组边界检查)
注:比用下标方式更高效(下标访问会触发数组边界检查)
//访问
let v = vec![1, 2, 3];
for i in &v {
println!("{i}");
}
let v = vec![1, 2, 3];
for i in &v {
println!("{i}");
}
//修改
let mut v = vec![1, 2, 3];
for i in &mut v {
*i += 10
}
let mut v = vec![1, 2, 3];
for i in &mut v {
*i += 10
}
存储不同类型的元素
通过使用枚举类型、特征对象来实现不同类型元素的存储
枚举
#[derive(Debug)]
enum IpAddr {
V4(String),
V6(String)
}
fn main() {
let v = vec![
IpAddr::V4("127.0.0.1".to_string()),
IpAddr::V6("::1".to_string())
];
for ip in v {
show_addr(ip)
}
}
fn show_addr(ip: IpAddr) {
println!("{:?}",ip);
}
enum IpAddr {
V4(String),
V6(String)
}
fn main() {
let v = vec![
IpAddr::V4("127.0.0.1".to_string()),
IpAddr::V6("::1".to_string())
];
for ip in v {
show_addr(ip)
}
}
fn show_addr(ip: IpAddr) {
println!("{:?}",ip);
}
特征对象
trait IpAddr {
fn display(&self);
}
struct V4(String);
impl IpAddr for V4 {
fn display(&self) {
println!("ipv4: {:?}",self.0)
}
}
struct V6(String);
impl IpAddr for V6 {
fn display(&self) {
println!("ipv6: {:?}",self.0)
}
}
fn main() {
let v: Vec<Box<dyn IpAddr>> = vec![
Box::new(V4("127.0.0.1".to_string())),
Box::new(V6("::1".to_string())),
];
for ip in v {
ip.display();
}
}
fn display(&self);
}
struct V4(String);
impl IpAddr for V4 {
fn display(&self) {
println!("ipv4: {:?}",self.0)
}
}
struct V6(String);
impl IpAddr for V6 {
fn display(&self) {
println!("ipv6: {:?}",self.0)
}
}
fn main() {
let v: Vec<Box<dyn IpAddr>> = vec![
Box::new(V4("127.0.0.1".to_string())),
Box::new(V6("::1".to_string())),
];
for ip in v {
ip.display();
}
}
Vector常用方法
扩容
动态数组意味着我们增加元素时,如果容量不足就会导致 vector 扩容(目前的策略是重新申请一块 2 倍大小的内存,再将所有元素拷贝到新的内存位置,同时更新指针数据)
fn main() {
let v = vec![0; 3]; // 默认值为 0,初始长度为 3
let v_from = Vec::from([0, 0, 0]);
assert_eq!(v, v_from);
}
let v = vec![0; 3]; // 默认值为 0,初始长度为 3
let v_from = Vec::from([0, 0, 0]);
assert_eq!(v, v_from);
}
初始容量
fn main() {
let mut v = Vec::with_capacity(10);
v.extend([1, 2, 3]); // 附加数据到 v
println!("Vector 长度是: {}, 容量是: {}", v.len(), v.capacity());
v.reserve(100); // 调整 v 的容量,至少要有 100 的容量
println!("Vector(reserve) 长度是: {}, 容量是: {}", v.len(), v.capacity());
v.shrink_to_fit(); // 释放剩余的容量,一般情况下,不会主动去释放容量
println!("Vector(shrink_to_fit) 长度是: {}, 容量是: {}", v.len(), v.capacity());
}
let mut v = Vec::with_capacity(10);
v.extend([1, 2, 3]); // 附加数据到 v
println!("Vector 长度是: {}, 容量是: {}", v.len(), v.capacity());
v.reserve(100); // 调整 v 的容量,至少要有 100 的容量
println!("Vector(reserve) 长度是: {}, 容量是: {}", v.len(), v.capacity());
v.shrink_to_fit(); // 释放剩余的容量,一般情况下,不会主动去释放容量
println!("Vector(shrink_to_fit) 长度是: {}, 容量是: {}", v.len(), v.capacity());
}
增删改查
let mut v = vec![1, 2];
assert!(!v.is_empty()); // 检查 v 是否为空
v.insert(2, 3); // 在指定索引插入数据,索引值不能大于 v 的长度, v: [1, 2, 3]
assert_eq!(v.remove(1), 2); // 移除指定位置的元素并返回, v: [1, 3]
assert_eq!(v.pop(), Some(3)); // 删除并返回 v 尾部的元素,v: [1]
assert_eq!(v.pop(), Some(1)); // v: []
assert_eq!(v.pop(), None); // 记得 pop 方法返回的是 Option 枚举值
v.clear(); // 清空 v, v: []
let mut v1 = [11, 22].to_vec(); // append 操作会导致 v1 清空数据,增加可变声明
v.append(&mut v1); // 将 v1 中的所有元素附加到 v 中, v1: []
v.truncate(1); // 截断到指定长度,多余的元素被删除, v: [11]
v.retain(|x| *x > 10); // 保留满足条件的元素,即删除不满足条件的元素
let mut v = vec![11, 22, 33, 44, 55];
// 删除指定范围的元素,同时获取被删除元素的迭代器, v: [11, 55], m: [22, 33, 44]
let mut m: Vec<_> = v.drain(1..=3).collect();
let v2 = m.split_off(1); // 指定索引处切分成两个 vec, m: [22], v2: [33, 44]
assert!(!v.is_empty()); // 检查 v 是否为空
v.insert(2, 3); // 在指定索引插入数据,索引值不能大于 v 的长度, v: [1, 2, 3]
assert_eq!(v.remove(1), 2); // 移除指定位置的元素并返回, v: [1, 3]
assert_eq!(v.pop(), Some(3)); // 删除并返回 v 尾部的元素,v: [1]
assert_eq!(v.pop(), Some(1)); // v: []
assert_eq!(v.pop(), None); // 记得 pop 方法返回的是 Option 枚举值
v.clear(); // 清空 v, v: []
let mut v1 = [11, 22].to_vec(); // append 操作会导致 v1 清空数据,增加可变声明
v.append(&mut v1); // 将 v1 中的所有元素附加到 v 中, v1: []
v.truncate(1); // 截断到指定长度,多余的元素被删除, v: [11]
v.retain(|x| *x > 10); // 保留满足条件的元素,即删除不满足条件的元素
let mut v = vec![11, 22, 33, 44, 55];
// 删除指定范围的元素,同时获取被删除元素的迭代器, v: [11, 55], m: [22, 33, 44]
let mut m: Vec<_> = v.drain(1..=3).collect();
let v2 = m.split_off(1); // 指定索引处切分成两个 vec, m: [22], v2: [33, 44]
切片
fn main() {
let v = vec![11, 22, 33, 44, 55];
let slice = &v[1..=3];
assert_eq!(slice, &[22, 33, 44]);
}
let v = vec![11, 22, 33, 44, 55];
let slice = &v[1..=3];
assert_eq!(slice, &[22, 33, 44]);
}
fn main() {
// 这里 Vec<T> 在调用 iter() 时被解引用成 &[T],所以可以访问 iter()
let result = vec![1, 2, 3, 4]
.iter()
.map(|v| v * v)
.filter(|v| *v < 16)
.take(1)
.collect::<Vec<_>>(); //到 collect 时才真正开始执行
println!("{:?}", result);
}
// 这里 Vec<T> 在调用 iter() 时被解引用成 &[T],所以可以访问 iter()
let result = vec![1, 2, 3, 4]
.iter()
.map(|v| v * v)
.filter(|v| *v < 16)
.take(1)
.collect::<Vec<_>>(); //到 collect 时才真正开始执行
println!("{:?}", result);
}
Vector的排序
整数数组排序
fn main() {
let mut vec = vec![1, 5, 10, 2, 15];
vec.sort_unstable();
assert_eq!(vec, vec![1, 2, 5, 10, 15]);
}
let mut vec = vec![1, 5, 10, 2, 15];
vec.sort_unstable();
assert_eq!(vec, vec![1, 2, 5, 10, 15]);
}
浮点数组排序
在浮点数当中,存在一个 NAN 的值,这个值无法与其他的浮点数进行对比,因此,浮点数类型并没有实现全数值可比较 Ord 的特性,而是实现了部分可比较的特性 PartialOrd
//会产生异常的代码
fn main() {
let mut vec = vec![1.0, 5.6, 10.3, 2.0, 15f32];
vec.sort_unstable();
assert_eq!(vec, vec![1.0, 2.0, 5.6, 10.3, 15f32]);
}
fn main() {
let mut vec = vec![1.0, 5.6, 10.3, 2.0, 15f32];
vec.sort_unstable();
assert_eq!(vec, vec![1.0, 2.0, 5.6, 10.3, 15f32]);
}
//解决上面的异常问题
fn main() {
let mut vec = vec![1.0, 5.6, 10.3, 2.0, 15f32];
vec.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
assert_eq!(vec, vec![1.0, 2.0, 5.6, 10.3, 15f32]);
}
fn main() {
let mut vec = vec![1.0, 5.6, 10.3, 2.0, 15f32];
vec.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
assert_eq!(vec, vec![1.0, 2.0, 5.6, 10.3, 15f32]);
}
对结构体数组进行排序
#[derive(Debug)]
struct Person {
name: String,
age: u32,
}
impl Person {
fn new(name: String, age: u32) -> Person {
Person { name, age }
}
}
fn main() {
let mut people = vec![
Person::new("Zoe".to_string(), 25),
Person::new("Al".to_string(), 60),
Person::new("John".to_string(), 1),
];
// 定义一个按照年龄倒序排序的对比函数
people.sort_unstable_by(|a, b| b.age.cmp(&a.age));
println!("{:?}", people);
}
struct Person {
name: String,
age: u32,
}
impl Person {
fn new(name: String, age: u32) -> Person {
Person { name, age }
}
}
fn main() {
let mut people = vec![
Person::new("Zoe".to_string(), 25),
Person::new("Al".to_string(), 60),
Person::new("John".to_string(), 1),
];
// 定义一个按照年龄倒序排序的对比函数
people.sort_unstable_by(|a, b| b.age.cmp(&a.age));
println!("{:?}", people);
}
不使用自定义对比函数
//实现 Ord 需要我们实现 Ord、Eq、PartialEq、PartialOrd 这些属性
#[derive(Debug, Ord, Eq, PartialEq, PartialOrd)]
struct Person {
name: String,
age: u32,
}
impl Person {
fn new(name: String, age: u32) -> Person {
Person { name, age }
}
}
fn main() {
let mut people = vec![
Person::new("Zoe".to_string(), 25),
Person::new("Al".to_string(), 60),
Person::new("Al".to_string(), 30),
Person::new("John".to_string(), 1),
Person::new("John".to_string(), 25),
];
people.sort_unstable();
println!("{:?}", people);
}
#[derive(Debug, Ord, Eq, PartialEq, PartialOrd)]
struct Person {
name: String,
age: u32,
}
impl Person {
fn new(name: String, age: u32) -> Person {
Person { name, age }
}
}
fn main() {
let mut people = vec![
Person::new("Zoe".to_string(), 25),
Person::new("Al".to_string(), 60),
Person::new("Al".to_string(), 30),
Person::new("John".to_string(), 1),
Person::new("John".to_string(), 25),
];
people.sort_unstable();
println!("{:?}", people);
}
字符串:统一utf-8编码
let one = 1.to_string(); // 整数到字符串
let float = 1.3.to_string(); // 浮点数到字符串
let slice = "slice".to_string(); // 字符串切片到字符串
let float = 1.3.to_string(); // 浮点数到字符串
let slice = "slice".to_string(); // 字符串切片到字符串
let mut s = String::from("run");
s.push_str("oob"); // 追加字符串切片
s.push('!'); // 追加字符
s.push_str("oob"); // 追加字符串切片
s.push('!'); // 追加字符
用+号拼接字符串
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2;
let s2 = String::from("world!");
let s3 = s1 + &s2;
包含字符串切片
Rust 中的字符串类型实质上记录了字符在内存中的起始位置和其长度
注:
1.字符串字面量是切片,例:let s: &str = "Hello, world!";
注:
1.字符串字面量是切片,例:let s: &str = "Hello, world!";
fn main() {
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
println!("{}={}+{}", s, part1, part2);
}
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
println!("{}={}+{}", s, part1, part2);
}
let world = &s[6..11]; 来说,world 是一个切片,该切片的指针指向 s 的第 7 个字节(索引从 0 开始, 6 是第 7 个字节),且该切片的长度是 5 个字节
使用 .. 表示范围的语法
..y 等价于 0..y
x.. 等价于位置 x 到数据结束
.. 等价于位置 0 到结束
..y 等价于 0..y
x.. 等价于位置 x 到数据结束
.. 等价于位置 0 到结束
统计
let s = "hello你好";
let len = s.chars().count();
let len = s.chars().count();
遍历字符串
fn main() {
let s = String::from("hello中文");
for c in s.chars() {
println!("{}", c);
}
}
let s = String::from("hello中文");
for c in s.chars() {
println!("{}", c);
}
}
从字符串中取单个字符
//注意:nth 函数是从迭代器中取出某值的方法,请不要在遍历中这样使用!因为 UTF-8 每个字符的长度不一定相等!
fn main() {
let s = String::from("EN中文");
let a = s.chars().nth(2);
println!("{:?}", a);
}
fn main() {
let s = String::from("EN中文");
let a = s.chars().nth(2);
println!("{:?}", a);
}
截取字符串字串
fn main() {
let s = String::from("EN中文");
let sub = &s[0..2];
println!("{}", sub);
}
let s = String::from("EN中文");
let sub = &s[0..2];
println!("{}", sub);
}
操作字符串方法
Push
在字符串尾部可以使用 push() 方法追加字符 char,也可以使用 push_str() 方法追加字符串字面量。这两个方法都是在原有的字符串上追加,并不会返回新的字符串。由于字符串追加操作要修改原来的字符串,则该字符串必须是可变的,即字符串变量必须由 mut 关键字修饰。
fn main() {
let mut s = String::from("Hello ");
s.push_str("rust");
println!("追加字符串 push_str() -> {}", s);
s.push('!');
println!("追加字符 push() -> {}", s);
}
let mut s = String::from("Hello ");
s.push_str("rust");
println!("追加字符串 push_str() -> {}", s);
s.push('!');
println!("追加字符 push() -> {}", s);
}
Insert
可以使用 insert() 方法插入单个字符 char,也可以使用 insert_str() 方法插入字符串字面量,与 push() 方法不同,这俩方法需要传入两个参数,第一个参数是字符(串)插入位置的索引,第二个参数是要插入的字符(串),索引从 0 开始计数,如果越界则会发生错误。由于字符串插入操作要修改原来的字符串,则该字符串必须是可变的,即字符串变量必须由 mut 关键字修饰。注意中文字符或者双字节数据,不能插入在中文字符的3个字符之间,需要按照UTF8的格式去解析特殊字符的个数
fn main() {
let mut s = String::from("Hello rust!");
s.insert(5, ',');
println!("插入字符 insert() -> {}", s);
s.insert_str(6, " I like");
println!("插入字符串 insert_str() -> {}", s);
}
let mut s = String::from("Hello rust!");
s.insert(5, ',');
println!("插入字符 insert() -> {}", s);
s.insert_str(6, " I like");
println!("插入字符串 insert_str() -> {}", s);
}
let byte = bytes[byte_count];
//根据数据位判断特殊字符字节数
if (byte >> 7) == 0 { // 最高位为0 表示一个字节
} else {
if (byte >> 5) == 0b110 { // 2字节
} else if (byte >> 4) == 0b1110 { // 三字节
} else if (byte >> 3) == 0b1110 { // 四字节
} else {
}
}
//根据数据位判断特殊字符字节数
if (byte >> 7) == 0 { // 最高位为0 表示一个字节
} else {
if (byte >> 5) == 0b110 { // 2字节
} else if (byte >> 4) == 0b1110 { // 三字节
} else if (byte >> 3) == 0b1110 { // 四字节
} else {
}
}
Replace
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);
}
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);
}
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);
}
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);
}
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);
}
let mut string_replace_range = String::from("I like rust!");
string_replace_range.replace_range(7..8, "R");
dbg!(string_replace_range);
}
Delete
pop
删除并返回字符串的最后一个字符
该方法是直接操作原来的字符串。但是存在返回值,其返回值是一个 Option 类型,如果字符串为空,则返回 None。
该方法是直接操作原来的字符串。但是存在返回值,其返回值是一个 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);
}
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);
}
remove
删除并返回字符串中指定位置的字符
该方法是直接操作原来的字符串。但是存在返回值,其返回值是删除位置的字符串,只接收一个参数,表示该字符起始索引位置。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);
}
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);
}
truncate
删除字符串中从指定位置开始到结尾的全部字符
该方法是直接操作原来的字符串。无返回值。该方法 truncate() 方法是按照字节来处理字符串的,如果参数所给的位置不是合法的字符边界,则会发生错误。
该方法是直接操作原来的字符串。无返回值。该方法 truncate() 方法是按照字节来处理字符串的,如果参数所给的位置不是合法的字符边界,则会发生错误。
fn main() {
let mut string_truncate = String::from("测试truncate");
string_truncate.truncate(3);
dbg!(string_truncate);
}
let mut string_truncate = String::from("测试truncate");
string_truncate.truncate(3);
dbg!(string_truncate);
}
clear
清空字符串
Concatenate
使用 + 或者 += 连接字符串
使用 + 或者 += 连接字符串,要求右边的参数必须为字符串的切片引用(Slice)类型。其实当调用 + 的操作符时,相当于调用了 std::string 标准库中的 add() 方法,这里 add() 方法的第二个参数是一个引用的类型。因此我们在使用 +, 必须传递切片引用类型。不能直接传递 String 类型。
+ 是返回一个新的字符串,所以变量声明可以不需要 mut 关键字修饰。
+ 是返回一个新的字符串,所以变量声明可以不需要 mut 关键字修饰。
fn main() {
let string_append = String::from("hello ");
let string_rust = String::from("rust");
// &string_rust会自动解引用为&str
let result = string_append + &string_rust;
let mut result = result + "!"; // `result + "!"` 中的 `result` 是不可变的
result += "!!!";
let sss = "你好" + "小虎"; //错误,"你好"是 &str类型,没有add方法
println!("连接字符串 + -> {}", result);
}
let string_append = String::from("hello ");
let string_rust = String::from("rust");
// &string_rust会自动解引用为&str
let result = string_append + &string_rust;
let mut result = result + "!"; // `result + "!"` 中的 `result` 是不可变的
result += "!!!";
let sss = "你好" + "小虎"; //错误,"你好"是 &str类型,没有add方法
println!("连接字符串 + -> {}", result);
}
fn add(self, s: &str) -> String
//self 是 String 类型的字符串 s1,该函数说明,只能将 &str 类型的字符串切片添加到 String 类型的 s1 上,然后返回一个新的 String 类型,所以 let s3 = s1 + &s2; 就很好解释了,将 String 类型的 s1 与 &str 类型的 s2 进行相加,最终得到 String 类型的 s3
fn main() {
let s1 = String::from("hello,");
let s2 = String::from("world!");
// 在下句中,s1的所有权被转移走了,因此后面不能再使用s1
let s3 = s1 + &s2;
assert_eq!(s3,"hello,world!");
// 下面的语句如果去掉注释,就会报错
// println!("{}",s1);
}
//s1 这个变量通过调用 add() 方法后,所有权被转移到 add() 方法里面, add() 方法调用后就被释放了,同时 s1 也被释放了。再使用 s1 就会发生错误。这里涉及到所有权转移(Move)的相关知识。
fn main() {
let s1 = String::from("hello,");
let s2 = String::from("world!");
// 在下句中,s1的所有权被转移走了,因此后面不能再使用s1
let s3 = s1 + &s2;
assert_eq!(s3,"hello,world!");
// 下面的语句如果去掉注释,就会报错
// println!("{}",s1);
}
//s1 这个变量通过调用 add() 方法后,所有权被转移到 add() 方法里面, add() 方法调用后就被释放了,同时 s1 也被释放了。再使用 s1 就会发生错误。这里涉及到所有权转移(Move)的相关知识。
使用 format! 连接字符串
//format! 这种方式适用于 String 和 &str 。format! 的用法与 print! 的用法类似,详见格式化输出。
fn main() {
let s1 = "hello";
let s2 = String::from("rust");
let s = format!("{} {}!", s1, s2);
println!("{}", s);
}
fn main() {
let s1 = "hello";
let s2 = String::from("rust");
let s = format!("{} {}!", s1, s2);
println!("{}", s);
}
String与&str
区别
&str(不可变)
str 是 Rust 核心语言类型,就是本章一直在讲的字符串切片(String Slice),常常以引用的形式出现(&str)。
凡是用双引号包括的字符串常量整体的类型性质都是 &str:
let s = "hello";//s 就是一个 &str 类型的变量。
凡是用双引号包括的字符串常量整体的类型性质都是 &str:
let s = "hello";//s 就是一个 &str 类型的变量。
String(可变)
Rust 标准公共库提供的一种数据类型,它的功能更完善——它支持字符串的追加、清空等实用的操作
字符串索引
字符串底层的数据存储格式是[u8],一个字节数组
let hello = String::from("Hola")
不允许索引字符串
字符串的不同表现形式
1.英文占1个字节、中文占3个字节、其他的也可能占不同字节
2.程序可以挑选自己想要的方式去使用,无需去管字符串从人类语言角度看长什么样
3.如果用索引,可能需要从0开始去遍历字符中来定位合法的字符
1.英文占1个字节、中文占3个字节、其他的也可能占不同字节
2.程序可以挑选自己想要的方式去使用,无需去管字符串从人类语言角度看长什么样
3.如果用索引,可能需要从0开始去遍历字符中来定位合法的字符
字符串转义
我们可以通过转义的方式 \ 输出 ASCII 和 Unicode 字符。
fn main() {
// 通过 \ + 字符的十六进制表示,转义输出一个字符
let byte_escape = "I'm writing \x52\x75\x73\x74!";
println!("What are you doing\x3F (\\x3F means ?) {}", byte_escape);
// \u 可以输出一个 unicode 字符
let unicode_codepoint = "\u{211D}";
let character_name = "\"DOUBLE-STRUCK CAPITAL R\"";
println!(
"Unicode character {} (U+211D) is called {}",
unicode_codepoint, character_name
);
// 换行了也会保持之前的字符串格式
// 使用\忽略换行符
let long_string = "String literals
can span multiple lines.
The linebreak and indentation here ->\
<- can be escaped too!";
println!("{}", long_string);
}
// 通过 \ + 字符的十六进制表示,转义输出一个字符
let byte_escape = "I'm writing \x52\x75\x73\x74!";
println!("What are you doing\x3F (\\x3F means ?) {}", byte_escape);
// \u 可以输出一个 unicode 字符
let unicode_codepoint = "\u{211D}";
let character_name = "\"DOUBLE-STRUCK CAPITAL R\"";
println!(
"Unicode character {} (U+211D) is called {}",
unicode_codepoint, character_name
);
// 换行了也会保持之前的字符串格式
// 使用\忽略换行符
let long_string = "String literals
can span multiple lines.
The linebreak and indentation here ->\
<- can be escaped too!";
println!("{}", long_string);
}
fn main() {
println!("{}", "hello \\x52\\x75\\x73\\x74");
let raw_str = r"Escapes don't work here: \x3F \u{211D}";
println!("{}", raw_str);
// 如果字符串包含双引号,可以在开头和结尾加 #
let quotes = r#"And then I said: "There is no escape!""#;
println!("{}", quotes);
// 如果还是有歧义,可以继续增加,没有限制
let longer_delimiter = r###"A string with "# in it. And even "##!"###;
println!("{}", longer_delimiter);
}
println!("{}", "hello \\x52\\x75\\x73\\x74");
let raw_str = r"Escapes don't work here: \x3F \u{211D}";
println!("{}", raw_str);
// 如果字符串包含双引号,可以在开头和结尾加 #
let quotes = r#"And then I said: "There is no escape!""#;
println!("{}", quotes);
// 如果还是有歧义,可以继续增加,没有限制
let longer_delimiter = r###"A string with "# in it. And even "##!"###;
println!("{}", longer_delimiter);
}
字符串深度剖析
str
就字符串字面值来说,我们在编译时就知道其内容,最终字面值文本被直接硬编码进可执行文件中,这使得字符串字面值快速且高效,这主要得益于字符串字面值的不可变性
String
对于 String 类型,为了支持一个可变、可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容,这些都是在程序运行时完成的
1. 首先向操作系统请求内存来存放 String 对象
2. 在使用完成后,将内存释放,归还给操作系统
1. 首先向操作系统请求内存来存放 String 对象
2. 在使用完成后,将内存释放,归还给操作系统
释放内存
变量在离开作用域后,就自动释放其占用的内存
{
let s = String::from("hello"); // 从此处起,s 是有效的
// 使用 s
} // 此作用域已结束,
// s 不再有效,内存被释放
let s = String::from("hello"); // 从此处起,s 是有效的
// 使用 s
} // 此作用域已结束,
// s 不再有效,内存被释放
映射表(Map)
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert("color", "red");
map.insert("size", "10 m^2");
println!("{}", map.get("color").unwrap());
}
fn main() {
let mut map = HashMap::new();
map.insert("color", "red");
map.insert("size", "10 m^2");
println!("{}", map.get("color").unwrap());
}
迭代器
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert("color", "red");
map.insert("size", "10 m^2");
for p in map.iter() {
println!("{:?}", p);
}
}
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert("color", "red");
map.insert("size", "10 m^2");
for p in map.iter() {
println!("{:?}", p);
}
}
//使用 insert 方法添加新的键值对
//如果没有键为 "color" 的键值对就添加它并设定值为 "red",否则将跳过。
map.entry("color").or_insert("red");
//直接修改对应的值
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert(1, "a");
if let Some(x) = map.get_mut(&1) {
*x = "b";
}
}
//如果没有键为 "color" 的键值对就添加它并设定值为 "red",否则将跳过。
map.entry("color").or_insert("red");
//直接修改对应的值
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert(1, "a");
if let Some(x) = map.get_mut(&1) {
*x = "b";
}
}
创建HashMap
使用 new 方法创建
所有的集合类型都是动态的,意味着它们没有固定的内存大小,因此它们底层的数据都存储在内存堆上,然后通过一个存储在栈中的引用类型来访问
使用 HashMap::with_capacity(capacity) 创建指定大小的 HashMap,避免频繁的内存分配和拷贝,提升性能
使用 HashMap::with_capacity(capacity) 创建指定大小的 HashMap,避免频繁的内存分配和拷贝,提升性能
use std::collections::HashMap;
// 创建一个HashMap,用于存储宝石种类和对应的数量
let mut my_gems = HashMap::new();
// 将宝石类型和对应的数量写入表中
my_gems.insert("红宝石", 1);
my_gems.insert("蓝宝石", 2);
my_gems.insert("河边捡的误以为是宝石的破石头", 18);
// 创建一个HashMap,用于存储宝石种类和对应的数量
let mut my_gems = HashMap::new();
// 将宝石类型和对应的数量写入表中
my_gems.insert("红宝石", 1);
my_gems.insert("蓝宝石", 2);
my_gems.insert("河边捡的误以为是宝石的破石头", 18);
使用迭代器和collect方法创建
普通
fn main() {
use std::collections::HashMap;
let teams_list = vec![
("中国队".to_string(), 100),
("美国队".to_string(), 10),
("日本队".to_string(), 50),
];
let mut teams_map = HashMap::new();
for team in &teams_list {
teams_map.insert(&team.0, team.1);
}
println!("{:?}",teams_map)
}
use std::collections::HashMap;
let teams_list = vec![
("中国队".to_string(), 100),
("美国队".to_string(), 10),
("日本队".to_string(), 50),
];
let mut teams_map = HashMap::new();
for team in &teams_list {
teams_map.insert(&team.0, team.1);
}
println!("{:?}",teams_map)
}
通过迭代器转换成map
fn main() {
use std::collections::HashMap;
let teams_list = vec![
("中国队".to_string(), 100),
("美国队".to_string(), 10),
("日本队".to_string(), 50),
];
let teams_map: HashMap<_,_> = teams_list.into_iter().collect();
println!("{:?}",teams_map)
}
use std::collections::HashMap;
let teams_list = vec![
("中国队".to_string(), 100),
("美国队".to_string(), 10),
("日本队".to_string(), 50),
];
let teams_map: HashMap<_,_> = teams_list.into_iter().collect();
println!("{:?}",teams_map)
}
所有权转移
HashMap 的所有权规则与其它 Rust 类型没有区别:
若类型实现 Copy 特征,该类型会被复制进 HashMap,因此无所谓所有权
若没实现 Copy 特征,所有权将被转移给 HashMap 中
若类型实现 Copy 特征,该类型会被复制进 HashMap,因此无所谓所有权
若没实现 Copy 特征,所有权将被转移给 HashMap 中
//name被移动了,所以在打印时报错
fn main() {
use std::collections::HashMap;
let name = String::from("Sunface");
let age = 18;
let mut handsome_boys = HashMap::new();
handsome_boys.insert(name, age);
println!("因为过于无耻,{}已经被从帅气男孩名单中除名", name);
println!("还有,他的真实年龄远远不止{}岁", age);
}
fn main() {
use std::collections::HashMap;
let name = String::from("Sunface");
let age = 18;
let mut handsome_boys = HashMap::new();
handsome_boys.insert(name, age);
println!("因为过于无耻,{}已经被从帅气男孩名单中除名", name);
println!("还有,他的真实年龄远远不止{}岁", age);
}
//改进
handsome_boys.insert(&name, age);
handsome_boys.insert(&name, age);
查询HashMap
get方法获取元素
//get 方法返回一个 Option<&i32> 类型:当查询不到时,会返回一个 None,查询到时返回 Some(&i32)
//&i32 是对 HashMap 中值的借用,如果不使用借用,可能会发生所有权的转移
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
let team_name = String::from("Blue");
let score: Option<&i32> = scores.get(&team_name);
//直接获得值类型的 score
let score: i32 = scores.get(&team_name).copied().unwrap_or(0);
//&i32 是对 HashMap 中值的借用,如果不使用借用,可能会发生所有权的转移
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
let team_name = String::from("Blue");
let score: Option<&i32> = scores.get(&team_name);
//直接获得值类型的 score
let score: i32 = scores.get(&team_name).copied().unwrap_or(0);
遍历
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
for (key, value) in &scores {
println!("{}: {}", key, value);
}
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
for (key, value) in &scores {
println!("{}: {}", key, value);
}
更新HashMap中的值
insert、get、entry
fn main() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert("Blue", 10);
// 覆盖已有的值
let old = scores.insert("Blue", 20);
assert_eq!(old, Some(10));
// 查询新插入的值
let new = scores.get("Blue");
assert_eq!(new, Some(&20));
// 查询Yellow对应的值,若不存在则插入新值
let v = scores.entry("Yellow").or_insert(5);
assert_eq!(*v, 5); // 不存在,插入5
// 查询Yellow对应的值,若不存在则插入新值
let v = scores.entry("Yellow").or_insert(50);
assert_eq!(*v, 5); // 已经存在,因此50没有插入
}
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert("Blue", 10);
// 覆盖已有的值
let old = scores.insert("Blue", 20);
assert_eq!(old, Some(10));
// 查询新插入的值
let new = scores.get("Blue");
assert_eq!(new, Some(&20));
// 查询Yellow对应的值,若不存在则插入新值
let v = scores.entry("Yellow").or_insert(5);
assert_eq!(*v, 5); // 不存在,插入5
// 查询Yellow对应的值,若不存在则插入新值
let v = scores.entry("Yellow").or_insert(50);
assert_eq!(*v, 5); // 已经存在,因此50没有插入
}
在已有值的基础上更新
use std::collections::HashMap;
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;
}
println!("{:?}", map);
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;
}
println!("{:?}", map);
哈希函数
目前,HashMap 使用的哈希函数是 SipHash,它的性能不是很高,但是安全性很高。SipHash 在中等大小的 Key 上,性能相当不错,但是对于小型的 Key (例如整数)或者大型 Key (例如字符串)来说,性能还是不够好。若你需要极致性能,例如实现算法,可以考虑这个库:ahash。
泛型与特性
泛型
在函数中定义泛型
fn largest<T>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
结构体与枚举中的泛型
struct Point<T> {
x: T,
y: T
}
//结构体方法也应该实现泛型机制
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
//注意,impl 关键字的后方必须有 <T>,因为它后面的 T 是以之为榜样的
x: T,
y: T
}
//结构体方法也应该实现泛型机制
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
//注意,impl 关键字的后方必须有 <T>,因为它后面的 T 是以之为榜样的
//impl 块本身的泛型并没有阻碍其内部方法具有泛型的能力:
impl<T, U> Point<T, U> {
fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
Point {
x: self.x,
y: other.y,
}
}
}
//将一个Point<T, U> 点的 x 与 Point<V, W> 点的 y 融合成一个类型为 Point<T, W> 的新点。
impl<T, U> Point<T, U> {
fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
Point {
x: self.x,
y: other.y,
}
}
}
//将一个Point<T, U> 点的 x 与 Point<V, W> 点的 y 融合成一个类型为 Point<T, W> 的新点。
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
Option<T> 是一个拥有泛型 T 的枚举类型,它第一个成员是 Some(T),存放了一个类型为 T 的值。得益于泛型的引入,我们可以在任何一个需要返回值的函数中,去使用 Option<T> 枚举类型来做为返回值,用于返回一个任意类型的值 Some(T),或者没有值 None。
如果函数正常运行,则最后返回一个 Ok(T),T 是函数具体的返回值类型,如果函数异常运行,则返回一个 Err(E),E 是错误类型。例如打开一个文件:如果成功打开文件,则返回 Ok(std::fs::File),因此 T 对应的是 std::fs::File 类型;而当打开文件时出现问题时,返回 Err(std::io::Error),E 对应的就是 std::io::Error 类型。
如果函数正常运行,则最后返回一个 Ok(T),T 是函数具体的返回值类型,如果函数异常运行,则返回一个 Err(E),E 是错误类型。例如打开一个文件:如果成功打开文件,则返回 Ok(std::fs::File),因此 T 对应的是 std::fs::File 类型;而当打开文件时出现问题时,返回 Err(std::io::Error),E 对应的就是 std::io::Error 类型。
针对值的泛型
针对类型实现的泛型,所有的泛型都是为了抽象不同的类型,那有没有针对值的泛型?
针对类型实现的泛型,所有的泛型都是为了抽象不同的类型,那有没有针对值的泛型?
fn display_array(arr: &[i32]) {
println!("{:?}", arr);
}
fn main() {
let arr: [i32; 3] = [1, 2, 3];
display_array(&arr);
let arr: [i32;2] = [1,2];
display_array(&arr);
}
println!("{:?}", arr);
}
fn main() {
let arr: [i32; 3] = [1, 2, 3];
display_array(&arr);
let arr: [i32;2] = [1,2];
display_array(&arr);
}
//泛型
fn display_array<T: std::fmt::Debug>(arr: &[T]) {
println!("{:?}", arr);
}
fn display_array<T: std::fmt::Debug>(arr: &[T]) {
println!("{:?}", arr);
}
//定义了一个类型为 [T; N] 的数组,其中 T 是一个基于类型的泛型参数,而重点在于 N 这个泛型参数,它是一个基于值的泛型参数!因为它用来替代的是数组的长度。
//语法是 const N: usize,表示 const 泛型 N ,它基于的值类型是 usize。
fn display_array<T: std::fmt::Debug, const N: usize>(arr: [T; N]) {
println!("{:?}", arr);
}
fn main() {
let arr: [i32; 3] = [1, 2, 3];
display_array(arr);
let arr: [i32; 2] = [1, 2];
display_array(arr);
}
//语法是 const N: usize,表示 const 泛型 N ,它基于的值类型是 usize。
fn display_array<T: std::fmt::Debug, const N: usize>(arr: [T; N]) {
println!("{:?}", arr);
}
fn main() {
let arr: [i32; 3] = [1, 2, 3];
display_array(arr);
let arr: [i32; 2] = [1, 2];
display_array(arr);
}
trait
定义了一组可以被共享的行为,只要实现了特征,你就能使用这组行为
impl <特性名> for <所实现的类型名>
Rust 同一个类可以实现多个特性,每个 impl 块只能实现一个。
Rust 同一个类可以实现多个特性,每个 impl 块只能实现一个。
默认方法
接口只能规范方法而不能定义方法,但特性可以定义方法作为默认方法,因为是"默认",所以对象既可以重新定义方法,也可以不重新定义方法使用默认的方法. 但是不能违反孤儿规则,trait和类型必须有一样在本地定义。
trait Descriptive {
//特性的默认方法
fn describe(&self) -> String {
String::from("[Object]")
}
}
impl Descriptive for Person {
//对象重新定义方法
fn describe(&self) -> String {
String::from("Person")
}
}
//特性的默认方法
fn describe(&self) -> String {
String::from("[Object]")
}
}
impl Descriptive for Person {
//对象重新定义方法
fn describe(&self) -> String {
String::from("Person")
}
}
特性做参数
--面向特性编程, 语法:impl Trait
--面向特性编程, 语法:impl Trait
语法:impl Trait
//实现了Descriptive特性的object参数
fn output(object: impl Descriptive) {
println!("{}", object.describe());
}
fn output(object: impl Descriptive) {
println!("{}", object.describe());
}
特征约束:<T : Trait>
//等效语法
fn output<T: Descriptive>(object: T) {
println!("{}", object.describe());
}
fn output<T: Descriptive>(object: T) {
println!("{}", object.describe());
}
//在有多个参数类型均是特性
fn output_two<T: Descriptive>(arg1: T, arg2: T) {
println!("{}", arg1.describe());
println!("{}", arg2.describe());
}
fn output_two<T: Descriptive>(arg1: T, arg2: T) {
println!("{}", arg1.describe());
println!("{}", arg2.describe());
}
多重约束(复杂场景)
impl Summary + Display
<T: Summary + Display>,
通过 + 指定多个 trait bound
impl Summary + Display
<T: Summary + Display>,
通过 + 指定多个 trait bound
特性做类型表示时,如果是多个特性,可以用+符号表示
注意:仅用于表示类型的时候,并不意味着可以在 impl 块中使用。
注意:仅用于表示类型的时候,并不意味着可以在 impl 块中使用。
fn notify(item: &(impl Summary + Display))
fn notify(item: impl Summary + Display)
fn notify<T: Summary + Display>(item: T)
fn notify(item: impl Summary + Display)
fn notify<T: Summary + Display>(item: T)
使用 where 关键字简化
where T: Display + Clone,
U: Clone + Debug
where T: Display + Clone,
U: Clone + Debug
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U)
可以简化为
fn some_function<T, U>(t: T, u: U) -> i32
where T: Display + Clone,
U: Clone + Debug
可以简化为
fn some_function<T, U>(t: T, u: U) -> i32
where T: Display + Clone,
U: Clone + Debug
使用特征约束有条件地实现方法或特征
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) {
}
}
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) {
}
}
只有 T 同时实现了 Display + PartialOrd 的 Pair<T> 才可以拥有cmp_display方法
//也可以有条件地实现特征, 例如,标准库为任何实现了 Display 特征的类型实现了 ToString 特征:
impl<T: Display> ToString for T {
// --snip--
}
我们可以对任何实现了 Display 特征的类型调用由 ToString 定义的 to_string 方法。
impl<T: Display> ToString for T {
// --snip--
}
我们可以对任何实现了 Display 特征的类型调用由 ToString 定义的 to_string 方法。
函数返回中的 impl Trait
fn returns_summarizable() -> impl Summary {
Weibo {
username: String::from("sunface"),
content: String::from(
"m1 max太厉害了,电脑再也不会卡",
)
}
}
Weibo {
username: String::from("sunface"),
content: String::from(
"m1 max太厉害了,电脑再也不会卡",
)
}
}
impl Trait 形式的返回值,在一种场景下非常非常有用,那就是返回的真实类型非常复杂,你不知道该怎么声明时(毕竟 Rust 要求你必须标出所有的类型),此时就可以用 impl Trait 的方式简单返回
通过derive派生特征
语法:
例:#[derive(Debug)]
例:#[derive(Debug)]
调用方法需要引入特征
使用 as 关键字做类型转换会有比较大的限制,如果你想要在类型转换上拥有完全的控制,例如处理转换错误,那么你将需要 TryInto
use std::convert::TryInto;
fn main() {
let a: i32 = 10;
let b: u16 = 100;
let b_ = b.try_into()
.unwrap();
if a < b_ {
println!("Ten is less than one hundred.");
}
}
fn main() {
let a: i32 = 10;
let b: u16 = 100;
let b_ = b.try_into()
.unwrap();
if a < b_ {
println!("Ten is less than one hundred.");
}
}
有条件实现方法
//A<T> 类型要求T必须已经实现 B 和 C 特性。
struct A<T> {}
impl<T: B + C> A<T> {
fn d(&self) {}
}
struct A<T> {}
impl<T: B + C> A<T> {
fn d(&self) {}
}
标准库trait
内存相关
Copy、Clone
Drop
标记
Sized
标记有具体大小的类型,编译器自动加,?Sized摆脱约束
Send
线程安全的,线程间移动
Sync
线程间的只读共享是安全的
Unpin
用于自引用
类型转换相关
From<T>, TryFrom<T>
实现了From<T>会自动实现Into<T>,反之不会
Into<T>, TryInto<T>
AsRef<T>
AsMut<T>
操作符相关
Deref
DerefMut
Debug,Display,Default
Read,Write
Iterator
trait对象
实现机理
胖指针:数据指针和虚表指针
返回多种类型的问题
报错:另寻出路(可以用下面的Box和&)
报错:另寻出路(可以用下面的Box和&)
fn returns_summarizable(switch: bool) -> impl Summary {
if switch {
Post {// ... }
} else {
Weibo { //... }
}
}
//其中 Post 和 Weibo 都实现了 Summary 特征,因此上面的函数试图通过返回 impl Summary 来返回这两个类型,但是编译器却无情地报错了,原因是 impl Trait 的返回值类型并不支持多种不同的类型返回
if switch {
Post {// ... }
} else {
Weibo { //... }
}
}
//其中 Post 和 Weibo 都实现了 Summary 特征,因此上面的函数试图通过返回 impl Summary 来返回这两个类型,但是编译器却无情地报错了,原因是 impl Trait 的返回值类型并不支持多种不同的类型返回
使用特征对象来代表泛型或具体的类型
1. 可以通过 & 引用
2. Box<T> 智能指针的方式来创建 特征对象。
包裹的值会被强制分配在堆上
1. 可以通过 & 引用
2. Box<T> 智能指针的方式来创建 特征对象。
包裹的值会被强制分配在堆上
fn draw1(x: Box<dyn Draw>) {}
fn draw2(x: &dyn Draw) {}
fn draw2(x: &dyn Draw) {}
采用泛型+特征约束
--但对象要是同类型才可以
--但对象要是同类型才可以
//数组中的对象必须是同一个类型
pub struct Screen<T: Draw> {
pub components: Vec<T>,
}
pub struct Screen<T: Draw> {
pub components: Vec<T>,
}
鸭子类型(duck typing)
--通过 Box::new 包装成 Box<dyn Draw> 特征对象,实现向上转型,也就是面向特征(接口)编程
--通过 Box::new 包装成 Box<dyn Draw> 特征对象,实现向上转型,也就是面向特征(接口)编程
特征对象的动态分发
静态分发
编译器会为每一个泛型参数对应的具体类型生成一份代码,编译期完成,无性能影响
动态分发
直到运行时,才能确定需要调用什么方法。之前代码中的关键字 dyn 正是在强调这一“动态”的特点。
特征对象大小不固定:这是因为,对于特征 Draw,类型 Button 可以实现特征 Draw,类型 SelectBox 也可以实现特征 Draw,因此特征没有固定大小
几乎总是使用特征对象的引用方式,如 &dyn Draw、Box<dyn Draw>
虽然特征对象没有固定大小,但它的引用类型的大小是固定的,它由两个指针组成(ptr 和 vptr),因此占用两个指针大小
一个指针 ptr 指向实现了特征 Draw 的具体类型的实例,也就是当作特征 Draw 来用的类型的实例,比如类型 Button 的实例、类型 SelectBox 的实例
另一个指针 vptr 指向一个虚表 vtable,vtable 中保存了类型 Button 或类型 SelectBox 的实例对于可以调用的实现于特征 Draw 的方法。当调用方法时,直接从 vtable 中找到方法并调用。之所以要使用一个 vtable 来保存各实例的方法,是因为实现了特征 Draw 的类型有多种,这些类型拥有的方法各不相同,当将这些类型的实例都当作特征 Draw 来使用时(此时,它们全都看作是特征 Draw 类型的实例),有必要区分这些实例各自有哪些方法可调用
几乎总是使用特征对象的引用方式,如 &dyn Draw、Box<dyn Draw>
虽然特征对象没有固定大小,但它的引用类型的大小是固定的,它由两个指针组成(ptr 和 vptr),因此占用两个指针大小
一个指针 ptr 指向实现了特征 Draw 的具体类型的实例,也就是当作特征 Draw 来用的类型的实例,比如类型 Button 的实例、类型 SelectBox 的实例
另一个指针 vptr 指向一个虚表 vtable,vtable 中保存了类型 Button 或类型 SelectBox 的实例对于可以调用的实现于特征 Draw 的方法。当调用方法时,直接从 vtable 中找到方法并调用。之所以要使用一个 vtable 来保存各实例的方法,是因为实现了特征 Draw 的类型有多种,这些类型拥有的方法各不相同,当将这些类型的实例都当作特征 Draw 来使用时(此时,它们全都看作是特征 Draw 类型的实例),有必要区分这些实例各自有哪些方法可调用
Self与self
有两个self,一个指代当前的实例对象,一个指代特征或者方法类型的别名
self指代的就是当前的实例对象,也就是 button.draw() 中的 button 实例,Self 则指代的是 Button 类型。
self指代的就是当前的实例对象,也就是 button.draw() 中的 button 实例,Self 则指代的是 Button 类型。
trait Draw {
fn draw(&self) -> Self;
}
#[derive(Clone)]
struct Button;
impl Draw for Button {
fn draw(&self) -> Self {
return self.clone()
}
}
fn draw(&self) -> Self;
}
#[derive(Clone)]
struct Button;
impl Draw for Button {
fn draw(&self) -> Self {
return self.clone()
}
}
特征对象的限制
不是所有特征都能拥有特征对象,只有对象安全的特征才行。当一个特征的所有方法都有如下属性时,它的对象才是安全的:
方法的返回类型不能是 Self
方法没有任何泛型参数
对象安全对于特征对象是必须的,因为一旦有了特征对象,就不再需要知道实现该特征的具体类型是什么了
方法的返回类型不能是 Self
方法没有任何泛型参数
对象安全对于特征对象是必须的,因为一旦有了特征对象,就不再需要知道实现该特征的具体类型是什么了
pub trait Clone {
fn clone(&self) -> Self;
}
//返回了 Self 类型,因此它是对象不安全的。
fn clone(&self) -> Self;
}
//返回了 Self 类型,因此它是对象不安全的。
进一步深入特征
关联类型
关联类型是在特征定义的语句块中,申明一个自定义类型,这样就可以在特征的方法签名中使用该类型
pub trait Iterator {
type Item; //自定义类型
fn next(&mut self) -> Option<Self::Item>;
}
type Item; //自定义类型
fn next(&mut self) -> Option<Self::Item>;
}
为什么不用泛型
为了代码的可读性,当你使用了泛型后,你需要在所有地方都写 Iterator<Item>,而使用了关联类型,你只需要写 Iterator,当类型定义复杂时,这种写法可以极大的增加可读性
默认泛型类型参数
当使用泛型类型参数时,可以为其指定一个默认的具体类型
如:
1. trait Add<RHS=Self> {}
2. type Output = Point;
如:
1. trait Add<RHS=Self> {}
2. type Output = Point;
//RHS=默认类型
trait Add<RHS=Self> {
type Output;
fn add(self, rhs: RHS) -> Self::Output;
}
//它给 RHS 一个默认值,也就是当用户不指定 RHS 时,默认使用两个同样类型的值进行相加,然后返回一个关联类型 Output
trait Add<RHS=Self> {
type Output;
fn add(self, rhs: RHS) -> Self::Output;
}
//它给 RHS 一个默认值,也就是当用户不指定 RHS 时,默认使用两个同样类型的值进行相加,然后返回一个关联类型 Output
为 Point 结构体提供 + 的能力,这就是运算符重载。
只有定义在 std::ops 中的运算符才能进行重载。
只有定义在 std::ops 中的运算符才能进行重载。
use std::ops::Add;
#[derive(Debug, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
#[derive(Debug, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
创建两个不同类型的相加
use std::ops::Add;
struct Millimeters(u32);
struct Meters(u32);
impl Add<Meters> for Millimeters {
type Output = Millimeters;
fn add(self, other: Meters) -> Millimeters {
Millimeters(self.0 + (other.0 * 1000))
}
}
//默认类型参数主要用于两个方面:
例:type Output = Millimeters;
减少实现的样板代码
扩展类型但是无需大幅修改现有的代码
struct Millimeters(u32);
struct Meters(u32);
impl Add<Meters> for Millimeters {
type Output = Millimeters;
fn add(self, other: Meters) -> Millimeters {
Millimeters(self.0 + (other.0 * 1000))
}
}
//默认类型参数主要用于两个方面:
例:type Output = Millimeters;
减少实现的样板代码
扩展类型但是无需大幅修改现有的代码
调用同名的方法
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
优先调用类型上的方法
fn main() {
let person = Human;
person.fly();
}
let person = Human;
person.fly();
}
调用特征上的方法
fn main() {
let person = Human;
Pilot::fly(&person); // 调用Pilot特征上的方法
Wizard::fly(&person); // 调用Wizard特征上的方法
person.fly(); // 调用Human类型自身的方法
}
let person = Human;
Pilot::fly(&person); // 调用Pilot特征上的方法
Wizard::fly(&person); // 调用Wizard特征上的方法
person.fly(); // 调用Human类型自身的方法
}
完全限定语法
完全限定语法可以用于任何函数或方法调用
语法:
<Type as Trait>::function(receiver_if_method, next_arg, ...);
使用场景:
当存在多个同名函数或方法,且 Rust 无法区分出你想调用的目标函数时
语法:
<Type as Trait>::function(receiver_if_method, next_arg, ...);
使用场景:
当存在多个同名函数或方法,且 Rust 无法区分出你想调用的目标函数时
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", Dog::baby_name());
println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
//在尖括号中,通过 as 关键字,我们向 Rust 编译器提供了类型注解,也就是 Animal 就是 Dog,而不是其他动物,因此最终会调用 impl Animal for Dog 中的方法,获取到其它动物对狗宝宝的称呼:puppy。
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", Dog::baby_name());
println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
//在尖括号中,通过 as 关键字,我们向 Rust 编译器提供了类型注解,也就是 Animal 就是 Dog,而不是其他动物,因此最终会调用 impl Animal for Dog 中的方法,获取到其它动物对狗宝宝的称呼:puppy。
特征定义中的特征约束
trait OutlinePrint: Display
trait OutlinePrint: Display
一个特征需要实现另一个特征,在特征定义中
如果你想要实现 OutlinePrint 特征,首先你需要实现 Display 特征。
如果你想要实现 OutlinePrint 特征,首先你需要实现 Display 特征。
use std::fmt::Display;
//这里用到了to_string方法
trait OutlinePrint: Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {} *", output);
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
//这里用到了to_string方法
trait OutlinePrint: Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {} *", output);
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
在外部类型上实现外部特征
就是特征或者类型必需至少有一个是本地的,才能在此类型上定义特征。
使用newtype 模式,简而言之:就是为一个元组结构体创建新类型。该元组结构体封装有一个字段,该字段就是希望实现特征的具体类型。
该封装类型是本地的,因此我们可以为此类型实现外部的特征。
该封装类型是本地的,因此我们可以为此类型实现外部的特征。
use std::fmt;
//包装元组结构体,实现特征
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
fn main() {
let w = Wrapper(vec![String::from("hello"), String::from("world")]);
println!("w = {}", w);
}
//包装元组结构体,实现特征
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
fn main() {
let w = Wrapper(vec![String::from("hello"), String::from("world")]);
println!("w = {}", w);
}
Rust生命周期
生命周期:引用的有效作用域
1. 大多数时候可以自动推导生命周期。
2. 在多种类型存在时,编译器往往要求我们手动标明类型<_>,当多个生命周期存在,且编译器无法推导出某个引用的生命周期时,需要手动标明生命周期
2. 在多种类型存在时,编译器往往要求我们手动标明类型<_>,当多个生命周期存在,且编译器无法推导出某个引用的生命周期时,需要手动标明生命周期
避免垂悬引用
{
let r;
{
let x = 5;
r = &x;
}
//原因是 r 所引用的值已经在使用之前被释放
println!("r: {}", r);
}
let r;
{
let x = 5;
r = &x;
}
//原因是 r 所引用的值已经在使用之前被释放
println!("r: {}", r);
}
结构体中使用字符串切片引用
fn main() {
struct Str<'a> {
content: &'a str //需要生命周期注解
}
let s = Str {
content: "string_slice"
};
println!("s.content = {}", s.content);
}
struct Str<'a> {
content: &'a str //需要生命周期注解
}
let s = Str {
content: "string_slice"
};
println!("s.content = {}", s.content);
}
借用检查
在编译期,Rust 会比较两个变量的生命周期,结果发现 r 明明拥有生命周期 'a,但是却引用了一个小得多的生命周期 'b,在这种情况下,编译器会认为我们的程序存在风险,因此拒绝运行。
函数中的生命周期
存在多个引用时,编译器有时会无法自动推导生命周期,此时就需要我们手动去标注,通过为参数标注合适的生命周期来帮助编译器进行借用检查的分析
//该函数签名表明对于某些生命周期 'a,函数的两个参数都至少跟 'a 活得一样久,
同时函数的返回引用也至少跟 'a 活得一样久
fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s2.len() > s1.len() {
s2
} else {
s1
}
}
fn main() {
let r;
{
let s1 = "rust";
let s2 = "ecmascript";
r = longer(s1, s2);
}
println!("{} is longer", r);
}
同时函数的返回引用也至少跟 'a 活得一样久
fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s2.len() > s1.len() {
s2
} else {
s1
}
}
fn main() {
let r;
{
let s1 = "rust";
let s2 = "ecmascript";
r = longer(s1, s2);
}
println!("{} is longer", r);
}
生命周期标注语法
以 ' 开头,名称往往是一个单独的小写字母
1. 一个生命周期标注,它自身并不具有什么意义,因为生命周期的作用就是告诉编译器多个引用之间的关系
2. 指定两个参数都具有相同的生命周期'a,'a的大小是取两个参数最小的生命周期
3. 函数的返回值如果是一个引用类型,那么它的生命周期只会来源于:
函数参数的生命周期
函数体中某个新建引用的生命周期
4. 生命周期总结
生命周期语法用来将函数的多个引用参数和返回值的作用域关联到一起,一旦关联到一起后,Rust 就拥有充分的信息来确保我们的操作是内存安全的。
注:标记的生命周期只是为了取悦编译器,让编译器不要难为我们,它不会改变任何引用的实际作用域。
1. 一个生命周期标注,它自身并不具有什么意义,因为生命周期的作用就是告诉编译器多个引用之间的关系
2. 指定两个参数都具有相同的生命周期'a,'a的大小是取两个参数最小的生命周期
3. 函数的返回值如果是一个引用类型,那么它的生命周期只会来源于:
函数参数的生命周期
函数体中某个新建引用的生命周期
4. 生命周期总结
生命周期语法用来将函数的多个引用参数和返回值的作用域关联到一起,一旦关联到一起后,Rust 就拥有充分的信息来确保我们的操作是内存安全的。
注:标记的生命周期只是为了取悦编译器,让编译器不要难为我们,它不会改变任何引用的实际作用域。
&i32 // 一个引用
&'a i32 // 具有显式生命周期的引用
&'a mut i32 // 具有显式生命周期的可变引用
&'a i32 // 具有显式生命周期的引用
&'a mut i32 // 具有显式生命周期的可变引用
//指定两个参数都具有相同的生命周期'a,'a的大小是取两个参数最小的生命周期
fn useless<'a>(first: &'a i32, second: &'a i32) {}
fn useless<'a>(first: &'a i32, second: &'a i32) {}
生命周期取最小的那个
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());//切片
}
println!("The longest string is {}", result);
//虽然string1, string2两个生命周期都是'a,但result的生命周期选最小的那个
}
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());//切片
}
println!("The longest string is {}", result);
//虽然string1, string2两个生命周期都是'a,但result的生命周期选最小的那个
}
只返回第一个参数
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}
//只需要标注x参数和返回值即可
x
}
//只需要标注x参数和返回值即可
返回悬垂引用
--解决办法:返回内部字符串的所有权
--解决办法:返回内部字符串的所有权
fn longest<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("really long string");
result.as_str() //报错
}
let result = String::from("really long string");
result.as_str() //报错
}
fn longest<'a>(_x: &str, _y: &str) -> String {
String::from("really long string")
}
fn main() {
let s = longest("not", "important");
}
String::from("really long string")
}
fn main() {
let s = longest("not", "important");
}
结构体中的生命周期
结构体中使用引用
只要为结构体中的每一个引用标注上生命周期即可
只要为结构体中的每一个引用标注上生命周期即可
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
}
//结构体 ImportantExcerpt 所引用的字符串 str 必须比该结构体活得更久。
//结构体引用的字符串活得比结构体久,这符合了编译器对生命周期的要求,因此编译通过
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
}
//结构体 ImportantExcerpt 所引用的字符串 str 必须比该结构体活得更久。
//结构体引用的字符串活得比结构体久,这符合了编译器对生命周期的要求,因此编译通过
//举一个反例
#[derive(Debug)]
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let i;
{
let novel = String::from("Call me Ishmael. Some years ago..."); //生命周期在{}内
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
i = ImportantExcerpt {
part: first_sentence,
};
}
println!("{:?}",i);
}
//可以看出结构体比它引用的字符串活得更久,引用字符串在内部语句块末尾 } 被释放后,println! 依然在外面使用了该结构体,因此会导致无效的引用,不出所料,编译报错
#[derive(Debug)]
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let i;
{
let novel = String::from("Call me Ishmael. Some years ago..."); //生命周期在{}内
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
i = ImportantExcerpt {
part: first_sentence,
};
}
println!("{:?}",i);
}
//可以看出结构体比它引用的字符串活得更久,引用字符串在内部语句块末尾 } 被释放后,println! 依然在外面使用了该结构体,因此会导致无效的引用,不出所料,编译报错
生命周期省略
什么样的会用省略大法
返回值的引用是获取自参数,这就意味着参数和返回值的生命周期是一样的。
注:省略规则不是万能的,若编译器不能确定某件事是正确时,会直接判为不正确,那么你还是需要手动标注生命周期
函数或者方法中,参数的生命周期被称为 输入生命周期,返回值的生命周期被称为 输出生命周期
返回值的引用是获取自参数,这就意味着参数和返回值的生命周期是一样的。
注:省略规则不是万能的,若编译器不能确定某件事是正确时,会直接判为不正确,那么你还是需要手动标注生命周期
函数或者方法中,参数的生命周期被称为 输入生命周期,返回值的生命周期被称为 输出生命周期
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
//返回值是一个引用类型,那么该引用只有两种情况:
//从参数获取
//从函数体内部新创建的变量获取
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
//返回值是一个引用类型,那么该引用只有两种情况:
//从参数获取
//从函数体内部新创建的变量获取
三条消除规则
1. 每一个引用参数都会获得独自的生命周期
2. 若只有一个输入生命周期(函数参数中只有一个引用类型),那么该生命周期会被赋给所有的输出生命周期,也就是所有返回值的生命周期都等于该输入生命周期
例如函数 fn foo(x: &i32) -> &i32,x 参数的生命周期会被自动赋给返回值 &i32,因此该函数等同于 fn foo<'a>(x: &'a i32) -> &'a i32
3. 若存在多个输入生命周期,且其中一个是 &self 或 &mut self,则 &self 的生命周期被赋给所有的输出生命周期
拥有 &self 形式的参数,说明该函数是一个 方法,该规则让方法的使用便利度大幅提升。
2. 若只有一个输入生命周期(函数参数中只有一个引用类型),那么该生命周期会被赋给所有的输出生命周期,也就是所有返回值的生命周期都等于该输入生命周期
例如函数 fn foo(x: &i32) -> &i32,x 参数的生命周期会被自动赋给返回值 &i32,因此该函数等同于 fn foo<'a>(x: &'a i32) -> &'a i32
3. 若存在多个输入生命周期,且其中一个是 &self 或 &mut self,则 &self 的生命周期被赋给所有的输出生命周期
拥有 &self 形式的参数,说明该函数是一个 方法,该规则让方法的使用便利度大幅提升。
方法中的生命周期
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {//第三规则
3
}
}
//impl 中必须使用结构体的完整名称,包括 <'a>,因为生命周期标注也是结构体类型的一部分
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {//第三规则
3
}
}
//impl 中必须使用结构体的完整名称,包括 <'a>,因为生命周期标注也是结构体类型的一部分
深入生命周期
无界生命周期
不安全代码(unsafe)经常会凭空产生引用或生命周期
fn f<'a, T>(x: *const T) -> &'a T {
unsafe {
&*x
}
}
unsafe {
&*x
}
}
参数 x 是一个裸指针,它并没有任何生命周期,然后通过 unsafe 操作后,它被进行了解引用,变成了一个 Rust 的标准引用类型,该类型必须要有生命周期,也就是 'a
要尽量避免这种无界生命周期。最简单的避免无界生命周期的方式就是在函数声明中运用生命周期消除规则。若一个输出生命周期被消除了,那么必定因为有一个输入生命周期与之对应。
生命周期约束 HRTB
生命周期约束跟特征约束类似,都是通过形如 'a: 'b 的语法,来说明两个生命周期的长短关系。
'a:'b
有两个引用 &'a i32 和 &'b i32,它们的生命周期分别是 'a 和 'b,若 'a >= 'b,则可以定义 'a:'b,表示 'a 至少要活得跟 'b 一样久。
struct DoubleRef<'a,'b:'a, T> {
r: &'a T,
s: &'b T //s要比r的生命周期长
}
r: &'a T,
s: &'b T //s要比r的生命周期长
}
闭包函数的消除规则
//编译器无法推测返回的引用和传入的引用谁活得更久!
let closure_slision = |x : &i32| -> &i32 {x};
let closure_slision = |x : &i32| -> &i32 {x};
用 Fn 特征解决闭包生命周期
fn main() {
let closure_slision = fun(|x: &i32| -> &i32 { x });
assert_eq!(*closure_slision(&45), 45);
// Passed !
}
fn fun<T, F: Fn(&T) -> &T>(f: F) -> F {
f
}
let closure_slision = fun(|x: &i32| -> &i32 { x });
assert_eq!(*closure_slision(&45), 45);
// Passed !
}
fn fun<T, F: Fn(&T) -> &T>(f: F) -> F {
f
}
再借用
fn main() {
let mut p = Point { x: 0, y: 0 };
let r = &mut p;
// reborrow! 此时对`r`的再借用不会导致跟上面的借用冲突
let rr: &Point = &*r;
// 再借用`rr`最后一次使用发生在这里,在它的生命周期中,我们并没有使用原来的借用`r`,因此不会报错
println!("{:?}", rr);
// 再借用结束后,才去使用原来的借用`r`
r.move_to(10, 10);
println!("{:?}", r);
}
let mut p = Point { x: 0, y: 0 };
let r = &mut p;
// reborrow! 此时对`r`的再借用不会导致跟上面的借用冲突
let rr: &Point = &*r;
// 再借用`rr`最后一次使用发生在这里,在它的生命周期中,我们并没有使用原来的借用`r`,因此不会报错
println!("{:?}", rr);
// 再借用结束后,才去使用原来的借用`r`
r.move_to(10, 10);
println!("{:?}", r);
}
一个复杂的例子
左侧的代码会报错
这是因为我们在 get_interface 方法中声明的 lifetime 有问题,该方法的参数的生命周期是 'a,而 List 的生命周期也是 'a,说明该方法至少活得跟 List 一样久,再回到 main 函数中,list 可以活到 main 函数的结束,因此 list.get_interface() 借用的可变引用也会活到 main 函数的结束,在此期间,自然无法再进行借用了。
这是因为我们在 get_interface 方法中声明的 lifetime 有问题,该方法的参数的生命周期是 'a,而 List 的生命周期也是 'a,说明该方法至少活得跟 List 一样久,再回到 main 函数中,list 可以活到 main 函数的结束,因此 list.get_interface() 借用的可变引用也会活到 main 函数的结束,在此期间,自然无法再进行借用了。
struct Interface<'a> {
manager: &'a mut Manager<'a>
}
impl<'a> Interface<'a> {
pub fn noop(self) {
println!("interface consumed");
}
}
struct Manager<'a> {
text: &'a str
}
struct List<'a> {
manager: Manager<'a>,
}
impl<'a> List<'a> {
//返回的借用可变引用参数也活到了main函数结束
pub fn get_interface(&'a mut self) -> Interface {
Interface {
manager: &mut self.manager
}
}
}
fn main() {
let mut list = List {
manager: Manager {
text: "hello"
}
};
list.get_interface().noop();
println!("Interface should be dropped here and the borrow released");
// 下面的调用会失败,因为同时有不可变/可变借用
// 但是Interface在之前调用完成后就应该被释放了
use_list(&list);
}
fn use_list(list: &List) {
println!("{}", list.manager.text);
}
manager: &'a mut Manager<'a>
}
impl<'a> Interface<'a> {
pub fn noop(self) {
println!("interface consumed");
}
}
struct Manager<'a> {
text: &'a str
}
struct List<'a> {
manager: Manager<'a>,
}
impl<'a> List<'a> {
//返回的借用可变引用参数也活到了main函数结束
pub fn get_interface(&'a mut self) -> Interface {
Interface {
manager: &mut self.manager
}
}
}
fn main() {
let mut list = List {
manager: Manager {
text: "hello"
}
};
list.get_interface().noop();
println!("Interface should be dropped here and the borrow released");
// 下面的调用会失败,因为同时有不可变/可变借用
// 但是Interface在之前调用完成后就应该被释放了
use_list(&list);
}
fn use_list(list: &List) {
println!("{}", list.manager.text);
}
//优化后
struct Interface<'b, 'a: 'b> {
manager: &'b mut Manager<'a>
}
impl<'b, 'a: 'b> Interface<'b, 'a> {
pub fn noop(self) {
println!("interface consumed");
}
}
struct Manager<'a> {
text: &'a str
}
struct List<'a> {
manager: Manager<'a>,
}
impl<'a> List<'a> {
pub fn get_interface<'b>(&'b mut self) -> Interface<'b, 'a>
where 'a: 'b {
Interface {
manager: &mut self.manager
}
}
}
fn main() {
let mut list = List {
manager: Manager {
text: "hello"
}
};
list.get_interface().noop();
println!("Interface should be dropped here and the borrow released");
// 下面的调用可以通过,因为Interface的生命周期不需要跟list一样长
use_list(&list);
}
fn use_list(list: &List) {
println!("{}", list.manager.text);
}
struct Interface<'b, 'a: 'b> {
manager: &'b mut Manager<'a>
}
impl<'b, 'a: 'b> Interface<'b, 'a> {
pub fn noop(self) {
println!("interface consumed");
}
}
struct Manager<'a> {
text: &'a str
}
struct List<'a> {
manager: Manager<'a>,
}
impl<'a> List<'a> {
pub fn get_interface<'b>(&'b mut self) -> Interface<'b, 'a>
where 'a: 'b {
Interface {
manager: &mut self.manager
}
}
}
fn main() {
let mut list = List {
manager: Manager {
text: "hello"
}
};
list.get_interface().noop();
println!("Interface should be dropped here and the borrow released");
// 下面的调用可以通过,因为Interface的生命周期不需要跟list一样长
use_list(&list);
}
fn use_list(list: &List) {
println!("{}", list.manager.text);
}
静态生命周期
生命周期注释有一个特别的:'static 。所有用双引号包括的字符串常量所代表的精确数据类型都是 &'static str ,'static 所表示的生命周期从程序运行开始到程序运行结束。
let s: &'static str = "我没啥优点,就是活得久,嘿嘿";
fn t() -> &'static str{
"qwert"
}
let s: &'static str = "我没啥优点,就是活得久,嘿嘿";
fn t() -> &'static str{
"qwert"
}
一个引用必须要活得跟剩下的程序一样久,才能被标注为 &'static。
fn main() {
let mark_twain: &str = "Samuel Clemens";
print_author(mark_twain);
}
fn print_author(author: &'static str) {
println!("{}", author);
}
let mark_twain: &str = "Samuel Clemens";
print_author(mark_twain);
}
fn print_author(author: &'static str) {
println!("{}", author);
}
use std::fmt::Display;
fn main() {
let mark_twain = "Samuel Clemens";
print(&mark_twain);
}
fn print<T: Display + 'static>(message: &T) {
println!("{}", message);
}
fn main() {
let mark_twain = "Samuel Clemens";
print(&mark_twain);
}
fn print<T: Display + 'static>(message: &T) {
println!("{}", message);
}
&'static 生命周期针对的仅仅是引用,而不是持有该引用的变量,对于变量来说,还是要遵循相应的作用域规则
use std::{slice::from_raw_parts, str::from_utf8_unchecked};
fn get_memory_location() -> (usize, usize) {
// “Hello World” 是字符串字面量,因此它的生命周期是 `'static`.
// 但持有它的变量 `string` 的生命周期就不一样了,它完全取决于变量作用域,对于该例子来说,也就是当前的函数范围
let string = "Hello World!";
let pointer = string.as_ptr() as usize;
let length = string.len();
(pointer, length)
// `string` 在这里被 drop 释放
// 虽然变量被释放,无法再被访问,但是数据依然还会继续存活
}
fn get_str_at_location(pointer: usize, length: usize) -> &'static str {
// 使用裸指针需要 `unsafe{}` 语句块
unsafe { from_utf8_unchecked(from_raw_parts(pointer as *const u8, length)) }
}
fn main() {
let (pointer, length) = get_memory_location();
let message = get_str_at_location(pointer, length);
println!(
"The {} bytes at 0x{:X} stored: {}",
length, pointer, message
);
// 如果大家想知道为何处理裸指针需要 `unsafe`,可以试着反注释以下代码
// let message = get_str_at_location(1000, 10);
}
fn get_memory_location() -> (usize, usize) {
// “Hello World” 是字符串字面量,因此它的生命周期是 `'static`.
// 但持有它的变量 `string` 的生命周期就不一样了,它完全取决于变量作用域,对于该例子来说,也就是当前的函数范围
let string = "Hello World!";
let pointer = string.as_ptr() as usize;
let length = string.len();
(pointer, length)
// `string` 在这里被 drop 释放
// 虽然变量被释放,无法再被访问,但是数据依然还会继续存活
}
fn get_str_at_location(pointer: usize, length: usize) -> &'static str {
// 使用裸指针需要 `unsafe{}` 语句块
unsafe { from_utf8_unchecked(from_raw_parts(pointer as *const u8, length)) }
}
fn main() {
let (pointer, length) = get_memory_location();
let message = get_str_at_location(pointer, length);
println!(
"The {} bytes at 0x{:X} stored: {}",
length, pointer, message
);
// 如果大家想知道为何处理裸指针需要 `unsafe`,可以试着反注释以下代码
// let message = get_str_at_location(1000, 10);
}
T:'static
T: 'static 与 &'static 有相同的约束:T 必须活得和程序一样久
错误处理
backtrace栈展开
要获取到栈回溯信息,你还需要开启 debug 标志,该标志在使用 cargo run 或者 cargo build 时自动开启(这两个操作默认是 Debug 运行方式)
panic 时的两种终止方式
栈展开
默认的方式就是 栈展开,这意味着 Rust 会回溯栈上数据和函数调用
直接终止
当你关心最终编译出的二进制可执行文件大小时,那么可以尝试去使用直接终止的方式,
配置修改 Cargo.toml 文件,实现在 release 模式下遇到 panic 直接终止:
[profile.release]
panic = 'abort'
配置修改 Cargo.toml 文件,实现在 release 模式下遇到 panic 直接终止:
[profile.release]
panic = 'abort'
线程 panic 后,程序是否会终止?
长话短说,如果是 main 线程,则程序会终止,如果是其它子线程,该线程会终止,但是不会影响 main 线程。因此,尽量不要在 main 线程中做太多任务,将这些任务交由子线程去做,就算子线程 panic 也不会导致整个程序的结束。
何时该使用panic!
正常返回
enum Result<T, E> {
Ok(T),
Err(E),
}
Ok(T),
Err(E),
}
简单粗爆
注:你确切的知道你的程序是正确时,可以使用panic,因为可以让代码更清晰(因为我们知道不会发生错误)
注:你确切的知道你的程序是正确时,可以使用panic,因为可以让代码更清晰(因为我们知道不会发生错误)
unwrap
成功则返回值,失败则 panic,总之不进行任何错误处理。
expect
expect 能够向 panic! 宏发送一段指定的错误信息。
panic原理剖析
当调用 panic! 宏时,它会
1. 格式化 panic 信息,然后使用该信息作为参数,调用 std::panic::panic_any() 函数
2. panic_any 会检查应用是否使用了 panic hook,如果使用了,该 hook 函数就会被调用(hook 是一个钩子函数,是外部代码设置的,用于在 panic 触发时,执行外部代码所需的功能)
3. 当 hook 函数返回后,当前的线程就开始进行栈展开:从 panic_any 开始,如果寄存器或者栈因为某些原因信息错乱了,那很可能该展开会发生异常,最终线程会直接停止,展开也无法继续进行
4. 展开的过程是一帧一帧的去回溯整个栈,每个帧的数据都会随之被丢弃,但是在展开过程中,你可能会遇到被用户标记为 catching 的帧(通过 std::panic::catch_unwind() 函数标记),此时用户提供的 catch 函数会被调用,展开也随之停止:当然,如果 catch 选择在内部调用 std::panic::resume_unwind() 函数,则展开还会继续。
还有一种情况,在展开过程中,如果展开本身 panic 了,那展开线程会终止,展开也随之停止。
一旦线程展开被终止或者完成,最终的输出结果是取决于哪个线程 panic:对于 main 线程,操作系统提供的终止功能 core::intrinsics::abort() 会被调用,最终结束当前的 panic 进程;如果是其它子线程,那么子线程就会简单的终止,同时信息会在稍后通过 std::thread::join() 进行收集。
1. 格式化 panic 信息,然后使用该信息作为参数,调用 std::panic::panic_any() 函数
2. panic_any 会检查应用是否使用了 panic hook,如果使用了,该 hook 函数就会被调用(hook 是一个钩子函数,是外部代码设置的,用于在 panic 触发时,执行外部代码所需的功能)
3. 当 hook 函数返回后,当前的线程就开始进行栈展开:从 panic_any 开始,如果寄存器或者栈因为某些原因信息错乱了,那很可能该展开会发生异常,最终线程会直接停止,展开也无法继续进行
4. 展开的过程是一帧一帧的去回溯整个栈,每个帧的数据都会随之被丢弃,但是在展开过程中,你可能会遇到被用户标记为 catching 的帧(通过 std::panic::catch_unwind() 函数标记),此时用户提供的 catch 函数会被调用,展开也随之停止:当然,如果 catch 选择在内部调用 std::panic::resume_unwind() 函数,则展开还会继续。
还有一种情况,在展开过程中,如果展开本身 panic 了,那展开线程会终止,展开也随之停止。
一旦线程展开被终止或者完成,最终的输出结果是取决于哪个线程 panic:对于 main 线程,操作系统提供的终止功能 core::intrinsics::abort() 会被调用,最终结束当前的 panic 进程;如果是其它子线程,那么子线程就会简单的终止,同时信息会在稍后通过 std::thread::join() 进行收集。
传播错误
错误关系
标准库中的
std::io::Error 是 IO 相关的错误结构体(实现了下面的Error)
std::error::Error是一个最最通用的标准错误特征
std::io::Error 是 IO 相关的错误结构体(实现了下面的Error)
std::error::Error是一个最最通用的标准错误特征
传播界的大明星: ?
注:? 操作符需要一个变量来承载正确的值,用于以下形式
let v = xxx()?;
xxx()?.yyy()?;
注:? 操作符需要一个变量来承载正确的值,用于以下形式
let v = xxx()?;
xxx()?.yyy()?;
普通处理
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
// 打开文件,f是`Result<文件句柄,io::Error>`
let f = File::open("hello.txt");
let mut f = match f {
// 打开文件成功,将file句柄赋值给f
Ok(file) => file,
// 打开文件失败,将错误返回(向上传播)
Err(e) => return Err(e),
};
// 创建动态字符串s
let mut s = String::new();
// 从f文件句柄读取数据并写入s中
match f.read_to_string(&mut s) {
// 读取成功,返回Ok封装的字符串
Ok(_) => Ok(s),
// 将错误向上传播
Err(e) => Err(e),
}
}
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
// 打开文件,f是`Result<文件句柄,io::Error>`
let f = File::open("hello.txt");
let mut f = match f {
// 打开文件成功,将file句柄赋值给f
Ok(file) => file,
// 打开文件失败,将错误返回(向上传播)
Err(e) => return Err(e),
};
// 创建动态字符串s
let mut s = String::new();
// 从f文件句柄读取数据并写入s中
match f.read_to_string(&mut s) {
// 读取成功,返回Ok封装的字符串
Ok(_) => Ok(s),
// 将错误向上传播
Err(e) => Err(e),
}
}
使用?处理
//如果结果是 Ok(T),则把 T 赋值给 f,如果结果是 Err(E),则返回该错误,所以 ? 特别适合用来传播错误。
use std::fs::File;
use std::io;
use std::io::Read;
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
use std::fs::File;
use std::io;
use std::io::Read;
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
自动类型提升
自动向上转型的原因
在于标准库中定义的 From 特征,该特征有一个方法 from,用于把一个类型转成另外一个类型,? 可以自动调用该方法,然后进行隐式类型转换。因此只要函数返回的错误 ReturnError 实现了 From<OtherError> 特征,那么 ? 就会自动把 OtherError 转换为 ReturnError。
fn open_file() -> Result<File, Box<dyn std::error::Error>> {
let mut f = File::open("hello.txt")?;
Ok(f)
}
//File::open 报错时返回的错误是 std::io::Error 类型,但是 open_file 函数返回的错误类型是 std::error::Error 的特征对象,可以看到一个错误类型通过 ? 返回后,变成了另一个错误类型,这就是 ? 的神奇之处。
let mut f = File::open("hello.txt")?;
Ok(f)
}
//File::open 报错时返回的错误是 std::io::Error 类型,但是 open_file 函数返回的错误类型是 std::error::Error 的特征对象,可以看到一个错误类型通过 ? 返回后,变成了另一个错误类型,这就是 ? 的神奇之处。
链式调用
? 还能实现链式调用,File::open 遇到错误就返回,没有错误就将 Ok 中的值取出来用于下一个方法调用,简直太精妙了
use std::fs::File;
use std::io;
use std::io::Read;
fn read_username_from_file() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
use std::io;
use std::io::Read;
fn read_username_from_file() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
更强处理
Rust 标准库为我们提供了 fs::read_to_string 函数,该函数内部会打开一个文件、创建 String、读取文件内容最后写入字符串并返回
use std::fs;
use std::io;
fn read_username_from_file() -> Result<String, io::Error> {
// read_to_string是定义在std::io中的方法,因此需要在上面进行引用
fs::read_to_string("hello.txt")
}
use std::io;
fn read_username_from_file() -> Result<String, io::Error> {
// read_to_string是定义在std::io中的方法,因此需要在上面进行引用
fs::read_to_string("hello.txt")
}
? 用于 Option 的返回
Option定义
pub enum Option<T> {
Some(T),
None
}
Some(T),
None
}
Result 通过 ? 返回错误,那么 Option 就通过 ? 返回 None
fn first(arr: &[i32]) -> Option<&i32> {
let v = arr.get(0)?;
Some(v)
}
//arr.get 返回一个 Option<&i32> 类型,因为 ? 的使用,如果 get 的结果是 None,则直接返回 None,如果是 Some(&i32),则把里面的值赋给 v
let v = arr.get(0)?;
Some(v)
}
//arr.get 返回一个 Option<&i32> 类型,因为 ? 的使用,如果 get 的结果是 None,则直接返回 None,如果是 Some(&i32),则把里面的值赋给 v
更简单版本
fn first(arr: &[i32]) -> Option<&i32> {
arr.get(0)
}
arr.get(0)
}
链式用法
fn last_char_of_first_line(text: &str) -> Option<char> {
text.lines().next()?.chars().last()
}
//展示了在链式调用中使用 ? 提前返回 None 的用法, .next 方法返回的是 Option 类型:如果返回 Some(&str),那么继续调用 chars 方法,如果返回 None,则直接从整个函数中返回 None,不再继续进行链式调用。
text.lines().next()?.chars().last()
}
//展示了在链式调用中使用 ? 提前返回 None 的用法, .next 方法返回的是 Option 类型:如果返回 Some(&str),那么继续调用 chars 方法,如果返回 None,则直接从整个函数中返回 None,不再继续进行链式调用。
带返回值的main函数
这样就能使用 ? 提前返回了,同时我们又一次看到了Box<dyn Error> 特征对象,因为 std::error:Error 是 Rust 中抽象层次最高的错误,其它标准库中的错误都实现了该特征,因此我们可以用该特征对象代表一切错误,就算 main 函数中调用任何标准库函数发生错误,都可以通过 Box<dyn Error> 这个特征对象进行返回。
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let f = File::open("hello.txt")?;
Ok(())
}
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let f = File::open("hello.txt")?;
Ok(())
}
panic!
不可恢复错误
不可恢复错误
fn main() {
panic!("error occured");
println!("Hello, Rust");
}
panic!("error occured");
println!("Hello, Rust");
}
被动触发
fn main() {
let v = vec![1, 2, 3];
v[99];
}
let v = vec![1, 2, 3];
v[99];
}
主动调用
fn main() {
panic!("crash and burn");
}
panic!("crash and burn");
}
可恢复的错误
在 Rust 中通过 Result<T, E> 枚举类作返回值来进行异常表达
enum Result<T, E> {
Ok(T),
Err(E),
}
Ok(T),
Err(E),
}
在 Rust 标准库中可能产生异常的函数的返回值都是 Result 类型的。例如:当我们尝试打开一个文件时:
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
match f {
Ok(file) => {
println!("File opened successfully.");
},
Err(err) => {
println!("Failed to open the file.");
}
}
}
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
match f {
Ok(file) => {
println!("File opened successfully.");
},
Err(err) => {
println!("Failed to open the file.");
}
}
}
if let语法简化match
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
if let Ok(file) = f {
println!("File opened successfully.");
} else {
println!("Failed to open the file.");
}
}
fn main() {
let f = File::open("hello.txt");
if let Ok(file) = f {
println!("File opened successfully.");
} else {
println!("Failed to open the file.");
}
}
可恢复错误按不可恢复错误处理
unwrap()
expect(message: &str)
use std::fs::File;
//这段程序相当于在 Result 为 Err 时调用 panic! 宏。两者的区别在于 expect 能够向 panic! 宏发送一段指定的错误信息。
fn main() {
let f1 = File::open("hello.txt").unwrap();
let f2 = File::open("hello.txt").expect("Failed to open.");
}
//这段程序相当于在 Result 为 Err 时调用 panic! 宏。两者的区别在于 expect 能够向 panic! 宏发送一段指定的错误信息。
fn main() {
let f1 = File::open("hello.txt").unwrap();
let f2 = File::open("hello.txt").expect("Failed to open.");
}
可恢复的错误的传递
自己编写一个函数在遇到错误时想传递出去
fn f(i: i32) -> Result<i32, bool> {
if i >= 0 { Ok(i) }
else { Err(false) }
}
fn main() {
let r = f(10000);
if let Ok(v) = r {
println!("Ok: f(-1) = {}", v);
} else {
println!("Err");
}
}
fn f(i: i32) -> Result<i32, bool> {
if i >= 0 { Ok(i) }
else { Err(false) }
}
fn main() {
let r = f(10000);
if let Ok(v) = r {
println!("Ok: f(-1) = {}", v);
} else {
println!("Err");
}
}
fn g(i: i32) -> Result<i32, bool> {
let t = f(i);
return match t {
Ok(i) => Ok(i),
Err(b) => Err(b)
};
}
let t = f(i);
return match t {
Ok(i) => Ok(i),
Err(b) => Err(b)
};
}
在 Result 对象后添加 ? 操作符将同类的 Err 直接传递出去
? 符的实际作用是将 Result 类非异常的值直接取出,如果有异常就将异常 Result 返回出去
? 符仅用于返回值类型为 Result<T, E> 的函数,其中 E 类型必须和 ? 所处理的 Result 的 E 类型一致。
? 符的实际作用是将 Result 类非异常的值直接取出,如果有异常就将异常 Result 返回出去
? 符仅用于返回值类型为 Result<T, E> 的函数,其中 E 类型必须和 ? 所处理的 Result 的 E 类型一致。
fn f(i: i32) -> Result<i32, bool> {
if i >= 0 { Ok(i) }
else { Err(false) }
}
fn g(i: i32) -> Result<i32, bool> {
let t = f(i)?;
Ok(t) // 因为确定 t 不是 Err, t 在这里已经是 i32 类型
}
fn main() {
let r = g(10000);
if let Ok(v) = r {
println!("Ok: g(10000) = {}", v);
} else {
println!("Err");
}
}
if i >= 0 { Ok(i) }
else { Err(false) }
}
fn g(i: i32) -> Result<i32, bool> {
let t = f(i)?;
Ok(t) // 因为确定 t 不是 Err, t 在这里已经是 i32 类型
}
fn main() {
let r = g(10000);
if let Ok(v) = r {
println!("Ok: g(10000) = {}", v);
} else {
println!("Err");
}
}
kind方法
描述:可以把try块在独立的函数中实现,将所有的异常都传递出去解决。
需要判断Result的Err类型,获取Err类型的函数是kind()
需要判断Result的Err类型,获取Err类型的函数是kind()
use std::io;
use std::io::Read;
use std::fs::File;
fn read_text_from_file(path: &str) -> Result<String, io::Error> {
let mut f = File::open(path)?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
fn main() {
let str_file = read_text_from_file("hello.txt");
match str_file {
Ok(s) => println!("{}", s),
Err(e) => {
match e.kind() {
io::ErrorKind::NotFound => {
println!("No such file");
},
_ => {
println!("Cannot read the file");
}
}
}
}
}
use std::io::Read;
use std::fs::File;
fn read_text_from_file(path: &str) -> Result<String, io::Error> {
let mut f = File::open(path)?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
fn main() {
let str_file = read_text_from_file("hello.txt");
match str_file {
Ok(s) => println!("{}", s),
Err(e) => {
match e.kind() {
io::ErrorKind::NotFound => {
println!("No such file");
},
_ => {
println!("Cannot read the file");
}
}
}
}
}
智能指针
Box<T>
Rc<T>引用计数
不支持Send和Sync,无法跨线程
RefCell<T>
实现了Send,没实现Sync,可以跨线程
Arc<T>
支持Send和Sync
Cow<'a,B>:Clone-on-Write
MutexGuard<T>
通过Drop trait确保资源释放
Option<T>
Result<T>
Rust面向对象
实现了数据的封装与继承并能基于数据调用方法。
封装
封装就是对外显示的策略,在 Rust 中可以通过模块的机制来实现最外层的封装,并且每一个 Rust 文件都可以看作一个模块,模块内的元素可以通过 pub 关键字对外明示
类
使用结构体或枚举类来实现类的功能
pub struct ClassName {
pub field: Type,
}
pub impl ClassName {
fn some_method(&self) {
// 方法函数体
}
}
pub enum EnumName {
A,
B,
}
pub impl EnumName {
fn some_method(&self) {
}
}
pub field: Type,
}
pub impl ClassName {
fn some_method(&self) {
// 方法函数体
}
}
pub enum EnumName {
A,
B,
}
pub impl EnumName {
fn some_method(&self) {
}
}
second.rs
pub struct ClassName {
field: i32,
}
impl ClassName {
pub fn new(value: i32) -> ClassName {
ClassName {
field: value
}
}
pub fn public_method(&self) {
println!("from public method");
self.private_method();
}
fn private_method(&self) {
println!("from private method");
}
}
main.rs
mod second;
use second::ClassName;
fn main() {
let object = ClassName::new(1024);
object.public_method();
}
pub struct ClassName {
field: i32,
}
impl ClassName {
pub fn new(value: i32) -> ClassName {
ClassName {
field: value
}
}
pub fn public_method(&self) {
println!("from public method");
self.private_method();
}
fn private_method(&self) {
println!("from private method");
}
}
main.rs
mod second;
use second::ClassName;
fn main() {
let object = ClassName::new(1024);
object.public_method();
}
继承
继承是多态(Polymorphism)思想的实现,多态指的是编程语言可以处理多种类型数据的代码。
在 Rust 中,通过特性(trait)实现多态。有关特性的细节已在"特性"章节给出。但是特性无法实现属性的继承,只能实现类似于"接口"的功能,所以想继承一个类的方法最好在"子类"中定义"父类"的实例。
在 Rust 中,通过特性(trait)实现多态。有关特性的细节已在"特性"章节给出。但是特性无法实现属性的继承,只能实现类似于"接口"的功能,所以想继承一个类的方法最好在"子类"中定义"父类"的实例。
设计模式
单例:
struct Singleton {
data: String,
}
struct Singleton {
data: String,
}
once_cell:crate
use once_cell::sync::Lazy;
static INSTANCE: Lazy<Singleton> = Lazy::new(|| Singleton {
data: String::from("Singleton instance"),
});
// 获取单例实例
let instance = INSTANCE.clone();
static INSTANCE: Lazy<Singleton> = Lazy::new(|| Singleton {
data: String::from("Singleton instance"),
});
// 获取单例实例
let instance = INSTANCE.clone();
Arc + Mutex
use std::sync::{Arc, Mutex};
impl Singleton {
// 获取单例实例的方法
fn get_instance() -> Arc<Mutex<Singleton>> {
// 使用懒加载创建单例实例
// 这里使用了 Arc 和 Mutex 来实现线程安全的单例
// 只有第一次调用 get_instance 时会创建实例,之后都会返回已创建的实例
static mut INSTANCE: Option<Arc<Mutex<Singleton>>> = None;
unsafe {
INSTANCE.get_or_insert_with(|| {
Arc::new(Mutex::new(Singleton {
data: String::from("Singleton instance"),
}))
}).clone()
}
}
}
impl Singleton {
// 获取单例实例的方法
fn get_instance() -> Arc<Mutex<Singleton>> {
// 使用懒加载创建单例实例
// 这里使用了 Arc 和 Mutex 来实现线程安全的单例
// 只有第一次调用 get_instance 时会创建实例,之后都会返回已创建的实例
static mut INSTANCE: Option<Arc<Mutex<Singleton>>> = None;
unsafe {
INSTANCE.get_or_insert_with(|| {
Arc::new(Mutex::new(Singleton {
data: String::from("Singleton instance"),
}))
}).clone()
}
}
}
Rust文件与IO
接收命令行参数
在 Rust 中主函数是个无参函数,环境参数需要开发者通过 std::env 模块取出,过程十分简单
fn main() {
let args = std::env::args();
println!("{:?}", args);
}
let args = std::env::args();
println!("{:?}", args);
}
命令行输入
use std::io::stdin;
fn main() {
let mut str_buf = String::new();
stdin().read_line(&mut str_buf)
.expect("Failed to read line.");
println!("Your input line is \n{}", str_buf);
}
fn main() {
let mut str_buf = String::new();
stdin().read_line(&mut str_buf)
.expect("Failed to read line.");
println!("Your input line is \n{}", str_buf);
}
std::io:stdin 包含 read_line 读取方法,可以读取一行字符串到缓冲区,返回值都是 Result 枚举类,用于传递读取中出现的错误,所以常用 expect 或 unwrap 函数来处理错误。
文件读取
文本读取
use std::fs;
fn main() {
let text = fs::read_to_string("D:\\text.txt").unwrap();
println!("{}", text);
}
fn main() {
let text = fs::read_to_string("D:\\text.txt").unwrap();
println!("{}", text);
}
文件流读取
use std::io::prelude::*;
use std::fs;
fn main() {
let mut buffer = [0u8; 5];
let mut file = fs::File::open("D:\\text.txt").unwrap();
file.read(&mut buffer).unwrap();
println!("{:?}", buffer);
file.read(&mut buffer).unwrap();
println!("{:?}", buffer);
}
use std::fs;
fn main() {
let mut buffer = [0u8; 5];
let mut file = fs::File::open("D:\\text.txt").unwrap();
file.read(&mut buffer).unwrap();
println!("{:?}", buffer);
file.read(&mut buffer).unwrap();
println!("{:?}", buffer);
}
std::fs 模块中的 File 类是描述文件的类,可以用于打开文件,再打开文件之后,我们可以使用 File 的 read 方法按流读取文件的下面一些字节到缓冲区(缓冲区是一个 u8 数组),读取的字节数等于缓冲区的长度。
文件写入
一次性写入
use std::fs;
fn main() {
fs::write("D:\\text.txt", "FROM RUST PROGRAM")
.unwrap();
}
fn main() {
fs::write("D:\\text.txt", "FROM RUST PROGRAM")
.unwrap();
}
流式写入
//注意:打开的文件一定存放在可变的变量中才能使用 File 的方法!
use std::io::prelude::*;
use std::fs::File;
fn main() {
let mut file = File::create("D:\\text.txt").unwrap();
file.write(b"FROM RUST PROGRAM").unwrap();
}
use std::io::prelude::*;
use std::fs::File;
fn main() {
let mut file = File::create("D:\\text.txt").unwrap();
file.write(b"FROM RUST PROGRAM").unwrap();
}
//File 类中不存在 append 静态方法,但是我们可以使用 OpenOptions 来实现用特定方法打开文件
//? 符的实际作用是将 Result 类非异常的值直接取出,如果有异常就将异常 Result 返回出去
use std::io::prelude::*;
use std::fs::OpenOptions;
fn main() -> std::io::Result<()> {
let mut file = OpenOptions::new()
.append(true).open("D:\\text.txt")?;
file.write(b" APPEND WORD")?;
Ok(())
}
//OpenOptions 是一个灵活的打开文件的方法,它可以设置打开权限,除append 权限以外还有 read 权限和 write 权限
//? 符的实际作用是将 Result 类非异常的值直接取出,如果有异常就将异常 Result 返回出去
use std::io::prelude::*;
use std::fs::OpenOptions;
fn main() -> std::io::Result<()> {
let mut file = OpenOptions::new()
.append(true).open("D:\\text.txt")?;
file.write(b" APPEND WORD")?;
Ok(())
}
//OpenOptions 是一个灵活的打开文件的方法,它可以设置打开权限,除append 权限以外还有 read 权限和 write 权限
//以读写权限打开一个文件可以这样写:
use std::io::prelude::*;
use std::fs::OpenOptions;
fn main() -> std::io::Result<()> {
let mut file = OpenOptions::new()
.read(true).write(true).open("D:\\text.txt")?;
file.write(b"COVER")?;
Ok(())
}
use std::io::prelude::*;
use std::fs::OpenOptions;
fn main() -> std::io::Result<()> {
let mut file = OpenOptions::new()
.read(true).write(true).open("D:\\text.txt")?;
file.write(b"COVER")?;
Ok(())
}
Rust并发编程
线程
线程(thread)是一个程序中独立运行的一个部分。
Rust 中通过 std::thread::spawn 函数创建新线程:
use std::thread;
use std::time::Duration;
fn spawn_function() {
for i in 0..5 {
println!("spawned thread print {}", i);
thread::sleep(Duration::from_millis(1));
}
}
fn main() {
thread::spawn(spawn_function);
for i in 0..3 {
println!("main thread print {}", i);
thread::sleep(Duration::from_millis(1));
}
}
use std::time::Duration;
fn spawn_function() {
for i in 0..5 {
println!("spawned thread print {}", i);
thread::sleep(Duration::from_millis(1));
}
}
fn main() {
thread::spawn(spawn_function);
for i in 0..3 {
println!("main thread print {}", i);
thread::sleep(Duration::from_millis(1));
}
}
闭包
|参数1, 参数2, ...| -> 返回值类型 {
// 函数体
}
|参数1, 参数2, ...| -> 返回值类型 {
// 函数体
}
闭包是可以保存进变量或作为参数传递给其他函数的匿名函数,不同于函数的是,它允许捕获调用者作用域中的值。
//使用闭包(closures)来传递函数作为参数
use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(|| {
for i in 0..5 {
println!("spawned thread print {}", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 0..3 {
println!("main thread print {}", i);
thread::sleep(Duration::from_millis(1));
}
}
use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(|| {
for i in 0..5 {
println!("spawned thread print {}", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 0..3 {
println!("main thread print {}", i);
thread::sleep(Duration::from_millis(1));
}
}
fn main() {
let inc = |num: i32| -> i32 {
num + 1
};
println!("inc(5) = {}", inc(5));
}
let inc = |num: i32| -> i32 {
num + 1
};
println!("inc(5) = {}", inc(5));
}
//闭包可以省略类型声明使用 Rust 自动类型判断机制:
fn main() {
let inc = |num| {
num + 1
};
println!("inc(5) = {}", inc(5));
}
fn main() {
let inc = |num| {
num + 1
};
println!("inc(5) = {}", inc(5));
}
使用闭包来简化代码
传统函数实现
use std::thread;
use std::time::Duration;
fn muuuuu(intensity: u32) -> u32 {
println!("muuuu.....");
thread::sleep(Duration::from_secs(2));
intensity
}
fn workout(intensity: u32, random_number: u32) {
if intensity < 25 {
println!(
"先做 {} 个俯卧撑!", muuuuu(intensity)
);
} else {
println!(
"跑步 {} 分钟!", muuuuu(intensity)
);
}
}
fn main() {
// 强度
let intensity = 10;
// 随机值用来决定某个选择
let random_number = 7;
// 开始健身
workout(intensity, random_number);
}
use std::time::Duration;
fn muuuuu(intensity: u32) -> u32 {
println!("muuuu.....");
thread::sleep(Duration::from_secs(2));
intensity
}
fn workout(intensity: u32, random_number: u32) {
if intensity < 25 {
println!(
"先做 {} 个俯卧撑!", muuuuu(intensity)
);
} else {
println!(
"跑步 {} 分钟!", muuuuu(intensity)
);
}
}
fn main() {
// 强度
let intensity = 10;
// 随机值用来决定某个选择
let random_number = 7;
// 开始健身
workout(intensity, random_number);
}
闭包实现
注:
闭包中最后一行表达式返回的值,就是闭包执行后的返回值,因此 action() 调用返回了 intensity 的值 10
let action = ||... 只是把闭包赋值给变量 action,并不是把闭包执行后的结果赋值给 action,因此这里 action 就相当于闭包函数,可以跟函数一样进行调用:action()
注:
闭包中最后一行表达式返回的值,就是闭包执行后的返回值,因此 action() 调用返回了 intensity 的值 10
let action = ||... 只是把闭包赋值给变量 action,并不是把闭包执行后的结果赋值给 action,因此这里 action 就相当于闭包函数,可以跟函数一样进行调用:action()
use std::thread;
use std::time::Duration;
fn workout(intensity: u32, random_number: u32) {
let action = || {
println!("muuuu.....");
thread::sleep(Duration::from_secs(2));
intensity
};
if intensity < 25 {
println!(
"先做 {} 个俯卧撑!", action()
);
}else {
println!(
"跑步 {} 分钟!", action()
);
}
}
fn main() {
// 动作次数
let intensity = 10;
// 随机值用来决定某个选择
let random_number = 7;
// 开始健身
workout(intensity, random_number);
}
use std::time::Duration;
fn workout(intensity: u32, random_number: u32) {
let action = || {
println!("muuuu.....");
thread::sleep(Duration::from_secs(2));
intensity
};
if intensity < 25 {
println!(
"先做 {} 个俯卧撑!", action()
);
}else {
println!(
"跑步 {} 分钟!", action()
);
}
}
fn main() {
// 动作次数
let intensity = 10;
// 随机值用来决定某个选择
let random_number = 7;
// 开始健身
workout(intensity, random_number);
}
闭包的类型推导
函数需要指标所有参数和返回值类型,原因在于函数会作为API给用户。
闭包不会作为API对外提供,它可以享受编译器的类型推导能力,无需标注参数和返回值的类型,但也可以显示地给类型进行标注。
闭包不会作为API对外提供,它可以享受编译器的类型推导能力,无需标注参数和返回值的类型,但也可以显示地给类型进行标注。
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
结构体中的闭包
T: Fn(u32) -> u32
该闭包拥有一个u32类型的参数,同时返回一个u32类型的值。
//Fn(u32) -> u32 是一个特征,用来表示 T 是一个闭包类型
struct Cacher<T>
where
T: Fn(u32) -> u32,
{
query: T,
value: Option<u32>,
}
struct Cacher<T>
where
T: Fn(u32) -> u32,
{
query: T,
value: Option<u32>,
}
每一个闭包实例都有独属于自己的类型,即使于两个签名一模一样的闭包,它们的类型也是不同的,因此你无法用一个统一的类型来标注 query 闭包。
impl<T> Cacher<T>
where
T: Fn(u32) -> u32,
{
fn new(query: T) -> Cacher<T> {
Cacher {
query,
value: None,
}
}
// 先查询缓存值 `self.value`,若不存在,则调用 `query` 加载
fn value(&mut self, arg: u32) -> u32 {
match self.value {
Some(v) => v,
None => {
let v = (self.query)(arg);
self.value = Some(v);
v
}
}
}
}
where
T: Fn(u32) -> u32,
{
fn new(query: T) -> Cacher<T> {
Cacher {
query,
value: None,
}
}
// 先查询缓存值 `self.value`,若不存在,则调用 `query` 加载
fn value(&mut self, arg: u32) -> u32 {
match self.value {
Some(v) => v,
None => {
let v = (self.query)(arg);
self.value = Some(v);
v
}
}
}
}
struct Cacher<T,E>
where
T: Fn(E) -> E,
E: Copy
{
query: T,
value: Option<E>,
}
impl<T,E> Cacher<T,E>
where
T: Fn(E) -> E,
E: Copy
{
fn new(query: T) -> Cacher<T,E> {
Cacher {
query,
value: None,
}
}
fn value(&mut self, arg: E) -> E {
match self.value {
Some(v) => v,
None => {
let v = (self.query)(arg);
self.value = Some(v);
v
}
}
}
}
fn main() {
}
#[test]
fn call_with_different_values() {
let mut c = Cacher::new(|a| a);
let v1 = c.value(1);
let v2 = c.value(2);
assert_eq!(v2, 1);
}
where
T: Fn(E) -> E,
E: Copy
{
query: T,
value: Option<E>,
}
impl<T,E> Cacher<T,E>
where
T: Fn(E) -> E,
E: Copy
{
fn new(query: T) -> Cacher<T,E> {
Cacher {
query,
value: None,
}
}
fn value(&mut self, arg: E) -> E {
match self.value {
Some(v) => v,
None => {
let v = (self.query)(arg);
self.value = Some(v);
v
}
}
}
}
fn main() {
}
#[test]
fn call_with_different_values() {
let mut c = Cacher::new(|a| a);
let v1 = c.value(1);
let v2 = c.value(2);
assert_eq!(v2, 1);
}
捕获作用域中的值
例
fn main() {
let x = 4;
let equal_to_x = |z| z == x;
let y = 4;
assert!(equal_to_x(y));
}
let x = 4;
let equal_to_x = |z| z == x;
let y = 4;
assert!(equal_to_x(y));
}
闭包对内存的影响
当闭包从环境中捕获一个值时,会分配内存去存储这些值。对于有些场景来说,这种额外的内存分配会成为一种负担。与之相比,函数就不会去捕获这些环境值,因此定义和使用函数不会拥有这种内存负担。
所谓闭包是有一块内存区域,保存着捕获变量,捕获变量是按不可变引用,还是按可变引用,或者所有权转移的方式,对闭包的特性起着很大的决定性作用。然后,复制语义和移动语义的所有权转移的影响是不同的,有时候转移了所有权闭包反而更自由了。最后,闭包捕获变量存储区的特性是确定了,就看闭包行为如何使用这些闭包捕获变量了。
三种Fn特征
函数参数的三种传入方式
转移所有权、可变借用、不可变借用
Fn特征也有三种
FnOnce (call_once(self))
-- 该类型的闭包会拿走捕获的变量所有权
-- 如果实现了 Copy 特征,则是拷贝
-- 该类型的闭包会拿走捕获的变量所有权
-- 如果实现了 Copy 特征,则是拷贝
该类型的闭包会拿走被捕获变量的所有权。Once 顾名思义,说明该闭包只能运行一次。
只能调用一次,一旦调用,Closure将丧失所有权。
只能调用一次,一旦调用,Closure将丧失所有权。
例
fn fn_once<F>(func: F)
where
F: FnOnce(usize) -> bool,
{
println!("{}", func(3));
println!("{}", func(4));
}
fn main() {
let x = vec![1, 2, 3];
fn_once(|z|{z == x.len()})
}
//仅实现 FnOnce 特征的闭包在调用时会转移所有权,所以显然不能对已失去所有权的闭包变量进行二次调用,以上代码会报错
where
F: FnOnce(usize) -> bool,
{
println!("{}", func(3));
println!("{}", func(4));
}
fn main() {
let x = vec![1, 2, 3];
fn_once(|z|{z == x.len()})
}
//仅实现 FnOnce 特征的闭包在调用时会转移所有权,所以显然不能对已失去所有权的闭包变量进行二次调用,以上代码会报错
fn fn_once<F>(func: F)
where
F: FnOnce(usize) -> bool + Copy,// 改动在这里
{
println!("{}", func(3));
println!("{}", func(4));
}
fn main() {
let x = vec![1, 2, 3];
fn_once(|z|{z == x.len()})
}
//func 的类型 F 实现了 Copy 特征,调用时使用的将是它的拷贝,所以并没有发生所有权的转移。
where
F: FnOnce(usize) -> bool + Copy,// 改动在这里
{
println!("{}", func(3));
println!("{}", func(4));
}
fn main() {
let x = vec![1, 2, 3];
fn_once(|z|{z == x.len()})
}
//func 的类型 F 实现了 Copy 特征,调用时使用的将是它的拷贝,所以并没有发生所有权的转移。
move
-- 强制闭包取得捕获变量的所有权
-- 强制闭包取得捕获变量的所有权
强制闭包取得捕获变量的所有权,可以在参数列表前添加 move 关键字。
就算是i32,也会被转移进来。
就算是i32,也会被转移进来。
//这种用法通常用于闭包的生命周期大于捕获变量的生命周期时,例如将闭包返回或移入其他线程。
use std::thread;
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();
use std::thread;
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();
FnMut (call_mut(&mut self))
--它以可变借用的方式捕获了环境中的值
--它以可变借用的方式捕获了环境中的值
它以可变借用的方式捕获了环境中的值。
能调用多次,每次调用Closure的内部状态会变化。
能调用多次,每次调用Closure的内部状态会变化。
例
fn main() {
let mut s = String::new();
let update_string = |str| s.push_str(str);//需要优化
update_string("hello");
println!("{:?}",s);
}
//编译器给出了非常清晰的提示,想要在闭包内部捕获可变借用,需要把该闭包声明为可变类型,也就是 update_string 要修改为 mut update_string
let mut s = String::new();
let update_string = |str| s.push_str(str);//需要优化
update_string("hello");
println!("{:?}",s);
}
//编译器给出了非常清晰的提示,想要在闭包内部捕获可变借用,需要把该闭包声明为可变类型,也就是 update_string 要修改为 mut update_string
fn main() {
let mut s = String::new();
let mut update_string = |str| s.push_str(str);
update_string("hello");
println!("{:?}",s);
}
//优化加了mut
let mut s = String::new();
let mut update_string = |str| s.push_str(str);
update_string("hello");
println!("{:?}",s);
}
//优化加了mut
fn main() {
let mut s = String::new();
let update_string = |str| s.push_str(str);//未使用mut关键词修饰
exec(update_string);
println!("{:?}",s);
}
fn exec<'a, F: FnMut(&'a str)>(mut f: F) {//接收可变的闭包
f("hello")
}
//为什么update_string没有用mut修饰却是一个可变类型的闭包?事实上,FnMut只是trait的名字,声明变量为FnMut和要不要mut没啥关系,FnMut是推导出的特征类型,mut是rust语言层面的一个修饰符,用于声明一个绑定是可变的。Rust从特征类型系统和语言修饰符两方面保障了我们的程序正确运行。
//在使用FnMut类型闭包时需要捕获外界的可变借用,因此我们常常搭配mut修饰符使用
let mut s = String::new();
let update_string = |str| s.push_str(str);//未使用mut关键词修饰
exec(update_string);
println!("{:?}",s);
}
fn exec<'a, F: FnMut(&'a str)>(mut f: F) {//接收可变的闭包
f("hello")
}
//为什么update_string没有用mut修饰却是一个可变类型的闭包?事实上,FnMut只是trait的名字,声明变量为FnMut和要不要mut没啥关系,FnMut是推导出的特征类型,mut是rust语言层面的一个修饰符,用于声明一个绑定是可变的。Rust从特征类型系统和语言修饰符两方面保障了我们的程序正确运行。
//在使用FnMut类型闭包时需要捕获外界的可变借用,因此我们常常搭配mut修饰符使用
Fn (call(&self))
--它以不可变借用的方式捕获环境中的值
--它以不可变借用的方式捕获环境中的值
Fn 特征,它以不可变借用的方式捕获环境中的值。
能多次调用,每次调用Closure不变。
能多次调用,每次调用Closure不变。
例
fn main() {
let s = "hello, ".to_string();
let update_string = |str| println!("{},{}",s,str);
exec(update_string);
println!("{:?}",s);
}
fn exec<'a, F: Fn(String) -> ()>(f: F) {
f("world".to_string())
}
let s = "hello, ".to_string();
let update_string = |str| println!("{},{}",s,str);
exec(update_string);
println!("{:?}",s);
}
fn exec<'a, F: Fn(String) -> ()>(f: F) {
f("world".to_string())
}
move和Fn
一个闭包实现了哪种 Fn 特征取决于该闭包如何使用被捕获的变量,而不是取决于闭包如何捕获它们。
例
fn main() {
let s = String::new();
let update_string = move || println!("{}",s); //闭包实现了FnOnce,也实现了Fn特征,因为s是不可变借用
exec(update_string);
}
fn exec<F: FnOnce()>(f: F) {
f()
}
//在上面的闭包中使用了 move 关键字,所以我们的闭包捕获了它,但是由于闭包对 s 的使用仅仅是不可变借用,因此该闭包实际上还实现了 Fn 特征。
let s = String::new();
let update_string = move || println!("{}",s); //闭包实现了FnOnce,也实现了Fn特征,因为s是不可变借用
exec(update_string);
}
fn exec<F: FnOnce()>(f: F) {
f()
}
//在上面的闭包中使用了 move 关键字,所以我们的闭包捕获了它,但是由于闭包对 s 的使用仅仅是不可变借用,因此该闭包实际上还实现了 Fn 特征。
fn main() {
let s = String::new();
let update_string = move || println!("{}",s);
exec(update_string);
}
fn exec<F: Fn()>(f: F) {
f()
}
//该闭包不仅仅实现了 FnOnce 特征,还实现了 Fn 特征,将代码修改成下面这样,依然可以编译:
let s = String::new();
let update_string = move || println!("{}",s);
exec(update_string);
}
fn exec<F: Fn()>(f: F) {
f()
}
//该闭包不仅仅实现了 FnOnce 特征,还实现了 Fn 特征,将代码修改成下面这样,依然可以编译:
有move或没有move的情况
没有move,自动判定变量的捕获方式
1. 若捕获变量是复制语义,即实现了Copy,比如i32,则按引用捕获,可变引用还是不可变引用,看闭包对该变量的使用行为;
2. 若捕获变量是移动语义,即没实现Copy,再看闭包行为
2.1. 若是没消费变量所有权,就算是改变了变量,都按引用捕获
2.2. 若是消费了变量所有权,则按所有权捕获
2. 若捕获变量是移动语义,即没实现Copy,再看闭包行为
2.1. 若是没消费变量所有权,就算是改变了变量,都按引用捕获
2.2. 若是消费了变量所有权,则按所有权捕获
没move关键字,不轻易捕获所有权,这是编译器默认行为
没有move,也有可能是所有权捕获
let s = "hello".to_string();
let c = || {
println!("s is {:?}", s);
s // 有这行后,主动对s的所有权进行了消费转移,就对s进行了所有权捕获;
// 没这行则会变成不可变引用捕获
};
let c = || {
println!("s is {:?}", s);
s // 有这行后,主动对s的所有权进行了消费转移,就对s进行了所有权捕获;
// 没这行则会变成不可变引用捕获
};
有move,也不一定是FnOnce
看捕获的类型,复制语义和移动语义的大不相同
如果move的是一个i32,那闭包里存个i32,复制语义的,多次调用也没问题,俨然一个Fn
如果move的本身是一个引用,那就看闭包行为会不会改变它,改变它了,是一个FnMut
如果move的本身是一个引用,那就看闭包行为会不会改变它,改变它了,是一个FnMut
let mut x = 5;
let mut c1 = move || x += 1; //有move是复制捕获,是impl FnMut
let mut c2 = || x += 1; //没有move,闭包行为改变了x,是impl FuMut
let mut c1 = move || x += 1; //有move是复制捕获,是impl FnMut
let mut c2 = || x += 1; //没有move,闭包行为改变了x,是impl FuMut
如果move的是一个所有权对象,如String,那也得看闭包行为怎么用这个对象,是只读式的,还是消费掉所有权
let s = "hello".to_string();
let c = move || println!("s is {:?}", s);// 虽然是所有权捕获,但依然可以多次调用,Fn
c();
c();
let c = move || println!("s is {:?}", s);// 虽然是所有权捕获,但依然可以多次调用,Fn
c();
c();
对闭包变量使用mut修饰,不一定是FnMut
let s = "hello".to_string();
let mut c = || println!("s is {:?}", s); // 除了编译器会提供一个多余的mut告警,c还是Fn
let mut c = || println!("s is {:?}", s); // 除了编译器会提供一个多余的mut告警,c还是Fn
三种Fn的关系
实际上,一个闭包并不仅仅实现某一种Fn特征
1.所有的闭包都自动实现了 FnOnce 特征,因此任何一个闭包都至少可以被调用一次
2.没有移出所捕获变量的所有权的闭包自动实现了 FnMut 特征
3.不需要对捕获变量进行改变的闭包自动实现了 Fn 特征
2.没有移出所捕获变量的所有权的闭包自动实现了 FnMut 特征
3.不需要对捕获变量进行改变的闭包自动实现了 Fn 特征
pub trait Fn<Args> : FnMut<Args> {
extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}
pub trait FnMut<Args> : FnOnce<Args> {
extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}
pub trait FnOnce<Args> {
type Output;
extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}
//看到没?从特征约束能看出来 Fn 的前提是实现 FnMut,FnMut 的前提是实现 FnOnce,因此要实现 Fn 就要同时实现 FnMut 和 FnOnce,这段源码从侧面印证了之前规则的正确性。
//从源码中还能看出一点:Fn 获取 &self,FnMut 获取 &mut self,而 FnOnce 获取 self。 在实际项目中,建议先使用 Fn 特征,然后编译器会告诉你正误以及该如何选择。
extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}
pub trait FnMut<Args> : FnOnce<Args> {
extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}
pub trait FnOnce<Args> {
type Output;
extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}
//看到没?从特征约束能看出来 Fn 的前提是实现 FnMut,FnMut 的前提是实现 FnOnce,因此要实现 Fn 就要同时实现 FnMut 和 FnOnce,这段源码从侧面印证了之前规则的正确性。
//从源码中还能看出一点:Fn 获取 &self,FnMut 获取 &mut self,而 FnOnce 获取 self。 在实际项目中,建议先使用 Fn 特征,然后编译器会告诉你正误以及该如何选择。
Fn/FnMut/FnOnce跟Copy/Clone关系不大
Fn/FnMut/FnOnce只是限定了可否重复调用,以何种方式调用,即调用时是否改变了闭包存储区的状态。至于能不能Copy/Clone,是另外一回事。
let s = "hello".to_string();
let c1 = move || println!("{s}"); // Fn,但没有实现Copy,因闭包捕获变量存储区有所有权
let mut x = 5;
let mut c = || x += 1; // FnMut,但没实现Copy,因为可变引用,要满足**可变不共享**规则
let c1 = move || println!("{s}"); // Fn,但没有实现Copy,因闭包捕获变量存储区有所有权
let mut x = 5;
let mut c = || x += 1; // FnMut,但没实现Copy,因为可变引用,要满足**可变不共享**规则
闭包作为函数返回值
fn factory(x:i32) -> Box<dyn Fn(i32) -> i32> {
let num = 5;
if x > 1{
Box::new(move |x| x + num)
} else {
Box::new(move |x| x - num)
}
}
let num = 5;
if x > 1{
Box::new(move |x| x + num)
} else {
Box::new(move |x| x - num)
}
}
闭包的生命周期
join方法
join 方法可以使子线程运行结束后再停止运行程序。
join 方法可以使子线程运行结束后再停止运行程序。
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 0..5 {
println!("spawned thread print {}", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 0..3 {
println!("main thread print {}", i);
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap();
}
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 0..5 {
println!("spawned thread print {}", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 0..3 {
println!("main thread print {}", i);
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap();
}
move强制所有权迁移
在子线程中尝试使用当前函数的资源,这一定是错误的!因为所有权机制禁止这种危险情况的产生,它将破坏所有权机制销毁资源的一定性。我们可以使用闭包的 move 关键字来处理
use std::thread;
fn main() {
let s = "hello";
let handle = thread::spawn(move || {
println!("{}", s);
});
handle.join().unwrap();
}
fn main() {
let s = "hello";
let handle = thread::spawn(move || {
println!("{}", s);
});
handle.join().unwrap();
}
消息传递
Rust 中一个实现消息传递并发的主要工具是通道(channel),通道有两部分组成,一个发送者(transmitter)和一个接收者(receiver)。
std::sync::mpsc 包含了消息传递的方法:
std::sync::mpsc 包含了消息传递的方法:
//子线程获得了主线程的发送者 tx,并调用了它的 send 方法发送了一个字符串,然后主线程就通过对应的接收者 rx 接收到了。
use std::thread;
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
});
let received = rx.recv().unwrap();
println!("Got: {}", received);
}
use std::thread;
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
});
let received = rx.recv().unwrap();
println!("Got: {}", received);
}
Rust组织管理
重要组织概念
任务一门编程语言如果不能组织代码都是难以深入的
重要组织概念
工作空间(WorkSpace)
对于大型项目,可以进一步将多个包联合在一起,组织成工作空间
项目(Package)--工程、软件包
-----
cargo是指集装箱里装的货物
crate代表箱子
package有软件包的意思,就是一堆软件打包放一起
-----
cargo是指集装箱里装的货物
crate代表箱子
package有软件包的意思,就是一堆软件打包放一起
1. 使用cargo执行new命令创建Rust工程时,工程目录下会建立一个Cargo.toml文件。
2. 工程的实质就是一个包,必须由一个Cargo.toml文件来管理,该文件描述了包的基本信息以及依赖项。
2. 工程的实质就是一个包,必须由一个Cargo.toml文件来管理,该文件描述了包的基本信息以及依赖项。
一个包最多包含一个库(lib)“箱”,可以包含任意数量的二进制“箱”
使用cargo new命令创建完包之后,src目录下会生成一个main.rs源文件,Cargo默认这个文件为二进制箱的根
一个 Cargo 提供的 feature,可以用来构建、测试和分享包。
Package就是一个项目,它包含有独立的Cargo.toml文件,以及因为功能性被组织在一起的一个或多个包。
一个 Package 只能包含一个库(library)类型的包,但是可以包含多个二进制可执行类型的包。
Package就是一个项目,它包含有独立的Cargo.toml文件,以及因为功能性被组织在一起的一个或多个包。
一个 Package 只能包含一个库(library)类型的包,但是可以包含多个二进制可执行类型的包。
例:二进制Package
//Cargo 为我们创建了一个名称是 my-project 的 Package
$ cargo new my-project
Created binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
$ ls my-project/src
main.rs
$ cargo new my-project
Created binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
$ ls my-project/src
main.rs
Cargo 为我们创建了一个名称是 my-project 的 Package,同时在其中创建了 Cargo.toml 文件,可以看一下该文件,里面并没有提到 src/main.rs 作为程序的入口,原因是 Cargo 有一个惯例:src/main.rs 是二进制包的根文件,该二进制包的包名跟所属 Package 相同,在这里都是 my-project,所有的代码执行都从该文件中的 fn main() 函数开始。
易混淆的Package和包
只要你牢记 Package 是一个项目工程,而包只是一个编译单元,基本上也就不会混淆这个两个概念了:src/main.rs 和 src/lib.rs 都是编译单元,因此它们都是包。
包(Crate)--也就是库文件/可执行文件
1. 是二进制程序文件或者库文件,存在于“包”中。
2. 是树状结构的,它的树根是编译器开始运行时编译的源文件所编译的程序
3. main.rs是二进制箱的根
2. 是树状结构的,它的树根是编译器开始运行时编译的源文件所编译的程序
3. main.rs是二进制箱的根
一个由多个模块组成的树形结构,可以作为三方库进行分发,也可以生成可执行文件进行运行
包是一个独立的可编译单元,它编译后会生成一个可执行文件或者一个库。
一个包会将相关联的功能打包在一起,使得该功能可以很方便的在多个项目中分享。
同一包中不能有同名的类型
一个包会将相关联的功能打包在一起,使得该功能可以很方便的在多个项目中分享。
同一包中不能有同名的类型
例:将该包通过 use rand,引入到当前项目的作用域中,就可以在项目中使用 rand 的功能:rand::XXX。
模块(Module)--如同java的类
1. 一个软件工程来说,需要组织规范来进行组织,主要结构往往是树
2. java组织功能模块的主要单位是类
3. 组织单位可以层层包含,像文件系统的目录结构一样
2. java组织功能模块的主要单位是类
3. 组织单位可以层层包含,像文件系统的目录结构一样
可以一个文件多个模块,也可以一个文件一个模块,模块可以被认为是真实项目中的代码组织单元
创建嵌套模块
使用 mod 关键字来创建新模块,后面紧跟着模块名称
模块可以嵌套,这里嵌套的原因是招待客人和服务都发生在前厅,因此我们的代码模拟了真实场景
模块中可以定义各种 Rust 类型,例如函数、结构体、枚举、特征等
所有模块均定义在同一个文件中
将功能相关的代码组织到一起,通过一个模块名称来说明这些代码为何组织在一起
模块可以嵌套,这里嵌套的原因是招待客人和服务都发生在前厅,因此我们的代码模拟了真实场景
模块中可以定义各种 Rust 类型,例如函数、结构体、枚举、特征等
所有模块均定义在同一个文件中
将功能相关的代码组织到一起,通过一个模块名称来说明这些代码为何组织在一起
// 餐厅前厅,用于吃饭
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 front_of_house {
mod hosting {
fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
模块树
src/main.rs 和 src/lib.rs 被称为包根(crate root)
展示了模块之间彼此的嵌套关系
用路径引用模块
绝对路径,从包根开始,路径名以包名或者 crate 作为开头
相对路径,从当前模块开始,以 self,super 或当前模块的标识符作为开头
相对路径,从当前模块开始,以 self,super 或当前模块的标识符作为开头
pub关键字
控制模块和模块中指定项的可见性。
使用super引用模块
调用父模块中的函数
fn serve_order() {}
// 厨房模块
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::serve_order();
}
fn cook_order() {}
}
// 厨房模块
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::serve_order();
}
fn cook_order() {}
}
使用self引用模块
就是引用自身模块中的项
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() {}
}
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,它的所有字段也将对外可见
将枚举设置为 pub,它的所有字段也将对外可见
模块与文件分离
当模块变多或者变大时,需要将模块放入一个单独的文件中,让代码更好维护。
当一个模块有许多子模块时,我们也可以通过文件夹的方式来组织这些子模块。
如果需要将文件夹作为一个模块,我们需要进行显示指定暴露哪些子模块
2种方法
1. 在 front_of_house 目录里创建一个 mod.rs,如果你使用的 rustc 版本 1.30 之前,这是唯一的方法。mod.rs 文件是一种特殊的文件名,它通常用于实现模块(module)的组织和管理
my_module/
├── mod.rs
├── foo.rs
└── bar.rs
├── mod.rs
├── foo.rs
└── bar.rs
mod.rs 文件用于定义 my_module 模块的公共 API,而 foo.rs 和 bar.rs 文件则用于实现这些 API。
在 mod.rs 文件中,通常会使用 pub use 语法来导出该模块中的公共 API,例如:
pub use foo::my_function1;
pub use bar::my_function2;
在这个示例中,我们使用 pub use 语法将 foo 模块中的 my_function1() 函数和 bar 模块中的 my_function2() 函数导出为 my_module 模块的公共 API。
总之,mod.rs 文件是 Rust 语言中非常常用的一个文件,它可以帮助我们组织和管理模块,并实现代码的可重用性和可读性。
在 mod.rs 文件中,通常会使用 pub use 语法来导出该模块中的公共 API,例如:
pub use foo::my_function1;
pub use bar::my_function2;
在这个示例中,我们使用 pub use 语法将 foo 模块中的 my_function1() 函数和 bar 模块中的 my_function2() 函数导出为 my_module 模块的公共 API。
总之,mod.rs 文件是 Rust 语言中非常常用的一个文件,它可以帮助我们组织和管理模块,并实现代码的可重用性和可读性。
2. 在 front_of_house 同级目录里创建一个与模块(目录)同名的 rs 文件 front_of_house.rs,在新版本里,更建议使用这样的命名方式来避免项目中存在大量同名的 mod.rs 文件。
库library
创建一个库类型的Package
$ cargo new my-lib --lib
Created library `my-lib` package
$ ls my-lib
Cargo.toml
src
$ ls my-lib/src
lib.rs
Created library `my-lib` package
$ ls my-lib
Cargo.toml
src
$ ls my-lib/src
lib.rs
举个例子
src/main.rs
binary crate的crate root
crate名与package名相同
src/lib.rs
package包含一个library crate
library crate的crate root
crate名与package名相同
Cargo把Crate root文件交给rustc构建library或binary
一个Package可以同时包含src/main.rs和src/lib.rs
这就说明上面这个例子包含一个binary crate,一个library crate
名称与package名相同
一个package可以有多个binary crate:
文件放在src/bin
每个文件是单独的binary crate
binary crate的crate root
crate名与package名相同
src/lib.rs
package包含一个library crate
library crate的crate root
crate名与package名相同
Cargo把Crate root文件交给rustc构建library或binary
一个Package可以同时包含src/main.rs和src/lib.rs
这就说明上面这个例子包含一个binary crate,一个library crate
名称与package名相同
一个package可以有多个binary crate:
文件放在src/bin
每个文件是单独的binary crate
使用use及受限可见性
描述:简化函数调用形式,可以使用use关键字把路径提前引入到当前作用域中
基本引入方式
绝对路径引入模块
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;//引入模块
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;//引入模块
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
相对路径引入模块中的函数
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use front_of_house::hosting::add_to_waitlist;//引入函数
pub fn eat_at_restaurant() {
add_to_waitlist();
add_to_waitlist();
add_to_waitlist();
}
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use front_of_house::hosting::add_to_waitlist;//引入函数
pub fn eat_at_restaurant() {
add_to_waitlist();
add_to_waitlist();
add_to_waitlist();
}
注:优先使用最细粒度(引入函数、结构体等)的引用方式,如果引起了某种麻烦(例如前面两种情况),再使用引入模块的方式。
避免同名引用
描述:要保证同一个模块中不存在同名项
2种方式处理
使用父模块的方式来调用
/使用父模块方式调用
use std::fmt;
use std::io;
fn function1() -> fmt::Result {/
// --snip--
}
fn function2() -> io::Result<()> {
// --snip--
}
use std::fmt;
use std::io;
fn function1() -> fmt::Result {/
// --snip--
}
fn function2() -> io::Result<()> {
// --snip--
}
as别名引用
//使用as别名引用
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
// --snip--
}
fn function2() -> IoResult<()> {
// --snip--
}
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
// --snip--
}
fn function2() -> IoResult<()> {
// --snip--
}
引入项再导出
使用 pub use 即可实现。这里 use 代表引入 hosting 模块到当前作用域,pub 表示将该引入的内容再度设置为可见。
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
//use在当前作用域有用,只是一个简写,加上了pub就会对外暴露出去,当前模块的父模块之类可以使用
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
//use在当前作用域有用,只是一个简写,加上了pub就会对外暴露出去,当前模块的父模块之类可以使用
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
pub mod foo { pub use bar; }
模块foo对外展示了一个bar,其他人可以使用foo::bar。如果你不pub use,其他人就用不了foo::bar。这个可以理解吧
模块foo对外展示了一个bar,其他人可以使用foo::bar。如果你不pub use,其他人就用不了foo::bar。这个可以理解吧
使用第三方包
修改 Cargo.toml 文件,在 [dependencies] 区域添加一行:rand = "0.8.3"
此时,如果你用的是 VSCode 和 rust-analyzer 插件,该插件会自动拉取该库,你可能需要等它完成后,再进行下一步(VSCode 左下角有提示)
此时,如果你用的是 VSCode 和 rust-analyzer 插件,该插件会自动拉取该库,你可能需要等它完成后,再进行下一步(VSCode 左下角有提示)
use rand::Rng;
fn main() {
let secret_number = rand::thread_rng().gen_range(1..101);
}
fn main() {
let secret_number = rand::thread_rng().gen_range(1..101);
}
使用{}简化引入方式
使用 {} 来一起引入进来,在大型项目中,使用这种方式来引入,可以减少大量 use 的使用
use std::collections::HashMap;
use std::collections::BTreeMap;
use std::collections::HashSet;
use std::cmp::Ordering;
use std::io;
use std::collections::BTreeMap;
use std::collections::HashSet;
use std::cmp::Ordering;
use std::io;
use std::collections::{HashMap,BTreeMap,HashSet};
use std::{cmp::Ordering, io};
use std::{cmp::Ordering, io};
同时引入模块和模块中的项
use std::io;
use std::io::Write;
use std::io::Write;
use std::io::{self, Write};
self
2个用途
use self::xxx,表示加载当前模块中的 xxx。此时 self 可省略
use xxx::{self, yyy},表示,加载当前路径下模块 xxx 本身,以及模块 xxx 下的 yyy
use xxx::{self, yyy},表示,加载当前路径下模块 xxx 本身,以及模块 xxx 下的 yyy
使用*引入模块下的所有项
use std::collections::*;
引入 std::collections 模块下的所有公共项,这些公共项自然包含了 HashMap,HashSet 等想手动引入的集合类型。
注:当使用 * 来引入的时候要格外小心,因为你很难知道到底哪些被引入到了当前作用域中,有哪些会和你自己程序中的名称相冲突
use std::collections::*;
struct HashMap;
fn main() {
let mut v = HashMap::new();
v.insert("a", 1);
}
struct HashMap;
fn main() {
let mut v = HashMap::new();
v.insert("a", 1);
}
std::collection::HashMap 被 * 引入到当前作用域,但是由于存在另一个同名的结构体,因此 HashMap::new 根本不存在
受限的可见性
控制了模块中哪些内容可以被外部看见,但是在实际使用时,光被外面看到还不行,我们还想控制哪些人能看
pub(crate) item;
item 虽然是对外可见的,但是只在当前包内可见,外部包无法引用到该 item。
让某一项可以在整个包中被使用
在包根中定义一个非 pub 类型的 X(父模块的项对子模块都是可见的,因此包根中的项对模块树上的所有模块都可见)
在子模块中定义一个 pub 类型的 Y,同时通过 use 将其引入到包根
mod a {
pub mod b {
pub fn c() {
println!("{:?}",crate::X);//可直接使用包根中的X
}
#[derive(Debug)]
pub struct Y; //要用use引入到包根
}
}
#[derive(Debug)]
struct X; //父模块中的X
use a::b::Y; //子模块中的Y
fn d() {
println!("{:?}",Y);
}
pub mod b {
pub fn c() {
println!("{:?}",crate::X);//可直接使用包根中的X
}
#[derive(Debug)]
pub struct Y; //要用use引入到包根
}
}
#[derive(Debug)]
struct X; //父模块中的X
use a::b::Y; //子模块中的Y
fn d() {
println!("{:?}",Y);
}
对于特定模块可见
通过 pub(in crate::a) 的方式,我们指定了模块 c 和常量 J 的可见范围都只是 a 模块中,a 之外的模块是完全访问不到它们的。
pub mod a {
pub const I: i32 = 3;
fn semisecret(x: i32) -> i32 {
use self::b::c::J;
x + J
}
pub fn bar(z: i32) -> i32 {
semisecret(I) * z
}
pub fn foo(y: i32) -> i32 {
semisecret(I) + y
}
mod b {
pub(in crate::a) mod c {
pub(in crate::a) const J: i32 = 4;
}
}
}
pub const I: i32 = 3;
fn semisecret(x: i32) -> i32 {
use self::b::c::J;
x + J
}
pub fn bar(z: i32) -> i32 {
semisecret(I) * z
}
pub fn foo(y: i32) -> i32 {
semisecret(I) + y
}
mod b {
pub(in crate::a) mod c {
pub(in crate::a) const J: i32 = 4;
}
}
}
限制可见性语法
pub(crate) 或 pub(in crate::a) 就是限制可见性语法,前者是限制在整个包内可见,后者是通过绝对路径,限制在包内的某个模块内可见,总结一下:
pub 意味着可见性无任何限制
pub(crate) 表示在当前包可见
pub(self) 在当前模块可见
pub(super) 在父模块可见
pub(in <path>) 表示在某个路径代表的模块中可见,其中 path 必须是父模块或者祖先模块
pub 意味着可见性无任何限制
pub(crate) 表示在当前包可见
pub(self) 在当前模块可见
pub(super) 在父模块可见
pub(in <path>) 表示在某个路径代表的模块中可见,其中 path 必须是父模块或者祖先模块
模块例子
1. 文件系统中,目录结构以斜线在路径字符串中表示对象的位置
2. Rust中路径分隔符是::
3. 绝对路径从crate关键词开始描述,相对路径从self或super关键词或一个标识符开始描述
2. Rust中路径分隔符是::
3. 绝对路径从crate关键词开始描述,相对路径从self或super关键词或一个标识符开始描述
mod nation {
mod government {
fn govern() {}
}
mod congress {
fn legislate() {}
}
mod court {
fn judicial() {}
}
}
mod government {
fn govern() {}
}
mod congress {
fn legislate() {}
}
mod court {
fn judicial() {}
}
}
绝对路径:crate::nation::government::govern();
相对路径:nation::government::govern();
相对路径:nation::government::govern();
访问权限
公共(public)
私有(private)
模块中的结构体
如果模块中定义了结构体,结构体除了其本身是私有的以外,其字段也默认是私有的。所以如果想使用模块中的结构体以及其字段,需要 pub 声明:
mod nation {
pub mod government {
pub fn govern() {}
}
mod congress {
pub fn legislate() {}
}
mod court {
fn judicial() {
super::congress::legislate();
}
}
}
fn main() {
nation::government::govern();
}
pub mod government {
pub fn govern() {}
}
mod congress {
pub fn legislate() {}
}
mod court {
fn judicial() {
super::congress::legislate();
}
}
}
fn main() {
nation::government::govern();
}
模块中的枚举
mod SomeModule {
pub enum Person {
King {
name: String
},
Queen
}
}
fn main() {
let person = SomeModule::Person::King{
name: String::from("Blue")
};
match person {
SomeModule::Person::King {name} => {
println!("{}", name);
}
_ => {}
}
}
pub enum Person {
King {
name: String
},
Queen
}
}
fn main() {
let person = SomeModule::Person::King{
name: String::from("Blue")
};
match person {
SomeModule::Person::King {name} => {
println!("{}", name);
}
_ => {}
}
}
引入模块
mod 模块名;
// main.rs
mod second_module; //引入模块
fn main() {
println!("This is the main module.");
println!("{}", second_module::message());
}
// second_module.rs
pub fn message() -> String {
String::from("This is the 2nd module.")
}
mod second_module; //引入模块
fn main() {
println!("This is the main module.");
println!("{}", second_module::message());
}
// second_module.rs
pub fn message() -> String {
String::from("This is the 2nd module.")
}
关键字
use关键字(解决局部模块路径长问题)
use关键字能够将模块标识符引入当前作用域
【解决局部模块路径过长的问题】
【解决局部模块路径过长的问题】
mod nation {
pub mod government {
pub fn govern() {}
}
}
use crate::nation::government::govern;
fn main() {
govern();
}
pub mod government {
pub fn govern() {}
}
}
use crate::nation::government::govern;
fn main() {
govern();
}
as关键字(同名添加别名)
有些情况下存在两个相同的名称,且同样需要导入,可以使用 as 关键字为标识符添加别名
use关键字可以与pub关键字配合使用
mod nation {
pub mod government {
pub fn govern() {}
}
pub use government::govern; //配合使用
}
fn main() {
nation::govern();
}
pub mod government {
pub fn govern() {}
}
pub use government::govern; //配合使用
}
fn main() {
nation::govern();
}
引入标准库
导入系统库
use std::f64::consts::PI;
fn main() {
println!("{}", (PI / 2.0).sin());
}
fn main() {
println!("{}", (PI / 2.0).sin());
}
所有的系统库模块都是被默认导入的,所以在使用的时候只需要使用 use 关键字简化路径就可以方便的使用了。
Rust基本概念
1. 函数体用花括号 {} 包裹,表达式之间用分号 ; 分隔
2.点 . 运算符、:: 运算符
1.访问结构体的成员函数或者变量使用点 . 运算符
2.访问命名空间(namespace)或者对象的静态函数使用双冒号 :: 运算符
2.访问命名空间(namespace)或者对象的静态函数使用双冒号 :: 运算符
3.简化引用(函数、数据类型)
要简化对命名空间内部的函数或者数据类型的引用,可以使用 use 关键字,比如 use std::fs
4. 变量的不可变性
5. 函数返回值
函数最后一个表达式就是函数的返回值
6. 派生宏
#[derive(Debug, Copy, Clone)]
#[derive()] 是 Rust 编程语言中的一个属性(attribute),用于自动为结构体(struct)或枚举(enum)实现特定的 traits(特性)
简化了手动编写这些代码:Traits 定义了类型可以实现的行为,使用 #[derive()] 属性可以让编译器为我们生成实现这些 traits 所需的代码。
简化了手动编写这些代码:Traits 定义了类型可以实现的行为,使用 #[derive()] 属性可以让编译器为我们生成实现这些 traits 所需的代码。
可派生的traits
Debug
通过实现 Debug trait,可以使用 println!("{:?}", my_struct) 来打印结构体的调试信息。
Clone
通过实现 Clone trait,可以使用 my_struct.clone() 创建结构体的克隆副本。
PartialEq、Eq
通过实现 PartialEq trait,可以进行结构体的部分相等性比较,而 Eq trait 则实现了完全相等性比较。
PartialOrd/Ord
通过实现 PartialOrd trait,可以对结构体进行部分有序性比较,而 Ord trait 实现了完全有序性比较。
Copy
使类型具有 “复制语义”(copy semantics)而非 “移动语义”(move semantics)
Hash
从 &T 计算哈希值(hash)
Default
创建数据类型的一个空实例
Clone 让数据结构可以被复制,而 Copy 则让数据结构可以在参数传递的时候自动按字节拷贝
7.错误处理
把错误封装在 Result 类型中,同时提供了 ? 操作符来传播错误
Result 类型是一个泛型数据结构,T 代表成功执行返回的结果类型,E 代表错误类型。
Result 类型是一个泛型数据结构,T 代表成功执行返回的结果类型,E 代表错误类型。
不过我们使用了 unwrap() 方法,只关心成功返回的结果,如果出错,整个程序会终止。
8.项目的组织
代码规模大时,无法用单一文件承载,需要多个文件甚至多个目录协同工作,这时我们可以用 mod 来组织代码。
多模块:mod
做法:
在项目的入口文件 lib.rs / main.rs 里,用 mod 来声明要加载的其它代码文件。如果模块内容比较多,可以放在一个目录下,在该目录下放一个 mod.rs 引入该模块的其它文件。
在项目的入口文件 lib.rs / main.rs 里,用 mod 来声明要加载的其它代码文件。如果模块内容比较多,可以放在一个目录下,在该目录下放一个 mod.rs 引入该模块的其它文件。
单项目/库:crate
crate 可以是可执行项目,也可以是一个库
cargo new -- lib 来创建一个库
cargo new -- lib 来创建一个库
在一个 crate 下,除了项目的源代码,单元测试和集成测试的代码也会放在 crate 里。
测试
单元测试
Rust 的单元测试一般放在和被测代码相同的文件中,使用条件编译 #[cfg(test)] 来确保测试代码只在测试环境下编译。
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
集成测试
集成测试一般放在 tests 目录下,和 src 平行。和单元测试不同,集成测试只能测试 crate 下的公开接口,编译时编译成单独的可执行文件。
运行
cargo test
多项目:workspace
代码规模增长,所有代码放在crate不是个好主意,可以使用workspace
一个 workspace 可以包含一到多个 crates,当代码发生改变时,只有涉及的 crates 才需要重新编译。当我们要构建一个 workspace 时,需要先在某个目录下生成一个如图所示的 Cargo.toml,包含 workspace 里所有的 crates,然后可以 cargo new 生成对应的 crates:
编译:MIR
.rs-->parse-->AST-->HIR-->MIR-->LLVM IR-->机器码
rustc --emit=mir main.rs 命令生成
常用vs插件
rust-analyzer
编译和分析你的 Rust 代码,提示代码中的错误,并对类型进行标注
rust syntax
代码提供语法高亮
better toml
Tabnine
基于 AI 的自动补全
Error Lens
让报错更清晰更直观
Rust 基础语法
变量
Rust 是强类型语言,但具有自动判断变量类型的能力, 如果编译器可以判断出类型,则类型可省略
如果要声明变量,需要使用 let 关键字
不可变变量
let a = 123; //不可变变量
a="abc"; //第一行的错误在于当声明 a 是 123 以后,a 就被确定为整型数字,不能把字符串类型的值赋给它。
a=4.45; //第二行的错误在于自动转换数字精度有损失,Rust 语言不允许精度有损失的自动数据类型转换。
a=456; //第三行的错误在于 a 不是个可变变量。
a=4.45; //第二行的错误在于自动转换数字精度有损失,Rust 语言不允许精度有损失的自动数据类型转换。
a=456; //第三行的错误在于 a 不是个可变变量。
可变变量
let mut a = 123;
a = 456;
a = 456;
常量
关键字:const
常量不使用mut,默认不可变,自始至终不可变
常量可以在任意作用域内声明,包括全局作用域,在声明的作用域内,常量在程序运行的整个过程中都有效。
常量不使用mut,默认不可变,自始至终不可变
常量可以在任意作用域内声明,包括全局作用域,在声明的作用域内,常量在程序运行的整个过程中都有效。
const a: i32 = 123;
let a = 456;//不合法,因为a是常量
let a = 456;//不合法,因为a是常量
静态变量
static
重新绑定
let a = 456;//未被使用警告
let a: u64 = 123;
let a: u64 = 123;
声明了 a 为无符号 64 位整型变量,如果没有声明类型,a 将自动被判断为有符号 32 位整型变量
重影(Shadowing)
重影就是指变量的名称可以被重新使用的机制
fn main() {
let x = 5;
let x = x + 1;
}
let x = 5;
let x = x + 1;
}
重影与可变变量的赋值不是一个概念,重影是指用同一个名字重新代表另一个变量实体,其类型、可变属性和值都可以变化。但可变变量赋值仅能发生值的变化。
Rust数据类型
基本类型
数值类型
整数型(Integer)
有符号(integer)
无符号(unsigned)
isize
取决于程序运行的计算机CPU类型
usize
取决于程序运行的计算机CPU类型
溢出
处理可能的溢出
使用 wrapping_* 方法在所有模式下都按照补码循环溢出规则处理,例如 wrapping_add
如果使用 checked_* 方法时发生溢出,则返回 None 值
使用 overflowing_* 方法返回该值和一个指示是否存在溢出的布尔值
使用 saturating_* 方法使值达到最小值或最大值
如果使用 checked_* 方法时发生溢出,则返回 None 值
使用 overflowing_* 方法返回该值和一个指示是否存在溢出的布尔值
使用 saturating_* 方法使值达到最小值或最大值
fn main() {
let a : u8 = 255;
let b = a.wrapping_add(20);
println!("{}", b); // 19
}
let a : u8 = 255;
let b = a.wrapping_add(20);
println!("{}", b); // 19
}
浮点数型(Floating-Point)
32 位浮点数(f32)和 64 位浮点数(f64)
fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}
NaN
对于数学上未定义的结果,例如对负数取平方根 -42.1.sqrt() ,会产生一个特殊的结果:Rust 的浮点数类型使用 NaN (not a number)来处理这些情况。
fn main() {
let x = (-42.0_f32).sqrt();
if x.is_nan() {
println!("未定义的数学行为")
}
}
let x = (-42.0_f32).sqrt();
if x.is_nan() {
println!("未定义的数学行为")
}
}
精度问题
//false
if 0.1 + 0.2 == 0.3 {
println!("true");
} else {
println!("false");
}
if (0.1_f64 + 0.2 - 0.3).abs() < 0.00001 {
println!("true");
}
if 0.1 + 0.2 == 0.3 {
println!("true");
} else {
println!("false");
}
if (0.1_f64 + 0.2 - 0.3).abs() < 0.00001 {
println!("true");
}
数字运算
加、减、乘、除、求余
位运算
与、或、非、异或、左移、右移
序列
生成连续数值
序列只允许用于数字或字符类型
序列只允许用于数字或字符类型
1..5 、1..=5
for i in 'a'..='z' {
println!("{}",i);
}
println!("{}",i);
}
as类型转换
As 来完成一个类型到另一个类型的转换
布尔型
bool
true,false
占用1个字节
字符型
char
表示单个Unicode字符
表示单个Unicode字符
占用4个字节
字符串
字符串字面量和字符串切片 &str
&str,16个字节,8字节指针,8字节长度
String 24字节 [8]指针 [8]容量 [8]长度
&String 8字节 [8]指针
单元类型
( )
其唯一的值也是()
main 函数就返回这个单元类型 (),你不能说 main 函数无返回值,因为没有返回值的函数在 Rust 中是有单独的定义的:发散函数( diverge function ),顾名思义,无法收敛的函数。
复合类型
复合类型是由其它类型组合而成
元组
元组用一对()包括的一组数据,可以包含不同种类的数据
let tup: (i32, f64, u8) = (500, 6.4, 1);
// tup.0 等于 500
// tup.1 等于 6.4
// tup.2 等于 1
let (x, y, z) = tup;
// y 等于 6.4
// tup.0 等于 500
// tup.1 等于 6.4
// tup.2 等于 1
let (x, y, z) = tup;
// y 等于 6.4
为了存取速度会对字段进行内存排序,避免不安全的访问和操作
用模式匹配解构元组
//用同样的形式把一个复杂对象中的值匹配出来。
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;//完成一次模式匹配
println!("The value of y is: {}", y);
}
fn main() {
let tup = (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_four = x.1;
let one = x.2;
}
let x: (i32, f64, u8) = (500, 6.4, 1);
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
}
元组中有指向堆的类型
let a: (i32, i32, String) = (5, 10, String::from("xxx"));
let b = a;//赋值引起内部元素所有权转移,元组整体所有权也会发生转移
let b = a;//赋值引起内部元素所有权转移,元组整体所有权也会发生转移
使用示例
//元组在函数返回值场景很常用,可以使用元组返回多个值
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)
}
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)
}
数组
特点
长度固定
元素必须有相同的类型
依次线性排列
array,用[]表示
数组array是存储在栈上
let a = [1, 2, 3, 4, 5];
// a 是一个长度为 5 的整型数组
let b = ["January", "February", "March"];
// b 是一个长度为 3 的字符串数组
let c: [i32; 5] = [1, 2, 3, 4, 5];
// c 是一个长度为 5 的 i32 数组
let first = a[0];
let second = a[1];
// 数组访问
a[0] = 123; // 错误:数组 a 不可变
let mut a = [1, 2, 3];
a[0] = 4; // 正确
// a 是一个长度为 5 的整型数组
let b = ["January", "February", "March"];
// b 是一个长度为 3 的字符串数组
let c: [i32; 5] = [1, 2, 3, 4, 5];
// c 是一个长度为 5 的 i32 数组
let first = a[0];
let second = a[1];
// 数组访问
a[0] = 123; // 错误:数组 a 不可变
let mut a = [1, 2, 3];
a[0] = 4; // 正确
某个值重复出现N次的数组
--值是基本类型
--值是基本类型
let d = [3; 5];// 等同于 let d = [3, 3, 3, 3, 3];
某个值重复出现N次的数组
--值是非基本类型
--值是非基本类型
//下面是错误写法
let array = [String::from("rust is good!"); 8];
let array = [String::from("rust is good!"); 8];
//正常写法
let array: [String; 8] = std::array::from_fn(|_i| String::from("rust is good!"));
println!("{:#?}", array);
let array: [String; 8] = std::array::from_fn(|_i| String::from("rust is good!"));
println!("{:#?}", array);
join拼接
let str_array = ["aaa", "bbb", "ccc", "ddd", "eee"];
let str = str_array.join(","); // 数组拼接为字符串
println!("join str {}", str);
let array: Vec<&str> = str.split(",").collect(); // 字符串拆分为数组
let str = str_array.join(","); // 数组拼接为字符串
println!("join str {}", str);
let array: Vec<&str> = str.split(",").collect(); // 字符串拆分为数组
切片
1.切片的长度可以与数组不同,并不是固定的,而是取决于你使用时指定的起始和结束位置
2.创建切片的代价非常小,因为切片只是针对底层数组的一个引用
3.切片类型[T]拥有不固定的大小,而切片引用类型&[T]则具有固定的大小,因为 Rust 很多时候都需要固定大小数据类型,因此&[T]更有用,&str字符串切片也同理
2.创建切片的代价非常小,因为切片只是针对底层数组的一个引用
3.切片类型[T]拥有不固定的大小,而切片引用类型&[T]则具有固定的大小,因为 Rust 很多时候都需要固定大小数据类型,因此&[T]更有用,&str字符串切片也同理
let a: [i32; 5] = [1, 2, 3, 4, 5];
let slice: &[i32] = &a[1..3];
assert_eq!(slice, &[2, 3]);
let slice: &[i32] = &a[1..3];
assert_eq!(slice, &[2, 3]);
函数项
零大小类型,会记录函数信息
let a = A::fun;
默认实现了Copy/Clone/Sync/Send/Fn/Fnmut/FnOnce
查看变量所占字节
std::mem::size_of_val()
std::mem::size_of::<T>()
函数与闭包
fn <函数名> ( <参数> ) <函数体>
Rust 函数名称的命名风格是小写字母以下划线分割
函数的位置可以随便放,Rust不关心我们在哪里定义了函数
函数参数
fn another_function(x: i32, y: i32) {
println!("x 的值为 : {}", x);
println!("y 的值为 : {}", y);
}
参数必须声明参数名称和类型
fn another_function(x: i32, y: i32) {
println!("x 的值为 : {}", x);
println!("y 的值为 : {}", y);
}
函数作为参数
// 两数之和
fn add(a: i32, b: i32) -> i32 {
a + b
}
// 传入参数和操作函数求值
fn result(a: i32, b: i32, expr: fn(i32, i32) -> i32) -> i32 {
expr(a, b)
}
// 函数作为参数
let sum_result = result(1, 2, add);
println!("sum_result = {}", sum_result);
fn add(a: i32, b: i32) -> i32 {
a + b
}
// 传入参数和操作函数求值
fn result(a: i32, b: i32, expr: fn(i32, i32) -> i32) -> i32 {
expr(a, b)
}
// 函数作为参数
let sum_result = result(1, 2, add);
println!("sum_result = {}", sum_result);
函数体的语句和表达式
语句
语句是执行某些操作且没有返回值的步骤
表达式
表达式有计算步骤且有返回值
用 {} 包括的块里编写一个较为复杂的表达式
fn main() {
let x = 5;
let y = {
let x = 3;
x + 1 //函数体表达式:此表达式的结果值是整个表达式块所代表的值
};
}
let x = 5;
let y = {
let x = 3;
x + 1 //函数体表达式:此表达式的结果值是整个表达式块所代表的值
};
}
注意:x + 1 之后没有分号,否则它将变成一条语句!
注意:函数体表达式并不能等同于函数体,它不能使用 return 关键字。
未实现(unimplemented)
unimplemented!() 告诉编译器该函数尚未实现,unimplemented!() 标记通常意味着我们期望快速完成主要代码,回头再通过搜索这些标记来完成次要代码,类似的标记还有 todo!()
函数定义可以嵌套
fn main() {
fn five() -> i32 {
5
}
println!("five() 的值为: {}", five());
}
fn five() -> i32 {
5
}
println!("five() 的值为: {}", five());
}
函数返回值
在参数声明之后用 -> 来声明函数返回值的类型
fn add(a: i32, b: i32) -> i32 {
return a + b;
}
return a + b;
}
函数作为返回值
enum Operator {
Add,
Multiply,
}
fn get_operation(opt: Operator) -> fn(i32, i32) -> i32 {
match opt {
Operator::Add => add,
Operator::Multiply => multiply,
}
}
// 函数作为返回值,result函数实现见函数作为参数代码
let sum_result1 = result(1, 2, get_operation(Operator::Add));
println!("sum_result1 = {}", sum_result1);
Add,
Multiply,
}
fn get_operation(opt: Operator) -> fn(i32, i32) -> i32 {
match opt {
Operator::Add => add,
Operator::Multiply => multiply,
}
}
// 函数作为返回值,result函数实现见函数作为参数代码
let sum_result1 = result(1, 2, get_operation(Operator::Add));
println!("sum_result1 = {}", sum_result1);
无返回值
单元类型()
单元类型()
fn clear(text: &mut String) -> () {
*text = String::from("");
}
*text = String::from("");
}
永不返回的发散函数!
该函数永不返回( diverge function ),特别的,这种语法往往用做会导致程序崩溃的函数
fn dead_end() -> ! {
panic!("你已经到了穷途末路,崩溃吧!");
}
fn dead_end() -> ! {
panic!("你已经到了穷途末路,崩溃吧!");
}
闭包
可以捕获环境变量
闭包的参数类型必须是确定的,不能是变化的
实现原理
没有任何捕获变量,实现FnOnce
捕获变量并进行修改,实现FnMut
捕获变量但不修改,实现Fn
逃逸闭包:作为函数返回值返回
fn c_mut() -> impl FnMut(i32) -> [i32; 3]{
let mut arr = [0, 1, 2];
move |i| { arr[0] = i; arr}
}
let mut arr = [0, 1, 2];
move |i| { arr[0] = i; arr}
}
流程控制
Rust条件语句
if <condition> { block 1 } else { block 2 }
条件表达式 condition 不需要用小括号
条件表达式必须是 bool 类型
这种语法中的 { block 1 } 和 { block 2 } 可以是函数体表达式
可以使用 if-else 结构实现类似于三元条件运算表达式 (A ? B : C) 的效果
fn main() {
let a = 3;
let number = if a > 0 { 1 } else { -1 };
println!("number 为 {}", number);
}
let a = 3;
let number = if a > 0 { 1 } else { -1 };
println!("number 为 {}", number);
}
Rust循环
while
fn main() {
let mut number = 1;
while number != 4 {
println!("{}", number);
number += 1;
}
println!("EXIT");
}
let mut number = 1;
while number != 4 {
println!("{}", number);
number += 1;
}
println!("EXIT");
}
for 循环使用三元语句控制循环,但是 Rust 中没有这种用法,需要用 while 循环来代替
let mut i = 0;
while i < 10 {
// 循环体
i += 1;
}
while i < 10 {
// 循环体
i += 1;
}
for
for item in &container {
// ...
}
// ...
}
1. 使用 for 时我们往往使用集合的引用形式,如果不使用引用的话,所有权会被转移(move)到 for 语句块中,后面就无法再使用这个集合了
2. 对于实现了 copy 特征的数组(例如 [i32; 10] )而言, for item in arr 并不会把 arr 的所有权转移,而是直接对其进行了拷贝,因此循环之后仍然可以使用 arr 。
2. 对于实现了 copy 特征的数组(例如 [i32; 10] )而言, for item in arr 并不会把 arr 的所有权转移,而是直接对其进行了拷贝,因此循环之后仍然可以使用 arr 。
for item in &mut collection {
// ...
}
// ...
}
在循环中,修改该元素,可以使用 mut 关键字
遍历一个线性数据据结构
获取元素的索引
fn main() {
let a = [4, 3, 2, 1];
// `.iter()` 方法把 `a` 数组变成一个迭代器
for (i, v) in a.iter().enumerate() {
println!("第{}个元素是{}", i + 1, v);
}
}
let a = [4, 3, 2, 1];
// `.iter()` 方法把 `a` 数组变成一个迭代器
for (i, v) in a.iter().enumerate() {
println!("第{}个元素是{}", i + 1, v);
}
}
不声明变量
//可以用 _ 来替代 i 用于 for 循环中,在 Rust 中 _ 的含义是忽略该值或者类型的意思
for _ in 0..10 {
// ...
}
for _ in 0..10 {
// ...
}
a.iter() 代表 a 的迭代器(iterator)
fn main() {
let a = [10, 20, 30, 40, 50];
for i in a.iter() {
println!("值为 : {}", i);
}
}
let a = [10, 20, 30, 40, 50];
for i in a.iter() {
println!("值为 : {}", i);
}
}
通过下标来访问数组
fn main() {
let a = [10, 20, 30, 40, 50];
for i in 0..5 {
println!("a[{}] = {}", i, a[i]);
}
}
let a = [10, 20, 30, 40, 50];
for i in 0..5 {
println!("a[{}] = {}", i, a[i]);
}
}
使用 .. 表示范围的语法
loop
原生的无限循环结构
fn main() {
let s = ['R', 'U', 'N', 'O', 'O', 'B'];
let mut i = 0;
loop {
let ch = s[i];
if ch == 'O' {
break;
}
println!("\'{}\'", ch);
i += 1;
}
}
let s = ['R', 'U', 'N', 'O', 'O', 'B'];
let mut i = 0;
loop {
let ch = s[i];
if ch == 'O' {
break;
}
println!("\'{}\'", ch);
i += 1;
}
}
loop 循环可以通过 break 关键字类似于 return 一样使整个循环退出并给予外部一个返回值,但返回值非必须
fn main() {
let s = ['R', 'U', 'N', 'O', 'O', 'B'];
let mut i = 0;
let location = loop {
let ch = s[i];
if ch == 'O' {
break i;
}
i += 1;
};
println!(" \'O\' 的索引为 {}", location);
}
let s = ['R', 'U', 'N', 'O', 'O', 'B'];
let mut i = 0;
let location = loop {
let ch = s[i];
if ch == 'O' {
break i;
}
i += 1;
};
println!(" \'O\' 的索引为 {}", location);
}
迭代器Iterator
For循环与迭代器
惰性初始化
例
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
for val in v1_iter {
println!("{}", val);
}
//创建了一个迭代器 v1_iter,此时不会发生任何迭代行为,只有在 for 循环开始后,迭代器才会开始迭代其中的元素
let v1_iter = v1.iter();
for val in v1_iter {
println!("{}", val);
}
//创建了一个迭代器 v1_iter,此时不会发生任何迭代行为,只有在 for 循环开始后,迭代器才会开始迭代其中的元素
next方法
迭代器之所以成为迭代器,就是因为实现了 Iterator 特征。
next 方法对迭代器的遍历是消耗性的,每次消耗它一个元素,最终迭代器中将没有任何元素,只能返回 None。
next 方法对迭代器的遍历是消耗性的,每次消耗它一个元素,最终迭代器中将没有任何元素,只能返回 None。
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// 省略其余有默认实现的方法
}
type Item;
fn next(&mut self) -> Option<Self::Item>;
// 省略其余有默认实现的方法
}
let values = vec![1, 2, 3];
{
let result = match IntoIterator::into_iter(values) {
mut iter => loop {
match iter.next() {
Some(x) => { println!("{}", x); },
None => break,
}
},
};
result
}
//IntoIterator::into_iter 是使用完全限定的方式去调用 into_iter 方法,这种调用方式跟 values.into_iter() 是等价的。
//同时我们使用了 loop 循环配合 next 方法来遍历迭代器中的元素,当迭代器返回 None 时,跳出循环。
{
let result = match IntoIterator::into_iter(values) {
mut iter => loop {
match iter.next() {
Some(x) => { println!("{}", x); },
None => break,
}
},
};
result
}
//IntoIterator::into_iter 是使用完全限定的方式去调用 into_iter 方法,这种调用方式跟 values.into_iter() 是等价的。
//同时我们使用了 loop 循环配合 next 方法来遍历迭代器中的元素,当迭代器返回 None 时,跳出循环。
Intolterator特征
Vec 动态数组实现了 IntoIterator 特征,因此可以通过 into_iter 将其转换为迭代器
impl<I: Iterator> IntoIterator for I {
type Item = I::Item;
type IntoIter = I;
#[inline]
fn into_iter(self) -> I {
self
}
}
type Item = I::Item;
type IntoIter = I;
#[inline]
fn into_iter(self) -> I {
self
}
}
into_iter, iter, iter_mut
into_iter 会夺走所有权
let values = vec![1, 2, 3];
for v in values.into_iter() {
println!("{}", v);
}
// 下面的代码将报错,因为 values 的所有权在上面 `for` 循环中已经被转移走
//println!("{:?}", values);
for v in values.into_iter() {
println!("{}", v);
}
// 下面的代码将报错,因为 values 的所有权在上面 `for` 循环中已经被转移走
//println!("{:?}", values);
iter 是借用
let values = vec![1, 2, 3];
let _values_iter = values.iter();
//不会报错,values只是借用了values中的元素
println!("{:?}", values);
.iter() 方法实现的迭代器,调用 next 方法返回的类型是 Some(&T)
let values = vec![1, 2, 3];
let _values_iter = values.iter();
//不会报错,values只是借用了values中的元素
println!("{:?}", values);
iter_mut 是可变借用
.iter_mut() 方法实现的迭代器,调用 next 方法返回的类型是 Some(&mut T),因此在 if let Some(v) = values_iter_mut.next() 中,v 的类型是 &mut i32,最终我们可以通过 *v = 0 的方式修改其值
let mut values = vec![1,2,3];
//对values中的元素进行可变借用
let mut values_iter_mut = values.iter_mut();
if let Some(v) = values_iter_mut.next() {
*v = 0;
}
println!("{:?}", values);
//对values中的元素进行可变借用
let mut values_iter_mut = values.iter_mut();
if let Some(v) = values_iter_mut.next() {
*v = 0;
}
println!("{:?}", values);
Iterator 和 IntoIterator 的区别
这两个其实还蛮容易搞混的,但我们只需要记住,Iterator 就是迭代器特征,只有实现了它才能称为迭代器,才能调用 next。
而 IntoIterator 强调的是某一个类型如果实现了该特征,它可以通过 into_iter,iter 等方法变成一个迭代器。
而 IntoIterator 强调的是某一个类型如果实现了该特征,它可以通过 into_iter,iter 等方法变成一个迭代器。
消费者与适配器
子主题
实现Iterator特征
enumerate
迭代器的性能
Rust所有权
计算机程序必须在运行时管理它们所使用的内存资源。
Java 语言编写的程序在虚拟机(JVM)中运行,JVM 具备自动回收内存资源的功能
所有权概念是为了让 Rust 在编译阶段更有效地分析内存资源的有用性以实现内存管理而诞生的概念
Java 语言编写的程序在虚拟机(JVM)中运行,JVM 具备自动回收内存资源的功能
所有权概念是为了让 Rust 在编译阶段更有效地分析内存资源的有用性以实现内存管理而诞生的概念
栈(Stack)与堆(Heap)
栈
栈按照顺序存储值并以相反顺序取出值,这也被称作后进先出
堆
与栈不同,对于大小未知或者可能变化的数据,我们需要将它存储在堆上。
当向堆上放入数据时,需要请求一定大小的内存空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的指针, 该过程被称为在堆上分配内存,有时简称为 “分配”(allocating)。
接着,该指针会被推入栈中,因为指针的大小是已知且固定的,在后续使用过程中,你将通过栈中的指针,来获取数据在堆上的实际内存位置,进而访问该数据。
接着,该指针会被推入栈中,因为指针的大小是已知且固定的,在后续使用过程中,你将通过栈中的指针,来获取数据在堆上的实际内存位置,进而访问该数据。
性能区别
写入方面:入栈比在堆上分配内存要快,因为入栈时操作系统无需分配新的空间,只需要将新数据放入栈顶即可。相比之下,在堆上分配内存则需要更多的工作,这是因为操作系统必须首先找到一块足够存放数据的内存空间,接着做一些记录为下一次分配做准备。
读取方面:得益于 CPU 高速缓存,使得处理器可以减少对内存的访问,高速缓存和内存的访问速度差异在 10 倍以上!栈数据往往可以直接存储在 CPU 高速缓存中,而堆数据只能存储在内存中。访问堆上的数据比访问栈上的数据慢,因为必须先访问栈再通过栈上的指针来访问内存。
因此,处理器处理分配在栈上数据会比在堆上的数据更加高效。
读取方面:得益于 CPU 高速缓存,使得处理器可以减少对内存的访问,高速缓存和内存的访问速度差异在 10 倍以上!栈数据往往可以直接存储在 CPU 高速缓存中,而堆数据只能存储在内存中。访问堆上的数据比访问栈上的数据慢,因为必须先访问栈再通过栈上的指针来访问内存。
因此,处理器处理分配在栈上数据会比在堆上的数据更加高效。
所有权规则
Rust 中的每个值都有一个变量,称为其所有者
一次只能有一个所有者
当所有者不在程序运行范围时,该值将被删除
变量范围
变量范围是变量的一个属性,其代表变量的可行域,默认从声明变量开始有效直到变量所在域结束。
{
// 在声明以前,变量 s 无效
let s = "runoob";
// 这里是变量 s 的可用范围
}
// 变量范围已经结束,变量 s 无效
// 在声明以前,变量 s 无效
let s = "runoob";
// 这里是变量 s 的可用范围
}
// 变量范围已经结束,变量 s 无效
内存和分配
所有"内存资源"都指的是堆所占用的内存空间。
有分配就有释放,程序不能一直占用某个内存资源。因此决定资源是否浪费的关键因素就是资源有没有及时的释放。
有分配就有释放,程序不能一直占用某个内存资源。因此决定资源是否浪费的关键因素就是资源有没有及时的释放。
Drop: Rust 没有明示释放的步骤是因为在变量范围结束的时候,Rust 编译器自动添加了调用释放资源函数的步骤。
帮助程序员在适当的地方添加了一个释放资源的函数调用,有效地解决一个史上最令程序员头疼的编程问题
帮助程序员在适当的地方添加了一个释放资源的函数调用,有效地解决一个史上最令程序员头疼的编程问题
变量和数据交互的方式
变量与数据交互方式主要有移动(Move)和克隆(Clone)两种:
复制与移动
多个变量可以在 Rust 中以不同的方式与相同的数据交互:
栈(复制作用)
浅拷贝只发生在栈上
基本数据类型有:
整数类型,例如 i32 、 u32 、 i64 等。
布尔类型 bool,值为 true 或 false 。
所有浮点类型,f32 和 f64。
字符类型 char。
仅包含以上类型数据的元组(Tuples)
不可变引入&T
整数类型,例如 i32 、 u32 、 i64 等。
布尔类型 bool,值为 true 或 false 。
所有浮点类型,f32 和 f64。
字符类型 char。
仅包含以上类型数据的元组(Tuples)
不可变引入&T
基本数据类型,存在栈中
let x = 5;
let y = x;
let x = 5;
let y = x;
程序将值 5 绑定到变量 x,然后将 x 的值复制并赋值给变量 y。现在栈中将有两个值 5。此情况中的数据是"基本数据"类型的数据,不需要存储到堆中,仅在栈中的数据的"移动"方式是直接复制,这不会花费更长的时间或更多的存储空间。
堆(所有权移动作用)
数据存到堆中
let s1 = String::from("hello");
let s2 = s1;
let s1 = String::from("hello");
let s2 = s1;
两个 String 对象在栈中,每个 String 对象都有一个指针指向堆中的 "hello" 字符串。在给 s2 赋值时,只有栈中的数据被复制了,堆中的字符串依然还是原来的字符串。
当变量超出范围时,Rust 自动调用释放资源函数并清理该变量的堆内存
在把 s1 的值赋给 s2 以后 s1 将不可以再被使用
在把 s1 的值赋给 s2 以后 s1 将不可以再被使用
例
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1); // 错误!s1 已经失效
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1); // 错误!s1 已经失效
克隆
一般情况下,长度较大的数据存放在堆中,且采用移动的方式进行数据交互。
如果需要将数据单纯的复制一份以供他用,可以使用数据的第二种交互方式——克隆
如果需要将数据单纯的复制一份以供他用,可以使用数据的第二种交互方式——克隆
//这里是真的将堆中的 "hello" 复制了一份,所以 s1 和 s2 都分别绑定了一个值,释放的时候也会被当作两个资源。
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
}
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
}
涉及函数的所有权机制
函数传值与返回
将值传递给函数,一样会发生 移动 或者 复制,就跟 let 语句一样
1. 值传递给函数参数
如果将变量当作参数传入函数,那么它和移动的效果是一样的。
fn main() {
let s = String::from("hello");
// s 被声明有效
takes_ownership(s);
// s 的值被当作参数传入函数
// 所以可以当作 s 已经被移动,从这里开始已经无效
let x = 5;
// x 被声明有效
makes_copy(x);
// x 的值被当作参数传入函数
// 但 x 是基本类型,依然有效
// 在这里依然可以使用 x 却不能使用 s
} // 函数结束, x 无效, 然后是 s. 但 s 已被移动, 所以不用被释放
fn takes_ownership(some_string: String) {
// 一个 String 参数 some_string 传入,有效
println!("{}", some_string);
} // 函数结束, 参数 some_string 在这里释放
fn makes_copy(some_integer: i32) {
// 一个 i32 参数 some_integer 传入,有效
println!("{}", some_integer);
} // 函数结束, 参数 some_integer 是基本类型, 无需释放
let s = String::from("hello");
// s 被声明有效
takes_ownership(s);
// s 的值被当作参数传入函数
// 所以可以当作 s 已经被移动,从这里开始已经无效
let x = 5;
// x 被声明有效
makes_copy(x);
// x 的值被当作参数传入函数
// 但 x 是基本类型,依然有效
// 在这里依然可以使用 x 却不能使用 s
} // 函数结束, x 无效, 然后是 s. 但 s 已被移动, 所以不用被释放
fn takes_ownership(some_string: String) {
// 一个 String 参数 some_string 传入,有效
println!("{}", some_string);
} // 函数结束, 参数 some_string 在这里释放
fn makes_copy(some_integer: i32) {
// 一个 i32 参数 some_integer 传入,有效
println!("{}", some_integer);
} // 函数结束, 参数 some_integer 是基本类型, 无需释放
2. 函数返回值的所有权机制
fn main() {
let s1 = gives_ownership();
// gives_ownership 移动它的返回值到 s1
let s2 = String::from("hello");
// s2 被声明有效
let s3 = takes_and_gives_back(s2);
// s2 被当作参数移动, s3 获得返回值所有权
} // s3 无效被释放, s2 被移动, s1 无效被释放.
fn gives_ownership() -> String {
let some_string = String::from("hello");
// some_string 被声明有效
return some_string;
// some_string 被当作返回值移动出函数
}
fn takes_and_gives_back(a_string: String) -> String {
// a_string 被声明有效
a_string // a_string 被当作返回值移出函数
}
let s1 = gives_ownership();
// gives_ownership 移动它的返回值到 s1
let s2 = String::from("hello");
// s2 被声明有效
let s3 = takes_and_gives_back(s2);
// s2 被当作参数移动, s3 获得返回值所有权
} // s3 无效被释放, s2 被移动, s1 无效被释放.
fn gives_ownership() -> String {
let some_string = String::from("hello");
// some_string 被声明有效
return some_string;
// some_string 被当作返回值移动出函数
}
fn takes_and_gives_back(a_string: String) -> String {
// a_string 被声明有效
a_string // a_string 被当作返回值移出函数
}
被当作函数返回值的变量所有权将会被移动出函数并返回到调用函数的地方,而不会直接被无效释放。
引用与租借
引用(变量地址):就是把变量的地址给租借者,通过解引用(*x)访问真实的值
租借(变量的值):就是租借变量的值,并未在栈中复制变量的值
租借(变量的值):就是租借变量的值,并未在栈中复制变量的值
引用
看作一种指针
实质上"引用"是变量的间接访问方式。
引用不具有所有权,只享有使用权(和租房一样)
实质上"引用"是变量的间接访问方式。
引用不具有所有权,只享有使用权(和租房一样)
& 运算符可以取变量的"引用"。
当一个变量的值被引用时,变量本身不会被认定无效。因为"引用"并没有在栈中复制变量的值:
当一个变量的值被引用时,变量本身不会被认定无效。因为"引用"并没有在栈中复制变量的值:
let str = String::from("A");
let r = &str;
let bytes1 = (*r).as_bytes(); // 返回切片
let bytes2 = (*r).into_bytes(); // 编译报错 cannot move out of `*r` which is behind a shared reference
let r = &str;
let bytes1 = (*r).as_bytes(); // 返回切片
let bytes2 = (*r).into_bytes(); // 编译报错 cannot move out of `*r` which is behind a shared reference
fn main() {
let s1 = String::from("hello");
let s2 = &s1;
println!("s1 is {}, s2 is {}", s1, s2);
}
let s1 = String::from("hello");
let s2 = &s1;
println!("s1 is {}, s2 is {}", s1, s2);
}
函数参数传递
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);//引入传递
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
let s1 = String::from("hello");
let len = calculate_length(&s1);//引入传递
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
悬垂引用
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
因为 s 是在 dangle 函数内创建的,当 dangle 的代码执行完毕后,s 将被释放,但是此时我们又尝试去返回它的引用。这意味着这个引用会指向一个无效的 String,这可不对!
解决办法
//直接返回String
fn no_dangle() -> String {
let s = String::from("hello");
s
}
fn no_dangle() -> String {
let s = String::from("hello");
s
}
租借(租借值的所有权)
【1. &mut 修饰可变的引用类型】
【1. &mut 修饰可变的引用类型】
引用不会获得值的所有权。
引用只能租借(Borrow)值的所有权。
引用本身也是一个类型并具有一个值,这个值记录的是别的值所在的位置,但引用不具有所指值的所有权
同一时刻,你只能拥有要么一个可变引用, 要么任意多个不可变引用
引用必须总是有效的
引用只能租借(Borrow)值的所有权。
引用本身也是一个类型并具有一个值,这个值记录的是别的值所在的位置,但引用不具有所指值的所有权
同一时刻,你只能拥有要么一个可变引用, 要么任意多个不可变引用
引用必须总是有效的
重新租借
//因为 s2 租借的 s1 已经将所有权移动到 s3,所以 s2 将无法继续租借使用 s1 的所有权。如果需要使用 s2 使用该值,必须重新租借
fn main() {
let s1 = String::from("hello");
let s2 = &s1;//s2租借了s1
let s3 = s1;//s1移动到s3,则s2失效
println!("{}", s2);
}
fn main() {
let s1 = String::from("hello");
let s2 = &s1;//s2租借了s1
let s3 = s1;//s1移动到s3,则s2失效
println!("{}", s2);
}
s2需要重新租借
let mut s2 = &s1;
s2 = &s3; // 重新从 s3 租借所有权
s2 = &s3; // 重新从 s3 租借所有权
租借修改所有者的值(可变的租借方式)
--如物业规定房主也可以修改房子结构,房主在租借时在合同中声明赋予你有这种权利
--如物业规定房主也可以修改房子结构,房主在租借时在合同中声明赋予你有这种权利
fn main() {
let mut s1 = String::from("run");
// s1 是可变的
let s2 = &mut s1;
// s2 是可变的引用
s2.push_str("oob");
println!("{}", s2);
}
let mut s1 = String::from("run");
// s1 是可变的
let s2 = &mut s1;
// s2 是可变的引用
s2.push_str("oob");
println!("{}", s2);
}
1. 可变引用不允许多重引用(同时只能存在一个),但不可变引用可以
2. 可变引用与不可变引用不能同时存在
考虑:
Rust 对可变引用的这种设计主要出于对并发状态下发生数据访问碰撞的考虑,在编译阶段就避免了这种事情的发生。
由于发生数据访问碰撞的必要条件之一是数据被至少一个使用者写且同时被至少一个其他使用者读或写,所以在一个值被可变引用时不允许再次被任何引用。
2. 可变引用与不可变引用不能同时存在
考虑:
Rust 对可变引用的这种设计主要出于对并发状态下发生数据访问碰撞的考虑,在编译阶段就避免了这种事情的发生。
由于发生数据访问碰撞的必要条件之一是数据被至少一个使用者写且同时被至少一个其他使用者读或写,所以在一个值被可变引用时不允许再次被任何引用。
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
//这段程序不正确,因为多重可变引用了 s。
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
//这段程序不正确,因为多重可变引用了 s。
作用域
引用的作用域 s 从创建开始,一直持续到它最后一次使用的地方,这个跟变量的作用域有所不同,变量的作用域从创建持续到某一个花括号 }
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// 新编译器中,r1,r2作用域在这里结束
let r3 = &mut s;
println!("{}", r3);
} // 老编译器中,r1、r2、r3作用域在这里结束
// 新编译器中,r3作用域在这里结束
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// 新编译器中,r1,r2作用域在这里结束
let r3 = &mut s;
println!("{}", r3);
} // 老编译器中,r1、r2、r3作用域在这里结束
// 新编译器中,r3作用域在这里结束
变量和数据交互的方式
| 类型 | 存储方式 | 所有权转移 | 底层实现 | Clone |
| ------------------------| ----------------------------------- ------- | -------------------- - | ---------- | ------------ -------|
| 引用借用类型 -- & | 分配内存存储指针或胖指针,指针指向的内存存储值 | 否 | | 默认支持 |
| 基础类型 | 分配的关联内存直接存储值 | 否 | | 默认支持 |
| 元组,普通数组 | 组合类型,每个元素不同 | 元素转移则整体发生转移 | | 默认支持 |
| 结构体,枚举,String,Vec, Box | 分配内存存储指针或胖指针, 指针指向的内存存储值 | 是 | 全是结构体 | 自定义结构体,枚举需要自行实现,其他默认支持 |
| ------------------------| ----------------------------------- ------- | -------------------- - | ---------- | ------------ -------|
| 引用借用类型 -- & | 分配内存存储指针或胖指针,指针指向的内存存储值 | 否 | | 默认支持 |
| 基础类型 | 分配的关联内存直接存储值 | 否 | | 默认支持 |
| 元组,普通数组 | 组合类型,每个元素不同 | 元素转移则整体发生转移 | | 默认支持 |
| 结构体,枚举,String,Vec, Box | 分配内存存储指针或胖指针, 指针指向的内存存储值 | 是 | 全是结构体 | 自定义结构体,枚举需要自行实现,其他默认支持 |
Rust Slice(切片)类型
切片(Slice)是对数据值的部分引用,如字符串切片、数组切片。
在Rust语言级别,只有一种字符串类型:str,通常是以引用类型出现&str,也就是字符串切片。虽然语言级别只有str类型,但在标准库里,有多种不同用途的字符串类型,用的最广的是String类型
注:
1. str 类型是硬编码进可执行文件,无法被修改,但是String则是一个可增长、可改变且具有所有权的UTF-8编码字符串
2. Rust用户提到字符串时,指的就是String类型和&str字符串切片类型,这两都是UTF-8编码
3. 其他类型的字符串,如:OsString,OsStr,CsString和CsStr等,分别对应的是具有所有权和被借用的变量
注:
1. str 类型是硬编码进可执行文件,无法被修改,但是String则是一个可增长、可改变且具有所有权的UTF-8编码字符串
2. Rust用户提到字符串时,指的就是String类型和&str字符串切片类型,这两都是UTF-8编码
3. 其他类型的字符串,如:OsString,OsStr,CsString和CsStr等,分别对应的是具有所有权和被借用的变量
1. 字符串切片(&str)
-- 指针类型
-- 指针类型
2. 数组切片(非字符串切片)
除了字符串以外,其他一些线性数据结构也支持切片操作
fn main() {
let arr = [1, 3, 5, 7, 9];
let part = &arr[0..3];
for i in part.iter() {
println!("{}", i);
}
}
let arr = [1, 3, 5, 7, 9];
let part = &arr[0..3];
for i in part.iter() {
println!("{}", i);
}
}
Rust结构体
结构体与元组区别
结构体的每个成员和其本身都有一个名字
结构体定义
struct Site {
domain: String,
name: String,
nation: String,
found: u32
}
struct Site {
domain: String,
name: String,
nation: String,
found: u32
}
注意:如果你常用 C/C++,请记住在 Rust 里 struct 语句仅用来定义,不能声明实例,结尾不需要 ; 符号,而且每个字段定义之后用 , 分隔。
元组常用于非定义的多值传递
let tup: (i32, f64, u8) = (500, 6.4, 1);
// tup.0 等于 500
// tup.1 等于 6.4
let (x, y, z) = tup;
// y 等于 6.4
// tup.0 等于 500
// tup.1 等于 6.4
let (x, y, z) = tup;
// y 等于 6.4
结构体
1.结构体实例
结构体类名 {
字段名 : 字段值,
...
}
字段名 : 字段值,
...
}
//在实例化结构体的时候用 JSON 对象的 key: value 语法来实现定义
let runoob = Site {
domain: String::from("www.runoob.com"),
name: String::from("RUNOOB"),
nation: String::from("China"),
found: 2013
};
let runoob = Site {
domain: String::from("www.runoob.com"),
name: String::from("RUNOOB"),
nation: String::from("China"),
found: 2013
};
简化结构体创建
当函数参数和结构体字段同名时,可以直接使用缩略的方式进行初始化
fn build_user(email: String, username: String) -> User {
User {
email: email,
username: username,
active: true,
sign_in_count: 1,
}
}
User {
email: email,
username: username,
active: true,
sign_in_count: 1,
}
}
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
结构体更新语法
//你想要新建一个结构体的实例,其中大部分属性需要被设置成与现存的一个结构体属性一样,仅需更改其中的一两个字段的值,可以使用结构体更新语法
let site = Site {
domain: String::from("www.runoob.com"),
name: String::from("RUNOOB"),
..runoob //runoob是已有的实例
};
注意:..runoob 后面不可以有逗号。这种语法不允许一成不变的复制另一个结构体实例,意思就是说至少重新设定一个字段的值才能引用其他实例的值
let site = Site {
domain: String::from("www.runoob.com"),
name: String::from("RUNOOB"),
..runoob //runoob是已有的实例
};
注意:..runoob 后面不可以有逗号。这种语法不允许一成不变的复制另一个结构体实例,意思就是说至少重新设定一个字段的值才能引用其他实例的值
结构体的内存排列
从图中可以清晰地看出 File 结构体两个字段 name 和 data 分别拥有底层两个 [u8] 数组的所有权(String 类型的底层也是 [u8] 数组),通过 ptr 指针指向底层数组的内存地址,这里你可以把 ptr 指针理解为 Rust 中的引用类型。
该图片也侧面印证了:把结构体中具有所有权的字段转移出去后,将无法再访问该字段,但是可以正常访问其它的字段。
该图片也侧面印证了:把结构体中具有所有权的字段转移出去后,将无法再访问该字段,但是可以正常访问其它的字段。
#[derive(Debug)]
struct File {
name: String,
data: Vec<u8>,
}
fn main() {
let f1 = File {
name: String::from("f1.txt"),
data: Vec::new(),
};
let f1_name = &f1.name;
let f1_length = &f1.data.len();
println!("{:?}", f1);
println!("{} is {} bytes long", f1_name, f1_length);
}
struct File {
name: String,
data: Vec<u8>,
}
fn main() {
let f1 = File {
name: String::from("f1.txt"),
data: Vec::new(),
};
let f1_name = &f1.name;
let f1_length = &f1.data.len();
println!("{:?}", f1);
println!("{} is {} bytes long", f1_name, f1_length);
}
编译器会进行重排字段,优化内存占用
struct A{//c++中为12
a:u8,
b: u32,
c:u16,
}
->
struct A{ //rust优化后,8
b: u32,
c:u16,
a:u8,
}
//指定C格式,不优化
#[repr(C)]
struct A{
a:u8,
b: u32,
}
#[repr(C)]
struct A{
a:u8,
b: u32,
}
2.元组结构体
有一种更简单的定义和使用结构体的方式:元组结构体。
与元组的区别是它有名字和固定的类型格式。它存在的意义是为了处理那些需要定义类型(经常使用)又不想太复杂的简单数据
与元组的区别是它有名字和固定的类型格式。它存在的意义是为了处理那些需要定义类型(经常使用)又不想太复杂的简单数据
struct Color(u8, u8, u8);
struct Point(f64, f64);
let black = Color(0, 0, 0);
let origin = Point(0.0, 0.0);
struct Point(f64, f64);
let black = Color(0, 0, 0);
let origin = Point(0.0, 0.0);
fn main() {
struct Color(u8, u8, u8);
struct Point(f64, f64);
let black = Color(0, 0, 0);
let origin = Point(0.0, 0.0);
println!("black = ({}, {}, {})", black.0, black.1, black.2);
println!("origin = ({}, {})", origin.0, origin.1);
}
struct Color(u8, u8, u8);
struct Point(f64, f64);
let black = Color(0, 0, 0);
let origin = Point(0.0, 0.0);
println!("black = ({}, {}, {})", black.0, black.1, black.2);
println!("origin = ({}, {})", origin.0, origin.1);
}
3.单元结构体
如果你定义一个类型,但是不关心该类型的内容, 只关心它的行为时,就可以使用 单元结构体:
struct AlwaysEqual;
let subject = AlwaysEqual;
// 我们不关心 AlwaysEqual 的字段数据,只关心它的行为,因此将它声明为单元结构体,然后再为它实现某个特征
impl SomeTrait for AlwaysEqual {
}
let subject = AlwaysEqual;
// 我们不关心 AlwaysEqual 的字段数据,只关心它的行为,因此将它声明为单元结构体,然后再为它实现某个特征
impl SomeTrait for AlwaysEqual {
}
结构体所有权
结构体必须掌握字段值所有权,因为结构体失效的时候会释放所有字段。
结构体赋值均会发生(包含结构体元组,例子中未展示),所有权转移
输出结构体
//第一行所示:一定要导入调试库 #[derive(Debug)] ,之后在 println 和 print 宏中就可以用 {:?} 占位符输出一整个结构体
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
println!("rect1 is {:?}", rect1);
}
//如果属性较多的话可以使用另一个占位符 {:#?} 。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
println!("rect1 is {:?}", rect1);
}
//如果属性较多的话可以使用另一个占位符 {:#?} 。
打印结构体信息
#[derive(Debug)]
例:println!("rect1 is {:?}", rect1);
1. 如果属性较多的话可以使用另一个占位符 {:#?} 。
1. 如果属性较多的话可以使用另一个占位符 {:#?} 。
dbg!
例:dbg!(&rect1);
1. 使用 dbg! 宏,它会拿走表达式的所有权,然后打印出相应的文件名、行号等 debug 信息
2. dbg! 输出到标准错误输出 stderr,而 println! 输出到标准输出 stdout。
1. 使用 dbg! 宏,它会拿走表达式的所有权,然后打印出相应的文件名、行号等 debug 信息
2. dbg! 输出到标准错误输出 stderr,而 println! 输出到标准输出 stdout。
结构体方法(用于实例,用.访问方法)
方法(Method)和函数(Function)类似,只不过它是用来操作结构体实例的。
Rust 语言不是面向对象的,从它所有权机制的创新可以看出这一点。但是面向对象的珍贵思想可以在 Rust 实现。
结构体方法的第一个参数必须是 &self,不需声明类型,因为 self 不是一种风格而是关键字。
Rust 语言不是面向对象的,从它所有权机制的创新可以看出这一点。但是面向对象的珍贵思想可以在 Rust 实现。
结构体方法的第一个参数必须是 &self,不需声明类型,因为 self 不是一种风格而是关键字。
struct Rectangle {
width: u32,
height: u32,
}
//结构体方法,这是实例的方法
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
println!("rect1's area is {}", rect1.area());
}
width: u32,
height: u32,
}
//结构体方法,这是实例的方法
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
println!("rect1's area is {}", rect1.area());
}
内部元素所有权转移
let c = CCC {
str: String::from("value"),
a: 10
};
let d = c.str; // 所有权转移
println!("{} {}", c.str, c.a); //错误: borrow of moved value: `c.str`
str: String::from("value"),
a: 10
};
let d = c.str; // 所有权转移
println!("{} {}", c.str, c.a); //错误: borrow of moved value: `c.str`
方法Method
定义方法
struct Circle {
x: f64,
y: f64,
radius: f64,
}
impl Circle {
// new是Circle的关联函数,因为它的第一个参数不是self,且new并不是关键字
// 这种方法往往用于初始化当前结构体的实例
fn new(x: f64, y: f64, radius: f64) -> Circle {
Circle {
x: x,
y: y,
radius: radius,
}
}
// Circle的方法,&self表示借用当前的Circle结构体
fn area(&self) -> f64 {
std::f64::consts::PI * (self.radius * self.radius)
}
}
x: f64,
y: f64,
radius: f64,
}
impl Circle {
// new是Circle的关联函数,因为它的第一个参数不是self,且new并不是关键字
// 这种方法往往用于初始化当前结构体的实例
fn new(x: f64, y: f64, radius: f64) -> Circle {
Circle {
x: x,
y: y,
radius: radius,
}
}
// Circle的方法,&self表示借用当前的Circle结构体
fn area(&self) -> f64 {
std::f64::consts::PI * (self.radius * self.radius)
}
}
//impl Rectangle {} 表示为 Rectangle 实现方法(impl 是实现 implementation 的缩写),这样的写法表明 impl 语句块中的一切都是跟 Rectangle 相关联的。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}
self、&self 和 &mut self
描述:在上面代码中 area 的签名中,我们使用 &self 替代 rectangle: &Rectangle,&self 其实是 self: &Self 的简写(注意大小写)。在一个 impl 块内,Self 指代被实现方法的结构体类型,self 指代此类型的实例,换句话说,self 指代的是 Rectangle 结构体实例
self 表示 Rectangle 的所有权转移到该方法中,这种形式用的较少
&self 表示该方法对 Rectangle 的不可变借用
&mut self 表示可变借用
&self 表示该方法对 Rectangle 的不可变借用
&mut self 表示可变借用
通过使用 self 作为第一个参数来使方法获取实例的所有权是很少见的,这种使用方式往往用于把当前的对象转成另外一个对象时使用,转换完后,就不再关注之前的对象,且可以防止对之前对象的误调用。
在一个 impl 块内,Self 指代被实现方法的结构体类型,self 指代此类型的实例
在一个 impl 块内,Self 指代被实现方法的结构体类型,self 指代此类型的实例
Rust的方法调用和C/C++区别
C/C++
对象:. 直接在对象上调用方法
引用:-> 在一个对象的指针上调用方法,这时需要先解引用指针
如果 object 是一个指针,那么 object->something() 和 (*object).something() 是一样的
Rust
描述:Rust 并没有一个与 -> 等效的运算符;相反,Rust 有一个叫 自动引用和解引用的功能
他是这样工作的:当使用 object.something() 调用方法时,Rust 会自动为 object 添加 &、&mut 或 * 以便使 object 与方法签名匹配。也就是说,这些代码是等价的:
p1.distance(&p2);
(&p1).distance(&p2);
p1.distance(&p2);
(&p1).distance(&p2);
关联函数
结构体函数(相当于静态方法,用::访问函数)
在 impl 块中却没有 &self 参数
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn create(width: u32, height: u32) -> Rectangle {
Rectangle { width, height }
}
}
fn main() {
let rect = Rectangle::create(30, 50);
println!("{:?}", rect);
}
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn create(width: u32, height: u32) -> Rectangle {
Rectangle { width, height }
}
}
fn main() {
let rect = Rectangle::create(30, 50);
println!("{:?}", rect);
}
定义在 impl 中且没有 self 的函数被称之为关联函数
需要用 :: 来调用,例如 let sq = Rectangle::new(3, 3);。这个方法位于结构体的命名空间中::: 语法用于关联函数和模块创建的命名空间
需要用 :: 来调用,例如 let sq = Rectangle::new(3, 3);。这个方法位于结构体的命名空间中::: 语法用于关联函数和模块创建的命名空间
impl Rectangle {
fn new(w: u32, h: u32) -> Rectangle {
Rectangle { width: w, height: h }
}
}
fn new(w: u32, h: u32) -> Rectangle {
Rectangle { width: w, height: h }
}
}
多个impl定义
为一个结构体定义多个 impl 块,目的是提供更多的灵活性和代码组织性
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn area(&self) -> u32 {
self.width * self.height
}
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
为枚举实现方法
枚举类型之所以强大,不仅仅在于它好用、可以同一化类型,还在于,我们可以像结构体一样,为枚举实现方法
#![allow(unused)]
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();
}
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();
}
Rust枚举类
枚举结构
实质就是联合体,共用一块内存
enum Book {
Papery, Electronic
}
Papery, Electronic
}
fn main() {
let book = Book::Papery;
println!("{:?}", book);
}
let book = Book::Papery;
println!("{:?}", book);
}
加入元组语法()
--枚举类成员加元组属性
--枚举类成员加元组属性
enum Book {
Papery(u32),
Electronic(String),
}
Papery(u32),
Electronic(String),
}
let book = Book::Papery(1001);
let ebook = Book::Electronic(String::from("url://..."));
let ebook = Book::Electronic(String::from("url://..."));
加入结构体语法{key:value}
--为属性命名,
--为属性命名,
enum Book {
Papery { index: u32 },
Electronic { url: String },
}
Papery { index: u32 },
Electronic { url: String },
}
let book = Book::Papery{index: 1001};
关联值存在所有权转移
内部元素的move,接着是整体枚举的move
关联值不存在所有权转移
整个枚举都存储在分配地址,两个副本,但是还是有move语义
Option枚举类
Option是Rust标准库中的枚举类,解决null的问题。一个变量要么有值:Some(T), 要么为空:None。使用 Option<T>,是为了从 Some 中取出其内部的 T 值以及处理没有值的情况
enum Option<T> {
Some(T),
None,
}
Some(T),
None,
}
可以省略 Option:: 直接写 None 或者 Some()。
Option是一种特殊的枚举类,可以含值分支选择
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);
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
Some(5) 与 Some(i) 匹配吗?当然匹配!它们是相同的成员。i 绑定了 Some 中包含的值,因此 i 的值是 5。接着匹配分支的代码被执行,最后将 i 的值加一并返回一个含有值 6 的新 Some。
模式匹配
let语句
let PATTERN = EXPRESSION;
let 模式 = 表达式;
let 模式 = 表达式;
let x = 5;
x 也是一种模式绑定,代表将匹配的值绑定到变量 x 上。因此,在 Rust 中,变量名也是一种模式
let (x, y) = (1, 2, 3);
将一个元组与模式进行匹配(模式和值的类型必需相同!),然后把 1, 2, 3 分别绑定到 x, y, z 上。
let book5 = Some(Book::Papery);
let b: Book;
if let Option::Some(book) = book5 {
b = book;//延时初始化b
} else {
return;
}
println!("b is {:?}", b);
let b: Book;
if let Option::Some(book) = book5 {
b = book;//延时初始化b
} else {
return;
}
println!("b is {:?}", b);
match
所有返回值表达式的类型必须一样!
描述:对某一类事物的分类,分类的目的是为了对不同的情况进行描述
match target {
模式1 => 表达式1,
模式2 => {
语句1;
语句2;
表达式2
},
_ => 表达式3
}
所有返回值表达式的类型必须一样!
枚举类附加属性定义成元组,在match块中需要临时指定一个名字
分支:整数、浮点数、字符和字符串切片引用(&str)
fn main() {
let t = "abc";
match t {
"abc" => println!("Yes"),
_ => {},
}
}
let t = "abc";
match t {
"abc" => println!("Yes"),
_ => {},
}
}
匹配命名变量
fn main() {
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);
}
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);
}
当 match 语句运行的时候发生了什么。第一个匹配分支的模式并不匹配 x 中定义的值,所以代码继续执行。
第二个匹配分支中的模式引入了一个新变量 y,它会匹配任何 Some 中的值。因为这里的 y 在 match 表达式的作用域中,而不是之前 main 作用域中,所以这是一个新变量,不是开头声明为值 10 的那个 y。这个新的 y 绑定会匹配任何 Some 中的值,在这里是 x 中的值。因此这个 y 绑定了 x 中 Some 内部的值。这个值是 5,所以这个分支的表达式将会执行并打印出 Matched,y = 5。
如果 x 的值是 None 而不是 Some(5),头两个分支的模式不会匹配,所以会匹配模式 _。这个分支的模式中没有引入变量 x,所以此时表达式中的 x 会是外部没有被遮蔽的 x,也就是 None。
一旦 match 表达式执行完毕,其作用域也就结束了,同理内部 y 的作用域也结束了。最后的 println! 会打印 at the end: x = Some(5), y = 10。
第二个匹配分支中的模式引入了一个新变量 y,它会匹配任何 Some 中的值。因为这里的 y 在 match 表达式的作用域中,而不是之前 main 作用域中,所以这是一个新变量,不是开头声明为值 10 的那个 y。这个新的 y 绑定会匹配任何 Some 中的值,在这里是 x 中的值。因此这个 y 绑定了 x 中 Some 内部的值。这个值是 5,所以这个分支的表达式将会执行并打印出 Matched,y = 5。
如果 x 的值是 None 而不是 Some(5),头两个分支的模式不会匹配,所以会匹配模式 _。这个分支的模式中没有引入变量 x,所以此时表达式中的 x 会是外部没有被遮蔽的 x,也就是 None。
一旦 match 表达式执行完毕,其作用域也就结束了,同理内部 y 的作用域也结束了。最后的 println! 会打印 at the end: x = Some(5), y = 10。
使用 | 语法匹配多个模式
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
通过序列 ..= 匹配值的范围
用于数字、字符类型
用于数字、字符类型
let x = 5;
match x {
1..=5 => println!("one through five"),
_ => println!("something else"),
}
match x {
1..=5 => println!("one through five"),
_ => println!("something else"),
}
使用模式来解构结构体、枚举、元组、数组和引用
使用字面值作为结构体模式的一部分进行解构
fn main() {
let p = Point { x: 0, y: 7 };
match p {
Point { x, y: 0 } => println!("On the x axis at {}", x),
Point { x: 0, y } => println!("On the y axis at {}", y),
Point { x, y } => println!("On neither axis: ({}, {})", x, y),
}
}
let p = Point { x: 0, y: 7 };
match p {
Point { x, y: 0 } => println!("On the x axis at {}", x),
Point { x: 0, y } => println!("On the y axis at {}", y),
Point { x, y } => println!("On neither axis: ({}, {})", x, y),
}
}
首先是 match 第一个分支,指定匹配 y 为 0 的 Point; 然后第二个分支在第一个分支之后,匹配 y 不为 0,x 为 0 的 Point; 最后一个分支匹配 x 不为 0,y 也不为 0 的 Point
解构枚举
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {
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
)
}
}
}
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {
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
)
}
}
}
解构嵌套的结构体和枚举
enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(Color),
}
fn main() {
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
)
}
_ => ()
}
}
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(Color),
}
fn main() {
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
)
}
_ => ()
}
}
忽略模式中的值
使用 _ 忽略整个值
fn foo(_: i32, y: i32) {
println!("This code only uses the y parameter: {}", y);
}
println!("This code only uses the y parameter: {}", y);
}
使用嵌套的 _ 忽略部分值
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, _, third, _, fifth) => {
println!("Some numbers: {}, {}, {}", first, third, fifth)
},
}
match numbers {
(first, _, third, _, fifth) => {
println!("Some numbers: {}, {}, {}", first, third, fifth)
},
}
用 .. 忽略剩余值
.. 模式会忽略模式中剩余的任何没有显式匹配的值部分
对于有多个部分的值,可以使用 .. 语法来只使用部分值而忽略其它值,这样也不用再为每一个被忽略的值都单独列出下划线
对于有多个部分的值,可以使用 .. 语法来只使用部分值而忽略其它值,这样也不用再为每一个被忽略的值都单独列出下划线
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),
}
x: i32,
y: i32,
z: i32,
}
let origin = Point { x: 0, y: 0, z: 0 };
match origin {
Point { x, .. } => println!("x is {}", x),
}
这里列出了 x 值,接着使用了 .. 模式来忽略其它字段,这样的写法要比一一列出其它字段,然后用 _ 忽略简洁的多。
用 .. 来忽略元组中间的某些值
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, .., last) => {
println!("Some numbers: {}, {}", first, last);
},
}
}
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, .., last) => {
println!("Some numbers: {}, {}", first, last);
},
}
}
这里用 first 和 last 来匹配第一个和最后一个值。.. 将匹配并忽略中间的所有值。
匹配守卫提供的额外条件
匹配守卫
是一个位于 match 分支模式之后的额外 if 条件,它能为分支模式提供更进一步的匹配条件。
if x < 5
let num = Some(4);
match num {
Some(x) if x < 5 => println!("less than five: {}", x),
Some(x) => println!("{}", x),
None => (),
}
match num {
Some(x) if x < 5 => println!("less than five: {}", x),
Some(x) => println!("{}", x),
None => (),
}
如果 num 为 Some(10),因为 10 不小于 5 ,所以第一个分支的匹配守卫为假。接着 Rust 会前往第二个分支,因为这里没有匹配守卫所以会匹配任何 Some 成员。
if n == y
--外部的孪量y
--外部的孪量y
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(n) if n == y => println!("Matched, n = {}", n),
_ => println!("Default case, x = {:?}", x),
}
println!("at the end: x = {:?}, y = {}", x, y);
}
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(n) if n == y => println!("Matched, n = {}", n),
_ => println!("Default case, x = {:?}", x),
}
println!("at the end: x = {:?}, y = {}", x, y);
}
匹配守卫 if n == y 并不是一个模式所以没有引入新变量。这个 y 正是 外部的 y 而不是新的覆盖变量 y,这样就可以通过比较 n 和 y 来表达寻找一个与外部 y 相同的值的概念了
或运算符 |
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"), //应该是与的关系
_ => println!("no"),
}
//等同于(4 | 5 | 6) if y => ...
在满足 x 属于 4 | 5 | 6 后才会判断 y 是否为 true
只有前面的表达式为true,再判断if y是否为true
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"), //应该是与的关系
_ => println!("no"),
}
//等同于(4 | 5 | 6) if y => ...
在满足 x 属于 4 | 5 | 6 后才会判断 y 是否为 true
只有前面的表达式为true,再判断if y是否为true
@绑定
@(读作 at)运算符允许为一个字段绑定另外一个变量。
例:另外一个变量 @
注:用途就是可以在语句中使用值,因为id不能用
例:另外一个变量 @
注:用途就是可以在语句中使用值,因为id不能用
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)
},
}
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)
},
}
打印出 Found an id in range: 5。通过在 3..=7 之前指定 id_variable @,我们捕获了任何匹配此范围的值并同时将该值绑定到变量 id_variable 上。
第二个分支只在模式中指定了一个范围,id 字段的值可以是 10、11 或 12,不过这个模式的代码并不知情也不能使用 id 字段中的值,因为没有将 id 值保存进一个变量。
最后一个分支指定了一个没有范围的变量,此时确实拥有可以用于分支代码的变量 id,因为这里使用了结构体字段简写语法。不过此分支中没有像头两个分支那样对 id 字段的值进行测试:任何值都会匹配此分支。
当你既想要限定分支范围,又想要使用分支的变量时,就可以用 @ 来绑定到一个新的变量上,实现想要的功能。
第二个分支只在模式中指定了一个范围,id 字段的值可以是 10、11 或 12,不过这个模式的代码并不知情也不能使用 id 字段中的值,因为没有将 id 值保存进一个变量。
最后一个分支指定了一个没有范围的变量,此时确实拥有可以用于分支代码的变量 id,因为这里使用了结构体字段简写语法。不过此分支中没有像头两个分支那样对 id 字段的值进行测试:任何值都会匹配此分支。
当你既想要限定分支范围,又想要使用分支的变量时,就可以用 @ 来绑定到一个新的变量上,实现想要的功能。
if let语法
//只有一个模式的值需要被处理时,用if let,否则用match
if let 模式 = 源变量 {
语句块
}
if let PATTERN = SOME_VALUE {
语句块
}
if let 模式 = 源变量 {
语句块
}
if let PATTERN = SOME_VALUE {
语句块
}
模式匹配
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
//a对应0,b对应7
let Point { x: a, y: b } = p;
assert_eq!(0, a);
assert_eq!(7, b);
}
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
//a对应0,b对应7
let Point { x: a, y: b } = p;
assert_eq!(0, a);
assert_eq!(7, b);
}
创建了变量 a 和 b 来匹配结构体 p 中的 x 和 y 字段
展示了模式中的变量名不必与结构体中的字段名一致
展示了模式中的变量名不必与结构体中的字段名一致
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
let Point { x, y } = p;
assert_eq!(0, x);
assert_eq!(7, y);
}
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
let Point { x, y } = p;
assert_eq!(0, x);
assert_eq!(7, y);
}
匹配结构体字段的模式简写:只需列出结构体字段的名称,则模式创建的变量会有相同的名称
解析结构体和元组
struct Point {
x: i32,
y: i32,
}
let ((feet, inches), Point {x, y}) = ((3, 10), Point { x: 3, y: -10 });
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;
assert_eq!(x, 114);
assert_eq!(y, 514);
let [x, y] = arr;
assert_eq!(x, 114);
assert_eq!(y, 514);
不定长数组
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 [x, ..] = arr {
assert_eq!(x, &114);
}
if let &[.., y] = arr {
assert_eq!(y, 514);
}
let arr: &[u16] = &[];
assert!(matches!(arr, [..]));
assert!(!matches!(arr, [x, ..]));
.. 模式会忽略模式中剩余的任何没有显式匹配的值部分
使用下划线开头忽略未使用的变量
不要警告未使用的变量
fn main() {
let _x = 5;
let y = 10;
}
let _x = 5;
let y = 10;
}
注:_x 仍会将值绑定到变量,而 _ 则完全不会绑定。
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{:?}", s);
//在这里s被借走,会报错
if let Some(_s) = s {
println!("found a string");
}
println!("{:?}", s);
//在这里s被借走,会报错
let s = Some(String::from("Hello!"));
if let Some(_) = s {
println!("found a string");
}
println!("{:?}", s);
//只使用下划线本身,则并不会绑定值,因为 s 没有被移动进 _
if let Some(_) = s {
println!("found a string");
}
println!("{:?}", s);
//只使用下划线本身,则并不会绑定值,因为 s 没有被移动进 _
@前绑定后解构(Rust 1.56 新增)
//使用 @ 还可以在绑定新变量的同时,对目标进行解构
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
// 绑定新变量 `p`,同时对 `Point` 进行解构
let p @ Point {x: px, y: py } = Point {x: 10, y: 23};
println!("x: {}, y: {}", px, py);
println!("{:?}", p);
let point = Point {x: 10, y: 5};
if let p @ Point {x: 10, y} = point {
println!("x is 10 and y is {} in {:?}", y, p);
} else {
println!("x was not 10 :(");
}
}
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
// 绑定新变量 `p`,同时对 `Point` 进行解构
let p @ Point {x: px, y: py } = Point {x: 10, y: 23};
println!("x: {}, y: {}", px, py);
println!("{:?}", p);
let point = Point {x: 10, y: 5};
if let p @ Point {x: 10, y} = point {
println!("x is 10 and y is {} in {:?}", y, p);
} else {
println!("x was not 10 :(");
}
}
while let条件循环
允许只要模式匹配就一直进行 while 循环
// Vec是动态数组
let mut stack = Vec::new();
// 向数组尾部插入元素
stack.push(1);
stack.push(2);
stack.push(3);
// stack.pop从数组尾部弹出元素
while let Some(top) = stack.pop() {
println!("{}", top);
}
let mut stack = Vec::new();
// 向数组尾部插入元素
stack.push(1);
stack.push(2);
stack.push(3);
// stack.pop从数组尾部弹出元素
while let Some(top) = stack.pop() {
println!("{}", top);
}
对于 while 来说,只要 pop 返回 Some 就会一直不停的循环。一旦其返回 None,while 循环停止。我们可以使用 while let 来弹出栈中的每一个元素
for循环
let a = [10, 20, 20, 30, 50];
for i in 0..a.len() { // 0-len
print!("{} ", a[i]);
}
// for修改元素
let mut a = [String::from("A"), String::from("B")];
for i in 0..a.len() {
a[i] = String::from("C");
}
for value in a.iter_mut() {
(*value) = String::from("D");
}
let v = vec!['a', 'b', 'c'];
for (index, value) in v.iter().enumerate() {
println!("{} is at index {}", value, index);
}
for i in 0..a.len() { // 0-len
print!("{} ", a[i]);
}
// for修改元素
let mut a = [String::from("A"), String::from("B")];
for i in 0..a.len() {
a[i] = String::from("C");
}
for value in a.iter_mut() {
(*value) = String::from("D");
}
let v = vec!['a', 'b', 'c'];
for (index, value) in v.iter().enumerate() {
println!("{} is at index {}", value, index);
}
这里使用 enumerate 方法产生一个迭代器,该迭代器每次迭代会返回一个 (索引,值) 形式的元组,然后用 (index,value) 来匹配。
| 使用方法 | 等价使用方式 | 所有权 |
| ----------------------------- | ------------------------------------------------- | ---------- |
| `for item in collection` | `for item in IntoIterator::into_iter(collection)` | 转移所有权 |
| `for item in &collection` | `for item in collection.iter()` | 不可变借用 |
| `for item in &mut collection` | `for item in collection.iter_mut()` | 可变借用 |
| ----------------------------- | ------------------------------------------------- | ---------- |
| `for item in collection` | `for item in IntoIterator::into_iter(collection)` | 转移所有权 |
| `for item in &collection` | `for item in collection.iter()` | 不可变借用 |
| `for item in &mut collection` | `for item in collection.iter_mut()` | 可变借用 |
函数参数
函数参数也是模式
fn foo(x: i32) {
// 代码
}
//其中 x 就是一个模式
// 代码
}
//其中 x 就是一个模式
fn print_coordinates(&(x, y): &(i32, i32)) {
println!("Current location: ({}, {})", x, y);
}
fn main() {
let point = (3, 5);
print_coordinates(&point);
}
//&(3, 5) 会匹配模式 &(x, y),因此 x 得到了 3,y 得到了 5。
println!("Current location: ({}, {})", x, y);
}
fn main() {
let point = (3, 5);
print_coordinates(&point);
}
//&(3, 5) 会匹配模式 &(x, y),因此 x 得到了 3,y 得到了 5。
matches!宏
可以将一个表达式跟模式进行匹配,然后返回匹配的结果true OR false
enum MyEnum {
Foo,
Bar
}
fn main() {
let v = vec![MyEnum::Foo,MyEnum::Bar,MyEnum::Foo];
//对 v 进行过滤,只保留类型是 MyEnum::Foo 的元素
v.iter().filter(|x| matches!(x, MyEnum::Foo));
}
Foo,
Bar
}
fn main() {
let v = vec![MyEnum::Foo,MyEnum::Bar,MyEnum::Foo];
//对 v 进行过滤,只保留类型是 MyEnum::Foo 的元素
v.iter().filter(|x| matches!(x, MyEnum::Foo));
}
0 条评论
下一页