目次

キーワード

概要

Ver. 7

名前のない複合型」で説明したように、 型には常によい名前が付くわけではなく、名無しにしておきたいことがあります。 そういう場合に使うもののうちの1つがC# 7で導入されたタプルです。

タプルの最大の用途は多値戻り値です。 関数の戻り値は引数と対になるものなので、タプルの書き心地は引数に近くなるように設計されています。

ポイント

  • (int x, int y)というような、引数みたいな書き方で「名前のない型」を作れます
  • この書き方をタプルと呼びます

タプル

C# 7で導入されたタプル(tuple)は、 (int x, int y)というような、引数みたいな書き方で「名前のない型」を作る機能です。

※ タプルの利用には、ValueTuple構造体という型が必要になります。 この型が標準ライブラリに取り込まれるのは .NET Framework 4.7、.NET Standard 1.7を予定しています。 それ以前のバージョンでタプルを使いたい場合には、以下のパッケージを参照する必要があります。

タプルという名前

最初に例を挙げた(int x, int y)という書き方は、2つのintの値xyを並べたものなわけですが、こういう「データを複数並べたもの」を意味する単語がタプルです。

英語では倍数を「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演算子 (C# 8.0 以降は使えるようになった)
  • 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)
    {
        // (C# 7.3 までは) is 演算子には使えない
        if(obj is (int x, int y))
        {
        }
    }
}

ただし、以下のように、配列やnull許容型を作る場合にはnewを使えます。

var a = new(int x, int y)[10]; // OK
var n = new(int x, int y)?();  // OK

new (int x, int y)という書き方は、将来的な言語拡張の予定と被る(被ってしまったら将来の拡張ができない)ため禁止しているようです。 is演算子は、C# 8.0で入った位置パターンとの競合を懸念して、C# 8.0までは認めていませんでした。

using System;

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

    static void M(object obj)
    {
        // C# 8.0 で入った構文
        // 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

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

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

拡張メソッドの解決

前節のような型違いのタプル間の変換は、拡張メソッドのオーバーロード解決の際にも働きます。

例えば以下のように、配列×2のタプルに対して、IEnumerable×2のタプルの拡張メソッドを呼べます。 (配列からIEnumerableへの変換は暗黙的に行えるので、このタプル間の変換も暗黙的に行えます。)

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        int[] a1 = new[] { 1, 2, 3 };
        string[] a2 = new[] { "a", "b", "c" };

        // 配列 ×2のタプルに対して、IEnumerable ×2のタプルの拡張メソッドを呼べる
        foreach (var (i, s) in (a1, a2).Zip())
        {
            Console.WriteLine($"{i}: {s}");
        }
    }
}

static class TupelExtensions
{
    // IEnumerable ×2 に対する拡張メソッド
    public static IEnumerable<(T1 x1, T2 x2)> Zip<T1, T2>(this (IEnumerable<T1> items1, IEnumerable<T2> items2) t)
        => t.items1.Zip(t.items2, (x1, x2) => (x1, x2));
}

タプルの入れ子

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

// タプルの入れ子
(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つは、引数でのオーバーロードが名前を見ていない(引数の型だけがシグネチャに含まれる)のだから、引数に倣って設計されているタプルでも、メンバー名は区別しないのが自然という理由です。

タプル要素名の推論

Ver. 7.1

C# 7.1から、タプル構築時に渡した変数からタプルの要素名を推論できるようになりました。 例えば以下のように、(x, y) と書くだけで、1要素目にx、2要素目に y という名前が付きます。 (これまでだと、(x: x, y: y) と書く必要がありました。)

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

// C# 7.0。t の要素には名前が付かない
Console.WriteLine(t.Item1);
Console.WriteLine(t.Item2);

// C# 7.1。(x, y) で (x: x, y: y) 扱い
// t の要素に x, y という名前が付く
Console.WriteLine(t.x);
Console.WriteLine(t.y);

以下のように、部分的な適用もされます。

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

ただし、名前に被りがあるときには推論が働きません。

var x = 1;
var t = (x, x);
Console.WriteLine(t.Item1); // t.x とは書けない
Console.WriteLine(t.Item2); // こっちも t.x とは書けない

var u = (x: 0, x);
Console.WriteLine(u.x); // u.x というと Item1 の方
Console.WriteLine(u.Item2); // Item2 の方は x とは書けない

名前がないので当然ですが、リテラルからは要素名の推論はできません

var t = (1, 2);
Console.WriteLine(t.Item1); // さすがに t.1 とかは書けない

また、メソッド名からは推論されません。 一方で、プロパティ名からは推論されます。 プロパティやフィールドの場合、インスタンス メンバーへのアクセスでも推論されます (t.xとかなら、タプル要素名はxになります。t?.xでも可)。

int F() => 1;
var s = "abc";

var t = (F(), s?.Length);
Console.WriteLine(t.Item1); // メソッド名からは推論されない(t.F はダメ)
Console.WriteLine(t.Length); // プロパティ名からは推論される( . でも ?. でも OK)

==、!= での比較

Ver. 7.3

C# 7.3で、タプル同士を ==!= 演算子で比較できるようになりました。

これは、後述するValueTupleの演算子が呼ばれるわけではなく、 コンパイラーによる特別な処理が入ります。

タプルに対する==比較は、以下のように、メンバーごとの==&&で繋いだものに展開されます。

void M((int a, (int x, int y) b) t)
{
    // このタプル == 比較は、
    Console.WriteLine(t == (1, (2, 3)));
    // こんな感じで、メンバーごとの == を && で繋いだものに展開される。
    Console.WriteLine(t.a == 1 && t.b.x == 2 && t.b.y == 3);
}

同様に、!=は以下のように、メンバーごとの!=||で繋いだものになります。

void N((int a, (int x, int y) b) t)
{
    // 同じく != 比較は、
    Console.WriteLine(t != (1, (2, 3)));
    // こんな感じで、メンバーごとの != を || で繋いだものに展開される。
    Console.WriteLine(t.a != 1 || t.b.x != 2 || t.b.y != 3);
}

ValueTuple==演算子やEqualsメソッドではなくこういうコンパイラーによる処理が入っているのは、 「タプル間の変換」で説明したような、メンバーごとの型変換を考慮してのことです。 例えば、以下のように、暗黙的型変換ができるもの同士の比較ができます。

(long a, (double x, decimal y) b) t = (1, (2, 3));

// byte → long
// float → double
// short → decimal
// という、暗黙的型変換ができるもの同士の比較
Console.WriteLine(t == ((byte)1, ((float)2, (short)3)));

ちなみに、ユーザー定義==!=演算子を持っている場合、そのユーザー定義のものが呼ばれます。 また、ユーザー定義であれば==bool以外の型を返すこともありますが、 その場合も、truefalse演算子があれば比較できます。

using System;

struct MyBool
{
    public bool Value;
    public MyBool(bool value) => Value = value;

    // 何が呼ばれてるかがわかるように WriteLine を挟む
    public static bool operator true(MyBool x) { Console.WriteLine("MyBool.true"); return x.Value; }
    public static bool operator false(MyBool x) { Console.WriteLine("MyBool.false"); return !x.Value; }
    public static implicit operator MyBool(bool b) => new MyBool(b);
}

struct MyInt
{
    public int Value;
    public MyInt(int value) => Value = value;
    public static MyBool operator ==(MyInt x, MyInt y) => x.Value == y.Value;
    public static MyBool operator !=(MyInt x, MyInt y) => x.Value != y.Value;
    public static implicit operator MyInt(int b) => new MyInt(b);
    public override bool Equals(object obj) => obj is MyInt x && Value == x.Value;
    public override int GetHashCode() => Value.GetHashCode();
}

public class Program
{
    static void Main()
    {
        (MyInt a, (MyInt x, MyInt y) b) t = (1, (2, 3));

        // MyInt の == に展開されるので、MyBool が得られる。
        // MyBool 同士の && で、MyBool の false 演算子が呼ばれる。
        // (この例の場合、"MyBool.false" が3回表示される。)
        // (false の方が呼ばれるのは C# の && の仕様。)
        Console.WriteLine(t == (1, (2, 3)));
    }
}

更新履歴

ブログ