为 DTO 使用可空引用类型的最佳实践 [关闭]

Posted

技术标签:

【中文标题】为 DTO 使用可空引用类型的最佳实践 [关闭]【英文标题】:Best practice for using Nullable Reference Types for DTOs [closed] 【发布时间】:2020-04-12 22:24:00 【问题描述】:

我有一个 DTO,它是通过从 DynamoDB 表中读取来填充的。假设它目前看起来像这样:

public class Item

    public string Id  get; set;  // PK so technically cannot be null
    public string Name  get; set;  // validation to prevent nulls but this doesn't stop database hacks
    public string Description  get; set;  // can be null

是否有任何最佳实践来解决这个问题?我宁愿避免使用非无参数构造函数,因为这与 Dynamo SDK(以及其他)中的 ORM 很不符。

public string Id get; set; = ""; 对我来说似乎很奇怪,因为这将永远发生,因为Id 是一个PK 并且永远不能为空。 "" 有什么用呢?

那么有什么最佳实践吗?

我是否应该将它们全部标记为 string? 以表示它们可以为空,即使有些永远不应该是。 我是否应该用"" 初始化IdName,因为它们应该永远不会为空,这表明了意图,即使永远不会使用""。 以上的一些组合

请注意:这是关于 C#8 nullable reference types 如果您不知道最好不要回答。

【问题讨论】:

除了= "",您可以使用= null! 来初始化您知道永远不会有效地为null 的属性(当编译器无法知道时)。如果Description 可以合法地为null,则应将其声明为string?。或者,如果对 DTO 的可空性检查比帮助更麻烦,您可以简单地将类型包装在 #nullable disable / #nullable restore 以仅关闭此类型的 NRT。 @JeroenMostert 你应该把它作为答案。 @Magnus:我不愿意回答任何询问“最佳实践”的问题;这样的事情是广泛的和主观的。我希望 OP 可以使用我的评论来制定自己的“最佳实践”。 @IvanGarcíaTopete:虽然我同意将字符串用作主键是不寻常的,甚至根据具体情况可能是不可取的,但 OP 的数据类型选择与问题无关。这可以很容易地应用于不是主键的必需的、不可为空的字符串属性,甚至是作为复合主键一部分的字符串字段,问题仍然存在。 @MassDotNet 可为空的引用类型不会阻止分配引用 null。它们仍然是可以具有空值的引用类型。它所做的只是添加编译器警告,让您知道您可能在预期非空值的地方使用空值。但应该注意的是,反序列化代码可能会考虑类型并拒绝空值,而如果关闭可空引用类型则不会。 【参考方案1】:

作为一个选项,您可以将 default 文字与 null forgiving operator 结合使用

public class Item

    public string Id  get; set;  = default!;
    public string Name  get; set;  = default!;
    public string Description  get; set;  = default!;

由于您的 DTO 是从 DynamoDB 填充的,因此您可以使用 MaybeNull/NotNull postcondition attributes 来控制可空性

MaybeNull 不可为空的返回值可能为空。 NotNull 可以为空的返回值永远不会为空。

但是这些属性只影响使用它们注释的成员的调用者的可空分析。通常,您将这些属性应用于方法返回、属性和索引器 getter。

因此,您可以考虑所有属性不可为空,并用MaybeNull 属性装饰它们,表明它们可能返回null

public class Item

    public string Id  get; set;  = "";
    [MaybeNull] public string Name  get; set;  = default!;
    [MaybeNull] public string Description  get; set;  = default!;


以下示例显示了更新后的Item 类的用法。如您所见,第二行没有显示警告,但第三行显示

var item = new Item();
string id = item.Id;
string name = item.Name; //warning CS8600: Converting null literal or possible null value to non-nullable type.

或者您可以将所有属性设置为可空属性并使用NoNull 表示返回值不能为null(例如Id

public class Item

    [NotNull] public string? Id  get; set; 
    public string? Name  get; set; 
    public string? Description  get; set; 

警告将与前面的示例相同。

还有AllowNull/DisallowNullprecondition attributes用于输入参数、属性和索引器设置器,以类似的方式工作。

AllowNull 不可为空的输入参数可能为空。 DisallowNull 可为空的输入参数永远不应为空。

我认为它不会对您有所帮助,因为您的类是从数据库中填充的,但是您可以使用它们来控制属性设置器的可空性,例如第一个选项

[MaybeNull, AllowNull] public string Description  get; set; 

第二个

[NotNull, DisallowNull] public string? Id  get; set; 

可以在devblog article中找到一些有用的细节和后置/前置条件示例

【讨论】:

抱歉,这是我第一次听说 null 宽恕运算符。在您的第一个代码示例中设置 default! 有什么作用? @MassDotNet null-forgiving operator 参考。在这里,它允许在使用默认值初始化属性时避免编译器警告 从 EF 内核中学到的一些知识可能会有所帮助:a) 如果我们真的知道某个属性永远不会为空,我们希望编译器将其视为不可为空(例如字符串、无属性)并使用 null-原谅运营商“让它工作”。 b)字符串/字符串?影响数据库方案的生成和读取方式 - 例如尝试将 NULL 从 DB 读取到字符串属性中会抛出异常。 - s.a. docs.microsoft.com/en-us/ef/core/miscellaneous/…【参考方案2】:

更新: 从 2021 年 3 月 9 日发布的 .NET 5.0.4 (SDK 5.0.201) 开始,以下方法现在将产生 @987654324 @ 警告。鉴于此,如@Pavel-Anikhouski’s answer 顶部所讨论的那样,使用容错运算符(即default!)将值设置为default 的方法现在是您最好的选择。


在这种情况下,教科书的答案是为您的Id 属性使用string?,但[NotNull] 属性装饰它:

public class Item

  [NotNull] public string? Id  get; set; 
  public string Name  get; set; 
  public string? Description  get; set; 

参考: 根据the documentation,[NotNull] 属性“指定输出不为空,即使相应类型允许。”

那么,这里到底发生了什么?

    首先,string? 返回类型可防止编译器警告您该属性在构造期间未初始化,因此默认null。 然后,[NotNull] 属性会在将属性分配给不可为空的变量或尝试取消引用它时防止警告,因为您通知编译器的静态流分析,实际上,这属性永远不会是null

警告: 与所有涉及 C# 可空性上下文的情况一样,技术上没有什么可以阻止您在这里仍然返回 null 值,因此可能会引入一些下游异常;即,没有开箱即用的运行时验证。所有 C# 提供的只是编译器警告。当您介绍[NotNull] 时,您实际上是通过向它提供有关您的业务逻辑的提示来覆盖该警告。因此,当您使用 [NotNull] 注释属性时,您将承担自己的承诺,即“这将永远发生,因为 Id 是一个 PK 并且永远不能为空。”

为了帮助您保持这一承诺,您可能另外希望使用[DisallowNull] 属性注释属性:

public class Item

  [NotNull, DisallowNull] public string? Id  get; set; 
  public string Name  get; set; 
  public string? Description  get; set; 

参考:根据the documentation,[DisallowNull]属性“指定null被禁止作为输入,即使相应的类型允许。”

这可能与您的情况无关,因为这些值是通过数据库分配的,但是如果您尝试将 null(able) 值分配给 Id[DisallowNull] 属性会给您一个警告, 即使返回类型允许它为空。在这方面,就 C# 的静态流分析而言,Id 的行为完全类似于string,同时允许值在构造之间保持未初始化对象和财产的人口。

注意: 正如其他人所提到的,您还可以通过为Id 分配默认值default!null! 来获得几乎相同的结果。 诚然,这在某种程度上是一种风格偏好。我更喜欢使用可空性注释,因为它们更明确并提供精细控制,而很容易滥用 ! 作为关闭的方式起来编译器。如果我知道我永远不会使用该值(即使它是默认值),使用值显式初始化属性也会让我烦恼。

【讨论】:

以上是关于为 DTO 使用可空引用类型的最佳实践 [关闭]的主要内容,如果未能解决你的问题,请参考以下文章

DTO:最佳实践

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

将可空整数初始化为默认值的最佳实践?

将可空引用类型转换为不可空引用类型,不那么冗长

向客户端发送数据的最佳实践是啥:POCO 还是 DTO?

Java对象拷贝原理剖析及最佳实践