捕获 ioexception 时需显式处理其子类异常(如 unauthorizedaccessexception),并用 ioexception 兜底;file.copy/move 跨卷非原子,需判断根路径;异步 io 异常须 await 后捕获;重试仅适用于特定 win32 错误码,且需指数退避;流操作优先用 using 确保安全释放。

捕获 IOException 时别漏掉子类异常
直接 catch IOException 看似稳妥,但很多具体错误(比如权限不足、磁盘满、文件被占用)实际抛出的是它的子类,如 UnauthorizedAccessException、DirectoryNotFoundException、PathTooLongException。这些异常不会被 IOException 捕获到,除非你显式处理或用基类兜底。
- 推荐写法:先按具体场景 catch 子类,再用
IOException做兜底 -
FileNotFoundException和DirectoryNotFoundException虽然继承自IOException,但语义明确,单独处理更利于日志和重试逻辑 - 注意 .NET 6+ 中部分 API(如
File.ReadAllTextAsync)在路径无效时可能抛ArgumentException,不属于 IO 异常体系,需额外判断
File.Copy 和 File.Move 的原子性陷阱
这两个操作看似简单,但跨卷移动(比如 C: → D:)本质是“复制 + 删除”,中间失败会导致数据残留或丢失;而同卷移动才真正是系统级原子重命名。异常发生时,状态不可预测。
- 不要假设
File.Move总是安全的——检查Path.GetPathRoot(source) == Path.GetPathRoot(destination)再决定是否走 move 或 fallback 到 copy + delete -
File.Copy的overwrite参数为true时,若目标只读,会抛UnauthorizedAccessException,不是IOException - 大文件操作建议加超时控制(用
CancellationToken),否则可能卡死在底层 Win32CopyFileEx调用中
异步 IO 方法的异常传播差异
File.ReadAllBytesAsync、StreamWriter.WriteAsync 这类异步方法,异常不会在调用时立即抛出,而是在 await 时才触发。如果没 await 或用了 .Wait(),异常会被包进 AggregateException,掩盖原始 IOException。
- 永远用
await+ 直接 catch,避免.Result或.Wait() - 异步流操作(如
Stream.CopyToAsync)在写入目标流失败时,异常类型取决于目标流实现——FileStream抛IOException,但MemoryStream不会抛任何 IO 异常 - 使用
FileStream构造函数时,FileShare.None在多线程下极易引发IOException(“该进程无法访问该文件”),应按需设为FileShare.Read
重试逻辑里绕不开的“瞬态错误”识别
不是所有 IOException 都适合重试。比如“拒绝访问”大概率是权限问题,重试无意义;而“设备未就绪”或“网络名称不再可用”可能是暂时性故障。
- 重点关注错误码:用
Marshal.GetHRForLastWin32Error()获取 Win32 错误码,ERROR_IO_PENDING (997)、ERROR_DEVICE_NOT_READY (21)、ERROR_NETWORK_UNREACHABLE (1231)才值得重试 - .NET 5+ 可用
IOException.HResult直接比对,避免 P/Invoke - 重试前务必 sleep(建议指数退避),否则可能加剧文件锁竞争或网络拥塞
- 别在 finally 里关流——如果构造
FileStream就失败了,Dispose会 NRE;用using语句最安全







