C# Tips | ファイル・ディレクトリ操作:空フォルダ削除

C sharp C#
スポンサーリンク

はじめに なぜ「空フォルダ削除」が業務で役に立つのか

業務システムでファイルを扱っていると、ログやバックアップ、インポート・エクスポート用の一時フォルダなどがどんどん増えていきます。
ファイルは削除しているのに、フォルダだけが階層深く残り続けて、「中身は空なのにフォルダ構造だけやたら複雑」という状態になりがちです。

人間がエクスプローラーで一つずつ確認して削除するのは、時間もかかるしミスもしやすいです。
そこで、「空のフォルダだけを自動で削除する」ユーティリティを用意しておくと、定期バッチやメンテナンス処理の中で、きれいな状態を保ちやすくなります。

ここでは、C# での「空フォルダ削除」の基本から、再帰的な削除、実務での注意点まで、初心者向けにかみ砕いて解説していきます。


基本の考え方 「空フォルダ」とは何かを定義する

「空フォルダ」の条件を言葉にしてみる

まずは、「空フォルダとは何か」をはっきりさせましょう。
プログラムを書く前に、日本語で条件を言葉にできるかがとても大事です。

一般的には、次のように考えます。

中にファイルが一つもない。
中にサブフォルダも一つもない。

つまり、「そのフォルダの直下に何も存在しない状態」です。
ここでポイントになるのが、「サブフォルダも含めて何もない」のか、「直下だけ見て判断する」のか、という違いです。

例えば、A フォルダの中に B フォルダがあり、B の中は空だとします。
このとき、「B は空フォルダだが、A は空ではない」とみなすのか、「B を消したあとなら A も空になるので、最終的には A も消したい」のか。
実務では後者、つまり「下から順に空を消していき、結果として上のフォルダも空になったら消す」という動きが欲しくなることが多いです。

この「下から順に」という考え方が、再帰処理のポイントになります。

Directory.Delete と recursive フラグ

C# でフォルダを削除する基本は Directory.Delete です。

Directory.Delete(path);
C#

これは、「フォルダが空であれば削除する」という動きです。
中にファイルやサブフォルダがあると、例外が発生して削除できません。

一方、次のように書くと、中身ごと全部削除します。

Directory.Delete(path, recursive: true);
C#

これは「中身を問わず全部消す」ので、今回やりたい「空フォルダだけを消す」とは目的が違います。
今回のテーマは、「中身があるフォルダは残し、空になったフォルダだけを消す」ことです。


単一フォルダが空かどうかを判定する

空フォルダ判定の基本メソッド

まずは、「このフォルダが空かどうか」を判定する小さなユーティリティから作ってみます。

using System;
using System.IO;

public static class DirectoryUtil
{
    public static bool IsDirectoryEmpty(string directoryPath)
    {
        if (!Directory.Exists(directoryPath))
        {
            throw new DirectoryNotFoundException($"ディレクトリが見つかりません: {directoryPath}");
        }

        string[] files = Directory.GetFiles(directoryPath);
        if (files.Length > 0)
        {
            return false;
        }

        string[] dirs = Directory.GetDirectories(directoryPath);
        if (dirs.Length > 0)
        {
            return false;
        }

        return true;
    }
}
C#

ここでやっていることは、とてもシンプルです。
指定したディレクトリの直下にファイルが一つでもあれば空ではない。
サブディレクトリが一つでもあれば空ではない。
どちらもゼロなら空、と判定しています。

使い方の例は次の通りです。

class Program
{
    static void Main()
    {
        string dir = @"C:\temp\work";

        try
        {
            bool empty = DirectoryUtil.IsDirectoryEmpty(dir);

            Console.WriteLine(empty
                ? "このフォルダは空です。"
                : "このフォルダには何か入っています。");
        }
        catch (DirectoryNotFoundException ex)
        {
            Console.WriteLine("エラー: " + ex.Message);
        }
    }
}
C#

この「空かどうか判定」は、後で「空なら削除する」という処理と組み合わせて使います。


単一フォルダの空フォルダ削除

「空なら削除する」ユーティリティ

次に、「指定フォルダが空なら削除する」というユーティリティを作ります。

using System;
using System.IO;

public static class DirectoryUtil
{
    public static bool DeleteIfEmpty(string directoryPath)
    {
        if (!Directory.Exists(directoryPath))
        {
            return false;
        }

        string[] files = Directory.GetFiles(directoryPath);
        if (files.Length > 0)
        {
            return false;
        }

        string[] dirs = Directory.GetDirectories(directoryPath);
        if (dirs.Length > 0)
        {
            return false;
        }

        Directory.Delete(directoryPath);
        return true;
    }
}
C#

戻り値の bool は、「削除したかどうか」を表しています。
削除できた場合は true、空ではなかったり存在しなかったりして削除しなかった場合は false です。

使い方の例は次の通りです。

class Program
{
    static void Main()
    {
        string dir = @"C:\temp\work";

        bool deleted = DirectoryUtil.DeleteIfEmpty(dir);

        Console.WriteLine(deleted
            ? "空フォルダだったので削除しました。"
            : "フォルダは空ではないか、存在しません。");
    }
}
C#

ここまでで、「単体のフォルダが空なら消す」という基本は押さえられました。
しかし、実務で本当に欲しいのは、「階層全体を見て、空になったフォルダを下から順に消していく」処理です。


再帰的な空フォルダ削除 下から順に消していく

なぜ「下から順に」なのか

例えば、次のようなフォルダ構造を考えます。

C:\logs
└─ 2025
└─ 01
└─ 23

一番下の「23」フォルダの中身を全部削除したとします。
このとき、「23」は空なので削除できます。
すると、「01」の中には何もなくなり、今度は「01」も空になります。
同じように「2025」も空になり、最終的には「2025」も削除したくなるかもしれません。

このように、「下のフォルダを消した結果、上のフォルダも空になる」ということがあるので、
空フォルダ削除は「一番下から順に」見ていく必要があります。

これをコードで表現するときに使うのが「再帰」です。

再帰的に空フォルダを削除するユーティリティ

次のコードは、「指定したフォルダ配下を再帰的にたどり、空になったフォルダを下から順に削除する」ユーティリティです。

using System;
using System.IO;

public static class EmptyDirectoryCleaner
{
    public static int DeleteEmptyDirectoriesRecursive(string rootDirectoryPath)
    {
        if (!Directory.Exists(rootDirectoryPath))
        {
            throw new DirectoryNotFoundException($"ディレクトリが見つかりません: {rootDirectoryPath}");
        }

        int deletedCount = 0;

        DeleteEmptyDirectoriesInternal(rootDirectoryPath, ref deletedCount);

        return deletedCount;
    }

    private static bool DeleteEmptyDirectoriesInternal(string directoryPath, ref int deletedCount)
    {
        string[] subDirs = Directory.GetDirectories(directoryPath);

        foreach (string subDir in subDirs)
        {
            DeleteEmptyDirectoriesInternal(subDir, ref deletedCount);
        }

        string[] files = Directory.GetFiles(directoryPath);
        subDirs = Directory.GetDirectories(directoryPath);

        if (files.Length == 0 && subDirs.Length == 0)
        {
            Directory.Delete(directoryPath);
            deletedCount++;
            return true;
        }

        return false;
    }
}
C#

ここでの重要ポイントを順番に整理します。

まず、DeleteEmptyDirectoriesRecursive は「入口」で、存在チェックをしてから内部メソッドを呼び出し、削除したフォルダ数を返します。
実際の処理は DeleteEmptyDirectoriesInternal に隠してあり、これは「自分自身を呼び出す」再帰メソッドです。

DeleteEmptyDirectoriesInternal の中では、最初にサブディレクトリ一覧を取得し、各サブディレクトリに対して自分自身を呼び出しています。
これにより、「一番下の階層まで潜ってから戻ってくる」という動きになります。

サブディレクトリの処理が終わったあとで、改めてファイルとサブディレクトリの数を数え直しています。
ここがとても大事です。
なぜなら、「サブディレクトリの中身を処理した結果、そのサブディレクトリ自体が削除されているかもしれない」からです。
そのため、「最初に取ったサブディレクトリ一覧」ではなく、「処理後の最新状態」を見直す必要があります。

そして、ファイル数もサブディレクトリ数もゼロなら、そのフォルダは空なので削除します。
削除したらカウンタを増やし、true を返しています(この戻り値は、必要に応じて「親側で使う」こともできますが、ここでは削除カウントだけに使っています)。

使い方の例

class Program
{
    static void Main()
    {
        string root = @"C:\logs";

        try
        {
            int deleted = EmptyDirectoryCleaner.DeleteEmptyDirectoriesRecursive(root);

            Console.WriteLine($"削除した空フォルダ数: {deleted}");
        }
        catch (DirectoryNotFoundException ex)
        {
            Console.WriteLine("エラー: " + ex.Message);
        }
    }
}
C#

このようにしておくと、「ログフォルダ配下の空フォルダを定期的に掃除するバッチ」などを簡単に作れます。


実務での注意点 「消していいフォルダ」と「消してはいけないフォルダ」

ルートフォルダを消すかどうかの設計

先ほどの実装では、「空になったフォルダは、ルートであっても削除する」動きになっています。
つまり、C:\logs 自体が空になった場合は、それも削除されます。

しかし、実務では「ルートフォルダは残したい」ということも多いです。
例えば、「C:\logs というフォルダ自体はシステムの前提なので、空でも消したくない」というケースです。

その場合は、「ルートは削除対象から外す」という条件を入れます。

public static int DeleteEmptyDirectoriesRecursive(string rootDirectoryPath)
{
    if (!Directory.Exists(rootDirectoryPath))
    {
        throw new DirectoryNotFoundException($"ディレクトリが見つかりません: {rootDirectoryPath}");
    }

    int deletedCount = 0;

    string[] subDirs = Directory.GetDirectories(rootDirectoryPath);

    foreach (string subDir in subDirs)
    {
        DeleteEmptyDirectoriesInternal(subDir, ref deletedCount);
    }

    return deletedCount;
}
C#

このように、「ルートは自分で処理せず、配下だけを再帰処理する」ように変えると、ルートフォルダは必ず残ります。
どちらが正しいかは業務ルール次第なので、「このフォルダは絶対に残す」という前提があるなら、コードでそれを表現しておくことが大切です。

「本当に空か?」の定義をどうするか

もう一つの注意点は、「本当に空か?」の定義です。
例えば、「隠しファイルやシステムファイルが入っているフォルダは、空とみなしてよいのか?」という問題があります。

Directory.GetFiles は、隠しファイルも含めて取得します。
つまり、「見た目には空に見えるけれど、実は隠しファイルが入っているフォルダ」は、空とは判定されません。

もし「隠しファイルは無視してよい」という運用なら、「隠し属性のファイルを除外して数える」といった工夫が必要になります。
逆に、「隠しファイルが入っているなら、なおさら消してはいけない」という運用なら、今の実装のままで問題ありません。

ここも、「自分たちの現場ではどう扱いたいか」を先に決めてから、コードに落とし込むのが大事です。


例外とエラー処理を意識した空フォルダ削除

起こり得る例外とその意味

空フォルダ削除では、次のような理由で例外が発生する可能性があります。

対象ディレクトリが存在しない。
権限がなくてフォルダの中身を列挙できない、または削除できない。
別プロセスがフォルダや中のファイルをロックしている。
ネットワークドライブの接続が切れている。

これらはすべて、「フォルダを削除できなかった理由」です。
実務では、「どのフォルダで、どんな理由で失敗したか」をログに残しておくと、後から調査しやすくなります。

呼び出し側での例外処理の例

using System;
using System.IO;

class Program
{
    static void Main()
    {
        string root = @"C:\logs";

        try
        {
            int deleted = EmptyDirectoryCleaner.DeleteEmptyDirectoriesRecursive(root);
            Console.WriteLine($"削除した空フォルダ数: {deleted}");
        }
        catch (DirectoryNotFoundException ex)
        {
            Console.WriteLine("ディレクトリが見つかりません: " + ex.Message);
        }
        catch (UnauthorizedAccessException ex)
        {
            Console.WriteLine("権限エラーが発生しました: " + ex.Message);
        }
        catch (IOException ex)
        {
            Console.WriteLine("入出力エラーが発生しました: " + ex.Message);
        }
        catch (Exception ex)
        {
            Console.WriteLine("想定外のエラーが発生しました: " + ex.Message);
        }
    }
}
C#

本番環境では、これらのメッセージをコンソールではなくログファイルや監視システムに出力することで、「定期バッチがどこまで成功しているか」「どこで詰まっているか」を可視化できます。


まとめ 実務で使える「空フォルダ削除」ユーティリティの考え方

空フォルダ削除は、一見地味ですが、「ログや一時フォルダが増え続けてカオスになる」ことを防ぐための大事なメンテナンス処理です。
だからこそ、「とりあえず Delete を呼ぶ」ではなく、「どこまで消すか」「何を空とみなすか」をきちんと設計しておく価値があります。

単一フォルダが空かどうかを判定する IsDirectoryEmpty のような小さなユーティリティを用意すること。
「空なら削除する」DeleteIfEmpty と、「階層全体を下から順に掃除する」再帰的な DeleteEmptyDirectoriesRecursive を分けて設計すること。
ルートフォルダを消すかどうか、隠しファイルをどう扱うかなど、「自分たちの現場でのルール」をコードに反映させること。
例外や失敗をログに残し、「どのフォルダで何が起きたか」を後から追えるようにしておくこと。

ここまで押さえておけば、「気づいたらフォルダ構造がゴミだらけ」という状態をかなり防げます。

タイトルとURLをコピーしました