目次

キーワード

概要

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")]); // (1, 2)

制限事項

ただ、いくつか、通常の型であれば書ける場所で、タプルのこの記法を使えないところがあります。 以下の3つです。

  • new演算子
  • is演算子
  • usingディレクティブ (これは将来的に認められる可能性あり)

例えば以下のコードはコンパイル エラーを起こします。

// using でエイリアスを付けることはできない
using T = (int x, int y);

class Program
{
    static void Main()
    {
        // var t = new T(1, 2); みたいなのと同じノリでは書けない
        var t1 = new(int x, int y)(1, 2);
        var t2 = new(int x, int y) { x = 1, y = 2 };
    }

    static void M(object obj)
    {
        // is 演算子には使えない
        if(obj is (int x, int y))
        {
        }
    }
}

これらは、将来的な言語拡張の予定と被る(被ってしまったら将来の拡張ができない)ため禁止しているようです。 new演算子は「newの型推論」、is演算子はパターン マッチングという機能が予定されています。

using System;

class Program
{
    static void Main()
    {
        var ticks = 100000;
        // C# 8?
        DateTime d = new(ticks); // 左辺から型推論して、new DateTime(ticks) が呼ばれる
    }

    static void M(object obj)
    {
        // C# 8?
        // is T 扱いじゃなくて、パターン マッチングで obj を x, y に分解
        if (obj is (int x, int y))
        {
            Console.WriteLine($"{x}, {y}");
        }
    }
}

また、タプルのメンバーは2つ以上な必要があります。()(int x)というようなタプルは現在の仕様では作れません。

() noneple;     // ダメ
(int x) oneple; // ダメ

// タプル構文で書けるのは2つ以上だけ
(int x, int y) twople; // OK

// タプル構文でなければ、0-tuple, 1-tuple も作れる
ValueTuple none;     // OK
ValueTuple<int> one; // OK

タプル リテラル

タプルは(1, 2)というような書き方でリテラルを書くことができます。 タプル リテラルは実引数リスト(引数を渡す側の書き方)に似ています。

// メソッド呼び出し時の F(1, 2); みたいなノリ
(int x, int y) t1 = (1, 2);

// メソッド呼び出し時の F(x: 1, y: 2); みたいなノリ
var t2 = (x: 1, y: 2);

nullのように単体では型が決まらないものも、左辺に型があれば推論が効きます。 一方で、左辺もvar等になっていて型が決まらない場合、コンパイル エラーになります。

// これは左辺から型推論が聞くので、null も書ける
(string s, int i) t1 = (null, 1);

// これはダメ。null の型が決まらない。
var t2 = (null, 1); // コンパイル エラー

メンバー参照

メンバーの参照の仕方は普通の型と変わりません。(int x, int y)であれば、xyという名前でアクセスできます。 ちなみに、タプルのメンバーは書き換え可能です。

var t = (x: 1, y: 2);
Console.WriteLine(t.x); // 1
Console.WriteLine(t.y); // 2

// メンバーごとに書き換え可能
t.x = 10;
t.y = 20;
Console.WriteLine(t.x); // 10
Console.WriteLine(t.y); // 20

// タプル自身も書き換え可能
t = (100, 200);
Console.WriteLine(t.x); // 100
Console.WriteLine(t.y); // 200

ちなみに、タプルのメンバーはフィールドになっています (プロパティではない)。 フィールドになっているということは、例えば、参照引数(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); // 2
Console.WriteLine(t.y); // 1

タプルの分解

タプルは、各メンバーを分解して、それぞれ別の変数に受けて使うことができます。

var t = (x: 1, y: 2);

// 分解宣言1
(int x1, int y1) = t; // x1, y1 を宣言しつつ、t を分解
// 分解宣言2
var (x2, y2) = t; // 分解宣言の簡易記法

// 分解代入
int x, y;
(x, y) = t; // 分解結果を既存の変数に代入

この分解は、タプル以外の型に対しても使えるものです。 詳しくは「複合型の分解」で説明します。

タプル間の変換

タプル間の代入は、一定の条件下では暗黙的変換が掛かります。

名前違いのタプル

タプル間の代入は、メンバーの宣言位置に基づいて行われます。 逆に言うと、名前は無関係で、メンバーの型の並びだけ一致していれば代入できます。

例えば以下のように書くと、1番目同士(xs)、2番目同士(yt)で値が代入されます。

(int s, int t) t1 = (x: 1, y: 2);
Console.WriteLine(t1.s); // 1
Console.WriteLine(t1.t); // 2

同名であっても、位置が優先です。以下のような書き方をすると、xyが入れ替わります。

(int y, int x) t2 = (x: 1, y: 2);
Console.WriteLine(t2.x); // 2
Console.WriteLine(t2.y); // 1

型違いのタプル

タプルのメンバーの型が違う場合、メンバーごとに暗黙的な変換がかかる場合に限り、 タプル間の暗黙的変換ができます。

例えば以下の場合、xyzも、それぞれが型変換できるので、タプルの暗黙的型変換が掛かります。

object x = "abc"; // string → object は OK
long y = 1; // int → long は OK
int? z = 2; // int → int? は OK
// ↓
(object x, long y, int? z) t = ("abc", 1, 2); // OK

逆に、以下の場合はコンパイル エラーになります。この例では全部のメンバーが変換不能ですが、全部でなくても、どれか一つでも変換できないと、タプル自体の変換もエラーになります。

string x = 1; // int → string は NG
int y = 1L; // long → int は NG
int z = default(int?); // int? → int は NG
// ↓
(string x, int y, int z) t = (1, 1L, default(int?)); // NG

タプルの入れ子

タプルは入れ子にできます。

// タプルの入れ子
(string a, (int x, int y) b) t1 = ("abc", (1, 2));
Console.WriteLine(t1.a);   // abc
Console.WriteLine(t1.b.x); // 1
Console.WriteLine(t1.b.y); // 2

// 型推論も可能
var t2 = (a: "abc", b: (x: 1, y: 2));

メンバー名も匿名

タプルは、メンバー名もなくして、完全に匿名(名無し)にすることもできます。 この場合、メンバーを使う際にはItem1Item2、…というような名前で参照します。

var t1 = (1, 2);
Console.WriteLine(t1.Item1); // 1
Console.WriteLine(t1.Item2); // 2

Item1Item2、… という名前は、後述するValueTuple構造体のメンバー名です。

冒頭や「名前のない複合型」で説明したように、 「メンバー名だけ見れば十分」だから型名を省略するのであって、 メンバー名まで省略するのとさすがにプログラムが読みづらくなります。 メンバー名も持っていない完全な匿名タプルは、おそらくかなり短い寿命でしか使わないでしょう。 例えば、すぐに別の(メンバー名のある)タプル型に代入したり、分解して変数に受けて使うことになります。

オーバーロード

型違いのタプルを使うのであれば、オーバーロードに使えます。 例えば、以下のメソッドFは、yの型が違うのでオーバーロード可能です。

// 型違いのタプルでのオーバーロードは可能
void F((int x, int y) t) { }
void F((int x, string y) t) { }

一方、型が一緒で名前だけが違うタプルではオーバーロードできません。 以下のメソッドGは、同じものが2つあるのでコンパイル エラーを起こします。

// 型が一緒で名前だけ違うタプルでのオーバーロードはダメ。コンパイル エラー
void G((int x, int y) t) { }
void G((int a, int b) t) { }

こういう仕様になっている理由は2つあります。 1つは、次節で説明するように、内部実装的に名前だけ違うタプルを区別できないという、技術的な理由。 もう1つは、引数でのオーバーロードが名前を見ていない(引数の型だけがシグネチャに含まれる)のだから、引数に倣って設計されているタプルでも、メンバー名は区別しないのが自然という理由です。

更新履歴

ブログ