タプルの内部実装

タプルがどういうコードに展開されるかについても話しておきましょう。

タプルを使ったコードを古いバージョンの.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}");

元々のxyという名前は、内部的には残っていません。ValueTuple構造体のメンバーであるItem1Item2に展開されます。

特に、一度objectdynamicを経由すると、名前を完全に紛失します。 以下のコードでは、xyが見つからず、実行時エラーを起こします。

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属性

とはいえ、名前をどこにも残さないと、ライブラリをまたいだ時にxyなどの名前が使えなくて困ります。 そこで、クラスのメンバーにタプルを使う場合には、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番目以降のメンバーに対しても、Item8Item9というような名前で参照できます。

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構造体にはItem8Item9という名前のメンバーはありません。 型引数の数が最大のもので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 oneple = 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以上のみ」とするのも変な話です。

更新履歴

ブログ