Як швидко перевірити, чи папка порожня (.NET)?


140

Я повинен перевірити, чи каталог на диску порожній. Це означає, що він не містить папок / файлів. Я знаю, що існує простий метод. Ми отримуємо масив FileSystemInfo і перевіряємо, чи кількість елементів дорівнює нулю. Щось схоже:

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 мс. Це дуже повільно, і я вважаю, що це можна зробити набагато швидше.

Будь-які пропозиції?


7
З цікавості, чому ви хочете перевірити каталог 250 разів?
ya23

2
@ ya23 Я вважаю, що хотілося б перевірити 250 різних каталогів. Не один - 250 разів.
Патьє Матьє

Відповіді:


282

Існує нова функція в Directoryі DirectoryInfo.NET 4, яка дозволяє їм повернути IEnumerableзамість масиву і почати повертати результати перед читанням всього вмісту каталогу.

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();
}

Мені подобається це рішення, чи можна його змусити перевіряти лише певні типи файлів? .Contains ("jpg") замість .any (), здається, не працює
Dennis

5
@Dennis, ви можете вказати шаблон підстановки під час виклику EnumerateFileSystemEntriesабо використовувати .Any(condition)(вказати умову як лямбда-вираз або як метод, який приймає шлях як параметр).
Томас Левеск

Надрукований може бути видалений з першого прикладу коду:return !items.GetEnumerator().MoveNext();
ГЕРІ

1
@gary, якщо ви це зробите, то перелік не буде утилізований, тому він заблокує каталог до тих пір, поки перелік не буде зібраний сміттям.
Томас Левеск

Здається, це добре працює для каталогів, що містять файли, але якщо Каталог містить інші директори, він повертається, кажучи, що він порожній.
Кайран

32

Ось додаткове швидке рішення, яке я нарешті реалізував. Тут я використовую WinAPI та функції FindFirstFile , FindNextFile . Це дозволяє уникнути перерахування всіх елементів у папці та зупиняється відразу після виявлення першого об’єкта в папці . Цей підхід в ~ 6 (!!) разів швидший, ніж описано вище. 250 дзвінків за 36 мс!

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();
}

Сподіваюся, це буде корисно комусь у майбутньому.


Дякуємо, що поділилися своїм рішенням.
Грег

3
Вам потрібно додати SetLastError = trueдо DllImportдля FindFirstFileтого , для Marshal.GetHRForLastWin32Error()виклику на роботу правильно, як описано в розділі Зауваження док MSDN для GetHRForLastWin32Error () .
Joel V. Earnest-DeYoung

Я думаю , що таку відповідь трохи краще , так як він також шукає файли в піддиректорії stackoverflow.com/questions/724148 / ...
Mayank

21

Ви можете спробувати Directory.Exists(path)і Directory.GetFiles(path)- напевно, менше накладних витрат (немає об’єктів - лише рядки тощо).


Як завжди, ви швидше відключите курок! Побий мене на кілька секунд! :-)
Церебр

Ви обоє були швидшими за мене ... чорт мою увагу до деталей ;-)
Іойн Кемпбелл

2
Не принесла мені нічого доброго, хоча; перша відповідь, і єдина без голосу ;-(
Марк Гравелл

Нефіксований ... у когось є сокира, щоб молоти,
міркує

1
Я не думаю, що GetFiles отримає список каталогів, тому, здається, було б гарною ідеєю також поставити чек і на GetDirectories
Kairan

18
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 файлами у кожній)


3
Ви можете покращити це, повернувшись, якщо "dirs" просто не порожній, без необхідності отримувати список файлів.
samjudson

3
Так, але що робити, якщо в ньому є тисячі файлів?
Томас Левеск

3
Ви також вимірюєте час для запису на консоль, що не є незначним.
ctusch

11

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

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

8

Якщо ви не проти залишити чистий C # і перейти на виклики WinApi , можливо, ви захочете розглянути функцію PathIsDirectoryEmpty () . Відповідно до MSDN, функція:

Повертає TRUE, якщо pszPath - порожній каталог. Повертає FALSE, якщо pszPath не є каталогом, або якщо він містить принаймні один файл, відмінний від ". або "..".

Це здається функцією, яка робить саме те, що ви хочете, тому, ймовірно, це добре оптимізоване для цього завдання (хоча я цього не перевіряв).

Щоб зателефонувати з C #, веб- сайт pinvoke.net повинен допомогти вам. (На жаль, вона ще не описує цю певну функцію, але ви повинні мати можливість знайти деякі функції з подібними аргументами та повернути тип і використати їх як основу для свого дзвінка. Якщо ви знову заглянете в MSDN, це говорить про те, що DLL для імпорту з shlwapi.dll)


Чудова ідея. Я не знав про цю функцію. Я спробую порівняти його ефективність з моїм підходом, який я описав вище. Якщо це зробить швидше, я повторно його використаю у своєму коді. Дякую.
zhe

4
Примітка для бажаючих пройти цей маршрут. Схоже, що цей метод PathIsDirectoryEmpty () від shlwapi.dll прекрасно працює на машинах Vista32 / 64 та XP32 / 64, але бомби на деяких машинах Win7. Це має бути пов'язане з версіями shlwapi.dll, що постачаються з різними версіями Windows. Остерігайся.
Alex_P

7

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

Він повертає рядковий рядок, що містить імена файлів (не FileInfos), і ви можете перевірити довжину масиву так само, як вище.


це ж питання, воно може бути повільним, якщо файлів багато ... але, мабуть, швидше, що GetFileSystemInfos
Thomas Levesque

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;
  }

Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories).Any()
Джонатан Гілберт

3

У будь-якому випадку вам доведеться пройти жорсткий диск для цієї інформації, і тільки це допоможе створити будь-який об'єкт і заповнити масив.


1
Правда, хоча створення деяких об'єктів передбачає пошук зайвих метаданих на диску, що може не знадобитися.
Адам Розенфілд

ACL буде потрібен для кожного об'єкта точно. Не обійтися. І як тільки вам доведеться їх шукати, ви можете також прочитати будь-яку іншу інформацію в заголовках MFT для файлів у папці.
Дон Реба

3

Мені невідомий спосіб, який настільки коротко підкаже, чи містить ця папка якісь інші папки чи файли, проте, використовуючи:

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

має сприяти продуктивності, оскільки обидва ці способи повернуть лише масив рядків з іменами файлів / каталогів, а не цілими об'єктами FileSystemInfo.


2

Дякую всім за відповіді. Я спробував використовувати методи Directory.GetFiles () та Directory.GetDirectories () . Гарні новини! Продуктивність покращилася ~ вдвічі! 229 дзвінків за 221 мс. Але також сподіваюся, що можна уникнути перерахування всіх елементів у папці. Погодьтеся, що все-таки зайва робота виконується. Ви не думаєте так?

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


1
З-за інтересу, які причини вам потрібні настільки високі показники для цієї операції?
meandmycode

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

2

Деякий час ви можете перевірити, чи існують якісь файли всередині підкаталогів, і ігнорувати ці порожні підкаталоги; у цьому випадку ви можете використовувати метод нижче:

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


0

Базується в коді Бред Паркс :

    public static bool DirectoryIsEmpty(string path)
    {
        if (System.IO.Directory.GetFiles(path).Length > 0) return false;

        foreach (string dir in System.IO.Directory.GetDirectories(path))
            if (!DirectoryIsEmpty(dir)) return false;

        return true;
    }

-1

Мій код дивовижний, що він просто 00: 00: 00.0007143 займав менше мілісекунд із 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);

На насправді, якщо помножити його на 229 і додати GetDirectories (), ви отримаєте той же результат, як у мене :)
Чже

-1

Ось щось, що може допомогти вам це зробити. Мені вдалося це зробити в двох ітераціях.

 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();
   }

-1

Оскільки ви все одно будете працювати з об'єктом DirectoryInfo, я б пішов з розширенням

public static bool IsEmpty(this DirectoryInfo directoryInfo)
{
    return directoryInfo.GetFileSystemInfos().Count() == 0;
}

-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

2
Простий, мабуть. Але неправильно. У нього є дві основні помилки: він не визначає, чи є якісь папки на шляху, лише файли, і він викине виняток на шляху, який не існує. Це також, ймовірно, буде повільніше, ніж оригінал OP, тому що я впевнений, що він отримує всі записи та фільтрує їх.
Ендрю Барбер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.