본문 바로가기

러스트의 제어 구조

기술적인 이야기/기타 개발 2020. 8. 5.
반응형

이번 편은 러스트(Rust)의 제어 구조 혹은 흐름 제어(Control Flow)에 관해서 살펴보는 글입니다. 매우 기초적인 내용이고 앞선 글들에서 일부 언급된 내용도 있어서 신선하지 않을 수도 있는 내용입니다.

제어 구조라는 개념은 소프트웨어에서 굉장히 중요합니다. 예를 들어 게임을 하는데 키를 눌러도 정해진 대로만 캐릭터가 움직인다면 무슨 의미가 있을까요? 이런 식으로 직렬 흐름에 변화를 가하는 것이 바로 이 제어 구조 혹은 흐름 제어입니다. 조건에 따라 흐름을 다르게 하면서 필요한 일을 할 수 있게 제어하는 것이지요.

논리식

본론으로 가기 전에 러스트의 논리식도 간단히 살펴봅시다. 다른 언어를 안다면 그다지 언급하지 않아도 될 정도로 러스트의 논리식도 평이(?)한 편입니다.

let cond = !(a > 0 || (b <= 10 && c != 2 && d == 5 && e));

논리식의 값은 이전 타입 편에서 언급한 bool 타입이며 따라서 값도 당연하게도 truefalse 두 가지를 가집니다. 수치 비교와 동등 비교 오퍼레이터도 동일하고 논리 값을 뒤집는 ! 오퍼레이터도 동일합니다. 논리 AND인 &&과 논리 OR인 || 역시 동일하네요. 굳이 설명하는 게 손만 아플 것 같으니 이 정도로 넘어갈게요.

 

if-else

if 그리고 else로 구성되는 제어 구조는 가장 기본적이라 볼 수 있습니다. 물론 러스트뿐만 아니라 다른 모든 언어에서도 말이지요. 단순한 논리식의 값을 이용해 이 식이 참(true)이라면 특정 구문 블록을 실행하고 거짓(false)이라면 거짓에 해당하는 블록을 실행합니다.

if condition1 {
    // condition1 == true
} else if condition2 {
    // condition2 == true
} else {
    // condition1 == false && condition2 == false
}

굳이 상세한 예가 필요할 것 같지 않아서 if-else 구문의 예제는 생략하고 위처럼 간략하게 정리했습니다. 당연하게도 condition에 해당하는 식이 true이면 해당 블록이 실행되고 아니라면 else로 넘어오게 되는 방식으로 흐름을 변경합니다. 다만 특이한 점이 있다면 if에서 검증하는 논리식에 괄호를 씌울 필요가 없다는 점이 있겠네요.

그 외에 러스트의 특수한 if 문법으로 let-if 구문이 있습니다.

let v = 10;
let r = if v % 2 == 0 { "even" } else { "odd" };

위의 코드에서 r"even"을 가지게 됩니다. if문의 조건에 따라 대입되는 값이 달라지는 것인데, 마치 C 언어의 ?와 비슷한 역할을 좀 더 (영어 문화권에) 친숙하게 표현한 것 같습니다. 만약 파이썬을 안다면 친숙한 표현일 수도 있습니다.

그리고 또 다른 특수한 if 문법으로 if-let이 있습니다. 위와 순서가 반대라는 점에 주의합시다.

if let Some(v) = some_option_variable {
    println!("{}", v);
}

러스트의 기본 타입 편에서 옵션(Option)에 대한 이야기를 할 때 거론했던 바로 그 문법입니다. 열거형(enum)의 각 항목이 값을 가질 때 해당 항목의 일치 여부와 값을 동시에 가져올 수 있는 멋진 구문입니다. 다만 스위프트(Swift)에도 비슷한 문법이 있는데 순서가 좀 달라서 헷갈리기도 하네요.

 

match

match는 이름만 봐서는 생소할 수도 있는데, 다른 언어의 switch-case 구문을 알고 있다면 아마도 친숙하게 느낄 수 있는 문법일 것 같습니다. 대충 아래의 예제를 봅시다.

let v = 10;
match v {
    10 => println!("ten"),
    100 => println!("hundred"),
    _ => println!("unknown")
}

위 코드는 v의 값이 10인지 아니면 100인지 혹은 그 외의 경우인지에 따라 3가지 분기로 동작하는 코드입니다. switch-case문과 비교하자면 그저 switch 대신 match가 쓰였고, case라는 명령이 없다는 점과, 콜론 대신 화살표 =>로 표기한 부분이 다르다는 점을 빼면 개념적으로 동일합니다. 마지막의 _는 default와도 동일한 개념이지요. 단순화된 면에서 가독성이 좋게 느껴집니다.

러스트의 match 구문은 단순히 switch-case와 비교하기엔 다른 특징이 있는데, 구문 자체가 값을 대체하는 용도로 사용될 수도 있습니다.

let v = 10;
let unit = match v {
    10 => "ten",
    100 => "hundred",
    _ => "unknown"
};
println!("{}", unit);

match의 결과를 unit이라는 변수에 받아서 그대로 활용하는 예제입니다. 역시 C 언어의 ?를 대체할 수 있는 유용한 기능일 거라 생각됩니다.

그 밖에 match는 여러 가지 케이스의 패턴을 표현하는 방법이 있습니다.

let unit = match v {
    0 | 1 => "binary",     // A
    2..=9 => "under 10",   // B
    10 => "ten",
    100 => "hundred",
    _ => "unknown"
};

위에서 주석으로 A로 표기한 부분은 0 혹은 1일 경우라는 표현이고, B라고 표기한 부분은 볌위(range)로 케이스를 표현하는 방식입니다.

다만 match에서 범위 표현을 할 때 중요한 점이 있는데, ..를 사용할 수 없습니다. 만약 위의 코드에서 B의 케이스를 2..9로 변경할 경우 아래와 같은 컴파일 오류를 볼 수 있습니다.

exclusive range pattern syntax is experimental.

2..9의 경우 읽는 사람에 따라서 2부터 9까지인지 혹은 2부터 8까지인지 오해할 수도 있는 표현일지도 모릅니다. 그래서 명확하게 표현할 수 있는 ..= 오퍼레이터로 범위를 표현하라는 의미 같습니다.

 

루프(Loops)

앞서 살펴본 if-else나 match의 경우는 조건에 따라 분기를 만드는 제어 구문입니다. 이와는 다르게 루프(Loop)는 분기가 아닌 반복을 만드는 제어 구문입니다.

가장 처음에 볼 것은 이름이 딱 맞아떨어지는 loop입니다.

loop

loop는 무한 반복 루프를 만드는 제어 구문입니다.

loop {
    // ...
}

loop 블록 안의 코드는 영원히 반복되기 때문에 의도(?)한 것이 아닌 이상 break로 탈출하는 코드를 작성해 두지 않으면 시스템에 매우 해로운 코드가 될 수 있으니 주의합시다.

while

while은 조건이 맞을 경우에만 블록의 코드를 루프 시키는 제어 구문입니다.

while condition {
    // ...
}

당연하게도 조건이 맞지 않으면 루프가 끝나고 다음 문장으로 넘어갑니다. 여기서 조건을 true로 고정시켜 버리면 loop와 동일해지겠네요.

for

for 루프는 범위 내에서 동작하는 루프로 아마도 모든 루프 구문 중 가장 많이 쓰이는 문법이 아닐까 생각됩니다. 아래 코드는 10회 반복되는 for 루프입니다.

for i in 0..10 {
    // ...
}

범위 문법을 이용해 10회의 루프를 돌며, 각 루프에서 i의 값은 0에서 9까지 각각 들어오게 됩니다.

만약 이런 범위를 이용해 for 루프를 구현하는 경우 별도의 순서 인덱스가 필요할 때도 있습니다. 이럴 때는 범위의 enumerate() 메서드를 활용할 수 있습니다.

for (i, v) in (10..15).enumerate() {
    println!("#{} {}", i, v);
}

이렇게 할 경우 i에는 인덱스가, v에는 값이 들어오게 됩니다. 즉 아래와 같이 콘솔에 표시됩니다.

#0 10
#1 11
#2 12
#3 13
#4 14

아무래도 이 구문도 자주 쓰일 것 같습니다.

루프의 제어와 탈출

루프를 제어하는 것은 두 가지 개념이 있습니다. 러스트도 다른 언어들과 흡사한 두 가지 기능이 제공됩니다.

하나는 루프의 현재 진행을 생략하고 다음 루프로 진행시키기 위한 continue 명령이 있습니다.

for i in 0..10 {
    if i % 2 == 0 {
        continue;
    }
    println!("{}", i);
}

위 코드는 i의 값이 홀수일 경우만 콘솔에 표시하고 짝수인 경우는 continue를 타게 되어서 콘솔에 표시하는 부분이 생략됩니다.

다음으로 루프에서 탈출시켜주는 명령인 break가 있습니다.

loop {
    // ...
    if condition {
        break;
    }
}

break를 이용하면 루프에서 강제로 빠져나오게 만들 수 있습니다. 만약 loop를 사용하게 된다면 break를 빼먹지 않도록 조심합시다.

 

맺음말

앞서 적은 글의 내용이 너무 길었던 탓일까요? 제어 구조 글을 쓰고 있는데 이렇게 내용이 짧아도 되나 하는 생각이 문득 들었습니다. '아 그건 내 마음이지 뭐'라는 생각이 바로 들기는 했지만, 어쨌든 그만큼 러스트도 다른 언어들과 제어 구조와 관련해서 다를 게 하나도 없다고도 볼 수 있겠네요. 그만큼 흔한 내용이기도 하지만, 첫 언어가 러스트이신 분이라면 도움이 될지도 모르겠네요.

다음 글은 러스트의 함수와 클로저에 관한 글로 이어집니다.

728x90
반응형

댓글