web-dev-qa-db-ja.com

ジェネリックでのボックス化とボックス化解除

整数のコレクションを作成する.NET 1.0の方法(たとえば)は次のとおりです。

ArrayList list = new ArrayList();
list.Add(i);          /* boxing   */
int j = (int)list[0]; /* unboxing */

これを使用することのペナルティは、ボックス化とボックス化解除によるタイプセーフティとパフォーマンスの欠如です。

.NET 2.0の方法は、ジェネリックスを使用することです。

List<int> list = new List<int>();
list.Add(i);
int j = list[0];

(私の理解では)ボクシングの代償は、ヒープ上にオブジェクトを作成し、スタックに割り当てられた整数を新しいオブジェクトにコピーし、ボックス化を解除する必要があることです。

ジェネリックの使用はこれをどのように克服しますか?スタックに割り当てられた整数はスタックに残り、ヒープからポイントされますか(範囲外になるとどうなるかというと、そうではありません)。スタックの別の場所にコピーする必要があるようです。

本当に何が起こっているのですか?

64
Itay Karo

コレクションに関して言えば、ジェネリックは実際のT[]配列を内部で利用することにより、ボックス化/ボックス化解除を回避することを可能にします。たとえば、List<T>T[]配列を使用してその内容を格納します。

arrayはもちろん参照型であるため、(CLRの現在のバージョンでは、yada yadaで)ヒープに格納されます。しかし、それはT[]であり、object[]ではないため、配列の要素は「直接」格納できます。つまり、それらはまだヒープ上にありますが、ヒープ上にあります配列ボックス化される代わりに、配列にはボックスへの参照が含まれます。

したがって、たとえばList<int>の場合、配列内にあるものは次のようになります。

 [1 2 3] 

これを、object[]を使用するArrayListと比較して、次のように「見える」ようにします。

 [* a * b * c] 

...ここで、*aなどは、オブジェクト(ボックス整数)への参照です。

 * a-> 1 
 * b-> 2 
 * c-> 3 

それらの大雑把なイラストを許してください。うまくいけば、あなたは私が何を意味するのか知っています。

64
Dan Tao

混乱は、スタック、ヒープ、変数の関係を誤解した結果です。これについて考える正しい方法は次のとおりです。

  • 変数は、タイプを持つ保管場所です。
  • 変数の存続期間は、短い場合と長い場合があります。 「短い」とは「現在の関数が戻るかスローするまで」を意味し、「長い」とは「それよりもおそらく長い」ことを意味します。
  • 変数の型が参照型である場合、変数の内容は、長期間保存される場所への参照です。変数のタイプが値タイプの場合、変数の内容は値です。

実装の詳細として、短命であることが保証されている格納場所をスタックに割り当てることができます。存続期間が長い可能性のある保管場所がヒープに割り当てられています。これは、「値型は常にスタックに割り当てられる」ことについては何も言っていないことに注意してください。値のタイプはnot常にスタックに割り当てられます:

int[] x = new int[10];
x[1] = 123;

x[1]は保管場所です。それは長命です。この方法よりも長持ちする可能性があります。したがって、ヒープ上になければなりません。 intが含まれているという事実は関係ありません。

あなたはboxed intが高価である理由を正しく言います:

ボクシングの代償は、ヒープ上にオブジェクトを作成し、スタックに割り当てられた整数を新しいオブジェクトにコピーし、ボックス化を解除する必要があることです。

あなたが間違っているのは、「スタックに割り当てられた整数」と言うことです。整数がどこに割り当てられたかは関係ありません。重要なのは、そのストレージヒープの場所への参照ではなく、ストレージ整数を含むであったことです。価格は、オブジェクトを作成してコピーを行う必要性です。これが関連する唯一のコストです。

それでは、なぜジェネリック変数は高価ではないのでしょうか? T型の変数があり、Tがintになるように構築されている場合、int型、period型の変数があります。 int型の変数は格納場所であり、intが含まれています。 その格納場所がスタック上にあるか、ヒープが完全に無関係であるか。関連するのは、格納場所ヒープ上の何かへの参照ではなく、格納場所intを含むであることです。ストレージの場所にはintが含まれているため、ボックス化とボックス化解除のコストを負担する必要はありません。ヒープに新しいストレージを割り当て、intを新しいストレージにコピーします。

それは今明らかですか?

63
Eric Lippert

ArrayListはobject型のみを処理するため、このクラスを使用するには、objectとのキャストが必要です。値型の場合、このキャストにはボックス化とボックス化解除が含まれます。

ジェネリックリストを使用すると、コンパイラーはその値タイプに特化したコードを出力するため、実際の値は、オブジェクトへの参照ではなくリストに格納されます。値が含まれています。したがって、ボクシングは必要ありません。

(私の理解では)ボクシングの代償は、ヒープ上にオブジェクトを作成し、スタックに割り当てられた整数を新しいオブジェクトにコピーし、ボックス化を解除する必要があることです。

値型は常にスタックにインスタンス化されると想定していると思います。これは当てはまりません-それらはヒープ上、スタック上、またはレジスターのいずれかに作成できます。これについての詳細は、Eric Lippertの記事を参照してください: 真偽値型

3
Mark Byers

Genericsを使用すると、リストの内部配列を、実際には_int[]_ではなく_object[]_と入力できるため、ボックス化が必要になります。

ジェネリックなしで起こることはここにあります:

  1. Add(1)を呼び出します。
  2. 整数_1_はオブジェクトにボックス化され、ヒープ上に新しいオブジェクトを構築する必要があります。
  3. このオブジェクトはArrayList.Add()に渡されます。
  4. ボックス化されたオブジェクトは_object[]_に詰め込まれます。

ここには、3つのレベルの間接参照があります:ArrayList-> _object[]_-> object-> int

ジェネリックで:

  1. Add(1)を呼び出します。
  2. Int 1はList<int>.Add()に渡されます。
  3. Intは_int[]_に詰め込まれます。

したがって、間接参照のレベルは2つしかありません:_List<int>_-> _int[]_-> int

他のいくつかの違い:

  • 非ジェネリックメソッドでは、値を格納するために合計8バイトまたは12バイト(1つのポインター、1つのint)が必要です。1つの割り当てでは4/8、もう1つの割り当てでは4バイトです。そして、これはおそらく整列とパディングのためにもっと多くなります。ジェネリックメソッドは、配列に4バイトのスペースしか必要としません。
  • 非ジェネリックメソッドでは、ボックス化されたintを割り当てる必要があります。ジェネリックメソッドにはありません。これはより速く、GCチャーンを減らします。
  • 非ジェネリックメソッドでは、値を抽出するためにキャストが必要です。これはタイプセーフではなく、少し遅いです。
3
cdhowie

.NET 1では、Addメソッドが呼び出されると、次のようになります。

  1. スペースはヒープに割り当てられます。新しい参照が行われます
  2. i変数の内容が参照にコピーされます
  3. 参照のコピーがリストの最後に置かれます

.NET 2の場合:

  1. 変数iのコピーがAddメソッドに渡されます
  2. そのコピーのコピーがリストの最後に置かれます

はい、i変数は引き続きコピーされます(結局のところ、これは値の型であり、値の型は常にメソッドのパラメーターであっても常にコピーされます)。しかし、ヒープ上に冗長コピーが作成されることはありません。

1
Tim Robinson

なぜあなたはWHEREの観点から値\オブジェクトが保存されていると考えていますか? C#では、CLRの選択内容に応じて、値の型をヒープと同様にスタックに格納できます。

ジェネリックの違いがどこにあるかWHATはコレクションに格納されます。 ArrayListの場合、コレクションにはボックス化されたオブジェクトへの参照が含まれ、List<int>にはint値自体が含まれます。

1