FAT32でDirectory.Delete(true)が「Unicode文字を対象のマルチバイトコードページにマップできません」になる原因と対処法

FAT32でフォーマットされたUSBメモリ上のフォルダーをC#の Directory.Delete(true) で一括削除したとき、「Unicode文字を対象のマルチバイトコードページにマップできません」という IOException に悩まされていませんか?本記事では Windows 10/.NET 9 環境で実際に発生した事例をもとに、原因と安全な削除方法を詳しく解説します。

目次

発生するエラーと環境の整理

まずは、問題の事象をはっきり整理しておきます。

エラーが発生する条件

  • OS:Windows 10
  • .NET:.NET 9(MAUI アプリだが MAUI 依存はなし。純粋な .NET / C# の問題)
  • 対象ドライブ:外付けドライブや USB メモリなど、FAT32 でフォーマットされたドライブ(例:E:)
  • フォルダー名:ヘブライ語など、非 Latin-1 文字を含むフォルダーやその配下
  • 削除コード: new DirectoryInfo(path).Delete(true);
  • 発生する例外: System.IO.IOException: No mapping for the Unicode character exists in the target multi-byte code page. 日本語環境だと概ね「Unicode 文字を対象のマルチバイト コードページにマップできません」といったメッセージになります。
  • 発生頻度:毎回ではなく、断続的に発生することがある

この状況は、アプリのロジックが悪いわけではなく、FAT32 上での列挙処理と非 Latin-1 文字の組み合わせに起因する、ランタイム/Win32 まわりの既知の挙動にぶつかっているケースと考えられます。

状況を一覧で確認

項目内容
OSWindows 10
.NET ランタイム.NET 9(MAUI アプリだが問題は .NET/IO 部分)
ファイルシステムFAT32(外付け HDD / USB メモリなど)
フォルダー名ヘブライ語など、非 Latin-1 文字を含むパス
削除 APIDirectoryInfo.Delete(true)
例外System.IO.IOException: No mapping for the Unicode character exists in the target multi-byte code page.
BitLocker有無に関係なく再現。BitLocker は無関係

原因の概要:FAT32 × 非 Latin-1 × 再帰削除の組み合わせ

このエラーのポイントは、次の 3 つの条件が同時にそろったときに表面化しやすいという点です。

  • 対象ドライブが FAT32
  • フォルダー・ファイル名に 非 Latin-1 文字(ヘブライ語など)を含む
  • DirectoryInfo.Delete(true) による再帰削除を行う

C# の DirectoryInfo.Delete(true) は、内部的に

  • フォルダー配下のファイル・ディレクトリを列挙する
  • 列挙された要素を順に削除していく

という処理を行います。この「列挙」の際、FAT32 + 非 Latin-1 文字という条件下で、Windows のコードページ変換がうまくいかず、ERROR_NO_UNICODE_TRANSLATION(=「Unicode を対象コードページに変換できない」)に相当するエラーが返ってしまう場合があります。

その結果として、.NET 側では System.IO.IOException として、上記のメッセージがスローされます。

なぜ FAT32 で起きやすく、NTFS / exFAT では起きにくいのか

ざっくり言うと、FAT32 は古い設計のファイルシステムであり、

  • Unicode を前提とした設計ではない
  • Windows 側での互換性のために、内部で ANSI/マルチバイトコードページへの変換経路を通ることがある

といった事情があります。一方で NTFS や exFAT は Unicode 対応がより良く、同じようなフォルダー名でも問題なく扱えることがほとんどです。

ファイルシステム非 Latin-1 文字を含むパスDirectoryInfo.Delete(true) の傾向
FAT32あり断続的に IOException が発生し得る
exFATあり通常は問題なく削除できる
NTFSあり通常は問題なく削除できる

つまり、本件はアプリの実装バグというより、FAT32 に Unicode リッチな名前を持つフォルダーを載せて再帰削除したときに露出する Windows/ランタイム側のクセに近い現象です。

BitLocker は原因ではない

元の事象では、BitLocker で暗号化されたドライブ(BitLocker To Go)上で発生していたため、「BitLocker が悪さをしているのでは?」と疑いたくなります。

しかし、BitLocker は NTFS や FAT32 といったファイルシステムの「下」にある暗号化レイヤーであり、ファイル名の列挙や文字コード変換に直接関与しません。実際に、BitLocker の有無に関わらず FAT32 + 非 Latin-1 文字 + Delete(true) の条件がそろうと再現し得るため、結論としては次のようになります。

  • BitLocker はこのエラーの 原因ではない
  • たまたま「BitLocker 付きの FAT32 ドライブ」で発生したため、BitLocker が疑われやすかった

したがって、対策として BitLocker をオフにする必要はありません。問題の本質は FAT32 上でのパス列挙と文字コードの扱いです。

DirectoryInfo.Delete(true) が危険になるパターン

new DirectoryInfo(path).Delete(true) はとても便利で、1 行でサブフォルダーごと削除できるため、サンプルや小規模ツールではよく使われます。しかし、以下のような条件があるときは注意が必要です。

  • 削除対象の深さが深い
  • 多言語(日本語、ヘブライ語、中国語など)のフォルダー名が混在する
  • FAT32 やネットワークドライブなど、やや「クセ」のあるファイルシステムやドライバー上

このような環境では、Delete(true) 内部の再帰処理に依存せず、Directory.EnumerateFiles と Directory.EnumerateDirectories を使って自前で列挙しながら削除する方が安全です。

推奨される回避策:自前で列挙して後行順に削除する

本件の回避策として最も効果的なのが、次の方針です。

  1. Directory.EnumerateFiles / Directory.EnumerateDirectories で自前で列挙
  2. ファイルをすべて削除
  3. サブディレクトリを再帰的に同じ手順で削除
  4. 最後に親ディレクトリを削除(後行順:中身 → 親)

元の質問では、シンプルな回避策として次のような手順で症状の解消が報告されています。

  1. EnumerateFiles でファイルを全削除
  2. EnumerateDirectories で列挙した各サブフォルダーに対して Directory.Delete(subDir, true)
  3. 親ディレクトリを Directory.Delete(path, false)

この方法だけでも、「FAT32 × 非 Latin-1 × Delete(true)」の問題パスをある程度避けられるため、実際にエラーが再現しなくなったとの報告があります。ただし、より確実にするなら、サブフォルダー側でも Delete(true) に頼らない 実装にしておく方が安心です。

堅牢版:Delete(true) を使わない再帰削除サンプル

ここでは、FAT32 上でもより安全に動作することを意図した、堅牢版のサンプルコードを紹介します。ポイントは次の通りです。

  • ファイル → サブフォルダー → 自分自身 の順に削除(後行順)
  • Directory.EnumerateFiles / Directory.EnumerateDirectories を利用
  • 削除前に 読み取り専用属性を解除(できる範囲で)
  • DirectoryInfo.Delete(true) を一切使わない
using System;
using System.IO;

public static class SafeDelete
{
    public static void DeleteDirectoryOnFat32(string path)
    {
        if (string.IsNullOrWhiteSpace(path) || !Directory.Exists(path))
        {
            return;
        }

        // 1) 先にファイルを削除
        foreach (var file in Directory.EnumerateFiles(path))
        {
            TryClearReadOnly(file);
            File.Delete(file);
        }

        // 2) サブフォルダーは再帰的に削除
        foreach (var dir in Directory.EnumerateDirectories(path))
        {
            DeleteDirectoryOnFat32(dir);
        }

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

    private static void TryClearReadOnly(string path)
    {
        try
        {
            var attr = File.GetAttributes(path);
            if ((attr & FileAttributes.ReadOnly) != 0)
            {
                File.SetAttributes(path, attr & ~FileAttributes.ReadOnly);
            }
        }
        catch
        {
            // 属性取得に失敗した場合は無視
            // 実運用ではログ出力などを行うとよい
        }
    }
}

コードの解説

処理説明
string.IsNullOrWhiteSpace(path) || !Directory.Exists(path)渡されたパスが空、または既に存在しない場合は何もせず終了します。
Directory.EnumerateFiles(path)対象ディレクトリ直下のファイルを列挙します。Enumerate* は遅延列挙のため、大量のファイルでもメモリ効率が良くなります。
TryClearReadOnly(file); File.Delete(file);削除前に読み取り専用属性を解除し、削除エラーを減らします。
Directory.EnumerateDirectories(path)サブディレクトリを列挙し、それぞれに対して再帰呼び出しを行います。
Directory.Delete(path, false);最後に「空になったはずの」対象ディレクトリを削除します。false なので、この時点で中身が残っていると例外になります。
TryClearReadOnly読み取り専用属性が立っている場合のみ、それを外します。失敗しても例外は握りつぶし、削除時の例外で気づけるようにしています。

このアプローチでは、列挙と削除の順序を完全に制御できるため、FAT32 上での変則的な挙動に引きずられにくくなります。また、FAT32 以外のファイルシステムでも問題なく動作するため、この実装に統一してしまっても構いません。

さらに堅牢にするための工夫

上記のサンプルはシンプルかつ堅牢ですが、実運用では次のような工夫を追加することも検討できます。

一時的なロックに備えたリトライ処理

ウイルス対策ソフトや別プロセスによる一時的なロックが原因で、File.Delete や Directory.Delete が失敗するケースがあります。そういった場合に備えて、軽量なリトライロジックを挟むと安定性が増します。

private static void DeleteWithRetry(string path, bool isDirectory, int retryCount = 3, int delayMs = 100)
{
    for (int i = 0; i < retryCount; i++)
    {
        try
        {
            if (isDirectory)
            {
                Directory.Delete(path, false);
            }
            else
            {
                File.Delete(path);
            }
            return;
        }
        catch (IOException) when (i < retryCount - 1)
        {
            System.Threading.Thread.Sleep(delayMs);
        }
        catch (UnauthorizedAccessException) when (i < retryCount - 1)
        {
            System.Threading.Thread.Sleep(delayMs);
        }
    }

    // リトライしてもダメなら例外をそのまま投げる
    if (isDirectory)
    {
        Directory.Delete(path, false);
    }
    else
    {
        File.Delete(path);
    }
}

このようなヘルパーを挟んでおくと、「たまに誰かが掴んでいて消せない」系のトラブルも減らせます。

ログ出力と進捗表示

大量のファイル/ディレクトリを削除するツールの場合、

  • 削除済みのファイル数
  • 現在削除中のパス
  • 失敗したファイル一覧

などをログへ出力しておくと、問題解析が非常に楽になります。特に今回のようなファイルシステム依存の問題では、「どのパスで」「どの例外が」起きたかが重要な手掛かりになります。

ファイルシステム選択という根本的な対策

アプリ側での回避はもちろん重要ですが、根本的には「どのファイルシステムを使うか」が安定性に大きく影響します。

ファイルシステム特徴本件との相性
FAT32古くからある互換性重視のファイルシステム。Unicode 周りはやや弱い。非 Latin-1 文字を多用するなら非推奨
exFATフラッシュメディア向けに設計された新しめのファイルシステム。Unicode 対応も比較的良好。FAT32 より安全。可能ならこちらを推奨。
NTFSWindows 標準の高機能ファイルシステム。アクセス制御やジャーナリングなどもサポート。サーバー/内蔵ディスクなら基本的にこれ。非 Latin-1 文字との相性も良い。

もし運用上許されるなら、FAT32 から exFAT / NTFS への乗り換えを検討することを強くおすすめします。特に多言語ファイル名を前提としたアプリでは、ファイルシステムの選択そのものが重要な設計要素になります。

確認用チェックリスト

自分の環境が本記事のケースに該当するかを確認するために、以下のチェックリストを活用してください。

  • 削除対象のドライブが FAT32 でフォーマットされている
  • 問題のフォルダー/ファイル名に ヘブライ語などの非 Latin-1 文字が含まれている
  • 削除に new DirectoryInfo(path).Delete(true) を使用している
  • 同じコードを NTFS / exFAT 上で試すとエラーが発生しない
  • Directory.EnumerateFiles / Directory.EnumerateDirectories を使った自前の再帰削除に切り替えるとエラーが再現しない

これらがすべて当てはまる場合、本記事で紹介した FAT32 × 非 Latin-1 × Delete(true) の問題に遭遇している可能性が高いと考えられます。

よくある疑問と回答

Q. NTFS ではまったく同じコードなのに問題が出ません。なぜですか?

A. NTFS は Unicode を前提としたファイルシステムであり、Windows の内部実装との相性も良いため、DirectoryInfo.Delete(true) 内部の列挙・削除処理で文字コードの問題が表面化しにくいと考えられます。FAT32 では古い設計や互換性維持のために、マルチバイトコードページに依存する経路が残っており、その差が今回のような事象として現れます。

Q. MAUI アプリ特有の問題ですか?

A. いいえ、MAUI 特有の問題ではありません。System.IO 名前空間の API(DirectoryInfo.Delete など)の挙動に関わるため、コンソールアプリや WPF、WinForms など、.NET 上のアプリケーション全般で同じ条件なら発生し得ます。

Q. BitLocker を解除すれば直りますか?

A. BitLocker は直接の原因ではないため、解除しても本質的な解決にはなりません。重要なのは FAT32 上の Unicode 名の扱いであり、BitLocker の有無にかかわらず同じ問題が発生し得ます。

Q. Directory.Delete(path, true) ではなく DirectoryInfo.Delete(true) を使うことに問題がありますか?

A. 本質的な問題は 「Delete(true) による自動再帰削除」の部分であり、Directory.Delete と DirectoryInfo.Delete のどちらかという違いではありません。どちらにしても、FAT32 では 自前で列挙して後行順に削除するパターンを採用した方が安全です。

Q. 非 Latin-1 文字を使わなければ安全ですか?

A. 少なくとも本記事のケースでは、ヘブライ語など非 Latin-1 文字を含むかどうかが再現条件の一つとなっています。とはいえ、将来的に別の言語や特殊文字で同様の問題が出ない保証もありません。「Delete(true) に頼らず、自前の再帰削除を行う」という方針にしておくことで、将来的なリスクも抑えられます。

まとめ:FAT32 上では Delete(true) からの卒業を

本記事で取り上げたポイントをまとめると、次のようになります。

  • FAT32 上で、ヘブライ語などの 非 Latin-1 文字を含むフォルダーを DirectoryInfo.Delete(true) で削除すると、断続的に IOException(「Unicode 文字を対象のマルチバイト コードページにマップできません」)が発生し得る
  • 原因は、FAT32 上での列挙処理と文字コード変換に絡むランタイム/Win32 の挙動であり、BitLocker は無関係
  • 回避策としては、Directory.EnumerateFiles / Directory.EnumerateDirectories を使い、自前で列挙しながら後行順(中身 → 親)の削除を行うのが有効
  • 読み取り専用属性の解除やリトライ処理を組み合わせることで、より堅牢な削除ロジックを構築できる
  • 可能であれば、FAT32 ではなく exFAT / NTFS を利用することで、同種の問題に遭遇しにくくなる

FAT32 は今でも USB メモリなどでよく使われますが、Unicode リッチな時代にはなかなか扱いが難しい側面があります。C#/.NET でマルチリンガルなファイル名を扱うアプリを作る場合は、ファイルシステムの特性と削除ロジックの設計を意識しておくことで、思わぬ IOException に悩まされる可能性をぐっと減らせるはずです。

この記事を書いた人

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

コメント

コメントする

目次