"ref" 参数和可为空检查的良好做法
Good practice for "ref" arguments and nullable checking
当我遇到以下情况时,最好的方法是什么?
上下文是启用了最新 nullable checking 的 .NET Core 3.x 应用程序,以及带有 ref
关键字的方法。真正的代码更复杂,但更简单的版本可能是这样的:
private static bool _initialized = false;
private static object _initializationLock = new object();
private static MyClass _initializationTarget; //suggestion to mark as nullable
public MyClass GetInstance()
{
return LazyInitializer.EnsureInitialized(
ref _initializationTarget,
ref _initialized,
ref _initializationLock,
() => new MyClass()
);
}
EnsureInitialized()
方法在开头采用 _initializationTarget
引用,即 null
,因此将其标记为可为空似乎是正确的调整。但是,同样的方法可以确保变量被正确填充。
我找不到比以下更好的模式了——但它真的是最好的吗?
private static bool _initialized = false;
private static object _initializationLock = new object();
private static MyClass? _initializationTarget; //marked as nullable
public MyClass GetInstance()
{
//the return value must also nullable
MyClass? inst = LazyInitializer.EnsureInitialized(
ref _initializationTarget,
ref _initialized,
ref _initializationLock,
() => new MyClass()
);
return inst!; //null-forgive here
}
TL;DR: 如果删除 initialized
参数,那么 EnsureInitialized()
将保证 returned 对象是 [NotNull]
—even if you pass in an unitialized MyClass?
reference—and thus there won't be a need to use the null-forgiving operator (!
).
完整答案
这是个好问题。您的具体示例的答案非常简单,但我也想借此机会解决您关于如何处理[=21]的一般问题=] 参数与 C# 8.0 的可空引用类型。这样做不仅有助于解决类似的其他情况,而且还可以解释为什么您的特定示例的解决方案有效。
具体例子
虽然 the EnsureInitialized()
documentation 并未完全清楚这一点,但您调用的特定重载适用于您可能 想要 一个 null
目标的情况。也就是说,如果您要为 initialized
参数传递值 true
,那么它将 return null
。这个条件就是为什么它 必须 return 可空类型。
由于您不想允许null
值——并且不使用值类型——您只需删除initialized
争论。您仍然需要将 _initializationTarget
声明为可为空,因为它未在您的构造函数中初始化。但是,此重载向编译器保证在 EnsureInitialized()
具有 运行:
之后 target
参数将不再是 null
private static object _initializationLock = new object();
private static MyClass? _initializationTarget; //marked as nullable
public MyClass GetInstance() =>
LazyInitializer.EnsureInitialized(
ref _initializationTarget,
ref _initializationLock,
() => new MyClass()
);
请注意,虽然它仍然传递一个 null(able) MyClass?
,但它自信地 return 是一个 MyClass
而无需求助于 null-forgiving operator (!
).
一般问题
随着您深入研究 C# 8.0 的可空引用类型,您会发现 Roslyn 的静态流分析中存在许多差距,这些差距无法通过简单地使用 ?
和 !
来消除歧义运营商单独。好在微软预见到了这个问题,为我们提供了多种attributes which can be used to provide compiler hints.
例子
这里有一个基本示例来说明一般问题:
public void EnsureNotNull(ref Object? input) => input ??= new Object();
如果您使用以下代码调用此方法,您将收到 CS8602
警告:
Object? object = null;
EnsureNotNull(ref object);
_ = object.ToString(); //CS8602; Dereference of a possibly null reference
但是,您可以通过将 [NotNull]
hint 应用于 input
参数来缓解这种情况:
public void EnsureNotNull([NotNull]ref Object? input) => input ??= new Object();
现在,在调用 EnsureNotNull()
之后对 object
的任何引用都将被(编译器)识别为不是 null
。
EnsureInitialized()
重载
如果计算 the source code for LazyInitializer
with the above in mind, the answer to your specific problem becomes a lot more clear. The overload you were calling 标记 target
为 [AllowNull]
,这等同于 returning MyClass?
:
public static T EnsureInitialized<T>([AllowNull] ref T target, ref bool initialized, [NotNull] ref object? syncLock, Func<T> valueFactory) => …
相比之下,the overload that I've recommended 实现了上面讨论的 [NotNull]
属性,相当于 returning MyClass
:
public static T EnsureInitialized<T>([NotNull] ref T? target, [NotNull] ref object? syncLock, Func<T> valueFactory) where T : class => …
如果您评估实际逻辑,您会发现它们基本相同,除了前者包含针对 initialized
为 true
的情况的免责条款——因此,允许 target
可能保持 null
.
由于您知道您正在使用class并且不想要null
值,但是,后者重载是最佳选择,并且实现了我对您的一般问题的回答中概述的确切做法。
当我遇到以下情况时,最好的方法是什么?
上下文是启用了最新 nullable checking 的 .NET Core 3.x 应用程序,以及带有 ref
关键字的方法。真正的代码更复杂,但更简单的版本可能是这样的:
private static bool _initialized = false;
private static object _initializationLock = new object();
private static MyClass _initializationTarget; //suggestion to mark as nullable
public MyClass GetInstance()
{
return LazyInitializer.EnsureInitialized(
ref _initializationTarget,
ref _initialized,
ref _initializationLock,
() => new MyClass()
);
}
EnsureInitialized()
方法在开头采用 _initializationTarget
引用,即 null
,因此将其标记为可为空似乎是正确的调整。但是,同样的方法可以确保变量被正确填充。
我找不到比以下更好的模式了——但它真的是最好的吗?
private static bool _initialized = false;
private static object _initializationLock = new object();
private static MyClass? _initializationTarget; //marked as nullable
public MyClass GetInstance()
{
//the return value must also nullable
MyClass? inst = LazyInitializer.EnsureInitialized(
ref _initializationTarget,
ref _initialized,
ref _initializationLock,
() => new MyClass()
);
return inst!; //null-forgive here
}
TL;DR: 如果删除 initialized
参数,那么 EnsureInitialized()
将保证 returned 对象是 [NotNull]
—even if you pass in an unitialized MyClass?
reference—and thus there won't be a need to use the null-forgiving operator (!
).
完整答案
这是个好问题。您的具体示例的答案非常简单,但我也想借此机会解决您关于如何处理[=21]的一般问题=] 参数与 C# 8.0 的可空引用类型。这样做不仅有助于解决类似的其他情况,而且还可以解释为什么您的特定示例的解决方案有效。
具体例子
虽然 the EnsureInitialized()
documentation 并未完全清楚这一点,但您调用的特定重载适用于您可能 想要 一个 null
目标的情况。也就是说,如果您要为 initialized
参数传递值 true
,那么它将 return null
。这个条件就是为什么它 必须 return 可空类型。
由于您不想允许null
值——并且不使用值类型——您只需删除initialized
争论。您仍然需要将 _initializationTarget
声明为可为空,因为它未在您的构造函数中初始化。但是,此重载向编译器保证在 EnsureInitialized()
具有 运行:
target
参数将不再是 null
private static object _initializationLock = new object();
private static MyClass? _initializationTarget; //marked as nullable
public MyClass GetInstance() =>
LazyInitializer.EnsureInitialized(
ref _initializationTarget,
ref _initializationLock,
() => new MyClass()
);
请注意,虽然它仍然传递一个 null(able) MyClass?
,但它自信地 return 是一个 MyClass
而无需求助于 null-forgiving operator (!
).
一般问题
随着您深入研究 C# 8.0 的可空引用类型,您会发现 Roslyn 的静态流分析中存在许多差距,这些差距无法通过简单地使用 ?
和 !
来消除歧义运营商单独。好在微软预见到了这个问题,为我们提供了多种attributes which can be used to provide compiler hints.
例子
这里有一个基本示例来说明一般问题:
public void EnsureNotNull(ref Object? input) => input ??= new Object();
如果您使用以下代码调用此方法,您将收到 CS8602
警告:
Object? object = null;
EnsureNotNull(ref object);
_ = object.ToString(); //CS8602; Dereference of a possibly null reference
但是,您可以通过将 [NotNull]
hint 应用于 input
参数来缓解这种情况:
public void EnsureNotNull([NotNull]ref Object? input) => input ??= new Object();
现在,在调用 EnsureNotNull()
之后对 object
的任何引用都将被(编译器)识别为不是 null
。
EnsureInitialized()
重载
如果计算 the source code for LazyInitializer
with the above in mind, the answer to your specific problem becomes a lot more clear. The overload you were calling 标记 target
为 [AllowNull]
,这等同于 returning MyClass?
:
public static T EnsureInitialized<T>([AllowNull] ref T target, ref bool initialized, [NotNull] ref object? syncLock, Func<T> valueFactory) => …
相比之下,the overload that I've recommended 实现了上面讨论的 [NotNull]
属性,相当于 returning MyClass
:
public static T EnsureInitialized<T>([NotNull] ref T? target, [NotNull] ref object? syncLock, Func<T> valueFactory) where T : class => …
如果您评估实际逻辑,您会发现它们基本相同,除了前者包含针对 initialized
为 true
的情况的免责条款——因此,允许 target
可能保持 null
.
由于您知道您正在使用class并且不想要null
值,但是,后者重载是最佳选择,并且实现了我对您的一般问题的回答中概述的确切做法。