다음을 통해 공유


튜플 및 기타 형식 분해

튜플은 메서드 호출에서 여러 값을 검색할 수 있는 간단한 방법을 제공합니다. 하지만 튜플을 검색한 후 튜플의 개별 요소를 처리해야 합니다. 다음 예에서 볼 수 있듯이 요소별로 작업하는 것은 번거롭습니다. 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 메서드를 만듭니다. 자세한 내용은 속성 정의의 위치 구문파생 레코드의 분해자 동작을 참조하세요.

참고 항목