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

本文关键字:内部 状态 改变 类型 不可变 | 更新日期: 2023-09-27 18:05:44

问题很简单。一个可以改变其内部状态而不被外部观察的类型可以被认为是不可变的吗?

简化的例子:

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

UPDATE:澄清线程安全问题,因为它会引起分心。

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

看情况。

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

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

当然,要做到这一点可能很棘手(对于可变的内部状态,您可能需要关注线程安全、序列化/封送行为等)。但是假设您做对了(至少在您需要的程度上),那么没有理由不认为这样的类型是不可变的。

显然,从编译器或优化器的角度来看,这样的类型通常不被认为是不可变的(除非编译器足够聪明或有一些"帮助",如提示或某些类型的先验知识),并且任何针对不可变类型的优化都可能不适用,如果是这样的话。

是的,不可变的可以改变的状态,只要这些改变是不可见用于软件的其他组件(通常是缓存)。相当就像量子物理学:一个事件必须有观察者才能成为一个事件。

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

  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 

但是是不可观察的

我要引用Clojure作者Rich Hickey的话:

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

如果一个纯函数为了产生一个不可变的返回值而改变了一些局部数据,那是可以的吗?

出于性能原因,对公开api的对象进行修改是完全合理的,这些api对外是不可变的。不可变对象的重要之处在于它们对外的不变性。封装在它们里面的一切都是公平的。

在某种程度上,在像c#这样的垃圾收集语言中,由于GC,所有对象都有一些状态。

我愿意冒这个险。

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

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

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

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

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