C# 具有泛型返回类型的可为null的引用类型

C# 具有泛型返回类型的可为null的引用类型,c#,generics,c#-8.0,nullable-reference-types,C#,Generics,C# 8.0,Nullable Reference Types,我正在使用新的C#8可空引用类型特性,在重构代码时,我想到了这个(简化的)方法: 但这感觉是错误的,因为它肯定是空的,所以我基本上是在对编译器撒谎 我怎样才能解决这个问题?我是否遗漏了一些非常明显的东西?我认为默认是此时您所能做的最好的选择 为什么public T?Get(字符串键)不起作用是因为可空引用类型与可空值类型非常不同 可空引用类型纯粹是编译时的事情。小问号和感叹号仅由编译器用于检查可能的空值。在运行时看来,string?和string完全相同 另一方面,可空值类型是Nullable的

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

但这感觉是错误的,因为它肯定是空的,所以我基本上是在对编译器撒谎


我怎样才能解决这个问题?我是否遗漏了一些非常明显的东西?

我认为
默认是此时您所能做的最好的选择

为什么
public T?Get(字符串键)
不起作用是因为可空引用类型与可空值类型非常不同

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

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

换句话说,如果说您想返回
T?
,就像说当
T
是引用类型时您想返回
T
,当
T
是值类型时,您想返回
null
。这听起来不像是方法的有效返回类型,是吗

作为一种非常糟糕的解决方法,您可以使用不同的名称声明两个方法—一个方法将
T
约束为值类型,另一个方法将
T
约束为引用类型:

公共电话?Get(字符串键),其中T:class { var wrapper=cacheService.Get(key); 返回wrapper.HasValue?反序列化(wrapper):null; } 公共电话?GetStruct(字符串键),其中T:struct { var wrapper=cacheService.Get(key); 返回wrapper.HasValue?(T?)反序列化(wrapper):null; }
你们非常接近。只需这样编写您的方法:

[return: MaybeNull]
public T Get<T>(string key)
{
    var wrapper = cacheService.Get(key);
    return wrapper.HasValue ? Deserialize<T>(wrapper) : default!;
}
[返回:MaybeNull]
公共T获取(字符串键)
{
var wrapper=cacheService.Get(key);
return wrapper.HasValue?反序列化(wrapper):默认值!;
}
您必须使用
默认值
以消除警告。但是您可以使用
[return:MaybeNull]
告诉编译器,即使它是不可为null的类型,也应该检查null

在这种情况下,如果开发人员使用您的方法并且没有检查null,他可能会得到警告(取决于流分析)

有关更多信息,请参阅Microsoft文档:

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

公共电话?获取(字符串键) { var wrapper=cacheService.Get(key); return wrapper.HasValue?反序列化(wrapper):默认值; }
注意,没有
默认表达式上的code>运算符。与原始示例相比,唯一的变化是将
添加到
T
返回类型。

除了Drew关于C#9的回答之外

T?Get(string key)
我们仍然需要在调用代码中区分可为null的ref类型和可为null的值类型:

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
SomeClass?c=获取(“键”);//返回类型是SomeClass?
什么课?c2=获取(“键”);//返回类型是SomeClass?
智力?i=Get(“key”);//返回类型是int?
int i2=Get(“key”);//返回类型为int

一直以来,一个问题是您不能同时编写支持
Nullable
和引用类型的方法。这看起来只是这个问题的延续。我发现的唯一好的解决方法是编写这类方法的
Get
GetStruct
版本。@craig我想
是一个“空断言运算符”。它告诉编译器“我'知道'这不是空的,所以请不要给我警告”。它被称为空原谅运算符:这很酷,只是
MaybeNull
不适用于
Task
(它假设任务可能是空的,而不是任务返回的值)。因此,如果您有一个异步泛型函数,您仍然会被卡住。如果您将.NET Framework与C#8一起使用,很遗憾这些注释不可用。@IgnacioCalvo是的,这很不幸。但您可以向通用实现添加约束。例如,如果添加
,其中T:notnull
,则调用者只能指定不可为null的类型。如果未指定约束,则调用方负责空检查。这很糟糕,但应该有效。在这种情况下,您不能使用
default
虽然…@HeikoG:它们是……你必须创建你自己的MaybeNullAttribute类,编译器接受它。似乎在
[return:MaybeNull]
中,即使没有
默认值,警告也会消失太好了!你知道在C#9规范的哪里提到了这一点吗?我真的找不到是什么语言变化使得这成为可能。我不相信C#9规范还存在。我找不到关于它的任何参考资料,但您可以通过添加Microsoft.Net.Compilers.Toolset的最新预发行版包来尝试,也可以在sharplab.io().Oof上尝试。抢手货因此,
Get()
方法的作者,现在在C#9中没有从编译器得到任何警告,可能会被误导,认为当
T
是一个结构时,他们可以返回一个
null
,而无需额外的努力。然而,实际上,调用方将获得一个非空的默认值。对我来说,这真是一个令人讨厌的小疏忽
[return: MaybeNull]
public T Get<T>(string key)
{
    var wrapper = cacheService.Get(key);
    return wrapper.HasValue ? Deserialize<T>(wrapper) : default!;
}
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