C#レコードクラスで作るファイルパス管理のサンプルコード3「メソッドチェーン」

C# コンピュータ
C#

LINQのように.でメソッドをつなげるスタイルを試してみました。

ソースコード

ファイル名:FileSystemPath.cs

public sealed record class FileSystemPath
{
    public string Path { get; init; }

    // コンストラクタ
    public FileSystemPath(string path)
    {
        Path = path;
    }
    // コンストラクタ
    public FileSystemPath(FileSystemPath pathObj)
    {
        Path = pathObj.Path;
    }
    // 文字列変換
    public override string ToString()
    {
        return this.Path;
    }

    // 名前
    public string Name
    {
        get => System.IO.Path.GetFileName(this.Path);
    }
}

ファイル名:FluentFilePath.cs

public sealed record class FluentFilePath
{
    public FileSystemPath InnerPath { get; init; }
    private bool _IsValid = true;

    // コンストラクタ
    public FluentFilePath(string path)
    {
        InnerPath = new FileSystemPath(path);
    }
    // コンストラクタ
    public FluentFilePath(FileSystemPath path) : this(path.Path)
    {
    }
    // Optionへ変換
    public Option<FileSystemPath> ToOption()
    {
        return new Option<FileSystemPath>(new FileSystemPath(this.InnerPath));
    }
    // 文字列へ変換
    public override string ToString()
    {
        return this.InnerPath.Path;
    }

    // ディレクトリが存在するか
    public bool IsDirectory
    {
        get => System.IO.Directory.Exists(this.InnerPath.Path);
    }

    // ディレクトリが有効の場合
    public FluentFilePath EnsureDirectoryExists()
    {
        if (!_IsValid) return this;

        var result = new FluentFilePath(this)
        {
            _IsValid = this.IsDirectory,
        };

        return result;
    }
    // 属性を取得
    public FileAttributes Attributes
    {
        get => System.IO.File.GetAttributes(this.InnerPath.Path);
    }
    // 不可視ファイルか?
    public bool IsHidden
    {
        get
        {
            return (Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
        }
    }
    // 不可視ファイル以外の場合有効
    public FluentFilePath EnsureNotHiddenFile()
    {
        if (!_IsValid) return this;

        var result = new FluentFilePath(this)
        {
            _IsValid = !this.IsHidden,
        };

        return result;
    }
    // システムファイルか?
    public bool IsSystem
    {
        get
        {
            return (Attributes & FileAttributes.System) == FileAttributes.System;
        }
    }
    // システムファイル以外の場合有効
    public FluentFilePath EnsureNotSystemFile()
    {
        if (!_IsValid) return this;

        var result = new FluentFilePath(this)
        {
            _IsValid = !this.IsSystem,
        };

        return result;
    }

    // サブディレクトリの一覧を取得
    public IEnumerable<FluentFilePath> GetSubDirectories()
    {
        // ディレクトリ?
        if (!this.IsDirectory) yield break;

        // サブディレクトリ
        string dir = this.InnerPath.Path;
        foreach(string subDir in System.IO.Directory.GetDirectories(dir))
        {
            var e = new FluentFilePath(subDir);
            if (e.IsSystem || e.IsHidden) continue;

            yield return e;
        }
    }
    // ファイルの一覧を取得
    public IEnumerable<FluentFilePath> GetFiles()
    {
        // ディレクトリ?
        if (!this.IsDirectory) yield break;

        // ファイルの一覧
        string dir = this.InnerPath.Path;
        foreach(string file in System.IO.Directory.GetFiles(dir))
        {
            var e = new FluentFilePath(file);
            if (e.IsSystem || e.IsHidden) continue;

            yield return e;
        }
    }
    // エンティティの一覧
    public IEnumerable<FluentFilePath> GetEntries()
    {
        // ディレクトリ?
        if (!this.IsDirectory) yield break;

        string dir = this.InnerPath.Path;
        
        // サブディレクトリの一覧
        foreach(var subDir in this.GetSubDirectories())
        {
            yield return subDir;
        }
        // ファイルの一覧
        foreach(var file in this.GetFiles())
        {
            yield return file;
        }
    }


}

ファイル名:FluentFileUtities.cs

public static class FluentFileUtities
{
    // 生成
    public static FluentFilePath CreatePath(string path)
    {
        return new FluentFilePath(path);
    }

    // ドライブの一覧
    public static IEnumerable<FluentFilePath> GetDrives()
    {
        foreach(var drive in DriveInfo.GetDrives())
        {
            if (drive.IsReady == false) continue;
            var path = CreatePath(drive.Name.TrimEnd('\\'));
            yield return path;
        }
    }

    // フォルダースタックを取得
    public static List<(string, FluentFilePath)> GetFolderStack(string baseDir)
    {
        string dir = baseDir;
        Stack<string> dirs = [];
        while (!string.IsNullOrEmpty(dir))
        {
            dirs.Push(dir);
            dir = Path.GetDirectoryName(dir) ?? "";
        }
        List<(string, FluentFilePath)> result = [];
        foreach (var subDir in dirs)
        {
            var name = Path.GetFileName(subDir) ?? "";
            if (string.IsNullOrEmpty(name))
            {
                name = subDir.TrimEnd('\\');
                var path = CreatePath(subDir);
                result.Add((name, path));
            }
        }
        return result;
    }

}

ファイル名:Option.cs

public readonly struct Option<T>
{
    private readonly T _value;
    public bool HasValue { get; }

    public Option(T value)
    {
        _value = value;
        HasValue = value is not null;
    }

    public TResult Match<TResult>(Func<T, TResult> Some, Func<TResult> None) =>
        HasValue ? Some(_value) : None();

    public Option<TResult> Map<TResult>(Func<T, TResult> mapper) =>
        HasValue ? new Option<TResult>(mapper(_value)) : new Option<TResult>();
    
    public T Unwrap() => HasValue ? _value : throw new InvalidOperationException("No value");


    public IEnumerable<T> AsEnumerable()
    {
        if (HasValue) yield return _value;
    }
}

ファイル名:Program.cs

class Program
{
    static void Main()
    {
        string path = @"J:\csharp\wpf\FileManager01";

        var list = FluentFileUtities.CreatePath(path)
            .EnsureDirectoryExists()
            .ToOption()
            .AsEnumerable();
        
        foreach(var e in list)
        {
            Console.WriteLine($"{e}");
            // J:\csharp\wpf\FileManager01
        }

        string path2 = @"J:\csharp\wpf\FileManager01\20250329080059.png";
        var list2 = FluentFileUtities.CreatePath(path2)
            .EnsureNotHiddenFile()
            .EnsureNotSystemFile()
            .ToOption()
            .AsEnumerable();
        foreach(var e in list2)
        {
            Console.WriteLine($"{e}");
            // J:\csharp\wpf\FileManager01\20250329080059.png
        }

        var list3 = FluentFileUtities.CreatePath(path)
            .GetEntries();
        foreach(var e in list3)
        {
            Console.WriteLine($"{e}");
            // J:\csharp\wpf\FileManager01\.vscode
            // J:\csharp\wpf\FileManager01\FileManager01.ConsoleApp
            // ~以下略~ サブディレクトリ→ファイルの順番
        }
    }
}

解説

メソッドチェーンは以前からどの用にして作るのか興味があったのですが、メソッドの戻り値に自分自身と同じ型を返しています。
普通のクラスの場合thisを返せば良さそうですが、レコードクラスですので、新規にオブジェクトを生成して返しています。
それと、内部でIsValidプロパティがfalseの場合はスルー、trueの場合処理する用にコードを書くとメソッドチェーンになるようです。

Option<T>へ変換することで、メソッドチェーンでif-thenのような分岐を表現することが出来る定番クラスを組み込みました。
さらにAsEnumerable()IEnumerable<T>へ変換することで、LINQのメソッドにつなげることが出来ます。

Program.csでメソッドチェーンを試すコードを書いてみました。メソッドチェーンする必要があるかどうかは別にして、ディレクトリが存在する場合のみ処理するとか、ファイルの属性が非表示で無いファイルのみ処理するなど、通常if文などの条件分岐が必要なケースですが、foreach()で代替するコードになりました。

コメント