【.net9-preview7】列挙可能な ReadOnlySpan<char>.Split() が追加されました

# はじめに

.net9-preview7 が公開されました。新機能のひとつに `ReadOnlySpan<char>.Split()` オーバーロードがあるようです。  
[Enumerate over ReadOnlySpan<char>.Split() segments](https://github.com/dotnet/core/blob/main/release-notes/9.0/preview/preview7/libraries.md#enumerate-over-readonlyspancharsplit-segments)

```cs
var span = "Hello, World!".AsSpan();
var index = 0;
foreach (var n in span.Split(','))
{
    switch (index)
    {
        case 0:
            Assert.Equal("Hello", span[n]);
            break;
        case 1:
            Assert.Equal(" World!", span[n]);
            break;
    }
    ++index;
}
```

## 新機能の利点

従来の `Sprit()` と比較して何が違うのかというと、**事前にバッファを確保する必要がなくなった点**です。

```cs
// 従来の方法
var span = "Hello, World!".AsSpan();
var buffer = (stackalloc Range[2]); // バッファの確保が必要
_ = span.Split(buffer, ',');

Assert.Equal("Hello", span[buffer[0]]);
Assert.Equal(" World!", span[buffer[1]]);
```

また列挙パターンになったため、**列挙の途中で処理を中断**することもできます。これは非常に長いテキストをパースするときに役立つかもしれません。

```cs
var span = longLongText.AsSpan();
foreach (var n in span.Split(','))
{
    if (span[n].SequenceEqual("特定のワード"))
        // 処理を中断
        break;
}
```

## パフォーマンス比較

<details>
<summary>テストコード</summary>

```cs
using Xunit;

public class _ReadOnlySpanSplitTest
{
    [Fact]
    static void HowToUse()
    {
        var span = "Hello, World!".AsSpan();
        var index = 0;
        foreach (var n in span.Split(','))
        {
            switch (index)
            {
                case 0:
                    Assert.Equal("Hello", span[n]);
                    break;
                case 1:
                    Assert.Equal(" World!", span[n]);
                    break;
            }
            ++index;
        }
    }

    [Fact]
    static void OlderSplitPattern()
    {
        var span = "Hello, World!".AsSpan();
        var buffer = (stackalloc Range[2]);
        _ = span.Split(buffer, ',');

        Assert.Equal("Hello", span[buffer[0]]);
        Assert.Equal(" World!", span[buffer[1]]);
    }

    static void Example(string longLongText)
    {
        var span = longLongText.AsSpan();
        foreach (var n in span.Split(','))
        {
            if (span[n].SequenceEqual("特定のワード"))
                // 処理を中断
                break;
        }
    }

    public static bool TrySplitIndex<T>(ReadOnlySpan<T> span, ReadOnlySpan<T> separator, out ReadOnlySpan<T> split, scoped ref int index) where T : IEquatable<T>
    {
        if (index < 0)
        {
            split = default;
            return false;
        }
        var span2 = span.Slice(index);
        var index2 = span2.IndexOfAny(separator);
        if (index2 < 0)
        {
            split = span2;
            index = -1;
        }
        else
        {
            split = span2.Slice(0, index2);
            index += index2 + separator.Length;
        }
        return true;
    }

    [Fact]
    static void TrySplitIndexTest()
    {
        var span = "Hello, World!".AsSpan();
        var index = 0;
        ReadOnlySpan<char> split;
        Assert.True(TrySplitIndex(span, ",".AsSpan(), out split, ref index));
        Assert.Equal("Hello", split);
        Assert.True(TrySplitIndex(span, ",".AsSpan(), out split, ref index));
        Assert.Equal(" World!", split);
        Assert.False(TrySplitIndex(span, ",".AsSpan(), out _, ref index));
    }

    static void TrySplitIndexHowToUse()
    {
        var span = "Hello, World!".AsSpan();
        var index = 0;
        while (TrySplitIndex(span, ",".AsSpan(), out var split, ref index))
        {
            // 処理
        }
    }

    static void Performance(Performance p)
    {
        const string text = "a,bb,ccc,dddd,eeeee,ffffff,ggggggg,hhhhhhhhh,iiiiiiiiii,jjjjjjjjjjj";
        p.AddTest("SpanSplitEnumerator", () =>
        {
            var span = text.AsSpan();
            for (int i = 0; i < 10000; ++i)
            {
                var sumLength = 0;
                foreach (var n in span.Split(','))
                    sumLength += span[n].Length;
            }
        });

        p.AddTest("SplitUseBuffer", () =>
        {
            var span = text.AsSpan();
            var buffer = (stackalloc Range[10]);
            for (int i = 0; i < 10000; ++i)
            {
                var sumLength = 0;
                var count = span.Split(buffer, ',');
                var splits = buffer.Slice(0, count);
                foreach (var n in splits)
                    sumLength += span[n].Length;
            }
        });

        p.AddTest("TrySplitIndex", () =>
        {
            var span = text.AsSpan();
            for (int i = 0; i < 10000; ++i)
            {
                var sumLength = 0;
                var index = 0;
                while (TrySplitIndex(span, ",", out var split, ref index))
                    sumLength += split.Length;
            }
        });

        p.AddTest("string.Split", () =>
        {
            for (int i = 0; i < 10000; ++i)
            {
                var sumLength = 0;
                foreach (var n in text.Split(','))
                    sumLength += n.Length;
            }
        });
    }
}

```

</details>

パフォーマンス的には従来の方法と同程度です。`string.Split()` よりパフォーマンスが良いです。

|Test|Score|%|CG0|
|---|---:|---:|---:|
|**SpanSplitEnumerator**|177|100.0%|0|
|SplitUseBuffer|181|102.3%|0|
|TrySplitIndex|249|140.7%|0|
|string.Split|62|35.0%|34|

実行環境: Windows11 x64 .NET Runtime 9.0.0  
**Score** は高いほどパフォーマンスがよいです。  
**GC0** はガベージコレクション回数を表します（少ないほどパフォーマンスがよい）。

- `string.Split` はヒープが発生しパフォーマンスもあまり良くないです。可能なら新しい方法を採用したいところです。
- `TrySplitIndex` は独自実装のメソッドです。なぜか最もパフォーマンスが良いです。

## おまけ `TrySplitIndex`

`Span<T>` の出始めの頃に自前で実装した関数です。列挙パターンではありませんが、ヒープが発生しないようになっています（詳細はテストコードを参照）。

```cs
var span = "Hello, World!".AsSpan();
var index = 0;
while (TrySplitIndex(span, ",".AsSpan(), out var split, ref index))
{
    // 処理
}
```