C#において、配列(Array)は非常に基本的かつ重要なデータ構造です。
しかし、C#を学び始めたばかりの方や、他の動的言語に慣れている方が最初につまずきやすいのが「配列のサイズは固定である」という点です。
一度宣言した配列の要素数を後から自由に変更することはできません。
そのため、要素を追加したい場合には、内部的な仕組みを理解した上で適切な手法を選択する必要があります。
本記事では、C#で配列に要素を追加するためのさまざまな手法を解説します。
伝統的なArray.Resizeから、最も推奨されるList<T>への変換、さらに最新のC# 12で導入されたコレクション式まで、パフォーマンスやコードの可読性を考慮した最適な選択肢を詳しく見ていきましょう。
なぜC#の配列には直接要素を追加できないのか
C#の配列は、メモリ上の連続した領域に配置されるデータ構造です。
配列が作成される際、実行環境(CLR)はそのサイズに応じたメモリを確保します。
もし後からサイズを自由に変更できるようにしてしまうと、隣接するメモリ領域に別のデータが存在する場合に、それらを破壊してしまう可能性があるからです。
そのため、C#で「配列に要素を追加する」という操作は、厳密には「新しいサイズの配列を別に作成し、元のデータをコピーした上で新しい要素を入れる」というプロセスを意味します。
この動作原理を理解しておくことは、大規模なデータを扱う際のパフォーマンス最適化において非常に重要です。
Array.Resizeメソッドを使用した要素の追加
最も直接的な方法の一つが、System.Array.Resizeメソッドを使用することです。
このメソッドは、指定した配列のサイズを変更するためのものですが、実際には背後で「新しい配列の生成」と「データのコピー」を行っています。
Array.Resizeの使い方
以下のコードは、既存の配列に1つの要素を追加する例です。
using System;
class Program
{
static void Main()
{
// 初期状態の配列
int[] numbers = { 1, 2, 3 };
// 配列のサイズを現在のサイズ + 1に変更
int newElement = 4;
Array.Resize(ref numbers, numbers.Length + 1);
// 新しく確保された最後のインデックスに値を代入
numbers[numbers.Length - 1] = newElement;
// 結果の出力
Console.WriteLine("追加後の配列:");
foreach (var n in numbers)
{
Console.WriteLine(n);
}
}
}
追加後の配列:
1
2
3
4
Array.Resizeの注意点
Array.Resizeは、引数にrefキーワードを必要とします。
これは、メソッド内で配列の参照そのものが書き換えられるためです。
頻繁にこのメソッドを呼び出すと、その都度メモリの再確保と全要素のコピーが発生するため、ループ内での使用などは避けるべきです。
List<T>クラスを利用する標準的な手法
実務において最も推奨されるのが、可変長配列であるSystem.Collections.Generic.List<T>を利用する方法です。
配列を一度List<T>に変換し、要素を追加した後に再び配列に戻すという手順を踏みます。
List<T>への変換と追加の手順
この方法は、追加操作が直感的であり、コードの可読性が高いのが特徴です。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
string[] fruits = { "Apple", "Banana" };
// 1. 配列をListに変換
List<string> fruitList = new List<string>(fruits);
// 2. Addメソッドで要素を追加
fruitList.Add("Orange");
fruitList.Add("Grapes");
// 3. 必要に応じて配列に戻す
string[] updatedFruits = fruitList.ToArray();
// 結果の出力
Console.WriteLine(string.Join(", ", updatedFruits));
}
}
Apple, Banana, Orange, Grapes
なぜList<T>が効率的なのか
List<T>は内部的に配列を持っていますが、サイズが足りなくなった際に「現在の2倍のサイズ」をまとめて確保する戦略(指数関数的なリサイズ)をとります。
これにより、要素を追加するたびにコピーが発生するのを防ぎ、平均的な時間計算量を抑えています。
頻繁に要素が増減する場合は、最初から配列ではなくList<T>のままデータを保持することを検討してください。
LINQを使用した宣言的な要素追加
関数型プログラミングのスタイルを好む場合、または一時的に要素を加えた新しいシーケンスを取得したい場合は、LINQ(Language Integrated Query)のAppendメソッドやConcatメソッドが有効です。
Appendによる単一要素の追加
Appendは、元の配列を変更せずに、新しい要素を末尾に追加した「列挙可能な状態(IEnumerable)」を返します。
using System;
using System.Linq;
class Program
{
static void Main()
{
int[] original = { 10, 20, 30 };
// Appendで要素を追加した新しいシーケンスを作成
// この時点ではまだ配列ではない
var query = original.Append(40);
// 配列として確定させる
int[] result = query.ToArray();
Console.WriteLine($"元の配列のサイズ: {original.Length}");
Console.WriteLine($"新しい配列: {string.Join("-", result)}");
}
}
元の配列のサイズ: 3
新しい配列: 10-20-30-40
Concatによる配列同士の結合
複数の要素を一度に追加したい場合は、Concatメソッドを使用します。
int[] first = { 1, 2 };
int[] second = { 3, 4 };
int[] combined = first.Concat(second).ToArray();
LINQを使用した方法はコードが非常に簡潔になりますが、ToArray()を呼び出す際に新しい配列が生成されるため、パフォーマンス面ではList<T>の直接操作に劣る場合があることに留意してください。
C# 12以降の最新の書き方:コレクション式とスプレッド演算子
C# 12から導入された「コレクション式(Collection Expressions)」を利用すると、配列の作成と結合が驚くほどシンプルに記述できるようになりました。
スプレッド演算子(..)を使用することで、既存の配列を展開して新しい配列に組み込むことができます。
スプレッド演算子による要素追加の例
using System;
class Program
{
static void Main()
{
int[] baseArray = { 1, 2, 3 };
// C# 12のコレクション式による要素の追加
// 既存の配列を展開しつつ、新しい要素を追加
int[] extendedArray = [.. baseArray, 4, 5];
// 先頭への追加も容易
int[] prependedArray = [0, .. extendedArray];
Console.WriteLine("拡張された配列: " + string.Join(", ", extendedArray));
Console.WriteLine("先頭に追加された配列: " + string.Join(", ", prependedArray));
}
}
拡張された配列: 1, 2, 3, 4, 5
先頭に追加された配列: 0, 1, 2, 3, 4, 5
この記法は非常に直感的で、モダンなC#開発における標準的なスタイルになりつつあります。
コンパイラによって最適化されるため、手動でLINQを書くよりも効率的なコードが生成される傾向にあります。
パフォーマンス比較と使い分け
配列への要素追加には複数の方法がありますが、シチュエーションによって最適なものは異なります。
以下の表に各手法の特徴をまとめました。
| 手法 | 可読性 | パフォーマンス | 推奨されるケース |
|---|---|---|---|
| Array.Resize | 普通 | 低 | 非常に稀にしかサイズを変更しない場合 |
| List<T>への変換 | 高 | 高(複数回追加に強い) | 最も一般的。要素の増減が多い場合 |
| LINQ (Append) | 高 | 中 | 関数型チェーンの中で一時的に追加したい場合 |
| コレクション式 (C# 12) | 最高 | 高 | 最新環境で、既存の配列から新しい配列を作る場合 |
メモリ効率を最大化する「Capacity」の指定
もし追加する要素の数が事前に予測できる場合は、List<T>のコンストラクタでキャパシティ(Capacity)を明示的に指定してください。
これにより、内部的なリサイズ処理を最小限に抑え、最速のパフォーマンスを引き出すことができます。
// 100個の要素が入ることがわかっている場合
var list = new List<int>(100);
特殊なケース:Span<T>とMemory<T>の活用
高度なパフォーマンスが要求されるシステム開発や、大量のデータを扱う数値計算などでは、Span<T>やReadOnlySpan<T>を利用して配列の一部を効率的に操作することがあります。
ただし、これらは「既存のメモリ領域を参照する」ための仕組みであり、サイズを変更(追加)するためのものではありません。
もし「スタック領域に確保したメモリに要素を順次追加したい」といった特殊なニーズがある場合は、ValueStringBuilder(内部実装用)のような手法や、ArrayPool<T>による配列の再利用を検討することになります。
一般的なアプリケーション開発においては、List<T>または最新のコレクション式で十分対応可能です。
まとめ
C#の配列は固定長という制約がありますが、言語の進化とともに、柔軟かつ安全に要素を追加するための強力な手段が提供されてきました。
- 基本的な考え方: 配列はリサイズできないため、実際には新しい配列を作成している。
- 標準的な選択: 頻繁に要素を追加するなら
List<T>を使用する。 - 最新の記法: C# 12以降であれば コレクション式
[.. array, element]が最もスマート。 - 使い捨ての操作: LINQの
AppendやConcatで簡潔に記述。
開発しているプロジェクトのターゲットフレームワーク(.NET 8/9など)や、処理の実行頻度に合わせて、最適な手法を選択してください。
特にパフォーマンスが重要視されるループ処理内での配列リサイズは、システム全体のボトルネックになりやすいため、「適切なコレクションの選択」こそが、クリーンで高速なC#コードを書くための第一歩となります。
