Rust #2: 코드 따라하기(1/2) - 2장 추측 게임 프로그래밍
개요
러스트 홈페이지에서 제공하는 The Rust Programming Language 온라인 북에 나와 있는 코드를 따라 해보면서 Rust의 특징을 파악하고 손에 익히도록 합니다.
2장 추측 게임 프로그래밍
여러 프로그래밍 언어에서 실습을 위해 예제로 곧잘 활용하는 숫자 맞추기 게임입니다. 온라인 북은 이를 통해 Cargo 설정파일 및 외부 Crate를 어떻게 포함시키는지 숫자 맞추기 게임을 구현하기 위해 간단한 Rust 코드를 학습 시킵니다.
cargo
를 이용해 기본 패키지를 생성합니다.
$ cargo new guessing_game
cd guessing_game
기본 cargo.toml
구조입니다. 지금은 자동생성된 toml 설정파일의 기본 구조만 살펴봅니다.
[package]
name = "guessing_game"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
기본 생성된 main.rs
소스코드 입니다.
fn main() {
println!("Hello, world!");
}
심지어 이 코드는 cargo run
을 통해 바로 실행도 잘 됩니다.
$ cargo run
Compiling guessing_game v0.1.0 (W:\rust-learning\guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 0.59s
Running `target\debug\guessing_game.exe`
Hello, world!
훌륭합니다. 기본 컴파일 속도도 빠르고, 별도의 의존성 없이 곧잘 실행되네요.
VS Code
로 코딩을 계속 진행합니다.
실행 명령을 통해 Rust를 실행하려면 launch.json
을 다음과 같이 작성합니다.
{
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug",
"program": "${workspaceFolder}/target/debug/guessing_game",
"args": [],
"cwd": "${workspaceFolder}"
}
]
}
기본 생성되는 설정파일에 program
의 경로만 설정했습니다. 이제 VS Code로 코딩을 계속할 수 있습니다.
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);
}
첫번째 그럴싸한 Rust 코드입니다. C# 프로그래머 입장에서는 대략적인 의미를 파악할 수 있는 형식과 무엇인지 알 수 없는 부분이 있습니다. 가령, use std::io
는 std의 io 모듈을 사용하겠다라는 영역
지정의 의미이지만, 왜 stdio() 앞에 io::
가 붙어야 하는지는 아직은 모르겠습니다. 함수명이 중복되는것을 최대한 막기 위해 모듈명을 적어줘야 하는게 기본 동작으로 보입니다. 그리고 fn은 function으로 함수를 의미한다고 유추할 수 있고 main() 함수가 최초 진입점임을 알 수 있습니다. println!()
형태의 함수는 느낌표가 들어가 특이한데, 매크로라는 의미라고 합니다. 친절하게 F12
를 누르면 해당 기능의 소스코드를 살펴볼 수 있습니다. 아마도 가변 함수 인자를 컴파일 타임에 처리하기 위함으로 보입니다.
guess
를 String:new()
로 할당하는데요, 특이한 점은 mut
키워드 입니다. 기본적으로 Rust는 변경가능한
변수가 아닌 변경불가능한
불변(immutable) 변수를 생성하는게 기본 동작으로 보입니다. io:stdin().read_line()
함수를 통해 입력한 값을 문자열로 받기 위함인데요, 반환값이 아닌 참조 인자로 받는 것은 특이한 점입니다. 아마 관련해서 Rust 언어의 특징이 드러날 것으로 보입니다.
read_line()
함수에서 오류가 발생할 경우 expect()
의 인자를 오류처리시 사용하는것으로 보입니다.
최종적으로 println!()
함수를 통해 입력된 값을 출력하네요. {}
에 값이 들어가는 것으로 보입니다.
다음으로 난수값을 이용하기 위해 rand Crate를 Cargo.toml
에 추가합니다.
[dependencies]
rand = "0.8.3"
이게 끝입니다. 다시 cargo run
을 하면 알아서 해당 Crate 및 의존 Crate들을 설치해줍니다.
use rand::Rng;
use std::io;
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);
}
use rand::Rng
를 통해 Rand Crate를 사용함을 알리고, rand::thread_rng().gen_range()
를 통해 난수값을 취합니다. 해당 함수의 정확한 의미는 아직 모릅니다. 1..101
형태로 범위 값을 사용할 수 있는데, python 등 동적 언어에서 이전부터 지원했었고, C#도 최근 버젼에서는 지원합니다.
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..101);
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!"),
}
}
String.trim()
및 parse()
를 통해 문자열을 숫자로 변환합니다. 어떻게 parse()
함수가 반환값을 추측할 수 있는지는 모르곘습니다. Rust의 특징으로 보이네요. => 반환값을 추측하는게 아니라 FromStr
형을 반환하는데 이게 숫자형으로 보입니다.
std:cmp::Ordering
모듈을 이용해 guess.cmp()
로 작거나 크거나 같음을 비교할 수 있습니다. match
키워드를 통해 상당히 세련되게 표현할 수가 있군요.
다음은 최종적인 코드 입니다.
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..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;
}
}
}
}
String의 parse()의 반환형인 Result는 Ok이거나 Err일 수 있는데, 함수형 언어의 영향을 받은 것으로 보입니다. Ok일 경우 반환된 값을 그대로 사용할 수 있고 Err일 경우 에러 관련 처리를 할 수 있습니다.
이렇게 2장을 둘러보면서 Rust언어의 특징을 대략적으로 조금이나마 파악할 수 있었습니다. Rust 표준 라이브러리에서는 반환값으로 Result 형을 대부분 사용할것 같고요, 이하 기본적인 구문은 C# 프로그래머 입장에서는 예측할 만한 범위에 아직은 있는 것 같습니다.
실행화면
$ cargo run
Guess the number!
Please input your guess.
5
Guess the number!
Please input your guess.
30
You guessed: 30
Too small!
Please input your guess.
40
You guessed: 40
Too small!
Please input your guess.
50
You guessed: 50
Too small!
Please input your guess.
60
You guessed: 60
Too small!
Please input your guess.
70
You guessed: 70
Too small!
Please input your guess.
80
You guessed: 80
Too small!
Please input your guess.
90
You guessed: 90
Too big!
Please input your guess.
85
You guessed: 85
Too big!
Please input your guess.
80
You guessed: 80
Too small!
Please input your guess.
83
You guessed: 83
Too small!
Please input your guess.
84
You guessed: 84
You win!