C#のタプルとは?使い方・戻り値・分解・ValueTupleを初心者向けに解説

はじめに

C#のタプルは、複数の値をひとつのまとまりとして扱うための仕組みです。たとえば、メソッドから「計算結果」と「成功したかどうか」を同時に返したり、LINQで処理中のデータに一時的な情報を付け加えたりできます。

C# 7.0以降では、丸括弧を使った簡潔な構文でValueTupleを利用できます。

C#
(string Name, int Age) user = ("田中", 30);

Console.WriteLine(user.Name);
Console.WriteLine(user.Age);

タプルを使えば、複数の戻り値を返すためだけにクラスを作成する必要がありません。一方で、要素数が多い場合や公開APIで利用する場合には、専用クラスやrecordのほうが適していることもあります。

この記事では、C#のタプルの基本的な使い方から、戻り値、分解、ValueTupleTupleの違い、LINQでの活用方法、注意点まで初心者向けに解説します。

1. C#のタプルとは

1-1. 複数の値をひとつにまとめられるデータ構造

タプルとは、複数の値を決められた順番でひとつにまとめるデータ構造です。

C#では、次のように異なる型の値をひとつのタプルに格納できます。

C#
(string, int, bool) user = ("佐藤", 25, true);

このタプルには、次の3つの値が含まれています。

  • string型の名前

  • int型の年齢

  • bool型の有効状態

配列とは異なり、タプルの各要素は同じ型である必要がありません。

C#
var product = ("ノートパソコン", 120000, 4.5);

この場合、コンパイラは要素の型をそれぞれstringintdoubleと推論します。

1-2. タプルを使うメリット

タプルには、主に次のようなメリットがあります。

1つ目は、複数の値を簡単にまとめられることです。

C#
var result = (10, 20);

2つ目は、メソッドから複数の値を返せることです。

C#
static (int Sum, int Difference) Calculate(int x, int y)
{
return (x + y, x - y);
}

3つ目は、要素に名前を付けることでコードの意味を分かりやすくできることです。

C#
var user = (Name: "鈴木", Age: 28);

Console.WriteLine(user.Name);
Console.WriteLine(user.Age);

複数の値を一時的に扱うだけであれば、専用クラスを定義するよりも少ないコードで実装できます。

1-3. クラス・構造体・配列との違い

タプル、クラス、構造体、配列には、それぞれ異なる特徴があります。

配列は、基本的に同じ型の値を複数格納するために使います。

C#
int[] numbers = { 10, 20, 30 };

タプルは異なる型の値を格納できます。

C#
var user = ("山田", 30, true);

クラスや構造体は、データだけでなく、メソッド、プロパティ、入力値の検証などを持たせられます。

C#
public class User
{
public string Name { get; set; } = "";
public int Age { get; set; }

public bool IsAdult()
{
return Age >= 18;
}
}

タプルは、一時的なデータやメソッド内部の中間結果に向いています。クラスや構造体は、明確な意味や振る舞いを持つデータを長期的に扱う場合に適しています。

1-4. C# 7.0以降で使えるValueTupleの特徴

C# 7.0以降のタプル構文では、内部的にSystem.ValueTupleが使われます。

C#
(int, string) data = (1, "C#");

ValueTupleには、次のような特徴があります。

  • 値型である

  • 丸括弧を使って簡潔に記述できる

  • 要素に名前を付けられる

  • 分解代入を利用できる

  • 値の変更が可能

  • タプル同士を比較できる

次のコードでは、名前付きタプルを作成しています。

C#
(int Id, string Name) user = (1, "高橋");

Console.WriteLine(user.Id);
Console.WriteLine(user.Name);

従来のSystem.Tupleよりも記述が簡潔で、要素名を使えるため、通常はValueTupleを利用するのが一般的です。

2. C#でタプルを作成する基本的な書き方

2-1. 丸括弧を使ってタプルを定義する方法

C#では、値を丸括弧で囲み、カンマで区切るとタプルを作成できます。

C#
var user = ("伊藤", 35);

このコードでは、文字列と整数を含むタプルを作成しています。

型を明示すると、次のように記述できます。

C#
(string, int) user = ("伊藤", 35);

タプルの各要素には、異なる型を指定できます。

C#
var data = (1, "サンプル", true, 3.14);

2-2. 型を明示してタプルを宣言する方法

タプルの型を明示する場合は、丸括弧内に要素の型を記述します。

C#
(string, int) user = ("加藤", 20);

要素名も型と一緒に指定できます。

C#
(string Name, int Age) user = ("加藤", 20);

型を明示すると、どのようなデータを格納するタプルなのかが分かりやすくなります。メソッドの戻り値やフィールドの型を定義する場合にも、この形式を使用します。

C#
static (string Name, int Age) GetUser()
{
return ("加藤", 20);
}

2-3. varを使ってタプルを宣言する方法

ローカル変数では、varを使って型を推論できます。

C#
var user = ("小林", 27);

この場合、コンパイラが要素の値から型を判断します。

名前付きタプルもvarで宣言できます。

C#
var user = (Name: "小林", Age: 27);

Console.WriteLine(user.Name);
Console.WriteLine(user.Age);

既存の変数を使った場合、その変数名がタプルの要素名として推論されることがあります。

C#
string name = "小林";
int age = 27;

var user = (name, age);

Console.WriteLine(user.name);
Console.WriteLine(user.age);

ただし、要素名を明示したほうが意味が伝わりやすい場合は、次のように記述するとよいでしょう。

C#
var user = (Name: name, Age: age);

2-4. タプルの要素をItem1・Item2で取得する方法

要素名を指定していないタプルでは、Item1Item2のようなプロパティを使って値を取得できます。

C#
var user = ("中村", 40);

Console.WriteLine(user.Item1);
Console.WriteLine(user.Item2);

実行結果は次のとおりです。

中村
40

要素が3つある場合は、Item3を使います。

C#
var product = ("マウス", 3000, true);

Console.WriteLine(product.Item1);
Console.WriteLine(product.Item2);
Console.WriteLine(product.Item3);

ただし、Item1Item2だけでは値の意味が分かりにくくなります。実際のコードでは、できるだけ要素名を付けることが推奨されます。

2-5. タプルの要素に名前を付ける方法

タプルの各要素には名前を付けられます。

C#
var user = (Name: "吉田", Age: 32);

型を明示する場合は、型の後ろに要素名を記述します。

C#
(string Name, int Age) user = ("吉田", 32);

値を記述する側でも名前を付けられます。

C#
(string Name, int Age) user = (Name: "吉田", Age: 32);

要素名を付けると、Item1Item2ではなく、意味のある名前で値へアクセスできます。

C#
Console.WriteLine(user.Name);
Console.WriteLine(user.Age);

2-6. 名前付きタプルの値を取得・変更する方法

名前付きタプルの値は、ドット記法で取得できます。

C#
var product = (Name: "キーボード", Price: 5000);

Console.WriteLine(product.Name);
Console.WriteLine(product.Price);

ValueTupleは変更可能な値型であるため、作成後に要素を書き換えられます。

C#
product.Name = "メカニカルキーボード";
product.Price = 8000;

Console.WriteLine(product.Name);
Console.WriteLine(product.Price);

ただし、readonlyなフィールドや読み取り専用の変数として扱われている場合は、要素を変更できません。

3. C#のメソッドでタプルを戻り値として使う方法

3-1. タプルを戻り値にする基本構文

メソッドの戻り値の型にタプルを指定すると、複数の値をまとめて返せます。

C#
static (int, int) Calculate(int x, int y)
{
return (x + y, x * y);
}

呼び出し元では、戻り値をタプルとして受け取ります。

C#
var result = Calculate(5, 3);

Console.WriteLine(result.Item1);
Console.WriteLine(result.Item2);

この方法では、Item1Item2だけを見ても値の意味が分かりません。戻り値には要素名を付けると読みやすくなります。

3-2. 名前付きタプルを返すメソッドの書き方

戻り値のタプルに名前を付ける場合は、型の後ろに要素名を記述します。

C#
static (int Sum, int Product) Calculate(int x, int y)
{
return (x + y, x * y);
}

呼び出し元では、指定した名前を使って値を取得できます。

C#
var result = Calculate(5, 3);

Console.WriteLine(result.Sum);
Console.WriteLine(result.Product);

戻り値を作るときにも名前を指定できます。

C#
static (int Sum, int Product) Calculate(int x, int y)
{
return (Sum: x + y, Product: x * y);
}

戻り値の型で要素名が定義されていれば、return側の名前は省略しても構いません。

3-3. 呼び出し元で戻り値を受け取る方法

タプルの戻り値は、ひとつの変数として受け取れます。

C#
var result = Calculate(10, 4);

Console.WriteLine(result.Sum);
Console.WriteLine(result.Product);

型を明示して受け取ることもできます。

C#
(int Sum, int Product) result = Calculate(10, 4);

分解して個別の変数として受け取ることも可能です。

C#
var (sum, product) = Calculate(10, 4);

Console.WriteLine(sum);
Console.WriteLine(product);

明示的な型を指定する場合は、次のように記述します。

C#
(int sum, int product) = Calculate(10, 4);

3-4. 複数の計算結果をまとめて返すサンプル

次のメソッドは、整数の配列から合計値、平均値、最大値を計算して返します。

C#
static (int Sum, double Average, int Max) Analyze(int[] numbers)
{
if (numbers.Length == 0)
{
throw new ArgumentException("配列に要素がありません。", nameof(numbers));
}

int sum = numbers.Sum();
double average = numbers.Average();
int max = numbers.Max();

return (sum, average, max);
}

呼び出し元では、次のように利用できます。

C#
int[] numbers = { 10, 20, 30, 40 };

var result = Analyze(numbers);

Console.WriteLine($"合計: {result.Sum}");
Console.WriteLine($"平均: {result.Average}");
Console.WriteLine($"最大: {result.Max}");

分解して受け取ることもできます。

C#
var (sum, average, max) = Analyze(numbers);

Console.WriteLine($"合計: {sum}");
Console.WriteLine($"平均: {average}");
Console.WriteLine($"最大: {max}");

3-5. out引数とタプルの使い分け

C#では、out引数を使って複数の値を返す方法もあります。

C#
static void Calculate(
int x,
int y,
out int sum,
out int product)
{
sum = x + y;
product = x * y;
}

呼び出し元は次のようになります。

C#
Calculate(5, 3, out int sum, out int product);

同じ処理をタプルで記述すると、次のようになります。

C#
static (int Sum, int Product) Calculate(int x, int y)
{
return (x + y, x * y);
}

一般的な複数戻り値では、タプルのほうが戻り値のまとまりを表現しやすく、メソッド呼び出しも簡潔です。

一方、int.TryParseのように、処理の成功・失敗を戻り値で返し、結果をout引数で取得する慣用的なAPIでは、outが適している場合があります。

C#
if (int.TryParse("123", out int number))
{
Console.WriteLine(number);
}

既存の.NET APIと一貫した形式にしたい場合や、結果を既存変数へ直接格納したい場合は、out引数も選択肢になります。

3-6. 非同期メソッドでタプルを返す方法

非同期メソッドでタプルを返す場合は、Taskの型引数にタプル型を指定します。

C#
static async Task<(string Name, int Age)> GetUserAsync()
{
await Task.Delay(100);

return ("佐々木", 26);
}

呼び出し元では、awaitを使って結果を受け取ります。

C#
var user = await GetUserAsync();

Console.WriteLine(user.Name);
Console.WriteLine(user.Age);

分解して受け取ることも可能です。

C#
var (name, age) = await GetUserAsync();

Console.WriteLine(name);
Console.WriteLine(age);

戻り値を持たないTaskではなく、必ずTask<(型, 型)>のようにタプル型を指定します。

4. C#のタプルを分解する方法

4-1. 分解宣言の基本構文

タプルの各要素を個別の変数へ取り出す操作を、分解または分解宣言と呼びます。

C#
var user = (Name: "松本", Age: 24);

(string name, int age) = user;

Console.WriteLine(name);
Console.WriteLine(age);

右辺にタプルを直接記述することもできます。

C#
(string name, int age) = ("松本", 24);

タプルの要素数と、左辺の変数数は一致している必要があります。

4-2. 既存の変数へタプルを分解代入する方法

すでに宣言されている変数へ、タプルの値を代入できます。

C#
string name;
int age;

(name, age) = ("井上", 31);

Console.WriteLine(name);
Console.WriteLine(age);

分解代入では、左辺の変数を事前に宣言しておく必要があります。

C#
int x = 0;
int y = 0;

(x, y) = (10, 20);

既存変数の値をまとめて更新したい場合に便利です。

4-3. varを使って簡潔に分解する方法

分解する変数の型は、varを使って推論できます。

C#
var user = (Name: "木村", Age: 29);

var (name, age) = user;

各変数に個別にvarを指定することもできます。

C#
(var name, var age) = user;

一般的には、次の形式が簡潔です。

C#
var (name, age) = user;

ただし、varを個々の要素と混在させることはできません。

C#
// コンパイルエラー
// (var name, int age) = user;

型を個別に指定する場合は、すべての型を明示します。

C#
(string name, int age) = user;

4-4. 不要な要素を破棄する方法

タプルの一部だけが必要な場合は、破棄を表すアンダースコア_を使用できます。

C#
var user = (Name: "清水", Age: 22, Email: "shimizu@example.com");

var (name, _, email) = user;

Console.WriteLine(name);
Console.WriteLine(email);

年齢が不要であるため、2番目の要素を_で破棄しています。

複数の要素を破棄することもできます。

C#
var (_, age, _) = user;

Console.WriteLine(age);

不要な値のために変数名を考える必要がなく、使用しない変数も作成されません。

4-5. foreachでタプルを分解する方法

タプルを格納したコレクションは、foreachの中で分解できます。

C#
var users = new List<(string Name, int Age)>
{
("田中", 30),
("佐藤", 25),
("鈴木", 28)
};

foreach (var (name, age) in users)
{
Console.WriteLine($"{name}: {age}歳");
}

不要な要素を破棄することもできます。

C#
foreach (var (name, _) in users)
{
Console.WriteLine(name);
}

Dictionary<TKey, TValue>の要素もKeyValuePair<TKey, TValue>として分解できます。

C#
var scores = new Dictionary<string, int>
{
["田中"] = 80,
["佐藤"] = 90
};

foreach (var (name, score) in scores)
{
Console.WriteLine($"{name}: {score}点");
}

4-6. 独自クラスをDeconstructメソッドで分解する方法

タプル以外の独自クラスでも、Deconstructメソッドを定義すると分解できます。

C#
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;
}
}

次のように、タプルと同じ形式で分解できます。

C#
var user = new User("山本", 33);

var (name, age) = user;

Console.WriteLine(name);
Console.WriteLine(age);

Deconstructは、戻り値を持たず、各要素をout引数で返すメソッドとして定義します。

拡張メソッドとして追加することもできます。

C#
public static class UserExtensions
{
public static void Deconstruct(
this User user,
out string name,
out int age)
{
name = user.Name;
age = user.Age;
}
}

5. ValueTupleとTupleクラスの違い

5-1. System.ValueTupleとは

System.ValueTupleは、C# 7.0以降のタプル構文で使用される値型です。

C#
ValueTuple<string, int> user =
new ValueTuple<string, int>("田中", 30);

通常は、より簡潔なタプル構文を使います。

C#
(string Name, int Age) user = ("田中", 30);

ValueTupleの主な特徴は次のとおりです。

  • 構造体として定義された値型

  • 要素の値を変更できる

  • 分解を利用できる

  • 名前付き要素を記述できる

  • 丸括弧による簡潔な構文を利用できる

5-2. System.Tupleとは

System.Tupleは、.NET Framework 4で追加された参照型のタプルです。

C#
Tuple<string, int> user =
Tuple.Create("田中", 30);

値はItem1Item2で取得します。

C#
Console.WriteLine(user.Item1);
Console.WriteLine(user.Item2);

Tupleの要素は読み取り専用です。作成後に値を変更できません。

C#
// 代入できない
// user.Item1 = "佐藤";

現在のC#で新しいコードを書く場合は、特別な理由がなければValueTupleを利用するほうが簡潔です。

5-3. 値型と参照型による違い

ValueTupleは値型、Tupleは参照型です。

C#
var valueTuple = (Name: "田中", Age: 30);
var tuple = Tuple.Create("田中", 30);

ValueTupleを別の変数に代入すると、値がコピーされます。

C#
var original = (Name: "田中", Age: 30);
var copy = original;

copy.Age = 40;

Console.WriteLine(original.Age); // 30
Console.WriteLine(copy.Age); // 40

一方、Tupleを別の変数に代入すると、同じオブジェクトへの参照がコピーされます。ただし、Tuple自体の要素は読み取り専用なので、作成後に値を書き換えることはできません。

値型であるValueTupleは、状況によってスタック上や別オブジェクトの内部に直接保持されます。objectやインターフェース型として扱う場合には、ボックス化が発生することがあります。

5-4. 要素名・可読性・パフォーマンスの違い

Tupleでは、要素へのアクセスにItem1Item2を使います。

C#
var user = Tuple.Create("田中", 30);

Console.WriteLine(user.Item1);
Console.WriteLine(user.Item2);

ValueTupleでは、要素に名前を付けられます。

C#
var user = (Name: "田中", Age: 30);

Console.WriteLine(user.Name);
Console.WriteLine(user.Age);

名前付きタプルのほうが、各値の意味を理解しやすくなります。

また、Tupleは通常オブジェクトの生成を伴いますが、ValueTupleは値型です。小さな一時データを扱う場合には、ValueTupleで不要なヒープ割り当てを避けられる可能性があります。

ただし、実際のパフォーマンスは、要素数、コピー回数、ボックス化、利用方法によって変わります。性能が重要な処理では、推測だけで判断せず計測することが大切です。

5-5. ValueTupleとTupleの使い分け

新しいC#コードでは、基本的にValueTupleが適しています。

C#
var result = (Success: true, Message: "登録しました");

Tupleを利用する場面としては、次のようなケースがあります。

  • 古いコードとの互換性を維持する

  • 既存APIがTupleを返している

  • 読み取り専用の参照型タプルが必要

  • C# 7.0より前のコンパイラ環境を使用している

通常のアプリケーション開発では、構文が簡潔で要素名も使えるValueTupleを第一候補にするとよいでしょう。

5-6. 古い.NET環境でValueTupleを利用する方法

古い.NET Frameworkなど、System.ValueTupleが標準ライブラリに含まれていない環境では、NuGetパッケージの追加が必要になる場合があります。

パッケージマネージャーコンソールでは、次のように追加できます。

PowerShell
Install-Package System.ValueTuple

.NET CLIを利用する場合は、次の形式です。

Bash
dotnet add package System.ValueTuple

ただし、タプル構文を利用するには、実行環境だけでなく、C# 7.0以降を扱えるコンパイラも必要です。

現在の.NETや新しい.NET Framework向けプロジェクトでは、通常、追加パッケージなしで利用できます。古いプロジェクトへ導入する場合は、対象フレームワークとコンパイラのバージョンを確認しましょう。

6. タプルを引数・コレクション・LINQで使う方法

6-1. メソッドの引数としてタプルを渡す方法

タプルは、メソッドの引数として渡せます。

C#
static void PrintUser((string Name, int Age) user)
{
Console.WriteLine($"{user.Name}: {user.Age}歳");
}

呼び出し時には、タプルをそのまま渡します。

C#
PrintUser(("田中", 30));

変数に格納してから渡すこともできます。

C#
var user = (Name: "佐藤", Age: 25);

PrintUser(user);

引数の意味が明確で、要素数が少ない場合には便利です。ただし、頻繁に利用するデータや入力値の検証が必要なデータには、専用クラスやrecordを検討しましょう。

6-2. Listにタプルを格納する方法

List<T>の型引数にタプル型を指定すると、タプルの一覧を作成できます。

C#
var users = new List<(string Name, int Age)>
{
("田中", 30),
("佐藤", 25),
("鈴木", 28)
};

要素を追加する場合は、Addメソッドを使用します。

C#
users.Add(("高橋", 35));

要素には名前でアクセスできます。

C#
foreach (var user in users)
{
Console.WriteLine($"{user.Name}: {user.Age}歳");
}

インデックスを使って値を取得することも可能です。

C#
Console.WriteLine(users[0].Name);

6-3. Dictionaryとタプルを組み合わせる方法

Dictionaryの値としてタプルを利用できます。

C#
var users = new Dictionary<int, (string Name, int Age)>
{
[1] = ("田中", 30),
[2] = ("佐藤", 25)
};

キーを指定してタプルを取得します。

C#
var user = users[1];

Console.WriteLine(user.Name);
Console.WriteLine(user.Age);

キーとしてタプルを使うこともできます。

C#
var distances = new Dictionary<(string From, string To), int>
{
[("東京", "大阪")] = 500,
[("東京", "名古屋")] = 350
};

int distance = distances[("東京", "大阪")];

Console.WriteLine(distance);

ValueTupleは値に基づく等価比較とハッシュコード生成に対応しているため、複数の値を組み合わせたキーとして利用できます。

6-4. foreachでタプルの一覧を処理する方法

タプルの一覧は、通常の変数として受け取れます。

C#
var products = new List<(string Name, int Price)>
{
("マウス", 3000),
("キーボード", 5000),
("モニター", 30000)
};

foreach (var product in products)
{
Console.WriteLine($"{product.Name}: {product.Price}円");
}

分解すると、より簡潔に記述できます。

C#
foreach (var (name, price) in products)
{
Console.WriteLine($"{name}: {price}円");
}

不要な要素は破棄できます。

C#
foreach (var (name, _) in products)
{
Console.WriteLine(name);
}

6-5. LINQのSelectでタプルを生成する方法

LINQのSelectでは、処理結果としてタプルを生成できます。

C#
var numbers = new[] { 1, 2, 3, 4, 5 };

var results = numbers.Select(
number => (Number: number, Square: number * number));

結果をforeachで処理します。

C#
foreach (var result in results)
{
Console.WriteLine(
$"{result.Number}の2乗は{result.Square}");
}

インデックスを含めることもできます。

C#
var indexed = numbers.Select(
(number, index) => (Index: index, Value: number));

foreach (var item in indexed)
{
Console.WriteLine($"{item.Index}: {item.Value}");
}

6-6. LINQの戻り値を名前付きタプルで扱う方法

LINQで複数の計算結果を保持したい場合は、名前付きタプルが便利です。

C#
var products = new[]
{
new { Name = "マウス", Price = 3000 },
new { Name = "キーボード", Price = 5000 },
new { Name = "モニター", Price = 30000 }
};

var results = products.Select(product =>
(
ProductName: product.Name,
TaxIncludedPrice: (int)(product.Price * 1.1)
));

呼び出し側では、要素名を使って処理できます。

C#
foreach (var result in results)
{
Console.WriteLine(
$"{result.ProductName}: {result.TaxIncludedPrice}円");
}

LINQの中間データであれば、専用クラスを定義せずに必要な値をまとめられます。

7. C#のタプルでよく使う操作

7-1. タプル同士を比較する方法

同じ要素数で、対応する要素同士を比較できる場合、タプルは==!=で比較できます。

C#
var a = (1, "C#");
var b = (1, "C#");

Console.WriteLine(a == b); // True

いずれかの要素が異なる場合はfalseになります。

C#
var a = (1, "C#");
var b = (2, "C#");

Console.WriteLine(a == b); // False

要素名は比較結果に影響しません。

C#
var user1 = (Name: "田中", Age: 30);
var user2 = (UserName: "田中", UserAge: 30);

Console.WriteLine(user1 == user2); // True

比較は先頭要素から順に行われます。すべての対応する要素が等しい場合に、タプル全体も等しいと判断されます。

7-2. タプルを入れ子にする方法

タプルの要素として、別のタプルを格納できます。

C#
var user = (
Name: "田中",
Address: (
Prefecture: "東京都",
City: "新宿区"
)
);

内側の要素には、次のようにアクセスします。

C#
Console.WriteLine(user.Name);
Console.WriteLine(user.Address.Prefecture);
Console.WriteLine(user.Address.City);

入れ子のタプルも分解できます。

C#
var (name, (prefecture, city)) = user;

Console.WriteLine(name);
Console.WriteLine(prefecture);
Console.WriteLine(city);

ただし、入れ子が深くなると構造を理解しにくくなります。複雑なデータには、専用クラスやrecordを使用したほうがよいでしょう。

7-3. 2つの変数の値をタプルで入れ替える方法

タプルの分解代入を使うと、一時変数を用意せずに2つの値を入れ替えられます。

C#
int x = 10;
int y = 20;

(x, y) = (y, x);

Console.WriteLine(x); // 20
Console.WriteLine(y); // 10

従来は、一時変数が必要でした。

C#
int temp = x;
x = y;
y = temp;

タプルを使った形式は、値の交換であることがひと目で分かります。

7-4. nullableなタプルを扱う方法

ValueTupleは値型なので、タプル全体をnullにしたい場合はnullable値型にします。

C#
(string Name, int Age)? user = null;

値を代入できます。

C#
user = ("田中", 30);

値が存在するか確認するには、パターンマッチングを使うと安全です。

C#
if (user is { } value)
{
Console.WriteLine(value.Name);
Console.WriteLine(value.Age);
}

HasValueValueを使う方法もあります。

C#
if (user.HasValue)
{
Console.WriteLine(user.Value.Name);
}

要素だけをnullableにすることもできます。

C#
(string? Name, int? Age) user = (null, null);

次の2つは意味が異なります。

C#
// タプル全体が存在しない可能性がある
(string Name, int Age)? user1 = null;

// タプルは存在するが、各要素がnullになる可能性がある
(string? Name, int? Age) user2 = (null, null);

7-5. タプルをJSONへシリアライズする際の注意点

タプルをJSONへシリアライズするときは、期待した要素名で出力されないことがあります。

名前付きタプルの要素名は、実行時のフィールド名そのものではありません。ValueTupleの実際のフィールドは、基本的にItem1Item2などです。

C#
var user = (Name: "田中", Age: 30);

System.Text.Jsonでは、フィールドを含める設定をしなければ、ValueTupleの値が期待どおりに出力されない場合があります。

C#
using System.Text.Json;

var options = new JsonSerializerOptions
{
IncludeFields = true
};

string json = JsonSerializer.Serialize(user, options);

Console.WriteLine(json);

この場合も、JSONのプロパティ名は通常Item1Item2になります。

外部へ送信するJSONでは、タプルではなく専用クラスやrecordを利用するほうが安全です。

C#
public record UserResponse(string Name, int Age);
C#
var user = new UserResponse("田中", 30);
string json = JsonSerializer.Serialize(user);

これなら、NameAgeという分かりやすいプロパティ名でJSONを生成できます。

7-6. タプルの要素数が多い場合の扱い方

C#のタプル構文では、8個以上の要素も記述できます。

C#
var data = (
1, 2, 3, 4, 5,
6, 7, 8, 9, 10
);

内部的なValueTupleでは、8番目の型引数が残りの要素を保持する入れ子構造として使われます。ただし、通常のC#タプル構文を使う限り、その内部構造を意識する必要はほとんどありません。

技術的に多くの要素を格納できても、要素数が多いタプルは次の問題を起こします。

  • 各要素の意味を把握しにくい

  • 順番を間違えやすい

  • メソッドのシグネチャが読みにくい

  • 要素追加時の影響が大きい

  • 分解代入が長くなる

要素が4個や5個を超え、ひとつの概念を表している場合は、クラス、構造体、recordなどへの切り替えを検討しましょう。

8. C#でタプルを使うべき場面・使わないほうがよい場面

8-1. 一時的に複数の値をまとめたい場面

タプルは、メソッド内部で一時的に複数の値をまとめたい場合に適しています。

C#
var range = (Min: 10, Max: 100);

短い範囲でしか使用しないデータであれば、専用の型を定義するよりも簡潔です。

たとえば、並べ替えのために値と優先順位をまとめる場合にも使えます。

C#
var items = new[]
{
(Name: "A", Priority: 2),
(Name: "B", Priority: 1)
};

var sorted = items.OrderBy(item => item.Priority);

8-2. メソッドから関連する複数の値を返したい場面

ひとつの処理から関連する複数の結果が得られる場合、タプルは便利です。

C#
static (int Min, int Max) GetRange(int[] numbers)
{
return (numbers.Min(), numbers.Max());
}

次のように受け取れます。

C#
var (min, max) = GetRange(new[] { 3, 8, 1, 5 });

Console.WriteLine(min);
Console.WriteLine(max);

戻り値が一時的に利用され、結果に複雑な振る舞いが必要ない場合に向いています。

8-3. LINQの中間データを扱いたい場面

LINQの処理中に、元の値と計算結果をまとめたい場合にもタプルが役立ちます。

C#
var results = Enumerable.Range(1, 5)
.Select(number => (
Number: number,
IsEven: number % 2 == 0
))
.Where(item => item.IsEven);
C#
foreach (var item in results)
{
Console.WriteLine(item.Number);
}

最終的な公開データではなく、クエリ内部だけで利用する中間データであれば、タプルを使うことで型定義を減らせます。

8-4. 意味の異なるデータを無理にまとめるべきでない理由

タプルには異なる型の値を自由に格納できますが、関係の薄い値を無理にまとめると、コードの意図が分かりにくくなります。

C#
var data = (
UserName: "田中",
ProductPrice: 3000,
ServerStatus: true
);

この3つの値は、ひとつの概念としてまとまっていません。このようなタプルは、どこで何のために使われるのか理解しにくくなります。

タプルにまとめる値は、同じ処理結果や同じ一時データなど、明確な関連性を持つものに限定しましょう。

8-5. 公開APIでは専用クラスやrecordを検討すべき理由

外部のコードから利用される公開APIでタプルを返すと、将来の仕様変更が難しくなる場合があります。

C#
public (string Name, int Age) GetUser()
{
return ("田中", 30);
}

後からメールアドレスを追加すると、戻り値の型そのものが変わります。

C#
public (string Name, int Age, string Email) GetUser()
{
return ("田中", 30, "tanaka@example.com");
}

利用側のコードにも修正が必要になる可能性があります。

専用のrecordを使えば、型の意味が明確になります。

C#
public record UserResult(
string Name,
int Age,
string Email);

専用型には、XMLドキュメント、属性、検証処理、メソッドなども追加できます。長期間維持する公開APIでは、タプルよりも専用型を検討する価値があります。

8-6. タプルからクラス・recordへ切り替える判断基準

次のような状況になったら、タプルから専用型への切り替えを検討しましょう。

  • 要素数が増えてきた

  • 同じタプル型を複数箇所で使っている

  • データに明確な名前や概念がある

  • 入力値の検証が必要

  • メソッドや計算プロパティを追加したい

  • JSONやデータベースとの変換に使う

  • 公開APIの戻り値として長期間利用する

  • 要素の追加や変更が予想される

単純なデータを不変オブジェクトとして表現するなら、recordが適しています。

C#
public record CalculationResult(
int Sum,
int Product);

状態や振る舞いを持たせる場合は、クラスが適しています。

C#
public class CalculationResult
{
public int Sum { get; }
public int Product { get; }

public CalculationResult(int sum, int product)
{
Sum = sum;
Product = product;
}

public string Format()
{
return $"合計: {Sum}, 積: {Product}";
}
}

9. C#のタプルを使う際の注意点

9-1. 要素名は実行時の型情報として扱われない

名前付きタプルは、ソースコード上では分かりやすく見えます。

C#
(string Name, int Age) user = ("田中", 30);

しかし、実行時の基本的な型はValueTuple<string, int>です。NameAgeという名前は、Item1Item2とは別の実フィールドとして定義されるわけではありません。

要素名は、コンパイラやメタデータによって補助的に扱われます。そのため、リフレクション、動的処理、シリアライズなどでは、要素名が期待どおりに利用されない場合があります。

外部データ形式のプロパティ名として要素名を確実に保持したい場合は、クラスやrecordを使用しましょう。

9-2. Item1・Item2の多用は可読性を下げる

次のコードでは、各値の意味をすぐに判断できません。

C#
var result = GetResult();

Console.WriteLine(result.Item1);
Console.WriteLine(result.Item2);

名前付きタプルを使うと、意味が明確になります。

C#
var result = GetResult();

Console.WriteLine(result.Success);
Console.WriteLine(result.Message);

メソッドの戻り値も、次のように定義します。

C#
static (bool Success, string Message) GetResult()
{
return (true, "処理が完了しました");
}

短いコードでも、できるだけ意味のある要素名を付けることが大切です。

9-3. 要素数が多いタプルは保守しにくい

要素数が多いタプルは、宣言や分解が長くなります。

C#
var user = (
Id: 1,
Name: "田中",
Age: 30,
Email: "tanaka@example.com",
IsActive: true,
CreatedAt: DateTime.Now
);

このデータは「ユーザー」という明確な概念を持っています。そのため、専用の型にしたほうが自然です。

C#
public record User(
int Id,
string Name,
int Age,
string Email,
bool IsActive,
DateTime CreatedAt);

要素数だけで機械的に判断する必要はありませんが、名前付き要素が増え、複数箇所で利用されるようになったら専用型を検討しましょう。

9-4. タプルの要素名が異なっても代入できる

タプルの型は、基本的に要素の型と順序で決まります。要素名は型の同一性を決めるものではありません。

C#
(string Name, int Age) user = ("田中", 30);

(string ProductName, int Price) product = user;

この代入は、どちらもstringintの組み合わせであるため可能です。

C#
Console.WriteLine(product.ProductName); // 田中
Console.WriteLine(product.Price); // 30

コンパイルできても、意味として正しいとは限りません。型の並びが同じだけで、異なる意味のデータを誤って代入する可能性があります。

型レベルで用途を区別したい場合は、専用クラスやrecordを使いましょう。

9-5. 値型であるValueTupleのコピー動作

ValueTupleは値型なので、変数へ代入したときに値がコピーされます。

C#
var original = (Count: 10, Message: "開始");
var copy = original;

copy.Count = 20;

Console.WriteLine(original.Count); // 10
Console.WriteLine(copy.Count); // 20

ただし、タプルの要素に参照型が含まれている場合、その参照先まで複製されるわけではありません。

C#
var original = (
Name: "データ",
Numbers: new List<int> { 1, 2 }
);

var copy = original;

copy.Numbers.Add(3);

Console.WriteLine(original.Numbers.Count); // 3

タプル自体はコピーされていますが、Numbersには同じList<int>への参照が格納されています。この違いを理解しておく必要があります。

9-6. null・defaultを扱う際の注意点

ValueTuple自体は値型なので、通常はnullを代入できません。

C#
(string Name, int Age) user;

// コンパイルエラー
// user = null;

nullを許可する場合は、nullable値型にします。

C#
(string Name, int Age)? user = null;

defaultを代入すると、各要素にその型の既定値が設定されます。

C#
(string Name, int Age) user = default;

Console.WriteLine(user.Name is null); // True
Console.WriteLine(user.Age); // 0

参照型要素がnull非許容として宣言されていても、defaultによって実際にはnullが入る可能性があります。

そのため、defaultで初期化したタプルを使用する場合は、各要素の状態を確認する必要があります。

C#
if (user.Name is not null)
{
Console.WriteLine(user.Name);
}

10. C#のタプルに関するよくある質問

10-1. タプルには何個まで値を格納できる?

C#のタプル構文では、8個以上の値も格納できます。

C#
var values = (
1, 2, 3, 4, 5,
6, 7, 8, 9, 10
);

ValueTupleのジェネリック型は、8番目の型引数を残りの要素を格納するための入れ子として利用します。C#コンパイラがこの構造を処理するため、通常は内部構造を意識せずに利用できます。

ただし、技術的に格納できることと、読みやすいコードであることは別です。要素数が多くなる場合は、クラス、構造体、recordへの変更を検討してください。

10-2. タプルの要素に異なる型を指定できる?

タプルの各要素には、異なる型を指定できます。

C#
var data = (
Id: 1,
Name: "商品A",
Price: 1980.5m,
IsAvailable: true,
CreatedAt: DateTime.Now
);

このタプルには、intstringdecimalboolDateTimeが含まれています。

配列は基本的に同じ型の値を格納しますが、タプルは要素ごとに型を指定できる点が特徴です。

10-3. タプルの値は後から変更できる?

ValueTupleの要素は、後から変更できます。

C#
var user = (Name: "田中", Age: 30);

user.Name = "佐藤";
user.Age = 31;

一方、System.Tupleの要素は読み取り専用です。

C#
var user = Tuple.Create("田中", 30);

// 変更できない
// user.Item1 = "佐藤";

また、ValueTupleであっても、読み取り専用のフィールドや値として扱われている場合には変更できません。

10-4. タプルをメソッドの戻り値に使うのは非推奨?

タプルを戻り値に使うこと自体は非推奨ではありません。関連する複数の値を一時的に返す用途では、分かりやすく実用的です。

C#
static (int Min, int Max) GetRange(int[] values)
{
return (values.Min(), values.Max());
}

ただし、次のような場合は専用型が適しています。

  • 戻り値が公開APIの一部になる

  • 要素数が多い

  • 同じ結果型を複数箇所で使う

  • 将来的に要素が増える可能性が高い

  • 値の検証やメソッドが必要

  • JSONなど外部形式へ変換する

メソッド内部や限定された範囲ではタプル、長期的に利用するドメインデータでは専用型、という使い分けが基本です。

10-5. ValueTupleを使うためにusingは必要?

丸括弧を使った通常のタプル構文では、System.ValueTupleを直接記述しないため、タプル専用のusingは基本的に必要ありません。

C#
var user = (Name: "田中", Age: 30);

ValueTuple<T1, T2>という型名を直接記述する場合、System名前空間をインポートしていなければ完全修飾名が必要です。

C#
System.ValueTuple<string, int> user =
new("田中", 30);

現在の一般的なC#プロジェクトでは、暗黙的なusingusing System;が利用されるため、通常は意識する必要がありません。

古い.NET環境では、usingではなく、System.ValueTupleパッケージの追加が必要になる場合があります。

10-6. タプルと匿名型はどのように使い分ける?

匿名型は、主にメソッド内部やLINQで、読み取り専用の一時オブジェクトを作成するために使います。

C#
var user = new
{
Name = "田中",
Age = 30
};

タプルは、複数の値をまとめて返したり、分解したりする場合に便利です。

C#
var user = (Name: "田中", Age: 30);
var (name, age) = user;

匿名型には次の特徴があります。

  • プロパティ名を持つ

  • 基本的に読み取り専用

  • メソッドの公開戻り値型として明示しにくい

  • LINQの内部処理に向いている

タプルには次の特徴があります。

  • メソッドの戻り値型として宣言できる

  • 分解できる

  • 要素を書き換えられる

  • 要素名は型の同一性に含まれない

LINQ内だけでオブジェクト形式のデータを扱うなら匿名型、複数値の受け渡しや分解が必要ならタプルが適しています。

10-7. タプルとrecordはどのように使い分ける?

タプルは、一時的で小さなデータのまとまりに適しています。

C#
var result = (Success: true, Message: "完了");

recordは、名前を持つ明確なデータモデルに適しています。

C#
public record OperationResult(
bool Success,
string Message);

タプルは定義が不要で、すぐに使える点がメリットです。一方、recordには次の利点があります。

  • 型そのものに名前を付けられる

  • 同じ型を複数箇所で再利用できる

  • プロパティ名を実行時にも保持できる

  • 属性やドキュメントを追加できる

  • メソッドや検証処理を追加できる

  • JSONへ自然にシリアライズできる

  • 公開APIで意味を明確にできる

一度しか使わない単純な中間結果にはタプル、アプリケーション内で意味を持つデータにはrecordを選ぶとよいでしょう。

まとめ

C#のタプルは、異なる型を含む複数の値を、ひとつのまとまりとして扱える機能です。C# 7.0以降では、ValueTupleを利用した簡潔なタプル構文を記述できます。

C#
var user = (Name: "田中", Age: 30);

メソッドの戻り値として使えば、複数の結果をまとめて返せます。

C#
static (int Sum, int Product) Calculate(int x, int y)
{
return (x + y, x * y);
}

分解を使うと、各要素を個別の変数として受け取れます。

C#
var (sum, product) = Calculate(5, 3);

タプルは、メソッド内部の一時データ、複数の戻り値、LINQの中間結果などに向いています。一方、要素数が多いデータ、複数箇所で再利用するデータ、公開API、JSONへ変換するデータには、クラスやrecordのほうが適しています。

タプルを使う際は、Item1Item2を多用せず、意味のある要素名を付けることが大切です。利用範囲とデータの役割を考え、タプルと専用型を適切に使い分けましょう。