콘텐츠로 이동

Rust 튜플

튜플(tuple)은 타입이 서로 다를 수도 있는 여러 값을 하나로 묶어요. 새로운 타입을 따로 정의하지 않고도 관련된 값들을 빠르게 묶을 수 있는 방법이에요.

fn main() {
let person: (&str, i32, bool) = ("지민", 30, true);
println!("{:?}", person);
}

실행 결과:

("지민", 30, true)
▶ 직접 실행해 보기

이 튜플은 &str, i32, bool을 하나의 값에 섞어 담고 있어요. 배열은 이렇게 할 수 없어요. 배열의 모든 요소는 같은 타입이어야 하거든요.

튜플 전체를 출력하려면 {} 대신 {:?}(디버그 형식)를 쓰세요. 그냥 {}로는 튜플을 출력할 수 없어요.

.0, .1, .2처럼 써요. 튜플의 위치는 0부터 시작해요.

fn main() {
let person = ("지민", 30, true);
println!("이름: {}", person.0);
println!("나이: {}", person.1);
println!("활성: {}", person.2);
}

실행 결과:

이름: 지민
나이: 30
활성: true
▶ 직접 실행해 보기

위치는 코드에 직접 적은 숫자여야 해요. person.i처럼 변수를 쓸 수는 없어요. 위치마다 타입이 다를 수 있는데, Rust는 컴파일할 때 타입을 알아야 하기 때문이에요.

모든 값을 한 줄에 각각의 변수로 꺼낼 수 있어요. 이걸 구조 분해(destructuring)라고 해요.

fn main() {
let person = ("지민", 30, true);
let (name, age, is_active) = person;
println!("{name}, {age}살, 활성: {is_active}");
}

실행 결과:

지민, 30살, 활성: true
▶ 직접 실행해 보기

.0, .1, .2로 계속 접근하는 것보다 이쪽이 더 읽기 쉬운 경우가 많아요. 특히 튜플의 필드가 두세 개를 넘어가면 더 그래요.

함수는 반환 타입을 하나만 선언할 수 있어서, 값을 여러 개 돌려주고 싶을 때는 튜플을 쓰는 게 자연스러워요.

fn min_max(numbers: &[i32]) -> (i32, i32) {
let min = *numbers.iter().min().unwrap();
let max = *numbers.iter().max().unwrap();
(min, max)
}
fn main() {
let (smallest, largest) = min_max(&[4, 8, 1, 9, 3]);
println!("최솟값: {smallest}, 최댓값: {largest}");
}

실행 결과:

최솟값: 1, 최댓값: 9
▶ 직접 실행해 보기

&[i32], .iter(), .unwrap()은 아직 신경 쓰지 않아도 돼요. 슬라이스, 반복자, Option 페이지에서 다뤄요. 여기서 눈여겨볼 부분은 반환 타입 (i32, i32)와, main이 그 값을 곧바로 두 변수로 구조 분해하는 모습이에요.

튜플을 mut로 선언하면 같은 .0, .1 위치를 이용해서 필드를 하나씩 바꿀 수 있어요. 값은 바꿀 수 있지만 모양은 바꿀 수 없어요. 튜플은 처음 만들었을 때의 필드 개수와 타입을 항상 그대로 유지해요.

구조 분해는 반대 방향으로도 동작해요. 이미 있는 변수에 값을 대입할 수도 있어요. 그러면 임시 변수 없이 한 줄로 두 값을 맞바꿀 수 있어요. 필드 중 일부만 필요하다면, 필요 없는 자리에는 _를 쓰세요.

fn main() {
let mut point = (3, 4);
point.0 += 10; // 필드 하나만 그 자리에서 바꿔요
println!("point = {:?}", point);
let mut a = 1;
let mut b = 2;
(a, b) = (b, a); // 두 값을 맞바꿔요
println!("a = {a}, b = {b}");
let (x, _) = point; // x만 남기고 두 번째 필드는 무시해요
println!("x = {x}");
}

실행 결과:

point = (13, 4)
a = 2, b = 1
x = 13
▶ 직접 실행해 보기

맞바꾸기가 동작하는 이유는 Rust가 오른쪽의 (b, a)를 먼저 만들기 때문이에요. 그다음에야 그 값을 a와 b에 풀어 넣어요. _와 패턴으로 할 수 있는 일은 패턴 매칭 페이지에 훨씬 많이 나와요.

사실 여러분은 모르는 사이에 이미 튜플을 하나 만났어요. ()는 유닛 타입(unit type)이라고 불러요. 함수가 의미 있는 값을 돌려주지 않을 때 반환하는 게 바로 이 타입이에요. fn main()의 반환 타입도 생략되어 있을 뿐 ()예요.

튜플과 구조체, 언제 무엇을 쓸까요

섹션 제목: “튜플과 구조체, 언제 무엇을 쓸까요”

튜플은 이름 없이 빠르게 값을 묶을 때 좋아요. 특히 함수에서 값 두세 개를 돌려줄 때 좋아요. 하지만 값의 묶음이 이름 붙은 필드를 가진 실제 개념을 나타낸다면(예를 들어 코드 곳곳에서 이름으로 부를 이름과 나이를 가진 Person), 보통은 구조체가 더 좋고 읽기 쉬운 선택이에요.

두 숫자의 합과 곱을 함께 돌려주는 함수 sum_and_product(a: i32, b: i32) -> (i32, i32)를 작성하세요. main에서 5와 6으로 호출하고 결과를 구조 분해해 보세요. 힌트: 함수의 마지막 줄은 튜플 자체인 (a + b, a * b)이고, 세미콜론은 없어요.

정답 보기
fn sum_and_product(a: i32, b: i32) -> (i32, i32) {
(a + b, a * b)
}
fn main() {
let (sum, product) = sum_and_product(5, 6);
println!("합 = {sum}, 곱 = {product}");
}

실행 결과:

합 = 11, 곱 = 30
▶ 직접 실행해 보기

2. 두 꼭짓점으로 만든 직사각형

섹션 제목: “2. 두 꼭짓점으로 만든 직사각형”

직사각형은 두 점의 튜플로 저장할 수 있어요. 각 점도 튜플이에요: let rect = ((1, 2), (5, 5));. 이 직사각형의 너비, 높이, 넓이를 구해 보세요. 힌트: 튜플은 중첩할 수 있고, rect.1.0은 “두 번째 점의 첫 번째 필드”라는 뜻이에요.

정답 보기
fn main() {
let rect = ((1, 2), (5, 5));
let width = rect.1.0 - rect.0.0;
let height = rect.1.1 - rect.0.1;
println!("너비 {width}, 높이 {height}, 넓이 {}", width * height);
}

실행 결과:

너비 4, 높이 3, 넓이 12
▶ 직접 실행해 보기

두 점을 한꺼번에 구조 분해할 수도 있어요: let ((x1, y1), (x2, y2)) = rect;. 타이핑할 건 늘어나지만, rect.1.0 - rect.0.0보다 x2 - x1이 읽기 쉬워요. 이렇게 점이 줄줄이 쌓이기 시작하면, 이름 붙은 필드를 가진 구조체가 더 명확하다는 신호예요.

  • 튜플은 타입이 서로 다를 수 있는 값들을 묶어요: (name, age, is_active).
  • 필드는 .0, .1, .2로 접근하거나, 구조 분해로 한꺼번에 꺼내요.
  • 튜플은 함수에서 여러 값을 돌려주는 자연스러운 방법이에요.
  • mut 튜플의 필드는 바꿀 수 있지만, 크기와 타입은 바꿀 수 없어요. (a, b) = (b, a);로 두 값을 맞바꿀 수 있어요.
  • 이름이 붙고 여러 번 다시 쓰는 묶음이라면 구조체를 쓰세요.

이해도 점검

1. 배열은 못 하는데 튜플은 담을 수 있는 것은 무엇일까요?

2. person이라는 튜플의 두 번째 요소에는 어떻게 접근할까요?

3. let mut a = 1; let mut b = 2; (a, b) = (b, a); 다음에 a와 b는 각각 얼마일까요?

점수: 0 / 3