C#で行列を扱う方法|2次元配列の作成・初期化・計算処理を初心者向けに解説

はじめに

C#で行列を扱う場合、基本的には2次元配列を使用します。2次元配列を利用すれば、縦方向の「行」と横方向の「列」を持つデータを表形式で管理できます。

行列は、次のような処理で活用されます。

  • 数値計算や統計処理

  • 画像データの管理

  • ゲームのマップ作成

  • 座標変換

  • 機械学習

  • 3Dグラフィックス

本記事では、C#で行列を作成・初期化する方法から、要素の取得、加算、乗算、転置などの計算処理までを初心者向けに解説します。2次元配列とジャグ配列の違いや、行列計算ライブラリの使い方についても紹介します。

1. C#における行列とは

1-1. 数学における行列の基本

行列とは、数値やデータを行と列に並べたものです。例えば、次のデータは2行3列の行列です。

1  2  3
4 5 6

横方向を「行」、縦方向を「列」と呼びます。この行列には2つの行と3つの列があるため、サイズは「2行3列」です。

行列の各データは「要素」と呼ばれ、行番号と列番号を使って位置を指定します。

数学では行番号と列番号を1から数えることが一般的ですが、C#の配列インデックスは0から始まります。そのため、左上の要素は[0, 0]です。

1-2. C#では行列を2次元配列で表現する

C#には、任意のサイズの数学的な行列を表す組み込みの汎用型はありません。基本的な行列は、次のような2次元配列で表現します。

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

この配列は2行3列です。

各要素には、次のようにアクセスします。

C#
Console.WriteLine(matrix[0, 0]); // 1
Console.WriteLine(matrix[1, 2]); // 6

matrix[行番号, 列番号]の順番で指定する点が重要です。

1-3. 2次元配列とジャグ配列の違い

C#で表形式のデータを扱う方法には、主に2次元配列とジャグ配列があります。

2次元配列は、すべての行が同じ列数を持つ長方形のデータです。

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

ジャグ配列は「配列の配列」であり、行ごとに異なる列数を持てます。

C#
int[][] jagged =
{
new int[] { 1, 2 },
new int[] { 3, 4, 5 },
new int[] { 6 }
};

要素の指定方法も異なります。

C#
int value1 = matrix[0, 1];
int value2 = jagged[0][1];

一般的な行列計算では、形状が一定の2次元配列が分かりやすいでしょう。一方、行ごとにデータ数が異なる場合はジャグ配列が適しています。

1-4. 行列を使う主な場面

C#の行列は、さまざまな場面で使用されます。

例えば、ゲームのマップは行と列を持つデータとして表現できます。

C#
int[,] map =
{
{ 0, 0, 1, 0 },
{ 0, 1, 1, 0 },
{ 0, 0, 0, 0 }
};

0を通行可能、1を障害物として扱えば、マップ情報を簡潔に管理できます。

画像処理では、各ピクセルの明るさや色情報を行列として扱えます。また、3Dグラフィックスでは、拡大、回転、移動などの座標変換に行列が使われます。

2. C#で行列を作成・初期化する方法

2-1. 行数と列数を指定して2次元配列を作成する

行数と列数を指定して2次元配列を作成する基本構文は、次のとおりです。

C#
データ型[,] 配列名 = new データ型[行数, 列数];

3行4列の整数型行列は、次のように作成します。

C#
int[,] matrix = new int[3, 4];

int型の配列を作成した場合、すべての要素は初期値の0になります。

C#
Console.WriteLine(matrix[0, 0]); // 0
Console.WriteLine(matrix[2, 3]); // 0

double型も初期値は0.0bool型はfalse、参照型はnullです。

2-2. 初期値を指定して行列を作成する

作成時に各要素の値を指定する場合は、配列初期化子を使います。

C#
int[,] matrix = new int[,]
{
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 }
};

型とnewを省略した次の書き方も可能です。

C#
int[,] matrix =
{
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 }
};

各行の要素数は同じでなければなりません。例えば、次の初期化はコンパイルエラーになります。

C#
// 各行の列数が異なるためエラー
int[,] matrix =
{
{ 1, 2 },
{ 3, 4, 5 }
};

行ごとに列数を変えたい場合は、ジャグ配列を使用します。

2-3. すべての要素を同じ値で初期化する

2次元配列のすべての要素を同じ値にする場合は、二重for文を使います。

C#
int[,] matrix = new int[3, 4];

for (int row = 0; row < matrix.GetLength(0); row++)
{
for (int column = 0; column < matrix.GetLength(1); column++)
{
matrix[row, column] = 10;
}
}

この処理により、すべての要素が10になります。

共通処理としてメソッド化することもできます。

C#
static void Fill(int[,] matrix, int value)
{
for (int row = 0; row < matrix.GetLength(0); row++)
{
for (int column = 0; column < matrix.GetLength(1); column++)
{
matrix[row, column] = value;
}
}
}

使用例は次のとおりです。

C#
int[,] matrix = new int[3, 4];
Fill(matrix, 5);

2-4. for文を使って連続した値を代入する

行列に1から順番に値を代入する例を見てみましょう。

C#
int[,] matrix = new int[3, 3];
int value = 1;

for (int row = 0; row < matrix.GetLength(0); row++)
{
for (int column = 0; column < matrix.GetLength(1); column++)
{
matrix[row, column] = value;
value++;
}
}

作成される行列は次のとおりです。

1  2  3
4 5 6
7 8 9

行番号と列番号を利用して値を計算することもできます。

C#
int[,] matrix = new int[3, 4];

for (int row = 0; row < matrix.GetLength(0); row++)
{
for (int column = 0; column < matrix.GetLength(1); column++)
{
matrix[row, column] = row * matrix.GetLength(1) + column + 1;
}
}

2-5. ユーザー入力やリストから行列を作成する

コンソールから入力された値で行列を作る場合は、入力文字列を分割して配列に変換します。

C#
Console.Write("行数を入力してください: ");
int rows = int.Parse(Console.ReadLine() ?? "0");

Console.Write("列数を入力してください: ");
int columns = int.Parse(Console.ReadLine() ?? "0");

int[,] matrix = new int[rows, columns];

for (int row = 0; row < rows; row++)
{
Console.WriteLine($"{row + 1}行目を空白区切りで入力してください:");

string[] values = (Console.ReadLine() ?? "")
.Split(' ', StringSplitOptions.RemoveEmptyEntries);

if (values.Length != columns)
{
throw new ArgumentException("入力した要素数が列数と一致しません。");
}

for (int column = 0; column < columns; column++)
{
matrix[row, column] = int.Parse(values[column]);
}
}

List<List<int>>から2次元配列を作る場合は、すべての行の列数が同じか確認します。

C#
List<List<int>> source = new()
{
new List<int> { 1, 2, 3 },
new List<int> { 4, 5, 6 }
};

int rows = source.Count;
int columns = source[0].Count;

if (source.Any(row => row.Count != columns))
{
throw new ArgumentException("各行の要素数が一致していません。");
}

int[,] matrix = new int[rows, columns];

for (int row = 0; row < rows; row++)
{
for (int column = 0; column < columns; column++)
{
matrix[row, column] = source[row][column];
}
}

3. C#で行列の要素を取得・変更する方法

3-1. 行番号と列番号を指定して要素を取得する

2次元配列の要素は、行番号と列番号を指定して取得します。

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

int value = matrix[1, 2];

Console.WriteLine(value); // 60

インデックスは0から始まるため、matrix[1, 2]は2行目・3列目の要素です。

3-2. 指定した要素の値を変更する

要素を変更する場合は、取得時と同じようにインデックスを指定して値を代入します。

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

matrix[0, 1] = 100;

Console.WriteLine(matrix[0, 1]); // 100

この処理後の行列は次の状態になります。

1  100
3 4

3-3. GetLengthメソッドで行数と列数を取得する

2次元配列の行数と列数は、GetLengthメソッドで取得します。

C#
int[,] matrix = new int[3, 4];

int rows = matrix.GetLength(0);
int columns = matrix.GetLength(1);

Console.WriteLine(rows); // 3
Console.WriteLine(columns); // 4

GetLength(0)は第1次元の長さ、つまり行数です。GetLength(1)は第2次元の長さ、つまり列数です。

次元数そのものはRankプロパティで確認できます。

C#
Console.WriteLine(matrix.Rank); // 2

3-4. Lengthプロパティとの違い

Lengthプロパティは、配列に含まれる全要素数を返します。

C#
int[,] matrix = new int[3, 4];

Console.WriteLine(matrix.GetLength(0)); // 3
Console.WriteLine(matrix.GetLength(1)); // 4
Console.WriteLine(matrix.Length); // 12

3行4列の配列には12個の要素があるため、Length12です。

行数や列数が必要な場合はGetLength、全要素数が必要な場合はLengthを使います。

3-5. インデックス範囲外エラーを防ぐ方法

存在しない位置を指定すると、IndexOutOfRangeExceptionが発生します。

C#
int[,] matrix = new int[2, 3];

// matrix[2, 0] = 10; // 行番号2は存在しない
// matrix[0, 3] = 10; // 列番号3は存在しない

2行の配列で使用できる行番号は01です。3列の配列で使用できる列番号は012です。

安全に要素へアクセスするには、範囲を確認します。

C#
int row = 1;
int column = 2;

bool isValid =
row >= 0 &&
row < matrix.GetLength(0) &&
column >= 0 &&
column < matrix.GetLength(1);

if (isValid)
{
Console.WriteLine(matrix[row, column]);
}
else
{
Console.WriteLine("指定した位置は範囲外です。");
}

4. C#で行列の全要素をループ処理する方法

4-1. 二重for文で行列を走査する

行列の各要素を位置情報とともに処理する場合は、二重for文を使います。

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

for (int row = 0; row < matrix.GetLength(0); row++)
{
for (int column = 0; column < matrix.GetLength(1); column++)
{
Console.WriteLine(
$"matrix[{row}, {column}] = {matrix[row, column]}");
}
}

外側のループで行を移動し、内側のループで各行の列を移動します。

4-2. foreach文で全要素を取得する

要素の位置が不要で、値だけを順番に取得したい場合はforeach文を使用できます。

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

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

出力順は次のとおりです。

1
2
3
4
5
6

ただし、foreach文の反復変数を使って配列要素を直接変更することはできません。要素を書き換える場合は、二重for文を使用します。

4-3. 行列を表形式でコンソールに表示する

行列を見やすく表示するには、要素ごとに区切りを入れ、行の終わりで改行します。

C#
int[,] matrix =
{
{ 1, 20, 300 },
{ 4000, 50, 6 }
};

for (int row = 0; row < matrix.GetLength(0); row++)
{
for (int column = 0; column < matrix.GetLength(1); column++)
{
Console.Write($"{matrix[row, column],6}");
}

Console.WriteLine();
}

,6は、6文字分の幅を確保して右寄せする指定です。実行結果は次のようになります。

     1    20   300
4000 50 6

4-4. 条件に一致する要素を検索する

特定の値が行列に含まれているか調べる場合は、二重for文で検索します。

C#
static bool TryFind(
int[,] matrix,
int target,
out int foundRow,
out int foundColumn)
{
for (int row = 0; row < matrix.GetLength(0); row++)
{
for (int column = 0; column < matrix.GetLength(1); column++)
{
if (matrix[row, column] == target)
{
foundRow = row;
foundColumn = column;
return true;
}
}
}

foundRow = -1;
foundColumn = -1;
return false;
}

使用例は次のとおりです。

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

if (TryFind(matrix, 50, out int row, out int column))
{
Console.WriteLine($"見つかりました: [{row}, {column}]");
}
else
{
Console.WriteLine("見つかりませんでした。");
}

4-5. 行列の最大値・最小値・合計値を求める

全要素を調べることで、最大値、最小値、合計値を求められます。

C#
int[,] matrix =
{
{ 4, 8, 1 },
{ 7, 3, 9 }
};

int max = matrix[0, 0];
int min = matrix[0, 0];
long sum = 0;

foreach (int value in matrix)
{
if (value > max)
{
max = value;
}

if (value < min)
{
min = value;
}

sum += value;
}

Console.WriteLine($"最大値: {max}");
Console.WriteLine($"最小値: {min}");
Console.WriteLine($"合計値: {sum}");
Console.WriteLine($"平均値: {(double)sum / matrix.Length}");

合計値が大きくなる可能性を考慮し、sumにはlong型を使用しています。平均値を求めるときは、整数同士の割り算にならないようにdoubleへ変換します。

5. C#で行列の基本計算を行う方法

5-1. 行列同士を加算する

行列同士を加算するには、同じ位置にある要素を足します。

C#
static int[,] Add(int[,] left, int[,] right)
{
if (left.GetLength(0) != right.GetLength(0) ||
left.GetLength(1) != right.GetLength(1))
{
throw new ArgumentException(
"加算する行列のサイズが一致していません。");
}

int rows = left.GetLength(0);
int columns = left.GetLength(1);
int[,] result = new int[rows, columns];

for (int row = 0; row < rows; row++)
{
for (int column = 0; column < columns; column++)
{
result[row, column] =
left[row, column] + right[row, column];
}
}

return result;
}

使用例は次のとおりです。

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

int[,] matrixB =
{
{ 5, 6 },
{ 7, 8 }
};

int[,] result = Add(matrixA, matrixB);

計算結果は次のとおりです。

6   8
10 12

5-2. 行列同士を減算する

減算も加算と同様に、対応する位置の要素を引きます。

C#
static int[,] Subtract(int[,] left, int[,] right)
{
if (left.GetLength(0) != right.GetLength(0) ||
left.GetLength(1) != right.GetLength(1))
{
throw new ArgumentException(
"減算する行列のサイズが一致していません。");
}

int rows = left.GetLength(0);
int columns = left.GetLength(1);
int[,] result = new int[rows, columns];

for (int row = 0; row < rows; row++)
{
for (int column = 0; column < columns; column++)
{
result[row, column] =
left[row, column] - right[row, column];
}
}

return result;
}

5-3. 行列をスカラー倍する

行列のスカラー倍とは、行列のすべての要素に同じ数を掛ける計算です。

C#
static double[,] MultiplyByScalar(
double[,] matrix,
double scalar)
{
int rows = matrix.GetLength(0);
int columns = matrix.GetLength(1);
double[,] result = new double[rows, columns];

for (int row = 0; row < rows; row++)
{
for (int column = 0; column < columns; column++)
{
result[row, column] =
matrix[row, column] * scalar;
}
}

return result;
}

次の行列を2倍してみます。

C#
double[,] matrix =
{
{ 1.5, 2.0 },
{ 3.5, 4.0 }
};

double[,] result = MultiplyByScalar(matrix, 2.0);

結果は次のとおりです。

3.0  4.0
7.0 8.0

5-4. 行列同士を掛け算する

行列の掛け算では、左側の行と右側の列の要素を順番に掛け、その合計を求めます。

C#
static double[,] Multiply(double[,] left, double[,] right)
{
int leftRows = left.GetLength(0);
int leftColumns = left.GetLength(1);
int rightRows = right.GetLength(0);
int rightColumns = right.GetLength(1);

if (leftColumns != rightRows)
{
throw new ArgumentException(
"左行列の列数と右行列の行数が一致していません。");
}

double[,] result = new double[leftRows, rightColumns];

for (int row = 0; row < leftRows; row++)
{
for (int column = 0; column < rightColumns; column++)
{
double sum = 0;

for (int k = 0; k < leftColumns; k++)
{
sum += left[row, k] * right[k, column];
}

result[row, column] = sum;
}
}

return result;
}

使用例は次のとおりです。

C#
double[,] matrixA =
{
{ 1, 2, 3 },
{ 4, 5, 6 }
};

double[,] matrixB =
{
{ 7, 8 },
{ 9, 10 },
{ 11, 12 }
};

double[,] result = Multiply(matrixA, matrixB);

計算結果は次のとおりです。

58   64
139 154

例えば、左上の58は次の計算で求められます。

1 × 7 + 2 × 9 + 3 × 11 = 58

5-5. 行列の積を計算できる条件

行列の積を計算するには、左側の行列の列数と、右側の行列の行数が一致している必要があります。

左側がm行n列、右側がn行p列の場合、計算結果はm行p列になります。

左側: 2行3列
右側: 3行4列
結果: 2行4列

一方、次の組み合わせは計算できません。

左側: 2行3列
右側: 2行4列

左側の列数が3、右側の行数が2で一致していないためです。

また、行列の掛け算には交換法則が一般には成立しません。つまり、A × BB × Aは同じ結果になるとは限らず、片方だけが計算可能な場合もあります。

5-6. 転置行列を作成する

転置行列とは、元の行と列を入れ替えた行列です。

C#
static double[,] Transpose(double[,] matrix)
{
int rows = matrix.GetLength(0);
int columns = matrix.GetLength(1);
double[,] result = new double[columns, rows];

for (int row = 0; row < rows; row++)
{
for (int column = 0; column < columns; column++)
{
result[column, row] = matrix[row, column];
}
}

return result;
}

次の2行3列の行列を転置します。

1  2  3
4 5 6

転置後は3行2列になります。

1  4
2 5
3 6

5-7. 単位行列を作成する

単位行列とは、左上から右下へ並ぶ対角成分が1で、それ以外が0の正方行列です。

1  0  0
0 1 0
0 0 1

C#では、次のように作成できます。

C#
static double[,] CreateIdentity(int size)
{
if (size <= 0)
{
throw new ArgumentOutOfRangeException(
nameof(size),
"サイズは1以上で指定してください。");
}

double[,] result = new double[size, size];

for (int i = 0; i < size; i++)
{
result[i, i] = 1.0;
}

return result;
}

使用例は次のとおりです。

C#
double[,] identity = CreateIdentity(3);

任意の行列に同じサイズの単位行列を掛けても、元の行列は変化しません。

6. C#で行列をメソッド化して扱いやすくする方法

6-1. 行列を引数としてメソッドに渡す

2次元配列は、通常の変数と同じようにメソッドの引数へ渡せます。

C#
static long Sum(int[,] matrix)
{
ArgumentNullException.ThrowIfNull(matrix);

long total = 0;

foreach (int value in matrix)
{
total += value;
}

return total;
}

使用例は次のとおりです。

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

long total = Sum(matrix);

Console.WriteLine(total); // 10

配列は参照型です。メソッド内で配列要素を書き換えると、呼び出し元の配列にも変更が反映されます。

C#
static void SetFirstElement(int[,] matrix)
{
matrix[0, 0] = 999;
}

元の配列を変更したくない場合は、新しい配列を作成して返します。

6-2. 計算結果の行列を戻り値として返す

行列計算では、入力された配列を直接変更せず、計算結果を新しい配列として返す設計が分かりやすいでしょう。

C#
static int[,] Copy(int[,] source)
{
ArgumentNullException.ThrowIfNull(source);

int rows = source.GetLength(0);
int columns = source.GetLength(1);
int[,] result = new int[rows, columns];

for (int row = 0; row < rows; row++)
{
for (int column = 0; column < columns; column++)
{
result[row, column] = source[row, column];
}
}

return result;
}

加算や転置などの処理でも、結果用の行列を新しく作成して返すことで、入力データを安全に保持できます。

6-3. 行列のサイズが一致しているか確認する

加算や減算の前には、行列の行数と列数が一致しているか確認します。

C#
static bool HasSameSize(Array left, Array right)
{
ArgumentNullException.ThrowIfNull(left);
ArgumentNullException.ThrowIfNull(right);

return left.Rank == 2 &&
right.Rank == 2 &&
left.GetLength(0) == right.GetLength(0) &&
left.GetLength(1) == right.GetLength(1);
}

int[,]だけを対象とする場合は、次のように記述できます。

C#
static bool HasSameSize(int[,] left, int[,] right)
{
return left.GetLength(0) == right.GetLength(0) &&
left.GetLength(1) == right.GetLength(1);
}

条件を満たさない場合は、処理を続行せず、分かりやすい例外を投げることが重要です。

6-4. 行列表示処理を共通メソッドにする

表示処理をメソッド化すると、計算結果を簡単に確認できます。

C#
static void PrintMatrix(double[,] matrix)
{
ArgumentNullException.ThrowIfNull(matrix);

for (int row = 0; row < matrix.GetLength(0); row++)
{
for (int column = 0; column < matrix.GetLength(1); column++)
{
Console.Write($"{matrix[row, column],10:F2}");
}

Console.WriteLine();
}
}

F2は、小数点以下を2桁で表示する書式です。

C#
double[,] matrix =
{
{ 1.25, 2.5 },
{ 3.75, 4.0 }
};

PrintMatrix(matrix);

整数用と小数用でメソッドを分けるほか、ジェネリック型を使って汎用化する方法もあります。

6-5. 行列計算クラスを作成する

複数の行列処理を使用する場合は、専用クラスにまとめると管理しやすくなります。

C#
public static class MatrixCalculator
{
public static double[,] Add(
double[,] left,
double[,] right)
{
ValidateNotNull(left, right);
ValidateSameSize(left, right);

int rows = left.GetLength(0);
int columns = left.GetLength(1);
double[,] result = new double[rows, columns];

for (int row = 0; row < rows; row++)
{
for (int column = 0; column < columns; column++)
{
result[row, column] =
left[row, column] + right[row, column];
}
}

return result;
}

public static double[,] Multiply(
double[,] left,
double[,] right)
{
ValidateNotNull(left, right);

int leftRows = left.GetLength(0);
int leftColumns = left.GetLength(1);
int rightRows = right.GetLength(0);
int rightColumns = right.GetLength(1);

if (leftColumns != rightRows)
{
throw new ArgumentException(
"行列の積を計算できないサイズです。");
}

double[,] result =
new double[leftRows, rightColumns];

for (int row = 0; row < leftRows; row++)
{
for (int column = 0;
column < rightColumns;
column++)
{
for (int k = 0; k < leftColumns; k++)
{
result[row, column] +=
left[row, k] * right[k, column];
}
}
}

return result;
}

public static double[,] Transpose(double[,] matrix)
{
ArgumentNullException.ThrowIfNull(matrix);

int rows = matrix.GetLength(0);
int columns = matrix.GetLength(1);
double[,] result = new double[columns, rows];

for (int row = 0; row < rows; row++)
{
for (int column = 0; column < columns; column++)
{
result[column, row] = matrix[row, column];
}
}

return result;
}

private static void ValidateNotNull(
double[,] left,
double[,] right)
{
ArgumentNullException.ThrowIfNull(left);
ArgumentNullException.ThrowIfNull(right);
}

private static void ValidateSameSize(
double[,] left,
double[,] right)
{
if (left.GetLength(0) != right.GetLength(0) ||
left.GetLength(1) != right.GetLength(1))
{
throw new ArgumentException(
"行列のサイズが一致していません。");
}
}
}

利用する側では、次のように記述できます。

C#
double[,] result =
MatrixCalculator.Add(matrixA, matrixB);

計算処理を1つのクラスにまとめることで、コードの重複を減らし、テストもしやすくなります。

7. C#でジャグ配列を行列として扱う方法

7-1. ジャグ配列を作成・初期化する

ジャグ配列は、配列の各要素に別の配列を格納する構造です。

C#
int[][] matrix = new int[3][];

matrix[0] = new int[2];
matrix[1] = new int[4];
matrix[2] = new int[3];

初期値を指定する場合は、次のように記述します。

C#
int[][] matrix =
{
new int[] { 1, 2 },
new int[] { 3, 4, 5, 6 },
new int[] { 7, 8, 9 }
};

簡略化した記述も可能です。

C#
int[][] matrix =
[
[1, 2],
[3, 4, 5, 6],
[7, 8, 9]
];

使用できる構文は、プロジェクトで設定されているC#の言語バージョンによって異なります。

7-2. ジャグ配列の要素を取得・変更する

ジャグ配列では、角括弧を2回使って要素へアクセスします。

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

int value = matrix[1][2];

Console.WriteLine(value); // 50

値を変更する場合も同様です。

C#
matrix[0][1] = 999;

Console.WriteLine(matrix[0][1]); // 999

2次元配列のmatrix[row, column]とは書き方が異なるため、混同しないようにしましょう。

7-3. 行ごとに列数が異なるデータを扱う

ジャグ配列の特徴は、行ごとに列数を変えられることです。

C#
string[][] categories =
{
new string[] { "りんご", "みかん" },
new string[] { "にんじん", "玉ねぎ", "じゃがいも" },
new string[] { "牛乳" }
};

全要素を処理する場合は、各行のLengthを使います。

C#
for (int row = 0; row < categories.Length; row++)
{
for (int column = 0;
column < categories[row].Length;
column++)
{
Console.WriteLine(categories[row][column]);
}
}

内側のループ条件に、先頭行の長さを固定して使わないことが重要です。

7-4. 2次元配列とジャグ配列の使い分け

表のようにすべての行の列数が同じ場合は、2次元配列が適しています。

C#
int[,] board = new int[8, 8];

行ごとに要素数が違う場合は、ジャグ配列が適しています。

C#
int[][] triangle =
{
new int[] { 1 },
new int[] { 2, 3 },
new int[] { 4, 5, 6 }
};

主な判断基準は次のとおりです。

  • 長方形のデータは2次元配列

  • 行ごとに長さが異なるデータはジャグ配列

  • 数学的な行列計算には2次元配列

  • 行単位で配列を差し替えたい場合はジャグ配列

  • 外部ライブラリを使う場合は、ライブラリが要求する形式を優先

7-5. 2次元配列とジャグ配列を相互変換する

2次元配列をジャグ配列へ変換するメソッドは、次のように作成できます。

C#
static int[][] ToJaggedArray(int[,] source)
{
ArgumentNullException.ThrowIfNull(source);

int rows = source.GetLength(0);
int columns = source.GetLength(1);
int[][] result = new int[rows][];

for (int row = 0; row < rows; row++)
{
result[row] = new int[columns];

for (int column = 0; column < columns; column++)
{
result[row][column] = source[row, column];
}
}

return result;
}

ジャグ配列を2次元配列へ変換する場合は、すべての行の長さが同じであることを確認します。

C#
static int[,] ToTwoDimensionalArray(int[][] source)
{
ArgumentNullException.ThrowIfNull(source);

if (source.Length == 0)
{
return new int[0, 0];
}

if (source[0] is null)
{
throw new ArgumentException(
"ジャグ配列にnullの行が含まれています。");
}

int columns = source[0].Length;

if (source.Any(row =>
row is null || row.Length != columns))
{
throw new ArgumentException(
"各行の列数が一致していません。");
}

int[,] result = new int[source.Length, columns];

for (int row = 0; row < source.Length; row++)
{
for (int column = 0; column < columns; column++)
{
result[row, column] = source[row][column];
}
}

return result;
}

8. C#の行列処理で発生しやすいエラーと対処法

8-1. IndexOutOfRangeExceptionが発生する原因

IndexOutOfRangeExceptionは、配列の範囲外にアクセスしたときに発生します。

C#
int[,] matrix = new int[2, 3];

// matrix[2, 0] = 1; // エラー

new int[2, 3]で使用できる行番号は01です。

ループ条件を<=にしてしまうミスにも注意が必要です。

C#
// 誤り
for (int row = 0; row <= matrix.GetLength(0); row++)
{
}

正しくは<を使用します。

C#
for (int row = 0; row < matrix.GetLength(0); row++)
{
}

8-2. 行数と列数を逆に指定するミス

2次元配列では、最初のインデックスが行、次のインデックスが列です。

C#
matrix[row, column]

列を先に指定してしまうと、誤った要素を取得したり、範囲外エラーが発生したりします。

変数名をijだけにせず、rowcolumnと明示すると間違いを減らせます。

C#
for (int row = 0; row < matrix.GetLength(0); row++)
{
for (int column = 0;
column < matrix.GetLength(1);
column++)
{
Console.WriteLine(matrix[row, column]);
}
}

8-3. サイズの異なる行列を加算するミス

行列の加算と減算では、両方の行数と列数が一致している必要があります。

2行3列 + 2行3列: 計算可能
2行3列 + 3行2列: 計算不可

処理を始める前に、サイズを検証します。

C#
if (left.GetLength(0) != right.GetLength(0) ||
left.GetLength(1) != right.GetLength(1))
{
throw new ArgumentException(
"行列のサイズが一致していません。");
}

8-4. 掛け算できないサイズの行列を指定するミス

行列の積では、左行列の列数と右行列の行数が一致しなければなりません。

C#
if (left.GetLength(1) != right.GetLength(0))
{
throw new ArgumentException(
"左行列の列数と右行列の行数が一致していません。");
}

加算では両方のサイズが同じである必要がありますが、乗算では確認する条件が異なります。

8-5. 配列がnullの場合に発生するエラー

配列がnullの状態でGetLengthや要素アクセスを実行すると、NullReferenceExceptionが発生します。

C#
int[,]? matrix = null;

// Console.WriteLine(matrix.GetLength(0));

メソッドの先頭でnullを検証しましょう。

C#
static void Print(int[,] matrix)
{
ArgumentNullException.ThrowIfNull(matrix);

// 表示処理
}

古い言語環境などでThrowIfNullを使わない場合は、次のように記述できます。

C#
if (matrix is null)
{
throw new ArgumentNullException(nameof(matrix));
}

ジャグ配列では、配列本体だけでなく各行がnullになっている可能性もあります。

C#
int[][] jagged = new int[3][];

if (jagged[0] is not null)
{
Console.WriteLine(jagged[0].Length);
}

8-6. 整数型の割り算による計算結果のずれ

int型同士で割り算をすると、小数部分が切り捨てられます。

C#
int sum = 5;
int count = 2;

double average = sum / count;

Console.WriteLine(average); // 2

正確な平均値を求めるには、少なくとも片方をdoubleへ変換します。

C#
double average = (double)sum / count;

Console.WriteLine(average); // 2.5

逆行列、正規化、平均値など、小数が発生する行列計算では、基本的にdoubleを使用するのが安全です。

9. C#で行列計算ライブラリを利用する方法

9-1. 自作処理とライブラリを使い分ける基準

配列の学習や単純な加算、転置程度であれば、二重for文による自作処理でも十分です。

一方、次のような処理が必要な場合は、行列計算ライブラリの利用を検討しましょう。

  • 逆行列

  • 行列式

  • 連立一次方程式

  • 固有値や固有ベクトル

  • QR分解

  • LU分解

  • 特異値分解

  • 大規模な疎行列の計算

高度な処理をすべて自作すると、計算精度、例外処理、性能などに問題が生じやすくなります。

9-2. System.Numericsで扱える行列

.NETのSystem.Numerics名前空間には、ベクトルや行列を扱う型が用意されています。

代表的な型は次のとおりです。

  • Vector2

  • Vector3

  • Vector4

  • Matrix3x2

  • Matrix4x4

  • Quaternion

Matrix4x4は4行4列に固定された単精度浮動小数点数の行列です。3Dグラフィックスや座標変換で便利ですが、任意の行数・列数を持つ汎用行列ではありません。

9-3. Matrix4x4の作成と基本操作

Matrix4x4を使用するには、System.Numericsを読み込みます。

C#
using System.Numerics;

行列は次のように作成できます。

C#
Matrix4x4 matrix = new Matrix4x4(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
10, 20, 30, 1);

単位行列は、Identityプロパティから取得できます。

C#
Matrix4x4 identity = Matrix4x4.Identity;

拡大、回転、移動用の行列も作成できます。

C#
Matrix4x4 scale =
Matrix4x4.CreateScale(2.0f);

Matrix4x4 rotation =
Matrix4x4.CreateRotationZ(MathF.PI / 4);

Matrix4x4 translation =
Matrix4x4.CreateTranslation(10, 20, 30);

行列同士は*演算子で乗算できます。

C#
Matrix4x4 combined =
scale * rotation * translation;

転置はTransposeメソッドで行います。

C#
Matrix4x4 transposed =
Matrix4x4.Transpose(combined);

逆行列はInvertメソッドで求められます。

C#
if (Matrix4x4.Invert(combined, out Matrix4x4 inverse))
{
Console.WriteLine("逆行列を計算できました。");
}
else
{
Console.WriteLine("逆行列が存在しません。");
}

9-4. Math.NET Numericsで行列を作成する

任意サイズの高度な行列計算には、Math.NET Numericsなどのライブラリが利用されます。

パッケージを導入する場合は、プロジェクトのディレクトリで次のコマンドを実行します。

Bash
dotnet add package MathNet.Numerics

行列を作成する基本例は次のとおりです。

C#
using MathNet.Numerics.LinearAlgebra;

Matrix<double> matrix =
Matrix<double>.Build.DenseOfArray(
new double[,]
{
{ 1, 2 },
{ 3, 4 }
});

行数と列数を指定して、すべての要素を同じ値で作成することもできます。

C#
Matrix<double> matrix =
Matrix<double>.Build.Dense(3, 4, 1.5);

単位行列は次のように作成します。

C#
Matrix<double> identity =
Matrix<double>.Build.DenseIdentity(3);

9-5. ライブラリで加算・乗算・転置を行う

Math.NET Numericsでは、演算子やメソッドを使って行列計算を簡潔に記述できます。

C#
using MathNet.Numerics.LinearAlgebra;

Matrix<double> matrixA =
Matrix<double>.Build.DenseOfArray(
new double[,]
{
{ 1, 2 },
{ 3, 4 }
});

Matrix<double> matrixB =
Matrix<double>.Build.DenseOfArray(
new double[,]
{
{ 5, 6 },
{ 7, 8 }
});

Matrix<double> added = matrixA + matrixB;
Matrix<double> subtracted = matrixA - matrixB;
Matrix<double> multiplied = matrixA * matrixB;
Matrix<double> transposed = matrixA.Transpose();

スカラー倍も演算子で記述できます。

C#
Matrix<double> scaled = matrixA * 2.0;

逆行列を計算する例は次のとおりです。

C#
Matrix<double> inverse = matrixA.Inverse();

ただし、すべての正方行列に逆行列が存在するわけではありません。実際のアプリケーションでは、計算条件や数値的な安定性も考慮する必要があります。

9-6. 大規模な行列計算で注意するポイント

大規模な行列では、要素数に比例して多くのメモリを使用します。

例えば、double型は通常1要素あたり8バイトです。1万行1万列の密行列には1億個の要素があるため、要素データだけでも約800MB必要になります。実際には配列やライブラリ内部の管理領域も考慮しなければなりません。

また、一般的な行列乗算の計算量は、正方行列の場合におおむねサイズの3乗で増えます。小さな行列では問題のない三重ループでも、サイズが大きくなると処理時間が急増します。

値の大部分が0の場合は、密行列ではなく疎行列の利用を検討しましょう。さらに、同じ計算を繰り返す場合は、不要な配列生成を減らすことも重要です。

性能が重要な処理では、次の点を確認します。

  • 密行列と疎行列の選択

  • 不要なコピーの削減

  • 適切な数値型の選択

  • 並列処理の有効性

  • 利用ライブラリの最適化機能

  • 計算精度と処理速度のバランス

10. C#の行列に関するよくある質問

10-1. 2次元配列の行数と列数はどのように取得する?

行数はGetLength(0)、列数はGetLength(1)で取得します。

C#
int[,] matrix = new int[3, 5];

int rows = matrix.GetLength(0);
int columns = matrix.GetLength(1);

Console.WriteLine(rows); // 3
Console.WriteLine(columns); // 5

Lengthは行数や列数ではなく、全要素数を返します。

C#
Console.WriteLine(matrix.Length); // 15

10-2. 行列を動的に追加・削除することはできる?

配列のサイズは作成時に固定されるため、2次元配列へ行や列を直接追加・削除することはできません。

サイズを変更する場合は、新しい配列を作成して要素をコピーします。

C#
static int[,] AddRow(int[,] source, int[] newRow)
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(newRow);

int rows = source.GetLength(0);
int columns = source.GetLength(1);

if (newRow.Length != columns)
{
throw new ArgumentException(
"追加する行の要素数が列数と一致しません。");
}

int[,] result = new int[rows + 1, columns];

for (int row = 0; row < rows; row++)
{
for (int column = 0; column < columns; column++)
{
result[row, column] = source[row, column];
}
}

for (int column = 0; column < columns; column++)
{
result[rows, column] = newRow[column];
}

return result;
}

頻繁に追加や削除を行う場合は、List<int[]>List<List<int>>の利用も検討しましょう。

10-3. 2次元配列をListで代用できる?

List<List<T>>を使えば、2次元に近いデータ構造を作成できます。

C#
List<List<int>> matrix = new()
{
new List<int> { 1, 2, 3 },
new List<int> { 4, 5, 6 }
};

要素へは次のようにアクセスします。

C#
Console.WriteLine(matrix[1][2]); // 6

Listは要素を動的に追加・削除できますが、各行の要素数が一致していることは保証されません。数学的な行列として使う場合は、サイズの検証が必要です。

固定サイズで単純な行列計算を行うなら2次元配列、行や要素を頻繁に増減するならListが適しています。

10-4. 行列を1次元配列に変換するには?

2次元配列を行単位で1次元配列へ変換する場合は、次のように処理します。

C#
static int[] Flatten(int[,] matrix)
{
ArgumentNullException.ThrowIfNull(matrix);

int columns = matrix.GetLength(1);
int[] result = new int[matrix.Length];

for (int row = 0; row < matrix.GetLength(0); row++)
{
for (int column = 0; column < columns; column++)
{
int index = row * columns + column;
result[index] = matrix[row, column];
}
}

return result;
}

次の行列を変換した場合を考えます。

1  2  3
4 5 6

結果は次の1次元配列です。

1  2  3  4  5  6

反対に、1次元配列を2次元配列へ変換するには、要素数が行数 × 列数と一致する必要があります。

C#
static int[,] Reshape(
int[] source,
int rows,
int columns)
{
ArgumentNullException.ThrowIfNull(source);

if (rows < 0 || columns < 0)
{
throw new ArgumentOutOfRangeException(
"行数と列数は0以上で指定してください。");
}

if (source.Length != rows * columns)
{
throw new ArgumentException(
"要素数が指定された行列サイズと一致しません。");
}

int[,] result = new int[rows, columns];

for (int index = 0; index < source.Length; index++)
{
int row = index / columns;
int column = index % columns;
result[row, column] = source[index];
}

return result;
}

10-5. 行列をCSVファイルから読み込むには?

単純な数値のCSVファイルであれば、各行を読み込み、カンマで分割して2次元配列へ変換できます。

C#
static double[,] LoadMatrixFromCsv(string path)
{
string[] lines = File.ReadAllLines(path);

if (lines.Length == 0)
{
return new double[0, 0];
}

string[][] cells = lines
.Select(line => line.Split(','))
.ToArray();

int columns = cells[0].Length;

if (columns == 0 ||
cells.Any(row => row.Length != columns))
{
throw new FormatException(
"CSVの列数が行ごとに異なります。");
}

double[,] result =
new double[cells.Length, columns];

for (int row = 0; row < cells.Length; row++)
{
for (int column = 0; column < columns; column++)
{
if (!double.TryParse(
cells[row][column],
out double value))
{
throw new FormatException(
$"{row + 1}行{column + 1}列を数値に変換できません。");
}

result[row, column] = value;
}
}

return result;
}

例えば、CSVファイルが次の内容であるとします。

csv
1.5,2.0,3.5
4.0,5.5,6.0

読み込みは次のように行います。

C#
double[,] matrix =
LoadMatrixFromCsv("matrix.csv");

引用符、セル内のカンマ、改行などを含む本格的なCSVを扱う場合は、単純なSplitでは正しく解析できません。その場合はCSV解析ライブラリの利用を検討しましょう。

10-6. 行列計算ではintとdoubleのどちらを使うべき?

整数だけを保存し、計算結果も必ず整数になる場合はintを使用できます。

C#
int[,] board =
{
{ 0, 1, 0 },
{ 1, 0, 1 }
};

一方、平均、割り算、座標変換、逆行列、統計処理などでは小数が発生するため、doubleが適しています。

C#
double[,] matrix =
{
{ 1.5, 2.25 },
{ 3.75, 4.0 }
};

intには、計算結果が型の上限や下限を超えるオーバーフローのリスクもあります。大きな整数の合計にはlongを使用する方法があります。

また、金額のように10進数としての精度が重要なデータにはdecimalが適することがあります。ただし、一般的な科学技術計算や行列計算ではdoubleが広く使われます。

まとめ

C#で行列を扱う基本的な方法は、int[,]double[,]などの2次元配列を使用することです。

行数と列数を指定して作成する場合は、次のように記述します。

C#
double[,] matrix = new double[3, 4];

行数と列数は、GetLengthメソッドで取得できます。

C#
int rows = matrix.GetLength(0);
int columns = matrix.GetLength(1);

全要素を処理する場合は、二重for文が基本です。

C#
for (int row = 0; row < matrix.GetLength(0); row++)
{
for (int column = 0;
column < matrix.GetLength(1);
column++)
{
Console.WriteLine(matrix[row, column]);
}
}

加算や減算では両方の行列サイズが一致している必要があります。乗算では、左行列の列数と右行列の行数が一致していなければなりません。

行ごとに列数が異なるデータにはジャグ配列、動的な追加や削除が多い場合にはListが適しています。逆行列や行列分解などの高度な計算には、System.NumericsやMath.NET Numericsなどのライブラリを活用すると効率的です。

まずは2次元配列の作成、要素の取得、二重ループによる走査から始め、必要に応じて加算、乗算、転置などの処理へ進むと、C#における行列の扱い方を段階的に理解できます。