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

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

喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
请添加图片描述

2.7.1. 借用(Borrowed) vs. 拥有(Owned)

针对Rust中几乎每一个函数、trait和类型,我们都需要决定:

  • 应该拥有数据
  • 还是持有对数据的引用

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

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

这种小类型基本都实现了Copy trait,但不是所有实现了Copy trait的类型都可以被称作“小类型”:比如[u8, 114514],它实现了Copy trait,但是由于它的元素太多了,所以存储和复制操作的开销太大,建议传引用。


Cow类型

有的时候我们还会无法确定代码是否拥有数据,因为它取决于运行时的情况。Cow类型(在 1.2.2. Rust的引用和指针 有过介绍)非常适合这种场景。

Cow允许在需要时持有引用或拥有值。如果在只有引用的情况下要求生成拥有的值,Cow将使用ToOwned trait在后台创建一个拥有的值,通常是通过克隆。一般情况下我们会在返回类型中使用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,也就是Drop trait)是在对象生命周期结束时自动调用的特殊方法,用于释放资源

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

析构函数通常是不允许失败的,并且是非阻塞执行的,但有时会有例外:

  • 释放资源时,可能需要关闭网络连接或写入日志文件,这些操作都有可能发生错误
  • 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

问题就出在Rust Drop trait不能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::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来进行模式匹配

一点注意

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


http://www.kler.cn/a/559341.html

相关文章:

  • 【行业解决方案篇八】【DeepSeek农业遥感:作物病虫害识别指南】
  • 使用 Spark NLP 实现中文实体抽取与关系提取
  • Linux之文件系统
  • vue2的计算属性
  • 【刷题】贪心算法
  • 算法笔记 03 —— 算法初步(上)
  • Java并发编程——ThreadLocal
  • openstack部署
  • HarmonyOS学习第3天: 环境搭建开启鸿蒙开发新世界
  • 聊聊istio服务网格
  • Grouped-Query Attention(GQA)详解: Pytorch实现
  • 低空经济应用场景细分赛道探索,无人机开源飞控二次开发详解
  • Web Worker:释放浏览器多线程的潜力
  • 麒麟v10 飞腾架构 配置Qt编译环境
  • Spring Boot3.x集成Flowable7.x(一)Spring Boot集成与设计、部署、发起、完成简单流程
  • 掌握 ElasticSearch 组合查询:Bool Query 详解与实践
  • DAY12 Tensorflow过拟合
  • STM32 HAL库0.96寸OLED显示液晶屏
  • 虚拟机 VirtualBox7 安装 ubuntu-Linux24.04.1LTS 和常用配置
  • DVWA 靶场