열거형

열거형은 여러 상수들의 집합으로 새로운 타입을 선언하는 방법입니다. 파이썬에서는 Enum 클래스를 상속해 열거형을 만들 수 있습니다. 아래와 같이 Languages 클래스를 선언하고, python, rust, javascript, go 4개의 값을 타입에 선언했습니다. 그리고 echo 메소드를 정의했는데, 이 메소드는 Enum 클래스에 미리 정의된 name 프로퍼티를 프린트합니다.

이렇게 선언된 열거형을 이용해, 어떤 변수의 값에 따라 다른 행동을 하도록 할 수 있습니다. 여기서 language 변수와 비교되는 값들이 Language 클래스의 값들인 Languages.* 라는 점을 기억하세요.

from enum import Enum


class Languages(Enum):
    PYTHON = "python"
    RUST = "rust"
    JAVASCRIPT = "javascript"
    GO = "go"

    def echo(self):
        print(self.name)


language = Languages.RUST
language.echo()

if language == Languages.PYTHON:
    print("I love Python")
elif language == Languages.GO:
    print("I love Go")
elif language == Languages.JAVASCRIPT:
    print("I love Javascript")
else:
    print("I love Rust🦀")

실행 결과

RUST
I love Rust🦀

러스트의 열거형은 enum 키워드로 선언이 가능합니다. 이때 값이 없는 열거형과 값이 있는 열거형 두 가지를 만들 수 있는데, 먼저 값이 없는 열거형을 만들어 보면 다음과 같습니다. impl 블럭을 이용해 열거형에서 사용할 메소드를 만들 수 있습니다. 이에 관련한 자세한 문법은 나중에 객체지향을 배우면서 좀더 자세히 다루겠습니다. 마지막으로, 파이썬에서 if 문을 사용한 것과 다르게, 러스트에서는 match 를 이용해 열거형의 값에 따라 다른 행동을 하도록 만듭니다.

fn main() {
    // Enum
    #[allow(dead_code)]
    #[derive(Debug)] // derive Debug trait, to print the enum
    enum Languages {
        Python,
        Rust,
        Javascript,
        Go,
    }

    impl Languages {
        fn echo(&self) {
            println!("{:?}", &self);
        }
    }

    let language = Languages::Rust;
    language.echo();

    // match
    match language {
        Languages::Python => println!("I love Python"),
        Languages::Go => println!("I love Go"),
        Languages::Javascript => println!("I love Javascript"),
        _ => println!("I love Rust🦀"),
    }
}

실행 결과

Rust
I love Rust🦀

열거형에 값을 지정하려면 열거형을 선언하면서 타입을 지정하면 됩니다. 열거형 변수 뒤에 (타입) 과 같이 입력하면 됩니다. 이제 열거형 변수를 선언할 때, 해당 타입에 대한 정보를 추가로 입력해줘야 합니다. 예를 들어, indo 라는 변수에 학년은 A, 이름은 indo라는 값을 넣으려면 다음과 같습니다.

let indo = Job::Student(Grade::A, "indo".to_string());

이제 indo 변수의 값에 따라 서로 다른 내용을 출력하도록 match 를 사용한 전체 코드는 다음과 같습니다.

#[allow(dead_code)]
fn main() {
    #[derive(Debug)] // derive Debug trait, to print the enum
    enum Grade {
        A,
        B,
        C,
    }

    enum Job {
        Student(Grade, String),
        Developer(String),
    }

    let indo = Job::Student(Grade::A, "indo".to_string());

    match indo {
        Job::Student(grade, name) => {
            println!("{} is a student with grade {:?}", name, grade);
        }
        Job::Developer(name) => {
            println!("{} is a developer", name);
        }
    }
}

실행 결과

indo is a student with grade A

Option 열거형

Option<T> 열거형은 Some(T)None 값을 가질 수 있습니다. Option<T> 열거형은 T 타입의 값이 있을 수도 있고 없을 수도 있음을 나타냅니다. Option<T> 열거형은 T 타입의 값이 있을 수도 있고 없을 수도 있음을 나타냅니다.

#![allow(unused)]
fn main() {
enum Option<T> {
    Some(T),
    None,
}
}

Option을 사용하려면, 열거형 변수 중 하나인 Some을 사용해 값을 감싸주기만 하면 됩니다. 만일 값이 없음을 나타내려면 None을 사용합니다.

fn main() {
    let some_number = Some(5);
    let some_string = Some("a string");

    let absent_number: Option<i32> = None;

    println!("{:?} {:?} {:?}", some_number, some_string, absent_number);
}

실행 결과

Some(5) Some("a string") None

match를 사용한 패턴 매칭

Option은 주로 match와 함께 사용됩니다. 그 이유는 다음 코드를 실행해 보면 알 수 있습니다.

fn check_len(vec: Vec<i32>) -> Option<usize> {
    match vec.len() {
        0 => None,
        _ => Some(vec.len()),
    }
}

fn main() {
    let nums = vec![1, 2, 3];

    match check_len(nums) {
        Some(len) => println!("Length: {}", len),
    }
}

실행 결과

error[E0004]: non-exhaustive patterns: `None` not covered
   --> src/main.rs:11:11
    |
11  |     match check_len(nums) {
    |           ^^^^^^^^^^^^^^^ pattern `None` not covered
    |

컴파일러가 match에서 None이 처리되지 않았다고 합니다. 즉 Optionmatch를 함께 사용하면, 값이 들어있는 경우와 들어있지 않은 경우 두 가지를 반드시 체크하게 됩니다. 덕분에 예상치 못한 결과가 발생하는 것을 막을 수 있습니다. None을 추가한 코드는 다음과 같습니다.

fn check_len(vec: Vec<i32>) -> Option<usize> {
    match vec.len() {
        0 => None,
        _ => Some(vec.len()),
    }
}

fn main() {
    let nums = vec![1, 2, 3];

    match check_len(nums) {
        Some(len) => println!("Length: {}", len),
        None => println!("No elements"),
    }
}

if let 구문

만일 Option의 결과에 따라서 특정 행동만 하고 싶다면, if let 구문을 사용하면 됩니다.

fn main() {
    let val = Some(3);
    match val {
        Some(3) => println!("three"),
        _ => (),
    }

    if let Some(3) = val {
        println!("three");
    }
}

Result<T, E> 열거형

Result<T, E> 열거형은 Ok(T)Err(E) 값을 가질 수 있습니다. Ok는 결과값이 정상적으로 존재함을 의미하고, Err는 에러가 발생했음을 나타냅니다.

#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}
}

match를 사용한 패턴 매칭

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => panic!("There was a problem opening the file: {:?}", error),
    };
}

if let 구문

if let 구문은 Result<T, E> 열거형의 값을 패턴 매칭하여 값을 반환합니다. 만약 Result<T, E> 열거형의 값이 Ok(T)라면 T 값을 반환합니다. 만약 Result<T, E> 열거형의 값이 Err(E)라면 Err(E) 값을 반환합니다.

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");

    if let Ok(file) = f {
        // 파일을 사용합니다.
    } else {
        // 파일을 열 수 없습니다.
    }
}

? 연산자

? 연산자는 함수 안에서 사용이 가능합니다. 이때 Result 의 결과값을 리턴합니다. ?을 사용하지 않으면 모든 Result를 리턴하는 경우를 아래와 같이 만들어야 합니다. ?가 사용된 함수의 리턴 타입이 Result여야 하기 때문에 마지막에 Ok(())를 리턴한다는 점에 주의하세요.

use std::fs::File;
use std::io;
use std::io::prelude::*;

struct Info {
    name: String,
    age: i32,
    rating: i32,
}

fn write_info(info: &Info) -> io::Result<()> {
    // Early return on error
    let mut file = match File::create("my_best_friends.txt") {
        Err(e) => return Err(e),
        Ok(f) => f,
    };
    if let Err(e) = file.write_all(format!("name: {}\n", info.name).as_bytes()) {
        return Err(e);
    }
    if let Err(e) = file.write_all(format!("age: {}\n", info.age).as_bytes()) {
        return Err(e);
    }
    if let Err(e) = file.write_all(format!("rating: {}\n", info.rating).as_bytes()) {
        return Err(e);
    }
    Ok(())
}

fn main() {
    if let Ok(_) = write_info(&Info {
        name: "John".to_string(),
        age: 32,
        rating: 10,
    }) {
        println!("Writing to file succeeded!");
    }
}

?를 사용하면 훨씬 간결한 코드를 만들 수 있습니다. 정리하자면, ?는 에러가 발생하면 에러를 즉시 리턴해 함수를 종료하고, Ok면 결과값만 리턴하고 다음 코드로 넘어갑니다.

use std::fs::File;
use std::io;
use std::io::prelude::*;

struct Info {
    name: String,
    age: i32,
    rating: i32,
}

fn write_info(info: &Info) -> io::Result<()> {
    let mut file = File::create("my_best_friends.txt")?;
    // Early return on error
    file.write_all(format!("name: {}\n", info.name).as_bytes())?;
    file.write_all(format!("age: {}\n", info.age).as_bytes())?;
    file.write_all(format!("rating: {}\n", info.rating).as_bytes())?;
    Ok(())
}

fn main() {
    if let Ok(_) = write_info(&Info {
        name: "John".to_string(),
        age: 32,
        rating: 10,
    }) {
        println!("Writing to file succeeded!");
    }
}