[Chapter2] Rust 추리게임 구현

hwwwa·2021년 10월 28일
0

🦀 Rust

목록 보기
2/25
post-thumbnail
post-custom-banner

새로운 프로젝트 준비

chapter1에서 생성했던 디렉토리인 projects로 이동하고 아래 예제를 입력해 새로운 프로젝트를 생성합니다.

$ cargo new guessing_game --bin
$ cd guessing_game

chapter1에서처럼 "Hello,world!" 프로그램이 생성됩니다.

cargo run 입력 시 Hello, world! 가 출력되는 것을 볼 수 있습니다.

추리값 처리하기

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);
}
  • io : 사용자의 입력을 받고 결과값을 표시하기 위한 input/output 라이브러리
  • fn : 새로운 함수를 선언
  • println! : string을 화면에 표시하는 매크로

값을 변수에 저장하기

아래의 코드로 사용자의 입력값을 저장할 공간을 생성할 수 있습니다.

let mut guess = String::new();

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

러스트에서 변수는 기본적으로 불변입니다. 변수 앞에 mut 을 사용하면 가변변수로 만들 수 있습니다.

let foo = 5; // immutable
let mut bar = 5; // mutable

String::new에 있는 ::은 new가 String 타입의 연관함수임을 나타냅니다. 연관함수는 하나의 타입을 위한 함수이며, 이 경우에는 하나의 String 인스턴스가 아니라 String 타입을 위한 함수입니다. 몇몇 언어에서는 이를 정적 메소드라고 부릅니다.

Result 타입으로 잠재된 실패 다루기

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

위의 코드는 사용자에게 입력을 받는 코드입니다.

만약 프로그램 시작점에 use std::io 가 없다면 함수 호출 시 std::io::stdin 처럼 작성해야합니다. 사용자로부터 입력을 받기 위해 표준 입력 핸들에서 .read_line(&mut guess) 메소드를 호출합니다. 또한 read_line에 &mut guess 를 인자로 하나 넘깁니다. read_line은 인자로 넘긴 문자열에 사용자 입력을 저장할 뿐만 아니라 하나의 값을 돌려줍니다. 여기서 돌려준 값은 io::Result 입니다.

Result 타입은 열거형(enumerations)로써 enums라고 부르기도 합니다. 열거형은 정해진 값들만 가질 수 있으며 이러한값들은 열거형의 variants라고 부릅니다. Result의 variants는 Ok와 Err입니다. Ok는 처리가 성공했음을 나타내며 내부적으로 성공적으로 생성된 결과를 가지고 있습니다. Err는 처리가 실패했음을 나타내고 그 이유에 대한 정보를 가지고 있습니다.

이러한 Result는 에러처리를 위한 정보를 표현하기 위해 사용되며 다른 타입들처럼 메소드들을 가지고 있습니다. io::Result 인스턴스는 expect 메소드를 가지고 있습니다. 만약 io::Result 인스턴스가 Err 일 경우 expect 메소드는 프로그램 작동을 멈추게 하고 인자로 넘겼던 메세지를 출력하도록 합니다. io::Result 가 Ok 라면 expect 는 Ok 가 가지고 있는 결과값을 돌려주어 사용할 수 있도록 합니다. 이 경우 결과값은 사용자가 표준 입력으로 입력했던 바이트의 개수입니다.

만약 expect 를 호출하지 않는다면 컴파일은 되지만 경고가 나타납니다.

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `std::result::Result` which must be used
  --> src/main.rs:10:5
   |
10 |     io::stdin().read_line(&mut guess);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: #[warn(unused_must_use)] on by default

println! 변경자(placeholder)를 이용한 값 출력

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

위의 라인은 사용자가 입력한 값을 저장한 문자열을 출력합니다. {} 는 변경자로써 값이 표시되는 위치를 나타냅니다. {} 를 이용해 하나 이상의 값을 표시할 수도 있습니다.

지금까지의 코드(추리게임의 처음 부분)를 cargo run 을 통해 테스트 합니다.

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

비밀번호 생성하기

다음으로 사용자가 추리하기 위한 비밀번호를 생성합니다. 비밀번호는 매번 달라야합니다. 1에서 100 사이의 임의의 수를 사용하도록 하겠습니다.

Crate를 사용해 더 많은 기능 가져오기

crate는 러스트의 패키지입니다. rand crate는 다른 프로그램에서 사용되기 위한 용도인 library crate입니다. rand 를 사용하는 코드를 작성하기 전 Cargo.toml 을 수정하여 rand 크레이트를 의존 리스트에 추가합니다.

[dependencies]

rand = "0.3.14"

dependencies에 추가 후 프로젝트 빌드를 합니다.

$ cargo build
    Updating registry `https://github.com/rust-lang/crates.io-index`
 Downloading rand v0.3.14
 Downloading libc v0.2.14
   Compiling libc v0.2.14
   Compiling rand v0.3.14
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs

크레이트를 업그레이드 하고싶다면 Cargo에서 제공하는 update 명령어를 사용할 수 있습니다.

$ cargo update

임의의 숫자 생성하기

src/main.rs 를 다음과 같이 업데이트합니다.

extern crate 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);
}

extern crate rand; 를 추가하여 rand crate를 사용할 수 있습니다. 이는 use rand 로도 표기할 수 있습니다. use rand::Rng 에서 Rng 는 정수 생성기가 구현한 메소드들을 정의한 trait입니다.

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

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

위의 코드는 Rng trait에 정의되어있는 get_range 메소드를 호출합니다. gen_range(1, 101) 는 1부터 100 사이의 임의의 숫자를 생성합니다. 생성된 임의의 숫자는 비밀번호(secret_number)가 되고 두 번째 줄에서 출력됩니다. 두 번째 줄은 최종 버전에서는 삭제됩니다.

프로그램을 실행해보면 매 실행마다 다른 숫자가 나타나는 것을 볼 수 있습니다.

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

비밀번호와 추리값을 비교하기

src/main.rs 를 다음과 같이 업데이트합니다.

extern crate rand;

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);

    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!"),
    }
}

std::cmp::Ordering에서 Ordering은 Result와 같은 열거형이지만 Ordering의 값은 두 개의 값을 비교할 때 나올 수 있는  LessGreaterEqual입니다. cmp 메소드는 두 값을 비교하고 Ordering 열거형을 반환합니다. 위의 코드를 통해 guess와 secret_number를  비교합니다. 또한, match 표현문을 이용해 cmp 의 결과에 따라 무엇을 할 것인지 결정할 수 있습니다.

하지만 guess는 String이고 secret_number는 정수형이므로 guess를 정수형으로 바꾸기 위해 아래의 코드를 사용합니다.

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

문자열의 parse 메소드는 문자열을 숫자형으로 파싱합니다. 다양한 종류의 정수형을 반환하기 때문에 let guess: u32처럼 정확한 타입을 명시해야 합니다.

프로그램을 실행합니다.

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

반복문을 이용하여 여러 번의 추리 허용

위의 코드는 한 번의 추리만 가능합니다. 반복문을 추가하여 여러 번의 추리를 허용하도록 변경합니다.

extern crate rand;

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;
            }
        }
    }
}

loop 키워드는 무한루프를 제공합니다. 추리값을 입력 받는 코드부터 모든 코드들을 반복문 내로 옮깁니다. 사용자가 정답을 맞췄을 때 게임이 종료되도록 break문도 추가해줍니다. 비밀번호를 출력하는 println!을 삭제해줍니다.

잘못된 입력값 처리하기

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

expect 메소드 호출을 match 표현식으로 바꾸는 것은 에러 발생 시 종료에서 처리로 바꾸는 일반적인 방법입니다. parse 가 문자열을 정수로 바꾸지 못했다면 Err를 반환합니다. 따라서 세 번째 줄 코드인 continue를 실행하여 loop 의 다음 반복으로 가서 또다른 추리 값을 요청하도록 합니다.

이제 최종 프로그램을 실행해봅니다.

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
     Running `target/guessing_game`
Guess the number!
Please input your guess.
50
You guessed: 50
Too big!
Please input your guess.
30
You guessed: 30
Too small!
Please input your guess.
40
You guessed: 40
You win!
post-custom-banner

0개의 댓글