소소한 개발 공부

[러스트] 추리 게임 본문

프로그래밍/Rust

[러스트] 추리 게임

이내내 2023. 7. 18. 05:55

- 실습 프로젝트로 러스트를 익혀 보자!

let, match, 메서드, 연관 함수(associated functions), 외부 크레이트(externals crates) 등의 활용을 배워보자.

 

Q. 1~100 사이의 임의의 정수를 생성한다. 다음으로 플레이어가 프로그램에 추리한 정수를 입력한다. 프로그램은 입력받은 추리값이 정답보다 높거나 낮은지 알려준다. 추리 값이 정답이라면 축하 메시지를 보여주고 종료된다. (=> 업앤다운)

1. 프로젝트 생성

$ cargo new guessing_game --bin
$ cd guessing_game

- cargo new : 프로젝트 이름 guessing_game 을 인자로 받는 명령어로 인자를 이름으로한 프로젝트 디렉터리를 생성한다.

  --bin 은 바이너리용 프로젝트를 생성하도록 하는 옵션

생성된 프로젝트는 기본적으로 src/main.rs 를 가지고 있으며 내용은 아래와 같다.

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

cargo run 프로젝트를 컴파일 및 실행할 수 있다.

 

2. 추리값 처리

1) 먼저 사용자로부터 입력을 받아 처리한다.

// io 라이브러리를 가져오기. io 라이브러리는 std 표준 라이브러리에 있음
// 러스트 프로그램은 기본적으로 prelude 라는 라이브러리들을 지원하며, 여기서 심볼을 가져오는데 
// 원하는 타입, 심볼이 prelude에 없다면 use로 가져와야 함.
use std::io;

// main - 함수의 진입점. 여기서부터 프로그램이 시작
fn main() {

// !는 러스트 매크로로, 함수 대신 매크로를 호출. 
// 문자열을 스크린에 출력
    println!("Guess the number!");

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

// let 으로 변수를 선언하고 mut 으로 해당 변수를 가변 변수로 명시 
// 가변 변수 guess 를 String 타입으로 선언 및 저장 공간 할당
// guess 변수에 String 인스턴스가 bind 됨 - 값을 확정 짓는 것
// :: 는 new 가 String 타입의 "연관함수" 임을 나타냄 <- 정적 메서드
// new : 빈 String 생성
    let mut guess = String::new();

// io 라이브러리의 연관함수인 stdin 호출
// read_line() 은 &mut guess를 인자로 하는 사용자 입력을 받는 메서드
// read_line 은 입력마다 문자열에 저장하므로 값을 저장할 공간이 필요 -> 매번 변경되므로 가변이어야 함
// & 는 참조자로 메모리를 여러번 복사하지 않고 접근하는 방법을 제공
// (&가 불변이라 &mut 으로 가변으로 바꿈)
    io::stdin().read_line(&mut guess)
        .expect("Failed to read line"); // Result 타입

// 변경자(placeholder) 로 값 출력
    println!("You guessed: {}", guess);
}

 

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

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

여기서, read_line은 인자로 넘긴 문자열(&mut guess)에 입력을 저장하며, io::Result 값을 반환한다.

Result 타입은 열거형(enumerations, enums)으로

-> 정해진 값만 가질 수 있으며, 이 정해진 값들을 열거형의 variants 라고 한다.

Result 의 variants 는

- Ok : 처리가 성공했음을 나타냄. 내부적으로 성공적으로 생성된 결과를 가짐

- Err : 처리가 실패했음을 나타냄. 그 이유에 대한 정보를 가짐

 

io::Result는 expect 메서드를 가지는데,

io::Result 가 Ok 일 경우 -> Ok 가 가진 결과값(표준입력으로 들어온 바이트 수)을 반환해 사용할 수 있도록 함.

io::Result 가 Err 일 경우 -> 프로그램이 작동을 멈추고 expect에 인자로 넘겼던 (에러)메시지를 출력하도록 함

- read_line 에서 Err 를 반환했다면, 운영체제로부터 생긴 에러 일 경우가 큼

- expect 를 호출하지 않는 경우, 컴파일은 되지만 Result를 사용하지 않았다는 warning 을 도출

 

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

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

{} 는 변경자로 값이 표시되는 위치를 나타내며, 하나 이상의 값을 표시할 수 있다.

첫번째 {}는 포맷 스트링 뒤 첫번째 값 (여기서는 guess) 를 나타내며,

아래와 같이 {} 의 개수 만큼 포맷 스트링 뒤에 붙는 인자를 보여줄 수 있다.

let x = 5;
let y = 10;

println!("x = {} and y = {}", x, y);
// output: x = 5 and y = 10

3. 비밀번호 생성하기

1~100 사이 매번 랜덤한 번호를 생성한다.

 -> rand 크레이트를 활용해보자.

* crate : 패키지 (러스트 코드 묶음)

* rand crate : 다른 프로그램에서 사용되기 위한 용도의 library crate

 

외부 크레이트를 사용하려면 Cargo.toml 의 [dependencies] 에 추가하면 된다.

[dependencies]

rand = "0.3.14"

- [dependencies]는 프로젝트가 의존하고 있는 외부 크레이트와 그 버전을 명시하는 곳

 

-> cargo build 혹은 cargo run로  rand 라이브러리를 다운로드

dependencies 에는 rand만 추가했지만, rand 가 libc 에 의존하므로 같이 다운로드 받아진다.

* Cargo 는 Crates.io (러스트 오픈소스) 데이터의 복사본인 레지스트리에서 라이브러리들을 가져온다.

$ 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.lock

Cargo.lock 은 모든 의존 패키지의 버전을 확인하고 이를 기록해 빌드 시 그 안에 명시된 버전을 사용하도록 하는 기능을 한다.

즉, dependencies 에 기재된 버전을 업그레이드 하지 않는 이상, 기재된 버전을 사용함으로써 안정된 버전을 사용하는 것을 보장한다.

-> 크레이트를 업그레이드 하고 싶다면, update 명령어를 사용하자.

이는 새로운 버전을 사용하게 하고, 만약 새 버전이 문제가 없다면 Cargo.lock 에 기재해 사용할 수 있게 한다.

그런데 현재 Cargo.toml에 v.0.3.~ 버전이 기재되어 있으므로 아래처럼 0.3.~ 버전대에서 업그레이드 할 수 있는 버전(0.3.15)을 찾는다.

$ cargo update
    Updating registry `https://github.com/rust-lang/crates.io-index`
    Updating rand v0.3.14 -> v0.3.15

다른 버전대로 업그레이드 하고 싶다면, Cargo.toml 을 변경하면 된다.

[dependencies]

rand = "0.4.0"

 

임의의 숫자 생성하기

이제 코드에서는 어떻게 달라지는지 보자.

extern crate rand;

use std::io;
use rand::Rng;

fn main() {
    ...
    
// read::thread_rng: OS가 seed 를 정하고 현재 스레드에서만 사용되는 정수 생성기를 반환
// get_range : 두 숫자를 인자로 받고 두 수 사이의 임의의 숫자를 반환(1 ~ 99 중 하나를 반환)
    let secret_number = rand::thread_rng().gen_range(1, 101);

    ...
}

상단에 rand 크레이트를 외부에서 가져오고, use 하는 문구가 추가되었다.

이제 rand:: 를 이용해 rand 내의 모든 것을 호출할 수 있다.

rand::Rng 는 정수 생성기가 구현한 메서드들을 정의한 trait 이다. (trait은 나중에 알아본다.)

 

각 크레이트에 대한 내용은 cargo doc --open 으로 공식 문서를 열어 확인할 수 있다.

 

4. 비밀번호와 추리값 비교하기

extern crate rand;

use std::io;
// Less, Greater, Equal 열거형을 나타냄
use std::cmp::Ordering;
use rand::Rng;

fn main() {
    ...

// 여기서 guess 를 u32 타입으로 섀도우해주어 match에서 타입 에러가 나지 않게 한다.
// trim : String 앞, 뒤의 스페이스 제거
// parse : String 을 정수형으로 파싱
// expect : 파싱 실패 시 에러 메시지 출력
    let guess: u32 = guess.trim().parse()
        .expect("Please type a number!");

    ...

// cmp : 두 값을 비교하며, 비교 가능한 모든 것에 대해 호출한다.
// 인자로 &secret_number를 받아 guess 와 비교한다.
// match 는 표현문으로 guess와 secret_number를 비교한 결과에 따라 어떤 println! 이 나올지 결정한다.
    match guess.cmp(&secret_number) {
        Ordering::Less    => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal   => println!("You win!"),
    }
}

match 표현식은 arm 으로 이뤄져있다.

- arm : match 의 각 케이스 ex) Ordering::Less    => println!("Too small!") 

 

매칭은 처음부터 순서대로 수행되는데, 만약 중간(Greater쯤)에서 매칭이 이뤄진다면,

해당하는 코드 (println!)를 실행하고 match문을 빠져나간다. (break 필요 x)

 

5. 반복으로 여러 번 추리하기

- loop 키워드를 사용해 반복문을 돌린다.

loop {
    문장 ...
}

- break 키워드로 빠져나간다.

break 키워드가 없으면, 무한 루프를 돌기 때문에 Ctrl+c 나 프로그램 내 비정상 종료 (Result expect - Err)일 경우에 빠져나갈 수 있다.

 

- continue 키워드

원하는 입력을 받지 못한 경우 입력 다음 처리를 건너뛰어 다시 루프를 시작하게 한다. 

// 반복 시작
    loop {
        ...
        
// expect -> match 로 바꿔 Err 시 종료를 Err 시 continue 처리로 바꾼다.
        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,     // <- guess 는 num 값을 갖게 된다.
            Err(_) => continue, // <- 여기서 해당 반복을 건너뛰고 다음 반복을 시작할 수 있다.
        };

        ...
        
        match guess.cmp(&secret_number) {
            Ordering::Less    => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal   => {
                println!("You win!");
                break; // <- 여기서 loop 를 빠져나갈 수 있다.
            }
        }
    }

* _ 는 모든 값과 매칭할 수 있다.

 

최종 코드

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