Champion issue: #190
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==
.
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 && temp1.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.
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.
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)
.
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 false
.
Those rules are similar to the rules involved for using a non-bool type in an if
statement and some other existing contexts.
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 typeB
, then compare it withnew B(4)
, and so on).
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.
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.
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 (§11.11) Relates to #190