我正在考虑与值类型相关的类似问题,并找到了一个“解决方案”。您会看到,您不能像在 C++ 中那样更改 C# 中的默认复制构造函数,因为它旨在实现轻量级且无副作用。但是,您可以做的是等到您实际访问该结构,然后检查它是否被复制。
这样做的问题是,与引用类型不同,结构没有真正的身份。只有按值相等。但是,它们仍然必须存储在内存中的某个位置,并且该地址可用于标识(尽管是暂时的)值类型。GC 在这里是一个问题,因为它可以移动对象,因此会更改结构所在的地址,因此您必须能够应对这种情况(例如,将结构的数据设为私有)。
在实践中,结构的地址可以从this
引用中获得,因为它ref T
在值类型的情况下很简单。我留下了从对我的库的引用中获取地址的方法,但是为此发出自定义 CIL 非常简单。在这个例子中,我创建了一个本质上是一个 byval 数组的东西。
public struct ByValArray<T>
{
//Backup field for cloning from.
T[] array;
public ByValArray(int size)
{
array = new T[size];
//Updating the instance is really not necessary until we access it.
}
private void Update()
{
//This should be called from any public method on this struct.
T[] inst = FindInstance(ref this);
if(inst != array)
{
//A new array was cloned for this address.
array = inst;
}
}
//I suppose a GCHandle would be better than WeakReference,
//but this is sufficient for illustration.
static readonly Dictionary<IntPtr, WeakReference<T[]>> Cache = new Dictionary<IntPtr, WeakReference<T[]>>();
static T[] FindInstance(ref ByValArray<T> arr)
{
T[] orig = arr.array;
return UnsafeTools.GetPointer(
//Obtain the address from the reference.
//It uses a lambda to minimize the chance of the reference
//being moved around by the GC.
out arr,
ptr => {
WeakReference<T[]> wref;
T[] inst;
if(Cache.TryGetValue(ptr, out wref) && wref.TryGetTarget(out inst))
{
//An object is found on this address.
if(inst != orig)
{
//This address was overwritten with a new value,
//clone the instance.
inst = (T[])orig.Clone();
Cache[ptr] = new WeakReference<T[]>(inst);
}
return inst;
}else{
//No object was found on this address,
//clone the instance.
inst = (T[])orig.Clone();
Cache[ptr] = new WeakReference<T[]>(inst);
return inst;
}
}
);
}
//All subsequent methods should always update the state first.
public T this[int index]
{
get{
Update();
return array[index];
}
set{
Update();
array[index] = value;
}
}
public int Length{
get{
Update();
return array.Length;
}
}
public override bool Equals(object obj)
{
Update();
return base.Equals(obj);
}
public override int GetHashCode()
{
Update();
return base.GetHashCode();
}
public override string ToString()
{
Update();
return base.ToString();
}
}
var a = new ByValArray<int>(10);
a[5] = 11;
Console.WriteLine(a[5]); //11
var b = a;
b[5]++;
Console.WriteLine(b[5]); //12
Console.WriteLine(a[5]); //11
var c = a;
a = b;
Console.WriteLine(a[5]); //12
Console.WriteLine(c[5]); //11
如您所见,此值类型的行为与每次复制对数组的引用时都将底层数组复制到新位置一样。
警告!!!使用此代码需要您自担风险,最好不要在生产代码中使用。这种技术在很多层面上都是错误和邪恶的,因为它假定了不应该拥有它的东西的身份。尽管这试图为这个结构“强制”值类型语义(“最终证明手段”),但在几乎任何情况下,对于实际问题肯定有更好的解决方案。另请注意,尽管我已尝试预见任何可预见的问题,但在某些情况下,这种类型会表现出非常意外的行为。