C#におけるデータ管理の基本となる配列には、大きく分けて「1次元配列」「多次元配列(矩形配列)」「ジャグ配列」の3種類が存在します。

その中でもジャグ配列は「配列の配列」とも呼ばれ、各要素がさらに配列を保持するという特殊な構造を持っています。

行ごとに異なる要素数を持たせることができるため、柔軟なデータ構造を構築する際に非常に強力なツールとなります。

本記事では、ジャグ配列の基礎から多次元配列との決定的な違い、そして実践的な活用方法までをプロの視点で詳しく解説します。

ジャグ配列とは何か

ジャグ配列 (Jagged Array) は、名前の通り「ギザギザな(Jagged)」構造を持つ配列です。

C#の型システムにおいては、「ある配列の要素として別の配列が格納されている」状態を指します。

一般的な多次元配列(矩形配列)が、すべての行において同じ列数を持つ「整った長方形」の形をしているのに対し、ジャグ配列は行ごとに列の長さが異なっていても構いません

例えば、1行目は3要素、2行目は5要素、3行目は2要素といった、不揃いなデータ集合を効率的に扱うことができます。

配列の配列という構造

ジャグ配列を宣言する際の記法は int[][] のように、角括弧を重ねて記述します。

これは「int型の配列」を要素に持つ「配列」であることを意味しています。

この構造を理解する上で重要なのは、親となる配列には、子となる各配列への参照(ポインタ)が格納されているという点です。

そのため、メモリ上では必ずしも連続した領域に配置されるわけではなく、各行の配列が独立したオブジェクトとして存在しています。

ジャグ配列の宣言と初期化

ジャグ配列を使用するには、まず外側の配列をインスタンス化し、その後、各要素(内側の配列)を個別にインスタンス化する必要があります。

ここでは、いくつかの初期化パターンを見ていきましょう。

基本的な宣言と段階的な初期化

最も標準的な方法は、まず行数を指定して宣言し、その後に各行のサイズを決める方法です。

C#
// 3つの「int型配列」を格納できるジャグ配列を宣言
int[][] jaggedArray = new int[3][];

// 各行の配列を異なるサイズで初期化
jaggedArray[0] = new int[3]; // 0行目は要素数3
jaggedArray[1] = new int[5]; // 1行目は要素数5
jaggedArray[2] = new int[2]; // 2行目は要素数2

// 値の代入
jaggedArray[0][0] = 10;
jaggedArray[1][4] = 50;
jaggedArray[2][1] = 100;

このコードでは、jaggedArray[0] そのものが一つの配列オブジェクトとなっていることが分かります。

宣言と同時に初期化する

あらかじめ格納するデータが決まっている場合は、簡略化された記法を使用できます。

C#
// 宣言と同時に値を代入する
int[][] jaggedArray2 = new int[][]
{
    new int[] { 1, 2, 3 },
    new int[] { 4, 5, 6, 7, 8 },
    new int[] { 9, 10 }
};

このように記述することで、コードの可読性が高まり、構造が視覚的にも分かりやすくなります。

ジャグ配列と多次元配列の違い

C#には、ジャグ配列と似た概念として「多次元配列(矩形配列)」があります。

初心者が最も混同しやすいポイントであり、パフォーマンスやメモリ効率に直結するため、両者の違いを正確に理解しておくことが重要です。

構造とメモリ配置の比較

多次元配列は int[3, 5] のように宣言され、メモリ上の連続した領域に配置されます。

これに対し、ジャグ配列は「配列を指す参照の配列」です。

特徴多次元配列 (int[,])ジャグ配列 (int[][])
形状常に矩形(すべての行が同じ長さ)各行で長さが異なっても良い(ギザギザ)
宣言方法new int[3, 5]new int[3][]
アクセスarray[i, j]array[i][j]
メモリ配置連続した単一のブロック複数のオブジェクトに分散
初期化一括で行われる親配列と子配列で段階的に行う必要がある

パフォーマンスの差異

一般的に、連続したメモリ領域を確保する多次元配列の方が高速であると思われがちですが、実はC#(.NET)においてはジャグ配列の方が高速に動作するケースが多いです。

その理由は、.NETのJITコンパイラが「1次元配列」へのアクセスに対して非常に高度な最適化(境界チェックの省略など)を行うためです。

ジャグ配列は「1次元配列の入れ子」であるため、この最適化の恩恵を受けやすいのです。

一方、多次元配列は専用の命令セットが使用されるため、最適化がかかりにくい傾向があります。

ジャグ配列の要素へのアクセスと反復処理

ジャグ配列内のデータを取り出すには、インデックスを重ねて指定します。

また、各行の長さが異なるため、ループ処理を行う際には注意が必要です。

for文によるアクセス

各行の要素数を取得するには、array[i].Length を使用します。

C#
int[][] jaggedArray = new int[][]
{
    new int[] { 10, 20 },
    new int[] { 30, 40, 50 },
    new int[] { 60 }
};

// 外側の配列のループ
for (int i = 0; i < jaggedArray.Length; i++)
{
    // 内側の各配列のループ
    for (int j = 0; j < jaggedArray[i].Length; j++)
    {
        Console.WriteLine($"Element at [{i}][{j}]: {jaggedArray[i][j]}");
    }
}
実行結果
Element at [0][0]: 10
Element at [0][1]: 20
Element at [1][0]: 30
Element at [1][1]: 40
Element at [1][2]: 50
Element at [2][0]: 60

foreach文によるアクセス

読み取り専用の処理であれば、foreach 文を使用するのが最も簡潔です。

C#
foreach (int[] row in jaggedArray)
{
    foreach (int element in row)
    {
        Console.Write(element + " ");
    }
    Console.WriteLine();
}

外側のループでは「配列」が取り出され、内側のループでその中の「要素」が取り出されるという流れを意識してください。

実践的な活用シーン

ジャグ配列は、単純なグリッドデータ以上の複雑な構造を表現するのに適しています。

1. グループ化されたデータの保持

例えば、ある学校のクラス名簿を管理する場合を考えます。

各クラス(1組、2組…)によって生徒数が異なる場合、多次元配列では最大人数に合わせる必要があり、メモリの無駄が生じます。

ジャグ配列なら、各クラスの人数にぴったりのサイズで構築できます。

2. CSVやテキストデータの解析

CSVファイルなどの外部データを読み込む際、行によって列数が異なる(可変長データ)ことがあります。

このようなデータをメモリ上に保持する際、ジャグ配列は最適なデータ構造となります。

3. スパース(疎)なデータ構造の表現

データの大部分がゼロや空であり、特定の箇所にのみ値が存在するような場合、必要な部分だけ配列を生成することで、メモリ消費を劇的に抑えることが可能です。

注意点とベストプラクティス

ジャグ配列を使用する際には、特有の注意点があります。

これらを怠ると、実行時エラーの原因となります。

NullReferenceException への警戒

ジャグ配列は宣言した直後、各要素は null で初期化されています。

内側の配列をインスタンス化せずにアクセスしようとすると、NullReferenceException が発生します。

C#
int[][] jagged = new int[3][];
// jagged[0][0] = 1; // ここでエラーが発生する!

jagged[0] = new int[5]; // 必ずインスタンス化が必要
jagged[0][0] = 1;       // これで安全

多次元配列との使い分けの基準

基本的には以下の基準で選択することをお勧めします。

多次元配列(矩形配列)

行列計算など、数学的な「面」としてデータを扱う場合。

すべてのデータの行・列が固定され、構造が変化しない場合。

可読性を重視し、array[x, y]という直感的な記法を使いたい場合。

ジャグ配列

各行の長さが異なる場合や、メモリやパフォーマンス最適化が重要な大規模データを扱う場合。

配列の配列として扱えるため、LINQなどの機能を柔軟に適用したい場合にも適している。

LINQを用いたジャグ配列の操作

C#の強力な機能であるLINQ(Language Integrated Query)は、ジャグ配列に対しても非常に有効です。

例えば、ジャグ配列をフラット化して(1次元にして)全ての要素の合計を求める処理は、次のように記述できます。

C#
using System;
using System.Linq;

int[][] jaggedArray = new int[][]
{
    new int[] { 1, 2 },
    new int[] { 3, 4, 5 },
    new int[] { 6 }
};

// SelectManyを使用して平坦化し、合計を算出
int totalSum = jaggedArray.SelectMany(row => row).Sum();

// 各行の最大値を抽出
var maxPerRow = jaggedArray.Select(row => row.Max());

Console.WriteLine($"Total Sum: {totalSum}");
Console.WriteLine($"Max values: {string.Join(", ", maxPerRow)}");
実行結果
Total Sum: 21
Max values: 2, 5, 6

SelectMany を活用することで、ネストされた構造を簡単に1次元として扱えるのは、ジャグ配列が「列挙可能なオブジェクトの集合」であるからこそ可能な芸当です。

まとめ

ジャグ配列は、C#における配列の中でも自由度が高く、かつパフォーマンス面でも優れた特性を持っています。

「配列の配列」という構造を正しく理解し、多次元配列との性質の違いを把握することで、より効率的でメモリに優しいプログラムを記述できるようになります。

特に、データ量が不定なシステムや、パフォーマンスが要求されるアルゴリズムの実装において、ジャグ配列は欠かせない存在です。

まずは基本的な宣言と初期化から慣れていき、徐々にLINQなどと組み合わせた高度な操作に挑戦してみてください。

今回紹介した以下のポイントを忘れないようにしましょう。

  • ジャグ配列は行ごとに異なる長さを持てる。
  • .NET環境では、多次元配列よりも高速に動作する傾向がある。
  • 内側の配列を使用する前には必ずインスタンス化が必要である。

これらを意識するだけで、あなたのC#プログラミングの幅は大きく広がるはずです。