C# のタプル型

C# のタプルは、軽量構文を使用して定義する型で、 構文がシンプルである、変換の規則が要素の数 ("基数" と呼ばれます) と種類に基づく、コピーと割り当ての規則が一貫している、などのメリットがあります。 そのトレードオフとして、タプルでは、継承に関連するオブジェクト指向の表現形式の一部がサポートされていません。 概要については、C# 7 の新機能のタプルに関するトピックのセクションをご覧ください。

このトピックでは、C# 7 でタプルに適用される言語の規則、タプルの使用方法、およびタプルを操作するための入門的ガイダンスについて説明します。

注意

新しいタプル機能を使用するには、ValueTuple 型が必要です。 型が含まれていないプラットフォームで使用する場合は、NuGet パッケージ System.ValueTuple を追加する必要があります。

これは、フレームワークで提供される型に依存するその他の言語機能に似ています。 たとえば、INotifyCompletion インターフェイスに依存する asyncawaitIEnumerable<T> に依存する LINQ などがあります。 ただし、.NET がプラットフォームにさらに依存しなくなりつつあるため、配信メカニズムもそれに応じて変わりつつあります。 .NET Framework が、言語コンパイラと同じ周期で配布されるとは限りません。 新しい言語機能が新しい型に依存する場合、それらの型は、言語機能の配布時に NuGet パッケージとして入手できます。 これらの新しい型は .NET 標準 API に追加され、フレームワークの一部として配信されるため、NuGet パッケージは必要なくなります。

詳しく見ていく前に、新しいタプルのサポートを追加した理由について説明します。 メソッドが返すのは 1 つのオブジェクトです。 タプルを使用すると、その 1 つのオブジェクトに複数の値を簡単にパッケージできます。

.NET Framework には Tuple ジェネリック クラスが既にありますが、 このクラスには 2 つの大きな制限がありました。 1 つは、Tuple クラスのプロパティに Item1Item2 という名前が付けられるというものです。 その名前にはセマンティック情報が保持されていません。 つまり、この Tuple 型を使用しても、各プロパティの情報を伝達することはできません。 新しい言語機能を使用すると、タプルの要素に意味的にわかりやすい名前を宣言して使用できます。

さらに、Tuple クラスが参照型であるのも、懸念事項の 1 つです。 Tuple 型を使用するには、オブジェクトを割り当てる必要があります。 ホット パスでは、これがアプリケーションのパフォーマンスに大きな影響を及ぼすことがあります。 そのため、タプルの言語サポートでは、新しい ValueTuple 構造体を活用します。

classstruct を作成して複数の要素を伝達すれば、この弱点を回避できますが、 その分、手間が増え、設計の意図がわかりにくくなります。 structclass を作成すると、データと動作の両方で型を定義しなければなりませんが、 1 つのオブジェクトに複数の値を格納したいだけという状況もよくあります。

これらの機能と ValueTuple ジェネリック構造体では、タプル型に動作 (メソッド) を追加できないという規則が適用されます。 すべての ValueTuple 型が "mutable 構造体" で、 各メンバー フィールドがパブリック フィールドであるため、 非常に軽量です。 ただし、不変性が重要な場合は、タプルを使用しないでください。

タプルは、class 型や struct 型に比べて、シンプルで柔軟なデータ コンテナーです。 両者の違いを見てみましょう。

名前付きのタプルと名前がないタプル

既存の Tuple 型で定義されたプロパティと同様、ValueTuple 構造体のフィールドには Item1Item2Item3 といった名前が付いています。 "名前のないタプル" には、この名前しか使用できません。 タプルに代替フィールド名を付けなかった場合は、名前のないタプルが作成されます。

var unnamed = ("one", "two");

前の例のタプルは、リテラル定数を使って初期化されており、C# 7.1 のタプル フィールド名プロジェクションを使って作成された要素名はありません。

タプルを初期化する際に、新しい機能を使用して、各フィールドにわかりやすい名前を付けることができます。 これによって、"名前付きタプル" が作成されます。 名前付きタプルには Item1Item2Item3 という名前の要素がまだ存在しますが、 名前を付けた要素に対してシノニムも設定されます。 名前付きタプルを作成するには、各要素の名前を指定します。 たとえば、タプル初期化の一環として名前を指定できます。

var named = (first: "one", second: "two");

コンパイラと言語によってシノニムが処理されるため、名前付きタプルを効果的に使用できるようになります。 IDE やエディターは Roslyn API を使用して、セマンティック名を読み取ります。 これにより、同じアセンブリ内の任意の場所で、セマンティック名によって名前付きタプルの要素を参照できます。 定義した名前は、コンパイル済み出力が生成されるときに、対応する Item* に置き換えられます。 これらの要素に設定した名前は、コンパイルされた Microsoft Intermediate Language (MSIL) には含まれません。

C# 7.1 以降、タプルのフィールド名は、タプルの初期化に使用した変数によって指定される場合があります。 これは、タプル プロジェクション初期化子と呼ばれます。 次のコードでは、要素 count (整数)、および sum (倍精度浮動小数点型) で accumulation という名前のタプルを作成します。

var sum = 12.5;
var count = 5;
var accumulation = (count, sum);

コンパイラは、パブリック メソッドまたはプロパティから返されたタプルに指定されている名前を伝える必要があります。 このような場合、コンパイラはメソッドに @System.Runtime.CompilerServices.TupleElementNames 属性を追加します。 この属性には、タプルの各要素に付けられた名前が含まれた @System.Runtime.CompilerServices.TupleElementNames.TransformNames リスト プロパティが含まれています。

注意

Visual Studio などの開発ツールも、そのメタデータを読み取り、メタデータ フィールド名を使用する IntelliSense などの機能を提供します。

名前付きタプルを相互に割り当てるための規則を理解するには、新しいタプルと ValueTuple 型の基本を理解しておくことが重要です。

タプル プロジェクション初期化子

一般に、タプル プロジェクション初期化子は、タプルの初期化ステートメントの右側にある変数またはフィールド名を使用して機能します。 明示的な名前が指定された場合は、射影された名前より優先されます。 たとえば、次の初期化子では、要素は localVariableOnelocalVariableTwo ではなく、explicitFieldOneexplicitFieldTwo になります。

var localVariableOne = 5;
var localVariableTwo = "some text";

var tuple = (explicitFieldOne: localVariableOne, explicitFieldTwo: localVariableTwo);

明示的な名前が指定されていないフィールドの場合、適用可能な暗黙的な名前が射影されます。 明示的または暗黙的のいずれかで、セマンティック名を指定するための要件はありません。 次の初期化子には、フィールド名 Item1 があり、その値は 42StringContent で、その値は "The answer to everything" です。

var stringContent = "The answer to everything";
var mixedTuple = (42, stringContent);

候補フィールド名がタプル フィールドに射影されない場合の条件は 2 つあります。

  1. 候補名が予約されているタプル名の場合。 例としては、Item3ToString または Rest です。
  2. 候補名が、別のタプル フィールド名 (明示的または暗黙的のいずれか) の複製である場合。

これらの条件によってあいまいさを回避します。 この名前がタプルのフィールドのフィールド名として使用される場合、あいまいさの原因となります。 この条件はどちらも、コンパイル時エラーを発生させることはありません。 代わりに、射影された名前のない要素には、射影されたセマンティック名がありません。 これらの条件の例を以下に示します。

var ToString = "This is some text";
var one = 1;
var Item1 = 5;
var projections = (ToString, one, Item1);
// Accessing the first field:
Console.WriteLine(projections.Item1);
// There is no semantic name 'ToString'
// Accessing the second field:
Console.WriteLine(projections.one);
Console.WriteLine(projections.Item2);
// Accessing the third field:
Console.WriteLine(projections.Item3);
// There is no semantic name 'Item`.

var pt1 = (X: 3, Y: 0);
var pt2 = (X: 3, Y: 4);

var xCoords = (pt1.X, pt2.X);
// There are no semantic names for the fields
// of xCoords. 

// Accessing the first field:
Console.WriteLine(xCoords.Item1);
// Accessing the second field:
Console.WriteLine(xCoords.Item2);

これらの条件は、タプル フィールド名プロジェクションが利用できなかった場合、C# 7.0 で記述されたコードに対する重大な変更になるため、コンパイラ エラーが発生することはありません。

割り当てとタプル

要素の数が同じで、これらの各要素に型の暗黙的な変換があるタプル型間での、代入がサポートされています。 他の変換は、割り当てでは考慮されません。 タプル型間で許可されている割り当ての種類を見てみましょう。

以降の例で使用されている変数について考えます。

// The 'arity' and 'shape' of all these tuples are compatible. 
// The only difference is the field names being used.
var unnamed = (42, "The meaning of life");
var anonymous = (16, "a perfect square");
var named = (Answer: 42, Message: "The meaning of life");
var differentNamed = (SecretConstant: 42, Label: "The meaning of life");

最初の 2 つの変数 unnamed および anonymous では、要素にセマンティック名が割り当てられていません。 フィールド名は Item1Item2 になります。 最後の 2 つの変数 named および differentName では、要素にセマンティック名が付けられています。 この 2 つのタプルでは、要素名が異なっていることに注意してください。

この 4 つのタプルに含まれている要素の数 ("基数" と呼ばれます) と要素の型は同じです。 このため、これらの割り当てはすべて機能します。

unnamed = named;

named = unnamed;
// 'named' still has fields that can be referred to
// as 'answer', and 'message':
Console.WriteLine($"{named.Answer}, {named.Message}");

// unnamed to unnamed:
anonymous = unnamed;

// named tuples.
named = differentNamed;
// The field names are not assigned. 'named' still has 
// fields that can be referred to as 'answer' and 'message':
Console.WriteLine($"{named.Answer}, {named.Message}");

// With implicit conversions:
// int can be implicitly converted to long
(long, string) conversion = named;

タプルの名前が割り当てられていないことに注意してください。 要素の値は、タプルの要素の順序に従って割り当てられます。

要素の型または数が異なるタプルを割り当てることはできません。

// Does not compile.
// CS0029: Cannot assign Tuple(int,int,int) to Tuple(int, string)
var differentShape = (1, 2, 3);
named = differentShape;

メソッドの戻り値としてのタプル

タプルはメソッドの戻り値として使用できます。これはタプルの一般的な使用方法の 1 つです。 その例を見てみましょう。 数値シーケンスの標準偏差を計算する次のメソッドについて考えます。

public static double StandardDeviation(IEnumerable<double> sequence)
{
    // Step 1: Compute the Mean:
    var mean = sequence.Average();

    // Step 2: Compute the square of the differences between each number 
    // and the mean:
    var squaredMeanDifferences = from n in sequence
                                 select (n - mean) * (n - mean);
    // Step 3: Find the mean of those squared differences:
    var meanOfSquaredDifferences = squaredMeanDifferences.Average();

    // Step 4: Standard Deviation is the square root of that mean:
    var standardDeviation = Math.Sqrt(meanOfSquaredDifferences);
    return standardDeviation;
}
注意

この例では、未修正のサンプル標準偏差を計算します。 修正後のサンプル標準偏差式は、Average 拡張メソッドで行われるのと同様に、平均値との差の二乗和を、N ではなく (N-1) で除算します。 標準偏差のこうした数式の間に生じる差の詳細については、統計値のテキストを参照してください。

これは、標準偏差の教科書どおりの数式に従っています。 正しい答えが生成されますが、きわめて非効率的な実装です。 このメソッドは、シーケンスを 2 回列挙します。1 回は平均値を生成するため、もう 1 回は平均値との差を 2 乗して、その平均値を生成するためです (前述のとおり、LINQ クエリは遅延評価されるため、平均値との差と、その差の平均値の計算で生成される列挙は 1 つだけです)。

シーケンスの列挙を 1 つだけ使用して標準偏差を計算する、別の数式があります。 この計算では、シーケンスを列挙しながら、2 つの値が生成されます。1 つはシーケンス内のすべての項目の合計、もう 1 つは各値の二乗和です。

public static double StandardDeviation(IEnumerable<double> sequence)
{
    double sum = 0;
    double sumOfSquares = 0;
    double count = 0;

    foreach (var item in sequence)
    {
        count++;
        sum += item;
        sumOfSquares += item * item;
    }

    var variance = sumOfSquares - sum * sum / count;
    return Math.Sqrt(variance / count);
}

このバージョンでは、シーケンスを 1 回だけ列挙しますが、 最適な再利用可能なコードとは言えません。 操作を続けていくと、さまざまな統計計算処理の多くが、シーケンス内の項目数、シーケンスの合計、およびシーケンスの二乗和を使用していることがわかります。 このメソッドをリファクタリングし、その 3 つの値すべてを生成するユーティリティ メソッドを作成しましょう。

ここで、タプルが非常に役に立ちます。

このメソッドを更新して、列挙中に計算された 3 つの値をタプルに格納しましょう。 そうすると、次のバージョンが作成されます。

public static double StandardDeviation(IEnumerable<double> sequence)
{
    var computation = (Count: 0, Sum: 0.0, SumOfSquares: 0.0);

    foreach (var item in sequence)
    {
        computation.Count++;
        computation.Sum += item;
        computation.SumOfSquares += item * item;
    }

    var variance = computation.SumOfSquares - computation.Sum * computation.Sum / computation.Count;
    return Math.Sqrt(variance / computation.Count);
}

Visual Studio のリファクタリング サポートにより、主要な統計情報の機能をプライベート メソッドに抽出できます。 これにより、3 つの値 SumSumOfSquaresCount を含むタプル型を返す private static メソッドが作成されます。

public static double StandardDeviation(IEnumerable<double> sequence)
{
    (int Count, double Sum, double SumOfSquares) computation = ComputeSumsAnSumOfSquares(sequence);

    var variance = computation.SumOfSquares - computation.Sum * computation.Sum / computation.Count;
    return Math.Sqrt(variance / computation.Count);
}

private static (int Count, double Sum, double SumOfSquares) ComputeSumsAnSumOfSquares(IEnumerable<double> sequence)
{
    var computation = (count: 0, sum: 0.0, sumOfSquares: 0.0);

    foreach (var item in sequence)
    {
        computation.count++;
        computation.sum += item;
        computation.sumOfSquares += item * item;
    }

    return computation;
}

編集を手動ですばやく行う必要がある場合は、使用できるオプションが他にもいくつかあります。 まず、var 宣言を使用することで、ComputeSumAndSumOfSquares メソッド呼び出しのタプルの結果を初期化できます。 ComputeSumAndSumOfSquares メソッド内に異なる 3 つの変数を作成することもできます。 最終的なバージョンは以下のようになります。

public static double StandardDeviation(IEnumerable<double> sequence)
{
    var computation = ComputeSumAndSumOfSquares(sequence);

    var variance = computation.SumOfSquares - computation.Sum * computation.Sum / computation.Count;
    return Math.Sqrt(variance / computation.Count);
}

private static (int Count, double Sum, double SumOfSquares) ComputeSumAndSumOfSquares(IEnumerable<double> sequence)
{
    double sum = 0;
    double sumOfSquares = 0;
    int count = 0;

    foreach (var item in sequence)
    {
        count++;
        sum += item;
        sumOfSquares += item * item;
    }

    return (count, sum, sumOfSquares);
}

この最終バージョンは、この 3 つの値を必要とするすべてのメソッド、またはそのサブセットで使用できます。

このようなタプルを返すメソッドで要素の名前を管理するオプションが他にもサポートされています。

戻り値の宣言からフィールド名を削除して、名前のないタプルを返すことができます。

private static (double, double, int) ComputeSumAndSumOfSquares(IEnumerable<double> sequence)
{
    double sum = 0;
    double sumOfSquares = 0;
    int count = 0;

    foreach (var item in sequence)
    {
        count++;
        sum += item;
        sumOfSquares += item * item;
    }

    return (sum, sumOfSquares, count);
}

このタプルのフィールドは、Item1Item2Item3 として扱う必要があります。 メソッドから返されたタプルの要素には、セマンティック名を指定することをお勧めします。

また、作成している LINQ クエリの最終的な結果が、選択したオブジェクトのプロパティが一部だけ含まれるプロジェクションとなるときも、タプルが非常に便利です。

従来、クエリの結果は、匿名型のオブジェクトのシーケンスに射影していましたが、 この方法には多くの制限が伴いました。メソッドの戻り値の型では、匿名型に名前を付けるのが簡単ではなかったからです。 代替手段として objectdynamic を結果の型に使用すると、パフォーマンス コストは大きくなります。

タプル型のシーケンスを返すのは簡単です。要素の名前と型は、コンパイル時に IDE ツールで使用することができます。 たとえば、次の ToDo アプリケーションを考えてみます。 ToDo リストの 1 つのエントリを表すために、次のようなクラスを定義します。

public class ToDoItem
{
    public int ID { get; set; }
    public bool IsDone { get; set; }
    public DateTime DueDate { get; set; }
    public string Title { get; set; }
    public string Notes { get; set; }    
}

モバイル アプリケーションでサポートされるのは、タイトルしか表示されないコンパクト形式の現在の ToDo 項目です。 その LINQ クエリでは、ID とタイトルのみが含まれるプロジェクションが作成されます。 タプルのシーケンスを返すメソッドは、その設計を適切に表現しています。

internal IEnumerable<(int ID, string Title)> GetCurrentItemsMobileList()
{
    return from item in AllItems
           where !item.IsDone
           orderby item.DueDate
           select (item.ID, item.Title);
}
注意

C# 7.1 では、タプル プロジェクションを使用して、匿名型で名前が付けられたプロパティと同様の方法で、要素を使用する名前付きタプルを作成できます。 上記のコードでは、クエリ プロジェクションの select ステートメントで、要素 IDTitle を含むタプルを作成します。

名前付きタプルは、署名に含めることができます。 これによって、コンパイラと IDE ツールは静的チェックを行い、結果が正しく使用されていることを確認できます。 名前付きタプルには静的情報も含まれているため、リフレクション、動的バインドなど、コストのかかるランタイム機能を使用して結果を操作する必要がありません。

分解

タプル内のすべての項目を展開するには、メソッドによって返されるタプルを分解します。 タプルは 2 とおりの方法で分解できます。 まず、かっこの中で各フィールドの型を明示的に宣言して、タプルの要素ごとに個別の変数を作成することができます。

public static double StandardDeviation(IEnumerable<double> sequence)
{
    (int count, double sum, double sumOfSquares) = ComputeSumAndSumOfSquares(sequence);

    var variance = sumOfSquares - sum * sum / count;
    return Math.Sqrt(variance / count);
}

また、かっこの外に var キーワードを使用して、タプルの各フィールドに対して暗黙的に型指定された変数を宣言することもできます。

public static double StandardDeviation(IEnumerable<double> sequence)
{
    var (sum, sumOfSquares, count) = ComputeSumAndSumOfSquares(sequence);

    var variance = sumOfSquares - sum * sum / count;
    return Math.Sqrt(variance / count);
}

var キーワードは、かっこ内のいずれか 1 つの変数宣言に使用することも、すべての変数宣言に使用することもできます。

(double sum, var sumOfSquares, var count) = ComputeSumAndSumOfSquares(sequence);

タプル内のフィールドすべての型が同じでも、かっこ外では使用できない型があることに注意してください。

ユーザー定義型の分解

上に示したように、すべてのタプル型を分解できます。 また、ユーザー定義型 (クラス、構造体、またはインターフェイス) も簡単に分解できます。

型の作成者は、型を構成するデータ要素を表す任意の数の out 変数に対して値を割り当てる Deconstruct メソッドを 1 つ以上定義できます。 たとえば、次の Person 型は、person オブジェクトを、名と姓を表す要素に分解する Deconstruct メソッドを定義しています。

public class Person
{
    public string FirstName { get; }
    public string LastName { get; }

    public Person(string first, string last)
    {
        FirstName = first;
        LastName = last;
    }

    public void Deconstruct(out string firstName, out string lastName)
    {
        firstName = FirstName;
        lastName = LastName;
    }
}

deconstruct メソッドを使用すると、Person から、FirstName プロパティと LastName プロパティを表す 2 つの文字列を割り当てることができます。

var p = new Person("Althea", "Goodwin");
var (first, last) = p;

自分で作成していない型を分解することもできます。 Deconstruct メソッドは、オブジェクトのアクセス可能なデータ メンバーを展開する拡張メソッドとして使用できます。 次の例は、Person から派生した Student 型と、Student を 3 つの変数 FirstNameLastNameGPA に分解する拡張メソッドを示しています。

public class Student : Person
{
    public double GPA { get; }
    public Student(string first, string last, double gpa) :
        base(first, last)
    {
        GPA = gpa;
    }
}

public static class Extensions
{
    public static void Deconstruct(this Student s, out string first, out string last, out double gpa)
    {
        first = s.FirstName;
        last = s.LastName;
        gpa = s.GPA;
    }
}

Student オブジェクトには、アクセス可能な Deconstruct メソッドが 2 つあります。Student 型に対して宣言された拡張メソッドと、Person 型のメンバーです。 両方ともスコープ内にあり、Student を 2 つまたは 3 つの変数に分解できます。 student を 3 つの変数に割り当てると、名、姓、GPA のすべてが返されます。 student を 2 つの変数に割り当てると、名と姓のみが返されます。

var s1 = new Student("Cary", "Totten", 4.5);
var (fName, lName, gpa) = s1;

クラスまたはクラス階層で複数の Deconstruct メソッドを定義するときには注意が必要です。 out パラメーターの数が同じ Deconstruct メソッドが複数あると、あいまいさが生じ、 呼び出し元が、必要な Deconstruct メソッドを簡単には呼び出せなくなる場合があります。

この例では、出力パラメーターが PersonDeconstruct メソッドには 2 つ、StudentDeconstruct メソッドには 3 つ含まれるため、呼び出しが不明確になる可能性は最小限に抑えられています。

まとめ

クラスや構造体では動作の定義が必要であるため、新しい言語とライブラリで名前付きタプルがサポートされたことで、動作を定義せずに複数の要素を格納するデータ構造設計が格段に扱いやすくなりました。 こうした型に対してタプルを使用するのは簡単です。 詳細な class または struct 構文を使用して型を作成しなくても、静的な型チェックのすべてのメリットを利用できます。 とは言っても、class や struct は、privateinternal のユーティリティ メソッドにとっては非常に便利です。 複数の要素を含む値がパブリック メソッドによって返される場合は、ユーザー定義型、class またはstruct を作成します。