열거형
열거형은 여러 상수들의 집합으로 새로운 타입을 선언하는 방법입니다. 파이썬에서는 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
이 처리되지 않았다고 합니다. 즉 Option
과 match
를 함께 사용하면, 값이 들어있는 경우와 들어있지 않은 경우 두 가지를 반드시 체크하게 됩니다. 덕분에 예상치 못한 결과가 발생하는 것을 막을 수 있습니다. 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!"); } }