C#のタプルとは?使い方・戻り値・ValueTuple・匿名型との違いを初心者向けに解説
はじめに
C#でプログラムを書いていると、「メソッドから計算結果と成功可否を同時に返したい」「名前と年齢のような関連する値をひとまとめにしたい」といった場面があります。
このようなときに便利なのが、C#のタプルです。タプルを使うと、新しいクラスを定義しなくても、異なる型の複数の値をひとつのデータとして扱えます。
(string Name, int Age) user = ("田中", 25);Console.WriteLine(user.Name);Console.WriteLine(user.Age);
C# 7.0以降では、ValueTupleを利用した簡潔なタプル構文が導入され、メソッドの戻り値や変数の分解にも使いやすくなりました。
この記事では、C#のタプルの基本的な使い方から、戻り値、分解、ValueTupleとSystem.Tupleの違い、匿名型やクラスとの使い分け、比較、JSONシリアライズ時の注意点まで解説します。
1. C#のタプルとは
1-1. 複数の値をひとつにまとめられるデータ構造
タプルとは、複数の値を決められた順序でひとつにまとめるデータ構造です。
配列やList<T>も複数の値を格納できますが、基本的にはすべての要素が同じ型でなければなりません。一方、タプルには異なる型の値を格納できます。
(string, int, bool) user = ("佐藤", 30, true);このタプルには、次の3つの値が格納されています。
string型の名前int型の年齢bool型の有効状態
タプルは各要素の型と順序が決まっているため、コンパイル時に型チェックが行われます。
1-2. C# 7.0以降で使いやすくなったValueTuple
C#には以前からSystem.Tupleがありましたが、C# 7.0以降ではSystem.ValueTupleを基盤とする新しいタプル構文が利用できるようになりました。
var user = (Name: "鈴木", Age: 28);C# 7.0以降のタプルには、次のような特徴があります。
丸括弧だけで簡潔に宣言できる
要素に名前を付けられる
分解して複数の変数に代入できる
メソッドの戻り値として記述しやすい
値型として扱われる
一般的に、現在のC#で「タプル」と呼ぶ場合は、このValueTupleを使った構文を指します。
1-3. タプルを使うメリットとデメリット
タプルの主なメリットは、複数の値を扱うためだけにクラスや構造体を定義する必要がないことです。
たとえば、最小値と最大値を返すだけであれば、専用クラスを作るよりもタプルを使ったほうが簡潔です。
static (int Min, int Max) GetRange(){return (10, 100);}タプルのメリットは次のとおりです。
少ないコードで複数の値をまとめられる
メソッドから複数の値を返しやすい
要素に名前を付ければ意味を表現できる
分解によって値を受け取りやすい
一方、次のようなデメリットがあります。
要素数が増えると読みづらくなる
データの役割や制約を型として表現しにくい
バリデーションやメソッドを持たせられない
公開APIで多用すると仕様変更に弱くなる
要素名は型そのものを構成する名前ではない
タプルは便利ですが、クラスやレコードを完全に置き換えるものではありません。
1-4. タプルが適しているケース・適していないケース
タプルは、一時的に関連する値をまとめたい場合に適しています。
具体的には、次のようなケースです。
メソッドから2~3個程度の値を返す
LINQの途中処理で一時的なデータを作る
辞書の複合キーを作る
パターンマッチングで複数条件を判定する
ローカル処理だけで使う小さなデータをまとめる
一方、次のようなケースでは、クラスやレコードなどの独自型が適しています。
要素数が多い
データを複数の場所で共有する
公開APIの引数や戻り値として長期間使用する
入力値の検証が必要
データに関連するメソッドを定義したい
継承やインターフェイスの実装が必要
JSONの項目名を安定させたい
「処理の中だけで一時的に使うデータか」「アプリケーション上で明確な意味を持つデータか」を基準に判断するとよいでしょう。
2. C#でタプルを作成する基本的な使い方
2-1. 丸括弧を使ってタプルを宣言する方法
C#のタプルは、複数の値を丸括弧で囲んで作成します。
var user = ("山田", 20);この例では、string型とint型の2要素を持つタプルが作成されます。
タプルリテラルは、原則として2個以上の要素を記述します。単に値を丸括弧で囲んだだけでは、1要素のタプルにはなりません。
var number = (10); // int型2-2. 型を明示してタプルを宣言する方法
タプルは、各要素の型を明示して宣言できます。
(string, int) user = ("山田", 20);要素名もあわせて指定できます。
(string Name, int Age) user = ("山田", 20);型を明示すると、変数がどのようなデータを保持するのかがコード上で分かりやすくなります。
(string ProductName, decimal Price, int Stock) product =("キーボード", 4980m, 15);2-3. varを使ってタプルを宣言する方法
右辺から型を推論できる場合は、varを使えます。
var user = ("山田", 20);要素名を付けたい場合は、右辺に名前を記述します。
var user = (Name: "山田", Age: 20);Console.WriteLine(user.Name);Console.WriteLine(user.Age);
既存の変数を使ってタプルを作る場合、変数名が要素名として推論されることもあります。
string name = "山田";int age = 20;var user = (name, age);
Console.WriteLine(user.name);Console.WriteLine(user.age);
ただし、明示的に名前を付けたほうが、意図が伝わりやすい場合もあります。
var user = (Name: name, Age: age);2-4. Item1・Item2で要素を取得する方法
要素名を指定していないタプルでは、Item1、Item2というプロパティのような形式で各要素にアクセスできます。
var user = ("山田", 20);Console.WriteLine(user.Item1);Console.WriteLine(user.Item2);
3個目以降も、Item3、Item4のようにアクセスします。
var product = ("マウス", 2980m, true);Console.WriteLine(product.Item1);Console.WriteLine(product.Item2);Console.WriteLine(product.Item3);
ただし、Item1やItem2だけでは値の意味が分かりにくいため、実際のコードでは名前付きタプルを使うのがおすすめです。
2-5. 要素に名前を付けて取得する方法
タプルの要素には、意味のある名前を付けられます。
var user = (Name: "山田", Age: 20);Console.WriteLine(user.Name);Console.WriteLine(user.Age);
型を明示する場合も、要素名を付けられます。
(string Name, int Age) user = ("山田", 20);名前付きタプルにしても、内部的な要素にはItem1やItem2があります。ただし、通常は名前を使ってアクセスしたほうが読みやすくなります。
Console.WriteLine(user.Name);// Console.WriteLine(user.Item1); も同じ要素を示す2-6. タプルの要素を書き換える方法
ValueTupleは値型ですが、読み取り専用でない変数に格納されている要素は書き換えられます。
var user = (Name: "山田", Age: 20);user.Name = "佐藤";user.Age = 21;
Console.WriteLine(user.Name);Console.WriteLine(user.Age);
一方、匿名型のプロパティは基本的に作成後に変更できません。この点は、タプルと匿名型の大きな違いです。
ただし、readonlyフィールドなど、タプル自体が読み取り専用の場所に格納されている場合は、要素を直接変更できません。
3. タプルをメソッドの戻り値として使う方法
3-1. 複数の値を戻り値として返す基本構文
タプルを使うと、メソッドから複数の値をひとつの戻り値として返せます。
static (int, int) GetNumbers(){return (10, 20);}呼び出し側では、戻り値をタプル変数に代入します。
var result = GetNumbers();Console.WriteLine(result.Item1);Console.WriteLine(result.Item2);
従来は、複数の値を返すためにクラスを作成したり、out引数を使ったりする必要がありました。タプルを使えば、より自然な戻り値として表現できます。
3-2. 名前付きタプルを戻り値にする方法
戻り値のタプルには、要素名を付けるのがおすすめです。
static (int Min, int Max) GetRange(){return (10, 100);}呼び出し側では、戻り値の要素名をそのまま利用できます。
var range = GetRange();Console.WriteLine(range.Min);Console.WriteLine(range.Max);
戻り値の意味が明確になるため、Item1やItem2を使うよりも可読性が高くなります。
戻す値にも名前を明示できます。
static (int Min, int Max) GetRange(){int min = 10;int max = 100;return (Min: min, Max: max);
}
3-3. 戻り値のタプルを変数に代入する方法
メソッドから返されたタプルは、通常の変数と同じように受け取れます。
static (string Name, int Age) GetUser(){return ("高橋", 32);}var user = GetUser();
Console.WriteLine($"名前: {user.Name}");Console.WriteLine($"年齢: {user.Age}");
型を明示して受け取ることもできます。
(string Name, int Age) user = GetUser();要素の型と順序が一致していれば、代入できます。
3-4. 分解(Deconstruction)で戻り値を受け取る方法
タプルは、分解を使って複数の変数に直接代入できます。
static (string Name, int Age) GetUser(){return ("高橋", 32);}var (name, age) = GetUser();
Console.WriteLine(name);Console.WriteLine(age);
型を明示することも可能です。
(string name, int age) = GetUser();分解を使うと、一度タプル変数を作成してから要素を取り出す必要がありません。
3-5. 不要な戻り値を破棄変数で受け取る方法
タプルの一部だけが必要な場合は、破棄を表す_を使用できます。
static (int Min, int Max) GetRange(){return (10, 100);}var (_, max) = GetRange();
Console.WriteLine(max);
この例では最小値を使用せず、最大値だけを受け取っています。
複数の不要な値を破棄することもできます。
static (string Name, int Age, string Address) GetUser(){return ("高橋", 32, "東京都");}var (name, _, _) = GetUser();
Console.WriteLine(name);
_を使うことで、どの戻り値を使用しないのかを明確にできます。
3-6. asyncメソッド・Taskでタプルを返す方法
非同期メソッドからタプルを返す場合は、Task<(型, 型)>の形式で戻り値を定義します。
static async Task<(bool Success, string Message)> SaveAsync(){await Task.Delay(100);return (true, "保存に成功しました");
}
呼び出し側ではawaitして結果を受け取ります。
var result = await SaveAsync();Console.WriteLine(result.Success);Console.WriteLine(result.Message);
分解して受け取ることもできます。
var (success, message) = await SaveAsync();Console.WriteLine(success);Console.WriteLine(message);
非同期処理の成功可否とメッセージ、取得データとステータスなどを同時に返したい場合に便利です。
ただし、公開APIで多くの情報を返す場合は、専用の結果クラスやレコードを定義したほうが保守しやすくなります。
4. タプルの分解と代入を活用する方法
4-1. タプルの各要素を別々の変数へ分解する
タプルは、各要素を個別の変数に分解できます。
var user = (Name: "伊藤", Age: 27);var (name, age) = user;
Console.WriteLine(name);Console.WriteLine(age);
分解時に宣言する変数名は、元のタプルの要素名と同じである必要はありません。
var (userName, userAge) = user;型を明示する場合は、次のように記述します。
(string userName, int userAge) = user;4-2. 既存の変数へタプルの値を代入する
分解は、新しい変数を宣言するときだけでなく、既存の変数への代入にも使えます。
string name;int age;(name, age) = ("中村", 35);
Console.WriteLine(name);Console.WriteLine(age);
メソッドの戻り値も既存の変数に代入できます。
string userName = "";int userAge = 0;(userName, userAge) = GetUser();
変数の宣言と代入を分けたい場合に便利です。
4-3. タプルを使って変数の値を入れ替える
タプル代入を利用すると、一時変数を使わずに値を入れ替えられます。
int a = 10;int b = 20;(a, b) = (b, a);
Console.WriteLine(a); // 20Console.WriteLine(b); // 10
従来の書き方では、一時変数が必要でした。
int temp = a;a = b;b = temp;タプル代入を使うと、値の交換であることがひと目で分かります。
4-4. foreachでタプルを分解して受け取る
タプルを格納したコレクションは、foreach内で分解できます。
var users = new List<(string Name, int Age)>{("山田", 20),("佐藤", 30),("鈴木", 25)};foreach (var (name, age) in users){Console.WriteLine($"{name}: {age}歳");}
Dictionary<TKey, TValue>の要素も、キーと値に分解できます。
var scores = new Dictionary<string, int>{["山田"] = 80,["佐藤"] = 90};foreach (var (name, score) in scores){Console.WriteLine($"{name}: {score}点");}
ループ内でitem.Item1やitem.Item2を書く必要がなくなり、処理の意図が明確になります。
4-5. クラスや構造体にDeconstructメソッドを定義する
分解はタプル専用の機能ではありません。クラスや構造体にDeconstructメソッドを定義すると、そのオブジェクトもタプルのように分解できます。
public class User{public string Name { get; }public int Age { get; }public User(string name, int age){Name = name;Age = age;}public void Deconstruct(out string name, out int age){name = Name;age = Age;}
}
次のように分解できます。
var user = new User("小林", 29);var (name, age) = user;
Console.WriteLine(name);Console.WriteLine(age);
用途に応じて複数のDeconstructメソッドをオーバーロードすることも可能です。
public void Deconstruct(out string name){name = Name;}5. ValueTupleとSystem.Tupleの違い
5-1. ValueTupleは値型・System.Tupleは参照型
ValueTupleとSystem.Tupleの大きな違いは、値型か参照型かという点です。
ValueTupleは構造体であり、値型です。
var valueTuple = (10, 20);System.Tupleはクラスであり、参照型です。
var tuple = Tuple.Create(10, 20);値型は変数へ代入すると値がコピーされます。
var first = (Count: 10, Name: "商品");var second = first;second.Count = 20;
Console.WriteLine(first.Count); // 10Console.WriteLine(second.Count); // 20
ただし、タプルの要素に参照型が含まれている場合、その参照先まで複製されるわけではありません。
5-2. 宣言方法と要素へのアクセス方法の違い
ValueTupleは、丸括弧を使って簡潔に宣言できます。
var user = (Name: "山田", Age: 20);System.Tupleは、コンストラクターまたはTuple.Createを使います。
var user = Tuple.Create("山田", 20);要素へのアクセス方法も異なります。
Console.WriteLine(user.Item1);Console.WriteLine(user.Item2);System.Tupleでは基本的にItem1、Item2を使用します。ValueTupleでは要素名を付けられるため、より読みやすいコードを書けます。
5-3. 要素名を付けられるかどうかの違い
ValueTupleでは、各要素に名前を付けられます。
var user = (Name: "山田", Age: 20);Console.WriteLine(user.Name);Console.WriteLine(user.Age);
System.Tupleの要素名はItem1、Item2などで固定されています。
var user = Tuple.Create("山田", 20);Console.WriteLine(user.Item1);Console.WriteLine(user.Item2);
要素名によってデータの意味を示せることは、ValueTupleの大きな利点です。
5-4. メモリ使用量とパフォーマンスの違い
System.Tupleは参照型であるため、通常はオブジェクトとしてヒープに割り当てられます。
一方、ValueTupleは値型です。状況によってはヒープへのオブジェクト割り当てを避けられるため、小さなタプルでは効率的に扱えることがあります。
ただし、値型であることが常に高速とは限りません。
要素数が多いタプルや、大きな構造体を要素に含むタプルは、代入や引数渡しの際にコピーコストが発生する可能性があります。また、objectやインターフェイス型として扱うとボックス化が発生することがあります。
パフォーマンスが重要な処理では、推測だけで判断せず、実際の利用環境で計測することが大切です。
5-5. ValueTupleとSystem.Tupleの使い分け
新しくC#のコードを書く場合は、通常ValueTupleを選べばよいでしょう。
var result = (Success: true, Message: "完了");System.Tupleを使う主なケースは、次のとおりです。
古いコードとの互換性を維持する
既存APIが
System.Tupleを受け取る参照型のタプルが必要
古いC#環境を使用する
特別な理由がなければ、読みやすく分解にも対応したValueTupleが適しています。
5-6. 古い.NET環境でValueTupleを使う際の注意点
比較的新しい.NETでは、System.ValueTupleが標準で利用できます。
一方、古い.NET Frameworkや古いターゲット環境では、NuGetからSystem.ValueTupleパッケージを追加しなければならない場合があります。
また、実行環境だけでなく、コンパイラがC# 7.0以降のタプル構文に対応している必要があります。
古いプロジェクトでエラーになる場合は、次の点を確認しましょう。
プロジェクトのターゲットフレームワーク
C#の言語バージョン
System.ValueTupleパッケージの有無使用しているIDEとビルドツールのバージョン
6. タプルと匿名型の違い
6-1. タプルと匿名型の基本的な特徴
匿名型は、クラスを明示的に定義せず、名前付きプロパティを持つオブジェクトを作成する機能です。
var user = new{Name = "山田",Age = 20};タプルでは、次のように記述します。
var user = (Name: "山田", Age: 20);どちらも一時的なデータをまとめる用途に使えますが、性質は異なります。
匿名型はコンパイラが生成する参照型であり、名前付きの読み取り専用プロパティを持ちます。ValueTupleは値型であり、各要素を書き換えられます。
6-2. メソッドの戻り値に使える範囲の違い
タプルは、メソッドの戻り値の型として直接宣言できます。
static (string Name, int Age) GetUser(){return ("山田", 20);}匿名型にはソースコード上で指定できる型名がないため、通常のメソッドの戻り値型として直接宣言できません。
// 匿名型を戻り値型として記述することはできないobjectやdynamicとして返す方法はありますが、静的な型情報が失われたり、実行時エラーの可能性が増えたりします。
匿名型は、主に同じメソッド内やLINQクエリ内で利用します。
6-3. 要素・プロパティを変更できるかの違い
ValueTupleの要素は、変数が読み取り専用でなければ変更できます。
var user = (Name: "山田", Age: 20);user.Age = 21;
匿名型のプロパティは、作成後に直接変更できません。
var user = new{Name = "山田",Age = 20};// user.Age = 21; // コンパイルエラー
匿名型の値を変更したい場合は、新しい匿名型のオブジェクトを作成します。
6-4. 型の安全性と可読性の違い
タプルと匿名型は、どちらもコンパイル時に型チェックされます。
匿名型はプロパティ名を必ず持つため、Item1やItem2のような意味の分かりにくいアクセスにはなりません。
var user = new{Name = "山田",Age = 20};Console.WriteLine(user.Name);
タプルも要素名を付ければ、同様に読みやすくできます。
var user = (Name: "山田", Age: 20);ただし、タプルの要素名は型の同一性には影響しません。次の2つは、要素の型と順序が同じタプル型として扱われます。
(string Name, int Age)(string ProductName, int Stock)データの意味を型として厳密に区別したい場合は、クラスやレコードが適しています。
6-5. LINQで匿名型が向いているケース
匿名型は、LINQで必要な項目だけを抽出するときに便利です。
var users = new[]{new { Name = "山田", Age = 20, Address = "東京" },new { Name = "佐藤", Age = 30, Address = "大阪" }};var result = users.Select(user => new{user.Name,user.Age});
匿名型はプロパティ名が明確で、LINQのselectやgroup byで使いやすいという特徴があります。
var grouped = users.GroupBy(user => new{user.Address,IsAdult = user.Age >= 18});クエリの途中だけで使い、メソッドの外へ公開しないデータには匿名型が適しています。
6-6. タプルと匿名型を選ぶ判断基準
メソッドから複数の値を返したい場合や、分解して受け取りたい場合はタプルが適しています。
var (name, age) = GetUser();LINQ内で名前付きプロパティを持つ一時的なオブジェクトを作りたい場合は、匿名型が便利です。
var result = users.Select(user => new{user.Name,IsAdult = user.Age >= 18});判断基準をまとめると、次のようになります。
戻り値に使う:タプル
分解したい:タプル
要素を変更したい:タプル
LINQ内部だけで使う:匿名型
名前付きの読み取り専用プロパティが必要:匿名型
複数のメソッドや層で共有する:クラスまたはレコード
7. タプルとクラス・構造体・out引数の違い
7-1. タプルとクラス・レコードの違い
タプルは、データを簡潔にまとめるための仕組みです。クラスやレコードは、アプリケーション上の意味を持つ独自の型を定義するために使います。
タプルの場合は、次のように記述します。
static (string Name, int Age) GetUser(){return ("山田", 20);}レコードを使う場合は、次のように定義できます。
public record User(string Name, int Age);static User GetUser(){return new User("山田", 20);}
レコードには型名があるため、「このデータはユーザーを表す」という意味を明確にできます。プロパティの追加、属性の指定、JSONシリアライズ、API仕様の管理もしやすくなります。
一時的な結果にはタプル、ドメイン上の意味を持つデータにはクラスやレコードが適しています。
7-2. タプルと構造体の違い
ValueTuple自体も構造体ですが、独自の構造体とは用途が異なります。
独自の構造体には、次の要素を定義できます。
型名
プロパティ
コンストラクター
メソッド
入力値の検証
インターフェイスの実装
public readonly struct Size{public int Width { get; }public int Height { get; }public Size(int width, int height){if (width < 0 || height < 0){throw new ArgumentOutOfRangeException();}Width = width;Height = height;}public int GetArea(){return Width * Height;}
}
タプルは、このような振る舞いや制約を持たせる用途には向いていません。
7-3. タプルとout引数の違い
out引数を使って、メソッドから複数の値を取得することもできます。
static bool TryDivide(int left,int right,out double result){if (right == 0){result = 0;return false;}result = (double)left / right;return true;
}
呼び出し側は次のようになります。
if (TryDivide(10, 2, out double result)){Console.WriteLine(result);}タプルを使う場合は、すべての結果を戻り値としてまとめられます。
static (bool Success, double Result) Divide(int left, int right){if (right == 0){return (false, 0);}return (true, (double)left / right);
}
一般的な結果オブジェクトとして扱うならタプルが分かりやすい一方、TryParse形式のAPIや、既存の.NET APIに合わせる場合はout引数が自然です。
7-4. 一時的なデータにはタプルが向いている理由
タプルは型定義が不要で、宣言した場所ですぐに利用できます。
var summary = (Total: 1000m,Count: 5,Average: 200m);数行の処理やひとつのメソッド内だけで使うデータに専用クラスを定義すると、コード量が増える場合があります。
次の条件に当てはまるなら、タプルが有力な選択肢です。
利用範囲が狭い
要素が少ない
データに振る舞いがない
長期間維持する公開仕様ではない
要素の関係が単純
7-5. 公開APIや複雑なデータには独自型が向いている理由
公開APIの戻り値にタプルを使うと、要素の追加や順序変更がAPIの互換性に影響します。
public (bool Success, string Message) Save(){// ...}後からエラーコードや保存日時を追加したくなると、呼び出し側への影響が大きくなる可能性があります。
専用型を定義しておけば、データの意味や仕様を明確にできます。
public record SaveResult(bool Success,string Message,string? ErrorCode);公開API、ライブラリ、複数レイヤー間のデータ受け渡し、外部システムとの通信では、クラスやレコードを優先すると保守しやすくなります。
7-6. 用途別の使い分け早見表
| 用途 | 適した選択肢 |
|---|---|
| メソッド内の一時的な値 | タプル |
| 2~3個の戻り値 | タプル |
| LINQ内の一時的な投影 | 匿名型 |
TryParse形式の処理 | out引数 |
| 明確な業務上の意味を持つデータ | クラス・レコード |
| 小さく不変な値を表す型 | 構造体・readonly record struct |
| 公開APIの引数・戻り値 | クラス・レコード |
| データに処理や検証を持たせる | クラス・構造体 |
| 複合キー | タプルまたは専用型 |
8. タプルでよく使う実践的なコード例
8-1. 最大値と最小値を同時に返す
数値の一覧から最小値と最大値を返す例です。
static (int Min, int Max) GetMinMax(IEnumerable<int> numbers){int[] values = numbers.ToArray();if (values.Length == 0){throw new ArgumentException("要素が必要です。",nameof(numbers));}return (values.Min(), values.Max());
}
呼び出し側では次のように利用できます。
var (min, max) = GetMinMax(new[] { 10, 3, 25, 8 });Console.WriteLine($"最小値: {min}");Console.WriteLine($"最大値: {max}");
8-2. 計算結果と成功可否を返す
ゼロ除算を考慮し、成功可否と計算結果を返す例です。
static (bool Success, double Result) TryCalculate(double left,double right){if (right == 0){return (false, 0);}return (true, left / right);
}
var result = TryCalculate(10, 2);if (result.Success){Console.WriteLine(result.Result);}else{Console.WriteLine("計算できませんでした。");}
より詳しいエラー情報が必要な場合は、メッセージも追加できます。
static (bool Success,double Result,string? ErrorMessage) TryCalculate(double left, double right){if (right == 0){return (false, 0, "0では割れません。");}return (true, left / right, null);
}
ただし、返す情報が増えて複雑になった場合は、専用の結果型を検討しましょう。
8-3. 名前と年齢など関連する値をまとめる
小規模な処理では、名前と年齢などの関連する値をタプルにまとめられます。
var user = (Name: "斎藤",Age: 24,IsActive: true);Console.WriteLine($"{user.Name}さん、{user.Age}歳、状態: {user.IsActive}");
引数として渡すこともできます。
static void PrintUser((string Name, int Age) user){Console.WriteLine($"{user.Name}: {user.Age}歳");}PrintUser(("斎藤", 24));
8-4. Listにタプルを格納する
List<T>の要素型としてタプルを指定できます。
var products = new List<(string Name, decimal Price)>{("マウス", 2980m),("キーボード", 4980m),("モニター", 29800m)};foreachで分解すると簡潔に処理できます。
foreach (var (name, price) in products){Console.WriteLine($"{name}: {price:N0}円");}要素の追加も可能です。
products.Add(("USBケーブル", 980m));コレクションがアプリケーションの主要データになる場合は、Productクラスやレコードを定義したほうが分かりやすくなります。
8-5. Dictionaryのキーや値にタプルを使う
タプルは、複数の値を組み合わせた辞書のキーとして利用できます。
var prices = new Dictionary<(string ProductCode, string StoreCode), decimal>{[("P001", "TOKYO")] = 1000m,[("P001", "OSAKA")] = 950m,[("P002", "TOKYO")] = 2000m};値を取得する例は次のとおりです。
var key = (ProductCode: "P001", StoreCode: "TOKYO");if (prices.TryGetValue(key, out decimal price)){Console.WriteLine(price);}
辞書の値側にもタプルを使えます。
var users = new Dictionary<int, (string Name, int Age)>{[1] = ("山田", 20),[2] = ("佐藤", 30)};8-6. LINQの結果をタプルとして取得する
LINQのSelectでタプルを作成できます。
var products = new[]{new { Name = "マウス", Price = 2980m },new { Name = "キーボード", Price = 4980m }};var result = products.Select(product => (ProductName: product.Name,TaxIncludedPrice: product.Price * 1.1m)).ToList();
取得した結果は、名前付き要素で参照できます。
foreach (var item in result){Console.WriteLine($"{item.ProductName}: {item.TaxIncludedPrice:N0}円");}分解して受け取ることも可能です。
foreach (var (productName, taxIncludedPrice) in result){Console.WriteLine($"{productName}: {taxIncludedPrice:N0}円");}8-7. switch式・パターンマッチングでタプルを使う
タプルパターンを使うと、複数の値の組み合わせをswitchで判定できます。
static string GetState(bool isLoggedIn, bool isAdmin){return (isLoggedIn, isAdmin) switch{(false, _) => "未ログイン",(true, true) => "管理者",(true, false) => "一般ユーザー"};}座標の判定にも利用できます。
static string GetPosition(int x, int y){return (x, y) switch{(0, 0) => "原点",(0, ) => "Y軸上",(, 0) => "X軸上",(> 0, > 0) => "第1象限",(< 0, > 0) => "第2象限",(< 0, < 0) => "第3象限",(> 0, < 0) => "第4象限"};}複数の条件をひとつの組み合わせとして表現できるため、複雑なif文を整理できます。
9. タプルを比較・判定する方法
9-1. ==演算子と!=演算子でタプルを比較する
対応する要素が比較可能であれば、タプル同士を==演算子で比較できます。
var first = (10, "A");var second = (10, "A");Console.WriteLine(first == second); // True
いずれかの要素が異なる場合はfalseになります。
var first = (10, "A");var second = (20, "A");Console.WriteLine(first == second); // FalseConsole.WriteLine(first != second); // True
比較は、対応する要素ごとに行われます。
9-2. Equalsメソッドでタプルを比較する
Equalsメソッドでも比較できます。
var first = (10, "A");var second = (10, "A");Console.WriteLine(first.Equals(second)); // True
要素の型や値が異なれば、等しいとは判定されません。
var first = (10, "A");var second = (10, "B");Console.WriteLine(first.Equals(second)); // False
通常のコードでは==のほうが読みやすいことが多いものの、オブジェクトとして比較する場面ではEqualsが使われることもあります。
9-3. タプルの要素名が等価性に影響しない理由
タプルの比較では、要素名ではなく、要素の値が比較されます。
var user = (Name: "山田", Age: 20);var product = (ProductName: "山田", Stock: 20);Console.WriteLine(user == product); // True
NameとProductName、AgeとStockでは意味が異なりますが、要素名はタプル型の同一性や値の等価性には影響しません。
要素の意味が異なるデータを確実に区別したい場合は、タプルではなく専用型を使用しましょう。
9-4. Dictionaryのキーとして使う場合の注意点
ValueTupleは等価性比較とハッシュコード生成に対応しているため、Dictionaryのキーとして使用できます。
var dictionary =new Dictionary<(int UserId, int ProductId), string>();dictionary[(1, 100)] = "購入済み";
検索時も、対応する要素の値が同じであれば一致します。
Console.WriteLine(dictionary[(1, 100)]);ただし、キーの要素には、比較結果とハッシュコードが安定する値を使用する必要があります。
タプルの要素に、状態によってEqualsやGetHashCodeの結果が変化するオブジェクトを含めると、辞書から正しく検索できなくなる可能性があります。
文字列、数値、列挙型、IDなど、不変として扱える値を組み合わせるのが安全です。
9-5. タプルパターンで複数条件を判定する方法
タプルパターンを使うと、複数の条件を同時に判定できます。
static string GetDiscount(bool isMember, int amount){return (isMember, amount) switch{(true, >= 10_000) => "20%割引",(true, >= 5_000) => "10%割引",(true, _) => "5%割引",(false, >= 10_000) => "5%割引",_ => "割引なし"};}if文で複数条件を並べるよりも、組み合わせの一覧として読みやすくなる場合があります。
10. タプルを使う際の注意点
10-1. 要素数が多いと可読性が低下する
タプルには多数の値を格納できますが、要素数が増えるほど意味を把握しにくくなります。
var data = (1,"山田",20,"東京",true,DateTime.Now,1000m);要素名を付ければ多少改善できますが、要素が多い時点で独自型を検討したほうがよいでしょう。
public record User(int Id,string Name,int Age,string Address,bool IsActive,DateTime CreatedAt,decimal Balance);目安として、2~3要素程度の単純な組み合わせにはタプル、それ以上で意味のあるデータにはクラスやレコードが向いています。
10-2. Item1・Item2のままでは意味が伝わりにくい
次のコードだけでは、Item1とItem2が何を意味するのか分かりません。
var result = GetResult();Console.WriteLine(result.Item1);Console.WriteLine(result.Item2);
可能な限り要素名を付けましょう。
static (bool Success, string Message) GetResult(){return (true, "完了");}var result = GetResult();Console.WriteLine(result.Success);Console.WriteLine(result.Message);
要素名を付けるだけで、コードの意図が大幅に伝わりやすくなります。
10-3. タプルの要素名は実行時の型情報ではない
名前付きタプルの要素名は、ValueTupleの実際のフィールド名を変更するものではありません。
(string Name, int Age) user = ("山田", 20);実行時の基礎となる型は、概念的には次の型です。
ValueTuple<string, int>実際のフィールドはItem1とItem2です。NameやAgeなどの名前は、コンパイラやメタデータによって補助的に管理されます。
そのため、リフレクション、動的処理、シリアライズなどでは、期待した要素名を取得できない場合があります。
10-4. nullを含むタプルとNullableタプルの違い
タプルの要素をnullにできるかどうかは、各要素の型によって決まります。
(string? Name, int? Age) user = (null, null);この例では、タプル自体は存在していますが、各要素がnullです。
一方、タプル全体をnullにしたい場合は、Nullable値型として宣言します。
(string Name, int Age)? user = null;値を取り出す場合は、nullチェックが必要です。
if (user is { } value){Console.WriteLine(value.Name);Console.WriteLine(value.Age);}両者の違いは次のとおりです。
// タプルは存在し、要素がnull(string? Name, int? Age) first = (null, null);// タプル自体がnull(string Name, int Age)? second = null;
Nullable参照型を有効にしているプロジェクトでは、要素ごとの?も適切に指定しましょう。
10-5. JSONへシリアライズする際の注意点
ValueTupleの要素はプロパティではなくフィールドです。
System.Text.Jsonは、標準設定では主にパブリックプロパティをシリアライズするため、設定によってはタプルが期待どおりのJSONになりません。
var user = (Name: "山田", Age: 20);フィールドを含める設定を使用しても、出力名がNameやAgeではなく、Item1やItem2になる可能性があります。
var options = new JsonSerializerOptions{IncludeFields = true};string json = JsonSerializer.Serialize(user, options);
タプルの要素名は実行時のフィールド名ではないため、APIのJSON形式としては安定しません。
外部へ送信するJSONでは、クラスやレコードを使うのが安全です。
public record UserResponse(string Name, int Age);var user = new UserResponse("山田", 20);string json = JsonSerializer.Serialize(user);10-6. publicメソッドの戻り値で多用しない
小規模な内部メソッドでは、タプルの戻り値は非常に便利です。
private static (int Min, int Max) GetRange(){return (10, 100);}一方、ライブラリや大規模なアプリケーションのpublicメソッドで多用すると、APIの意味が分かりにくくなることがあります。
public (bool Success, string Message, int Code) Execute(){// ...}専用型にすると、ドキュメント化や機能追加がしやすくなります。
public record ExecuteResult(bool Success,string Message,int Code);戻り値がアプリケーション上の重要な概念である場合は、名前のある型を定義しましょう。
10-7. 値型のコピーコストに注意する
ValueTupleは値型であるため、代入や値渡しでは内容がコピーされます。
var first = (A: 1,B: 2,C: 3,D: 4,E: 5);var second = first;
小さなタプルであれば、通常は大きな問題になりません。
ただし、要素数が多い場合や、大きな構造体を複数含む場合、頻繁なコピーがパフォーマンスに影響する可能性があります。
大きなデータを扱う場合は、次の選択肢も検討しましょう。
専用のクラスを使う
小さな識別子だけをタプルに含める
in引数を検討するベンチマークで実際の影響を確認する
11. C#のタプルに関するよくある質問
11-1. タプルには最大で何個の値を格納できる?
C#のタプル構文では、多数の要素を記述できます。
var data = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);内部で使われるValueTupleには、残りの要素を別のValueTupleとして保持する仕組みがあります。一般的には7要素ごとに入れ子の構造が使われます。
そのため、C#の構文上は8個を超える要素も扱えます。
ただし、技術的に格納できることと、読みやすいコードであることは別です。要素が多い場合は、クラス、レコード、構造体などの独自型を使用しましょう。
11-2. タプルの型や要素数は後から変更できる?
宣言済みのタプル変数について、要素の型や要素数を後から変更することはできません。
var user = (Name: "山田", Age: 20);// user = ("山田", 20, true); // 要素数が異なる// user = (Name: "山田", Age: "20"); // 型が異なる
各要素の値は、対応する型の範囲内で変更できます。
user.Name = "佐藤";user.Age = 30;要素数や型が異なるデータを扱う場合は、別の変数または別の型が必要です。
11-3. タプルの要素名を後から変更できる?
宣言済みの変数の要素名そのものを、実行時に変更することはできません。
var user = (Name: "山田", Age: 20);ただし、同じ値を異なる要素名で受け取ることはできます。
(string UserName, int UserAge) copied = user;Console.WriteLine(copied.UserName);Console.WriteLine(copied.UserAge);
要素名はタプル型の同一性には影響しないため、このような代入が可能です。
分解するときに、任意の変数名を付けることもできます。
var (displayName, yearsOld) = user;11-4. タプルを引数としてメソッドに渡せる?
タプルはメソッドの引数として渡せます。
static void PrintUser((string Name, int Age) user){Console.WriteLine($"{user.Name}: {user.Age}歳");}呼び出し側では、タプルリテラルを直接渡せます。
PrintUser(("山田", 20));変数に格納してから渡すこともできます。
var user = (Name: "佐藤", Age: 30);PrintUser(user);
ただし、引数として多くの場所で使うデータなら、専用のクラスやレコードを定義したほうが型の意味を明確にできます。
11-5. タプルにnullを設定できる?
タプルの各要素が参照型またはNullable値型であれば、その要素にnullを設定できます。
(string? Name, int? Age) user = (null, null);ValueTuple自体は値型なので、通常はタプル全体にnullを代入できません。
// (string Name, int Age) user = null; // コンパイルエラータプル全体をnullにしたい場合は、?を付けます。
(string Name, int Age)? user = null;11-6. タプルと配列・Listはどう使い分ける?
タプルは、役割や型が異なる複数の値をひとまとまりにするときに使います。
var user = (Name: "山田", Age: 20);配列やList<T>は、同じ役割と型を持つ複数の値を並べるときに使います。
var scores = new List<int> { 80, 90, 70 };使い分けの例は次のとおりです。
// 1人分の名前と年齢(string Name, int Age) user = ("山田", 20);// 複数人分の名前と年齢var users = new List<(string Name, int Age)>{("山田", 20),("佐藤", 30)};
タプルは固定された構造を表し、配列やListは可変個数の要素を表すと考えると分かりやすいでしょう。
11-7. 戻り値にはタプルとクラスのどちらを使うべき?
戻り値が少数の一時的な値であり、利用範囲が狭い場合はタプルが適しています。
private static (int Min, int Max) GetRange(){return (10, 100);}次の条件に当てはまる場合は、クラスやレコードが適しています。
戻り値の項目が多い
複数のメソッドで使用する
公開APIとして提供する
将来的に項目が追加される可能性が高い
JSONへシリアライズする
データに検証や処理を持たせる
業務上の明確な意味を持つ
public record RangeResult(int Min, int Max);迷った場合は、「そのデータに名前のある独自型を与える価値があるか」を基準に考えましょう。
まとめ
C#のタプルは、異なる型の複数の値をひとつにまとめられる機能です。
C# 7.0以降では、ValueTupleを利用した簡潔な構文を使用できます。
var user = (Name: "山田", Age: 20);メソッドから複数の値を返すことも可能です。
static (bool Success, string Message) Execute(){return (true, "処理が完了しました");}戻り値は、分解して個別の変数で受け取れます。
var (success, message) = Execute();タプルは、少数の関連する値を一時的にまとめる場面に適しています。一方、要素数が多いデータ、公開API、複数箇所で共有するデータ、JSONとして外部へ公開するデータには、クラスやレコードなどの独自型が向いています。
タプルを使う際は、Item1やItem2のままにせず、できるだけ意味のある要素名を付けましょう。
var result = (Success: true,Message: "登録に成功しました");タプル、匿名型、クラス、レコード、構造体、out引数には、それぞれ適した用途があります。コードを短くすることだけを目的にせず、データの利用範囲、意味、将来の変更可能性を考慮して使い分けることが重要です。

