【问题标题】:Good practice for "ref" arguments and nullable checking“ref”参数和可空检查的良好做法
【发布时间】:2023-03-07 23:17:01
【问题描述】:

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

上下文是一个启用了最新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
}

【问题讨论】:

  • 能分享一下LazyInitializer.EnsureInitialized的方法吗?
  • 这是标准库的一部分:docs.microsoft.com/en-us/dotnet/api/…
  • 在我看来,您的方法在这种情况下是最好的。首先,通过将字段_initializationTarget 声明为可为空的引用类型,您隐含地表示它可以包含null 值。它使编译器能够检测到它可以为空的用法。例如,如果您声明另一个使用_initializationTarget 的方法而不检查它是否为null。其次,方法LazyInitializer.EnsureInitialized 不保证返回non-null 值。可以返回null有几种情况,比如_initializedtrue_initializationTargetnull
  • 但在您的情况下,您肯定知道LazyInitializer.EnsureInitialized 必须返回non-null 值,因为提供的参数保证返回值不会是null。代码分析器不够聪明,无法识别您的情况 LazyInitializer.EnsureInitialized 保证结果不为空。因此,要满足代码分析器,您必须使用null-forgiving operator
  • @IliarTurdushev:没有很好的文档记录,但实际上有重载的EnsureEnabled() 被注释为[NotNull] 而不是[MaybeNull]——因此向编译器保证target 引用在执行后不再是null。在这些情况下,不需要容错运算符。不幸的是,该文档不包括可空性属性,并且注释未能突出这种细微差别。我在回答中对此进行了扩展,并附有源代码链接。

标签: c# .net-core .net-core-3.0 c#-8.0 nullable-reference-types


【解决方案1】:

TL;DR:如果你删除initialized参数,那么EnsureInitialized()将保证返回的对象是[NotNull]——即使你传入了一个统一的MyClass?引用——因此不需要使用null-forgiving operator (!)。


完整答案

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

具体例子

虽然the EnsureInitialized() documentation 并没有完全清楚地说明这一点,但您调用的特定重载是针对您可能想要null 目标的情况。也就是说,如果您要为 initialized 参数传递一个值 true,那么它将返回 null。这个条件就是为什么它必须返回一个可为空的类型。

由于您想要允许null 值——并且不使用值类型——你只需删除initialized 参数。您仍然需要将您的 _initializationTarget 声明为可为空,因为它没有在您的构造函数中被初始化。但是,此重载向编译器保证 target 参数在 EnsureInitialized() 运行后将不再是 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?,但它可以自信地返回 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 的任何引用都将(由编译器)知道不是@​​987654361@。

EnsureInitialized() 重载

如果您根据上述情况评估the source code for LazyInitializer,您的具体问题的答案就会变得更加清晰。 overload you were callingtarget 标记为[AllowNull],相当于返回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] 属性,相当于返回MyClass

public static T EnsureInitialized<T>([NotNull] ref T? target, [NotNull] ref object? syncLock, Func<T> valueFactory) where T : class => …

如果您评估实际逻辑,您会发现它们基本相同,除了前者包含一个转义子句,用于initializedtrue 的场景,因此,允许target 可能保持null

因为你知道你正在使用一个类并且想要一个null值,然而,后一个重载是最好的选择,并且实施我对您的一般问题的回答中概述的确切做法。

【讨论】:

  • 优秀的答案,应该是官方文档的一部分!我仍然很少练习 nullables,并且我完全跳过了(暂时)一些涉及泛型的代码部分。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-04-29
  • 1970-01-01
  • 1970-01-01
  • 2023-03-16
相关资源
最近更新 更多