C#でプログラミングを行う際、要素が一つも含まれない「空の配列」を扱う場面は頻繁に登場します。
関数の戻り値としてnullの代わりに空配列を返したり、データの初期状態を定義したりする場合など、その用途は多岐にわたります。
かつては new T[0] と記述するのが一般的でしたが、現在のC#ではパフォーマンスや可読性の観点から、より洗練された書き方が推奨されています。
本記事では、Array.EmptyとC# 12で導入された空コレクション式の違いを中心に、状況に応じた最適な空配列の初期化方法を徹底解説します。
C#における空配列初期化の変遷
C#の進化に伴い、空の配列を生成する方法はいくつか追加されてきました。
それぞれの書き方には、単なる構文の違いだけでなく、内部的な動作(メモリ割り当て)やパフォーマンス上の特性に大きな違いがあります。
まずは、これまで使われてきた主な手法を整理してみましょう。
- new T[0]: 最も古くからある、要素数0を指定してインスタンスを生成する方法。
- Array.Empty<T>(): .NET Framework 4.6 / .NET Core 1.0 以降で導入された、キャッシュを利用する方法。
- [] (コレクション式): C# 12 / .NET 8 以降で導入された、最も簡潔な最新の記述方法。
かつて主流だった new T[0] は、呼び出されるたびに新しいメモリ領域を確保するため、ループ内などで多用するとガベージコレクション(GC)に負荷をかける原因となっていました。
これに対し、近年の手法は「いかに効率的にメモリを扱うか」という点に主眼が置かれています。
伝統的な手法:new T[0] とその課題
まずは基本となる new 演算子を用いた初期化について見ていきます。
// 伝統的な空配列の生成
string[] oldStyle = new string[0];
// 何らかの処理
Console.WriteLine($"要素数: {oldStyle.Length}");
このコードは直感的で分かりやすいですが、大きな欠点があります。
それは、実行されるたびに新しい配列オブジェクトがヒープ領域に作成されるという点です。
空の配列は中身が変更されることがないため、本来であればシステム全体で一つの「空インスタンス」を共有しても問題ありません。
しかし、new キーワードを使うと必ず新しいインスタンスが生成されるため、メモリの無駄遣いが発生します。
小規模なプログラムでは問題になりませんが、高頻度で呼ばれるライブラリや大規模システムでは、このわずかなメモリ割り当てが累積してパフォーマンス低下を招くことがあります。
推奨される手法:Array.Empty<T> による最適化
.NET Framework 4.6以降、空配列の生成におけるベストプラクティスとして定着したのが Array.Empty<T>() です。
Array.Empty の内部挙動
Array.Empty<T>() は、ジェネリック型を活用した「シングルトンパターン」を採用しています。
特定の型 T に対して、一度だけ空の配列を作成し、以降の呼び出しではその同じインスタンスを再利用し続けます。
using System;
class Program
{
static void Main()
{
// Array.Emptyを使用して空配列を取得
string[] empty1 = Array.Empty<string>();
string[] empty2 = Array.Empty<string>();
// 同一のインスタンスを指しているか確認
bool isSame = ReferenceEquals(empty1, empty2);
Console.WriteLine($"empty1とempty2は同一インスタンスか: {isSame}");
}
}
empty1とempty2は同一インスタンスか: True
上記の通り、何度呼び出しても同一のインスタンスを返すため、メモリの割り当て(アロケーション)がゼロになります。
GCの負荷を軽減できるため、特別な理由がない限りは new T[0] よりもこちらを使用すべきです。
C# 12の最新構文:コレクション式 []
2023年にリリースされたC# 12では、配列やリストの初期化を統一的な構文で行える「コレクション式」が導入されました。
空の配列を初期化する場合、単に [] と記述するだけで済むようになりました。
コレクション式の書き方
// C# 12以降の推奨される書き方
string[] modernEmpty = [];
// 型推論を利用する場合(左辺に型が必要)
IEnumerable<int> numbers = [];
この構文の最大のメリットは、コードが圧倒的に短く、読みやすくなることです。
また、単に書きやすいだけでなく、コンパイラによる最適化が自動的に行われる点も重要です。
コレクション式の内部的な動作
C# 12のコンパイラは、配列の初期化に [] が使われた場合、可能な限り Array.Empty<T>() への呼び出しに置き換えます。
つまり、開発者が意識しなくても、自動的に最もパフォーマンスの良いコード(メモリ再利用)が生成される仕組みになっています。
ただし、代入先の型が List<T> など配列以外の場合は、それぞれの型に適した「空の状態」を生成します。
ターゲットの型に合わせて最適な初期化コードをコンパイラが選択してくれるため、現代のC#開発においてはこのコレクション式が第一選択肢となります。
各手法の比較まとめ
それぞれの初期化手法の特徴を以下の表にまとめました。
| 手法 | 登場バージョン | メモリ効率 | 推奨度 | 特徴・備考 |
|---|---|---|---|---|
new T[0] | 初期(C# 1.0) | 低い (毎回生成) | 低 | 古いコードでよく見られるが、現代では非推奨。 |
Array.Empty<T>() | .NET 4.6 / Core 1.0 | 高い (キャッシュ) | 高 | 明示的に「空の配列」であることを示せる。 |
[] (コレクション式) | C# 12 / .NET 8 | 高い (最適化) | 最高 | 最も簡潔。内部で Array.Empty が呼ばれる。 |
なぜ new T[0] は今でも動くのか?
後方互換性を維持するため、既存の構文が廃止されることは稀です。
古いプロジェクトをメンテナンスする場合や、C# 12未満の環境(古いUnityバージョンやレガシーな.NET Framework環境など)では、依然として旧来の書き方をする必要があります。
しかし、新しいプロジェクトを始めるのであれば、あえて古い書き方を選ぶ理由はありません。
実践的な活用シーンと注意点
空の配列を適切に使い分けるための、具体的なユースケースを解説します。
1. 関数の戻り値としての空配列
C#では、データが存在しない場合に null を返すと、呼び出し側で NullReferenceException のリスクが生じます。
これを避けるために、「データがない=要素数0の配列を返す」という設計が推奨されます。
public string[] GetNames(int id)
{
if (id <= 0)
{
// nullを返すのではなく、空配列を返す
return [];
}
return ["Alice", "Bob"];
}
このように記述することで、利用者は foreach ループを回す際などに null チェックを行う必要がなくなり、コードの堅牢性が向上します。
2. LINQとの組み合わせ
LINQメソッド( Enumerable.Any や Select など)は空のコレクションに対しても安全に動作します。
using System;
using System.Linq;
string[] tags = [];
// 要素が空でも例外は発生せず、単に false が返る
if (tags.Any())
{
Console.WriteLine("タグが存在します");
}
else
{
Console.WriteLine("タグはありません");
}
3. Array.Empty と [] の使い分け基準
「どちらを使っても同じ最適化がされるなら、どちらを使うべきか?」という疑問が生じるかもしれません。
基本的には 「コレクション式 [] 」 を使うのが現在の主流ですが、以下の観点で使い分けることもあります。
- 可読性重視なら
[]: コードがノイズレスになり、意図が明確になります。 - 型を明示したいなら
Array.Empty<T>(): 特にメソッドの引数に直接渡す際、ターゲット型が曖昧な場合には、明示的に型を記述できるArray.Empty<string>()の方がエラーを避けやすい場合があります。
パフォーマンス検証:アロケーションの差
実際にどれほどの差が出るのか、概念的なベンチマークの観点から説明します。
100万回のループ内で空配列を生成した場合、new int[0] は100万個のオブジェクトを生成し、その分だけGCがメモリ回収に動かなければなりません。
一方で Array.Empty<int>() または [] は、最初に作られた1つのインスタンスを100万回参照するだけです。
この差は、特にサーバーサイドのアプリケーションや、フレームレートが重要なゲーム開発において、「GC Stop-the-World(GCによるプログラムの一時停止)」を抑えるという大きなメリットに繋がります。
空配列と null の決定的な違い
初心者の方が混同しやすいのが「空配列」と「null」の違いです。
- null: 「配列そのものが存在しない」状態。アクセスすると
NullReferenceException。 - 空配列: 「配列という容器は存在するが、中身が0個である」状態。アクセス(Lengthの取得など)しても安全。
プログラムの品質を高めるためには、「コレクションを返すメソッドは原則として null を返さない」というルールを徹底することが重要です。
その際に、今回紹介した効率的な空配列の生成手法が役立ちます。
まとめ
C#で空の配列を初期化する方法は、言語の進化とともに「より短く、より効率的」に変化してきました。
- C# 12以降の環境であれば、迷わずコレクション式
[]を使用してください。これが最も現代的で効率的な書き方です。 - C# 12未満、かつ.NET Framework 4.6以降の環境であれば、
Array.Empty<T>()を使用するのがベストです。 - new T[0] は、メモリ効率の観点から現代の開発では使用を控えるべきです。
「たかが空の配列」と思われるかもしれませんが、こうした細かな最適化の積み重ねが、最終的なアプリケーションのパフォーマンスとメンテナンス性に大きく寄与します。
最新の構文を活用して、クリーンで効率的なコードを目指しましょう。
