I was thinking about a similar issue related to value types, and found out a "solution" to this. You see, you cannot change the default copy constructor in C# like you can in C++, because it's intended to be lightweight and side effects-free. However, what you can do is wait until you actually access the struct, and then check if it was copied.
The problem with this is that unlike reference types, structs have no real identity; there is only by-value equality. However, they still have to be stored at some place in memory, and this address can be used to identify (albeit temporarily) a value type. The GC is a concern here, because it can move objects around, and therefore change the address at which the struct is located, so you would have to be able to cope with that (e.g. make the struct's data private).
In practice, the address of the struct can be obtained from the this
reference, because it's a simple ref T
in case of a value type. I leave the means to obtain the address from a reference to my library, but it's quite simple to emit custom CIL for that. In this example, I create something what is essentially a byval array.
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
As you can see, this value type behaves exactly as if the underlying array was copied to a new location every time the reference to the array is copied.
WARNING!!! Use this code only at your own risk, and preferably never in a production code. This technique is wrong and evil at so many levels, because it assumes identity for something that shouldn't have it. Although this tries to "enforce" value type semantics for this struct ("the end justifies the means"), there are certainly better solutions to the real problem in almost any case. Also please note that although I have tried to foresee any foreseeable issues with this, there could be cases where this type will show quite an unexpected behaviour.