비욘드 JS: 러스트 - Enum

dante Yoon·2022년 12월 22일
1

beyond js

목록 보기
6/20
post-thumbnail

동영상 강의로 보기

Enum을 왜 써야 돼?

Enum은 가독성을 높여 줍니다.

포커 카드의 하트, 다이아몬드, 클로바, 스페이드를 표현할 때 일반적인 문자열이나 숫자형보다는 enum을 사용하는게 더 읽기 쉬울 것입니다. 개발자로 하여금 특정 도메인에 있어서 의사소통을 하는 것이 용이하게 해주며 어떤 객체가 현재 어떤 역할을 수행하고 있는지 이해하기 쉽게 합니다.

원본: Michał Parzuchowski
서버: 거기 다이아몬드 좀 건내줘!

Enum은 컴파일 타임 에러를 잘 잡는데 도움을 줍니다.

여러 종류의 카드 게임 중 포커 카드만 사용해야 하는 게임이 있다면, Enum 그룹에 없는 카드를 사용하는 함수에서는 전달된 argument가 enum의 멤버가 아니기 때문에 컴파일 에러를 발생시킵니다.

Enum은 효율적인 코드를 작성할 수 있게 도와줍니다.

러스트에서 효율적인 코드를 작성하는 방법은 크게 두가지 입니다.

메모리에 Enum으로 데이터를 표현하는 것

소수의 variants를 Enum으로 표현한다면 러스트 컴파일러는 Enum을 사용함으로써 좀 더 메모리 효율적으로 값을 표현할 수 있습니다.

enum SmallEnum {
  A,
  B,
  C
}

러스트 컴파일러는 SmallEnum을 사용함으로써 세 개의 값을 하나의 바이트 만을 메모리에 담아둠으로 표현할 수 있습니다. 세 가지의 경우가 있음으로 u8 이나 i8 타입을 사용하여 다른 바이트를 할당시키는 것보다 더욱 메모리 효율적으로 값을 표현할 수 있습니다.

패턴 매칭을 enum을 통해 사용하는 것

let x: u8 = 5;

match x {
  0 => println!("x is zero"),
  1 => println!("x is one"),
  2 => println!("x is two"),
  3 => println!("x is three"),
  4 => println!("x is something else"),
}

패턴 매칭 - 원본사진: Jason Sung

패턴 매칭을 사용할 때 여러 개의 variant를 가진 하나의 enum을 활용할 수 있습니다. 러스트 컴파일일러는 패턴 매칭 시 다른 타입의 값보다 enum을 사용함으로써 더욱 효율적으로 메모리를 사용해 값을 표현할 수 있습니다.

앞선 예제 코드에서 러스트 컴파일러는 u8 타입의 값 대신에 적은 수의 variant를 표현한 enum을 통해서 효율적으로 패턴 매칭을 사용할 수 있게 되었습니다.

그래서 러스트에서 Enum은 어떻게 사용할까

struct가 특정 도메인에 해당하는 여러 데이터를 하나의 그룹으로 묶어서 표현했다면, enum은 하나의 데이터를 여러 개의 경우의 수로 표현할 수 있습니다.

만약 Rectangle Circle Triangle 이라는 모양을 나타내는 값을 표현해야 한다면 Shape이라는 enum 사용을 생각해볼 수 있습니다.

또 한가지 예로는 ip의 종류와 같이 여러 가지 경우의 수중에 한번에 최대 한가지의 값만 표현할 수 있는 경우 enum 사용이 struct 사용보다 더 적합합니다.

enum IpAddrKind {
  V4,
  V6,
}

Enum Values

enum에서 사용하는 각 값들, 그러니까 각 경우의 수들을 variants라고 부릅니다.

let four = IpAddrKind::V4;
let six = IpAddrKind:V6;

double colon (::)를 사용해 enum의 값인 variant를 사용하고 있습니다. 이 때 four,six는 모두 enum인 IpAddrKind 타입입니다.

다음 함수를 봅시다.

fn route(ip_kind: IpAddrKind) {}

그리고 우리는 각 variant를 사용해서 다음과 같이 함수 호출을 할 수 있습니다.

route(IpAddrKind::V4);
route(IpAddrKind::V6);
enum IpAddrKind {
  V4,
  V6,
}

struct. IpAddr {
  kind: IpAddrKind,
  address: Strnig,
}

let home = IpAddr {
  kind: IpAddrKind::V4,
  address: String::from("127.0.0.1"),
} 

let loopback = IdAddr {
  kind: IpAddrKind::V6,
  address: String::from("::1"),
}

우리는 struct IpAddr를 만들고 두 필드 kind, address를 선언했습니다.
그리고 home, loopback이라는 두 가지의 인스턴스를 생성해 address 필드에 각기 다른 ip 주소를 바인딩시켰습니다.

하지만 이렇게 각 enum 타입에 해당하는 값을 하나의 인스턴스로 가지고 있을 필요가 없습니다.
enum을 사용해 좀 더 효율적이고 직관적으로 표현할 수 있습니다.

enum IpAddr {
  V4(String),
  V6(String),
}

let home = IpAddr:V4(String::from("127.0.0.1"));

let loopback = IpAddr::V6(String::from("::1"));

우리는 enum의 두 variant에 데이터를 결합시켰습니다. 따라서 별도의 struct를 선언하지 않아도 하나의 enum을 패턴 바인딩할 때 데이터까지 같이 특정 변수에 바인딩시킬 수 있게 되었죠.

IpAddr::V4()는 String 타입의 argument를 받아들이는 함수 호출입니다. 그리고 IpAddr 타입의 인스턴스를 반환하죠. 우리는 enum을 정의함으로 constructor function을 갖게된 것입니다.

enum variant 의 데이터는 어떤 형식도 될 수 있습니다.

struct Ipv4Addr {
  // --snip--
}

struct Ipv6Addr {
  // --snip--
}

enum IpAddr {
 V4(Ipv4Addr),
 V6(Ipv6Addr),
}

위 코드는 enum의 각 variant에 저장되는 데이터에 그 어떤 타입도 들어갈 수 있음을 보여줍니다. struct가 들어갔는데요, 이 외에도 enum이 들어갈 수도 있습니다.

enum의 variant에 저장되는 값들은 꼭 같지 않아도 됩니다.

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}
  • Quit은 아무런 데이터가 없습니다.
  • Move는 struct처럼 필드를 가지고 있습니다.
  • Write는 단일 문자열을 가지고 있습니다.
  • ChangeColor는 세 개의 i32 값을 가지고 있습니다.

Option enum

러스트에서 Option enum은 값의 존재 유무를 표현할 수 있습니다. Option은 두 가지 variant를 가집니다. Some, None이 그것입니다.

Some은 값이 존재한다는 것을 표현하고 None은 그 반대입니다.

fn divide(numerator: f64, denominator: f64) -> Option<f64> {
    if denominator == 0.0 {
        None
    } else {
        Some(numerator / denominator)
    }
}

fn main() {
    let result = divide(10.0, 2.0);
    match result {
        Some(value) => println!("Result: {}", value),
        None => println!("Cannot divide by Zero"),
    }
}

위의 코드에서 divide 함수는 두 가지 부동 소수점 타입을 argument로 받고 Option<f64> 타입을 반환합니다. 만약 분자가 0이라면 나눗셈이 성립되지 않으므로 함수는 None을 반환합니다.
그렇지 않은 경우는 나눗셈의 결과를 반환합니다.

값의 존재 유무에 대한 표현이 가능하기 때문에 Rust에서 Option 값은 매우 유용합니다. null과 같은 nullish한 값을 사용하지 않아도 되며 이에 따라 런타임에서 undefined를 참조해 reference error가 발생할 여지도 없어집니다.

이 Option 타입을 사용함으로써 안전하게 referencing 하는 것이 보장되게 되었습니다.

러스트가 null이 없다고?

Null은 백해무익한 존재가 아닙니다. 특정 이유에 따라 값이 존재하지 않아야 하는 경우 null은 해당 도메인을 표현하는 좋은 표현 수단이 됩니다.

하지만 러스트는 null이 없습니다.

이에 대한 대안으로 Option enum을 통해 값이 존재하지 않음을 표현할 수 있습니다.
아래에서 T는 제너릭입니다.

enum Option<T> {
   None,
   Some(T),
}

let some_number = Some(5);
let some_char = Some('e');

let absent_number: Option<i32> = None;

Some 사용 시 다른 enum의 variant 참조와 다르게 :: double colon을 사용하지 않아도 됩니다.
하지만 None을 사용할 때는 타입 선언을 명시적으로 해주어야 합니다.

Option<T>와 T는 다른 타입이기 때문에 컴파일러는 Option<i8>의 값이 유효한 값일지라도 T와 Option<T> 간의 numeric operation을 허락하지 않습니다

profile
성장을 향한 작은 몸부림의 흔적들

0개의 댓글