3

(This is a question about C# nullable reference types and generics.)

I know that I can use NotNullIfNotNullAttribute to specify that the return value of my method may be null if, and only if, an input is null:

[return: NotNullIfNotNull(nameof(defaultValue))]
public T? GetValueOrDefault<T>(T? defaultValue)
{ 
    return GetValueFromSomewhere<T>() ?? defaultValue;
}

Is there something similar for methods returning IEnumerable<T>? I'd like to tell the compiler that all elements of the returned IEnumerable are non-null if the input parameter is non-null.

[return: ???]
public IEnumerable<T?> GetValuesOrDefault<T>(T? defaultValue)
{
    return GetValuesFromSomewhere<T>().Select(x => x ?? defaultValue);
}

Here's a MCVE (fiddle) to play around with:

#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics.CodeAnalysis;
                    
public class Program
{
    public static void Main()
    {
        string? s1 = GetValueOrDefault<string>(null);
        string s2 = GetValueOrDefault<string>("");   // works, C# realizes that the return value can't be null
        
        IEnumerable<string?> e1 = GetValuesOrDefault<string>(null);
        IEnumerable<string> e2 = GetValuesOrDefault<string>("");   // I want to get rid of the warning here
    }
    
    [return: NotNullIfNotNull(nameof(defaultValue))]
    public static T? GetValueOrDefault<T>(T? defaultValue)
    { 
        return GetValueFromSomewhere<T>() ?? defaultValue;
    }
    
    public static IEnumerable<T?> GetValuesOrDefault<T>(T? defaultValue)
    {
        return GetValuesFromSomewhere<T>().Select(x => x ?? defaultValue);
    }
    
    // Dummy placeholders for my MCVE. My real methods read values from my DB repository.
    private static T? GetValueFromSomewhere<T>() => default(T);
    private static IEnumerable<T?> GetValuesFromSomewhere<T>() => new T?[] { default(T) };
}
Heinzi
  • 167,459
  • 57
  • 363
  • 519
  • Does this help you? https://stackoverflow.com/questions/75475662/linq-where-doesnt-remove-null-from-type/75476487#75476487 – itsdaniel0 Mar 04 '23 at 09:44
  • 1
    @itsdaniel0 I would argue that question is not about how to filter out nulls. Actually nulls in the result are pretty much appropriate in some cases - see the `IEnumerable e1 = GetValuesOrDefault(null);` call. – Guru Stron Mar 04 '23 at 20:59

2 Answers2

2

I think what you want is already possible.

In this case, both the input and output whose nullability we want to modify are of type T, a type parameter on the enclosing method. We don't need [NotNullIfNotNull] here — we can just use method type argument inference to achieve what we want. SharpLab.

// ok
IEnumerable<string> a = GetValuesOrDefault(new[] { "a", null }, defaultValue: "b");
IEnumerable<string> b = GetValuesOrDefault(new[] { "a", "b" }, defaultValue: "b");

// warning: Nullability of reference types in value of type 'IEnumerable<string?>' doesn't match target type 'IEnumerable<string>'.
IEnumerable<string> c = GetValuesOrDefault(new[] { "a", null }, defaultValue: null);


IEnumerable<T> GetValuesOrDefault<T>(IEnumerable<T?> valuesFromSomewhere, T defaultValue)
{
    return valuesFromSomewhere.Select(x => x ?? defaultValue);
}

This gives us the point of control we want here: If the defaultValue going in is non-nullable, the result IEnumerable has a non-nullable type argument. If the defaultValue going in may be null, the result IEnumerable has a nullable type argument.

Guru Stron
  • 102,774
  • 10
  • 95
  • 132
Rikki Gibson
  • 4,136
  • 23
  • 34
1

No, sadly but currently there is no such thing (at least via attributes). There same issue can be observed with tasks, which is discussed here (or this one), but based on comments it seems that at the moment only task-specific and async-await case are taken into consideration (see also this discussion).

Guru Stron
  • 102,774
  • 10
  • 95
  • 132