【问题标题】:How to create immutable objects in C#?如何在 C# 中创建不可变对象?
【发布时间】:2011-02-23 21:20:21
【问题描述】:

在关于Best practices for C# pattern validation 的问题中,投票最高的答案 说:

我倾向于在构造函数中执行所有验证。这是必须的,因为我几乎总是创建不可变对象。

究竟如何在 C# 中创建不可变对象?您只使用readonly 关键字吗?

如果您想在 Entity Framework 生成的模型类的构造函数中进行验证,这将如何工作?

会像下面这样吗?

public partial readonly Person
{
    public Person()
}

【问题讨论】:

  • 不是重复的:因为这个问题是关于验证,而不是创建。
  • 我问的是验证,而不是创建。
  • 它不是重复的,因为问题是询问 EF 生成的类。

标签: c# validation entity-framework-4 immutability


【解决方案1】:

这里有趣的问题是您来自 cmets 的问题:

你会拥有什么样的对象而不需要在某些时候修改值?我猜不是模型课,对吗?我不得不在我的数据库中更改一个人的名字 - 这不符合这个想法。

好吧,考虑一下已经不可变的东西。数字是不可变的。一旦你有了数字 12,它就是 12。你不能改变它。如果您有一个包含 12 的变量,您可以将变量的内容更改为 13,但您更改的是 变量,而不是 数字 12。

字符串也是如此。 “abc”是“abc”,它永远不会改变。如果您有一个包含“abc”的变量,您可以将其更改为“abcd”,但不会更改“abc”,而是更改变量。

列表呢? {12, "abc"} 是 12 后跟 "abc" 的列表,并且该列表永远不会改变。列表 {12, "abcd"} 是一个不同的列表。

这就是事情出轨的地方。因为在 C# 中,您可以采用任何一种方式。如果允许列表在不改变其身份的情况下改变其内容,则可以说这两个列表之间存在引用身份。

当您谈论“模型”时,您一针见血。您是否正在为改变的东西建模?如果是这样,那么用变化的类型对其进行建模可能是明智的。这样做的好处是模型的特征与被建模的系统相匹配。不利的一面是,执行诸如“回滚”功能之类的事情变得非常棘手,您可以在其中“撤消”更改。

也就是说,如果你将 {12, "abc"} 变异为 {12, "abcd"} 然后想要回滚这个变异,你是怎么做的呢?如果列表是不可变的,您只需保留这两个值并选择您希望哪一个成为“当前”值。如果列表是可变的,那么您必须让撤消逻辑围绕一个知道如何撤消突变的“撤消函数”。

至于你的具体例子,你当然可以创建一个不可变的数据库。如何更改不可变数据库中某人的姓名?你没有。您创建一个新 数据库,其中包含您想要的数据。不可变类型的诀窍是高效地执行此操作,而无需复制数十亿字节。不可变数据结构设计需要找到巧妙的方法在两个几乎相同的结构之间共享状态。

【讨论】:

  • 感谢您的精彩解释,我一直在为可变和不可变对象的概念苦苦挣扎。我发现很难掌握何时在 C# 或 C++ 中使用它。
【解决方案2】:

将所有字段声明为只读是朝着创建不可变对象迈出的重要一步,但仅此还不够。这是因为只读字段仍然可以是对可变对象的引用。

在 C# 中,编译器不强制执行不变性。你只需要小心。

【讨论】:

  • ..有时您可以返回这些可变字段的不可变(或只读)版本,例如List<T>.AsReadOnly 供您使用。
  • +1 表示最后一行。事实上,C# 没有提供糖来从现有对象创建一个新对象,但其中一个属性已更改。即使可以使对象不可变,创建新副本也太乏味了。
【解决方案3】:

这个问题有两个方面:

  1. 实例化对象时的不可变类型
  2. EF 实例化对象时的不可变类型

第一个方面需要这样的结构:

public class MyClass
{
  private readonly string _myString;
  public string MyString
  {
    get
    {
      return _myString;
    }
  }

  public MyClass(string myString)
  {
    // do some validation here

    _myString = myString;
  }
}

现在的问题 - EF。 EF 需要无参数构造函数,并且 EF 必须在属性上有设置器。我问的很像question here。

您的类型必须如下所示:

public class MyClass
{
  private string _myString;
  public string MyString
  {
    get
    {
      return _myString;
    }
    private set
    {
      _myString = value;
    }
  }

  public MyClass(string myString)
  {
    // do some validation here

    _myString = myString;
  }

  // Not sure if you can change accessibility of constructor - I can try it later
  public MyClass()
  {}
}

您还必须通知 EF MyString 属性的私有设置器 - 这是在 EDMX 文件的实体属性中配置的。显然,当 EF 从 DB 中具体化对象时,将不会进行验证。此外,您将无法使用 ObjectContext.CreateObject 之类的方法(您将无法填充对象)。

Entity Object T4 模板和默认代码生成创建工厂方法 CreateMyClass 而不是带有参数的构造函数。 POCO T4 模板不生成工厂方法。

我没有先用 EF 代码尝试这个。

【讨论】:

    【解决方案4】:
    【解决方案5】:

    不可变值对象是不能更改的值对象。你不能修改它的状态,你必须创建新的

    查看 Eric Lippert 的博客:

    不变性的种类 https://docs.microsoft.com/en-us/archive/blogs/ericlippert/immutability-in-c-part-one-kinds-of-immutability

    看看

    Immutable object pattern in C# - what do you think?

    【讨论】:

    • 好的,所以我提出这个问题的答案的基本前提是,一旦它通过了构造函数,它就可以 100% 获得犹太洁食认证并且有效?您将拥有什么样的对象,您不需要在某些时候修改值?我猜不是模型课,对吗?我不得不在我的数据库中更改一个人的名字 - 这不符合这个想法。
    【解决方案6】:

    如果您想在 Entity Framework 生成的模型类的构造函数中进行验证,这将如何工作?

    它在这种情况下不起作用,因为 EF 要求实体类的属性是公共的,否则它无法实例化它。

    但欢迎您在代码中进一步使用不可变对象。

    【讨论】:

      【解决方案7】:

      C# 9 将新功能名称命名为 Record。如果您想让单个属性不可变,则仅初始化属性非常有用。如果您希望整个对象是不可变的并且表现得像一个值,那么您应该考虑将其声明为记录:

      public data class Person
      {
          public string FirstName { get; init; }
          public string LastName { get; init; }
      }
      

      类声明中的 data 关键字将其标记为记录。

      参考:https://devblogs.microsoft.com/dotnet/welcome-to-c-9-0/#records

      【讨论】:

        【解决方案8】:

        @Eric Lippert 很好的评论,但除了回答问题:

        你会拥有什么样的对象而不需要修改 某个时候的价值观?我猜不是模型课,对吗?我有 不得不在我的数据库中更改一个人的名字——这不合适 有了这个想法。

        假设您有一个大型数据结构,并且您想查询其信息,但它一直在变化。您需要某种锁定系统,以确保当有人将某物从一个地方存入另一个地方时,您不会说尝试计算系统中的总数。 (比如说仓库管理系统)

        这很难做到,因为这些事情总是以意想不到的方式影响事物,数据在你的脚下发生变化。

        如果您可以在不更新大型数据结构时将其冻结,从而无法更改内存并以一致的状态暂停,该怎么办?现在,当您想再次更改它时,您必须将数据结构复制到一个新位置,而且它相当大,所以这是一个缺点,但好处是您不必锁定任何东西,因为数据的新副本未共享直到它被更新。这意味着任何人在任何时候都可以读取数据结构的最新副本,做复杂的事情。

        如果您讨厌处理并发问题并且没有太多数据要处理,那么这个概念非常有用。 (例如,如果 1MB 的数据并以 10/秒的速度更新,则复制了 10MB 的数据)

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2021-05-04
          • 1970-01-01
          • 1970-01-01
          • 2010-12-18
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多