csharplang/proposals/csharp-7.3/tuple-equality.md
Bill Wagner f69b012846 markdown fixes for 7.3 proposals
These were:

1. adding `csharp` as the language identifier.
1. making relative links to content published on docs.microsoft.com
1. small grammar fixes.
2019-03-07 16:04:57 -05:00

5.8 KiB

Support for == and != on tuple types

Allow expressions t1 == t2 where t1 and t2 are tuple or nullable tuple types of same cardinality, and evaluate them roughly as temp1.Item1 == temp2.Item1 && temp1.Item2 == temp2.Item2 (assuming var temp1 = t1; var temp2 = t2;).

Conversely it would allow t1 != t2 and evaluate it as temp1.Item1 != temp2.Item1 || temp1.Item2 != temp2.Item2.

In the nullable case, additional checks for temp1.HasValue and temp2.HasValue are used. For instance, nullableT1 == nullableT2 evaluates as temp1.HasValue == temp2.HasValue ? (temp1.HasValue ? ... : true) : false.

When an element-wise comparison returns a non-bool result (for instance, when a non-bool user-defined operator == or operator != is used, or in a dynamic comparison), then that result will be either converted to bool or run through operator true or operator false to get a bool. The tuple comparison always ends up returning a bool.

As of C# 7.2, such code produces an error (error CS0019: Operator '==' cannot be applied to operands of type '(...)' and '(...)'), unless there is a user-defined operator==.

Details

When binding the == (or !=) operator, the existing rules are: (1) dynamic case, (2) overload resolution, and (3) fail. This proposal adds a tuple case between (1) and (2): if both operands of a comparison operator are tuples (have tuple types or are tuple literals) and have matching cardinality, then the comparison is performed element-wise. This tuple equality is also lifted onto nullable tuples.

Both operands (and, in the case of tuple literals, their elements) are evaluated in order from left to right. Each pair of elements is then used as operands to bind the operator == (or !=), recursively. Any elements with compile-time type dynamic cause an error. The results of those element-wise comparisons are used as operands in a chain of conditional AND (or OR) operators.

For instance, in the context of (int, (int, int)) t1, t2;, t1 == (1, (2, 3)) would evaluate as temp1.Item1 == temp2.Item1 && temp1.Item2.Item1 == temp2.Item2.Item1 && temp2.Item2.Item2 == temp2.Item2.Item2.

When a tuple literal is used as operand (on either side), it receives a converted tuple type formed by the element-wise conversions which are introduced when binding the operator == (or !=) element-wise.

For instance, in (1L, 2, "hello") == (1, 2L, null), the converted type for both tuple literals is (long, long, string) and the second literal has no natural type.

Deconstruction and conversions to tuple

In (a, b) == x, the fact that x can deconstruct into two elements does not play a role. That could conceivably be in a future proposal, although it would raise questions about x == y (is this a simple comparison or an element-wise comparison, and if so using what cardinality?). Similarly, conversions to tuple play no role.

Tuple element names

When converting a tuple literal, we warn when an explicit tuple element name was provided in the literal, but it doesn't match the target tuple element name. We use the same rule in tuple comparison, so that assuming (int a, int b) t we warn on d in t == (c, d: 0).

Non-bool element-wise comparison results

If an element-wise comparison is dynamic in a tuple equality, we use a dynamic invocation of the operator false and negate that to get a bool and continue with further element-wise comparisons.

If an element-wise comparison returns some other non-bool type in a tuple equality, there are two cases:

  • if the non-bool type converts to bool, we apply that conversion,
  • if there is no such conversion, but the type has an operator false, we'll use that and negate the result.

In a tuple inequality, the same rules apply except that we'll use the operator true (without negation) instead of the operator true.

Those rules are similar the rules involved for using a non-bool type in an if statement and some other existing contexts.

Evaluation order and special cases

The left-hand-side value is evaluated first, then the right-hand-side value, then the element-wise comparisons from left to right (including conversions, and with early exit based on existing rules for conditional AND/OR operators).

For instance, if there is a conversion from type A to type B and a method (A, A) GetTuple(), evaluating (new A(1), (new B(2), new B(3))) == (new B(4), GetTuple()) means:

  • new A(1)
  • new B(2)
  • new B(3)
  • new B(4)
  • GetTuple()
  • then the element-wise conversions and comparisons and conditional logic is evaluated (convert new A(1) to type B, then compare it with new B(4), and so on).

Comparing null to null

This is a special case from regular comparisons, that carries over to tuple comparisons. The null == null comparison is allowed, and the null literals do not get any type. In tuple equality, this means, (0, null) == (0, null) is also allowed and the null and tuple literals don't get a type either.

Comparing a nullable struct to null without operator==

This is another special case from regular comparisons, that carries over to tuple comparisons. If you have a struct S without operator==, the (S?)x == null comparison is allowed, and it is interpreted as ((S?).x).HasValue. In tuple equality, the same rule is applied, so (0, (S?)x) == (0, null) is allowed.

Compatibility

If someone wrote their own ValueTuple types with an implementation of the comparison operator, it would have previously been picked up by overload resolution. But since the new tuple case comes before overload resolution, we would handle this case with tuple comparison instead of relying on the user-defined comparison.


Relates to relational and type testing operators Relates to #190