C# プログラミングにおいて、データの集合を効率的に扱うための最も基本的なデータ構造が「配列(Array)」です。

配列は、同じ型の変数を複数まとめて管理するための仕組みであり、メモリ効率やアクセス速度の面で非常に優れた特性を持っています。

しかし、初心者から中級者へとステップアップする過程で、配列の宣言方法の多様性や、後発の List<T> との使い分け、さらには最新の C# で導入された効率的なメモリ操作手法などに戸惑うことも少なくありません。

本記事では、C# における配列の基礎から、実務で必須となる応用操作、そして最新の言語仕様に基づいた高度なテクニックまでを網羅的に解説します。

この記事を読み終える頃には、プロジェクトの要件に合わせて最適なデータ構造を選択し、パフォーマンスを最大限に引き出す配列操作ができるようになっているはずです。

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

C# の配列は、同一のデータ型の要素をメモリ上の連続した領域に配置するためのデータ構造です。

配列を使用することで、個別に変数を用意することなく、インデックス(添字)を使用して大量のデータを一括管理できます。

配列の主な特徴

配列には、他のデータ構造と比較して以下のような明確な特徴があります。

  1. 固定長である:一度インスタンス化(作成)すると、そのサイズを変更することはできません。
  2. ゼロベースのインデックス:要素へのアクセスは 0 から始まる番号を使用します。
  3. 型安全性:宣言時に指定した型以外の要素を格納することはできません。
  4. 参照型である:配列自体はヒープ領域に確保されるオブジェクトであり、変数はそのメモリ番地を保持します。

C# の配列はすべて System.Array クラスを継承しており、このクラスが提供する強力なメソッド群を利用して、ソートや検索などの操作を行うことができます。

配列の宣言と初期化のバリエーション

C# では、配列を宣言し、初期化する方法が複数存在します。

プログラムの文脈や可読性の観点から、最適な方法を選択することが重要です。

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

最も標準的な方法は、型名の後ろに角括弧 [] を付け、new キーワードを使用してサイズを指定する方法です。

C#
// 整数型の要素を5個持つ配列を宣言
int[] numbers = new int[5];

// 文字列型の要素を3個持つ配列を宣言
string[] names = new string[3];

このとき、数値型の場合は 0、参照型の場合は null、論理型(bool)の場合は false で各要素が自動的に初期化されます。

初期値を指定した宣言

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

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

C#
// 配列初期化子を使用した例
int[] numbers = new int[] { 10, 20, 30, 40, 50 };

// さらに簡略化した記述(暗黙的な型指定)
int[] simplifiedNumbers = { 1, 2, 3, 4, 5 };

// varキーワードを使用した例
var autoNumbers = new[] { 100, 200, 300 };

C# 12 以降の新しい構文:コレクション式

最新の C# 12 では、より簡潔に配列を記述できる コレクション式 が導入されました。

角括弧 [] だけで配列を表現できるため、コードの可読性が大幅に向上します。

C#
// C# 12 以降の推奨される書き方
int[] modernArray = [1, 2, 3, 4, 5];

// 空の配列も直感的
string[] emptyArray = [];

注意点として、古いバージョンの .NET フレームワークや C# コンパイラを使用している環境では、コレクション式は利用できません。

その場合は、従来の new[] { ... } 形式を使用してください。

配列の基本操作と要素へのアクセス

配列を定義した後は、データの取得や書き換え、ループ処理による一括操作が必要になります。

要素の取得と代入

インデックスを指定してアクセスします。

インデックスは 0 から始まり、配列の長さ - 1 が最後の要素を指します。

C#
int[] scores = [80, 90, 75];

// 要素の取得
int firstScore = scores[0]; // 80

// 要素の書き換え
scores[1] = 95; // 90 を 95 に変更

もし存在しないインデックス(例:負の値や scores.Length 以上の値)にアクセスしようとすると、実行時に IndexOutOfRangeException が発生し、プログラムが異常終了します。

配列の長さの取得

配列の要素数は Length プロパティで取得できます。

C#
int[] data = [10, 20, 30, 40];
Console.WriteLine($"要素数は {data.Length} です。");

インデックスと範囲(Index and Range)

C# 8.0 で導入された「インデックス」と「範囲」の機能を使うと、配列の末尾からのアクセスや、一部の切り出し(スライス)が容易になります。

  • ^1:末尾から 1 番目の要素(最後の要素)
  • 1..3:インデックス 1 から 3 の手前(2 まで)の範囲
C#
int[] vals = [10, 20, 30, 40, 50];

int last = vals[^1];     // 50
int[] sub = vals[1..4];  // [20, 30, 40]

配列の反復処理(ループ)

配列内のすべての要素に対して処理を行うには、for 文または foreach 文を使用します。

C#
string[] fruits = ["Apple", "Banana", "Cherry"];

// foreach文(読み取り専用や単純な処理に最適)
foreach (string fruit in fruits)
{
    Console.WriteLine(fruit);
}

// for文(インデックスが必要な場合に便利)
for (int i = 0; i < fruits.Length; i++)
{
    Console.WriteLine($"{i}番目の要素は {fruits[i]} です");
}

多次元配列とジャグ配列

C# では、格子状のデータ構造を扱う「多次元配列」と、配列の中に配列を格納する「ジャグ配列(Jagged Array)」の 2 種類が利用可能です。

多次元配列(矩形配列)

すべての行の長さが同じである配列です。

カンマを使用して宣言します。

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

// アクセス方法
int val = matrix[1, 2]; // 6 (2行目の3列目)

ジャグ配列(配列の配列)

各行の長さが異なる可能性のある配列です。

C#
// 各要素が int[] 型である配列
int[][] jagged = new int[3][];
jagged[0] = [1, 2];
jagged[1] = [3, 4, 5, 6];
jagged[2] = [7];

// アクセス方法
int jVal = jagged[1][2]; // 5
特徴多次元配列 [,]ジャグ配列 [][]
メモリ構造連続した単一ブロック独立した複数の配列への参照
柔軟性全ての行が同じ長さ行ごとに長さが違っても良い
パフォーマンス読み取りが高速な場合が多い.NET の最適化により高速な場合が多い

System.Arrayクラスによる高度な操作

すべての配列は System.Array クラスのメソッドを利用できます。

これにより、複雑なアルゴリズムを自分で実装することなく、一般的な操作を実行できます。

ソート(並べ替え)

Array.Sort メソッドを使用すると、配列の要素を昇順に並べ替えることができます。

C#
int[] data = [5, 2, 8, 1, 9];
Array.Sort(data);

// 結果: [1, 2, 5, 8, 9]

検索

特定の要素がどこにあるかを探すには Array.IndexOf を使用します。

C#
string[] members = ["Alice", "Bob", "Charlie"];
int index = Array.IndexOf(members, "Bob"); // 1

反転とクリア

C#
// 順序を逆にする
Array.Reverse(data);

// 特定の範囲を既定値でクリア(削除ではない)
Array.Clear(data, 0, data.Length);

LINQ による強力な操作

using System.Linq; を追加することで、配列に対して関数型の操作が可能になります。

C#
using System.Linq;

int[] scores = [45, 78, 92, 30, 85];

// 80点以上の要素だけを抽出し、リスト化
int[] highScores = scores.Where(s => s >= 80).ToArray();

// 平均値を計算
double average = scores.Average();

// 最大値を取得
int max = scores.Max();

LINQ は非常に便利ですが、内部的にオブジェクトの生成が行われるため、極限のパフォーマンスが求められるループ内での多用には注意が必要です。

配列と List<T> の違いと使い分け

実務開発において最も頻繁に議論されるのが「配列と List<T> のどちらを使うべきか」という点です。

徹底比較表

比較項目配列 (T[])リスト (List<T>)
サイズ固定可変(動的に追加・削除可能)
パフォーマンス最高(メモリオーバーヘッド最小)高いが、サイズ拡張時に再確保が発生
メソッドの豊富さ基本的(Arrayクラスを使用)非常に豊富(Add, Remove, Insert等)
メモリ消費要素分のみ予備の容量(Capacity)分、少し多い
用途サイズが決まっている、性能重視サイズが不明、頻繁なデータの増減

使い分けの指針

基本的には、「要素数が動的に変化する場合は List<T>、サイズが不変でパフォーマンスを追求する場合は配列」という基準で選択します。

配列を選ぶべきケース

パフォーマンスがクリティカルな計算処理。

実行中にデータの個数が変わらない固定サイズの集合(例:1週間の曜日、12ヶ月の定義など)。

外部APIやハードウェア制御において、特定のメモリレイアウトが要求される場合。

List<T> を選ぶべきケース

ユーザーの入力やデータベースの結果に応じてデータ数が増減する場合。

AddRemoveメソッドを多用してデータを整理したい場合。

一般的なビジネスロジックの実装(可読性と開発効率を優先)。

最新の C# におけるパフォーマンス最適化:Span<T>

現代的な C# 開発において、配列操作のパフォーマンスを語る上で欠かせないのが Span<T>ReadOnlySpan<T> です。

これらは C# 7.2 で導入された構造体で、配列の一部をコピーすることなく効率的に参照する仕組みを提供します。

Span<T> を使ったメモリ効率化

従来の vals[1..4] のようなスライス操作は、元の配列のコピーを作成するためメモリを消費します。

しかし、AsSpan() を使用すると、同じメモリ領域を指し示したまま、安全に部分アクセスが可能です。

C#
using System;

public class SpanExample
{
    public static void Main()
    {
        int[] largeData = new int[1000];
        // 初期化処理...

        // 配列の一部をコピーせずに参照(100番目から50個分)
        Span<int> slice = largeData.AsSpan(100, 50);
        
        // sliceに対する操作は、元のlargeDataに反映される
        slice[0] = 999;
        
        Console.WriteLine(largeData[100]); // 999 が出力される
    }
}

Span<T> は、特に文字列処理(ReadOnlySpan<char>)や、大量のパケットデータを扱うネットワークプログラミングなどで劇的な速度改善をもたらします。

配列使用時の注意点とベストプラクティス

配列は強力ですが、誤った使い方をするとバグの原因やパフォーマンス低下を招きます。

以下のポイントに注意しましょう。

1. 多次元配列よりもジャグ配列を検討する

.NET ランタイム(CLR)は、ジャグ配列 [][] のアクセス最適化に長けています。

多次元配列 [, ] は構文こそスマートですが、境界チェックのコストがわずかに高くつく傾向があります。

計算速度を重視する場合は、ジャグ配列を検討してください。

2. Null チェックを怠らない

配列の変数は参照型であるため、初期化される前や条件によっては null である可能性があります。

アクセス前にチェックするか、空の配列 [] で初期化しておく習慣をつけましょう。

3. Array.Empty<T>() の活用

要素数が 0 の配列が必要な場合、new int[0] とするよりも Array.Empty<int>() を使用してください。

これにより、メモリ上に不必要なインスタンスが何度も生成されるのを防ぎ、既存の空配列インスタンスを再利用できます。

C#
// 推奨される空配列の生成
int[] empty = Array.Empty<int>();

// C# 12 以降ならこれも効率的
int[] modernEmpty = [];

実践サンプルプログラム:データの統計処理

最後に、これまでの知識を統合したサンプルプログラムを紹介します。

ユーザーから入力された数値データを配列に格納し、ソート、平均、最大値の取得を行う実用的なコードです。

C#
using System;
using System.Linq;

namespace ArrayMaster
{
    class Program
    {
        static void Main(string[] args)
        {
            // 5つの数値を格納する配列を宣言
            int[] numbers = new int[5];

            Console.WriteLine("5つの数値を入力してください:");

            for (int i = 0; i < numbers.Length; i++)
            {
                Console.Write($"[{i + 1}/5] : ");
                if (int.TryParse(Console.ReadLine(), out int result))
                {
                    numbers[i] = result;
                }
                else
                {
                    Console.WriteLine("エラー: 数値を入力してください。0として扱います。");
                    numbers[i] = 0;
                }
            }

            // 配列のソート
            Array.Sort(numbers);

            Console.WriteLine("\n--- 処理結果 ---");
            Console.WriteLine($"昇順ソート: {string.Join(", ", numbers)}");

            // LINQを使用した統計
            double average = numbers.Average();
            int max = numbers.Max();
            int min = numbers.Min();

            Console.WriteLine($"平均値: {average:F2}");
            Console.WriteLine($"最大値: {max}");
            Console.WriteLine($"最小値: {min}");

            // 特定の条件を満たすデータの抽出(LINQ)
            var evens = numbers.Where(n => n % 2 == 0);
            Console.WriteLine($"偶数のみ: {string.Join(", ", evens)}");
        }
    }
}
実行結果
5つの数値を入力してください:
[1/5] : 45
[2/5] : 12
[3/5] : 89
[4/5] : 34
[5/5] : 67

--- 処理結果 ---
昇順ソート: 12, 34, 45, 67, 89
平均値: 49.40
最大値: 89
最小値: 12
偶数のみ: 12, 34

まとめ

C# の配列は、シンプルながらも非常に奥が深く、ランタイムのメモリ管理を最大限に活かせる強力なツールです。

本記事では、基本的な宣言・初期化から、C# 12 の最新構文であるコレクション式、そして List<T> との使い分けや Span<T> による最適化までを解説しました。

  • サイズが固定なら「配列」、可変なら「List<T>」を選択する。
  • 最新の C# ではコレクション式 [] を活用して簡潔に記述する。
  • パフォーマンスが重要な場面では Span<T> やジャグ配列を検討する。

これらの基本と応用を使い分けることで、あなたの書く C# コードはより堅牢で、かつ高速なものへと進化します。

配列の特性を正しく理解し、日々の開発において最適なデータ構造を選択できるエンジニアを目指しましょう。