1

在 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呢?

4

3 回答 3

2

好吧,不幸的是,由于可空引用类型和可空值类型之间的差异——像 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;
} 

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

于 2020-12-01T08:24:13.117 回答
0

好的,尝试更多,有两种解决方案。一,编写两个版本的泛型,一个限制为结构,另一个限制为类。但是,您将不得不更改它们的名称,因为约束不足以区分函数重载。

另一种方式你不必限制它,但你必须 return default,而不是null在你的实现中。但这并不意味着它会回归null。只有当这是传入类型的默认值时,它才会返回null,这意味着如果您想要返回 null,您还必须在调用泛型之前先将不可为空的类型预处理为可空的变体。

虽然这可行,但我将这种方法称为“代码气味”,而是将其更改为基于“尝试”的模式。您仍然会 return default,但作为out参数,然后返回一个布尔值作为返回值,告诉您是否实际找到匹配项,或者换句话说,您是否应该忽略该 out 参数。

这样,对于不可为空的类型,如 int,您仍然会知道由于返回的布尔值的额外“信息”而没有匹配,这将解决零本身可能匹配的情况。(即输出为零且返回为假表示不匹配,而输出为零且返回为真表示您找到了匹配项,零。)

但回到代码...

这是示例。

[return:MaybeNull] // Suppress compiler warning that 'default' may be null
public static T Find<T>(this IEnumerable<T> items, Func<T, bool> predicate){

    foreach(var item in items)
        if(predicate(item))
            return item;

    return default;
}

现在考虑以下整数序列:

var seq = new []{ 1, 2, 3, 4 };

当您运行此代码时...

bool MyIntPredicate(int value)
    => value == 5;

var x = seq.Find(MyIntPredicate);

Console.WriteLine($"X is {x}");

它输出以下内容:

X is 0

但是,如果您希望 null 表示未找到,则必须创建T一个可为 null 的类型,但这也意味着您需要一个采用可为 null 类型的谓词。

bool MyNullableIntPredicate(int? value)
    => value == 5;

// Convert the int array to a nullable-int array via `OfType`, then use the nullable-int predicate.
var y = seq.OfType<int?>().FirstOrDefault(MyNullableIntPredicate);

Console.WriteLine($"Y is {y?.ToString() ?? "null"}");

运行上述程序,您现在会得到以下输出:

X is null

如果你添加5到数组中,在这两种情况下你都会得到这个......

X is 5
于 2020-11-27T16:20:11.807 回答
0

MaybeNull 属性不会改变返回值,它只是警告(通常对人),表明“未找到值被返回 - 不是在谈论 null”。这是为什么?您可以返回 null 的 T 对吗?你不能。结构没有空值。并且仅返回结构的默认值(例如 int = 0)并不表示默认值不是“未找到值”。你可以用T吗?但在某些情况下,您可能不能或不想返回 T?(可为空)可能是因为继承或其他原因。因此,您可以通知调用者有关 MaybeNot 的信息,他们可以用特殊情况威胁返回值

不错的阅读:https ://endjin.com/blog/2020/07/dotnet-csharp-8-nullable-references-maybenull


-----------编辑-----------基于评论

看起来如何避免为引用类型和结构创建两个单独的泛型类的唯一方法是使泛型类不受约束的 T 和可以将空标记返回为“T”的方法?

 public static class NullableGeneric<T>
    {
        public static Dictionary<int,T> Values { get; set; } = new Dictionary<int,T>();
#nullable enable       
        public static T? TryFind(int id) 
        {
            var isFound = Values.TryGetValue(id, out T value);
            return isFound ? value : default; // default of T? is null even for structs;
        }
#nullable disable
    
    }

你可以用

 NullableGeneric<int?>.Values.Add(1, 5);
 int? structType = NullableGeneric<int?>.TryFind(0); // null
 int? structType2 = NullableGeneric<int?>.TryFind(1); // 5

基于这个博客

C# 9 更新: ? 运算符现在适用于不受约束的泛型类型。所以,你应该使用 T? 而不是 [MayBeNull] 属性。

另一个问题是返回新的自定义类,其中包含 bool "isNotFound" 属性和 value 属性。(您可以使用包含 Option 类的 LanguageExt.Core - 类似于 Nullable 但它具有更多功能并允许引用类型和结构)

老实说,我发现整个可空性的事情非常棘手

于 2020-11-27T16:27:22.427 回答