首页 > 解决方案 > 如何从标有 [return:MaybeNull] 的泛型中正确返回“null”?

问题描述

在 Microsoft 的可空性文档中,似乎存在相互矛盾的信息。

此页面上,它显示以下内容(重要部分以粗体/斜体显示):

通用定义和可空性

正确传达泛型类型和泛型方法的空状态需要特别小心。额外的关注源于可空值类型和可空引用类型根本不同的事实。Anint?是 的同义词Nullable<int>,而string?isstring带有编译器添加的属性。结果是编译器在T?不知道T是 aclass还是 a的情况下无法生成正确的代码struct

这个事实并不意味着您不能使用可为空的类型(值类型或引用类型)作为封闭泛型类型的类型参数。List<string?> 和 List<int?> 都是 List 的有效实例。

它的意思是你不能使用T?在没有约束的泛型类或方法声明中。例如,Enumerable.FirstOrDefault<TSource>(IEnumerable<TSource>)将不会更改为 returnT?。您可以通过添加structorclass约束来克服此限制。有了这些约束中的任何一个,编译器就知道如何为 T 和 T? 生成代码。

好的,所以如果你想T?在泛型中使用,你必须将它限制为 astructclass. 很简单。

但是然后在下一页中,他们这样说(再次以粗体/斜体强调):

指定后置条件:MaybeNull 和 NotNull

假设您有一个具有以下签名的方法:

public Customer FindCustomer(string lastName, string firstName)

您可能已经编写了这样的方法,以便null在未找到所搜索的名称时返回。null清楚地表明没有找到该记录。在此示例中,您可能会将返回类型从 更改CustomerCustomer?。将返回值声明为可空引用类型清楚地指定了此 API 的意图。

由于通用定义和可空性中涵盖的原因,该技术不适用于通用方法。您可能有一个遵循类似模式的通用方法:

public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)

您不能指定返回值为T?[but the] 方法null在未找到所查找的项目时返回。由于您不能声明T?返回类型,因此您将MaybeNull注释添加到方法返回:

[return: MaybeNull]
public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)

前面的代码通知调用者合约暗示了一个不可为空的类型,但返回值实际上可能是null. MaybeNull当您的 API 应该是不可为空的类型(通常是泛型类型参数)时使用该属性,但可能存在null会返回的实例。

然而...

即使直接从文档中复制该代码并为其提供一个简单地返回的默认实现null,它也不会编译!

[return: MaybeNull]
public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)
    => null;

我尝试了第一个链接页面中也提到的null-forgiving运算符null!(在“将属性初始化为空”部分下),但这不起作用。您不能使用default其中任何一个,因为它不会返回null值类型int,例如返回零,如下所示:

[return: MaybeNull]
public static T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)
    => default;

var seq = new []{ 1, 2, 3 };
bool MyPredicate(int value) => false;
var x = Find(seq, MyPredicate);
Console.WriteLine($"X is {x}");

输出:

X is 0

那么我在这里错过了什么?你如何成功地实现他们的示例代码而不诉诸使用T?which 需要将其类型约束为classor struct?如果你必须这样做,那有什么意义MaybeNull呢?

标签: c#nullc#-8.0c#-9.0nullability

解决方案


好吧,不幸的是,由于可空引用类型和可空值类型之间的差异——像 Swift 这样的限制没有——C# 不支持我所追求的。

相反,正如我在其他答案中提到的那样,因此您不应使用“返回 null 表示不匹配”模式。相反,您应该使用基于 try 的模式,该模式返回布尔值并使用 out 参数作为感兴趣的值(如果有)。在里面,你仍然使用default(所以你不必约束T)但是你有那个额外的布尔值告诉你是否应该忽略默认值本身。这种方式适用于类和结构类型,更重要的是,对于诸如int默认值不是 null 而是零的类型,它将帮助您区分表示它匹配零 (return = true) 或存在的谓词没有传递谓词(return = false),您可以忽略该零。

诀窍在于使用该NotNullWhen属性告诉调用者,当函数的返回值为 true 时,out 参数永远不会为 null,因此只要在访问 out 参数之前检查返回值,您也不必检查是否为空,并且代码流分析也不会显示“可能为空”的警告。

这是上述函数的重构......

public static bool TryFind<T>(this IEnumerable<T> items, Func<T, bool> predicate, [NotNullWhen(true)] out T result){
    
    foreach(var item in items){
        if(predicate(item)){
            result = item;
            return true;
        }
    }

    result = default;
    return false;
} 

是时候重构一些旧代码了!


推荐阅读