“ref”参数和可空检查的良好实践

Mar*_*ari 4 c# .net-core c#-8.0 nullable-reference-types .net-core-3.0

当我遇到以下情况时,最好采取什么方法?

\n

上下文是一个启用了最新的可为空检查的.NET Core 3.x 应用程序,以及一个带有ref关键字的方法。真正的代码更复杂,但更简单的版本可能是这样的:

\n
private static bool _initialized = false;\nprivate static object _initializationLock = new object();\nprivate static MyClass _initializationTarget;  //suggestion to mark as nullable\n\npublic MyClass GetInstance()\n{\n    return LazyInitializer.EnsureInitialized(\n        ref _initializationTarget,\n        ref _initialized,\n        ref _initializationLock,\n        () => new MyClass()\n        );\n}\n
Run Code Online (Sandbox Code Playgroud)\n

EnsureInitialized()方法采用位于开头_initializationTarget的引用null,因此将其标记为可为空似乎是正确的调整。然而,同样的方法可以确保变量被正确填充。

\n

我找不到比下面的\xe2\x80\x94更好的模式,但这真的是最好的吗?

\n
private static bool _initialized = false;\nprivate static object _initializationLock = new object();\nprivate static MyClass? _initializationTarget;  //marked as nullable\n\npublic MyClass GetInstance()\n{\n    //the return value must also nullable\n    MyClass? inst = LazyInitializer.EnsureInitialized(\n        ref _initializationTarget,\n        ref _initialized,\n        ref _initializationLock,\n        () => new MyClass()\n        );\n\n    return inst!;  //null-forgive here\n}\n
Run Code Online (Sandbox Code Playgroud)\n

Jer*_*ney 5

TL;DR:如果删除initialized参数,则将EnsureInitialized()保证返回的对象是[NotNull]\xe2\x80\x94,即使你传入一个统一的MyClass?引用 \xe2\x80\x94,因此不需要使用null - 宽容运算符( !)。

\n
\n

完整答案

\n

这是一个很好的问题。您的具体示例的答案非常简单,但我也想借此机会解决您有关如何使用C# 8.0 可空引用类型处理参数的一般问题。ref这样做不仅有助于解决类似的其他场景,而且还可以解释为什么的特定示例的解决方案有效。

\n

具体例子

\n

虽然文档EnsureInitialized()没有完全清楚地说明这一点,但您调用的特定重载适用于您可能需要目标的情况null。也就是说,如果您要传递参数的值trueinitialized那么它将返回null。这个条件就是为什么它必须返回可为空类型的原因。

\n

由于您不想允许null值\xe2\x80\x94 并且不使用值类型\xe2\x80\x94,因此您只需删除该initialized参数即可。您仍然需要将您声明为_initializationTarget可为空,因为它没有在您的构造函数中初始化。然而,这种重载向编译器保证参数在运行后将target不再存在:nullEnsureInitialized()

\n
private static object _initializationLock = new object();\nprivate static MyClass? _initializationTarget;  //marked as nullable\n\npublic MyClass GetInstance() =>\n    LazyInitializer.EnsureInitialized(\n        ref _initializationTarget,\n        ref _initializationLock,\n        () => new MyClass()\n    );\n
Run Code Online (Sandbox Code Playgroud)\n

请注意,虽然它仍然传入 null(able) MyClass?,但它会自信地返回 a MyClass,而无需求助于null-forgiving 运算符( !)。

\n

一般问题

\n

当您进一步深入研究 C# 8.0 的可空引用类型时,您将发现 Roslyn 静态流分析中的许多差距,这些差距无法通过仅使用 and?运算!符来消除歧义。幸运的是,微软预见到了这个问题,为我们提供了多种可以用来提供编译器提示的属性

\n

例子

\n

这是一个说明一般问题的基本示例:

\n
public void EnsureNotNull(ref Object? input) => input ??= new Object();\n
Run Code Online (Sandbox Code Playgroud)\n

如果您使用以下代码调用此方法,您将收到警告CS8602

\n
Object? object = null;\nEnsureNotNull(ref object);\n_ = object.ToString(); //CS8602; Dereference of a possibly null reference\n
Run Code Online (Sandbox Code Playgroud)\n

[NotNull]但是,您可以通过将提示应用于参数来缓解这种情况input

\n
public void EnsureNotNull([NotNull]ref Object? input) => input ??= new Object();\n
Run Code Online (Sandbox Code Playgroud)\n

现在,任何对objectafterEnsureNotNull()被调用的引用都将被(编译器)知道不是null

\n

EnsureInitialized()超载

\n

如果您考虑到上述内容来评估源代码LazyInitializer,那么您的特定问题的答案就会变得更加清晰。您调用的重载标记为target[AllowNull]这相当于返回MyClass?

\n
public static T EnsureInitialized<T>([AllowNull] ref T target, ref bool initialized, [NotNull] ref object? syncLock, Func<T> valueFactory) => \xe2\x80\xa6\n
Run Code Online (Sandbox Code Playgroud)\n

相比之下,我推荐的重载实现了[NotNull]上面讨论的属性,这相当于返回MyClass

\n
public static T EnsureInitialized<T>([NotNull] ref T? target, [NotNull] ref object? syncLock, Func<T> valueFactory) where T : class => \xe2\x80\xa6\n
Run Code Online (Sandbox Code Playgroud)\n

如果您评估实际逻辑,您会发现它们基本上是相同的,除了前者包含一个针对initialized\ truexe2\x80\x94 场景的转义子句,因此允许target潜在地保留null

\n

然而,由于您知道您正在使用一个类并且不需要一个null值,因此后一个重载是最佳选择,并且实现了我对您的一般问题的回答中概述的确切实践。

\n