65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (35投票s)

2019年4月7日

CPOL

4分钟阅读

viewsIcon

47827

downloadIcon

1131

比 System.IO.Directory 的 IEnumerable 方法 EnumerateDirectories、EnumerateFiles 和 EnumerateFileSystemEntries 更快、更好的替代方案

引言

最近,我正在处理一个需要读取 Windows 目录内容的的项目,所以我使用了 .NET 提供的 System.IO.Directory 类的 EnumerateDirectoriesEnumerateFilesEnumerateFileSystemEntries 方法。不幸的是,使用这些功能有一个很大的缺点,那就是如果它们遇到一个当前用户无权访问的文件系统条目,它们会立即中断 - 而不是处理这种错误并继续,它们只会返回它们在中断之前收集到的内容 - 并且不会完成任务。

从这些方法之外处理这个问题是不可能的,因为如果你处理它,你将只能在返回的 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);
        }
© . All rights reserved.