C#でプログラミングを行う際、要素が一つも含まれない「空の配列」を扱う場面は頻繁に登場します。

関数の戻り値としてnullの代わりに空配列を返したり、データの初期状態を定義したりする場合など、その用途は多岐にわたります。

かつては new T[0] と記述するのが一般的でしたが、現在のC#ではパフォーマンスや可読性の観点から、より洗練された書き方が推奨されています。

本記事では、Array.EmptyとC# 12で導入された空コレクション式の違いを中心に、状況に応じた最適な空配列の初期化方法を徹底解説します。

C#における空配列初期化の変遷

C#の進化に伴い、空の配列を生成する方法はいくつか追加されてきました。

それぞれの書き方には、単なる構文の違いだけでなく、内部的な動作(メモリ割り当て)やパフォーマンス上の特性に大きな違いがあります。

まずは、これまで使われてきた主な手法を整理してみましょう。

  1. new T[0]: 最も古くからある、要素数0を指定してインスタンスを生成する方法。
  2. Array.Empty<T>(): .NET Framework 4.6 / .NET Core 1.0 以降で導入された、キャッシュを利用する方法。
  3. [] (コレクション式): C# 12 / .NET 8 以降で導入された、最も簡潔な最新の記述方法。

かつて主流だった new T[0] は、呼び出されるたびに新しいメモリ領域を確保するため、ループ内などで多用するとガベージコレクション(GC)に負荷をかける原因となっていました。

これに対し、近年の手法は「いかに効率的にメモリを扱うか」という点に主眼が置かれています。

伝統的な手法:new T[0] とその課題

まずは基本となる new 演算子を用いた初期化について見ていきます。

C#
// 伝統的な空配列の生成
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 に対して、一度だけ空の配列を作成し、以降の呼び出しではその同じインスタンスを再利用し続けます。

C#
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#
// 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の配列を返す」という設計が推奨されます。

C#
public string[] GetNames(int id)
{
    if (id <= 0)
    {
        // nullを返すのではなく、空配列を返す
        return []; 
    }
    
    return ["Alice", "Bob"];
}

このように記述することで、利用者は foreach ループを回す際などに null チェックを行う必要がなくなり、コードの堅牢性が向上します。

2. LINQとの組み合わせ

LINQメソッド( Enumerable.AnySelect など)は空のコレクションに対しても安全に動作します。

C#
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] は、メモリ効率の観点から現代の開発では使用を控えるべきです。

「たかが空の配列」と思われるかもしれませんが、こうした細かな最適化の積み重ねが、最終的なアプリケーションのパフォーマンスとメンテナンス性に大きく寄与します。

最新の構文を活用して、クリーンで効率的なコードを目指しましょう。