2018-11-17 (土)
[C#] 高速でファイルとフォルダを列挙する
Win 32 APIのFindFirstFileExを使用して実装します。Directoryクラスと同じような静的メソッドで実装するので、そのまま差し替えられます。
環境
- Windows 10 Pro 64bit 1803
- Visual Studio Community 2017 15.8.2
- .NET Framework 4.6.1
- C# 7.1
前提条件
- Windows 7以降のみ対応
- アクセス権限確認(FileIOPermission)を省略した実装
- Nativeな例外処理(Marshal.GetLastWin32Error)を省略した実装
十分な動作確認をしてないので、注意してください。
使用方法
Directory
クラスと同じようなメソッドで使用できます。列挙される順番も同じはずです。
// 戻り値は IEnumerable<string> ファイル(フォルダ)のフルパスを返す
DirectoryUtil.EnumerateFiles(path, searchPattern, searchOption);
DirectoryUtil.EnumerateDirectories(path, searchPattern, searchOption);
DirectoryUtil.EnumerateFileSystemEntries(path, searchPattern, searchOption);
// 戻り値は IEnumerable<FileData> ファイル(フォルダ)の詳細を返す
DirectoryUtil.EnumerateFilesData(path, searchPattern, searchOption);
DirectoryUtil.EnumerateDirectoriesData(path, searchPattern, searchOption);
DirectoryUtil.EnumerateFileSystemEntriesData(path, searchPattern, searchOption);
FileData
クラスはFileInfo
クラスに似た定義です。
public class FileData
{
public FileAttributes Attributes { get; }
public bool IsFile => (Attributes & FileAttributes.Directory) == 0;
public bool IsDirectory => (Attributes & FileAttributes.Directory) != 0;
public DateTime CreationTimeUtc { get; }
public DateTime CreationTime => CreationTimeUtc.ToLocalTime();
public DateTime LastAccessTimeUtc { get; }
public DateTime LastAccesTime => LastAccessTimeUtc.ToLocalTime();
public DateTime LastWriteTimeUtc { get; }
public DateTime LastWriteTime => LastWriteTimeUtc.ToLocalTime();
public long Length { get; }
public string Name { get; }
public string FullName { get; }
internal FileData(ref string fullName, ref NativeMethods.WIN32_FIND_DATA findData)
{
Attributes = findData.dwFileAttributes;
CreationTimeUtc = findData.ToCreationTimeUtc;
LastAccessTimeUtc = findData.ToLastAccessTimeUtc;
LastWriteTimeUtc = findData.ToLastWriteTimeUtc;
Length = ((long)findData.nFileSizeHigh << 32) + findData.nFileSizeLow;
Name = findData.cFileName;
FullName = fullName;
}
public override string ToString() => FullName;
}
実装
DirectoryUtil.cs
にpublic
定義を実装します。
public static partial class DirectoryUtil
{
public static IEnumerable<string> EnumerateFiles(string path, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly)
{
return EnumerateFullName(path, searchPattern, searchOption, true, false);
}
public static IEnumerable<string> EnumerateDirectories(string path, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly)
{
return EnumerateFullName(path, searchPattern, searchOption, false, true);
}
public static IEnumerable<string> EnumerateFileSystemEntries(string path, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly)
{
return EnumerateFullName(path, searchPattern, searchOption, true, true);
}
public static IEnumerable<FileData> EnumerateFilesData(string path, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly)
{
return EnumerateFileData(path, searchPattern, searchOption, true, false);
}
public static IEnumerable<FileData> EnumerateDirectoriesData(string path, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly)
{
return EnumerateFileData(path, searchPattern, searchOption, false, true);
}
public static IEnumerable<FileData> EnumerateFileSystemEntriesData(string path, string searchPattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly)
{
return EnumerateFileData(path, searchPattern, searchOption, true, true);
}
}
public class FileData
{
public FileAttributes Attributes { get; }
public bool IsFile => (Attributes & FileAttributes.Directory) == 0;
public bool IsDirectory => (Attributes & FileAttributes.Directory) != 0;
public DateTime CreationTimeUtc { get; }
public DateTime CreationTime => CreationTimeUtc.ToLocalTime();
public DateTime LastAccessTimeUtc { get; }
public DateTime LastAccesTime => LastAccessTimeUtc.ToLocalTime();
public DateTime LastWriteTimeUtc { get; }
public DateTime LastWriteTime => LastWriteTimeUtc.ToLocalTime();
public long Length { get; }
public string Name { get; }
public string FullName { get; }
internal FileData(ref string fullName, ref NativeMethods.WIN32_FIND_DATA findData)
{
Attributes = findData.dwFileAttributes;
CreationTimeUtc = findData.ToCreationTimeUtc;
LastAccessTimeUtc = findData.ToLastAccessTimeUtc;
LastWriteTimeUtc = findData.ToLastWriteTimeUtc;
Length = ((long)findData.nFileSizeHigh << 32) + findData.nFileSizeLow;
Name = findData.cFileName;
FullName = fullName;
}
public override string ToString() => Name;
}
DirectoryUtilEnumerable.cs
にinternal
定義を実装します。
ISelector<T>
インターフェイスを実装したクラスをEnumerateCore<T>
メソッドに渡せば、戻り値の拡張はできます。
(以下はprivate定義にしているので、このままでは外部から拡張できませんが。)
public static partial class DirectoryUtil
{
internal static class NativeMethods
{
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern SafeFindFileHandle FindFirstFileEx(
string lpFileName,
FINDEX_INFO_LEVELS fInfoLevelId,
out WIN32_FIND_DATA lpFindFileData,
FINDEX_SEARCH_OPS fSearchOp,
IntPtr lpSearchFilter,
FIND_FIRST_EX dwAdditionalFlags);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, BestFitMapping = false)]
internal static extern bool FindNextFile(SafeFindFileHandle hFindFile, out WIN32_FIND_DATA
lpFindFileData);
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[DllImport("kernel32.dll")]
internal static extern bool FindClose(IntPtr handle);
internal enum FINDEX_INFO_LEVELS
{
Standard = 0,
Basic = 1
}
internal enum FINDEX_SEARCH_OPS
{
SearchNameMatch = 0,
SearchLimitToDirectories = 1,
SearchLimitToDevices = 2
}
internal enum FIND_FIRST_EX
{
CaseSensitive = 1,
LargeFetch = 2
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
[BestFitMapping(false)]
internal struct WIN32_FIND_DATA
{
public FileAttributes dwFileAttributes;
public FILE_TIME ftCreationTime;
public FILE_TIME ftLastAccessTime;
public FILE_TIME 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;
internal bool IsRelative => cFileName == "." || cFileName == "..";
internal bool IsFile => (dwFileAttributes & FileAttributes.Directory) == 0;
internal bool IsDirectory => (dwFileAttributes & FileAttributes.Directory) != 0;
internal DateTime ToCreationTimeUtc => DateTime.FromFileTimeUtc(ftCreationTime.ToTicks());
internal DateTime ToLastAccessTimeUtc => DateTime.FromFileTimeUtc(ftLastAccessTime.ToTicks());
internal DateTime ToLastWriteTimeUtc => DateTime.FromFileTimeUtc(ftLastWriteTime.ToTicks());
public override string ToString() => cFileName;
}
[StructLayout(LayoutKind.Sequential)]
internal struct FILE_TIME
{
public FILE_TIME(long fileTime)
{
ftTimeLow = (uint)fileTime;
ftTimeHigh = (uint)(fileTime >> 32);
}
public long ToTicks()
{
return ((long)ftTimeHigh << 32) + ftTimeLow;
}
internal uint ftTimeLow;
internal uint ftTimeHigh;
}
internal sealed class SafeFindFileHandle : SafeHandleZeroOrMinusOneIsInvalid
{
internal SafeFindFileHandle() : base(true)
{
}
protected override bool ReleaseHandle()
{
return FindClose(handle);
}
}
}
private interface ISelector<T>
{
T Create(ref string fullName, ref NativeMethods.WIN32_FIND_DATA findData);
}
private class FullNameSelector : ISelector<string>
{
public string Create(ref string fullName, ref NativeMethods.WIN32_FIND_DATA findData) => fullName;
}
private class FileDataSelector : ISelector<FileData>
{
public FileData Create(ref string fullName, ref NativeMethods.WIN32_FIND_DATA findData) => new FileData(ref fullName, ref findData);
}
private static IEnumerable<string> EnumerateFullName(string path, string searchPattern, SearchOption searchOption, bool includeFiles, bool includeDirs)
{
return Enumerate(path, searchPattern, searchOption, includeFiles, includeDirs, new FullNameSelector());
}
private static IEnumerable<FileData> EnumerateFileData(string path, string searchPattern, SearchOption searchOption, bool includeFiles, bool includeDirs)
{
return Enumerate(path, searchPattern, searchOption, includeFiles, includeDirs, new FileDataSelector());
}
private static IEnumerable<T> Enumerate<T>(string path, string searchPattern, SearchOption searchOption, bool includeFiles, bool includeDirs, ISelector<T> selector)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
if (searchPattern == null)
throw new ArgumentNullException(nameof(searchPattern));
if (searchOption != SearchOption.TopDirectoryOnly && searchOption != SearchOption.AllDirectories)
throw new ArgumentOutOfRangeException(nameof(searchOption));
return EnumerateCore(Path.GetFullPath(path).TrimEnd('\\'), searchPattern, searchOption, includeFiles, includeDirs, selector);
}
private static IEnumerable<T> EnumerateCore<T>(string dir, string searchPattern, SearchOption searchOption, bool includeFiles, bool includeDirs, ISelector<T> selector)
{
// extend MAX_PATH
var search = (dir.StartsWith(@"\\", StringComparison.OrdinalIgnoreCase)
? @"\\?\UNC\" + dir.Substring(2)
: @"\\?\" + dir) + @"\" + searchPattern;
Queue<string> subDirs = null;
using (var fileHandle = NativeMethods.FindFirstFileEx(search,
NativeMethods.FINDEX_INFO_LEVELS.Basic,
out var findData,
NativeMethods.FINDEX_SEARCH_OPS.SearchNameMatch,
IntPtr.Zero,
NativeMethods.FIND_FIRST_EX.LargeFetch))
{
if (fileHandle.IsInvalid) yield break;
do
{
if (findData.IsRelative) continue;
var path = dir + @"\" + findData.cFileName;
if (findData.IsFile)
{
if (includeFiles)
yield return selector.Create(ref path, ref findData);
}
else if (findData.IsDirectory)
{
if (includeDirs)
yield return selector.Create(ref path, ref findData);
if (searchOption == SearchOption.AllDirectories)
{
subDirs = subDirs ?? new Queue<string>();
subDirs.Enqueue(path);
}
}
} while (NativeMethods.FindNextFile(fileHandle, out findData));
}
if (subDirs == null) yield break;
while (subDirs.Count > 0)
{
foreach (var path in EnumerateCore(subDirs.Dequeue(), searchPattern, searchOption, includeFiles, includeDirs, selector))
yield return path;
}
}
}
注意
Windows7より前は未対応
Windows Vista以前の場合は、FindFirstFileEx
の引数に注意する必要があるようです。
FindExInfoBasic
は使用できない。FIND_FIRST_EX_LARGE_FETCH
は使用できない。- pinvoke.net: FindFirstFileEx (kernel32)
こんな感じで対応が必要ですが、列挙速度は落ちるかも知れません。
FINDEX_INFO_LEVELS findInfoLevel = FINDEX_INFO_LEVELS.FindExInfoStandard;
int additionalFlags = 0;
if (Environment.OSVersion.Version.Major >= 6)
{
findInfoLevel = FINDEX_INFO_LEVELS.FindExInfoBasic;
additionalFlags = FIND_FIRST_EX_LARGE_FETCH;
}
WIN32_FIND_DATAの定義の注意点
System.Runtime.InteropServices.ComTypes.FILETIMEは使わない
- 上記は
int
で定義されているが、uint
を定義した構造体を使用する - .NET Frameworkの実装も
uint
になっている - ほとんど問題ないがMacで作成したZIPで問題が発生することがある?
FILETIMEはlongにしない
FILETIME
構造体で定義し、long
で定義しない方が良いみたいです。
FILETIME structure (Windows)
またuint
でHigh, Lowで直接定義せずに、FILETIME構造体を定義した方が良いのかも知れません。
// OK
public FILE_TIME ftCreationTime;
public FILE_TIME ftLastAccessTime;
public FILE_TIME ftLastWriteTime;
// NG
public System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime;
public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime;
public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime;
// NG
public long ftCreationTime;
public long ftLastAccessTime;
public long ftLastWriteTime;
// NG
public uint ftCreationTime_dwLowDateTime;
public uint ftCreationTime_dwHighDateTime;
public uint ftLastAccessTime_dwLowDateTime;
public uint ftLastAccessTime_dwHighDateTime;
public uint ftLastWriteTime_dwLowDateTime;
public uint ftLastWriteTime_dwHighDateTime;
[StructLayout(LayoutKind.Sequential)]
internal struct FILE_TIME
{
public FILE_TIME(long fileTime)
{
ftTimeLow = (uint)fileTime;
ftTimeHigh = (uint)(fileTime >> 32);
}
public long ToTicks()
{
return ((long)ftTimeHigh << 32) + ftTimeLow;
}
internal uint ftTimeLow;
internal uint ftTimeHigh;
}
感謝
- 参考記事
- FindFirstFile の使用例
- FindFirstFileEx の使用例 (マルチスレッドでトータルサイズやカウントを計測)
- FindFirstFileExのオプションについて
- マルチスレッドの検索例
- NTFS解析で更に高速化?
- ドキュメント関係
関連記事
新着記事