C#を用いた開発において、データの集合を扱う際に最も頻繁に利用されるのが配列です。

その中でも、縦と横の広がりを持つ2次元配列は、表形式のデータ管理やゲームのマップデータ、画像処理など、幅広いシーンで活用されます。

しかし、C#の2次元配列には「多次元配列」と「ジャグ配列」という2つの異なる形式が存在し、それぞれの宣言方法やメモリ構造には大きな違いがあります。

この記事では、C#における2次元配列の宣言から初期化、具体的な使い方、そして2つの形式の使い分けまで、プロフェッショナルの視点で詳しく解説します。

C#における2次元配列の基礎知識

C#で2次元以上のデータを扱う場合、大きく分けて2つの手法があります。

一つは、すべての行の要素数が等しい「多次元配列 (矩形配列)」であり、もう一つは、配列の中に配列を格納する「ジャグ配列 (配列の配列)」です。

一般的に「2次元配列」と呼ぶ場合、文脈によってこれら両方を指すことがありますが、文法や動作が明確に異なるため、それぞれの特性を正しく理解することが重要です。

まずは、より直感的で表形式に近い「多次元配列」の宣言方法から見ていきましょう。

多次元配列 (矩形配列) の宣言と初期化

多次元配列は、各次元の要素数が固定された長方形 (矩形) 状の構造を持ちます。

C#では、型名の後のブラケット内にカンマ , を記述することで宣言します。

基本的な宣言とインスタンス作成

多次元配列を宣言する際は、行数と列数を指定してインスタンスを生成します。

C#
// 3行4列の2次元配列を宣言
int[,] matrix = new int[3, 4];

このコードでは、3行4列の合計12個の要素を持つ配列がメモリ上に確保されます。

初期値を指定しない場合、数値型であれば 0、参照型であれば null で初期化されます。

宣言と同時に初期化する

配列の宣言と同時に具体的な値を代入することも可能です。

この場合、要素数からサイズが自動的に推論されるため、サイズの指定を省略できます。

C#
// 宣言と同時に値を代入(サイズ指定を省略可能)
int[,] data = new int[,]
{
    { 10, 20, 30 },
    { 40, 50, 60 }
};

// さらに簡略化した書き方
int[,] simpleData = 
{
    { 1, 2, 3 },
    { 4, 5, 6 }
};

多次元配列では、すべての行が同じ列数を持たなければならないという制約があります。

例えば、1行目が3列なのに2行目が2列しかないといった定義は、コンパイルエラーとなります。

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

ジャグ配列は、その名の通り「ギザギザ (Jagged)」な配列を許容する構造です。

これは「配列を要素として持つ配列」であり、各行ごとに異なる列数を持たせることができます。

ジャグ配列の宣言

ジャグ配列は、ブラケットを重ねて [][] と記述します。

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

注意点として、最初の宣言時には「行の数」のみを指定します。

各行の実体となる配列は、個別に生成する必要があります。

各要素の初期化

ジャグ配列の各要素 (行) に対して、個別に配列を割り当てます。

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

// 値を代入しながらの初期化も可能
jaggedArray[0] = new int[] { 1, 2, 3 };

このように、行ごとに長さが異なる柔軟なデータ構造を構築できるのがジャグ配列の最大の特徴です。

多次元配列とジャグ配列の比較

どちらの形式を使用すべきか迷った際のために、主要な違いを表にまとめました。

比較項目多次元配列 (matrix[,])ジャグ配列 (array[][])
構造矩形 (すべての行が同長)自由 (行ごとに長さが異なる)
メモリ配置連続した単一のメモリブロック配列の参照が分散して配置される
アクセス速度インデックス計算により若干低速な場合があるCLRによる最適化が効きやすく高速な傾向
宣言の簡潔さ簡潔に記述できる各行の初期化が必要でやや複雑
主な用途行列演算、座標系、固定表データのばらつきがあるリストの集合

パフォーマンスが極めて重要なループ処理においては、C#のJITコンパイラがジャグ配列の境界チェック最適化を行いやすいため、ジャグ配列の方が実行速度で有利になるケースが多いという特性があります。

2次元配列の要素へのアクセスと操作

宣言した配列のデータを取り出したり、書き換えたりする方法を解説します。

要素の読み書き

多次元配列とジャグ配列では、インデックスの指定方法が異なります。

C#
// 多次元配列へのアクセス
int[,] matrix = new int[2, 2];
matrix[0, 1] = 100; // 0行1列目に代入
int val1 = matrix[0, 1];

// ジャグ配列へのアクセス
int[][] jagged = new int[2][];
jagged[0] = new int[2];
jagged[0][1] = 200; // 0行目の配列の1番目の要素に代入
int val2 = jagged[0][1];

多次元配列は [i, j]、ジャグ配列は [i][j] と記述します。

この違いは、ジャグ配列が「配列の配列」であるという構造を反映しています。

配列のサイズを取得する

ループ処理などで配列の要素数を知りたい場合、Length プロパティや GetLength メソッドを使用します。

多次元配列の場合

Length を使用すると、全次元の合計要素数が返されます。

特定の次元の要素数を取得するには GetLength(次元インデックス) を使用します。

C#
int[,] matrix = new int[3, 5];
Console.WriteLine(matrix.Length);       // 出力: 15 (3 * 5)
Console.WriteLine(matrix.GetLength(0)); // 出力: 3 (行数)
Console.WriteLine(matrix.GetLength(1)); // 出力: 5 (列数)

ジャグ配列の場合

ジャグ配列の Length は、親となる配列の要素数 (行数) を返します。

各行の要素数は、その要素自体にアクセスして Length を取得します。

C#
int[][] jagged = new int[3][];
jagged[0] = new int[10];
Console.WriteLine(jagged.Length);    // 出力: 3 (行数)
Console.WriteLine(jagged[0].Length); // 出力: 10 (0行目の列数)

繰り返し処理による全要素の走査

2次元配列を扱う上で最も一般的な操作が、ループによる全走査です。

for文による入れ子のループ

インデックスを直接制御したい場合は、for 文をネスト (入れ子) にして使用します。

C#
// 多次元配列の走査
int[,] matrix = { { 1, 2 }, { 3, 4 }, { 5, 6 } };

for (int i = 0; i < matrix.GetLength(0); i++)
{
    for (int j = 0; j < matrix.GetLength(1); j++)
    {
        Console.Write(matrix[i, j] + " ");
    }
    Console.WriteLine();
}
実行結果
// 出力結果
1 2 
3 4 
5 6

foreach文による走査

値を読み取るだけであればforeach 文が非常に便利です。

ただし、多次元配列において foreach を使用すると、2次元の構造が無視され、全要素が1次元のように順次取り出されます。

C#
int[,] matrix = { { 1, 2 }, { 3, 4 } };

foreach (int val in matrix)
{
    Console.WriteLine(val);
}

一方、ジャグ配列で foreach を使用する場合、外側のループでは「行 (配列)」が取り出されるため、さらに内側でループを回す必要があります。

実践的なサンプルコード:成績管理システム

2次元配列の活用例として、3人の生徒の4科目のテスト点数を管理し、それぞれの平均点を算出するプログラムを紹介します。

C#
using System;

class Program
{
    static void Main()
    {
        // 生徒3人、科目4つの成績データを多次元配列で定義
        // 行:生徒(Aさん、Bさん、Cさん)
        // 列:科目(国語、数学、英語、理科)
        int[,] scores = {
            { 80, 75, 90, 85 }, // Aさんの点数
            { 60, 50, 70, 55 }, // Bさんの点数
            { 95, 100, 85, 90 } // Cさんの点数
        };

        string[] students = { "Aさん", "Bさん", "Cさん" };

        Console.WriteLine("--- 成績一覧 ---");

        for (int i = 0; i < scores.GetLength(0); i++)
        {
            int sum = 0;
            Console.Write($"{students[i]}: ");

            for (int j = 0; j < scores.GetLength(1); j++)
            {
                int currentScore = scores[i, j];
                sum += currentScore;
                Console.Write($"{currentScore, 4}");
            }

            double average = (double)sum / scores.GetLength(1);
            Console.WriteLine($" | 平均点: {average:F1}");
        }
    }
}
実行結果
--- 成績一覧 ---
Aさん:   80  75  90  85 | 平均点: 82.5
Bさん:   60  50  70  55 | 平均点: 58.8
Cさん:   95 100  85  90 | 平均点: 92.5

このサンプルでは、scores.GetLength(0) で生徒数を、scores.GetLength(1) で科目数を取得し、動的にループ回数を制御しています。

これにより、科目の数や生徒の数が増減しても、コードの主要なロジックを修正することなく対応可能です。

2次元配列を扱う際の注意点とTips

1. インデックス外アクセス (IndexOutOfRangeException)

配列を扱う上で最も多いエラーが、確保したサイズ以上のインデックスにアクセスしようとすることです。

特に2次元配列では、行と列のインデックスを逆に指定してしまうミスが散見されます。

ループの条件式には常に GetLengthLength を使用し、数値を直接入力 (マジックナンバー) しないように心がけましょう。

2. メモリ効率と初期化

巨大な2次元配列を宣言すると、連続したメモリ領域が必要になります。

特に多次元配列 int[10000, 10000] のようなケースでは、一度に大量のメモリを消費するため、OutOfMemoryException のリスクがあります。

疎なデータ (ほとんどの要素が0や空のデータ) を扱う場合は、配列ではなく Dictionary<(int, int), T> などの利用も検討すべきです。

3. 多次元配列のメソッド渡し

メソッドに2次元配列を渡す際は、型情報を正確に記述する必要があります。

C#
// 多次元配列を受け取るメソッド
static void PrintMatrix(int[,] matrix) { /* 処理 */ }

// ジャグ配列を受け取るメソッド
static void PrintJagged(int[][] jagged) { /* 処理 */ }

これらは型として互換性がないため、多次元配列を期待しているメソッドにジャグ配列を渡すことはできません。

プロジェクト全体でどちらの形式を採用するか、設計段階で統一しておくことが望ましいです。

まとめ

C#における2次元配列は、データの構造や用途に応じて「多次元配列」「ジャグ配列」を使い分けることが重要です。

  • 多次元配列 (矩形配列):全ての行が同じ長さで、数学的な行列や固定のグリッドデータに最適。
  • ジャグ配列 (配列の配列):行ごとに長さが異なる柔軟な構造が可能で、パフォーマンス面でも有利な場合が多い。

宣言方法やインデックスの指定法 ([,][][] か) には明確な違いがあるため、本記事で紹介した基本構文をしっかりと定着させましょう。

配列の操作に慣れることは、より複雑なデータ構造やアルゴリズムを理解するための第一歩となります。

データの性質を見極め、最適な配列形式を選択して、効率的なコーディングを目指してください。