ボックス化とは
まず、ボックス化とは何か?
ひとことで言えば、値型のデータをオブジェクト(参照型)に変換するプロセスのことです。
以下のような型変換は典型的なボックス化の例です。
ちなみに、オブジェクトから値型に変換することをボックス化の解除と言います。
int num = 1;
object obj = num; // ボックス化
obj = 123;
i = (int)obj; // ボックス化の解除
「いやいやこんな型変換使わないでしょ😗」と思われるかもしれませんが、実は色々な場面で不意に起きがちです。
例えば、様々な型を受け付けるmyList
という変数を宣言する際に、var myList = new List<object>();
と宣言してしまうと値型のデータをadd
するたびにボックス化が起きます。
多くの型を受け入れ可能で一見便利に見えるボックス化ですが、アプリケーションに悪影響を及ぼす可能性があるため、使用するかどうかは慎重に検討すべきです。ボックス化の解除も同様ですね。
なぜ悪影響が及ぶのか?
ボックス化が悪影響を及ぼす理由を以下の2点の観点から考察していきます。
- メモリの割り当て
- 処理のオーバーヘッド
1. メモリの割り当て
ボックス化が起きる際、値型のデータを参照型のオブジェクトの内部にコピーして値を格納します。
参照型として扱うということは、メモリはヒープ領域という領域に割り当てられます。
それに対して値型のデータの場合、スタック領域という領域に割り当てらます。
スタック領域のデータは処理終了とともに破棄されていきますが、ヒープ領域のインスタンスはガベージコレクションの対象となり、使い終わった後もガベージコレクションによって破棄されるまでメモリに残り続けます。
スタック領域で持てるデータをわざわざヒープ領域に割り当てることでガベージコレクションの負荷を増加させ、アプリケーションのパフォーマンスを低下を招くというわけですね。
2. 処理のオーバヘッド
上記のメモリ負荷に加え、ボックス化、ボックス化解除の操作自体が、単純な値の代入や参照に比べて計算コストが高いです。
値型のデータには必要のない値コピーのコストやガベージコレクションの管理コストも踏まえるとこれは自明ですね。
特に頻繁に実行される処理やループ処理内でのボックス化には注意です。
見逃しがちなボックス化集
最後に実装者が見逃して不意に起きがちなボックス化の例をいくつか挙げておきます。
実装時は要注意!
値型でインターフェース実装
interface IExampleInterface { }
struct MyStruct : IExampleInterface { }
// 構造体は値型のため、構造体がインターフェースを実装するとボックス化
IExampleInterface obj = new MyStruct();
ジェネリックでないコレクションの使用
using System.Collections;
// ジェネリックでないリストに値型のデータを追加でボックス化
var list = new ArrayList();
list.Add(123);
パラメータがobject型のメソッドを呼び出し
void WriteObject(object obj)
{
Console.WriteLine(obj);
}
// 値型をobject型のパラメータに渡すとボックス化
WriteObject(123);
補間文字列で値型を使用 → C#10以降では改善されてました
int height = 3776;
Console.WriteLine($"富士山の標高は{height}mです");
【追記】
C#10で可能な限りボックス化が起きないように改善されたみたいですね。
ただ、string.Concat
や+
演算子で値型を連結する際は依然としてボックス化が起きるようです。
値型のGetType()
メソッド呼び出し
int num = 123;
Type t = num.GetType();
params object[]
を使用するメソッド
void WriteObjects(params object[] values)
{
foreach (var value in values) Console.WriteLine(value);
}
int one = 1, two = 2, three = 3;
WriteObjects(one, two, three);
参考
Effective C# 6.0/7.0 Bill Wagner (著), 鈴木 幸敏 (監修, 翻訳)