Как быстро проверить, пуста ли папка (.NET)?


144

Я должен проверить, пуст ли каталог на диске. Это означает, что он не содержит никаких папок / файлов. Я знаю, что есть простой способ. Получаем массив 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 () - очень сложный метод. Фактически, он перечисляет все объекты файловой системы папки, получает все их свойства, создает объекты, заполняет типизированный массив и т.д. И все это просто для проверки Length. Это же глупо, правда?

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

Какие-либо предложения?


7
Зачем из любопытства проверять каталог 250 раз?
ya23

2
@ ya23 Я полагаю, что нужно проверить 250 разных каталогов. Ни одного 250 раз.
Mathieu Pagé

Ответы:


290

Существует новая функция 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 (), похоже, не работает
Деннис

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

Приведение типов можно удалить из первого примера кода:return !items.GetEnumerator().MoveNext();
gary

1
@gary, если вы это сделаете, перечислитель не будет удален, поэтому он заблокирует каталог, пока перечислитель не будет собран сборщиком мусора.
Thomas Levesque

Кажется, это нормально работает для каталогов, содержащих файлы, но если каталог содержит других директоров, он возвращается, говоря, что он пуст.
Кайран

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в DllImportfor , как описано в разделе «Примечания» документа MSDN для GetHRForLastWin32Error () . FindFirstFileMarshal.GetHRForLastWin32Error()
Joel V. Earnest-DeYoung

Я думаю, что следующий ответ немного лучше, поскольку он также ищет файлы в подкаталогах stackoverflow.com/questions/724148/…
Mayank

22

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


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

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

2
Но это не пошло мне на пользу; первый ответ, и единственный без голосования ;-(
Марк Грейвелл

Незакрепленный ... у кого-то есть топор, который надо заткнуть, мне кажется
Марк Грейвелл

1
Я не думаю, что GetFiles получит список каталогов, так что неплохо было бы также проверить и GetDirectories
Кайран

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 05

3
Да, но что, если в нем тысячи файлов?
Thomas Levesque

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

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
Томас Левеск

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 дальнейшая оптимизация невозможна. Я собираюсь поиграть с функцией WinAPI FindFirstFile . Надеюсь, это поможет.


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 (), вы получите тот же результат, что и мой :)
zhe

-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.