在C#中列出目录和文件并不是只有一个API那么简单。System.IO命名空间提供了Directory静态类和DirectoryInfo实例类两条路线,各自又有同步、异步、惰性枚举等不同方法。选对API可以显著降低内存峰值,选错则可能在数十万文件的目录下卡死进程。本文从最常用的几个方法入手,逐步展开递归遍历、结果排序、权限处理和性能对比。

基础方法:Directory.GetFiles 与 Directory.GetDirectories
Directory静态类提供了最直接的文件列举能力。GetFiles方法接受目录路径作为第一个参数,可选地传入搜索模式与搜索选项。如果不指定搜索选项,默认只检查当前目录,不会自动进入子目录。返回结果是一个字符串数组,每个元素是文件的完整路径,可以直接交给File.ReadAllText、File.Copy等方法使用。
这种一次性返回数组的方式有一个明显特征:当GetFiles执行完成时,文件列表已经完全加载到内存中。对于包含几千个文件的目录,这点内存开销可以忽略不计;但如果目标目录存在几十万甚至上百万个文件,数组本身加上每个字符串对象的开销会迅速累积,可能导致短暂的卡顿甚至内存压力。因此,当目录规模较大或者只需要逐个处理文件时,后续介绍的EnumerateFiles会更合适。
using System;
using System.IO;
class ListFilesBasic
{
static void Main()
{
string path = @"C:\example\project";
string[] files = Directory.GetFiles(path);
Console.WriteLine("文件总数:" + files.Length);
foreach (string file in files)
{
Console.WriteLine(file);
}
// 只列出 .cs 文件
string[] csFiles = Directory.GetFiles(path, "*.cs", SearchOption.TopDirectoryOnly);
string[] dirs = Directory.GetDirectories(path);
foreach (string dir in dirs)
{
Console.WriteLine("目录:" + dir);
}
}
}
GetFiles的搜索模式支持星号和问号作为通配符,例如*.cs会匹配所有扩展名为cs的文件,file?.txt可以匹配file1.txt但不会匹配file10.txt。搜索模式不是正则表达式,不能使用方括号或点号进行更复杂的匹配。如果需要按正则规则筛选文件,可以先列出所有文件,再用Regex类进行二次过滤。
使用 DirectoryInfo 和 FileInfo 获取详细信息
Directory静态类适合只需要路径字符串的场景,但如果还需要读取文件大小、创建时间、修改时间、只读属性等元数据,则应该使用DirectoryInfo和FileInfo。DirectoryInfo的构造函数接收目录路径,之后可以反复调用GetFiles、GetDirectories等方法,避免每次调用都重复解析路径。
DirectoryInfo.GetFiles返回的是FileInfo对象数组。FileInfo封装了Length、LastWriteTime、CreationTime、Attributes等属性,这些属性在访问时才会触发对应的文件系统查询。如果对同一文件读取多个属性,FileInfo内部会缓存部分信息,但多个FileInfo对象之间没有批量查询的优化,因此在需要读取大量文件元数据时,仍需衡量性能。
using System;
using System.IO;
using System.Linq;
class ListWithDirectoryInfo
{
static void Main()
{
DirectoryInfo di = new DirectoryInfo(@"C:\example\project");
FileInfo[] files = di.GetFiles("*.cs", SearchOption.TopDirectoryOnly);
// 按修改时间倒序排列
var sorted = files.OrderByDescending(f => f.LastWriteTime);
foreach (FileInfo fi in sorted)
{
Console.WriteLine($"{fi.Name} - {fi.Length} 字节 - {fi.LastWriteTime}");
}
}
}
与Directory.GetFiles类似,DirectoryInfo.GetFiles同样支持SearchOption.AllDirectories实现递归列出。但GetFiles返回数组的特性意味着递归模式会在方法返回前访问完全部子目录,并把所有FileInfo对象装入内存。如果只需要按修改时间排序,可以先用DirectoryInfo.EnumerateFiles获得IEnumerable<FileInfo>,再用OrderByDescending处理,但要注意排序本身仍然需要缓存全部元素,因此惰性枚举在排序场景下的内存优势会被排序操作抵消。
递归遍历目录树与 EnumerateFiles 的惰性枚举
EnumerateFiles是解决大目录内存问题的关键。它与GetFiles使用相同的参数签名,但返回类型是IEnumerable<string>。这个接口本身不会立即执行枚举,只有在foreach开始迭代时,才会逐步访问文件系统并返回一个文件路径。这意味着可以在处理完前100个文件后提前退出,而不必等待剩余文件被读取。
除了提前退出,惰性枚举还允许边遍历边处理。例如从日志目录中读取每个文件并解析内容,如果使用GetFiles,必须先拿到全部文件路径再循环;而EnumerateFiles可以在拿到第一个路径时就进入处理逻辑,降低启动延迟。面对千万级文件目录时,这种差异会变得非常明显。
using System;
using System.IO;
using System.Collections.Generic;
class LazyEnumeration
{
static void Main()
{
string root = @"C:\example\project";
IEnumerable<string> allFiles = Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories);
int count = 0;
foreach (string file in allFiles)
{
Console.WriteLine(file);
count++;
if (count > 100)
{
Console.WriteLine("已显示前100项,提前退出");
break;
}
}
}
}
SearchOption.AllDirectories虽然方便,但它会递归所有层级的子目录。如果目录树中存在指向父目录的符号链接或Junction,可能造成无限循环。Windows的符号链接默认不会在SearchOption.AllDirectories中被自动跳过,因此在处理用户提供的任意目录时,最好自己实现递归逻辑,并加入深度限制或循环检测。
using System;
using System.IO;
using System.Collections.Generic;
class SafeRecursiveLister
{
public static IEnumerable<string> SafeEnumerateFiles(string root, string pattern = "*")
{
IEnumerable<string> files;
try
{
files = Directory.EnumerateFiles(root, pattern, SearchOption.TopDirectoryOnly);
}
catch (UnauthorizedAccessException)
{
yield break;
}
catch (DirectoryNotFoundException)
{
yield break;
}
foreach (string file in files)
{
yield return file;
}
IEnumerable<string> dirs;
try
{
dirs = Directory.EnumerateDirectories(root, "*", SearchOption.TopDirectoryOnly);
}
catch (UnauthorizedAccessException)
{
yield break;
}
catch (DirectoryNotFoundException)
{
yield break;
}
foreach (string dir in dirs)
{
foreach (string nestedFile in SafeEnumerateFiles(dir, pattern))
{
yield return nestedFile;
}
}
}
static void Main()
{
foreach (string f in SafeEnumerateFiles(@"C:\example\project"))
{
Console.WriteLine(f);
}
}
}
上面的SafeEnumerateFiles使用yield return构建了一个安全的惰性枚举器。它在访问子目录时单独捕获UnauthorizedAccessException和DirectoryNotFoundException,跳过无权限或不存在的目录,而不是中断整个遍历。实际项目中可以在此基础上增加深度计数器,当递归深度超过某个阈值时停止继续深入,有效防止目录环路。
权限处理、异常与性能对比
列出目录时最常遇到的异常是UnauthorizedAccessException和DirectoryNotFoundException。前者出现在当前用户没有读取某个子目录权限时,例如访问C:\Windows\System32或C:\Program Files下的受保护目录。后者通常是因为目标目录在调用前被删除或移动。对于.NET Core 3.0及以上版本,可以使用EnumerationOptions来简化权限处理。
using System;
using System.IO;
class EnumerationOptionsDemo
{
static void Main()
{
string root = @"C:\Windows\System32";
var options = new EnumerationOptions
{
RecurseSubdirectories = true,
IgnoreInaccessible = true,
ReturnSpecialDirectories = false
};
foreach (string file in Directory.EnumerateFiles(root, "*.dll", options))
{
Console.WriteLine(file);
}
}
}
EnumerationOptions的IgnoreInaccessible属性设置为true后,枚举器会自动跳过无法访问的目录,不再抛出异常。RecurseSubdirectories等价于SearchOption.AllDirectories。这个选项在.NET Framework中不可用,旧项目只能手动捕获异常。ReturnSpecialDirectories为false表示不返回系统特殊目录,进一步减少干扰项。
| 方法 | 返回类型 | 内存占用 | 适用场景 |
|---|---|---|---|
| Directory.GetFiles | string[] | 高 | 小目录、需要随机访问 |
| Directory.EnumerateFiles | IEnumerable<string> | 低 | 大目录、流式处理 |
| DirectoryInfo.GetFiles | FileInfo[] | 高 | 需要文件元数据 |
| DirectoryInfo.EnumerateFiles | IEnumerable<FileInfo> | 低 | 需要元数据且目录大 |
从表格可以看出,只要目录可能包含大量文件,就应该优先选择Enumerate系列。如果需要一次性获取所有结果并多次随机访问,GetFiles返回数组会更加简单。实践中还可以结合任务并行库处理文件内容,但要注意并行读取同一个机械硬盘上的文件并不会带来线性加速,反而可能因磁头抖动降低性能。
C#列出目录DirectoryInfoEnumerateFiles修改时间:2026-09-26 18:14:53