1

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; }
    }
}
Cains
  • 23
  • 6
  • 1
    nullable value and reference types are *very different* types of things. You can't express "nullable irrespective of the type being a value or reference type" – Damien_The_Unbeliever Jan 19 '21 at 08:15
  • 1
    "*I don't think this constraint should be necessary*" - What you think and what the specs say are 2 different things – TheGeneral Jan 19 '21 at 08:17
  • 1
    To hammer home the point, I'd suggest you modify `Test` as suggested in the answers given (constrained to `struct`), and also create a `Test2` constrained to `class`. Compile both and then decompile with ILDASM. You'll see that the code for both is *different*. – Damien_The_Unbeliever Jan 19 '21 at 08:31
  • @Damien_The_Unbeliever if what I want is impossible in the current compiler then that's fine, I just want to know for sure as what I desire should theoretically be possible. – Cains Jan 19 '21 at 08:33
  • The question mark means something very different if the type in front of it is a value type or a reference type. It is just an artifact of the C# language that both look the same. – Klaus Gütter Jan 19 '21 at 08:34
  • @KlausGütter I realize this, I am just hoping that the compiler can take `reference?` and `value?` and be able to tell that in every possible case both should be assignable to null, so the assignment is fine. If it can't then it can't, I just want confirmation that there's no other way to get the compiler to realize this. – Cains Jan 19 '21 at 08:40
  • @OlivierRogier I already made the note "Note that I cannot constraint TOption to be a value type in my actual case which fixes this problem" which addresses your confusion. Can you be more clear on what exactly the difference is between my question body and my edits? – Cains Jan 19 '21 at 08:45
  • Thank you for taking the time to share your question. What you asking for is unclear and misleading. What is your goal & difficulty? What have you done so far? Please try to better explain your issue, dev env, data types & expected result, as well as to share more or less code (no screenshot), images or sketches of screens, user stories or scenario diagrams. To help you improve your requests please read [How do I ask a good question](https://stackoverflow.com/help/how-to-ask) & [Writing the perfect question](https://codeblog.jonskeet.uk/2010/08/29/writing-the-perfect-question). –  Jan 19 '21 at 08:50
  • OP wrote: "*I never stated that the generic must be an enum, I am asking how to assign this field to null when the generic happens to be an enum while still respecting this generic. Adding your constraints (`where TOption : struct, Enum`) would not be this same generic. – Cains*" –  Jan 19 '21 at 08:55
  • @OlivierRogier Can you be more specific about what you seek to add or don't understand about my question? Others here have clearly understood the question along with the edits, and I've provided a full example, my goal, and things I cannot do such as constraining the generic. What you have quoted is already explicitly stated in the question body. – Cains Jan 19 '21 at 08:56
  • 1
    @Cains This *is why all answered and commented* to solve your problem with *enums*, at first glance ... because of the *title, the code and the problem exposed*. And you've told everyone that's not suitable. Tons of comments means question needed details or clarity (I can't vote again). Thus I deleted my answer and upvoted @.00110001. So take a look at https://xyproblem.info/, please. –  Jan 19 '21 at 09:05
  • @OlivierRogier This was the case before my edits to the question body, but I have long since made edits clarifying the things you point out and others have answered accordingly. You're asking for clarifications that I have already added to the question, I'm not sure what else to add for you as you aren't being specific as to what is missing. – Cains Jan 19 '21 at 09:23
  • @Cains Title & code sample remain misleading, sorry. If you ask for eg to have `class OR struct, Enum` as generic type parameter contrainst to be nullable, there is a problem because no OR yet, and [generics](https://stackoverflow.com/questions/1208153/c-sharp-generics-compared-to-c-templates) are strongly typed as they are not [templates](https://stackoverflow.com/questions/15857544/what-are-the-differences-between-c-templates-and-java-c-generics-and-what-are): [MS Docs](https://docs.microsoft.com/dotnet/csharp/programming-guide/generics/differences-between-cpp-templates-and-csharp-generics) –  Jan 19 '21 at 09:32

4 Answers4

3

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; }
}
Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • @.Sweeper What is `notnull` and how a not null can be null here: `t != null` ? How this solve the OP problem compared to @.00110001 answer ? –  Jan 19 '21 at 09:37
  • 1
    It seems like what I want in an ideal world just isn't gonna happen with the CLR, despite all the new nullability additions to the C# language. I'll see if your work-around with a custom nullable type works in my case, otherwise I'll just have to re-design with this limitation in mind. – Cains Jan 19 '21 at 09:39
  • @OlivierRogier See the explanation of it in [this list](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/constraints-on-type-parameters). I put it there just to prevent nested nullables like `MyNullable`. – Sweeper Jan 19 '21 at 09:40
  • @.Sweeper I can't check in VS2017 but I don't understand how a not nullable can be null and solves the OP problem. –  Jan 19 '21 at 09:41
  • @Cains Do note that the built in `Nullable` has lots of syntactic sugar and (un)boxing optimisations, which your own `Nullable` won't have, which is why this is an "ugly" workaround. – Sweeper Jan 19 '21 at 09:43
  • @OlivierRogier That's like asking how on earth can `Nullable` represent a nullable value when `T` must be a non-nullable struct... The fundamental problem here is that there is no way to say "a nullable type, I don't care whether it's value or reference" that the CLR can understand. So one way is to just create such a type. – Sweeper Jan 19 '21 at 09:46
1

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; }
    }
}
Klaus Gütter
  • 11,151
  • 6
  • 31
  • 36
  • I noticed this quickly after posting and made a note addressing this. TOption may not be a struct and I don't need it to be, it just happens to be one in this example. What I need is nullability, not a definite value type. – Cains Jan 19 '21 at 08:16
  • If TOption is a reference type, what should `Nullable` mean? – Klaus Gütter Jan 19 '21 at 08:17
  • I suppose `?` is just purely syntactic sugar then? Ideally I would hope that if it's a reference type it would just simply be the type as it is already nullable. What I'm actually after here is to make the field nullable -- which a reference type would already be, and a value type would be contained in `Nullable`. – Cains Jan 19 '21 at 08:19
  • See https://learn.microsoft.com/en-us/dotnet/api/system.nullable-1: `public struct Nullable where T : struct`: so Nullable works only with value types. – Klaus Gütter Jan 19 '21 at 08:24
  • I've made an edit above addressing your new suggestion. I think this may not be possible due to limitations of the language/compiler. A big detail here is that I am using nullable reference types as well, so I'm not sure if `?` is necessarily a straight drop-in for `Nullable`. – Cains Jan 19 '21 at 08:28
1

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.

halfer
  • 19,824
  • 17
  • 99
  • 186
TheGeneral
  • 79,002
  • 9
  • 103
  • 141
  • Unfortunately this isn't suitable to me as I also need non-nullable fields of that generic, I added further detail in my question on this. – Cains Jan 19 '21 at 08:50
0

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;
        }

    }
Benzara Tahar
  • 2,058
  • 1
  • 17
  • 21