我在 CMMI 5 级公司面试中被问到如何在 C# 中创建可变和不可变类。我听说过 mutable 和 immutable 这意味着可以改变和不能改变,比如String和StringBuilder。
但是,我不知道如何创建可变类和不可变类。为此,我们有 String 和 String builder。但是在创建一个的时候,我被迫在网上搜索了这个,但找不到任何有用的东西,所以想到这里问。
但是,我尝试通过在类中定义一个属性并在其 Getter 上创建它来创建它,我创建了一个新的字符串对象来复制它。但没有成功理解。
另外,我已经提到在 stackoverflow 中已经提出了一个关于不可变和可变的问题。但是,我的问题是不同的。我想知道我是否想创建一个可变类,那么除了使用 String 或其他可变类之外,我将如何使用它。
截至 2020 年末,用于 .NET 5.0 的 C# 9.0 已发布,它支持不可变记录类型(支持复制构造函数,并使用with
运算符轻松创建具有新属性值的新实例)。
C# 没有const
C++ 提供的相同级别的 -正确性支持(忽略代码协定),但它仍然提供readonly
修饰符(以及 C# 6.0 中真正的只读自动属性),这有帮助。
C# 也缺乏对 Record 类型的语法支持,不幸的是,它是从 C# 7 中拉出来的,所以我们必须再等一年(更新:截至 2018 年年中,C# 8.0 预计会有 Record 类型,但 C# 8.0 可能鉴于其非常长的新功能列表,直到 2020 年才会最终发布)。
无论如何,.NET 中的不可变类型只是一个 POCO 1,在构造后不能修改其状态。请注意,只有当您的类型的每个字段都标记为 asreadonly
并且每个复杂(即非标量)成员也受到类似的约束时,编译器才会强制执行此操作。
如果您的类型有任何数组成员,则该类型不能真正不可变,因为 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 ); } }
}
Run Code Online (Sandbox Code Playgroud)
它是不可变的,因为每个字段(即它的实例状态)都是readonly
不可变的(我们只知道这一点,因为System.String
众所周知是不可变的)。如果foo
更改为StringBuilder
orXmlElement
那么它将不再是不可变的。
请注意,严格来说,readonly
修饰符不是不变性所必需的,它只是使其更易于演示,并且确实增加了一定程度的编译时强制执行(可能还有一些运行时优化)。
为了比较,这个类不是不可变的(即它是可变的):
class Mutable {
private readonly Int32[] values;
public Mutable(Int32 values) {
this.values = values;
}
public Int32[] GetValues() {
return this.values;
}
}
Run Code Online (Sandbox Code Playgroud)
它是可变的,因为:
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"
Run Code Online (Sandbox Code Playgroud)
如果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;
}
}
Run Code Online (Sandbox Code Playgroud)
Array.Clone()
在构造过程中被浅复制(使用) - 因此对传递给构造函数的对象的任何未来更改都不会影响任何Immutable
类实例。
values
数组包含不可变的、非标量的值,那么构造函数必须对元素执行“深拷贝”,以确保其值与其他地方的任何未来更改隔离。values
数组永远不会直接暴露给消费者。GetValues()
返回内部数组的IReadOnlyList<T>
视图(这是 .NET 4.5 中的新功能)。这比返回ReadOnlyCollection<T>
包装器(在 .NET 2.0 中引入)更轻量级。1:POCO 是“Plain Old CLR Object”类型,这在实践中意味着任何class
或struct
不要求它继承某个父超类型或实现任何特定接口。该术语通常用于指代像 Linq-to-SQL、Entity Framework 或 NHibernate 这样的 ORM 库(在它们的早期版本中),它们(在它们的早期版本中)要求每个实体类从某些基本实体类型派生,或使用某些技术(例如INotifyPropertyChanged
)。有关更多详细信息,请参见此处:Entity Framework 中的 POCO 是什么?