使用 WinAPI 的 System.IO.Directory 替代方案






4.80/5 (35投票s)
比 System.IO.Directory 的 IEnumerable 方法 EnumerateDirectories、EnumerateFiles 和 EnumerateFileSystemEntries 更快、更好的替代方案
引言
最近,我正在处理一个需要读取 Windows 目录内容的的项目,所以我使用了 .NET 提供的 System.IO.Directory
类的 EnumerateDirectories
、EnumerateFiles
和 EnumerateFileSystemEntries
方法。不幸的是,使用这些功能有一个很大的缺点,那就是如果它们遇到一个当前用户无权访问的文件系统条目,它们会立即中断 - 而不是处理这种错误并继续,它们只会返回它们在中断之前收集到的内容 - 并且不会完成任务。
从这些方法之外处理这个问题是不可能的,因为如果你处理它,你将只能在返回的 IEnumerable
中获得部分结果。
我到处寻找这个问题的解决方案,但我找不到一个不使用上述方法的解决方法。所以我决定尝试使用 Windows API 并创建替代方法。结果不仅更好(在方法不会因“访问被拒绝”而中断方面),而且似乎比 .NET 的原始方法还要快。
Using the Code
项目本身是一个类库类型,它不可执行,但构建它会将这些方法编译成一个 DLL 文件,您可以在另一个项目中引用它,并从那里像这样使用它
using System.IO;
DirectoryAlternative.EnumerateDirectories
(path, "*", SearchOption.AllDirectories).ToList<string>();
我使用了与原始过程相同的命名空间 (System.IO
),并将类命名为 DirectoryAlternative
- 因此用法将尽可能类似于原始类。
这些方法本身也被命名为相同的方式,它们使用相同的参数,从外部看起来与原始方法完全相同。
这是一个方法用法的例子
System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
string path = "V:\\MUSIC";
List<string> en = new List<string>();
sw.Start();
try { en = Directory.EnumerateDirectories
(path, "*", SearchOption.AllDirectories).ToList<string>(); } catch { }
sw.Stop();
Console.WriteLine("Directory.EnumerateDirectories : {0} ms / {1} entries",
sw.ElapsedMilliseconds.ToString("N0"), en.Count.ToString("N0"));
sw.Reset();
en = new List<string>();
sw.Start();
en = DirectoryAlternative.EnumerateDirectories(path, "*",
SearchOption.AllDirectories).ToList<string>();
sw.Stop();
Console.WriteLine("DirectoryAlternative.EnumerateDirectories :
{0} ms / {1} entries", sw.ElapsedMilliseconds.ToString("N0"), en.Count.ToString("N0"));
sw.Reset();
en = new List<string>();
sw.Start();
try { en = Directory.EnumerateFiles(path, "*",
SearchOption.AllDirectories).ToList<string>(); } catch { }
sw.Stop();
Console.WriteLine("Directory.EnumerateFiles : {0} ms / {1} entries",
sw.ElapsedMilliseconds.ToString("N0"), en.Count.ToString("N0"));
sw.Reset();
en = new List<string>();
sw.Start();
en = DirectoryAlternative.EnumerateFiles
(path, "*", SearchOption.AllDirectories).ToList<string>();
sw.Stop();
Console.WriteLine("DirectoryAlternative.EnumerateFiles : {0} ms / {1} entries",
sw.ElapsedMilliseconds.ToString("N0"), en.Count.ToString("N0"));
sw.Reset();
en = new List<string>();
sw.Start();
try { en = Directory.EnumerateFileSystemEntries
(path, "*", SearchOption.AllDirectories).ToList<string>(); } catch { }
sw.Stop();
Console.WriteLine("Directory.EnumerateFileSystemEntries : {0} ms / {1} entries",
sw.ElapsedMilliseconds.ToString("N0"), en.Count.ToString("N0"));
sw.Reset();
en = new List<string>();
sw.Start();
en = DirectoryAlternative.EnumerateFileSystemEntries
(path, "*", SearchOption.AllDirectories).ToList<string>();
sw.Stop();
Console.WriteLine("DirectoryAlternative.EnumerateFileSystemEntries : {0} ms / {1} entries",
sw.ElapsedMilliseconds.ToString("N0"), en.Count.ToString("N0"));
Console.ReadKey();
上面的代码片段直接比较了原始方法的性能和 DirectoryAlternative
方法的性能 - 我使用了一个非常大的目录,其中包含 70,000+ 个文件系统条目
正如您所看到的,DirectoryAlternative
方法的运行速度快了大约 50%。
代码工作原理
代码使用几个 Win API 函数来在文件系统中移动(我相信这些相同的函数在原始的 .NET 方法中使用)
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
struct WIN32_FIND_DATA
{
public uint dwFileAttributes;
public System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime;
public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime;
public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime;
public uint nFileSizeHigh;
public uint nFileSizeLow;
public uint dwReserved0;
public uint dwReserved1;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string cFileName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
public string cAlternateFileName;
}
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern bool FindClose(IntPtr hFindFile);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern IntPtr FindFirstFile
(string lpFileName, out WIN32_FIND_DATA lpFindFileData);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern bool FindNextFile
(IntPtr hFindFile, out WIN32_FIND_DATA lpFindFileData);
简而言之
FindFirstFile
使用提供的模式 (lpFileName
) 搜索它能找到的第一个文件系统条目,并返回此文件的句柄 (IntPtr
)FindNextFile
搜索与指定模式匹配的下一个文件系统条目 - 我们使用此方法来遍历所有文件/目录FindClose
用于关闭句柄
所有文件信息都收集在 WIN32_FIND_DATA
struct
中,并作为 out
类型参数返回。
有关这些方法的更多信息,您可以在 此处查找。
主要方法是 Enumerate
方法。所有其他方法都围绕着这个方法。
private static void Enumerate(string path, string searchPattern,
SearchOption searchOption, ref List<string> retValue, EntryType entryType)
{
WIN32_FIND_DATA findData;
if (path.Last<char>() != '\\') path += "\\";
AdjustSearchPattern(ref path, ref searchPattern);
searchPattern = searchPattern.Replace("*.*", "*");
Text.RegularExpressions.Regex rx = new Text.RegularExpressions.Regex(
"^" +
Text.RegularExpressions.Regex.Escape(path) +
Text.RegularExpressions.Regex.Escape(searchPattern)
.Replace("\\*", ".*")
.Replace("\\?", ".")
+ "$"
, Text.RegularExpressions.RegexOptions.IgnoreCase);
IntPtr hFile = FindFirstFile(path + "*", out findData);
List<string> subDirs = new List<string>();
if (hFile.ToInt32() != -1)
{
do
{
if (findData.cFileName == "." || findData.cFileName == "..") continue;
if ((findData.dwFileAttributes &
(uint)FileAttributes.Directory) == (uint)FileAttributes.Directory)
{
subDirs.Add(path + findData.cFileName);
if ((entryType == EntryType.Directories ||
entryType == EntryType.All) && rx.IsMatch(path + findData.cFileName))
retValue.Add(path + findData.cFileName);
}
else
{
if ((entryType == EntryType.Files ||
entryType == EntryType.All) && rx.IsMatch(path + findData.cFileName))
retValue.Add(path + findData.cFileName);
}
} while (FindNextFile(hFile, out findData));
if (searchOption == SearchOption.AllDirectories)
foreach (string subdir in subDirs)
Enumerate(subdir, searchPattern, searchOption, ref retValue, entryType);
}
FindClose(hFile);
}
该方法接受原始 Enumerate
方法的所有参数 (path
, searchPattern
, searchOption
),以及一个按引用传递的参数 retValue
,以及一个 enum
类型的 entryType
private enum EntryType { All = 0, Directories = 1, Files = 2 };
这个 enum
用作一个选择器,用于确定是只返回目录、只返回文件还是两者都返回。
Enumerate
方法调用 FindFirstFile
,然后通过调用 FindNextFile
迭代所有其他文件系统条目。如果 entryType = Files
,它将把所有文件添加到 retValue
列表中。对于 Directories
,它将只添加目录,对于 All
,它将添加两者。
该方法总是搜索所有文件系统条目 (searchOption = "*"
),一个 Regex
(正则表达式) 对象负责过滤应该实际返回哪些文件和/或文件夹。这是该方法第一个版本的升级,该版本向 FindFirstFile
API 函数提供了 path + searchPattern
参数,不幸的是,事实证明这仅适用于 *.* 搜索,而不适用于特定文件(例如 *.jpg),因为 searchOption = AllDirectories
, 因为没有匹配此搜索模式的子文件夹,因此只返回了顶级目录中的文件。
如果 searchOption = AllDirectories
,则递归调用 Enumerate
方法。 来自所有(递归)调用的结果都收集在一个变量 retValue
中。我使用一个按引用传递的参数来传递每个递归调用的结果,因为返回和连接列表已被证明非常非常慢 - 但是,如果有人更喜欢使用 Enumerate
方法的 List 返回类型,则 List.AddRange
方法的工作速度也一样快。
最后,每个调用的文件搜索句柄将通过调用方法 FindClose
关闭。
测试代码
我创建了一小段代码(控制台应用程序)用于测试目的 - 与 System.IO.Directory 方法(.NET 标准版本)的比较
static void Main(string[] args)
{
Stopwatch sw = new Stopwatch();
string path = @"V:\MUSIC";
List<string> searchPatterns = new List<string>();
searchPatterns.Add("*.*");
searchPatterns.Add("*.mp3");
searchPatterns.Add("*.jpg");
searchPatterns.Add("Iron*");
searchPatterns.Add("Iron Maiden\\*.mp?");
searchPatterns.Add("IRON MAIDEN");
searchPatterns.Add("Iron Maiden\\*.jp?g");
List<SearchOption> searchOptions = new List<SearchOption>();
searchOptions.Add(SearchOption.AllDirectories);
searchOptions.Add(SearchOption.TopDirectoryOnly);
List<Func<string, string, SearchOption, IEnumerable<string>>> funcs =
new List<Func<string, string, SearchOption, IEnumerable<string>>>();
funcs.Add(DirectoryAlternative.EnumerateFiles);
funcs.Add(Directory.EnumerateFiles);
funcs.Add(DirectoryAlternative.EnumerateDirectories);
funcs.Add(Directory.EnumerateDirectories);
funcs.Add(DirectoryAlternative.EnumerateFileSystemEntries);
funcs.Add(Directory.EnumerateFileSystemEntries);
IEnumerable<string> list;
int cnt;
System.Reflection.MethodInfo mi;
Console.WriteLine("METHOD MODULE SEARCHPATTERN SEARCHOPTION TIME COUNT");
Console.WriteLine("=====================================================================================================================");
foreach (string searchPattern in searchPatterns)
{
foreach (SearchOption searchOption in searchOptions)
{
foreach (Func<string, string, SearchOption, IEnumerable<string>>
func in funcs)
{
sw.Restart();
list = func(path, searchPattern, searchOption);
cnt = list.Count();
sw.Stop();
mi = System.Reflection.RuntimeReflectionExtensions.GetMethodInfo(func);
Console.WriteLine(Wrap(mi.Name, 19) + " "
+ Wrap(mi.Module.Name, 29) + " "
+ Wrap(searchPattern, 19) + " "
+ Wrap(searchOption == SearchOption.TopDirectoryOnly ?
"root" : "all", 19) + " "
+ Wrap(sw.ElapsedMilliseconds.ToString("N0") + "ms", 19) + " "
+ cnt.ToString());
Console.ReadKey();
}
}
}
Console.WriteLine();
Console.WriteLine("THE END!!!");
Console.ReadKey();
}
static string Wrap(string str, int len)
{
if (str.Length > len)
return "..." + str.Substring(str.Length - len + 3, len - 3);
else
return str.PadRight(len);
}