什么是可变类。我们如何在 C# 中创建可变和不可变的类

Run*_*bit 3 c#-4.0

我在 CMMI 5 级公司面试中被问到如何在 C# 中创建可变和不可变类。我听说过 mutable 和 immutable 这意味着可以改变不能改变,比如StringStringBuilder

但是,我不知道如何创建可变类和不可变类。为此,我们有 String 和 String builder。但是在创建一个的时候,我被迫在网上搜索了这个,但找不到任何有用的东西,所以想到这里问。

但是,我尝试通过在类中定义一个属性并在其 Getter 上创建它来创建它,我创建了一个新的字符串对象来复制它。但没有成功理解。

另外,我已经提到在 stackoverflow 中已经提出了一个关于不可变和可变的问题。但是,我的问题是不同的。我想知道我是否想创建一个可变类,那么除了使用 String 或其他可变类之外,我将如何使用它。

Dai*_*Dai 6

更新:

截至 2020 年末,用于 .NET 5.0 的 C# 9.0 已发布,它支持不可变记录类型(支持复制构造函数,并使用with运算符轻松创建具有新属性值的新实例)。

原始答案,在 C# 9.0(和 8.0)发布之前编写:

C# 没有constC++ 提供的相同级别的 -正确性支持(忽略代码协定),但它仍然提供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更改为StringBuilderorXmlElement那么它将不再是不可变的。

请注意,严格来说,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)

它是可变的,因为:

  1. Int32[] (数组类型)是可变的
  2. 它通过以下方式返回对可变数组的引用 GetValues
  3. 它在构造期间接受可变对象参数。

这是一个示例,说明为什么它不是不可变的:

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”类型,这在实践中意味着任何classstruct不要求它继承某个父超类型或实现任何特定接口。该术语通常用于指代像 Linq-to-SQL、Entity Framework 或 NHibernate 这样的 ORM 库(在它们的早期版本中),它们(在它们的早期版本中)要求每个实体类从某些基本实体类型派生,或使用某些技术(例如INotifyPropertyChanged)。有关更多详细信息,请参见此处:Entity Framework 中的 POCO 是什么?