C# Tips | ファイル・ディレクトリ操作:拡張子変更

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

はじめに なぜ「拡張子変更」が業務で役に立つのか

業務システムでは、「処理前は .tmp として保存しておき、処理が成功したら .csv にリネームする」「受信したファイルを .dat から .bak に変えて退避する」「アプリ独自の拡張子を一括で .zip に戻す」といった、「ファイル名はそのまま、中身もそのまま、拡張子だけ変えたい」という場面がよくあります。
このときに使うのが「拡張子変更」です。

拡張子変更は、ファイルの中身を書き換えるわけではなく、「ファイル名(パス)」を変更するだけの操作です。
C# では、Path クラスで拡張子を扱い、File.Move で名前を変える、という組み合わせが基本パターンになります。
ここでは、プログラミング初心者向けに、単体ファイルの拡張子変更から、フォルダ内の一括変更、実務で使えるユーティリティ化まで、丁寧に解説していきます。


基本の考え方 拡張子は「文字列」ではなく「部品」として扱う

Path.GetExtension と Path.ChangeExtension を知る

まず押さえておきたいのは、「拡張子を自分で文字列操作でいじらない」ということです。
たとえば "report.csv""report.bak" にしたいとき、Replace(".csv", ".bak") のように書くと、ファイル名にたまたま .csv が含まれていた場合におかしなことになります。

C# には、拡張子を安全に扱うためのメソッドが用意されています。

using System;
using System.IO;

class Program
{
    static void Main()
    {
        string path = @"C:\data\report.csv";

        string ext  = Path.GetExtension(path);              // ".csv"
        string name = Path.GetFileNameWithoutExtension(path); // "report"

        Console.WriteLine(ext);
        Console.WriteLine(name);
    }
}
C#

Path.GetExtension は「拡張子(先頭のドット込み)」を返し、
Path.GetFileNameWithoutExtension は「拡張子を除いたファイル名」を返します。

さらに便利なのが Path.ChangeExtension です。

string path = @"C:\data\report.csv";

string newPath = Path.ChangeExtension(path, ".bak");
// newPath は "C:\data\report.bak"
C#

このように、「拡張子を変えた新しいパス」を安全に作ってくれます。
この「パスを作る」ことと、「実際にファイル名を変える」ことを分けて考えるのが、拡張子変更の基本です。

拡張子を変える=ファイルをリネームする

拡張子を変えるということは、実際には「ファイル名(パス)を変える」ことです。
C# では、ファイル名の変更は File.Move で行います。

using System;
using System.IO;

class Program
{
    static void Main()
    {
        string oldPath = @"C:\data\report.csv";
        string newPath = @"C:\data\report.bak";

        File.Move(oldPath, newPath);

        Console.WriteLine("ファイル名(拡張子)を変更しました。");
    }
}
C#

ここで重要なのは、「コピーではなく移動(リネーム)である」という点です。
元のファイルは残らず、新しい名前のファイルだけが存在する状態になります。


単体ファイルの拡張子変更ユーティリティ

一番基本的な拡張子変更メソッド

先ほどの考え方を組み合わせて、「指定したファイルの拡張子を変更する」ユーティリティメソッドを作ってみます。

using System;
using System.IO;

public static class FileExtensionUtil
{
    public static string ChangeExtension(string filePath, string newExtension)
    {
        if (!File.Exists(filePath))
        {
            throw new FileNotFoundException($"拡張子を変更したいファイルが見つかりません: {filePath}", filePath);
        }

        if (!newExtension.StartsWith("."))
        {
            newExtension = "." + newExtension;
        }

        string newPath = Path.ChangeExtension(filePath, newExtension);

        File.Move(filePath, newPath);

        return newPath;
    }
}
C#

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

class Program
{
    static void Main()
    {
        string path = @"C:\data\report.tmp";

        string newPath = FileExtensionUtil.ChangeExtension(path, ".csv");

        Console.WriteLine("新しいパス: " + newPath);
    }
}
C#

ここで深掘りしたいポイントがいくつかあります。

まず、「拡張子の先頭にドットが付いていなくても受け付ける」ようにしていることです。
呼び出し側が "csv" と書いても、.csv と書いても動くように、StartsWith(".") で補正しています。

次に、「Path.ChangeExtension で新しいパスを作り、File.Move で実際に名前を変えている」ことです。
この二段構えにすることで、「パスの組み立て」と「ファイル操作」を分けて考えられます。

既に同名ファイルがある場合の扱い

実務では、「変更後の名前のファイルが既に存在している」ことがあります。
その場合、File.MoveIOException を投げて失敗します。

上書きするかどうかを制御したい場合は、引数でフラグを受け取るようにします。

public static class FileExtensionUtil
{
    public static string ChangeExtension(string filePath, string newExtension, bool overwrite)
    {
        if (!File.Exists(filePath))
        {
            throw new FileNotFoundException($"拡張子を変更したいファイルが見つかりません: {filePath}", filePath);
        }

        if (!newExtension.StartsWith("."))
        {
            newExtension = "." + newExtension;
        }

        string newPath = Path.ChangeExtension(filePath, newExtension);

        if (File.Exists(newPath))
        {
            if (!overwrite)
            {
                throw new IOException($"変更後のファイルが既に存在します: {newPath}");
            }

            File.Delete(newPath);
        }

        File.Move(filePath, newPath);

        return newPath;
    }
}
C#

こうしておくと、「上書きしたいときだけ overwrite: true を渡す」という明示的な設計にできます。
業務的には、「処理成功時に .tmp → .csv に変えるが、同名ファイルがあったらエラーにしたい」など、要件に合わせて使い分けられます。


フォルダ内のファイルを一括で拡張子変更する

指定フォルダ直下のファイルをまとめて変更

次に、「フォルダ内の特定の拡張子をまとめて別の拡張子に変える」ユーティリティを考えてみます。
たとえば、「.log を .log.bak に変える」「.dat を .csv に変える」といったケースです。

using System;
using System.IO;

public static class BulkExtensionUtil
{
    public static int ChangeExtensionsInDirectory(
        string directoryPath,
        string fromExtension,
        string toExtension,
        bool overwrite)
    {
        if (!Directory.Exists(directoryPath))
        {
            throw new DirectoryNotFoundException($"対象ディレクトリが見つかりません: {directoryPath}");
        }

        if (!fromExtension.StartsWith("."))
        {
            fromExtension = "." + fromExtension;
        }

        if (!toExtension.StartsWith("."))
        {
            toExtension = "." + toExtension;
        }

        int count = 0;

        foreach (string filePath in Directory.GetFiles(directoryPath))
        {
            if (!string.Equals(Path.GetExtension(filePath), fromExtension, StringComparison.OrdinalIgnoreCase))
            {
                continue;
            }

            string newPath = Path.ChangeExtension(filePath, toExtension);

            if (File.Exists(newPath))
            {
                if (!overwrite)
                {
                    continue;
                }

                File.Delete(newPath);
            }

            File.Move(filePath, newPath);
            count++;
        }

        return count;
    }
}
C#

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

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

        int changed = BulkExtensionUtil.ChangeExtensionsInDirectory(
            dir,
            fromExtension: ".log",
            toExtension: ".log.bak",
            overwrite: true
        );

        Console.WriteLine($"拡張子を変更したファイル数: {changed}");
    }
}
C#

ここでの重要ポイントは、「拡張子の比較に Path.GetExtension を使っている」ことです。
ファイル名全体を文字列で見ているわけではなく、「拡張子部分だけ」を取り出して比較しているので、安全で意図が明確です。

サブフォルダも含めて再帰的に変更する

業務では、「フォルダ配下のサブフォルダも含めて一括で拡張子を変えたい」こともよくあります。
その場合は、再帰的にディレクトリをたどるようにします。

public static class BulkExtensionUtil
{
    public static int ChangeExtensionsRecursive(
        string directoryPath,
        string fromExtension,
        string toExtension,
        bool overwrite)
    {
        if (!Directory.Exists(directoryPath))
        {
            throw new DirectoryNotFoundException($"対象ディレクトリが見つかりません: {directoryPath}");
        }

        if (!fromExtension.StartsWith("."))
        {
            fromExtension = "." + fromExtension;
        }

        if (!toExtension.StartsWith("."))
        {
            toExtension = "." + toExtension;
        }

        int count = 0;
        ChangeExtensionsInternal(directoryPath, fromExtension, toExtension, overwrite, ref count);
        return count;
    }

    private static void ChangeExtensionsInternal(
        string directoryPath,
        string fromExtension,
        string toExtension,
        bool overwrite,
        ref int count)
    {
        foreach (string filePath in Directory.GetFiles(directoryPath))
        {
            if (!string.Equals(Path.GetExtension(filePath), fromExtension, StringComparison.OrdinalIgnoreCase))
            {
                continue;
            }

            string newPath = Path.ChangeExtension(filePath, toExtension);

            if (File.Exists(newPath))
            {
                if (!overwrite)
                {
                    continue;
                }

                File.Delete(newPath);
            }

            File.Move(filePath, newPath);
            count++;
        }

        foreach (string subDir in Directory.GetDirectories(directoryPath))
        {
            ChangeExtensionsInternal(subDir, fromExtension, toExtension, overwrite, ref count);
        }
    }
}
C#

このようにしておくと、「ログフォルダ全体の .log を .log.bak に変える」「受信フォルダ配下の .tmp を .dat に変える」といった処理を簡単に書けます。


実務での典型パターンと注意点

「処理前は .tmp、成功したら本来の拡張子」というパターン

業務でよくあるのが、「書き込み途中のファイルを他のプロセスに見せたくない」という要件です。
この場合、次のような流れで拡張子を使い分けます。

一時的な拡張子(.tmp など)でファイルを書き込む。
書き込みが完全に終わったら、拡張子を .csv など本来のものに変更する。
他のプロセスは「本来の拡張子のファイルだけを見る」ようにする。

このときのコードイメージは次のようになります。

string tempPath = @"C:\data\sales_202501.tmp";
string finalPath = @"C:\data\sales_202501.csv";

File.WriteAllText(tempPath, "ここにCSVの中身が入るイメージ");

FileExtensionUtil.ChangeExtension(tempPath, ".csv", overwrite: true);
C#

ここでのポイントは、「ファイルの中身が完全に書き終わるまで、本来の拡張子を名乗らない」という設計です。
これにより、別プロセスが「中途半端なファイル」を読んでしまう事故を防げます。

「拡張子だけ変えても中身は変わらない」ことを忘れない

拡張子変更は、あくまで「名前を変えるだけ」です。
中身の形式が変わるわけではありません。

たとえば、バイナリデータのファイルを .txt に変えても、テキストファイルになるわけではありません。
業務で拡張子を変えるときは、「中身の形式と拡張子の意味が一致しているか」を必ず意識する必要があります。

拡張子は、「このファイルはこういう形式ですよ」という「ラベル」のようなものです。
ラベルだけ変えても、中身が変わらなければ、読み手側(他システムや人間)が混乱する可能性があります。


例外とエラー処理を意識した拡張子変更

どんな例外が起こり得るか

拡張子変更(=ファイル名変更)では、次のような理由で例外が発生する可能性があります。

ファイルが存在しない。
変更後の名前のファイルが既に存在している。
権限がなくてリネームできない。
別プロセスがファイルをロックしている。
パスが不正、または長すぎる。

呼び出し側での例外処理の例を見てみます。

using System;
using System.IO;

class Program
{
    static void Main()
    {
        string path = @"C:\data\report.tmp";

        try
        {
            string newPath = FileExtensionUtil.ChangeExtension(path, ".csv", overwrite: false);
            Console.WriteLine("拡張子変更に成功しました: " + newPath);
        }
        catch (FileNotFoundException ex)
        {
            Console.WriteLine("ファイルが見つかりません: " + ex.Message);
        }
        catch (IOException ex)
        {
            Console.WriteLine("入出力エラーが発生しました: " + ex.Message);
        }
        catch (UnauthorizedAccessException ex)
        {
            Console.WriteLine("権限エラーが発生しました: " + ex.Message);
        }
        catch (Exception ex)
        {
            Console.WriteLine("想定外のエラーが発生しました: " + ex.Message);
        }
    }
}
C#

実務では、これらのメッセージをログに残しておくことで、「どのファイルの拡張子変更に、どんな理由で失敗したか」を後から追跡できます。


まとめ 実務で使える拡張子変更ユーティリティの考え方

拡張子変更は、一見「ただ名前を変えるだけ」の地味な処理ですが、業務システムでは「状態管理」や「安全なファイル公開」のためにとてもよく使われます。
だからこそ、「文字列置換でごまかす」のではなく、「パス操作」と「ファイル操作」をきちんと分けて設計することが大切です。

Path.GetExtensionPath.GetFileNameWithoutExtensionPath.ChangeExtension を使って、「拡張子を部品として扱う」こと。
File.Move で実際のリネームを行い、存在チェックや上書き可否をユーティリティに閉じ込めること。
フォルダ内の一括変更や再帰的変更をユーティリティ化し、「このフォルダ配下の .tmp を全部 .dat にする」といった業務要件を簡潔に書けるようにすること。
「処理前は .tmp、成功したら本来の拡張子」というパターンなど、拡張子を使った状態管理の考え方を取り入れること。
例外やエラーの原因をログに残し、「どのファイルが、なぜ変えられなかったのか」を後から追えるようにしておくこと。

ここまで押さえておけば、「手作業で名前を変えていた運用」や「中途半端なファイルを他システムが読んでしまう事故」を、かなり減らせます。

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