추측 게임을 통해 Rust의 개념에 대해 알아보고, let
, match
, method
, 관련 function
, 외부 crates
사용 방법에 대해 알아보자.
cargo new guessing_game
cd guessing_game
추측 게임 프로그램의 첫 부분은 사용자에게 입력을 요청하고, 해당 입력이 예상한 형식인지 확인하는 것 입니다.
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);
}
사용자의 입력을 받기 위해 std::io
라이브러리를 사용하였습니다.
Rust의 기본 라이브러리 정보는 prelude에서 확인할 수 있습니다.
다음과 같이 사용자 입력값을 저장할 변수를 초기화 할 수 있습니다.
let mut guess = String::new();
let
은 변수를 생성하는데 사용되는 문법 입니다.
mut
은 변수를 수정할 수 있는 특성을 부여하는 문법 입니다.
let apples = 5; // 수정이 불가능한 변수
let mut bananas = 5; // 수정이 가능한 변수
다시 코드로 돌아가서 ley mut guess
는 변경 가능한 변수 quess
를 의미합니다.
String
은 표준 라이브러리 에서 제공하는 문자열 타입이며, new
를 이용해 비어있는 새로운 String
타입의 인스턴스를 생성하게 됩니다.
코드 첫 줄에 입력한 use std::io
를 통해 터미널 표준 입력에 대한 핸들러 stdin
함수의 사용할 수 있게 됩니다.
io::stdin()
.read_line(&mut guess)
.read_line(&mut guess)
의 read_line
메서드를 호출하여 사용자로 부터 입력을 받고 그 값을 guess
변수에 저장합니다.
&
인수를 이용하여 &mut guess
를 인수로 전달하면 guess
변수를 수정할 수 있는 참조를 반환합니다.
참조 변수는 메모리를 여러번 복사할 필요 없이 데이터의 코드 위치를 참조하는 것을 허용합니다.
Rust는 메모리 참조 쉽고 안전하게 사용할 수 있도록 제공하며, 세부사항은 신경쓰지 않아도 됩니다.
변수의 선언과 마찬가지로 &mut guess
는 변경 가능한 참조 변수, &guess
는 변경 불가능한 참조 변수를 의미합니다.
expect
는 read_line
의 실패를 처리하는 데 사용됩니다.
.expect("Failed to read line");
.method_name()
구문을 사용하는 메서드를 호출할 때 긴 줄을 구분하기 위해 개행 및 공백을 사용하는것이 좋습니다.
즉 다음 두 코드는 동일하게 동작하는 것입니다.
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
io::stdin().read_line(&mut guess).expect("Failed to read line");
다시 코드로 돌아와서, read_line
은 사용자가 입력한 내용을 변수에 전달하는 기능을 하지만, std::Result
도 반환 합니다.
사용자가 입력한 내용이 없을 경우 오류를 발생시키는 것을 방지하기 위해 Result
타입을 사용합니다.
Result
는 enum
타입으로, Ok
또는 Err`를 가지고 있습니다.
Err
는 작업 실패를 의미하며, 실패 방법이나 이유에 대한 정보를 가지고 있습니다.
이러현 Result
타입의 목적은 오류 처리 정보를 인코딩 하는 것으로, 이 Result
에는 호출 가능한 expect
메서드가 있으며, io::Result
가 Err
일 경우 expect
에 의해 메시지가 출력 되는 것 입니다.
또한 exprct
를 호출하지 않을 경우 컴파일은 되지만 경고가 출력됩니다.
$ cargo build
Compiling guessing_game v0.1.0 (/rust/study/guessing_game)
warning: unused `Result` that must be used
--> src/main.rs:10:5
|
10 | / io::stdin()
11 | | .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.24s
Rust는 read_line
에서 오류 처리를 제공하는 Result
에 대한 반환 값을 사용하지 않았다고 경고하고 있습니다.
컴파일 단계에서 warning
메시지는 실행에는 문제가 없을지 몰라도, 문제가 발생했을 때 프로그램을 중단시키기 때문에 expect
를 이용해 오류 북구를 처리하는 것이 좋습니다.
println!
을 이용해 입력받은 guess
를 출력 합니다.
println!("You guessed: {}", guess);
중괄호를 이용하여 변수를 출력할 수 있습니다.
cargo run
명령어를 사용하여 추측 게임의 첫 번째 부분을 테스트해 보겠습니다.
cargo run
Compiling guessing_game v0.1.0 (/rust/study/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 0.24s
Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
10
You guessed: 10
게임의 첫 부분인 키보드 입력을 받고 출력하는 부분까지 완성 되었습니다.
사용자가 추측해야 하는 비밀번호를 생성 해야 합니다.
비밀번호는 매번 달라야 게임이 재미있으며, 너무 어렵지 않도록 1에서 100까지의 숫자를 생성 해야 합니다.
Rust는 아직 표준 라이브러리에서 난수 기능을 제공하지 않고 있기 때문에 rand crates를 사용할 것입니다.
crates는 다른 프로젝트에서 사용할 수 있도록 제공되는 Rust 소스 코드 파일의 모음입니다.
다음과 같이 Cargo.toml
에 [dependencies]
섹션 정보에 crates 정보를 추가해 rand
crates를 사용할 수 있도록 설정합니다.
rand = "0.8.4"
위 정보는 rand
crates 0.8.4
버전을 사용하겠다는 의미입니다.
Cargo는 버전 정보 작성을 위한 Semantic Versioning을 지원합니다.
버전 정보 0.8.4
는 실제로는 ^0.8.4
의 줄임말이며, 0.8.4
이상 0.9.0
이하의 모든 버전을 의미합니다.
Cargo는 ^0.8.4
에 해당하는 버전이 호환되는 API를 갖춘것으로 간주하며, 최신 패치 릴리스를 얻을 수 있음을 보장합니다.
코드 변경없이 프로젝트를 빌드해 보겠습니다.
cargo build
Updating crates.io index
Downloaded rand_core v0.6.3
Downloaded cfg-if v1.0.0
Downloaded rand_chacha v0.3.1
Downloaded ppv-lite86 v0.2.15
Downloaded rand v0.8.4
Downloaded libc v0.2.108
Downloaded getrandom v0.2.3
Downloaded 7 crates (740.3 KB) in 0.96s
Compiling libc v0.2.108
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.15
Compiling getrandom v0.2.3
Compiling rand_core v0.6.3
Compiling rand_chacha v0.3.1
Compiling rand v0.8.4
Compiling guessing_game v0.1.0 (/rust/study/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 25.97s
외부 종속성이 생겼으므로 Cargo는 crates.io의 데이터 복사본인 레지스트리에서 모든 최신 버전을 가져옵니다.
crates.io는 Rust 생태계의 다른 사람들이 사용할 수 있도록 오픈소스 Rust 프로젝트를 게시하는 곳 입니다.
레지스트리를 업데이트한 후 Cargo는 [dependencies]
섹션을 확인하고 가지고 있지 않은 crates를 다운로드 합니다.
Cargo.toml
에는 rand
만 추가하였지만, rand
에 종속된 다른 crates도 다운로드하고 이를 컴파일해, 사용 가능한 종속성을 갖춘 프로젝트를 만들어 내는 것입니다.
변경 없이 다시한번 cargo build
를 실행할 경우, Cargo는 이미 종속성을 다운로드하고 컴파일 했기 때문에 Finished
만 출력됩니다.
종속성 정보 변경 없이 코드만 수정되었을 경우에도 종속성 다운로드 없이 빌드만 진행하게 됩니다.
만약 rand
crates가 버전업이 되었고, 제공되는 기능이 변경 되어 프로젝트의 코드를 손상시킨다 해도, Cargo.lock
에 지정된 버전 정보를 이용하기 때문에 재현 가능한 빌드를 구성할 수 있습니다.
즉 Cargo.lock
에 명시된 버전으로 crates 사용함으로 프로젝트가 유지될 수 있습니다.
crates를 명시적으로 업데이트할 경우 cargo update
를 이용해 버전을 업데이트할 수 있습니다.
rand
crates를 추가했으므로, 이제 rand
를 사용하여 난수를 생성할 수 있습니다.
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..101);
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);
}
rand::thread_rng
는 난수를 생성하는 랜덤 생성기를 반환합니다.
현재 실행 스레드에 로컬이고 운영 체제에 의해 시드됩니다. 그런다음 난수 생성기에서 gen_range
를 사용하여 난수를 생성합니다.
이 gen_range
메서드는 범위 식을 인수로 사용하고 범위에서 난수를 생성합니다.
여기서 범위 표현식의 형식은 start..end
이며, 하한에는 포함되지만 상한에는 배타적이므로, 1에서 100사이의 숫자를 생성하려면 1..101
과 같이 지정하거나, 1..=100
와 같이 범위를 전달할 수도 있습니다.
crates의 메소드와 함수가 어떤 특성을 사용하는지 알아보기 위해서는
cargo doc
를 실행하면 됩니다.cargo doc --open
를 실행해 모든 종속 항목에서 제공하는 문서를 로컬에서 빌드하고 브라우저에서 확인할 수 있습니다.
테스트 중 어떤 비밀번호가 생성되는지 확인할 수 있도록 비밀번호를 출력하는 코드도 추가했습니다.
프로그램을 실행해 봅시다.
cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.28s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 54
Please input your guess.
10
You guessed: 10
cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 42
Please input your guess.
31
You guessed: 31
1과 100사이의 임의의 숫자가 생성되는 것을 확인할 수 있습니다.
이제 사용자 입력과 난수가 있으므로 비교할 수 있습니다.
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!"),
}
}
표준 라이브러리 std::cmp::Ordering
이 추가 되었습니다.
Result
와 마찬가지로 Ordering
은 enum 타입이며, Less
, Greater
, Equal
형태를 가집니다.
cmp
메서드는 두 값을 비교할 수 있는 모든 값에 대해 호출할 수 있습니다. 비교하려는 대상에 대한 참조가 필요합니다.
여기서 guess
에 secret_number
를 비교하고 use
문으로 포함시킨 Ordering
enum 타입을 반환합니다.
추측값 guess
에 비밀번호 secret_number
를 사용하여 cmp
에 대한 호출로 반환된 Ordering
을 기반으로 다음에 수행할 작업을 결정합니다.
arm
은 패턴과 match
표현식의 시작 부분에서 주어진 값이 이 arm
패턴에 맞을 경우 실행 되어야 하는 코드로 구성 됩니다.
Rust는 match
에 주어진 값을 받아 각 arm
의 패턴을 차례로 확인합니다. 이는 이 패턴이 일치하면 이 값을 반환하고 이 값이 일치하지 않으면 다음 패턴을 확인합니다.
이체 해당 코드를 실행해 봅시다.
cargo run
Compiling guessing_game v0.1.0 (/rust/study/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}`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `guessing_game` due to previous error
오류의 핵심은 타입이 일치하지 않는다는 것 입니다. Rust는 강력한 정적 타입 시스템을 가지고 있습니다.
입력 받은 guess
는 String
타입이고 secret_number
는 i32
타입입니다.
궁극적으로 String
타입으로 받고 있는 guess
를 실수형으로 변환하여 숫자로 secret_number와 비교하도록 수정해야 합니다.
let guess: u32 = guess.trim().parse().expect("Please type a number!");
정수 타입의 guess
를 생성합니다. 하지만 이전에 이미 String 타입의 guess
를 선언 했습니다.
Rust는 이미 선언된 타입을 새 값으로 사용하는 새도잉을 허용하기 때문에 이같이 재사용하는 것이 가능합니다.
String
인스턴스의 .trim()
은 앞뒤 공백을 제거합니다. 이를 통해 read_line
을 충족하기 위해 사용자가 Enter
키를 누른 결과인 \n
을 제거합니다.
문자열의 parse
메서드는 문자열을 일종의 숫자로 구문 분석을 합니다.
이 방법은 다양한 숫자 타입을 구문 분석할 수 있기 때문에 let guess: u32
를 사용하여 정확한 숫자 타입을 Rust에 알려야 합니다.
guess
뒤에 콜론(:
)은 변수의 유형에 대한 선언을 의미합니다.
parse
에 대한 호출은 쉽게 오류를 일으킬 수 있습니다. 이를 처리하기 위해 expect
메서드를 사용합니다. parse
메서드는 read_line
와 마찬가지로 Result
를 반환합니다.
이제 프로그램을 다시 실행해 보겠습니다.
cargo run
Compiling guessing_game v0.1.0 (/rust/study/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 96
Please input your guess.
44
You guessed: 44
Too small!
사용자의 입력 값과 비밀번호와 비교하고 그 메시지를 출력하는 대부분의 동작이 정상적으로 수행되었습니다.
하지만 단 한번의 실행을 통해 하나의 추측만을 시도할 수 있습니다. 이제 루프를 추가하여 반복적인 비교를 진행할 차례입니다.
이 loop
키워드는 무한 루프를 만듭니다.
사용자가 숫자를 추측할 수 있는 더 많은 기회를 제공하기 위해 추가할 것입니다.
// --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!"),
}
}
}
사용자의 입력을 받고 비교하는 부분을 loop
안에 넣었습니다. 이제 반복적으로 사용자의 입력을 받고 비교할 수 있게 되었습니다.
프로그램을 종료하려면 Ctrl+C
를 눌러 실행을 종료할 수 있습니다.
사용자가 정확한 비밀번호를 넣으면 프로그램이 종료되도록 프로그램을 수정해 보겠습니다.
// --snip--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
break
를 사용하여 사용자가 비밀번호를 올바르게 추측하였을 때 프로그램은 You win!
이라는 메시지를 출력한 후 종료하기 됩니다.
break
는 loop
를 종료하는 것을 의미하지만 loop
가 프로그램의 마지막 부분이기 때문입니다.
게이의 동작을 더욱 구체화 하기위해 사용자가 숫자가 아닌 것을 입력할 때 프로그램이 충돌하는 대신 사용자가 계속 게임을 진행할 수 있도록, 숫자가 아닌 것은 무시하도록 수정합니다.
// --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--
parse
로 문자열을 숫자로 성공적으로 변환할 수 있는 경우 Ok
결과에 숫자가 포함 된 값을 반환합니다.
그 Ok
값은 첫 번째 arm
의 패턴과 일치하고 일치 표현식은 구문 분석하여 생성된 num
값을 반환하고 Ok
값 안에 넣습니다.
parse
가 문자를 숫자로 변환할 수 없는 경우 오류에 대한 추가 정보가 포함된 Err
값을 반환합니다.
Err
는 첫 번재 arm
패턴과는 일치하지 않지만, 두 번째 패턴과 일치하게 됩니다. 밑줄(_
)은 포괄적인 값을 의미하며, 내부의 어떤 정보가 있는지 무관하게 모든 Err
값을 일치시키게 됩니다.
Err
가 발생하였을 때 continue
를 실행하여 루프의 다음 반복으로 이동하여 다른 게임을 계속 진행하도록 지시합니다.
따라서 프로그램은 parse
에 대한 모든 오류를 효과적으로 무시하게 됩니다.
이제 모든 것이 예상대로 실행되는지 확인해 보겠습니다.
cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 84
Please input your guess.
44
You guessed: 44
Too small!
Please input your guess.
99
You guessed: 99
Too big!
Please input your guess.
81
You guessed: 81
Too small!
Please input your guess.
85
You guessed: 85
Too big!
Please input your guess.
84
You guessed: 84
You win!
모든기능이 잘 동작했습니다. 이제 비밀번호를 누출하는 println!
을 삭제해 코드를 완성합니다.
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..101);
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;
}
}
}
}
랜덤한 숫자를 생성하고 이를 사용자가 추측하는 게임을 성공적으로 구현했습니다.
이를 통해 let
, match
, function
, 외부 crates
등의 기본적인 Rust 개념을 익혔습니다.