はじめに 「パス正規化」って何をすること?
「パス正規化」という言葉、ちょっと堅いですよね。
でもやっていることはシンプルで、「バラバラな書き方のパスを、同じルールで整える」ことです。
同じ場所を指しているのに、文字列としては違うパスがたくさんあります。
C:\data\logs\..\logs\app.logC:\data\logs\.\app.logC:\data\logs\\app.log
全部、実質的には「C:\data\logs\app.log」を指しています。
こういう「揺れ」をそのままにしておくと、比較・ログ出力・設定ファイル・キャッシュキーなど、いろんなところで地味に困ります。
そこで、「パスを一度“正規化”してから扱う」という習慣を付けておくと、
業務コードがかなり安定します。
ここでは、C# 初心者向けに、「パス正規化とは何をするのか」「どうやってやるのか」「どこで効いてくるのか」を、例題付きで丁寧に解説します。
パス正規化でやりたいことをざっくり整理する
代表的な「揺れ」のパターン
パスの「揺れ」は、主に次のようなものがあります。
同じ場所なのに、. や .. が含まれている。
区切り文字が重複している(\\ が連続している)。
末尾の \ が付いていたりいなかったりする。
相対パスと絶対パスが混在している。
スラッシュとバックスラッシュが混ざっている(Windows でよくある)。
正規化とは、これらを「一定のルールで整える」ことです。
例えば、「絶対パスにする」「. と .. を解決する」「区切り文字を OS 標準にそろえる」「末尾の区切りは付けない(または必ず付ける)」など、プロジェクトとしてルールを決めておきます。
C# では、このうちかなりの部分を Path.GetFullPath に任せることができます。
一番の基本 Path.GetFullPath で正規化する
相対パスを絶対パスにしつつ、. と .. を解決する
Path.GetFullPath は、「パスの正規化」の中心になるメソッドです。
using System;
using System.IO;
class Program
{
static void Main()
{
string p1 = @"C:\data\logs\..\logs\app.log";
string p2 = @"C:\data\logs\.\app.log";
string p3 = @"C:\data\logs\\app.log";
Console.WriteLine(Path.GetFullPath(p1));
Console.WriteLine(Path.GetFullPath(p2));
Console.WriteLine(Path.GetFullPath(p3));
}
}
C#どれも、出力はだいたいこんな感じになります。
C:\data\logs\app.logC:\data\logs\app.logC:\data\logs\app.log
ここでの重要ポイントは、次の二つです。
. や .. を解決してくれる。
区切りの重複なども、いい感じに整えてくれる。
つまり、「人間が雑に書いたパス」を、「OS が理解しやすいきれいな形」にしてくれるわけです。
相対パスも正規化される(カレントディレクトリ基準)
相対パスを渡した場合も、Path.GetFullPath は「カレントディレクトリ基準」で絶対パスにしてくれます。
string relative = @"logs\..\logs\app.log";
string absolute = Path.GetFullPath(relative);
Console.WriteLine(absolute);
C#例えばカレントディレクトリが C:\Apps\MyTool なら、
結果は C:\Apps\MyTool\logs\app.log のようになります。
「相対か絶対か」「. や .. が含まれているか」を気にせず、
とりあえず GetFullPath を通しておく、というのはかなり強力なパターンです。
実務で使える「パス正規化」ユーティリティ
まずはシンプルな NormalizePath
毎回 Path.GetFullPath を直書きするのではなく、
「パスを受け取って、正規化されたパスを返す」ユーティリティを用意しておくと、コードの意図がはっきりします。
using System;
using System.IO;
public static class PathUtil
{
public static string NormalizePath(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
throw new ArgumentException("パスが空です。", nameof(path));
}
return Path.GetFullPath(path);
}
}
C#使い方の例は次の通りです。
class Program
{
static void Main()
{
string raw = @"C:\data\logs\..\logs\\app.log";
string normalized = PathUtil.NormalizePath(raw);
Console.WriteLine("元のパス : " + raw);
Console.WriteLine("正規化後 : " + normalized);
}
}
C#これだけでも、「パス比較」「ログ出力」「設定ファイルへの保存」などで、かなり扱いやすくなります。
基準ディレクトリを指定して正規化する
「相対パスを、特定の基準ディレクトリから見た絶対パスとして正規化したい」ことも多いです。
その場合は、Path.Combine と Path.GetFullPath を組み合わせます。
public static class PathUtil
{
public static string NormalizePath(string baseDirectory, string path)
{
if (string.IsNullOrWhiteSpace(path))
{
throw new ArgumentException("パスが空です。", nameof(path));
}
if (Path.IsPathRooted(path))
{
return Path.GetFullPath(path);
}
if (string.IsNullOrWhiteSpace(baseDirectory))
{
baseDirectory = Environment.CurrentDirectory;
}
string combined = Path.Combine(baseDirectory, path);
return Path.GetFullPath(combined);
}
}
C#ここでの重要ポイントは、
「絶対パスが来たらそのまま正規化」「相対パスが来たら基準ディレクトリと結合してから正規化」
という二段構えにしていることです。
使い方の例は次の通りです。
class Program
{
static void Main()
{
string baseDir = AppContext.BaseDirectory;
string p1 = @"logs\..\logs\app.log";
string p2 = @"C:\data\logs\app.log";
Console.WriteLine(PathUtil.NormalizePath(baseDir, p1));
Console.WriteLine(PathUtil.NormalizePath(baseDir, p2));
}
}
C#こうしておくと、「設定ファイルに相対パスが書かれていても、絶対パスが書かれていても、とりあえず NormalizePath に通せばいい」という設計にできます。
正規化しておくと何が嬉しいのか
パス比較が正しくできる
正規化していないと、次のような比較が false になってしまいます。
string a = @"C:\data\logs\app.log";
string b = @"C:\data\logs\..\logs\app.log";
Console.WriteLine(a == b); // false
C#でも、正規化してから比較すれば、同じ場所だと分かります。
string na = Path.GetFullPath(a);
string nb = Path.GetFullPath(b);
Console.WriteLine(na == nb); // true(大文字小文字は Windows では無視されることが多い)
C#キャッシュキー、辞書のキー、重複チェックなど、「同じファイルかどうか」を判定したい場面では、
必ず「正規化してから比較する」という癖を付けておくと安全です。
ログや設定ファイルに「きれいなパス」を残せる
ログにパスを出すとき、C:\data\logs\..\logs\app.log のようなパスが出てくると、
人間が見たときに「結局どこ?」となります。
正規化してから出力しておけば、常に「C:\data\logs\app.log」のような形で残るので、
調査がしやすくなります。
設定ファイルに保存するときも同じで、
「一度正規化してから保存する」と決めておくと、
後から読み込んだときに余計な揺れを気にしなくて済みます。
区切り文字や末尾の「\」をどう扱うか
区切り文字は基本的に Path に任せる
Windows では \、Linux では / が区切り文字ですが、
C# の Path.GetFullPath や Path.Combine は、実行環境に応じた区切り文字を自動で使ってくれます。
つまり、正規化のときに「自分で / を \ に置き換える」ようなことは、基本的には不要です。
むしろ、Path.DirectorySeparatorChar や Path.AltDirectorySeparatorChar を意識しすぎるより、
「結合と正規化は Path に任せる」と割り切ったほうがシンプルです。
末尾の区切りをどうするかは「プロジェクトのルール」で決める
フォルダパスの末尾に \ を付けるかどうかは、プロジェクトごとにルールを決めておくとよいです。
例えば、「内部的にはフォルダパスは末尾なしで統一する」と決めたとします。
その場合は、正規化の最後に「末尾の区切りを削る」処理を入れます。
public static string NormalizeDirectoryPath(string path)
{
string full = Path.GetFullPath(path);
full = full.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
return full;
}
C#逆に、「フォルダパスは必ず末尾に区切り付き」と決めるなら、EnsureTrailingSeparator のようなユーティリティを組み合わせます。
どちらにせよ、「ルールを決めて、ユーティリティで強制する」ことが大事です。
人間の記憶力に頼ると、必ず揺れます。
例外とエラー処理を意識したパス正規化
GetFullPath が投げる可能性のある例外
Path.GetFullPath は、次のような場合に例外を投げることがあります。
パスに不正な文字が含まれている。
パスが OS の制限より長すぎる。
ドライブ指定が不正。
業務ユーティリティとしては、「正規化に失敗したらどうするか」を決めておく必要があります。
例外をそのまま上に投げて、呼び出し側でログに残す。
ログだけ出して、処理対象から除外する。
など、要件に応じて選びます。
呼び出し側での扱い方の例
using System;
using System.IO;
class Program
{
static void Main()
{
string raw = @"C:\data\logs\..\logs\\app.log";
try
{
string normalized = PathUtil.NormalizePath(raw);
Console.WriteLine("正規化後: " + normalized);
}
catch (ArgumentException ex)
{
Console.WriteLine("パスが不正です: " + ex.Message);
}
catch (PathTooLongException ex)
{
Console.WriteLine("パスが長すぎます: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("想定外のエラー: " + ex.Message);
}
}
}
C#実務では、「どの元パスを正規化しようとして失敗したか」をログに残しておくと、
後から原因を追いやすくなります。
まとめ 実務で使える「パス正規化」ユーティリティの考え方
パス正規化は、「地味だけど効く」系のテクニックです。
ここをサボると、パス比較・ログ・設定・キャッシュなど、あらゆるところで小さな違いに悩まされます。
押さえておきたいポイントはこうです。
パスの揺れ(.・..・区切り重複・相対/絶対の混在)を、一定ルールで整えるのが「正規化」。Path.GetFullPath は、「相対→絶対」「.・.. の解決」「区切りの整理」を一気にやってくれる強力なメソッド。
基準ディレクトリを指定したいときは、Path.Combine と GetFullPath を組み合わせたユーティリティを作る。
パス比較・ログ出力・設定保存の前に「一度正規化してから扱う」という習慣を付ける。
フォルダパスの末尾の区切りは、プロジェクトとしてルールを決めて、ユーティリティで強制する。
ここまで押さえておけば、「パス周りでよく分からない不具合に時間を取られる」ことはかなり減ります。
