튜플 및 기타 형식 분해

튜플은 메서드 호출에서 여러 값을 검색할 수 있는 간단한 방법을 제공합니다. 하지만 튜플을 검색한 후 튜플의 개별 요소를 처리해야 합니다. 다음 예에서 볼 수 있듯이 요소별로 작업하는 것은 번거롭습니다. 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);
    }
}

개체에서 여러 필드 및 속성 값을 검색하는 작업도 똑같이 번거로울 수 있습니다. 멤버별로 변수에 필드 또는 속성 값을 할당해야 하기 때문입니다.

단일 분해 작업으로 튜플에서 여러 요소를 검색하거나 개체에서 여러 필드, 속성 및 계산된 값을 검색할 수 있습니다. 튜플을 분해하려면 해당 요소를 개별 변수에 할당합니다. 개체를 분해할 때는 선택한 값을 개별 변수에 할당합니다.

튜플

C#에서는 튜플 분해를 기본적으로 지원하므로 한 작업에서 튜플의 모든 항목을 패키지 해제할 수 있습니다. 튜플을 분해하는 일반 구문은 정의하는 구문과 유사합니다. 즉, 대입문 왼쪽에서 각 요소가 할당되는 변수를 괄호로 묶습니다. 예를 들어, 다음 문은 4-튜플의 요소를 4개의 개별 변수에 할당합니다.

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

다음과 같은 세 가지 방법으로 튜플을 분해합니다.

  • 괄호 안에 각 필드의 형식을 명시적으로 선언할 수 있습니다. 다음 예에서는 이 방법을 사용하여 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, "'var (...)' 형식 분해는 'var'에 대한 특정 형식을 허용하지 않습니다."가 생성됩니다.

튜플의 각 요소를 변수에 할당해야 합니다. 요소를 생략하면 컴파일러에서 오류 CS8132, "'x' 요소의 튜플을 'y' 변수로 분해할 수 없습니다."를 생성합니다.

무시 항목이 있는 튜플 요소

튜플을 분해할 때 일부 요소 값에만 관심이 있는 경우가 종종 있습니다. 값을 무시하도록 선택한 쓰기 전용 변수인 무시 항목에 대한 C#의 지원을 활용할 수 있습니다. 무시 항목은 할당에서 밑줄 문자("_")로 선택됩니다. 원하는 수의 값을 모두 하나의 무시 항목 _로 표시하여 무시할 수 있습니다.

다음 예제에서는 무시 항목과 함께 튜플을 사용하는 방법을 보여 줍니다. QueryCityDataForYears 메서드는 도시 이름, 지역, 연도, 해당 연도의 도시 모집단, 두 번째 해, 두 번째 해의 도시 모집단이 포함된 6개의 튜플을 반환합니다. 이 예제는 이러한 두 연도 사이의 인구 변화를 보여 줍니다. 튜플에서 사용 가능한 데이터 중 도시 면적에는 관심이 없고 디자인 타임에 도시 이름과 두 날짜를 알고 있습니다. 따라서 튜플에 저장된 두 가지 인구 값에만 관심이 있고 나머지 값은 무시 항목으로 처리할 수 있습니다.

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#은 recordDictionaryEntry 형식 이외의 튜플이 아닌 형식을 분해하기 위한 기본 지원을 제공하지 않습니다. 그러나 클래스, 구조체 또는 인터페이스의 만든 이는 하나 이상의 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 메서드에서 반환된 항목 중 선택한 항목을 무시할 수 있습니다. 각 무시 항목은 “_”라는 변수로 정의하며 단일 분해 작업에 여러 무시 항목을 포함할 수 있습니다.

다음 예제에서는 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확장 메서드 구현을 통해 해당 형식의 개체를 분해하여 관심 있는 값을 반환할 수 있습니다.

다음 예제에서는 System.Reflection.PropertyInfo 클래스에 대한 두 개의 Deconstruct 확장 메서드를 정의합니다. 첫 번째 메서드는 속성의 형식, 정적 속성인지 인스턴스 속성인지 여부, 읽기 전용인지 여부, 인덱싱되었는지 여부 등 속성의 특성을 나타내는 값 집합을 반환합니다. 두 번째 메서드는 속성의 접근성을 나타냅니다. get 및 set 접근자의 접근성이 다를 수 있으므로 부울 값은 속성에 별도의 get 및 set 접근자가 있는지 여부와 있는 경우 접근성이 동일한지 여부를 나타냅니다. 접근자가 하나만 있거나 get 및 set 접근자 모두 동일한 접근성을 갖는 경우 access 변수는 속성의 접근성을 전체적으로 나타냅니다. 그러지 않으면 get 및 set 접근자의 접근성이 getAccesssetAccess 변수로 표시됩니다.

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 형식

둘 이상의 위치 매개 변수를 사용하여 record 형식을 선언하는 경우 컴파일러는 record 선언의 각 위치 매개 변수에 대해 out 매개 변수를 사용하여 Deconstruct 메서드를 만듭니다. 자세한 내용은 속성 정의의 위치 구문파생 레코드의 분해자 동작을 참조하세요.

참고 항목