ディレクトリ ツリーを反復処理する方法 (C# プログラミング ガイド)

"ディレクトリ ツリーを反復処理する" とは、指定したルート フォルダー以下の入れ子になっている各サブディレクトリ内の各ファイルにアクセスすることです。 必ずしもファイルを 1 つ 1 つ開く必要はありません。 ファイルまたはサブディレクトリの名前だけを string として取得することも、その他の情報を System.IO.FileInfo または System.IO.DirectoryInfo オブジェクトの形式で取得することもできます。

注意

Windows では、"ディレクトリ" と "フォルダー" という用語は同義です。 多くのドキュメントおよびユーザー インターフェイスのテキストでは、"フォルダー" という用語が使用されますが、.NET クラス ライブラリでは、"ディレクトリ" という用語が使用されます。

最も容易なケース、つまり、指定したルート以下のすべてのディレクトリのアクセス許可があることが確実にわかっている場合は、System.IO.SearchOption.AllDirectories フラグを使用できます。 このフラグは、指定したパターンと一致する、入れ子にされたすべてのサブディレクトリを返します。 このフラグを使用する方法を次の例に示します。

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

この方法の弱点は、指定したルート以下のサブディレクトリの 1 つが DirectoryNotFoundException または UnauthorizedAccessException を発生させた場合、メソッド全体が失敗し、ディレクトリが返されないという点です。 GetFiles メソッドを使用する場合も同様です。 特定のサブフォルダーに関するこのような例外を処理する必要がある場合は、次の例のように、ディレクトリ ツリーを手動で移動する必要があります。

ディレクトリ ツリーを手動で移動するときは、最初にファイルを処理 ("後順走査") するか、最初にサブディレクトリを処理 ("前順走査") することができます。 後順走査を実行する場合は、そのフォルダー自体のすぐ下にあるファイルにアクセスした後、現在のフォルダーの下にあるツリー全体を走査します。 前順走査は、現在のフォルダーのファイルを取得する前にツリー全体を走査するもう 1 つの方法です。 このドキュメントで後述する例では、後順走査を実行しますが、前順走査を実行するように簡単に変更できます。

また、再帰とスタック ベース走査のどちらを使用するかを選択できます。 このドキュメント内の後の例では、両方の方法を示しています。

ファイルおよびフォルダーに対してさまざまな操作を実行する必要がある場合は、単一のデリゲートを使用して呼び出すことができる個別の関数に操作をリファクタリングすることで、これらの例をモジュール化できます。

注意

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

再帰を使用せずにディレクトリ ツリー内のファイルおよびフォルダーを反復処理する方法を、次の例に示します。 この方法では、後入れ先出し (LIFO) スタックである、一般的な Stack<T> コレクション型を使用します。

ここで処理される例外や、各ファイルまたは各フォルダーに対して実行される操作は、あくまで例として用意したものです。 実際の要件を満たす際には、このコードを修正する必要があります。 詳細については、コード内のコメントを参照してください。

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 オブジェクトを作成するか、追加処理が必要なファイルを開くことができます。

信頼性の高いプログラミング

堅牢性の高いファイル反復処理コードでは、ファイル システムの数多くの複雑な部分を考慮する必要があります。 Windows ファイル システムの詳細については、「NTFS の概要」を参照してください。

関連項目