【问题标题】:Immutable Class Construction Design不可变类构造设计
【发布时间】:2011-06-10 12:03:47
【问题描述】:

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

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

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

class ParagraphStyle {
    public TextAlignment Alignment {get;}
    public float FirstLineHeadIndent {get;}
    // ...
}

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

要模仿这种现有模式,需要引入某种类型的“可变”ParagraphStyle 类型,该类型会“复制”成员(以提供设置器),然后提供一个接受可变类型的 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 = ...,
});

所以,这很有效,支持 IDE 中的代码完成,并使关于如何构造事物的事情相当明显......但它确实看起来相当重复。

有没有更好的办法?

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

var anonymousTypeInstance = new {
    Foo = "string",
    Bar = 42;
};
anonymousTypeInstance.Foo = "another-value"; // compiler error

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

// Option 2:
class ParagraphStyle {
    public ParagraphStyle (TextAlignment alignment, float firstLineHeadIndent,
            /* ... */ ) {...}
}

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

如果我们尝试模仿匿名类型,似乎我们可以在“不可变”类型中使用“设置一次”属性,从而放弃“可变”变体:

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

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

这样做的问题是属性只能设置一次并不明显(编译器当然不会抱怨),并且初始化不是线程安全的。因此,添加一个Commit() 方法变得很诱人,这样对象就可以知道开发人员已经完成了属性的设置(因此,如果调用了它们的设置器,则会导致以前未设置的所有属性都抛出),但这似乎让事情变得更糟,而不是更好。

有没有比可变/不可变类拆分更好的设计?还是我注定要处理成员重复?

【问题讨论】:

  • 我意识到这可能令人难以置信,但我正在使用现有的 C# 代码库。将整个东西移植到 F# 不是一个选项,即使它是一个选项,F# 库发布的 MSR-SSLA 许可证看起来也不适合我使用,特别是 (ii)(c)。
  • 这似乎是重复的:stackoverflow.com/questions/263585/…
  • 可能不是一个答案,但我过去通过实施 ISupportInitialize 处理过类似的要求。调用 EndInit() 后,您将变得不可变,并在调用 setter 时抛出。不是编译时安全的,但文档和尽早抛出通常会有所帮助。

标签: .net immutability


【解决方案1】:

在几个项目中,我使用了流利的方法。 IE。大多数通用属性(例如名称、位置、标题)是通过 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;
  }
  // ...
}

MemberwiseClone 只要我们的类是真正不可变的就可以了。

【讨论】:

  • 虽然这可行且有用,但它也不是一个可扩展的解决方案。如果您需要设置例如15 个值,WithFoo() mutator 方法会导致在获得最终结果之前创建 14 个“临时”实例。根据您的类型的“重”程度,这可能是相当多的垃圾。 WithFoo() 也不支持集合初始化器,这可能不是一个巨大的损失,但我非常喜欢它们;-)。 Foo+FooBuilder 习语似乎是一个合理的折衷方案。
  • 我在最近的一个项目中再次尝试了这种方法,我完全同意,创建所有这些包装器很无聊。我同意 Builder 更优雅,更易读。顺便说一句,深度嵌套的结构呢? FP 数据结构通过重用未更改的成员来尽可能减少内存占用。
  • 你也可以提供一个 Fluent builder 的构造函数,它将不可变对象作为参数来初始化内部字段。因此,您只能在需要修改的 Foos 上使用 WithFoo() :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-09-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-09-27
相关资源
最近更新 更多