Reputation: 23
I'm using C# 9 and I ran into this odd problem, so I wrote a simple example below that demonstrates it. I need to set the value of a nullable enum to null, but I get an error when doing it through a generic type. If I hard-code the enum type into the class it works fine, so I'm not sure why it doesn't work when the same type is used as a generic. It seems like TOption?
is treated as TOption
with the nullable part ignored, which would make this error make sense as it would be trying to assign null to a non-nullable value type.
Is this a strange conflict between nullable types, value types, and compiler assumptions? Shouldn't TOption?
be treated exactly the same as Option?
when Option
is used as the generic type?
Note that I cannot constraint TOption
to be a value type in my actual case which fixes this problem, and I don't think this constraint should be necessary. I don't need TOption
to be a value type, I just need that field to be nullable -- regardless if it's a class or struct.
In regards to putting Option?
in for TOption
, I still need fields that treat it as non-nullable. So I cannot do this, I need the actual type in the generic but I need to be able to distinguish non-nullable and nullable fields of that type -- independent of the type being a struct or class. I should point out that I am using nullable reference types, so classes are treated as non-nullable unless specified with ?
.
public class Program
{
public static void Main(string[] args)
{
var test = new Test<Option>();
test.Option1 = null;
test.Option2 = null; // Cannot convert null to 'Program.Option' because it is a non-nullable value type
}
public enum Option { A, B, C }
public class Test<TOption>
{
public Option Option0 { get; set; }
public Option? Option1 { get; set; }
public TOption? Option2 { get; set; }
}
}
Upvotes: 1
Views: 4848
Reputation: 81533
The closest you can get to what you want without constraining to struct
, or using default
, it to supply the nullable type in the generic parameter (which may or may not be suitable to you)
var test = new Test<Option?>
{
Option1 = null,
Option2 = null // works
};
Console.WriteLine(test.Option2.HasValue);
The problem with this, is the generic class still has no idea whether it's constrained to a struct or a class internally, which may still limit you in various ways depending on what the use cases are here.
So based on your updated requirements, if you can't use a nullable type; you need a nullable generic instance property; and you can't constrain to a struct, then you may need to rethink your problem. The CLR can't work out at compile time that generic parameter you supply can be nullable, so will produce a compiler error.
Upvotes: 1
Reputation: 272895
The reason why this fails is very similar to the explanation I gave here. While in that case there was a workaround, in your case you actually want a nullable value type, which makes this rather impossible.
T?
means two very different things to the CLR depending on what T
is. If T
is a value type, it means Nullable<T>
. If T
is a reference type, T?
actually is the same as T
(with some attributes) as far as the CLR is concerned.
So when the compiler is compiling your code, what type would it say that Option2
is of? In other words, if you inspected the members of typeof(Test<>)
(note the open type) using reflection, what would be the type of the Option2
property?
In your ideal world, you would want Option2
to be of type Nullable<TOption>
when TOption
is a value type, and be of type TOption
when TOption
is a reference type. But if we were to inspect the type of the property of typeof(Test<>)
, which type would we get? It can't be both, can it?
In reality, the compiler chooses TOption
as the type of Option2
and treats Option2
's type to be nullable, but also keeping in mind that it could be a non-nullable value type too.
This is why it's not possible to achieve a "nullable value-and-reference type" just by saying T?
.
A rather ugly workaround is to create your own Nullable<T>
that doesn't constraint T
to value types:
struct MyNullable<T> where T: notnull {
private T value;
public bool HasValue {
get;
}
public T Value {
get {
if (HasValue) {
return value;
} else {
throw new InvalidOperationException("Value is not present!");
}
}
}
public MyNullable(T t) {
value = t;
HasValue = t != null;
}
}
Now you can do:
public class Test<TOption>
{
public Option Option0 { get; set; }
public Option? Option1 { get; set; }
public MyNullable<TOption> Option2 { get; set; }
}
Upvotes: 3
Reputation: 2207
Why not something like this?
public class Program
{
public enum EnumOption { A, B, C }
public class ClassOption { public int A { get; set; } }
public interface InterfaceOption { public int A { get; set; } }
public struct StructOption { int A; }
public class Test<TOption>
{
public TOption GenericOption { get; set; }
}
// main entry
public static void Main(string[] args)
{
// reference type type
new Test<ClassOption>().GenericOption = null;
new Test<InterfaceOption>().GenericOption = null;
// nullable value type
new Test<StructOption?>().GenericOption = null;
new Test<EnumOption?>().GenericOption = null;
new Test<int?>().GenericOption = null;
// non nullable value type, use default for init/comparison
new Test<StructOption?>().GenericOption = default;
new Test<EnumOption?>().GenericOption = default;
new Test<int?>().GenericOption = default;
}
}
Upvotes: 0
Reputation: 12007
Nullable<T>
requires T
to be a value type (see documentation) but this cannot be derived by the compiler from your generic type. So you need to help him using a constraint:
public class Test<TOption> where TOption : struct
{
public Option? Option1 { get; set; }
public TOption? Option2 { get; set; }
}
EDIT: As you now say that you cannot rely on TOption being a value type, you need to restructure the whole thing by specifying Nullable
in the generic type instantiation instead of the definition:
public class Program
{
public static void Main(string[] args)
{
var test = new Test<Option?>();
test.Option1 = null;
test.Option2 = null;
}
public enum Option { A, B, C }
public class Test<TOption>
{
public Option? Option1 { get; set; }
public TOption Option2 { get; set; }
}
}
Upvotes: 1