C# Tips | ファイル・ディレクトリ操作:パス正規化

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

はじめに 「パス正規化」って何をすること?

「パス正規化」という言葉、ちょっと堅いですよね。
でもやっていることはシンプルで、「バラバラな書き方のパスを、同じルールで整える」ことです。

同じ場所を指しているのに、文字列としては違うパスがたくさんあります。

C:\data\logs\..\logs\app.log
C:\data\logs\.\app.log
C:\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.log
C:\data\logs\app.log
C:\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.CombinePath.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.GetFullPathPath.Combine は、実行環境に応じた区切り文字を自動で使ってくれます。

つまり、正規化のときに「自分で /\ に置き換える」ようなことは、基本的には不要です。
むしろ、Path.DirectorySeparatorCharPath.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.CombineGetFullPath を組み合わせたユーティリティを作る。
パス比較・ログ出力・設定保存の前に「一度正規化してから扱う」という習慣を付ける。
フォルダパスの末尾の区切りは、プロジェクトとしてルールを決めて、ユーティリティで強制する。

ここまで押さえておけば、「パス周りでよく分からない不具合に時間を取られる」ことはかなり減ります。

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