Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nullable tracking doesn't work well with linq #37468

Open
YairHalberstadt opened this issue Jul 25, 2019 · 21 comments
Open

nullable tracking doesn't work well with linq #37468

YairHalberstadt opened this issue Jul 25, 2019 · 21 comments

Comments

@YairHalberstadt
Copy link
Contributor

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'.

@vatsalyaagrawal vatsalyaagrawal added this to Backlog (Critical) in IDE Nullability Work via automation Jul 25, 2019
@vatsalyaagrawal vatsalyaagrawal removed this from Backlog (Critical) in IDE Nullability Work Jul 25, 2019
@sharwell
Copy link
Member

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.

@RikkiGibson
Copy link
Contributor

RikkiGibson commented Jul 25, 2019

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.

@YairHalberstadt
Copy link
Contributor Author

Related is dotnet/csharplang#2388

@YairHalberstadt
Copy link
Contributor Author

YairHalberstadt commented Jul 26, 2019

I raised an issue in CoreFx to see what they thought of adding a WhereNotNull method:

https://github.com/dotnet/corefx/issues/39798

@rosenbjerg
Copy link

I don't see how the expected behaviour is automatic casting/conversion from string? to string.
What has led you to expect this?

@YairHalberstadt
Copy link
Contributor Author

@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.

@rosenbjerg
Copy link

@YairHalberstadt I agree that it makes sense for this simple use case, but won't it be hard to get consistent analysis results for more complex scenarios? That might not actually be a problem, but I would value consistent behaviour higher than extra "smartness" in certain cases

@Spongman
Copy link

I'm having the same issue, but with IObservable<T?>.Where(t => t != null). the solution is not quite as obvious.

@jaredpar jaredpar modified the milestones: Compiler.Net5, 16.8 Jun 23, 2020
@jaredpar jaredpar modified the milestones: 16.8, Compiler.Next Jul 10, 2020
@redodin2
Copy link

redodin2 commented Aug 27, 2020

Looks like an extension method lost the context. However, you can use query syntax

public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> that) 
    where T : class => from item in that where item != null select item;

@SpaceOgre
Copy link

SpaceOgre commented Nov 18, 2020

Is there a fix for problems like this:

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

Atm I get a CS8604 on new MyClass(x.Prop) where Prop obviously can't be null. To a pragma or something for every case like this is not good since it makes the code harder to read.

And I would rather not have to write code like this just to not get the warning:

var res = new List<MyClass>();
foreach (var item in list)
{
	if (item.Prop== null) continue;
	res.Add(new MyClass(item.Prop));
}

At this stage I'm just thinking of hiding the CS8604 warning so I don't have to deal with all the extra issues it causes and that is bad.

Oh and why is this not giving me an warning 🤔:

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

@canton7
Copy link
Contributor

canton7 commented Nov 18, 2020

@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!));

@SpaceOgre
Copy link

@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!));

Awsome! Thank you 👍

@sjb-sjb
Copy link

sjb-sjb commented Nov 26, 2020

Here's another example:

 Delegate[] invocationList = userCallback.GetInvocationList(); // userCallback is a multicast delegate
 [...] from @delegate in invocationList where @delegate?.Target != null select new WeakReference<object>(@delegate.Target) [...]

The WeakReference constructor requires a non-nullable reference argument, and the where clause means that the argument is definitely not null, yet C# 9 gives a warning that the argument is possibly null.

The reason I don't want to just throw ! onto the @delegate.Target is that our coding standard says ! can only be added in places where an exception will occur if the expression is null -- e.g. just before a dereference. The purpose of that standard is to prevent null from being accidentally propagated around and throughout the code, which after all is one of the main reasons for having nullability checking in the first place.

@Pzixel
Copy link

Pzixel commented Aug 10, 2021

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

@kapsiR
Copy link

kapsiR commented Sep 4, 2021

It seems there is a similar issue with Task<T> and Task<T?>.

warning CS8620: Argument of type 'Task' cannot be used for parameter 'task' of type 'Task<string?>' in 'Task Extensions.Bar(Task<string?> task)' due to differences in the nullability of reference types.

SharpLab example (Task<string> as parameter for a method that accepts Task<string?>)

Should I create a separate issue for that case?

@RikkiGibson
Copy link
Contributor

See dotnet/csharplang#3950 and related issues.

@VAllens
Copy link

VAllens commented Dec 23, 2021

This scene, can be tracked?

image

class Program {
    static void Main(string[] args) {
        List<Model> models = new();
        models.Add(new Model {
            Id = Guid.NewGuid()
        });
        models.Add(new Model {
            Id = Guid.NewGuid(),
            Address = "Address1"
        });

        List<string> addresses = models.Where(x => x.Address != null).Select(x => x.Address).ToList();
    }
}

@RikkiGibson
Copy link
Contributor

The nullability improvements working group discussed this issue yesterday.

@mungojam
Copy link

mungojam commented Nov 3, 2022

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

@RikkiGibson
Copy link
Contributor

I think if we didn't want to introduce any "real" tracking of the query variable, I would personally prefer adding WhereNotNull over only recognizing .Where(x => x != null).

@TKharaishvili
Copy link

In TypeScript I do this:

strings.flatMap(x => x != null ? [x] : [])

looks much uglier in C#:

strings.SelectMany(x => x != null ? new[] { x } : Array.Empty<string>())

but maybe that could be helped by this - dotnet/csharplang#5354

my two cents...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Development

No branches or pull requests