C#プログラミングにおいて、大量のデータを効率的に管理するための仕組みとして「配列」は欠かせない存在です。

その中でも、行列や空間的なデータを扱う際に威力を発揮するのが多次元配列です。

多次元配列を正しく理解し、適切に使いこなすことは、アルゴリズムの構築やデータ構造の設計において非常に重要です。

本記事では、多次元配列の基礎となる宣言や初期化から、実戦で役立つループ処理、さらには混同されやすいジャグ配列との違いまで、エンジニアが知っておくべき知識を体系的に解説します。

C#における多次元配列の基本概念

C#の配列には、大きく分けて「1次元配列」「多次元配列(矩形配列)」「ジャグ配列(配列の配列)」の3種類が存在します。

今回メインで扱う多次元配列は、行と列が揃った「矩形(くけい)」の形をしているのが最大の特徴です。

例えば、数学の行列のように、2行3列のデータを保持したい場合、多次元配列を使用すると直感的にデータを配置できます。

多次元配列はメモリ上で連続した領域として確保されるため、構造がシンプルであり、格子状のデータを扱うゲーム開発や画像処理、数値計算などで頻繁に利用されます。

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

C#で多次元配列を宣言するには、型名の後の角括弧 [] の中にカンマ , を記述します。

カンマの数によって、2次元、3次元といった次元数が決まります。

2次元配列の宣言と生成

2次元配列は、最も一般的に利用される多次元配列です。

以下のコードは、2行3列の整数型配列を宣言し、生成する例です。

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

// 値を代入する
matrix[0, 0] = 1;
matrix[0, 1] = 2;
matrix[0, 2] = 3;
matrix[1, 0] = 4;
matrix[1, 1] = 5;
matrix[1, 2] = 6;

このように、matrix[行, 列] という形式でアクセスします。

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

初期化子を用いた宣言

宣言と同時に値を代入する場合は、波括弧 {} を使用した初期化子を用いるのが便利です。

C#
// 初期化子を使用した宣言
int[,] matrix = new int[,]
{
    { 10, 20, 30 },
    { 40, 50, 60 }
};

// 型推論(var)を使用する場合
var matrix2 = new int[,]
{
    { 1, 2 },
    { 3, 4 },
    { 5, 6 }
};

初期化子を使う場合、すべての行の要素数が一致していなければならないという制約があります。

これが後述するジャグ配列との大きな違いです。

3次元以上の多次元配列

3次元配列以上も同様のルールで作成可能です。

例えば、3次元空間の座標データを扱う場合は以下のように記述します。

C#
// 2x3x4の3次元配列
int[,,] cube = new int[2, 3, 4];

// 初期化の例
int[,,] cubeInit = new int[,,]
{
    {
        { 1, 2 }, { 3, 4 }
    },
    {
        { 5, 6 }, { 7, 8 }
    }
};

次元が増えるほどコードの可読性が低下するため、実務では3次元程度に留めるか、クラスや構造体を用いてデータ構造を整理することが推奨されます。

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

多次元配列の特定の要素にアクセスするには、角括弧の中に各次元のインデックスを指定します。

値の取得と更新

以下のプログラムは、配列の要素を書き換え、コンソールに出力する基本的な操作を示しています。

C#
using System;

class Program
{
    static void Main()
    {
        // 2x2の配列を作成
        string[,] map = new string[2, 2]
        {
            { "A1", "A2" },
            { "B1", "B2" }
        };

        // 要素の取得
        string val = map[1, 0];
        Console.WriteLine($"取得した値: {val}");

        // 要素の更新
        map[1, 0] = "Updated";
        Console.WriteLine($"更新後の値: {map[1, 0]}");
    }
}
実行結果
取得した値: B1
更新後の値: Updated

配列のサイズを取得するプロパティ

多次元配列を扱う際、配列全体の要素数だけでなく、特定の次元の長さを取得したい場面が多くあります。

プロパティ・メソッド説明
Length配列に含まれる全要素の合計数を返します。
Rank配列の次元数を返します。
GetLength(int dimension)指定した次元(0から開始)の要素数を返します。

例えば、2行3列の配列において Length6 を返しますが、1次元目(行)の長さを知りたい場合は GetLength(0) を使用して 2 を取得します。

多次元配列のループ処理

多次元配列の全要素を処理する場合、for 文または foreach 文を使用します。

用途に応じてこれらを使い分ける必要があります。

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

特定の行や列のインデックスを利用したい場合は、for 文を入れ子(ネスト)にして記述します。

この際、GetLengthメソッドを使用して各次元の境界を取得するのがベストプラクティスです。

C#
using System;

class Program
{
    static void Main()
    {
        int[,] data = {
            { 10, 20, 30 },
            { 40, 50, 60 }
        };

        // GetLength(0)は行数、GetLength(1)は列数を取得
        for (int i = 0; i < data.GetLength(0); i++)
        {
            for (int j = 0; j < data.GetLength(1); j++)
            {
                Console.Write($"[{i},{j}] = {data[i, j]} ");
            }
            Console.WriteLine(); // 行の終わりで改行
        }
    }
}
実行結果
[0,0] = 10 [0,1] = 20 [0,2] = 30 
[1,0] = 40 [1,1] = 50 [1,2] = 60

foreach文による全要素の走査

インデックス情報が不要で、単に全要素に対して何らかの処理を行いたい場合は、foreach 文が最も簡潔です。

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

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

注意点として、多次元配列に対して foreach を使用すると、次元を無視してフラットに(1次元配列のように)要素が列挙されます。

順序は最初の次元から順に処理されます(2次元なら 0,0 → 0,1 → 1,0 → 1,1 の順)。

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

C#には、多次元配列と非常によく似た「ジャグ配列(Jagged Array)」があります。

これらは混同されやすいですが、メモリ構造や柔軟性の面で大きな違いがあります。

ジャグ配列とは

ジャグ配列は「配列を要素として持つ配列」です。

各行(要素である配列)の長さが異なっても良いため、「ギザギザ(Jagged)な配列」と呼ばれます。

C#
// ジャグ配列の宣言([]が2つ並ぶ)
int[][] jagged = new int[3][];

// 各行を個別に生成
jagged[0] = new int[] { 1, 2 };
jagged[1] = new int[] { 3, 4, 5, 6 };
jagged[2] = new int[] { 7 };

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

特徴多次元配列 int[,]ジャグ配列 int[][]
形状常に矩形(すべての行が同じ長さ)行ごとに長さが異なっても良い
メモリ配置連続した単一のメモリブロック複数のオブジェクト(配列)の集合
アクセス速度計算式によるアクセス(わずかに遅い場合がある)配列参照を辿る(CLRの最適化により高速な傾向)
宣言方法new int[2, 3]new int[2][] その後、各行を生成

どちらを使うべきか

  • 多次元配列を選ぶべき場面
    行列データや3D座標など、データが固定的な矩形構造を持っており、数学的な直感性を重視する場合。
  • ジャグ配列を選ぶべき場面
    各グループのデータ数がバラバラな場合や、.NET内部での実行速度(JIT最適化)を最大限に引き出したい場合。

多次元配列の実践的な活用例

ここでは、多次元配列を用いた具体的なプログラム例として、2次元の座標グリッド上での距離計算を紹介します。

C#
using System;

class GridSystem
{
    static void Main()
    {
        // 3x3のマップ(標高データなど)
        double[,] heightMap = {
            { 10.5, 12.0, 15.2 },
            { 9.8,  11.5, 14.0 },
            { 8.5,  10.0, 13.5 }
        };

        // 指定した2点間の標高差を計算する関数
        PrintElevationDifference(heightMap, 0, 0, 2, 2);
    }

    static void PrintElevationDifference(double[,] map, int r1, int c1, int r2, int c2)
    {
        double diff = Math.Abs(map[r1, c1] - map[r2, c2]);
        Console.WriteLine($"点({r1},{c1})と点({r2},{c2})の標高差は {diff} です。");
    }
}
実行結果
点(0,0)と点(2,2)の標高差は 3 です。

このように、多次元配列を引数としてメソッドに渡す際は、double[,] map のように型を明示します。

配列は参照型であるため、メソッド内での変更は呼び出し元の配列にも影響します。

パフォーマンスとメモリに関する高度な考察

多次元配列はメモリ効率が良い一方で、アクセスパフォーマンスにおいてジャグ配列に一歩譲ることがあります。

その理由は、.NETランタイム(CLR)の最適化にあります。

配列アクセスのオーバーヘッド

C#の多次元配列 int[i, j] へのアクセス時、内部的にはインデックスの計算(index = i * 列数 + j)と境界チェックが行われます。

一方、ジャグ配列 int[i][j] は「配列の配列」であるため、CLRが各配列の境界チェックをより効率的に最適化(ILレベルでの最適化)しやすい傾向にあります。

しかし、現代のプロセッサにおいては、多次元配列のような「空間的な局所性(メモリが連続していること)」がキャッシュ効率を高めることもあります。

極端なパフォーマンスが要求される数値シミュレーションなどでない限り、コードの読みやすさを優先して選択するのが良いでしょう。

Span<T> との組み合わせ

最新のC#では、Span<T>Memory<T> を使用して、多次元データをより安全かつ高速に扱う手法も増えています。

多次元配列そのものを Span にキャストすることは直接できませんが、1次元配列を多次元的に解釈することで、パフォーマンスを劇的に向上させることが可能です。

多次元配列を使用する際の注意点

インデックスの範囲外(IndexOutOfRangeException)

多次元配列で最も多いエラーは、存在しないインデックスへのアクセスです。

特にループ処理で GetLength を使わずに数値を直接指定(マジックナンバー)すると、配列のサイズ変更時にバグの原因となります。

C#
int[,] data = new int[3, 5];
// 誤り: 列の最大インデックスは4
// data[0, 5] = 100; // 実行時に例外が発生

多次元配列のコピー

Array.Copy メソッドを使用して多次元配列をコピーする場合、コピー先も同じ形状(次元数と各次元の長さ)である必要があります。

C#
int[,] source = { { 1, 2 }, { 3, 4 } };
int[,] target = new int[2, 2];

Array.Copy(source, target, source.Length);

この操作は「浅いコピー(Shallow Copy)」です。

配列の要素が参照型(クラスなど)の場合、中身のインスタンスまでは複製されないため注意してください。

まとめ

C#の多次元配列は、規則正しいグリッド状のデータを管理するのに最適なデータ構造です。

本記事で解説した以下のポイントを理解することで、より堅牢なプログラムが記述できるようになります。

  • 宣言と初期化[,] 記法を用い、すべての行が同じ長さを持つ矩形構造を作る。
  • ループ処理GetLength(dimension) を活用して、安全に行列を走査する。
  • ジャグ配列との使い分け:形状の柔軟性が必要ならジャグ配列、構造の定型性とメモリの連続性を重視するなら多次元配列を選択する。
  • 境界チェックの遵守:インデックスの管理を徹底し、例外を防止する。

多次元配列は、リスト(List<T>)などの動的コレクションに比べてサイズ変更が難しいという側面もありますが、その分構造が明確でメモリ効率に優れています。

用途に合わせて適切な配列を選択し、効率的なデータ処理を実現しましょう。