C# のタプル(Tuple)は、複数の値を一時的にひとまとめにする軽量なデータ構造です。クラスや構造体を定義するほどでもない場面で、複数の値をまとめて扱いたいときに便利です。C# 7.0 で構文が大きく刷新され、現在では ValueTuple ベースの簡潔な記法が主流になっています。
基本的な使い方
タプルの生成
括弧 () の中にカンマ区切りで値を並べるだけでタプルを作れます。
var point = (3, 4);
Console.WriteLine(point.Item1); // 3
Console.WriteLine(point.Item2); // 4
要素には Item1・Item2・Item3… という名前で既定アクセスできます。
要素に名前を付ける
要素に名前を付けると可読性が上がります。
var point = (X: 3, Y: 4);
Console.WriteLine(point.X); // 3
Console.WriteLine(point.Y); // 4
型として明示する場合も、同様に名前を付けられます。
(int X, int Y) point = (3, 4);
Console.WriteLine(point.X); // 3
Console.WriteLine(point.Y); // 4
複数の型を混在させる
タプルは異なる型の値を組み合わせられます。
var person = (Name: "Alice", Age: 30, IsActive: true);
Console.WriteLine(person.Name); // Alice
Console.WriteLine(person.Age); // 30
Console.WriteLine(person.IsActive); // True
推論された変数名(Inferred Variable Names)— C# 7.1
C# 7.1 から、変数名からタプルの要素名を自動的に推論できます。ValueTuple 内の名前を明示しなくても、元の変数名がそのまま要素名になります。
int x = 10;
int y = 20;
// C# 7.1:変数名 x・y がそのまま要素名になる
var point = (x, y);
Console.WriteLine(point.x); // 10
Console.WriteLine(point.y); // 20
オブジェクトのプロパティからも推論されます。
var user = new { Name = "Bob", Age = 25 };
var tuple = (user.Name, user.Age);
Console.WriteLine(tuple.Name); // Bob
Console.WriteLine(tuple.Age); // 25
推論された名前はコンパイル時の名前であり、リフレクションや ToString() には現れません。
タプルの等値比較(Tuple Equality / Inequality)— C# 7.3
C# 7.3 からタプル同士を == と != で比較できます。要素数が同じで、対応する各要素を順に比較して全員一致すれば等しいと判定されます。
var a = (1, "hello");
var b = (1, "hello");
var c = (2, "hello");
Console.WriteLine(a == b); // True
Console.WriteLine(a == c); // False
Console.WriteLine(a != c); // True
要素名は比較に影響しません。構造と値が同じであれば等しいとみなされます。
var p = (X: 1, Y: 2);
var q = (A: 1, B: 2); // 名前が異なる
Console.WriteLine(p == q); // True(名前は無視される)
null を含む Nullable タプルにも対応しています。
(int, string)? x = (1, "a");
(int, string)? y = null;
Console.WriteLine(x == y); // False
メソッドの戻り値としてのタプル(Method Return Values)
タプルの最も実践的な用途の一つが複数値の返却です。out 引数やラッパークラスを作る手間が省けます。
static (int Min, int Max) GetRange(int[] values)
{
return (values.Min(), values.Max());
}
int[] numbers = { 3, 1, 4, 1, 5, 9, 2, 6 };
var range = GetRange(numbers);
Console.WriteLine(range.Min); // 1
Console.WriteLine(range.Max); // 9
分解して受け取る
戻り値はそのまま分解して個別の変数に受け取れます(後述)。
var (min, max) = GetRange(numbers);
Console.WriteLine($"最小: {min}, 最大: {max}");
複雑な情報をまとめて返す
static (bool Success, string Message, int StatusCode) Validate(string input)
{
if (string.IsNullOrEmpty(input))
return (false, "入力が空です", 400);
if (input.Length > 100)
return (false, "入力が長すぎます", 400);
return (true, "OK", 200);
}
var result = Validate("hello");
if (result.Success)
Console.WriteLine($"[{result.StatusCode}] {result.Message}");
破棄(Discards)と タプル — C# 7.0
タプルの要素のうち、不要な値を捨てるには _(アンダースコア=破棄変数)を使います。
static (int Code, string Message, DateTime Timestamp) GetResult()
=> (200, "OK", DateTime.UtcNow);
// Timestamp は不要なので _ で捨てる
var (code, message, _) = GetResult();
Console.WriteLine($"{code}: {message}");
複数の不要な要素を破棄できます。
// Code だけ必要
var (_, _, timestamp) = GetResult();
Console.WriteLine(timestamp);
破棄は変数ではないため、_ を後から読み取ることはできません。意図を明確にするための構文です。
パターンマッチング switch 式とタプル(Tuple Pattern Matching switch Expressions)— C# 8.0
C# 8.0 の switch 式でタプルを使うと、複数の値の組み合わせに応じた分岐を簡潔に書けます。
状態機械の例
static string Transition(string state, string action) =>
(state, action) switch
{
("Idle", "Start") => "Running",
("Running", "Pause") => "Paused",
("Paused", "Resume") => "Running",
("Running", "Stop") => "Idle",
("Paused", "Stop") => "Idle",
_ => throw new InvalidOperationException($"無効な遷移: {state} + {action}")
};
Console.WriteLine(Transition("Idle", "Start")); // Running
Console.WriteLine(Transition("Running", "Pause")); // Paused
Console.WriteLine(Transition("Paused", "Resume")); // Running
FizzBuzz の例
static string FizzBuzz(int n) =>
(n % 3 == 0, n % 5 == 0) switch
{
(true, true) => "FizzBuzz",
(true, false) => "Fizz",
(false, true) => "Buzz",
(false, false) => n.ToString()
};
for (int i = 1; i <= 15; i++)
Console.WriteLine(FizzBuzz(i));
タプルと switch 式の組み合わせにより、複雑な条件分岐をテーブルのように宣言的に書けます。
タプルの分解(Deconstructing Tuples)— C# 7.0
タプルを分解すると、各要素を個別の変数に展開できます。
新しい変数への分解
var person = (Name: "Alice", Age: 30);
var (name, age) = person;
Console.WriteLine(name); // Alice
Console.WriteLine(age); // 30
型を明示することもできます。
(string name, int age) = person;
既存の変数への分解
すでに宣言された変数にも分解できます。
string name;
int age;
(name, age) = ("Bob", 25);
Console.WriteLine($"{name}, {age}");
メソッド戻り値の即時分解
var (min, max) = GetRange(new[] { 5, 2, 8, 1, 9 });
Console.WriteLine($"min={min}, max={max}");
_ を使った部分分解
不要な要素は _ で破棄できます(前述の破棄と同じです)。
var (_, _, code) = (false, "Error", 404);
Console.WriteLine(code); // 404
クラス・構造体への Deconstruct メソッドの追加
タプルだけでなく、独自クラスにも Deconstruct メソッドを定義すると分解できます。
class Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) { X = x; Y = y; }
public void Deconstruct(out int x, out int y)
{
x = X;
y = Y;
}
}
var point = new Point(3, 4);
var (x, y) = point; // Deconstruct が呼ばれる
Console.WriteLine($"x={x}, y={y}");
拡張メソッドとして外部から追加することもできます。
static class PointExtensions
{
public static void Deconstruct(this Point p, out int x, out int y)
=> (x, y) = (p.X, p.Y);
}
位置パターンマッチング(Deconstructing Tuples with Positional Pattern Matching)— C# 8.0
C# 8.0 では、位置パターン(Positional Pattern) を使って switch 式・is 演算子の中でタプルや Deconstruct 持ちのオブジェクトを同時に分解しながらパターンマッチできます。
タプルの位置パターン
static string Classify(int x, int y) =>
(x, y) switch
{
(0, 0) => "原点",
(> 0, 0) => "X 軸正",
(< 0, 0) => "X 軸負",
(0, > 0) => "Y 軸正",
(0, < 0) => "Y 軸負",
(> 0, > 0) => "第一象限",
(< 0, > 0) => "第二象限",
(< 0, < 0) => "第三象限",
(> 0, < 0) => "第四象限",
_ => "不明"
};
Console.WriteLine(Classify(3, 4)); // 第一象限
Console.WriteLine(Classify(0, 0)); // 原点
Console.WriteLine(Classify(-1, 0)); // X 軸負
Deconstruct を持つクラスへの位置パターン
Deconstruct メソッドを定義したクラスも同様に扱えます。
class Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) { X = x; Y = y; }
public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}
static string Describe(Point p) =>
p switch
{
(0, 0) => "原点",
(> 0, > 0) => "第一象限",
(< 0, > 0) => "第二象限",
_ => "その他"
};
Console.WriteLine(Describe(new Point(5, 3))); // 第一象限
Console.WriteLine(Describe(new Point(0, 0))); // 原点
is 演算子との組み合わせ
is 演算子でも位置パターンが使えます。
Point p = new Point(3, 4);
if (p is (> 0, > 0))
Console.WriteLine("第一象限にあります");
変数への束縛も同時にできます。
if (p is (int px, int py) && px == py)
Console.WriteLine($"x と y が等しい: {px}");
まとめ
| 機能 | 導入 | 要点 |
|---|---|---|
| 基本的なタプル | C# 7.0 | (値, 値) で複数値をまとめる |
| 名前付き要素 | C# 7.0 | (Name: "Alice", Age: 30) で可読性向上 |
| 推論された変数名 | C# 7.1 | 変数名からタプル要素名を自動推論 |
== / != 比較 |
C# 7.3 | 要素を順に比較して等値判定(名前は無視) |
| メソッド戻り値 | C# 7.0 | out 引数やラッパークラス不要で複数値を返せる |
破棄(_) |
C# 7.0 | 不要な要素を _ で捨てる |
Deconstruct |
C# 7.0 | タプルや独自型を個別変数に展開 |
| タプル switch 式 | C# 8.0 | 複数値の組み合わせを宣言的に分岐 |
| 位置パターン | C# 8.0 | switch・is で分解しながらパターンマッチ |
タプルはクラスを定義するコストなしに複数の値をまとめられる強力な機能です。一方で、長期間使い回す複雑なデータ構造には名前付きクラスや record を選ぶなど、用途に応じて使い分けることが重要です。