可能返回 default(T) 的异步泛型方法的正确可空注释

Posted

技术标签:

【中文标题】可能返回 default(T) 的异步泛型方法的正确可空注释【英文标题】:Proper nullable annotation for async generic method that may return default(T) 【发布时间】:2020-07-08 12:24:38 【问题描述】:

我正在将代码库转换为具有可为空引用类型的 C#8。我遇到了一种类似于in this question 但异步的方法。

public async Task<T> GetAsync<T>()

    // sometimes returns default(T); => warning CS8603 Possible null reference return

T 可以是任何类型,包括可为空的引用类型或可为空的值类型。

明确地说,我理解为什么这种方法会触发警告。我想知道的是可以用什么注解来解决。

我知道我可以使用#nullable disable 或default(T)!,但我希望得到的不是“锤子”。 我知道我不能使用 [return: MaybNull],因为这将适用于 Task 本身,而不是 T

我可以应用任何其他属性/注释来使编译器满意,或者default(T)!是我唯一的选择吗?

【问题讨论】:

请详细解释为什么possible duplicate you referred to 不能解决您的问题。您是否尝试过在 return 语句中使用 null-forgiving 运算符?鉴于另一个问题已经指出这是解决警告的方法,你为什么认为你会在这里得到任何不同的答案?您是否了解 MaybeNull 注释和 null-forgiving 运算符正在做两件不同的事情?前者是修复警告的原因,后者确保调用者意识到这种可能性。 @PeterDuniho 重复项返回 T,因此他们可以使用 return: MaybeNull。在我的情况下,我返回 Task 以便该属性将应用于任务,而不是 T。正如我在问题中提到的,我知道在这里使用 default(T)! 作为解决方案,但感觉就像一个 hack;我很好奇这是否是唯一的解决方案。 “重复项返回 T,因此他们可以使用 return: MaybeNull” - 是的,但这是与您在此处询问的警告完全不同的问题。你想同时问两个问题吗?如果是这样,您需要通过在这里只问一个问题来解决这个问题,并为第二个问题发布第二个问题。破解与否,使用 null-forgiving is 是准确的答案,正如您已经看过的可能重复项中所提供的那样。 @PeterDuniho 我不是在询问警告;我明白为什么警告被省略了。我想知道是否有解决这种情况的“干净”选项。我已经更新了措辞,希望能减少混淆。正如你所说,在这种情况下,! 是唯一的选择,unlike 在另一个有更好选择的问题中。 这能回答你的问题吗? Nullable reference types: How to specify "T?" type without constraining to class or struct 【参考方案1】:

根据我的经验,您可以使用Task&lt;T?&gt; GetAsync&lt;T&gt;() where T: class 来解决您的问题。

【讨论】:

当然,但是通过要求 T 是引用类型来改变函数的语义【参考方案2】:

C# 9中我们可以通过添加?来解决

public async Task<T?> GetAsync<T>()

    return default;

但是你需要在调用代码中区分可以为空的值类型和可以为空的引用类型。要获取可为空的 ref 类型作为返回值,您可以使用 &lt;T&gt;&lt;T?&gt; 调用该方法:

SomeClass? c = await GetAsync<SomeClass>(); // return type is SomeClass?
SomeClass? c2 = await GetAsync<SomeClass?>(); // return type is SomeClass?

要获得可为空的值类型,您需要使用&lt;T?&gt; 调用它:

int? i = await GetAsync<int?>(); // return type is int?
int i2 = await GetAsync<int>(); // return type is int

附:我想知道微软如何解释我们why they can't allow unconstrained T?,然后在下一个 C# 版本中这样做:)

另一种选择是使用 C# 8 答案中的代码。

C# 8 答案

我们不能拥有async Task&lt;T?&gt; GetAsync&lt;T&gt;(),因为SomeClass?SomeStruct? 非常不同。 default! 也不是最好的选择,因为我们可以通过在调用代码中调用 GetAsync&lt;SomeClass&gt;() 来获得非可空引用类型的可空引用。

更好的选择是让两个不同的方法使用相同的私有方法:

public class Storage

    ...
    public Task<T?> GetClassAsync<T>() where T : class
    
        return GetAsync<T?>();
    
    public Task<T?> GetStructAsync<T>() where T : struct
    
        return GetAsync<T?>();
    
    private async Task<T> GetAsync<T>()
    
        if (condition)
            return default!;
        string json = await GetJsonAsync();
        T result = JsonSerializer.Deserialize<T>(json);
        return result;
    

及用法:

// return type is SomeClass?
SomeClass? classResult = await storage.GetClassAsync<SomeClass>();

// return type is int?
int? structResult = await storage.GetStructAsync<int>();


// warning: Nullability of type argument doesn't match 'class' constraint
SomeClass? classResult = await storage.GetClassAsync<SomeClass?>();
// error: The type 'int?' must be a non-nullable value type
int? structResult2 = await storage.GetStructAsync<int?>();

【讨论】:

很高兴了解 C#9 解决方案! 我得说我认为 .net 正在变得一团糟。仅此功能的名称“可为空的引用类型”就说明了他们在创建简单、清晰的概念时有多么困难(因为所有引用类型一直都可以为空,这与“可空”至少是半可空的值类型不同。 System.Nullable 结构的合理简写,它是一种值类型,当然不能为空。我责怪 javascript 小子的涌入!:D【参考方案3】:

通过搜索和进行更多研究,在这种情况下抑制警告的首选方法似乎是在 default(T) 上使用 ! 运算符。

【讨论】:

以上是关于可能返回 default(T) 的异步泛型方法的正确可空注释的主要内容,如果未能解决你的问题,请参考以下文章

java中一个方法的返回值可能是integer或者Double,请问该怎么定义泛型接收

C#对泛型实例化对像的方法

编写高质量代码改善C#程序的157个建议——建议35:使用default为泛型类型变量指定初始值

如何根据T创建不同行为的泛型方法?

如何在 Delphi 中测试泛型类型变量是不是与 Default(T) 相等?

JAVA中的泛型用法一种: <T> 返回值用法。