Posts C# Tuple, ValueTuple
Post
Cancel

C# Tuple, ValueTuple

Note


Tuple, ValueTupleC# 7.0에 처음 도입되었다.

두 개 이상의 타입을 함께 묶어 사용할 때,

클래스나 구조체를 따로 정의하지 않고 곧바로 사용할 수 있게 해준다.


Tuple


클래스 튜플 타입.

클래스 타입이므로 전달할 때 복사가 발생하지 않고, 참조를 전달한다.

Tuple<T1, T2> 같이 명시적으로 타입명을 작성해야 한다.


1
Tuple<int, float> tuple = (10, 20f); // 불가능

아쉽게도 위와 같은 편리한 생성은 안되고,

1
Tuple<int, float> tuple = new Tuple<int, float>(10, 20f);

이렇게 명시적으로 생성자를 호출하여 생성해야 한다.


제네릭 인자는 Tuple<T>, Tuple<T1, T2>, … , Tuple<T1, ... , T8>

이렇게 1개부터 8개까지 지정하여 사용할 수 있다.

내부 값들은 필드가 아니라 프로퍼티로 존재한다.


ValueTuple


구조체 튜플 타입.

구조체 타입이므로 전달할 때마다 복사가 발생한다.

ValueTuple<T1, T2> 같이 타입명을 명시적으로 작성하지 않아도

(T1, T2)와 같은 문법으로 간편하게 사용할 수 있다.


이 때 적용되는 타입은 ValueTuple 타입 자체가 아니고

ValueTuple<T>, ValueTuple<T1, T2>과 같은 제네릭 타입이다.

Tuple<>처럼 제네릭 인자는 1개부터 8개까지 사용할 수 있다.

그리고 내부 값들은 필드 형태로 저장된다.


제네릭 인자가 없는 ValueTuple 구조체 타입은 실제 사용을 위한 타입이 아니며,

대신 Create<T1>(T1 item1), Create<T1, T2>(T1 item1, T2 item2)와 같은 정적 메소드를 제공한다.


타입 추론


<int, float, string> 타입의 ValueTuple을 사용하려면

1
ValueTuple<int, float, string> tuple = (1, 0.5f, "abc");

혹은

1
(int, float, string) tuple = (1, 0.5f, "abc");

처럼 작성하면 된다.

하지만 타입 부분이 너무 길게 늘어진다는 단점이 있다.


1
var tuple = (1, 0.5f, "abc");

image

var 키워드는 타입을 명시적으로 작성하지 않고,

초기화되는 값의 타입에 따라 추론하여 컴파일 타임에 변수의 타입을 지정해준다.

이를 이용하면 특히 튜플 타입의 변수에 대해

일일이 필드의 타입을 명시하지 않고 편리하게 선언할 수 있다.


튜플을 지역변수로 분해하기


선언과 동시에 초기화하기

1
(int, float, string) tuple = (1, 0.5f, "abc");

이런 튜플이 있을 때,

1
(int i, float f, string s) = tuple;

이렇게 각각의 지역 변수 선언과 동시에

튜플을 분해하여 값을 초기화할 수 있다.


1
var (i, f, s) = tuple;

var를 통한 타입 추론을 활용하여

위와 같이 선언과 동시에 초기화할 수도 있다.


무시 항목 사용하기

1
(int i, _, string s) = tuple;

이렇게 무시 항목(_)을 사용하여, 튜플의 일부 필드를 무시하고

원하는 필드들만 지역변수로 선언과 동시에 초기화할 수 있다.


1
var (i, _, s) = tuple;

이런 방식도 가능하다.


유사한 문법

튜플을 사용하지는 않지만 위와 유사한 문법이 있다.

1
(int i, float f) = (123, 45f);
1
var (i, f) = (123, 45f);

이렇게 서로 다른 타입의 지역 변수들에 대해,

한 줄로 묶어서 선언과 동시에 초기화할 수 있다.


이미 선언된 변수들에 튜플 분해하여 전달하기

1
2
3
4
5
6
7
// 튜플
(int, float, string) tuple = (1, 0.5f, "abc");

// 지역 변수들
int i;
float f;
string s;

위와 같이 튜플과 지역변수들이 선언되어 있다.

1
(i, f, s) = tuple;

튜플의 값을 이렇게 필드 순서대로 지역 변수들에 전달할 수 있으며,

1
(i, _, s) = tuple;

전달을 원치 않는 부분은 무시 항목(_)을 사용하여 건너뛰고,

다른 필드들만 전달할 수도 있다.


불가능한 문법 형태

1
2
3
string s; // 미리 선언된 지역 변수

(int i, float f, s) = tuple; // 튜플의 분해와 동시에 선언 및 초기화

분해와 동시에 선언 및 초기화, 그리고 미리 선언된 지역 변수에 대한 초기화는

‘분해의 혼합 선언 및 식’이라고 하며, 동시에 이루어질 수 없다.


1
tuple = (1, _, _); // 불가능

튜플의 필드를 동시에 초기화할 때 위와 같이 일부만 초기화하는 것은 안된다.


필드 이름 직접 지정하기


1
(int, float, string) tuple = (1, 0.5f, "abc");

위에서 작성한 변수 tuple에 대한 각각의 필드는

.item1, .item2, .item3을 통해 참조할 수 있다.

그런데 필드 이름들이 아무런 의미를 담고 있지 않는, 그저 item일 뿐이니

이대로 반복적으로 사용하면 가독성이 영 좋지 않을 것 같다.

따라서 이 필드들에 직접 이름을 지정해줄 수 있다.


1
(int index, float ratio, string name) tuple = (1, 0.5f, "abc");

각 필드에 순서대로 index, ratio, name이라는 이름을 붙여주었다.

이제 item~ 대신, 지정된 이름을 통해 참조할 수 있다.


1
(int, float ratio, string) tuple = (1, 0.5f, "abc");

이렇게 원하는 필드에만 명시적으로 이름을 붙여줄 수 있다.

이름이 지정되지 않은 필드들은 순서대로 item~ 이름을 갖는다.


튜플 타입 매개변수


1
2
3
4
public void Method(int id, string name)
{
    // Do Something
}

이 메소드는 int, string 타입 매개변수를 각각 전달 받는다.

1
2
3
4
public void Method((int id, string name) tuple)
{
    // Do Something
}

이 메소드는 (int, string) 타입의 튜플 매개변수 하나를 전달 받는다.

물론 이런 방식이 필요하다면 사용해야 하겠지만

튜플 타입의 매개변수는 가독성이 썩 좋지 않고 혼동을 유발할 수 있으므로,

굳이 필요하지 않다면 사용하지 않는 것이 좋을 것 같다.


튜플을 반환하는 메소드


튜플의 진정한 편의성은 메소드 반환 타입으로서의 사용에 있다.


1
2
3
4
5
6
private float[] GetData()
{
    float[] retArr = { 1f, 2f, 3f, 4f }; // 임의의 반환 데이터

    return retArr;
}

위와 같이 float[] 타입을 리턴하는 메소드가 있다.

만약 이 배열 데이터의 유효성을 검증하고 사용하려면

1
2
3
4
5
6
float[] arr = GetData();

if (arr != null && arr.Length > 0)
{
    // Do Something
}

이렇게 일단 메소드를 통해 데이터를 전달 받은 다음,

데이터를 사용하는 부분에서 검증해야 한다.


그런데 튜플을 활용하면 이런 검사를 미리 완료하여 전달할 수 있다.

1
2
3
4
5
6
7
8
/// <summary> 완성된 배열 데이터, 데이터 유효성을 함께 리턴 </summary>
private (bool, float[]) GetData()
{
    float[] retArr = { 1f, 2f, 3f, 4f }; // 임의의 반환 데이터

    // 미리 유효성 검증 완료하여 반환
    return (retArr != null && retArr.Length > 0, retArr);
}

데이터를 받아 사용하는 부분에서는

1
2
3
4
5
6
(bool isValid, float[] arr) data = GetData();

if (data.isValid)
{
    // Do Something
}

이렇게 완성된 데이터들을 전달받아 곧바로 사용할 수 있다.


반환되는 튜플의 필드 이름 지정하기

1
2
3
4
private (bool isValid, float[] arr) GetData()
{
    return ...;
}

이렇게 메소드의 반환부에 각 튜플 필드의 이름을 명시적으로 지정하고,

1
2
3
4
5
6
var data = GetData();

if (data.isValid)
{
    // Do Something
}

var로 타입 추론을 하면 그 이름 그대로 사용하도록 할 수도 있다.


필요하지 않은 반환 값 무시하기

위의 경우를 예시로,

데이터의 유효성만 필요하거나 혹은 데이터 자체만 필요한 경우

1
2
(bool isValid, _) = GetData();
(_, var arrData) = GetData();

이런 식으로 무시 항목을 사용하여 필터링할 수 있다.

1
2
(_, _) = GetData();
_ = GetData();

심지어 위와 같은 방식도 가능한데,

이렇게 한다고 해서 반환 값의 메모리 할당을 방지하여 최적화를 한다거나 그렇지는 않다.


튜플 형변환 연산자


1
2
3
4
5
class Student
{
    public int id;
    public string name;
}

클래스가 있다.

이 클래스 타입의 객체가 (int, string) 타입의 튜플로 형변환 되도록 하려면

1
2
3
4
5
6
7
8
9
10
class Student
{
    public int id;
    public string name;

    public static implicit operator (int id, string name)(Student student)
    {
        return (student.id, student.name);
    }
}

이렇게 작성하면 되고,

1
2
Student student = new Student();
(int, string) tuple = student;

위와 같이 사용할 수 있다.


참고

1
2
Student student = new Student();
(int, string) = student; // Deconstruct() 호출

이런 형태는 튜플 형변환이 아니라 Deconstruct() 메소드 호출이며,

전혀 다른 문법이다.


References


This post is licensed under CC BY 4.0 by the author.