Generics and Nullable (class vs struct)

Viewed 726

EDIT: see my update below on my stance on Null in C#8.0 before giving me options to patterns and such


Original question

I am trying to upgrade my base libraries to be "null enabled", aka using the C# 8.0 <Nullable>enable</Nullable> flag.

When trying to work with abstractions, and in specific generics, I ran into some problems. Consider the following code snippet which takes in an Action and converts this to a Func<TResult>. This is pre-nullable-enable:

public static Func<TResult> ToFunc<TResult>(this Action action)
        => () => { action(); return default; }
;

but post-nullable-enable I seem to struggle, since I cant make TResult nullable (or TResult?) as wit would require a constraint of either where TResult: class or where TResult: struct. There is no way I can combine these two constraints to let the compiler know that TResult can be a class or a valuetype. Which at the moment I find annoying - as one should be able to express it doesn't matter whether class or struct, it'll be nullable (regardless of previous .NET intherited design).

So, post-nullable-enable I seem to have only one option, and that is code duplication, examples below:

public static Func<TResult?> ToFuncClass<TResult>(this Action action)
        where TResult : class
        => () => { action(); return null; }
;

public static Func<TResult?> ToFuncStruct<TResult>(this Action action)
        where TResult : struct
        => () => { action(); return null; }
;

Both the code duplication as well as the naming schemes that come with it bother me a lot. I might be misunderstanding proper usage, or maybe I'm missing another feature of the spec, but how would you solve this?


UPDATE: In fact, I think I'd rather stick to my own "null-handling" implementations than using C#8.0's nullable feature. As a "Void" object or fleshed out "Option/Maybe/None"-solution seems to communicate things better. The only thing I am concerned about is that it isn't very helpful in moving the language forward, training new coders and introduces another third party/non native solutation to a universal provlem we all have dealing with null. Because own implementations of handling with null are great and all, but come up differently in each code base, need to be maintained by the community, and you have various different flavours. So it would be so helpful and hugely beneficial if the language enforced it fully, and if a standard rised. Which I hoped this would be - clearly it is not, and I understand. But I feel this is a missed opportunity.

Thx Yves

1 Answers

The reason is explained in Try out Nullable Reference Types in the section The issue with T?. Essentially, there's no way around this.

First, what is T when we use T?? Is it nullable or non-nullable ?

A natural definition of T? would mean, "any nullable type". However, this would imply that T would mean "any non-nullable type", and that is not true! It is possible to substitute a T with a nullable value type today (such as bool?).

Second, the types used in each case are different - a string? is still a string while an int? is a Nullable<int>. The generated concrete methods in each case are completely different. In one case you get a

Func<string> ToFuncClass<string>(this Action action)

In the other, you get a

Func<Nullable<int>> ToFuncStruct<int>(this Action)

Next, it’s important to note that a nullable reference type is not the same thing as a nullable value type. Nullable value types map to a concrete class type in .NET. So int? is actually Nullable. But for string?, it’s actually the same string but with a compiler-generated attribute annotating it. This is done for backwards compatibility. In other words, string? is kind of a "fake type", whereas int? is not.

The article's example demonstrates this :

This distinction between nullable value types and nullable reference types comes up in a pattern such as this:

void M<T>(T? t) where T: notnull

This would mean that the parameter is the nullable version of T, and T is constrained to be notnull.

If T were a string, then the actual signature of M would be :

M<string>([NullableAttribute] T t)

but if T were an int, then M would be

M<int>(Nullable<int> t)

These two signatures are fundamentally different, and this difference is not reconcilable.

Related