C#を利用したアプリケーション開発において、2つの関連するデータを1つの単位として管理したい場面は頻繁に発生します。
例えば、座標系における X と Y、商品の「ID」と「名称」、あるいは関数の戻り値として2つの値を同時に返したい場合などが挙げられます。
C++のような言語には std::pair が標準で存在しますが、C#には「Pair」という名前の単一のクラスは存在せず、用途に応じて複数の選択肢から最適な手法を選ぶ必要があります。
本記事では、C#で「ペアのリスト」を扱うための主要な手法である KeyValuePair、Tuple / ValueTuple、そしてモダンなC#の象徴である record について、それぞれの特徴や使い分け、パフォーマンスの差異を詳しく解説します。
KeyValuePairを用いたペアの管理
System.Collections.Generic.KeyValuePair<TKey, TValue> は、元々 Dictionary クラスの要素として設計された構造体です。
名前の通り、「キー」と「値」のペアを保持することに特化しています。
KeyValuePairの特徴と用途
KeyValuePair の最大の特徴は、プロパティ名が Key と Value に固定されていることです。
これは、データの本質が「何かしらの一意識別子(Key)」と「それに関連付けられた実体(Value)」である場合に非常に適しています。
また、KeyValuePair は構造体(値型)であるため、大量のデータを扱う際にヒープメモリを圧迫しにくいというメリットがあります。
ただし、一度生成すると Key や Value を後から変更することはできない「不変(Immutable)」な設計となっています。
KeyValuePairの使用例
以下のコードは、文字列のキーと数値の値をペアにしてリストで管理する例です。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// KeyValuePairのリストを初期化
List<KeyValuePair<string, int>> scoreList = new List<KeyValuePair<string, int>>
{
new KeyValuePair<string, int>("Alice", 85),
new KeyValuePair<string, int>("Bob", 92),
new KeyValuePair<string, int>("Charlie", 78)
};
// 要素の追加
scoreList.Add(new KeyValuePair<string, int>("Dave", 88));
// データの参照
foreach (var pair in scoreList)
{
Console.WriteLine($"Name: {pair.Key}, Score: {pair.Value}");
}
}
}
Name: Alice, Score: 85
Name: Bob, Score: 92
Name: Charlie, Score: 78
Name: Dave, Score: 88
KeyValuePair は、LINQの ToDictionary メソッドなどとの親和性が高く、一時的に辞書形式のデータをリストとして保持したい場合に非常に便利です。
しかし、「Key」と「Value」という名前がデータの意味(例:XとY、始点と終点など)にそぐわない場合、コードの可読性が低下するという欠点があります。
TupleとValueTupleによる柔軟なペア操作
C# 7.0以降、より柔軟に複数の値を扱う仕組みとして ValueTuple(値タプル) が導入されました。
それ以前から存在する System.Tuple クラス(参照型タプル)と比較して、現在では ValueTuple を使用するのが一般的です。
System.Tuple(古い形式)
旧来の Tuple クラスは参照型であり、Item1, Item2 といった名前でアクセスします。
// 参照型タプルの例(現在はあまり推奨されない)
var oldTuple = new Tuple<string, int>("Apple", 100);
Console.WriteLine(oldTuple.Item1);
参照型であるため、インスタンス化のたびにメモリの割り当て(アロケーション)が発生し、大量のリストを作成するとパフォーマンスに影響を与える可能性があります。
ValueTuple(推奨される現代的な形式)
ValueTupleは、C#においてペアを扱う最も軽量で強力な方法の一つです。
構造体であるため軽量であり、かつ「要素に名前を付けられる」という画期的な特徴を持っています。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// 括弧 () を使った簡潔な宣言と、要素への命名
List<(string Name, int Price)> productList = new List<(string Name, int Price)>();
productList.Add(("Apple", 150));
productList.Add(("Banana", 200));
productList.Add(("Orange", 120));
// 名前付きプロパティでアクセス可能
foreach (var product in productList)
{
Console.WriteLine($"Product: {product.Name}, Price: {product.Price}円");
}
// 分解コンストラクトを利用した取得
var (firstName, firstPrice) = productList[0];
Console.WriteLine($"最初の要素: {firstName} ({firstPrice})");
}
}
Product: Apple, Price: 150円
Product: Banana, Price: 200円
Product: Orange, Price: 120円
最初の要素: Apple (150)
ValueTuple の利点は、Item1 などの汎用的な名前ではなく、Name や Price といった 文脈に合わせた名前 を型定義の段階で付与できる点にあります。
これにより、クラスを定義するほどではないが、意味のあるデータの組を扱いたい場合に最適な選択肢となります。
recordを用いた型安全なペアの定義
C# 9.0で導入された record(レコード型)は、データの保持を主目的とする型を定義するのに最適です。
ペアのリストを扱う際、そのペアがアプリケーション全体で重要な意味を持つ場合、record を使うことが最も推奨されます。
recordのメリット
record は、デフォルトで 値ベースの比較(Value-based equality) をサポートしています。
通常のクラスでは、2つのインスタンスが同じ値を持っていても参照が異なれば「別物」と判定されますが、レコード型であれば「内容が同じなら等しい」と判定されます。
これは、ペアの重複チェックやリスト内の検索において非常に強力です。
recordによるリスト管理の実装
using System;
using System.Collections.Generic;
// 1行で定義可能な位置指定レコード
public record GeoPoint(double Latitude, double Longitude);
class Program
{
static void Main()
{
// recordのリストを作成
var points = new List<GeoPoint>
{
new GeoPoint(35.6895, 139.6917), // 東京
new GeoPoint(34.6937, 135.5023), // 大阪
new GeoPoint(35.0116, 135.7681) // 京都
};
// データの表示
foreach (var p in points)
{
Console.WriteLine($"緯度: {p.Latitude}, 経度: {p.Longitude}");
}
// 値ベースの比較のデモ
var tokyo1 = new GeoPoint(35.6895, 139.6917);
var tokyo2 = new GeoPoint(35.6895, 139.6917);
Console.WriteLine($"等価性チェック: {tokyo1 == tokyo2}"); // Trueが返る
}
}
緯度: 35.6895, 経度: 139.6917
緯度: 34.6937, 135.5023
緯度: 35.0116, 135.7681
等価性チェック: True
record は内部的にクラス(または構造体)として振る舞いますが、ボイラープレートコード(プロパティ定義やコンストラクタなど)を記述する必要がなく、非常に簡潔です。
また、with 式を用いることで、一部の値を変更した新しいペアのコピーを簡単に作成できるなど、不変性を保ったプログラミングを強力に支援します。
各手法の比較と使い分けの指針
これまでに紹介した3つの主要な手法について、どのような基準で選択すべきかを以下の表にまとめました。
| 手法 | 型の種類 | プロパティ名 | 主な用途 | 特徴 |
|---|---|---|---|---|
| KeyValuePair | 構造体 | Key, Value | 辞書データの処理 | 標準ライブラリとの親和性が高い。 |
| ValueTuple | 構造体 | 自由(任意) | 一時的なデータの組 | 軽量、宣言が非常に楽。戻り値に最適。 |
| record | クラス / 構造体 | 自由(固定) | 意味のあるデータ定義 | 等価性判定が強力。永続的なデータ管理。 |
選択に迷った際は、以下のガイドラインを参考にしてください。
- 一時的なスコープ(メソッド内など)で手軽にペアを作りたい場合
この場合はValueTupleが最適です。
型定義を別途作成する必要がなく、コードの記述量を最小限に抑えられます。
- 複数のメソッドをまたいで受け渡される「意味のあるデータ」の場合
この場合はrecordを定義すべきです。
プロパティ名が型として固定されるため、チーム開発においても「何を表すデータか」が明確になります。
- Dictionaryから抽出したデータをそのまま扱いたい場合
既存のライブラリや
Dictionaryクラスとの連携が前提であれば、KeyValuePairを使うのが最も自然です。
リスト操作の応用:ソートと検索
ペアのリストを作成した後、特定の要素に基づいてソート(並び替え)を行いたいケースは多いでしょう。
LINQを活用することで、どの手法を使っていても柔軟に操作が可能です。
ValueTupleのリストをソートする
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
var students = new List<(string Name, int Score)>
{
("Alice", 85),
("Bob", 92),
("Charlie", 78)
};
// スコアの降順でソート
var sorted = students.OrderByDescending(s => s.Score).ToList();
foreach (var s in sorted)
{
Console.WriteLine($"{s.Name}: {s.Score}");
}
}
}
Bob: 92
Alice: 85
Charlie: 78
このように、名前付きの ValueTuple を使用していれば、LINQのラムダ式内でもプロパティ名(s.Score)が補完されるため、ミスが少なくなります。
パフォーマンスとメモリの考慮事項
大量のペア(数万件以上)をリストに格納する場合、値型(struct)か参照型(class)か の違いが顕著に現れます。
ValueTuple や KeyValuePair は値型であるため、リストの要素として直接メモリ上に配置されます。
これにより、ガベージコレクション(GC)の負荷を軽減できる可能性があります。
一方で、値型のサイズが大きくなりすぎると、引数としての受け渡しやコピーのコストが増大します。
一方、record(特にデフォルトの参照型レコード)は、各要素がヒープ領域に確保されます。
リストにはその「参照(ポインタ)」が格納されるため、大きなデータを扱う際のコピーコストは低いですが、要素数が多いほどGCの対象となるオブジェクトが増える点に注意が必要です。
パフォーマンスが極めて重要なシステムでは、record struct を使用することで、recordの利便性と構造体の軽量さを両立させるという選択肢もあります。
// record struct(値型)として定義
public record struct FastPair(int ID, double Value);
まとめ
C#でペアのリストを扱う方法は、言語の進化とともに洗練されてきました。
かつては KeyValuePair や Tuple クラスが主流でしたが、現在のモダンなC#開発においては、「一時的な利用ならValueTuple」「永続的・構造的なデータならrecord」 という使い分けがベストプラクティスとなっています。
- KeyValuePair:辞書操作やレガシーなAPIとの親和性を重視する場合。
- ValueTuple:軽量さ、記述の簡潔さ、そしてメソッド内での一時的な集計作業など。
- record:データの等価性を重視し、名前の付いた明確な型として定義したい場合。
それぞれの特徴を理解し、プロジェクトの規模や用途に合わせて最適な手法を選択することで、保守性が高くパフォーマンスに優れたコードを実現できるでしょう。
最新のC#機能を積極的に活用し、クリーンなデータ構造を設計してください。






