【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会克隆数据,以获得所有权,并进行修改。

看一个例子:

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需要确保所有数据已写入磁盘(防止数据丢失)。
  • 网络连接 :在 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()不是异步的:

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()方法 ,让用户手动调用,确保资源被正确释放,并支持Resultasync处理错误。

看例子:

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来进行模式匹配

一点注意

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

相关推荐
云知谷7 分钟前
【HTML】网络数据是如何渲染成HTML网页页面显示的
开发语言·网络·计算机网络·html
lang201509288 分钟前
Spring Boot 官方文档精解:构建与依赖管理
java·spring boot·后端
lly2024061 小时前
SQL ROUND() 函数详解
开发语言
大宝剑1701 小时前
python环境安装
开发语言·python
why技术1 小时前
从18w到1600w播放量,我的一点思考。
java·前端·后端
lly2024061 小时前
CSS3 多媒体查询
开发语言
间彧1 小时前
Redis Cluster vs Sentinel模式区别
后端
间彧1 小时前
🛡️ 构建高可用缓存架构:Redis集群与Caffeine多级缓存实战
后端
间彧1 小时前
构建本地缓存(如Caffeine)+ 分布式缓存(如Redis集群)的二级缓存架构
后端