C#における配列は、同一のデータ型を持つ複数の値を効率的に管理するための基本的なデータ構造です。

その中でも「3次元配列」は、平面的な広がりを持つ2次元配列に「奥行き」や「層」の概念を加えたものであり、立体的な空間データや時系列を含むグリッドデータなどを扱う際に非常に強力なツールとなります。

実務においては、3DCGのボクセルデータ、多角的な統計データ、あるいは複雑なシミュレーションの計算グリッドなどで多用されます。

3次元配列を使いこなすためには、単なる構文の理解だけでなく、メモリ空間上の配置や、多重ループによる効率的なアクセス手法、さらには「ジャグ配列(配列の配列)」との違いを明確に理解しておくことが不可欠です。

本記事では、C#における3次元配列の宣言、初期化、要素へのアクセス方法から、実践的な操作方法やパフォーマンス上の注意点まで、初心者から中級者の方まで役立つ情報を網羅的に解説します。

3次元配列の基礎知識

C#の配列には、大きく分けて「多次元配列(Rectangular Arrays)」と「ジャグ配列(Jagged Arrays)」の2種類が存在します。

3次元配列は一般的に多次元配列の一種として扱われ、すべての次元において要素数が揃っている立方体や直方体のような構造を持ちます。

3次元配列の構造をイメージする際は、「高さ(層)」「行」「列」の3つの軸を考えると分かりやすくなります。

例えば、[2, 3, 4] というサイズの配列は、「3行4列のシートが2枚重なっている状態」を指します。

3次元配列の宣言とインスタンス化

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

宣言と同時にインスタンス化を行う場合は、new キーワードを使用して各次元の要素数を指定します。

C#
// 3次元配列の宣言
int[,,] cube;

// インスタンス化(層:2, 行:3, 列:4)
cube = new int[2, 3, 4];

// 宣言とインスタンス化を同時に行う
double[,,] space = new double[10, 10, 10];

上記の例では、int[2, 3, 4] という配列を作成しました。

これは合計で 2 × 3 × 4 = 24個 の要素を保持できるメモリ領域を確保したことになります。

3次元配列の初期化方法

配列の作成時にあらかじめ値を代入しておく「初期化」には、いくつかの書き方があります。

要素数が多い3次元配列では、初期化子の記述が複雑になりやすいため、構造を正しく把握することが重要です。

明示的な初期化子を使用する方法

中括弧 {} を入れ子にすることで、3次元の構造を表現します。

外側から順に「第1次元(層)」「第2次元(行)」「第3次元(列)」に対応します。

C#
// 3次元配列の初期化
int[,,] array3D = new int[2, 2, 3] 
{
    {
        { 1, 2, 3 }, // 0層目の0行目
        { 4, 5, 6 }  // 0層目の1行目
    },
    {
        { 7, 8, 9 }, // 1層目の0行目
        { 10, 11, 12 } // 1層目の1行目
    }
};

型推論(var)を用いた簡略化

C#では var キーワードを使用することで、右辺の型から変数の型を推論させることができます。

ただし、多次元配列の場合は右辺で型を明示する必要があります。

C#
// varを使用した初期化
var data = new int[,,] {
    { { 1, 1 }, { 2, 2 } },
    { { 3, 3 }, { 4, 4 } }
};

要素へのアクセスと代入

3次元配列の特定の要素にアクセスするには、角括弧の中に3つのインデックスをカンマ区切りで指定します。

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

値の読み取りと書き込み

C#
using System;

class Program
{
    static void Main()
    {
        // 2x2x2の配列を作成
        int[,,] map = new int[2, 2, 2];

        // 値の代入 (層, 行, 列)
        map[0, 1, 0] = 500;
        map[1, 0, 1] = 999;

        // 値の参照
        int val1 = map[0, 1, 0];
        int val2 = map[1, 0, 1];

        Console.WriteLine($"map[0, 1, 0] の値: {val1}");
        Console.WriteLine($"map[1, 0, 1] の値: {val2}");
    }
}
実行結果
map[0, 1, 0] の値: 500
map[1, 0, 1] の値: 999

多重ループによる全要素の操作

3次元配列のすべての要素を処理する場合、通常は3重の for 文を使用します。

この際、配列の各次元の長さを取得するために GetLength メソッドを使用するのが一般的です。

GetLengthメソッドの活用

Array.Length プロパティは配列内の全要素の合計数を返しますが、各次元ごとの要素数を取得するには GetLength(次元インデックス) を使用します。

引数は0から始まり、第1次元なら0、第2次元なら1を指定します。

C#
using System;

class Program
{
    static void Main()
    {
        // 3次元配列の定義
        int[,,] matrix = new int[2, 3, 4];

        // 各次元の長さを取得
        int layers = matrix.GetLength(0);  // 2
        int rows = matrix.GetLength(1);    // 3
        int columns = matrix.GetLength(2); // 4

        int counter = 1;

        // 3重ループで値を代入
        for (int i = 0; i < layers; i++)
        {
            for (int j = 0; j < rows; j++)
            {
                for (int k = 0; k < columns; k++)
                {
                    matrix[i, j, k] = counter++;
                }
            }
        }

        // 3重ループで値を表示
        for (int i = 0; i < layers; i++)
        {
            Console.WriteLine($"--- Layer {i} ---");
            for (int j = 0; j < rows; j++)
            {
                for (int k = 0; k < columns; k++)
                {
                    Console.Write($"{matrix[i, j, k]:D2} ");
                }
                Console.WriteLine();
            }
        }
    }
}
実行結果
--- Layer 0 ---
01 02 03 04 
05 06 07 08 
09 10 11 12 
--- Layer 1 ---
13 14 15 16 
17 18 19 20 
21 22 23 24

foreach文による列挙

要素を読み取るだけであれば、foreach 文を使用することも可能です。

foreach は次元に関係なく、メモリ上に配置されている順番(最後の次元が最も速く変化する順)にすべての要素を走査します。

C#
foreach (int val in matrix)
{
    // 全要素を順番に処理(読み取り専用)
    Console.Write(val + " ");
}

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

3次元データを扱う方法は、int[,,] のような多次元配列だけではありません。

int[][][] のように記述する「ジャグ配列(配列の配列)」も存在します。

これらには明確な違いがあるため、用途に応じて使い分ける必要があります。

多次元配列(Rectangular Array)の特徴

  • 構文:int[,,] array
  • メモリ構造:単一のメモリブロックに連続して配置される。
  • 形状:常に「直方体」であり、各層の各行の長さはすべて等しい。
  • パフォーマンス:インデックス計算に若干のオーバーヘッドがあるが、メモリの局所性は高い。

ジャグ配列(Jagged Array)の特徴

  • 構文:int[][][] array
  • メモリ構造:配列の要素がさらに配列への参照となっている。
  • 形状:各次元の要素数が異なっても良い(「デコボコ」な配列が可能)。
  • パフォーマンス:CLR(共通言語ランタイム)によって最適化されやすく、要素へのアクセス速度は多次元配列より高速な場合が多い。

どちらを選ぶべきか

明確な「立体構造」を管理し、すべてのデータが揃っている場合は 多次元配列 が適しています。

一方で、データ量が非常に多く、層や行によってデータ数が異なる場合(疎なデータなど)は、メモリ効率の観点から ジャグ配列 を検討すべきです。

実践例:3次元空間の温度シミュレーション

より具体的な活用シーンとして、3次元空間の各地点における温度データを管理するプログラムを考えてみましょう。

例えば、部屋の空間をグリッド状に分割し、各地点(X, Y, Z座標)の温度を保持・計算する場合です。

C#
using System;

class TemperatureMap
{
    static void Main()
    {
        // 空間の定義 (横5m, 縦5m, 高さ3mを1m刻みで管理)
        double[,,] roomTemp = new double[5, 5, 3];

        // 初期温度の設定 (簡易的な代入)
        for (int x = 0; x < roomTemp.GetLength(0); x++)
        {
            for (int y = 0; y < roomTemp.GetLength(1); y++)
            {
                for (int z = 0; z < roomTemp.GetLength(2); z++)
                {
                    // 高くなるほど温度が上がるシミュレーション値
                    roomTemp[x, y, z] = 20.0 + z * 2.5;
                }
            }
        }

        // 特定の地点 (2, 2, 2) の温度を確認
        double targetTemp = roomTemp[2, 2, 2];
        Console.WriteLine($"座標 (2, 2, 2) の温度: {targetTemp}度");

        // 全体の平均温度を算出
        double total = 0;
        foreach (double t in roomTemp)
        {
            total += t;
        }
        double average = total / roomTemp.Length;
        Console.WriteLine($"部屋全体の平均温度: {average:F2}度");
    }
}
実行結果
座標 (2, 2, 2) の温度: 25度
部屋全体の平均温度: 22.50度

パフォーマンス向上のためのヒント

大規模な3次元配列を扱う際、パフォーマンスがボトルネックになることがあります。

以下の点に注意することで、より効率的なプログラムを記述できます。

1. ループの順序とキャッシュ効率

C#の多次元配列は、メモリ上では「最後の次元(列)」が連続するように配置されます。

そのため、ループを回す際は、もっとも内側のループが最後の次元(GetLengthの最大インデックス)にアクセスするように構成するのが最もキャッシュ効率が良く、高速です。

C#
// 効率的なアクセス順序
for (int i = 0; i < dim0; i++)
    for (int j = 0; j < dim1; j++)
        for (int k = 0; k < dim2; k++)
            process(array[i, j, k]); // k(最後の次元)を一番内側にする

2. 配列の境界チェックのオーバーヘッド

C#は配列アクセスごとに境界チェック(IndexOutOfRangeExceptionの確認)を行います。

多次元配列の場合、このチェックが複雑になることがあります。

極限のパフォーマンスが求められる画像処理や数値計算では、多次元配列を 1次元配列 として確保し、インデックス計算(index = i * (width * height) + j * width + k)を自前で行う手法も一般的です。

3. Span<T> の検討

C# 7.2以降で導入された Span<T> を利用すると、配列の一部を効率的に切り出したり、ポインタに近い速度で安全にアクセスしたりすることが可能です。

ただし、多次元配列から直接 Span を作成することは難しいため、やはり1次元配列として管理するか、ジャグ配列の一部を切り出す形になります。

3次元配列でよくあるエラーと対策

3次元配列の操作中に発生しやすいトラブルとその回避策をまとめます。

IndexOutOfRangeException

原因:指定したインデックスが 0 未満、または配列の長さ(`GetLength`)以上の値になっている。

対策:ループの終了条件を i < array.GetLength(n) と厳密に指定する。

OutOfMemoryException

原因:3次元配列は次元が増えるごとに要素数が爆発的に増加するため、メモリ不足に陥りやすい。

対策:int[500, 500, 500] は 125,000,000 要素となり、int 型(4バイト)なら約500MBを消費します。

必要な解像度を見極め、巨大すぎる配列は避けるか、List などの動的構造や別のデータ構造を検討してください。

次元の取り違え

原因:GetLength(0)GetLength(2) を逆に使ってしまうなど。

対策:変数名に layerCount, rowCount, colCount のような明確な名前を付け、マジックナンバーの使用を避けて意図を明確にする。

まとめ

C#の3次元配列は、複雑な立体構造や多角的なデータを管理するための非常に強力なデータ形式です。

基本的な「宣言・初期化・アクセス」のルールを正しく理解し、GetLength メソッドを用いた適切なループ処理を行うことで、バグの少ない堅牢なコードを記述できます。

本記事で解説した以下のポイントを振り返りましょう。

  • 3次元配列は 型名[,,] で宣言し、直方体のような均一な構造を持つ。
  • 初期化は入れ子の {} を使い、インデックスは常に0から始まる。
  • 全要素の走査には3重の for 文、またはシンプルな foreach 文を使う。
  • パフォーマンスを意識する場合、ループの順序(内側を最後の次元にする)に注意する。
  • 形状が不揃いなデータには「ジャグ配列」を検討する。

3次元配列をマスターすることで、空間シミュレーション、ゲームのマップデータ管理、科学技術計算など、プログラミングの幅は大きく広がります。

まずは小さなサイズから実際にコードを書き、メモリ構造とアクセスの感覚を掴んでみてください。