52
votes

I'm playing around a bit with the new C# 8 nullable reference types feature, and while refactoring my code I came upon this (simplified) method:

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

Now, this gives a warning

Possible null reference return

which is logical, since default(T) will give null for all reference types. At first I thought I would change it to the following:

public T? Get<T>(string key)

But this cannot be done. It says I either have to add a generic constraint where T : class or where T : struct. But that is not an option, as it can be both (I can store an int or int? or an instance of FooBar or whatever in the cache). I also read about a supposed new generic constraint where class? but that did not seem to work.

The only simple solution I can think of is changing the return statement using a null forgiving operator:

return wrapper.HasValue ? Deserialize<T>(wrapper) : default!;

But that feels wrong, since it can definitely be null, so I'm basically lying to the compiler here..

How can I fix this? Am I missing something utterly obvious here?

4
It has always been an issue that you can't write methods that both support Nullable<T> and reference types at the same time. This looks like just a continuation of that issue. The only good workaround I have found is writing both a Get and GetStruct version of these kinds of methods.Dave Cousineau

4 Answers

27
votes

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

25
votes

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

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.

4
votes

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