Rust 공식문서

Devjacob·2023년 3월 11일
0
post-thumbnail
  1. Guessing Game 프로그래밍.

원문: https://doc.rust-lang.org/book/ch02-00-guessing-game-tutorial.html

Guessing Game 프로그래밍 해보기

실습 프로젝트를 함께 진행하면서 Rust에 대해 알아보자.
이 장에서는 실제 프로그램에서 사용하는 방법을 보여줌으로써 몇 가지 일반적인 Rust 개념을 소개한다.
let, match, 메서드, 관련 함수, 외부 Crate 등에 대해 배우게 된다.

새로운 프로젝트 셋팅하기

새 프로젝트를 설정하려면 1장에서 만든 프로젝트 디렉토리로 이동하여 다음과 같이 Cargo를 사용하여 새 프로젝트를 만듭니다:

$ cargo new guessing_game
$ cd guessing_game

cargo new는 프로젝트 이름(guessing_game)을 첫 번째 파라미터로 받습니다.
두 번째 명령은 새 프로젝트의 디렉토리로 경로 이동 합니다.

생성된 Cargo.toml 파일을 확인합니다:

[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"

// See more keys and their definitions at
// https://doc.rustlang.org/cargo/reference/manifest.html
[dependencies]

챕터1에서 봤듯이, cargo.new 는 hello world 프로그램을 기본적으로 제공한다.

src/main.rs file을 확인해보면 다음과 같다.

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

이제 "Hello World!" 프로그램을 컴파일 한 후에 cargo run 커맨드를 이용하여 동작시켜보자

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

run 커맨드는 이 게임에서처럼 프로젝트를 빠르게 반복해야 할 때 유용하며,
다음 반복으로 넘어가기 전에 각 반복을 빠르게 테스트할 수 있다.

이제 src/main.rs 파일을 다시 열고 모든 코드를 작성 해보자.

Processing a Guess

추측 게임 프로그램의 첫 번째 부분은 사용자 입력을 요청하고, 그 입력을 처리하고, 입력이 예상된 형식인지 확인합니다.
먼저 플레이어가 추측을 입력할 수 있도록 합니다.
목록 2-1의 코드를 src/main.rs에 입력합니다.

use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin().read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}

목록 2-1: 사용자로부터 추측을 받아 출력하는 코드

이 코드에는 많은 정보가 포함되어 있으므로 한 줄씩 살펴보자.
사용자 입력을 받은 다음 결과를 출력으로 인쇄하려면 io 입력/출력 라이브러리를 불러 와야한다.

io 라이브러리는 표준 라이브러리인 std에서 제공된다:

use std::io;

기본적으로 Rust에는 모든 프로그램의 범위로 가져오는 표준 라이브러리에 정의된 항목이 있다.
이 세트를 전주곡이라고 하며,
표준 라이브러리 문서에서 이 세트의 모든 항목을 확인할 수 있습니다.

사용하려는 유형이 전주곡에 없는 경우 use 문을 사용하여 해당 유형을 명시적으로 가져와야 한다.
std::io 라이브러리를 사용하면 사용자 입력을 받는 기능을 비롯하여 유용한 기능을 사용할 수 있다.

1장에서 살펴본 것처럼 main 함수는 프로그램의 시작점:

fn 구문은 새 함수를 선언하고, 괄호()는 매개 변수가 없음을 나타내며, 중괄호 {는 함수 본문에서 시작됩니다.

1장에서도 배웠듯이 println! 은 문자열을 화면에 인쇄하는 매크로:

println!("Guess the number!");
println!("Please input your guess.");

이 코드는 게임이 무엇인지에 대한 프롬프트를 출력하고 사용자에게 입력을 요청한다.

변수에 값 저장하기

사용자 입력을 저장할 변수를 다음과 같이 생성:

let mut guess = String::new();

let 문을 사용하여 변수를 생성합니다.

다음은 또 다른 예제:

let apples = 5;

이 줄은 apples라는 새 변수를 생성하고 5라는 값에 바인딩한다.
Rust에서 변수는 기본적으로 불변이므로 변수에 값을 지정하면 그 값은 변경되지 않는다.
이 개념에 대해서는 3장의 "변수와 가변성" 섹션에서 자세히 설명하겠다.
변수를 변경 가능하게 만들려면 변수 이름 앞에 mut을 추가한다:

let apples = 5; // immutable
let mut bananas = 5; // mutable

추측 게임 프로그램으로 돌아가서, 이제 let mut guess가 guess라는 가변 변수를 도입한다.
등호(=)는 이제 변수에 무언가를 바인딩하고 싶다는 것을 Rust에게 알려준다.
등호 오른쪽에는 guess가 바인딩되는 값이 있는데,
이는 String의 새 인스턴스를 반환하는 함수인 String::new를 호출한 결과.
String은 표준 라이브러리에서 제공하는 문자열 유형으로,
확장 가능한 UTF-8로 인코딩된 텍스트 비트이다.

new 줄의 :: 구문은 new가 String 유형의 연관 함수임을 나타냅니다. 연관 함수는 특정 유형(이 경우 String)에 구현된 함수입니다. 이 new 함수는 빈 문자열을 새로 만든다.
new 함수는 어떤 종류의 새로운 값을 만드는 함수의 일반적인 이름이기 때문에,
많은 유형에서 new 함수를 찾을 수 있다.

전체적으로 보면, let mut guess = String::new(); 줄은 현재 빈 새 String 인스턴스에 바인딩된 변경 가능한 변수를 생성했다.

유저의 입력 받기

프로그램의 첫 줄에 표준 라이브러리의 입력/출력 기능을 std::io; 를 사용하여 포함했다.
이제 사용자 입력을 처리할 수 있는 io 모듈에서 stdin 함수를 호출해보자:

io::stdin()
.read_line(&mut guess)

프로그램을 시작할 때 std::io; 를 사용하여 io 라이브러리를 import 하지 않았다면 이 함수 호출을 std::io::stdin으로 작성하여 함수를 사용할 수 있다.
stdin 함수는 터미널의 표준 입력에 대한 처리를 나타내는 유형인 std::io::Stdin의 인스턴스를 반환한다.

.read_line(&mut guess)은 표준 입력 처리에서 read_line 메서드를 호출하여 사용자로부터 입력을 받는다.
또한 사용자 입력을 어떤 문자열에 저장할지 알려주기 위해 read_line에 인자로 &mut guess를 전달한다.
read_line의 전체 작업은 사용자가 표준 입력에 입력하는 모든 것을 가져와서,
내용을 덮어쓰지 않고 문자열에 추가하는 것이므로 해당 문자열을 인수로 전달한다.
메서드가 문자열의 내용을 변경할 수 있도록 문자열 인수는 변경 가능해야 한다.

& 는 이 인수가 참조임을 나타내며, 이를 사용하면 해당 데이터를 메모리에 여러 번 복사할 필요 없이,
코드의 여러 부분에서 하나의 데이터에 액세스할 수 있는 방법을 제공한다.
참조는 복잡한 기능이지만, Rust의 주요 장점 중 하나는 참조를 사용하는 것이 안전하고 쉬운게 장점이다.
이 프로그램을 완성하기 위해 이러한 세부 사항을 많이 알 필요는 없다.
지금은 변수와 마찬가지로 참조도 기본적으로 불변이라는 것만 알아두면 된다.
따라서 변경 가능하게 만들려면 &guess가 아닌 &mut guess를 작성해야 한다.
(4장에서 참조에 대해 더 자세히 설명되어 있음.)

Result를 활용하여 잠재적인 오류 처리

이제 세 번째 텍스트 줄에 대해 논의하고 있지만 여전히 하나의 논리적 코드 줄의 일부라는 점에 유의하자.
다음 부분은 이 메서드이다:

.expect("Failed to read line");

이 코드를 이렇게 작성할 수도 있다:

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

그러나 한 줄이 길면 읽기 어려우므로 나누는 것이 가장 좋다.
.method_name() 구문으로 메서드를 호출할 때 긴 줄을 나누기 위해 개행과 들여쓰는 것이 좋습니다.
이제 이 줄이 무엇을 하는지 살펴보겠습니다.

앞서 언급했듯이 read_line은 사용자가 입력하는 모든 내용을 전달한 문자열에 넣지만, Result 값도 반환한다.
Result는 열거형이라고도 하며, 여러 가능한 상태 중 하나에 속할 수 있는 유형이다.
가능한 각 상태를 variant라고 부른다.

6장에서는 열거형에 대해 더 자세히 다룰 것입니다.
Result 타입의 목적은 오류 처리 정보를 인코딩하는 것이다.

Result 의 variant 는 Ok와 Err 이다.
Ok Variant는 작업이 성공했음을 나타내며 Ok 안에는 성공적으로 생성된 값이 들어 있다.
Err Variant는 작업이 실패했음을 의미하며 Err에는 작업이 실패한 방법 또는 이유에 대한 정보가 포함되어 있다.

다른 유형의 값과 마찬가지로 Result type의 값에는 메서드가 정의되어 있다.
Result의 인스턴스에는 호출할 수 있는 expect method가 있다.
Result 인스턴스가 Err 값인 경우, expect는 프로그램의 충돌을 야기하고,
expect 에게 인자로 전달된 매세지를 표시한다.

read_line methodErr을 반환하면 기본 운영 체제에서 발생한 오류의 결과일 가능성이 높다.
이 Result 인스턴스가 Ok 값인 경우, expect는 Ok가 보유하고 있는 return 값을 가져와서 사용자가 사용할 수 있도록 해당 값만 반환한다. 이 경우 해당 값은 사용자 입력의 바이트 수.

만약 expect를 호출하지 않으면, 컴파일은 되겠지만 warning을 받을것.

$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused Result that must be used
--> src/main.rs:10:5
|
10 | io::stdin().read_line(&mut guess);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: #[warn(unused_must_use)] on by default
= note: this Result may be an Err variant, which should be handled
warning: guessing_game (bin "guessing_game") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.59s

Rust는 read_line에서 return 된 Result 값을 사용하지 않았다고 경고하며,
이는 프로그램이 가능한 오류를 처리하지 않았음을 나타낸다.

경고를 억제하는 올바른 방법은 실제로 오류 처리 코드를 작성하는 것이지만,
여기서는 문제가 발생했을 때 이 프로그램을 충돌시키고 싶기 때문에 기대값을 사용할 수 있다.
오류에서 복구하는 방법은 9장에서 배워보자.

println! placeholder를 통해서 값을 출력하기

닫는 중괄호를 제외하면 지금까지 코드에서 논의할 내용은 단 한 줄뿐:

println!("You guessed: {guess}");

이 코드는 사용자의 입력이 포함된 문자열을 인쇄한다.
중괄호 {} 집합은 자리 표시자.
{}는 값을 제자리에 고정하는 작은 게의 집게로 생각해보자.
변수 값을 인쇄할 때 변수 이름은 중괄호 안에 들어갈 수 있다.
표현식 평가 결과를 인쇄할 때는 형식 문자열에 빈 중괄호를 넣은 다음 쉼표로 구분된 표현식 목록과 함께 형식 문자열을 따라 각 빈 중괄호 자리 표시자에 동일한 순서로 인쇄한다.
변수와 표현식의 결과를 println! 호출 한 번으로 인쇄하면 다음과 같이 표시됩니다:

let x = 5;
let y = 10;
println!("x = {x} and y + 2 = {}", y + 2);

이 코드는 x=5, y+2 = 12 를 출력한다.

첫번째 파트 테스팅

guessing game의 첫번쨰 파트를 테스트 해보자. cargo run 커맨드를 사용해서 동작 시켜보자.

$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 6.44s
Running target/debug/guessing_game
Guess the number!
Please input your guess.
6
You guessed: 6

이 시점에서 키보드에서 입력을 받은 다음 출력하는 게임의 첫 번째 부분이 완료되었다.

난수 생성하기

다음으로 사용자가 추측할 난수를 생성해야 합니다. 난수는 매번 달라야 게임을 두 번 이상 플레이하는 재미가 있다.
게임이 너무 어렵지 않도록 1에서 100 사이의 난수를 사용해보자.
Rust는 아직 표준 라이브러리에 난수 기능을 포함하지 않는다.
하지만 Rust 팀은 해당 기능이 포함된 랜덤 Crate를 제공한다.

Using a Crate to Get More Functionality

Crate는 Rust 소스 코드 파일의 모음이라는 것을 기억하자.
우리가 빌드하고 있는 프로젝트는 실행 파일인 Binary crate이다.

rand crate 는 라이브러리 crate로 다른 프로그램에서 사용하기 위한 코드가 포함되어 있으며 자체적으로 실행할 수 없다.

external crate를 조정하는 것은 Cargo가 정말 빛을 발하는 부분이다.
rand 를 사용하는 코드를 작성하기 전에 랜드를 사용하는 crate를 dependency 요소로 포함하도록 Cargo.toml 파일을 수정해야 한다.

지금 해당 파일을 열고 Cargo에서 생성한 [dependencies] 섹션 헤더 아래 하단에 다음 줄을 추가해보자.
이 버전 번호와 함께 여기에 있는 것처럼 정확하게 rand를 지정해야 하며, 그렇지 않으면 이 튜토리얼의 코드 예제가 작동하지 않을 수 있다:

파일명:Cargo.toml

[dependencies]
rand = "0.8.5"

Cargo.toml 파일에서 헤더 뒤에 오는 모든 항목은 다른 섹션이 시작될 때까지 계속되는 해당 섹션의 일부.
[dependency]에서 프로젝트가 의존하는 external crate와 필요한 해당 crate의 버전을 Cargo에 알려줍니다.

이 경우, 시맨틱 버전 지정자 0.8.5로 랜덤 상자를 지정합니다.
Cargo는 버전 번호를 작성하는 표준인 시맨틱 버전(SemVer라고도 함)을 이해한다.
0.8.5 지정자는 실제로 ^0.8.5의 줄임말로, 0.8.5 이상 0.9.0 미만의 모든 버전을 의미한다.

Cargo는 이러한 버전에 0.8.5 버전과 호환되는 Public API가 있는 것으로 간주하며,
이 사양은 이 장의 코드와 컴파일되는 최신 패치 릴리스를 받을 수 있도록 보장한다.
0.9.0 이상 버전은 다음 예제에서 사용하는 것과 동일한 API를 보장하지 않는다.

이제 코드를 변경하지 않고 목록 2-2와 같이 프로젝트를 빌드해보자.

$ cargo build
Updating crates.io index
Downloaded rand v0.8.5
Downloaded libc v0.2.127
Downloaded getrandom v0.2.7
Downloaded cfg-if v1.0.0
Downloaded ppv-lite86 v0.2.16
Downloaded rand_chacha v0.3.1
Downloaded rand_core v0.6.3
Compiling libc v0.2.127
Compiling getrandom v0.2.7
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.16
Compiling rand_core v0.6.3
Compiling rand_chacha v0.3.1
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53s
목록 2-2: rand crate를 dependency로 추가한 후 cargo build를 실행할 때의 출력문

운영 체제에 따라 버전 번호가 다를 수 있으며(SemVer 덕분에 모두 코드와 호환가능),
줄이 다른 순서로 표시될 수 있다.

external dependency를 포함하면 Cargo는 해당 dependency에 필요한 모든 최신 버전을 레지스트리에서 가져오는데,
이 레지스트리는 Crates.io의 데이터 복사본.
Crates.io는 Rust 생태계의 사람들이 다른 사람들이 사용할 수 있도록 오픈 소스 Rust 프로젝트를 게시하는 곳이다.

레지스트리를 업데이트한 후 Cargo는 [dependency] 섹션을 확인하여 아직 다운로드되지 않은 Crate를 모두 다운로드한다.
이 경우에는 rand만 dependency로 나열했지만, 랜드가 작동하는 데 필요한 다른 crate도 Cargo가 가져온다.
crate를 다운로드한 후 Rust는 crate를 컴파일한 다음 사용 가능한 dependency을 사용하여 프로젝트를 컴파일한다.

변경하지 않고 Cargo Build를 바로 다시 실행하면 Finished 라인 외에 어떤 출력도 얻지 못한다.
Cargo는 이미 종속 요소를 다운로드하고 컴파일한 상태이며 Cargo.toml 파일에서 종속 요소에 대해 아무것도 변경하지 않았음을 알고 있다.
또한 Cargo는 사용자가 코드를 변경하지 않았다는 것을 알고 있으므로 해당 코드도 다시 컴파일하지 않는다.

만약 src/main.ru을 열고, 자잘한 변화를 주고 저장한 후에 빌드를 하면 이 출력문만 볼수있다.

$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs

이 줄은 Cargo가 src/main.rs 파일에 대한 작은 변경 사항으로만 빌드를 업데이트한다는 것을 보여준다.
dependency는 변경되지 않았으므로,
Cargo는 이미 다운로드하고 컴파일한 종속성을 재사용할 수 있다는 것을 알고 있다.

Cargo.lock 파일을 사용하여 재현 가능한 빌드 보장

Cargo에는 코드를 빌드할 때마다 동일한 아티팩트를 다시 빌드할 수 있도록 하는 메커니즘이 있다:
Cargo는 사용자가 별도로 지정하지 않는 한 지정한 dependency 버전만 사용한다.

예를 들어 다음 주에 rand crate의 0.8.6 버전이 출시되는데,

이 버전에는 중요한 버그 수정이 포함되어 있지만 코드를 망가뜨리는 회귀도 포함되어 있다고 가정해 보자.

이를 처리하기 위해 Rust는 cargo build를 처음 실행할 때 Cargo.lock 파일을 생성하므로,
이제 guessing game 디렉터리에 이 파일이 있습니다.

프로젝트를 처음 빌드할 때,
Cargo는 기준에 맞는 모든 종속성 버전을 파악한 다음 Cargo.lock 파일에 기록한다.

나중에 프로젝트를 빌드할 때,
Cargo는 Cargo.lock 파일이 존재하는지 확인하고 버전을 다시 파악하는 모든 작업을 수행하지 않고 이 파일에 지정된 버전을 사용한다.

이렇게 하면 자동으로 재현 가능한 빌드를 생성할 수 있다.

즉, Cargo.lock 파일 덕분에 명시적으로 업그레이드하기 전까지는 프로젝트가 0.8.5 버전으로 유지된다.

Cargo.lock 파일은 재현 가능한 빌드에 중요하기 때문에 프로젝트의 나머지 코드와 함께 소스 제어에 체크하는 경우가 많다.

새로운 버전을 받기 위한 crate 업데이트

Crate를 업데이트하고 싶을 때 Cargo는 update 커멘드을 제공하며,
이 커멘드는 Cargo.lock 파일을 무시하고 Cargo.toml에서 사양에 맞는 모든 최신 버전을 찾아낸다.

그러면 Cargo가 해당 버전을 Cargo.lock 파일에 기록한다.
그렇지 않으면 기본적으로 Cargo는 0.8.5 이상 0.9.0 미만 버전만 찾는다.
Rand crate에서 0.8.6과 0.9.0의 두 가지 새 버전을 출시한 경우, cargo update를 실행하면 다음과 같은 내용이 표시된다:

$ cargo update
Updating crates.io index
Updating rand v0.8.5 -> v0.8.6

카고는 0.9.0 를 무시한다.

현재 사용 중인 랜드 크레이트의 버전이 0.8.6임을 알리는 Cargo.lock 파일에도 변경 사항이 표시된다.

랜드 버전 0.9.0 또는 0.9.x 시리즈의 모든 버전을 사용하려면 Cargo.toml 파일을 다음과 같이 업데이트 해야한다:

[dependencies]
rand = "0.9.0"

다음에 cargo build를 실행하면 cargo에서 사용 가능한 crate 레지스트리를 업데이트하고 지정한 새 버전에 따라 rand 요구 사항을 재평가합니다.

Cargo그 생태계에 대해서는 14장에서 더 자세히 설명할 것이 많지만,지금은 여기까지만 알아두면 된다.

Cargo는 라이브러리를 매우 쉽게 재사용할 수 있게 해주므로,
러스트 사용자는 여러 패키지로 조립된 소규모 프로젝트를 작성할 수 있습니다.

랜덤 숫자 생성

rand를 사용하여 추측할 숫자를 생성해보자.
다음 단계는 목록 2-3에 표시된 것처럼 src/main.rs를 업데이트하는 것이다.

파일명:src/main.rs
use std::io;
use rand::Rng;

fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
Listing 2-3: Adding code to generate a random number

먼저 use rand::Rng; 줄을 추가하자.
Rng 특성은 난수 생성기가 구현하는 메서드를 정의하며,
해당 메서드를 사용하려면 이 특성이 범위 내에 있어야 한다.
10장에서는 특성에 대해 자세히 다룰 것이다.

다음으로 중간에 두 줄을 추가한다.
첫 번째 줄에서는 현재 실행 중인 스레드에 로컬이고 운영 체제에 의해 시드된 난수 생성기를 제공하는 rand::thread_rng 함수를 호출한다.

그런 다음 난수 생성기에서 gen_range 메서드를 호출한다.
이 메서드는 rand::Rng; 문을 사용하여 범위로 가져온 Rng 특성에 의해 정의된다. gen_range 메서드는 범위 표현식을 인수로 받아 해당 범위의 난수를 생성한다.
여기서 사용하는 범위 표현식은 시작 ...=끝 형식을 취하고 하한과 상한을 포함하므로,
1에서 100 사이의 숫자를 요청하려면 1...=100을 지정해야 한다.

두 번째 새 줄은 난수를 출력합니다.
이 기능은 프로그램을 테스트하기 위해 개발하는 동안에는 유용하지만 최종 버전에서는 삭제할 것.

프로그램을 몇번 실행 해보자:

$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53s
Running target/debug/guessing_game
Guess the number!
The secret number is: 7
Please input your guess.
4
You guessed: 4

$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running target/debug/guessing_game
Guess the number!
The secret number is: 83
Please input your guess.
5
You guessed: 5

사용자 입력 수와 난수 비교

이제 사용자 입력과 난수를 얻었으므로 이를 비교할 수 있다.

이 단계는 목록 2-4에 나와 있습니다.

이 코드는 아직 컴파일되지 않으므로 나중에 설명하겠다.

파일명:src/main.rs

use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
// --snip--
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}

목록 2-4: 두 숫자 비교 시 가능한 반환 값 처리하기

다른 use 문을 추가하여 표준 라이브러리에서 std::cmp::Ordering이라는 유형을 범위로 가져온다.
Ordering 유형은 또 다른 열거형이며 Less, Greater, Equal 변형이 있다.
두 값을 비교할 때 가능한 세 가지 결과이다.

그런 다음 하단에 Ordering 유형을 사용하는 5개의 새 줄을 추가한다.
cmp 메서드는 두 값을 비교하며 비교할 수 있는 모든 값에 대해 호출할 수 있다.
여기서는 guess와 secret_number를 비교하고 있습니다.
그런 다음 use 문을 통해 범위로 가져온 Ordering 열거형의 변형을 반환한다.
match expression을 사용하여 guess 및 secret_number의 값과 함께 cmp 호출 후,
반환된 Ordering의 변형을 기반으로 다음에 수행할 작업을 결정한다.

match expreesion은 arm으로 구성된다.
arm은 일치시킬 패턴과 일치시킬 값이 해당 arm의 패턴에 맞을 때 실행해야 하는 코드로 구성된다.
Rust는 match에게 주어진 값을 받아서 각 arm의 패턴을 차례로 살펴본다.
패턴과 match 구문은 코드에서 발생할 수 있는 다양한 상황을 표현하고 모든 상황을 처리할 수 있도록 해주는 강력한 Rust 기능입니다.
이 기능들은 각각 6장과 18장에서 자세히 다룰 것입니다.

여기서 사용하는 일치 표현식을 예로 들어보겠습니다.
사용자가 50을 입력했고 무작위로 생성된 번호가 38이라고 가정해 보겠습니다.

코드에서 50과 38을 비교하면 50이 38보다 크므로 cmp 메서드는 Ordering::Greater를 반환한다.
match expression은 Ordering::Greater 값을 가져와 각 arm의 패턴을 확인하기 시작한다.

첫 번째 암의 패턴인 Ordering::Less를 살펴본 결과 Ordering::Greater 값이 Ordering::Less와 일치하지 않으므로 해당 arm의 코드를 무시하고 다음 arm으로 이동한다.

다음 arm의 패턴은 Ordering::Greater로, Ordering::Greater와 일치하므로,
해당 암의 관련 코드가 실행되어 화면에 too big! 을 출력한다.

일치 표현식은 첫 번째 일치에 성공한 후에 종료되므로 이 시나리오에서는 마지막 arm을 보지 않는다.

그러나 목록 2-4의 코드는 아직 컴파일되지 않을것이다. 시도해보자 :

$ cargo build
   Compiling libc v0.2.86
   Compiling getrandom v0.2.2
   Compiling cfg-if v1.0.0
   Compiling ppv-lite86 v0.2.10
   Compiling rand_core v0.6.2
   Compiling rand_chacha v0.3.0
   Compiling rand v0.8.5
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types
  --> src/main.rs:22:21
   |
22 |     match guess.cmp(&secret_number) {
   |                 --- ^^^^^^^^^^^^^^ expected struct `String`, found integer
   |                 |
   |                 arguments to this function are incorrect
   |
   = note: expected reference `&String`
              found reference `&{integer}`
note: associated function defined here

For more information about this error, try `rustc --explain E0308`.
error: could not compile `guessing_game` due to previous error

오류의 핵심은 일치하지 않는 유형이 있다는 것이다.
Rust는 강력하고 정적인 타입 시스템을 가지고 있다.

하지만 유형 추론 기능도 있다.

let mut guess = String::new()를 작성했을 때,
Rust는 추측이 문자열이어야 한다는 것을 추론하고 해당 유형을 작성하지 않았다.

반면 secret_number는 숫자 타입이다.

32비트 숫자 i32, 부호 없는 32비트 숫자 u32, 64비트 숫자 i64 등 Rust의 숫자 유형 중 일부는 1에서 100 사이의 값을 가질 수 있다.

Rust가 다른 숫자 유형을 유추할 수 있도록 다른 곳에 유형 정보를 추가하지 않는 한,
Rust는 기본적으로 비밀 번호의 유형인 i32를 사용한다.

이 오류의 원인은 Rust가 문자열과 숫자 유형을 비교할 수 없기 때문이다.

궁극적으로 프로그램이 입력으로 읽은 문자열을 실수 유형으로 변환하여 비밀 번호와 숫자로 비교할 수 있도록 해보자.
이를 위해 메인 함수 본문에 이 코드를 추가해보자:

파일명:src/main.rs

    // --snip--

    let mut guess = String::new();

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

    let guess: u32 = guess.trim().parse().expect("Please type a number!");

    println!("You guessed: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }

let guess: u32 = guess.trim().parse().expect("Please type a number!");

guess이라는 변수를 만들자. 하지만 잠깐만, 프로그램에 이미 guess라는 변수가 있지 않나?

하지만 다행히도 Rust를 사용하면 guess의 이전 값을 새 값으로 shadowing 할 수 있다.

섀도잉을 사용하면 guess 변수 이름을 재사용할 수 있으므로,
예를 들어 guess_str과 guess와 같은 두 개의 고유 변수를 굳이 만들지 않아도 된다.

이 기능은 3장에서 더 자세히 다루겠지만, 지금은 한 유형에서 다른 유형으로 값을 변환할 때 자주 사용된다는 점을 알아두자.

이 새 변수를 guess.trim().parse() 표현식에 바인딩하자.
표현식의 guess는 입력이 문자열로 포함된 원래의 guess 변수를 참조한다.

문자열 인스턴스의 trim method는 시작과 끝의 공백을 제거하는데,
이는 문자열을 숫자 데이터만 포함할 수 있는 u32와 비교하기 위해 반드시 수행해야 하는 작업이다.
사용자는 엔터 키를 눌러 read_line을 충족하고 숫자를 입력해야 하며,
그러면 문자열에 개행 문자가 추가된다.

예를 들어, 사용자가 5를 입력하고 Enter 키를 누르면 guess 값은 다음과 같이 표시된다:
5\n. n은 "개행"을 나타낸다.

(Windows에서 Enter 키를 누르면 carriage 리턴과 개행인 \r\n이 생성된다.)
trim method는 \n 또는 \r\n을 제거하여 5만 남는다.

문자열의 parse method는 문자열을 다른 타입으로 변환한다.

여기서는 문자열을 숫자로 변환하는 데 사용한다.

let guess: u32를 사용하여 원하는 정확한 숫자 유형을 Rust에 알려줘야 한다.

guess 뒤에 오는 콜론(:)은 변수의 유형을 Rust에 알려준다.
Rust에는 몇 가지 기본 제공 숫자 유형이 있으며, 여기에 표시된 u32는 부호 없는 32비트 정수이다.

u32는 작은 양의 숫자에 대한 좋은 기본 선택이다.
다른 숫자 유형에 대해서는 3장에서 배우게 될 것이다.

또한, 이 예제 프로그램의 secret_number 와의 비교는 Rust가 secret_numberu32여야 한다고 추론한다는 것을 의미한다.
이제 비교는 동일한 유형의 두 값 사이에서 이루어진다

parse method는 숫자로 변환될 수 있는 글자일때만 동작하기 때문에 쉽게 에러를 야기할 수 있다.

예를 들어 문자열에 A👍%가 포함된 경우 이를 숫자로 변환할 방법이 없다.

실패할 수 있기 때문에 parse 메서드는 read_line 메서드와 마찬가지로 Result type을 반환한다.
("결과의 잠재적 실패 처리하기"에서 설명).

expect 메서드를 다시 사용하여 이 Result를 같은 방식으로 처리해보자.
문자열에서 숫자를 생성할 수 없어 parse가 Err 결과 변형을 반환하는 경우,
expect는 게임을 멈추고 우리가 제공한 메시지를 출력합니다.
parse가 문자열을 숫자로 성공적으로 변환할 수 있으면 Result의 Ok 변형을 리턴하고
기대는 Ok 값에서 원하는 숫자를 리턴합니다.

이제 프로그램을 실행 해보자:

$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 0.43s
Running target/debug/guessing_game
Guess the number!
The secret number is: 58
Please input your guess.
76
You guessed: 76
Too big!

추측하기 전에 공백이 추가되었지만 프로그램은 여전히 사용자가 76을 입력했다는 것을 확인했다.
프로그램을 몇 번 실행하여 숫자를 정확하게 맞혔는지, 너무 높은 숫자를 입력했는지, 너무 낮은 숫자를 입력했는지 등 다양한 종류의 입력에 따른 다른 동작을 확인합니다.

현재 대부분의 게임이 작동하고 있지만 사용자는 한 가지 추측만 할 수 있다.
루프를 추가하여 이를 변경 해보자.

반복문을 이용한 다중 입력 허용

loop 키워드는 무한 루프를 생성합니다.
사용자가 숫자를 맞출 수 있는 기회를 더 많이 제공하기 위해 루프를 추가할 것입니다:

파일명:src/main.rs

    // --snip--

    println!("The secret number is: {secret_number}");

    loop {
        println!("Please input your guess.");

        // --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => println!("You win!"),
        }
    }
}

보시다시피, 추측 입력 프롬프트부터 모든 것을 루프로 옮겼다.
루프 내부의 줄을 각각 네 칸씩 들여쓰고 프로그램을 다시 실행해보자.
이제 프로그램이 계속해서 다른 추측을 요청하므로 실제로 새로운 문제가 발생한다.
그건 바로 사용자가 종료할 수가 없다는것.

사용자는 언제든지 키보드 단축키 ctrl-c를 사용하여 프로그램을 중단할 수 있다.
하지만 "입력 받은 번호와 난수 비교"의 parse discussion에서 언급했듯이,
사용자가 숫자가 아닌 답을 입력하면 프로그램이 충돌하는 방법이 있다.
이 점을 이용해 아래와 같이 사용자가 종료할 수 있도록 할 수 있다:

$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 1.50s
Running target/debug/guessing_game
Guess the number!
The secret number is: 59
Please input your guess.
45
You guessed: 45
Too small!
Please input your guess.
60
You guessed: 60
Too big!
Please input your guess.
59
You guessed: 59
You win!
Please input your guess.
quit
thread 'main' panicked at 'Please type a number!: ParseIntError { kind: InvalidDigit }', src/main.rs:28:47
note: run with RUST_BACKTRACE=1 environment variable to display a backtrace

quit을 입력하면 게임이 종료되지만,
숫자가 아닌 다른 입력을 입력해도 게임이 종료된다.
정확한 숫자를 맞혔을 때도 게임이 종료되기를 원하기 때문에 이는 차선책이라고 할 수 있다.

숫자를 맞춘후 종료

유저가 이겼을떄 프로그램이 종료 되도록 break 구문을 추가 해보자:

파일명:src/main.rs

        // --생략--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

You Win 뒤에 break를 추가해줌으로써 유저가 숫자를 맞추면 프로그램이 반복문을 탈출하도록 한다.

루프는 main 의 마지막 부분이기 때문에 루프를 종료한다는 것은 프로그램을 종료한다는 의미이기도 하다.

잘못된 입력 처리

게임의 동작을 더 세분화하기 위해 사용자가 숫자가 아닌 것을 입력할 때,
프로그램을 충돌시키는 대신 숫자가 아닌 것을 무시하여 사용자가 계속 추측할 수 있도록 만들어 보자.
목록 2-5에 표시된 것처럼 추측이 문자열에서 u32로 변환되는 줄을 변경하여 이를 수행할 수 있습니다.

파일명: src/main.rs

        // 생략

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

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {guess}");

        // 생략

목록 2-5: 숫자가 아닌 추측을 무시하고 프로그램 충돌 대신 다른 추측을 요청하기

expect 에서 match expression으로 전환하여 오류 발생 시 충돌에서 오류 처리로 전환합니다. parse 는 Result type을 반환하고 Result는 Ok 및 Err type을 포함하는 열거형이라는 점을 기억하세요.
여기서는 cmp 메서드의 Ordering 결과에서와 마찬가지로 match 표현식을 사용하고 있습니다.

parse가 문자열을 숫자로 성공적으로 변환할 수 있으면 결과 숫자가 포함된 Ok 값을 반환한다.
Ok 값은 첫 번째 arm의 패턴과 일치하며, match expression은 parse가 생성한 num 값을 반환하고 Ok 안에 넣는다.
이 숫자는 우리가 생성하는 새 guess 변수의 원하는 위치에 바로 배치된다.

parse가 문자열을 숫자로 변환할 수 없는 경우 오류에 대한 자세한 정보가 포함된 Err 값을 반환 한다.
Err 값은 첫 번째 일치 항목의 Ok(num) 패턴과 일치하지 않지만, 두 번째 일치 항목의 Err() 패턴과 일치한다.
밑줄인
는 포괄적인 값으로, 이 예제에서는 어떤 정보가 들어 있든 상관없이 모든 Err 값을 일치시키겠다는 의미이다.
따라서 프로그램은 두 번째 arm의 코드를 실행하고 계속하여 루프의 다음 반복으로 이동하여 다른 입력을 요청한다.
따라서 사실상 프로그램은 parse에서 발생할 수 있는 모든 오류를 무시한다.

이제 프로그램의 모든 것이 예상대로 작동할것이다. 한번 시도 해보자:

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

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

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {guess}");

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

목록 2-6: 전체 추측 게임 코드

이 시점에서 guessing game을 성공적으로 구축했다.

요약

이 프로젝트는 let, match, functions, external crate 사용 등 많은 새로운 Rust 개념을 소개하는 실습 방식이다.

다음 몇 장에서는 이러한 개념에 대해 더 자세히 배우게 될 것이다.

3장에서는 변수, 데이터 타입, 함수 등 대부분의 프로그래밍 언어가 가지고 있는 개념을 다루고 Rust에서 이러한 개념을 사용하는 방법을 보여 줄것이다.

4장에서는 다른 언어와 다른 Rust의 특징인 소유권에 대해 살펴 볼것이다.

5장에서는 구조체와 메서드 구문에 대해 설명하고,

6장에서는 열거형의 작동 원리를 설명한다.

profile
Blockchain developer

0개의 댓글