roslyn: nullable tracking doesn't work well with linq

Version Used: master

Steps to Reproduce:

using System;
using System.Collections.Generic;
using System.Linq;

#nullable enable
public class C {
    public IEnumerable<string> WhereNotNull(IEnumerable<string?> strings)
    {
        return strings.Where(x => x != null);
    }
}

Expected Behavior:

No warning

Actual Behavior:

warning CS8619: Nullability of reference types in value of type ‘IEnumerable<string?>’ doesn’t match target type ‘IEnumerable<string>’.

About this issue

  • Original URL
  • State: open
  • Created 5 years ago
  • Reactions: 61
  • Comments: 21 (9 by maintainers)

Most upvoted comments

@rosenbjerg One of the aim of NRTs is to work well with existing C# patterns. Given that Where(x => x != null) is commonly used to return a collection containing non-null elements, NRTs should recognize this pattern, and treat the returned enumerable as having a non-nullable type parameter.

I hit this recently. I noticed that strings.OfType<string>() is functionally equivalent and works with nullable reference types, but I’m not sure it would perform as well.

I think a helper like this would be nice to have in the standard library for this very simple case:

static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> enumerable) where T : class
{
    foreach (var t in enumerable)
    {
        if (t != null)
        {
            yield return t;
        }
    }
}

It’s conceivable that nullable analysis could track flow state between lambdas or track the element null state of collections to support slightly more complex cases like the ones below:

class Widget
{
    string? Prop { get; set; }

    static void Query1(List<Widget> widgets)
    {
        // hypothetically, we carry over the WhenTrue state
        // from the Where clause over to the Select
        _ = widgets
            .Where(w => w.Prop != null)
            .Select(w => w.Prop.GetHashCode());
    }

    static void Query2(List<Widget?> widgets)
    {
        if (widgets.All(w => w != null)
        {
            // `widgets` is considered a List<Widget> here
            _ = widgets[0].ToString();
        }
    }
}

While I think this is worth thinking about, it seems very fraught and limited to a small subset of real-world queries. Ideally the user should be able to figure out why the compiler thinks something is null, so arguably it’s better to avoid doing the analysis even for the simple LINQ cases if it’s just going to cause confusion about why they’re getting warnings in this query but not that one.

To cover the main case, maybe the BCL could add a function .WhereNotNull().

Sometimes LINQ code doesn’t provoke a NRT warning while the equivalent method chain does:

using System;
using System.Collections.Generic;
using System.Linq;

#nullable enable

public class C {
    public void M(IEnumerable<string?> s) {
        var x = from c in s
        select c.Length;
    }
    
    public void N(IEnumerable<string?> s) {
        var x = s.Select(c => c.Length);
    }
}

Expected Behavior:

Both lines show “warning CS8602: Dereference of a possibly null reference.”

Actual Behavior:

LINQ version doesn’t produce any warnings and allows to silently defererence null

@SpaceOgre The easiest way to suppress individual nullability warnings is to use the null-forgiving operator !:

var myClasses = list
	.Where(x => x.Prop != null)
	.Select(x => new MyClass(x.Prop!));