The pitfall of ImmutableArray<T> and its lack of value semantics
Recently I wanted to replace a custom collection type that comes with value semantics in some old component by a ready-to-use collection type to get rid of my custom implementation and the ImmutableArray<T> type was the first one that came to my mind.
At first sight this type looks like that it was made especially for this kind of scenario because it is implementing the IEquatable<T> interface and you somehow expected that it performs equality comparison based on its contained items.
Unfortunately, this is a pitfall.
Basically, the ImmutableArray<T> is a very thin wrapper around a regular array. Perfoming an equality comparison between two instance of an ImmutableArray<T> will simply check whether these two instances both refer to the same array. That's all. So there is no expected element-wise equality comparison of their contained items and in the end also no value semantics.
Recommended by LinkedIn
Fortunately, the corresponding specs and unit tests of the concerned component that ensure value semantics did their job well and failed after replacing the custom collection type with ImmutableArray<T>.
We started a lively discussion about this issue in the dotnet/runtime repository on GitHub and came up with some interesting ideas and solutions. So if you also have fallen into this trap it might be worth to have look there.
In the end I will continue using a custom collection until a ready-to-use collection with value semantics will be available:
using System.Collections
public sealed class ImmutableEnumerable<T> : IEnumerable<T>, IEquatable<ImmutableEnumerable<T>>
{
// This type is owner of the contained collection; no one can change this from outside.
// Use normal array for performance and memory footprint reasons; could also be an ImmutableArray<T> to prevent changing items directly once it has been created.
private readonly T[] items;
private ImmutableEnumerable(T[] items)
{
this.items = items;
}
public static ImmutableEnumerable<T> Create(IEnumerable<T> items)
=> new(items.ToArray());
// ToDo: Provide implicit type conversion, if required
public static bool operator ==(ImmutableEnumerable<T> left, ImmutableEnumerable<T> right)
=> Equals(left, right);
public static bool operator !=(ImmutableEnumerable<T> left, ImmutableEnumerable<T> right)
=> !(left == right);
public override int GetHashCode()
{
var hashCode = new HashCode();
foreach (var item in this.items)
{
hashCode.Add(item);
}
return hashCode.ToHashCode();
}
public override bool Equals(object? obj)
=> this.Equals(obj as ImmutableEnumerable<T>);
public bool Equals(ImmutableEnumerable<T>? other)
{
if (ReferenceEquals(null, other))
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
return this.items.SequenceEqual(other.items);
}
public IEnumerator<T> GetEnumerator()
=> this.items.AsEnumerable().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> this.GetEnumerator();
};