问题很简单。可以改变其内部状态而不能从外部观察的类型可以被视为不可变吗?
简化示例:
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;
}
}
}
更新:澄清了线程安全性问题,因为它造成了分心。
答案 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
字段中不会影响字符串的哈希码;它只会加速对价值的重复请求。