2007-12-18

列舉子資料夾的方法(.NET)

簡單, 就 Directory.GetDirectories() 就好啦, 但是如果要列舉的對象有著為數眾多(比如說有好幾萬)的子資料夾或是檔案的話, 那可有得等了, 而且還要有充足的記憶體供它使用, 因為它的回傳值是 string[], 所以如果有類似的需求的話, 只能另想它法囉!

當下想到的就是 WIN32 API 的 FindFirstFile, FindNextFile 函式, 可是要在 .NET 環境中使用的話, 還是稍稍封裝一下比較好, 所以第一步當然是要準備那些 DllImport 的宣告, 和最煩人的 WIN32_FIND_DATA 資料結構囉, 自己參考文件照樣重新製作一個當然也是可以, 可是如果有現成的話該多好!

先用 Reflector 參考一下 Directory.GetDirectories 是怎麼寫的, 它既然要列舉資料夾, 應該也一定會用到那些 API 才對, 果然! 相關的宣告如下:

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr FindFirstFile(string fileName, [In, Out] WIN32_FIND_DATA data);

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern bool FindNextFile(IntPtr hndFindFile, [In, Out, MarshalAs(UnmanagedType.LPStruct)] WIN32_FIND_DATA lpFindFileData);

[DllImport("kernel32.dll")]
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
private static extern bool FindClose(IntPtr handle);
[Serializable, StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto), BestFitMapping(false)]
private class WIN32_FIND_DATA
{
    internal int dwFileAttributes;
    internal int ftCreationTime_dwLowDateTime;
    internal int ftCreationTime_dwHighDateTime;
    internal int ftLastAccessTime_dwLowDateTime;
    internal int ftLastAccessTime_dwHighDateTime;
    internal int ftLastWriteTime_dwLowDateTime;
    internal int ftLastWriteTime_dwHighDateTime;
    internal int nFileSizeHigh;
    internal int nFileSizeLow;
    internal int dwReserved0;
    internal int dwReserved1;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
    internal string cFileName;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
    internal string cAlternateFileName;
}

接下來要就要自己實作一個回傳 IEnumerable 的方法, 每列舉一次只回傳一筆結果, 既可避免等待的問題, 又可以隨時停止, 如果在 .NET 1.x 的時代, 要實作一個 IEnumerable 比較麻煩, 但是 .NET 2.0 因為有 yield 這個新元素可以使用, 所以問題就簡化到一個不可思議的地步, 所以先實作一個內部使用的 internalGetFileDirectoryNames 的方法, 如下:

private const int FILE_ATTRIBUTE_DIRECTORY = 0x10;
private static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);
private static IEnumerable internalGetFileDirectoryNames(string path, string searchPattern, bool includeFiles, bool includeDirs, SearchOption searchOption)
{
    WIN32_FIND_DATA data = new WIN32_FIND_DATA();
    IntPtr findHandle = FindFirstFile(Path.Combine(path, searchPattern), data);

    if (findHandle == INVALID_HANDLE_VALUE) yield break;

    try
    {
        List<string> dirs = new List<string>();
        do
        {
            bool isDirectory = (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
            if (data.cFileName != "." && data.cFileName != "..")
            {
                if (isDirectory && searchOption == SearchOption.AllDirectories) dirs.Add(data.cFileName);

                if ((isDirectory && includeDirs) || (!isDirectory && includeFiles))
                {
                    yield return Path.Combine(path, data.cFileName);
                }
            }
        }
        while (FindNextFile(findHandle, data));

        foreach (string dir in dirs)
        {
            foreach (string f in internalGetFileDirectoryNames(Path.Combine(path, dir), searchPattern, includeFiles, includeDirs, searchOption))
                yield return Path.Combine(path, f);
        }
    }
    finally
    {
        bool r = FindClose(findHandle);
        System.Diagnostics.Debug.Assert(r);
    }
}

最後, 模擬原生的 Directory.GetFiles, Directory.GetDirectories, Directory.GetFileSystemEntries 函式簽名, 自己 overload 一下就搞定啦, 以下列出最多屬性的函式宣告:

public static IEnumerable GetFiles(string path, string searchPattern, SearchOption searchOption)
{
    return internalGetFileDirectoryNames(path, searchPattern, true, false, searchOption);
}
public static IEnumerable GetDirectories(string path, string searchPattern, SearchOption searchOption)
{
    return internalGetFileDirectoryNames(path, searchPattern, false, true, searchOption);
}
public static IEnumerable GetFileSystemEntries(string path, string searchPattern, SearchOption searchOption)
{
    return internalGetFileDirectoryNames(path, searchPattern, true, true, searchOption);
}

完成啦! 完整的程式碼在這裡!

沒有留言: