C# 文件系统API的异常处理 C#处理IOException的最佳实践是什么

来源:这里教程网 时间:2026-02-21 17:42:27 作者:

捕获
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 和原始路径上下文。

相关推荐