C#を用いた開発において、配列の結合は非常に頻繁に行われる操作の一つです。
かつては LINQ や Array.Copy を用いるのが一般的でしたが、近年の .NET の進化により、パフォーマンスと可読性を両立させた新しい記述法が登場しています。
本記事では、初心者から上級者までが活用できる配列結合のテクニックを網羅し、実行速度やメモリ効率の観点から最適な手法を詳しく解説します。
C#における配列結合の重要性と進化
C#の配列(Array)は、一度作成するとそのサイズを変更することができない固定長のデータ構造です。
そのため、複数の配列を一つにまとめるには、結合後のサイズを確保した新しい配列を生成し、そこに各要素をコピーするというプロセスが必要になります。
かつては、このコピー処理を手動で実装するか、比較的低速な LINQ に頼るしかありませんでした。
しかし、C# 12で導入された「スプレッド要素」や、Span<T> によるメモリ操作の最適化により、現代の C# 開発ではより洗練されたコードが書けるようになっています。
1. 最も直感的な手法:LINQのConcatメソッド
System.Linq 名前空間に含まれる Concat メソッドを使用する方法は、コードの可読性が非常に高いことが特徴です。
LINQによる結合の仕組み
Concat メソッドは、2つのシーケンスを連結した IEnumerable<T> を返します。
これを配列として扱うには、最後に ToArray() を呼び出す必要があります。
using System;
using System.Linq;
class Program
{
static void Main()
{
int[] array1 = { 1, 2, 3 };
int[] array2 = { 4, 5, 6 };
// LINQを使用して配列を結合
int[] result = array1.Concat(array2).ToArray();
// 結果の出力
Console.WriteLine(string.Join(", ", result));
}
}
1, 2, 3, 4, 5, 6
メリットとデメリット
メリットは、何といってもその「読みやすさ」です。
メソッドチェーンで記述できるため、3つ以上の配列を結合する場合も簡潔に書けます。
一方で、パフォーマンス面でのオーバーヘッドが最大のデメリットです。
内部的に列挙子(Enumerator)を生成し、動的なリサイズを伴う可能性があるため、大量のデータを扱うループ内での使用は避けるべきです。
2. パフォーマンス重視の古典的手法:Array.Copy
実行速度を最優先する場合、古くから使われている Array.Copy メソッドが非常に有効です。
この手法は、あらかじめ合算したサイズの配列を確保し、メモリブロックを物理的にコピーします。
Array.Copy の実装例
using System;
class Program
{
static void Main()
{
string[] first = { "Apple", "Banana" };
string[] second = { "Cherry", "Date" };
// 1. 新しい配列のサイズを決定
string[] combined = new string[first.Length + second.Length];
// 2. 最初の配列をコピー
Array.Copy(first, 0, combined, 0, first.Length);
// 3. 次の配列をコピー(コピー先の開始インデックスに注意)
Array.Copy(second, 0, combined, first.Length, second.Length);
Console.WriteLine(string.Join(", ", combined));
}
}
Apple, Banana, Cherry, Date
なぜ Array.Copy は速いのか
Array.Copy は、.NET ランタイムレベルで最適化された メモリの直接コピー(memmoveに近い処理)を行うため、LINQ よりも圧倒的に高速です。
特にプリミティブ型(int, byteなど)の配列では、その差が顕著に現れます。
ただし、記述が冗長になりやすく、インデックス計算のミス(オフオフバイワンエラーなど)を招くリスクがあります。
3. モダンC#の決定版:スプレッド要素 (C# 12以降)
C# 12からは、コレクション式(Collection Expressions)とともに「スプレッド要素(Spread element)」が導入されました。
これは、.. 記法を用いて配列を他の配列内に展開する手法です。
スプレッド要素によるスマートな結合
using System;
class Program
{
static void Main()
{
int[] part1 = { 10, 20 };
int[] part2 = { 30, 40 };
int[] part3 = { 50, 60 };
// C# 12のコレクション式とスプレッド要素を使用
int[] combined = [..part1, ..part2, ..part3];
Console.WriteLine(string.Join(", ", combined));
}
}
10, 20, 30, 40, 50, 60
モダンな書き方の利点
この記法は、可読性とパフォーマンスのバランスが最も優れています。
コンパイラはこのコードを解釈し、背後で最適なメモリ確保とコピー処理を生成します。
LINQ のような列挙子によるオーバーヘッドがなく、Array.Copy に近い速度を保ちつつ、直感的な記述が可能です。
今後の C# 開発における標準的な選択肢と言えるでしょう。
4. メモリ効率を極める:Span<T> と ReadOnlySpan<T>
パフォーマンスが極めて重要なシステムや、ガベージコレクション(GC)の発生を抑えたい場合には、Span<T> を活用します。
Spanを用いた高度な結合
Span<T> はスタック上に確保可能なメモリのビューであり、ヒープへの割り当てを最小限に抑えることができます。
using System;
class Program
{
static void Main()
{
int[] arrA = { 1, 2 };
int[] arrB = { 3, 4 };
// 新しい配列を確保
int[] result = new int[arrA.Length + arrB.Length];
// Spanとして操作
Span<int> span = result.AsSpan();
// 各配列をスライスに対してコピー
arrA.CopyTo(span.Slice(0, arrA.Length));
arrB.CopyTo(span.Slice(arrA.Length));
Console.WriteLine(string.Join(", ", result));
}
}
1, 2, 3, 4
CopyTo や TryCopyTo メソッドは、境界チェックが厳密に行われるため、安全かつ高速です。
特に、大規模なバイナリデータの処理や、ネットワークパケットの構築において Spanの活用は必須スキルとなっています。
5. 手法別のパフォーマンス比較
各手法がどの程度のパフォーマンス差を生むのか、一般的な傾向を以下の表にまとめました。
| 手法 | 可読性 | 実行速度 | メモリ消費 | 推奨されるケース |
|---|---|---|---|---|
| LINQ (Concat) | 最高 | 低い | 多い | 小規模な配列、読みやすさ重視 |
| Array.Copy | 低い | 非常に高い | 最小 | 大規模データ、レガシーコード |
| List<T>.AddRange | 普通 | 中程度 | やや多い | 結合数が動的に変わる場合 |
| スプレッド要素 (..) | 高い | 高い | 最小 | 現代の標準的な開発 |
| Span<T> | 普通 | 最高 | 最小 | 高負荷な処理、リアルタイムシステム |
注意点: LINQ は便利ですが、ループ内で数万回呼び出すような処理では、アプリケーション全体のパフォーマンスを著しく低下させる原因になります。
6. 特殊なケース:動的な結合と List<T>
結合したい配列の数が事前に決まっていない、あるいは条件によって追加する配列が変わる場合は、List<T> をクッションとして利用するのが最適です。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
var list = new List<int>();
int[] data1 = { 1, 2 };
int[] data2 = { 3, 4 };
list.AddRange(data1);
list.AddRange(data2);
// 最後に配列へ変換
int[] result = list.ToArray();
Console.WriteLine(string.Join(", ", result));
}
}
List<T> は内部的に動的な配列を持っており、容量(Capacity)が不足すると自動的に拡張されます。
あらかじめ必要な総数が予測できる場合は、new List<int>(totalSize) のように初期容量を指定することで、再割り当てのコストを削減できます。
7. 実践的なTips:Nullや空の配列への対処
実際の開発では、結合対象の配列が null である可能性を考慮しなければなりません。
Null条件演算子とスプレッド要素の組み合わせ
C# 12のスプレッド要素は非常に強力ですが、対象が null の場合は例外が発生します。
これを防ぐには、以下のようなガード処理が推奨されます。
int[]? source = GetSourceOrNull();
// sourceがnullなら空配列として扱う
int[] combined = [..source ?? Array.Empty<int>(), ..otherArray];
Array.Empty<T>() は、型ごとにキャッシュされた空の配列を返すため、new int[0] を生成するよりもメモリ効率が良くなります。
8. プリミティブ型に特化した Buffer.BlockCopy
もし扱うデータが byte, int, double などのプリミティブ型であり、かつ数メガバイトを超えるような巨大な配列を結合する場合、Buffer.BlockCopy という選択肢もあります。
using System;
class Program
{
static void Main()
{
int[] src1 = { 1, 2, 3 };
int[] src2 = { 4, 5, 6 };
int[] dest = new int[src1.Length + src2.Length];
// バイト単位でのコピー(intは4バイトなので注意が必要)
Buffer.BlockCopy(src1, 0, dest, 0, src1.Length * sizeof(int));
Buffer.BlockCopy(src2, 0, dest, src1.Length * sizeof(int), src2.Length * sizeof(int));
Console.WriteLine(string.Join(", ", dest));
}
}
Buffer.BlockCopy は、配列の型を考慮せず「メモリ上のバイト列」としてコピーを行います。
そのため、オブジェクトの参照を含む配列には使用できませんが、数値データの大量処理においては最速の手段となり得ます。
まとめ
C#で配列を結合する手法は、時代とともに進化してきました。
現在の開発において最適な選択肢を整理すると以下のようになります。
- 通常時: C# 12の「スプレッド要素
[..a, ..b]」を使用する。これが最もバランスに優れています。 - 古いバージョンや特定のフレームワーク:
Array.Copyを使用してパフォーマンスを担保する。 - 可読性最優先の使い捨てコード: LINQ の
Concat().ToArray()を使用する。 - 極限の最適化:
Span<T>やBuffer.BlockCopyを検討する。
開発しているアプリケーションの要件( .NET バージョン、データ量、実行環境)に合わせて、これらの手法を使い分けてください。
特に、最新の C# 機能を積極的に取り入れることで、コードの健全性と実行速度を同時に向上させることが可能です。
配列操作は基本中の基本ですが、その裏側にあるメモリ管理や言語仕様を理解することで、より高品質なプログラムを記述できるようになります。
ぜひ、本記事で紹介した手法を実際のプロジェクトで試してみてください。






