所以我们都认识到不可变类型的好处,特别是在多线程场景中 . (或者至少我们都应该意识到这一点;参见例如System.String . )
但是,我没有看到的是关于创建不可变实例的讨论,特别是设计指南 .
例如,假设我们想要具有以下不可变类:
class ParagraphStyle {
public TextAlignment Alignment {get;}
public float FirstLineHeadIndent {get;}
// ...
}
我见过的最常见的方法是拥有可变/不可变的类型,例如:可变List<T>和不可变ReadOnlyCollection<T>类型或可变StringBuilder和不可变String类型 .
要模仿这个现有模式,需要引入某种类型的"mutable" ParagraphStyle
类型"duplicates"成员(提供setter),然后提供一个 ParagraphStyle
构造函数,它接受mutable类型作为参数
// 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个属性提供“有用”的重载是一个噩梦的秘诀 . 我完全拒绝了这一点 .
如果我们试图模仿匿名类型,似乎我们可以在“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;
}
}
// ...
}
这个问题是它抱怨,并且初始化不是线程安全的 . 因此,添加 Commit()
方法变得很诱人,以便对象可以知道开发人员已完成设置属性(从而导致之前未设置的所有属性在调用其setter时抛出),但这似乎是一个让事情变得更糟,不是更好的方法 .
有没有比可变/不可变类拆分更好的设计?或者我注定要处理成员重复?
1 回答
在一些项目中,我使用了流利的方法 . 即大多数通用属性(例如名称,位置, Headers )是通过ctor定义的,而其他通过Set方法返回新的不可变实例来更新 .
这里的MemberwiseClone很好,我们的课程真的非常不可改变 .