【问题标题】:Nullable reference type in C#8 when using DTO classes with an ORM使用带有 ORM 的 DTO 类时,C#8 中的可空引用类型
【发布时间】:2019-11-30 16:52:51
【问题描述】:

我在具有数据传输对象 (DTO) 类的项目中激活了此功能,如下所示:

public class Connection
    {
        public string ServiceUrl { get; set; }
        public string? UserName { get; set; }
        public string? Password { get; set; }
        //... others 
    }

但我得到了错误:

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

这是一个 DTO 类,所以我没有初始化属性。这将是初始化类以确保属性不为空的代码的责任。

例如,调用者可以这样做:

var connection = new Connection
{
  ServiceUrl=some_value,
  //...
}

我的问题:启用 C#8 的可空性上下文时如何处理 DTO 类中的此类错误?

【问题讨论】:

  • 当您按照您显示的方式进行构造初始化时,默认构造函数会运行(将 ServiceUrl 初始化为其默认值(对于字符串来说,它是 null))。然后,初始化语句运行。如果您希望它不可为空,则需要退出所有已初始化的构造函数
  • POCO 并不意味着它没有构造函数或属性未初始化。这意味着Plain Old C# Object。就像任何其他对象一样,没有从任何特殊类继承。我怀疑你有一个不同的问题。如何创建 DTO - Data Transfer Objects
  • This should be the responsibility of the one initializing the class to ensure that the properties are non-null 类应始终处于有效状态。如果不允许null,则该属性从不为空。也许,您应该为 URL 使用专门的类而不是基本字符串,该类的值可以是 None 或`Missing`,例如 F# 中的 Option 类。 C# 8 允许您编写此类类并使用模式匹配检查它们
  • @Panagiotis Kanavos,在我的例子中,这个类有一个约束是无参数构造函数。所以我必须使用属性初始化器:public string ServiceUrl { get; set; } = default! ;。我希望 Roslyn 将来可能有一种方法来处理 ctor 范围之外的后期初始化。我使用的是 MayBe(如 Option 类),但我在 c#8 中为新代码切换到可为空的引用类型。
  • NRT 不再是 Maybes,事实上,Maybes 现在更重要且更易于使用。当然,前提是它们是使用 C# 习惯用法构建的,使它们易于使用模式匹配

标签: c# orm dto c#-8.0 nullable-reference-types


【解决方案1】:

您可以执行以下任一操作:

  1. EF Core 建议 initializing to null!null-forgiving operator

    public string ServiceUrl { get; set; } = null! ;
    //or
    public string ServiceUrl { get; set; } = default! ;
    
  2. 使用支持字段:

    private string _ServiceUrl;
    public string ServiceUrl
    {
        set => _ServiceUrl = value;
        get => _ServiceUrl
               ?? throw new InvalidOperationException("Uninitialized property: " + nameof(ServiceUrl));
    }
    

【讨论】:

  • 问题中提到他不能使用!操作符。
  • 我不能用!使用类型,但我提到的链接显示如何使用 null 进行初始化!。这是一种解决方法。我修改了问题。
  • "null!" 是 C# 8 中添加的 (null-forgiving) 运算符。正确的文档参考应为 docs.microsoft.com/en-us/dotnet/csharp/language-reference/…跨度>
  • 我希望我可以只写字符串!而不是字符串 .. { 获取;放; } = null!,这变得如此重复......或者只是标记类,它的所有属性都不应为空......
  • 我非常不同意 EF Core 文档的建议,因为它会将对象置于无效状态 - 使用 ! 运算符只是在掩盖问题,因为希望没有人尝试过在对象中使用从未初始化的 string 属性将导致 NRE。如果 EF Core 支持主构造函数,更好的解决方案是:如果 C# 具有更好的构造函数人体工程学。
【解决方案2】:

如果它不可为空,那么在初始化对象时编译器可以做什么?

字符串的默认值为null,所以你会

  1. 要么需要在声明中指定一个字符串默认值

    public string ServiceUrl { get; set; } = String.Empty;

  2. 或者在默认构造函数中初始化值,这样你就可以摆脱警告了

  3. 使用! 运算符(您不能使用)

  4. 如 robbpriestley 所述,使其可以为空。

【讨论】:

  • 当然也可以添加一个构造函数,接受服务 URL 作为不可为空的字符串。
  • ServiceUrl' 不能是 =String.Empty。还有其他不能为空的复杂类型的属性,所以我用default like: 'public MyClass MyProperty { get;放; } = 默认! ' 停止警告。这个类(和其他类)是无参数构造函数,最近在 ctor 范围之外初始化。
【解决方案3】:

我一直在使用新的 Nullable Reference Types (NRT) 功能,我必须承认我最大的抱怨是编译器在类中给你这些警告' 声明。

在我的工作中,我构建了一个微服务,试图解决所有导致代码相当复杂的警告,尤其是在处理作为 Nuget 包共享给 .NET Core 消费者的 EF Core、AutoMapper 和 DTO 时。 这个非常简单的微服务很快就变得一团糟,仅仅因为 NRT 功能导致我疯狂地使用非流行的编码风格。

在阅读了 Cezary Piątek 的文章 Improving non-nullable reference types handling 后,我发现了很棒的 SmartAnalyzers.CSharpExtensions.Annotations Nuget 包。

此 Nuget 包将不可为空的责任转移到实例化对象的调用者代码,而不是类声明。

在他的文章中,他说我们可以通过在您的 .cs 文件之一中写入以下行来在整个程序集中激活此功能

[assembly: InitRequiredForNotNull]

例如,您可以将其放入您的 Program.cs 文件中,但我个人更喜欢直接在我的 .csproj 中激活它

<ItemGroup>
    <AssemblyAttribute Include="SmartAnalyzers.CSharpExtensions.Annotations.InitRequiredForNotNullAttribute" />
</ItemGroup>

此外,我通过在我的 .editorconfig 文件中进行设置,将默认的 CSE001 Missing initialization for properties 错误更改为警告

[*.cs]
dotnet_diagnostic.CSE001.severity = warning

您现在可以像往常一样使用您的 Connection 类,而不会出现任何错误

var connection = new Connection()
{
    ServiceUrl = "ServiceUrl"
};

只需注意一件事。让我们这样考虑你的班级

public class Connection
{
    public string ServiceUrl { get; }
    public string? UserName { get; }
    public string? Password { get; }

    public Connection(string serviceUrl, string? userName = null, string? password = null)
    {
        if (string.IsNullOrEmpty(serviceUrl))
            throw new ArgumentNullException(nameof(serviceUrl));

        ServiceUrl = serviceUrl;
        UserName = userName;
        Password = password;
    }
}

在这种情况下,当您像这样实例化对象时

var connection = new Connection("serviceUrl");

SmartAnalyzers.CSharpExtensions.Annotations Nuget 包没有分析您的构造函数来检查您是否真的在初始化所有不可为空的引用类型。它只是信任它并相信您在构造函数中正确地做事。因此,即使您忘记了这样的不可为空的成员,它也不会引发任何错误

public Connection(string serviceUrl, string? userName = null, string? password = null)
{
    if (string.IsNullOrEmpty(serviceUrl))
        throw new ArgumentNullException(serviceUrl);

    UserName = userName;
    Password = password;
}

我希望你会喜欢这个 Nuget 包背后的想法,它已成为我在所有新的 .NET Core 项目中安装的默认包。

【讨论】:

  • 它似乎只在visual studio中工作,而不是vs code。也许我错过了一些东西,因为我对 .NET 很陌生。
  • @BeniaminoBaggins 我从不使用 VS Code 进行 C# 编程,但我只是用它打开了我的解决方案,看来你是对的,由于某些原因,这个库不适用于 VS Code。但是,我不确定当今用于 C# 编程的 VS Code 的状态。它显示我编译 errors 但我的项目编译和调试正常。真的没有意义。所以也许 VS Code 还不是 C# 编程的理想选择,我不知道...考虑在Github repository 上开票,也许开发者实际上可以解决这个问题!
  • 好的,谢谢。这些错误实际上只是警告,因此可以解释为什么它会编译。是的,我认为可以向 NuGet 包开发人员提出针对此问题的罚单。 Vs Code 非常适合 C#,我还没有遇到过此类问题
  • @BeniaminoBaggins 实际上这就是我用粗体写 errors 的原因,因为 VS Code 确实向我显示了一些警告和错误(与可空引用类型无关,但误报缺少依赖项)。这就是我说的原因,我不确定用于 C# 编程的 VS Code 的当前状态...
  • 哦,真的。嗯,是的,这是对 VS Code 中 C# 的奇怪和合理的担忧
【解决方案4】:

在某些情况下可能会派上用场的另一件事:

[SuppressMessage("Compiler", "CS8618")]

可以在成员或整个类型之上使用。


要考虑的另一件事是在文件顶部添加#nullable disable 以禁用整个文件的可空引用。

【讨论】:

    【解决方案5】:

    通常 DTO 类存储在单独的文件夹中,因此我只需根据路径模式在 .editorconfig 文件中禁用此诊断:

    [{**/Responses/*.cs,**/Requests/*.cs}]
    # CS8618: Non-nullable field is uninitialized. Consider declaring as nullable.
    dotnet_diagnostic.CS8618.severity = none
    

    【讨论】:

      【解决方案6】:

      可空引用类型的好处是可以有效地管理空引用异常。所以最好使用指令#nullable enable。它可以帮助开发人员在运行时避免空引用异常。

      如何帮助避免空引用异常: 在静态流分析期间,编译器将确保以下两件事。

      1. 变量已明确分配给非空值。
      2. 变量或表达式之前已针对 null 进行过检查 取消引用它。

      如果不满足以上条件,编译器会抛出警告。 注意:所有这些活动都是在编译时发出的。

      空引用类型有什么特别之处:

      在 C# 8.0 之后,引用类型被视为不可为空。如果引用类型变量没有正确处理空值,那么编译器会及时发出警告。

      如果我们知道引用变量是 NULLABLE,我们可以做什么:

      1. 附加“?”在变量(示例)字符串之前?姓名
      2. 使用“null forgiving operator”(示例)字符串名称 {get;set;} = 空值!;或字符串名称 {get;set;} = 默认!;
      3. 使用“支持字段” (示例):

        私有字符串_name; 公共字符串名称 {get { return _name; } 设置 {_name = value;} }

      如果我们知道引用变量是非空的,我们可以做什么:

      如果它不可为空,则使用构造函数初始化属性。

      不可为空的引用类型的好处:

      1. 更好地处理空引用异常
      2. 借助编译时警告,开发人员可以及时更正他们的代码。
      3. 开发人员在设计类时会将意图传达给编译器。 此后编译器将在整个代码中强制执行意图。

      【讨论】:

        【解决方案7】:

        要消除 DTO 上的警告,请在 DTO 的 cs 文件的开头指定: #pragma 警告禁用 CS8618

        最后: #pragma 警告恢复 CS8618

        【讨论】:

        • OP 提出了“我的问题:启用 C#8 的可空性上下文时如何处理 DTO 类中的此类错误?”。根据经验,您的解决方案有效。不幸的是,在任何 MSDN 文档中似乎都没有关于使用此批准或空宽恕运算符的明确参考。也许你可以详细说明?
        猜你喜欢
        • 2018-07-18
        • 1970-01-01
        • 2020-03-23
        • 2020-04-19
        • 1970-01-01
        • 2020-04-12
        • 2019-12-17
        • 1970-01-01
        相关资源
        最近更新 更多