1

In Microsoft's nullability documentation, there appears to be conflicting information.

On this page, it says the following (important part in bold/italic):

Generic definitions and nullability

Correctly communicating the null state of generic types and generic methods requires special care. The extra care stems from the fact that a nullable value type and a nullable reference type are fundamentally different. An int? is a synonym for Nullable<int>, whereas string? is string with an attribute added by the compiler. The result is that the compiler can't generate correct code for T? without knowing if T is a class or a struct.

This fact doesn't mean you can't use a nullable type (either value type or reference type) as the type argument for a closed generic type. Both List<string?> and List<int?> are valid instantiations of List.

What it does mean is that you can't use T? in a generic class or method declaration without constraints. For example, Enumerable.FirstOrDefault<TSource>(IEnumerable<TSource>) won't be changed to return T?. You can overcome this limitation by adding either the struct or class constraint. With either of those constraints, the compiler knows how to generate code for both T and T?.

Ok, so if you want to use T? in a generic, you have to constrain it to either a struct or class. simple enough.

But Then in the following page, they say this (again, emphasis in bold/italic):

Specify post-conditions: MaybeNull and NotNull

Suppose you have a method with the following signature:

public Customer FindCustomer(string lastName, string firstName)

You've likely written a method like this to return null when the name sought wasn't found. The null clearly indicates that the record wasn't found. In this example, you'd likely change the return type from Customer to Customer?. Declaring the return value as a nullable reference type specifies the intent of this API clearly.

For reasons covered under Generic definitions and nullability that technique does not work with generic methods. You may have a generic method that follows a similar pattern:

public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)

You can't specify that the return value is T? [but the] method returns null when the sought item isn't found. Since you can't declare a T? return type, you add the MaybeNull annotation to the method return:

[return: MaybeNull]
public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)

The preceding code informs callers that the contract implies a non-nullable type, but the return value may actually be null. Use the MaybeNull attribute when your API should be a non-nullable type, typically a generic type parameter, but there may be instances where null would be returned.

However...

Even copying that code straight from the documentation and giving it a default implementation that simply returns null, it won't compile!

[return: MaybeNull]
public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)
    => null;

I tried the null-forgiving operator, null!, also mentioned in the first-linked page (under the section 'Initialize the property to null') but that didn't work. You can't use default either because that doesn't return null for value types like int which return zero instead as shown here:

[return: MaybeNull]
public static T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)
    => default;

var seq = new []{ 1, 2, 3 };
bool MyPredicate(int value) => false;
var x = Find(seq, MyPredicate);
Console.WriteLine($"X is {x}");

Output:

X is 0

So what am I missing here? How do you successfully implement their example code without resorting to using T? which requires type-constraining it to either class or struct? And if you did have to do that, then what's the point of MaybeNull?

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286

3 Answers3

2

Ok, so unfortunately, thanks to the difference between a nullable reference type and a nullable value type--a limitation that something like Swift doesn't have--what I'm after is not supported by C#.

Instead, as mentioned in my other answer, because of this you shouldn't use the 'returning null means no match' pattern. Instead, you should use a try-based pattern that returns a boolean and utilizes an out parameter for the value of interest (if any). Inside, you still use default (so you don't have to constrain T) but you have that extra boolean telling you whether the default itself should be ignored or not. This way will properly work for both class and struct types, and more importantly, for types such as int where the default is not null, but rather zero, it will help you differentiate between a predicate indicating it matched zero (return = true) or there was no passing predicate (return = false) and you can ignore that zero.

The trick is in using the NotNullWhen attribute to tell callers that the out parameter will never be null when the return value of the function is true, so as long as you check the return value before accessing the out parameter, you don't also have to check for null, and code-flow analysis will not display 'possible null' warnings either.

Here's the refactor of the above function...

public static bool TryFind<T>(this IEnumerable<T> items, Func<T, bool> predicate, [NotNullWhen(true)] out T result){
    
    foreach(var item in items){
        if(predicate(item)){
            result = item;
            return true;
        }
    }

    result = default;
    return false;
} 

Time to go refactor some old code!

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
0

Ok, experimenting more, there are two solutions to this. One, write two versions of your generic, one constrained to structs and the other to classes. You will have to change their name however because a constraint is not enough to differentiate function overloads.

The other way you don't have to constrain it, but you instead have to return default, not null in your implementation. But that still doesn't mean it will return null. It will only return null if that's the default for the passed-in type, meaning if you want null back, you have to also pre-process your non-nullable types into a nullable variant first before calling the generic.

While this works, I would call such an approach 'code-smell' and would instead change it to a 'Try'-based pattern. You would still return default, but as an out parameter, then return a boolean as the return value telling you if a match was actually found, or in other words, whether you should ignore that out parameter.

That way, for the non-nullable types, like int, you would still know there was no match due to the extra 'information' of the returned boolean which would address cases where zero itself could be a match. (i.e. the out as zero with a return as false means no match whereas an out as zero with a return of true means you found a match, zero.)

But back to the code...

Here's the example.

[return:MaybeNull] // Suppress compiler warning that 'default' may be null
public static T Find<T>(this IEnumerable<T> items, Func<T, bool> predicate){

    foreach(var item in items)
        if(predicate(item))
            return item;

    return default;
}

Now consider the following sequence of ints:

var seq = new []{ 1, 2, 3, 4 };

When you run this code...

bool MyIntPredicate(int value)
    => value == 5;

var x = seq.Find(MyIntPredicate);

Console.WriteLine($"X is {x}");

it outputs the following:

X is 0

However, if you want a null to indicate not found, you have to make T a nullable type, but that also means you need a predicate that takes a nullable type.

bool MyNullableIntPredicate(int? value)
    => value == 5;

// Convert the int array to a nullable-int array via `OfType`, then use the nullable-int predicate.
var y = seq.OfType<int?>().FirstOrDefault(MyNullableIntPredicate);

Console.WriteLine($"Y is {y?.ToString() ?? "null"}");

Running the above, you now get This outputs the following:

X is null

If you add 5 to the array, in both cases you get this...

X is 5
Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
0

MaybeNull attribute doesn't change return value it is only only warning (usually for people) that indicates "not found value be returned - not talking about null". Why is that? You can return null of T right? You can't. Structures doesn't have null value. And returning just default value of structures (for example int = 0) doesn't indicates that the default value is not "not found value". You can use T? But it may be cases that you can't or don't want to return T? (Nullable) Maybe because inheritence or whatever. So you can notify caller about MaybeNot and they can threat returned value with special case

Nice reading: https://endjin.com/blog/2020/07/dotnet-csharp-8-nullable-references-maybenull


-----------Edit----------- based on comments

Looks like only way how to avoid making two sepperate generic classes for reference types and structs is making generic class unconstrained T and methods that can return null mark as "T?"

 public static class NullableGeneric<T>
    {
        public static Dictionary<int,T> Values { get; set; } = new Dictionary<int,T>();
#nullable enable       
        public static T? TryFind(int id) 
        {
            var isFound = Values.TryGetValue(id, out T value);
            return isFound ? value : default; // default of T? is null even for structs;
        }
#nullable disable
    
    }

You can test this with

 NullableGeneric<int?>.Values.Add(1, 5);
 int? structType = NullableGeneric<int?>.TryFind(0); // null
 int? structType2 = NullableGeneric<int?>.TryFind(1); // 5

based of this blog

C# 9 update: The ? operator now works on unconstrained generic types. So, you should use T? instead of the [MayBeNull] attribute.

Another sulltion would be returning new custom class wich contains bool "isNotFound" property and value property. (you can use LanguageExt.Core wich contains Option class - similar to Nullable but it has way more features and allows both reference types and structs)

to be honest i find whole nullablity thing very tricky

JanH
  • 107
  • 1
  • 9
  • I think what frustrates me the most is that nullable value types are `Nullable` where nullable reference types are bolted on to existing types for compatibility reasons. This is in contrast to something like Swift where Nullable is the same for value and reference types, and reference types alone can never be null. Again, I know why they did it... for compatibility reasons, but I think it would have made more sense to standardize across behaviors rather than trying to add functionality because it would have avoided exactly this, making it feel 'bolted on' instead of integrated. – Mark A. Donohoe Nov 27 '20 at 19:04
  • [...continued] It just means I have to have two versions of my generics, one for `structs` and one for `classes`, both also specifying `notnull`, then I can freely use `T?` as the return type to get the desired behavior. – Mark A. Donohoe Nov 27 '20 at 19:06
  • You can have T? For both reference and value types no? – JanH Nov 27 '20 at 19:24
  • Yes, but they are actually two distinct underlying types so you can't use the same generic for them. Try it! It won't let you compile it if you try setting `null` as the return value. That's what I'm trying to work around. – Mark A. Donohoe Nov 28 '20 at 01:11
  • The above has always worked because default has always been allowed. That's not quite what I'm after. Consider writing a generic extension that you want to return `null` based on some condition, or to return the passed-in value of type T if the condition fails... `public static T NullIfFailed(this T value, Func predicate) => value != null && predicate(value) ? value : null;`. Now call that against an integer, but try to return null. You can't use `default` because that would return zero. You explicitly want it to return null. – Mark A. Donohoe Nov 30 '20 at 01:18
  • Also, I've been trying to find a reference to your comment about C#9 now supporting the unconstrained use of T? but haven't found it. Plus, I'm *using* C#9 and again, the above won't compile. I have to write it as two separate functions with one constrained to structs and the other constrained to classes to get it to work. – Mark A. Donohoe Nov 30 '20 at 01:19
  • You wrote "The above has always worked" and later you wrote "the above won't compile" . My code compiles on my machine with .net 5 (c# 9). You could share compile error with me in order to help you. Just few notes to your `public static T NullIfFailed` You need keep in mind if T is struct of course you can't return null because struct can't be null. You would have to return some new class CanBeNull instead of T – JanH Nov 30 '20 at 16:16
  • also default of T? when T is either struct or class will result in null. – JanH Nov 30 '20 at 16:22
  • returning `default` has always been allowed. That's what I meant. I was trying to write a function that could use either a class *or* struct for `T`, and, depending on some condition, to return `null`, or `T`, i.e. `T?`. My point is in C# you cannot use the same generic for both classes or structs and return null. In Swift, Nullable is its own type (it's actually an enum) and both structs and classes themselves can *never* be null, so you can use a single generic for all types. C# doesn't allow you to do that. – Mark A. Donohoe Dec 01 '20 at 07:55
  • To illustrate this again, start by write a 'FirstMatch' function that takes in an array of `int`s, and a predicate that takes an `int` and returns a `bool`. Then, if all iterations of the predicate over that array return false, return `null`, meaning nothing matched the predicate. To do so the return type has to be `int?` Now instead of writing it to take in `int`s, write it as a generic of `T`. It will no longer compile because you have to further say `T` is a struct or a class, meaning you need two versions of that function, one for structs and one for classes. Hope that makes sense now. – Mark A. Donohoe Dec 01 '20 at 07:57
  • You are right c# doesn't allow when T is struct or class that just T can't have value of null - no way around that your function should not return null then: options here 1) "Try" pattern: make function return bool with "is found" value and found value should be returned via out parameter - it is standard .net pattern, example `bool TryFindFirst(Func predicate, out foundNumber)` you should drop the idea of returning just null. 2) wrap the default value in custom "Nullable" class that allows both structs and classes – JanH Dec 02 '20 at 11:09
  • 1
    Just saw your another answer saying exactly what I just wrote. Good job – JanH Dec 02 '20 at 11:13
  • Yeah, frustrating that C# doesn't support proper nullable types, but at least they have the attributes to decorate your code, so it's a start. – Mark A. Donohoe Dec 02 '20 at 23:18
  • 1
    Ultimate solution is to use wrapper object indicating if inner value is "not found" because out parameters doesn't work when your method is async and all nullable magic is not intuitive or possible – JanH Dec 03 '20 at 09:54