タプル

配列は、同じデータ型の複数の値を一括して扱うことができます。
異なるデータ型を一括して扱う場合、クラスや構造体にする方法もありますが、これらを定義するほどでもないような場合にはタプルを使用することができます。

タプルはC#7.0、.NET Framework4.7から使用可能です。

タプルの定義と作成


//int型とdouble型を持つタプル
(int x, double y) tuple;
tuple.x = 1;
tuple.y = 2.0;

タプルは二つ以上のデータ型名をコンマで区切り、丸括弧で括ることで定義します。
記法は引数リストとほぼ同じです。

タプルはユーザー定義型の一種で、新しいデータ型を定義します。
データ型なので、変数の宣言等で「int」などのデータ型を記述する場所に記述できます。
xyなどはタプルが持つ要素名で、これを通してタプル変数から要素アクセスできます。
要素は読み書きどちらも可能です。

タプルは要素数が二つ以上必要で、要素が一つのタプルや要素のないタプルを作ることはできません。

タプルは内部的にValueTupleという構造体を使用しています。
ValueTuple構造体を直接呼び出してインスタンスを作る場合は要素がひとつ、またはゼロのタプルを作ることができます。

タプルリテラル

タプル変数には、そのタプル型と同じデータ型の値を丸括弧で括ったもので初期化、および代入をすることができます。
これをタプルリテラルといいます。


//タプルリテラルで初期化
(int x, double y) tuple = (1, 2.0);

int a = 3;
double b = 4.0;
tuple = (a, b); //変数も使用可能

タプルリテラルでは要素名を指定することができます。
その場合は受け取り側(左辺)は型推論を使用します。
型推論を使用しない場合はタプルリテラル側の要素名は無視されます。


//(int x, double y)に型推論される
var tuple = (x: 1, y: 2.0);

//タプルリテラル側の名前は無視される
(int i, double d) t2 = (x: 1, y: 2.0); //i, d
(int, double) t3 = (x: 1, y: 2.0); //item1, item2

タプルリテラルにnullが含まれる場合は型推論が働かないため、受け取り側でデータ型の明示が必要です。


(string, int) t1 = (null, 1);

//コンパイルエラー
//var t2 = (null, 1);

要素名の省略

タプルは要素名を省略して定義が可能です。


(int, double) tuple;

この場合、各要素には先頭から順にItem1Item2...という名前が割り当てられます。


(int, double) tuple = (1, 2.0);
Console.WriteLine(tuple.Item1);
Console.WriteLine(tuple.Item2);

データ型や要素名を省略した、最も簡単なタプルは以下のようになります。


//(int item1, doule item2)
var tuple = (1, 2.0);

タプルの配列

タプルは配列にして使用することもできます。


var arr1 = new(int x, double y)[3];

//宣言と同時に初期化
var arr2 = new (int x, double y)[]
{
    (1, 2.0),
    (3, 4.0)
};

メソッドの戻り値としての使用

タプルは匿名型に似ていて、データ型そのものの名前は持ちません。
名前が必要ないような、ごく局所的なデータに使用されます。
よく使用されるのはメソッドの処理結果として複数の値を返したい場合です。


static (int min, int max) MinMax(int[] array)
{
    if (array == null || array.Length == 0)
        return (int.MinValue, int.MaxValue);

    var min = int.MaxValue;
    var max = int.MinValue;
    foreach (var n in array)
    {
        min = Math.Min(n, min);
        max = Math.Max(n, max);
    }
    return (min, max);
}

static void Main(string[] args)
{
    var a = MinMax(new int[] { 1, 2, 3, 4, 5 });
    var b = MinMax(new int[] { -1, 234, 56});

    Console.WriteLine("min: {0}, max: {1}", a.min, a.max);
    Console.WriteLine("min: {0}, max: {1}", b.min, b.max);
}
min: 1, max: 5
min: -1, max: 234

メソッドから複数の結果を得るには引数の参照渡しでも可能ですが、処理の結果を戻り値で受け取るというのは自然ですし、値を受け取る変数をわざわざ宣言しなくて良いというメリットもあります。
タプルはvarによる型推論も可能なので受け取りも簡単です。

タプルの分解

タプルの各要素は、それぞれを変数に分解するための構文が用意されています。


var tuple = (1, 2.0, "abc");

//三つの変数に分解
(int n, double d, string s) = tuple;

上記の例では要素をそれぞれ変数ndsに分解します。
値は先頭から順にコピーが行われます。

分解には型推論を使用することもできます。


var tuple = (1, 2.0, "abc");

//すべて型推論
var (n1, d1, s1) = tuple;

//d2とs2は型推論
(int n2, var d2, var s2) = tuple;

最初にvarを使用すると、すべての変数に対して型推論が働きます。
個別に型推論をすることもできます。

タプルの分解はすでに宣言されている変数に代入することもできます。


int n = 0;
double d = 2.0;
string s; //初期化していなくても良い

var tuple = (1, 2.0, "abc");
(n, d, s) = tuple;

C#10.0以降は、既存変数への代入と、変数を宣言して代入とを混在することができます。


int n = 0;
double d = 2.0;

var tuple = (1, 2.0, "abc");
(n, d, string s) = tuple;

受け取る値の破棄

タプルの分解はタプルのすべての要素を変数に受けとる必要があります。
必要のない要素がある場合でも受け取り側で記述を省略することはできませんが、アンダーバー(_)を指定することで値を破棄することができます。


var tuple = (1, 2.0, 'a', "bcd");

//受け取り側の変数の数が足りないとエラー
//var (n, d) = tuple;

//1と'a'は受け取り、残りは破棄
var (n, _, c, _) = tuple;

アンダーバーは値を受け取らない指定なので、_という名前の変数は作成されません。

タプル以外の分解

タプル以外のデータ型は分解構文で分解することはできませんが、クラスや構造体にDeconstructというメソッドを定義すると分解できるようになります。


//分解したいクラス
public class Person
{
    public string firstName;
    public string lastName;
    public int age;

    //firstNameとlastNameを分解
    public void Deconstruct(out string firstName, out string lastName)
    {
        firstName = this.firstName;
        lastName = this.lastName;
    }
    //firstNameとlastNameとageを分解
    public void Deconstruct(out string firstName, out string lastName, out int age)
    {
        firstName = this.firstName;
        lastName = this.lastName;
        age = this.age;
    }
}

static void Main(string[] args)
{
    var person = new Person()
    {
        firstName = "John",
        lastName = "Doe",
        age = 20
    };

    //タプルと同様の構文で分解が可能
    var (first1, last1) = person;
    var (first2, last2, age2) = person;
}

Deconstructメソッドは分解したい要素をout参照で受け取ります。
戻り値はvoid型です。

Deconstructメソッドをオーバーロードすることは可能ですが、分解メソッドとして有効になるのは「引数の数の違いによるオーバーロード」です。
分解構文は対象の値のデータ型ではなく値の数(先頭からの順序)でオーバーロードを解決するので、引数の数が同じ(データ型が異なる)オーバーロードを定義した場合は分解できません。


//分解したいクラス
public class Person
{
    public string firstName;
    public string lastName;
    public int age;

    //firstNameとlastNameを分解
    public void Deconstruct(out string firstName, out string lastName)
    {
        firstName = this.firstName;
        lastName = this.lastName;
    }
    //firstNameとlastNameをfullnameに結合し、ageも分解
    public void Deconstruct(out string fullName, out int age)
    {
        fullName = $"{this.firstName} {this.lastName}";
        age = this.age;
    }
}

static void Main(string[] args)
{
    var person = new Person()
    {
        firstName = "John",
        lastName = "Doe",
        age = 20
    };

    //分解するデータの数が同じ場合は
    //データ型を明示しても分解できない
    //どちらもコンパイルエラー
    (string first, string last) = person;
    (string full, int age) = person;
}

なお、Deconstructメソッドは拡張メソッドの形であっても良いので、publicな値であれば自作クラス(構造体)以外の型にも分解構文を利用可能にできます。