应直接尝试打开文件并捕获异常,而非预判是否存在;Windows下优先通过IOException.HResult判断错误码32(0x80070020)识别占用,避免字符串匹配或FileShare试探。

文件被占用时 File.Open 会直接抛异常,别靠“是否存在”来判断
很多同学先调 File.Exists,再调 File.Open,以为“存在就一定能打开”,结果一运行就崩在 IOException:“The process cannot access the file because it is being used by another process”。这是因为文件存在 ≠ 可访问 —— 占用是瞬时、排他的状态,检查和打开之间存在竞态窗口。
正确做法是:**直接尝试打开,用异常捕获代替预判**。C# 的 IO 操作本就是“乐观打开 + 异常兜底”的设计哲学。
- 用
FileAccess.Read和FileShare.None最能暴露占用(其他进程哪怕只以FileShare.Read打开,也会被阻断) - 避免用
File.OpenRead—— 它默认FileShare.Read,可能误判为“没被占” - 不要捕获泛型
Exception,只捕获IOException和UnauthorizedAccessException,前者大概率是占用,后者才是权限问题
IOException 的具体错误码比消息文本更可靠
IOException 的 Message 字符串会随系统语言变化(比如中文系统报“该进程无法访问该文件”,英文系统报“The process cannot access...”),但底层 Win32 错误码稳定。在 Windows 上,文件被占用几乎总是对应错误码 32(ERROR_SHARING_VIOLATION)。
所以判断逻辑应基于 IOException.HResult 或 Marshal.GetHRForLastWin32Error(),而不是字符串匹配。
- 检查
ex.HResult == unchecked((int)0x80070020)(即 Win32 错误码 32) - 或用
ex.InnerException?.HResult,某些嵌套场景异常会包一层 - 跨平台代码中,Linux/macOS 不走 Win32 错误码,此时需依赖
ex.Message.Contains("busy")等保守 fallback,但 Windows 主流环境优先信 HResult
想“非阻塞检测”?别用 FileStream 构造函数的 FileShare 参数试探
有人试图用不同 FileShare 值反复试开,比如先试 FileShare.None,失败再试 FileShare.Read……这不可靠,且危险:
- 即使
FileShare.Read成功打开,也不代表原文件“没被占用”——可能另一个进程正以FileShare.Write写入,你读到了脏数据 - 每次
FileStream构造都实际获取句柄,频繁试探会快速耗尽句柄资源(尤其在循环里) - .NET 6+ 的
File.OpenHandle虽轻量,但依然需要SafeFileHandle显式释放,漏掉就内存泄漏
真要非阻塞,唯一合理方式是:用 try { using var _ = File.Open(...); } catch (IOException ex) when (IsSharingViolation(ex)) { /* 占用 */ },然后立刻释放——别留着句柄等后续操作。
异步打开(File.OpenAsync)不会绕过占用检查
看到 “Async” 就以为能“避开冲突”?不是的。File.OpenAsync 只是把同步 IO 搬到线程池执行,底层调用的仍是同一个 Windows API(CreateFile),占用检测逻辑完全一致。它解决的是“不卡主线程”,不是“不撞锁”。
如果你在 UI 线程或 ASP.NET Core 请求线程里开文件,用异步确实防卡死,但异常类型、触发时机、判断方式全都不变。
- 仍要捕获
IOException,仍要看HResult - 注意
async void无法捕获异常,必须用async Task并 await - ASP.NET Core 中,若在中间件里开大文件,记得设
HttpContext.Request.EnableBuffering()避免流已被读取完
事情说清了就结束。真正难的从来不是“怎么写 try-catch”,而是理解:文件占用是操作系统级互斥,不是 .NET 层面能软化或预测的状态;所有绕开它的企图,最后都得回到“敢不敢直面异常”这个原点。


















