タプル型 (C# リファレンス)

"タプル" 機能を使用すると、軽量のデータ構造に複数のデータ要素を簡潔な構文でグループ化できます。 次の例は、タプル変数を宣言して初期化し、そのデータ メンバーにアクセスする方法を示しています。

(double, int) t1 = (4.5, 3);
Console.WriteLine($"Tuple with elements {t1.Item1} and {t1.Item2}.");
// Output:
// Tuple with elements 4.5 and 3.

(double Sum, int Count) t2 = (4.5, 3);
Console.WriteLine($"Sum of {t2.Count} elements is {t2.Sum}.");
// Output:
// Sum of 3 elements is 4.5.

前の例で示したように、タプル型を定義するには、すべてのデータ メンバーの型と、必要に応じてフィールド名を指定します。 タプル型でメソッドを定義することはできませんが、次の例に示すように、.NET によって提供されるメソッドを使用できます。

(double, int) t = (4.5, 3);
Console.WriteLine(t.ToString());
Console.WriteLine($"Hash code of {t} is {t.GetHashCode()}.");
// Output:
// (4.5, 3)
// Hash code of (4.5, 3) is 718460086.

タプル型は等値演算子==!= をサポートしています。 詳しくは、「タプルの等値性」セクションをご覧ください。

タプル型は値型であり、タプル要素はパブリック フィールドです。 そのため、タプルは "変更可能な" 値の型になります。

任意の多数の要素を持つタプルを定義できます。

var t =
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18,
19, 20, 21, 22, 23, 24, 25, 26);
Console.WriteLine(t.Item26);  // output: 26

タプルのユース ケース

特に一般的なタプルのユース ケースの 1 つが、メソッドの戻り値の型です。 つまり、out メソッド パラメーターを定義するのではなく、次の例のようにメソッドの結果をタプルの戻り値の型でグループ化できます。

int[] xs = [4, 7, 9];
var limits = FindMinMax(xs);
Console.WriteLine($"Limits of [{string.Join(" ", xs)}] are {limits.min} and {limits.max}");
// Output:
// Limits of [4 7 9] are 4 and 9

int[] ys = [-9, 0, 67, 100];
var (minimum, maximum) = FindMinMax(ys);
Console.WriteLine($"Limits of [{string.Join(" ", ys)}] are {minimum} and {maximum}");
// Output:
// Limits of [-9 0 67 100] are -9 and 100

(int min, int max) FindMinMax(int[] input)
{
    if (input is null || input.Length == 0)
    {
        throw new ArgumentException("Cannot find minimum and maximum of a null or empty array.");
    }

    // Initialize min to MaxValue so every value in the input
    // is less than this initial value.
    var min = int.MaxValue;
    // Initialize max to MinValue so every value in the input
    // is greater than this initial value.
    var max = int.MinValue;
    foreach (var i in input)
    {
        if (i < min)
        {
            min = i;
        }
        if (i > max)
        {
            max = i;
        }
    }
    return (min, max);
}

前の例で示したように、返されたタプルのインスタンスを直接操作することも、別の変数に分解することもできます。

タプル型は匿名型の代わりに、たとえば LINQ クエリで使用することもできます。 詳細については、「匿名型またはタプル型の選択」を参照してください。

通常、タプルは関連性の低いデータ要素をグループ化するために使用します。 パブリック API では、クラスまたは構造体型を定義することを検討してください。

タプルのフィールド名

次の例に示すように、タプルの初期化式またはタプル型の定義でタプル フィールドの名前を明示的に指定します。

var t = (Sum: 4.5, Count: 3);
Console.WriteLine($"Sum of {t.Count} elements is {t.Sum}.");

(double Sum, int Count) d = (4.5, 3);
Console.WriteLine($"Sum of {d.Count} elements is {d.Sum}.");

フィールド名を指定しない場合、次の例に示すように、タプルの初期化式で対応する変数の名前からそれが推論される場合があります。

var sum = 4.5;
var count = 3;
var t = (sum, count);
Console.WriteLine($"Sum of {t.count} elements is {t.sum}.");

これはタプル プロジェクション初期化子と呼ばれます。 変数の名前は、次の場合、タプル フィールド名に投影されません。

  • 候補名が Item3ToStringRest などのタプル型のメンバー名である。
  • 候補名が、別のタプル フィールド名 (明示的または暗黙的のいずれか) の複製である。

上記の場合は、フィールドの名前を明示的に指定するか、既定の名前でフィールドにアクセスします。

タプル フィールドの既定の名前は、Item1Item2Item3 などです。 次の例に示すように、フィールド名が明示的に指定されていたり推論されていたりする場合でも、フィールドの既定の名前をいつでも使用できます。

var a = 1;
var t = (a, b: 2, 3);
Console.WriteLine($"The 1st element is {t.Item1} (same as {t.a}).");
Console.WriteLine($"The 2nd element is {t.Item2} (same as {t.b}).");
Console.WriteLine($"The 3rd element is {t.Item3}.");
// Output:
// The 1st element is 1 (same as 1).
// The 2nd element is 2 (same as 2).
// The 3rd element is 3.

タプルの代入タプルの等価比較では、フィールド名が考慮されません。

コンパイル時に、コンパイラは既定以外のフィールド名を、対応する既定の名前に置き換えます。 このため、明示的に指定された、または推論されたフィールド名は実行時に使用できません。

ヒント

.NET コード スタイル規則 IDE0037 を有効にし、タプル フィールドの推論される名前または明示的な名前に優先を設定します。

C# 12 以降では、using ディレクティブを使用してタプル型のエイリアスを指定できます。 次の例では、許可された MinMax 値に対して 2 つの整数値を持つタプル型の global using エイリアスを追加します。

global using BandPass = (int Min, int Max);

エイリアスを宣言した後、そのタプル型のエイリアスとして BandPass 名を使用できます。

BandPass bracket = (40, 100);
Console.WriteLine($"The bandpass filter is {bracket.Min} to {bracket.Max}");

エイリアスでは新しい ''型'' は導入されませんが、既存の型のシノニムのみが作成されます。 基になるタプル型と同じように、BandPass エイリアスで宣言されたタプルを分解できます。

(int a , int b) = bracket;
Console.WriteLine($"The bracket is {a} to {b}");

タプルの割り当てや分解と同様に、タプル メンバー名が一致する必要はありません。型は一致する必要があります。

同様に、同じアリティとメンバー型を持つ 2 番目のエイリアスは、元のエイリアスと同じように使用できます。 2 番目のエイリアスは次のように宣言できます。

using Range = (int Minimum, int Maximum);

Range タプルを BandPass タプルに割り当てることができます。 すべてのタプルの割り当てと同様に、フィールド名が一致する必要はなく、型とアリティだけが一致する必要があります。

Range r = bracket;
Console.WriteLine($"The range is {r.Minimum} to {r.Maximum}");

タプル型のエイリアスでは、タプルを使用するときにより多くのセマンティック情報が提供されます。 新しい型は導入されません。 タイプ セーフを提供するには、代わりに位置指定 record を宣言する必要があります。

タプルの代入と分解

C# では、次の両方の条件を満たすタプル型間の代入がサポートされます。

  • 両方のタプル型に、同じ数の要素がある
  • タプルのそれぞれの位置で、右側のタプル要素の型が対応する左側のタプル要素の型と同じか、暗黙的に変換可能である

タプル要素の値は、タプル要素の順序に従って代入されます。 次の例に示すように、タプル フィールドの名前は無視され、代入されません。

(int, double) t1 = (17, 3.14);
(double First, double Second) t2 = (0.0, 1.0);
t2 = t1;
Console.WriteLine($"{nameof(t2)}: {t2.First} and {t2.Second}");
// Output:
// t2: 17 and 3.14

(double A, double B) t3 = (2.0, 3.0);
t3 = t2;
Console.WriteLine($"{nameof(t3)}: {t3.A} and {t3.B}");
// Output:
// t3: 17 and 3.14

また、代入演算子 = を使用して、別の変数でタプル インスタンスを "分解する" こともできます。 さまざまな方法でこれを行うことができます。

  • かっこの外にある var キーワードを使用して、暗黙的に型指定された変数を宣言し、コンパイラによってその型が推論されるようにする。

    var t = ("post office", 3.6);
    var (destination, distance) = t;
    Console.WriteLine($"Distance to {destination} is {distance} kilometers.");
    // Output:
    // Distance to post office is 3.6 kilometers.
    
  • かっこ内の各変数の型を明示的に宣言する。

    var t = ("post office", 3.6);
    (string destination, double distance) = t;
    Console.WriteLine($"Distance to {destination} is {distance} kilometers.");
    // Output:
    // Distance to post office is 3.6 kilometers.
    
  • かっこ内で (var を使用して) 一部の型を明示的に宣言し、他の型を暗黙的に宣言します。

    var t = ("post office", 3.6);
    (var destination, double distance) = t;
    Console.WriteLine($"Distance to {destination} is {distance} kilometers.");
    // Output:
    // Distance to post office is 3.6 kilometers.
    
  • 既存の変数を使用する。

    var destination = string.Empty;
    var distance = 0.0;
    
    var t = ("post office", 3.6);
    (destination, distance) = t;
    Console.WriteLine($"Distance to {destination} is {distance} kilometers.");
    // Output:
    // Distance to post office is 3.6 kilometers.
    

分解式のターゲットには、既存の変数と、分解宣言で宣言された変数の両方を含めることができます。

また、分解とパターン マッチングを組み合わせて、タプル内のフィールドの特性を調べることもできます。 次の例では、いくつかの整数をループ処理し、3 で割り切れるものを出力します。 Int32.DivRem のタプルの結果を分解し、Remainder 0 と照合します。

for (int i = 4; i < 20;  i++)
{
    if (Math.DivRem(i, 3) is ( Quotient: var q, Remainder: 0 ))
    {
        Console.WriteLine($"{i} is divisible by 3, with quotient {q}");
    }
}

タプルとその他の型の分解の詳細については、「タプルとその他の型の分解」を参照してください。

タプルの等値性

タプル型は、==!= 演算子をサポートしています。 これらの演算子により、左側のオペランドのメンバーが、タプル要素の順序に従って、右側のオペランドの対応するメンバーと比較されます。

(int a, byte b) left = (5, 10);
(long a, int b) right = (5, 10);
Console.WriteLine(left == right);  // output: True
Console.WriteLine(left != right);  // output: False

var t1 = (A: 5, B: 10);
var t2 = (B: 5, A: 10);
Console.WriteLine(t1 == t2);  // output: True
Console.WriteLine(t1 != t2);  // output: False

前の例で示したように、==!= の演算では、タプルのフィールド名は考慮されません。

2 つのタプルは、次の両方の条件が満たされている場合に比較できます。

  • 両方のタプルが、同じ数の要素を保持している。 たとえば、t1t2 の要素数が異なる場合、t1 != t2 はコンパイルされません。
  • タプルの位置ごとに、左側と右側のタプルのオペランドの対応する要素が、==!= の演算子と比較されます。 たとえば、1(1, 2) と比較できないため、(1, (2, 3)) == ((1, 2), 3) はコンパイルされません。

==!= の演算子によって、タプルがショートサーキット方式で比較されます。 つまり、等しくない要素のペアか、タプルの端に到達するとすぐに、演算が停止します。 ただし、比較の前には必ず、次の例に示すように、"すべての" タプル要素が評価されます。

Console.WriteLine((Display(1), Display(2)) == (Display(3), Display(4)));

int Display(int s)
{
    Console.WriteLine(s);
    return s;
}
// Output:
// 1
// 2
// 3
// 4
// False

out パラメーターとしてのタプル

通常は、out パラメーターを持つメソッドを、タプルを返すメソッドにリファクタリングします。 ただし、out パラメーターがタプル型である場合もあります。 次の例に、out パラメーターとしてタプルを操作する方法を示します。

var limitsLookup = new Dictionary<int, (int Min, int Max)>()
{
    [2] = (4, 10),
    [4] = (10, 20),
    [6] = (0, 23)
};

if (limitsLookup.TryGetValue(4, out (int Min, int Max) limits))
{
    Console.WriteLine($"Found limits: min is {limits.Min}, max is {limits.Max}");
}
// Output:
// Found limits: min is 10, max is 20

タプルと System.Tuple

System.ValueTuple 型によってサポートされる C# のタプルは、System.Tuple タプルで表されるタプルとは異なります。 主な相違点は、次のとおりです。

  • System.ValueTuple 型は値の型です。 System.Tuple 型は参照型です。
  • System.ValueTuple 型は変更可能です。 System.Tuple 型は変更不可能です。
  • System.ValueTuple 型のデータ メンバーはフィールドです。 System.Tuple 型のデータ メンバーはプロパティです。

C# 言語仕様

詳細については、次を参照してください。

関連項目