-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ImmutableArray<T> struct implements IEquatable<T> interface but does not stick to the rules of properly implementing value semantics #77183
Comments
Tagging subscribers to this area: @dotnet/area-system-collections Issue DetailsDescriptionThe When comparing two instance of a Reproduction Steps
Expected behavior
Actual behaviorAlready described in Description and Reproduction Steps sections. Regression?No response Known WorkaroundsNo response ConfigurationNo response Other informationThe
|
|
And even if we wanted to implement structural equality at this point, it would most likely be a breaking change. |
I think the typical guidance here is to use SequenceEqual, but the behavior can be a little troubling if you want to deeply compare records which contain collections, for example. (Records use EqualityComparer.Default to compare their fields.) SharpLab using System;
using System.Collections.Generic;
using System.Linq;
var r1 = new R1(new List<R2> { new R2(new List<int> { 1 }) });
var r2 = new R1(new List<R2> { new R2(new List<int> { 1 }) });
Console.WriteLine(r1.Equals(r2)); // False
Console.WriteLine(Enumerable.SequenceEqual(r1.Items, r2.Items)); // False
Console.WriteLine(r1.Equals(r2)); // False
Console.WriteLine(Enumerable.SequenceEqual(r1.Items[0].NestedItems, r2.Items[0].NestedItems)); // True
record R1(List<R2> Items);
record R2(List<int> NestedItems); That said, I'm not sure the problem is really "fixable", except that if you want these kinds of equality semantics, to take care to use SequenceEqual or equivalent methods when comparing collections, and perhaps do the same when you implement IEquatable.Equals. |
(Partially unrelated) Just wanted to say that while I agree that of course changing the behavior of |
@Sergio0694 Perhaps exposing an EqualityComparer combinator could help in that regard? public static class EqualityComparer
{
public static IEqualityComparer<TList> CreateListComparer<TList, TElement>(IEqualityComparer<TElement> elementComparer)
where TList : IList<TElement> { }
} |
As @RikkiGibson and @Sergio0694 already mentioned the current equatable behavior of the 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 @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. |
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 🤔
Old prototype (click to expand):public readonly struct EquatableArray<T> : IEquatable<EquatableArray<T>>
where T : IEquatable<T>
{
private readonly T[] array;
public bool Equals(EquatableArray<T> array)
{
return AsImmutableArray().SequenceEqual(array.AsImmutableArray());
}
public override bool Equals([NotNullWhen(true)] object? obj)
{
return obj is EquatableArray<T> array && Equals(this, array);
}
public override int GetHashCode()
{
if (this.array is not T[] array)
{
return 0;
}
HashCode hashCode = default;
foreach (T item in array)
{
hashCode.Add(item);
}
return hashCode.ToHashCode();
}
public ImmutableArray<T> AsImmutableArray()
{
return Unsafe.As<T[], ImmutableArray<T>>(ref Unsafe.AsRef(in this.array));
}
public static EquatableArray<T> FromImmutableArray(ImmutableArray<T> array)
{
EquatableArray<T> array2 = default;
Unsafe.AsRef(in array2.array) = Unsafe.As<ImmutableArray<T>, T[]>(ref array);
return array2;
}
public static implicit operator EquatableArray<T>(ImmutableArray<T> array)
{
return FromImmutableArray(array);
}
public static implicit operator ImmutableArray<T>(EquatableArray<T> array)
{
return array.AsImmutableArray();
}
public static bool operator ==(EquatableArray<T> left, EquatableArray<T> right)
{
return left.Equals(right);
}
public static bool operator !=(EquatableArray<T> left, EquatableArray<T> right)
{
return !left.Equals(right);
}
} See updated code here. And you'd use it like so: record MyRecord(EquatableArray<string> Names, EquatableArray<int> Numbers);
ImmutableArray<string> names = GetNames();
ImmutableArray<int> numbers = GetNumbers();
MyRecord myRecord = new(names, numbers); // Implicit conversion
names = myRecord.Names;
numbers = myRecord.Numbers; // Also implicit conversion I might actually try this out in a branch of the MVVM Toolkit and see how this goes 😄 UPDATE: well this was an absolute success, this is amazing 🎉 |
@oliverzick @Sergio0694 I had that problem as well. I wrote myself a struct wrapper that confers value semantics to any list. Something like: struct ValueList<T> : IList<T>
{
IList<T> inner;
IEqualityComparer<T> elementComparer;
//...
}
static class ValueList
{
public static ValueList<T> Create<T>(IList<T> inner) => ...
}
var bytes = ValueList.Create(myByteArray); That type can ease a lot of pain, especially in the context of LINQ. The main drawback is that the |
Yup this is not something I'd recommend for public APIs. In my scenario though I was just referring to models in incremental generator steps, which are all internal anyway, so that's not an issue. @eiriktsarpalis that experiment was really a success both in the MVVM Toolkit and ComputeSharp, and I've also discovered yesterday that @jkoritzinsky has also been using an approach much like this in all the various COM interop generators as well. I wonder if this same idea could also help you simplify things in the System.Text.Json generators? Just really feels like a much simpler approach for incremental models 😄 |
@Sergio0694 It's good to see that a discussion like this one creates new ideas. And even what you have posted looks great.
We are not exposing the owned collection which in turn prevents access from outside. In addition anyone can create new collection instances by using corresponding And even there is no restriction on the generic type parameter that enforces to implement |
I came here to report the same issue. Without deep comparison, the automatic The same problem exists for all the |
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 |
Ran into this today, but in my case the lack of structural equality by default didn't just break the caching in the incremental pipeline. My generation function is doing a |
How about special-casing ImmutableArray in the default equality implementation of records? It's not ideal, but just throwing in the idea. |
@eatdrinksleepcode Just read that message — this is a side point to the rest of this conversation, but definitely do not capture symbols in your incremental models, ever. It doesn't matter whether you use a custom comparer or whatever to fix incrementality, symbols should just never flow across incremental steps because they cause compilations to be rooted across incremental runs. |
@Sergio0694 aren't they just data structures? What is the alternative? |
@mrpmorris Create your own type(s) that capture the symbol info you want and copy those over. |
I find that this article captures good guidance on how to write good incremental source generators. |
@Joe4evr that's what I hoped you'd say :) |
why? |
FWIW I ended up defining a bunch of equatable collection types for my own pet project source generators: |
+1 to fixing this. Requiring 99% of uses of source generators to use a custom type to work at all is a terrible user experience. |
None of the built-in collection types do equality across the entire sequence of elements for It is an explicit design decision for them to be this way, going back to .NET Framework 1.0. It is non-desirable, potentially very expensive, and may even have security concerns/implications to do element-wise equality by default. -- Security concerns can come in from potentially unbounded or unknown processing time which can lead to things like DDoS opportunities or the like. It can lead to catastrophic performance when used with other collections, dictionaries, etc. Operations like comparing or formatting collections should do the simple thing by default, because these operations can be expected to be frequently called. You should then have separate APIs, such as This is highly unlikely to be changed, but a different API/interface might be possible if there is sufficient justification. |
I am interested in the possibility of 'IStructuralEquatable'. I would want for records to be able to easily use that interface for equality checks on fields, and perhaps to add automatic implementation of the interface on record types as well. However, I also feel like I have seen relatively few requests/upvotes to address the "half-structural" equality that records have in practice. So there's a lot of other features I would want to do first. |
I thought c# had gotten inherent deep value-equality through records + immutable-collections. Background: While writing equals-assertion in a test, instead of using the assertion-library's slow, 'magic' value-comparison I wanted to utilise fast, well-defined behaviour in c# records. (Better to require c# knowledge.) |
Surprised at this. Has anyone created a package for an equatable collection? |
I've published this package that includes a number of immutable equatable collections that I use in my source generators. Variations of the same types are also used by generators in this repo. |
Why not introduce a warning when an There seems to be consensus that:
A warning would remove the pitfall without any breaking changes. Even if it is off by default, it would allow users to remove the danger of using this interface by mistake. |
Rather, Thus, while comparing two This is likewise exactly how types like |
There are cases where reference equality is useful but these are extremely small in domain modelling. It's a 0.01% case among the code that I am familiar with (which is biased to be predominantly F#, where modelling of domains is prominent). Your "not the norm" includes records and tuples but also strings. Add structural equality on lists, DUs, and record types and you have the F# world in which no one needs to bother with reference equality. The shallow vs deep issue and the content/structural/sequence distinctions go away assuming everything inside has structural equality. Having equality (semantics) depend on whether something is a struct or a reference type (implementation details) is a horrible idea and something dotnet1.0 clearly got wrong, and something that the generic For F# and domain modelling the |
Such equality is not a viable default in many (if not most) domains due to the potential for security issues, such as DDoS style attacks. It’s something that already exists has to be explicitly considered and specially handled for strings and which causes numerous potential for issues to otherwise unassuming users. you ultimately want to support multiple types of equality and need them to be differentiated by some form of contract or interface. IEquatable is, by design, intended for allowing use of types with collections and sets. This in part also implies having bounded defaults so that otherwise basic operations do not present pita of failure by default. Sequence equality is a different concept that may warrant its own separate interface |
Description
The
ImmutableArray<T>
struct implements theIEquatable<T>
interface but does not stick to the rules of implementing value semantics acoording to the IEquatable 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
Expected behavior
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 ofImmutableArray<T>
. This also includes desired behavior of Equals and GetHashCode methods according to interface specification.The text was updated successfully, but these errors were encountered: