C#のDirectory.DeleteとEnumerateFilesの違いと安全なディレクトリ削除の実装パターン

C# でディレクトリを丸ごと消したいとき、Directory.Delete(path, true) で一発削除するか、Directory.EnumerateFiles/Directory.EnumerateDirectories で中身を全部列挙してから削除するかで迷うことがあります。本記事では両者の違い・使い分け・実務での注意点を、サンプルコード付きで徹底的に解説します。

目次

Directory.Delete / DirectoryInfo.Delete の基本動作

まずは Directory.Delete と DirectoryInfo.Delete の基本から整理します。どちらもディレクトリ削除用の API で、再帰削除(サブディレクトリごと削除) を指定できる点がポイントです。

代表的なシグネチャ


// static メソッド
Directory.Delete(string path);                // 空ディレクトリのみ削除
Directory.Delete(string path, bool recursive); // recursive = true で再帰削除

// インスタンス メソッド
var di = new DirectoryInfo(path);
di.Delete();                  // 空ディレクトリのみ削除
di.Delete(bool recursive);    // true で再帰削除

recursive: true を指定した場合、ディレクトリ配下のファイル・サブディレクトリをすべて削除してから、最後に自身のディレクトリを削除する、という動きになります。つまり内部的には「列挙 → 削除」の処理をまとめてやってくれているイメージです。

再帰削除が内部で行っていることのイメージ

実装詳細はフレームワーク内部に隠蔽されていますが、概念的には次のような手順で動きます。

  • 対象ディレクトリ配下のファイルを列挙し、すべて削除する
  • 対象ディレクトリ配下のサブディレクトリを列挙し、それぞれに対して再帰的に同じ処理を行う
  • 最終的に、対象ディレクトリ自身を削除する

つまり、あなたが Directory.EnumerateFiles / Directory.EnumerateDirectories でがんばって書こうとしている処理を、フレームワーク側で「標準的なやり方」で実装済みだと考えられます。

Directory.EnumerateFiles / EnumerateDirectories の基本

一方、Directory.EnumerateFiles / Directory.EnumerateDirectories は「削除」ではなく「列挙」専用の API です。こちらは ファイルやディレクトリの一覧をストリームとして順次取り出せる のが特徴です。


foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories))
{
    Console.WriteLine(file);
}

類似の GetFiles / GetDirectories は「結果を一気に配列で返す」のに対して、Enumerate* 系は 必要になったタイミングで順次列挙 されます。そのため、巨大ディレクトリを扱う際には Enumerate* を使うとメモリ効率に優れます。

削除処理を自前で実装する場合は、典型的には次のようなコードになります。


void DeleteDirectoryManually(string path)
{
    // まずファイルを削除
    foreach (var file in Directory.EnumerateFiles(path))
    {
        File.Delete(file);
    }

    // 次にサブディレクトリを再帰的に削除
    foreach (var dir in Directory.EnumerateDirectories(path))
    {
        DeleteDirectoryManually(dir);
    }

    // 最後に自分自身を削除
    Directory.Delete(path, recursive: false);
}

このようにして手動で「列挙 → 削除」を行うと、細かな挙動を自分でコントロールできる反面、コード量も責任範囲も増えます。

「DirectoryInfo.Delete(true) は手動列挙+削除と同等か?」の結論

冒頭の質問に対する結論を整理すると、次のようになります。

  • 機能面ではほぼ同等:どちらも「ディレクトリの中身を全て削除してから本体を削除」するという意味では同じ。
  • コードの見た目と責任範囲が違う:Delete(true) は「標準的な再帰削除」を一括で任せるのに対し、手動列挙は「細かな挙動を自前で定義する」スタイル。
  • エラー処理やカスタマイズ性で選び分ける:シンプルに全部消したいだけなら Delete(true)、特殊な要件があるなら手動列挙。

概要を比較した表が次です。

項目Directory.Delete(path, true)Enumerate して手動削除
コード量1 行で済む再帰処理などやや多くなる
カスタマイズ性標準的な削除のみ拡張子・属性・サイズなど自由に条件指定可能
エラー処理原則「途中で例外 → 処理中断」ファイルごとに try/catch して継続・ログ出力などがしやすい
進捗表示自前で介入しにくい件数カウントや進捗パーセントを表示しやすい
読みやすさ「丸ごと削除」の意図が非常に明確読む人によっては意図を把握しにくい
保守性標準 API に任せるので変更に強い自作ロジックのバグ・仕様差異を自分で面倒を見る必要あり

つまり、「全消し」したいだけなら Directory.Delete(path, true)(または DirectoryInfo.Delete(true))が原則ベストです。手動列挙は「標準的でない特殊要件」を満たしたいときにだけ使う、という位置づけにしておくとシンプルに保てます。

おすすめの標準コード(最短・実用形)

もっともシンプルで読みやすく、実務でもそのまま使える形は次のようなコードです。


using System.IO;

string path = @"C:\path\to\delete";

if (Directory.Exists(path))
{
    var di = new DirectoryInfo(path);

    // ここで必要なら「危険なパスではないか」をチェックしてもよい
    // 例: 絶対パスか? 特定のルート配下か? など

    di.Delete(recursive: true); // または Directory.Delete(path, true);
}

この書き方は、意図が非常に明確です。

  • このコードを見た人は「このディレクトリを丸ごと消しているんだな」とすぐに理解できる
  • 不要なロジックを一切書いていないので、バグの入り込む余地が少ない
  • 内部の削除順序など、「標準的な振る舞い」は .NET に任せられる

業務コードでも、特殊な要件がなければまずこの形を採用し、要件が出てきたら初めて手動列挙を検討するのが保守性の高い設計です。

手動列挙が向く具体的なケース

では、どのような場合に Directory.EnumerateFiles / Directory.EnumerateDirectories を使って手動削除するのが有利になるのでしょうか。典型的なパターンを整理します。

一部のファイルを残したい・条件付きで削除したい

  • 特定の拡張子(例: .log)だけ削除したい
  • サイズの大きいファイルだけ優先的に削除したい
  • 更新日時が古いものだけ削除し、新しいものは残したい

このようなケースでは、手動列挙で条件を付けて削除する必要があります。


void DeleteOldLogFiles(string dirPath, TimeSpan maxAge)
{
    var threshold = DateTime.Now - maxAge;

    foreach (var file in Directory.EnumerateFiles(dirPath, "*.log"))
    {
        var info = new FileInfo(file);
        if (info.LastWriteTime < threshold)
        {
            info.Delete();
        }
    }
}

このような「ポリシーベースの削除」は、Directory.Delete(path, true) では行えません。

読み取り専用属性・システム属性を外してから削除したい

Windows ファイルには ReadOnly や System などの属性が付いていることがあります。これらが付いたままだと削除に失敗するケースがあり、事前に属性を外したいというニーズがよくあります。


void ForceDeleteFile(string filePath)
{
    if (File.Exists(filePath))
    {
        var attr = File.GetAttributes(filePath);
        if ((attr & FileAttributes.ReadOnly) != 0 ||
            (attr & FileAttributes.System)   != 0)
        {
            // 読み取り専用・システム属性を外す
            attr &= ~FileAttributes.ReadOnly;
            attr &= ~FileAttributes.System;
            File.SetAttributes(filePath, attr);
        }

        File.Delete(filePath);
    }
}

これをディレクトリ全体に適用するなら、手動で列挙しながら属性を外していく必要があります。

失敗ファイルをスキップしつつログに記録したい

Directory.Delete(path, true) は、途中で何らかの例外(アクセス拒否、ファイル使用中など)が発生すると全体が失敗します。「失敗したファイルは飛ばして、削除できたもの・できなかったものをログに残したい」 といったユースケースでは、手動で try/catch しながら進めるのが現実的です。


void DeleteDirectoryWithLogging(string path, TextWriter logger)
{
    foreach (var file in Directory.EnumerateFiles(path))
    {
        try
        {
            File.Delete(file);
            logger.WriteLine($"Deleted file: {file}");
        }
        catch (Exception ex)
        {
            logger.WriteLine($"Failed to delete file: {file} - {ex.Message}");
        }
    }

    foreach (var dir in Directory.EnumerateDirectories(path))
    {
        DeleteDirectoryWithLogging(dir, logger);
    }

    try
    {
        Directory.Delete(path, false);
        logger.WriteLine($"Deleted dir: {path}");
    }
    catch (Exception ex)
    {
        logger.WriteLine($"Failed to delete dir: {path} - {ex.Message}");
    }
}

このようなきめ細かいエラー処理は、自前実装ならではの強みです。

巨大ディレクトリで進捗を表示したい

数十万ファイル規模のディレクトリを削除するようなツールを作る場合、「あとどれくらいで終わりそうか」をユーザーに見せたいことがあります。その場合は、削除対象件数をカウントしながら進捗を表示できる手動列挙が向きます。


void DeleteWithProgress(string path, IProgress<double> progress)
{
    var allFiles = Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)
                            .ToList();
    int total = allFiles.Count;
    int deleted = 0;

    foreach (var file in allFiles)
    {
        File.Delete(file);
        deleted++;
        progress.Report((double)deleted / total);
    }

    // ファイル削除後にディレクトリを削除
    foreach (var dir in Directory.EnumerateDirectories(path, "*", SearchOption.AllDirectories)
                                 .OrderByDescending(d => d.Length))
    {
        Directory.Delete(dir, false);
    }

    Directory.Delete(path, false);
}

この例では一旦 ToList() しているためメモリ消費は増えますが、進捗率を計算するためには「総数の把握」が必要になるので、どこかでこのような工夫が必要です。

例外・エラーパターンと対処ポイント

Directory.Delete でも手動削除でも、現実的にはさまざまな例外に遭遇します。代表的なものと対策を整理します。

例外発生原因の例対処の方向性
UnauthorizedAccessExceptionアクセス権不足、読み取り専用ディレクトリなどアクセス権の付け直し、属性変更、管理者権限で実行
IOExceptionファイルが他プロセスで使用中、I/O エラーリトライ、一定時間後に再試行、ロック元プロセスの見直し
DirectoryNotFoundException既に削除済み、パスの打ち間違い事前の存在チェック、競合を前提に try/catch で握る
PathTooLongException古い環境での長いパス問題長いパスサポートの有効化、パス設計の見直し

実務コードでは、多くの場合「削除に失敗したらログに残して継続する」のか「失敗したら即座に処理を止める」のかを仕様として決めておくことが重要です。その仕様に合わせて、Directory.Delete をそのまま使うか、手動列挙+個別 try/catch にするかが変わってきます。

読み取り専用ファイルを含むディレクトリの削除

読み取り専用属性を持つファイルが含まれていると、Directory.Delete(path, true) で削除に失敗することがあります。そうした「ちょっと頑固なファイル」を含むディレクトリを確実に削除したい場合は、事前に属性を外す手動処理を挟むのが安全です。


void DeleteDirectoryForce(string path)
{
    if (!Directory.Exists(path))
    {
        return;
    }

    // まず配下のファイルの属性を調整しつつ削除
    foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories))
    {
        try
        {
            var attr = File.GetAttributes(file);
            if ((attr & FileAttributes.ReadOnly) != 0 ||
                (attr & FileAttributes.System) != 0)
            {
                attr &= ~FileAttributes.ReadOnly;
                attr &= ~FileAttributes.System;
                File.SetAttributes(file, attr);
            }
            File.Delete(file);
        }
        catch
        {
            // ログに書くなど、必要に応じて処理
        }
    }

    // 次に空になったディレクトリを下層から削除
    foreach (var dir in Directory.EnumerateDirectories(path, "*", SearchOption.AllDirectories)
                                 .OrderByDescending(d => d.Length))
    {
        try
        {
            Directory.Delete(dir, false);
        }
        catch
        {
            // ログなど
        }
    }

    // 最後にルートを削除
    Directory.Delete(path, false);
}

ここまでやるケースはそれほど多くはありませんが、「どうしても削除してほしい」という要件があるツールでは、こうしたフォールバックロジックが役に立ちます。

シンボリックリンク/ジャンクションの扱い

Windows では、ディレクトリに対して シンボリックリンク や ジャンクション(再解析ポイント) を作成することができます。これらの扱いには注意が必要です。

  • リンクそのものを消したいのか
  • リンク先の中身まで再帰的に削除したいのか(通常はやりたくない)

一般的には「リンク先まで巻き込んで消したくない」ケースが多いため、リンクを検出してスキップする 実装にしておく方が安全です。


bool IsReparsePoint(string path)
{
    var attr = File.GetAttributes(path);
    return (attr & FileAttributes.ReparsePoint) != 0;
}

void DeleteDirectoryWithoutFollowingLinks(string path)
{
    foreach (var file in Directory.EnumerateFiles(path))
    {
        if (IsReparsePoint(file))
        {
            // シンボリックリンクの中身までは辿らない
            File.Delete(file); // リンクそのものだけ削除
        }
        else
        {
            File.Delete(file);
        }
    }

    foreach (var dir in Directory.EnumerateDirectories(path))
    {
        if (IsReparsePoint(dir))
        {
            // リンク先を辿らず、リンクディレクトリだけ削除
            Directory.Delete(dir, false);
        }
        else
        {
            DeleteDirectoryWithoutFollowingLinks(dir);
        }
    }

    Directory.Delete(path, false);
}

Directory.Delete(path, true) も通常はリンク先を辿ることはありませんが、リンクの扱いに明確なポリシーを持ちたい場合 は、このように自前で制御できる手動列挙方式が適しています。

並行実行とレースコンディションの現実

削除処理は、多くの場合「他のプロセス・スレッドと競合する」ことを前提に考えた方が安全です。例えば次のようなケースがあります。

  • 削除対象のディレクトリを、別プロセスが監視してファイルを書き込んでいる
  • Web アプリや Windows サービスが、ログファイルを出力し続けている
  • ユーザーがエクスプローラーで開いていたり、ウイルススキャンが走っている

このような状況では、削除処理と同時にファイルが作成・オープンされ、列挙したときには存在しなかったファイルが、削除しようとしたタイミングでは存在する といったレースコンディションが発生します。

重要なのは、これは Directory.Delete(path, true) を使っても、手動列挙をしても起こり得る という点です。どちらの API も「トランザクション削除」ではないので、途中で状態が変わることは避けられません。

対策としては、次のような方針が考えられます。

  • 削除対象ディレクトリを、アプリ側で「閉鎖」してから実行する(新規ファイルを書き込まないようにする)
  • 失敗時に一定回数リトライする(短時間のロックであれば解消を待つ)
  • どうしても消せないファイルがあった場合はログに残し、それ以上は追わない

例えば、簡易的なリトライ付き削除関数は次のように書けます。


bool TryDeleteDirectory(string path, int retryCount = 3, int delayMs = 500)
{
    for (int i = 0; i < retryCount; i++)
    {
        try
        {
            Directory.Delete(path, recursive: true);
            return true;
        }
        catch (IOException)
        {
            // 使用中など、一時的なエラーのことが多い
        }
        catch (UnauthorizedAccessException)
        {
            // 権限周りの問題も、一時的に解消されることがある
        }

        Thread.Sleep(delayMs);
    }

    return false;
}

このように「削除は必ずしも一発では成功しない」ことを前提に、API の選択とエラー処理を考えるのが現実的です。

安全対策:うっかり重要ディレクトリを消さないために

Directory.Delete(path, true) は非常に強力な API なので、誤って重要なディレクトリを指定してしまうと取り返しがつきません。実務では、次のようなガードを入れておくのがおすすめです。

  • 削除対象パスは絶対パスに正規化してから扱う
  • 許可されたルート配下かどうかをチェックする(例: C:\AppData\Temp 配下のみ削除を許可)
  • プロダクション環境では、危険なパスを明示的にブロックする(C:\Windows やドライブ直下など)
  • 本番運用前に、ログ出力だけ行う「ドライラン(試し削除)」モードを用意する

bool IsAllowedDeleteTarget(string root, string target)
{
    var rootFull   = Path.GetFullPath(root).TrimEnd(Path.DirectorySeparatorChar);
    var targetFull = Path.GetFullPath(target).TrimEnd(Path.DirectorySeparatorChar);

    return targetFull.StartsWith(rootFull, StringComparison.OrdinalIgnoreCase);
}

こうしたガードロジックを一度きちんと作っておけば、以降の削除系コードは安心して Directory.Delete(path, true) を呼びやすくなります。

ユースケース別のおすすめパターン

ここまでの内容を踏まえて、実務でありがちなユースケースごとに「どちらを使うべきか」を簡単に整理します。

ユースケースおすすめ APIポイント
アプリの一時フォルダを起動時に全削除Directory.Delete(path, true)単純な全削除。失敗したらログだけ残して続行でもよい。
ログフォルダで古いファイルだけ削除EnumerateFiles + File.Delete更新日時による条件付き削除が必要。
インストーラ/アンインストーラでアプリフォルダを完全削除Directory.Delete(path, true) + 必要ならフォールバック基本は一発削除、頑固なファイル用に属性解除ロジックを追加。
巨大ディレクトリを削除する GUI ツールEnumerate + 進捗表示ユーザー体験的に進捗バーが欲しい。
自動クリーンアップバッチ(夜間実行)Directory.Delete(path, true)シンプルさ優先。失敗は次回のバッチに任せるのも手。

まとめ:まずは Directory.Delete(path, true) を基準に考える

最後に、本記事のポイントを整理します。

  • Directory.Delete(path, true) と「Enumerate で列挙してから削除」は、機能面ではほぼ同等
  • 違いは「誰が削除の細部をコントロールするか」であり、標準挙動で足りるならフレームワークに任せた方がシンプルで安全
  • 一方で、次のような場合は手動列挙が有利:
    • 一部のファイルだけ消したい・残したい
    • 読み取り専用・システム属性を外してから削除したい
    • 失敗したファイルを個別に記録・集計したい
    • 進捗表示やキャンセルなど、UI と連携したい
    • シンボリックリンクの扱いを厳密に制御したい
  • どちらの方法でも、アクセス権・ファイル使用中・長いパス・並行実行といった現実の問題には注意が必要
  • 安全対策(パス検証・ガード)をきちんと入れておくと、「削除コード」を安心して再利用できる

C# でディレクトリ削除を実装する際は、「とりあえず全部列挙して自前で削除を書く」のではなく、まずは Directory.Delete(path, true) で済ませられないかを検討し、それで足りない要件が出てきたときにだけ手動列挙に切り替える、という方針がおすすめです。そうすることで、コードの見通しと保守性を高いレベルで保つことができます。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次