当前位置: 首页 > news >正文

【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和类型,我们都需要决定:
- 应该拥有数据
- 还是持有对数据的引用

如果你的代码需要数据的所有权,那么就必须要存储拥有的数据。当你的代码拥有数据时,必须让调用者提供拥有的数据,而不是引用或克隆。这样就可以让调用者控制内存分配,并且可以清楚地看到使用相关接口的成本。

如果代码不需要拥有数据,那么就应用数据的引用来执行操作。但是有例外:像i32boolf64这类“小类型”,直接存储和复制的成本与通过引用存储的成本基本相同。

这种小类型基本都实现了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: 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,也就是Droptrait)是在对象生命周期结束时自动调用的特殊方法,用于释放资源

析构函数一般由Droptrait来实现,Droptrait定义了一个drop方法,通过在一个类型的生命周期结束时自动调用droptrait来释放资源。

析构函数通常是不允许失败的,并且是非阻塞执行的,但有时会有例外:
- 释放资源时,可能需要关闭网络连接或写入日志文件,这些操作都有可能发生错误
-drop方法可能需要执行阻塞任务,例如等待一个线程结束或等待一个异步任务的完成


I/O操作与析构函数的问题

I/O(输入/输出)相关的类型(如文件、网络连接等)中,资源管理非常重要,而Drop机制(析构函数)可以确保在对象被丢弃时,正确地执行清理操作,避免资源泄漏。

更具体地说:
-文件操作:在文件对象被丢弃时,Drop需要确保所有数据已写入磁盘(防止数据丢失)。
-网络连接:在TcpStreamUdpSocket被丢弃时,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的所有权并暴露任何的错误(使用Result<T,E>)或异步性(使用async fn),这些都是与销毁相关的。

“松散的线程”(dangling threads)指的是:
- 资源(如线程、数据库连接、文件句柄等)未正确清理,导致进程退出时仍然存在占用
- 例如,某些后台任务未正常终止,可能会继续运行、泄露资源或阻碍进程退出

“显式的析构函数”指的是:
- 由于Rust的Drop不能返回Result<T, E>,也不能async(因为drop()不能await),所以无法处理异步清理或错误
- 因此,我们可以提供一个显式的close()shutdown()方法,让用户手动调用,确保资源被正确释放,并支持Resultasync处理错误。

看例子:

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) -> Result<File, 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来进行模式匹配
一点注意

显式的析构函数需要在文档中突出显示。

http://www.jsqmd.com/news/1257138/

相关文章:

  • this关键字
  • Windows键盘重映射终极指南:用SharpKeys打造你的个性化键盘布局
  • DP(动态规划)入门相关书籍
  • Steam创意工坊模组免费下载终极指南:跨平台玩家的完美解决方案
  • 2025 全球数字经济大会揭晓:融云荣膺首批「出海严选服务商」
  • Claude中转站知识库问答:回答可靠性来自资料治理
  • 算法程序与设计
  • C语言程序设计第10天/数组
  • MCU芯片AS32X601如何使用 DMA 单次模式搬运 ADC 多通道采样数据?
  • 5分钟快速上手:Sketchfab 3D模型下载终极指南
  • 阿里巴巴造出0.8B超小文档解析神器,端到端解析时代来了?
  • HarmonyOS7 暗黑模式:3 步让你的 App 支持深色主题
  • 「盘点」界面控件DevExtreme (v23.1, v23.2)可访问性增强
  • Sketchfab 3D模型下载完整指南:免费获取高质量3D资源
  • 零成本AI股票分析:5分钟打造你的个人智能投资助手
  • 2026深圳招商加盟服务行业GEO优化公司选型指南丨生成式引擎优化服务商深度测评 - 企业新闻快传
  • API中转站成本控制:测试流量和正式账单必须分开
  • Token 单价更低,Agent 任务为什么反而更贵:4 层成本口径 + 最小事件账本 + 3 个决策问题
  • FastbootEnhance:Windows上最完整的Android设备管理工具终极指南
  • Qwen3.8 上百炼:国产推理 API 这条路,又往前推了一步
  • WarcraftHelper:魔兽争霸3玩家的5大游戏体验优化解决方案
  • 【Rust中级教程】2.11. API设计原则之受约束性(constrained) Pt.2:封闭trait(sealed trait)、重新导出(re-exports)、自动trait
  • StreamFX终极指南:5分钟掌握OBS专业级直播特效
  • Veeam备份虚拟机报错Unable to allocate RAM for VM snapshot完整解决方案
  • 3分钟快速上手:Beyond Compare 5密钥生成完整指南
  • 短视频封面转化率卡在2.3%?权威实测:9种AI工具横向评测(附GPU占用/出图速度/版权风险矩阵表)
  • Code date creation的开发技术介绍
  • 零基础掌握AI股票分析:3步打造个人智能投资系统
  • 构建基于LLM的智能股票分析系统:架构设计与实战应用
  • 深圳零食消费品牌行业GEO优化公司选型指南丨2026年生成式引擎优化服务商深度测评 - 企业新闻快传