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 int
The declaration of
M1
is legal today. BecauseT
is 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
M2
is what's proposed to allow. BecauseT
is unconstrained,T?
is "the type ofdefault(T)
". When instantiated withint
the type ofdefault(int)
isint
, 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 onT
but 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