3. 프로그래밍 언어의 보편적인 컨셉

Devjacob·2023년 3월 12일
0

Rust 공식문서

목록 보기
1/2

원문:https://doc.rust-lang.org/book/ch03-00-common-programming-concepts.html

이 장에서는 거의 모든 프로그래밍 언어에 등장하는 개념과 Rust에서 어떻게 작동하는지를 다룬다.
많은 프로그래밍 언어의 핵심에는 많은 공통점이 있다.
이 장에서 소개하는 개념 중에는 Rust만의 고유한 개념은 없지만,
Rust의 맥락에서 논의하고 이러한 개념 사용과 관련된 규칙을 설명하겠다.

특히 변수, 기본 types, 함수, 주석 및 제어 흐름에 대해 알 수 있게 된다.
이러한 기초는 모든 Rust 프로그램에서 사용되며, 배우면 강력한 기초를 다질 수 있다.

  • 러스트는 다른 프로그래밍 언어들과 거의 유사한 예약어를 가지고 있다.
    예약어들은 변수 및 함수로 선언될 수 없다.

몇몇개는 아직 함수로서 기능이 없는 경우가 있지만 예약어로 지정된 경우가 있는데,
그 이유는 향후에 러스트에서 기능을 후가할 예정이기 때문이다.
예약어 리스트는 Appendix A에서 확인할 수 있다.

3.1 변수 와 가변성

"변수에 값 저장하기" 섹션에서 언급했듯이 기본적으로 변수는 불변이다.
이는 Rust가 제공하는 안전성과 손쉬운 동시성을 활용하는 방식으로 코드를 작성할 수 있도록 Rust가 제공하는 많은 장치 중 하나이다.

하지만 변수를 가변적으로 만들 수 있는 옵션도 있다.
Rust가 어떻게 그리고 왜 불변성을 선호하도록 권장하는지,
그리고 때때로 불변성을 선택하지 않는 것이 좋은 이유를 살펴보자.

변수가 불변인 경우, 값이 이름에 바인딩되면 해당 값을 변경할 수 없다.
이를 설명하기 위해 프로젝트 디렉터리에 cargo new variables 명령어를 사용하여 새 프로젝트를 생성하자.

그런 다음 new variables 디렉터리에서 src/main.rs를 열고 해당 코드를 아직 컴파일되지 않은 다음 코드로 바꿔보자:

fn main() {
    let x = 5;
    println!("The value of x is: {x}");
    x = 6;
    println!("The value of x is: {x}");
}

저장하고 cargo run을 사용하여 프로그램을 동작 시켜보자.

이 출력문과 같이 불변성 오류에 대한 오류 메시지가 보일것이다:

$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0384]: cannot assign twice to immutable variable x
--> src/main.rs:4:5
|
2 | let x = 5;
| -
| |
| first assignment to x
| help: consider making this binding mutable: mut x
3 | println!("The value of x is: {x}");
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
For more information about this error, try rustc --explain E0384.
error: could not compile variables due to previous error

이 예제는 컴파일러가 프로그램에서 오류를 찾는 데 어떻게 도움이 되는지 보여준다.

컴파일러 오류는 실망스러울 수 있지만,
실제로는 프로그램이 원하는 작업을 아직 안전하게 수행하지 못한다는 의미일 뿐,
여러분이 훌륭한 프로그래머가 아니라는 것을 의미하지는 않는다.
숙련된 러스트 사용자도 여전히 컴파일러 오류를 만난다.

불변 x 변수에 값을 두번 할당하려고 했기 때문에,
불변 변수 x에 두 번 할당할 수 없다는 오류 메시지가 표시되었다.

불변으로 지정된 값을 변경하려고 할 때,
컴파일 타임 오류가 발생하는 것은 바로 이러한 상황이 버그로 이어질 수 있기 때문이다.
코드의 한 부분이 값이 절대 변경되지 않는다는 가정 하에 작동하는데,

다른 부분이 해당 값을 변경하면 코드의 첫 번째 부분이 설계된 대로 작동하지 않을 수 있다.
특히 두 번째 코드가 값을 가끔씩만 변경하는 경우에는 이런 종류의 버그의 원인을 사후에 추적하기 어려울 수 있다.

Rust 컴파일러는 값이 변경되지 않는다고 명시하면 실제로 변경되지 않음을 보장하므로,
사용자가 직접 추적할 필요가 없습니다.
따라서 코드를 더 쉽게 추론할 수 있습니다.

하지만 가변성은 매우 유용할 수 있으며 코드를 더 편리하게 작성할 수 있다.
변수는 기본적으로 불변이지만, 2장에서와 같이 변수 이름 앞에 mut을 추가하여 변수를 변경 가능하게 만들 수 있다.

mut를 추가하면 코드의 다른 부분이 이 변수의 값을 변경할 것임을 표시하여 향후 코드를 읽는 사람에게 의도를 전달할 수도 있다.

예를 들어 src/main.rs를 다음과 같이 변경해 보겠다:

fn main() {
    let mut x = 5;
    println!("The value of x is: {x}");
    x = 6;
    println!("The value of x is: {x}");
}

프로그램을 동작 시키면 이런 결과를 받는다:

$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished dev [unoptimized + debuginfo] target(s) in 0.30s
Running target/debug/variables
The value of x is: 5
The value of x is: 6

mut을 사용하면 x에 바인딩된 값을 5에서 6으로 변경할 수 있다.
궁극적으로 가변성을 사용할지 여부를 결정하는 것은 사용자에게 달려 있으며,
특정 상황에서 가장 명확하다고 생각되는 것이 무엇인지에 따라 달라진다.

상수

불변 변수와 마찬가지로 상수는 이름에 바인딩되어 변경이 허용되지 않는 값이지만,
상수와 변수에는 몇 가지 차이점이 있다.

첫째, 상수와 함께 mut를 사용할 수 없다.
상수는 기본적으로 불변일 뿐만 아니라 항상 불변이다.

let 키워드 대신 const 키워드를 사용하여 상수를 선언하고 값의 유형을 명시해 주어야 한다.
타입과 타입 지정은 다음 섹션인 '데이터 타입'에서 다룰 예정이므로 자세한 설명은 생략하겠다.
항상 유형을 명시해주어야 한다는 점만 기억하자.

상수 선언에 대한 예시를 보자:

const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

상수의 이름은 "Three_HOURS_IN_SECONDS" 이고,
이것의 값은 60(1분의 초 수)에 60(1시간의 분 수)을 곱한 값에 3(이 프로그램에서 계산하려는 시간 수)을 곱한 결과로 설정된다.

Rust의 상수 작명 규칙은 단어 사이에 밑줄과 함께 모두 대문자를 사용하는 것이다.
컴파일러는 컴파일 시 제한된 연산 집합을 평가할 수 있으므로 이 상수를 10,800으로 설정하는 대신 이 값을 이해하고 검증하기 쉬운 방식으로 작성하도록 선택할 수 있다.
상수를 선언할 때 사용할 수 있는 연산에 대한 자세한 내용은 Rust 레퍼런스의 상수 평가 섹션을 참조하자.

상수는 선언된 범위 내에서 프로그램이 실행되는 전체 시간 동안 유효하다.
이 속성을 사용하면 상수는 게임의 플레이어가 획득할 수 있는 최대 점수나 빛의 속도와 같이 프로그램의 여러 부분에서 알아야 할 수 있는 애플리케이션 도메인의 값에 유용하게 사용할 수 있다.

프로그램 전체에서 사용되는 하드코딩된 값을 상수로 작명하면 향후 코드 유지 관리자에게 해당 값의 의미를 전달할 때 유용하다.
또한 나중에 하드코딩된 값을 업데이트해야 하는 경우 코드에서 변경해야 할 위치가 한 곳만 있으면 도움이 된다.

새도잉

2장의 guessing game 튜토리얼에서 보았듯이, 이전 변수와 같은 이름으로 새 변수를 선언할 수 있다.
첫 번째 변수는 두 번째 변수에 의해 가려진다고 하는데, 이는 사용자가 변수 이름을 사용할 때 컴파일러가 두 번째 변수를 보게 된다는 것을 의미한다.

사실상 두 번째 변수는 첫 번째 변수를 오버셰도하여,
변수 이름이 섀도 처리되거나 범위가 종료될 때까지 변수 이름을 사용하는 모든 것을 자신에게 가져갑니다.
다음과 같이 동일한 변수 이름을 사용하고 let 키워드를 반복해서 사용하면 변수를 섀도잉할 수 있다:
파일명:src/main.rs

fn main() {
    let x = 5;

    let x = x + 1;

    {
        let x = x * 2;
        println!("The value of x in the inner scope is: {x}");
    }

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

이 프로그램은 먼저 x에 5를 바인딩한다.
그런 다음 let x =를 반복하여 원래 값을 취하고 1을 더하여 새 변수 x를 생성하므로 x의 값은 6이 된다.
그런 다음 중괄호로 생성된 내부 범위 내에서 세 번째 let 문도 x를 새도 처리하고 새 변수를 생성하여 이전 값에 2를 곱하여 x의 값을 12로 만든다.

해당 범위가 끝나면 내부 섀도잉이 종료되고 x는 6으로 돌아간다.
이 프로그램을 실행하면 다음과 같이 출력된다:

$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running target/debug/variables
The value of x in the inner scope is: 12
The value of x is: 6

섀도잉은 변수를 mut로 표시하는 것과는 다른데,
실수로 let 키워드를 사용하지 않고 이 변수에 재할당을 시도하면 컴파일 타임 오류가 발생하기 때문이다.
let을 사용하면 값에 대해 몇 가지 변형을 수행하지만 해당 변형을 완료한 후에는 변수를 변경할 수 없게 만들 수 있다.

mut와 섀도잉의 또 다른 차이점은 let 키워드를 다시 사용하면 사실상 새 변수를 만드는 것이므로,
값의 유형은 변경하되 동일한 이름을 재사용할 수 있다는 것이다.

예를 들어,
프로그램에서 사용자에게 공백 문자를 입력하여 텍스트 사이에 원하는 공백 개수를 표시하도록 요청한 다음 해당 입력을 숫자로 저장하고 싶다고 가정해 보겠다:

    let spaces = "   ";
    let spaces = spaces.len();

첫 번째 공백 변수는 문자열 유형이고 두 번째 공백 변수는 숫자 유형이다.
따라서 섀도잉을 사용하면 spaces_str, spaces_num과 같은 다른 이름을 만들지 않아도 되며, 대신 더 간단한 spaces 이름을 재사용할 수 있다.
하지만 여기에 아래와 같이 mut를 사용하려고 하면 컴파일 타임 오류가 발생한다:

    let mut spaces = "   ";
    spaces = spaces.len();

변수의 유형을 변경할 수 없다는 오류 메시지가 표시된다:

$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types
--> src/main.rs:3:14
|
2 | let mut spaces = " ";
| ----- expected due to this value
3 | spaces = spaces.len();
| ^^^^^^^^^^^^ expected &str, found usize
For more information about this error, try rustc --explain E0308.
error: could not compile variables due to previous error

변수의 작동 원리를 살펴보았으니 이제 변수가 가질 수 있는 더 많은 데이터 유형을 살펴보자.

3.2 데이터 타입

Rust의 모든 값은 특정 데이터 유형으로,
어떤 종류의 데이터가 지정되고 있는지 알려주어 Rust가 해당 데이터로 작업하는 방법을 알 수 있도록 한다.
스칼라와 컴파운드의 두 가지 데이터 유형 하위 집합을 살펴보자.

Rust는 정적 타입 언어이므로 컴파일 시점에 모든 변수의 유형을 알아야 한다는 점을 명심하자. 컴파일러는 일반적으로 값과 사용 방법을 기반으로 어떤 유형을 사용할지 유추할 수 있다.

2장의 "입력값과 비밀 번호 비교하기" 섹션에서 parse를 사용하여 문자열을 숫자 타입으로 변환한 경우처럼 여러 타입이 가능한 경우에는 다음과 같이 타입 어노테이션을 추가해야 한다:

let guess: u32 = "42".parse().expect("Not a number!");

앞의 코드에 표시된 : u32 유형을 지정해주지 않으면 Rust에서 다음 오류가 표시되는데,
이는 컴파일러가 어떤 유형을 사용할지 알기 위해 더 많은 정보가 필요하다는 의미이다:

$ cargo build
Compiling notype_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0282]: type annotations needed
--> src/main.rs:2:9
|
2 | let guess = "42".parse().expect("Not a number!");
| ^^^^^
|
help: consider giving guess an explicit type
|
2 | let guess:
= "42".parse().expect("Not a number!");
| +++
For more information about this error, try rustc --explain E0282.
error: could not compile no_type_annotations due to previous error

다른 데이터 유형에 대해 다른 유형을 지정 할 수 있다.

스칼라 타입

스칼라 타입은 단일 값을 나타낸다.
Rust에는 정수, 부동 소수점 숫자, 논리 자료형, 문자의 네 가지 기본 스칼라 유형이 있다.
다른 프로그래밍 언어에서도 이러한 스칼라 유형을 알아볼 수 있다.
이제 Rust에서 어떻게 작동하는지 살펴보자.

정수 타입
정수는 분수 성분이 없는 숫자이다.
2장에서는 하나의 정수 유형인 u32 유형을 사용했다.
이 타입 선언은 연관된 값이 32비트의 공간을 차지하는 부호 없는 정수(부호 있는 정수 타입은 u 대신 i로 시작)여야 함을 나타낸다.

표 3-1은 Rust에 내장된 정수 유형을 보여준다.
이러한 변형을 사용하여 정수 값의 유형을 선언할 수 있다.

각 variant는 부호화되거나 부호화되지 않을 수 있으며 명시적인 크기가 있다.
부호와 부호 없음은 숫자가 음수일 수 있는지,
즉 숫자에 부호가 있어야 하는지(부호 있음) 아니면 양수일 때만 부호 없이 표시할 수 있는지(부호 없음)를 나타낸다.

종이에 숫자를 적을 때 부호가 중요한 경우에는 더하기 기호나 빼기 기호를 사용하여 숫자를 표시하지만,
숫자가 양수라고 가정해도 안전한 경우에는 부호 없이 숫자를 표시하는 것과 비슷하다.
부호가 있는 숫자는 2의 보수 표현을 사용하여 저장된다.

각 부호화된 variant는 -(2n - 1)에서 2n - 1 - 1까지의 숫자를 저장할 수 있으며,
여기서 n은 해당 변형이 사용하는 비트 수이다.

따라서 i8은 -(27)에서 27 - 1까지의 숫자를 저장할 수 있으며, 이는 -128에서 127에 해당한다.
부호 없는 변형은 0에서 2n - 1까지의 숫자를 저장할 수 있으므로 u8은 0에서 28 - 1, 즉 0에서 255까지의 숫자를 저장할 수 있다.

또한, 표에서 "arch"로 표시된 대로 프로그램이 실행되는 컴퓨터의 아키텍처에 따라 isizeusize 유형이 달라진다:
64비트 아키텍처를 사용하는 경우 64비트, 32비트 아키텍처를 사용하는 경우 32비트이다.

정수 리터럴은 표 3-2에 표시된 모든 형식으로 작성할 수 있다.
여러 숫자 유형이 될 수 있는 숫자 리터럴은 유형 접미사(예: 57u8)를 사용하여 유형을 지정할 수 있다.
숫자 리터럴은 _를 시각적 구분 기호로 사용하여 숫자를 더 쉽게 읽을 수 있도록 할 수도 있다.
예: 1_000, 1000을 지정한 것과 동일한 값을 갖는다)

그렇다면 어떤 유형의 정수를 사용할지 어떻게 알 수 있을까?
확실하지 않은 경우, 일반적으로 정수 유형은 기본값이 i32로 설정되어 있는 Rust의 기본값이 좋은 선택이다.
isize 또는 usize를 사용하는 주요 상황은 일종의 컬렉션을 정렬할 때이다.

정수 오버플로우
0에서 255 사이의 값을 담을 수 있는 u8 유형의 변수가 있다고 가정해 보자.
이 변수를 256과 같이 이 범위를 벗어나는 값으로 변경하려고 하면 정수 오버플로가 발생하여 두 가지 동작 중 하나가 발생할 수 있다.
디버그 모드에서 컴파일할 때 Rust는 정수 오버플로를 검사하여 이 동작이 발생하면 런타임에 프로그램이 패닉 상태에 빠지도록 한다.
Rust는 프로그램이 오류와 함께 종료될 때 패닉이라는 용어를 사용하며
, 패닉에 대해서는 9장의 "패닉으로 복구할 수 없는 오류!" 섹션에서 더 자세히 설명한다.

부동 소수점 유형
Rust에는 소수점이 있는 숫자인 부동 소수점 숫자에 대한 두 가지 기본 유형도 있다.
Rust의 부동 소수점 유형은 각각 32비트와 64비트 크기인 f32와 f64이다.

기본 유형은 f64인데, 최신 CPU에서는 f32와 속도가 거의 같지만 더 정밀하게 처리할 수 있기 때문이다.
모든 부동 소수점 유형은 부호화된다.

다음은 부동 소수점 숫자가 실제로 작동하는 것을 보여주는 예제이다:

파일명: src/main.rs

fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
}

부동 소수점 숫자는 IEEE-754 표준에 따라 표시된다.
f32 유형은 단정밀도 부동 소수점이고, f64는 배정밀도이다.

숫자 연산
Rust는 덧셈, 뺄셈, 곱셈, 나눗셈, 나머지와 같은 모든 숫자 유형에서 기대할 수 있는 기본적인 수학적 연산을 지원한다.
정수 나누기는 0을 향해 가장 가까운 정수로 잘라낸다.
다음 코드는 let 문에서 각 숫자 연산을 사용하는 방법을 보여준다:

파일명:src/main.rs

fn main() {
    // addition
    let sum = 5 + 10;

    // subtraction
    let difference = 95.5 - 4.3;

    // multiplication
    let product = 4 * 30;

    // division
    let quotient = 56.7 / 32.2;
    let truncated = -5 / 3; // Results in -1

    // remainder
    let remainder = 43 % 5;
}

이 코드에 있는 각 표현식은 수학 연산자를 사용하고 단일 값으로 평가한 다음 변수에 바인딩한다.
Appendix B에는 Rust가 제공하는 모든 연산자 목록이 포함되어 있다.

논리 연산자 유형
대부분의 다른 프로그래밍 언어와 마찬가지로 Rust의 부울 유형에는 두 가지 가능한 값이 있습니다: true 그리고 false.

논리 연산자는 1바이트 크기이다.
Rust의 논리 연산자 유형은 bool을 사용하여 지정된다. 예를 들어:

파일명:src/main.rs

fn main() {
    let t = true;

    let f: bool = false; // with explicit type annotation
}

논리 연산자 값을 사용하는 주요 방법은 if 표현식과 같은 조건문을 사용하는 것이다.
"제어 흐름" 섹션에서 Rust에서 if 표현식이 어떻게 작동하는지에 대해 다뤄보겠다.

문자 타입
Rust의 char 유형은 언어에서 가장 원시적인 알파벳 유형이다.
다음은 문자 값을 선언하는 몇 가지 예제이다:

fn main() {
    let c = 'z';
    let z: char = 'ℤ'; // with explicit type annotation
    let heart_eyed_cat = '😻';
}

큰따옴표를 사용하는 문자열 리터럴과 달리 작은따옴표로 문자 리터럴을 지정한다는 점에 유의하세요.
Rust의 문자 유형은 4바이트 크기이며 유니코드 스칼라 값을 나타내므로 ASCII보다 훨씬 더 많은 것을 나타낼 수 있다.

악센트 문자, 중국어, 일본어, 한국어 문자, 이모티콘, 0폭 공백은 모두 Rust에서 유효한 문자 값이다.
유니코드 스칼라 값의 범위는 U+0000에서 U+D7FF, U+E000에서 U+10FFFF까지다.

그러나 "문자"는 유니코드에서 실제로는 개념이 아니므로 "문자"가 무엇인지에 대한 인간의 직관과 Rust의 문자가 무엇인지가 일치하지 않을 수 있다.
이 주제는 8장의 "문자열로 UTF-8 인코딩된 텍스트 저장하기"에서 자세히 설명하겠다.

컴파운드 유형
컴파운드 유형은 여러 값을 하나의 타입으로 그룹화할 수 있다.
Rust에는 튜플과 배열이라는 두 가지 기본 복합 유형이 있다.

튜플 유형
튜플은 다양한 유형을 가진 여러 값을 하나의 복합 유형으로 그룹화하는 일반적인 방법이다.
튜플은 길이가 고정되어 있어 한 번 선언하면 크기가 커지거나 줄어들지 않는다.

괄호 안에 쉼표로 구분된 값 목록을 작성하여 튜플을 만든다.
튜플의 각 위치에는 유형이 있으며, 튜플에 있는 서로 다른 값의 유형이 동일할 필요는 없다.
이 예제에서는 선택적 유형 주석을 추가했다:
파일명:src/main.rs

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}

튜플은 단일 복합 요소로 간주되므로 변수 tup은 전체 튜플에 바인딩된다.
튜플에서 개별 값을 가져오려면 다음과 같이 패턴 일치를 사용하여 튜플 값을 해체할 수 있다:

fn main() {
    let tup = (500, 6.4, 1);

    let (x, y, z) = tup;

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

이 프로그램은 먼저 튜플을 생성하고 이를 변수 tup에 바인딩한다.
그런 다음 let 패턴을 사용하여 tup을 세 개의 개별 변수인 x, y, z로 변환한다.

이를 구조 해체라고 하는데, 단일 튜플을 세 부분으로 나누기 때문이다.
마지막으로 프로그램은 6.4인 y 값을 출력한다.

또한 마침표(.) 뒤에 액세스하려는 값의 인덱스를 사용하여 튜플 요소에 직접 액세스할 수도 있다. 예를 들어:

fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);

    let five_hundred = x.0;

    let six_point_four = x.1;

    let one = x.2;
}

이 프로그램은 튜플 x를 생성한 다음 각각의 인덱스를 사용하여 튜플의 각 요소에 접근 한다.
대부분의 프로그래밍 언어와 마찬가지로 튜플의 첫 번째 인덱스는 0이다.

값이 없는 튜플에는 유닛이라는 특별한 이름이 있다.
이 값과 해당 유형은 모두 쓰여진() 값이며 빈 값 또는 빈 반환 유형을 나타낸다.
표현식은 다른 값을 반환하지 않는 경우 암시적으로 단위 값을 반환한다.

배열 유형
여러 값의 컬렉션을 만드는 또 다른 방법은 배열을 사용하는 것이다.
튜플과 달리 배열의 모든 요소는 동일한 유형을 가져야 한다.
다른 언어의 배열과 달리 Rust의 배열은 길이가 고정되어 있다.

배열의 값은 대괄호 안에 쉼표로 구분된 목록으로 작성한다:

파일명: src/main.rs

fn main() {
    let a = [1, 2, 3, 4, 5];
}

배열은 데이터를 힙이 아닌 스택에 할당하거나(스택과 힙에 대해서는 4장에서 자세히 설명하겠다),
항상 고정된 수의 요소를 확보하고 싶을 때 유용하다.
하지만 배열은 벡터 유형만큼 유연하지 않다.

벡터는 표준 라이브러리에서 제공하는 유사한 컬렉션 유형으로, 크기를 늘리거나 줄일 수 있다.
배열을 사용할지 벡터를 사용할지 잘 모르겠다면 벡터를 사용해야 할 가능성이 높다.
8장에서는 벡터에 대해 더 자세히 설명한다.

let months = ["January", "February", "March", "April", "May", "June", "July",
              "August", "September", "October", "November", "December"];

배열의 유형은 대괄호 안에 각 요소의 유형과 세미콜론, 그리고 배열의 요소 수를 다음과 같이 작성한다:

let a: [i32; 5] = [1, 2, 3, 4, 5];

여기서 i32는 각 요소의 유형입니다.
세미콜론 뒤의 숫자 5는 배열에 5개의 요소가 포함되어 있음을 나타낸다.

또한 다음과 같이 초기값을 지정한 다음 세미콜론과 대괄호로 배열의 길이를 지정하여 각 요소에 동일한 값을 포함하도록 배열을 초기화할 수도 있다:

let a = [3; 5];

a라는 이름의 배열에는 5개의 요소가 포함되며 처음에는 모두 3이라는 값으로 설정된다.
이는 let a = [3, 3, 3, 3, 3]을 작성하는 것과 같지만 좀 더 간결하게 표현한다.

배열 요소 접근하기
배열은 스택에 할당할 수 있는 알려진 고정 크기의 단일 메모리 뭉치이다.
다음과 같이 인덱싱을 사용하여 배열의 요소에 액세스할 수 있다:

파일명:src/main.rs

fn main() {
    let a = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];
}

이 예제에서 첫 번째라는 이름의 변수는 배열의 인덱스 [0]에 있는 값이므로 값 1을 가져온다. second라는 이름의 변수는 배열의 인덱스 [1]에서 값 2를 가져온다.

잘못된 배열 요소 접근 방식
배열의 끝을 지나서 배열의 요소에 접근하려고 하면 어떻게 되는지 살펴보자.
2장의 guessing game과 유사한 이 코드를 실행하여 사용자로부터 배열 인덱스를 가져온다고 가정:

파일명:src/main.rs

use std::io;

fn main() {
    let a = [1, 2, 3, 4, 5];

    println!("Please enter an array index.");

    let mut index = String::new();

    io::stdin()
        .read_line(&mut index)
        .expect("Failed to read line");

    let index: usize = index
        .trim()
        .parse()
        .expect("Index entered was not a number");

    let element = a[index];

    println!("The value of the element at index {index} is: {element}");
}

이 코드는 성공적으로 컴파일된다 . cargo run을 사용하여 이 코드를 실행하고,
0, 1, 2, 3 또는 4를 입력하면 프로그램이 배열의 해당 인덱스에 해당하는 값을 출력한다.
대신 10과 같이 배열의 끝을 지나가는 숫자를 입력하면 다음과 같은 출력이 표시된다:

thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 10', src/main.rs:19:19
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

인덱싱 작업에서 잘못된 값을 사용하는 지점에서 프로그램이 런타임 오류를 발생시켰다.
프로그램이 오류 메시지와 함께 종료되고 최종 println! 문을 실행하지 않았다.
인덱싱을 사용하여 요소에 액세스하려고 하면 Rust는 사용자가 지정한 인덱스가 배열 길이보다 작은지 확인한다.

인덱스가 길이보다 크거나 같으면 Rust는 패닉 상태에 빠진다.
특히 이 경우 컴파일러는 사용자가 나중에 코드를 실행할 때 어떤 값을 입력할지 알 수 없기 때문에 이 검사는 런타임에 수행해야 한다.

이것은 Rust의 메모리 안전 원칙이 실제로 작동하는 예이다.
많은 저수준 언어에서는 이러한 종류의 검사가 수행되지 않으며, 잘못된 인덱스를 제공하면 잘못된 메모리에 액세스할 수 있다.

Rust는 메모리 액세스를 허용하고 계속 진행하는 대신 즉시 종료함으로써 이러한 종류의 오류로부터 사용자를 보호합니다. 9장에서는 Rust의 오류 처리와 당황하지 않고 잘못된 메모리 액세스를 허용하지 않는 읽기 쉽고 안전한 코드를 작성하는 방법에 대해 자세히 설명한다.

3.3 함수

함수는 Rust 코드에서 널리 사용된다.
이미 언어에서 가장 중요한 함수 중 하나인 많은 프로그램의 시작점인 main 함수를 살펴봤다.
또한 새로운 함수를 선언할 수 있는 fn 키워드도 봤을 것이다.

Rust 코드는 모든 문자를 소문자로 사용하고 별도의 단어에 밑줄을 긋는 기존의 함수 및 변수 이름 스타일인 스네이크 케이스를 사용한다.
다음은 함수 정의 예제가 포함된 프로그램입니다:

파일명: src/main.rs

fn main() {
    println!("Hello, world!");

    another_function();
}

fn another_function() {
    println!("Another function.");
}

Rust에서 함수를 정의하려면 fn을 입력한 다음 함수 이름과 괄호를 입력한다.
중괄호는 컴파일러에게 함수 본문이 시작하고 끝나는 위치를 알려준다.

정의한 함수는 이름 뒤에 괄호를 입력하여 호출할 수 있다.
another_function는 프로그램에 정의되어 있으므로 main 함수 내부에서 호출할 수 있다.
소스 코드에서 메인 함수 뒤에 another_function을 정의했지만, 그 전에 정의할 수도 있다.

Rust는 함수를 어디에 정의하든 상관하지 않으며,
호출자가 볼 수 있는 범위의 어딘가에 정의되어 있는지만 확인하면 된다.

함수를 더 자세히 살펴보기 위해 functions라는 이름의 새 바이너리 프로젝트를 시작해보자.
another_function 예제를 src/main.rs에 배치하고 실행 해보자.
다음과 같은 출력이 표시된다:

$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 0.28s
Running target/debug/functions
Hello, world!
Another function.

코드는 main 함수에 나타나는 순서대로 실행된다.
먼저 "Hello, world!" 메시지가 출력된 다음 another_function이 호출되고 그 메시지가 출력된다.

파라미터
함수의 시그니처의 일부인 매개변수를 갖도록 함수를 정의할 수 있다.
함수에 매개변수가 있는 경우 해당 매개변수에 대한 구체적인 값을 제공할 수 있다.
엄밀히 말하면 구체적인 값을 인자라고 하지만,

일상적인 대화에서 사람들은 함수의 정의에 있는 변수나 함수를 호출할 때 전달되는 구체적인 값에 대해 파라미터와 인자를 혼용하여 사용하는 경향이 있다.
(이건 진짜다. 파라미터, 아규먼트, 에트리뷰트 부르고 싶은대로 막 부른다)

another_function에 매개변수를 추가해보자:
파일명:src/main.rs

fn main() {
    another_function(5);
}

fn another_function(x: i32) {
    println!("The value of x is: {x}");
}

이 프로그램을 실행하면 다음과 같은 결과가 출력 된다:

$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 1.21s
Running target/debug/functions
The value of x is: 5

another_function은 x라는 하나의 파라미터를 갖는다.
x의 타입은 i32로 명시되어 있다.
another_function에 5를 전달하면,
매크로 println!는 x가 포함된 중괄호 쌍이 형식 문자열에 있는 곳에 5를 넣는다.

함수 시그니처에서는 각 매개변수의 유형을 선언해야 한다.
함수 정의에 타입 어노테이션이 필요하다는 것은,

컴파일러가 코드의 다른 곳에서 사용자가 의미하는 타입을 파악하기 위해 그들을 사용할 필요가 거의 없다는 것을 의미한다.
또한 컴파일러는 함수가 예상하는 유형을 알면 더 유용한 오류 메시지를 표시할 수 있다.

여러 매개변수를 정의할 때는 다음과 같이 쉼표로 매개변수 선언을 구분하자:

파일명: src/main.rs

fn main() {
    print_labeled_measurement(5, 'h');
}

fn print_labeled_measurement(value: i32, unit_label: char) {
    println!("The measurement is: {value}{unit_label}");
}

이 예에서는 두 개의 매개 변수가 있는 print_labeled_measurement라는 함수를 만든다.
첫 번째 매개변수의 이름은 value이며 i32 타입이다.
두 번째 매개 변수의 이름은 unit_label이고 타입은 char이다.
함수는 값과 단위 레이블을 모두 포함하는 텍스트를 출력한다.

이 코드를 실행 해보자.
현재 함수 프로젝트의 src/main.rs 파일에 있는 프로그램을 앞의 예제로 바꾸고
cargo run을 사용하여 실행하자:

$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running target/debug/functions
The measurement is: 5h

value의 값으로 5를, 단위 레이블의 값으로 'h'를 사용하여 함수를 호출했기 때문에
프로그램 출력에 해당 값이 출력된다.

구문 과 표현식
함수 본문은 선택적으로 표현식으로 끝나는 일련의 구문으로 구성된다.
지금까지 살펴본 함수에는 종료 표현식이 포함되어 있지 않았지만,
표현식이 문장의 일부인 것을 확인했다.

Rust는 표현식 기반 언어이므로 이 점을 이해해야 한다.
다른 언어에는 동일한 구분이 없으므로 구문과 표현식이 무엇인지,
그리고 그 차이가 함수 본문에 어떤 영향을 미치는지 살펴보자.

  • 구문은 어떤 작업을 수행하고 값을 반환하지 않는 명령어이다.
  • 표현식은 결과 값으로 평가합니다. 몇 가지 예를 살펴보겠습니다.

사실 우리는 이미 구문과 표현식을 사용했다.
변수를 만들고 let 키워드를 사용하여 변수에 값을 할당하는 것은 구문이다.
목록 3-1에서 "let y = 6;" 은 구문이다.

파일명:src/main.rs

fn main() { let y = 6; }
목록 3-1: 하나의 구문을 포함하는 main 함수 선언

함수 정의도 구문이며, 앞의 예제 전체가 그 자체로 구문다.

구문은 값을 반환하지 않는다.
따라서 다음 코드에서 시도하는 것처럼 let 문을 다른 변수에 할당할 수 없으며 오류가 발생한다:

파일명: src/main.rs

fn main() { let x = (let y = 6); }
이 프로그램을 실행하면 다음과 같은 오류가 표시된다:

$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found let statement
--> src/main.rs:2:14
|
2 | let x = (let y = 6);
| ^^^
error: expected expression, found statement (let)
--> src/main.rs:2:14
|
2 | let x = (let y = 6);
| ^^^^^^^^^
|
= note: variable declaration using let is a statement
error[E0658]: let expressions in this position are unstable
--> src/main.rs:2:14
|
2 | let x = (let y = 6);
| ^^^^^^^^^
|
= note: see issue #53667 https://github.com/rust-lang/rust/issues/53667 for more information
warning: unnecessary parentheses around assigned value
--> src/main.rs:2:13
|
2 | let x = (let y = 6);
| ^ ^
|
= note: #[warn(unused_parens)] on by default
help: remove these parentheses
|
2 - let x = (let y = 6);
2 + let x = let y = 6;
|
For more information about this error, try rustc --explain E0658.
warning: functions (bin "functions") generated 1 warning
error: could not compile functions due to 3 previous errors; 1 warning emitted

let y = 6 구문은 값을 반환하지 않으므로 x가 바인딩할 대상이 없다.
이는 대입이 대입의 값을 반환하는 C나 Ruby와 같은 다른 언어의 경우와 다르다.
다른 언어에서는 x = y = 6을 작성하여 x와 y가 모두 6 값을 갖도록 할 수 있지만 Rust에서는 그렇지 않다.

표현식은 값으로 평가되며 Rust에서 작성하는 나머지 코드의 대부분을 구성한다.
값 11로 평가되는 표현식인 5 + 6과 같은 수학 연산을 예로 들어 보겠다.

표현식은 문의 일부가 될 수 있다: 목록 3-1에서 let y = 6; 문의 6은 6이라는 값으로 평가되는 표현식이다.

함수 호출은 표현식이다.
매크로를 호출하는 것도 표현식이다.
예를 들어 중괄호로 만든 새 범위 블록은 표현식이다:

파일명: src/main.rs

fn main() {
    let y = {
        let x = 3;
        x + 1
    };

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

{ let x = 3; x + 1 }
이 코드의 경우는 4로 평가되는 블록이다.
이 값은 let 문의 일부로 y에 바인딩된다.
x + 1 줄에는 지금까지 본 대부분의 줄과 달리 끝에 세미콜론이 없다는 점에 유의하자.

표현식에는 종료 세미콜론이 포함되지 않는다.
표현식 끝에 세미콜론을 추가하면 표현식이 구문으로 바뀌고 값을 반환하지 않는다.
다음에서 함수 반환 값과 표현식을 살펴볼 때 이 점을 염두에 두자.

함수와 반환값
함수는 함수를 호출하는 코드에 값을 반환할 수 있다.
반환 값의 이름은 지정하지 않지만 화살표(->) 뒤에 반환 값의 유형을 선언해야 한다.
Rust에서 함수의 반환 값은 함수 본문 블록의 최종 표현식 값과 동일하다.

반환 키워드를 사용하고 값을 지정하여 함수에서 조기에 반환할 수 있지만 대부분의 함수는 암시적으로 마지막 표현식을 반환한다.
다음은 값을 반환하는 함수의 예이다:

파일명: src/main.rs

fn five() -> i32 {
    5
}

fn main() {
    let x = five();

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

여기에는 함수 호출, 매크로, 심지어 함수 five에는 숫자 5 자체만 있다.
이는 Rust에서 완벽하게 유효한 함수이다.
함수의 반환 유형도 -> i32로 지정되어 있다는 점에 유의하자.
이 코드를 실행해 보자. 출력은 다음과 같아야 한다:

$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 0.30s
Running target/debug/functions
The value of x is: 5

five 함수의 5는 반환 값이므로 반환 유형이 i32인 것이다.
이를 좀 더 자세히 살펴보자.

먼저, let x = five(); 줄은 함수의 반환값을 사용하여 변수를 초기화한다는 것을 보여준다.
함수 five는 5를 반환하므로 이 줄은 다음과 동일하다:
let x = 5;
둘째, 함수 five는 매개 변수가 없고 반환 값의 유형을 정의하지만,
반환하려는 값이 있는 표현식이기 때문에 함수 본문은 세미콜론이 없는 5로 되어 있다.

다른 예를 살펴보자:

파일명: src/main.rs

fn main() {
    let x = plus_one(5);

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

fn plus_one(x: i32) -> i32 {
    x + 1
}

이 코드를 실행하면 x의 값이 출력된다: 6.
그러나 x + 1이 포함된 줄의 끝에 세미콜론을 배치하여 표현식에서 구문으로 변경하면 오류가 발생한다:

파일명: src/main.rs

fn main() {
    let x = plus_one(5);

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

fn plus_one(x: i32) -> i32 {
    x + 1;
}

이 코드를 컴파일하면 다음과 같은 오류가 발생한다:

$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error[E0308]: mismatched types
--> src/main.rs:7:24
|
7 | fn plus_one(x: i32) -> i32 {
| -------- ^^^ expected i32, found ()
| |
| implicitly returns () as its body has no tail or return expression
8 | x + 1;
| - help: remove this semicolon
For more information about this error, try rustc --explain E0308.
error: could not compile functions due to previous error

주요 오류 메시지인 타입 불일치는 이 코드의 핵심 문제를 보여준다.
plus_one 함수의 정의에는 i32를 반환한다고 명시되어 있지만 구문은 단위 유형인 ()로 표현되는 값으로 평가되지 않는다.

따라서 아무 것도 반환되지 않으므로 함수 정의와 모순되어 오류가 발생한다.
이 출력에서 Rust는 이 문제를 해결하는 데 도움이 될 수 있는 메시지를 제공한다.
세미콜론을 제거하면 오류가 해결될 수 있다는 제안이다.

3.4 코멘트

모든 프로그래머는 코드를 이해하기 쉽게 만들기 위해 노력하지만 때로는 추가적인 설명이 필요할 때가 있다.

이러한 경우 프로그래머는 소스 코드에 컴파일러는 무시하지만 소스 코드를 읽는 사람들이 유용하게 사용할 수 있는 주석을 남긴다.

다음은 간단한 주석의 예이다:

// hello, world

Rust에서 관용적 주석 스타일은 두 개의 슬래시로 주석을 시작하고 주석은 줄이 끝날 때까지 계속된다.
한 줄을 넘어서는 주석의 경우 다음과 같이 각 줄에 //를 포함해야 한다:

// So we’re doing something complicated here, long enough that we need
// multiple lines of comments to do it! Whew! Hopefully, this comment will
// explain what’s going on.

코멘트는 코드가 포함된 줄의 끝에 배치할 수도 있다:

파일명:src/main.rs

fn main() {
    let lucky_number = 7; // I’m feeling lucky today
}

하지만 주석이 달린 코드 위에 별도의 줄에 주석이 있는 이 형식으로 사용되는 경우가 더 많다:

파일명: src/main.rs

fn main() {
    // I’m feeling lucky today
    let lucky_number = 7;
}

Rust에는 또 다른 종류의 주석인 문서 주석도 있는데,
이는 14장의 'Crates.io에 crate 배포하기' 섹션에서 설명한다.

3.5 흐름 제어

조건이 참인지 여부에 따라 일부 코드를 실행하고,
조건이 참일 때 일부 코드를 반복적으로 실행하는 기능은 대부분의 프로그래밍 언어에서 기본 구성 요소이다.
Rust 코드의 실행 흐름을 제어할 수 있는 가장 일반적인 구조는 if 표현식과 루프이다.

if 표현식
if 표현식을 사용하면 조건에 따라 코드를 분기할 수 있다.
조건을 지정한 다음 "이 조건이 충족되면 이 코드 블록을 실행합니다."라고 명시할 수 있다.
조건이 충족되지 않으면 이 코드 블록을 실행하지 마시오."라고 명시할 수 있다.

프로젝트 디렉터리에 branches라는 새 프로젝트를 생성하여 if 표현식을 살펴보자.
src/main.rs 파일에 다음을 입력해보자:

파일명: src/main.rs

fn main() {
    let number = 3;

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

모든 if 표현식은 키워드 if로 시작하고 조건이 뒤따른다.
이 코드의 경우 조건은 변수 숫자의 값이 5보다 작은지 여부를 확인한다.
조건 바로 뒤에 조건이 참일 경우 실행할 코드 블록을 중괄호 안에 배치한다.

2장의 "입력값과 비밀 번호 비교하기" 섹션에서 설명한 일치 표현식의 arm과 마찬가지로,
if 표현식의 조건과 관련된 코드 블록을 arm이라고 부르기도 한다.

선택적으로, 조건이 false로 평가될 경우,
프로그램이 실행할 대체 코드 블록을 제공하기 위해 여기서 선택한 else 표현식을 포함할 수도 있다.
else 표현식을 제공하지 않고 조건이 거짓인 경우 프로그램은 if 블록을 건너뛰고 다음 코드 블록으로 넘어간다.

이 코드를 실행하면 다음과 같은 출력이 표시된다:

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

number의 값을 조건을 false로 만드는 값으로 변경하여 어떤 일이 발생하는지 확인해 보자:

let number = 7;
프로그램을 다시 실행하고 출력을 확인해보자:

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

이 코드의 조건은 반드시 논리 연산자이어야 한다는 점도 주목할 필요가 있다.
조건이 논리연산자가 아닐 경우 오류가 발생한다.
예를 들어 다음 코드를 실행해 보자:

파일명:src/main.rs

fn main() {
    let number = 3;

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

이번에는 if 조건이 3의 값으로 평가되고 Rust가 오류를 알려줍니다:

$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: mismatched types
--> src/main.rs:4:8
|
4 | if number {
| ^^^^^^ expected bool, found integer
For more information about this error, try rustc --explain E0308.
error: could not compile branches due to previous error

이 오류는 Rust가 논리연산자를 기대했지만 정수를 가져왔음을 나타낸다.
Ruby 및 JavaScript와 같은 언어와 달리 Rust는 논리연산자가 아닌 유형을 논리연산자로 자동 변환하려고 시도하지 않는다.

항상 명시적으로 논리연산자를 조건으로 하는 if를 제공해야 한다.
예를 들어 숫자가 0이 아닌 경우에만 if 코드 블록이 실행되도록 하려면,
if 표현식을 다음과 같이 변경하면 된다:

파일명:src/main.rs

fn main() {
    let number = 3;

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

이 코드를 실행하면 코드는 number를 0이 아닌 수로 출력한다.

여러 조건을 else if로 처리
if 와 else를 결합하여 여러 조건을 사용할 수 있다.
예를 들면 다음과 같다:

파일명: 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.31s
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장에서는 이러한 경우에 사용할 수 있는 강력한 Rust 분기 구조체 match에 대해 설명한다.

let 구문에서 if 사용하기
if는 표현식이므로 목록 3-2에서와 같이 let 문의 오른쪽에 사용하여 결과를 변수에 할당할 수 있다.

파일 이름: src/main.rs

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

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

목록 3-2: if 표현식의 결과를 변수에 할당하기
number 변수는 if 표현식의 결과에 따라 값에 바인딩된다.
이 코드를 실행하여 어떤 일이 발생하는지 확인해보자:

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

코드 블록은 그 안의 마지막 표현식까지 평가되며 숫자 자체도 표현식이라는 점을 기억하자.
이 경우 전체 if 표현식의 값은 어떤 코드 블록이 실행되는지에 따라 달라진다.

즉, if의 각 arm에서 결과가 될 수 있는 값은 동일한 타입이어야 하며,
목록 3-2에서는 if의 arm과 else의 arm의 결과가 모두 i32 정수이다.
다음 예제에서와 같이 유형이 일치하지 않으면 오류가 발생한다:

파일 이름: src/main.rs

fn main() {
    let condition = true;

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

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

이 코드를 컴파일하려고 하면 오류가 발생합니다.
if 및 else 의 arm에는 호환되지 않는 값 타입이 있으며,
Rust는 프로그램에서 문제를 찾을 수 있는 위치를 정확히 알려준다:

$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: if and else have incompatible types
--> src/main.rs:4:44
|
4 | let number = if condition { 5 } else { "six" };
| - ^^^^^ expected integer, found &str
| |
| expected because of this
For more information about this error, try rustc --explain E0308.
error: could not compile branches due to previous error

if 블록의 표현식은 정수로 평가되고 else 블록의 표현식은 문자열로 평가된다.
변수는 단일 타입을 가져야 하고 Rust는 컴파일 시 숫자 변수가 어떤 타입인지 확실히 알아야 하기 때문에 이 방식은 작동하지 않는다.
숫자 유형을 알면 컴파일러가 숫자를 사용하는 모든 곳에서 해당 유형이 유효한지 확인할 수 있다.

숫자 유형이 런타임에만 결정되는 경우,
컴파일러가 모든 변수에 대해 여러 가상의 유형을 추적해야 하는 경우,
코드가 더 복잡해지고 보장할 수 있는 것이 줄어들기 때문에 Rust는 이를 수행할 수 없다.

루프를 이용한 반복
코드 블록을 두 번 이상 실행하는 것이 유용한 경우가 많다.
이 작업을 위해 Rust는 루프 본문 내부의 코드를 끝까지 실행한 다음 바로 처음부터 다시 시작하는 여러 개의 루프를 제공한다.
루프를 실험하기 위해 루프라는 새 프로젝트를 만들어 보자.

Rust에는 loop, while, for의 세 가지 종류의 루프가 있습니다. 각각을 사용해보자.

loop를 사용한 코드 반복
loop 키워드는 Rust가 코드 블록을 영원히 또는 사용자가 명시적으로 중지하라고 지시할 때까지 반복해서 실행하도록 지시한다.

예를 들어, loops 디렉터리의 src/main.rs 파일을 다음과 같이 변경해보자:

파일명: src/main.rs

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

이 프로그램을 실행하면 프로그램을 수동으로 중지할 때까지 "again!"이 계속 반복해서 출력된다. 대부분의 단말기는 키보드 단축키 ctrl-c를 통해서 연속 루프에 갇힌 프로그램을 중단할 수 있다. 한번 시도해 보자:

$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished dev [unoptimized + debuginfo] target(s) in 0.29s
Running target/debug/loops
again!
again!
again!
again!
^Cagain!

기호 ^C는 Ctrl-C를 누른 위치를 나타낸다.
인터럽트 신호를 받았을 때 코드가 루프에서 어디에 있었는지에 따라 ^C 뒤에 again!이 표시될 수도 있고 표시되지 않을 수도 있다.

다행히도 Rust는 코드를 사용하여 루프에서 벗어나는 방법도 제공한다.
루프 내에 break 키워드를 배치하여 루프 실행을 언제 중단할지 프로그램에 알릴 수 있다.

2장의 "정답을 맞춘 후 종료하기" 섹션의 guessing gmae에서 사용자가 정확한 숫자를 맞춰서 게임에서 이겼을 때 프로그램을 종료하기 위해 이 작업을 수행한 것을 기억하자.

또한 guessing game에서 continue를 사용했는데,
이는 루프에서 루프 반복의 나머지 코드를 건너뛰고 다음 반복으로 이동하도록 프로그램에 지시한다.

루프에서 return값 받기
루프의 용도 중 하나는 스레드가 작업을 완료했는지 확인하는 등 실패할 수 있는 작업을 다시 시도하는 것이다.
또한 해당 작업의 결과를 루프 밖으로 나머지 코드에 전달해야 할 수도 있다.

이렇게 하려면 루프를 중지하는 데 사용하는 break 표현식 뒤에 반환할 값을 추가하면,
다음과 같이 해당 값이 루프 밖으로 반환되어 사용할 수 있다:

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {result}");
}

루프 전에 counter라는 변수를 선언하고 0으로 초기화한다.
그런 다음 result라는 변수를 선언하여 루프에서 반환된 값을 보관한다.
루프를 반복할 때마다 카운터 변수에 1을 더한 다음 카운터가 10과 같은지 확인한다.
10이면 카운터 * 2 값과 함께 break 키워드를 사용한다.
루프가 끝나면 세미콜론을 사용하여 결과값을 할당하는 문을 종료한다.
마지막으로 result 값 20을 인쇄한다.

여러 루프를 구분하기 위한 루프 레이블
루프 안에 루프가 있는 경우 해당 지점에서 가장 안쪽 루프에 break 및 continue을 적용한다.
선택적으로 루프에 루프 레이블을 지정하여 해당 키워드가 가장 안쪽 루프가 아닌 레이블이 지정된 루프에 적용되도록 break 또는 continue과 함께 사용할 수 있다.
루프 레이블은 작은따옴표로 시작해야 한다. 다음은 두 개의 중첩 루프가 있는 예시이다:

fn main() {
    let mut count = 0;
    'counting_up: loop {
        println!("count = {count}");
        let mut remaining = 10;

        loop {
            println!("remaining = {remaining}");
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }

        count += 1;
    }
    println!("End count = {count}");
}

외부 루프에는 'counting_up'이라는 레이블이 있으며 0에서 2까지 더한다.
레이블이 없는 내부 루프는 10에서 9까지 감소한다.

레이블을 지정하지 않은 첫 번째 break는 내부 루프만 종료한다.
break 'counting_up; 문은 외부 루프를 종료한다.
그 후 이 코드가 출력된다:

$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished dev [unoptimized + debuginfo] target(s) in 0.58s
Running target/debug/loops
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2

while 조건문의 루프
프로그램에서 루프 내에서 조건을 평가해야 하는 경우가 종종 있다.
조건이 참이면 루프가 실행된다.

조건이 참이 아니게 되면 프로그램은 break를 호출하여 루프를 중지한다.
loop, if, else, break의 조합을 사용하여 이와 같은 동작을 구현할 수 있으며,
원한다면 지금 프로그램에서 이를 시도해 볼 수 있다.

하지만 이 패턴은 매우 일반적이기 때문에 Rust에는 이를 위한 언어 구조인 while 루프가 내장되어 있다.
목록 3-3에서는 while을 사용하여 프로그램을 세 번 반복하고 매번 카운트다운한 다음, 루프가 끝나면 메시지를 출력하고 종료한다.

파일명:src/main.rs

fn main() {
    let mut number = 3;

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

        number -= 1;
    }

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

목록 3-3: 조건이 true인 동안 코드를 실행하기 위한 while 루프 사용
이 구조는 루프, if, else, break를 사용할 때 필요한 많은 중첩을 제거하며 더 명확하다.
조건이 참으로 평가되는 동안에는 코드가 실행되고, 그렇지 않으면 루프가 종료된다.

for를 사용한 collection 루핑
배열과 같은 컬렉션의 요소를 반복하려면 while 구문을 사용하도록 선택할 수 있다.
예를 들어, 목록 3-4의 루프는 배열 a의 각 요소를 출력한다.

파일명: 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 += 1;
    }
}

목록 3-4: while 루프를 사용하여 컬렉션의 각 요소 접근하기

여기서 코드는 배열의 요소를 통해 카운트업한다.
인덱스 0에서 시작하여 배열의 최종 인덱스에 도달할 때까지(즉, index <5가 더 이상 참이 아닐 때) 반복한다.
이 코드를 실행하면 배열의 모든 요소가 출력된다:

$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished dev [unoptimized + debuginfo] target(s) in 0.32s
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라는 값에 도달하더라도 배열에서 여섯 번째 값을 가져오기 전에 루프 실행이 중지된다.

그러나 이 접근 방식은 오류가 발생하기 쉽다.
인덱스 값이나 테스트 조건이 올바르지 않으면 프로그램이 패닉 상태에 빠질 수 있다.

예를 들어, 배열의 정의를 네 개의 요소를 갖도록 변경했지만 인덱스가 4 미만인 동안으로 조건을 업데이트하는 것을 잊어버린 경우 코드가 패닉 상태에 빠질 수 있다.

또한 컴파일러가 루프를 반복할 때마다 인덱스가 배열의 범위 내에 있는지 조건부 검사를 수행하기 위해 런타임 코드를 추가하기 때문에 속도가 느려진다.

보다 간결한 대안으로 for 루프를 사용하여,
컬렉션의 각 항목에 대해 몇 가지 코드를 실행할 수 있다.
for 루프는 목록 3-5의 코드와 같다.

파일명: src/main.rs

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

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

목록 3-5: for 루프를 사용하여 컬렉션의 각 요소 반복하기

이 코드를 실행하면 목록 3-4와 동일한 출력을 볼 수 있다.
더 중요한 것은, 이제 코드의 안전성이 향상되어 배열의 끝을 넘어가거나 충분히 멀리 가지 않아 일부 항목이 누락되는 버그가 발생할 가능성이 없어졌다는 점이다.

목록 3-4에서 사용한 방법처럼 배열의 값 개수를 변경할 때 for 루프를 사용하면 다른 코드를 변경하는 것을 기억할 필요가 없다.

for 루프는 안전성과 간결성 덕분에 Rust에서 가장 일반적으로 사용되는 루프 구조이다.
목록 3-3의 동안 루프를 사용한 카운트다운 예제에서처럼 특정 코드를 특정 횟수만큼 실행하려는 상황에서도 대부분의 러스트 사용자는 for 루프를 사용한다.

이를 수행하는 방법은 표준 라이브러리에서 제공하는 Range를 사용하여 한 숫자에서 시작하여 다른 숫자보다 먼저 끝나는 순서대로 모든 숫자를 생성하는 것이다.

다음은 for 루프와 아직 설명하지 않은 다른 방법인 rev를 사용하여 범위를 반전시키는 카운트다운의 모습이다:

파일명: src/main.rs

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

요약
변수, 스칼라 및 복합 데이터 유형, 함수, 주석, if 표현식, 루프에 대해 배워보았다.
이 장에서 설명한 개념을 연습하려면 다음을 수행하는 프로그램을 작성해 보자:

  • 화씨를 섭씨로 바꾸기
  • n번쨰 피보나치 숫자 만들기
  • 노래의 반복을 활용하여 크리스마스 캐롤 "12일의 크리스마스"의 가사를 출력 해보자.
profile
Blockchain developer

0개의 댓글