タプルの内部実装
タプルがどういうコードに展開されるかについても話しておきましょう。
タプルを使ったコードを古いバージョンの.NET上で動かしたり、
タプルを使ったライブラリを古いバージョンのC#から参照したり、
別のプログラミング言語から参照したい場合もあります。
そのために、タプルは、ValueTuple
という構造体に展開されます。
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); // (x: 3, y: 5)
var p = t.Item1 * t.Item2; // t.x * t.y
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); // x, yという名前が実行時になくてエラーに
Console.WriteLine(s2);
}
private static dynamic Sum(dynamic d) => d.x + d.y;
TupleElementNames属性
とはいえ、名前をどこにも残さないと、ライブラリをまたいだ時に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構造体の中身
タプルの展開結果にあたる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なフィールドだけを持つ構造体です。 それに、値の比較用の各種インターフェイスが実装されています。
メンバーが9個以上のタプル
最初に言った通り、ValueTuple
構造体の型引数は、最大のものでも8個です。
では、メンバーが9個以上のタプルを作るとどうなるかというと、入れ子のValueTuple
構造体が作られます。
例えば、以下のようなコードを書いたとします。
メンバー名も匿名で作ったので ItemN
(N
は正の整数)といったような名前でメンバーを読み書きすることになります。
C#上は、8番目以降のメンバーに対しても、Item8
、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);
ValueTuple
構造体にはItem8
、Item9
という名前のメンバーはありません。
型引数の数が最大のもので8メンバーで、その8つ目のメンバーの名前はRest
(残り)です。
そして、以下のように、C#上Item9
であれば展開結果的にはRest
のさらにItem2
というように、入れ子のメンバー参照に展開されます。
C# 上 | コンパイル結果 |
---|---|
Item8 |
Rest.Item1 |
Item9 |
Rest.Item2 |
… | … |
Item15 |
Rest.Rest.Item1 |
Item16 |
Rest.Rest.Item2 |
… | … |
ValueTuple構造体の定義場所
C# 7のリリースに合わせて、ValueTuple
構造体は標準ライブラリに取り込まれる予定です。
一方で、古い.NET (.NET Framework 4.6.2以前、.NET Standard 1.6以前)上でタプルを使いたい場合、
以下のライブラリを参照します。この中にValueTuple
構造体や、TupleElementNames
属性が定義されています。
型引数0、1のValueTuple
前述の通り、タプルのメンバーは2つ以上な必要があって、()
や(int x)
というようなタプルは作れません。
一方で、ValueTuple
構造体には、型引数0個と1個のものが存在します。
// メンバー0個、1個のものは、構造体はあるけど、タプル構文は使えない
var noneple = new ValueTuple();
var class="reserved">new ValueTuple<int>(1);
// メンバー2個以上はタプル構文を使える
var twople = (1, 2); // new ValueTuple<int, int>(1, 2);
var threeple = (1, 2, 3); // new ValueTuple<int, int, int>(1, 2, 3);
型引数0個のValueTuple
(0-tuple)は、いわゆるUnit型です。
void
の代わりにこの型を使うことで、戻り値がある場合とない場合のコードを統一的に書けてうれしい場合があります。
一方、型引数1個のもの(1-tuple)も、用途としては0-tupleと同じです。
型引数2個以上のものと並べて、戻り値や引数の個数違いを統一的に書けます。
例えば、以下の2つのコードはどちらの方が統一性があっていいかという話になります。
// タプルでは0、1は書けない
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);
// こう書けると統一性があってきれい(C# 7では書けない)
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以上のみ」とするのも変な話です。