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

Can an immutable type change its internal state?

问题很简单。可以改变其内部状态而无需从外部观察到的类型是否可以被视为 immutable?

简化示例:

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;    
         }
    }
}

更新:澄清了线程安全问题,因为它会导致分心。

视情况而定。

如果您正在为客户端代码的作者编写文档或作为客户端代码的作者进行推理,那么您关心的是组件的接口(即它的外部可观察状态和行为)而不是它的实现细节(就像内部表示一样)。

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

当然,这可能很难做到正确(内部状态可变,您可能需要关注线程安全,,等等)。但是假设你做对了(至少在你需要的范围内)没有理由考虑这种类型不可变。

显然,从编译器或优化器的角度来看,这种类型通常不被认为是不可变的(除非编译器足够智能或具有一些 "help" 提示或某些类型的先验知识) 以及针对不可变类型的任何优化可能不适用,如果是这种情况。

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

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

  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 

然而,不可观察

我会伸出我的脖子...

不,不可变对象不能在 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");
    }
}

我要去 quote Clojure author Rich Hickey here:

If a tree falls in the woods, does it make a sound?

If a pure function mutates some local data in order to produce an immutable return value, is that ok?

出于性能原因,对公开不可变 API 的对象进行变异是完全合理的。不可变对象的重要之处在于它们对外部的不可变性。封装在其中的所有内容都是公平的游戏。

在垃圾收集语言(如 C#)中,由于 GC,所有对象都具有某种状态。作为一个平时不应该关心的消费者。

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

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

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

请注意,根据此规则,subclass 可以定义不可变基 class 中包含的字段以外的字段,但不得以违反上述不变式的方式公开它们。此外,class 可能包含可变字段,前提是它们永远不会以影响 class 的可见状态的任何方式改变。考虑 Java 的 string class 中的 hash 字段。如果它非零,则字符串的 hashCode 值等于字段中存储的值。如果为零,则字符串的 hashCode 值是对字符串封装的不可变字符序列进行某些计算的结果。将上述计算结果存入hash字段,不会影响字符串的哈希码;它只会加快对值的重复请求。