【Rust中级教程】2.8. API设计原则之灵活性(flexible) Pt.4:显式析构函数的问题及3种解决方案

📅 2026/7/24 20:45:35
【Rust中级教程】2.8. API设计原则之灵活性(flexible) Pt.4:显式析构函数的问题及3种解决方案
2.8. API设计原则之灵活性(flexible) Pt.4显式析构函数的问题及3种解决方案2.8.1. 显式析构函数的问题添加显式析构函数时会遇到问题- 当某一类型实现了Drop在析构函数中无法将该类型的任何字段移出。因为在显式析构函数运行后drop()仍会被调用它接受mut self要求self的所有部分都没被移动。-Drop接收的是mut self而不是self因此Drop无法实现简单地调用显式析构函数并忽略其结果(因为Drop不拥有self)以上一篇文章的例子为基础如果我们加上既实现了Droptrait又写了close方法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)?; // 获取文件描述符 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(()) } } //实现drop trait impl Drop for File { fn drop(mut self) { let _ self.close(); //调用close方法来丢弃 println!(File dropped); } } 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()); }输出error[E0507]: cannot move out of *self which is behind a mutable reference -- src/main.rs:59:17 | 59 | let _ self.close(); //调用close方法来丢弃 | ^^^^ ------- *self moved due to this method call | | | move occurs because *self has type File, which does not implement the Copy trait | note: File::close takes ownership of the receiver self, which moves *self -- src/main.rs:33:14 | 33 | fn close(self) - Result(), Error { | ^^^^ note: if File implemented Clone, you could clone the value -- src/main.rs:6:1 | 6 | struct File { | ^^^^^^^^^^^ consider implementing Clone for this type ... 59 | let _ self.close(); //调用close方法来丢弃 | ---- you could clone this value报错信息显示无法从*self中移出值因为它位于mut self后面。2.8.2. 解决方案首先需要说明的是没有完美的解决方案我们只能尽力弥补。解决方案1把结构体包装OptionT里然后再套一层结构体我们可以将顶层方案作为包装了OptionT的新类型这样OptionT内部持有一个类型这个类型包含所有的字段。这个时候我们就需要两个析构函数外边一个里面一个。在这两个析构函数中使用Option::take函数来获取数据所有权从而移除值。由于内部类型没有实现Drop所以你可以获取所有字段的所有权。缺点想在顶层类型上提供的所有方法现在都必须添加通过OptionT这层壳来获取内部类型上字段的代码。我们在上文的代码例基础上进行修改步骤1改掉File的定义套一层壳首先我们得把两个字段移到另一个结构体里套在OptionT中作为File结构体的字段/// 一个表示文件句柄的类型 struct InnerFile { /// 文件名 name: String, /// 文件描述符 fd: i32, } /// 给InnerFile套了一个壳 struct File { /// 把InnerFile包装在OptionT中 inner: OptionInnerFile, }步骤2修改File上的方法File上有两个方法我们都需要添加通过OptionT这层壳来获取内部类型上字段的代码。首先是open方法/// 一个构造函数打开一个文件并返回一个 File 实例 fn open(name: str) - ResultFile, Error { // 使用 OpenOptions 打开文件具备读写权限 let file: StdFile OpenOptions::new() .read(true) .write(true) .open(name)?; // 获取文件描述符 let fd: i32 file.into_raw_fd(); // 返回一个 File 实例 Ok(File { inner: Some( InnerFile { name: name.to_string(), fd, }) }) }由于这个代码只有返回值设计了File结构体所以也只有返回值需要改接下来是close方法/// 一个显式的析构函数关闭文件并返回任何错误 fn close(mut self) - Result(), Error { // 记得self要变为mut self否则take不了 // 使用模式匹配提取出字段的值 if let Some(inner) self.inner.take() { let name inner.name; let fd inner.fd; println!(Closing file: {} with fd: {}, name, fd); // 使用 FromRawFd 将 fd 转换回 File let file: std::fs::File unsafe { std::fs::File::from_raw_fd(fd) }; // 刷新文件数据到磁盘 file.sync_all()?; // 将文件截断为 0 字节 file.set_len(0)?; // 再次刷新文件 file.sync_all()?; // 丢弃文件实例它会自动关闭文件 drop(file); // 返回成功 Ok(()) } else { // 如果inner字段是None说明文件已经被关闭或丢弃返回错误 Err(Error::new( std::io::ErrorKind::Other, File is already closed, )) } }参数传进去之后先得通过模式匹配来获取字段的值如果如果inner字段是None也就是模式匹配不成功的情况下我们需要自己写一个错误返回步骤3修改Droptrait的实现Drop::drop方法需要修改:fn drop(mut self) { // 使用模式匹配获取字段值 if let Some(inner) self.inner.take() { let name inner.name; let fd inner.fd; println!(Dropping file: {} (fd: {}), name, fd); // 使用 FromRawFd 将 fd 转换回 File let file: std::fs::File unsafe { std::fs::File::from_raw_fd(fd) }; // 丢弃file实例 drop(file); } else { // 如果inner字段是None的话说明文件已经被丢弃或关闭不做任何操作 } }参数传进去之后先得通过模式匹配来获取字段的值如果inner字段是None的话说明文件已经被丢弃或关闭不做任何操作步骤4微修主函数主函数中需要提取字段值的地方需要修改fn main() { // ...前文无修改已省略 // 打印文件名和 fd (这里需要修改) println!(File name: {}, fd: {}, file.inner.as_ref().unwrap().name, file.inner.as_ref().unwrap().fd ); // ...后文无修改已省略 }原始类型是OptionInnerFile调用.as_ref()后变成OptionInnerFile变成了OptionInnerFile再使用unwrap提取出来的值就是引用而不是所有的值file.inner是一个OptionInnerFile类型。而直接访问Option的值需要转移所有权或匹配处理例如通过take()或unwrap()这会销毁Option的内部值所以得要as_ref整体代码use std::os::fd::{FromRawFd, IntoRawFd}; use std::fs::{File as StdFile, OpenOptions, metadata}; use std::io::Error; /// 一个表示文件句柄的类型 struct InnerFile { /// 文件名 name: String, /// 文件描述符 fd: i32, } /// 给InnerFile套了一个壳 struct File { /// 把InnerFile包装在OptionT中 inner: OptionInnerFile, } impl File { /// 一个构造函数打开一个文件并返回一个 File 实例 fn open(name: str) - ResultFile, Error { // 使用 OpenOptions 打开文件具备读写权限 let file: StdFile OpenOptions::new() .read(true) .write(true) .open(name)?; // 获取文件描述符 let fd: i32 file.into_raw_fd(); // 返回一个 File 实例 Ok(File { inner: Some( InnerFile { name: name.to_string(), fd, }) }) } /// 一个显式的析构函数关闭文件并返回任何错误 fn close(mut self) - Result(), Error { // 记得self要变为mut self否则take不了 // 使用模式匹配提取出字段的值 if let Some(inner) self.inner.take() { let name inner.name; let fd inner.fd; println!(Closing file: {} with fd: {}, name, fd); // 使用 FromRawFd 将 fd 转换回 File let file: std::fs::File unsafe { std::fs::File::from_raw_fd(fd) }; // 刷新文件数据到磁盘 file.sync_all()?; // 将文件截断为 0 字节 file.set_len(0)?; // 再次刷新文件 file.sync_all()?; // 丢弃文件实例它会自动关闭文件 drop(file); // 返回成功 Ok(()) } else { // 如果inner字段是None说明文件已经被关闭或丢弃返回错误 Err(Error::new( std::io::ErrorKind::Other, File is already closed, )) } } } // 实现drop trait用于在值离开作用域时运行的一些代码 impl Drop for File { fn drop(mut self) { // 使用模式匹配获取字段值 if let Some(inner) self.inner.take() { let name inner.name; let fd inner.fd; println!(Dropping file: {} (fd: {}), name, fd); // 使用 FromRawFd 将 fd 转换回 File let file: std::fs::File unsafe { std::fs::File::from_raw_fd(fd) }; // 丢弃file实例 drop(file); } else { // 如果inner字段是None的话说明文件已经被丢弃或关闭不做任何操作 } } } 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.inner.as_ref().unwrap().name, file.inner.as_ref().unwrap().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()); }解决方案2把字段包装在OptionT里我们也可以保持结构体不变把每个字段的值都套在OptionT里需要获取所有权使用时用Option::take就可以需要引用时用.as_ref()加.unwrap()就可以。如果类型具有合理的空值那么效果很好。缺点如果你必须将几乎每个字段都包装在Option中然后对这些字段的每次访问都进行匹配的unwrap就会使代码变得很繁琐。我们在上文的代码例基础上进行修改步骤1改掉File的定义为每一个字段添加一层OptionT/// 一个表示文件句柄的类型 struct File { /// 文件名 name: OptionString, /// 文件描述符 fd: Optioni32, }步骤2修改File上的方法File上有两个方法我们都需要添加通过OptionT这层壳来获取内部类型上字段的代码。首先是open方法/// 一个构造函数打开一个文件并返回一个 File 实例 fn open(name: str) - ResultFile, Error { // 使用 OpenOptions 打开文件具备读写权限 let file: StdFile OpenOptions::new() .read(true) .write(true) .open(name)?; // 获取文件描述符 let fd: i32 file.into_raw_fd(); // 返回一个 File 实例 Ok(File { name: Some(name.to_string()), fd: Some(fd), }) }open方法的参数没有涉及到File结构体所以接收参数部分不用修改open方法的返回值涉及到了File得为每个字段添上Some变体其次是close方法/// 一个显式的析构函数关闭文件并返回任何错误 fn close(mut self) - Result(), Error { // 模式匹配并使用std::mem::take取出name字段的值 if let Some(name) std::mem::take(mut self.name) { //模式匹配并使用std::mem::take取出fd字段的值 if let Some(fd) std::mem::take(mut self.fd) { // 打印 println!(Closing file: {} with fd: {}, name, fd); // 使用 FromRawFd 将 fd 转换回 File let file: std::fs::File unsafe { std::fs::File::from_raw_fd(fd) }; // 刷新文件数据到磁盘 file.sync_all()?; // 将文件截断为 0 字节 file.set_len(0)?; // 再次刷新文件 file.sync_all()?; // 丢弃文件实例它会自动关闭文件 drop(file); // 返回成功 Ok(()) } else { // 如果fd字段是None说明文件已经被关闭或丢弃返回一个错误 Err(Error::new( std::io::ErrorKind::Other, File descriptor already dropped or taken, )) } } else { // 如果name字段是None说明文件已经被关闭或丢弃返回一个错误 Err(Error::new( std::io::ErrorKind::Other, File name already dropped or taken, )) } }参数要先经过模式匹配并使用std::mem::take取出里面的值如果任意字段是None说明文件已经被关闭或丢弃返回一个错误步骤3修改Droptrait的实现fn drop(mut self) { // 使用模式匹配获取字段值 if let Some(name) self.name.take() { if let Some(fd) self.fd.take() { println!(Dropping file: {} (fd: {}), name, fd); // 使用 FromRawFd 将 fd 转换回 let file: std::fs::File unsafe { std::fs::File::from_raw_fd(fd) }; // 丢弃file实例 drop(file); } else { // 如果fd字段是None说明文件已经被关闭或丢弃不做任何操作 } } else { // 如果name字段是None说明文件已经被关闭或丢弃不做任何操作 } }参数要先经过模式匹配并使用std::mem::take取出里面的值如果任意字段是None说明文件已经被关闭或丢弃不做任何操作步骤4微修主函数fn main() { // ...前文无修改已省略 // 打印文件名和 fd (这里需要修改) println!(File name: {}, fd: {}, file.name.as_ref().unwrap(), file.fd.as_ref().unwrap() ); // ...后文无修改已省略 }原始类型被OptionT包裹调用.as_ref()后获得里面值的引用变成了引用之后再使用unwrap提取出来的值就是引用而不是所有的值而直接访问Option的值需要转移所有权或匹配处理例如通过take()或unwrap()这会销毁Option的内部值所以得要as_ref整体代码use std::os::fd::{FromRawFd, IntoRawFd}; use std::fs::{File as StdFile, OpenOptions, metadata}; use std::io::Error; /// 一个表示文件句柄的类型 struct File { /// 文件名 name: OptionString, /// 文件描述符 fd: Optioni32, } impl File { /// 一个构造函数打开一个文件并返回一个 File 实例 fn open(name: str) - ResultFile, Error { // 使用 OpenOptions 打开文件具备读写权限 let file: StdFile OpenOptions::new() .read(true) .write(true) .open(name)?; // 获取文件描述符 let fd: i32 file.into_raw_fd(); // 返回一个 File 实例 Ok(File { name: Some(name.to_string()), fd: Some(fd), }) } /// 一个显式的析构函数关闭文件并返回任何错误 fn close(mut self) - Result(), Error { // 模式匹配并使用使用std::mem::take取出name字段的值 if let Some(name) std::mem::take(mut self.name) { //模式匹配并使用使用std::mem::take取出fd字段的值 if let Some(fd) std::mem::take(mut self.fd) { // 打印 println!(Closing file: {} with fd: {}, name, fd); // 使用 FromRawFd 将 fd 转换回 File let file: std::fs::File unsafe { std::fs::File::from_raw_fd(fd) }; // 刷新文件数据到磁盘 file.sync_all()?; // 将文件截断为 0 字节 file.set_len(0)?; // 再次刷新文件 file.sync_all()?; // 丢弃文件实例它会自动关闭文件 drop(file); // 返回成功 Ok(()) } else { // 如果fd字段是None说明文件已经被关闭或丢弃返回一个错误 Err(Error::new( std::io::ErrorKind::Other, File descriptor already dropped or taken, )) } } else { // 如果name字段是None说明文件已经被关闭或丢弃返回一个错误 Err(Error::new( std::io::ErrorKind::Other, File name already dropped or taken, )) } } } // 实现drop trait用于在值离开作用域时运行的一些代码 impl Drop for File { fn drop(mut self) { // 使用模式匹配获取字段值 if let Some(name) self.name.take() { if let Some(fd) self.fd.take() { println!(Dropping file: {} (fd: {}), name, fd); // 使用 FromRawFd 将 fd 转换回 let file: std::fs::File unsafe { std::fs::File::from_raw_fd(fd) }; // 丢弃file实例 drop(file); } else { // 如果fd字段是None说明文件已经被关闭或丢弃不做任何操作 } } else { // 如果name字段是None说明文件已经被关闭或丢弃不做任何操作 } } } 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.as_ref().unwrap(), file.fd.as_ref().unwrap() ); // 关闭文件并处理任何错误 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()); }方法3将数据持有在ManuallyDrop类型内将数据持有在ManuallyDrop类型内它会解引用内部类型不必再使用unwrap。在drop中进行销毁时可使用ManuallyDrop::take来获取所有权。缺点ManuallyDrop::take是不安全的需要放在unsafe块中。我们在上文的代码例基础上进行修改步骤1改掉File的定义为每一个字段添加一层ManuallyDrop/// 一个表示文件句柄的类型 struct File { /// 文件名 name: ManuallyDropString, /// 文件描述符 fd: ManuallyDropi32, }步骤2修改File上的方法File上有两个方法我们都需要添加通过ManuallyDrop这层壳来获取内部类型上字段的代码。首先是open方法/// 一个构造函数打开一个文件并返回一个 File 实例 fn open(name: str) - ResultFile, Error { // 使用 OpenOptions 打开文件具备读写权限 let file: StdFile OpenOptions::new() .read(true) .write(true) .open(name)?; // 取得文件描述符的所有权 let fd: i32 file.into_raw_fd(); // 返回一个 File 实例 Ok(File { name: ManuallyDrop::new(name.to_string()), fd: ManuallyDrop::new(fd), }) }open方法的参数没有涉及到File结构体所以接收参数部分不用修改open方法的返回值涉及到了File每个字段都得用ManuallyDrop::new来传值其次是close方法/// 一个显式的析构函数关闭文件并返回任何错误 fn close(mut self) - Result(), Error { // 使用std::mem::replace将name字段替换为一个空字符串把原来的值给name let name std::mem::replace(mut self.name, ManuallyDrop::new(.to_string())); // 使用std::mem::replace将fd字段替换为一个无效的值(-1)把原来的值给fd let fd std::mem::replace(mut self.fd, ManuallyDrop::new(-1)); // 打印 println!(Closing file: {:?} with fd: {:?}, name, fd); // 使用 FromRawFd 将 fd 转换回 File let file: std::fs::File unsafe { std::fs::File::from_raw_fd(*fd) //这里fd要先解引用 }; // 刷新文件数据到磁盘 file.sync_all()?; // 将文件截断为 0 字节 file.set_len(0)?; // 再次刷新文件 file.sync_all()?; // 丢弃文件实例它会自动关闭文件 drop(file); // 返回成功 Ok(()) }使用std::mem::replace将name字段替换为一个空字符串把原来的值给name使用std::mem::replace将fd字段替换为一个无效的值(-1)把原来的值给fdstd::fs::File::from_raw_fd(*fd)中参数要先解引用也就是写*fd步骤3修改Droptrait的实现fn drop(mut self) { // 使用ManuallyDrop::take取出name字段的值并检查是否是空字符串 let name unsafe { ManuallyDrop::take(mut self.name) }; // 使用ManuallyDrop::take取出fd字段的值并检查是否是无效的值 let fd unsafe { ManuallyDrop::take(mut self.fd) }; //打印 println!(Dropping file: {:?} (fd: {:?}), name, fd); // 如果fd字段不是无效的值说明文件还没有被关闭或丢弃需要执行丢弃操作 if fd ! -1 || !name.is_empty() { let file unsafe { std::fs::File::from_raw_fd(fd) }; // 丢弃 drop(file); } }使用ManuallyDrop::take取出name和fd字段的值并检查是否是空字符串或无效的值如果fd字段不是无效的值(不是-1)或是name字段不是空字符串就说明文件还没有被关闭或丢弃需要执行丢弃操作其实这里不用两个判断条件(fd ! -1 || !name.is_empty())一个就够了因为name和fd字段的值的变化是一起的一个无效就代表着整个结构体都还未被清理。步骤4微修主函数fn main() { // ...前文无修改已省略 // 打印文件名和 fd (这里需要修改) println!(File name: {}, fd: {}, *file.name, *file.fd); // ...后文无修改已省略 }使用解引用来打印值整体代码use std::os::fd::{FromRawFd, IntoRawFd}; use std::fs::{File as StdFile, OpenOptions, metadata}; use std::io::Error; use std::mem::ManuallyDrop; /// 一个表示文件句柄的类型 struct File { /// 文件名 name: ManuallyDropString, /// 文件描述符 fd: ManuallyDropi32, } impl File { /// 一个构造函数打开一个文件并返回一个 File 实例 fn open(name: str) - ResultFile, Error { // 使用 OpenOptions 打开文件具备读写权限 let file: StdFile OpenOptions::new() .read(true) .write(true) .open(name)?; // 获取文件描述符 let fd: i32 file.into_raw_fd(); // 返回一个 File 实例 Ok(File { name: ManuallyDrop::new(name.to_string()), fd: ManuallyDrop::new(fd), }) } /// 一个显式的析构函数关闭文件并返回任何错误 fn close(mut self) - Result(), Error { // 使用std::mem::replace将name字段替换为一个空字符串把原来的值给name let name std::mem::replace(mut self.name, ManuallyDrop::new(.to_string())); // 使用std::mem::replace将fd字段替换为一个无效的值(-1)把原来的值给fd let fd std::mem::replace(mut self.fd, ManuallyDrop::new(-1)); // 打印 println!(Closing file: {:?} with fd: {:?}, name, fd); // 使用 FromRawFd 将 fd 转换回 File let file: std::fs::File unsafe { std::fs::File::from_raw_fd(*fd) //这里fd要先解引用 }; // 刷新文件数据到磁盘 file.sync_all()?; // 将文件截断为 0 字节 file.set_len(0)?; // 再次刷新文件 file.sync_all()?; // 丢弃文件实例它会自动关闭文件 drop(file); // 返回成功 Ok(()) } } // 实现drop trait用于在值离开作用域时运行的一些代码 impl Drop for File { fn drop(mut self) { // 使用ManuallyDrop::take取出name字段的值并检查是否是空字符串 let name unsafe { ManuallyDrop::take(mut self.name) }; // 使用ManuallyDrop::take取出fd字段的值并检查是否是无效的值 let fd unsafe { ManuallyDrop::take(mut self.fd) }; //打印 println!(Dropping file: {:?} (fd: {:?}), name, fd); // 如果fd字段不是无效的值说明文件还没有被关闭或丢弃需要执行丢弃操作 if fd ! -1 || !name.is_empty() { let file unsafe { std::fs::File::from_raw_fd(fd) }; // 丢弃 drop(file); } } } 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()); }三种方案的选择这三种方案的选择要根据实际情况通常第二个方案。但是如果真的字段太多要写的unwrap太多的话就需要考虑其他的方案。如果代码足够简单可以轻松检查代码的安全性那么第三种ManuallyDrop方案也是非常好的。