2009-04-16 4 views
102

Я должен проверить, если каталог на диске пуст. Это означает, что он не содержит никаких папок/файлов. Я знаю, что есть простой метод. Мы получаем массив FileSystemInfo и проверяем, равен ли число элементов нулю. Что-то вроде этого:Как быстро проверить, пуста ли папка (.NET)?

public static bool CheckFolderEmpty(string path) 
{ 
    if (string.IsNullOrEmpty(path)) 
    { 
     throw new ArgumentNullException("path"); 
    } 

    var folder = new DirectoryInfo(path); 
    if (folder.Exists) 
    { 
     return folder.GetFileSystemInfos().Length == 0; 
    } 

    throw new DirectoryNotFoundException(); 
} 

Этот подход кажется ОК. НО!! Это очень, очень плохо с точки зрения производительности. GetFileSystemInfos() - очень жесткий метод. Фактически, он перечисляет все объекты файловой системы в папке, получает все их свойства, создает объекты, заполняет типизированный массив и т. Д. И все это просто для проверки длины. Это глупо, не так ли?

Я просто профилировал такой код и определил, что ~ 250 вызовов такого метода выполняются в ~ 500 мс. Это очень медленно, и я считаю, что это можно сделать гораздо быстрее.

Любые предложения?

+6

Из любопытства, почему вы хотели бы проверку каталога 250 раз? – ya23

+1

@ ya23 Предположим, вы хотели бы проверить 250 разных каталогов. Ни одного 250 раз. –

ответ

25

Вот дополнительное быстрое решение, что я наконец-то реализована.Здесь я использую WinAPI и функции FindFirstFile, FindNextFile. Это позволяет избежать перечисления всех элементов в папке и останавливается сразу после обнаружения первого объекта в папке. Этот подход составляет ~ 6 (!!) раз быстрее, чем описано выше. 250 звонков в 36ms!

private static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1); 

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] 
private 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", CharSet=CharSet.Auto)] 
private static extern IntPtr FindFirstFile(string lpFileName, out WIN32_FIND_DATA lpFindFileData); 

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

[DllImport("kernel32.dll")] 
private static extern bool FindClose(IntPtr hFindFile); 

public static bool CheckDirectoryEmpty_Fast(string path) 
{ 
    if (string.IsNullOrEmpty(path)) 
    { 
     throw new ArgumentNullException(path); 
    } 

    if (Directory.Exists(path)) 
    { 
     if (path.EndsWith(Path.DirectorySeparatorChar.ToString())) 
      path += "*"; 
     else 
      path += Path.DirectorySeparatorChar + "*"; 

     WIN32_FIND_DATA findData; 
     var findHandle = FindFirstFile(path, out findData); 

     if (findHandle != INVALID_HANDLE_VALUE) 
     { 
      try 
      { 
       bool empty = true; 
       do 
       { 
        if (findData.cFileName != "." && findData.cFileName != "..") 
         empty = false; 
       } while (empty && FindNextFile(findHandle, out findData)); 

       return empty; 
      } 
      finally 
      { 
       FindClose(findHandle); 
      } 
     } 

     throw new Exception("Failed to get directory first file", 
      Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error())); 
    } 
    throw new DirectoryNotFoundException(); 
} 

Надеюсь, это будет полезно для кого-то в будущем.

+0

Благодарим вас за то, что поделились своим решением. – Greg

+3

Вам нужно добавить 'SetLastError = true' в' DllImport' для 'FindFirstFile' для того, чтобы вызов' Marshal.GetHRForLastWin32Error() 'работал правильно, как описано в разделе« Примечания »в документе [MSDN doc для GetHRForLastWin32Error() ] (http://msdn.microsoft.com/en-us/library/system.runtime.interopservices.marshal.gethrforlastwin32error.aspx). –

15

Вы можете попробовать Directory.Exists(path) и Directory.GetFiles(path) - возможно, меньше накладных расходов (без объектов - только строки и т. Д.).

+0

Как всегда, вы быстрее всего спускаетесь! Убей меня на несколько секунд! :-) – Cerebrus

+0

Вы были быстрее, чем я ... проклятье мое внимание к деталям ;-) –

+2

Не сделал мне ничего хорошего; первый ответ, и единственный без голосования ;-( –

7

Я не знаю о статистике производительности на этом, но вы пробовали использовать статический метод Directory.GetFiles()?

Он возвращает строковый массив, содержащий имена файлов (не FileInfos), и вы можете проверить длину массива так же, как указано выше.

+0

, это может быть медленным, если есть много файлов ... но, вероятно, это быстрее, чем GetFileSystemInfos. –

3

Вам потребуется перейти на жесткий диск для этой информации в любом случае, и это само по себе вызовет любое создание объекта и заполнение массива.

+1

Правда, хотя создание некоторых объектов связано с поиском дополнительных метаданных на диске, которые могут быть необязательными. –

+0

ACL будет необходим для каждого объекта точно. Нет никакого способа обойти его. И как только вам придется искать их, вы можете также прочитать любую другую информацию в заголовках MFT для файлов в этой папке. –

19
private static void test() 
{ 
    System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); 
    sw.Start(); 

    string [] dirs = System.IO.Directory.GetDirectories("C:\\Test\\"); 
    string[] files = System.IO.Directory.GetFiles("C:\\Test\\"); 

    if (dirs.Length == 0 && files.Length == 0) 
     Console.WriteLine("Empty"); 
    else 
     Console.WriteLine("Not Empty"); 

    sw.Stop(); 
    Console.WriteLine(sw.ElapsedMilliseconds); 
} 

Это быстрый тест вернулся в 2 мс для папки в порожнем и, когда она содержит вложенные папки & файлы (5 папок с 5 файлов в каждой)

+2

Вы можете улучшить это возвращаясь, если «dirs» сразу не пусто, без необходимости получать список файлов. – samjudson

+2

Да, но что, если есть пески файлов в нем? –

+2

Вы также измеряете время записи на консоль, что не является незначительным. – ctusch

3

Я не знаю способа, который будет сжато сказать вам, если данная папка содержит другие папки или файлы, однако, используя:

Directory.GetFiles(path); 
& 
Directory.GetDirectories(path); 

должно помочь производительности, так как эти методы будут возвращать только массив строк с именами файлов/каталогов, а не целые FileSystemIn fo объектов.

2

Спасибо, всем, за ответы. Я пытался использовать Directory.GetFiles() и Directory.GetDirectories() методов. Хорошие новости! Производительность улучшилась ~ дважды! 229 звонков в 221 мс. Но также я надеюсь, что можно избежать перечисления всех элементов в папке. Согласитесь, что все еще выполняется ненужная работа. Разве вы так не думаете?

После всех исследований я пришел к выводу, что при чистом .NET дальнейшая оптимизация невозможна. Я собираюсь сыграть с функцией WinAPI FindFirstFile. Надеюсь, это поможет.

+1

Из интереса, каковы причины, по которым вам нужна такая высокая производительность для этой операции? – meandmycode

+1

Вместо того, чтобы отвечать на свой вопрос, отметьте один из правильных ответов как ответ (возможно, первый опубликовал или самый чистый). Таким образом, будущие пользователи stackoverflow будут видеть лучший ответ прямо под ваш вопрос! –

-1

Мой код это удивительно просто взял 00: 00: 00,0007143 менее milisecond с 34 файла в папке

System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); 
    sw.Start(); 

    bool IsEmptyDirectory = (Directory.GetFiles("d:\\pdf").Length == 0); 

    sw.Stop(); 
    Console.WriteLine(sw.Elapsed); 
+0

Собственно, если вы умножаете его на 229 и добавляете GetDirectories(), вы получите тот же результат, что и мой :) – zhe

8

Если вы не возражаете, оставляя чистый C# и собирается для WinApi звонков, то вы можете рассмотреть функцию PathIsDirectoryEmpty(). Согласно MSDN, функция:

Возвращает TRUE, если pszPath - пустой каталог. Возвращает FALSE, если pszPath не является каталогом или содержит хотя бы один файл, отличный от "." или "..".

Это похоже на функцию, которая делает именно то, что вы хотите, поэтому она, вероятно, хорошо оптимизирована для этой задачи (хотя я ее не тестировал).

Чтобы позвонить ему с C#, pinvoke.net сайт должен вам помочь. (К сожалению, он еще не описывает эту определенную функцию, но вы должны иметь возможность находить некоторые функции с похожими аргументами и возвращать их там и использовать их в качестве основы для своего вызова. Если вы снова посмотрите в MSDN, в нем говорится, что DLL для импорта из: shlwapi.dll)

+0

Отличная идея. Я не знал об этой функции. Я попытаюсь сравнить его производительность с моим подходом, который я описал выше. Если это будет сделано быстрее, я буду использовать его в своем коде. Спасибо. – zhe

+3

Примечание для тех, кто хочет пройти этот маршрут. Похоже, что этот метод PathIsDirectoryEmpty() из shlwapi.dll отлично работает на машинах Vista32/64 и XP32/64, но на некоторых компьютерах Win7 запускается бомба. Это должно быть связано с версиями shlwapi.dll, поставляемыми с различными версиями Windows. Осторожно. –

216

В разделе «Справочник и DirectoryInfo» в .NET 4 появилась новая функция, которая позволяет возвращать IEnumerable вместо массива и начинает возвращать результаты перед чтением всего содержимого каталога.

See here и there

public bool IsDirectoryEmpty(string path) 
{ 
    IEnumerable<string> items = Directory.EnumerateFileSystemEntries(path); 
    using (IEnumerator<string> en = items.GetEnumerator()) 
    { 
     return !en.MoveNext(); 
    } 
} 

EDIT: видя, что ответ снова, я понимаю, этот код может быть гораздо проще ...

public bool IsDirectoryEmpty(string path) 
{ 
    return !Directory.EnumerateFileSystemEntries(path).Any(); 
} 
+5

сладкий! любите одну строку кода решения. upvoted. – pearcewg

+0

Мне нравится это решение, можно ли его проверить только для определенных типов файлов? .Contains («jpg») вместо .any(), похоже, не работает – Dennis

+2

@Dennis, вы можете указать шаблон подстановки в вызове 'EnumerateFileSystemEntries' или использовать' .Any (condition) '(указать условие как лямбда-выражение или как метод, который принимает путь в качестве параметра). –

0

Вы должны также обернуть тест в блок try/catch, чтобы убедиться, что вы правильно обрабатываете DirectoryNotFoundException. Это классическое условие гонки, если папка удаляется сразу после того, как вы проверили, существует ли она.

4

Я уверен, что другие ответы более быстрые, и ваш вопрос спросил, есть ли папка с файлами или папками ... но я думаю, что большую часть времени люди рассматривали бы каталог пустым, если он не содержит файлы. т.е. он по-прежнему «пуст» для меня, если он содержит пустые подкаталоги ... это может не соответствовать вашему использованию, но может быть для других!

public bool DirectoryIsEmpty(string path) 
    { 
    int fileCount = Directory.GetFiles(path).Length; 
    if (fileCount > 0) 
    { 
     return false; 
    } 

    string[] dirs = Directory.GetDirectories(path); 
    foreach (string dir in dirs) 
    { 
     if (! DirectoryIsEmpty(dir)) 
     { 
     return false; 
     } 
    } 

    return true; 
    } 
-2

Используйте это. Это просто.

Public Function IsDirectoryEmpty(ByVal strDirectoryPath As String) As Boolean 
     Dim s() As String = _ 
      Directory.GetFiles(strDirectoryPath) 
     If s.Length = 0 Then 
      Return True 
     Else 
      Return False 
     End If 
    End Function 
+1

Простой, возможно, но неправильный. Он имеет две основные ошибки: он не обнаруживает, * папки * находятся в пути, только файлы, и он будет генерировать исключение на пути, который не существует. Вероятно, он также будет * медленнее *, чем OP, потому что я уверен, что он получает все записи и фильтрует их. –

2

Некоторое время вы можете проверить, существуют ли какие-либо файлы внутри подкаталогов и игнорировать эти пустые вспомогательные каталоги; в этом случае вы можете использоваться метод ниже:

public bool isDirectoryContainFiles(string path) { 
    if (!Directory.Exists(path)) return false; 
    return Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories).Any(); 
} 
6

Я использую это для папок и файлов (не знаю, если это оптимальный)

if(Directory.GetFileSystemEntries(path).Length == 0) 
0

Вот то, что может помочь вам сделать это. Мне удалось сделать это за две итерации.

private static IEnumerable<string> GetAllNonEmptyDirectories(string path) 
    { 
    var directories = 
     Directory.EnumerateDirectories(path, "*.*", SearchOption.AllDirectories) 
     .ToList(); 

    var directoryList = 
    (from directory in directories 
    let isEmpty = Directory.GetFiles(directory, "*.*", SearchOption.AllDirectories).Length == 0 
    where !isEmpty select directory) 
    .ToList(); 

    return directoryList.ToList(); 
    } 
0

Легко и просто:

int num = Directory.GetFiles(pathName).Length; 

if (num == 0) 
{ 
    //empty 
} 
Смежные вопросы