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

Posted

技术标签:

【中文标题】有没有一种方便的方法来过滤一系列 C# 8.0 可空引用,只保留非空值?【英文标题】:Is there a convenient way to filter a sequence of C# 8.0 nullable references, retaining only non-nulls? 【发布时间】:2020-02-10 20:14:02 【问题描述】:

我有这样的代码:

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());

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

没有可空性警告,因为nonNullItems 类型被推断为IEnumerable&lt;string&gt;。 我不需要添加未经检查的不可为空性断言,例如 item!(因为我想从编译器的健全性检查中受益,而不是依赖于我是一个无错误的编码器) 我不添加运行时检查的不可为空性断言(因为这在代码大小和运行时都是无意义的开销,并且在人为错误失败的情况下比理想情况晚)。 解决方案或编码模式可以更广泛地应用于可为空引用类型的其他项目序列。

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

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

有更好的选择吗?

【问题讨论】:

这能回答你的问题吗? Using Linq's Where/Select to filter out null and convert the type to non-nullable cannot be made into an extension method 【参考方案1】:

我认为您必须以一种或另一种方式帮助编译器。调用.Where() 绝不会返回非空值。也许微软可以添加一些逻辑来确定像你这样的基本场景,但 AFAIK 现在不是这种情况。

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

public static class Extension

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

【讨论】:

对应的值类型是WhereNotNull&lt;T&gt;(this IEnumerable&lt;T?&gt; source) where T : struct =&gt; source.Where(t =&gt; t != null).Select(t =&gt; t.GetValueOrDefault()) @JeroenMostert 您是否有理由使用.GetValueOrDefault() 而不是.Value 所以这行得通:我对此感到惊讶,因为乍一看我认为这个尾随 ! 只断言有关可枚举的内容,而不是它的内容 - 但显然 ! 只是关闭检查整个类型(甚至表达式)? @EamonNerbonne: 是的:如果知道值不为空,GetValueOrDefault 会省略一个分支,而.Value 必须保留空检查。诚然,这是一个微优化。 @mu88:我当然不介意,如果我介意的话,我会提供我自己的答案。 :-)【参考方案2】:

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

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

因此,一种选择是使用 null 宽恕运算符,俗称“该死运算符”。但是,您自己会涉及到这一点,而不是在您使用该集合的代码上散布感叹号,您可以在生成集合的额外步骤中加入,至少对我而言,这使得它更可口:

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

这会将nonNullItems 标记为IEnumerable&lt;string&gt; 而不是IEnumerable&lt;string?&gt;,因此可以在其余代码中正确处理。

【讨论】:

如果需要不止一次,这种模式当然可以在扩展方法WhereNotNull(或者更确切地说是两个,同时处理可空值类型)中捕获。 由于很多编译器支持都是简单的类型推断,我希望编译器看到Where(s =&gt; s != null) 并自行进行推断,而不是添加额外的扩展方法。但是,它会在紧要关头。 我可以想象一个规则有很多潜在的问题,基本上必须根据传入的 lambda 更改 .Where() 的签名。这将特别令人困惑,因为它可能会因最轻微的复杂性而中断(传递任意编译的委托将不起作用,除非编译器“以防万一”添加更多上下文;不是Enumerable.Where 的代码可能表现不同等)很好,是的,但充满了复杂性。 @JeroenMostert 我怀疑它是否会被实施,但我实际上并不认为这些并发症特别严重。这同样适用于大多数流敏感类型。提取方法后,您无法轻易捕获流程。 所以就代码的可维护性而言,我非常谨慎地在所有地方添加!;这使得很难理解编译器仍在检查多少,以及没有检查多少。 @mu88 对只声明一次的方法的建议看起来更容易接受。【参考方案3】:

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

替换

var nonNullItems = items.Where(item =&gt; item != null)

var nonNullItems = items.OfType&lt;string&gt;()

这将为nonNullItems 生成IEnumerable&lt;string&gt; 的推断类型,并且此技术可以应用于任何可为空的引用类型。

【讨论】:

“第三个要点”我的意思是在过滤器之外没有 附加 检查,即没有像 Where(o =&gt; o != null).Select(o=&gt; o ?? throw ...) 这样的东西,因为虽然这会正确推断,但它效率低下,而且我我会做很多这样的事情,我可以手动仔细检查一个像Where(o=&gt;o!=null) 这样的微小而琐碎的实现——我只是不希望它感染整个代码库。 我确实考虑了 OfType 解决方案,但令人讨厌的是,我需要明确指定类型,以至于我不会真正调用 OfType&lt;&gt; 使用内联的普遍适用的策略。而且,如果您制作了自定义扩展方法或其他东西,您仍然会发现 OfType 非常慢的事实;在我的微基准测试中,一长串字符串要慢 4 倍;并且不寒而栗,您曾经敢将 valuetype 与 OfType 一起使用......所有这些拳击......【参考方案4】:

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

【讨论】:

以上是关于有没有一种方便的方法来过滤一系列 C# 8.0 可空引用,只保留非空值?的主要内容,如果未能解决你的问题,请参考以下文章

使用 c# 为 windows phone 8.0 编程录音机

C# 8.0 默认接口实现

C#各版本新增加功能

有没有一种方便的方法来引用 Svelte 组件中的 DOM 元素?

Elasticsearch7.8.0版本入门——JavaAPI操作(查询并字段过滤文档)

spamassassin检查分数C#代码