概要
レコード型を使用していると、シーケンスについて構造が同じか(参照が同じかではなく)で同値性を評価してほしいときがあります。
読み取り専用のシーケンスを表す型は、フレームワークにいくつかあります。これらは同値性の評価を参照が同じかで評価するため、望ましい挙動になりません。
-
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));
}
}