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

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

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

Ответ 1

Вот экстренное быстрое решение, которое я наконец реализовал. Здесь я использую 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();
}

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

Ответ 2

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

Ответ 3

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 файлами в каждой)

Ответ 4

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

Ответ 5

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

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

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

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

Ответ 6

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

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

Ответ 7

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

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

Ответ 8

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

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

Ответ 9

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

Ответ 10

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

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

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

Ответ 11

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

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

Ответ 12

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

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

Ответ 13

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

Ответ 14

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

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

Ответ 15

Мой код потрясающий, он просто взял 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);

Ответ 16

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

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