C#において、複数のコレクションやリストを一つにまとめる操作は、データ処理の基本でありながら、パフォーマンスやコードの可読性に直結する重要な要素です。

かつては AddRange や LINQ の Concat が主流でしたが、近年のアップデートにより、直感的かつ高速な「スプレッド演算子」が導入されるなど、選択肢はさらに広がっています。

本記事では、C#のプロフェッショナルが知っておくべき Listを結合するあらゆる手法 を網羅します。

各メソッドの内部挙動、メモリ効率、そして最新のC# 12で導入されたコレクション式による革新的な書き方まで、実務で即座に役立つ知識を詳しく解説していきます。

AddRangeメソッド:既存のリストを拡張する

AddRangeメソッド は、すでに存在する List<T> インスタンスの末尾に、別のコレクションを追加するための最も標準的な手法です。

このメソッドの最大の特徴は、「破壊的な変更」 である点にあります。

つまり、元のリストを直接書き換えて要素を増やすため、新しいリストを生成するコストを抑えることができます。

AddRangeの基本的な使い方

AddRange は、引数に IEnumerable<T> を受け取ります。

そのため、リストだけでなく、配列やスタック、キューなども結合のソースとして指定可能です。

C#
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // 元のリストを作成
        List<string> fruits = new List<string> { "Apple", "Banana" };
        
        // 追加したいコレクション(配列でもOK)
        string[] moreFruits = { "Orange", "Grape" };

        // AddRangeで結合
        fruits.AddRange(moreFruits);

        // 結果を出力
        foreach (var fruit in fruits)
        {
            Console.WriteLine(fruit);
        }
    }
}
実行結果
Apple
Banana
Orange
Grape

内部挙動とキャパシティの管理

AddRange を使用する際、内部では Capacity(容量)の自動調整 が行われます。

List<T> は内部的に配列を保持していますが、要素数が現在の容量を超えると、より大きな配列が再確保され、既存の要素がコピーされます。

もし追加する要素数が事前にわかっている場合、あらかじめ Capacity を設定しておくことで、再確保の回数を減らし、実行速度を向上させることが可能です。

大量のデータを扱う際には、「不必要なリサイズを避ける」 ことが最適化の定石となります。

Enumerable.Concatメソッド:LINQによる非破壊的な結合

System.Linq 名前空間で提供される Concatメソッド は、元のリストを変更せずに、2つのシーケンスを連結した新しいシーケンスを返します。

これは関数型プログラミングのスタイルに近く、副作用を避けたい場合に非常に有効です。

Concatの基本的な使い方

Concat は遅延評価(Lazy Evaluation)を行うため、実際に要素が必要になるまで結合処理は実行されません。

リストとして結果を確定させたい場合は、最後に ToList() を呼び出す必要があります。

C#
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<int> first = new List<int> { 1, 2, 3 };
        List<int> second = new List<int> { 4, 5, 6 };

        // 非破壊的に結合。元のfirstとsecondは変更されない
        IEnumerable<int> combined = first.Concat(second);

        // リストとして具体化
        List<int> resultList = combined.ToList();

        Console.WriteLine($"Result Count: {resultList.Count}");
        Console.WriteLine($"Original First Count: {first.Count}");
    }
}
実行結果
Result Count: 6
Original First Count: 3

Concatのメリットと注意点

Concat のメリットは、コードの可読性が高く、メソッドチェーンを利用して複数の結合やフィルタリングを連続して記述できる点にあります。

しかし、パフォーマンス面でのオーバーヘッド には注意が必要です。

ToList() を呼び出した瞬間、新しいリストが生成され、全要素がコピーされます。

また、結合対象が多数ある場合に Concat を何重にも重ねると、イテレータのネストが深くなり、列挙時のパフォーマンスが低下する恐れがあります。

3つ以上のリストを結合する場合は、後述するスプレッド演算子や、新しいリストを作成して AddRange を繰り返す方が効率的です。

スプレッド演算子(..):C# 12からのモダンな記述

C# 12から導入された コレクション式(Collection Expressions) とその中で使える スプレッド演算子(..) は、リストの結合における最新かつ最も推奨される方法の一つです。

JavaScriptなどの言語で馴染みのある記法がC#でも利用可能になり、宣言的で美しいコードが書けるようになりました。

スプレッド演算子の基本的な使い方

角括弧 [] を使用してリストを定義し、その中で既存のコレクションの前に .. を付けることで、その要素を展開して結合できます。

C#
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<int> part1 = [10, 20];
        List<int> part2 = [30, 40];

        // スプレッド演算子を使用して新しいリストを生成
        List<int> combined = [.. part1, .. part2, 50, 60];

        foreach (var item in combined)
        {
            Console.Write($"{item} ");
        }
    }
}
実行結果
10 20 30 40 50 60

なぜスプレッド演算子が優れているのか

スプレッド演算子による結合は、単なるシンタックスシュガー(書き方の工夫)に留まりません。

コンパイラが背後で 最適な結合ロジックを自動選択 してくれます。

例えば、結合対象のサイズが事前にわかっている場合、コンパイラは内部的に適切なキャパシティを確保してからコピーを行うコードを生成します。

これにより、開発者が手動で最適化を行わなくても、Concat よりも高速で、AddRange に匹敵するパフォーマンスを得ることができます。

また、「読みやすさ」 という点でも、どの変数がどの位置に展開されるかが一目でわかるため、保守性が飛躍的に向上します。

InsertRangeメソッド:特定の位置に差し込む

リストの末尾ではなく、特定の位置(インデックス) に別のリストを挿入したい場合は、InsertRange メソッドを使用します。

InsertRangeの基本的な使い方

C#
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<string> baseList = new List<string> { "Start", "End" };
        List<string> middleItems = new List<string> { "Middle1", "Middle2" };

        // インデックス1("End"の前)に挿入
        baseList.InsertRange(1, middleItems);

        Console.WriteLine(string.Join(", ", baseList));
    }
}
実行結果
Start, Middle1, Middle2, End

パフォーマンスに関する考慮事項

InsertRange は非常に便利ですが、挿入位置が先頭に近ければ近いほど、後続の要素を全て後ろにずらすコスト が発生します。

大規模なリストに対して、ループ内で頻繁に InsertRange を呼び出すような処理は避け、可能な限り一度の結合で済ませるか、データ構造自体を見直す(例えば LinkedList<T> を検討するなど)ことが重要です。

パフォーマンスの比較と最適な選択基準

どの結合方法を選択すべきかは、対象となるデータの規模や、元のリストを保持する必要があるかどうかによって決まります。

ここでは、主要な3手法の比較をまとめます。

手法可変性速度読みやすさ推奨シーン
AddRange破壊的(元のリストを変更)高速普通既存リストにデータを追加する場合
Concat非破壊(新しいシーケンス)低速(遅延評価)高いLINQチェーンの一部として扱う場合
スプレッド演算子非破壊(新しいリスト)高速(最適化あり)非常に高い新しいリストを直感的に作成する場合

大量データを扱う場合のテクニック

数万件規模の要素を持つリストを結合する場合、最もボトルネックになるのは「メモリの再確保」と「コピー操作」です。

以下のコードのように、あらかじめ必要な総数を計算して Capacity を確保する手法は、今でも非常に有効な最適化手段です。

C#
// 理論上最も高速な手動結合の例
var totalCount = listA.Count + listB.Count;
var result = new List<int>(totalCount); // 最初に必要なサイズを確保
result.AddRange(listA);
result.AddRange(listB);

最新のC#であれば、これと同様の最適化をコンパイラに任せられる スプレッド演算子 を第一選択とし、既存のインスタンスを再利用する必要がある場面でのみ AddRange を使うのが、2020年代後半のスタンダードな開発スタイルと言えるでしょう。

重複を除外して結合する方法:Unionメソッド

単に結合するだけでなく、「重複した要素を排除して一意にしたい」 という要件がある場合は、LINQの Union メソッドを使用します。

Unionの基本的な使い方

Union は、数学的な「和集合」を求めます。

内部的にハッシュセットを使用するため、要素数が多い場合でも、手動で重複チェックを行うより効率的に動作します。

C#
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<int> list1 = new List<int> { 1, 2, 3 };
        List<int> list2 = new List<int> { 3, 4, 5 };

        // 3が重複しているが、結果には一つだけ含まれる
        var unionList = list1.Union(list2).ToList();

        Console.WriteLine(string.Join(", ", unionList));
    }
}
実行結果
1, 2, 3, 4, 5

ただし、Union は要素の比較に GetHashCodeEquals を使用します。

自作のクラス(オブジェクト)を格納したリストを結合する場合は、適切にこれらをオーバーライドするか、IEqualityComparer<T> を実装した比較用クラスを渡す必要がある点に注意してください。

特殊なケース:Nullを考慮した結合

実務のプログラミングで頻繁に遭遇するのが、結合対象のリストが null である可能性です。

AddRangenull を渡すと ArgumentNullException がスローされます。

これを防ぐための安全な結合パターンとして、「空合体演算子(??)」 の活用が推奨されます。

C#
List<int>? optionalList = GetOptionalData(); // nullが返る可能性がある

// nullなら空のリストとして扱うことでエラーを回避
var safeList = [.. baseList, .. (optionalList ?? [])];

このように記述することで、データの有無にかかわらず安全かつ簡潔に結合処理を完結させることができます。

まとめ

C#におけるListの結合は、言語の進化とともに、より「簡潔」で「高性能」な方向へと進化してきました。

  • 既存のリストを拡張したい場合 は、メモリ効率に優れた AddRange を使用する。
  • 新しいリストを生成しつつ、可読性を重視したい場合 は、最新の スプレッド演算子(..) を第一選択とする。
  • LINQのクエリ操作の一部として結合したい場合 は、Concat を利用する。
  • 重複を排除したい場合 は、Union を活用する。

状況に応じてこれらの手法を使い分けることで、バグが少なく、かつパフォーマンスの高いアプリケーションを構築することができます。

特にC# 12以降の環境であれば、コレクション式を積極的に採用し、コードの意図を明確に保つことを心がけましょう。