Ver. 7
「名前のない複合型」で説明したように、
型には常によい名前が付くわけではなく、名無しにしておきたいことがあります。
そういう場合に使うもののうちの1つがC# 7で導入されたタプルです。
タプルの最大の用途は多値戻り値です。
関数の戻り値は引数と対になるものなので、タプルの書き心地は引数に近くなるように設計されています。
ポイント
(int x, int y)
というような、引数みたいな書き方で「名前のない型」を作れます
- この書き方をタプルと呼びます
C# 7で導入されたタプル(tuple)は、
(int x, int y)
というような、引数みたいな書き方で「名前のない型」を作る機能です。
前節の戻り値は「最小値と最大値と平均値を並べたもの」なわけですが、こういう「データを複数並べたもの」を意味する単語がタプルです。
英語では倍数を「double, triple, quadruple, ...」などという単語で表しますが、これを一般化して n-tuple (nは0以上の任意の整数)と書くことがあり、これがタプルの語源です。
n倍、n重、n連結というような意味しかなく、まさに「名前のない複合型」にピッタリの単語です。
(int x, int y)
みたいな書き方で、1つの型を表します。
タプルの型の書き方はメソッドの仮引数リスト(引数を受け取る側の書き方)に似ていて、()
の中に「型名 メンバー名」を ,
区切りで並べます。
これは、型を書ける場所であれば大体どこにでもこの「型」を書けます。
まず、以下のように、フィールドや戻り値などの型にできます。
class Sample
{
private (int x, int y) value;
public (int x, int y) GetValue() => value;
}
以下のように、ローカル変数の型としても明示できます。
var s = new Sample();
(int x, int y) t = s.GetValue();
もちろん、var
を使った型推論も効きます。
![varで型推論]()
また、ジェネリックな型の型引数にも使えます。
var dic = new Dictionary<(string s, string t), (int x, int y)>
{
{ ("a", "b"), (1, 2) },
{ ("x", "y"), (4, 8) },
};
Console.WriteLine(dic[("a", "b")]);
次節の「タプル リテラル」という書き方もあるのであまり使わないとは思いますが、
一応、new
演算子を使った初期化もできます。
var t1 = new (int x, int y) (1, 2);
var t2 = new (int x, int y) { x = 1, y = 2 };
ちなみに、タプルのメンバーは2つ以上な必要があります。()
や(int x)
というようなタプルは現在の仕様では作れません。
タプルは(1, 2)
というような書き方でリテラルを書くことができます。
タプル リテラルは実引数リスト(引数を渡す側の書き方)に似ています。
(int x, int y) t1 = (1, 2);
var t2 = (x: 1, y: 2);
null
のように単体では型が決まらないものも、左辺に型があれば推論が効きます。
一方で、左辺もvar
等になっていて型が決まらない場合、コンパイル エラーになります。
(string s, int i) t1 = (null, 1);
var t2 = (null, 1);
メンバーの参照の仕方は普通の型と変わりません。(int x, int y)
であれば、x
、y
という名前でアクセスできます。
ちなみに、タプルのメンバーは書き換え可能です。
var t = (x: 1, y: 2);
Console.WriteLine(t.x);
Console.WriteLine(t.y);
t.x = 10;
t.y = 20;
Console.WriteLine(t.x);
Console.WriteLine(t.y);
t = (100, 200);
Console.WriteLine(t.x);
Console.WriteLine(t.y);
ちなみに、タプルのメンバーはフィールドになっています
(プロパティではない)。
フィールドになっているということは、例えば、参照引数(ref
)に直接渡せます
(これが、プロパティだと無理)。
例えば以下のようなメソッドがあったとします。
static void Swap<T>(ref T x, ref T y)
{
var t = x;
x = y;
y = t;
}
このとき、以下のようにタプルのメンバーを渡せます。
var t = (x: 1, y: 2);
Swap(ref t.x, ref t.y);
Console.WriteLine(t.x);
Console.WriteLine(t.y);
タプルは、各メンバーを分解して、それぞれ別の変数に受けて使うことができます。
var t = (x: 1, y: 2);
(int x1, int y1) = t;
var (x2, y2) = t;
int x, y;
(x, y) = t;
この分解は、タプル以外の型に対しても使えるものです。
詳しくは「複合型の分解」で説明します。
タプル間の代入は、一定の条件下では暗黙的変換が掛かります。
タプル間の代入は、メンバーの宣言位置に基づいて行われます。
逆に言うと、名前は無関係で、メンバーの型の並びだけ一致していれば代入できます。
例えば以下のように書くと、1番目同士(x
→ s
)、2番目同士(y
→ t
)で値が代入されます。
(int s, int t) t1 = (x: 1, y: 2);
Console.WriteLine(t1.s);
Console.WriteLine(t1.t);
同名であっても、位置が優先です。以下のような書き方をすると、x
、y
が入れ替わります。
(int y, int x) t2 = (x: 1, y: 2);
Console.WriteLine(t2.x);
Console.WriteLine(t2.y);
タプルのメンバーの型が違う場合、メンバーごとに暗黙的な変換がかかる場合に限り、
タプル間の暗黙的変換ができます。
例えば以下の場合、x
もy
もz
も、それぞれが型変換できるので、タプルの暗黙的型変換が掛かります。
object x = "abc";
long y = 1;
int? z = 2;
(object x, long y, int? z) t = ("abc", 1, 2);
逆に、以下の場合はコンパイル エラーになります。この例では全部のメンバーが変換不能ですが、全部でなくても、どれか一つでも変換できないと、タプル自体の変換もエラーになります。
string x = 1;
int y = 1L;
int z = default(int?);
(string x, int y, int z) t = (1, 1L, default(int?));
タプルは入れ子にできます。
(string a, (int x, int y) b) t1 = ("abc", (1, 2));
Console.WriteLine(t1.a);
Console.WriteLine(t1.b.x);
Console.WriteLine(t1.b.y);
var t2 = (a: "abc", b: (x: 1, y: 2));
タプルは、メンバー名もなくして、完全に匿名(名無し)にすることもできます。
この場合、メンバーを使う際にはItem1
、Item2
、…というような名前で参照します。
var t1 = (1, 2);
Console.WriteLine(t1.Item1);
Console.WriteLine(t1.Item2);
Item1
、Item2
、… という名前は、後述するValueTuple
構造体のメンバー名です。
冒頭や「名前のない複合型」で説明したように、
「メンバー名だけ見れば十分」だから型名を省略するのであって、
メンバー名まで省略するのとさすがにプログラムが読みづらくなります。
メンバー名も持っていない完全な匿名タプルは、おそらくかなり短い寿命でしか使わないでしょう。
例えば、すぐに別の(メンバー名のある)タプル型に代入したり、分解して変数に受けて使うことになります。
タプルがどういうコードに展開されるかについても話しておきましょう。
タプルを使ったコードを古いバージョンの.NET上で動かしたり、
タプルを使ったライブラリを古いバージョンのC#から参照したり、
別のプログラミング言語から参照したい場合もあります。
そのために、タプルは、ValueTuple
という構造体に展開されます。
タプルは、コンパイルの結果としてはValueTuple
構造体(System
名前空間)に展開されます。
例えば、以下のようなコードを考えます。
var t = (x: 3, y: 5);
var p = t.x * t.y;
var (x, y) = t;
Console.WriteLine($"{x} × {y} = {p}");
以下のようなコードに展開されます。
var t = new ValueTuple<int, int>(3, 5);
var p = t.Item1 * t.Item2;
var x = t.Item1;
var y = t.Item2;
Console.WriteLine($"{x} × {y} = {p}");
元々のx
やy
という名前は、内部的には残っていません。ValueTuple
構造体のメンバーであるItem1
やItem2
に展開されます。
特に、一度object
やdynamic
を経由すると、名前を完全に紛失します。
以下のコードでは、x
やy
が見つからず、実行時エラーを起こします。
private static void Dynamic()
{
var a = new { x = 3, y = 5 };
var s1 = Sum(a);
Console.WriteLine(s1);
var t = (x: 3, y: 5);
var s2 = Sum(t);
Console.WriteLine(s2);
}
private static dynamic Sum(dynamic d) => d.x + d.y;
とはいえ、名前をどこにも残さないと、ライブラリをまたいだ時にx
、y
などの名前が使えなくて困ります。
そこで、クラスのメンバーにタプルを使う場合には、TupleElementNames
属性(System.Runtime.CompilerServices
名前空間)を付けて、
C#コンパイラーには名前がわかるようにしています。
例えば、以下のような引数も戻り値もタプルなメソッドを書いたとします。
public (int x, int y) F((int a, int b) t) => (t.a + t.b, t.a - t.b);
このメソッドは、以下のように展開されます。タプルがValueTuple
構造体に化けますが、TupleElementNames
属性を付けて名前を残します。
[return: TupleElementNames(new[] { "x", "y" })]
public ValueTuple<int, int> F([TupleElementNames(new[] { "a", "b" })] ValueTuple<int, int> t)
=> new ValueTuple<int, int>(t.Item1 + t.Item2, t.Item1 - t.Item2);
C#コンパイラーは、この情報を元に、タプルの名前を復元します。
タプルの展開結果にあたるValueTuple
は、型引数が0~8個の合計9個の構造体があります。
例えば、型引数2個のものは以下のような定義になっています。
[StructLayout(LayoutKind.Auto)]
public struct ValueTuple<T1, T2>
: IEquatable<ValueTuple<T1, T2>>, IStructuralEquatable, IStructuralComparable, IComparable, IComparable<ValueTuple<T1, T2>>
{
public T1 Item1;
public T2 Item2;
public ValueTuple(T1 item1, T2 item2)
{
Item1 = item1;
Item2 = item2;
}
}
基本的には、publicなフィールドだけを持つ構造体です。
それに、値の比較用の各種インターフェイスが実装されています。
最初に言った通り、ValueTuple
構造体の型引数は、最大のものでも8個です。
では、メンバーが9個以上のタプルを作るとどうなるかというと、入れ子のValueTuple
構造体が作られます。
例えば、以下のようなコードを書いたとします。
メンバー名も匿名で作ったので、9番目のメンバーの参照にItem9
という名前が使われています。
var t = (1, 2, 3, 4, 5, 6, 7, 8, 9);
Console.WriteLine(t.Item9);
このコードは、以下のように展開されます。
var t = new ValueTuple<int, int, int, int, int, int, int, ValueTuple<int, int>>(
1, 2, 3, 4, 5, 6, 7, new ValueTuple<int, int>(8, 9));
Console.WriteLine(t.Rest.Item2);
C# 7のリリースに合わせて、ValueTuple
構造体は標準ライブラリに取り込まれる予定です。
一方で、古い.NET (.NET Framework 4.6.2以前、.NET Standard 1.6以前)上でタプルを使いたい場合は、
以下のライブラリを参照します。この中にValueTuple
構造体や、TupleElementNames
属性が定義されています。
前述の通り、タプルのメンバーは2つ以上な必要で、()
や(int x)
というようなタプルは作れません。
一方で、ValueTuple
構造体には、型引数0個と1個のものが存在します。
var noneple = new ValueTuple();
var oneple = new ValueTuple<int>(1);
var twople = (1, 2);
var threeple = (1, 2, 3);
型引数0個のValueTuple
(0-tuple)は、いわゆるUnit型です。
void
の代わりにこの型を使うことで、戻り値がある場合とない場合のコードを統一的に書けてうれしい場合があります。
一方、型引数1個のもの(1-tuple)も、用途としては0-tupleと同じです。
型引数2個以上のものと並べて、戻り値や引数の個数違いを統一的に書けます。
例えば、以下の2つのコードはどちらの方が統一性があっていいかという話になります。
async Task F0() { }
async Task<int> F1() => 1;
async Task<(int x1, int x2)> F2() => (1, 2);
async Task<(int x1, int x2, int x3)> F3() => (1, 2, 3);
async Task<()> F0() { }
async Task<(int x1)> F1() => (1);
async Task<(int x1, int x2)> F2() => (1, 2);
async Task<(int x1, int x2, int x3)> F3() => (1, 2, 3);
特に、ソースコード生成などでまとめて、個数違いのメソッドを生成したい場合などには、0-tupleや1-tupleがほしくなります。
0個と1個の時だけ特別扱いが必要になるかどうかという問題です。
0-tupleと1-tupleがあれば、特別扱いなしでソースコード生成ができて楽です。
ということで、0-tuple、1-tupleの需要はあるんですが、問題があって構文を提供できていません。
1-tupleになるであろう構文は(1)
というような形になるはずですが、
これが、C#の既存の構文ですでに、単に1
と同じ意味で解釈されるため、1-tupleを作れません。
0-tupleの方の()
は、これまでは書けなかった書き方なので別にC# 7で追加できますが、
1-tupleだけ飛ばして「0か2以上のみ」とするのも変な話です。
タプルには、毛色の似た機能が2つあります。
- 匿名型 … タプルと同様に、名前がない型
- 出力引数 … 複数の戻り値を返すのに使える
タプルは、名前がない型という観点で言うと、匿名型と似ています。
しかし、「名前のない複合型」で説明したように、
出自・用途の違いから、内部実装は結構異なります。
以下の表のようになります。
|
タプル |
匿名型 |
主な用途 |
多値戻り値 |
部分的なメンバー抜き出し |
展開結果 |
ValueTuple 構造体+属性 |
クラスの生成 |
型の種類 |
値型 |
参照型 |
見た目 |
引数の書き方に似ている |
オブジェクト初期化子の書き方に似ている |
展開結果の差は用途の差から来ています。
タプルは戻り値として使います。publicなメンバーの型にも使うことになるので、ライブラリ間をまたげる必要があります。
ValueTuple
構造体に展開することで、ライブラリをまたいでも同じ構造体を参照する状態になります。
一方、匿名型は、ライブラリごとにそれぞれクラスを生成します(「匿名型」参照)。
同じ型に見えて、ライブラリをまたぐと別クラスになってしまいます。
このことから、匿名型は、メソッドの戻り値など、publicになりうる場所には書けません。
メソッド内のローカルな部分で完結して使う必要があります。
とはいえ、ValueTuple
構造体に展開では、前節での説明の通り、実行時に名前を紛失します。
dynamic
や、式木での利用にはタプルは向きません。匿名型を使います。
値型か参照型かも実装が異なりますが、これも、戻り値として使う、その後すぐに分解して使うという想定だと、値型の方が実行性能的に有利だからです。
用途が変われば最適な実装は変わります。
多値戻り値という用途だと、出力引数という手段もあります。
一般的に言うと、多値戻り値には今後タプルを使うのがおすすめです。
出力引数の方が煩雑な書き方になりがちだからです。
比較のために簡単な例を挙げてみましょう。まず、C# 6以前の出力引数を使ったものです。
static void F(Point p)
{
int x, y;
Deconstruct(p, out x, out y);
Console.WriteLine($"{x}, {y}");
}
static void Deconstruct(Point p, out int x, out int y)
{
x = p.X;
y = p.Y;
}
1個1個out
修飾子を付けて回るのは結構な煩雑さです。
呼び出す前に別途変数宣言が必要なのも面倒です。
これらは単に煩雑なだけなので我慢すれば何とかなりますが、
致命的なのは非同期メソッドで使えないことです。
ちなみに、煩雑さはC# 7で多少マシになりました。出力変数という構文(説明ページ準備中。でき次第リンク)が追加されて、以下のように書けます。
static void F(Point p)
{
Deconstruct(p, out var x, out var y);
Console.WriteLine($"{x}, {y}");
}
static void Deconstruct(Point p, out int x, out int y) => (x, y) = (p.X, p.Y);
でも、相変わらず長くなりがちです。
また、非同期メソッドで使えない点は変わりません。
タプルを使えばこの問題は解決です。
static async Task F(Point p)
{
var t1 = Deconstruct(p);
Console.WriteLine($"{t1.x}, {t1.y}");
var (x, y) = Deconstruct(p);
Console.WriteLine($"{x}, {y}");
var t2 = await DeconstructAsync(p);
Console.WriteLine($"{t2.x}, {t2.y}");
}
static (int x, int y) Deconstruct(Point p) => (p.X, p.Y);
static async Task<(int x, int y)> DeconstructAsync(Point p) => (p.X, p.Y);
一方で、出力引数を使いたくなる場面も残っています。
TryParse
のように、bool
値を返してif
ステートメントなどの条件式内で使いたい場合
- オーバーロードを呼び分けたい場合
if
内で使いたい場合は、例えば以下のようなコードになります。
static void TryPattern()
{
var s = Console.ReadLine();
if (int.TryParse(s, out var x)) Console.WriteLine(x);
}
これはさすがにタプルを使う方が煩雑です。
static void TuplePattern()
{
var s = Console.ReadLine();
var (success, x) = Parse(s);
if (success) Console.WriteLine(x);
}
static (bool success, int value) Parse(string s) => int.TryParse(s, out var x) ? (true, x) : (false, 0);
もっとも、C# 7では、以下のような is
演算子を使ったnull
チェックで同様のことをすると言う手もあります。
この書き方を型スイッチと呼びます(説明ページ準備中。でき次第リンク)。
static void NullCheckPattern()
{
var s = Console.ReadLine();
if (ParseOrDefault(s) is int x) Console.WriteLine(x);
}
static int? ParseOrDefault(string s) => int.TryParse(s, out var x) ? x : default(int?);
もう1つ、オーバーロードですが、C#では(というか.NETでは)、引数でのオーバーロードはできますが、戻り値でのオーバーロードはできません。
そこで、以下のように、オーバーロードに関しては出力引数の方が有利になります。
static void F(out int x, out int y) => (x, y) = (1, 2);
static void F(out int id, out string name) => (id, name) = (1, "abc");
static (int x, int y) F() => (1, 2);
static (int id, string name) F() => (1, "abc");