Практическое руководство. Перебор дерева папок (Руководство по программированию на C#)

Под фразой "итерация дерева каталога" подразумевается доступ к каждому файлу в каждом вложенном подкаталоге в заданной корневой папке на любую глубину. Необязательно открывать каждый файл. Можно просто извлечь имя файла или подкаталога как string или можно извлечь дополнительные сведения в форме объекта FileInfo или DirectoryInfo.

Примечание

В Windows термины "каталог" и "папка" являются взаимозаменяемыми.В большинстве документации и в текстах пользовательского интерфейса используется термин "папка", но в библиотеке классов .NET Framework используется термин "каталог".

В простейшем случае, когда точно известно, что имеются права доступа ко всем каталогам в указанном корне, можно использовать флаг System.IO.SearchOption.AllDirectories. Этот флаг возвращает все вложенные подкаталоги, соответствующие заданному шаблону. В следующем примере показано, как использовать этот флаг.

root.GetDirectories("*.*", System.IO.SearchOption.AllDirectories);

Слабая сторона в этом подходе заключается в том, что если какой-либо из подкаталогов в указанном корне вызовет DirectoryNotFoundException или UnauthorizedAccessException, то весь метод закончится неудачей и каталоги не будут возвращены. Это также относится к использованию метода GetFiles. Если необходимо обработать эти исключения в определенных подпапках, необходимо вручную пройти по дереву каталога, как показано в следующих примерах.

При проходе вручную по дереву каталога можно обработать сначала подкаталоги (обход в прямом порядке) или файлы (обход в обратном порядке). При выполнении обхода в прямом порядке проходится целое дерево под текущей папкой перед итерацией файлов, которые находятся в самой папке. В приведенных ниже примерах выполняется обход в обратном порядке, но его можно легко изменить на выполнение обхода в прямом порядке.

Еще одним параметром является использование рекурсии или обхода на основе стека. В приведенных ниже примерах показываются оба подхода.

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

Примечание

Файловые системы NTFS могут содержать точки повторной обработки в форме точек соединения, символических ссылок и жестких ссылок.Методы .NET Framework, например GetFiles и GetDirectories, не возвратят подкаталоги под точкой повторной обработки.Такое поведение предотвратит риск входа в бесконечный цикл, когда две точки повторной обработки ссылаются друг на друга.В общем, следует быть предельно осторожными при работе с точками повторной обработки, чтобы избежать случайного изменения или удаления файлов.При получении тщательного контроля над точками повторной обработки используйте вызов неуправляемого кода или машинный код для прямого вызова подходящих методов файловой системы Win32.

Пример

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

Определенные обрабатываемые исключения и определенные выполняемые в каждом файле или папке параметры предоставляются только в качестве примеров. Этот код следует изменить для соответствия конкретным требованиям. Дополнительные сведения см. в комментариях в коде.

public class RecursiveFileSearch
{
    static System.Collections.Specialized.StringCollection log = new System.Collections.Specialized.StringCollection();

    static void Main()
    {
        // Start with drives if you have to search the entire computer. 
        string[] drives = System.Environment.GetLogicalDrives();

        foreach (string dr in drives)
        {
            System.IO.DriveInfo di = new System.IO.DriveInfo(dr);

            // Here we skip the drive if it is not ready to be read. This 
            // is not necessarily the appropriate action in all scenarios. 
            if (!di.IsReady)
            {
                Console.WriteLine("The drive {0} could not be read", di.Name);
                continue;
            }
            System.IO.DirectoryInfo rootDir = di.RootDirectory;
            WalkDirectoryTree(rootDir);
        }

        // Write out all the files that could not be processed.
        Console.WriteLine("Files with restricted access:");
        foreach (string s in log)
        {
            Console.WriteLine(s);
        }
        // Keep the console window open in debug mode.
        Console.WriteLine("Press any key");
        Console.ReadKey();
    }

    static void WalkDirectoryTree(System.IO.DirectoryInfo root)
    {
        System.IO.FileInfo[] files = null;
        System.IO.DirectoryInfo[] subDirs = null;

        // First, process all the files directly under this folder 
        try
        {
            files = root.GetFiles("*.*");
        }
        // This is thrown if even one of the files requires permissions greater 
        // than the application provides. 
        catch (UnauthorizedAccessException e)
        {
            // This code just writes out the message and continues to recurse. 
            // You may decide to do something different here. For example, you 
            // can try to elevate your privileges and access the file again.
            log.Add(e.Message);
        }

        catch (System.IO.DirectoryNotFoundException e)
        {
            Console.WriteLine(e.Message);
        }

        if (files != null)
        {
            foreach (System.IO.FileInfo fi in files)
            {
                // In this example, we only access the existing FileInfo object. If we 
                // want to open, delete or modify the file, then 
                // a try-catch block is required here to handle the case 
                // where the file has been deleted since the call to TraverseTree().
                Console.WriteLine(fi.FullName);
            }

            // Now find all the subdirectories under this directory.
            subDirs = root.GetDirectories();

            foreach (System.IO.DirectoryInfo dirInfo in subDirs)
            {
                // Resursive call for each subdirectory.
                WalkDirectoryTree(dirInfo);
            }
        }            
    }
}

В следующем примере показана итерация файлов и папок в дереве каталога без использования рекурсии. Этот метод использует обычный тип коллекции Stack, который является стеком типа "последним пришел — первым вышел" (LIFO).

Определенные обрабатываемые исключения и определенные выполняемые в каждом файле или папке параметры предоставляются только в качестве примеров. Этот код следует изменить для соответствия конкретным требованиям. Дополнительные сведения см. в комментариях в коде.

public class StackBasedIteration
{
    static void Main(string[] args)
    {
        // Specify the starting folder on the command line, or in  
        // Visual Studio in the Project > Properties > Debug pane.
        TraverseTree(args[0]);

        Console.WriteLine("Press any key");
        Console.ReadKey();
    }

    public static void TraverseTree(string root)
    {
        // Data structure to hold names of subfolders to be 
        // examined for files.
        Stack<string> dirs = new Stack<string>(20);

        if (!System.IO.Directory.Exists(root))
        {
            throw new ArgumentException();
        }
        dirs.Push(root);

        while (dirs.Count > 0)
        {
            string currentDir = dirs.Pop();
            string[] subDirs;
            try
            {
                subDirs = System.IO.Directory.GetDirectories(currentDir);
            }
            // An UnauthorizedAccessException exception will be thrown if we do not have 
            // discovery permission on a folder or file. It may or may not be acceptable  
            // to ignore the exception and continue enumerating the remaining files and  
            // folders. It is also possible (but unlikely) that a DirectoryNotFound exception  
            // will be raised. This will happen if currentDir has been deleted by 
            // another application or thread after our call to Directory.Exists. The  
            // choice of which exceptions to catch depends entirely on the specific task  
            // you are intending to perform and also on how much you know with certainty  
            // about the systems on which this code will run. 
            catch (UnauthorizedAccessException e)
            {                    
                Console.WriteLine(e.Message);
                continue;
            }
            catch (System.IO.DirectoryNotFoundException e)
            {
                Console.WriteLine(e.Message);
                continue;
            }

            string[] files = null;
            try
            {
                files = System.IO.Directory.GetFiles(currentDir);
            }

            catch (UnauthorizedAccessException e)
            {

                Console.WriteLine(e.Message);
                continue;
            }

            catch (System.IO.DirectoryNotFoundException e)
            {
                Console.WriteLine(e.Message);
                continue;
            }
            // Perform the required action on each file here. 
            // Modify this block to perform your required task. 
            foreach (string file in files)
            {
                try
                {
                    // Perform whatever action is required in your scenario.
                    System.IO.FileInfo fi = new System.IO.FileInfo(file);
                    Console.WriteLine("{0}: {1}, {2}", fi.Name, fi.Length, fi.CreationTime);
                }
                catch (System.IO.FileNotFoundException e)
                {
                    // If file was deleted by a separate application 
                    //  or thread since the call to TraverseTree() 
                    // then just continue.
                    Console.WriteLine(e.Message);
                    continue;
                }
            }

            // Push the subdirectories onto the stack for traversal. 
            // This could also be done before handing the files. 
            foreach (string str in subDirs)
                dirs.Push(str);
        }
    }
}

Обычно проверка каждой папки, чтобы определить, имеет ли приложение разрешение на ее открытие, занимает слишком много времени. Поэтому пример кода просто включает эту часть операции в блок try/catch. Блок catch можно изменить, чтобы при отказе в доступе к папке предпринималась попытка повысить права и затем вновь предпринималась попытка получить доступ. Как правило, следует перехватывать только те исключения, которые можно обработать, не оставляя приложение в неопределенном состоянии.

Если необходимо сохранить содержимое дерева каталога либо в памяти, либо на диске, наилучшим параметром будет сохранение только свойства FullName (типа string) для каждого файла. Затем можно использовать эту строку для создания нового объекта FileInfo или DirectoryInfo по мере необходимости или открыть любой файл, для которого требуется дополнительная обработка.

Отказоустойчивость

Надежный код итерации файла должен учитывать сложности файловой системы. Дополнительные сведения см. в Техническом справочнике по NTFS.

См. также

Ссылки

System.IO

Основные понятия

LINQ и каталоги файлов

Другие ресурсы

Файловая система и реестр (Руководство по программированию на C#)