是否有一种方便的方法来过滤一系列 C# 8.0 可空引用,仅保留非空引用?

Is there a convenient way to filter a sequence of C# 8.0 nullable references, retaining only non-nulls?

我有这样的代码:

IEnumerable<string?> items = new [] { "test", null, "this" };
var nonNullItems = items.Where(item => item != null); //inferred as IEnumerable<string?>
var lengths = nonNullItems.Select(item => item.Length); //nullability warning here
Console.WriteLine(lengths.Max());

如何以方便的方式编写此代码,以便:

我知道这个解决方案,它利用了 C# 8.0 编译器中的流敏感类型,但它......不是很漂亮,主要是因为它太长且嘈杂:

var notNullItems = items.SelectMany(item => 
    item != null ? new[] { item } : Array.Empty<string>())
);

还有更好的选择吗?

不幸的是,您必须告诉编译器您比它更了解情况。

一个原因是 Where 方法没有以让编译器理解不可空性保证的方式注释,实际上也不可能对其进行注释。可能需要将额外的启发式方法添加到编译器以理解一些基本情况,例如这个,但目前我们没有。

因此,一种选择是使用 null 宽容运算符,俗称 "dammit operator"。然而,您自己触及了这一点,而不是在您 使用 集合的代码中到处撒上感叹号,而是可以在生成集合时增加一个额外的步骤,至少我,让它更美味:

var nonNullItems = items.Where(item => item != null).Select(s => s!);

这会将 nonNullItems 标记为 IEnumerable<string> 而不是 IEnumerable<string?>,从而在您的其余代码中得到正确处理。

我认为您必须以某种方式帮助编译器。调用 .Where() 永远不会安全地返回非空值。也许 Microsoft 可以添加一些逻辑来确定像您这样的基本场景,但据我所知,目前情况并非如此。

但是,您可以像这样编写一个简单的扩展方法:

public static class Extension
{
    public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> o) where T:class
    {
        return o.Where(x => x != null)!;
    }
}

我不知道这个答案是否符合您第 3 个要点的标准,但您的 .Where() 过滤器也不符合,所以...

替换

var nonNullItems = items.Where(item => item != null)

var nonNullItems = items.OfType<string>()

这将为 nonNullItems 生成 IEnumerable<string> 的推断类型,并且此技术可应用于任何可为 null 的引用类型。

正在考虑为 C# 10 提供 FWIW 特殊支持:https://github.com/dotnet/csharplang/issues/3951