不可变类型可以改变其内部状态吗?

时间:2015-07-30 11:09:58

标签: c# immutability

问题很简单。可以改变其内部状态而不能从外部观察的类型可以被视为不可变吗?

简化示例:

public struct Matrix
{
    bool determinantEvaluated;
    double determinant;

    public double Determinant 
    {
         get //asume thread-safe correctness in implementation of the getter
         {
             if (!determinantEvaluated)
             {
                  determinant = getDeterminant(this);
                  determinantEvaluated = true;
             }

             return determinant;    
         }
    }
}

更新:澄清了线程安全性问题,因为它造成了分心。

5 个答案:

答案 0 :(得分:33)

取决于。

如果您要记录客户端代码的作者或作为客户端代码的作者推理,那么您关心的是组件的接口(即,其外部可观察的状态和行为),而不是其实现细节(如内部代表)。

从这个意义上说,一个类型即使缓存状态也是不可变的,即使它是懒惰地初始化等等 - 只要这些突变在外部是不可观察的。换句话说,如果类型在通过其公共接口(或其他预期用例,如果有)使用时表现为不可变,则该类型是不可变的。

当然,要做到这一点可能很棘手(内部状态可变,您可能需要关注线程安全性,serialization/marshaling behavior等)。但假设你确实做到了(至少你需要的程度), 没有理由认为这种类型是不可变的。

显然,从编译器或优化器的角度来看,这种类型通常不被认为是不可变的(除非编译器足够智能或者有一些“帮助”,如提示或某些类型的先验知识)和任何优化如果是这种情况,那些用于不可变类型的可能不适用。

答案 1 :(得分:17)

是的,不可变的可以更改其状态,前提是更改是 看不见用于软件的其他组件(通常是缓存)。相当 像量子物理:一个事件应该有一个观察者成为一个事件。

在您的情况下,可能的实现是这样的:

  public class Matrix {
    ...
    private Lazy<Double> m_Determinant = new Lazy<Double>(() => {
      return ... //TODO: Put actual implementation here
    });

    public Double Determinant {
      get {
        return m_Determinant.Value;
      }
    }
  }

请注意,Lazy<Double> m_Determinant 更改状态

m_Determinant.IsValueCreated 
然而,

unobservable

答案 2 :(得分:7)

我要去quote Clojure author Rich Hickey here

  

如果一棵树落在树林里,它会发出声音吗?

     

如果纯函数改变某些本地数据以产生不可变的返回值,那可以吗?

出于性能原因,修改暴露API的对象是完全合理的,这些API对外部是不可变的。关于不可变对象的重要之处在于它们对外界的不变性。封装在其中的一切都是公平的游戏。

在C#这样的垃圾收集语言中,由于GC,所有对象都有一些状态。作为一个通常不应该关注你的消费者。

答案 3 :(得分:5)

我会伸出脖子......

不,不可变对象不能在C#中更改其内部状态,因为观察其内存是一个选项,因此您可以观察到未初始化状态。证明:

public struct Matrix
{
    private bool determinantEvaluated;
    private double determinant;

    public double Determinant
    {
        get
        {
            if (!determinantEvaluated)
            {
                determinant = 1.0;
                determinantEvaluated = true;
            }

            return determinant;
        }
    }
}

然后......

public class Example
{
    public static void Main()
    {
        var unobserved = new Matrix();
        var observed = new Matrix();

        Console.WriteLine(observed.Determinant);

        IntPtr unobservedPtr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof (Matrix)));
        IntPtr observedPtr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Matrix)));

        byte[] unobservedMemory = new byte[Marshal.SizeOf(typeof (Matrix))];
        byte[] observedMemory = new byte[Marshal.SizeOf(typeof (Matrix))];

        Marshal.StructureToPtr(unobserved, unobservedPtr, false);
        Marshal.StructureToPtr(observed, observedPtr, false);



        Marshal.Copy(unobservedPtr, unobservedMemory, 0, Marshal.SizeOf(typeof (Matrix)));
        Marshal.Copy(observedPtr, observedMemory, 0, Marshal.SizeOf(typeof (Matrix)));

        Marshal.FreeHGlobal(unobservedPtr);
        Marshal.FreeHGlobal(observedPtr);

        for (int i = 0; i < unobservedMemory.Length; i++)
        {
            if (unobservedMemory[i] != observedMemory[i])
            {
                Console.WriteLine("Not the same");
                return;
            }
        }

        Console.WriteLine("The same");
    }
}

答案 4 :(得分:2)

将类型指定为不可变的目的是建立以下不变量:

  • 如果永远观察到两个不可变类型的实例是相等的,那么对一个实例的任何公开可观察的引用都可以替换为对另一个的引用而不影响其中任何一个的行为。

因为.NET提供了比较任意两个引用是否相等的能力,所以不可能在不可变实例之间实现完美的等价。尽管如此,如果将引用相等性检查视为在类对象负责的事物范围之外,则上述不变量仍然非常有用。

请注意,在此规则下,子类可以定义超出不可变基类中包含的字段,但不得以违反上述不变量的方式公开它们。此外,类可以包括可变字段,只要它们不会以任何影响类的可见状态的方式改变。考虑类似Java的hash类中的string字段。如果它不为零,则字符串的hashCode值等于字段中存储的值。如果它为零,则字符串的hashCode值是对字符串封装的不可变字符序列执行某些计算的结果。将上述计算的结果存储到hash字段中不会影响字符串的哈希码;它只会加速对价值的重复请求。