您如何从标有 [return:MaybeNull] 的泛型中正确 return `null`?

How do you properly return `null` from a generic marked with [return:MaybeNull]?

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

this page上,它说了以下内容(bold/italic中的重要部分):

Generic definitions and nullability

Correctly communicating the null state of generic types and generic methods requires special care. The extra care stems from the fact that a nullable value type and a nullable reference type are fundamentally different. An int? is a synonym for Nullable<int>, whereas string? is string with an attribute added by the compiler. The result is that the compiler can't generate correct code for T? without knowing if T is a class or a struct.

This fact doesn't mean you can't use a nullable type (either value type or reference type) as the type argument for a closed generic type. Both List<string?> and List<int?> are valid instantiations of List.

What it does mean is that you can't use T? in a generic class or method declaration without constraints. For example, Enumerable.FirstOrDefault<TSource>(IEnumerable<TSource>) won't be changed to return T?. You can overcome this limitation by adding either the struct or class constraint. With either of those constraints, the compiler knows how to generate code for both T and T?.

好的,所以如果你想在泛型中使用 T?,你 必须 将其限制为 structclass.够简单了。

但是然后在 following page 中,他们这样说(再次强调 bold/italic):

Specify post-conditions: MaybeNull and NotNull

Suppose you have a method with the following signature:

public Customer FindCustomer(string lastName, string firstName)

You've likely written a method like this to return null when the name sought wasn't found. The null clearly indicates that the record wasn't found. In this example, you'd likely change the return type from Customer to Customer?. Declaring the return value as a nullable reference type specifies the intent of this API clearly.

For reasons covered under Generic definitions and nullability that technique does not work with generic methods. You may have a generic method that follows a similar pattern:

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

You can't specify that the return value is T? [but the] method returns null when the sought item isn't found. Since you can't declare a T? return type, you add the MaybeNull annotation to the method return:

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

The preceding code informs callers that the contract implies a non-nullable type, but the return value may actually be null. Use the MaybeNull attribute when your API should be a non-nullable type, typically a generic type parameter, but there may be instances where null would be returned.

然而...

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

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

我尝试了 null-forgiving 运算符,null!,在第一个链接的页面(在 'Initialize the property to null' 部分下)也提到了,但是没有用。您也不能使用 default,因为它不会 return null 用于 int 之类的值类型,而 return 为零,如下所示:

[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? 的情况下成功实现他们的示例代码,这需要将其类型约束为 classstruct?如果您确实必须这样做,那么 MaybeNull 的意义何在?

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

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

While this works, I would call such an approach 'code-smell' and would instead change it to a 'Try'-based pattern. You would still return default, but as an out parameter, then return a boolean as the return value telling you if a match was actually found, or in other words, whether you should ignore that out parameter.

That way, for the non-nullable types, like int, you would still know there was no match due to the extra 'information' of the returned boolean which would address cases where zero itself could be a match. (i.e. the out as zero with a return as false means no match whereas an out as zero with a return of true means you found a match, zero.)

但回到代码...

示例如下。

[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 成为可空类型,但这也意味着您需要一个采用可空类型的谓词。

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

MaybeNull 属性不会更改 return 值,它只是一个警告(通常是针对人们的),表示“未找到值 returned - 不是谈论 null”。这是为什么?你可以 return null of T 对吗?你不能。结构没有空值。并且 return 只是结构的默认值(例如 int = 0)并不表示默认值不是“未找到的值”。你可以用T?但在某些情况下,您不能或不想 return T? (可为空)可能是因为继承或其他原因。所以你可以通知调用者关于 MaybeNot 并且他们可以威胁 returned value with special case

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


------------编辑------------ 根据评论

看起来唯一的方法就是如何避免为引用类型和结构制作两个单独的泛型 classes 使泛型 class 不受约束 T 和方法可以 return null 标记为“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

基于此blog

C# 9 update: The ? operator now works on unconstrained generic types. So, you should use T? instead of the [MayBeNull] attribute.

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

老实说,我发现整个可空性问题非常棘手

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

相反,正如我在其他回答中提到的,因此您不应使用 'returning null means no match' 模式。相反,您应该使用一个基于尝试的模式,该模式 return 是一个布尔值,并使用一个输出参数作为感兴趣的值(如果有的话)。在内部,您仍然使用 default(因此您不必限制 T)但是您有额外的布尔值告诉您是否应忽略默认值本身。这种方式将适用于 class 和结构类型,更重要的是,对于 int 等默认值不是 null 而是零的类型,它将帮助您区分指示它的谓词匹配零(return = true)或没有传递谓词(return = false),您可以忽略该零。

诀窍在于使用 NotNullWhen 属性告诉调用者当函数的 return 值为真时,out 参数永远不会为 null,因此只要您检查 return 访问 out 参数之前的值,您也不必检查是否为 null,代码流分析也不会显示 'possible null' 警告。

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

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;
} 

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