Skip to content
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

C#6 draft spec specification discrepancy in "Reference type equality operators" section for nullable value types #3359

Closed
michkot opened this issue Apr 14, 2020 · 4 comments
Labels

Comments

@michkot
Copy link

michkot commented Apr 14, 2020

A sub-heading of the issue could be: reference type vs. value type vs. nullable (value/reference?) type

Basic problem: in a generic class with unconstrained T, for some T x, x == null is NOT always false for value types: when T is nullable value type, it depends on the HasValue property!

The current publicly searchable specification (C#6 draft) differs from C#5 (final spec) and (most probably) the compiler/JIT reality.
It's quite unfortunate the C#6 draft still contains error comming from the original MS C#5 spec, which were corrected in the finalized/ECMA C#5 spec.

Refers to dotnet/docs#8251 (I read it while being half-way through writing this issue 🙄... After googling "C# language specification", going on the end of the page, finding "Formal C# Language Specification" issue 😢)

The "current public spec" https://github.com/dotnet/csharplang/blob/892af9016b/spec/expressions.md#reference-type-equality-operators (current version) being fed into
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#reference-type-equality-operators
which claims to be draft of C#6 spec, says:

Reference type equality operators

...
The predefined reference type equality operators require one of the following:

  • Both operands are a value of a type known to be a reference_type or the literal null. Furthermore, an explicit reference conversion (Explicit reference conversions) exists from the type of either operand to the type of the other operand.
  • One operand is a value of type T where T is a type_parameter and the other operand is the literal null. Furthermore T does not have the value type constraint.

...
Notable implications of these rules are:

...

  • If an operand of a type parameter type T is compared to null, and the run-time type of T is a value type, the result of the comparison is false.

...
The following example checks whether an argument of an unconstrained type parameter type is null.

class C<T>
{
    void F(T x) {
        if (x == null) throw new ArgumentNullException();
        ...
    }
}

The x == null construct is permitted even though T could represent a value type, and the result is simply defined to be false when T is a value type.

While the C#5 spec says:

12.11.7 Reference type equality operators
...
• Both operands are a value of a type known to be a reference-type or the literal null. Furthermore, an
explicit identity or reference conversion (§11.3.5) exists from either operand to the type of the other
operand.
• One operand is the literal null, and the other operand is a value of type T where T is a typeparameter that is not known to be a value type, and does not have the value type constraint.
o If at runtime T is a non-nullable value type, the result of == is false and the result of != is true.
o If at runtime T is a nullable value type, the result is computed from the HasValue property of the
operand, as described in (§12.11.10).
o If at runtime T is a reference type, the result is true if the operand is null, and false otherwise.
...
[Example: The following example checks whether an argument of an unconstrained type parameter type is null.
same code here
The x == null construct is permitted even though T could represent a non-nullable value type, and the result is simply defined to be false when T is a non-nullable value type. end example]

There are more differences between C#5 and C#6 draft in these sections, for example C#5 spec "guarantees" that no boxing occurs:

Operands of predefined reference type equality operators are never boxed.

=============
Note: There is also a section about [Nullable] == null in the spec, and that one is the same in both. This section does not apply to the problem, it only applies when the expression is (known to be) of a nullable type.

C#6 draft
C#5: 12.11.10 Equality operators between nullable value types and the null literal

Edit: removed the citation of "Equality operators and null" / "12.11.10 Equality operators between nullable value types and the null literal" sections as they are not vital to the issue.
Edit #2: I made the important parts of citations bold and the introduction of issues more clear.

@bbarry
Copy link
Contributor

bbarry commented Apr 14, 2020

I don't see a problem.

For an operation of one of the forms

x == null
null == x
x != null
null != x

where x is an expression of a nullable type, if operator overload resolution (Binary operator overload resolution) fails to find an applicable operator, the result is instead computed from the HasValue property of x. Specifically, the first two forms are translated into !x.HasValue, and last two forms are translated into x.HasValue.

This doesn't apply to the case of

class C<T>
{
    void F(T x) {
        if (x == null) throw new ArgumentNullException();
        ...
    }
}

because x here is not a nullable type. It is an unconstrained generic type and therefore equality to it is defined to be reference equality.

@michkot
Copy link
Author

michkot commented Apr 14, 2020

@bbarry Yes, that makes the Equality operators and null / 12.11.10 Equality operators between nullable value types and the null literal of the respective specs not apply to the generic case. The issue stems from the (12.11.7) Reference type equality operators. I will make an edit/add note to the issue.

@michkot
Copy link
Author

michkot commented May 15, 2020

The same problem also occurs in this docs section:
https://docs.microsoft.com/cs-cz/dotnet/csharp/programming-guide/generics/constraints-on-type-parameters#unbounded-type-parameters

the comparison will always return false if the type argument is a value type.

false! should be something like

the comparison will always return false if the type argument is a non-nullable value type.

@BillWagner
Copy link
Member

closing as this is fixed in the dotnet/csharpstandard repo for the C# 6 draft.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants