소개
The Match Control Flow Construct에 오신 것을 환영합니다. 이 랩은 Rust Book의 일부입니다. LabEx 에서 Rust 기술을 연습할 수 있습니다.
이 랩에서는 Rust 의 강력한 match 제어 흐름 구문을 살펴봅니다. 이 구문을 사용하면 패턴 매칭을 수행하고 일치하는 패턴에 따라 코드를 실행할 수 있습니다.
The match 제어 흐름 구문
Rust 는 match라는 매우 강력한 제어 흐름 구문을 가지고 있습니다. 이 구문을 사용하면 값을 일련의 패턴과 비교한 다음 일치하는 패턴에 따라 코드를 실행할 수 있습니다. 패턴은 리터럴 값, 변수 이름, 와일드카드 등 다양한 요소로 구성될 수 있습니다. 18 장에서는 다양한 종류의 패턴과 그 기능에 대해 다룹니다. match의 강력함은 패턴의 표현력과 컴파일러가 모든 가능한 경우를 처리하는지 확인한다는 사실에서 비롯됩니다.
match 표현식을 동전 분류 기계와 같다고 생각해보세요. 동전은 다양한 크기의 구멍이 있는 트랙을 따라 미끄러져 내려가고, 각 동전은 들어갈 수 있는 첫 번째 구멍으로 떨어집니다. 마찬가지로, 값은 match의 각 패턴을 거치며, 값과 "맞는" 첫 번째 패턴에서 값은 실행 중에 사용될 관련 코드 블록으로 떨어집니다.
동전 이야기를 해보죠. match를 사용하여 예를 들어보겠습니다! 알 수 없는 미국 동전을 받아 동전 분류 기계와 유사한 방식으로 어떤 동전인지 결정하고 센트 단위의 값을 반환하는 함수를 작성할 수 있습니다. 이는 Listing 6-3 에 나와 있습니다.
1 enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
2 match coin {
3 Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
Listing 6-3: enum 과 enum 의 변형을 패턴으로 사용하는 match 표현식
value_in_cents 함수에서 match를 분석해 보겠습니다. 먼저 match 키워드와 그 뒤에 표현식을 나열합니다. 이 경우 값은 coin [2]입니다. 이는 if와 함께 사용되는 표현식과 매우 유사하지만 큰 차이점이 있습니다. if의 경우 표현식은 부울 값을 반환해야 하지만 여기서는 모든 유형을 반환할 수 있습니다. 이 예에서 coin의 유형은 [1]에서 정의한 Coin enum 입니다.
다음은 match arm 입니다. arm 은 패턴과 일부 코드로 구성됩니다. 여기 첫 번째 arm 은 Coin::Penny 값인 패턴과 패턴과 실행할 코드를 구분하는 => 연산자 [3]를 가지고 있습니다. 이 경우 코드는 값 1입니다. 각 arm 은 쉼표로 다음 arm 과 구분됩니다.
match 표현식이 실행되면 결과 값을 각 arm 의 패턴과 순서대로 비교합니다. 패턴이 값과 일치하면 해당 패턴과 관련된 코드가 실행됩니다. 해당 패턴이 값과 일치하지 않으면 실행은 다음 arm 으로 계속 진행됩니다. 이는 동전 분류 기계와 매우 유사합니다. 필요한 만큼 많은 arm 을 가질 수 있습니다. Listing 6-3 에서 match는 4 개의 arm 을 가지고 있습니다.
각 arm 과 관련된 코드는 표현식이며, 일치하는 arm 의 표현식의 결과 값은 전체 match 표현식에 대해 반환되는 값입니다.
Listing 6-3 과 같이 각 arm 이 값을 반환하는 경우처럼 match arm 코드가 짧으면 일반적으로 중괄호를 사용하지 않습니다. match arm 에서 여러 줄의 코드를 실행하려면 중괄호를 사용해야 하며, arm 뒤의 쉼표는 선택 사항입니다. 예를 들어, 다음 코드는 Coin::Penny로 메서드가 호출될 때마다 "Lucky penny!"를 출력하지만 블록의 마지막 값인 1을 반환합니다.
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
값에 바인딩되는 패턴
match arm 의 또 다른 유용한 기능은 패턴과 일치하는 값의 부분에 바인딩될 수 있다는 것입니다. 이것이 enum 변형에서 값을 추출하는 방법입니다.
예를 들어, enum 변형 중 하나를 변경하여 내부에 데이터를 저장해 보겠습니다. 1999 년부터 2008 년까지 미국은 한 면에 50 개 주 각각에 대해 다른 디자인을 가진 쿼터를 주조했습니다. 다른 동전은 주 디자인을 갖지 않았으므로 쿼터만 이 추가 값을 갖습니다. Listing 6-4 에서와 같이 Quarter 변형을 변경하여 내부에 저장된 UsState 값을 포함하도록 하여 이 정보를 enum에 추가할 수 있습니다.
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
--snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
Listing 6-4: Quarter 변형이 UsState 값도 포함하는 Coin enum
친구가 50 개 주 쿼터를 모두 수집하려고 한다고 가정해 보겠습니다. 동전 종류별로 느슨한 변화를 정렬하는 동안 각 쿼터와 관련된 주의 이름을 외쳐서 친구가 가지고 있지 않은 경우 컬렉션에 추가할 수 있도록 합니다.
이 코드의 match 표현식에서 Coin::Quarter 변형의 값과 일치하는 패턴에 state라는 변수를 추가합니다. Coin::Quarter가 일치하면 state 변수는 해당 쿼터의 주 값에 바인딩됩니다. 그런 다음 다음과 같이 해당 arm 의 코드에서 state를 사용할 수 있습니다.
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}!", state);
25
}
}
}
value_in_cents(Coin::Quarter(UsState::Alaska))를 호출하면 coin은 Coin::Quarter(UsState::Alaska)가 됩니다. 해당 값을 각 match arm 과 비교할 때 Coin::Quarter(state)에 도달할 때까지 일치하는 항목이 없습니다. 그 시점에서 state에 대한 바인딩은 값 UsState::Alaska가 됩니다. 그런 다음 println! 표현식에서 해당 바인딩을 사용하여 Quarter에 대한 Coin enum 변형에서 내부 주 값을 얻을 수 있습니다.
Option<T>을 사용한 매칭
이전 섹션에서 Option<T>를 사용할 때 Some 케이스에서 내부 T 값을 얻고 싶었습니다. Coin enum 에서 했던 것처럼 match를 사용하여 Option<T>를 처리할 수도 있습니다! 동전을 비교하는 대신 Option<T>의 변형을 비교하지만 match 표현식이 작동하는 방식은 동일하게 유지됩니다.
Option<i32>를 받아 내부에 값이 있으면 해당 값에 1 을 더하는 함수를 작성한다고 가정해 보겠습니다. 내부에 값이 없으면 함수는 None 값을 반환하고 어떠한 연산도 시도하지 않아야 합니다.
이 함수는 match 덕분에 작성하기 매우 쉬우며 Listing 6-5 와 같습니다.
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
1 None => None,
2 Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five); 3
let none = plus_one(None); 4
Listing 6-5: Option<i32>에 match 표현식을 사용하는 함수
plus_one의 첫 번째 실행을 자세히 살펴보겠습니다. plus_one(five) [3]를 호출하면 plus_one 본문의 변수 x는 값 Some(5)를 갖습니다. 그런 다음 각 match arm 과 비교합니다.
None => None,
Some(5) 값은 패턴 None [1]과 일치하지 않으므로 다음 arm 으로 계속 진행합니다.
Some(i) => Some(i + 1),
Some(5)가 Some(i) [2]와 일치합니까? 네, 그렇습니다! 동일한 변형이 있습니다. i는 Some에 포함된 값에 바인딩되므로 i는 값 5를 갖습니다. 그런 다음 match arm 의 코드가 실행되므로 i의 값에 1 을 더하고 총 6이 내부에 있는 새로운 Some 값을 생성합니다.
이제 Listing 6-5 에서 x가 None [4]인 plus_one의 두 번째 호출을 고려해 보겠습니다. match에 들어가 첫 번째 arm [1]과 비교합니다.
일치합니다! 더할 값이 없으므로 프로그램이 중지되고 =>의 오른쪽에 있는 None 값을 반환합니다. 첫 번째 arm 이 일치했으므로 다른 arm 은 비교되지 않습니다.
match와 enum 을 결합하는 것은 많은 상황에서 유용합니다. Rust 코드에서 이 패턴을 많이 보게 될 것입니다. enum 에 대해 match를 수행하고, 변수를 내부에 있는 데이터에 바인딩한 다음, 이를 기반으로 코드를 실행합니다. 처음에는 약간 까다롭지만 익숙해지면 모든 언어에서 이 기능을 사용하고 싶을 것입니다. 이는 일관되게 사용자가 가장 좋아하는 기능입니다.
Match 는 완전해야 함
match에 대해 논의해야 할 또 다른 측면이 있습니다. arm 의 패턴은 모든 가능성을 다루어야 합니다. 버그가 있고 컴파일되지 않는 plus_one 함수의 이 버전을 고려해 보십시오.
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}
None 케이스를 처리하지 않았으므로 이 코드는 버그를 유발합니다. 다행히 Rust 가 어떻게 잡아야 하는지 아는 버그입니다. 이 코드를 컴파일하려고 하면 다음과 같은 오류가 발생합니다.
error[E0004]: non-exhaustive patterns: `None` not covered
--> src/main.rs:3:15
|
3 | match x {
| ^ pattern `None` not covered
|
note: `Option<i32>` defined here
= note: the matched value is of type `Option<i32>`
help: ensure that all possible cases are being handled by adding
a match arm with a wildcard pattern or an explicit pattern as
shown
|
4 ~ Some(i) => Some(i + 1),
5 ~ None => todo!(),
|
Rust 는 모든 가능한 경우를 다루지 않았다는 것을 알고 있으며, 심지어 어떤 패턴을 잊었는지도 알고 있습니다! Rust 의 Match 는 *exhaustive(완전)*합니다. 코드가 유효하려면 모든 마지막 가능성을 소진해야 합니다. 특히 Option<T>의 경우 Rust 가 None 케이스를 명시적으로 처리하는 것을 잊지 못하게 할 때, null 이 있을 수 있는데 값을 가지고 있다고 가정하는 것을 방지하여 앞서 논의한 10 억 달러의 실수를 불가능하게 만듭니다.
Catch-all 패턴과 _ 플레이스홀더
enum 을 사용하면 몇 가지 특정 값에 대해 특별한 작업을 수행할 수 있지만, 다른 모든 값에 대해 하나의 기본 작업을 수행할 수도 있습니다. 주사위를 굴렸을 때 3 이 나오면 플레이어가 움직이지 않고 대신 멋진 새 모자를 얻는 게임을 구현한다고 상상해 보십시오. 7 이 나오면 플레이어는 멋진 모자를 잃습니다. 다른 모든 값에 대해 플레이어는 게임 보드에서 해당 숫자만큼 칸을 이동합니다. 다음은 주사위 굴림의 결과를 하드코딩하고, 실제로 구현하는 것은 이 예제의 범위를 벗어나므로 본문이 없는 함수로 모든 다른 로직을 나타내는, 해당 로직을 구현하는 match입니다.
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
1 other => move_player(other),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}
처음 두 arm 의 패턴은 리터럴 값 3과 7입니다. 다른 모든 가능한 값을 다루는 마지막 arm 의 경우 패턴은 other [1]로 명명하기로 선택한 변수입니다. other arm 에 대해 실행되는 코드는 변수를 move_player 함수에 전달하여 사용합니다.
이 코드는 u8이 가질 수 있는 모든 가능한 값을 나열하지 않았음에도 불구하고 컴파일됩니다. 마지막 패턴이 특별히 나열되지 않은 모든 값과 일치하기 때문입니다. 이 catch-all 패턴은 match가 완전해야 한다는 요구 사항을 충족합니다. 패턴이 순서대로 평가되므로 catch-all arm 을 마지막에 넣어야 합니다. catch-all arm 을 먼저 넣으면 다른 arm 은 실행되지 않으므로 catch-all 뒤에 arm 을 추가하면 Rust 가 경고합니다!
Rust 에는 catch-all 을 원하지만 catch-all 패턴에서 값을 사용하고 싶지 않을 때 사용할 수 있는 패턴도 있습니다. _는 모든 값과 일치하고 해당 값에 바인딩되지 않는 특수 패턴입니다. 이는 Rust 에게 값을 사용하지 않을 것이라고 알려주므로 Rust 는 사용하지 않는 변수에 대해 경고하지 않습니다.
게임 규칙을 변경해 보겠습니다. 이제 3 또는 7 이외의 숫자를 굴리면 다시 굴려야 합니다. 더 이상 catch-all 값을 사용할 필요가 없으므로 other라는 변수 대신 _를 사용하도록 코드를 변경할 수 있습니다.
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}
이 예제는 마지막 arm 에서 다른 모든 값을 명시적으로 무시하고 있으므로 완전성 요구 사항도 충족합니다. 아무것도 잊지 않았습니다.
마지막으로, 게임 규칙을 한 번 더 변경하여 3 또는 7 이외의 숫자를 굴리면 턴에 다른 일이 일어나지 않도록 하겠습니다. "튜플 타입"에서 언급한 유닛 값 (빈 튜플 타입) 을 _ arm 과 함께 제공되는 코드로 사용하여 이를 표현할 수 있습니다.
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => (),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
여기서 우리는 Rust 에게 이전 arm 의 패턴과 일치하지 않는 다른 값을 사용하지 않을 것이며, 이 경우 코드를 실행하고 싶지 않다고 명시적으로 말하고 있습니다.
패턴과 매칭에 대한 자세한 내용은 18 장에서 다룰 것입니다. 지금은 match 표현식이 약간 장황한 상황에서 유용할 수 있는 if let 구문으로 넘어가겠습니다.
요약
축하합니다! Match 제어 흐름 구성 (The Match Control Flow Construct) 랩을 완료했습니다. LabEx 에서 더 많은 랩을 연습하여 실력을 향상시킬 수 있습니다.