不可变类构造设计
所以我们都意识到不可变类型的好处,特别是在多线程场景中。 (或者至少我们都应该意识到这一点;请参阅 System.String。)
但是,我还没有看到关于创建不可变实例的太多讨论,特别是设计指南。
例如,假设我们想要拥有以下不可变类:
class ParagraphStyle {
public TextAlignment Alignment {get;}
public float FirstLineHeadIndent {get;}
// ...
}
我见过的最常见的方法是拥有可变/不可变“对”类型,例如可变 列表
要模仿这种现有模式,需要引入某种类型的“可变”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()
方法变得很诱人,以便对象可以知道开发人员已完成属性设置(从而导致所有先前未设置的属性在它们的 setter 被设置时抛出)调用),但这似乎是一种让事情变得更糟而不是更好的方法。
有没有比可变/不可变类拆分更好的设计?或者我注定要处理成员重复的问题?
So we all realize the benefits of immutable types, particularly in multithreaded scenarios. (Or at least we should all realize that; see e.g. System.String.)
However, what I haven't seen is much discussion for creating immutable instances, specifically design guidelines.
For example, suppose we want to have the following immutable class:
class ParagraphStyle {
public TextAlignment Alignment {get;}
public float FirstLineHeadIndent {get;}
// ...
}
The most common approach I've seen is to have mutable/immutable "pairs" of types, e.g. the mutable List<T> and immutable ReadOnlyCollection<T> types or the mutable StringBuilder and immutable String types.
To mimic this existing pattern would require the introduction of some type of "mutable" ParagraphStyle
type which "duplicates" the members (to provide setters), and then provide a ParagraphStyle
constructor which accepts the mutable type as an argument
// 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 = ...,
});
So, this works, supports code completion within the IDE, and makes things reasonably obvious about how to construct things...but it does seem fairly duplicative.
Is there a better way?
For example, C# anonymous types are immutable, AND allow using "normal" property setters for initialization:
var anonymousTypeInstance = new {
Foo = "string",
Bar = 42;
};
anonymousTypeInstance.Foo = "another-value"; // compiler error
Unfortunately, the closest way to duplicate these semantics in C# is to use constructor parameters:
// Option 2:
class ParagraphStyle {
public ParagraphStyle (TextAlignment alignment, float firstLineHeadIndent,
/* ... */ ) {...}
}
But this doesn't "scale" well; if your type has e.g. 15 properties, a constructor with 15 parameters is anything but friendly, and providing "useful" overloads for all 15 properties is a recipe for a nightmare. I'm rejecting this outright.
If we try to mimic anonymous types, it seems that we could use "set-once" properties in the "immutable" type, and thus drop the "mutable" variant:
// Option 3:
class ParagraphStyle {
bool alignmentSet;
TextAlignment alignment;
public TextAlignment Alignment {
get {return alignment;}
set {
if (alignmentSet) throw new InvalidOperationException ();
alignment = value;
alignmentSet = true;
}
}
// ...
}
The problem with this is that it it's not obvious that properties can be set only once (the compiler certainly won't complain), and initialization isn't thread-safe. It thus becomes tempting to add a Commit()
method so that the object can know that the developer is done setting the properties (thus causing all properties that haven't previously been set to throw if their setter is invoked), but this seems to be a way to make things worse, not better.
Is there a better design than the mutable/immutable class split? Or am I doomed to deal with member duplication?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
在几个项目中,我使用了流畅的方法。即,大多数通用属性(例如名称、位置、标题)是通过 ctor 定义的,而其他属性则使用返回新的不可变实例的 Set 方法进行更新。
一旦我们的类真正做到了深度不可变,MemberwiseClone 就可以了。
In a couple projects I was using fluent approach. I.e. most generic properties (e.g. name, location, title) are defined via ctor while others are updated with Set methods returning new immutable instance.
MemberwiseClone is ok here as soon our class is truly deeply immutable.