可空引用类型与泛型返回类型

63

我正在尝试使用新的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(string key)
但是这是不可能的。它说我要么添加一个泛型约束where T:class或where T:struct。但这不是一个选项,因为它可以是两者(我可以将int或int?或FooBar实例或任何东西存储在缓存中)。 我还读到了一个所谓的新泛型约束where class?,但那似乎行不通。
我唯一能想到的简单解决方案是使用空值运算符更改返回语句:
return wrapper.HasValue ? Deserialize<T>(wrapper) : default!;

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

我该如何解决这个问题?我是不是漏掉了某些非常明显的东西?


3
在编写方法时,同时支持 Nullable<T> 和引用类型一直是一个问题。这似乎只是该问题的延续。我找到的唯一好的解决办法是编写这些方法的 GetGetStruct 两个版本。 - Dave Cousineau
4个回答

36
你离答案非常接近。只需要像这样编写你的方法:

你离答案非常接近。只需要像这样编写你的方法:

[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,则可能会收到警告(取决于流分析)。
更多信息,请参见微软文档:指定后置条件:MaybeNull 和 NotNull

9
除了 MaybeNull 不能与 Task<T> 一起使用之外,这很棒(它假设 Task 可能为空,而不是 Task 返回的值可能为空)。因此,如果您有一个异步泛型函数,仍然会遇到问题。 - Ignacio Calvo
1
如果你正在使用带有C# 8的.NET框架,则很遗憾这些类型的注释不可用。 - HeikoG
1
@HeikoG:它们有点复杂..你必须创建自己的MaybeNullAttribute类,然后编译器才能接受它。 - duedl0r
1
似乎使用 [return: MaybeNull],即使没有 default!,警告也会消失。 - kofifus
1
正如@TheRubberDuck所提到的,C#9中的答案已经过时。建议添加此链接以解释原因。 - Ivan Danilov
显示剩余7条评论

29

我认为在这种情况下,default! 是你能做的最好的。

之所以 public T? Get<T>(string key) 不起作用,是因为可空引用类型与可空值类型非常不同。

可空引用类型仅仅是编译时的事情。问号和感叹号只是编译器用来检查可能的 null 值。对于运行时来说,string?string 是一样的。

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

换句话说,如果你想返回 T?,就等于是说你想要返回 TT 是引用类型时,并返回 Nullable<T>T 是值类型时。这似乎不是一个有效的方法返回类型,对吧?

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

1
@craig 我认为 ! 是一个“空断言运算符”。它告诉编译器:“我‘知道’这不是空的,所以请不要给我警告”。 - Dave Cousineau
1
它被称为空值容忍运算符:https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-forgiving - 321X

12
在C# 9中,您可以更自然地表达无约束泛型的空值性:
public T? Get<T>(string key)
{
    var wrapper = cacheService.Get(key);
    return wrapper.HasValue ? Deserialize<T>(wrapper) : default;
}

注意,在默认表达式上没有 ! 运算符。与您原始示例唯一的区别是在 T 返回类型中添加了 ?

太好了!你知道在C#9规范的哪里提到了这个吗?我找不到是哪个语言变更使这成为可能。 - Razzie
我不认为C# 9规范已经存在。我找不到任何相关的参考资料,但你可以通过添加最新的Microsoft.Net.Compilers.Toolset预发行包或在sharplab.io上尝试它(示例)。 - Drew Noakes
这将如何与通用返回类型一起工作,例如 Task<T>(int, T) 值元组?我猜它不会。 - Tobias Knauss
@TobiasKnauss,你能否提供一个Gist或SO问题的链接来展示你所说的内容? - Drew Noakes
例如方法:public static (int Index, T Element) IndexAndElementOfFirst<T> (this IEnumerable<T> i_collection, Func<T, bool> i_selector) { ... 如果是值类型,不应该使用T?。...} - Tobias Knauss

9

除了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

7
哦,发现了。所以在 C# 9 中,Get() 方法的编写者可能不会收到任何警告信息,因此误以为当 T 是一个结构体时,可以轻松返回一个 Nullable<T>。然而实际上,调用者将得到一个非空的默认值。对我来说,这似乎是一个非常恶劣的小疏忽。 - Matt Jenkins

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接