JS/TIL(Today I Learned)

2025-03-31 <튜플에 대하여>

프린스 알리 2025. 3. 31.

 

C# 튜플(ValueTuple) 정리

C#의 튜플(Tuple) 기능은 여러 데이터 요소를 하나의 가벼운 데이터 구조로 묶는 간결한 구문을 제공한다. 이 글에서는 튜플의 선언, 초기화, 멤버 접근, 주요 사용 사례, 필드 이름 지정, 형식 별칭, 할당 및 구조 분해 등의 내용을 다룬다.


1. 튜플 기본 사용법

(double, int) t1 = (4.5, 3);
Console.WriteLine($"요소 {t1.Item1}와 {t1.Item2}를 가진 튜플.");
// 출력: 요소 4.5와 3를 가진 튜플.

(double Sum, int Count) t2 = (4.5, 3);
Console.WriteLine($"{t2.Count}개의 요소의 합은 {t2.Sum}입니다.");
// 출력: 3개의 요소의 합은 4.5입니다.
  • 튜플을 정의할 때는 각 데이터 멤버의 타입과 선택적으로 이름을 지정한다.
  • 튜플 타입 내에 메서드는 정의할 수 없지만, .NET에서 제공하는 메서드는 사용할 수 있다.
(double, int) t = (4.5, 3);
Console.WriteLine(t.ToString());
Console.WriteLine($"해시코드: {t.GetHashCode()}");
  • 튜플 타입은 값 타입이며, 요소는 public 필드이다. 따라서 변경 가능한 값 타입(mutable struct)이다.

튜플은 매우 많은 수의 요소도 가질 수 있다:

var t = (1, 2, ..., 26); // 최대 26개 이상도 가능
Console.WriteLine(t.Item26);  // 출력: 26

2. 튜플의 사용 사례

메서드에서 여러 값 반환

(int min, int max) FindMinMax(int[] input)
{
    if (input is null || input.Length == 0)
        throw new ArgumentException("배열이 null이거나 비어 있음");

    var min = int.MaxValue;
    var max = int.MinValue;

    foreach (var i in input)
    {
        if (i < min) min = i;
        if (i > max) max = i;
    }

    return (min, max);
}

사용 예시:

var (minimum, maximum) = FindMinMax(new[] { 1, 5, 9 });
Console.WriteLine($"최솟값: {minimum}, 최댓값: {maximum}");
  • out 키워드를 사용하는 대신 튜플을 통해 여러 값을 간편하게 반환할 수 있다.
  • 튜플은 LINQ 쿼리에서도 익명 타입 대신 사용할 수 있다.
  • 느슨하게 관련된 데이터 그룹화에 유용하다.
  • 공용 API에서는 클래스나 구조체 정의를 고려하는 것이 좋다.

3. 튜플 필드 이름

튜플의 필드 이름은 다음과 같이 명시적으로 지정할 수 있다:

var t = (Sum: 4.5, Count: 3);
Console.WriteLine($"{t.Count}개의 합은 {t.Sum}");

다음과 같이 타입 정의 시에도 지정 가능하다:

(double Sum, int Count) d = (4.5, 3);

필드 이름 자동 추론 (Projection Initializers)

변수명을 필드 이름으로 자동 추론한다:

var sum = 4.5;
var count = 3;
var t = (sum, count);
Console.WriteLine($"{t.count}개의 합은 {t.sum}");

단, 다음과 같은 경우에는 이름이 자동으로 추론되지 않는다:

  • 후보 이름이 Item3, ToString, Rest와 같은 튜플 기본 멤버 이름일 경우
  • 중복되는 필드 이름이 존재할 경우

명시적으로 지정하거나 기본 이름(Item1, Item2, ...)으로 접근할 수 있다:

var a = 1;
var t = (a, b: 2, 3);
Console.WriteLine(t.Item1); // 1 (a)
Console.WriteLine(t.Item2); // 2 (b)
Console.WriteLine(t.Item3); // 3

컴파일 시 명시적/추론된 이름은 내부적으로 기본 이름으로 대체되며, 런타임에서는 사용할 수 없다.


4. 튜플 타입에 별칭 지정 (C# 12 이상)

using 지시문으로 튜플 타입에 별칭(alias)을 줄 수 있다:

global using BandPass = (int Min, int Max);
BandPass bracket = (40, 100);
Console.WriteLine($"{bracket.Min} ~ {bracket.Max}");

별칭은 새로운 타입을 생성하는 것이 아니라, 기존 튜플 타입에 의미를 부여하는 용도이다.

다음과 같이 구조 분해도 가능하다:

(int a, int b) = bracket;

서로 호환되는 다른 별칭도 사용할 수 있다:

using Range = (int Minimum, int Maximum);
Range r = bracket;

→ 타입과 요소 수만 일치하면 이름이 달라도 할당 가능하다.

※ 진정한 타입 안정성이 필요하다면 Positional Record를 사용하는 것이 더 적절하다.


5. 튜플 할당과 구조 분해

두 튜플 타입 간에 다음 조건을 만족하면 할당이 가능하다:

  • 요소의 수가 같음
  • 각 요소의 타입이 동일하거나 암시적으로 변환 가능함
(int a, int b) = (1, 2);  // 구조 분해
(int, int) t1 = (3, 4);
(int x, int y) = t1;

마무리

튜플은 C#에서 다수의 값을 손쉽게 묶고 처리할 수 있는 매우 유용한 기능이다.

  • 일시적 데이터 그룹화
  • 메서드 반환
  • 구조 분해
  • 별칭을 통한 가독성 향상 등 다양한 용도로 활용 가능하다.

단, 공용 API에서는 되도록 클래스를 정의하고, 복잡하거나 재사용이 필요한 구조에는 레코드나 구조체를 고려하는 것이 좋다.


참고

댓글