C#异步枚举文件 C#如何使用Directory.EnumerateFiles进行高效遍历

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

Directory.EnumerateFiles 为什么比 GetFiles 更适合大目录

它返回

IEnumerable<string></string>
而非一次性加载全部路径到内存,遍历时按需获取文件名,避免在含数万文件的目录中触发
OutOfMemoryException
。尤其适合后续要过滤、投影或提前中断的场景。

但注意:枚举本身仍是同步 I/O,不等于“异步遍历”——它不会释放当前线程。真需要非阻塞,得配合

Task.Run
或用
FileSystemWatcher
+ 异步回调组合实现。

不要在 UI 线程直接调用它处理超大目录,否则界面卡死 路径通配符(如
"*.log"
)由 Windows API 原生支持,效率高;正则过滤必须靠 C# 侧
.Where()
,会遍历全部再筛选
若需访问文件属性(大小、时间戳),
EnumerateFiles
只返回路径,额外
FileInfo
构造会引发重复系统调用——此时应改用
EnumerateFileSystemEntries
配合
new FileInfo()
批量缓存

如何安全处理权限不足或路径不存在的异常

Directory.EnumerateFiles
在遇到拒绝访问(
UnauthorizedAccessException
)或路径不存在(
DirectoryNotFoundException
)时直接抛异常,不跳过。无法像 PowerShell 的
Get-ChildItem -ErrorAction SilentlyContinue
那样静默忽略。

常见做法是封装一层可恢复的枚举器:

public static IEnumerable<string> SafeEnumerateFiles(string path, string searchPattern = "*.*", SearchOption searchOption = SearchOption.TopDirectoryOnly)
{
    try
    {
        return Directory.EnumerateFiles(path, searchPattern, searchOption);
    }
    catch (UnauthorizedAccessException)
    {
        yield break;
    }
    catch (DirectoryNotFoundException)
    {
        yield break;
    }
}
不能用
try/catch
包裹整个
foreach
循环——异常发生在
MoveNext()
内部,循环外捕获不到
子目录递归时,某一级失败会导致整个枚举终止;如需继续遍历兄弟目录,得手动实现目录树迭代(用
EnumerateDirectories
+ 逐层
EnumerateFiles
某些网络路径(如断开的 NAS)可能抛
IOException
,建议一并 catch

配合 LINQ 实现延迟过滤与提前退出

利用其返回

IEnumerable
的特性,把耗时操作(如正则匹配、字符串处理)和终止条件(如只取前 100 个)留在枚举过程中,避免无谓遍历:

var recentLogs = Directory.EnumerateFiles(@"C:\App\Logs", "*.log", SearchOption.AllDirectories)
    .Where(p => File.GetLastWriteTime(p) > DateTime.Now.AddDays(-7))
    .Take(100)
    .ToArray(); // 此时才真正触发遍历
Take(n)
后接
ToArray()
ToList()
是触发执行的关键,纯链式调用不干活
File.GetLastWriteTime(p)
是同步磁盘 I/O,每调一次都可能慢;如果只是按名称过滤,优先用
searchPattern
参数(如
"error_*.log"
)交由系统完成
不要在
Where
中做复杂解析(如读文件头),那会把延迟优势完全抵消

想真正异步?得绕开 EnumerateFiles 自己调度

Directory.EnumerateFiles
没有异步重载,.NET 6+ 的
FileSystemEnumerable
也未暴露异步接口。所谓“异步枚举文件”,本质是把同步枚举扔进线程池:

var files = await Task.Run(() =>
    Directory.EnumerateFiles(@"D:\Data", "*.csv")
        .Where(f => f.Length > 1000)
        .Take(50)
        .ToArray());
这能释放调用线程(比如 ASP.NET Core 的请求线程),但**不减少总耗时,也不降低 I/O 压力** 高频调用时注意线程池饥饿——大量
Task.Run
可能挤占 CPU 密集型任务资源
若目标是响应式流(如 Web API 分块返回文件列表),应考虑用
IAsyncEnumerable<string></string>
手动包装,每次
yield return
前加
await Task.Yield()
让出控制权

真正的异步文件系统操作仍受限于 Windows API 和底层驱动,目前没有银弹。别被“异步枚举”字面误导——先厘清你要解的是阻塞问题,还是吞吐瓶颈,或是 UI 响应性。

相关推荐