Convert tuple equality proposal into spec text (#3206)

This commit is contained in:
Rex Jaeschke 2020-02-18 12:38:36 -05:00 committed by Bill Wagner
parent 5ade4ff123
commit c1693320d6

View file

@ -1,54 +1,59 @@
# 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;`).
This proposal specifies the changes required to the [C# 7.2 (draft) Language specification](../../spec/introduction.md) to support equality and inequality operations on tuples.
Conversely it would allow `t1 != t2` and evaluate it as `temp1.Item1 != temp2.Item1 || temp1.Item2 != temp2.Item2`.
## Relational and type-testing operators
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`.
> Add the following section after [Enumeration comparison operators](../../spec/expressions.md#enumeration-comparison-operators):
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==`.
### Tuple comparison operators
## Details
The predefined tuple equality operators are:
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.
```csharp
bool operator ==(Tup1 t1, Tup2 t2);
bool operator !=(Tup1 t1, Tup2 t2);
```
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 any tuple or nullable tuple types `Tup1` and `Tup2`, `t1` and `t2` shall have the same number of elements, and operators `==` and `!=` shall be defined for the types of each corresponding element pair. Consider the following:
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.
The result of `==` is `true` if the values in each corresponding element pair compare equal using the operator `==` for their types. Otherwise, the result is `false`.
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.
The result of `!=` is `true` if any comparison of the values in each corresponding element pair compare unequal using the operator `!=` for their types. Otherwise, the result is `false`.
Both operands 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.
### 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.
Element names are ignored during tuple comparison.
### Tuple element names
When a tuple literal is used as an operand, it takes on a converted tuple type formed by the element-wise conversions that 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.
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)`.
In the nullable tuple case, additional checks for `t1.HasValue` and/or `t2.HasValue` shall be performed.
### Non-bool element-wise comparison results
When an element-wise comparison returns a non-`bool` result, if that comparison is dynamic in a tuple equality, a dynamic invocation of the operator `false` shall be used with the result being negated to get a `bool`.
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 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.
- if the non-bool type converts to `bool`, that conversion is applied,
- if there is no such conversion, but the type has an operator `false`, that is used the result is negated.
In a tuple inequality, the same rules apply except that we'll use the operator `true` (without negation) instead of the operator `false`.
In a tuple inequality, the same rules apply except that the operator `true` is used without negation.
Those rules are similar to the rules involved for using a non-bool type in an `if` statement and some other existing contexts.
When binding the `==` (or `!=`) operator, the usual rules are: (1) dynamic case, (2) overload resolution, and (3) fail. However, in the case of tuple comparison, a new rule is inserted between (1) and (2): if both operands of a comparison operator are tuples, the comparison is performed element-wise. This tuple equality is also lifted onto nullable tuples. If prior to the addition of tuple comparison to C#, a program defined `ValueTuple` types with `==` or `!=` operators, those operators would have been chosen by overload resolution. However, with the addition of comparison support and the new rule above, the comparison is handled by tuple comparison instead of the user-defined comparison.
## 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).
Regarding the order of evaluation of `t1` and `t2`, `t1` is evaluated first followed by `t2`, then the element-wise comparisons going from left-to-right. Consider the following.
If there is a conversion from type `A` to type `B` and a method `(A, A) GetTuple()`, the comparison
```csharp
(new A(1), (new B(2), new B(3))) == (new B(4), GetTuple())
```
is evaluated thus:
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)`
@ -56,22 +61,13 @@ For instance, if there is a conversion from type `A` to type `B` and a method `(
- `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.
> Add the following to the end of section [Equality operators and null](../../spec/expressions.md#Equality-operators-and-null):
### Comparing a nullable struct to `null` without `operator==`
### Equality operators and null
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
In tuple equality, expressions such as `(0, null) == (0, null)` and `(0, null) != (0, null)` are allowed with neither `null` nor the tuple literals having a type.
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](../../spec/expressions.md#relational-and-type-testing-operators)
Relates to [#190](https://github.com/dotnet/csharplang/issues/190)
Consider the case of a type `struct S` without `operator==`. The comparison `(S?)x == null` 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 also allowed.