0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

概要

レコード型を使用していると、シーケンスについて構造が同じか(参照が同じかではなく)で同値性を評価してほしいときがあります。

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

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

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

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

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()ユーザー定義のコレクション式のために必要です。メソッドに属性を付けてメンバー表示されないようにしています。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 はガベージコレクション回数を表します(少ないほどパフォーマンスがよい)。

コード

テストコード
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));
    }
}

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?