소개
숫자 맞추기 게임 프로그래밍에 오신 것을 환영합니다. 이 랩은 Rust Book의 일부입니다. LabEx 에서 Rust 기술을 연습할 수 있습니다.
이 랩에서는 Rust 로 숫자 맞추기 게임을 구현할 것입니다. 프로그램이 임의의 숫자를 생성하고 플레이어에게 추측하도록 요청하며, 추측이 너무 낮은지 또는 너무 높은지에 대한 피드백을 제공하고, 플레이어가 정답을 맞히면 축하합니다.
숫자 맞추기 게임 프로그래밍
함께 실습 프로젝트를 진행하며 Rust 에 뛰어들어 봅시다! 이 챕터에서는 실제 프로그램에서 사용하는 방법을 보여줌으로써 몇 가지 일반적인 Rust 개념을 소개합니다. let, match, 메서드 (methods), 연관 함수 (associated functions), 외부 크레이트 (external crates) 등에 대해 배우게 됩니다! 다음 챕터에서는 이러한 아이디어를 더 자세히 살펴보겠습니다. 이 챕터에서는 기본 사항을 연습할 것입니다.
고전적인 초보 프로그래밍 문제인 숫자 맞추기 게임을 구현할 것입니다. 작동 방식은 다음과 같습니다. 프로그램은 1 에서 100 사이의 임의의 정수를 생성합니다. 그런 다음 플레이어에게 추측을 입력하라는 메시지를 표시합니다. 추측이 입력되면 프로그램은 추측이 너무 낮은지 또는 너무 높은지 표시합니다. 추측이 정확하면 게임은 축하 메시지를 출력하고 종료됩니다.
새 프로젝트 설정
새 프로젝트를 설정하려면 1 장에서 생성한 project 디렉토리로 이동하여 다음과 같이 Cargo 를 사용하여 새 프로젝트를 만듭니다.
cargo new guessing_game
cd guessing_game
첫 번째 명령인 cargo new는 프로젝트 이름 (guessing_game) 을 첫 번째 인수로 사용합니다. 두 번째 명령은 새 프로젝트의 디렉토리로 변경합니다.
생성된 Cargo.toml 파일을 살펴보십시오.
파일 이름: Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"
## See more keys and their definitions at
https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
1 장에서 보았듯이 cargo new는 "Hello, world!" 프로그램을 생성합니다. src/main.rs 파일을 확인하십시오.
파일 이름: src/main.rs
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 파일을 다시 엽니다. 이 파일에 모든 코드를 작성할 것입니다.
추측 처리
숫자 맞추기 게임 프로그램의 첫 번째 부분은 사용자 입력을 요청하고, 해당 입력을 처리하며, 입력이 예상된 형식인지 확인합니다. 먼저 플레이어가 추측을 입력하도록 허용합니다. Listing 2-1 의 코드를 src/main.rs에 입력하십시오.
파일 이름: 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}");
}
Listing 2-1: 사용자로부터 추측을 얻어 출력하는 코드
이 코드는 많은 정보를 포함하고 있으므로 한 줄씩 살펴보겠습니다. 사용자 입력을 얻은 다음 결과를 출력으로 인쇄하려면 io 입/출력 라이브러리를 범위 내로 가져와야 합니다. io 라이브러리는 std로 알려진 표준 라이브러리에서 제공됩니다.
use std::io;
기본적으로 Rust 에는 모든 프로그램의 범위 내로 가져오는 표준 라이브러리에 정의된 일련의 항목이 있습니다. 이 세트는 prelude라고 하며, https://doc.rust-lang.org/std/prelude/index.html에서 모든 것을 볼 수 있습니다.
사용하려는 타입이 prelude 에 없으면 use 문을 사용하여 해당 타입을 명시적으로 범위 내로 가져와야 합니다. std::io 라이브러리를 사용하면 사용자 입력을 허용하는 기능을 포함하여 여러 유용한 기능을 사용할 수 있습니다.
1 장에서 보았듯이 main 함수는 프로그램의 진입점입니다.
fn 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 에서 변수는 기본적으로 불변 (immutable) 입니다. 즉, 변수에 값을 할당하면 해당 값은 변경되지 않습니다. 이 개념은 "변수와 가변성"에서 자세히 논의할 것입니다. 변수를 가변 (mutable) 하게 만들려면 변수 이름 앞에 mut를 추가합니다.
let apples = 5; // immutable
let mut bananas = 5; // mutable
참고:
//구문은 줄 끝까지 이어지는 주석을 시작합니다. Rust 는 주석의 모든 내용을 무시합니다. 주석에 대해서는 3 장에서 자세히 논의할 것입니다.
숫자 맞추기 게임 프로그램으로 돌아가서, 이제 let mut guess가 guess라는 가변 변수를 도입한다는 것을 알았습니다. 등호 (=) 는 Rust 에 이제 변수에 무언가를 바인딩하려는 것을 알려줍니다. 등호 오른쪽에 있는 것은 guess가 바인딩된 값이며, 이는 String::new를 호출한 결과입니다. String::new은 String의 새 인스턴스를 반환하는 함수입니다. String은 표준 라이브러리에서 제공하는 문자열 타입으로, 크기가 늘어날 수 있는 UTF-8 인코딩 텍스트입니다.
::new 줄의 :: 구문은 new가 String 타입의 연관 함수임을 나타냅니다. 연관 함수(associated function) 는 이 경우 String과 같이 타입에 대해 구현된 함수입니다. 이 new 함수는 새롭고 빈 문자열을 생성합니다. 많은 타입에서 new 함수를 찾을 수 있는데, 이는 어떤 종류의 새 값을 만드는 함수에 대한 일반적인 이름이기 때문입니다.
전체적으로, let mut guess = String::new(); 줄은 현재 새롭고 빈 String 인스턴스에 바인딩된 가변 변수를 생성했습니다. 휴!
사용자 입력 받기
프로그램의 첫 번째 줄에서 use std::io;를 사용하여 표준 라이브러리에서 입/출력 기능을 포함했음을 기억하십시오. 이제 io 모듈에서 stdin 함수를 호출하여 사용자 입력을 처리할 수 있습니다.
io::stdin()
.read_line(&mut guess)
프로그램 시작 부분에서 use std::io;를 사용하여 io 라이브러리를 가져오지 않았다면, 이 함수 호출을 std::io::stdin으로 작성하여 여전히 함수를 사용할 수 있습니다. stdin 함수는 터미널의 표준 입력을 처리하는 핸들을 나타내는 타입인 std::io::Stdin의 인스턴스를 반환합니다.
다음으로, .read_line(&mut guess) 줄은 표준 입력 핸들에서 read_line 메서드를 호출하여 사용자로부터 입력을 받습니다. 또한 &mut guess를 read_line에 인수로 전달하여 사용자 입력을 저장할 문자열을 지정합니다. read_line의 전체 작업은 사용자가 표준 입력에 입력한 모든 내용을 문자열에 추가하는 것입니다 (내용을 덮어쓰지 않고). 따라서 해당 문자열을 인수로 전달합니다. 문자열 인수는 메서드가 문자열의 내용을 변경할 수 있도록 가변적이어야 합니다.
&는 이 인수가 참조(reference) 임을 나타내며, 여러 코드 부분이 데이터를 여러 번 메모리에 복사할 필요 없이 하나의 데이터 조각에 액세스할 수 있는 방법을 제공합니다. 참조는 복잡한 기능이며, 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는 열거형(enumeration) 이며, 종종 enum이라고 불리며, 여러 가능한 상태 중 하나일 수 있는 타입입니다. 각 가능한 상태를 변형(variant) 이라고 부릅니다.
6 장에서는 열거형에 대해 자세히 다룰 것입니다. 이러한 Result 타입의 목적은 오류 처리 정보를 인코딩하는 것입니다.
Result의 변형은 Ok와 Err입니다. Ok 변형은 작업이 성공했음을 나타내며, Ok 내부에는 성공적으로 생성된 값이 있습니다. Err 변형은 작업이 실패했음을 의미하며, Err에는 작업이 실패한 방법 또는 이유에 대한 정보가 포함되어 있습니다.
모든 타입의 값과 마찬가지로 Result 타입의 값에는 메서드가 정의되어 있습니다. Result의 인스턴스에는 호출할 수 있는 expect 메서드가 있습니다. 이 Result의 인스턴스가 Err 값인 경우, expect는 프로그램을 충돌시키고 expect에 인수로 전달한 메시지를 표시합니다. read_line 메서드가 Err를 반환하는 경우, 이는 기본 운영 체제에서 발생한 오류의 결과일 가능성이 큽니다. 이 Result의 인스턴스가 Ok 값인 경우, expect는 Ok가 가지고 있는 반환 값을 가져와서 해당 값만 반환하므로 사용할 수 있습니다. 이 경우, 해당 값은 사용자의 입력에 있는 바이트 수입니다.
expect를 호출하지 않으면 프로그램이 컴파일되지만 경고가 표시됩니다.
$ 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에서 반환된 Result 값을 사용하지 않았으며, 이는 프로그램이 가능한 오류를 처리하지 않았음을 나타냅니다.
경고를 억제하는 올바른 방법은 실제로 오류 처리 코드를 작성하는 것이지만, 이 경우 문제가 발생하면 프로그램을 충돌시키고 싶으므로 expect를 사용할 수 있습니다. 9 장에서 오류로부터 복구하는 방법에 대해 배우게 됩니다.
println! 자리 표시자를 사용하여 값 출력하기
닫는 중괄호 외에는 지금까지 코드에서 논의할 줄이 하나 더 있습니다.
println!("You guessed: {guess}");
이 줄은 이제 사용자의 입력을 포함하는 문자열을 출력합니다. {} 중괄호 집합은 자리 표시자입니다. {}를 값을 제자리에 고정하는 작은 게 집게발이라고 생각하십시오. 변수의 값을 출력할 때 변수 이름을 중괄호 안에 넣을 수 있습니다. 표현식의 평가 결과를 출력할 때는 형식 문자열에 빈 중괄호를 넣은 다음, 각 빈 중괄호 자리 표시자에 출력할 표현식의 쉼표로 구분된 목록을 동일한 순서로 형식 문자열을 따릅니다. 변수와 표현식의 결과를 println! 한 번 호출하여 출력하는 것은 다음과 같습니다.
let x = 5;
let y = 10;
println!("x = {x} and y + 2 = {}", y + 2);
이 코드는 x = 5 and y = 12를 출력합니다.
첫 번째 부분 테스트하기
추측 게임의 첫 번째 부분을 테스트해 보겠습니다. 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 팀은 해당 기능을 갖춘 rand crate 를 https://crates.io/crates/rand에서 제공합니다.
더 많은 기능을 얻기 위해 크레이트 사용하기
크레이트는 Rust 소스 코드 파일의 모음이라는 것을 기억하세요. 우리가 구축해 온 프로젝트는 실행 파일인 바이너리 크레이트입니다. rand 크레이트는 다른 프로그램에서 사용하도록 설계된 코드를 포함하고 자체적으로 실행될 수 없는 라이브러리 크레이트입니다.
Cargo 가 외부 크레이트를 조정하는 방식은 Cargo 가 진정으로 빛을 발하는 부분입니다. rand를 사용하는 코드를 작성하기 전에, Cargo.toml 파일을 수정하여 rand 크레이트를 종속성으로 포함해야 합니다. 지금 해당 파일을 열고 Cargo 가 생성한 [dependencies] 섹션 헤더 아래에 다음 줄을 추가합니다. 여기에 표시된 대로 정확하게 rand를 지정하고, 이 버전 번호를 사용해야 합니다. 그렇지 않으면 이 튜토리얼의 코드 예제가 작동하지 않을 수 있습니다.
파일 이름: Cargo.toml
[dependencies]
rand = "0.8.5"
Cargo.toml 파일에서 헤더 뒤에 오는 모든 것은 다른 섹션이 시작될 때까지 해당 섹션의 일부입니다. [dependencies]에서 Cargo 에게 프로젝트가 의존하는 외부 크레이트와 해당 크레이트의 어떤 버전을 요구하는지 알려줍니다. 이 경우, 시맨틱 버전 지정자 0.8.5를 사용하여 rand 크레이트를 지정합니다. Cargo 는 버전 번호를 작성하기 위한 표준인 시맨틱 버전 관리 (때로는 SemVer라고도 함) 를 이해합니다. 지정자 0.8.5는 실제로 ^0.8.5의 약어이며, 이는 최소 0.8.5 이상이지만 0.9.0 미만인 모든 버전을 의미합니다.
Cargo 는 이러한 버전을 버전 0.8.5 와 호환되는 공용 API 를 갖는 것으로 간주하며, 이 사양은 이 장의 코드와 함께 컴파일될 최신 패치 릴리스를 얻도록 보장합니다. 버전 0.9.0 이상은 다음 예제에서 사용하는 것과 동일한 API 를 갖는다고 보장할 수 없습니다.
이제 코드를 변경하지 않고 Listing 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 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_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
Listing 2-2: rand 크레이트를 종속성으로 추가한 후 cargo build를 실행한 결과
다른 버전 번호 (하지만 SemVer 덕분에 모두 코드와 호환됨) 와 다른 줄 (운영 체제에 따라 다름) 이 표시될 수 있으며, 줄의 순서가 다를 수 있습니다.
외부 종속성을 포함하면 Cargo 는 종속성이 필요로 하는 모든 것의 최신 버전을 레지스트리에서 가져옵니다. 레지스트리는 Crates.io 의 데이터 사본이며 https://crates.io에 있습니다. Crates.io 는 Rust 생태계의 사람들이 다른 사람들이 사용할 수 있도록 오픈 소스 Rust 프로젝트를 게시하는 곳입니다.
레지스트리를 업데이트한 후 Cargo 는 [dependencies] 섹션을 확인하고 아직 다운로드되지 않은 나열된 모든 크레이트를 다운로드합니다. 이 경우, rand만 종속성으로 나열했지만 Cargo 는 rand가 작동하기 위해 의존하는 다른 크레이트도 가져왔습니다. 크레이트를 다운로드한 후 Rust 는 이를 컴파일한 다음 종속성을 사용할 수 있는 상태로 프로젝트를 컴파일합니다.
변경 사항 없이 즉시 cargo build를 다시 실행하면 Finished 줄을 제외하고는 아무런 출력도 얻지 못합니다. Cargo 는 이미 종속성을 다운로드하고 컴파일했음을 알고 있으며, Cargo.toml 파일에서 이에 대해 아무것도 변경하지 않았습니다. Cargo 는 또한 코드에 대해 아무것도 변경하지 않았다는 것을 알고 있으므로 다시 컴파일하지도 않습니다. 할 일이 없으면 단순히 종료됩니다.
src/main.rs 파일을 열고 사소한 변경을 한 다음 저장하고 다시 빌드하면 두 줄의 출력만 표시됩니다.
$ 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 파일에 대한 작은 변경 사항으로 빌드를 업데이트했음을 보여줍니다. 종속성이 변경되지 않았으므로 Cargo 는 이미 다운로드하고 컴파일한 것을 재사용할 수 있음을 알고 있습니다.
Cargo.lock 파일을 사용하여 재현 가능한 빌드 보장하기
Cargo 는 사용자와 다른 사람이 코드를 빌드할 때마다 동일한 아티팩트를 다시 빌드할 수 있도록 보장하는 메커니즘을 가지고 있습니다. Cargo 는 달리 지시하지 않는 한, 지정한 종속성의 버전만 사용합니다. 예를 들어, 다음 주에 rand 크레이트의 버전 0.8.6 이 출시되었고, 해당 버전에는 중요한 버그 수정 사항이 포함되어 있지만 코드에 문제를 일으키는 회귀도 포함되어 있다고 가정해 보겠습니다. 이를 처리하기 위해 Rust 는 cargo build를 처음 실행할 때 Cargo.lock 파일을 생성하므로 이제 guessing_game 디렉토리에 이 파일이 있습니다.
프로젝트를 처음 빌드할 때 Cargo 는 기준에 맞는 모든 종속성의 버전을 파악한 다음 이를 Cargo.lock 파일에 기록합니다. 향후 프로젝트를 빌드할 때 Cargo 는 Cargo.lock 파일이 존재함을 확인하고 버전 파악 작업을 다시 수행하는 대신 여기에 지정된 버전을 사용합니다. 이를 통해 자동으로 재현 가능한 빌드를 가질 수 있습니다. 즉, Cargo.lock 파일 덕분에 명시적으로 업그레이드할 때까지 프로젝트는 0.8.5 를 유지합니다. Cargo.lock 파일은 재현 가능한 빌드에 중요하므로 프로젝트의 나머지 코드와 함께 소스 제어에 자주 체크인됩니다.
새 버전을 얻기 위해 크레이트 업데이트하기
크레이트를 업데이트하려는 경우, Cargo 는 update 명령을 제공합니다. 이 명령은 Cargo.lock 파일을 무시하고 Cargo.toml에서 사양에 맞는 모든 최신 버전을 파악합니다. 그런 다음 Cargo 는 해당 버전을 Cargo.lock 파일에 기록합니다. 그렇지 않으면, 기본적으로 Cargo 는 0.8.5 보다 크고 0.9.0 보다 작은 버전만 찾습니다. rand 크레이트가 두 개의 새 버전 0.8.6 및 0.9.0 을 출시한 경우, cargo update를 실행하면 다음과 같은 출력을 볼 수 있습니다.
$ cargo update
Updating crates.io index
Updating rand v0.8.5 - > v0.8.6
Cargo 는 0.9.0 릴리스를 무시합니다. 이 시점에서 현재 사용 중인 rand 크레이트의 버전이 0.8.6 임을 나타내는 Cargo.lock 파일의 변경 사항도 확인할 수 있습니다. rand 버전 0.9.0 또는 0.9._x_ 시리즈의 모든 버전을 사용하려면, 대신 Cargo.toml 파일을 다음과 같이 업데이트해야 합니다.
[dependencies]
rand = "0.9.0"
다음에 cargo build를 실행하면 Cargo 는 사용 가능한 크레이트의 레지스트리를 업데이트하고 지정한 새 버전에 따라 rand 요구 사항을 다시 평가합니다.
Cargo 와 해당 생태계에 대해 더 많은 이야기가 있지만, 이에 대해서는 14 장에서 논의할 것입니다. 지금은 이 정도만 알아두면 됩니다. Cargo 는 라이브러리를 매우 쉽게 재사용할 수 있도록 하므로 Rustaceans 는 여러 패키지에서 조립된 더 작은 프로젝트를 작성할 수 있습니다.
난수 생성하기
rand를 사용하여 추측할 숫자를 생성해 보겠습니다. 다음 단계는 Listing 2-3 에 표시된 대로 src/main.rs를 업데이트하는 것입니다.
파일 이름: src/main.rs
use std::io;
1 use rand::Rng;
fn main() {
println!("Guess the number!");
2 let secret_number = rand::thread_rng().gen_range(1..=100);
3 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: 난수를 생성하는 코드 추가
먼저 use rand::Rng; 라인 [1]을 추가합니다. Rng 트레이트는 난수 생성기가 구현하는 메서드를 정의하며, 이러한 메서드를 사용하려면 이 트레이트가 범위 내에 있어야 합니다. 10 장에서 트레이트에 대해 자세히 다룰 것입니다.
다음으로, 중간에 두 줄을 추가하고 있습니다. 첫 번째 줄 [2]에서, 사용할 특정 난수 생성기를 제공하는 rand::thread_rng 함수를 호출합니다. 이 함수는 현재 실행 스레드에 로컬이며 운영 체제에 의해 시드됩니다. 그런 다음 난수 생성기에서 gen_range 메서드를 호출합니다. 이 메서드는 use rand::Rng; 문을 사용하여 범위 내로 가져온 Rng 트레이트에 의해 정의됩니다. gen_range 메서드는 범위 표현식을 인수로 받아 해당 범위 내에서 난수를 생성합니다. 여기서 사용하는 범위 표현식은 start..=end 형식을 취하며 하한 및 상한을 포함하므로 1 과 100 사이의 숫자를 요청하려면 1..=100을 지정해야 합니다.
참고: 어떤 트레이트를 사용하고 어떤 메서드와 함수를 크레이트에서 호출해야 하는지 정확히 알 수는 없으므로, 각 크레이트에는 사용 지침이 포함된 문서가 있습니다. Cargo 의 또 다른 멋진 기능은
cargo doc --open명령을 실행하면 모든 종속성에서 제공하는 문서를 로컬로 빌드하여 브라우저에서 열어준다는 것입니다. 예를 들어,rand크레이트의 다른 기능에 관심이 있다면cargo doc --open을 실행하고 왼쪽 사이드바에서rand를 클릭하십시오.
두 번째 새 줄 [3]은 비밀 번호를 출력합니다. 프로그램을 테스트할 수 있도록 프로그램을 개발하는 동안 유용하지만, 최종 버전에서는 삭제할 것입니다. 프로그램이 시작하자마자 답을 출력한다면 게임으로서의 의미가 없을 것입니다!
프로그램을 몇 번 실행해 보십시오.
$ 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
서로 다른 난수를 얻어야 하며, 모두 1 과 100 사이의 숫자여야 합니다. 잘하셨습니다!
추측을 비밀 번호와 비교하기
이제 사용자 입력과 난수가 있으므로 이를 비교할 수 있습니다. 해당 단계는 Listing 2-4 에 나와 있습니다. 이 코드는 아직 컴파일되지 않으며, 그 이유는 곧 설명하겠습니다.
파일 이름: src/main.rs
use rand::Rng;
1 use std::cmp::Ordering;
use std::io;
fn main() {
--snip--
println!("You guessed: {guess}");
2 match guess.3 cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
Listing 2-4: 두 숫자를 비교하여 가능한 반환 값 처리하기
먼저 std::cmp::Ordering이라는 유형을 표준 라이브러리에서 범위 내로 가져오는 또 다른 use 문 [1]을 추가합니다. Ordering 유형은 또 다른 열거형 (enum) 이며 Less, Greater, Equal 변형을 갖습니다. 이는 두 값을 비교할 때 가능한 세 가지 결과입니다.
그런 다음 Ordering 유형을 사용하는 다섯 줄을 추가합니다. cmp 메서드 [3]는 두 값을 비교하며 비교할 수 있는 모든 항목에서 호출할 수 있습니다. 비교하려는 항목에 대한 참조를 사용합니다. 여기서는 guess를 secret_number와 비교합니다. 그런 다음 use 문으로 범위 내로 가져온 Ordering 열거형의 변형을 반환합니다. match 표현식 [2]을 사용하여 guess와 secret_number의 값으로 cmp를 호출하여 반환된 Ordering의 변형에 따라 다음에 수행할 작업을 결정합니다.
match 표현식은 arm으로 구성됩니다. arm 은 일치시킬 pattern과 match에 제공된 값이 해당 arm 의 패턴에 맞는 경우 실행해야 하는 코드로 구성됩니다. Rust 는 match에 제공된 값을 가져와 각 arm 의 패턴을 차례로 확인합니다. 패턴과 match 구문은 강력한 Rust 기능입니다. 이를 통해 코드가 발생할 수 있는 다양한 상황을 표현하고 모든 상황을 처리할 수 있습니다. 이러한 기능은 각각 6 장과 18 장에서 자세히 다룰 것입니다.
여기서 사용하는 match 표현식의 예제를 살펴보겠습니다. 사용자가 50 을 추측했고 이번에 무작위로 생성된 비밀 번호가 38 이라고 가정해 보겠습니다.
코드가 50 을 38 과 비교하면 cmp 메서드는 50 이 38 보다 크기 때문에 Ordering::Greater를 반환합니다. match 표현식은 Ordering::Greater 값을 가져와 각 arm 의 패턴을 확인하기 시작합니다. 첫 번째 arm 의 패턴인 Ordering::Less를 보고 Ordering::Greater 값이 Ordering::Less와 일치하지 않으므로 해당 arm 의 코드를 무시하고 다음 arm 으로 이동합니다. 다음 arm 의 패턴은 Ordering::Greater이며, 이는 Ordering::Greater와 일치합니다! 해당 arm 의 관련 코드가 실행되어 화면에 Too big!을 출력합니다. match 표현식은 첫 번째 성공적인 일치 후 종료되므로 이 시나리오에서는 마지막 arm 을 보지 않습니다.
그러나 Listing 2-4 의 코드는 아직 컴파일되지 않습니다. 시도해 보겠습니다.
$ cargo build
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
|
= note: expected reference `&String`
found reference `&{integer}`
오류의 핵심은 유형 불일치가 있다는 것입니다. Rust 는 강력하고 정적인 유형 시스템을 가지고 있습니다. 그러나 유형 추론도 있습니다. let mut guess = String::new()을 작성했을 때 Rust 는 guess가 String이어야 함을 추론할 수 있었고 유형을 작성하지 않아도 되었습니다. 반면에 secret_number는 숫자 유형입니다. Rust 의 몇 가지 숫자 유형은 1 과 100 사이의 값을 가질 수 있습니다. i32는 32 비트 숫자, u32는 부호 없는 32 비트 숫자, i64는 64 비트 숫자 등입니다. 달리 지정하지 않는 한 Rust 는 i32를 기본값으로 사용하며, 이는 Rust 가 다른 숫자 유형을 추론하도록 하는 다른 유형 정보를 추가하지 않는 한 secret_number의 유형입니다. 오류의 이유는 Rust 가 문자열과 숫자 유형을 비교할 수 없기 때문입니다.
궁극적으로 프로그램이 입력으로 읽는 String을 실제 숫자 유형으로 변환하여 비밀 번호와 수치적으로 비교하려고 합니다. 이를 위해 main 함수 본문에 다음 줄을 추가합니다.
파일 이름: src/main.rs
use std::io;
use rand::Rng;
use std::cmp::Ordering;
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");
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!"),
}
}
guess라는 변수를 만듭니다. 하지만 잠깐, 프로그램에 이미 guess라는 변수가 있지 않습니까? 그렇습니다. 하지만 Rust 는 이전 guess 값을 새 값으로 섀도잉할 수 있습니다. 섀도잉을 사용하면 예를 들어 guess_str 및 guess와 같이 두 개의 고유한 변수를 만들 필요 없이 guess 변수 이름을 재사용할 수 있습니다. 이에 대해서는 3 장에서 자세히 다루겠지만, 지금은 이 기능이 한 유형의 값을 다른 유형으로 변환하려는 경우에 자주 사용된다는 것을 알아두십시오.
이 새 변수를 표현식 guess.trim().parse()에 바인딩합니다. 표현식의 guess는 입력을 문자열로 포함하는 원래 guess 변수를 참조합니다. String 인스턴스의 trim 메서드는 시작과 끝의 모든 공백을 제거합니다. 이는 문자열을 숫자 데이터만 포함할 수 있는 u32와 비교하기 위해 수행해야 합니다. 사용자는 read_line을 충족하고 추측을 입력하기 위해 Enter 키를 눌러야 하며, 이는 문자열에 줄 바꿈 문자를 추가합니다. 예를 들어 사용자가 5를 입력하고 Enter 키를 누르면 guess는 다음과 같습니다. 5\n. \n은 "줄 바꿈"을 나타냅니다. (Windows 에서는 Enter 키를 누르면 캐리지 리턴과 줄 바꿈, \r\n이 발생합니다.) trim 메서드는 \n 또는 \r\n을 제거하여 5만 남습니다.
문자열의 parse 메서드는 문자열을 다른 유형으로 변환합니다. 여기서는 문자열에서 숫자로 변환하는 데 사용합니다. let guess: u32를 사용하여 원하는 정확한 숫자 유형을 Rust 에 알려야 합니다. guess 뒤의 콜론 (:) 은 Rust 에 변수의 유형을 주석 처리하라고 알려줍니다. Rust 에는 몇 가지 내장 숫자 유형이 있습니다. 여기서 보이는 u32는 부호 없는 32 비트 정수입니다. 작은 양수에 대한 좋은 기본 선택입니다. 3 장에서 다른 숫자 유형에 대해 배우게 됩니다.
또한 이 예제 프로그램의 u32 주석과 secret_number와의 비교는 Rust 가 secret_number도 u32여야 함을 추론한다는 것을 의미합니다. 이제 비교는 동일한 유형의 두 값 사이에서 이루어집니다!
parse 메서드는 논리적으로 숫자로 변환할 수 있는 문자에 대해서만 작동하므로 쉽게 오류가 발생할 수 있습니다. 예를 들어 문자열에 A👍%가 포함된 경우 이를 숫자로 변환할 방법이 없습니다. 실패할 수 있으므로 parse 메서드는 read_line 메서드와 마찬가지로 Result 유형을 반환합니다 ("Result 로 잠재적 실패 처리"에서 설명). expect 메서드를 다시 사용하여 이 Result를 동일하게 처리합니다. parse가 문자열에서 숫자를 만들 수 없기 때문에 Err Result 변형을 반환하면 expect 호출이 게임을 중단하고 제공한 메시지를 출력합니다. parse가 문자열을 숫자로 성공적으로 변환할 수 있으면 Result의 Ok 변형을 반환하고 expect는 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
use std::io;
use rand::Rng;
use std::cmp::Ordering;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
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 = 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!"),
}
}
}
보시다시피, 추측 입력 프롬프트부터 모든 것을 루프로 이동했습니다. 루프 내부의 줄을 각각 4 칸 더 들여쓰고 프로그램을 다시 실행하십시오. 이제 프로그램은 영원히 다른 추측을 요청할 것이며, 실제로 새로운 문제를 야기합니다. 사용자가 종료할 수 없는 것처럼 보입니다!
사용자는 항상 키보드 단축키 ctrl-C 를 사용하여 프로그램을 중단할 수 있습니다. 하지만 "추측을 비밀 번호와 비교하기"의 parse 토론에서 언급했듯이, 이 만족할 줄 모르는 괴물을 탈출하는 또 다른 방법이 있습니다. 사용자가 숫자가 아닌 답을 입력하면 프로그램이 충돌합니다. 이를 활용하여 사용자가 종료할 수 있도록 할 수 있습니다.
$ 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
--snip--
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의 마지막 부분이기 때문입니다.
잘못된 입력 처리
게임의 동작을 더욱 개선하기 위해 사용자가 숫자가 아닌 값을 입력할 때 프로그램을 충돌시키는 대신, 게임이 숫자가 아닌 값을 무시하여 사용자가 계속 추측할 수 있도록 하겠습니다. Listing 2-5 에 표시된 대로 guess가 String에서 u32로 변환되는 줄을 변경하여 이를 수행할 수 있습니다.
파일 이름: src/main.rs
--snip--
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}");
--snip--
Listing 2-5: 프로그램 충돌 대신 숫자가 아닌 추측을 무시하고 다른 추측을 요청합니다.
expect 호출에서 match 표현식으로 전환하여 오류 발생 시 충돌하는 대신 오류를 처리합니다. parse가 Result 타입을 반환하고 Result는 Ok와 Err 변형을 갖는 열거형 (enum) 임을 기억하십시오. cmp 메서드의 Ordering 결과와 마찬가지로 여기에서 match 표현식을 사용하고 있습니다.
parse가 문자열을 숫자로 성공적으로 변환할 수 있다면 결과 숫자를 포함하는 Ok 값을 반환합니다. 해당 Ok 값은 첫 번째 arm 의 패턴과 일치하고, match 표현식은 parse가 생성하여 Ok 값 안에 넣은 num 값만 반환합니다. 해당 숫자는 우리가 만들고 있는 새로운 guess 변수에서 원하는 위치에 정확히 위치하게 됩니다.
parse가 문자열을 숫자로 변환할 수 없다면 오류에 대한 더 많은 정보를 포함하는 Err 값을 반환합니다. Err 값은 첫 번째 match arm 의 Ok(num) 패턴과 일치하지 않지만 두 번째 arm 의 Err(_) 패턴과 일치합니다. 밑줄, _는 모든 값을 포괄하는 값입니다. 이 예에서는 모든 Err 값과 일치시키려고 합니다. 즉, 그 안에 어떤 정보가 있든 상관없이 일치시키려고 합니다. 따라서 프로그램은 두 번째 arm 의 코드인 continue를 실행합니다. 이는 프로그램이 loop의 다음 반복으로 이동하여 다른 추측을 요청하도록 지시합니다. 따라서 프로그램은 parse가 발생할 수 있는 모든 오류를 효과적으로 무시합니다!
이제 프로그램의 모든 것이 예상대로 작동해야 합니다. 시도해 보겠습니다.
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 4.45s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 61
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
99
You guessed: 99
Too big!
Please input your guess.
foo
Please input your guess.
61
You guessed: 61
You win!
훌륭합니다! 마지막 작은 조정을 통해 추측 게임을 완료할 것입니다. 프로그램이 여전히 비밀 번호를 출력하고 있음을 기억하십시오. 이는 테스트에는 좋았지만 게임을 망칩니다. 비밀 번호를 출력하는 println!을 삭제해 보겠습니다. Listing 2-6 은 최종 코드를 보여줍니다.
파일 이름: src/main.rs
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;
}
}
}
}
Listing 2-6: 완성된 추측 게임 코드
이 시점에서 추측 게임을 성공적으로 구축했습니다. 축하합니다!
요약
축하합니다! 추측 게임 프로그래밍 랩을 완료했습니다. LabEx 에서 더 많은 랩을 연습하여 기술을 향상시킬 수 있습니다.