Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generics and Nullable (class vs struct)

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

like image 309
Yves Schelpe Avatar asked Oct 10 '19 07:10

Yves Schelpe


People also ask

Can struct be generic?

In addition to generic classes, you can also create a generic struct. Like a class, the generic struct definition serves as a sort of template for a strongly-typed struct. When you declare a variable of this struct type, you provide a type for its generic parameter.

Is Nullable a struct?

The Nullable type is an instance of System. Nullable<T> struct. Here T is a type which contains non-nullable value types like integer type, floating-point type, a boolean type, etc. For example, in nullable of integer type you can store values from -2147483648 to 2147483647, or null value.

Can a generic be null?

This means that you can put any object in a collection because all classes in the C# programming language extend from the object base class. Also, we cannot simply return null from a generic method like in normal method. Below is the error that a generic method will throw if we are trying to return null.

What are generic constraints in C#?

The where clause in a generic definition specifies constraints on the types that are used as arguments for type parameters in a generic type, method, delegate, or local function. Constraints can specify interfaces, base classes, or require a generic type to be a reference, value, or unmanaged type.


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.

like image 156
Panagiotis Kanavos Avatar answered Oct 26 '22 22:10

Panagiotis Kanavos