具有泛型返回类型的可空引用类型

Posted

技术标签:

【中文标题】具有泛型返回类型的可空引用类型【英文标题】:Nullable reference types with generic return type 【发布时间】:2019-07-02 18:50:15 【问题描述】:

我正在尝试使用新的 C# 8 可空引用类型功能,在重构我的代码时,我遇到了这个(简化的)方法:

public T Get<T>(string key)

    var wrapper = cacheService.Get(key);
    return wrapper.HasValue ? Deserialize<T>(wrapper) : default;

现在,这给出了一个警告

可能的空引用返回

这是合乎逻辑的,因为default(T) 将为所有引用类型提供 null。一开始我以为我会改成下面这样:

public T? Get&lt;T&gt;(string key)

但这是做不到的。它说我要么必须添加通用约束where T : classwhere T : struct。但这不是一个选项,因为它可以是两者(我可以存储intint?FooBar 的实例或缓存中的任何内容)。 我还读到了一个假定的新通用约束where class?,但这似乎不起作用。

我能想到的唯一简单的解决方案是使用 null 宽恕运算符来更改 return 语句

return wrapper.HasValue ? Deserialize<T>(wrapper) : default!;

但这感觉不对,因为它肯定可以为空,所以我在这里基本上是在对编译器撒谎..

我该如何解决这个问题?我在这里遗漏了一些非常明显的东西吗?

【问题讨论】:

不能同时编写支持Nullable&lt;T&gt;和引用类型的方法一直是个问题。这看起来只是该问题的延续。我发现的唯一好的解决方法是同时编写这些方法的GetGetStruct 版本。 【参考方案1】:

你们很亲密。只需像这样编写您的方法:

[return: MaybeNull]
public T Get<T>(string key)

    var wrapper = cacheService.Get(key);
    return wrapper.HasValue ? Deserialize<T>(wrapper) : default!;

您必须使用default! 来消除警告。但是你可以用[return: MaybeNull] 告诉编译器它应该检查null,即使它是一个不可为空的类型。

在这种情况下,如果开发人员使用您的方法并且不检查 null。

有关详细信息,请参阅 Microsoft 文档:Specify post-conditions: MaybeNull and NotNull

【讨论】:

这很酷,只是MaybeNull 不能与Task&lt;T&gt; 一起工作(它假定任务可能为空,而不是任务返回的值)。所以如果你有一个异步泛型函数,你仍然会被卡住。 如果您使用带有 C# 8 的 .NET Framework,那么遗憾的是这些注释不可用。 @IgnacioCalvo 是的,这很不幸。但是您可以为您的通用实现添加一个约束。例如,如果您添加where T: notnull,则调用者只能指定一个不可为空的类型。如果您未指定约束,则调用者负责空检查。这很糟糕,但应该可以。在这种情况下,你不能使用 default! 虽然... @HeikoG:它们在某种程度上......你必须创建自己的 MaybeNullAttribute 类,编译器会接受它。 似乎与[return: MaybeNull] 一样,即使没有default!,警告也会消失【参考方案2】:

我认为default! 是目前你能做的最好的。

public T? Get&lt;T&gt;(string key) 不起作用的原因是可空引用类型与可空值类型非常不同

可空引用类型纯粹是编译时的事情。小问号和感叹号仅由编译器用于检查可能的空值。在运行时的眼中,string?string 完全相同。

另一方面,可空值类型是Nullable&lt;T&gt; 的语法糖。当编译器编译你的方法时,它需要决定你的方法的返回类型。如果T 是引用类型,则您的方法将具有返回类型T。如果T 是值类型,则您的方法将具有Nullable&lt;T&gt; 的返回类型。但是当T 可以两者兼有时,编译器不知道如何处理它。它当然不能说“如果T 是引用类型,则返回类型是T,如果T 是引用类型,则返回类型是Nullable&lt;T&gt;。”因为 CLR 不会理解这一点。一个方法应该只有一个返回类型。

换句话说,说你想返回T?就像说你想在T是引用类型时返回T,当T是一个值类型时返回Nullable&lt;T&gt;。这听起来不像是一个有效的方法返回类型,是吗?

作为一个非常糟糕的解决方法,您可以声明两个具有不同名称的方法 - 一个将 T 限制为值类型,另一个将 T 限制为引用类型:

public T? Get<T>(string key) where T : class

    var wrapper = cacheService.Get(key);
    return wrapper.HasValue ? Deserialize<T>(wrapper) : null;


public T? GetStruct<T>(string key) where T : struct

    var wrapper = cacheService.Get(key);
    return wrapper.HasValue ? (T?)Deserialize<T>(wrapper) : null;

【讨论】:

@craig 我认为! 是一个“空断言运算符”。它告诉编译器“我'知道'这不是空的,所以请不要给我警告”。 它被称为 null-forgiving 运算符:docs.microsoft.com/en-us/dotnet/csharp/language-reference/…【参考方案3】:

在 C# 9 中,您可以更自然地表达无约束泛型的可空性:

public T? Get<T>(string key)

    var wrapper = cacheService.Get(key);
    return wrapper.HasValue ? Deserialize<T>(wrapper) : default;

请注意,default 表达式上没有 ! 运算符。与原始示例的唯一变化是将 ? 添加到 T 返回类型。

【讨论】:

太棒了!您知道 C#9 规范中的哪个地方提到了这一点吗?我真的找不到是什么语言变化使这成为可能。 我不相信 C# 9 规范存在。我找不到任何参考,但您可以通过添加最新的预发布包Microsoft.Net.Compilers.Toolset 或在sharplab.io (example) 上进行尝试。【参考方案4】:

除了 Drew 关于 C# 9

的回答

有了T? Get&lt;T&gt;(string key),我们仍然需要在调用代码中区分可以为空的引用类型和可以为空的值类型:

SomeClass? c = Get<SomeClass?>("key"); // return type is SomeClass?
SomeClass? c2 = Get<SomeClass>("key"); // return type is SomeClass?

int? i = Get<int?>("key"); // return type is int?
int i2 = Get<int>("key"); // return type is int

【讨论】:

哎呀。接得好。因此,Get() 方法的编写者现在没有从 C# 9 中的编译器收到警告,他们可能会误以为当 T 是一个结构时,他们可以毫不费力地返回一个 Nullable&lt;T&gt;。而事实上,调用者将获得一个非空的默认值。对我来说,这似乎是一个非常讨厌的小疏忽。

以上是关于具有泛型返回类型的可空引用类型的主要内容,如果未能解决你的问题,请参考以下文章

匹配参数的可空性和返回类型的泛型类型参数

C# 8中的可空引用类型

可空引用类型 - 通过接受的参数返回类型可空性

未从 FirstOrDefault 公开的可空引用类型信息

可空引用类型:如何指定“T?”类型不限于类或结构

.NET Framework 项目中的可空引用类型不能与 IntelliSense 一起使用