如何循环访问目录树(C# 编程指南)

短语“循环访问目录树”的意思是访问特定根文件夹下的每个嵌套子目录中的每个文件,可以是任意深度。 不需要打开每个文件。 可以以 string 的形式只检索文件或子目录的名称,也可以以 System.IO.FileInfoSystem.IO.DirectoryInfo 对象的形式检索其他信息。

注意

在 Windows 中,术语“目录”和“文件夹”可以互换使用。 大多数文档和用户界面文本使用术语“文件夹”,但 .NET 类库使用术语“目录”。

在最简单的情况下,如果你确信拥有指定根目录下的所有目录的访问权限,则可以使用 System.IO.SearchOption.AllDirectories 标志。 此标志返回与指定的模式匹配的所有嵌套的子目录。 下面的示例演示如何使用此标志。

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

此方法的缺点是,如果指定根目录下的任何子目录引发 DirectoryNotFoundExceptionUnauthorizedAccessException 异常,则整个方法失败且不返回任何目录。 使用 GetFiles 方法时也是如此。 如果需要处理特定子文件夹中的异常,则必须手动遍历目录树,如以下示例所示。

手动遍历目录树时,可以先处理文件(前序遍历),或者先处理子目录(后序遍历)。 如果执行前序遍历,则可直接访问该文件夹本身下的文件,然后遍历当前文件夹下的整个树。 后序遍历是另一种方法,在访问当前文件夹的文件之前遍历下面的整个树。 本文档后面的示例执行的是前序遍历,但你可以轻松地修改它们以执行后序遍历。

另一种选择是,是使用递归遍历还是基于堆栈的遍历。 本文档后面的示例演示了这两种方法。

如果需要对文件和文件夹执行各种操作,则可以模块化这些示例,方法是将操作重构为可使用单个委托进行调用的单独的函数。

注意

NTFS 文件系统可以包含交接点、符号链接和硬链接等形式的重解析点。 诸如 GetFilesGetDirectories 等 .NET 方法不会返回重分析点下的任何子目录。 当两个重解析点相互引用时,此行为可防止进入无限循环。 通常,处理重解析点时应格外小心,以确保不会无意中修改或删除文件。 如果需要精确控制重解析点,请使用平台调用或本机代码直接调用相应的 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<T> 集合类型,此集合类型是一个后进先出 (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)。 然后可以根据需要使用此字符串创建新的 FileInfoDirectoryInfo 对象,或打开需要进行其他处理的任何文件。

可靠编程

可靠的文件迭代代码必须考虑文件系统的诸多复杂性。 有关 Windows 文件系统的详细信息,请参阅 NTFS 概述

请参阅