【C#】タプルの色々な宣言方法と受け取り方

C#でタプルの宣言方法と受け取る方法ですがバリエーションがいくつかあって知らないと冗長な書き方になっているときがあるので、宣言のしかたと受け取り方を紹介したいと思います。

確認環境

  • .NET 5, C#9.0
  • VisualSturio 2019
  • Windows 10

宣言方法と受け取り方

メソッドの戻り値として使用するタプルの宣言方法は、(1) 各要素に名前を指定しないで方法と、(2) 各要素に名前を付けて返す2種類があります。宣言方法によって受け取り方も若干異なります。

// 要素に名前を付けないで返す
public static (int, int, int, int) GetValues1()
{
    return (0, 1, 2, 3);
}

// 受け取るときには Item1, Item2 のような名前が自動的に付与される
var v1 = GetValues1();
int v11 = v1.Item1;
int v12 = v1.Item1;

// 要素に名前を付けて返す
public static (int a, int b, int c, int d) GetValues2()
{
    return (0, 1, 2, 3);
}

// 自分でつけた変数名で受け取れる
var v2 = GetValues2();
int v21 = v2.a;
int v22 = v2.b;

基本的に後者の名前を付ける方が推奨です。

次に受け取り方です。

// (1) 完全な宣言で受け取る
(int, int, int, int) ret1 = GetValues1();
int a1 = ret1.Item1;
int b1 = ret1.Item2;
int c1 = ret1.Item3;
int d1 = ret1.Item4;

// (2) varで受け取る
var ret2 = GetValues1();
int a2 = ret2.Item1;
int b2 = ret2.Item2;
int c2 = ret2.Item3;
int d2 = ret2.Item4;

// (3) 各要素に名前を付けて受け取る
(int myA, int myB, int myC, int myD) ret3 = GetValues1();
int a3 = ret3.myA;
int b3 = ret3.myB;
int c3 = ret3.myC;
int d3 = ret3.myD;

// (4) 各要素の変数名を指定して受け取る
var (a, b, c, d) = GetValues1();

// (5) 匿名で受け取る
(int myA2, int myB2, int myC2, int myD2) = GetValues1();
int a4 = myA2;
int b4 = myB2;
int c4 = myC2;
int d4 = myD2;

また必要のない変数は _ (アンダーバー)を指定すると破棄できます。

// 2~4番目の値は破棄する
(int myA2, int _, int _, int _) = GetValues1();
int a = myA2;

書き方が色々あるので混乱してしまいそうですが使いやすいのは、(2) の名前を付けて var で受け取る + 宣言元のメソッドで名前を指定する、か、 (4) の変数名を指定して受け取るのがコードの記述が簡単です。

ただし、このタプルをさらに別のオブジェクトに渡すよう状態態になったときはクラスや構造体の利用を検討しましょう。何度もタプルで返すと追いずらいですし、宣言が繰り返し出現して冗長で実装効率が長期的に見ると低下します。短い区間で短期的な値の組み合わせの利用がタプルの使いどころです。

クラスをタプルに分解する

自作のクラスでは「分解」という言語機能を使用するとオブジェクトの中身をタプルとして取り出すことができます。例えば以下のようなクラスがあったときに「void Deconstruct(...」という特殊なメソッド宣言によって受け取り側は値をタプルで受け取ることができます。

public class Sample
{
    public string Name { get; }
    public int ID { get; }

    public Sample(string name, int id) => (Name, ID) = (name, id);

    // タプルとして返す分解宣言
    public void Deconstruct(out string name, out int id)
    {
        name = Name;
        id = ID;
    }
}

// オブジェクトを作成して
Sample sample = new("Takap", 123);

// (1) Tupleに代入する事ができる
var (name, id) = sample;

// (2) こんな感じで受け取ることもできる
(string name2, int id2) = sample;

この時は受け取り方にやや制限があって上記のような受け取り方しかできません(通常のイコールの代入は Sample 型となってしまいます)

この分解の構文は、複数の値を返すけどクラスを作成したくない時に一時的な値の組み合わせとして値を取り出したい、ようなケースで主に使用しますが、そんなに多様する事は無いと思います。逆に自分の作成したコードでこの分解が頻繁に使用される設計は若干問題がある可能性がります。したがって頻出する場合は少し見直した方が良いかと思います。

以上です。