英語で読む

次の方法で共有


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

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

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

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

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

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

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

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

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

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

    上記の例は煩雑であるため、お勧めしません。

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

    C#
    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#
    public static void Main()
    {
        string city = "Raleigh";
        int population = 458880;
    
        (city, population, double area) = QueryCityData("New York City");
    
        // Do something with the data.
    }
    

タプル内のすべてのフィールドの型が同じでも、かっこ外で特定の型を指定することはできません。 これを行うと、コンパイラ エラー CS8136 "分解 'var (...)' 形式では 'var' に特定の型を指定できません" が生成されます。

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

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

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

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

C#
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 メソッドは、名、ミドルネーム、および姓を返します。

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

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

C#
var (fName, mName, lName) = p;

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

  • 名と姓。
  • 名、ミドルネーム、姓。
  • 名、姓、市区町村名、都道府県名。
C#
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 つの文字列 (名、姓、市、州) に分解し、姓と州を破棄しています。

C#
// 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 つまたは複数実装することで、このようなオブジェクトを分解することができます。

Deconstruct クラスの System.Reflection.PropertyInfo 拡張メソッドを 2 つ定義する例を次に示します。 最初のメソッドは、プロパティの特性を示す値のセットを返します。 2 つ目の拡張メソッドは、プロパティのアクセシビリティを示します。 ブール値は、プロパティに個別の get アクセサーと set アクセサーがあるか、または異なるアクセシビリティがあるかを示します。 アクセサーが 1 つのみの場合、または get と set の両方のアクセサーのアクセシビリティが同じである場合、access 変数は、全体としてそのプロパティのアクセシビリティを示します。 それ以外の場合、get アクセサーと set アクセサーのアクセシビリティは getAccess 変数と setAccess 変数で示されます。

C#
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> であり、分解することができます。 次の例を確認してください。

C#
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.");
}

record

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

関連項目