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

时间:2016-10-03 06:11:45

标签: c#-4.0

我在CMMI 5级公司的采访中被问到如何在C#中创建一个可变且不可变的类。我听说过mutable和immutable意味着可以改变无法改变,比如 String StringBuilder

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

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

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

1 个答案:

答案 0 :(得分:4)

C#对const没有相同级别的支持--C ++提供的正确性(忽略代码约定),但它仍然提供readonly修饰符(和真正的只读自动 - C#6.0中的属性有帮助。

C#也缺乏对Record类型的语法支持,遗憾的是它们是从C#7中提取的,因此我们将不得不再等一年(更新:截至2018年中期,C#8.0预计会有Record类型,但是在给定its very long list of new features)到2020年之前,C#8.0可能不会最终发布。

无论如何,.NET中的不可变类型只是一个POCO 1 ,它在构造之后不能修改其状态。请注意,只有当您的类型的每个字段都标记为readonly并且每个复杂(即非标量)成员也受到类似约束时,才会由编译器强制执行此操作。

如果你的类型有任何数组成员,那么类型不能真正不可变,因为在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更改为StringBuilderXmlElement,那么它将不再是不可变的。

请注意,严格来说,readonly修饰符对于不可变性不是必需的,它只是使演示更容易,并且它确实增加了一定程度的编译时强制执行(并且可能一些运行时优化)。

为了比较,这个类不是不可变的(即它是可变的):

class Mutable {
    private readonly Int32[] values;
    public Mutable(Int32 values) {
        this.values = values;
    }

    public Int32[] GetValues() {
        return this.values;
    }
}

这是可变的,因为:

  1. Int32[](数组类型)是可变的
  2. 它通过GetValues
  3. 返回对可变数组的引用
  4. 它在构造期间接受可变对象参数。
  5. 这是一个展示为什么它不是永恒不变的例子:

    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"
    

    如果Mutable是不可变的,那么在使用values的API时,Mutable的后续更改将不可见:第二次调用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;
        }
    }
    

    *。在构造期间,输入数组被浅层复制(使用Array.Clone()) - 因此,对传递给构造函数的对象的任何未来更改都不会影响任何Immutable类实例。     *如果values数组包含非不可变的非标量值,则构造函数必须执行元素的“深层复制”,以确保其值与其他地方的任何未来更改隔离。 * values数组永远不会直接暴露给消费者。 *。 GetValues()返回内部数组的IReadOnlyList<T> 视图(这是.NET 4.5中的新增功能)。这比返回ReadOnlyCollection<T>包装器(在.NET 2.0中引入)更轻量级。

    1 :POCO是一种“普通旧CLR对象”类型,实际上意味着任何classstruct没有任何要求继承某些父超类型或实现任何特定的接口。该术语通常用于引用ORM库,如Linq-to-SQL,Entity Framework或NHibernate(在其早期版本中)要求每个实体类从某种基本实体类型派生,或采用某些技术(例如{{1} })。有关详情,请参阅此处:What is POCO in Entity Framework?