タプルは、クラスを宣言せずに、少数の決まった数の値を1つの値にまとめます。("Ana", 31) は string と int のタプルです。タプルは、メソッドから2つか3つのものを返したり、メソッドの中で一時的な組を保持したりするための、C#らしい方法です。
タプルを作る
型もかっこで書きます。(string, int) は、最初の要素が string、2番目が int のタプル型です。
出力:
Ana is 31
(3, 4)
179.70
(Ana, 32)
名前がない場合、要素は Item1、Item2、Item3 のようになります。これらのタプルは System.ValueTuple 構造体なので、別の変数に代入すると値がコピーされ、コピーを変更しても元のタプルは変わりません。
名前付きの要素
Item1 では値が何なのかがわかりません。型かリテラルのどちらかで要素に名前を付けます。
出力:
Ana, 31
Desk lamp costs 24.99
Ana
Ana
名前はコンパイル時の便宜です。実行時には上の2つのタプルはどちらもただの ValueTuple<string, int> の値です。だから名前の異なるタプルに person を代入してもコンパイルできます。一致しなければならないのは、要素の型とその順序だけです。
C# 7.1以降では、リテラルの中の変数から名前も推論されるので、var t = (name, age); と書くと、名前を2回書かなくても t.name と t.age が使えます。C# 7.0では、それらの要素は Item1 と Item2 にしかなりません。
メソッドから複数の値を返す
これが、タプルが言語に追加された主な理由です。それ以前の選択肢は、out 引数、使い捨てのクラス、あるいは匿名の Item1 を持つ System.Tuple でした。
出力:
Lowest 60, highest 95
Average 78.75
戻り値の型の要素名が、呼び出し側から見える名前になります。同じメソッドを out 引数で書いたものと比べてみます。
static void Stats(int[] scores, out int min, out int max, out double average) { ... }
Stats(scores, out int min, out int max, out double avg);
out 引数は、成功したかどうかを bool で示して値を一緒に返す TryParse の形では、引き続き慣例になっています。「いくつかのものを計算して返す」なら、タプルのほうが読みやすくなります。refとoutを参照してください。
分解
分解を使うと、1つの文でタプルを別々の変数に展開できます。
出力:
Lima 24.5
Lima
24.5
2 1
var (city, temp) の変数名は自由に付けられます。分解は位置によって行われるので、タプルの要素名と一致する必要はありません。入れ替えの (a, b) = (b, a) は、まず古い値からタプルを作ってから代入するので、どちらの値も失われません。
自分で作ったクラスも、out 引数を持つ Deconstruct メソッドを宣言すれば分解に対応できます。C# 7と.NET Core 2.0以降では KeyValuePair にもこのメソッドがあるので、辞書のループではヘッダーで各エントリを展開できます。
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 (x, y) = new Point(3, 4);
foreach (var (name, score) in scoresByName) // Dictionary<string, int>
{
Console.WriteLine($"{name}: {score}");
}
同じ foreach (var (a, b) in listOfTuples) の形は、タプルのリストにも使えます。次の節のように、タプル全体を1つのループ変数として受け取り、名前付きの要素を読む方法でも同じように動きます。
リスト、LINQ、辞書でのタプル
名前付きタプルのリストは、メソッドの中でデータの行を保持する手軽な方法です。
出力:
Chloe 95
Ana 88
Ben 72
(Ana, B), (Chloe, A)
Ben
False
ValueTuple は Equals と GetHashCode を要素ごとに実装しているので、2つの別の場所で作った (2, 7) が同じ辞書のエントリを見つけます。そのため、タプルは最も簡単な複合キーになります。クラスも、手書きのハッシュ関数も、row + ":" + col のような文字列の連結も要りません。
タプルの等価性
Equals は要素ごとに比較し、タプルが導入されたときから使えます。
出力:
True
False
C# 7.3ではタプルに == と != が追加されました。これらも同じ要素ごとの比較にコンパイルされ、名前は無視されます。
if ((order.Status, order.Paid) == ("shipped", true)) { ... }
Console.WriteLine((1, "x") == (1, "x")); // True
System.TupleとValueTuple
.NET 4では、Tuple.Create で作るクラスの System.Tuple が導入されました。今もフレームワークにあり、古いコードやAPIに今も現れるので、「C# tuple」で検索するとよくこちらにたどり着きます。
ValueTuple(C# 7) | System.Tuple(.NET 4) | |
|---|---|---|
| 構文 | (1, "a")、(int, string) | Tuple.Create(1, "a")、Tuple<int, string> |
| 種類 | 構造体(値型) | クラス(参照型) |
| 要素の名前 | あり | なし、Item1、Item2 のみ |
| 変更可能 | 可、要素はフィールド | 不可、読み取り専用のプロパティ |
| 分解 | 可 | 可、拡張メソッドを通して |
出力:
Ben 25
(Ben, 25)
ToValueTuple() と ToTuple() で2つを相互に変換できます。新しいコードでは値タプルを優先します。ヒープに何も確保せず、名前も持てるからです。
代わりにクラスやレコードを使うべきとき
タプルが最も適しているのは、まとまりが局所的で明らかな場合です。メソッドが2つの数値を返す、LINQクエリが次のステップに組を渡す、辞書が2つの部分からなるキーを必要とする、といった場合です。次のようなときは名前付きの型に切り替えます。
- 同じ形が複数の公開メソッドのシグネチャに現れる。5か所にある
(string, string, int)は、まだ書かれていないクラスです。 - 値に振る舞いや検証が必要な場合、または要素が3つか4つより多い場合。
- シリアル化のようにデータが境界を越える。名前は実行時に存在しないので、Newtonsoft.Jsonは
Item1とItem2を書き出し、System.Text.Jsonはタプルの要素がフィールドであり既定ではフィールドを飛ばすので{}を書き出します。
C# 9のレコードは、本物の名前を持ちながら、タプルの便利さのほとんどを提供します。record Score(string Name, int Points); には、値の等価性、読みやすい ToString、分解が組み込まれています。レコードを参照してください。
よくある間違い
- 名前が実行時にも残ると思う。 リフレクションやシリアライザーには
Item1、Item2が見えます(あるいは何も見えません)。 - コピーを変更する。 タプルは構造体です:
var t2 = t1; t2.Item1 = 5;ではt1は変わりません。 - 要素の多いタプル。 3つか4つを超えるなら、名前付きの型のほうが読みやすくなります。
- 分解の順序を取り違える。 分解は位置によります。
(Name, Age)のタプルでvar (age, name) = person;と書くと名前がageに入り、varが型を推論するので、何も警告されません。
よくある質問
C#のメソッドから複数の値を返すには?
タプルを返します。戻り値の型を (int Min, int Max) と宣言し、return (lowest, highest); と書きます。呼び出し側は result.Min と result.Max を読むか、var (min, max) = MinMax(data); で分解します。代替手段としては、TryParse パターンに合う out 引数や、結果自体に意味がある場合の小さなクラスやレコードがあります。
C#の名前付きタプルとは何ですか?
要素が Item1、Item2 ではなく名前を持つタプルです。(string Name, int Age) person = ("Ana", 31); とすると person.Name と書けます。名前はコンパイル時にしか存在せず、内部では ValueTuple<string, int> のままで、Item1 も引き続き使えます。
C#のTupleとValueTupleの違いは何ですか?
System.Tuple(.NET 4から)はクラスで、ヒープに確保され、不変で、要素は Item1、Item2 だけです。System.ValueTuple(C# 7)は変更可能なフィールドを持つ構造体で、要素の名前と (a, b) 構文に対応し、すべてのタプルリテラルが作るのはこちらです。新しいコードでは値タプルを使います。
C#のタプルの分解はどう動きますか?
var (name, age) = person; は2つの変数を宣言し、タプルの要素を順に代入します。既存の変数に (a, b) = (b, a) と代入することもでき、これで値が入れ替わります。破棄の _ で要素を飛ばせます:var (_, age) = person;。
C#でタプルを==で比較できますか?
C# 7.3以降、== と != はタプルを要素ごとに比較します:(1, "a") == (1, "a") は true です。名前は無視され、位置と値だけが重要です。C# 7.3より前は、値タプルが同じ方法で実装している Equals を使います。