如何在C#中创建不可变对象?

36

在一个关于C#模式验证最佳实践的问题中,得票最高的回答如下:

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

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

如果您想在Entity Framework生成的模型类的构造函数中进行验证,该怎么做呢?

它看起来像下面这样吗?

public partial readonly Person
{
    public Person()
}

6
可能是如何创建不可变类?的重复问题。 - Kyle Trauberman
4
不是重复的问题:因为这个问题是关于验证,而不是创建。 - ChrisW
3
我正在询问验证,而不是创建。 - delete
2
这并不是重复问题,因为该问题询问的是EF生成的类。 - Ladislav Mrnka
7个回答

70

这里有一个来自评论的有趣问题:

什么样的对象不需要在某个时刻修改值呢?我猜测不是模型类,对吗?我曾经需要更改数据库中一个人的姓名 - 这不符合这个想法。

好的,考虑一下已经是不可变的事物。数字是不可变的。一旦你拥有数字12,它就是12了。你无法改变它。如果你有一个包含数字12的变量,你可以将变量的内容更改为13,但你改变的是变量而不是数字12

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

那列表怎么办呢?{12,“abc”} 是由12和“abc”组成的列表,该列表永远不会改变。列表 {12,“abcd”} 是一个不同的列表

这就是事情开始变得混乱的地方。因为在C#中,你可以两种方式做到这一点。如果允许列表在不改变身份的情况下更改其内容,则可以说这两个列表之间具有引用标识

当你谈论“模型”时,你说得很对。你正在建模一个会发生变化的东西吗?如果是的话,那么使用能够更改的类型来建模可能是明智的选择。其好处在于模型的特性与被建模的系统相匹配。缺点是实现“回滚”功能(即“撤销”更改)变得非常棘手。

也就是说,如果你把{12, "abc"}变成了{12, "abcd"},然后想要撤销这个操作,你该怎么做呢?如果这个列表是不可变的,你只需要保留两个值并选择哪一个值作为“当前”值。如果这个列表是可变的,那么你必须让撤销逻辑保留一个“撤销函数”,这个函数知道如何撤销这个变化。

针对你具体的例子,你当然可以创建一个不可变的数据库。那么如果你想要修改这个不可变数据库中某人的姓名,你该怎么做呢?你不能直接修改,而是要创建一个新的数据库,其中包含你想要的数据。不可变类型的技巧在于高效地进行操作,而不会复制大量字节。不可变数据结构的设计需要找到聪明的方法在两个几乎相同的结构之间共享状态。


1
感谢您的出色解释,我总是在可变和不可变对象的概念上苦苦挣扎。我发现很难理解何时在C#或C++中使用它。 - Gilad

12

将所有字段声明为只读是创建不可变对象的一个好步骤,但仅此还不足。这是因为只读字段仍然可以是对可变对象的引用。

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


2
有时候,您可以返回这些可变字段的不可变(或只读)版本,例如对于列表,使用List<T>.AsReadOnly - MasterMastic
1
事实上,C#并没有提供一种简便的方法来从现有对象创建一个新对象,并改变其中一个属性。即使可以将对象设置为不可变,创建一个新副本也是非常繁琐的。+1 - nawfal

6
这个问题有两个方面:
  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需要无参构造函数,并且必须在属性上有setter。我问过一个非常类似的问题在这里
你的类型必须像这样:
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属性的私有setter - 这在EDMX文件中的实体属性中进行配置。显然,当EF从数据库实例化对象时,将不会进行验证。另外,您将无法使用诸如ObjectContext.CreateObject之类的方法(您将无法填充对象)。
Entity Object T4 模板和默认代码生成会创建CreateMyClass工厂方法而不是具有参数的构造函数。POCO T4模板不会生成工厂方法。
我没有尝试在EF Code First中使用这个。

2

好的,那么我基于这个问题的基本前提是,一旦它通过了构造函数,它就可以被100%认证为合法和有效?你会拥有什么样的对象,不需要在某个时候修改其值?我猜不是一个模型类,对吗?我不得不更改数据库中一个人的名字 - 这与这个想法不符。 - delete

1
如果您想在Entity Framework生成的模型类的构造函数中进行验证,那么这个方法会如何工作呢?
在这种情况下,它不会起作用,因为EF要求实体类的属性必须是公共的,否则它无法实例化。
但是您可以在代码中进一步使用不可变对象。

1

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

0

@Eric Lippert 很好的评论,但是除此之外,回答这个问题:

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

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

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

如果您能够在不更新大型数据结构时将其冻结,以便不会更改任何内存并且在一致状态下暂停,那该多好?现在,当您再次想要更改它时,您必须将数据结构复制到新位置,而且它相当大,所以这是一个缺点,但好处是您无需锁定任何内容,因为数据的新副本未共享,直到它已被更新。这意味着任何人在任何时候都可以读取最新的数据结构副本,并进行复杂操作。

如果您讨厌处理并发问题并且没有太多数据要处理,那么这是非常有用的概念。(例如,如果数据大小为1MB,每秒更新10次,则需要复制10MB的数据)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接