Equals() をオーバーライドした読み取り専用メモリ

# 概要

[レコード型](https://learn.microsoft.com/ja-jp/dotnet/csharp/fundamentals/types/records#examples)を使用していると、シーケンスについて**構造が同じか**（参照が同じかではなく）で同値性を評価してほしいときがあります。


読み取り専用のシーケンスを表す型は、フレームワークにいくつかあります。これらは同値性の評価を参照が同じかで評価するため、望ましい挙動になりません。

- `ReadOnlyMemory<T>`: 読み取り専用メモリ
- `System.Collections.Immutable.ImmutableArray<T>`: 変更不可のリスト
- `System.Collections.Generic.IReadOnlyList<T>`: 読み取り専用リストのインターフェイス
- `System.ArraySegment<T>`: 使っているのを見たことがない・・・

構造を評価するものには `System.Collections.IStructuralEquatable` がありますが、これに依存したコードを見たことがないです・・・。個人的にはあまり積極的に使うものでもないのかなと思います。ジェネリック版もないですし。

以上により、シーケンスの同値性を構造で評価する型を自前で実装します。

```cs
file readonly record struct StructuralReadOnlyMemory<T>(ReadOnlyMemory<T> Memory)
{
    // 他のメンバーは省略
    public bool Equals(StructuralReadOnlyMemory<T> other) => this.Span.SequenceEqual(other.Span);

    public override int GetHashCode()
    {
        var span = this.Span.Slice(Math.Max(0, this.Length - 10));
        var hash = new HashCode();
        foreach (var n in span)
            hash.Add(n);

        return hash.ToHashCode();
    }

    [EditorBrowsable(EditorBrowsableState.Never)]
    public ReadOnlySpan<T>.Enumerator GetEnumerator() => this.Memory.Span.GetEnumerator();
}

file record struct CardCase(string Name, StructuralReadOnlyMemory<int> Cards);

void 使い方()
{
    var a = new CardCase("Cards", [1, 2, 3]);
    var b = new CardCase("Cards", [1, 2, 3]);
    Assert.Equal(a, b);
}
```

- `GetHashCode()` ではシーケンス要素が 10 以上あるとき、後ろの 10 しか評価していません。これは計算量を O(1) にする工夫です。
- `GetEnumerator()` は[ユーザー定義のコレクション式](https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/operators/collection-expressions#collection-builder)のために必要です。メソッドに属性を付けてメンバー表示されないようにしています。`ReadOnlySpan<T>` 経由での列挙と比較してパフォーマンスが悪いので、使い方に注意が必要です。このメソッドは呼ばず、`StructuralReadOnlyMemoryRecord<T>.Span` を使用してください。

| Test                                    |  Score |      % |  CG0 |
| --------------------------------------- | -----: | -----: | ---: |
| foeach(Span)                            | 26,631 | 100.0% |    0 |
| foreach(StructuralReadOnlyMemoryRecord) | 14,114 |  53.0% |    0 |

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

## コード

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

```cs
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Xunit;

[CollectionBuilder(typeof(StructuralReadOnlyMemoryBuilder), nameof(StructuralReadOnlyMemoryBuilder.Create))]
file readonly record struct StructuralReadOnlyMemory<T>(ReadOnlyMemory<T> Memory)
{
    [System.Text.Json.Serialization.JsonIgnore]
    public ReadOnlySpan<T> Span => this.Memory.Span;

    [System.Text.Json.Serialization.JsonIgnore]
    public int Length => this.Memory.Length;

    public bool Equals(StructuralReadOnlyMemory<T> other) => this.Span.SequenceEqual(other.Span);

    public override int GetHashCode()
    {
        var span = this.Span.Slice(Math.Max(0, this.Length - 10));
        var hash = new HashCode();
        foreach (var n in span)
            hash.Add(n);

        return hash.ToHashCode();
    }

    [EditorBrowsable(EditorBrowsableState.Never)]
    public ReadOnlySpan<T>.Enumerator GetEnumerator() => this.Memory.Span.GetEnumerator();

    public static implicit operator ReadOnlyMemory<T>(StructuralReadOnlyMemory<T> memory) => memory.Memory;

    public static implicit operator ReadOnlySpan<T>(StructuralReadOnlyMemory<T> memory) => memory.Span;
}

file static class StructuralReadOnlyMemoryBuilder
{
    internal static StructuralReadOnlyMemory<T> Create<T>(ReadOnlySpan<T> span) => new StructuralReadOnlyMemory<T>(span.ToArray());
}

file record struct CardCase(string Name, StructuralReadOnlyMemory<int> Cards);

public class _StructuralReadOnlyMemoryTest
{

    [Fact]
    void 使い方()
    {
        var a = new CardCase("Cards", [1, 2, 3]);
        var b = new CardCase("Cards", [1, 2, 3]);
        Assert.Equal(a, b);
    }

    [Fact]
    void Constructor_Length_Span_Operator()
    {
        ReadOnlySpan<int[]?> testCases = [
            [],
            null,
            [1, 2, 3],
        ];

        foreach (var n in testCases)
        {
            var memory = new StructuralReadOnlyMemory<int>(n);
            Assert.Equal(n.AsMemory(), memory.Memory);
            Assert.Equal(n.AsSpan(), memory.Span);
            Assert.Equal(n?.Length ?? 0, memory.Length);
            Assert.Equal((ReadOnlyMemory<int>)n, (ReadOnlyMemory<int>)memory);
            Assert.Equal((ReadOnlySpan<int>)n, (ReadOnlySpan<int>)memory);
        }
    }

    [Fact]
    void CollectionBuilder()
    {
        var a = StructuralReadOnlyMemoryBuilder.Create([1, 2, 3]);
        Assert.Equal(new StructuralReadOnlyMemory<int>(new[] { 1, 2, 3 }), a);

        StructuralReadOnlyMemory<int> b = [1, 2, 3];
        Assert.Equal(new StructuralReadOnlyMemory<int>(new[] { 1, 2, 3 }), b);
    }

    [Fact]
    void EqualsTest()
    {
        StructuralReadOnlyMemory<int> a = [1, 2, 3];
        StructuralReadOnlyMemory<int> b = [1, 2, 3];
        StructuralReadOnlyMemory<int> c = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
        Assert.True(a.Equals(a));
        Assert.True(a.Equals(b));
        Assert.True(b.Equals(a));
        Assert.False(a.Equals(c));
        Assert.False(c.Equals(a));
    }

    [Fact]
    void GetHashCodeTest()
    {
        StructuralReadOnlyMemory<int> a = [1, 2, 3];
        StructuralReadOnlyMemory<int> b = [1, 2, 3];
        StructuralReadOnlyMemory<int> c = [3, 2, 1];
        Assert.Equal(a.GetHashCode(), b.GetHashCode());
        Assert.NotEqual(a.GetHashCode(), c.GetHashCode());

        StructuralReadOnlyMemory<int> d = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,];
        StructuralReadOnlyMemory<int> e = [-1, -2, -3, -4, -5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,];
        Assert.Equal(d.GetHashCode(), e.GetHashCode());
    }
}

file record struct StructuralReadOnlyMemoryRecord(int X, int Y, StructuralReadOnlyMemory<int> Memory);

public class _StructuralReadOnlyMemoryRecordTest
{
    [Fact]
    void JSONでシリアル化できることのテスト()
    {
        var record = new StructuralReadOnlyMemoryRecord(1, 2, [1, 2, 3]);
        var json = System.Text.Json.JsonSerializer.Serialize(record);
        var deserialized = System.Text.Json.JsonSerializer.Deserialize<StructuralReadOnlyMemoryRecord>(json);
        Assert.Equal(record, deserialized);
    }

    static Action Span_ForEach()
    {
        var records = new int[1000];
        var memory = new StructuralReadOnlyMemory<int>(records);
        return () =>
        {
            var sum = 0;
            foreach (var record in memory.Span)
            {
                sum += record;
            }
        };
    }

    static Action StructuralReadOnlyMemoryRecord_ForEach()
    {
        var records = new int[1000];
        var memory = new StructuralReadOnlyMemory<int>(records);
        return () =>
        {
            var sum = 0;
            foreach (var record in memory)
            {
                sum += record;
            }
        };
    }
}

file record struct ReadOnlyMemoryRecord(int X, int Y, ReadOnlyMemory<int> Memory);

public class _ReadOnlyMemoryRecordTest
{
    [Fact]
    void 同値性のテスト()
    {
        var a = new ReadOnlyMemory<int>([1, 2, 3]);
        var b = new ReadOnlyMemory<int>([1, 2, 3]);
        var c = new ReadOnlyMemory<int>([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
        Assert.True(a.Equals(a));
        // 失敗する
        Assert.False(a.Equals(b));
        // 失敗する
        Assert.False(b.Equals(a));
        Assert.False(a.Equals(c));
        Assert.False(c.Equals(a));
    }

    [Fact]
    void ハッシュコードのテスト()
    {
        var a = new ReadOnlyMemory<int>([1, 2, 3]);
        var b = new ReadOnlyMemory<int>([1, 2, 3]);
        var c = new ReadOnlyMemory<int>([3, 2, 1]);
        // 失敗する
        Assert.NotEqual(a.GetHashCode(), b.GetHashCode());
        Assert.NotEqual(a.GetHashCode(), c.GetHashCode());

        var d = new ReadOnlyMemory<int>([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,]);
        var e = new ReadOnlyMemory<int>([-1, -2, -3, -4, -5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,]);
        // 失敗する
        Assert.NotEqual(d.GetHashCode(), e.GetHashCode());
    }

    [Fact]
    void JSONでシリアル化できることのテスト()
    {
        var record = new ReadOnlyMemoryRecord(1, 2, new ReadOnlyMemory<int>([1, 2, 3]));
        // オブジェクトは正しくシリアル化・シリアル化解除される
        var json = System.Text.Json.JsonSerializer.Serialize(record);
        var deserialized = System.Text.Json.JsonSerializer.Deserialize<ReadOnlyMemoryRecord>(json);
        // 失敗する
        Assert.NotEqual(record, deserialized);

        Assert.Equal(record.X, deserialized.X);
        Assert.Equal(record.Y, deserialized.Y);
        Assert.True(record.Memory.Span.SequenceEqual(deserialized.Memory.Span));
    }
}

file record struct ImmutableArrayRecord(int X, int Y, System.Collections.Immutable.ImmutableArray<int> Memory);

public class _ImmutableArrayRecordTest
{
    [Fact]
    void 同値性のテスト()
    {
        System.Collections.Immutable.ImmutableArray<int> a = [1, 2, 3];
        System.Collections.Immutable.ImmutableArray<int> b = [1, 2, 3];
        System.Collections.Immutable.ImmutableArray<int> c = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
        Assert.True(a.Equals(a));
        // 失敗する
        Assert.False(a.Equals(b));
        // 失敗する
        Assert.False(b.Equals(a));
        Assert.False(a.Equals(c));
        Assert.False(c.Equals(a));
    }

    [Fact]
    void ハッシュコードのテスト()
    {
        System.Collections.Immutable.ImmutableArray<int> a = [1, 2, 3];
        System.Collections.Immutable.ImmutableArray<int> b = [1, 2, 3];
        System.Collections.Immutable.ImmutableArray<int> c = [3, 2, 1];
        // 失敗する
        Assert.NotEqual(a.GetHashCode(), b.GetHashCode());
        Assert.NotEqual(a.GetHashCode(), c.GetHashCode());

        System.Collections.Immutable.ImmutableArray<int> d = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,];
        System.Collections.Immutable.ImmutableArray<int> e = [-1, -2, -3, -4, -5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,];
        // 失敗する
        Assert.NotEqual(d.GetHashCode(), e.GetHashCode());
    }

    [Fact]
    void JSONでシリアル化できることのテスト()
    {
        var record = new ImmutableArrayRecord(1, 2, [1, 2, 3]);
        // オブジェクトは正しくシリアル化・シリアル化解除される
        var json = System.Text.Json.JsonSerializer.Serialize(record);
        var deserialized = System.Text.Json.JsonSerializer.Deserialize<ImmutableArrayRecord>(json);
        // 失敗する
        Assert.NotEqual(record, deserialized);

        Assert.Equal(record.X, deserialized.X);
        Assert.Equal(record.Y, deserialized.Y);
        Assert.True(record.Memory.SequenceEqual(deserialized.Memory));
    }
}

```

</details>