C#レコードクラスで作るファイルパス管理のサンプルコード

C# コンピュータ
C#

C#のレコードクラスを試してみました。

サンプルコード

ファイル名:FileSystemPath.cs

public record class FileSystemPath
{
    public string Path { get; }
    private static readonly char[] InvalidChars = { '"', '<', '>', '|', '\0' };
    public FileSystemPath(string path)
    {
        if (string.IsNullOrWhiteSpace(path))
        {
            throw new ArgumentException("パスが空白または null です。", nameof(path));
        }
        // 無効な文字チェック
        if (path.IndexOfAny(InvalidChars) >= 0)
        {
            throw new ArgumentException($"パスに無効な文字が含まれています。{path}", nameof(path));
        }

        Path = path;
    }
    public override string ToString() => Path;

    // 親ディレクトリを取得
    public FileSystemPath GetDirectory()
    {
        string? dir = System.IO.Path.GetDirectoryName(this.Path);
        if (string.IsNullOrEmpty(dir))
        {
            throw new InvalidOperationException("親ディレクトリが取得できません。");
        }

        return new FileSystemPath(dir);
    }
    // ルートディレクトリか?
    public bool IsRoot()
    {
        return string.Equals(
            System.IO.Path.GetFullPath(this.Path),
            System.IO.Path.GetPathRoot(this.Path),
            StringComparison.OrdinalIgnoreCase
        );
    }
    // 拡張子を取得
    public string GetExtension() => System.IO.Path.GetExtension(this.Path);
    // ファイル名を取得
    public string GetFileName() => System.IO.Path.GetFileName(this.Path);
    // 拡張子抜きファイル名を取得
    public string GetBaseName() => System.IO.Path.GetFileNameWithoutExtension(this.Path);
    // ルートディレクトリを取得
    public FileSystemPath GetRoot()
    {
        string rootStr = System.IO.Path.GetPathRoot(this.Path) ?? "";

        return new FileSystemPath(rootStr);
    }

    /*
    // 絶対パスを取得 (文字列操作ではなく、実行環境依存)
    public string GetFullPath() => System.IO.Path.GetFullPath(this.Path);
    */

    // 拡張子があるか?
    public bool HasExtension() => System.IO.Path.HasExtension(this.Path);

    // ルートが含まれているか?
    public bool IsPathRooted() => System.IO.Path.IsPathRooted(this.Path);

    // 連結
    public FileSystemPath Combine(FileSystemPath path)
    {
        string result = System.IO.Path.Combine(this.Path, path.Path);
        return new FileSystemPath(result);
    }
    // 拡張子を追加
    public FileSystemPath WithExtension(string ext)
    {
        if (string.IsNullOrWhiteSpace(ext))
        {
            throw new ArgumentException("拡張子が空です", nameof(ext));
        }

        // 拡張子先頭にドットがなければ追加
        if (!ext.StartsWith("."))
        {
            ext = "." + ext;
        }
        string newPath = (this.ToString() + ext);
        return new FileSystemPath(newPath);
    }
    // 拡張子の置換
    public FileSystemPath ChangeExtension(string newExt)
    {
        if (string.IsNullOrWhiteSpace(newExt))
        {
            throw new ArgumentException("拡張子が空です", nameof(newExt));
        }

        // 拡張子先頭にドットがなければ追加
        if (!newExt.StartsWith("."))
        {
            newExt = "." + newExt;
        }

        string newPath = System.IO.Path.ChangeExtension(this.ToString(), newExt);
        return new FileSystemPath(newPath);
    } 
    // 相対パスを取得
    public FileSystemPath GetRelativePath(FileSystemPath basePath)
    {
        if (basePath is null)
            throw new ArgumentNullException(nameof(basePath));

        string relativePath = System.IO.Path.GetRelativePath(basePath.ToString(), this.ToString());
        return new FileSystemPath(relativePath);
    }   
}

ファイル名:Program.cs

class Program
{
    static void Main()
    {
        try
        {
            var path = new FileSystemPath("");
        }
        catch (Exception e)
        {
            Console.WriteLine($"{e.Message}");
            // パスが空白または null です。 (Parameter 'path')
        }

        try
        {
            var path = new FileSystemPath("<>");
        }
        catch (Exception e)
        {
            Console.WriteLine($"{e.Message}");
            // パスに無効な文字が含まれています。<> (Parameter 'path')
        }

        string samplePath = @"F:/tmp/sample.txt";
        var path1 = new FileSystemPath(samplePath);
        var path2 = new FileSystemPath(samplePath);

        if (path1 == path2)
        {
            Console.WriteLine($"path1とpath2は同じ");
        }

        /*
            path1.Path = @"F:/"; // 変更できない
        */

        // ToString();
        Console.WriteLine($"{path1}");
        // F:/tmp/sample.txt

        path1 = new FileSystemPath(@"D:\hoge");
        if (path1.IsRoot() == true)
        {
            Console.WriteLine(@"{path1}はルートディレクトリ");
        }
        else
        {
            path1 = path1.GetDirectory();
            Console.WriteLine($"親ディレクトリ:{path1}");
            // 親ディレクトリ:D:\
        }

        path2 = new FileSystemPath(@"J:\qqq.txt");
        Console.WriteLine($"拡張子:{path2.GetExtension()}");
        // 拡張子:.txt
        Console.WriteLine($"ファイル名:{path2.GetFileName()}");
        // ファイル名:qqq.txt
        Console.WriteLine($"ファイル名:{path2.GetBaseName()}");
        // ファイル名:qqq
        Console.WriteLine($"ルートディレクトリ:{path2.GetRoot()}");
        // ルートディレクトリ:J:\

        var path3 = new FileSystemPath("abc.txt");
        /*
        Console.WriteLine($"絶対パス:{path3.GetFullPath()}");
        // 絶対パス:J:\csharp\console\FileSystemPath01\abc.txt
        */
        Console.WriteLine($"{path2}拡張子があるか?:{path2.HasExtension()}");
        // J:\qqq.txt拡張子があるか?:True
        Console.WriteLine($"{path2}ルートを含むか?:{path2.IsPathRooted()}");
        // J:\qqq.txtルートを含むか?:True

        var path4 = path1.Combine(path3);
        Console.WriteLine($"連結後:{path4}");
        // 連結後:D:\abc.txt

        var path5 = path4.WithExtension(".bak");
        Console.WriteLine($"拡張子追加:{path5}");
        // 拡張子追加:D:\abc.txt.bak

        var path6 = path4.ChangeExtension("log");
        Console.WriteLine($"拡張子置換:{path6}");
        // 拡張子置換:D:\abc.log

        var path7 = new FileSystemPath(@"D:\foo\bar\sample.txt");
        var path8 = new FileSystemPath(@"D:\foo");
        var path9 = path7.GetRelativePath(path8);
        Console.WriteLine($"相対パス:{path9}");
        // 相対パス:bar\sample.txt
    }
}

解説

C#のレコードクラスは、オブジェクト同士を比較する際に、インスタンスが異なっていても保持している値が同じであれば同一と判定されるのが大きな特徴です。

今回のサンプルコードでいうと、FileSystemPathクラスの内部にある.Pathプロパティがそれにあたります。

さらに、.Pathプロパティは読み取り専用(getのみ)で、コンストラクタで初期値をセットした後は変更できません。
つまり、このクラスは不変(イミュータブル)なオブジェクトとして設計されています。

そのため、ファイルパスを変更・加工したい場合は、元のオブジェクトを変更するのではなく、新しいFileSystemPathオブジェクトを作成することになります。

コメント