タプルとその他の型の分解

タプルでは、軽量な処理でメソッドの呼び出しから複数の値を取得することができます。 ただし、タプルを取得した場合は、その個々の要素を処理する必要があります。 要素ごとに作業を行うのは面倒です。例を以下に示します。 QueryCityData メソッドから 3 タプルが返され、その各要素は別の操作の変数に代入されます。

public class Example
{
    public static void Main()
    {
        var result = QueryCityData("New York City");

        var city = result.Item1;
        var pop = result.Item2;
        var size = result.Item3;

         // Do something with the data.
    }

    private static (string, int, double) QueryCityData(string name)
    {
        if (name == "New York City")
            return (name, 8175133, 468.48);

        return ("", 0, 0);
    }
}

1 つのオブジェクトから複数のフィールドとプロパティの値を取得するのも同様に面倒です。メンバーごとにフィールドまたはプロパティの値を変数に代入する必要があります。

C# 7.0 以降では、単一の ''分解'' 操作で、タプルから複数の要素を取得したり、オブジェクトから複数のフィールド、プロパティ、および計算値を取得したりできます。 タプルを分解するには、その要素を個々の変数に代入します。 オブジェクトを分解するときに、選択した値を個々の変数に割り当てます。

タプル

C# には、タプルの分解を組み込みでサポートしているという特長があり、単一操作でタプル内のすべての項目を展開することができます。 タプルを分解する一般的な構文は、タプルを定義する構文と似ています。代入ステートメントの左側で変数をかっこで囲み、その各要素に割り当てられます。 たとえば、次のステートメントでは、4 タプルの要素を 4 つの別の変数に代入します。

var (name, address, city, zip) = contact.GetAddressInfo();

タプルの分解には 3 つの方法があります。

  • かっこ内の各フィールドの型を明示的に宣言することができます。 次の例では、この方法を使用して、QueryCityData メソッドによって返される 3 タプルを分解します。

    public static void Main()
    {
        (string city, int population, double area) = QueryCityData("New York City");
    
        // Do something with the data.
    }
    
  • C# で各変数の型を推定するには、var キーワードを使用できます。 var キーワードはかっこの外に配置します。 次の例では、QueryCityData メソッドによって返される 3 タプルを分解するときに型の推定を使用します。

    public static void Main()
    {
        var (city, population, area) = QueryCityData("New York City");
    
        // Do something with the data.
    }
    

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

    public static void Main()
    {
        (string city, var population, var area) = QueryCityData("New York City");
    
        // Do something with the data.
    }
    

    これは面倒であるため、推奨されません。

  • 最後に、既に宣言されている変数にタプルを分解することができます。

    public static void Main()
    {
        string city = "Raleigh";
        int population = 458880;
        double area = 144.8;
    
        (city, population, area) = QueryCityData("New York City");
    
        // Do something with the data.
    }
    
  • C# 10 以降では、変数の宣言と代入を分解に混在させることができます。

    public static void Main()
    {
        string city = "Raleigh";
        int population = 458880;
    
        (city, population, double area) = QueryCityData("New York City");
    
        // Do something with the data.
    }
    

タプル内のすべてのフィールドの型が同じでも、かっこ外で特定の型を指定することはできません。 そのようにすると、コンパイル エラー CS8136 "分解 `変数 (...)` フォームは特定の種類の '変数' を許可しません" が生成されます。

タプルの各要素を変数に代入する必要があります。 いずれかの要素を省略すると、コンパイラでエラー CS8132 " 'x' 要素のタプルを 'y' 変数に分解することはできません" が生成されます。

破棄を使用するタプル要素

タプルを分解する場合、一部の要素の値のみが必要なことがよくあります。 C# 7.0 以降、C# の破棄のサポートを利用できるようになりました。破棄は、無視対象と選択した値が指定された書き込み専用の変数です。 破棄を選択するには、代入でアンダースコア文字 ("_") を使用します。 必要に応じて任意の数の値を破棄できます。そのため、すべての値を 1 つの破棄 _ で表すことができます。

破棄を含むタプルの使用例を次に示します。 QueryCityDataForYears メソッドによって、市区町村名、その地域、年、市区町村のその年の人口、2 つ目の年、市区町村のその 2 つ目の年の人口という 6 タプルが返されます。 この例は、2 つの年の間に変化した人口数を示しています。 タプルから使用できるデータのうち、市区町村の地域は使用しません。また、指定時に市区町村名と 2 つの日付はわかっています。 そのため、タプルに格納されている 2 つの人口値のみが必要であり、残りの値は破棄対象として処理できます。

using System;

public class ExampleDiscard
{
    public static void Main()
    {
        var (_, _, _, pop1, _, pop2) = QueryCityDataForYears("New York City", 1960, 2010);

        Console.WriteLine($"Population change, 1960 to 2010: {pop2 - pop1:N0}");
    }

    private static (string, double, int, int, int, int) QueryCityDataForYears(string name, int year1, int year2)
    {
        int population1 = 0, population2 = 0;
        double area = 0;

        if (name == "New York City")
        {
            area = 468.48;
            if (year1 == 1960)
            {
                population1 = 7781984;
            }
            if (year2 == 2010)
            {
                population2 = 8175133;
            }
            return (name, area, year1, population1, year2, population2);
        }

        return ("", 0, 0, 0, 0, 0);
    }
}
// The example displays the following output:
//      Population change, 1960 to 2010: 393,149

ユーザー定義データ型

C# では、record および DictionaryEntry 型以外の非タプル型を分解するための組み込みサポートは提供されていません。 ただし、クラス、構造体、またはインターフェイスの作成者であれば、1 つまたは複数の Deconstruct メソッドを実装することで、型のインスタンスを分解することができます。 このメソッドからは void が返され、分解される各値はメソッド シグネチャの out パラメーターで示されます。 たとえば、Person クラスの次の Deconstruct メソッドは、名、ミドル ネーム、姓を返します。

public void Deconstruct(out string fname, out string mname, out string lname)

その後、p という Person クラスのインスタンスを、次のコードのような代入で分解できます。

var (fName, mName, lName) = p;

次の例では、Deconstruct メソッドをオーバーロードし、Person オブジェクトの多様な組み合わせのプロパティを返します。 各オーバーロードから以下が返されます。

  • 名と姓。
  • 名、ミドルネーム、姓。
  • 名、姓、市区町村名、州名。
using System;

public class Person
{
    public string FirstName { get; set; }
    public string MiddleName { get; set; }
    public string LastName { get; set; }
    public string City { get; set; }
    public string State { get; set; }

    public Person(string fname, string mname, string lname,
                  string cityName, string stateName)
    {
        FirstName = fname;
        MiddleName = mname;
        LastName = lname;
        City = cityName;
        State = stateName;
    }

    // Return the first and last name.
    public void Deconstruct(out string fname, out string lname)
    {
        fname = FirstName;
        lname = LastName;
    }

    public void Deconstruct(out string fname, out string mname, out string lname)
    {
        fname = FirstName;
        mname = MiddleName;
        lname = LastName;
    }

    public void Deconstruct(out string fname, out string lname,
                            out string city, out string state)
    {
        fname = FirstName;
        lname = LastName;
        city = City;
        state = State;
    }
}

public class ExampleClassDeconstruction
{
    public static void Main()
    {
        var p = new Person("John", "Quincy", "Adams", "Boston", "MA");

        // Deconstruct the person object.
        var (fName, lName, city, state) = p;
        Console.WriteLine($"Hello {fName} {lName} of {city}, {state}!");
    }
}
// The example displays the following output:
//    Hello John Adams of Boston, MA!

パラメーター数が同じ Deconstruct メソッドが複数あると、あいまいになります。 異なる数のパラメーター、つまり "アリティ" を持つ Deconstruct メソッドを定義するように注意する必要があります。 オーバーロードの解決時に、パラメーター数が同じ Deconstruct メソッドを区別できません。

破棄を使用するユーザー定義型

タプルの場合と同様に、破棄を使用して、Deconstruct メソッドから返される項目のうち、選択した項目を無視できます。 各破棄は "_" という変数で定義されます。また、1 つの破棄操作に複数の破棄を含めることができます。

Person オブジェクトを 4 つの文字列 (名、姓、市区町村、州) に分解し、姓と州を破棄する例を次に示します。

// Deconstruct the person object.
var (fName, _, city, _) = p;
Console.WriteLine($"Hello {fName} of {city}!");
// The example displays the following output:
//      Hello John of Boston!

ユーザー定義型の拡張メソッド

クラス、構造体、またはインターフェイスを作成していない場合でも、目的の値を返す Deconstruct拡張メソッドを 1 つまたは複数実装することで、このようなオブジェクトを分解することができます。

System.Reflection.PropertyInfo クラスの Deconstruct 拡張メソッドを 2 つ定義する例を次に示します。 1 つ目の拡張メソッドは、型、静的かインスタンスか、読み取り専用かどうか、インデックスが作成されているかどうかなど、プロパティの特徴を示す値のセットを返します。 2 つ目の拡張メソッドは、プロパティのアクセシビリティを示します。 get アクセサーと set アクセサーのアクセシビリティは異なる可能性があるため、ブール値は、プロパティの get アクセサーと set アクセサーが異なるかどうか、また異なる場合はアクセシビリティが同じかどうかを示します。 アクセサーが 1 つのみの場合、または get と set の両方のアクセサーのアクセシビリティが同じである場合、access 変数は、全体としてそのプロパティのアクセシビリティを示します。 それ以外の場合、get アクセサーと set アクセサーのアクセシビリティは getAccess 変数と setAccess 変数で示されます。

using System;
using System.Collections.Generic;
using System.Reflection;

public static class ReflectionExtensions
{
    public static void Deconstruct(this PropertyInfo p, out bool isStatic,
                                   out bool isReadOnly, out bool isIndexed,
                                   out Type propertyType)
    {
        var getter = p.GetMethod;

        // Is the property read-only?
        isReadOnly = ! p.CanWrite;

        // Is the property instance or static?
        isStatic = getter.IsStatic;

        // Is the property indexed?
        isIndexed = p.GetIndexParameters().Length > 0;

        // Get the property type.
        propertyType = p.PropertyType;
    }

    public static void Deconstruct(this PropertyInfo p, out bool hasGetAndSet,
                                   out bool sameAccess, out string access,
                                   out string getAccess, out string setAccess)
    {
        hasGetAndSet = sameAccess = false;
        string getAccessTemp = null;
        string setAccessTemp = null;

        MethodInfo getter = null;
        if (p.CanRead)
            getter = p.GetMethod;

        MethodInfo setter = null;
        if (p.CanWrite)
            setter = p.SetMethod;

        if (setter != null && getter != null)
            hasGetAndSet = true;

        if (getter != null)
        {
            if (getter.IsPublic)
                getAccessTemp = "public";
            else if (getter.IsPrivate)
                getAccessTemp = "private";
            else if (getter.IsAssembly)
                getAccessTemp = "internal";
            else if (getter.IsFamily)
                getAccessTemp = "protected";
            else if (getter.IsFamilyOrAssembly)
                getAccessTemp = "protected internal";
        }

        if (setter != null)
        {
            if (setter.IsPublic)
                setAccessTemp = "public";
            else if (setter.IsPrivate)
                setAccessTemp = "private";
            else if (setter.IsAssembly)
                setAccessTemp = "internal";
            else if (setter.IsFamily)
                setAccessTemp = "protected";
            else if (setter.IsFamilyOrAssembly)
                setAccessTemp = "protected internal";
        }

        // Are the accessibility of the getter and setter the same?
        if (setAccessTemp == getAccessTemp)
        {
            sameAccess = true;
            access = getAccessTemp;
            getAccess = setAccess = String.Empty;
        }
        else
        {
            access = null;
            getAccess = getAccessTemp;
            setAccess = setAccessTemp;
        }
    }
}

public class ExampleExtension
{
    public static void Main()
    {
        Type dateType = typeof(DateTime);
        PropertyInfo prop = dateType.GetProperty("Now");
        var (isStatic, isRO, isIndexed, propType) = prop;
        Console.WriteLine($"\nThe {dateType.FullName}.{prop.Name} property:");
        Console.WriteLine($"   PropertyType: {propType.Name}");
        Console.WriteLine($"   Static:       {isStatic}");
        Console.WriteLine($"   Read-only:    {isRO}");
        Console.WriteLine($"   Indexed:      {isIndexed}");

        Type listType = typeof(List<>);
        prop = listType.GetProperty("Item",
                                    BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
        var (hasGetAndSet, sameAccess, accessibility, getAccessibility, setAccessibility) = prop;
        Console.Write($"\nAccessibility of the {listType.FullName}.{prop.Name} property: ");

        if (!hasGetAndSet | sameAccess)
        {
            Console.WriteLine(accessibility);
        }
        else
        {
            Console.WriteLine($"\n   The get accessor: {getAccessibility}");
            Console.WriteLine($"   The set accessor: {setAccessibility}");
        }
    }
}
// The example displays the following output:
//       The System.DateTime.Now property:
//          PropertyType: DateTime
//          Static:       True
//          Read-only:    True
//          Indexed:      False
//
//       Accessibility of the System.Collections.Generic.List`1.Item property: public

システム型の拡張メソッド

システム型によっては、Deconstruct メソッドが便宜上用意されています。 たとえば、System.Collections.Generic.KeyValuePair<TKey,TValue> 型には、この機能があります。 System.Collections.Generic.Dictionary<TKey,TValue> を反復処理する場合、各要素は KeyValuePair<TKey, TValue> であり、分解することができます。 次の例を確認してください。

Dictionary<string, int> snapshotCommitMap = new(StringComparer.OrdinalIgnoreCase)
{
    ["https://github.com/dotnet/docs"] = 16_465,
    ["https://github.com/dotnet/runtime"] = 114_223,
    ["https://github.com/dotnet/installer"] = 22_436,
    ["https://github.com/dotnet/roslyn"] = 79_484,
    ["https://github.com/dotnet/aspnetcore"] = 48_386
};

foreach (var (repo, commitCount) in snapshotCommitMap)
{
    Console.WriteLine(
        $"The {repo} repository had {commitCount:N0} commits as of November 10th, 2021.");
}

Deconstruct メソッドを持たないシステム型には、これを追加することができます。 次の拡張メソッドについて考えてみましょう。

public static class NullableExtensions
{
    public static void Deconstruct<T>(
        this T? nullable,
        out bool hasValue,
        out T value) where T : struct
    {
        hasValue = nullable.HasValue;
        value = nullable.GetValueOrDefault();
    }
}

この拡張メソッドを使用すると、すべての Nullable<T> 型を (bool hasValue, T value) のタプルに分解することができます。 次の例に、この拡張メソッドを使用するコードを示します。

DateTime? questionableDateTime = default;
var (hasValue, value) = questionableDateTime;
Console.WriteLine(
    $"{{ HasValue = {hasValue}, Value = {value} }}");

questionableDateTime = DateTime.Now;
(hasValue, value) = questionableDateTime;
Console.WriteLine(
    $"{{ HasValue = {hasValue}, Value = {value} }}");

// Example outputs:
// { HasValue = False, Value = 1/1/0001 12:00:00 AM }
// { HasValue = True, Value = 11/10/2021 6:11:45 PM }

record

2 つ以上の位置指定パラメーターを使用して record 型を宣言すると、コンパイラでは、record 宣言内の位置指定パラメーターごとに out パラメーターを使用する Deconstruct メソッドを作成します。 詳細については、「プロパティ定義の位置指定構文」および「派生レコードのデコンストラクターの動作」を参照してください。

関連項目