具有集合属性的记录类型和具有值语义的集合
record types with collection properties & collections with value semantics
在 C# 9 中,我们现在(终于)有了记录类型:
public record SomeRecord(int SomeInt, string SomeString);
这给了我们一些好处,比如值语义:
var r1 = new SomeRecord(0, "zero");
var r2 = new SomeRecord(0, "zero");
Console.WriteLine(r1 == r2); // true - property based equality
在试验此功能时,我意识到定义一个(非字符串)引用类型的 属性 可能会导致违反直觉的行为(尽管如果你仔细想想,这完全可以解释):
public record SomeRecord(int SomeInt, string SomeString, int[] SomeArray);
var r1 = new SomeRecord(0, "test", new[] {1,2});
var r2 = new SomeRecord(0, "test", new[] {1,2});
Console.WriteLine(r1 == r2); // false, since int[] is a non-record reference type
.Net(或第 3 方)中是否存在可用于此场景的具有值语义的集合类型?我查看了 ImmutableArray 之类的,但这些也不提供此功能。
目前好像没有这样的类型。
您可以自己实现它,但如果您需要以 production-ready 的方式实现它,请注意其含义。正如@ryanholden8 的 所展示的那样,它并不像乍看起来那么简单!
对于我的用例(作为一个简化的例子),我使用 this gist 来装饰 IImutableList 并且可以按如下方式使用:
var r1 = new SomeRecord(0, "test", new[] { 1, 2 }.ToImmutableList().WithValueSemantics());
var r2 = new SomeRecord(0, "test", new[] { 1, 2 }.ToImmutableList().WithValueSemantics());
Console.WriteLine(r1 == r2); // true
显然要注意非常大的列表对性能的影响。
没有开箱即用的解决方案。你可以尝试但是从
Collection。 Microsoft 专门针对您不想创建整个样板文件(添加、删除等)的情况。您可以使用我们的方法来处理相同的概念:
ValueCollection.cs
现在也可以通过方便的 package
我们的团队遇到了类似的问题,并根据@jeroenh 的想法开始实施。但是,我们 运行 解决了无法再从 json 和 System.Text.Json
反序列化的记录问题。 Here's the gist(也在下面发布)以及我们必须创建的所有内容,以支持记录上的这种深度平等。
ImmutableArrayWithDeepEquality
using System.Collections.Generic;
using System.Linq;
namespace System.Collections.Immutable
{
[System.Text.Json.Serialization.JsonConverter(typeof(JsonConverterForImmutableArrayWithDeepEqualityFactory))]
public struct ImmutableArrayWithDeepEquality<T> : IEquatable<ImmutableArrayWithDeepEquality<T>>, IEnumerable, IEnumerable<T>
{
private readonly ImmutableArray<T> _list;
public ImmutableArrayWithDeepEquality(ImmutableArray<T> list) => _list = list;
#region ImmutableArray Implementation
public T this[int index] => _list[index];
public int Count => _list.Length;
public ImmutableArrayWithDeepEquality<T> Add(T value) => _list.Add(value).WithDeepEquality();
public ImmutableArrayWithDeepEquality<T> AddRange(IEnumerable<T> items) => _list.AddRange(items).WithDeepEquality();
public ImmutableArrayWithDeepEquality<T> Clear() => _list.Clear().WithDeepEquality();
public ImmutableArray<T>.Enumerator GetEnumerator() => _list.GetEnumerator();
public int IndexOf(T item, int index, int count, IEqualityComparer<T> equalityComparer) => _list.IndexOf(item, index, count, equalityComparer);
public ImmutableArrayWithDeepEquality<T> Insert(int index, T element) => _list.Insert(index, element).WithDeepEquality();
public ImmutableArrayWithDeepEquality<T> InsertRange(int index, IEnumerable<T> items) => _list.InsertRange(index, items).WithDeepEquality();
public int LastIndexOf(T item, int index, int count, IEqualityComparer<T> equalityComparer) => _list.LastIndexOf(item, index, count, equalityComparer);
public ImmutableArrayWithDeepEquality<T> Remove(T value, IEqualityComparer<T> equalityComparer) => _list.Remove(value, equalityComparer).WithDeepEquality();
public ImmutableArrayWithDeepEquality<T> RemoveAll(Predicate<T> match) => _list.RemoveAll(match).WithDeepEquality();
public ImmutableArrayWithDeepEquality<T> RemoveAt(int index) => _list.RemoveAt(index).WithDeepEquality();
public ImmutableArrayWithDeepEquality<T> RemoveRange(IEnumerable<T> items, IEqualityComparer<T> equalityComparer) => _list.RemoveRange(items, equalityComparer).WithDeepEquality();
public ImmutableArrayWithDeepEquality<T> RemoveRange(int index, int count) => _list.RemoveRange(index, count).WithDeepEquality();
public ImmutableArrayWithDeepEquality<T> Replace(T oldValue, T newValue, IEqualityComparer<T> equalityComparer) => _list.Replace(oldValue, newValue, equalityComparer).WithDeepEquality();
public ImmutableArrayWithDeepEquality<T> SetItem(int index, T value) => _list.SetItem(index, value).WithDeepEquality();
public bool IsDefaultOrEmpty => _list.IsDefaultOrEmpty;
public static ImmutableArrayWithDeepEquality<T> Empty = new(ImmutableArray<T>.Empty);
#endregion
#region IEnumerable
IEnumerator IEnumerable.GetEnumerator() => (_list as IEnumerable).GetEnumerator();
IEnumerator<T> IEnumerable<T>.GetEnumerator() => (_list as IEnumerable<T>).GetEnumerator();
#endregion
#region IEquatable
public bool Equals(ImmutableArrayWithDeepEquality<T> other) => _list.SequenceEqual(other);
public override bool Equals(object obj) => obj is ImmutableArrayWithDeepEquality<T> other && Equals(other);
public static bool operator ==(ImmutableArrayWithDeepEquality<T>? left, ImmutableArrayWithDeepEquality<T>? right) => left is null ? right is null : left.Equals(right);
public static bool operator !=(ImmutableArrayWithDeepEquality<T>? left, ImmutableArrayWithDeepEquality<T>? right) => !(left == right);
public override int GetHashCode()
{
unchecked
{
return _list.Aggregate(19, (h, i) => h * 19 + i!.GetHashCode());
}
}
#endregion
}
public static class ImmutableArrayWithDeepEqualityEx
{
public static ImmutableArrayWithDeepEquality<T> WithDeepEquality<T>(this ImmutableArray<T> list) => new(list);
public static ImmutableArrayWithDeepEquality<T> ToImmutableArrayWithDeepEquality<T>(this IEnumerable<T> list) => new(list.ToImmutableArray());
}
}
ImmutableArrayWithDeepEquality JsonConverter
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace System.Collections.Immutable
{
public class JsonConverterForImmutableArrayWithDeepEqualityFactory : JsonConverterFactory
{
public override bool CanConvert(Type typeToConvert)
=> typeToConvert.IsGenericType
&& typeToConvert.GetGenericTypeDefinition() == typeof(ImmutableArrayWithDeepEquality<>);
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var elementType = typeToConvert.GetGenericArguments()[0];
var arrayType = typeof(JsonConverterForImmutableArrayWithDeepEquality<>);
var converter = (JsonConverter)Activator.CreateInstance(
arrayType.MakeGenericType(elementType),
BindingFlags.Instance | BindingFlags.Public,
binder: null,
args: null,
culture: null)!;
return converter;
}
private class JsonConverterForImmutableArrayWithDeepEquality<T> : JsonConverter<ImmutableArrayWithDeepEquality<T>>
{
public override ImmutableArrayWithDeepEquality<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartArray)
{
throw new JsonException();
}
reader.Read();
List<T> elements = new();
while (reader.TokenType != JsonTokenType.EndArray)
{
var value = JsonSerializer.Deserialize<T>(ref reader, options);
if (value is not null)
{
elements.Add(value);
}
reader.Read();
}
return elements.ToImmutableArrayWithDeepEquality();
}
public override void Write(Utf8JsonWriter writer, ImmutableArrayWithDeepEquality<T> value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value.AsEnumerable(), options);
}
}
}
}
ImmutableDictionaryWithDeepEquality
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
namespace System.Collections.Immutable
{
[JsonConverter(typeof(JsonConverterForImmutableDictionaryWithDeepEqualityFactory))]
public class ImmutableDictionaryWithDeepEquality<TKey, TValue> : IEquatable<ImmutableDictionaryWithDeepEquality<TKey, TValue>>, IEnumerable<KeyValuePair<TKey, TValue>>, IEnumerable, IReadOnlyCollection<KeyValuePair<TKey, TValue>> where TKey : notnull
{
private readonly ImmutableDictionary<TKey, TValue> _dictionary;
public ImmutableDictionaryWithDeepEquality(ImmutableDictionary<TKey, TValue> dictionary) => _dictionary = dictionary;
#region ImmutableArray Implementation
public TValue this[TKey index] => _dictionary[index];
public int Count => _dictionary.Count;
public ImmutableDictionaryWithDeepEquality<TKey, TValue> Add(TKey key, TValue value) => _dictionary.Add(key, value).WithDeepEquality();
public ImmutableDictionaryWithDeepEquality<TKey, TValue> AddRange(IEnumerable<KeyValuePair<TKey, TValue>> pairs) => _dictionary.AddRange(pairs).WithDeepEquality();
public ImmutableDictionaryWithDeepEquality<TKey, TValue> Clear() => _dictionary.Clear().WithDeepEquality();
public ImmutableDictionary<TKey, TValue>.Enumerator GetEnumerator() => _dictionary.GetEnumerator();
public ImmutableDictionaryWithDeepEquality<TKey, TValue> Remove(TKey key) => _dictionary.Remove(key).WithDeepEquality();
public ImmutableDictionaryWithDeepEquality<TKey, TValue> RemoveRange(IEnumerable<TKey> keys) => _dictionary.RemoveRange(keys).WithDeepEquality();
public ImmutableDictionaryWithDeepEquality<TKey, TValue> SetItem(TKey key, TValue value) => _dictionary.SetItem(key, value).WithDeepEquality();
public bool IsEmpty => _dictionary.IsEmpty;
public static ImmutableDictionaryWithDeepEquality<TKey, TValue> Empty = new(ImmutableDictionary<TKey, TValue>.Empty);
#endregion
#region IEnumerable
IEnumerator IEnumerable.GetEnumerator() => (_dictionary as IEnumerable).GetEnumerator();
IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator() => (_dictionary as IEnumerable<KeyValuePair<TKey, TValue>>).GetEnumerator();
#endregion
#region IEquatable
public bool Equals(ImmutableDictionaryWithDeepEquality<TKey, TValue> other) => _dictionary.SequenceEqual(other);
public override bool Equals(object obj) => obj is ImmutableDictionaryWithDeepEquality<TKey, TValue> other && Equals(other);
public static bool operator ==(ImmutableDictionaryWithDeepEquality<TKey, TValue>? left, ImmutableDictionaryWithDeepEquality<TKey, TValue>? right) => left is null ? right is null : right is not null && left.Equals(right);
public static bool operator !=(ImmutableDictionaryWithDeepEquality<TKey, TValue>? left, ImmutableDictionaryWithDeepEquality<TKey, TValue>? right) => !(left == right);
public override int GetHashCode()
{
unchecked
{
return _dictionary.Aggregate(19, (h, i) => h * 19 + i.Key.GetHashCode() + (i.Value?.GetHashCode() ?? 0));
}
}
#endregion
}
public static class ImmutableDictionaryWithDeepEqualityEx
{
public static ImmutableDictionaryWithDeepEquality<TKey, TValue> WithDeepEquality<TKey, TValue>(this ImmutableDictionary<TKey, TValue> dictionary) where TKey : notnull => new(dictionary);
public static ImmutableDictionaryWithDeepEquality<TKey, TValue> ToImmutableDictionaryWithDeepEquality<TKey, TValue>(this IEnumerable<KeyValuePair<TKey, TValue>> list) where TKey : notnull => new(list.ToImmutableDictionary());
}
}
ImmutableDictionaryWithDeepEquality JsonConverter
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace System.Collections.Immutable
{
public class JsonConverterForImmutableDictionaryWithDeepEqualityFactory : JsonConverterFactory
{
public override bool CanConvert(Type typeToConvert) =>
typeToConvert.IsGenericType
&& typeToConvert.GetGenericTypeDefinition() == typeof(ImmutableDictionaryWithDeepEquality<,>);
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var keyType = typeToConvert.GetGenericArguments()[0];
var valueType = typeToConvert.GetGenericArguments()[1];
var dictionaryType = typeof(JsonConverterForImmutableDictionaryWithDeepEquality<,>);
var converter = (JsonConverter)Activator.CreateInstance(
dictionaryType.MakeGenericType(keyType, valueType),
BindingFlags.Instance | BindingFlags.Public,
binder: null,
args: null,
culture: null)!;
return converter;
}
private class JsonConverterForImmutableDictionaryWithDeepEquality<TKey, TValue> : JsonConverter<ImmutableDictionaryWithDeepEquality<TKey, TValue>> where TKey : notnull
{
public override ImmutableDictionaryWithDeepEquality<TKey, TValue> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartArray)
{
throw new JsonException();
}
reader.Read();
Dictionary<TKey, TValue> elements = new();
while (reader.TokenType != JsonTokenType.EndArray)
{
var value = JsonSerializer.Deserialize<KeyValuePair<TKey, TValue>>(ref reader, options);
elements.Add(value.Key, value.Value);
reader.Read();
}
return elements.ToImmutableDictionaryWithDeepEquality();
}
public override void Write(Utf8JsonWriter writer, ImmutableDictionaryWithDeepEquality<TKey, TValue> value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value.AsEnumerable(), options);
}
}
}
}
ImmutableDeepEqualityTests
using System.Collections.Generic;
using Xunit;
namespace System.Collections.Immutable.Tests
{
public class ImmutableDeepEqualityTests
{
[Fact]
public void ArraysWithSameValues_AreConsideredEqual()
{
var array1 = new int[] { 1, 2, 3 }.ToImmutableArrayWithDeepEquality();
var array1Copy = new int[] { 1, 2, 3 }.ToImmutableArrayWithDeepEquality();
Assert.Equal(array1, array1Copy);
Assert.True(array1 == array1Copy);
Assert.False(array1 != array1Copy);
Assert.True(array1.Equals(array1Copy));
}
[Fact]
public void ArraysWithDifferentValues_AreConsideredNotEqual()
{
var array1 = new int[] { 1, 2, 3 }.ToImmutableArrayWithDeepEquality();
var array2 = new int[] { 4, 5, 6 }.ToImmutableArrayWithDeepEquality();
Assert.NotEqual(array1, array2);
Assert.False(array1 == array2);
Assert.True(array1 != array2);
Assert.False(array1.Equals(array2));
}
[Fact]
public void DictionariesWithSameValues_AreConsideredEqual()
{
var dict1 = new Dictionary<string, string> { { "Key1", "1" }, { "Key2", "2" } }.ToImmutableDictionaryWithDeepEquality();
var dict1Copy = new Dictionary<string, string> { { "Key1", "1" }, { "Key2", "2" } }.ToImmutableDictionaryWithDeepEquality();
Assert.Equal(dict1, dict1Copy);
Assert.True(dict1 == dict1Copy);
Assert.False(dict1 != dict1Copy);
Assert.True(dict1.Equals(dict1Copy));
}
[Fact]
public void DictionariesWithDifferentKeys_AreConsideredNotEqual()
{
var dict1 = new Dictionary<string, string> { { "Key1", "1" }, { "Key2", "2" } }.ToImmutableDictionaryWithDeepEquality();
var dict2 = new Dictionary<string, string> { { "KeyA", "1" }, { "KeyB", "2" } }.ToImmutableDictionaryWithDeepEquality();
Assert.NotEqual(dict1, dict2);
Assert.False(dict1 == dict2);
Assert.True(dict1 != dict2);
Assert.False(dict1.Equals(dict2));
}
[Fact]
public void DictionariesWithDifferentValues_AreConsideredNotEqual()
{
var dict1 = new Dictionary<string, string> { { "Key1", "1" }, { "Key2", "2" } }.ToImmutableDictionaryWithDeepEquality();
var dict2 = new Dictionary<string, string> { { "Key1", "A" }, { "Key2", "B" } }.ToImmutableDictionaryWithDeepEquality();
Assert.NotEqual(dict1, dict2);
Assert.False(dict1 == dict2);
Assert.True(dict1 != dict2);
Assert.False(dict1.Equals(dict2));
}
[Fact]
public void RecordsUseDeepEquality()
{
var array1 = new int[] { 1, 2, 3 }.ToImmutableArrayWithDeepEquality();
var array1Copy = new int[] { 1, 2, 3 }.ToImmutableArrayWithDeepEquality();
var dict1 = new Dictionary<string, string> { { "Key1", "1" }, { "Key2", "2" } }.ToImmutableDictionaryWithDeepEquality();
var dict1Copy = new Dictionary<string, string> { { "Key1", "1" }, { "Key2", "2" } }.ToImmutableDictionaryWithDeepEquality();
var model1 = new TestModel(array1, dict1);
var model1Copy = new TestModel(array1Copy, dict1Copy);
Assert.Equal(model1, model1Copy);
Assert.True(model1 == model1Copy);
Assert.False(model1 != model1Copy);
Assert.True(model1.Equals(model1Copy));
}
[Fact]
public void Records_ThatUseDeepEquality_CanSerialize()
{
var array1 = new int[] { 1, 2, 3 }.ToImmutableArrayWithDeepEquality();
var dict1 = new Dictionary<string, string> { { "Key1", "1" } }.ToImmutableDictionaryWithDeepEquality();
var model1 = new TestModel(array1, dict1);
var json = System.Text.Json.JsonSerializer.Serialize(model1);
Assert.Equal("{\"Ints\":[1,2,3],\"Dictionary\":[{\"Key\":\"Key1\",\"Value\":\"1\"}]}", json);
}
[Fact]
public void Records_ThatUseDeepEquality_CanDeserialize()
{
var json = "{\"Ints\":[1,2,3],\"Dictionary\":[{\"Key\":\"Key1\",\"Value\":\"1\"}]}";
var modelFromJson = System.Text.Json.JsonSerializer.Deserialize<TestModel>(json);
var array1 = new int[] { 1, 2, 3 }.ToImmutableArrayWithDeepEquality();
var dict1 = new Dictionary<string, string> { { "Key1", "1" } }.ToImmutableDictionaryWithDeepEquality();
var model1 = new TestModel(array1, dict1);
Assert.Equal(model1, modelFromJson);
}
private sealed record TestModel(
ImmutableArrayWithDeepEquality<int> Ints,
ImmutableDictionaryWithDeepEquality<string, string> Dictionary);
}
}
结论
如您所见,实施、支持和维护并非易事。在查看了自己的需求之后,我们发现深度平等只需要掩盖产生重复值的上游问题,而在一开始就不需要创建重复值。
在 C# 9 中,我们现在(终于)有了记录类型:
public record SomeRecord(int SomeInt, string SomeString);
这给了我们一些好处,比如值语义:
var r1 = new SomeRecord(0, "zero");
var r2 = new SomeRecord(0, "zero");
Console.WriteLine(r1 == r2); // true - property based equality
在试验此功能时,我意识到定义一个(非字符串)引用类型的 属性 可能会导致违反直觉的行为(尽管如果你仔细想想,这完全可以解释):
public record SomeRecord(int SomeInt, string SomeString, int[] SomeArray);
var r1 = new SomeRecord(0, "test", new[] {1,2});
var r2 = new SomeRecord(0, "test", new[] {1,2});
Console.WriteLine(r1 == r2); // false, since int[] is a non-record reference type
.Net(或第 3 方)中是否存在可用于此场景的具有值语义的集合类型?我查看了 ImmutableArray 之类的,但这些也不提供此功能。
目前好像没有这样的类型。
您可以自己实现它,但如果您需要以 production-ready 的方式实现它,请注意其含义。正如@ryanholden8 的
对于我的用例(作为一个简化的例子),我使用 this gist 来装饰 IImutableList 并且可以按如下方式使用:
var r1 = new SomeRecord(0, "test", new[] { 1, 2 }.ToImmutableList().WithValueSemantics());
var r2 = new SomeRecord(0, "test", new[] { 1, 2 }.ToImmutableList().WithValueSemantics());
Console.WriteLine(r1 == r2); // true
显然要注意非常大的列表对性能的影响。
没有开箱即用的解决方案。你可以尝试但是从 Collection。 Microsoft 专门针对您不想创建整个样板文件(添加、删除等)的情况。您可以使用我们的方法来处理相同的概念: ValueCollection.cs
现在也可以通过方便的 package
我们的团队遇到了类似的问题,并根据@jeroenh 的想法开始实施。但是,我们 运行 解决了无法再从 json 和 System.Text.Json
反序列化的记录问题。 Here's the gist(也在下面发布)以及我们必须创建的所有内容,以支持记录上的这种深度平等。
ImmutableArrayWithDeepEquality
using System.Collections.Generic;
using System.Linq;
namespace System.Collections.Immutable
{
[System.Text.Json.Serialization.JsonConverter(typeof(JsonConverterForImmutableArrayWithDeepEqualityFactory))]
public struct ImmutableArrayWithDeepEquality<T> : IEquatable<ImmutableArrayWithDeepEquality<T>>, IEnumerable, IEnumerable<T>
{
private readonly ImmutableArray<T> _list;
public ImmutableArrayWithDeepEquality(ImmutableArray<T> list) => _list = list;
#region ImmutableArray Implementation
public T this[int index] => _list[index];
public int Count => _list.Length;
public ImmutableArrayWithDeepEquality<T> Add(T value) => _list.Add(value).WithDeepEquality();
public ImmutableArrayWithDeepEquality<T> AddRange(IEnumerable<T> items) => _list.AddRange(items).WithDeepEquality();
public ImmutableArrayWithDeepEquality<T> Clear() => _list.Clear().WithDeepEquality();
public ImmutableArray<T>.Enumerator GetEnumerator() => _list.GetEnumerator();
public int IndexOf(T item, int index, int count, IEqualityComparer<T> equalityComparer) => _list.IndexOf(item, index, count, equalityComparer);
public ImmutableArrayWithDeepEquality<T> Insert(int index, T element) => _list.Insert(index, element).WithDeepEquality();
public ImmutableArrayWithDeepEquality<T> InsertRange(int index, IEnumerable<T> items) => _list.InsertRange(index, items).WithDeepEquality();
public int LastIndexOf(T item, int index, int count, IEqualityComparer<T> equalityComparer) => _list.LastIndexOf(item, index, count, equalityComparer);
public ImmutableArrayWithDeepEquality<T> Remove(T value, IEqualityComparer<T> equalityComparer) => _list.Remove(value, equalityComparer).WithDeepEquality();
public ImmutableArrayWithDeepEquality<T> RemoveAll(Predicate<T> match) => _list.RemoveAll(match).WithDeepEquality();
public ImmutableArrayWithDeepEquality<T> RemoveAt(int index) => _list.RemoveAt(index).WithDeepEquality();
public ImmutableArrayWithDeepEquality<T> RemoveRange(IEnumerable<T> items, IEqualityComparer<T> equalityComparer) => _list.RemoveRange(items, equalityComparer).WithDeepEquality();
public ImmutableArrayWithDeepEquality<T> RemoveRange(int index, int count) => _list.RemoveRange(index, count).WithDeepEquality();
public ImmutableArrayWithDeepEquality<T> Replace(T oldValue, T newValue, IEqualityComparer<T> equalityComparer) => _list.Replace(oldValue, newValue, equalityComparer).WithDeepEquality();
public ImmutableArrayWithDeepEquality<T> SetItem(int index, T value) => _list.SetItem(index, value).WithDeepEquality();
public bool IsDefaultOrEmpty => _list.IsDefaultOrEmpty;
public static ImmutableArrayWithDeepEquality<T> Empty = new(ImmutableArray<T>.Empty);
#endregion
#region IEnumerable
IEnumerator IEnumerable.GetEnumerator() => (_list as IEnumerable).GetEnumerator();
IEnumerator<T> IEnumerable<T>.GetEnumerator() => (_list as IEnumerable<T>).GetEnumerator();
#endregion
#region IEquatable
public bool Equals(ImmutableArrayWithDeepEquality<T> other) => _list.SequenceEqual(other);
public override bool Equals(object obj) => obj is ImmutableArrayWithDeepEquality<T> other && Equals(other);
public static bool operator ==(ImmutableArrayWithDeepEquality<T>? left, ImmutableArrayWithDeepEquality<T>? right) => left is null ? right is null : left.Equals(right);
public static bool operator !=(ImmutableArrayWithDeepEquality<T>? left, ImmutableArrayWithDeepEquality<T>? right) => !(left == right);
public override int GetHashCode()
{
unchecked
{
return _list.Aggregate(19, (h, i) => h * 19 + i!.GetHashCode());
}
}
#endregion
}
public static class ImmutableArrayWithDeepEqualityEx
{
public static ImmutableArrayWithDeepEquality<T> WithDeepEquality<T>(this ImmutableArray<T> list) => new(list);
public static ImmutableArrayWithDeepEquality<T> ToImmutableArrayWithDeepEquality<T>(this IEnumerable<T> list) => new(list.ToImmutableArray());
}
}
ImmutableArrayWithDeepEquality JsonConverter
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace System.Collections.Immutable
{
public class JsonConverterForImmutableArrayWithDeepEqualityFactory : JsonConverterFactory
{
public override bool CanConvert(Type typeToConvert)
=> typeToConvert.IsGenericType
&& typeToConvert.GetGenericTypeDefinition() == typeof(ImmutableArrayWithDeepEquality<>);
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var elementType = typeToConvert.GetGenericArguments()[0];
var arrayType = typeof(JsonConverterForImmutableArrayWithDeepEquality<>);
var converter = (JsonConverter)Activator.CreateInstance(
arrayType.MakeGenericType(elementType),
BindingFlags.Instance | BindingFlags.Public,
binder: null,
args: null,
culture: null)!;
return converter;
}
private class JsonConverterForImmutableArrayWithDeepEquality<T> : JsonConverter<ImmutableArrayWithDeepEquality<T>>
{
public override ImmutableArrayWithDeepEquality<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartArray)
{
throw new JsonException();
}
reader.Read();
List<T> elements = new();
while (reader.TokenType != JsonTokenType.EndArray)
{
var value = JsonSerializer.Deserialize<T>(ref reader, options);
if (value is not null)
{
elements.Add(value);
}
reader.Read();
}
return elements.ToImmutableArrayWithDeepEquality();
}
public override void Write(Utf8JsonWriter writer, ImmutableArrayWithDeepEquality<T> value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value.AsEnumerable(), options);
}
}
}
}
ImmutableDictionaryWithDeepEquality
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
namespace System.Collections.Immutable
{
[JsonConverter(typeof(JsonConverterForImmutableDictionaryWithDeepEqualityFactory))]
public class ImmutableDictionaryWithDeepEquality<TKey, TValue> : IEquatable<ImmutableDictionaryWithDeepEquality<TKey, TValue>>, IEnumerable<KeyValuePair<TKey, TValue>>, IEnumerable, IReadOnlyCollection<KeyValuePair<TKey, TValue>> where TKey : notnull
{
private readonly ImmutableDictionary<TKey, TValue> _dictionary;
public ImmutableDictionaryWithDeepEquality(ImmutableDictionary<TKey, TValue> dictionary) => _dictionary = dictionary;
#region ImmutableArray Implementation
public TValue this[TKey index] => _dictionary[index];
public int Count => _dictionary.Count;
public ImmutableDictionaryWithDeepEquality<TKey, TValue> Add(TKey key, TValue value) => _dictionary.Add(key, value).WithDeepEquality();
public ImmutableDictionaryWithDeepEquality<TKey, TValue> AddRange(IEnumerable<KeyValuePair<TKey, TValue>> pairs) => _dictionary.AddRange(pairs).WithDeepEquality();
public ImmutableDictionaryWithDeepEquality<TKey, TValue> Clear() => _dictionary.Clear().WithDeepEquality();
public ImmutableDictionary<TKey, TValue>.Enumerator GetEnumerator() => _dictionary.GetEnumerator();
public ImmutableDictionaryWithDeepEquality<TKey, TValue> Remove(TKey key) => _dictionary.Remove(key).WithDeepEquality();
public ImmutableDictionaryWithDeepEquality<TKey, TValue> RemoveRange(IEnumerable<TKey> keys) => _dictionary.RemoveRange(keys).WithDeepEquality();
public ImmutableDictionaryWithDeepEquality<TKey, TValue> SetItem(TKey key, TValue value) => _dictionary.SetItem(key, value).WithDeepEquality();
public bool IsEmpty => _dictionary.IsEmpty;
public static ImmutableDictionaryWithDeepEquality<TKey, TValue> Empty = new(ImmutableDictionary<TKey, TValue>.Empty);
#endregion
#region IEnumerable
IEnumerator IEnumerable.GetEnumerator() => (_dictionary as IEnumerable).GetEnumerator();
IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator() => (_dictionary as IEnumerable<KeyValuePair<TKey, TValue>>).GetEnumerator();
#endregion
#region IEquatable
public bool Equals(ImmutableDictionaryWithDeepEquality<TKey, TValue> other) => _dictionary.SequenceEqual(other);
public override bool Equals(object obj) => obj is ImmutableDictionaryWithDeepEquality<TKey, TValue> other && Equals(other);
public static bool operator ==(ImmutableDictionaryWithDeepEquality<TKey, TValue>? left, ImmutableDictionaryWithDeepEquality<TKey, TValue>? right) => left is null ? right is null : right is not null && left.Equals(right);
public static bool operator !=(ImmutableDictionaryWithDeepEquality<TKey, TValue>? left, ImmutableDictionaryWithDeepEquality<TKey, TValue>? right) => !(left == right);
public override int GetHashCode()
{
unchecked
{
return _dictionary.Aggregate(19, (h, i) => h * 19 + i.Key.GetHashCode() + (i.Value?.GetHashCode() ?? 0));
}
}
#endregion
}
public static class ImmutableDictionaryWithDeepEqualityEx
{
public static ImmutableDictionaryWithDeepEquality<TKey, TValue> WithDeepEquality<TKey, TValue>(this ImmutableDictionary<TKey, TValue> dictionary) where TKey : notnull => new(dictionary);
public static ImmutableDictionaryWithDeepEquality<TKey, TValue> ToImmutableDictionaryWithDeepEquality<TKey, TValue>(this IEnumerable<KeyValuePair<TKey, TValue>> list) where TKey : notnull => new(list.ToImmutableDictionary());
}
}
ImmutableDictionaryWithDeepEquality JsonConverter
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace System.Collections.Immutable
{
public class JsonConverterForImmutableDictionaryWithDeepEqualityFactory : JsonConverterFactory
{
public override bool CanConvert(Type typeToConvert) =>
typeToConvert.IsGenericType
&& typeToConvert.GetGenericTypeDefinition() == typeof(ImmutableDictionaryWithDeepEquality<,>);
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var keyType = typeToConvert.GetGenericArguments()[0];
var valueType = typeToConvert.GetGenericArguments()[1];
var dictionaryType = typeof(JsonConverterForImmutableDictionaryWithDeepEquality<,>);
var converter = (JsonConverter)Activator.CreateInstance(
dictionaryType.MakeGenericType(keyType, valueType),
BindingFlags.Instance | BindingFlags.Public,
binder: null,
args: null,
culture: null)!;
return converter;
}
private class JsonConverterForImmutableDictionaryWithDeepEquality<TKey, TValue> : JsonConverter<ImmutableDictionaryWithDeepEquality<TKey, TValue>> where TKey : notnull
{
public override ImmutableDictionaryWithDeepEquality<TKey, TValue> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartArray)
{
throw new JsonException();
}
reader.Read();
Dictionary<TKey, TValue> elements = new();
while (reader.TokenType != JsonTokenType.EndArray)
{
var value = JsonSerializer.Deserialize<KeyValuePair<TKey, TValue>>(ref reader, options);
elements.Add(value.Key, value.Value);
reader.Read();
}
return elements.ToImmutableDictionaryWithDeepEquality();
}
public override void Write(Utf8JsonWriter writer, ImmutableDictionaryWithDeepEquality<TKey, TValue> value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value.AsEnumerable(), options);
}
}
}
}
ImmutableDeepEqualityTests
using System.Collections.Generic;
using Xunit;
namespace System.Collections.Immutable.Tests
{
public class ImmutableDeepEqualityTests
{
[Fact]
public void ArraysWithSameValues_AreConsideredEqual()
{
var array1 = new int[] { 1, 2, 3 }.ToImmutableArrayWithDeepEquality();
var array1Copy = new int[] { 1, 2, 3 }.ToImmutableArrayWithDeepEquality();
Assert.Equal(array1, array1Copy);
Assert.True(array1 == array1Copy);
Assert.False(array1 != array1Copy);
Assert.True(array1.Equals(array1Copy));
}
[Fact]
public void ArraysWithDifferentValues_AreConsideredNotEqual()
{
var array1 = new int[] { 1, 2, 3 }.ToImmutableArrayWithDeepEquality();
var array2 = new int[] { 4, 5, 6 }.ToImmutableArrayWithDeepEquality();
Assert.NotEqual(array1, array2);
Assert.False(array1 == array2);
Assert.True(array1 != array2);
Assert.False(array1.Equals(array2));
}
[Fact]
public void DictionariesWithSameValues_AreConsideredEqual()
{
var dict1 = new Dictionary<string, string> { { "Key1", "1" }, { "Key2", "2" } }.ToImmutableDictionaryWithDeepEquality();
var dict1Copy = new Dictionary<string, string> { { "Key1", "1" }, { "Key2", "2" } }.ToImmutableDictionaryWithDeepEquality();
Assert.Equal(dict1, dict1Copy);
Assert.True(dict1 == dict1Copy);
Assert.False(dict1 != dict1Copy);
Assert.True(dict1.Equals(dict1Copy));
}
[Fact]
public void DictionariesWithDifferentKeys_AreConsideredNotEqual()
{
var dict1 = new Dictionary<string, string> { { "Key1", "1" }, { "Key2", "2" } }.ToImmutableDictionaryWithDeepEquality();
var dict2 = new Dictionary<string, string> { { "KeyA", "1" }, { "KeyB", "2" } }.ToImmutableDictionaryWithDeepEquality();
Assert.NotEqual(dict1, dict2);
Assert.False(dict1 == dict2);
Assert.True(dict1 != dict2);
Assert.False(dict1.Equals(dict2));
}
[Fact]
public void DictionariesWithDifferentValues_AreConsideredNotEqual()
{
var dict1 = new Dictionary<string, string> { { "Key1", "1" }, { "Key2", "2" } }.ToImmutableDictionaryWithDeepEquality();
var dict2 = new Dictionary<string, string> { { "Key1", "A" }, { "Key2", "B" } }.ToImmutableDictionaryWithDeepEquality();
Assert.NotEqual(dict1, dict2);
Assert.False(dict1 == dict2);
Assert.True(dict1 != dict2);
Assert.False(dict1.Equals(dict2));
}
[Fact]
public void RecordsUseDeepEquality()
{
var array1 = new int[] { 1, 2, 3 }.ToImmutableArrayWithDeepEquality();
var array1Copy = new int[] { 1, 2, 3 }.ToImmutableArrayWithDeepEquality();
var dict1 = new Dictionary<string, string> { { "Key1", "1" }, { "Key2", "2" } }.ToImmutableDictionaryWithDeepEquality();
var dict1Copy = new Dictionary<string, string> { { "Key1", "1" }, { "Key2", "2" } }.ToImmutableDictionaryWithDeepEquality();
var model1 = new TestModel(array1, dict1);
var model1Copy = new TestModel(array1Copy, dict1Copy);
Assert.Equal(model1, model1Copy);
Assert.True(model1 == model1Copy);
Assert.False(model1 != model1Copy);
Assert.True(model1.Equals(model1Copy));
}
[Fact]
public void Records_ThatUseDeepEquality_CanSerialize()
{
var array1 = new int[] { 1, 2, 3 }.ToImmutableArrayWithDeepEquality();
var dict1 = new Dictionary<string, string> { { "Key1", "1" } }.ToImmutableDictionaryWithDeepEquality();
var model1 = new TestModel(array1, dict1);
var json = System.Text.Json.JsonSerializer.Serialize(model1);
Assert.Equal("{\"Ints\":[1,2,3],\"Dictionary\":[{\"Key\":\"Key1\",\"Value\":\"1\"}]}", json);
}
[Fact]
public void Records_ThatUseDeepEquality_CanDeserialize()
{
var json = "{\"Ints\":[1,2,3],\"Dictionary\":[{\"Key\":\"Key1\",\"Value\":\"1\"}]}";
var modelFromJson = System.Text.Json.JsonSerializer.Deserialize<TestModel>(json);
var array1 = new int[] { 1, 2, 3 }.ToImmutableArrayWithDeepEquality();
var dict1 = new Dictionary<string, string> { { "Key1", "1" } }.ToImmutableDictionaryWithDeepEquality();
var model1 = new TestModel(array1, dict1);
Assert.Equal(model1, modelFromJson);
}
private sealed record TestModel(
ImmutableArrayWithDeepEquality<int> Ints,
ImmutableDictionaryWithDeepEquality<string, string> Dictionary);
}
}
结论
如您所见,实施、支持和维护并非易事。在查看了自己的需求之后,我们发现深度平等只需要掩盖产生重复值的上游问题,而在一开始就不需要创建重复值。