C#でアーカイブ内パスと通常ファイルパスを統一管理する方法(record struct + 拡張メソッド活用)

コンピュータ

アーカイブ内の画像ファイルのパスと普通の画像ファイルパスを同じ変数で扱いたい。

・zipファイル内のパス(の書式)
/path/to/archive.zip|001.jpg
|が区切り文字で前半がアーカイブファイルのパス、後半がアーカイブファイル内のエントリパスに成ります。

・ローカルストレージのパス
/path/to/002.jpg

最初に分割するコードを書いてみます。

class Program
{
    static void Main()
    {
        string zipPathType = @"/path/to/archive.zip|001.jpg";
        string localPathType = @"/path/to/002.jpg";

        string[] paths = [zipPathType, localPathType];

        foreach (var path in paths)
        {
            // 分割
            string[] array = path.Split('|');
            if (array.Length == 2)
            {
                Console.WriteLine($"Archive:{array[0]}");
                Console.WriteLine($"Entry:{array[1]}");
            }
            else if (array.Length == 1)
            {
                Console.WriteLine($"Local:{array[0]}");
            }
            else
            {
                throw new ArgumentException($"書式誤り:{path}");
            }

        }
        // 結果
        // Archive:/path/to/archive.zip
        // Entry:001.jpg
        // Local:/path/to/002.jpg
    }
}

Split(‘|’)で分割し、配列の要素数でzipアーカイブとローカルファイルを判定しています。

次のステップとして分割部分をstaticメソッドにします。

class Program
{
    static (string, string?) Parse(string input)
    {
        // 分割
        var parts = input.Split('|');
        return parts.Length switch
        {
            1 => (parts[0], null),
            2 => (parts[0], parts[1]),
            _ => throw new ArgumentException($"書式誤り:{input}"),
        };
    }
    static void Main()
    {
        string zipPathType = @"/path/to/archive.zip|001.jpg";
        string localPathType = @"/path/to/002.jpg";

        string[] paths = [zipPathType, localPathType];

        foreach (var path in paths)
        {
            (var path1, var path2) = Parse(path);
            if (path2 is not null)
            {
                Console.WriteLine($"Archive:{path1}");
                Console.WriteLine($"Entry:{path2}");
            }
            else
            {
                Console.WriteLine($"Local:{path1}");
            }
        }
        // 結果
        // Archive:/path/to/archive.zip
        // Entry:001.jpg
        // Local:/path/to/002.jpg
    }
}

分割ルーチンをParse()というメソッドに分離しました。戻り値はタプルを使い2つの要素を返しています。
それに合わせて、呼び出し元の分岐ルーチンも要素数から、変数のnullチェックに変更しています。

nullチェックのコードは好みで無いので隠蔽します。

namespace ImageSourceManager;
public record struct ImageSource
{
    public string Archive;
    public string? Entry;

    public ImageSource(string archive, string? entry)
    {
        Archive = archive;
        Entry = entry;
    }

    public bool IsArchive()
    {
        return (Entry is not null);
    }
}
class Program
{
    static ImageSource Parse(string input)
    {
        // 分割
        var parts = input.Split('|');
        return parts.Length switch
        {
            1 => new ImageSource(parts[0], null),
            2 => new ImageSource(parts[0], parts[1]),
            _ => throw new ArgumentException($"書式誤り:{input}"),
        };
    }
    static void Main()
    {
        string zipPathType = @"/path/to/archive.zip|001.jpg";
        string localPathType = @"/path/to/002.jpg";

        string[] paths = [zipPathType, localPathType];

        foreach (var path in paths)
        {
            ImageSource imageSource = Parse(path);
            if (imageSource.IsArchive())
            {
                Console.WriteLine($"Archive:{imageSource.Archive}");
                Console.WriteLine($"Entry:{imageSource.Entry}");
            }
            else
            {
                Console.WriteLine($"Local:{imageSource.Archive}");
            }
        }
        // 結果
        // Archive:/path/to/archive.zip
        // Entry:001.jpg
        // Local:/path/to/002.jpg
    }
}

ちょっと回りくどいですが、戻り値をrecord structにしてnullチェックをそちらに分離しました。

C#のstructはclassと似ていますが、参照ではなく値型で代入でコピーが発生します。
比較的小さな値を使うことに向いているといわれています。

パースとnullチェック部分をさらに分離してみます。

namespace ImageSourceManager;
public record struct ImageSource
{
    public string Archive;
    public string? Entry;

    public ImageSource(string archive, string? entry)
    {
        Archive = archive;
        Entry = entry;
    }
}
public static class ImageSourceEx
{
    public static bool IsArchive(this ImageSource source)
    {
        return (source.Entry is not null);
    }
}

public static class ImageSourceParser
{
    public static ImageSource Parse(this string input)
    {
        // 分割
        var parts = input.Split('|');
        return parts.Length switch
        {
            1 => new ImageSource(parts[0], null),
            2 => new ImageSource(parts[0], parts[1]),
            _ => throw new ArgumentException($"書式誤り:{input}"),
        };
    }
}

class Program
{
    static void Main()
    {
        string zipPathType = @"/path/to/archive.zip|001.jpg";
        string localPathType = @"/path/to/002.jpg";

        string[] paths = [zipPathType, localPathType];

        foreach (var path in paths)
        {
            ImageSource imageSource = path.Parse();
            if (imageSource.IsArchive())
            {
                Console.WriteLine($"Archive:{imageSource.Archive}");
                Console.WriteLine($"Entry:{imageSource.Entry}");
            }
            else
            {
                Console.WriteLine($"Local:{imageSource.Archive}");
            }
        }
        // 結果
        // Archive:/path/to/archive.zip
        // Entry:001.jpg
        // Local:/path/to/002.jpg
    }
}

拡張メソッドを使ってパース部分とnullチェックを分離しています。拡張メソッドは、staticメソッドの糖衣構文(syntactic sugar)ですので、見た目メソッドですが出来ることはstaticメソッドのそれとなります。

record structは純粋にデータ型の定義だけとし、振る舞いは拡張メソッドに分離することが目的となります。

コードが長くなってきたので短いコードにリファクタリング。

namespace ImageSourceManager;
public record struct ImageSource(string Archive, string? Entry);
public static class ImageSourceExtensions
{
    public static ImageSource ToImageSource(this string input)
    {
        // 分割
        var parts = input.Split('|', 2);
        return parts.Length switch
        {
            1 => new (parts[0], null),
            2 => new (parts[0], parts[1]),
            _ => throw new ArgumentException($"書式誤り:{input}"),
        };
    }
    public static bool IsArchive(this ImageSource source)
         => (source.Entry is not null);
    public static string ToDisplayPath(this ImageSource s)
        => IsArchive(s) ? $"{s.Archive}|{s.Entry}" : s.Archive;
}

class Program
{
    static void Main()
    {
        string zipPathType = @"/path/to/archive.zip|001.jpg";
        string localPathType = @"/path/to/002.jpg";

        string[] paths = [zipPathType, localPathType];

        foreach (var path in paths)
        {
            ImageSource imageSource = path.ToImageSource();
            if (imageSource.IsArchive())
            {
                Console.WriteLine($"Archive:{imageSource.Archive}");
                Console.WriteLine($"Entry:{imageSource.Entry}");
            }
            else
            {
                Console.WriteLine($"Local:{imageSource.ToDisplayPath()}");
            }
        }
        // 結果
        // Archive:/path/to/archive.zip
        // Entry:001.jpg
        // Local:/path/to/002.jpg
    }
}

Parse()だと拡張メソッドの名前としては曖昧な感じがするのでToImageSource()にしました。

コメント