Jak szybko sprawdzić, czy folder jest pusty (.NET)?

140

Muszę sprawdzić, czy katalog na dysku jest pusty. Oznacza to, że nie zawiera żadnych folderów / plików. Wiem, że jest prosta metoda. Otrzymujemy tablicę FileSystemInfo i sprawdzamy, czy liczba elementów jest równa zero. Coś w tym stylu:

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

To podejście wydaje się OK. ALE!! Z punktu widzenia wydajności jest to bardzo, bardzo złe. GetFileSystemInfos () jest bardzo trudną metodą. Właściwie to wylicza wszystkie obiekty systemu plików folderu, pobiera wszystkie ich właściwości, tworzy obiekty, wypełnia wpisaną tablicę itp. A wszystko po to, by po prostu sprawdzić Długość. To głupie, prawda?

Właśnie sprofilowałem taki kod i ustaliłem, że ~ 250 wywołań takiej metody jest wykonywanych w ~ 500 ms. Jest to bardzo powolne i uważam, że można to zrobić znacznie szybciej.

Jakieś sugestie?

zhe
źródło
7
Z ciekawości dlaczego chcesz sprawdzać katalog 250 razy?
ya23
2
@ ya23 Przypuszczam, że ktoś chciałby sprawdzić 250 różnych katalogów. Ani jednego 250 razy.
Mathieu Pagé

Odpowiedzi:

282

Jest to nowa funkcja Directoryi DirectoryInfo.NET 4, który pozwala im zwracać IEnumerablezamiast tablicy, i zacząć zwracania wyników przed przeczytaniem całej zawartości katalogu.

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

EDYCJA: widząc tę ​​odpowiedź ponownie, zdaję sobie sprawę, że ten kod można uczynić znacznie prostszym ...

public bool IsDirectoryEmpty(string path)
{
    return !Directory.EnumerateFileSystemEntries(path).Any();
}
Thomas Levesque
źródło
Podoba mi się to rozwiązanie, czy można sprawdzić tylko określone typy plików? .Zawiera („jpg”) zamiast .any () nie wydaje się działać
Dennis
5
@Dennis, możesz określić wzorzec wieloznaczny w wywołaniu EnumerateFileSystemEntrieslub użyć .Any(condition)(określić warunek jako wyrażenie lambda lub jako metodę, która przyjmuje ścieżkę jako parametr).
Thomas Levesque
Rzutowanie można usunąć z pierwszego przykładu kodu:return !items.GetEnumerator().MoveNext();
gary
1
@gary, jeśli to zrobisz, moduł wyliczający nie zostanie usunięty, więc zablokuje katalog, dopóki moduł wyliczający nie zostanie usunięty.
Thomas Levesque,
Wydaje się, że działa to dobrze w przypadku katalogów zawierających pliki, ale jeśli katalog zawiera innych dyrektorów, wraca z informacją, że jest pusty.
Kairan
32

Oto bardzo szybkie rozwiązanie, które w końcu wdrożyłem. Tutaj używam WinAPI i funkcji FindFirstFile , FindNextFile . Pozwala to uniknąć wyliczania wszystkich elementów w folderze i zatrzymuje się zaraz po wykryciu pierwszego obiektu w folderze . To podejście jest ~ 6 (!!) razy szybsze niż opisano powyżej. 250 połączeń w 36 ms!

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

Mam nadzieję, że w przyszłości komuś się przyda.

zhe
źródło
Dziękujemy za udostępnienie rozwiązania.
Greg,
3
Musisz dodać SetLastError = truedo DllImportfor FindFirstFile, aby Marshal.GetHRForLastWin32Error()wywołanie działało poprawnie, zgodnie z opisem w sekcji uwagi w dokumencie MSDN dla GetHRForLastWin32Error () .
Joel V. Earnest-DeYoung
Myślę, że następująca odpowiedź jest trochę lepsza, ponieważ szuka również plików w podkatalogach stackoverflow.com/questions/724148/ ...
Mayank
21

Można spróbować Directory.Exists(path)i Directory.GetFiles(path)- prawdopodobnie mniej napowietrznych (żadne przedmioty - tylko struny itp).

Marc Gravell
źródło
Jak zawsze jesteś najszybszy ze spustu! Pokonaj mnie o kilka sekund! :-)
Cerebrus
Oboje byliście szybsi ode mnie ... cholera moja dbałość o szczegóły ;-)
Eoin Campbell
2
Nie pomogło mi to jednak; pierwsza odpowiedź i jedyna bez głosu ;-(
Marc Gravell
Unfixed ... ktoś ma siekierę do szlifowania, wydaje mi się
Marc Gravell
1
Nie sądzę, że GetFiles otrzyma listę katalogów, więc dobrym pomysłem wydaje się sprawdzenie również
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);
}

Ten szybki test powrócił w ciągu 2 milisekund dla folderu, gdy jest pusty i zawiera podfoldery i pliki (5 folderów po 5 plików w każdym)

Eoin Campbell
źródło
3
Możesz to poprawić, zwracając natychmiast, jeśli katalog „dirs” nie jest pusty, bez konieczności pobierania listy plików.
samjudson
3
Tak, ale co, jeśli są w nim tysiące plików?
Thomas Levesque
3
Mierzysz także czas pisania na konsolę, co nie jest bez znaczenia.
ctusch
11

Używam tego do folderów i plików (nie wiem, czy to optymalne)

    if(Directory.GetFileSystemEntries(path).Length == 0)
Jmu
źródło
8

Jeśli nie masz nic przeciwko pozostawieniu czystego C # i przejściu do wywołań WinApi , możesz rozważyć funkcję PathIsDirectoryEmpty () . Według MSDN funkcja:

Zwraca wartość TRUE, jeśli pszPath jest pustym katalogiem. Zwraca FALSE, jeśli pszPath nie jest katalogiem lub jeśli zawiera przynajmniej jeden plik inny niż „.” lub „..”.

Wygląda na to, że jest to funkcja, która robi dokładnie to, co chcesz, więc prawdopodobnie jest dobrze zoptymalizowana do tego zadania (chociaż nie testowałem tego).

Aby wywołać to z C #, witryna pinvoke.net powinna ci pomóc. (Niestety, nie opisuje jeszcze tej konkretnej funkcji, ale powinieneś być w stanie znaleźć tam niektóre funkcje z podobnymi argumentami i typem zwracania i użyć ich jako podstawy do wywołania. Jeśli spojrzysz ponownie na MSDN, zobaczysz, że biblioteka DLL do zaimportowania to shlwapi.dll)

akavel
źródło
Świetny pomysł. Nie wiedziałem o tej funkcji. Spróbuję porównać jego osiągi z moim podejściem, które opisałem powyżej. Jeśli będzie działać szybciej, wykorzystam go ponownie w moim kodzie. Dzięki.
zhe
4
Uwaga dla tych, którzy chcą iść tą trasą. Wygląda na to, że ta metoda PathIsDirectoryEmpty () z shlwapi.dll działa dobrze na maszynach Vista32 / 64 i XP32 / 64, ale bombarduje niektóre maszyny Win7. Musi to mieć coś wspólnego z wersjami shlwapi.dll dostarczanymi z różnymi wersjami systemu Windows. Strzec się.
Alex_P
7

Nie wiem o statystykach wydajności tego, ale czy próbowałeś użyć Directory.GetFiles()metody statycznej?

Zwraca tablicę ciągów zawierającą nazwy plików (nie FileInfos) i możesz sprawdzić długość tablicy w taki sam sposób, jak powyżej.

Cerebrus
źródło
ten sam problem, może działać wolno, jeśli jest wiele plików ... ale prawdopodobnie jest szybszy niż GetFileSystemInfos
Thomas Levesque
4

Jestem pewien, że inne odpowiedzi są szybsze, a twoje pytanie dotyczyło tego, czy folder zawiera pliki lub foldery ... ale myślę, że przez większość czasu ludzie uznaliby katalog za pusty, gdyby nie zawierał plików. tj. nadal jest dla mnie „pusty”, jeśli zawiera puste podkatalogi ... może to nie pasować do twojego użytku, ale może do innych!

  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;
  }
Brad Parks
źródło
Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories).Any()
Jonathan Gilbert
3

W każdym przypadku będziesz musiał udać się na dysk twardy, aby uzyskać te informacje, a samo to ma pierwszeństwo przed tworzeniem obiektów i wypełnianiem tablic.

Don Reba
źródło
1
To prawda, chociaż tworzenie niektórych obiektów wymaga wyszukania dodatkowych metadanych na dysku, które mogą nie być konieczne.
Adam Rosenfield,
Lista ACL byłaby z pewnością wymagana dla każdego obiektu. Nie da się tego obejść. A kiedy już będziesz musiał je odszukać, równie dobrze możesz przeczytać wszelkie inne informacje w nagłówkach MFT dotyczące plików w folderze.
Don Reba
3

Nie znam metody, która zwięźle powie Ci, czy dany folder zawiera inne foldery lub pliki, jednak za pomocą:

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

powinno zwiększyć wydajność, ponieważ obie te metody zwracają tylko tablicę ciągów z nazwami plików / katalogów, a nie całe obiekty FileSystemInfo.

CraigTP
źródło
2

Dziękuję wszystkim za odpowiedzi. Próbowałem użyć Directory.GetFiles () i Directory.GetDirectories () metod . Dobre wieści! Wydajność poprawiła się ~ dwukrotnie! 229 połączeń w 221 ms. Ale mam też nadzieję, że da się uniknąć wyliczania wszystkich pozycji w folderze. Zgadzam się, że nadal niepotrzebna praca jest wykonywana. Nie sądzisz?

Po wszystkich badaniach doszedłem do wniosku, że w czystym .NET dalsza optymalizacja jest niemożliwa. Mam zamiar bawić się funkcją WinAPI FindFirstFile . Mam nadzieję, że to pomoże.

zhe
źródło
1
Z jakich powodów potrzebujesz tak wysokiej wydajności do tej operacji?
meandmycode
1
Zamiast odpowiadać na własne pytanie, zaznacz jedną z poprawnych odpowiedzi jako odpowiedź (prawdopodobnie pierwszą opublikowaną lub najjaśniejszą). W ten sposób przyszli użytkownicy stackoverflow zobaczą najlepszą odpowiedź bezpośrednio pod Twoim pytaniem!
Ray Hayes,
2

Czasami możesz chcieć sprawdzić, czy jakieś pliki istnieją w podkatalogach i zignorować te puste podkatalogi; w takim przypadku możesz skorzystać z poniższej metody:

public bool isDirectoryContainFiles(string path) {
    if (!Directory.Exists(path)) return false;
    return Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories).Any();
}
Leng Weh Seng
źródło
2

Łatwe i proste:

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

if (num == 0)
{
   //empty
}
Matheus Miranda
źródło
0

Z siedzibą w kodzie Brad Parks :

    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;
    }
Ángel Ibáñez
źródło
-1

Mój kod jest niesamowity, po prostu zajęło 00: 00: 00.0007143 mniej niż milisekundę z 34 plikami w folderze

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

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

     sw.Stop();
     Console.WriteLine(sw.Elapsed);
Prashant Cholachagudda
źródło
Właściwie, jeśli pomnożysz to przez 229 i dodasz GetDirectories (), otrzymasz ten sam wynik, co mój :)
zhe
-1

Oto coś, co może ci w tym pomóc. Udało mi się to zrobić w dwóch iteracjach.

 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();
   }
Gabriel Marius Popescu
źródło
-1

Ponieważ i tak zamierzasz pracować z obiektem DirectoryInfo, wybrałbym rozszerzenie

public static bool IsEmpty(this DirectoryInfo directoryInfo)
{
    return directoryInfo.GetFileSystemInfos().Count() == 0;
}
The_Black_Smurf
źródło
-2

Użyj tego. To proste.

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
Puffgroovy
źródło
2
Może proste. Ale niepoprawne. Ma dwa główne błędy: nie wykrywa, czy w ścieżce znajdują się jakiekolwiek foldery , tylko pliki, i zgłasza wyjątek w ścieżce, która nie istnieje. Prawdopodobnie będzie również wolniejszy niż oryginał OP, ponieważ jestem prawie pewien, że pobiera wszystkie wpisy i filtruje je.
Andrew Barber,