C# 中的不可变对象模式 - 你怎么看?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/263585/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-03 20:25:51  来源:igfitidea点击:

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

c#functional-programmingdesign-patternsimmutability

提问by Lars Fastrup

I have over the course of a few projects developed a pattern for creating immutable (readonly) objects and immutable object graphs. Immutable objects carry the benefit of being 100% thread safe and can therefore be reused across threads. In my work I very often use this pattern in Web applications for configuration settings and other objects that I load and cache in memory. Cached objects should always be immutable as you want to guarantee they are not unexpectedly changed.

我在几个项目的过程中开发了一种模式来创建不可变(只读)对象和不可变对象图。不可变对象具有 100% 线程安全的优点,因此可以跨线程重用。在我的工作中,我经常在 Web 应用程序中使用这种模式来进行配置设置和其他加载并缓存在内存中的对象。缓存对象应该始终是不可变的,因为您希望保证它们不会被意外更改。

Now, you can of course easily design immutable objects as in the following example:

现在,您当然可以轻松设计不可变对象,如下例所示:

public class SampleElement
{
  private Guid id;
  private string name;

  public SampleElement(Guid id, string name)
  {
    this.id = id;
    this.name = name;
  }

  public Guid Id
  {
    get { return id; }
  }

  public string Name
  {
    get { return name; }
  }
}

This is fine for simple classes - but for more complex classes I do not fancy the concept of passing all values through a constructor. Having setters on the properties is more desirable and your code constructing a new object gets easier to read.

这对于简单的类来说很好——但是对于更复杂的类,我不喜欢通过构造函数传递所有值的概念。在属性上设置 setter 更可取,并且构建新对象的代码更易于阅读。

So how do you create immutable objects with setters?

那么如何使用 setter 创建不可变对象呢?

Well, in my pattern objects start out as being fully mutable until you freeze them with a single method call. Once an object is frozen it will stay immutable forever - it cannot be turned into a mutable object again. If you need a mutable version of the object, you simply clone it.

好吧,在我的模式中,对象一开始是完全可变的,直到您使用单个方法调用冻结它们。一旦一个对象被冻结,它将永远保持不变 - 它不能再次变成可变对象。如果您需要对象的可变版本,只需克隆它即可。

Ok, now on to some code. I have in the following code snippets tried to boil the pattern down to its simplest form. The IElement is the base interface that all immutable objects must ultimately implement.

好的,现在开始一些代码。我在以下代码片段中尝试将模式归结为最简单的形式。IElement 是所有不可变对象最终必须实现的基本接口。

public interface IElement : ICloneable
{
  bool IsReadOnly { get; }
  void MakeReadOnly();
}

The Element class is the default implementation of the IElement interface:

Element 类是 IElement 接口的默认实现:

public abstract class Element : IElement
{
  private bool immutable;

  public bool IsReadOnly
  {
    get { return immutable; }
  }

  public virtual void MakeReadOnly()
  {
    immutable = true;
  }

  protected virtual void FailIfImmutable()
  {
    if (immutable) throw new ImmutableElementException(this);
  }

  ...
}

Let's refactor the SampleElement class above to implement the immutable object pattern:

让我们重构上面的 SampleElement 类来实现不可变对象模式:

public class SampleElement : Element
{
  private Guid id;
  private string name;

  public SampleElement() {}

  public Guid Id
  {
    get 
    { 
      return id; 
    }
    set
    {
      FailIfImmutable();
      id = value;
    }
  }

  public string Name
  {
    get 
    { 
      return name; 
    }
    set
    {
      FailIfImmutable();
      name = value;
    }
  }
}

You can now change the Id property and the Name property as long as the object has not been marked as immutable by calling the MakeReadOnly() method. Once it is immutable, calling a setter will yield an ImmutableElementException.

现在,只要对象尚未通过调用 MakeReadOnly() 方法标记为不可变,就可以更改 Id 属性和 Name 属性。一旦它是不可变的,调用 setter 将产生一个 ImmutableElementException。

Final note: The full pattern is more complex than the code snippets shown here. It also contains support for collections of immutable objects and complete object graphs of immutable object graphs. The full pattern enables you to turn an entire object graph immutable by calling the MakeReadOnly() method on the outermost object. Once you start creating larger object models using this pattern the risk of leaky objects increases. A leaky object is an object that fails to call the FailIfImmutable() method before making a change to the object. To test for leaks I have also developed a generic leak detector class for use in unit tests. It uses reflection to test if all properties and methods throw the ImmutableElementException in the immutable state. In other words TDD is used here.

最后说明:完整模式比此处显示的代码片段更复杂。它还包含对不可变对象集合和不可变对象图的完整对象图的支持。完整模式使您能够通过调用最外层对象的 MakeReadOnly() 方法使整个对象图不可变。一旦您开始使用这种模式创建更大的对象模型,泄漏对象的风险就会增加。泄漏对象是在对对象进行更改之前未能调用 FailIfImmutable() 方法的对象。为了测试泄漏,我还开发了一个用于单元测试的通用泄漏检测器类。它使用反射来测试所有属性和方法是否在不可变状态下抛出 ImmutableElementException。换句话说,这里使用了 TDD。

I have grown to like this pattern a lot and find great benefits in it. So what I would like to know is if any of you are using similar patterns? If yes, do you know of any good resources that document it? I am essentially looking for potential improvements and for any standards that might already exist on this topic.

我越来越喜欢这种模式,并从中发现了很大的好处。所以我想知道你们中是否有人在使用类似的模式?如果是,您是否知道任何记录它的好资源?我本质上是在寻找潜在的改进以及在这个主题上可能已经存在的任何标准。

采纳答案by Marc Gravell

For info, the second approach is called "popsicle immutability".

有关信息,第二种方法称为“冰棒不变性”。

Eric Lippert has a series of blog entries on immutability starting here. I'm still getting to grips with the CTP (C# 4.0), but it looks interesting what optional / named parameters (to the .ctor) might do here (when mapped to readonly fields)... [update: I've blogged on this here]

Eric Lippert 有一系列关于不变性的博客条目,从这里开始。我仍然在掌握 CTP (C# 4.0),但它看起来很有趣可选/命名参数(到 .ctor)可能在这里做什么(当映射到只读字段时)...... [更新:我写过博客在这里]

For info, I probably wouldn't make those methods virtual- we probably don't want subclasses being able to make it non-freezable. If you want them to be able to add extra code, I'd suggest something like:

有关信息,我可能不会创建这些方法virtual- 我们可能不希望子类能够使其不可冻结。如果您希望他们能够添加额外的代码,我建议您这样做:

[public|protected] void Freeze()
{
    if(!frozen)
    {
        frozen = true;
        OnFrozen();
    }
}
protected virtual void OnFrozen() {} // subclass can add code here.

Also - AOP (such as PostSharp) might be a viable option for adding all those ThrowIfFrozen() checks.

此外 - AOP(例如 PostSharp)可能是添加所有这些 ThrowIfFrozen() 检查的可行选项。

(apologies if I have changed terminology / method names - SO doesn't keep the original post visible when composing replies)

(抱歉,如果我更改了术语/方法名称 - 所以在撰写回复时不会保持原始帖子可见)

回答by Cory Foy

You are still dealing with state, and thus can still be bitten if your objects are parallelized before being made immutable.

您仍在处理状态,因此如果您的对象在变为不可变之前被并行化,则仍然可能被咬。

A more functional way might be to return a new instance of the object with each setter. Or create a mutable object and pass that in to the constructor.

更实用的方法可能是使用每个 setter 返回对象的新实例。或者创建一个可变对象并将其传递给构造函数。

回答by dalle

System.String is a good example of a immutable class with setters and mutating methods, only that each mutating method returns a new instance.

System.String 是具有 setter 和变异方法的不可变类的一个很好的例子,只是每个变异方法都返回一个新实例。

回答by Andrew Bullock

I dont like the idea of being able to change an object from a mutable to an immutable state, that kind of seems to defeat the point of design to me. When are you needing to do that? Only objects which represent VALUES should be immutable

我不喜欢能够将对象从可变状态更改为不可变状态的想法,这对我来说似乎违背了设计的初衷。你什么时候需要这样做?只有代表 VALUES 的对象才应该是不可变的

回答by Konrad Rudolph

After my initial discomfort about the fact that I had to create a new System.Drawing.Pointon each modification, I've wholly embraced the concept some years ago. In fact, I now create every field as readonlyby default and only change it to be mutable if there's a compelling reason – which there is surprisingly rarely.

在我最初System.Drawing.Point对每次修改都必须创建一个新的事实感到不安之后,几年前我完全接受了这个概念。事实上,我现在readonly默认创建每个字段,并且只有在有令人信服的理由时才将其更改为可变的——这非常罕见。

I don't care very much about cross-threading issues, though (I rarely use code where this is relevant). I just find it much, much better because of the semantic expressiveness. Immutability is the very epitome of an interface which is hard to use incorrectly.

不过,我不太关心跨线程问题(我很少使用相关的代码)。由于语义表达能力,我发现它好多了,好多了。不变性是难以错误使用的接口的缩影。

回答by Herms

Another option would be to create some kind of Builder class.

另一种选择是创建某种 Builder 类。

For an example, in Java (and C# and many other languages) String is immutable. If you want to do multiple operations to create a String you use a StringBuilder. This is mutable, and then once you're done you have it return to you the final String object. From then on it's immutable.

例如,在 Java(以及 C# 和许多其他语言)中,字符串是不可变的。如果要执行多个操作来创建字符串,请使用 StringBuilder。这是可变的,然后一旦完成,您就会将它返回给您最终的 String 对象。从那时起,它是一成不变的。

You could do something similar for your other classes. You have your immutable Element, and then an ElementBuilder. All the builder would do is store the options you set, then when you finalize it it constructs and returns the immutable Element.

您可以为其他课程做类似的事情。你有你的不可变元素,然后是一个 ElementBuilder。构建器要做的就是存储您设置的选项,然后当您完成它时,它会构造并返回不可变元素。

It's a little more code, but I think it's cleaner than having setters on a class that's supposed to be immutable.

这是一个多一点的代码,但我认为它比在一个应该是不可变的类上设置 setter 更干净。

回答by Charles Bretana

The (relatively) new Software Design paradigm called Domain Driven design, makes the distinction between entity objects and value objects.

(相对)新的软件设计范式称为领域驱动设计,区分了实体对象和值对象。

Entity Objects are defined as anything that has to map to a key-driven object in a persistent data store, like an employee, or a client, or an invoice, etc... where changing the properties of the object implies that you need to save the change to a data store somewhere, and the existence of multiple instances of a class with the same "key" imnplies a need to synchronize them, or coordinate their persistence to the data store so that one instance' changes do not overwrite the others. Changing the properties of an entity object implies you are changing something about the object - not changing WHICH object you are referencing...

实体对象被定义为任何必须映射到持久数据存储中的键驱动对象的东西,如员工、客户或发票等......其中更改对象的属性意味着您需要将更改保存到某个数据存储中,并且具有相同“密钥”的类的多个实例的存在意味着需要同步它们,或将它们的持久性协调到数据存储中,以便一个实例的更改不会覆盖其他实例. 更改实体对象的属性意味着您正在更改该对象的某些内容 - 而不是更改您正在引用的对象...

Value objects otoh, are objects that can be considered immutable, whose utility is defined strictly by their property values, and for which multiple instances, do not need to be coordinated in any way... like addresses, or telephone numbers, or the wheels on a car, or the letters in a document... these things are totally defined by their properties... an uppercase 'A' object in an text editor can be interchanged transparently with any other uppercase 'A' object throughout the document, you don't need a key to distinguish it from all the other 'A's In this sense it is immutable, because if you change it to a 'B' (just like changing the phone number string in a phone number object, you are not changing the data associated with some mutable entity, you are switching from one value to another... just as when you change the value of a string...

值对象 otoh,是可以被认为是不可变的对象,其效用由它们的属性值严格定义,并且多个实例不需要以任何方式进行协调……例如地址、电话号码或轮子在汽车上,或文档中的字母……这些东西完全由它们的属性定义……文本编辑器中的大写“A”对象可以与整个文档中的任何其他大写“A”对象透明地互换,你不需要一个键来区分它与所有其他的“A” 从这个意义上说它是不可变的,因为如果你把它改成“B”(就像改变电话号码对象中的电话号码串一样,你不是更改与某个可变实体关联的数据,您正在从一个值切换到另一个值...就像你改变一个字符串的值一样......

回答by Robert Paulson

Expanding on the point by @Cory Foy and @Charles Bretana where there is a difference between entities and values. Whereas value-objects should always be immutable, I really don't think that an object should be able to freeze themselves, or allow themselves to be frozen arbitrarily in the codebase. It has a really bad smell to it, and I worry that it could get hard to track down where exactly an object was frozen, and why it was frozen, and the fact that between calls to an object it could change state from thawed to frozen.

@Cory Foy 和 @Charles Bretana 扩展了实体和值之间存在差异的观点。而值对象应该总是不可变的,我真的不认为一个对象应该能够冻结自己,或者允许自己在代码库中任意冻结。它有一种非常难闻的气味,我担心很难追踪一个对象究竟在哪里被冻结,为什么它被冻结,以及在对一个对象的调用之间它可能会改变状态从解冻到冻结的事实.

That isn't to say that sometimes you want to give a (mutable) entity to something and ensure it isn't going to be changed.

这并不是说有时您想为某物提供一个(可变的)实体并确保它不会被更改。

So, instead of freezing the object itself, another possibility is to copy the semantics of ReadOnlyCollection< T >

所以,不是冻结对象本身,另一种可能性是复制 ReadOnlyCollection<T> 的语义

List<int> list = new List<int> { 1, 2, 3};
ReadOnlyCollection<int> readOnlyList = list.AsReadOnly();

Your object can take a part as mutable when it needs it, and then be immutable when you desire it to be.

您的对象可以在需要时将其作为可变的一部分,然后在您希望它成为时保持不变。

Note that ReadOnlyCollection< T > also implements ICollection< T > which has an Add( T item)method in the interface. However there is also bool IsReadOnly { get; }defined in the interface so that consumers can check before calling a method that will throw an exception.

请注意, ReadOnlyCollection< T > 还实现了 ICollection< T > ,它Add( T item)在接口中有一个方法。然而bool IsReadOnly { get; },在接口中也定义了,以便消费者可以在调用会抛出异常的方法之前进行检查。

The difference is that you can't just set IsReadOnly to false. A collection either is or isn't read only, and that never changes for the lifetime of the collection.

不同之处在于您不能仅将 IsReadOnly 设置为 false。集合要么是只读的,要么不是只读的,并且在集合的生命周期内永远不会改变。

It would be nice at time to have the const-correctness that C++ gives you at compile time, but that starts to have it's own set of problems and I'm glad C# doesn't go there.

在编译时拥有 C++ 为您提供的常量正确性会很好,但这开始有它自己的一系列问题,我很高兴 C# 没有去那里。



ICloneable- I thought I'd just refer back to the following:

ICloneable- 我想我会参考以下内容:

Do not implement ICloneable

Do not use ICloneable in public APIs

不实现 ICloneable

不要在公共 API 中使用 ICloneable

Brad Abrams - Design Guidelines, Managed code and the .NET Framework

Brad Abrams - 设计指南、托管代码和 .NET Framework

回答by spoulson

Just a tip to simplify the element properties: Use automatic propertieswith private setand avoid explicitly declaring the data field. e.g.

只是一个提示以简化元件特性:使用自动属性private set和避免显式地声明了数据字段。例如

public class SampleElement {
  public SampleElement(Guid id, string name) {
    Id = id;
    Name = name;
  }

  public Guid Id {
    get; private set;
  }

  public string Name {
    get; private set;
  }
}

回答by Lars Fastrup

Here is a new video on Channel 9 where Anders Hejlsberg from 36:30 in the interview starts talking about immutability in C#. He gives a very good use case for popsicle immutability and explains how this is something you are currently required to implement yourself. It was music to my ears hearing him say it is worth thinking about better support for creating immutable object graphs in future versions of C#

这是第 9 频道上的一个新视频,Anders Hejlsberg 在采访中从 36:30 开始谈论 C# 中的不变性。他为冰棒不变性提供了一个非常好的用例,并解释了这是您目前需要自己实现的东西。听他说值得考虑在未来版本的 C# 中更好地支持创建不可变对象图,这让我耳目一新

Expert to Expert: Anders Hejlsberg - The Future of C#

专家对专家:Anders Hejlsberg - C# 的未来