C#を用いたアプリケーション開発において、複数のデータを格子状や表形式で管理したい場面は頻繁に発生します。

例えば、エクセルのようなスプレッドシートのデータ処理、ゲームにおける2Dマップの座標管理、あるいは数学的な行列計算などがその代表例です。

C#にはこれらを実現するための手法として「2次元配列」が用意されています。

本記事では、C#における2次元配列の基礎から、宣言・初期化のバリエーション、ループによる走査方法、そして混同されやすい「ジャグ配列」との違いまで、エンジニアが実務で直面するポイントを網羅して詳しく解説します。

2次元配列とは

C#における2次元配列とは、同じ型の要素を「行(Row)」と「列(Column)」の2つのインデックスを用いて管理するデータ構造のことです。

メモリ上では連続した領域として確保され、すべての行が同じ数の列を持つ「矩形(くけい)配列」となります。

1次元配列がデータの「列」であるのに対し、2次元配列はデータの「面」を表すと考えると理解しやすいでしょう。

C#の多次元配列は、型名の後ろの角カッコ内にカンマを記述することで表現されます。

例えば、int[,] は整数の2次元配列を指します。

2次元配列の宣言と初期化

2次元配列を使用するには、まず変数を宣言し、必要なメモリ領域を確保(インスタンス化)する必要があります。

C#では、用途に応じていくつかの初期化方法が提供されています。

基本的な宣言とインスタンス化

最も標準的な方法は、配列のサイズを明示的に指定してインスタンスを作成する方法です。

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

// 宣言と代入を分ける場合
string[,] board;
board = new string[10, 10];

この場合、各要素は型の規定値(intなら 0、参照型なら null)で自動的に初期化されます。

初期化子を用いた宣言

宣言と同時に具体的な値を代入する場合は、波カッコ {} を使用した初期化子を利用するのが便利です。

C#
// 初期化子を使って3行2列の配列を作成
int[,] points = new int[3, 2] 
{
    { 10, 20 },
    { 30, 40 },
    { 50, 60 }
};

// 型推論(var)と省略記法を用いた場合
var table = new[,] 
{
    { "ID", "Name" },
    { "1", "Alice" },
    { "2", "Bob" }
};

初期化子を使用する場合、すべての行の要素数が一致していなければなりません。

もし行ごとに要素数が異なる場合は、後述するジャグ配列を使用する必要があります。

要素へのアクセスと書き換え

2次元配列の特定の要素にアクセスするには、array[行インデックス, 列インデックス] という形式を使用します。

値の取得と代入

C#の配列インデックスは常に 0 から始まることに注意してください。

C#
using System;

class Program
{
    static void Main()
    {
        int[,] data = new int[2, 2];

        // 値の代入
        data[0, 0] = 100;
        data[0, 1] = 200;
        data[1, 0] = 300;
        data[1, 1] = 400;

        // 値の取得
        int value = data[1, 0];
        Console.WriteLine($"1行0列目の値: {value}");
        
        // 書き換え
        data[0, 0] = 999;
        Console.WriteLine($"0行0列目の新しい値: {data[0, 0]}");
    }
}
実行結果
1行0列目の値: 300
0行0列目の新しい値: 999

もし指定したインデックスが配列の範囲外であった場合、実行時に IndexOutOfRangeException がスローされます。

動的にインデックスを指定する場合は、事前に配列のサイズを確認することが重要です。

ループ処理による走査

2次元配列の全要素を処理する場合、通常は for 文の二重ループ、もしくは foreach 文を使用します。

for文による二重ループ

for 文を使用する場合、配列の各次元の長さを取得するために GetLength() メソッドを使用します。

Length プロパティは全次元の合計要素数を返してしまうため、注意が必要です。

C#
using System;

class Program
{
    static void Main()
    {
        int[,] grid = {
            { 1, 2, 3 },
            { 4, 5, 6 }
        };

        // GetLength(0) は行数、GetLength(1) は列数を返す
        for (int i = 0; i < grid.GetLength(0); i++)
        {
            for (int j = 0; j < grid.GetLength(1); j++)
            {
                Console.Write($"{grid[i, j]} ");
            }
            Console.WriteLine(); // 行の終わりに改行
        }
    }
}
実行結果
1 2 3 
4 5 6

foreach文による走査

foreach 文を使用すると、2次元配列を「1次元的に」走査することができます。

行や列の区別が必要なく、単に全要素を処理したい場合に適しています。

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

foreach (int n in numbers)
{
    Console.WriteLine(n);
}

foreachは左上から右下に向かって順番に要素を取り出します。

ただし、現在の行番号や列番号を直接取得することはできないため、インデックスが必要な場合は for 文を選択しましょう。

2次元配列に関連する主なプロパティとメソッド

2次元配列を操作する上で欠かせない便利なプロパティとメソッドを整理します。

プロパティ / メソッド説明
Length配列に含まれる全要素の合計数を取得します(例:3×4なら12)。
Rank配列の次元数を取得します。2次元配列なら必ず 2 になります。
GetLength(dimension)指定した次元(0から開始)の要素数を取得します。
GetLowerBound(0)指定した次元の最小インデックス(通常は0)を取得します。
GetUpperBound(0)指定した次元の最大インデックスを取得します。

特に GetLength() は、多次元配列を安全にループ処理するために最も多用されるメソッドです。

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

C#には「2次元配列」の他に「配列の配列」と呼ばれるジャグ配列(Jagged Array)が存在します。

これらは似て非なるものであり、特性を理解して使い分ける必要があります。

構造の違い

  • 2次元配列 (Multidimensional Array): メモリ上の一つのブロックとして確保される矩形のデータ。すべての行の長さが同じです。
  • ジャグ配列 (Jagged Array): 「配列を要素に持つ配列」です。各行が独立した配列オブジェクトであるため、行ごとに長さが異なっても構いません。

構文の比較

項目2次元配列ジャグ配列
型の宣言int[,]int[][]
インスタンス化new int[3, 2]new int[3][](子配列は別途初期化)
アクセスa[i, j]a[i][j]

どちらを使うべきか

2次元配列が適しているケース:

  • データが完全な矩形(表形式)であることが保証されている場合。
  • 数学的行列の計算を行う場合。
  • 構文をシンプルに保ちたい場合。

ジャグ配列が適しているケース:

  • 各行のデータの長さがバラバラである場合(例:文章ごとの単語リスト)。
  • パフォーマンスが極めて重要な場合。 意外かもしれませんが、C#のJITコンパイラはジャグ配列の最適化に長けており、2次元配列よりも高速に動作することが多いです。
  • 配列の配列を引数に取る外部ライブラリを利用する場合。

パフォーマンスとメモリの最適化

大規模なデータを扱う場合、2次元配列のメモリレイアウトとアクセス速度の関係に注意が必要です。

2次元配列はメモリ上で連続した1つの領域に配置されます。

そのため、「行方向(右方向)」に連続してアクセスする方が、キャッシュ効率が高まり高速に動作します。

C#
// 推奨されるアクセス順序(行優先)
for (int i = 0; i < rows; i++) {
    for (int j = 0; j < cols; j++) {
        process(array[i, j]);
    }
}

逆に、列方向に飛び飛びでアクセスすると、CPUのキャッシュミスが発生しやすくなり、処理速度が低下する可能性があります。

また、C# 7.0以降で導入された Span<T>Memory<T> を活用することで、配列のコピーを避けつつ安全に高速なメモリ操作を行うことも可能ですが、多次元配列を直接 Span に変換するには少し工夫(1次元配列として扱う等)が必要です。

実践的な活用例:簡易的な在庫管理システム

最後に、2次元配列を用いた実践的なコード例を紹介します。

複数の店舗(行)における各商品(列)の在庫数を管理するシミュレーションです。

C#
using System;

class InventoryManager
{
    static void Main()
    {
        // 3つの店舗における、4種類の商品の在庫数
        // 行:店舗(新宿店、渋谷店、池袋店)
        // 列:商品(PC、マウス、キーボード、モニタ)
        int[,] inventory = {
            { 5, 12, 8, 3 },
            { 2, 0, 15, 6 },
            { 10, 5, 2, 7 }
        };

        string[] shops = { "新宿店", "渋谷店", "池袋店" };
        string[] products = { "PC", "マウス", "キーボード", "モニタ" };

        Console.WriteLine("--- 在庫一覧 ---");

        for (int i = 0; i < inventory.GetLength(0); i++)
        {
            Console.WriteLine($"【{shops[i]}】");
            for (int j = 0; j < inventory.GetLength(1); j++)
            {
                // 在庫が0の場合は警告表示
                string status = inventory[i, j] == 0 ? " (!!! 欠品中 !!!)" : "";
                Console.WriteLine($"  - {products[j]}: {inventory[i, j]}個{status}");
            }
        }

        // 特定商品の更新例:渋谷店のPCが入荷
        inventory[1, 0] += 5;
        Console.WriteLine("\nシステム更新:渋谷店にPCが5台入荷しました。");
        Console.WriteLine($"現在の渋谷店 PC在庫: {inventory[1, 0]}個");
    }
}
実行結果
--- 在庫一覧 ---
【新宿店】
  PC: 5個
  マウス: 12個
  キーボード: 8個
  モニタ: 3個
【渋谷店】
  PC: 2個
  マウス: 0個 (!!! 欠品中 !!!)
  キーボード: 15個
  モニタ: 6個
【池袋店】
  PC: 10個
  マウス: 5個
  キーボード: 2個
  モニタ: 7個

システム更新:渋谷店にPCが5台入荷しました。
現在の渋谷店 PC在庫: 7個

このように、2つの異なる軸(この例では店舗と商品)を持つデータを直感的に管理できるのが、2次元配列の最大のメリットです。

まとめ

C#の2次元配列は、格子状のデータを扱う際に非常に強力で、かつ直感的な操作が可能なデータ構造です。

  • 宣言と初期化int[行, 列] の形式で行い、初期化子による一括代入も可能。
  • 要素のアクセスarray[i, j] を使い、インデックスは必ず0から始まる。
  • ループ処理GetLength(dimension) を用いて各次元のサイズを取得し、二重ループで走査するのが基本。
  • ジャグ配列との使い分け:固定的な矩形データなら2次元配列、柔軟な行の長さや高い実行速度を求めるならジャグ配列を選択。

2次元配列をマスターすることで、より複雑なデータ構造をプログラム内でシンプルに表現できるようになります。

まずは基本的な操作に慣れ、用途に応じてジャグ配列や List<T> のネストなどと比較しながら、最適な選択ができるようになりましょう。