62

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?

Neuron
  • 5,141
  • 5
  • 38
  • 59
Razzie
  • 30,834
  • 11
  • 63
  • 78
  • 3
    It has always been an issue that you can't write methods that both support `Nullable` 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 Aug 11 '19 at 23:44

4 Answers4

34

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

Marcus Kaseder
  • 948
  • 1
  • 8
  • 19
  • 9
    This is cool except that `MaybeNull` doesn't work with `Task` (it assumes that the Task may be null, not the value returned by the Task). So you are still stuck if you have an async generic function. – Ignacio Calvo Feb 05 '20 at 10:56
  • 1
    If you are using .NET Framework with C# 8 these kind of annotations are sadly not available. – HeikoG Mar 19 '20 at 14:13
  • @IgnacioCalvo Yes, that's unfortunate. But you could add a constraint to your generic implementation. For example, if you add `where T: notnull` the caller is only able to specify a non-nullable-type. If you do not specify the constraint, the caller is responsible for null-checks. It's bad, but should work. In that case, you can't use `default!` though... – Marcus Kaseder Mar 22 '20 at 11:42
  • 1
    @HeikoG: They are somehow..you have to create your own MaybeNullAttribute class, and the compiler accepts it. – duedl0r Jun 19 '20 at 00:52
  • 1
    seems like with `[return: MaybeNull]` the warning goes away even without `default!` – kofifus Jul 03 '20 at 09:49
  • One downside of the approach is that the method signature won't tell clients that the return value is nullable. They'll only know as soon as they try to dereference it without checking. That's not the case with the [other answer](https://stackoverflow.com/a/54594285/1025555), which will force you to implement the method twice, however. – Good Night Nerd Pride Aug 02 '20 at 10:33
  • Why wouldn't this signature tell clients that it's nullable? That's the very purpose of this attribute, unless there's a bug. I'm having a similar problem: what about Task GetAsync() in which case [return: MaybeNull] tells that Task can be null and doesn't solve the problem. Defining the method twice gives "Type already defines a member called 'Get' with the same parameter types". – Etienne Charland Sep 08 '20 at 21:06
  • There is a solution `#pragma warning disable CS8603` but that one does have the disadvantage of not telling callers that it's nullable! – Etienne Charland Sep 08 '20 at 21:22
  • In the case of `Task` return type, I'm opting for taking an extra `T defaultValue` parameter. This also allows properties like `int Speed` to have a default value of 1 instead of 0, and solves all issues regarding nullables. – Etienne Charland Sep 08 '20 at 21:47
  • This is no longer necessary in C# 9; consider updating your answer to stay relevant. – TheRubberDuck Oct 20 '21 at 19:30
  • @IgnacioCalvo: I have the same issue, but with the return type `(int, T)`, which is a `ValueTuple`. – Tobias Knauss Mar 12 '22 at 21:27
  • 1
    As @TheRubberDuck mentioned, the answer is outdated in C#9. Consider adding [this link](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/unconstrained-type-parameter-annotations) that explains why. – Ivan Danilov Nov 03 '22 at 01:14
29

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;
}
Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • 1
    @craig I think `!` is a "null assertion operator". It tells the compiler "I 'know' this is not null so don't give me a warning please". – Dave Cousineau Aug 11 '19 at 23:47
  • 1
    It's called the null-forgiving operator: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-forgiving – 321X Aug 06 '20 at 12:37
12

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.

Drew Noakes
  • 300,895
  • 165
  • 679
  • 742
  • Great! Do you know where in the C#9 spec this is mentioned? I can't really find what language change makes this possible. – Razzie Sep 22 '20 at 08:24
  • I don't believe a C# 9 spec exists yet. I can't find any reference on it, but you can try it out by adding the latest prerelease package of `Microsoft.Net.Compilers.Toolset`, or on sharplab.io ([example](https://sharplab.io/#v2:EYLgtghgzgLgpgJwD4GIB2BXANliwtwAEcaeBAsAFAACAzIdQEyEDCVA3lYdw/QCoB+QgFkAPHwB8ACgCUXHp0o9lDAOyEAJnABmEbDADc87gF8qJoA=)). – Drew Noakes Sep 24 '20 at 10:06
  • How will this work with generic return types, e.g. `Task` or `(int, T)` value tuple? I guess it does not. – Tobias Knauss Mar 12 '22 at 21:36
  • @TobiasKnauss can you link a gist or SO question showing what you're talking about? – Drew Noakes Mar 13 '22 at 01:34
  • e.g. method: `public static (int Index, T Element) IndexAndElementOfFirst (this IEnumerable i_collection, Func i_selector) { ... if (!i_collection.Any ()) return (-1, default); ...}` which should *not* be `T?` for value types. – Tobias Knauss Mar 14 '22 at 05:31
9

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
AlbertK
  • 11,841
  • 5
  • 40
  • 36
  • 7
    Oof. Good catch. So the writer of the `Get()` method, who now gets no warnings from the compiler in C# 9, might be lulled into thinking that when `T` is a struct, they can return a `Nullable` without extra effort. Whereas, in fact, the caller will get a non-null default value. Seems like a really nasty little oversight to me. – Matt Jenkins Feb 28 '21 at 14:53