Como verificar rapidamente se a pasta está vazia (.NET)?

140

Eu tenho que verificar, se o diretório no disco está vazio. Isso significa que ele não contém nenhuma pasta / arquivo. Eu sei que existe um método simples. Obtemos uma matriz de FileSystemInfo e verificamos se a contagem de elementos é igual a zero. Algo parecido:

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

Essa abordagem parece bem. MAS!! É muito, muito ruim do ponto de vista do desempenho. GetFileSystemInfos () é um método muito difícil. Na verdade, ele enumera todos os objetos do sistema de arquivos da pasta, obtém todas as suas propriedades, cria objetos, preenche a matriz digitada, etc. E tudo isso apenas para verificar Comprimento. Isso é estúpido, não é?

Acabei de criar um perfil desse código e determinei que ~ 250 chamadas desse método são executadas em ~ 500ms. Isso é muito lento e acredito que é possível fazê-lo muito mais rápido.

Alguma sugestão?

zhe
fonte
7
Por curiosidade, por que você gostaria de verificar o diretório 250 vezes?
ya23 16/04
2
@ ya23 Suponho que alguém gostaria de verificar 250 diretórios diferentes. Nem um único 250 vezes.
Mathieu Pagé

Respostas:

282

Há um novo recurso no Directorye DirectoryInfono .NET 4, que lhes permite retornar uma IEnumerablevez de uma matriz, e começar a retornar resultados antes de ler todo o conteúdo do diretório.

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

EDIT: vendo essa resposta novamente, percebo que esse código pode ser muito mais simples ...

public bool IsDirectoryEmpty(string path)
{
    return !Directory.EnumerateFileSystemEntries(path).Any();
}
Thomas Levesque
fonte
Eu gosto dessa solução, ela pode ser feita para verificar apenas determinados tipos de arquivos? .Contains ( "jpg") em vez de .any () não parecem funcionar
Dennis
5
@Dennis, você pode especificar um padrão curinga na chamada EnumerateFileSystemEntriesou usar .Any(condition)(especificar a condição como uma expressão lambda ou como um método que usa um caminho como parâmetro).
Thomas Levesque
O typecast pode ser removido do primeiro exemplo de código:return !items.GetEnumerator().MoveNext();
gary
1
@gary, se você fizer isso, o enumerador não será descartado; portanto, ele bloqueará o diretório até que o enumerador seja coletado como lixo.
Thomas Levesque
Isso parece funcionar bem para diretórios que contêm arquivos, mas se o diretório contém outros diretores, ele volta dizendo que está vazio.
Kairan
32

Aqui está a solução extra rápida que eu finalmente implementei. Aqui estou usando o WinAPI e as funções FindFirstFile , FindNextFile . Permite evitar a enumeração de todos os itens da pasta e para logo após a detecção do primeiro objeto na pasta . Essa abordagem é ~ 6 (!!) vezes mais rápida do que a descrita acima. 250 chamadas em 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();
}

Espero que seja útil para alguém no futuro.

zhe
fonte
Obrigado por compartilhar sua solução.
Greg
3
Você precisa adicionar SetLastError = trueao DllImportpara FindFirstFilepara que a Marshal.GetHRForLastWin32Error()chamada funcione corretamente, conforme descrito na seção Comentários do documento do MSDN para GetHRForLastWin32Error () .
Joel V. Earnest-DeYoung
Acho que a resposta a seguir é um pouco melhor, pois também procura os arquivos nos subdiretórios stackoverflow.com/questions/724148/…
Mayank
21

Você pode tentar Directory.Exists(path)e Directory.GetFiles(path)- provavelmente menos sobrecarga (sem objetos - apenas strings etc.).

Marc Gravell
fonte
Como sempre, você é o mais rápido possível! Bata-me por alguns segundos lá! :-)
Cerebrus 16/04/2009
Você estava tanto mais rápido do que eu ... porra minha atenção aos detalhes ;-)
Eoin Campbell
2
Não me fez nenhum bem; primeira resposta, e o único sem um voto ;-(
Marc Gravell
Não corrigido ... alguém tem um machado para moer, parece
Marc Gravell
1
Eu não acho que GetFiles terá uma lista de diretórios, por isso parece ser uma boa idéia para colocar em um cheque de GetDirectories bem
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);
}

Esse teste rápido voltou em 2 milissegundos para a pasta quando vazia e ao conter subpastas e arquivos (5 pastas com 5 arquivos em cada)

Eoin Campbell
fonte
3
Você pode melhorar isso retornando se 'dirs' não estiver vazio imediatamente, sem precisar obter a lista de arquivos.
samjudson
3
Sim, mas e se houver milhares de arquivos nele?
Thomas Levesque
3
Você também está medindo o tempo para gravar no console, o que não é desprezível.
21413 ctusch
11

Eu uso isso para pastas e arquivos (não sei se é o ideal)

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

Se você não se importa em deixar o C # puro e fazer chamadas WinApi , considere a função PathIsDirectoryEmpty () . De acordo com o MSDN, a função:

Retorna TRUE se pszPath for um diretório vazio. Retorna FALSE se pszPath não for um diretório ou se contiver pelo menos um arquivo que não seja "." ou "..".

Essa parece ser uma função que faz exatamente o que você deseja, portanto provavelmente está bem otimizada para essa tarefa (embora eu não a tenha testado).

Para chamá-lo de C #, o site pinvoke.net deve ajudá-lo. (Infelizmente, ainda não descreve essa função, mas você deve encontrar algumas funções com argumentos semelhantes e retornar o tipo lá e usá-las como base para sua chamada. Se você olhar novamente para o MSDN, ele indicará que a DLL da qual importar é shlwapi.dll)

akavel
fonte
Boa ideia. Eu não sabia sobre essa função. Vou tentar comparar seu desempenho com a minha abordagem, que descrevi acima. Se for mais rápido, vou reutilizá-lo no meu código. Obrigado.
zhe
4
Uma nota para quem quer seguir esse caminho. Parece que esse método PathIsDirectoryEmpty () do shlwapi.dll funciona bem em máquinas Vista32 / 64 e XP32 / 64, mas funciona bem em algumas máquinas Win7. Deve ter algo a ver com versões do shlwapi.dll fornecidas com diferentes versões do Windows. Cuidado.
112810 Alex_P
7

Não conheço as estatísticas de desempenho desta, mas você já tentou usar o Directory.GetFiles()método estático?

Ele retorna uma matriz de cadeias contendo nomes de arquivos (não FileInfos) e você pode verificar o comprimento da matriz da mesma maneira que acima.

Cerebrus
fonte
mesmo problema, ele pode ser lenta se houver muitos arquivos ... mas é provavelmente mais rápido que GetFileSystemInfos
Thomas Levesque
4

Tenho certeza de que as outras respostas são mais rápidas e sua pergunta perguntou se uma pasta continha ou não arquivos ou pastas ... mas acho que na maioria das vezes as pessoas consideram um diretório vazio se ele não contiver arquivos. ou seja, ainda está "vazio" para mim se contiver subdiretórios vazios ... isso pode não ser adequado ao seu uso, mas pode ser para outros!

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

De qualquer forma, você terá que usar o disco rígido para obter essas informações, e isso sozinho superará qualquer criação de objeto e preenchimento de matriz.

Don Reba
fonte
1
É verdade que, embora a criação de alguns objetos envolva a pesquisa de metadados extras no disco que podem não ser necessários.
Adam Rosenfield
A ACL seria necessária para todos os objetos, com certeza. Não há maneira de contornar isso. E depois que você precisar procurá-las, também poderá ler qualquer outra informação nos cabeçalhos da MFT para os arquivos na pasta.
Don Reba
3

Não estou ciente de um método que informará sucintamente se uma determinada pasta contém outras pastas ou arquivos, no entanto, usando:

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

deve ajudar o desempenho, pois esses dois métodos retornarão apenas uma matriz de cadeias com os nomes dos arquivos / diretórios, em vez de objetos inteiros FileSystemInfo.

CraigTP
fonte
2

Obrigado a todos, pelas respostas. Tentei usar os métodos Directory.GetFiles () e Directory.GetDirectories () . Boas notícias! O desempenho melhorou ~ duas vezes! 229 chamadas em 221ms. Mas também espero que seja possível evitar a enumeração de todos os itens da pasta. Concorde que ainda o trabalho desnecessário está sendo executado. Você não acha?

Depois de todas as investigações, cheguei a uma conclusão de que, sob o .NET puro, é impossível uma otimização adicional. Vou jogar com a função FindFirstFile do WinAPI . Espero que ajude.

zhe
fonte
1
Fora de interesse, quais são os motivos pelos quais você precisa de alto desempenho para esta operação?
meandmycode
1
Em vez de responder sua própria pergunta, marque uma das respostas corretas como a resposta (provavelmente a primeira postada ou a mais clara). Dessa forma, os futuros usuários do stackoverflow verão a melhor resposta logo abaixo da sua pergunta!
21411 Ray Hayes
2

Algum tempo, convém verificar se existem arquivos dentro dos subdiretórios e ignorar esses subdiretórios vazios; Nesse caso, você pode usar o método abaixo:

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

Fácil e simples:

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

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

Baseado no 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
fonte
-1

Meu código é incrível, levou apenas 00: 00: 00.0007143 menos de milissegundo com 34 arquivos na pasta

   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
fonte
Na verdade, se você multiplicar por 229 e adicionar GetDirectories (), você vai obter o mesmo resultado, como o meu :)
zhe
-1

Aqui está algo que pode ajudá-lo a fazer isso. Consegui fazer isso em duas iterações.

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

Como você trabalha com um objeto DirectoryInfo de qualquer maneira, eu usaria uma extensão

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

Usa isto. É simples.

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
fonte
2
Simples, talvez. Mas incorreto. Ele possui dois bugs principais: ele não detecta se há pastas no caminho, apenas arquivos, e lançará uma exceção em um caminho que não existe. Também é provável que seja realmente mais lento que o original do OP, porque tenho quase certeza de que ele obtém todas as entradas e as filtra.
Andrew Barber