Geo*_*ung 5 c# generics nullable c#-8.0 nullable-reference-types
我正在 C# 8.0 中设计一个nullable启用的接口,目标是 .Net Standard 2.0(使用Nullable 包)和 2.1。我现在面临这个问题T?。
在我的示例中,我正在为缓存构建一个接口,该接口存储Stream由string键标识的 s/字节数据,即文件系统可以通过一个简单的实现。每个条目还由一个版本标识,该版本应该是通用的。例如,此版本可以是另一个string键(如 etag)、 anint或 a date。
public interface ICache<TVersionIdentifier> where TVersionIdentifier : notnull
{
// this method should return a nullable version of TVersionIdentifier, but this is not expressable due to
// "The issue with T?" https://devblogs.microsoft.com/dotnet/try-out-nullable-reference-types/
Task<TVersionIdentifier> GetVersionAsync(string file, CancellationToken cancellationToken = default);
// TVersionIdentifier should be not nullable here, which is what we get with the given code
Task<Stream> GetAsync(string file, TVersionIdentifier version, CancellationToken cancellationToken = default);
// ...
}
Run Code Online (Sandbox Code Playgroud)
虽然我明白问题T?是什么以及为什么它对编译器来说是一个重要的问题,但我不知道如何处理这种情况。
我想到了一些选择,但它们似乎都不是最佳选择:
禁用nullable接口,手动标记不可为空的出现TVersionIdentifier:
#nullable disable
public interface ICache<TVersionIdentifier>
{
Task<TVersionIdentifier> GetVersionAsync(string file, CancellationToken cancellationToken = default);
// notice the DisallowNullAttribute
Task<Stream> GetAsync(string file, [DisallowNull] TVersionIdentifier version, CancellationToken cancellationToken = default);
// ..
}
#nullable restore
Run Code Online (Sandbox Code Playgroud)
这似乎没有帮助。在ICache<string>启用可为空的上下文中实现时,Task<string?> GetVersionAsync生成警告,因为签名不匹配。编译器很可能知道给定的类型TVersionIdentifier是不可为空的并强制执行它的规则,即使ICache不知道它。对于像IList<T>这样的流行接口是有道理的。
这会导致警告,因此这似乎不是一个真正的选择。
为成员的实现禁用可空。虽然在任何一种情况下都会产生警告,但似乎因此禁用nullable了接口(这真的有意义吗?)。
#nullable disable
public Task<string> GetVersionAsync(string file, CancellationToken cancellationToken = default)
{
return Task.FromResult((string)null);
}
#nullable restore
Run Code Online (Sandbox Code Playgroud)像 (2) 一样,但对整个实现类(以及接口)禁用可空。也许这是最重要的,因为它清楚地表达了可为空引用类型/泛型/...的概念,不适用于此类,并且调用者必须像之前(C# 8.0 之前)那样处理这种情况。
#nullable disable
class FileSystemCache : ICache<string>
{
// ...
}
#nullable restore
Run Code Online (Sandbox Code Playgroud)选项 (2) 或 (3) 但禁止编译器警告而不是禁用可空。也许编译器后来得出了错误的结论,所以这是一个坏主意?
与 (1) 类似,但对于实现者有约定:禁用nullable接口,但使用[DisallowNull]和[NotNull]手动进行注释(参见 (1) 中的代码)。TVersionIdentifier在所有实现中手动使用可空类型(我们不能强制执行此操作)。这可能会让我们尽可能接近正确注释的程序集。我们实现的消费者在他们不应该使用空值时会得到警告,并且他们会得到正确注释的返回值。虽然这种方式不是很自我记录。任何可能的实施者都需要阅读我们的文档以完全理解我们的意图。因此,我们的接口对于可能的实现来说不是一个好的模型,因为它遗漏了一些方面。人们可能不会想到这一点。
走哪条路?我错过了另一种方式吗?有没有我遗漏的相关方面?
我认为如果微软在博客文章中建议了一个可能的解决方法,那就太好了。
从 C# 9 开始,接口可以这样声明:
public interface ICache<TVersionIdentifier> where TVersionIdentifier : notnull
{
Task<Stream> GetAsync(string file, TVersionIdentifier version, CancellationToken cancellationToken = default);
Task<TVersionIdentifier?> GetVersionAsync(string file, CancellationToken cancellationToken = default);
// ...
}
Run Code Online (Sandbox Code Playgroud)
但它没有实现我试图实现的目标。
的实现ICache<string>将正确地具有成员:
public class StringVersionedCache : ICache<string>
{
public Task<Stream> GetAsync(string file, string version, CancellationToken cancellationToken = default)
{
// ...
}
public Task<string?> GetVersionAsync(string file, CancellationToken cancellationToken = default)
{
// ...
}
// ...
}
Run Code Online (Sandbox Code Playgroud)
但的实现ICache<int>将错误地包含这些成员:
public class IntVersionedCache : ICache<int>
{
public Task<Stream> GetAsync(string file, int version, CancellationToken cancellationToken = default)
{
// ...
}
// WRONG: needs to be Task<int?>
public Task<int> GetVersionAsync(string file, CancellationToken cancellationToken = default)
{
// ...
}
// ...
}
Run Code Online (Sandbox Code Playgroud)
@rikki-gibson 提到了Nullable 引用类型:如何指定“T?” 在注释中键入而不限制类或结构,谢谢。它是相关的并进一步有助于理解问题,但并不能解决问题(见上文)。
恕我直言,我能想到的最干净的解决方案在Returning nullable and null in single C# generic method?中进行了描述。。
这样声明接口:
public interface ICache<TVersionIdentifier> where TVersionIdentifier : notnull
{
Task<Stream> GetAsync(string file, [DisallowNull] TVersionIdentifier version, CancellationToken cancellationToken = default);
Task<bool> TryGetVersionAsync(string file, [NotNullWhen(true)] out TVersionIdentifier? result, CancellationToken cancellationToken = default);
// ...
}
Run Code Online (Sandbox Code Playgroud)
另请注意该[NotNullWhen(true)]属性并查看相应的文档。感谢 C# 9,可以有一个可为空的输出参数out TVersionIdentifier? result。如果TVersionIdentifier是int,则不会int(int?如上所述)。鉴于Try*方法在 .Net 中的典型使用方式,该函数的意图和语义很容易理解。使用该NotNullWhen属性,我们也可以进行正确的编译器空检查。因此,这个解决方案满足了我的设计目标。