UPDATE:
截至 2020 年底,.NET 5.0 的 C# 9.0 已发布,支持不可变记录类型(支持复制构造函数,并支持with
运算符轻松创建具有新属性值的新实例)。
原始答案,在 C# 9.0(和 8.0)发布之前编写:
C# 没有相同级别的支持const
-C++ 提供的正确性(忽略代码契约),但它仍然提供readonly
修饰符(以及 C# 6.0 中真正的只读自动属性)有帮助。
C# 还缺乏对 Record 类型的语法支持,不幸的是,它是从 C# 7 中删除的,所以我们必须再等一年(Update:截至 2018 年中期,C# 8.0 预计将拥有 Record 类型,但考虑到 C# 8.0 可能要到 2020 年才会最终发布它有很长的新功能列表).
Anyway, an immutable type in .NET is just a POCO1 which cannot have its state modified after construction. Note that this is only enforced by the compiler if your type has every field tagged as readonly
and that every complex (i.e. non-scalar) member is also similarly constrained.
如果您的类型具有任何数组成员,则该类型不可能是真正不可变的,因为 C# 中没有强制执行只读缓冲区(C++ 中有)。这意味着在实践中,C# 中的“不可变”类型只是一个精心设计的 POCO,消费者(将遵守规则(例如无反射))可以在使用它时做出某些假设,例如不可变类型本质上是线程安全的。但仅此而已。运行时没有特殊的 AOT 或 JIT 优化,也没有表现出任何特殊的行为。这是一个非常“人为因素”的事情。
下面这个类是不可变的:
class Immutable {
private readonly String foo;
public Immutable(String foo, String bar) {
this.foo = foo;
this.Bar = bar;
}
public String Bar { get: }
public String Baz { get { return this.foo.Substring( 0, 2 ); } }
}
它是不可变的,因为每个字段(即它的实例状态)都是readonly
并且不可变(我们知道这一点只是因为System.String
众所周知是不可变的)。如果foo
被改为StringBuilder
or XmlElement
那么它就不再是一成不变的了。
请注意,严格来说,readonly
修饰符对于不变性来说并不是必需的,它只是使它更容易演示,并且它确实增加了一定程度的编译时强制(并且possibly一些运行时优化)。
为了比较,这个类不是不可变的(即它是可变的):
class Mutable {
private readonly Int32[] values;
public Mutable(Int32 values) {
this.values = values;
}
public Int32[] GetValues() {
return this.values;
}
}
它是可变的,因为:
-
Int32[]
(数组类型)是可变的
- 它通过以下方式返回对可变数组的引用
GetValues
- 它在构造期间接受可变对象参数。
这是一个示例,说明了为什么它不是不可变的:
Int32[] values = { 0, 1, 2, 3 };
Mutable mutable = new Mutable( values );
Print( mutable.GetValues() ); // prints "0, 1, 2, 3"
values[0] = 5;
Print( mutable.GetValues() ); // prints "5, 1, 2, 3"
If Mutable
是不可变的,然后进行后续更改values
使用时将不可见Mutable
的API:第二次调用Print
将显示与第一个相同的输出。
但是,即使您使用数组或复杂类型,您也可以拥有不可变类型:这是通过隐藏所有修改状态的方法来完成的。例如,返回ReadOnlyCollection<Int32>
代替Int32[]
并始终对传入的所有复杂和可变值执行深度复制/克隆。但是编译器、JIT 和运行时仍然不够复杂,无法确定这会导致对象类型不可变 - 因此为什么您必须记录它并相信您的消费者能够正确使用它(或者如果您是消费者,请相信您的上游开发人员)他们正确实施了)
下面是包含数组的不可变类型的示例:
class Immutable {
private readonly Int32[] values;
public Mutable(Int32 values) {
if( values == null ) throw new ArgumentNullException(nameof(values));
this.values = (Int32[])values.Clone();
}
public IReadOnlyList<Int32> GetValues() {
return this.values;
}
}
- The input array is shallow-copied (using
Array.Clone()
) during construction - so any future changes to the object passed into the constructor won't affect any Immutable
class instances.
- If the
values
数组包含非不可变、非标量值,那么构造函数必须执行元素的“深层复制”,以确保其值与其他地方的任何未来更改隔离。
- The
values
array 永远不会直接暴露给消费者。
-
GetValues()
返回一个IReadOnlyList<T>
view内部数组的(这是 .NET 4.5 中的新功能)。这比返回一个更轻量级ReadOnlyCollection<T>
包装器(在 .NET 2.0 中引入)。
1: A POCO is a "Plain Old CLR Object" type, which in-practice means any class
or struct
without any requirements that it inherit some parent supertype or implement any particular interface. The term is often used in reference to ORM libraries like Linq-to-SQL, Entity Framework or NHibernate which (in their earlier versions) required each entity class to derive from some base entity type, or employ certain techniques (e.g. INotifyPropertyChanged
). See here for more details: What is POCO in Entity Framework?