【问题标题】:Nullable Reference Types and the Options Pattern可空引用类型和选项模式
【发布时间】:2020-01-24 23:03:01
【问题描述】:

我们如何将不可为空的引用类型Options pattern结合使用?

假设我们有一个名为 MyOptions 的选项模型。

需要这些选项的服务将IOptions<MyOptions> options 注入到构造函数中。

IServiceCollection 上配置选项,如下所示:

services
    .AddOptions<MyOptions>()
    .Configure(options =>
    {
        options.Name = "ABC";
    });

现在,问题在于MyOptions的定义:

public sealed class MyOptions
{
    public string Name { get; set; }
}

产生警告:

CS8618 不可为空的属性“名称”未初始化。考虑将属性声明为可为空。

  1. 我们不想让Name 可以为空,因为我们需要在任何地方进行传统的空检查(这与不可为空的引用类型的目的背道而驰)
  2. 我们无法创建构造函数来强制使用不可为空的name 值创建MyOptions 类,因为Configure 方法为我们构造了选项实例
  3. 我们不能使用 null-forgiving 运算符 技巧 (public string name { get; set; } = null!;),因为这样我们就不能确保设置了 Name 属性,我们最终会得到一个 nullName 属性中,这不是预期的(在服务内)

还有其他我忘记考虑的选项吗?

【问题讨论】:

  • string.Empty 是否适合您的用例?我假设您对填充值的检查是使用string.IsNullOrWhiteSpace
  • 您检查过 IValidateOptions 吗? docs.microsoft.com/en-us/dotnet/api/…
  • 你是对的,我们仍然需要验证空字符串(或其他约束)。我刚刚发现我们可以将额外的.Validate() 调用链接到可能满足我们需求的选项注册方法。谢谢。
  • 是的,我同意。我认为他们试图与现有的“可空值类型”功能保持一致,但恕我直言,与拥有一个更准确地描述该功能实际功能的名称相比,这不是一个重要的目标。 :)
  • @huysentruitw 问题不是关于模式,而是关于它的初始化机制,这与任何反序列化器没有什么不同。配置有同样的问题——两种机制都使用基于属性的初始化,因此会产生可空性错误。 两个都必须使用基于构造函数的初始化来避免该问题。

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


【解决方案1】:

看来,您在这里有两种可能的选择。第一个是使用空字符串(而不是null 值)初始化Options 属性以避免null 检查

public sealed class MyOptions
{
    public string Name { get; set; } = "";
}

第二个是使所有属性都可以为空,并使用DisallowNull前置条件和NotNull后置条件来装饰它们。

DisallowNull 表示可空输入参数永远不应为空,NotNull - 可空返回值永远不会为空。 但是这些属性只影响使用它们注释的成员的调用者的可空分析。因此,您表示您的属性永远不能返回null 或设置为null,尽管声明可以为空

public sealed class MyOptions
{
    [NotNull, DisallowNull]public string? Name { get; set; }
}

以及使用示例

var options = new MyOptions();
options.Name = null; //warning CS8625: Cannot convert null literal to non-nullable reference type.
options.Name = "test";

但是下一个示例没有显示警告,因为可空分析在对象初始化器中还不能正常工作,请参阅 Roslyn 存储库中的 GitHub 问题 40127

var options = new MyOptions { Name = null }; //no warning

编辑:此问题已修复,于 2020 年 3 月在 16.5 版中发布,在将 VS 更新到最新版本后应该会消失。)

属性getter的同一张图,下面的示例没有显示任何警告,因为您指出可以为空的返回类型不能是null

var options = new MyOptions();
string test = options.Name.ToLower();

但尝试设置 null 值并获取它会生成警告(编译器足够聪明,可以检测到此类情况)

var options = new MyOptions() { Name = null };
string test = options.Name.ToLower(); //warning CS8602: Dereference of a possibly null reference.

【讨论】:

    【解决方案2】:

    如果该属性的预期行为是它最初可能包含 null 但绝不应设置为 null,请尝试使用 DisallowNullAttribute

    #nullable enable
    
    using System.Diagnostics.CodeAnalysis;
    
    public sealed class MyOptions
    {
        [DisallowNull]
        public string? Name { get; set; }
    
        public static void Test()
        {
            var options = new MyOptions();
            options.Name = null; // warning
            options.Name = "Hello"; // ok
        }
    
        public static void Test2()
        {
            var options = new MyOptions();
            options.Name.Substring(1); // warning on dereference
        }
    }
    

    【讨论】:

    【解决方案3】:

    您应该选择选项 3)。初始化过程中不可为空的属性是否为空都没有关系。重要的是稍后选项实例的使用者的观点。

    我们可以通过使用[Required] 属性注释它然后在选项构建器上调用ValidateDataAnnotations() 来确保选项属性不会为空,例如:

    public class MyOptions {
        [Required] public string MyRequiredText { get; set; } = null!;
        public string? MyOptionalText { get; set; };
    }
    
    services.AddOptions<MyOptions>()
        .Bind(Configuration.GetSection("MySettings"))
        .Configure(o => arbitrary configuration action here...)
        .ValidateDataAnnotations();
    
    // When options are consumed from DI by `IOptions` or similar interfaces, 
    // it is certain that MyRequiredText will not be null - in such case, exception will be thrown instead
    

    当从 DI 请求选项并且框架首先创建实例时,它会在执行所有注册的配置处理程序后验证属性上的所有属性。如果验证失败(例如 required 属性为 null 或空字符串),则抛出异常,这是您应该追求的。

    【讨论】:

      猜你喜欢
      • 2020-08-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-04-06
      • 1970-01-01
      • 1970-01-01
      • 2022-01-08
      相关资源
      最近更新 更多