.NET Framework < 4.0でEnumerateFiles/EnumerateDirectories

最近になって、プログラミングの世界に舞い戻ってきた。別に必要でもなんでも無いから遠ざかっていて、今も似たような感じだけど、なぜか唐突に始めた感じだ。
さて、昨日今日でやっつけたC#プログラムを作る際、どうしても数万ファイルを探索することになる処理が出てきた。
そういった処理は、例によってDirectory.GetFilesをAllDirectoriesオプションを付けて呼び出し、繰り返しで検索するのがテンプレだ。しかし、GetFilesは配列を返すメソッドなので、ファイル数が多くなってくると、やたらと時間を食うしリソースも食う。
これのイテレータを返すバージョンは無いの?と疑問に思って探してみると、.NET Framework 4.0からの実装ということが分かった。
4.0から?2.0からの間違いじゃないの?どうしてこんな機能を実装してなかったんだ……
嘆いていたって仕方ないので自力救済を図るしかない。従来のWin32アプリケーションは普通、ファイルの列挙にFind***系APIを用いる。Directory.GetFilesなどはこれのフロントエンドだろう。それに倣って、P/Invokeでそれらを呼び出し、yieldをぶん回す関数を考え付いた。
どこかで誰か思いついてそうなネタだろうけどね。

using System;
using System.IO;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;

namespace ExtIO
{
    public static class DirectoryEx
    {
        internal static readonly IntPtr INVALID_HANDLE_VALUE = (IntPtr)(-1);

        internal class SafeFindHandle : SafeHandle
        {
            public SafeFindHandle()
                : base(INVALID_HANDLE_VALUE, true)
            {
            }

            internal SafeFindHandle(IntPtr existingHandle, bool ownsHandle)
                : base(existingHandle, ownsHandle)
            {
            }

            public override bool IsInvalid
            {
                get { return IsClosed || handle == INVALID_HANDLE_VALUE; }
            }

            protected override bool ReleaseHandle()
            {
                if (!IsInvalid)
                {
                    FindClose(handle);
                    return true;
                }
                return false;
            }
        }

        // From P/Invoke.net
        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        internal struct WIN32_FIND_DATA
        {
            public FileAttributes dwFileAttributes;
            public System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime;
            public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime;
            public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime;
            public int nFileSizeHigh;
            public int 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.Auto)]
        internal static extern SafeFindHandle FindFirstFileW(string lpFileName, out WIN32_FIND_DATA lpFindFileData);


        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        internal static extern bool FindNextFile(SafeFindHandle hFindFile, out WIN32_FIND_DATA
           lpFindFileData);

        [DllImport("kernel32.dll", SetLastError = true)]
        internal static extern bool FindClose(IntPtr hFindFile);

        public static IEnumerable<DirectoryInfo> EnumerateDirectories(string searchPath, string searchPattern, SearchOption searchOption)
        {
            WIN32_FIND_DATA findData;
            Regex compiledPattern = new Regex(searchPattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);

            using (SafeFindHandle findHandle = FindFirstFileW(Path.Combine(searchPath, "*"), out findData))
            {
                if (!findHandle.IsInvalid) {
                    do
                    {                       
                        if ((findData.dwFileAttributes & FileAttributes.Directory) != 0)
                        {
                            if (findData.cFileName != "." && findData.cFileName != ".." && compiledPattern.IsMatch(findData.cFileName))
                            {
                                string foundPath = Path.Combine(searchPath, findData.cFileName);
                                yield return new DirectoryInfo(foundPath);
                                if (searchOption == SearchOption.AllDirectories)
                                {
                                    foreach (DirectoryInfo subItem in EnumerateDirectories(foundPath, searchPattern, searchOption))
                                    {
                                        yield return subItem;
                                    }
                                }
                            }
                        }
                    } while (FindNextFile(findHandle, out findData));
                }
            }

            yield break;
        }

        public static IEnumerable<FileInfo> EnumerateFiles(string searchPath, string searchPattern, SearchOption searchOption)
        {
            WIN32_FIND_DATA findData;
            Regex compiledPattern = new Regex(searchPattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);

            foreach (DirectoryInfo dirInfo in EnumerateDirectories(searchPath, ".*", searchOption))
            {
                using (SafeFindHandle findHandle = FindFirstFileW(Path.Combine(dirInfo.FullName, "*"), out findData))
                {
                    if (!findHandle.IsInvalid)
                    {
                        do
                        {  
                            if ((findData.dwFileAttributes & FileAttributes.Directory) == 0)
                            {
                                if (compiledPattern.IsMatch(findData.cFileName))
                                {
                                    string foundPath = Path.Combine(dirInfo.FullName, findData.cFileName);
                                    yield return new FileInfo(foundPath);
                                }
                            }
                        } while (FindNextFile(findHandle, out findData));
                    }
                }
            }
            yield break;
        }
    }
}