てくメモ

trivial な notes

【C#】ReadOnlySpan<char>.Split をつくったけれど、反省があった話


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


ReadOnlySpan<char>ってSplitが無いんだ、と思ったことがあるのは自分だけではないはず。
この記事は、① それをつくってみた、② けれどそれが自分が欲しかったものと違って反省があった、という内容。


まずは①。

区切り文字で区切られた複数のReadOnlySpan<char>を取得ということで、イテレーターを考えた。

public static class ReadOnlySpanCharExtentions
{
    public static SplitSpan Split(this ReadOnlySpan<char> span, char separator) => new(span, separator);

    public readonly ref struct SplitSpan
    {
        private readonly ReadOnlySpan<char> span;
        private readonly char separator;

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

        public ReadOnlySpanCharSplitEnumerator GetEnumerator() => new(span, separator);
    }

    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 ReadOnlySpan<char> Current => span.Slice(start, length);

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

            if (span.Length < start) return false;

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

            return true;
        }
    }
}

拡張メソッドReadOnlySpan<char>.Split(char)GetEnumeratorを持つ専用の構造体を取得でき、それを foreach で列挙できる。

foreach はGetEnumeratorで取得できる型がCurrentMoveNextを持っていればよい。(パターンベース)

また、型が ref 構造体であることによって、ref 構造体 = ReadOnlySpanをフィールドに持つことができる。


以下のような感じで、BenchmarkDotNet で確認してみる。(Config 等は略)

// 「、」のあとに半角空白アリ
private readonly string str = "あのイーハトーヴォのすきとおった風、 夏でも底に冷たさをもつ青いそら、 うつくしい森で飾られたモリーオ市、 郊外のぎらぎらひかる草の波";

[Benchmark]
public void StringSplit()
{
    var split = str.Split('、', StringSplitOptions.TrimEntries);
    var success = false;
    foreach(var item in split)
    {
        if(item == "郊外のぎらぎらひかる草の波") success = true;
    }

    if (success is false) throw new InvalidOperationException();
}

[Benchmark]
public void SpanSplit()
{
    var split = str.AsSpan().Split('、');
    var success = false;
    foreach (var item in split)
    {
        if (item.Trim().SequenceEqual("郊外のぎらぎらひかる草の波")) success = true;
    }

    if (success is false) throw new InvalidOperationException();
}
      Method |     Mean |    Error |  StdDev |   Gen0 | Allocated |
------------ |---------:|---------:|--------:|-------:|----------:|
 StringSplit | 189.1 ns | 19.73 ns | 1.08 ns | 0.0865 |     272 B |
   SpanSplit | 107.0 ns | 15.05 ns | 0.82 ns |      - |         - |

ゼロアロケーションで気持ちいい😊


しかし、――ということで次の②。反省があった話。

使ってみるまで気付かなかった(バカ)のだけれど、列挙を必要とすることが自分のSplitを使いたい場面と合っていなかった。

具体的には、本来欲しかったのは次のような使い心地。

// Deconstruct とかも入れたあくまで概念的な例
// ReadOnlySpan<char> span;
var (n, name, param) = span.Split(',');

イテレーターでなくリストでの取得が必要だった。


そして、リストとなると汎用的にするよりケースに合わせたほうがよさそうだなぁ、といったことなどを考え、ついてはきちんと考えてから手を動かす方がよかったと反省しましたというお話。


追記:リベンジ記事
aneuf.hatenablog.com