我们的代码库(.NET Standard 2.0库(中有以下方法:
public Task<T> GetDefaultTask<T>()
{
return Task.FromResult(default(T));
}
我们目前正在尝试转换到C#8.0的Nullability,并在上面的代码中得到警告:
警告CS8604:"任务任务"中参数"result"的引用参数可能为null。FromResult(T结果('。
为什么会收到此警告?对我来说,将null
作为参数传递给Task.FromResult
看起来非常好。
重要提示:我们希望允许Task包含null值。但是添加Task<T?>
会迫使我们添加类型约束,而这是我们无法做到的
如果T
是不可为null的引用类型,则不应将null
传递给Task.FromResult<T>
。Task.FromResult
的实现不关心空引用,您可以使用Task.FromResult(default(T)!)
,但GetDefaultTask
的调用方可能会收到Task<string>
,而实际上它应该是Task<string?>
。像GetDefaultTask<string>().Result.Length
这样的代码会在没有警告的情况下编译,并在运行时导致null引用异常。
据我所知,目前在这种情况下不可能正确地注释返回类型。
不允许将方法声明为Task<T?> GetDefaultTask<T>()
,因为T
可以是结构或引用类型,并且可为null的结构和引用类型的表示方式不同。
如果T
被约束为一个参考类型:,则可以干净地解决这个问题
public Task<T?> GetDefaultTask<T>() where T : class
但是,根据T
参数的来源,添加该约束可能会导致调用链上的问题。
对于泛型返回值可以是结构或引用(如Enumerable.FirstOrDefault
(的类似情况,有[MaybeNull]
属性,但它只能应用于返回值本身(在这种情况下是任务(,而不能应用于任务的泛型参数。
对我来说,将null作为参数传递给
Task.FromResult
看起来非常好。
不,这是个坏主意。
如果调用者为T
指定了一个不可为null的类型,那么default(T)
可以被认为是"未定义的"(它实际上是null
,但这是C#8.0实现不可为Null的引用类型的一个主要缺点(即它们可以仍然是null
,grrrr(。考虑:
// Compiled with C# 8.0's non-nullable reference-types enabled.
Task<String> task = GetDefaultTask<String>();
String result = await task;
Console.WriteLine( result.Length ); // <-- NullReferenceException at runtime even though the C# compiler reported `result` cannot be null.
对于没有足够类型约束的泛型类型,避免在C#8.0中使用default
/default(T)
。
这个问题有几个解决方案:
1:指定调用者提供的默认值:
public Task<T> GetDefaultTask<T>( T defaultValue )
{
return Task.FromResult( defaultValue );
}
因此,调用站点需要更新,如果调用方试图在运行时使用null
而不是异常,C#编译器将发出警告或错误:
Task<String> task = GetDefaultTask<String>( defaultValue: null ); // <-- compiler error or warning because `null` cannot be used here.
String result = await task;
Console.WriteLine( result.Length );
2:在不同的方法上添加结构与类的约束:
struct
/值类型的default(T)
可能是有意义的(或者它可能和null
一样危险…(,因为我们可以安全地使用default(T)
(其中T : struct
,而不是default(T)
,其中T : class
(,我们可以为这种情况添加不同的重载:
public Task<T> GetDefaultTask<T>()
where T : struct
{
return Task.FromResult( default(T) );
}
public Task<T> GetDefaultTask<T>( T defaultValue )
where T : class
{
return Task.FromResult( defaultValue );
}
(请注意,不能纯粹基于泛型类型约束来重载方法——只能通过泛型参数计数和通常的参数类型来重载