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

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

はじめに なぜ「パス結合」が業務でめちゃくちゃ大事なのか

ファイルやディレクトリを扱うコードを書くとき、ほぼ必ず出てくるのが「パスの結合」です。
たとえば「ログフォルダのパス」と「ファイル名」からフルパスを作る、「ルートフォルダ」と「サブフォルダ」と「ファイル名」をつなげる、といった処理です。

ここで安易に basePath + "\\" + fileName のような文字列連結をしてしまうと、区切り文字の重複や不足、OS 依存、絶対パスが混ざったときの挙動などで、地味にバグります。
C# には、これを安全にやるための Path.Combine という強力なメソッドが用意されています。

ここでは、プログラミング初心者向けに、「なぜ文字列連結ではなく Path.Combine を使うべきか」「どういう挙動をするのか」「どこでハマりやすいのか」を、例題を交えながら丁寧に解説していきます。


基本の API Path.Combine の使い方

最もシンプルな 2 つのパス結合

まずは一番シンプルな例から見てみましょう。

using System;
using System.IO;

class Program
{
    static void Main()
    {
        string basePath = @"C:\logs";
        string fileName = "app.log";

        string fullPath = Path.Combine(basePath, fileName);

        Console.WriteLine(fullPath); // C:\logs\app.log
    }
}
C#

Path.Combine は、「パスの区切り文字(\)をいい感じに補ってくれる」メソッドです。
basePath の末尾に \ が付いていてもいなくても、結果は同じになります。

string p1 = Path.Combine(@"C:\logs", "app.log");     // C:\logs\app.log
string p2 = Path.Combine(@"C:\logs\", "app.log");    // C:\logs\app.log
C#

この「末尾の \ を気にしなくていい」というのが、業務コードではかなり効いてきます。
呼び出し側が「\ 付きかどうか」を意識しなくて済むので、バグの入り込む余地が減ります。

3 つ以上のパスを結合する

Path.Combine は、2 つだけでなく、3 つ、4 つ、配列でも結合できます。

string path1 = Path.Combine(@"C:\data", "import", "2025");
Console.WriteLine(path1); // C:\data\import\2025

string path2 = Path.Combine(
    @"C:\data",
    "import",
    "2025",
    "01");

Console.WriteLine(path2); // C:\data\import\2025\01

string[] segments = { @"C:\data", "import", "2025", "01", "sales.csv" };
string path3 = Path.Combine(segments);

Console.WriteLine(path3); // C:\data\import\2025\01\sales.csv
C#

「ルート」「サブフォルダ」「さらにサブフォルダ」「ファイル名」といった構造を、そのまま引数の並びで表現できるので、コードの意図が読みやすくなります。


文字列連結と Path.Combine の違い

文字列連結で起こりがちなバグ

よくあるダメな例をあえて書いてみます。

string basePath = @"C:\logs";
string fileName = "app.log";

string fullPath = basePath + "\\" + fileName;
C#

一見問題なさそうですが、次のようなケースで崩れます。

string basePath1 = @"C:\logs";
string basePath2 = @"C:\logs\";

string f1 = basePath1 + "\\" + "app.log"; // C:\logs\\app.log
string f2 = basePath2 + "\\" + "app.log"; // C:\logs\\\app.log
C#

区切り文字が二重、三重になってしまいます。
Windows はある程度許容してくれますが、見た目も悪いし、パス比較などをするときに余計な差分になります。

逆に、区切りを付け忘れるとこうなります。

string basePath = @"C:\logs";
string fullPath = basePath + "app.log"; // C:\logsapp.log
C#

これは完全に別のパスです。
こういう「人間のうっかり」を防ぐために、Path.Combine を使う価値があります。

Path.Combine がやってくれること

Path.Combine は、次のようなことを自動でやってくれます。

  • 末尾・先頭の区切り文字を見て、必要な分だけ \ を挿入する
  • OS に応じた区切り文字(Windows なら \)を使う
  • 2 個目以降に「ルート付きパス」が来た場合の扱いを決めてくれる(後述)

つまり、「パスの区切りを自分で考えない」ためのメソッドです。
パスは文字列ですが、「ただの文字列」として扱うと痛い目を見る、という感覚を持っておくといいです。


重要ポイント 2 個目以降に「絶対パス」が来たときの挙動

ここが初心者が一番ハマりやすいところなので、しっかり深掘りします。

2 個目以降にルート付きパスが来ると、前は無視される

Path.Combine の仕様として、「最初の引数以外にルート付きパス(絶対パス)が来た場合、それ以前の部分は無視される」というルールがあります。

例を見てみましょう。

string p = Path.Combine(
    @"C:\base",
    @"D:\other",
    "file.txt");

Console.WriteLine(p); // D:\other\file.txt
C#

C:\base は完全に無視されて、結果は D:\other\file.txt になります。
「全部つながる」と思っていると、ここで盛大にバグります。

もう一つ。

string p = Path.Combine(
    @"C:\base",
    @"\absolute\path",
    "file.txt");

Console.WriteLine(p); // \absolute\path\file.txt
C#

この場合も、C:\base は無視されて、結果は \absolute\path\file.txt になります。

つまり、「2 個目以降に絶対パスを渡すと、そこから先が新しいルートとして扱われる」ということです。

なぜこの仕様が危険になりうるのか

例えば、「ユーザー入力のパス」をそのまま Path.Combine に渡すようなコードを書いたとします。

string baseDir = @"C:\safe\root";
string userInput = textBoxPath.Text; // ユーザーが入力

string fullPath = Path.Combine(baseDir, userInput);
C#

ユーザーが ..\..\Windows\system32 のような相対パスを入れてくるだけならまだしも、
D:\ から始まる絶対パスを入れてきた場合、baseDir は完全に無視されます。

「必ず C:\safe\root 配下にしかアクセスさせないつもりだった」のに、
実際にはどこにでもアクセスできてしまう、というセキュリティ的な問題につながります。

実務では、「2 個目以降に絶対パスが来ないようにバリデーションする」「絶対パスが来たらエラーにする」といった対策を入れることが多いです。


実務で使えるパス結合ユーティリティ

「絶対パスを許さない」安全な Combine

業務ユーティリティとしては、「2 個目以降に絶対パスが来たら例外にする」ラッパーを用意しておくと安心です。

using System;
using System.IO;
using System.Linq;

public static class SafePath
{
    public static string Combine(string basePath, params string[] segments)
    {
        if (string.IsNullOrWhiteSpace(basePath))
        {
            throw new ArgumentException("basePath が空です。", nameof(basePath));
        }

        if (segments == null || segments.Length == 0)
        {
            return basePath;
        }

        foreach (string seg in segments)
        {
            if (string.IsNullOrEmpty(seg))
            {
                continue;
            }

            if (Path.IsPathRooted(seg))
            {
                throw new ArgumentException(
                    $"絶対パスは結合できません: {seg}",
                    nameof(segments));
            }

            basePath = Path.Combine(basePath, seg);
        }

        return basePath;
    }
}
C#

ここでの重要ポイントは、Path.IsPathRooted を使って「絶対パスかどうか」を判定していることです。
絶対パスが混ざっていたら例外を投げることで、「気づかないうちに basePath が無視される」事故を防ぎます。

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

class Program
{
    static void Main()
    {
        string root = @"C:\data";

        string p1 = SafePath.Combine(root, "import", "2025", "01");
        Console.WriteLine(p1); // C:\data\import\2025\01

        try
        {
            string p2 = SafePath.Combine(root, @"D:\other", "file.txt");
        }
        catch (ArgumentException ex)
        {
            Console.WriteLine("エラー: " + ex.Message);
        }
    }
}
C#

こうしておくと、「このユーティリティを通している限り、必ず root 配下のパスになる」という前提でコードを書けます。

「末尾に必ずディレクトリ区切りを付ける」ユーティリティ

フォルダパスを扱うとき、「末尾に \ が付いているかどうか」を気にしたくない場面も多いです。
その場合は、Path.CombinePath.DirectorySeparatorChar を組み合わせたユーティリティを用意しておくと便利です。

public static class PathEx
{
    public static string EnsureTrailingSeparator(string directoryPath)
    {
        if (string.IsNullOrWhiteSpace(directoryPath))
        {
            throw new ArgumentException("ディレクトリパスが空です。", nameof(directoryPath));
        }

        directoryPath = Path.GetFullPath(directoryPath);

        char sep = Path.DirectorySeparatorChar;

        if (!directoryPath.EndsWith(sep.ToString()))
        {
            directoryPath += sep;
        }

        return directoryPath;
    }
}
C#

これを使うと、次のように書けます。

string dir1 = PathEx.EnsureTrailingSeparator(@"C:\data");
string dir2 = PathEx.EnsureTrailingSeparator(@"C:\data\");

Console.WriteLine(dir1); // C:\data\
Console.WriteLine(dir2); // C:\data\
C#

このように、「フォルダパスは必ず末尾に区切り付き」というルールを決めておくと、
後続の処理(相対パス結合など)が書きやすくなります。


パス結合とプラットフォーム依存の話

区切り文字は OS によって違う

Windows では \、Unix 系(Linux, macOS)では / がパスの区切り文字です。
C# の Path.Combine は、実行環境に応じた区切り文字を自動で使ってくれます。

つまり、次のように書いても、Windows なら \、Linux なら / で結合されます。

string path = Path.Combine("data", "import", "2025");
Console.WriteLine(path); // 実行環境に応じた区切りで表示される
C#

業務で Windows しか想定していないとしても、「区切り文字を自分で書かない」習慣を付けておくと、
将来の移植やテスト環境の違いに強くなります。

Path.DirectorySeparatorChar を意識する場面

自分でパス文字列を加工するときは、'\\' を直書きするのではなく、
Path.DirectorySeparatorChar を使うと、OS に依存しないコードになります。

char sep = Path.DirectorySeparatorChar;
string path = "data" + sep + "import" + sep + "2025";
C#

とはいえ、基本的には「結合は全部 Path.Combine に任せる」が正解で、
自分で区切り文字を扱うのは最小限にしたほうが安全です。


例外とエラー処理を意識したパス結合

どんなときに問題が起こるか

Path.Combine 自体は、単に文字列を結合するだけなので、
それ単体で例外を投げることはあまり多くありません。

ただし、結合結果を Path.GetFullPath に渡したり、
実際に File.ExistsDirectory.CreateDirectory に渡したりするときに、次のような問題が表面化します。

パスが OS の制限より長すぎる。
パスに不正な文字が含まれている。
意図しない絶対パスが混ざっていて、想定外の場所を指している。

特に 3 つ目は、「Combine の仕様を知らないと気づきにくい」ので要注意です。

呼び出し側での扱い方の例

using System;
using System.IO;

class Program
{
    static void Main()
    {
        string root = @"C:\data";
        string sub = "import";
        string file = "sales.csv";

        try
        {
            string path = SafePath.Combine(root, sub, file);

            Console.WriteLine("結合結果: " + path);

            if (!File.Exists(path))
            {
                Console.WriteLine("ファイルが存在しません。");
            }
        }
        catch (ArgumentException ex)
        {
            Console.WriteLine("パスの指定が不正です: " + ex.Message);
        }
        catch (PathTooLongException ex)
        {
            Console.WriteLine("パスが長すぎます: " + ex.Message);
        }
        catch (Exception ex)
        {
            Console.WriteLine("想定外のエラー: " + ex.Message);
        }
    }
}
C#

実務では、「どの basePath とどのセグメントを結合しようとして失敗したか」をログに残しておくと、
後から原因を追いやすくなります。


まとめ 実務で使える「パス結合」ユーティリティの考え方

パス結合は、「ファイル・ディレクトリ操作のすべての入り口」と言っていいくらい、頻繁に出てきます。
だからこそ、「なんとなく文字列連結」で済ませるのではなく、きちんと設計しておく価値があります。

押さえておきたいポイントを整理すると、こうなります。

Path.Combine を使えば、末尾の \ を気にせず安全に結合できる。
2 個目以降に絶対パス(ルート付きパス)が来ると、それ以前は無視される仕様を必ず理解しておく。
ユーザー入力など、信頼できない文字列をそのまま Combine しない。必要なら Path.IsPathRooted でチェックする。
「絶対パスを許さない SafePath.Combine」のようなラッパーを用意して、ルート配下からはみ出さない設計にする。
区切り文字は自分で書かず、基本は Path.Combine に任せる。どうしても必要なら Path.DirectorySeparatorChar を使う。

ここまで押さえておけば、「パス結合まわりでハマる時間」はかなり減ります。

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