¿Cómo verificar rápidamente si la carpeta está vacía (.NET)?

140

Tengo que verificar si el directorio en el disco está vacío. Significa que no contiene ninguna carpeta / archivo. Sé que hay un método simple. Obtenemos una matriz de FileSystemInfo y verificamos si el recuento de elementos es igual a cero. Algo como eso:

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

Este enfoque parece estar bien. ¡¡PERO!! Es muy, muy malo desde una perspectiva de rendimiento. GetFileSystemInfos () es un método muy difícil. En realidad, enumera todos los objetos del sistema de archivos de la carpeta, obtiene todas sus propiedades, crea objetos, llena la matriz escrita, etc. Y todo esto simplemente para verificar la longitud. Eso es estúpido, ¿no?

Acabo de perfilar dicho código y determiné que ~ 250 llamadas de dicho método se ejecutan en ~ 500ms. Esto es muy lento y creo que es posible hacerlo mucho más rápido.

¿Alguna sugerencia?

zhe
fuente
77
Por curiosidad, ¿por qué le gustaría revisar el directorio 250 veces?
ya23
2
@ ya23 Supongo que a uno le gustaría revisar 250 directorios diferentes. Ni una sola 250 veces.
Mathieu Pagé

Respuestas:

282

Hay una nueva característica en Directoryy DirectoryInfo.NET 4 que les permite devolver IEnumerableuna matriz en lugar de una matriz, y comenzar a devolver resultados antes de leer todo el contenido del directorio.

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

EDITAR: al ver esa respuesta nuevamente, me doy cuenta de que este código se puede hacer mucho más simple ...

public bool IsDirectoryEmpty(string path)
{
    return !Directory.EnumerateFileSystemEntries(path).Any();
}
Thomas Levesque
fuente
Me gusta esta solución, ¿se puede hacer que verifique solo ciertos tipos de archivos? .Contains ("jpg") en lugar de .any () no parecía funcionar
Dennis
55
@Dennis, puede especificar un patrón comodín en la llamada a EnumerateFileSystemEntries, o usar .Any(condition)(especifique la condición como una expresión lambda o como un método que toma una ruta como parámetro).
Thomas Levesque
El typecast se puede eliminar del primer ejemplo de código:return !items.GetEnumerator().MoveNext();
gary
1
@gary, si hace eso, el enumerador no se eliminará, por lo que bloqueará el directorio hasta que se recolecte basura.
Thomas Levesque
Esto parece funcionar bien para los directorios que contienen archivos, pero si el directorio contiene otros directores, vuelve diciendo que está vacío.
Kairan
32

Aquí está la solución extra rápida, que finalmente implementé. Aquí estoy usando WinAPI y las funciones FindFirstFile , FindNextFile . Permite evitar la enumeración de todos los elementos en la carpeta y se detiene justo después de detectar el primer objeto en la carpeta . Este enfoque es ~ 6 (!!) veces más rápido que el descrito anteriormente. ¡250 llamadas en 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();
}

Espero que sea útil para alguien en el futuro.

zhe
fuente
Gracias por compartir tu solución.
Greg
3
Debe agregar SetLastError = truea DllImportfor FindFirstFilepara que la Marshal.GetHRForLastWin32Error()llamada funcione correctamente, como se describe en la sección Comentarios del documento de MSDN para GetHRForLastWin32Error () .
Joel V. Earnest-DeYoung
Creo que la siguiente respuesta es un poco mejor, ya que también busca los archivos en los subdirectorios stackoverflow.com/questions/724148/…
Mayank
21

Podría intentar Directory.Exists(path)y Directory.GetFiles(path), probablemente menos gastos generales (sin objetos, solo cadenas, etc.).

Marc Gravell
fuente
Como siempre, ¡eres más rápido fuera del gatillo! ¡Golpéame por unos segundos allí! :-)
Cerebrus
Ambos fueron más rápidos que yo ... maldita sea mi atención al detalle ;-)
Eoin Campbell
2
Sin embargo, no me hizo ningún bien; primera respuesta, y la única sin voto ;-(
Marc Gravell
Sin arreglar ... alguien tiene un hacha para moler, creo
Marc Gravell
1
No creo que GetFiles obtenga una lista de Directorios, por lo que parece una buena idea poner un cheque para GetDirectories también
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);
}

Esta prueba rápida regresó en 2 milisegundos para la carpeta cuando estaba vacía y cuando contenía subcarpetas y archivos (5 carpetas con 5 archivos en cada una)

Eoin Campbell
fuente
3
Podría mejorar esto si regresa si 'dirs' no está vacío de inmediato, sin tener que obtener la lista de archivos.
samjudson 05 de
3
Sí, pero ¿y si hay miles de archivos en él?
Thomas Levesque
3
También está midiendo el tiempo para escribir en la consola, que no es insignificante.
ctusch
11

Lo uso para carpetas y archivos (no sé si es óptimo)

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

Si no le importa dejar C # puro e ir a las llamadas de WinApi , puede considerar la función PathIsDirectoryEmpty () . Según el MSDN, la función:

Devuelve VERDADERO si pszPath es un directorio vacío. Devuelve FALSE si pszPath no es un directorio o si contiene al menos un archivo que no sea "." o "..".

Esa parece ser una función que hace exactamente lo que quieres, por lo que probablemente esté bien optimizada para esa tarea (aunque no lo he probado).

Para llamarlo desde C #, el sitio pinvoke.net debería ayudarlo. (Desafortunadamente, todavía no describe esta determinada función, pero debería poder encontrar algunas funciones con argumentos similares y devolver el tipo allí y usarlas como base para su llamada. Si vuelve a buscar en el MSDN, dice que la DLL para importar es shlwapi.dll)

akavel
fuente
Gran idea. No sabía sobre esta función. Trataré de comparar su rendimiento con mi enfoque, que describí anteriormente. Si fuera más rápido, lo reutilizaré en mi código. Gracias.
zhe
44
Una nota para aquellos que quieran ir por esta ruta. Parece que este método PathIsDirectoryEmpty () de shlwapi.dll funciona bien en las máquinas Vista32 / 64 y XP32 / 64, pero bombardea en algunas máquinas Win7. Debe tener algo que ver con las versiones de shlwapi.dll que se envían con diferentes versiones de Windows. Tener cuidado.
Alex_P
7

No sé acerca de las estadísticas de rendimiento de este, pero ¿has intentado usar el Directory.GetFiles()método estático?

Devuelve una matriz de cadena que contiene nombres de archivo (no FileInfos) y puede verificar la longitud de la matriz de la misma manera que anteriormente.

Cerebro
fuente
mismo problema, puede ser lento si hay muchos archivos ... pero probablemente sea más rápido que GetFileSystemInfos
Thomas Levesque
4

Estoy seguro de que las otras respuestas son más rápidas, y su pregunta preguntaba si una carpeta contenía o no archivos o carpetas ... pero creo que la mayoría de las veces la gente consideraría un directorio vacío si no contiene archivos. es decir, todavía está "vacío" para mí si contiene subdirectorios vacíos ... ¡esto puede no ser adecuado para su uso, pero puede ser para otros!

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

Tendrá que ir al disco duro para obtener esta información en cualquier caso, y esto solo superará cualquier creación de objetos y relleno de matriz.

Don reba
fuente
1
Es cierto, aunque crear algunos de los objetos implica buscar metadatos adicionales en el disco que podrían no ser necesarios.
Adam Rosenfield
La ACL sería necesaria para cada objeto con seguridad. No hay manera de evitarlo. Y una vez que tenga que buscarlos, también podría leer cualquier otra información en los encabezados MFT para los archivos en la carpeta.
Don Reba
3

Sin embargo, no conozco un método que le diga sucintamente si una carpeta determinada contiene otras carpetas o archivos, sin embargo, usando:

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

debería ayudar al rendimiento ya que estos dos métodos solo devolverán una matriz de cadenas con los nombres de los archivos / directorios en lugar de los objetos FileSystemInfo completos.

CraigTP
fuente
2

Gracias a todos por las respuestas. Traté de usar Directory.GetFiles () y Directory.GetDirectories () métodos . ¡Buenas noticias! ¡El rendimiento mejoró ~ dos veces! 229 llamadas en 221ms. Pero también espero que sea posible evitar la enumeración de todos los elementos de la carpeta. De acuerdo, que todavía se está ejecutando el trabajo innecesario. ¿No lo crees así?

Después de todas las investigaciones, llegué a la conclusión de que, bajo puro .NET, una optimización adicional es imposible. Voy a jugar con la función FindFirstFile de WinAPI . Espero que ayude.

zhe
fuente
1
Por interés, ¿cuáles son las razones por las que necesita un rendimiento tan alto para esta operación?
meandmycode
1
En lugar de responder su propia pregunta, marque una de las respuestas correctas como la respuesta (probablemente la primera publicada o la más clara). ¡De esta forma, los futuros usuarios de stackoverflow verán la mejor respuesta justo debajo de su pregunta!
Ray Hayes
2

En algún momento es posible que desee verificar si existen archivos dentro de los subdirectorios e ignorar esos subdirectorios vacíos; en este caso puedes usar el método a continuación:

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

Fácil y simple

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

if (num == 0)
{
   //empty
}
Matheus Miranda
fuente
0

Basado en el código de 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
fuente
-1

Mi código es increíble, solo tomó 00: 00: 00.0007143 menos de milisegundos con 34 archivos en la carpeta

   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
fuente
En realidad, si lo multiplica por 229 y agrega GetDirectories (), obtendrá el mismo resultado que el mío :)
zhe
-1

Aquí hay algo que podría ayudarlo a hacerlo. Logré hacerlo en dos iteraciones.

 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
fuente
-1

Como de todos modos vas a trabajar con un objeto DirectoryInfo, iría con una extensión

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

Utilizar este. Es sencillo.

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
fuente
2
Simple, tal vez. Pero incorrecto Tiene dos errores principales: no detecta si hay carpetas en la ruta, solo archivos, y arrojará una excepción en una ruta que no existe. También es probable que sea más lento que el OP original, porque estoy bastante seguro de que obtiene todas las entradas y las filtra.
Andrew Barber