プライベートメンバー_private bool[] boolArray;
_とメソッドChangeBoolValue(int index, bool Value)
を持つ構造体MyStruct
があります。
フィールド_public MyStruct bools { get; private set; }
_を持つクラスMyClass
があります
既存のオブジェクトから新しいMyStructオブジェクトを作成し、メソッドChangeBoolValue()を適用すると、参照されたものではなく参照が新しいオブジェクトにコピーされたため、両方のオブジェクトのブール配列が変更されます。例えば:
_MyStruct A = new MyStruct();
MyStruct B = A; //Copy of A made
B.ChangeBoolValue(0,true);
//Now A.BoolArr[0] == B.BoolArr[0] == true
_
より深いコピーを実装するようにコピーを強制する方法はありますか、それとも同じ問題が発生しないこれを実装する方法はありますか?
MyStructは値型であり、参照が伝播することを望まなかったため、特に構造体にしました。
ランタイムは構造体の高速メモリコピーを実行します。私が知る限り、構造体に独自のコピー手順を導入したり強制したりすることはできません。独自のClone
メソッドまたはコピーコンストラクターを導入することはできますが、それらを使用するように強制することはできません。
可能であれば、構造体を不変(または不変クラス)にするか、この問題を回避するために一般的に再設計するのが最善の策です。あなたがAPIの唯一の消費者である場合、おそらくあなたは特別な警戒を続けることができます。
Jon Skeet(およびその他)はこの問題について説明しており、例外もありますが、一般的に言えば、可変構造体は悪です。 構造体に参照型のフィールドを含めることができます
(ディープ)コピーを作成する簡単な方法の1つは、(リフレクションを使用するため)最速ではありませんが、BinaryFormatter
を使用して元のオブジェクトをMemoryStream
にシリアル化し、そこから逆シリアル化することです。 MemoryStream
から新しいMyStruct
へ。
static public T DeepCopy<T>(T obj)
{
BinaryFormatter s = new BinaryFormatter();
using (MemoryStream ms = new MemoryStream())
{
s.Serialize(ms, obj);
ms.Position = 0;
T t = (T)s.Deserialize(ms);
return t;
}
}
クラスと構造体で機能します。
回避策として、以下を実装します。
構造体には、BoolArray
の内容を変更できる2つのメソッドがあります。次のように、構造体のコピー時に配列を作成するのではなく、変更の呼び出しが行われたときにBoolArrayが新たに作成されます。
public void ChangeBoolValue(int index, int value)
{
bool[] Copy = new bool[4];
BoolArray.CopyTo(Copy, 0);
BoolArray = Copy;
BoolArray[index] = value;
}
これは、BoolArrayの大幅な変更を伴う使用には適していませんが、構造体の使用は多くのコピーであり、ほとんど変更されていません。これは、変更が必要な場合にのみ配列への参照を変更します。
奇妙なセマンティクスを回避するには、可変参照型のフィールドを保持する構造体は、次の2つのいずれかを実行する必要があります。
他の人が指摘しているように、構造体が配列をシミュレートできるようにする1つの方法は、構造体が配列を保持し、要素が変更されるたびにその配列の新しいコピーを作成することです。もちろん、そのようなことはとてつもなく遅いでしょう。別のアプローチは、最後のいくつかのミューテーション要求のインデックスと値を格納するロジックを追加することです。配列を読み取ろうとするたびに、値が最近書き込まれた値の1つであるかどうかを確認し、そうである場合は、配列内の値ではなく、構造体に格納されている値を使用します。構造体内のすべての「スロット」がいっぱいになったら、配列のコピーを作成します。このアプローチは、更新が多くの異なる要素にヒットした場合に配列を再生成する場合と比べて、せいぜい「のみ」一定の速度を提供しますが、更新の非常に大部分が少数の要素にヒットした場合に役立つ可能性があります。
更新の特別な集中度が高い可能性が高いが、要素が多すぎて構造体に完全に収まらない場合の別のアプローチは、「メイン」配列と「更新」配列への参照を保持することです。 「updates」配列がメイン配列のどの部分を表すかを示す整数。更新では、「更新」配列の再生成が必要になることがよくありますが、それはメイン配列よりもはるかに小さい可能性があります。 「updates」配列が大きくなりすぎた場合、メイン配列は、その中に組み込まれた「updates」配列によって表される変更を使用して再生成できます。
これらのアプローチの最大の問題は、効率的なコピーを可能にしながら一貫した値型セマンティクスを提示するようにstruct
を設計できる一方で、構造体のコードを一目見ただけではそれが明らかにならないことです(構造体にFoo
というパブリックフィールドがあるという事実により、Foo
がどのように動作するかが非常に明確になる、プレーンオールドデータ構造体と比較して)。
構造体は正しく渡されるとコピーされますか?そう:
public static class StructExts
{
public static T Clone<T> ( this T val ) where T : struct => val;
}
使用法:
var clone = new AnyStruct ().Clone ();
値型に関連する同様の問題について考えていたところ、これに対する「解決策」を見つけました。ご覧のとおり、C++のようにC#のデフォルトのコピーコンストラクターを変更することはできません。ただし、実際に構造体にアクセスするまで待ってから、それがコピーされたかどうかを確認することができます。
これに伴う問題は、参照型とは異なり、構造体には実際のIDがないことです。値による平等のみがあります。ただし、それらはメモリ内のどこかに格納する必要があり、このアドレスを使用して(一時的ではありますが)値の型を識別できます。 GCはオブジェクトを移動できるため、ここで懸念されます。そのため、構造体が配置されているアドレスが変更されるため、それに対処できる必要があります(たとえば、構造体のデータをプライベートにする)。
実際には、構造体のアドレスはthis
参照から取得できます。これは、単純なref T
値型の場合。 means を残して、ライブラリへの参照からアドレスを取得しますが、そのためのカスタム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
ご覧のとおり、この値型は、配列への参照がコピーされるたびに、基になる配列が新しい場所にコピーされたかのように正確に動作します。
警告!!!このコードは自己責任でのみ使用してください。本番コードでは使用しないでください。このテクニックは、それを持ってはならない何かのアイデンティティを前提としているため、非常に多くのレベルで間違っていて邪悪です。これはこの構造体の値型セマンティクスを「強制」しようとしますが(「終わりは手段を正当化する」)、ほとんど任意のの場合に実際の問題に対するより良い解決策が確かにあります。また、これに関して予測可能な問題を予測しようとしましたが、このタイプがまったく予期しない動作を示す場合があることにも注意してください。