捕获 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),否则可能卡死在底层 Win32
CopyFileEx调用中
异步 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语句最安全 事情说清了就结束。真正难的不是 catch 哪个异常,而是区分哪些错误能自动恢复、哪些必须人工介入——这得靠日志里带上的 HResult 和原始路径上下文。
