-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Allow fine grained deep control of Record Equality #3835
Comments
When we were designing record equality, we considered extension points to allow for deep customization. However, we considered this outside the purview of records themselves as there's so much variation: I want my |
This allows the user to provide all those extension points without requiring source generation to change - in fact it solves this problem generically for all types (not just records) albeit it requires adoption before it's valuable. Records are a great way to instantly drive up that adoption. |
Easy. Just do this (using my example above): public class MyComparer : EnumerableValueComparer
{
public override bool Equals(object? x, object? y)
{
if (x is String xString && y is String yString)
return xString.Equals(yString, StringComparison.InvariantCultureIgnoreCase);
return base.Equals(x, y);
}
public override int GetHashCode(object obj)
{
if (obj is string str)
return str.GetHashCode(StringComparison.InvariantCultureIgnoreCase);
return base.GetHashCode(obj);
}
} |
I don't really see how this suggestion helps with customizing the equality comparison of nested values within a record. You're suggesting the ability to override a single |
My gut feeling is this proposal covers 90%+ of cases where you need to adjust equality comparisons. I think cases where you need to compare some strings one way but other strings another are much rarer. |
A single And given that nominal records are proposed as a replacement for POCOs, I think the need to compare different elements of type |
But in such cases you should explicitly control equality to use the correct StringComparison anyway. The suggested mechanism only applies to cases where the default equality is considered good enough. |
Then why need this proposal at all? If you're writing your own equality comparers anyway why not reuse them within a custom I'm with @333fred , there's too much variation in this space. Records intended to provide value equality in the same vein as structs. As with structs if you wish to influence that equality it's your responsibility to override and provide your own implementation. |
I agree on the need: almost all of my record-like objects currently have some form of custom equality for at least one of their members. Without the ability to customise the equality used for each member individually, records have very little benefit for me. However, I don't like this proposal for a few reasons:
I'd like to see some way of customising per-member equality however. There's a bit of a discussion here, which contains the suggestion: record A(
[EqualityComparer(typeof(SomeCustomComparer))] ImmutableArray<B> Bs,
string S) { } You can also imagine something like: record A(ImmutableArray<B> Bs, string S)
{
[Equality(nameof(B))]
private static bool Equals(ImmutableArray<B> left, ImmutableArray<B> right) => ...;
} or: [Equality(nameof(Bs), typeof(SomeCustomComparer))]
record A(ImmutableArray<B> Bs, string S) { } Granted you can implement the above with a source generator: maybe that's the best way. You lose uniformity across code bases for something which I suspect is going to be quite common, though. |
I can't help but think that the problem here is that the records are incorrectly structured, not that the compiler needs to do anything different. record B(ImmutableArray<string> Strings, int I) {} In this example, the need for strings to be compared in a case-insensitive manner screams to me that there's a semantic type buried in the declaration. I'd solve this by extracting the semantic type and being explicit:
Chances are that In short, rather than inventing complex infrastructure that inevitably will only meet 80% of the needs (at best), I think its better to lean into the type system for this level of control. Also, it seems to me that code like this public static bool Equals(A a1, A a2)
{
return a1.Bs.Length == a2.Bs.Length
&& a1.Bs.Zip(a2.Bs, (b1, b2) => Equals(b1, b2)).All(x => x)
&& a1.S == a2.S;
} is a violation of the law of Demeter, just lurking to cause you future pain. |
I stand by my response earlier that this is probably rarely needed. However it is actually rather simple to do: public class MyComparer : EnumerableValueComparer
{
public override bool Equals(object? x, object? y)
{
if (x is List<List<string>> && y is List<List<string>>)
return InvariantCultureComparer.Instance.Equals(x, y);
return base.Equals(x, y);
}
public override int GetHashCode(object obj)
{
if (obj is List<List<string>>)
return InvariantCultureComparer.Instance.GetHashCode(obj);
return base.GetHashCode(obj);
}
}
public class InvariantCultureComparer : EnumerableValueComparer
{
public override bool Equals(object? x, object? y)
{
if (x is String xString && y is String yString)
return xString.Equals(yString, StringComparison.InvariantCultureIgnoreCase);
return base.Equals(x, y);
}
public override int GetHashCode(object obj)
{
if (obj is string str)
return str.GetHashCode(StringComparison.InvariantCultureIgnoreCase);
return base.GetHashCode(obj);
}
}
I think you misunderstand the intention behind my proposal. This is less about changing the equality for records you own, but more about modifying equality for records you don't own, or adjusting the default equality for records you own for a particular use case.
That's simple to do: record A(ImmutableArray<B> Bs, string S)
{
public bool Equals(A? other) => Equals(other, EnumerableValueComparer.Instance);
} However as I said above this is less about changing the equality for records you own, but more about modifying equality for records you don't own, or adjusting the default equality for records you own for a particular use case.
Responded above.
Since
As I said above this is less about changing the equality for records you own, but more about modifying equality for records you don't own, or adjusting the default equality for records you own for a particular use case. Therefore I think focusing on functionality over performance is appropriate.
It's actually quite simple to get reusability by wrapping the public interface EqualityComparerPart
{
bool IsApplicable(object? obj);
bool Equals(object? a, object? b, IEqualityComparer equalityComparer);
bool GetHashCode(object obj, IEqualityComparer equalityComparer);
} However this issue is long enough as it is without going into the particulars of that design. |
That was given as a secondary example, and is clearly a made up use case. Besides you may not own the type being compared. Regardless there are countless cases where I have wanted to equate types not using their equals methods, and have been forced to rewrite reams of code to achieve this.
That was just an example of the shortest code you would have to write today, not an example of what code you should write. |
A little bit off-topic, but you can use public static bool Equals(B b1, B b2)
{
return b1.Strings.SequenceEqual(b2.Strings) && b1.I == b2.I;
}
// or case insensitive
public static bool Equals(B b1, B b2)
{
return b1.Strings.SequenceEqual(b2.Strings, StringComparer.OrdinalIgnoreCase) && b1.I == b2.I;
}
|
That feels like an exceptionally rare thing to want to do and something that the language and BCL should not be encouraging. Records should provide their own identity and equality. But, if you really, really need to do this, you can already by providing an |
You clearly have a blessed existence |
Considered records haven't shipped yet I think it's kind of odd to be claiming that a feature is required to override how their equality is calculated. 😀 As for overriding the equality of other arbitrary types, sure that's sometimes a necessity. That is why Lastly, this proposal requires modifications to the BCL, which are not going to happen by .NET 5.0 when records ship. And the BCL has already expressed concern about adding DIM to commonly-used interfaces, refusing to do so with |
|
Closing as records have shipped and this didn't seem very popular anyway |
Problem Statement
Currently if you want to modify how record equality works you need to define the Equals method manually. If you need to change something deep in a set of nested records, you need to manually write the equality method for all of them. This commonly occurs if you want to compare collections using value rather than reference equality.
For example lets say you have the following types:
And you wanted to compare the ImmutableArrays via value equality, you would have to do this:
Which is a lot of boilerplate, none of which is reusable if you then want to e.g. now also compare strings via OrdinalIgnoreCase.
There is no way to take an existing implementation of
Equals
and to tweak it slightly using custom rules.Suggested solution
We essentially use a form of the visitor pattern to allow this:
Update
IEquatable<T>
as follows:Add an abstract class
EqualityComparer
:When Generating records, add an implementation for
Equals(T? other, IEqualityComparer equalityComparer)
. E.g. in our example given above it would generate:Then it is possible for users to write an IEqualityComparer once off which compares all collections correctly and use it everywhere. For example:
Then to compare two instances of
A
using value equality, you can just do:a1.Equals(a2, EnumerableValueComparer.Instance
.The text was updated successfully, but these errors were encountered: