Menu

C# 튜플: 이름 붙은 튜플, 여러 값 반환, 분해

C# 튜플은 타입을 선언하지 않고 몇 개의 값을 하나로 묶습니다. 값 튜플 문법, 이름 붙은 요소, 메서드에서 여러 값 반환하기, var (a, b)로 분해하기, 딕셔너리의 튜플 키, 그리고 ValueTuple과 예전 System.Tuple의 차이를 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

튜플은 클래스를 선언하지 않고 작고 정해진 개수의 값을 하나의 값으로 묶습니다. ("Ana", 31)은 string과 int로 이루어진 튜플입니다. 튜플은 메서드에서 두세 가지를 반환하고, 메서드 안에서 잠깐 쓰는 쌍을 담는 관용적인 방법입니다.

튜플 만들기

타입도 괄호로 씁니다. (string, int)는 첫 요소가 string, 두 번째가 int인 튜플 타입입니다.

출력:

Ana is 31
(3, 4)
179.70
(Ana, 32)

이름이 없으면 요소는 Item1, Item2, Item3 등입니다. 이 튜플들은 System.ValueTuple 구조체이므로, 다른 변수에 대입하면 값이 복사되고 복사본을 바꿔도 원본은 그대로입니다.

이름 붙은 요소

Item1은 그 값이 무엇인지 아무것도 말해 주지 않습니다. 타입이나 리터럴에서 요소에 이름을 붙이세요:

출력:

Ana, 31
Desk lamp costs 24.99
Ana
Ana

이름은 컴파일 시점의 편의입니다. 실행 시점에는 위의 두 튜플이 모두 평범한 ValueTuple<string, int> 값이며, 그래서 person을 이름이 다른 튜플에 대입해도 컴파일됩니다. 요소 타입과 순서만 맞으면 됩니다.

C# 7.1 이상은 리터럴 안의 변수로부터 이름도 추론하므로, var t = (name, age);라고 하면 이름을 두 번 쓰지 않고도 t.name과 t.age를 얻습니다. C# 7.0에서는 이 요소들이 Item1과 Item2뿐입니다.

메서드에서 여러 값 반환하기

튜플이 언어에 추가된 주된 이유가 이것입니다. 그 전의 선택지는 out 매개변수, 일회용 클래스, 또는 이름 없는 Item1을 가진 System.Tuple이었습니다.

출력:

Lowest 60, highest 95
Average 78.75

반환 타입의 요소 이름이 호출하는 쪽이 보는 이름이 됩니다. 같은 메서드를 out 매개변수로 쓴 것과 비교해 보세요:

static void Stats(int[] scores, out int min, out int max, out double average) { ... }

Stats(scores, out int min, out int max, out double avg);

bool이 성공 여부를 알리고 값이 함께 돌아오는 TryParse 모양에는 out 매개변수가 여전히 관례입니다. "여러 가지를 계산해서 반환한다"에는 튜플이 더 읽기 좋습니다. ref와 out을 참고하세요.

분해

분해는 한 문장으로 튜플을 별도의 변수들로 풀어 줍니다:

출력:

Lima 24.5
Lima
24.5
2 1

var (city, temp)의 변수 이름은 마음대로 정할 수 있습니다. 분해는 위치로 이루어지므로 튜플의 요소 이름과 맞을 필요가 없습니다. 교환 (a, b) = (b, a)는 먼저 이전 값들로 튜플을 만든 뒤 대입하므로 어느 값도 사라지지 않습니다.

직접 만든 클래스도 out 매개변수를 가진 Deconstruct 메서드를 선언하면 분해를 지원할 수 있습니다. C# 7과 .NET Core 2.0 이상에서는 KeyValuePair에도 이 메서드가 있으므로, 딕셔너리 루프가 헤더에서 각 항목을 풀 수 있습니다:

class Point
{
    public int X { get; }
    public int Y { get; }
    public Point(int x, int y) { X = x; Y = y; }
    public void Deconstruct(out int x, out int y) { x = X; y = Y; }
}

var (x, y) = new Point(3, 4);

foreach (var (name, score) in scoresByName)   // Dictionary<string, int>
{
    Console.WriteLine($"{name}: {score}");
}

같은 foreach (var (a, b) in listOfTuples) 형태가 튜플 리스트에도 동작합니다. 다음 절처럼 튜플 전체를 하나의 루프 변수로 받아 이름 붙은 요소를 읽는 것도 똑같이 잘 동작합니다.

리스트, LINQ, 딕셔너리 안의 튜플

이름 붙은 튜플의 리스트는 메서드 안에서 데이터 행을 담는 가벼운 방법입니다:

출력:

Chloe 95
Ana 88
Ben 72
(Ana, B), (Chloe, A)
Ben
False

ValueTuple은 Equals와 GetHashCode를 요소별로 구현하므로, 서로 다른 두 곳에서 만든 (2, 7)이 같은 딕셔너리 항목을 찾습니다. 그래서 튜플은 가장 간단한 복합 키입니다. 클래스도, 직접 쓴 해시 함수도, row + ":" + col 같은 문자열 이어 붙이기도 필요 없습니다.

튜플 같음 비교

Equals는 요소별로 비교하며 튜플이 도입될 때부터 동작했습니다:

출력:

True
False

C# 7.3은 튜플에 ==와 !=를 추가했으며, 같은 요소별 비교로 컴파일되고 역시 이름은 무시합니다:

if ((order.Status, order.Paid) == ("shipped", true)) { ... }
Console.WriteLine((1, "x") == (1, "x"));   // True

System.Tuple과 ValueTuple

.NET 4는 Tuple.Create로 만드는 클래스 System.Tuple을 도입했습니다. 지금도 프레임워크에 있고 예전 코드와 API에 나타나므로, "C# tuple"을 검색하면 자주 이것이 나옵니다.

ValueTuple(C# 7)System.Tuple(.NET 4)
문법(1, "a"), (int, string)Tuple.Create(1, "a"), Tuple<int, string>
종류구조체(값 형식)클래스(참조 형식)
요소 이름있음없음, Item1, Item2뿐
변경 가능예, 요소가 필드아니요, 읽기 전용 속성
분해가능가능, 확장 메서드를 통해

출력:

Ben 25
(Ben, 25)

ToValueTuple()과 ToTuple()은 둘 사이를 변환합니다. 새 코드에서는 값 튜플을 우선 쓰세요. 힙에 아무것도 할당하지 않고 이름을 담을 수 있습니다.

대신 클래스나 레코드를 써야 할 때

튜플은 묶음이 지역적이고 뻔할 때 가장 좋습니다. 메서드가 숫자 두 개를 반환하거나, LINQ 쿼리가 다음 단계로 쌍을 넘기거나, 딕셔너리에 두 부분짜리 키가 필요할 때입니다. 다음 경우에는 이름 붙은 타입으로 바꾸세요:

  • 같은 모양이 여러 public 메서드 시그니처에 나타날 때. 다섯 곳에 있는 (string, string, int)는 아직 작성되지 않은 클래스입니다.
  • 값에 동작, 검증, 또는 서너 개보다 많은 요소가 필요할 때.
  • 데이터가 직렬화 같은 경계를 넘을 때. 이름은 실행 시점에 존재하지 않으므로 Newtonsoft.Json은 Item1과 Item2를 쓰고, System.Text.Json은 튜플 요소가 기본적으로 건너뛰는 필드이므로 {}를 씁니다.

C# 9 레코드는 진짜 이름과 함께 튜플의 편리함 대부분을 줍니다. record Score(string Name, int Points);에는 값 같음 비교, 읽기 좋은 ToString, 분해가 기본으로 들어 있습니다. 레코드를 참고하세요.

흔한 실수

  • 이름이 실행 시점에도 남아 있으리라 기대하기. 리플렉션과 직렬 변환기는 Item1, Item2를 봅니다(또는 아무것도 보지 못합니다).
  • 복사본 바꾸기. 튜플은 구조체입니다. var t2 = t1; t2.Item1 = 5;는 t1을 바꾸지 않습니다.
  • 요소가 많은 튜플. 서너 개를 넘으면 이름 붙은 타입이 더 읽기 좋습니다.
  • 분해 순서 혼동. 위치 기반입니다. (Name, Age) 튜플에서 var (age, name) = person;은 age에 이름을 넣으며, var가 타입을 추론하므로 아무것도 경고하지 않습니다.

자주 묻는 질문

C#에서 메서드가 여러 값을 반환하려면 어떻게 하나요?

튜플을 반환하세요. 반환 타입을 (int Min, int Max)로 선언하고 return (lowest, highest);라고 씁니다. 호출하는 쪽은 result.Min과 result.Max를 읽거나 var (min, max) = MinMax(data);로 분해합니다. 대안은 TryParse 패턴에 어울리는 out 매개변수, 또는 결과 자체에 의미가 있을 때 쓰는 작은 클래스나 레코드입니다.

C#의 이름 붙은 튜플이란 무엇인가요?

요소가 Item1, Item2 대신 이름을 갖는 튜플입니다. (string Name, int Age) person = ("Ana", 31);이라고 하면 person.Name이라고 쓸 수 있습니다. 이름은 컴파일 시점에만 존재하며, 내부적으로는 여전히 ValueTuple<string, int>이고 Item1도 계속 동작합니다.

C#에서 Tuple과 ValueTuple의 차이는 무엇인가요?

System.Tuple(.NET 4)은 클래스입니다. 힙에 할당되고, 불변이며, 요소 이름은 항상 Item1, Item2뿐입니다. System.ValueTuple(C# 7)은 변경 가능한 필드를 가진 구조체이고, 요소 이름과 (a, b) 문법을 지원하며, 모든 튜플 리터럴이 만드는 것이 이것입니다. 새 코드에서는 값 튜플을 쓰세요.

C#에서 튜플 분해는 어떻게 동작하나요?

var (name, age) = person;은 변수 두 개를 선언하고 튜플의 요소를 순서대로 대입합니다. (a, b) = (b, a)처럼 기존 변수에 대입할 수도 있으며, 이렇게 하면 두 값이 교환됩니다. 무시 항목 _로 요소를 건너뛸 수도 있습니다: var (_, age) = person;.

C#에서 튜플을 ==로 비교할 수 있나요?

C# 7.3부터 ==와 !=는 튜플을 요소별로 비교합니다: (1, "a") == (1, "a")는 true입니다. 이름은 무시되고 위치와 값만 중요합니다. C# 7.3 이전에는 값 튜플이 같은 방식으로 구현하는 Equals를 쓰세요.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기