是否有一种方便的方法来过滤一系列 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());
如何以方便的方式编写此代码,以便:
- 没有可空性警告,因为类型
nonNullItems
被推断为 IEnumerable<string>
.
- 我不需要添加未经检查的不可空性断言,例如
item!
(因为我想从编译器完整性检查中受益,而不是依赖于我是一个没有错误的编码器)
- 我不添加运行时检查的不可空性断言(因为这在代码大小和运行时都是毫无意义的开销,并且在人为错误失败的情况下比理想情况晚)。
- 该解决方案或编码模式可以更普遍地应用于可空引用类型的其他项目序列。
我知道这个解决方案,它利用了 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
我有这样的代码:
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<string>
. - 我不需要添加未经检查的不可空性断言,例如
item!
(因为我想从编译器完整性检查中受益,而不是依赖于我是一个没有错误的编码器) - 我不添加运行时检查的不可空性断言(因为这在代码大小和运行时都是毫无意义的开销,并且在人为错误失败的情况下比理想情况晚)。
- 该解决方案或编码模式可以更普遍地应用于可空引用类型的其他项目序列。
我知道这个解决方案,它利用了 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