-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Add "Create" method to EqualityComparer<> class #24460
Comments
Not quite as handy since we need two delegates, and there isn't as much of an existing use as there is with |
What is the value? Example of usage today vs. in future? How commonly is it used? From a brief look it seems today's state - inheritance + overriding 2 methods is not too much extra code compared to the API proposed above. |
I also find that very useful. Though I'd prefer the method to be in a shorter version: public static IEqualityComparer<T> Create(params Expression<Func<T, object>>[] properties); Which will compare the two objects if the specified properties are equal, and will generate hash-codes based on the provided lambda properties. Usage: var eqComparer1 = EqualityComparer<Contact>.Create(c => c.FullName);
var eqComparer2 = EqualityComparer<Contact>.Create(c => c.FirstName, c => c.LastName); Here's some untested prototype: public class EqualityComparerImpl<T> : IEqualityComparer<T>
{
public static EqualityComparerImpl<T> Create(
params Expression<Func<T, object>>[] properties) =>
new EqualityComparerImpl<T>(properties);
PropertyInfo[] _properties;
EqualityComparerImpl(Expression<Func<T, object>>[] properties)
{
if (properties == null)
throw new ArgumentNullException(nameof(properties));
if (properties.Length == 0)
throw new ArgumentOutOfRangeException(nameof(properties));
var length = properties.Length;
var extractions = new PropertyInfo[length];
for (int i = 0; i < length; i++)
{
var property = properties[i];
extractions[i] = ExtractProperty(property);
}
_properties = extractions;
}
public bool Equals(T x, T y)
{
if (ReferenceEquals(x, y))
//covers both are null
return true;
if (x == null || y == null)
return false;
var len = _properties.Length;
for (int i = 0; i < _properties.Length; i++)
{
var property = _properties[i];
if (!Equals(property.GetValue(x), property.GetValue(y)))
return false;
}
return true;
}
public int GetHashCode(T obj)
{
if (obj == null)
return 0;
var hashes = _properties.Select(pi => pi.GetValue(obj)?.GetHashCode() ?? 0).ToArray();
return Combine(hashes);
}
static int Combine(int[] hashes)
{
int result = 0;
foreach (var hash in hashes)
{
uint rol5 = ((uint)result << 5) | ((uint)result >> 27);
result = ((int)rol5 + result) ^ hash;
}
return result;
}
static PropertyInfo ExtractProperty(Expression<Func<T, object>> property)
{
if (property.NodeType != ExpressionType.Lambda)
throwEx();
var body = property.Body;
if (body.NodeType == ExpressionType.Convert)
if (body is UnaryExpression unary)
body = unary.Operand;
else
throwEx();
if (!(body is MemberExpression member))
throwEx();
if (!(member.Member is PropertyInfo pi))
throwEx();
return pi;
void throwEx() =>
throw new NotSupportedException($"The expression '{property}' isn't supported.");
}
} And BTW, there IS demand for this. |
Is it hard to write a class implementing That's why in every private "util" library I use, I eventually wind up with a delegate-based implementation of And to be clear: writing a new Does it come up a ton? Again, nope. I may need this maybe two or three times a year, tops. But then, so what? It's such a small thing to add, and yet a glaring omission, from the otherwise very helpful and convenient .NET API. Is there a place to vote for this? As you can see, my vote would be a solid "yes". |
@peterduniho upvote top post. It is sortable on GitHub by number of votes there ... |
@karelz thanks. I don't see an "upvote" option per se, but I assume you mean the "thumbs up reaction" option. "Vote" submitted. :) |
Can't even write a .Distinct(..) without also writing up a whole new IEqualityComparer class implementation, just for one LINQ clause, this is ridiculous. Maybe Distinct(..) itself should tuck this away with its own overload(s) with a single lambda or a growable (params style) set of lambdas listing all the properties to find distinction on for just this one filter statement. The output of this being a generated implementation of IEqualityComparer. In which case I suppose it would be handled by a different team / group with a different github work item. |
Can someone update the sample to make a compelling case for the currently proposed API? The sample code was for a different proposal. If |
I'm not sure what sample you mean, so I'm probably missing part of the point of the request. My apologies about that.. But this isn't just an issue with The fact that utility libriaries used internally in numerous projects have defined this functionality over and over strongly suggests it'd be a useful addition to the API (for what it's worth, this has come up in pretty much every non-trivial project I've been involved with over the last ten years or so). Yes, it's true that this could be addressed in other ways. For example, just forget about it and let everyone keep doing what they've been doing (i.e. create the helper methods themselves). Or each of the places in .NET where |
Given where we are at in 5.0.0 I don't this this will make it. I do support the scenario, I've come across this before and have felt it jarring that I need to stop coding and create a new type, just when I want to tweak the comparison behavior of a collection. |
Needed that today to avoid repeatedly writing classes for Distinct in LINQ
Usage (with dummy lambdas for the sake of argument): |
Came across the need for such a method today while working with Roslyn incremental values. I suspect there might be value in making the nodes1.SequenceEqual(nodes2, EqualityComparer<SyntaxNode>.Create((n1, n2) => n1.IsEquivalentTo(n2))); Calling @tannergooding @terrajobst thoughts? It's fairly pervasive pattern that should eliminate the need for boilerplate classes for most APIs that accept |
Not sure, that Maybe, for your cases it is better to use explicit delegate? nodes1.SequenceEqual(nodes2, EqualityComparer<SyntaxNode>.Create((n1, n2) => n1.IsEquivalentTo(n2), _ => throw new NotSupportedException())); |
Looks good as proposed namespace System.Collections.Generic
{
public partial class EqualityComparer<T> : IEqualityComparer<T>, IEqualityComparer
{
public static EqualityComparer<T> Create(Func<T?, T?, bool> equals, Func<T, int> getHashCode);
}
} |
@bartonjs, in implementing this and using it in various places, I think we should make public static EqualityComparer<T> Create(Func<T?, T?, bool> equals, Func<T, int>? getHashCode = null); The only real downside is if GetHashCode was actually needed for a use and someone neglected to supply it because it's optional, but in such cases, they'll likely get an exception almost immediately. |
I'm not sure that I agree that "null-accepting" should immediately jump to "null-defaulting", but I guess I agree with the logic that the exception is fast-coming. But, yeah, if anyone had said "oh, it's super common in writing tests to not specify an implementation for GetHashCode" then we'd've slapped on the question mark. |
EDITED by @stephentoub on 4/26/2022 to update proposal:
namespace System.Collections.Generic { public abstract class EqualityComparer<T> : IEqualityComparer<T>, IEqualityComparer { + public static EqualityComparer<T> Create(Func<T?, T?, bool> equals, Func<T, int> getHashCode); } }
To discuss:
getHashCode
optional such that the returned instance's GetHashCode would throwNotSupportedException
if it's used.From @ViIvanov on December 14, 2017 20:15
Now we have a
Create
method in Comparer<> class:I think, it will be usable to have an analog in EqualityComparer<>:
Copied from original issue: dotnet/coreclr#15526
The text was updated successfully, but these errors were encountered: