-
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
ValueTuple throws for null comparer, unlike other BCL APIs #21168
Comments
cc: @stephentoub @ianhays @danmosemsft |
I think @jcouv owns VAlueTuple. |
Given that |
How does |
@jcouv It's not a number of types. Out of dozens of types, the odd ones out now are ValueTuple, Tuple, and Array, and only when they are accessed via IStructuralEquatable and IStructuralComparable. And it has nothing to do with the value being compared. The BCL convention is for null comparer to indicate the default comparer, for good reasons listed in the original post. |
Thanks for the clarification. I'll follow up this week with @stephentoub and @AlexGhiondea to come to a decision for |
Sounds like a reasonable proposal, although not high priority. In any case, I most likely will not be able to get to this in the next couple of weeks to catch this wave. |
@jcouv I'm motivated to get this through. I'll go ahead and PR if that's okay. |
@jnm2 That'd be awesome! Thanks for offering :-) Note that tests live in coreFX. To test a coreCLR change against existing coreFX tests, see instructions. While you're in there, would you consider making this optimization (https://github.com/dotnet/corefx/issues/18666, suggested by @azhmur)? |
@jcouv @omariom says 18666 is waiting for https://github.com/dotnet/coreclr/issues/6688. Can you clarify? |
@jnm2 Separate PR is probably safer. Thanks Yes, I think doing all three types ( For the ValueTuple optimization, the optimization would not be required if the runtime could do it itself (dotnet/coreclr#6688). But I don't expect the runtime issue to make progress soon, so it is good to optimize from the library itself. |
@jcouv Which is better? Acts just like the comparerless bool IStructuralEquatable.Equals(object other, IEqualityComparer comparer)
{
if (other == null || !(other is ValueTuple<T1, T2, T3>)) return false;
var objTuple = (ValueTuple<T1, T2, T3>)other;
return (comparer ?? EqualityComparer<T1>.Default).Equals(Item1, objTuple.Item1)
&& (comparer ?? EqualityComparer<T2>.Default).Equals(Item2, objTuple.Item2)
&& (comparer ?? EqualityComparer<T3>.Default).Equals(Item3, objTuple.Item3);
} Fewer branches, acts just like the comparerless bool IStructuralEquatable.Equals(object other, IEqualityComparer comparer)
{
if (other == null || !(other is ValueTuple<T1, T2, T3>)) return false;
var objTuple = (ValueTuple<T1, T2, T3>)other;
if (comparer == null) return Equals(objTuple);
return comparer.Equals(Item1, objTuple.Item1)
&& comparer.Equals(Item2, objTuple.Item2)
&& comparer.Equals(Item3, objTuple.Item3);
} Fewer branches, acts more like the normal logic of using a single comparer, also boxes: bool IStructuralEquatable.Equals(object other, IEqualityComparer comparer)
{
if (other == null || !(other is ValueTuple<T1, T2, T3>)) return false;
var objTuple = (ValueTuple<T1, T2, T3>)other;
if (comparer == null) comparer = EqualityComparer<object>.Default;
return comparer.Equals(Item1, objTuple.Item1)
&& comparer.Equals(Item2, objTuple.Item2)
&& comparer.Equals(Item3, objTuple.Item3);
} |
I would typically lean towards |
@jnm2
I didn't understand that part. The comparison at this point is on elements, so |
@jcouv Right, I mixed myself up there. So we'd want to call |
@jcouv Last question, is this a worthwhile optimization? bool IStructuralEquatable.Equals(object other, IEqualityComparer comparer)
{
if (other == null || !(other is ValueTuple<T1, T2, T3>)) return false;
var objTuple = (ValueTuple<T1, T2, T3>)other;
if (comparer == null)
{
return object.Equals(Item1, objTuple.Item1)
&& object.Equals(Item2, objTuple.Item2)
&& object.Equals(Item3, objTuple.Item3);
}
else
{
return comparer.Equals(Item1, objTuple.Item1)
&& comparer.Equals(Item2, objTuple.Item2)
&& comparer.Equals(Item3, objTuple.Item3);
}
} Or, alternatively, should |
Other code in coreFX seems to use bool IStructuralEquatable.Equals(object other, IEqualityComparer comparer)
{
if (other == null || !(other is ValueTuple<T1, T2, T3>)) return false;
var objTuple = (ValueTuple<T1, T2, T3>)other;
comparer = comparer ?? EqualityComparer<object>.Default;
... Whether or not to cache, I'd also lean towards not caching absent benchmarking. |
That is not preferential to |
@jnm2 |
Sure, if that's what you want: if (comparer == null)
{
comparer = EqualityComparer<object>.Default;
}
return comparer.Equals(Item1, objTuple.Item1)
&& comparer.Equals(Item2, objTuple.Item2)
&& comparer.Equals(Item3, objTuple.Item3); However ValueTuple is full of this convention: int IStructuralComparable.CompareTo(object other, IComparer comparer)
{
if (other == null) return 1;
if (!(other is ValueTuple<T1, T2, T3>))
{
throw new ArgumentException(SR.Format(SR.ArgumentException_ValueTupleIncorrectType, this.GetType().ToString()), nameof(other));
}
var objTuple = (ValueTuple<T1, T2, T3>)other;
int c = comparer.Compare(Item1, objTuple.Item1);
if (c != 0) return c;
c = comparer.Compare(Item2, objTuple.Item2);
if (c != 0) return c;
return comparer.Compare(Item3, objTuple.Item3);
} |
Ok, never mind for braces then :-) |
Sure thing. I don't care, just checking. |
@jcouv When adding the null checks, I was going to go with |
The coreclr PR is up dotnet/coreclr#11345 |
@jnm2 I'm not sure either (between Just to keep you updated, the compat council is still discussing the question. I'll let you know once settles (hopefully with an approval). |
@jnm2 The compat discussion is still not settled, but it seems that we shouldn't update the implementation in coreFX. Otherwise, we'd be introducing a significant break from people moving from 4.6.x plus package (no NRE) to 4.7 (throws NRE). |
This was done with https://github.com/dotnet/corefx/issues/18432. I believe that this is par for the course and one of the reasons that https://github.com/terrajobst/platform-compat is being developed. |
The conclusion is that the benefits do not outweigh the costs. See dotnet/coreclr#11345 (comment) Thanks for the discussion and I look forward to the next one! =) |
Continuing from https://github.com/dotnet/corefx/issues/18432.
You would expect this to succeed, but it throws
NullReferenceException
:I'll quite often take advantage of the fact that all BCL APIs use
EqualityComparer<T>.Default
when you pass null, and chain constructors and other methods with the parameterIEqualityComparer<T> comparer = null
. If my own constructor or extension method takesIEqualityComparer<T> comparer = null
, I assume that I can pass that into the BCL method. It's not intuitive to make it the call site's responsibility to check for null and passEqualityComparer<object>.Default
or call one or the other BCL overload depending whethercomparer = null
.It's not critical since the workaround is to pass
comparer ?? EqualityComparer<object>.Default
instead ofcomparer
. This is an API gotcha that may go unnoticed until code is in the field though.ValueTuple<*>.IStructuralEquatable.Equals
andValueTuple<*>.IStructuralComparable.CompareTo
have no null comparer check. If it followed the convention set by all other BCL methods, it would useEqualityComparer<object>.Default
if you pass null.Array.IStructuralEquatable.Equals
andArray.IStructuralComparable.CompareTo
, andTuple<*>.IStructuralEquatable.Equals
andTuple<*>.IStructuralComparable.CompareTo
have the same problem. They are in coreclr. Should I open an issue over there?The text was updated successfully, but these errors were encountered: