はじめに なぜ「拡張子変更」が業務で役に立つのか
業務システムでは、「処理前は .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.Move は IOException を投げて失敗します。
上書きするかどうかを制御したい場合は、引数でフラグを受け取るようにします。
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.GetExtension、Path.GetFileNameWithoutExtension、Path.ChangeExtension を使って、「拡張子を部品として扱う」こと。File.Move で実際のリネームを行い、存在チェックや上書き可否をユーティリティに閉じ込めること。
フォルダ内の一括変更や再帰的変更をユーティリティ化し、「このフォルダ配下の .tmp を全部 .dat にする」といった業務要件を簡潔に書けるようにすること。
「処理前は .tmp、成功したら本来の拡張子」というパターンなど、拡張子を使った状態管理の考え方を取り入れること。
例外やエラーの原因をログに残し、「どのファイルが、なぜ変えられなかったのか」を後から追えるようにしておくこと。
ここまで押さえておけば、「手作業で名前を変えていた運用」や「中途半端なファイルを他システムが読んでしまう事故」を、かなり減らせます。

