てくメモ

trivial な notes

【C#】ReadOnlySpan<char>.Split リベンジ


【追記】
.NET 8 ではReadOnlySpan<char>.Split(Span<Range>, char, StringSplitOptions)とできる拡張メソッドが追加され、この記事でやりたかったようなことが標準で行えるようになっています。


【C#】ReadOnlySpan<char>.Split をつくったけれど、反省があった話 - てくメモ
上記記事で、ReadOnlySpan<char>.Splitイテレーター的に実装してみたけど、それだとvar (a, b, c) = span.Split(',')みたいに使えなくて、それをやるならリストを返さなきゃいけなくてあんまり、、、という感じで反省、と〆た。

しかし、RangeSpanに詰めるのが妥協点にできるな、と考えたのがこの記事。


具体的には、セパレーターで区切るイテレーターに、カレントのRangeを返すメソッド(以下のコードではCurrentRange)を用意する。

public ref struct ReadOnlySpanCharSplitEnumerator
{
    private int start = 0;
    private int length = -1;
    private readonly ReadOnlySpan<char> span;
    private readonly char separator;

    internal ReadOnlySpanCharSplitEnumerator(ReadOnlySpan<char> span, char separator)
    {
        this.span = span;
        this.separator = separator;
    }

    public ReadOnlySpanCharSplitEnumerator GetEnumerator() => this;

    public ReadOnlySpan<char> Current => span.Slice(start, length);

    public Range CurrentRange => start..(start + length);

    public bool MoveNext()
    {
        start = start + length + 1;

        if (span.Length < start) return false;

        var separatorPosition = span[start..].IndexOf(separator);
        length = 0 < separatorPosition
            ? separatorPosition
            : span.Length - start;

        return true;
    }
}

そして、RangeSpanに詰めるメソッドを用意。

public static void Split(scoped ReadOnlySpan<char> chars, scoped Span<Range> resultBuffer, out int written, char separator)
{
    ReadOnlySpanCharSplitEnumerator e = new(chars, separator);

    for (written = 0; written < resultBuffer.Length; written++)
    {
        if (e.MoveNext() is false) break;

        resultBuffer[written] = e.CurrentRange;
    }
}

利便のために拡張メソッドも用意。

public static Span<Range> Split(this ReadOnlySpan<char> span, Span<Range> resultBuffer, out int written, char separator)
{
    SpanUtil.Split(span, resultBuffer, out written, separator);
    return resultBuffer; // 受けたバッファの span を返すのはよくないけど、今回は気持ちよさのためにこれで
}


以下のように使う。

private string[] lines = new string[]
{
    "28,aa,128",
    "523,bb,255",
    "1024,cccc,-2",
    "2000,d,10000"
};

public (int, string, int)[] SpanSplit()
{
    (int, string, int)[] result = new (int, string, int)[lines.Length];
    Span<Range> ranges = stackalloc Range[3];

    for (int i = 0; i < lines.Length; i++)
    {
        var line = lines[i].AsSpan();
        var (index, name, value) = line.Split(ranges, out var _, ',');
        result[i] = (int.Parse(line[index]), new(line[name]), int.Parse(line[value]));
    }

    return result;
}

型がRangeとはいえ、分解で受けることができた。

また副次的に、分解で受けたRange変数でのスライシングが正規表現の名前付きキャプチャっぽくなり、インデックスで扱うString.Splitよりリーダビリティが高いと言えなくもない気がする。


反省というところから妥協できるところまでもっていけて満足😊