RustBook - 2. Programming a Guessing Game

숲사람·2022년 3월 23일
0

Rust

목록 보기
2/15

이 시리즈는 Rust Book을 공부하고 정리한 문서입니다. 댓글로 많은 조언 부탁드립니다.
Rust Book: https://doc.rust-lang.org/book/


2장에서는 예제 프로그램을 만들어보면서 Rust 언어에 대한 전체 Overview를 한다. 만들어볼 프로그램은 Guessing Game : 프로그램이 랜덤으로 숫자를 생성하고 사용자가 맞추는 게임이다. 사용자가 숫자를 입력하면 프로그램은 맞출때까지 값이 큰지 작은지 알려준다.

2장에서는 다음의 내용들을 대략적으로 알아볼것이다.

  • let,
  • match,
  • methods,
  • associated functions,
  • using external crates

2.1 Setting Up a New Project

$ cargo new guessing_game
$ cd guessing_game
$ cargo run

2.2 사용자 Input 처리

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

한줄씩 살펴보자

use std::io;

라이브러리를 import 할때는 use라는 키워드를 사용한다. ::로 scope를 지정. std 라이브러리의 io 라이브러리를 사용한다는 의미

fn main() {

fn 키워드로 함수라는것을 명시 그리고 함수명에 () .

println!("Guess the number!");
println!("Please input your guess.");

print 하는 매크로함수. std::io 가 필요없음. 매크로 함수랑 라이브러리 함수의 차이? 마지막에 느낌표!가 있는 함수가 매크로함수 이다.

2.2.1 변수

let mut guess = String::new();

요약: mutable 변수 생성하고 String의new함수를 통해 빈 String 인스턴스를 바인드함.

let 으로 변수 선언. 참고로 let foo = bar; 는 bar라는 변수를 foo 변수에 바인드 하는것.

mut 키워드를 사용해야 변수가 말그대로 변수가 된다. mutable해진다. 안쓰면 상수이다. (3장에서 다시 살펴볼예정) 만약 mut 키워드로 선언하지 않은 immutable 변수 val에 값을 변경하면 아래와 같은 에러 발생.

error[E0384]: cannot assign twice to immutable variable `val`

String은 utf-8 로 인코딩된 표준 문자열 객체이자 문자열 타입.
String::new() 함수를 호출하면 리턴값으로 String 객체의 인스턴스를 반환. guess 변수에 bind 했다. (rust 에서는 초기화를 bind라고 표현한다).

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

use str::io 를 선언했기 때문에 io:: 모듈로 바로 함수 사용. use 안쓰고 std::io::stdin() 로 써도됨. stdin 함수는 std::io::Stdin 객체의 인스턴스를 리턴한다.

.read_line(&mut guess) 유저로부터 "문자열" 입력을 받는 함수.
& 는 reference 라고 불린다. 매번 메모리에 복사할 필요없이 코드가 데이터에 여러번 접근하게 해줌. 포인터같은 개념(?) 변수와 마찬가지로 reference도 기본적으로 immutable하기 때문에 명시적으로 mut 키워드를 사용해줘야한다. read_line으로 입력받는 값은 mutable 해야하기 때문에 &guess 대신 &mut guess 를 사용한것.

2.2.2 Result 타입으로 잠재적 failure 핸들링

.expect("Failed to read line");

read_line함수는 io::Result 타입의 값을 리턴한다. 이 타입은 enums 값들을 가지고 있다. (에러넘버와 같은것일듯) 6장에서 다시 살펴본다. Result 타입은 OkErr 두가지 값을 가지고 있다. read_line함수가 Err를 리턴하거나 Ok를 리턴하냐에 따라 성공여부가 결정된다. expect() 는 Result 타입에 관한 예외처리 함수이다.

expect() 를 사용하지 않으면 빌드시에 std::result::Result 를 사용해야한다는 경고 메시지가 발생한다. (빌드는 성공)

try catch 같은 예외처리 문법이고, 함수단위의 예외처리를 하는 컴포넌트이다.

2.2.3 println! 에서 값 출력

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

서식문자같은걸 사용할 필요없이 문자열이든 변수든 {} 로 다됨.

let x = 5;
let y = 10;

println!("x = {} and y = {}", x, y);

최신 Rust버전에서는 아래와 같이 사용하는게 가능함.

println!("x = {x} and y = {y}");

2.3 외부 Crate 사용하기

Crate는 외부 패키지를 말한다. 이장에서는 외부 rand crate를 사용할것이다. 사용방법은 toml 파일에 아래 추가.

[dependencies]

rand = "0.3.14"

그리고 cargo build 하면 자동으로 다운받고 rand crate를 컴파일한다. https://crates.io/ 컴파일도중에 libc 로 같이 컴파일 되는데 rand의 디펜던시에 libc 가 있기 때문이다.

2.4 Cargo.lock

만약 컴파일한 프로젝트가 외부 crate 패키지의 특정 버전에 의존한다면? Cargo.lock파일은 그것을 관리하고. 다른사람이 이 프로젝트를 컴파일할때도 필요한 버전을 사용하여 빌드하게 도와준다.

2.5 Random Number 생성

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

use rand::Rng 는 랜덤넘버 생성 함수들을 사용하기 위해 필요한 Trait 이다. 10장에서 더 알아본다.

다음의 명령을 내리면 현재 사용하는 패키지의 doc문서를 브라우저에서 보여준다. 엄청난 기능 ㄷㄷ

 $ cargo doc --open

2.6 비교구문 (match)

use std::io;
use std::cmp::Ordering;
use rand::Rng;

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

match 는 switch 와 유사한 문법이다. match 는 조건식이 아닌 리턴값(또는 변수값)에 의해 분기를 처리하는 구문이다. 실행 구문의 형식은 arm's 패턴이다. =>를 기준으로 좌측은 조건, 우측은 실행할 코드이다. match는 c의 전처리함수처럼 결과적으로 조건에 따른 실행코드만 남고 나머지는 사라지는것으로 생각하는게 이해하기 쉬울듯하다.

OrderingResult와 같이 열거형(Enum) 타입이고 값은 Less Grater Equal 을 가지고 있다.

guess의 cmp() 메소드는 Ordering 타입을 리턴한다. guess.cmp(&secret_number) 는 quess 와 secret_number 를 비교한다.
match 는 그 연산 결과인 Ordering 에 따라 무엇을 할것인지 결정하는 표현이다. Ordering 타입을 반환하는 모든 메소드를 사용할 수 있을듯. 아래와 같은식으로.

match <func returning Ordering> {
    Ordering::Less    => <do something1>
    Ordering::Grater    => <do something2>    
}

더 일반적으로는 리턴하는 모든 타입을 아래와 같이 처리할 수 있다. switch문이라고 보면 이해가 쉬울것 같다.

match <func returning type> {
    <return type1>   =>  <do something 1>
    <return type2>   =>  <do something 2>

}

2.7 형 변환

문제는 guess는 문자열 타입이고 secret_number는 int타입이라는것이다. cargo run하면 빌드 실패한다.

중간에 다음을 추가하여 형변환을 한다.

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

일단 String.trim() 은 처음과 끝의 빈칸 제거함. String의 parse() 메소드는 문자열을 숫자형으로 파싱한다.

let guess: u32 변수명에 : 를 붙이면 정확한 타입을 명시 하는것이 가능하다.

parse() 메소드는 숫자형문자가 아닌 문자가 들어오면 실패한다. parse()는 Result 타입을 반환한다. 따라서 예외 핸들링을 expect() 로 해준다. 만약 parse()에 성공하면 Result::Ok 를 반환할 것이고 expect()가 u32값을 unwrapping해 줄 것이다. 만약 실패하면 Result::Err 를 반환하고 인자로 전달된 메시지와 함께 패닉이 발생한다.

2.8 반복문

loop 키워드를 사용하고, continue / break를 사용해서 루프를 탈출한다. 조건은 match 를 사용했다. if 사용해도됨.

    loop {

        //do something

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}
let guess: u32 = match guess.trim().parse() {
    Ok(num) => num,
    Err(_) => continue,
};

위에서 살펴봤드시 parse().expec()를 하면 패닉이 발생하고 프로그램이 종료된다. 만약 예외 발생시 패닉이 아니라 다른 처리를 하고싶다면 match를 사용할 수 있다. 루프 내에 위와 같이 match 를 사용하여 잘못된 값이 들어와도 패닉이 발생하는게 아니라 continue되게 할수 있다.

parse() 메소드 가 정상적으로 성공했다면 return 값을 포함한 Ok 값이 리턴될것이다. 이 Ok 값이 첫번째 arm패턴과 일치한다면 num을 return. 아니면 루프를 continue한다. Err(_) 의 _는 모든 값을 catch한다는것을 의미한다.

profile
기록 & 정리 아카이브용

0개의 댓글