Comment vérifier rapidement si le dossier est vide (.NET)?

141

Je dois vérifier si le répertoire sur le disque est vide. Cela signifie qu'il ne contient aucun dossier / fichier. Je sais qu'il existe une méthode simple. Nous obtenons un tableau de FileSystemInfo et vérifions si le nombre d'éléments est égal à zéro. Quelque chose comme ca:

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

Cette approche semble correcte. MAIS!! C'est très, très mauvais du point de vue des performances. GetFileSystemInfos () est une méthode très difficile. En fait, il énumère tous les objets du système de fichiers du dossier, récupère toutes leurs propriétés, crée des objets, remplit un tableau typé etc. Et tout cela simplement pour vérifier la longueur. C'est stupide, non?

Je viens de profiler ce code et j'ai déterminé que ~ 250 appels d'une telle méthode sont exécutés en ~ 500 ms. C'est très lent et je pense qu'il est possible de le faire beaucoup plus rapidement.

Aucune suggestion?

zhe
la source
7
Par curiosité, pourquoi voudriez-vous consulter l'annuaire 250 fois?
ya23
2
@ ya23 Je suppose que l'on aimerait vérifier 250 répertoires différents. Pas une seule 250 fois.
Mathieu Pagé

Réponses:

283

Il existe une nouvelle fonctionnalité dans Directoryet DirectoryInfodans .NET 4 qui leur permet de renvoyer un IEnumerableau lieu d'un tableau et de commencer à renvoyer des résultats avant de lire tout le contenu du répertoire.

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

EDIT: en revoyant cette réponse, je me rends compte que ce code peut être rendu beaucoup plus simple ...

public bool IsDirectoryEmpty(string path)
{
    return !Directory.EnumerateFileSystemEntries(path).Any();
}
Thomas Levesque
la source
J'aime cette solution, peut-elle être faite pour vérifier uniquement certains types de fichiers? .Contains ("jpg") au lieu de .any () ne semble pas fonctionner
Dennis
5
@Dennis, vous pouvez spécifier un modèle générique dans l'appel à EnumerateFileSystemEntries, ou utiliser .Any(condition)(spécifier la condition en tant qu'expression lambda ou en tant que méthode qui prend un chemin comme paramètre).
Thomas Levesque
Le transtypage peut être supprimé du premier exemple de code:return !items.GetEnumerator().MoveNext();
gary
1
@gary, si vous faites cela, l'énumérateur ne sera pas supprimé, donc il verrouille le répertoire jusqu'à ce que l'énumérateur soit récupéré.
Thomas Levesque
Cela semble fonctionner correctement pour les répertoires contenant des fichiers, mais si le répertoire contient d'autres directeurs, il revient en disant qu'il est vide.
Kairan
32

Voici la solution ultra rapide, que j'ai finalement implémentée. Ici, j'utilise WinAPI et les fonctions FindFirstFile , FindNextFile . Il permet d'éviter l'énumération de tous les éléments dans le dossier et s'arrête juste après la détection du premier objet dans le dossier . Cette approche est ~ 6 (!!) fois plus rapide que celle décrite ci-dessus. 250 appels en 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();
}

J'espère que cela sera utile à quelqu'un à l'avenir.

zhe
la source
Merci d'avoir partagé votre solution.
Greg
3
Vous devez ajouter SetLastError = trueau DllImportpour FindFirstFilepour que l' Marshal.GetHRForLastWin32Error()appel fonctionne correctement, comme décrit dans la section Notes du document MSDN pour GetHRForLastWin32Error () .
Joel V.Earnest-DeYoung
Je pense que la réponse suivante n'est guère meilleure car elle recherche également les fichiers dans les sous-répertoires stackoverflow.com/questions/724148/...
Mayank
21

Vous pouvez essayer Directory.Exists(path)et Directory.GetFiles(path)- probablement moins de frais généraux (pas d'objets - juste des chaînes, etc.).

Marc Gravell
la source
Comme toujours, vous êtes le plus rapide sur la gâchette! Battez-moi de quelques secondes! :-)
Cerebrus
Vous avez été tous les deux plus rapides que moi ... putain mon attention aux détails ;-)
Eoin Campbell
2
Cela ne m'a fait aucun bien, cependant; première réponse, et la seule sans vote ;-(
Marc Gravell
Non fixé ... quelqu'un a une hache à broyer, pense
Marc Gravell
1
Je ne pense pas que GetFiles obtiendra une liste de répertoires, il semble donc être une bonne idée de vérifier également 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);
}

Ce test rapide est revenu en 2 millisecondes pour le dossier lorsqu'il était vide et lorsqu'il contenait des sous-dossiers et des fichiers (5 dossiers avec 5 fichiers chacun)

Eoin Campbell
la source
3
Vous pouvez améliorer cela en retournant si 'dirs' n'est pas vide tout de suite, sans avoir à obtenir la liste des fichiers.
samjudson
3
Oui, mais que faire s'il contient des milliers de fichiers?
Thomas Levesque
3
Vous mesurez également le temps d'écriture sur la console, ce qui n'est pas négligeable.
ctusch
11

J'utilise ceci pour les dossiers et les fichiers (je ne sais pas si c'est optimal)

    if(Directory.GetFileSystemEntries(path).Length == 0)
Jmu
la source
8

Si cela ne vous dérange pas de laisser C # pur et de passer des appels WinApi , vous pouvez envisager la fonction PathIsDirectoryEmpty () . Selon le MSDN, la fonction:

Renvoie TRUE si pszPath est un répertoire vide. Renvoie FALSE si pszPath n'est pas un répertoire ou s'il contient au moins un fichier autre que "." ou "..".

Cela semble être une fonction qui fait exactement ce que vous voulez, donc elle est probablement bien optimisée pour cette tâche (bien que je ne l'ai pas testé).

Pour l'appeler depuis C #, le site pinvoke.net devrait vous aider. (Malheureusement, il ne décrit pas encore cette fonction, mais vous devriez être en mesure de trouver des fonctions avec des arguments similaires et de renvoyer le type là-bas et de les utiliser comme base pour votre appel. Si vous regardez à nouveau dans le MSDN, il indique que la DLL à importer est shlwapi.dll)

Akavel
la source
Bonne idée. Je ne connaissais pas cette fonction. Je vais essayer de comparer ses performances avec mon approche, que j'ai décrite ci-dessus. Si ça marche plus vite, je le réutiliserai dans mon code. Merci.
zhe
4
Une note pour ceux qui veulent emprunter cette voie. Il semble que cette méthode PathIsDirectoryEmpty () de shlwapi.dll fonctionne bien sur les machines Vista32 / 64 et XP32 / 64, mais bombe sur certaines machines Win7. Il doit s'agir de versions de shlwapi.dll livrées avec différentes versions de Windows. Il faut se méfier.
Alex_P
7

Je ne connais pas les statistiques de performances de celui-ci, mais avez-vous essayé d'utiliser la Directory.GetFiles()méthode statique?

Il renvoie un tableau de chaînes contenant des noms de fichiers (pas FileInfos) et vous pouvez vérifier la longueur du tableau de la même manière que ci-dessus.

Cerebrus
la source
même problème, cela peut être lent s'il y a beaucoup de fichiers ... mais c'est probablement plus rapide que GetFileSystemInfos
Thomas Levesque
4

Je suis sûr que les autres réponses sont plus rapides, et votre question demandait si un dossier contenait des fichiers ou des dossiers ... mais je pense que la plupart du temps, les gens considéreraient un répertoire vide s'il ne contient pas de fichiers. c'est-à-dire qu'il est toujours "vide" pour moi s'il contient des sous-répertoires vides ... cela peut ne pas convenir à votre usage, mais peut-être pour d'autres!

  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
la source
Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories).Any()
Jonathan Gilbert
3

Dans tous les cas, vous devrez aller sur le disque dur pour obtenir ces informations, et cela seul l'emportera sur toute création d'objet et remplissage de tableau.

Don Reba
la source
1
C'est vrai, bien que la création de certains objets implique la recherche de métadonnées supplémentaires sur le disque qui peuvent ne pas être nécessaires.
Adam Rosenfield
L'ACL serait certainement nécessaire pour chaque objet. Il n'y a pas moyen de contourner cela. Et une fois que vous devez les rechercher, vous pouvez également lire toute autre information dans les en-têtes MFT pour les fichiers du dossier.
Don Reba
3

Je ne connais pas une méthode qui vous dira succinctement si un dossier donné contient d'autres dossiers ou fichiers, cependant, en utilisant:

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

devrait améliorer les performances car ces deux méthodes ne renverront qu'un tableau de chaînes avec les noms des fichiers / répertoires plutôt que des objets FileSystemInfo entiers.

CraigTP
la source
3

Facile et simple:

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

if (num == 0)
{
   //empty
}
Matheus Miranda
la source
2

Merci à tous pour les réponses. J'ai essayé d'utiliser Directory.GetFiles () et Directory.GetDirectories () . Bonnes nouvelles! Les performances se sont améliorées ~ deux fois! 229 appels en 221 ms. Mais j'espère aussi qu'il est possible d'éviter l'énumération de tous les éléments du dossier. D'accord, que le travail inutile est toujours en cours d'exécution. Tu ne penses pas?

Après toutes les enquêtes, je suis arrivé à une conclusion, que sous .NET pur, une optimisation supplémentaire est impossible. Je vais jouer avec la fonction FindFirstFile de WinAPI . J'espère que cela aidera.

zhe
la source
1
Par intérêt, quelles sont les raisons pour lesquelles vous avez besoin d'une telle performance pour cette opération?
Meandmycode
1
Plutôt que de répondre à votre propre question, marquez l'une des bonnes réponses comme réponse (probablement la première affichée ou la plus claire). De cette façon, les futurs utilisateurs de stackoverflow verront la meilleure réponse juste sous votre question!
Ray Hayes
2

Vous voudrez peut-être vérifier si des fichiers existent dans les sous-répertoires et ignorer ces sous-répertoires vides; dans ce cas, vous pouvez utiliser la méthode ci-dessous:

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

Basé dans le code 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
la source
-1

Mon code est incroyable, il a juste fallu 00: 00: 00.0007143 moins de milliseconde avec 34 fichiers dans le dossier

   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
la source
En fait, si vous le multipliez par 229 et ajoutez GetDirectories (), vous obtiendrez le même résultat que le mien :)
zhe
-1

Voici quelque chose qui pourrait vous aider à le faire. J'ai réussi à le faire en deux itérations.

 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
la source
-1

Puisque vous allez de toute façon travailler avec un objet DirectoryInfo, j'irais avec une extension

public static bool IsEmpty(this DirectoryInfo directoryInfo)
{
    return directoryInfo.GetFileSystemInfos().Count() == 0;
}
Le_Black_Smurf
la source
-2

Utilisez ceci. C'est simple.

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
la source
2
Simple, peut-être. Mais incorrect. Il a deux bogues majeurs: il ne détecte pas si des dossiers sont dans le chemin, uniquement des fichiers, et il lèvera une exception sur un chemin qui n'existe pas. Il est également susceptible d'être plus lent que l'original de l'OP, car je suis à peu près sûr qu'il obtient toutes les entrées et les filtre.
Andrew Barber