C#のTupleとは?使い方・ValueTupleとの違い・複数値を返す方法を初心者向けに解説
はじめに
C#でメソッドから複数の値を返したいときや、関連する値を一時的にまとめたいときに便利なのが「タプル」です。
現在のC#では、(string Name, int Age)のように記述するタプル型、つまりValueTupleを使う方法が一般的です。一方、以前から.NETに用意されているSystem.Tupleクラスも存在します。両者は名前が似ていますが、値型と参照型、変更可能かどうか、要素名を付けられるかなど、多くの違いがあります。Microsoft Learn+1
この記事では、C#のTupleとValueTupleの違い、基本的な使い方、メソッドから複数の値を返す方法、分解、注意点まで初心者向けに解説します。
1. C#のTuple(タプル)とは
1-1. 複数の値をひとまとまりにできるデータ構造
タプルとは、複数の値を順番に並べ、1つのデータとして扱えるデータ構造です。
たとえば、ユーザーの名前と年齢をまとめる場合、次のように記述できます。
C#(string Name, int Age) user = ("田中", 25);
Console.WriteLine(user.Name);
Console.WriteLine(user.Age);
このタプルには、string型の名前とint型の年齢が格納されています。配列と異なり、異なるデータ型の値を同じまとまりに含められることが特徴です。Microsoft Learn+1
1-2. Tupleを使うメリット
C#でタプルを使う主なメリットは、専用のクラスを定義しなくても、複数の値を簡潔にまとめられることです。
たとえば、計算結果として合計値と平均値を返すだけなら、次のような専用クラスを作る必要はありません。
C#public class CalculationResult
{
public int Total { get; set; }
public double Average { get; set; }
}
タプルを使えば、戻り値を次のように記述できます。
C#static (int Total, double Average) Calculate(int[] numbers)
{
int total = numbers.Sum();
double average = numbers.Average();
return (total, average);
}
コード量を減らしながら、戻り値の意味も要素名で表現できます。
1-3. Tupleが活躍する主な場面
C#のTupleは、主に次のような場面で活躍します。
メソッドから複数の値を返す
最大値と最小値など、関連する計算結果をまとめる
LINQの処理中に一時的なデータを作る
座標や範囲など、少数の値を一組として扱う
成否を示す値と処理結果を同時に返す
タプルは、関連する値を軽量かつ一時的にまとめる用途に適しています。一方、公開APIや長期間保持する業務データでは、クラス、構造体、recordなどの専用型も検討することが推奨されます。Microsoft Learn
1-4. C#の「タプル型」とSystem.Tupleの違い
C#で「Tuple」と呼ばれるものには、大きく分けて次の2種類があります。
C#// C#のタプル型(内部的にはValueTuple)
(string Name, int Age) valueTuple = ("田中", 25);
// System.Tupleクラス
Tuple<string, int> tuple = Tuple.Create("田中", 25);
(string Name, int Age)という構文は、C#の言語機能として提供されるタプル型です。実体には主にSystem.ValueTuple<T1, T2>などの値型が使われます。
一方、System.Tuple<T1, T2>はクラスとして提供される参照型です。この記事では、区別が必要な箇所では前者を「タプル型」または「ValueTuple」、後者を「System.Tuple」と表記します。Microsoft Learn+1
2. C#のタプル型(ValueTuple)の基本的な使い方
2-1. タプルを宣言して値を代入する
タプル型は、丸括弧の中に各要素の型を並べて宣言します。
C#(string, int) user = ("佐藤", 30);
左辺の(string, int)がタプルの型、右辺の("佐藤", 30)がタプルの値です。
値を個別に出力するには、Item1とItem2を使います。
C#Console.WriteLine(user.Item1); // 佐藤
Console.WriteLine(user.Item2); // 30
2-2. varを使って型を省略する
右辺から型を推論できる場合は、varを使用できます。
C#var product = ("キーボード", 4980m);
この場合、コンパイラーはproductを、おおむね次のタプル型として扱います。
C#(string, decimal) product = ("キーボード", 4980m);
要素の型が明らかな場合は、varを使うと簡潔に記述できます。
2-3. Item1・Item2で各要素を取得する
名前を付けていないタプルでは、各要素にItem1、Item2、Item3という既定名でアクセスできます。
C#var result = (200, "成功", true);
Console.WriteLine(result.Item1); // 200
Console.WriteLine(result.Item2); // 成功
Console.WriteLine(result.Item3); // True
明示的な要素名がある場合でも、Item1などの既定名は利用できます。Microsoft Learn
2-4. 異なるデータ型の値を格納する
タプルの各要素には、異なるデータ型を指定できます。
C#(int Id, string Name, bool IsActive, DateTime CreatedAt) user =
(1, "鈴木", true, DateTime.Now);
この例では、整数、文字列、真偽値、日時を1つのタプルに格納しています。
ただし、型安全性は維持されます。たとえば、Idに文字列を代入するとコンパイルエラーになります。
C#user.Id = 10;
// コンパイルエラー
// user.Id = "10";
2-5. タプルの要素を書き換える
ValueTupleの要素は変更できます。
C#var point = (X: 10, Y: 20);
point.X = 30;
point.Y = 40;
Console.WriteLine(point); // (30, 40)
ValueTupleは変更可能な値型であり、要素は公開フィールドとして実装されています。System.Tupleの読み取り専用プロパティとは、この点が大きく異なります。Microsoft Learn+1
3. 名前付きタプルで要素を分かりやすくする方法
3-1. タプルの各要素に名前を付ける
タプルの要素には、型の後ろに名前を記述できます。
C#(string Name, int Age) user = ("高橋", 28);
値を作る側で名前を付けることも可能です。
C#var user = (Name: "高橋", Age: 28);
どちらの場合も、NameとAgeを使って要素へアクセスできます。
3-2. 名前を使って要素へアクセスする
名前付きタプルでは、次のように意味の分かる名前で値を取得できます。
C#var product = (Name: "マウス", Price: 2980m);
Console.WriteLine(product.Name);
Console.WriteLine(product.Price);
product.Item1やproduct.Item2でもアクセスできますが、NameやPriceを使うほうが、各値の意味を理解しやすくなります。
3-3. 代入する値から要素名を推論させる
変数をそのままタプルへ格納すると、変数名がタプルの要素名として推論されることがあります。
C#string name = "伊藤";
int age = 32;
var user = (name, age);
Console.WriteLine(user.name);
Console.WriteLine(user.age);
この機能はタプル射影初期化子と呼ばれます。ただし、Item3、ToString、Restなどタプルのメンバー名と衝突する場合や、同じ候補名が重複する場合は推論されません。Microsoft Learn
3-4. Item1・Item2より名前付きタプルが適している理由
次のコードだけを見ても、Item1とItem2が何を示すのかは分かりにくいでしょう。
C#var result = GetUser();
Console.WriteLine(result.Item1);
Console.WriteLine(result.Item2);
名前付きタプルなら、コードの意図が明確になります。
C#var result = GetUser();
Console.WriteLine(result.Name);
Console.WriteLine(result.Age);
複数の値を返すメソッドでは、原則として意味の分かる要素名を付けると、呼び出し側の可読性が向上します。
3-5. 要素名を付ける際の注意点
要素名には、値の意味が伝わる名前を使います。
C#// 分かりにくい
(int A, int B) GetRange()
{
return (1, 100);
}
// 分かりやすい
(int Minimum, int Maximum) GetRange()
{
return (1, 100);
}
また、タプルの要素名は、実行時に独立したフィールド名として保持されるわけではありません。コンパイル時には既定のItem1、Item2などに対応付けられ、メソッドシグネチャなどで必要な名前は属性によって表現されます。要素名は可読性を高めますが、新しい型を定義するものではありません。Microsoft Learn+1
4. タプルを分解して複数の変数で受け取る方法
4-1. 分解(Deconstruction)の基本構文
タプルの要素を、それぞれ別の変数へ取り出す操作を「分解」と呼びます。
C#var user = (Name: "山田", Age: 24);
(string name, int age) = user;
Console.WriteLine(name);
Console.WriteLine(age);
左辺の変数には、タプルの位置に対応する値が代入されます。Microsoft Learn+1
4-2. varを使ってタプルを分解する
すべての型を推論できる場合は、左辺の外側にvarを記述できます。
C#var user = ("山田", 24);
var (name, age) = user;
各変数に個別にvarを指定する書き方もあります。
C#(var name, var age) = user;
通常は、より簡潔なvar (name, age)が使いやすいでしょう。
4-3. 既存の変数へタプルの値を代入する
すでに宣言されている変数へ分解代入することもできます。
C#string name = string.Empty;
int age = 0;
var user = ("中村", 35);
(name, age) = user;
この場合、左辺では変数を新しく宣言していないため、varや型名は付けません。Microsoft Learn
4-4. 不要な値を破棄(ディスカード)する
不要な要素は、アンダースコア_を使って破棄できます。
C#var user = (Id: 1, Name: "小林", Age: 29);
var (_, name, age) = user;
Console.WriteLine(name);
Console.WriteLine(age);
この例ではIdを受け取らず、NameとAgeだけを変数へ代入しています。1回の分解で複数のディスカードを使うこともできます。Microsoft Learn+1
4-5. タプルを使って変数の値を入れ替える
タプル代入を使うと、一時変数を用意せずに値を入れ替えられます。
C#int left = 10;
int right = 20;
(left, right) = (right, left);
Console.WriteLine(left); // 20
Console.WriteLine(right); // 10
右辺の値が先に評価されてから左辺へ代入されるため、簡潔に交換できます。
5. メソッドからTupleで複数の値を返す方法
5-1. 名前なしタプルを戻り値に指定する
メソッドの戻り値には、タプル型を直接指定できます。
C#static (int, int) GetRange()
{
return (1, 100);
}
呼び出し側では、Item1とItem2で値を参照します。
C#var range = GetRange();
Console.WriteLine(range.Item1);
Console.WriteLine(range.Item2);
5-2. 名前付きタプルを戻り値に指定する
実際のコードでは、戻り値の意味が分かるように名前を付けるのがおすすめです。
C#static (int Minimum, int Maximum) GetRange()
{
return (1, 100);
}
呼び出し側では、次のように利用できます。
C#var range = GetRange();
Console.WriteLine(range.Minimum);
Console.WriteLine(range.Maximum);
タプルは、従来out引数で返していた複数の結果を、1つの戻り値としてまとめる用途に適しています。Microsoft Learn+1
5-3. 戻り値をタプル型の変数で受け取る
戻り値の型を明示して受け取ることもできます。
C#(int Minimum, int Maximum) range = GetRange();
ただし、右辺から型が明確な場合は、varを使ったほうが簡潔です。
C#var range = GetRange();
メソッド側で要素名を定義していれば、varで受け取ってもrange.Minimumのようにアクセスできます。
5-4. 戻り値を分解して複数の変数で受け取る
戻り値をそのまま複数の変数へ分解できます。
C#var (minimum, maximum) = GetRange();
Console.WriteLine(minimum);
Console.WriteLine(maximum);
型を明示する場合は、次のように記述します。
C#(int minimum, int maximum) = GetRange();
タプル型の変数自体が不要な場合は、分解して受け取ると後続処理を簡潔にできます。
5-5. 成否と結果を同時に返す
処理の成否と結果を同時に返すこともできます。
C#static (bool Success, int Value) TryConvertToInt(string text)
{
if (int.TryParse(text, out int value))
{
return (true, value);
}
return (false, 0);
}
呼び出し側では、次のように判定できます。
C#var result = TryConvertToInt("123");
if (result.Success)
{
Console.WriteLine(result.Value);
}
ただし、.NETの一般的なTryXxxパターンでは、boolの戻り値とout引数が広く使われています。既存のAPI規約に合わせたい場合は、次の形も有力です。
C#static bool TryConvertToInt(string text, out int value)
{
return int.TryParse(text, out value);
}
5-6. 非同期メソッドで複数の値を返す
非同期メソッドでタプルを返す場合は、Task<TResult>のTResult部分にタプル型を指定します。
C#static async Task<(string Name, int Age)> GetUserAsync()
{
await Task.Delay(100);
return ("加藤", 27);
}
呼び出し側では、awaitした結果を通常のタプルとして扱えます。
C#var user = await GetUserAsync();
Console.WriteLine(user.Name);
Console.WriteLine(user.Age);
分解して受け取ることも可能です。
C#var (name, age) = await GetUserAsync();
値を返す非同期メソッドでは一般にTask<TResult>を使うため、タプルはTask<(T1, T2)>の形で指定します。Microsoft Learn+1
6. System.Tupleの作り方と使い方
6-1. Tuple.Createメソッドで生成する
System.Tupleは、Tuple.Createメソッドを使って作成できます。
C#Tuple<string, int> user = Tuple.Create("斎藤", 31);
varを使うこともできます。
C#var user = Tuple.Create("斎藤", 31);
Tuple.Createでは、引数から各要素の型が推論されます。
6-2. Tupleクラスのコンストラクターで生成する
コンストラクターを直接呼び出して生成することも可能です。
C#var user = new Tuple<string, int>("斎藤", 31);
ただし、型名を繰り返す必要があるため、通常はTuple.Createのほうが簡潔です。System.Tupleは、コンストラクターとTuple.Createのどちらでも作成できます。Microsoft Learn
6-3. Item1・Item2で要素を参照する
System.Tupleの要素は、Item1やItem2という読み取り専用プロパティで参照します。
C#var user = Tuple.Create("斎藤", 31);
Console.WriteLine(user.Item1);
Console.WriteLine(user.Item2);
System.Tupleでは、C#のタプル構文のようにNameやAgeという独自の要素名は付けられません。
また、要素は読み取り専用なので、次の代入はできません。
C#// コンパイルエラー
// user.Item1 = "新しい名前";
6-4. System.Tupleをメソッドの戻り値にする
System.Tupleも、メソッドの戻り値として利用できます。
C#static Tuple<int, int> GetRange()
{
return Tuple.Create(1, 100);
}
呼び出し側では、次のように受け取ります。
C#Tuple<int, int> range = GetRange();
Console.WriteLine(range.Item1);
Console.WriteLine(range.Item2);
既存コードや、System.Tupleを前提としたAPIとの互換性が必要な場合に使われます。
6-5. 8個以上の要素を持つTupleの仕組み
System.Tupleの一般的な型は、1個から7個までの直接的な要素を持ちます。8個以上を表現する場合は、8番目の型引数TRestに別のTupleを入れて入れ子にします。Microsoft Learn+1
C#var values = new Tuple<
int, int, int, int, int, int, int,
Tuple<int>
>(
1, 2, 3, 4, 5, 6, 7,
Tuple.Create(8)
);
Console.WriteLine(values.Item1); // 1
Console.WriteLine(values.Rest.Item1); // 8
通常は手作業で複雑な入れ子を記述するより、要素数を減らすか専用の型を定義したほうが分かりやすくなります。
7. TupleとValueTupleの違い
7-1. Tupleは参照型、ValueTupleは値型
System.Tupleはクラスであり、参照型です。
C#Tuple<string, int> tuple = Tuple.Create("田中", 25);
ValueTupleは構造体であり、値型です。
C#(string Name, int Age) valueTuple = ("田中", 25);
値型は代入時に値がコピーされ、参照型は基本的にオブジェクトへの参照がコピーされます。Microsoft Learn+1
7-2. Tupleは変更不可、ValueTupleは変更可能
System.Tupleの要素は読み取り専用プロパティなので、生成後に書き換えられません。
C#var tuple = Tuple.Create("田中", 25);
// tuple.Item2 = 26; // コンパイルエラー
ValueTupleの要素はフィールドであり、書き換えられます。
C#var valueTuple = (Name: "田中", Age: 25);
valueTuple.Age = 26;
この違いにより、System.Tupleは読み取り専用、ValueTupleは可変のデータとして扱われます。Microsoft Learn+1
7-3. 要素名を付けられるかどうか
C#のタプル型では、要素に分かりやすい名前を付けられます。
C#(string Name, int Age) user = ("田中", 25);
System.Tupleでは、要素は常にItem1、Item2などの名前で参照します。
C#Tuple<string, int> user = Tuple.Create("田中", 25);
厳密には、独自名はValueTuple構造体自体の実行時フィールド名ではなく、C#コンパイラーが扱うタプル要素名です。そのため、型の受け渡し方によっては要素名が失われ、Item1などでしか参照できなくなる場合があります。Microsoft Learn+1
7-4. 要素がプロパティかフィールドか
System.Tupleの要素は、読み取り専用のプロパティです。
C#tuple.Item1
ValueTupleの要素は、公開フィールドです。
C#valueTuple.Item1
valueTuple.Name
ValueTupleのItem1フィールドは変更可能として定義されています。Microsoft Learn+1
7-5. メモリ割り当てとパフォーマンスの違い
System.Tupleは参照型なので、通常はオブジェクトの割り当てが発生します。ValueTupleは値型なので、格納される場所や利用方法によっては、個別のオブジェクト割り当てを避けられます。
ただし、「ValueTupleなら必ずスタックに配置される」とは限りません。クラスのフィールド、配列の要素、クロージャーなどに含まれる場合は、その格納先の一部になります。また、objectやインターフェース型へ変換すると、ボックス化が発生することがあります。
少数の要素を短期間扱う場合はValueTupleが効率的になりやすい一方、大きな値型を何度も受け渡すとコピーコストが増える可能性があります。
7-6. 等値比較の違い
C#のタプル型では、対応する位置の要素を順番に比較できます。
C#var left = (Name: "田中", Age: 25);
var right = (UserName: "田中", UserAge: 25);
Console.WriteLine(left == right); // True
要素名は比較結果に影響せず、要素の位置と値が比較されます。Microsoft Learn+1
System.TupleはEqualsによる構造的な比較を実装しています。
C#var left = Tuple.Create("田中", 25);
var right = Tuple.Create("田中", 25);
Console.WriteLine(left.Equals(right)); // True
一方、System.Tupleで==を使うと、クラス型として参照が比較されます。
C#Console.WriteLine(left == right); // False
System.Tupleの値を比較したい場合は、Equalsを使用する必要があります。
7-7. TupleとValueTupleの比較表
| 比較項目 | System.Tuple | ValueTuple・C#のタプル型 |
|---|---|---|
| 種類 | 参照型 | 値型 |
| 宣言例 | Tuple<string, int> | (string Name, int Age) |
| 作成方法 | Tuple.Create(...) | (...) |
| 要素 | 読み取り専用プロパティ | 変更可能なフィールド |
| 独自の要素名 | 基本的に不可 | 可能 |
| 分解 | 拡張メソッド経由で可能 | 言語構文で自然に可能 |
| 値の変更 | 不可 | 可能 |
==による比較 | 基本的に参照比較 | 要素ごとの比較 |
| 主な用途 | 既存APIとの互換性 | 現在の一般的なC#コード |
7-8. 現在のC#ではValueTupleが一般的な理由
ValueTupleを利用するC#のタプル構文は、短く記述でき、要素名、分解、複数戻り値などの言語機能と自然に連携します。
C#var (minimum, maximum) = GetRange();
System.TupleではItem1、Item2だけで値の意味を表す必要がありますが、名前付きタプルならコードそのものに意味を持たせられます。そのため、新しくC#コードを書く場合は、特別な互換性要件がなければValueTupleを利用するのが一般的です。Microsoft Learn+1
8. Tupleとほかの複数値を返す方法の使い分け
8-1. out引数との違い
out引数を使っても、複数の値を返せます。
C#static bool TryGetUser(int id, out string name)
{
if (id == 1)
{
name = "田中";
return true;
}
name = string.Empty;
return false;
}
タプルを使う場合は、複数の結果を1つの戻り値として表現できます。
C#static (bool Success, string Name) GetUser(int id)
{
if (id == 1)
{
return (true, "田中");
}
return (false, string.Empty);
}
通常の計算結果を複数返す場合は、タプルのほうが自然に読めることがあります。一方、TryParseのような既存のTryパターンに合わせる場合や、条件付きのnull状態を属性で表現したい場合は、boolとoutの組み合わせが適しています。
8-2. クラス・構造体・recordとの違い
タプルは、少数の値を一時的にまとめる用途に向いています。
C#(int Minimum, int Maximum) range = (1, 100);
一方、次の条件に該当する場合は、クラス、構造体、recordなどの専用型が適しています。
多くの場所で繰り返し使用する
バリデーションやメソッドが必要
業務上の重要な概念を表す
要素数が多い
公開APIの戻り値として長期間維持する
将来フィールドが追加される可能性が高い
C#public record UserResult(
int Id,
string Name,
bool IsActive
);
専用型は名前そのものが意味を持つため、コードの意図やAPIの契約を明確にできます。
8-3. 匿名型との違い
匿名型は、主にメソッド内やLINQで一時的なデータを作る際に使われます。
C#var user = new
{
Name = "田中",
Age = 25
};
匿名型のプロパティは読み取り専用であり、通常は宣言したメソッドの外へ型を明示して返せません。
タプルは戻り値の型として指定でき、分解も可能です。
C#static (string Name, int Age) GetUser()
{
return ("田中", 25);
}
匿名型は参照型、ValueTupleは値型であり、匿名型のプロパティとValueTupleのフィールドという違いもあります。Microsoft Learn
8-4. 配列やListとの違い
配列やList<T>は、同じ種類の値を複数扱う場合に適しています。
C#int[] scores = { 80, 90, 70 };
List<string> names = new() { "田中", "佐藤", "鈴木" };
タプルは、個数と役割が決まっている異なる種類の値をまとめる場合に適しています。
C#(string Name, int Score) result = ("田中", 80);
データ件数が増減するなら配列やList、固定された少数の項目をまとめるならタプルという使い分けが基本です。
8-5. KeyValuePairとの違い
KeyValuePair<TKey, TValue>は、キーと値のペアを表す型です。
C#var item = new KeyValuePair<int, string>(1, "田中");
用途がキーと値に限定される場合や、辞書の要素を扱う場合に適しています。
タプルは、必ずしもキーと値の関係ではないデータも表現できます。
C#var item = (Id: 1, Name: "田中");
2つの値でも、意味がキーと値でなければ名前付きタプルのほうが分かりやすい場合があります。
8-6. 一時的なデータにはタプルを使う
メソッド内部の短い処理や、複数の計算結果を一時的に渡す場合はタプルが便利です。
C#var statistics = (
Minimum: numbers.Min(),
Maximum: numbers.Max(),
Average: numbers.Average()
);
使用範囲が狭く、各要素の意味が明確なら、専用型を作るより簡潔に記述できます。
8-7. 公開APIや複雑なデータには専用の型を使う
公開APIの戻り値としてタプルを使うと、要素の追加や順番の変更が呼び出し側に影響します。
C#public (int Id, string Name) GetUser()
将来、メールアドレスや状態などが追加される可能性が高い場合は、専用型のほうが変更へ対応しやすくなります。
C#public sealed record UserDto(
int Id,
string Name,
string Email
);
MicrosoftのC#リファレンスでも、タプルは緩やかに関連するデータをまとめる用途に使い、公開APIではクラスや構造体の定義を検討するよう案内されています。Microsoft Learn
9. C#でTupleを使うときの注意点
9-1. 要素数を増やしすぎない
C#のタプル型は、多数の要素を記述できます。内部では、8番目以降の要素を入れ子のValueTupleとして扱う仕組みがあります。Microsoft Learn+1
しかし、次のようなタプルは読みづらくなります。
C#var user = (
Id: 1,
Name: "田中",
Age: 25,
Email: "example@example.com",
Address: "東京都",
IsActive: true,
CreatedAt: DateTime.Now
);
要素が増えてきたら、クラスやrecordへ切り替えましょう。
9-2. Item1・Item2だけでは可読性が下がる
次のコードでは、各値の意味が呼び出し側から分かりません。
C#static (int, int) GetResult()
{
return (10, 20);
}
名前付きタプルにすると、意図が明確になります。
C#static (int Minimum, int Maximum) GetResult()
{
return (10, 20);
}
短い局所処理を除き、戻り値には意味のある要素名を付けるのがおすすめです。
9-3. 要素名はタプル型の同一性に影響しない
次の2つは、要素名が異なっていても、要素数と各位置の型が同じタプル型として代入できます。
C#(int X, int Y) point = (10, 20);
(int Width, int Height) size = point;
この代入では、名前ではなく位置に従って値が移動します。
C#Console.WriteLine(size.Width); // 10
Console.WriteLine(size.Height); // 20
タプル型の同一性や代入では、要素名ではなく、要素数と位置ごとの型が重要です。Microsoft Learn+1
9-4. 値型のコピーコストに注意する
ValueTupleは値型なので、変数への代入や値渡しで内容がコピーされます。
C#var original = (X: 10, Y: 20);
var copied = original;
copied.X = 100;
Console.WriteLine(original.X); // 10
Console.WriteLine(copied.X); // 100
要素数が少なければ大きな問題になりにくいものの、大きな構造体や多数の要素を含むタプルを頻繁に渡すと、コピーコストが増える可能性があります。
9-5. 可変なValueTupleの予期しない変更に注意する
ValueTupleは変更可能ですが、値型であるため、取得したコピーを書き換えても元の値へ反映されないことがあります。
C#var users = new List<(string Name, int Age)>
{
("田中", 25)
};
var user = users[0];
user.Age = 26;
Console.WriteLine(users[0].Age); // 25
users[0]から取り出した値はコピーです。リスト内の値を変更するには、変更後のタプルを再代入します。
C#var user = users[0];
user.Age = 26;
users[0] = user;
9-6. nullを含むタプルの扱いに注意する
ValueTuple自体は値型なので、通常はタプル全体をnullにできません。
C#(string Name, int Age) user = ("田中", 25);
タプル全体をnullにしたい場合は、nullable値型にします。
C#(string Name, int Age)? user = null;
一方、タプル内部の参照型要素は、型をnullableとして宣言できます。
C#(string? Name, int Age) user = (null, 25);
nullable参照型を有効にしている場合は、stringとstring?を適切に使い分けましょう。Microsoft Learn+1
9-7. 辞書のキーとして使う場合の注意点
ValueTupleは等値比較とハッシュコードの生成を実装しているため、複数項目からなる辞書キーとして利用できます。
C#var users = new Dictionary<(int CompanyId, int UserId), string>
{
[(1, 100)] = "田中",
[(1, 101)] = "佐藤"
};
Console.WriteLine(users[(1, 100)]);
複合キーを簡潔に表現できる便利な方法です。
ただし、辞書へ登録したキーのコピーを変更しても、登録済みキーが変更されるわけではありません。また、キーの要素に可変オブジェクトを含め、その等値性やハッシュコードが後から変化すると、検索できなくなる可能性があります。辞書キーには、実質的に不変な値を使いましょう。
10. Tupleに関するよくある疑問
10-1. Tupleとタプル型は同じものですか
広い意味では、どちらも複数の値をまとめるタプルです。しかし、C#では次の2つを区別する必要があります。
C#// C#のタプル型・ValueTuple
(string Name, int Age) user1 = ("田中", 25);
// System.Tuple
Tuple<string, int> user2 = Tuple.Create("田中", 25);
前者は主にValueTupleを利用する値型、後者はSystem.Tupleクラスという参照型です。
10-2. ValueTupleを使うためにSystem.ValueTupleの追加は必要ですか
現在の.NETや、ValueTupleを標準で含む対象フレームワークでは、通常、System.ValueTupleパッケージを個別に追加する必要はありません。
古い.NET Frameworkなど、対象環境にValueTupleの実装が含まれていない場合は、NuGetのSystem.ValueTupleパッケージが必要になることがあります。System.ValueTupleは.NET Framework 4.7以降などで標準提供されています。Microsoft Learn+1
また、タプル構文を使うには、対応したC#言語バージョンのコンパイラーも必要です。
10-3. タプルには最大何個まで値を格納できますか
C#のタプル構文では、8個以上を含む多数の要素を記述できます。実装上は、7個の要素と残りのタプルを保持する構造を入れ子にして表現します。MicrosoftのC#リファレンスでも、多数の要素を持つタプルの例が示されています。Microsoft Learn+1
ただし、要素数が多いタプルは可読性が低いため、実務上は専用の型へ変更するべきです。
10-4. タプル全体をnullにできますか
System.Tupleは参照型なので、タプル全体をnullにできます。
C#Tuple<string, int>? tuple = null;
ValueTupleは値型なので、そのままではnullにできません。nullable値型にすればnullを代入できます。
C#(string Name, int Age)? valueTuple = null;
10-5. タプルの要素名が参照できないのはなぜですか
タプルを名前なしの型として受け取ると、元の要素名を使えないことがあります。
C#var source = (Name: "田中", Age: 25);
(string, int) destination = source;
Console.WriteLine(destination.Item1);
// Console.WriteLine(destination.Name); // コンパイルエラー
destinationの宣言型にはNameとAgeが含まれていないため、既定名のItem1とItem2を使います。
要素名を使いたい場合は、受け取り側でも名前を指定します。
C#(string Name, int Age) destination = source;
タプルの要素名は型の実行時同一性には影響せず、代入や比較では基本的に無視されます。Microsoft Learn+1
10-6. TupleとValueTupleは相互変換できますか
.NETには、System.TupleとValueTupleを変換するToTupleおよびToValueTuple拡張メソッドがあります。
C#var valueTuple = (Name: "田中", Age: 25);
Tuple<string, int> tuple = valueTuple.ToTuple();
(string, int) convertedBack = tuple.ToValueTuple();
変換後のValueTupleでは、元の名前付きタプルの要素名が自動的に復元されるわけではありません。必要に応じて受け取り側で名前を指定します。
C#(string Name, int Age) user = tuple.ToValueTuple();
これらの変換メソッドはSystem.TupleExtensionsによって提供されています。Microsoft Learn+1
10-7. 戻り値が多い場合もタプルを使うべきですか
戻り値が2個や3個程度で、処理結果として一時的に使うなら、名前付きタプルが便利です。
C#static (int Minimum, int Maximum, double Average) Analyze(int[] values)
{
return (
values.Min(),
values.Max(),
values.Average()
);
}
一方、要素数が多い場合、複数のメソッドで共通利用する場合、入力チェックや振る舞いを持たせたい場合は、専用のクラスやrecordを使いましょう。
C#public sealed record AnalysisResult(
int Minimum,
int Maximum,
double Average
);
タプルは「型を定義するほどではない小規模な複数値」に適しています。戻り値が複雑になった時点で専用型へ切り替えることが、保守しやすいC#コードにつながります。
まとめ
C#のTupleは、複数の値を1つにまとめ、メソッドから複数の結果を返すときに便利な仕組みです。
現在のC#では、次のような名前付きタプルを使う方法が一般的です。
C#static (string Name, int Age) GetUser()
{
return ("田中", 25);
}
var (name, age) = GetUser();
C#のタプル型は内部的にValueTupleを利用する値型で、要素名、分解、変更、要素ごとの等値比較などを利用できます。一方、System.Tupleは参照型であり、要素は読み取り専用のItem1、Item2プロパティです。
一時的な複数値や小規模な戻り値には名前付きタプルを使い、公開API、要素数が多いデータ、業務上重要な概念にはクラス、構造体、recordなどの専用型を使い分けましょう。

