C# notnull constraint with nullable types have unexpected behavior
Asked Answered
N

2

16

I have read about notnull constraint in C# and it was written that "This allows either value types or non-nullable reference types but not nullable reference types." (the quote is from "Programming C# - 10.0 By Ian Griffiths)

I tried checking this constraint in the code below:

MyTestClass<int?> instance1 = new MyTestClass<int?>();
MyTestClass<string?> instance2 = new MyTestClass<string?>();

public class MyTestClass<T> where T : notnull
{
    T Value { get; set; }

    public MyTestClass()
    {
        Value = default(T);
        if (Value == null)
            Console.WriteLine($"Type of T is {typeof(T)} and its default value is 'Null'");
        else
            Console.WriteLine($"Type of T is {typeof(T)} and its default value is {Value}");
    }
}

as you can see I instantiated my generic class with nullable types int? (nullable value type) and string? (nullable reference type) and it still works for me.

It also prints the output like this for me:

Type of T is System.Nullable`1[System.Int32] and its default value is 'Null'
Type of T is System.String and its default value is 'Null'
Type of T is System.Int32 and its default value is 0
Type of T is System.String and its default value is 'Null'"

It behaves 'string?' as 'string' and detects both as non-nullable. what can be the reason for these to happen?

Nonbelligerent answered 13/1 at 13:48 Comment(3)
Where is that quote from? The documentation here disagrees. Note that violating the constraint does not give you an error, just a warning.Rusticate
And non-nullable reference types are a compiler trick, nothing at runtimeAlbie
@Rusticate The quote is from "Programming C# 10.0" book by Ian Griffiths.Nonbelligerent
F
7

The notnull constraint, as defined, "limits the type parameter to non-nullable types. The type may be a value type or a non-nullable reference type." [1]

The constraint is available for code in a nullable enable context, and on compilation checks for type parameters that do not match the constraint (i.e. string?, int[]?), and creates a warning and not an error, specifically CS8714: Nullability of type argument doesn't match 'notnull' constraint.

So, programs will compile when breaking the notnull constraint, but will raise a compile-time warning when it can. It's important to mention that it only raises a warning when it can because "Generic declarations that include the notnull constraint can be used in a nullable oblivious context, but compiler does not enforce the constraint." [1]

So, nullable reference types can be passed as valid type parameters to type parameters specified with the notnull constraint, but should not be. A warning should be being raised in your demo unless it is not running in a #nullable enable context.

Sources:

  1. https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/where-generic-type-constraint
Ferdinandferdinanda answered 14/1 at 7:29 Comment(0)
N
9

In the past few days I have been searching about this question and found out that the reason for it could be:

  1. The notnull constraint in C# is used to ensure that a type parameter is not null. According to the official documentation, the notnull constraint can be applied to either value types or non-nullable reference types, but not nullable reference types

  2. This code instantiates a generic class with nullable types int? and string? and it still works. However, the 'notnull' constraint is not being enforced for these types because the 'notnull' constraint is only enforced at compile-time and not at runtime. Since C# 8.0, developers can annotate reference types as nullable (e.g., string?) to indicate to the compiler that the variable may hold null. This feature is known as nullable reference types. It is intended for static analysis by the compiler to issue warnings and does not affect the actual runtime type of the variable. The runtime type of string and string? is the same, and thus they both satisfy the notnull constraint.

  3. Even with the notnull constraint, nullable value types are allowed because they are represented as a System.Nullable struct, which is a value type itself and is not actually null. It just has the ability to represent null through a HasValue property. So the code compiles because int? is a Nullable struct and this is not a nullable reference type but a value type, which satisfies the notnull constraint.

Also via the link below you can check out documentation of notnull constraint.

https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/constraints-on-type-parameters

Nonbelligerent answered 13/1 at 13:52 Comment(1)
If "the 'notnull' constraint is only enforced at compile-time," why isn't it being enforced at compile time in this case?Handy
F
7

The notnull constraint, as defined, "limits the type parameter to non-nullable types. The type may be a value type or a non-nullable reference type." [1]

The constraint is available for code in a nullable enable context, and on compilation checks for type parameters that do not match the constraint (i.e. string?, int[]?), and creates a warning and not an error, specifically CS8714: Nullability of type argument doesn't match 'notnull' constraint.

So, programs will compile when breaking the notnull constraint, but will raise a compile-time warning when it can. It's important to mention that it only raises a warning when it can because "Generic declarations that include the notnull constraint can be used in a nullable oblivious context, but compiler does not enforce the constraint." [1]

So, nullable reference types can be passed as valid type parameters to type parameters specified with the notnull constraint, but should not be. A warning should be being raised in your demo unless it is not running in a #nullable enable context.

Sources:

  1. https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/where-generic-type-constraint
Ferdinandferdinanda answered 14/1 at 7:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.