具有泛型 return 类型的可空引用类型

Nullable reference types with generic return type

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

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

现在,这会发出警告

Possible null reference return

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

public T? Get<T>(string key)

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

我能想到的唯一简单解决方案是使用 null 宽容运算符 :

更改 return 语句
return wrapper.HasValue ? Deserialize<T>(wrapper) : default!;

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

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

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

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

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

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

换句话说,当 T 是参考时,说你想要 return T? 就像说你想要 return T类型,当 T 是值类型时,return Nullable<T>。这听起来不像是方法的有效 return 类型,是吗?

作为一个非常糟糕的解决方法,您可以声明两个具有不同名称的方法 - 一个 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;
}

你们非常亲密。像这样写你的方法:

[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

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

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

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

除了 Drew 关于 C# 9

的回答

有了 T? Get<T>(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