可空引用类型和选项模式

Posted

技术标签:

【中文标题】可空引用类型和选项模式【英文标题】: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 不可为空的属性“名称”未初始化。考虑将属性声明为可为空。

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

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

【问题讨论】:

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

【讨论】:

补充推荐阅读:docs.microsoft.com/en-us/dotnet/csharp/nullable-attributes【参考方案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 或空字符串),则抛出异常,这是您应该追求的。

【讨论】:

以上是关于可空引用类型和选项模式的主要内容,如果未能解决你的问题,请参考以下文章

可空类型产生警告,因为项目已启用可空引用类型

可空引用类型 - 通过接受的参数返回类型可空性

使用可空引用类型时,OData 元数据不生成可空方面

C# 8中的可空引用类型

可空类型是引用类型吗?

可空引用类型意外 CS8629 可空值类型可能为带有临时变量的空