runtime: ImmutableArray struct implements IEquatable interface but does not stick to the rules of properly implementing value semantics
Description
The ImmutableArray<T>
struct implements the IEquatable<T>
interface but does not stick to the rules of implementing value semantics acoording to the IEquatable<T> interface.
When comparing two instance of a ImmutableArray<T>
both instance are considered equal only when both instances refer to the same contained array instance. Instead both instances should be considered equal when the contents of the contained collections are equal.
Reproduction Steps
using System.Collections.Immutable;
var collection = Enumerable.Range(1, 5);
var instance1 = collection.ToImmutableArray();
var instance2 = collection.ToImmutableArray();
// Result = False ---> Wrong result for equality in terms of value semantics
var result = instance1.Equals(instance2);
Console.WriteLine($"Result: {result}"); // Outputs "Result: False"
Expected behavior
using System.Collections.Immutable;
var collection = Enumerable.Range(1, 5);
var instance1 = collection.ToImmutableArray();
var instance2 = collection.ToImmutableArray();
// Result = True ---> Should be true because both instances represent a collection of integers from 1 to 5 which are equal in terms of value semantics defined by IEquatable<T> interface
var result = instance1.Equals(instance2);
Console.WriteLine($"Result: {result}"); // Outputs "Result: True"
Actual behavior
Already described in Description and Reproduction Steps sections.
Regression?
No response
Known Workarounds
No response
Configuration
No response
Other information
The IEquatable<T>
interface should by properly implemented according to its specification to ensure value semantics of ImmutableArray<T>
. This also includes desired behavior of Equals and GetHashCode methods according to interface specification.
About this issue
- Original URL
- State: open
- Created 2 years ago
- Reactions: 8
- Comments: 20 (12 by maintainers)
(Partially unrelated) Just wanted to say that while I agree that of course changing the behavior of
IEquatable<T>
here is definitely a breaking change and not doable, the current behavior is indeed extremely unfortunate when using lots of records like @RikkiGibson mentioned. This is extremely common when writing source generators (as incrementa generators require you to define models for your incremental steps), and the current situation is that the second you have even a single record parameter that’s an immutable array, you just fall off a cliff and have to go all the way down to implementing equality manually for the whole record type, which is a pain. It would be nice if there was some way to improve this scenario, as it’s arguably pretty common and unfortunately (1) extremely verbose and (2) extremely error prone, as if you don’t know this behavior or just forget when refactoring, you’ll silently break your entire incremental pipeline after that step 😅As @RikkiGibson and @Sergio0694 already mentioned the current equatable behavior of the
ImmutableArray<T>
struct - whether this was intended or not - is a pitfall, unfortunately.At least I have experienced this on heavily utilizing records (e.g. when working with DDD or object-functional techniques that involve a lot of immutability and types that come with value semantics). So I was introducing new records representing composites that own their contained collections. Then I was looking for some out-of-the-box immutable collection and the
ImmutableArray<T>
was the only one I could find which is implementing theIEquatable<T>
interface. After replacing a custom immutable collection type that comes with value semantics the corresponding specs and tests were broken. So I digged a bit deeper and detected that theImmutableArray<T>
was actually not what I thought it is, according to its name and implementation of theIEquatable<T>
interface. It looks like @Sergio0694 came across a similar situation. 😅@eiriktsarpalis I know that changing this behavior afterwards would be a breaking change. In my opinion it would be great if there was an out-of-the-box immutable collection or bare enumerable implementation that comes with value semantics and can be used within records.
@mrpmorris Create your own type(s) that capture the symbol info you want and copy those over.
Just wanted to mention that this issue keeps popping up very often whenever I see people writing source generators (myself included). I had to copy-paste my
EquatableArray<T>
type in over 4 different projects for the same reason, and it’s just not ideal. This feels like a common enough issue for source generator authors that it’d be nice if this could either be addressed inImmutableArray<T>
itself, or maybe we should propose a separate API that could be used in these cases? I know some other generator authors (eg. @jkoritzinsky) have alternative solutions to this, but it just seels like everyone is reinventing ways to solve the same issue, so maybe just having a proper solution available for everyone would be better? 🤔You know what, that’s… Actually a very good idea. I reckon using something like that might very well make authoring records much much simpler, as there would be no more a need to handroll custom comparers every single time 🤔
I’ve hacked together really quick just to play around:Old prototype (click to expand):
See updated code here.
And you’d use it like so:
I might actually try this out in a branch of the MVVM Toolkit and see how this goes 😄 It would seem like this would simplify a ton of code!
UPDATE: well this was an absolute success, this is amazing 🎉
IEquatable<T>
doesn’t require types implement deep value semantics. It implies no contract other than some loosely defined “equality” that’s up for interpretation by types that implement it. It’s not completely uncommon for either shallow equality or ignored properties to be common in implementations in C# or other languages.