如何解决类型参数的 "The issue with T?"/nullable 约束?
How to solve "The issue with T?"/nullable constraint on type parameter?
我正在使用启用了 nullable
的 C# 8.0 设计接口,目标是 .Net Standard 2.0(使用 Nullable package) and 2.1. I am now facing The issue with T?
.
在我的示例中,我正在为存储 Stream
s/byte 数据的缓存构建一个接口,由 string
键标识,即文件系统可以通过简单的实现.每个条目都另外由一个版本标识,该版本应该是通用的。例如,此版本可以是另一个 string
密钥(如 etag)、int
或 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);
// ...
}
虽然我知道 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
这似乎没有帮助。在可空上下文中实现 ICache<string>
时,Task<string?> GetVersionAsync
会生成警告,因为签名不匹配。编译器很可能知道为 TVersionIdentifier
指定的类型是不可空的并强制执行它的规则,即使 ICache
不知道它。对于像 IList<T>
这样的流行界面,这是有道理的。
这会导致警告,因此这似乎不是一个真正的选择。
为成员的实现禁用nullable。虽然在任何一种情况下都会产生警告,但似乎随后为接口禁用了 nullable
(这真的有意义吗?)。
#nullable disable
public Task<string> GetVersionAsync(string file, CancellationToken cancellationToken = default)
{
return Task.FromResult((string)null);
}
#nullable restore
与 (2) 类似,但对整个实现 class(以及接口)禁用可为空。也许这是最重要的,因为它清楚地表达了 可空引用的概念 types/generics/... 不适用于此 class 并且调用者必须处理这种情况就像他们之前不得不做的那样(C# 8.0 之前)。
#nullable disable
class FileSystemCache : ICache<string>
{
// ...
}
#nullable restore
选项 (2) 或 (3) 但抑制编译器警告而不是禁用可为空。也许编译器之后得出了错误的结论,所以这是个坏主意?
与 (1) 类似,但对实现者有约定:为接口禁用 nullable
,但手动使用 [DisallowNull]
和 [NotNull]
进行注释(参见 ( 1)).在所有实现中手动使用可空类型作为 TVersionIdentifier
(我们不能强制执行此操作)。这可能使我们尽可能接近正确注释的程序集。我们实施的消费者在不应该使用 null 时会收到警告,并且他们会得到正确注释的 return 值。虽然这种方式不是很自我记录。任何可能的实施者都需要阅读我们的文档以充分理解我们的意图。因此,我们的接口对于可能的实现来说不是一个好的模型,因为它遗漏了一些方面。人们可能没有想到这一点。
走哪条路?还有另一种我错过的方法吗?我是否遗漏了任何相关方面?
我认为,如果 Microsoft 在博客中提出可能的解决方法,那就太好了 post。
从 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);
// ...
}
它并没有达到我想要达到的目的。
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)
{
// ...
}
// ...
}
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)
{
// ...
}
// ...
}
@rikki-gibson在评论中提到了Nullable reference types: How to specify "T?" type without constraining to class or struct,谢谢。它是真实的,并进一步帮助理解问题,但并没有解决问题(见上文)。
恕我直言,我想出的最干净的解决方案在 中有所描述。
这样声明接口:
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);
// ...
}
还要注意 [NotNullWhen(true)]
属性和 see the corresponding docs。感谢 C# 9,可以有一个可为 null 的输出参数 out TVersionIdentifier? result
。如果 TVersionIdentifier
是 int
,这将是 int
而不是 int?
(如上所述)。考虑到 Try*
方法在 .Net 中的典型使用方式,该函数的意图和语义很容易理解。使用 NotNullWhen
属性,我们也可以进行正确的编译器空检查。因此,这个解决方案符合我的设计目标。
我正在使用启用了 nullable
的 C# 8.0 设计接口,目标是 .Net Standard 2.0(使用 Nullable package) and 2.1. I am now facing The issue with T?
.
在我的示例中,我正在为存储 Stream
s/byte 数据的缓存构建一个接口,由 string
键标识,即文件系统可以通过简单的实现.每个条目都另外由一个版本标识,该版本应该是通用的。例如,此版本可以是另一个 string
密钥(如 etag)、int
或 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);
// ...
}
虽然我知道 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
这似乎没有帮助。在可空上下文中实现
ICache<string>
时,Task<string?> GetVersionAsync
会生成警告,因为签名不匹配。编译器很可能知道为TVersionIdentifier
指定的类型是不可空的并强制执行它的规则,即使ICache
不知道它。对于像IList<T>
这样的流行界面,这是有道理的。这会导致警告,因此这似乎不是一个真正的选择。
为成员的实现禁用nullable。虽然在任何一种情况下都会产生警告,但似乎随后为接口禁用了
nullable
(这真的有意义吗?)。#nullable disable public Task<string> GetVersionAsync(string file, CancellationToken cancellationToken = default) { return Task.FromResult((string)null); } #nullable restore
与 (2) 类似,但对整个实现 class(以及接口)禁用可为空。也许这是最重要的,因为它清楚地表达了 可空引用的概念 types/generics/... 不适用于此 class 并且调用者必须处理这种情况就像他们之前不得不做的那样(C# 8.0 之前)。
#nullable disable class FileSystemCache : ICache<string> { // ... } #nullable restore
选项 (2) 或 (3) 但抑制编译器警告而不是禁用可为空。也许编译器之后得出了错误的结论,所以这是个坏主意?
与 (1) 类似,但对实现者有约定:为接口禁用
nullable
,但手动使用[DisallowNull]
和[NotNull]
进行注释(参见 ( 1)).在所有实现中手动使用可空类型作为TVersionIdentifier
(我们不能强制执行此操作)。这可能使我们尽可能接近正确注释的程序集。我们实施的消费者在不应该使用 null 时会收到警告,并且他们会得到正确注释的 return 值。虽然这种方式不是很自我记录。任何可能的实施者都需要阅读我们的文档以充分理解我们的意图。因此,我们的接口对于可能的实现来说不是一个好的模型,因为它遗漏了一些方面。人们可能没有想到这一点。
走哪条路?还有另一种我错过的方法吗?我是否遗漏了任何相关方面?
我认为,如果 Microsoft 在博客中提出可能的解决方法,那就太好了 post。
从 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);
// ...
}
它并没有达到我想要达到的目的。
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)
{
// ...
}
// ...
}
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)
{
// ...
}
// ...
}
@rikki-gibson在评论中提到了Nullable reference types: How to specify "T?" type without constraining to class or struct,谢谢。它是真实的,并进一步帮助理解问题,但并没有解决问题(见上文)。
恕我直言,我想出的最干净的解决方案在
这样声明接口:
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);
// ...
}
还要注意 [NotNullWhen(true)]
属性和 see the corresponding docs。感谢 C# 9,可以有一个可为 null 的输出参数 out TVersionIdentifier? result
。如果 TVersionIdentifier
是 int
,这将是 int
而不是 int?
(如上所述)。考虑到 Try*
方法在 .Net 中的典型使用方式,该函数的意图和语义很容易理解。使用 NotNullWhen
属性,我们也可以进行正确的编译器空检查。因此,这个解决方案符合我的设计目标。