可変の重要なタイプの危険性について

困難な運命がプラットフォームにもたらしたほとんどのプログラマーは、値型と参照型の存在を認識しています。 そして、それらの多くは、名前に加えて、これらのタイプのオブジェクトのメモリ内およびセマンティクス内での位置など、他の違いがあることをよく知っています。



最初の違い(少なくとも完全を期すために言及する価値があります)については、参照型のインスタンスは常にマネージドヒープに配置されますが、重要な型のインスタンスはデフォルトでスタックに配置されますが、マネージドに移行できますこれは、パッケージ化、参照型のメンバーであることに加え、クロージャー(*)などのトリッキーなエキゾチックなC#言語構造でそれらを使用する場合に多くの原因があります。



この違いは非常に重要であり、重要なタイプが存在し使用されているため、このタイプのペアには別の、それほど重要ではないセマンティックの違いがあります。 名前が示すように、重要な型は、関数に渡されるたびに、または関数から返されるときにコピーされる値です。 また、名前のプロンプトに従ってコピーを行うと、コピーは転送されて元のバージョンではなく返されますが、変更しようとすると、元のインスタンスではなくコピーが変更されます。





理論的には、最後のステートメントは非常に単純で明白なため、注目に値しないように見えますが、C#では、コピーが非常に暗黙的であるため、開発者が考えている完全に異なるインスタンスのコピーにつながり、開発者を導く瞬間がいくつかあります(開発者)わずかな混乱に。



これらの例をいくつか見てみましょう。



1.オブジェクトプロパティの形式の可変有効型





コピーがかなり明示的な比較的単純な例から始めましょう。 Mutableと呼ばれる可変の意味のあるタイプ(これだけでなく、以降のすべての例にも役立つ)と、指定されたタイプのプロパティを含むクラスAがあるとします。



struct Mutable

{

public Mutable( int x, int y)

: this ()

{

X = x;

Y = y;

}

public void IncrementX() { X++; }

public int X { get ; private set ; }

public int Y { get ; set ; }

}

class A

{

public A() { Mutable = new Mutable(x: 5, y: 5); }

public Mutable Mutable { get ; private set ; }

}




* This source code was highlighted with Source Code Highlighter .








これまでのところ、何もおもしろくないようですが、次の例を見てみましょう。



A a = new A();

a.Mutable.Y++;




* This source code was highlighted with Source Code Highlighter .








最も興味深いのは、2行目( a.Mutable.Y ++;)がC#言語の観点から間違っているため、このコードがまったくコンパイルされないことです。 同じ名前のプロパティから戻るときにMutable構造体の値がコピーされるため、コンパイラーは、コンパイル段階で既に一時オブジェクトを変更しても意味がないことを認識します。これは、エラーメッセージで雄弁です:“ error CS1612:Cannot return the value of ' System.Collections.Generic.IList <MutableValueTypes.Mutable> .this [int]「変数ではないため 」。 このコード行では、L値ではない値を変更するだけであるため、C ++言語にある程度精通しているすべての人にとって、この動作は非常に理解しやすいものです。



コンパイラーは++演算子のセマンティクスを理解しますが、一般に、特定の関数が現在のオブジェクトに対して何を行うか、特に変更するかどうかはわかりません。 また、前のコードフラグメントでYプロパティの++演算子を呼び出すことはできませんが、 Xの IncrementXメソッドを静かに呼び出すことができます。



Console .WriteLine( " Mutable.X: {0}" , a.Mutable.X);

a.Mutable.IncrementX();

Console .WriteLine( "Mutable.X IncrementX(): {0}" , a.Mutable.X);




* This source code was highlighted with Source Code Highlighter .








前のコードは正しく動作しませんが、肉眼でエラーを見つけることは必ずしも簡単ではありません。 Mutableクラスのプロパティにアクセスするたびに、 IncrementXメソッドが呼び出される新しいコピーが作成されますが、コピーの変更は元のオブジェクトの変更とは関係がないため、コンソールへの出力は前のコードフラグメントに対応します。



元の値Mutable.X:5



IncrementX()を呼び出した後のMutable.X: 5



「うーん...超自然的ではない」とあなたは言います、そしてあなたは正しいでしょう...他のより興味深いケースを見るまで。



2.可変の意味のある型と読み取り専用修飾子





クラスBを見てみましょう。このクラスには、 読み取り専用フィールドに可変の可変構造が含まれています



class B

{

public readonly Mutable M = new Mutable(x: 5, y: 5);

}




* This source code was highlighted with Source Code Highlighter .








繰り返しますが、これはロケット科学ではなく、最も単純なクラスであり、唯一の欠点はオープンフィールドの使用です。 しかし、このフィールドのオープン性は単純な例と利便性によるものであり、設計エラーではないため、この些細なことに注意を払うべきではありません。 代わりに、このクラスの使用の簡単な例と得られた結果に注意を払う必要があります。



B b = new B();

Console .WriteLine( " MX: {0}" , bMX);

bMIncrementX();

bMIncrementX();

bMIncrementX();

Console .WriteLine( "MX IncrementX: {0}" , bMX);




* This source code was highlighted with Source Code Highlighter .








結果として何が出力されますか? 8? (プロパティXの初期値は5であり、既知のように5 + 3、8に等しいことを思い出させてください。7の方が良いかもしれませんが、残念ながら8になります)または、おそらく-8ですか? 冗談。



Mは返されるたびにコピーされるプロパティではないようですので、答え8は非常に論理的です。 ただし、コンパイラー(およびC#言語仕様も)は私たちと意見が異なり、このコードを実行した結果、 MXは5のままになります。



オリジナルMX:5



IncrementX()を3回呼び出した後のMX:5



ここでのことは、仕様に従って、コンストラクター外で読み取り専用フィールドにアクセスすると、 IncrementXメソッドが呼び出される一時変数が生成されることです。 実際、以前のコードフラグメントは、コンパイラによって次のように書き換えられます。



Console .WriteLine( " MX: {0}" , bMX);

Mutable tmp1 = bM;

tmp1.IncrementX();

Mutable tmp2 = bM;

tmp2.IncrementX();

Mutable tmp3 = bM;

tmp3.IncrementX();

Console .WriteLine( "MX IncrementX: {0}" , bMX);




* This source code was highlighted with Source Code Highlighter .








(はい、 読み取り専用修飾子を削除すると、期待される結果が得られます。IncrementXメソッドを3回呼び出した後、変数Mの Xプロパティの値は8になります。)



3.配列とリスト





可変で重要な型の非自明な動作の次の、しかし最後ではないのは、配列とリストでの使用です。 したがって、可変の重要なタイプの1つの要素をコレクション、たとえばList <T>リストに入れましょう。



List <Mutable> lm = new List <Mutable> { new Mutable(x: 5, y: 5) };



* This source code was highlighted with Source Code Highlighter .








リストインデクサーは共通のプロパティであるため、その動作は最初のセクションで説明したものと変わりません。リストアイテムにアクセスするたびに、ソースアイテムではなく、そのコピーを受け取ります。



lm[0].Y++; //

lm[0].IncrementX(); //




* This source code was highlighted with Source Code Highlighter .








次に、配列を使用して同じ操作を実行してみましょう。



Mutable[] am = new Mutable[] { new Mutable(x: 5, y: 5) };

Console .WriteLine( " X: {0}, Y: {1}" , am[0].X, am[0].Y);

am[0].Y++;

am[0].IncrementX();

Console .WriteLine( " X: {0}, Y: {1}" , am[0].X, am[0].Y);




* This source code was highlighted with Source Code Highlighter .








この場合、ほとんどの開発者は、配列インデクサーが同様の方法で動作し、要素のコピーを返すと想定します。その後、コードが変更されます。 また、C#は関数から「管理ポインター」を返す可能性をサポートしていないため、他のオプションはないようです。 結局、できることは、変数(エイリアス)の同義語を作成し、 refキーワードまたはoutキーワードを使用して別の関数に渡すことだけですが、オブジェクトのフィールドの1つへのリンクを返す関数を記述することはできません。



しかし、C#は一般的なケースでは管理リンクの戻り値をサポートしていませんが、配列要素のコピーだけでなく、それへのリンクを取得できる特別なILコード命令の形式で特別な最適化があります(奇妙なことに、この命令はldelemaと呼ばれます )。 この機能により、前のフラグメントは完全に正しい(文字列am [0] .Y ++;を含む)だけでなく、コピーではなく配列の要素を直接変更することもできます。 前のコードフラグメントを実行すると、配列のゼロオブジェクトをコンパイル、実行、および直接変更することがわかります。



初期値X:5、Y:5



新しい値X:6、Y:6



ただし、上記で検討した配列がIList <T>などのインターフェイスの1つに縮小された場合、特別なIL命令を生成する形式のすべてのストリートマジックは除外され、このセクションの冒頭で説明した動作が得られます。



Mutable[] am = new Mutable[] { new Mutable(x: 5, y: 5) };

IList<Mutable> lst = am;

lst[0].Y++; //

lst[0].IncrementX(); //




* This source code was highlighted with Source Code Highlighter .








4.そして、なぜこれがすべて必要なのですか?





質問は合理的です。特に、自分で意味のある型を作成する頻度を思い出せば、さらにそれを変更可能にする頻度を思い出せばなおさらです。 しかし、この知識には利点があります。 まず第一に、私たちは世界で唯一のプログラマーではありません。推測することは難しくありません。ひどい力でコードをリベットし、独自の可変構造を作成する他の「ガブリク」がたくさんあります。 また、個人的にそのような「gavrik」がチームにない場合でも、それらは他のチーム、例えば、.Net Framework開発チームにいます。 はい、.Net Frameworkには十分な数の可変の重要なタイプがあります。不注意に使用すると、コストのかかる驚きが発生する可能性があります(**)。



可変の重要な型の古典的な例は、 Point構造体とListEnumeratorなどの列挙です。 そして、最初のケースで足を切り落とすのが非常に難しい場合、2番目のケースで健康になります:



var x = new { Items = new List < int > { 1, 2, 3 }.GetEnumerator() };

while (x.Items.MoveNext())

{

Console .WriteLine(x.Items.Current);

}




* This source code was highlighted with Source Code Highlighter .








(このコードをLINQPadまたはMainメソッドにコピーして実行します。)



おわりに





可変の意味のある型は完全な悪であると断定的に言うことは、 goto演算子の包括的な悪について話すのと同じくらい間違っています。 大規模な産業用システムでプログラマがgoto演算子を直接使用すると、コードの理解と保守が困難になり、エラーを検索する際に隠れたエラーや頭痛の種になることが知られています。 同じ理由で、可変の重要なタイプに注意する必要があります。それらを調理する方法を知っている場合、それらの注意深いアプリケーションは優れたパフォーマンス最適化になります。 しかし、この効率性は、C#言語の仕様をまだ歯で学んでおらず、重要な型でusing構造体を使用することでコピーがクリアされることをまだ知らない隣人が、後で戻ってくるかもしれません(***)。



重要な型の使用はすでに最適化されているため、使用する価値があることを証明する必要があり、パフォーマンスが向上します。 可変の重要な型の使用は2乗の最適化です(変更時にコピーを保存するため)。したがって、重要な型を可変にする前に、 n倍ではなくn倍と考える必要があります。



-----------------------------



(*)クロージャーは、複雑な名前から思われるほどひどい獣ではありません。 そして、突然、何らかの理由でこれに関する知識がわからない場合、これはそれを修正する大きな理由に過ぎません: 「C#のクロージャ」



(**)最も興味深いのは、可変の意味のある型が唯一の疑わしいソリューションからほど遠いことであり、その兆候は.Net Frameworkで簡単に見つけることができます。 同様に疑わしい設計上のもう1つの決定は、 仮想イベントの動作(以前に説明しました)であり、それらのあいまいな動作すべてについて、.Net Frameworkにも存在します(たとえば、 ObservableCollectionクラスのPropertyChangedおよびCollectionChangedイベントは仮想です)



(***)これは、Eric Lippertの記事(可変で意味のあるタイプを最大の普遍的な悪と見なしている)の1つへのわずかな参照です。IDisposableインターフェイスを実装する可変で意味のあるタイプを使用すると、 ボックスに、それは質問です。



All Articles