不可改变的阶级结构设计

jon*_*onp 5 .net immutability

所以我们都意识到不可变类型的好处,特别是在多线程场景中.(或者至少我们都应该意识到这一点;参见例如System.String.)

但是,我没有看到很多关于创建不可变实例的讨论,特别是设计指南.

例如,假设我们想要具有以下不可变类:

class ParagraphStyle {
    public TextAlignment Alignment {get;}
    public float FirstLineHeadIndent {get;}
    // ...
}
Run Code Online (Sandbox Code Playgroud)

我见过的最常见的方法是拥有可变/不可变的"对"类型,例如可变的List <T>和不可变的ReadOnlyCollection <T>类型或可变的StringBuilder和不可变的String类型.

为了模仿这个现有的模式,需要引入某种类型的"可变" ParagraphStyle类型,它"复制"成员(提供setter),然后提供一个ParagraphStyle构造函数,接受可变类型作为参数

// Option 1:
class ParagraphStyleCreator {
    public TextAlignment {get; set;}
    public float FirstLineIndent {get; set;}
    // ...
}

class ParagraphStyle {
    // ... as before...
    public ParagraphStyle (ParagraphStyleCreator value) {...}
}

// Usage:
var paragraphStyle = new ParagraphStyle (new ParagraphStyleCreator {
    TextAlignment = ...,
    FirstLineIndent = ...,
});
Run Code Online (Sandbox Code Playgroud)

因此,这有效,支持IDE中的代码完成,并使得如何构建事物的事情相当明显......但它确实看起来相当重复.

有没有更好的办法?

例如,C#匿名类型是不可变的,并允许使用"普通"属性设置器进行初始化:

var anonymousTypeInstance = new {
    Foo = "string",
    Bar = 42;
};
anonymousTypeInstance.Foo = "another-value"; // compiler error
Run Code Online (Sandbox Code Playgroud)

不幸的是,在C#中复制这些语义的最接近的方法是使用构造函数参数:

// Option 2:
class ParagraphStyle {
    public ParagraphStyle (TextAlignment alignment, float firstLineHeadIndent,
            /* ... */ ) {...}
}
Run Code Online (Sandbox Code Playgroud)

但这并不能很好地"扩展"; 如果您的类型具有例如15个属性,则具有15个参数的构造函数是友好的,并且为所有15个属性提供"有用"重载是一个噩梦的秘诀.我完全拒绝这一点.

如果我们试图模仿匿名类型,似乎我们可以在"immutable"类型中使用"set-once"属性,从而删除"mutable"变体:

// Option 3:
class ParagraphStyle {
    bool alignmentSet;
    TextAlignment alignment;

    public TextAlignment Alignment {
        get {return alignment;}
        set {
            if (alignmentSet) throw new InvalidOperationException ();
            alignment = value;
            alignmentSet = true;
        }
    }
    // ...
}
Run Code Online (Sandbox Code Playgroud)

这样做的问题是,属性只能设置一次并不明显(编译器肯定不会抱怨),并且初始化不是线程安全的.因此,添加一个Commit()方法变得很诱人,这样对象就可以知道开发人员已经完成了设置属性(从而导致以前没有设置的所有属性在调用setter时都抛出),但这似乎是一种方式使事情变得更糟,而不是更好.

是否有比mutable/immutable类拆分更好的设计?或者我注定要处理成员重复?

ole*_*egz 2

在几个项目中我使用了流畅的方法。即,大多数通用属性(例如名称、位置、标题)是通过 ctor 定义的,而其他属性则使用返回新的不可变实例的 Set 方法进行更新。

class ParagraphStyle {
  public TextAlignment Alignment {get; private set;}
  public float FirstLineHeadIndent {get; private set;}
  // ...
  public ParagraphStyle WithAlignment(TextAlignment ta) {
      var newStyle = (ParagraphStyle)MemberwiseClone();
      newStyle.TextAlignment = ta;
  }
  // ...
}
Run Code Online (Sandbox Code Playgroud)

一旦我们的类真正做到了深度不可变,MemberwiseClone 就可以了。