【Rust中级教程】2.7. API设计原则之灵活性(flexible) Pt.3:借用 vs. 拥有、`Cow`类型、可失败和阻塞的析构函数及解决办法

发布时间:2026/7/24 22:44:51
【Rust中级教程】2.7. API设计原则之灵活性(flexible) Pt.3:借用 vs. 拥有、`Cow`类型、可失败和阻塞的析构函数及解决办法 2.7. API设计原则之灵活性(flexible) Pt.3借用 vs. 拥有、Cow类型、可失败和阻塞的析构函数及解决办法2.7.1. 借用(Borrowed) vs. 拥有(Owned)针对Rust中几乎每一个函数、trait和类型我们都需要决定- 应该拥有数据- 还是持有对数据的引用如果你的代码需要数据的所有权那么就必须要存储拥有的数据。当你的代码拥有数据时必须让调用者提供拥有的数据而不是引用或克隆。这样就可以让调用者控制内存分配并且可以清楚地看到使用相关接口的成本。如果代码不需要拥有数据那么就应用数据的引用来执行操作。但是有例外像i32、bool和f64这类“小类型”直接存储和复制的成本与通过引用存储的成本基本相同。这种小类型基本都实现了Copytrait但不是所有实现了Copytrait的类型都可以被称作“小类型”比如[u8; 114514]它实现了Copytrait但是由于它的元素太多了所以存储和复制操作的开销太大建议传引用。Cow类型有的时候我们还会无法确定代码是否拥有数据因为它取决于运行时的情况。Cow类型(在1.2.2. Rust的引用和指针 有过介绍)非常适合这种场景。Cow允许在需要时持有引用或拥有值。如果在只有引用的情况下要求生成拥有的值Cow将使用ToOwnedtrait在后台创建一个拥有的值通常是通过克隆。一般情况下我们会在返回类型中使用Cow来表示有时会分配内存的函数。也就是说- 如果数据无需修改Cow可以借用现有数据避免额外的内存分配。- 如果数据需要修改Cow会克隆数据以获得所有权并进行修改。看一个例子use std::borrow::Cow; fn process_data(data: Cowstr) { 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也就是Droptrait是在对象生命周期结束时自动调用的特殊方法用于释放资源。析构函数一般由Droptrait来实现Droptrait定义了一个drop方法通过在一个类型的生命周期结束时自动调用droptrait来释放资源。析构函数通常是不允许失败的并且是非阻塞执行的但有时会有例外- 释放资源时可能需要关闭网络连接或写入日志文件这些操作都有可能发生错误-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问题就出在RustDroptrait不能await因为drop()不是异步的trait Drop { fn drop(mut self); }drop()不能await意味着它无法执行异步清理任务如async关闭数据库连接。但异步清理通常需要await比如async fn close_connection() { // 模拟关闭数据库连接 println!(Closing database connection...); }这段代码无法在Drop里直接调用因为Drop不能await。一种常见的做法是在drop()里启动另一个异步执行器executor来运行清理代码例如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代表的是一个异步计算的值async fn cleanup() { println!(Cleaning up...); }这个cleanup()方法返回的是一个Future它不会立即执行而是需要执行器executor去轮询它。struct MyResource; impl Drop for MyResource { fn drop(mut self) { let fut async { println!(Cleaning up...); }; // 这里创建了 Future但没人执行它 } }这里drop()里创建了一个Future但它不会自己运行必须有执行器executor来驱动它。如果没有可用的执行器Future就永远不会执行导致清理任务无法完成。解决方案——显式的析构函数讲完了Future我们回到解决这两类问题来。如果用户不想留下“松散的线程”那么我们可以提供一个显式的析构函数。这种析构函数通常是一个方法它获得self的所有权并暴露任何的错误使用ResultT,E或异步性(使用async fn)这些都是与销毁相关的。“松散的线程”dangling threads指的是- 资源如线程、数据库连接、文件句柄等未正确清理导致进程退出时仍然存在占用- 例如某些后台任务未正常终止可能会继续运行、泄露资源或阻碍进程退出“显式的析构函数”指的是- 由于Rust的Drop不能返回ResultT, E也不能async因为drop()不能await所以无法处理异步清理或错误。- 因此我们可以提供一个显式的close()或shutdown()方法让用户手动调用确保资源被正确释放并支持Result或async处理错误。看例子use std::os::fd::{FromRawFd, IntoRawFd}; 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) - ResultFile, Error { // 使用 OpenOptions 打开文件具备读写权限 let file: StdFile OpenOptions::new() .read(true) .write(true) .open(name)?; // 取得文件描述符的所有权不要用 as_raw_fd // 否则 StdFile 被 drop 时会关掉 fd而我们手里还留着一份拷贝 let fd: i32 file.into_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::fs::File::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来进行模式匹配一点注意显式的析构函数需要在文档中突出显示。