喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=^・ω・^=)
2.7.1. 借用(Borrowed) vs. 拥有(Owned)
针对Rust中几乎每一个函数、trait和类型,我们都需要决定:
- 应该拥有数据
- 还是持有对数据的引用
如果你的代码需要数据的所有权,那么就必须要存储拥有的数据。当你的代码拥有数据时,必须让调用者提供拥有的数据,而不是引用或克隆。这样就可以让调用者控制内存分配,并且可以清楚地看到使用相关接口的成本。
如果代码不需要拥有数据,那么就应用数据的引用来执行操作。但是有例外:像i32
、bool
和f64
这类"小类型",直接存储和复制的成本与通过引用存储的成本基本相同。
这种小类型基本都实现了Copy
trait,但不是所有实现了Copy
trait的类型都可以被称作"小类型":比如[u8, 114514]
,它实现了Copy
trait,但是由于它的元素太多了,所以存储和复制操作的开销太大,建议传引用。
Cow
类型
有的时候我们还会无法确定代码是否拥有数据,因为它取决于运行时的情况。Cow
类型(在1.2.2. Rust的引用和指针 有过介绍)非常适合这种场景。
Cow
允许在需要时持有引用或拥有值 。如果在只有引用的情况下要求生成拥有的值,Cow
将使用ToOwned
trait在后台创建一个拥有的值,通常是通过克隆。一般情况下我们会在返回类型中使用Cow
来表示有时会分配内存的函数。
也就是说:
- 如果数据无需修改 ,
Cow
可以借用现有数据,避免额外的内存分配。 - 如果数据需要修改 ,
Cow
会克隆数据,以获得所有权,并进行修改。
看一个例子:
rust
use std::borrow::Cow;
fn process_data(data: Cow<str>) {
if data.contains("invalid") {
// 包含了"invalid"就要进行修改,进行修改就得先获得所有权
let owned_data = data.into_owned();
// ...一些修改操作
println!("{}", owned_data); // 最后输出
}
else {
// 这里不包含修改操作,所以只需要读取它
println!("Data: {}", data);
}
}
fn main() {
let input1 = "Hello, world!";
process_data(Cow::Borrowed(input1));
let input2 = "This is invalid data".to_string();
process_data(Cow::Owned(input2));
}
process_data
函数的逻辑我写在注释里了- 主函数中,
input1
不包含"invalid",没有修改操作,只需要读取。所以不需要传入持有的值,传入引用(Cow::Borrowed(input1)
)即可 input2
包含"invalid",需要进行修改操作,所以要传入持有的值(Cow::Owned(input2)
)即可
什么时候该考虑获得数据所有权
有时候引用生命周期会让接口特别复杂,难以使用。如果用户在使用的过程中遇到了编译问题,这表明我们需要(即使不必要)拥有数据的所有权。
如果要这样做,第一步该考虑把容易克隆 或不涉及性能敏感的数据换成拥有的值,而不是直接对大块数据的内容机械能堆分配。这样做可以避免性能问题并提高接口的可用性。
2.7.2. 可失败和阻塞的析构函数(Fallible and Blocking Destructors)
析构函数(Destructors,也就是Drop
trait)是在对象生命周期结束 时自动调用的特殊方法,用于释放资源。
析构函数一般由Drop
trait来实现,Drop
trait定义了一个drop
方法,通过在一个类型的生命周期结束时自动调用drop
trait来释放资源。
析构函数通常是不允许失败的,并且是非阻塞执行的,但有时会有例外:
- 释放资源时,可能需要关闭网络连接或写入日志文件,这些操作都有可能发生错误
drop
方法可能需要执行阻塞任务,例如等待一个线程结束或等待一个异步任务的完成
I/O操作与析构函数的问题
在 I/O(输入/输出)相关的类型 (如文件、网络连接等)中,资源管理 非常重要,而Drop机制 (析构函数)可以确保在对象被丢弃时,正确地执行清理操作,避免资源泄漏。
更具体地说:
- 文件操作 :在文件对象被丢弃时,Drop需要确保所有数据已写入磁盘(防止数据丢失)。
- 网络连接 :在
TcpStream
或UdpSocket
被丢弃时,Drop需要确保连接正确关闭,防止资源泄露。 - 数据库连接 :在数据库连接对象超出作用域时,Drop需要断开连接,释放服务器端的资源。
问题在于:在Rust的Drop机制(析构函数)中,如果执行清理操作时发生了错误,没有直接的方式返回Result
让调用者处理,唯一能做的就是触发panic!
让程序崩溃。
异步代码与析构函数的问题
异步代码也有类似的问题------在Rust的异步编程 (async/await) 中,通常希望在Drop(析构函数)中执行清理操作,比如:
- 关闭数据库连接
- 刷新并关闭文件
- 关闭 WebSocket 或 TCP 连接
- 释放锁或资源
然而,异步代码执行时,可能会遇到 其他任务仍在等待(pending),比如:
- 网络I/O操作未完成
- 其他async任务仍然在等待信号
- 当前任务需要await,但Drop不能await
问题就出在Rust Drop
trait不能await,因为drop()
不是异步的:
rust
trait Drop {
fn drop(&mut self);
}
drop()
不能await,意味着它无法执行异步清理任务(如async关闭数据库连接)。- 但异步清理通常需要await,比如:
rust
async fn close_connection() {
// 模拟关闭数据库连接
println!("Closing database connection...");
}
这段代码无法在Drop里直接调用 ,因为 Drop
不能await。
一种常见的做法是在drop()
里启动另一个异步执行器(executor) 来运行清理代码,例如:
rust
impl Drop for MyAsyncResource {
fn drop(&mut self) {
tokio::spawn(async {
self.close().await;
});
}
}
- 这样可以在
drop()
里执行async任务。 - 但问题是:如果
drop()
发生在main()
结束或其他async任务完成后,可能还没执行完drop()
里的任务,程序就退出了。
针对这两类问题
针对这两类问题,没有完美的解决方案 ,只能是通过Drop
来尽力清理。如果清理出错误了,至少我们尝试了,就只能让程序忽略错误并继续了。
如果还有可用的执行器,我们可以尝试生成一个Future来做清理,但如果Future永不会允许,那也没办法。
一点拓展:关于Future
在Rust的异步模型中,Future代表的是一个异步计算的值:
rust
async fn cleanup() {
println!("Cleaning up...");
}
- 这个
cleanup()
方法返回的是一个Future,它不会立即执行 ,而是需要执行器(executor)去轮询它。
rust
struct MyResource;
impl Drop for MyResource {
fn drop(&mut self) {
let fut = async {
println!("Cleaning up...");
};
// 这里创建了 `Future`,但没人执行它!
}
}
- 这里
drop()
里创建了一个Future,但它不会自己运行,必须有执行器(executor)来驱动它。 - 如果没有可用的执行器,Future就永远不会执行,导致清理任务无法完成。
解决方案------显式的析构函数
讲完了Future,我们回到解决这两类问题来。
如果用户不想留下"松散的线程",那么我们可以提供一个显式的析构函数。这种析构函数通常是一个方法,它获得self
的所有权并暴露任何的错误(使用Result<T,E>
)或异步性(使用async fn
),这些都是与销毁相关的。
"松散的线程"(dangling threads)指的是:
资源(如线程、数据库连接、文件句柄等)未正确清理,导致进程退出时仍然存在占用
例如,某些后台任务未正常终止,可能会继续运行、泄露资源或阻碍进程退出
"显式的析构函数"指的是:由于Rust的
Drop
不能返回Result<T, E>
,也不能async
(因为drop()
不能await),所以无法处理异步清理或错误。因此,我们可以提供一个显式的
close()
或shutdown()
方法 ,让用户手动调用,确保资源被正确释放,并支持Result
或async
处理错误。
看例子:
rust
use std::os::fd::AsRawFd;
use std::fs::{File as StdFile, OpenOptions, metadata};
use std::io::Error;
/// 一个表示文件句柄的类型
struct File {
/// 文件名
name: String,
/// 文件描述符
fd: i32,
}
impl File {
/// 一个构造函数,打开一个文件并返回一个 File 实例
fn open(name: &str) -> Result<File, Error> {
// 使用 OpenOptions 打开文件,具备读写权限
let file: StdFile = OpenOptions::new()
.read(true)
.write(true)
.open(name)?;
// 获取文件描述符
let fd: i32 = file.as_raw_fd();
// 返回一个 File 实例
Ok(File {
name: name.to_string(),
fd,
})
}
/// 一个显式的析构函数,关闭文件并返回任何错误
fn close(self) -> Result<(), Error> {
// 使用 FromRawFd 将 fd 转换回 File
let file: std::fs::File = unsafe {
std::os::unix::io::FromRawFd::from_raw_fd(self.fd)
};
// 刷新文件数据到磁盘
file.sync_all()?;
// 将文件截断为 0 字节
file.set_len(0)?;
// 再次刷新文件
file.sync_all()?;
// 丢弃文件实例,它会自动关闭文件
drop(file);
// 返回成功
Ok(())
}
}
fn main() {
// 创建一个名为 "test.txt" 的文件,并写入一些内容
std::fs::write("test.txt", "Hello, world!").unwrap();
// 打开文件并获取 File 实例
let file: File = File::open("test.txt").unwrap();
// 打印文件名和 fd
println!("File name: {}, fd: {}", file.name, file.fd);
// 关闭文件并处理任何错误
match file.close() {
Ok(()) => println!("File closed successfully"),
Err(e) => println!("Error closing file: {}", e),
}
// 检查关闭后的文件大小
let metadata = metadata("test.txt").unwrap();
println!("File size: {} bytes", metadata.len());
}
- 重要信息我都写在代码的注释里了
close
就是一个显式的析构函数,它会关闭文件并返回任何错误,其参数是self
,返回的是Result
- 在主函数中我们显式地调用了析构函数,使用
match
来进行模式匹配
一点注意
显式的析构函数需要在文档中突出显示。