Nullable reference types with generic return type

Solution 1:

You were very close. Just write your method like this:

[return: MaybeNull]
public T Get<T>(string key)
{
    var wrapper = cacheService.Get(key);
    return wrapper.HasValue ? Deserialize<T>(wrapper) : default!;
}

You have to use the default! to get rid of the warning. But you can tell the compiler with [return: MaybeNull] that it should check for null even if it's a non-nullable type.

In that case, the dev may get a warning (depends on flow analytics) if he uses your method and does not check for null.

For further info, see Microsoft documentation: Specify post-conditions: MaybeNull and NotNull

Solution 2:

I think default! is the best you can do at this point.

The reason why public T? Get<T>(string key) doesn't work is because nullable reference types are very different from nullable value types.

Nullable reference types is purely a compile time thing. The little question marks and exclamation marks are only used by the compiler to check for possible nulls. To the eyes of the runtime, string? and string are exactly the same.

Nullable value types on the other hand, is syntactic sugar for Nullable<T>. When the compiler compiles your method, it needs to decide the return type of your method. If T is a reference type, your method would have return type T. If T is a value type, your method would have a return type of Nullable<T>. But the compiler don't know how to handle it when T can be both. It certainly can't say "the return type is T if T is a reference type, and it is Nullable<T> if T is a reference type." because the CLR wouldn't understand that. A method is supposed to only have one return type.

In other words, by saying that you want to return T? is like saying you want to return T when T is a reference type, and return Nullable<T> when T is a value type. That doesn't sound like a valid return type for a method, does it?

As a really bad workaround, you could declare two methods with different names - one has T constrained to value types, and the other has T constrained to reference types:

public T? Get<T>(string key) where T : class
{
    var wrapper = cacheService.Get(key);
    return wrapper.HasValue ? Deserialize<T>(wrapper) : null;
}

public T? GetStruct<T>(string key) where T : struct
{
    var wrapper = cacheService.Get(key);
    return wrapper.HasValue ? (T?)Deserialize<T>(wrapper) : null;
}

Solution 3:

In C# 9 you are able to express nullability of unconstrained generics more naturally:

public T? Get<T>(string key)
{
    var wrapper = cacheService.Get(key);
    return wrapper.HasValue ? Deserialize<T>(wrapper) : default;
}

Note there's no ! operator on the default expression. The only change from your original example is the addition of ? to the T return type.

Solution 4:

In addition to Drew's answer about C# 9

Having T? Get<T>(string key) we still need to distinguish nullable ref types and nullable value types in the calling code:

SomeClass? c = Get<SomeClass?>("key"); // return type is SomeClass?
SomeClass? c2 = Get<SomeClass>("key"); // return type is SomeClass?

int? i = Get<int?>("key"); // return type is int?
int i2 = Get<int>("key"); // return type is int