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.
}
}
Foo<string> and Foo<string?> are compiled to the same Foo<System.String>.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 intThe declaration of
M1is legal today. BecauseTis constrained to a (nonnullable) value type,T?is known to be a nullable value type, and hence, when instantiated withint, the return type isint?.The declaration of
M2is what's proposed to allow. BecauseTis unconstrained,T?is "the type ofdefault(T)". When instantiated withintthe type ofdefault(int)isint, so that is the return type.In other words, for the same provided
Tthese two methods have different return types, even though the only difference is that one has a constraint onTbut the other does not.The cognitive dissonance here was a major part of why we didn't embrace
T?for unconstrainedT.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With