Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How should I deal with the problem about generics with the nullable option from C# 8.0?

Tags:

c#

Recently I have a trouble between if a generic type is a class or a struct, in detail, when I declare T to have neither constraint of class nor struct and also the #nullable option is enabled then the generic type gets some unexpected behaviors. For example, if I construct a member with the type T? and T is specified to a non-nullable struct type, the T? type does not operate as a nullable struct. Here's an explanation code about it:

public class MyClass<T> {
    ...
    public MyClass (T value, T? another) { ... }

    public void DoExample () {
        MyClass<int> a = new MyClass<int>(1, null); /* causes the CS1503 error, indicating int cannot receive null.
        In other words, T? is still int, not the nullable int.*/
    }
}

What and how should I do to settle this problem?


Maybe Solved

Finally I've found an incomplete but well-reasonable solution to resolve it. The following would help us approach the problem.

public class NullableWrapper<T> where T : notnull /* this constraint is not necessary
but if you want every type that specifies T to be non-nullable
and/or would not like to consider the null reference exception which may arise at Equals,
GetHashCode, and ToString, that would modify the type accuracy and/or your need. */ {
    public T Value { get; set; } = default!;

    internal NullableWrapper (T value) => this.Value = value;

    public override bool Equals (object? obj) => this.Value.Equals(obj);
    public override int GetHashCode () => this.Value.GetHashCode();
    public override string? ToString () => $"NullableWrapper({this.Value})";

    public static implicit operator NullableWrapper<T> (T itself)
        => new NullableWrapper<T>(itself);
    public static implicit operator T (NullableWrapper<T> itself)
        => itself.Value;
}

Then, my prior code is altered to what appears below:

public class MyClass<T> where T : notnull {
    ...
    public MyClass (T value, NullableWrapper<T>? another) { ... }

    public void DoExample () {
        MyClass<int> a = new MyClass<int>(1, null); // this causes none of error.
        MyClass<string> b = new MyClass<string>("hello", null); // similarly, no error happens.
        MyClass<double> c = new MyClass<double>(2.6, 5.2); // it's equivalent to the previous.
        MyClass<string> d = new MyClass<string>("asd", "def"); // as well.
    }
}
like image 662
YehHyunLee Avatar asked Mar 02 '23 18:03

YehHyunLee


1 Answers

  • Nullable reference types exist only at compile time: Foo<string> and Foo<string?> are compiled to the same Foo<System.String>.
  • Nullable value types are different: int is System.Int32, int? is System.Nullable<System.Int32>.

When you have class MyClass<T> without where T : struct, the compiler defaults to the nullable reference type behavior, where int? does not really make sense, but is still treated as Int32, and not Nullable<Int32> like you expect, and null is not a valid value for Int32.

It is not possible to have a generic type like this as of .NET 5 / C# 9. It works for either class or for struct, but not both.

@insane_developer provided a link to the LDM meeting where this was discussed: https://github.com/dotnet/csharplang/blob/master/meetings/2019/LDM-2019-11-25.md#problem-1-t-and-t-mean-different-things

Problem 1: T? and T? mean different things

The first problem is not technical, but one of perception and language regularity. Consider:

public T? M1<T>(T t) where T : struct;
public T? M2<T>(T t);

var i1 = M1(7); // i1 is int?
var i2 = M2(7); // i2 is int

The declaration of M1 is legal today. Because T is constrained to a (nonnullable) value type, T? is known to be a nullable value type, and hence, when instantiated with int, the return type is int?.

The declaration of M2 is what's proposed to allow. Because T is unconstrained, T? is "the type of default(T)". When instantiated with int the type of default(int) is int, so that is the return type.

In other words, for the same provided T these two methods have different return types, even though the only difference is that one has a constraint on T but the other does not.

The cognitive dissonance here was a major part of why we didn't embrace T? for unconstrained T.

like image 146
Pavel Tupitsyn Avatar answered May 08 '23 08:05

Pavel Tupitsyn