제어문

조건의 상태가 참인지에 따라 어떤 코드의 실행 여부를 결정하거나 조건이 만족되는 동안 반복 수행을 하는 것은 대부분의 프로그래밍 언어의 기초 문법입니다. 우리가 실행 흐름을 제어할 수 있는 가장 보편적인 작성 방식은 if표현식과 반복문 입니다.

if표현식

if표현식은 우리의 코드가 조건에 따라 분기할 수 있게 합니다. 우리가 조건을 제공하는 것은 다음 서술과 같죠. “만약 조건이 충족되면, 이 코드 블럭을 실행하세요. 만약 충족되지 않았다면 코드 블럭을 실행하지 마세요."

branches로 명명된 새 프로젝트를 우리의 projects 디렉토리에 생성하고 if식을 탐구합시다. src/main.rs 파일에 다음의 내용을 기입하세요:

Filename: src/main.rs

fn main() {
    let number = 3;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

모든 if표현식은 if란 키워드로 시작하며 뒤이어 조건이 옵니다. 이번 경우에 조건은 변수 number가 5보다 작은 값을 가지는지 여부가 됩니다. 조건이 참일 때 실행하는 코드 블록은 조건 바로 뒤 중괄호로 된 블록에 배치됩니다. if식의 조건과 관련된 코드 블럭은 우리가 2장의 “비밀번호 추리 게임”에서 다뤘던 match식의 갈래(arms)와 마찬가지로 *갈래(arms)*로 불립니다. 선택적으로, 우리는 이번 경우에서 처럼 else식을 포함시킬 수 있는데, 이는 조건이 거짓으로 산출될 경우 실행시킬 코드 블럭을 프로그램에 제공합니다. 당신이 else식을 제공하지 않는데 조건이 거짓이 되면, 프로그램은 if블록을 생략하고 다음 순서의 코드를 실행하게 될 겁니다.

이 코드를 실행해보세요; 다음과 같은 결과를 얻을 수 있을 겁니다:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
     Running `target/debug/branches`
condition was true

number의 값을 조건을 거짓으로 만들 값으로 변경하면 무슨 일이 일어날지 살펴보도록 합시다:

let number = 7;

프로그램을 다시 실행시키면, 다음과 같은 결과를 보게 됩니다:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
     Running `target/debug/branches`
condition was false

주의해야 할 중요한 점은 이번 코드의 조건은 반드시 bool이어야 합니다. 만약 bool이 아닐 경우 어떤 일이 일어나는지는 다음의 코드를 실행하면 알 수 있을 겁니다:

Filename: src/main.rs

fn main() {
    let number = 3;

    if number {
        println!("number was three");
    }
}

if의 조건이 3으로 산출되고, Rust는 에러를 발생시킵니다.

error[E0308]: mismatched types
 --> src/main.rs:4:8
  |
4 |     if number {
  |        ^^^^^^ expected bool, found integral variable
  |
  = note: expected type `bool`
             found type `{integer}`

이 에러가 나타내는 것은 Rust가 bool을 기대하였으나 정수형이 왔다는 겁니다. Rust는 boolean 타입이 아닌 것을 boolean 타입으로 자동 변환하지 않습니다. Ruby나 Javascript와는 다르죠. 우리는 반드시 명시적으로 booleanif의 조건으로 사용해야 합니다. 만약 우리가 if표현식의 코드 블록을 숫자가 0이 아닐 시에 실행하고 싶다면, 다음처럼, 우리는 if표현식을 변경할 수 있습니다.

Filename: src/main.rs

fn main() {
    let number = 3;

    if number != 0 {
        println!("number was something other than zero");
    }
}

이번 코드를 실행시키면 number was something other than zero가 출력 될 겁니다.

else if와 다수 조건

우리는 ifelse 사이에 else if식을 추가 결합하여 다양한 조건을 다룰 수 있습니다. 예제를 보시죠:

Filename: src/main.rs

fn main() {
    let number = 6;

    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }
}

이번 프로그램은 분기할 수 있는 네 개의 경로를 갖습니다. 이를 수행하면, 다음과 같은 결과를 얻게 될 겁니다:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
     Running `target/debug/branches`
number is divisible by 3

이 프로그램이 실행될 때, if식을 차례대로 검사하고 검사 조건이 참일 때의 첫 번째 본문을 실행합니다. 주목할 점은 6을 2로 나누어 떨어짐에도 불구하고 number is divisible by 2이 출력되지 않는데, else의 블럭에 위치한 number is not divisible by 4, 3, or 2도 마찬가지입니다. 이렇게 되는 이유는 Rust가 첫 번째로 조건이 참이 되는 블록만 찾아 실행하고, 한번 찾게 되면 나머지는 검사하지 않기 때문입니다.

너무 많은 else if식의 사용은 당신의 코드를 이해하기 어렵게 하므로, 둘 이상일 경우 코드를 리팩토링하게 될 수도 있습니다. 이런 경우를 위해 6장에서 match라 불리는 강력한 분기 생성자를 다룹니다.

let구문에서 if 사용하기

if가 표현식이기 때문에, 항목 3-4에서 처럼, 우리는 이를 let 구문의 우측에 사용할 수 있죠.

Filename: src/main.rs

fn main() {
    let condition = true;
    let number = if condition {
        5
    } else {
        6
    };

    println!("The value of number is: {}", number);
}

Listing 3-4: if 표현식의 결과값을 변수에 대입하기

변수 number에는 if식에서 산출된 값이 bound되게 됩니다. 어떤 일이 일어날지 코드를 실행해보죠:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
     Running `target/debug/branches`
The value of number is: 5

기억하세요! 코드 블록은 그들의 마지막에 위치한 표현식을 산출하며 숫자는 그 자체로 표현식이라는 것을요. 이 경우 전체 if식의 값은 실행되는 코드 블럭에 따라 다릅니다. 그렇기에 if식에 속한 각 갈래의 결과는 반드시 같은 타입이여야 합니다. 항목 3-4에서 if갈래와 else갈래는 모두 i32 정수형을 결과 값으로 가집니다. 하지만 만약 다음 예제처럼 유형이 다르면 어떻게 될까요?

Filename: src/main.rs

fn main() {
    let condition = true;

    let number = if condition {
        5
    } else {
        "six"
    };

    println!("The value of number is: {}", number);
}

우리가 이번 코드를 실행시키려고 하면 에러를 얻게 됩니다. ifelse 갈래의 값 타입이 호환되지 않고, Rust는 정확히 프로그램의 어느 지점에 문제가 있는지 보여줍니다.

error[E0308]: if and else have incompatible types
 --> src/main.rs:4:18
  |
4 |       let number = if condition {
  |  __________________^
5 | |         5
6 | |     } else {
7 | |         "six"
8 | |     };
  | |_____^ expected integral variable, found reference
  |
  = note: expected type `{integer}`
             found type `&str`

if 블록이 정수형을 산출하는 식이고 else 블록은 문자열을 산출하는 식 입니다. 이런 경우가 성립하지 않는 이유는 변수가 가질 수 있는 타입이 오직 하나이기 때문입니다. Rust는 컴파일 시에 number 변수의 타입이 뭔지 확실히! 정의해야 합니다. 그래야 number가 사용되는 모든 곳에서 유효한지 검증할 수 있으니까요. Rust는 number의 타입을 실행 시에 정의되도록 할 수 없습니다. 컴파일러가 모든 변수의 다양한 타입을 추적해서 알아내야 한다면 컴파일러는 보다 복잡해지고 보증할 수 있는 것은 적어지게 됩니다.

반복문과 반복

코드 블록을 한 번 이상 수행하는 것은 자주 유용합니다. 반복 작업을 위해서, Rust는 몇 가지 반복문을 제공합니다. 반복문은 반복문 시작부터 끝까지 수행하고 다시 처음부터 수행합니다. 반복문의 실험해보기 위해 loops으로 명명된 새 프로젝트를 작성해 봅시다.

Rust가 제공하는 세 가지 반복문: loop, while, 그리고 for을 모두 사용해 봅시다.

loop와 함께 코드의 반복 수행

loop keyword는 Rust에게 그만두라고 명시하여 알려주기 전까지 코드 블럭을 반복 수행합니다. 예제로, 우리의 loops디렉토리에 src/main.rs를 다음처럼 변경하세요:

Filename: src/main.rs

fn main() {
    loop {
        println!("again!");
    }
}

이 프로그램을 실행시키면, 우리는 프로그램을 강제 정지하기 전까지 again!이 반복 출력되는 것을 보게 됩니다. 대부분의 터미널은 단축키 ctrl-C를 통해서 무한루프에 빠진 프로그램을 정지시키는 기능을 지원합니다. 한번 시도해 보세요:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
     Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!

기호 ^C는 우리가 ctrl-C를 눌렀을 때의 위치입니다. 코드가 정지 신호를 받은 시점에 따라 ^C 이후에 again!이 출력될 수도 아닐 수도 있습니다.

다행스럽게도, Rust는 보다 안정적으로 루프에서 벗어날 수 있는 방법을 제공합니다. 우리는 break keyword 를 위치시켜 프로그램이 언제 루프를 멈춰야 하는지 알려줄 수 있습니다. 상기시켜 드리자면 2장 “추리 게임”에서 사용자가 모든 숫자를 정확히 추리했을 경우 프로그램을 종료시키기 위해 사용했었습니다.

while와 함께하는 조건부 반복

반복문 내에서 조건을 산출하는 것은 자주 유용합니다. 조건이 참인 동안 반복문을 수행합니다. 조건이 참이 아니게 된 경우에 break을 호출하여 반복을 정지시킵니다. 이런 패턴의 반복문을 구현하자면 loop, if, else, 그리고 break를 혼합해야 합니다; 원한다면 이렇게 사용해도 됩니다.

하지만, 이런 패턴은 매우 보편적이기 때문에 이와 동일한 구조자가 Rust에는 내장되어 있으며, 이를 while 반복문이라 부릅니다. 다음의 예제를 통해 while을 사용해 봅시다: 프로그램은 세 번 반복되고, 반복 때마다 카운트 다운됩니다. 마침내 반복이 끝나면 다른 메시지를 출력하고 종료됩니다:

Filename: src/main.rs

fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{}!", number);

        number = number - 1;
    }

    println!("LIFTOFF!!!");
}

이 구조자는 loop, if, else 및 break를 사용하는 경우 필요한 많은 중첩을 제거하며, 더 깔끔합니다. 조건이 true인 동안 코드가 실행되고; 그렇지 않으면 루프에서 벗어납니다.

for와 함께하는 콜렉션 반복하기

우리는 while 구조자를 통해 배열과 같은, 콜렉션의 각 요소에 걸쳐 반복 수행 할 수 있습니다. 예를 들어서, Listing 3-5을 살펴봅시다:

Filename: src/main.rs

fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

    while index < 5 {
        println!("the value is: {}", a[index]);

        index = index + 1;
    }
}

Listing 3-5: while 반복문을 사용해 콜렉션의 각 요소들을 순회하기

여기서, 코드는 배열의 요소에 걸쳐 카운트를 증가시킵니다. 이 색인은 0에서 시작하고, 배열의 마지막 순서까지 반복됩니다 (즉, index < 5가 참이 아닐 때까지). 이 코드를 수행하면 배열의 모든 요소가 출력되게 됩니다.

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished dev [unoptimized + debuginfo] target(s) in 0.32 secs
     Running `target/debug/loops`
the value is: 10
the value is: 20
the value is: 30
the value is: 40
the value is: 50

예상했던 대로, 5개인 배열 모든 값이 터미널에 표시됩니다. index 값이 5에 오는 시점에, 그러니까 배열의 6번째 값에 접근하기 전에 반복은 중지되어야 합니다.

그러나 이런 방식은 에러가 발생하기 쉽습니다; 우리가 정확한 길이의 색인을 사용하지 못하면 프로그램은 패닉을 발생합니다. 또한 느린데, 이유는 컴파일러가 실행 간에 반복문을 통해 반복될 때마다 요소에 대한 조건 검사를 수행하는 런타임 코드를 추가하기 때문입니다.

보다 효율적인 대안으로, 우리는 for 반복문을 사용하여 콜렉션의 각 요소에 대한 코드를 수행할 수 있습니다. for 반복문은 다음 Listing 3-6과 같습니다:

Filename: src/main.rs

fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a.iter() {
        println!("the value is: {}", element);
    }
}

Listing 3-6: for 반복문을 사용해 콜렉션의 각 요소를 순회하기

우리가 이 코드를 수행하면, 항목 3-5와 같은 결과를 볼 수 있습니다. 더 중요한 것은, 우리는 이제 코드의 안전성을 높이고 배열의 끝을 넘어가거나 충분한 길이를 지정하지 못해 일부 아이템이 누락되어 발생할 수있는 버그의 가능성을 제거했습니다.

예를 들어, 코드 3-5의 코드에서 a 배열 에서 항목을 제거 했지만 조건을 while index < 4로 업데이트하지 않으면 코드는 패닉을 발생합니다. for루프를 사용하면, 당신이 배열의 수를 변경 한 경우에도 다른 코드를 변경해야 할 필요가 없습니다. (역주 : 당신은 살면서 변경한 배열의 수를 기억하고 있는가?)

for반복문이 안전하고 간결하기 때문에 이들은 가장 보편적으로 사용되는 반복문 구조자입니다. 항목 3-5에서처럼 while반복문을 사용하여 특정 횟수만큼 코드를 반복하려는 경우에도, 대부분의 Rust 사용자들은 for반복문 을 사용하고자 할 것 입니다. 이런 사용을 위해 Rust에서 기본 라이브러리로 제공하는 Range를 사용하게 됩니다. Range는 한 숫자에서 다른 숫자 전까지 모든 숫자를 차례로 생성합니다.

여기 for반복문과 아직 설명하지 않은 range를 역순하는 rev메소드를 사용하는 카운트다운 프로그램이 있습니다:

Filename: src/main.rs

fn main() {
    for number in (1..4).rev() {
        println!("{}!", number);
    }
    println!("LIFTOFF!!!");
}

꽤 괜찮은 코드인것 같죠?

결론

해냈어요! 무지 긴 장이었어: 우리는 변수, 스칼라, if식과 반복문까지 배웠어요! 혹시 이번 장에서 나온 내용을 연습해보고 싶으면 다음을 수행하는 프로그램을 만들어 보세요.

  • 화씨와 섭씨를 상호 변환.
  • n번째 피보나치 수열 생성.
  • 크리스마스 캐롤 “The Twelve Days of Christmas”의 가사를 반복문을 활용해 출력.

다음으로 넘어갈 준비가 되셨습니까? 우리는 이제 일반적인 다른 언어에는 존재하지 않는 개념에 대해서 다루고자 합니다 : 소유권.