[Rust] 열거자 (enumuerations)

MS Choi·2022년 7월 13일
0

Rust Study

목록 보기
4/4

열거자를 공부해야하나요?

C,C++,JAVA등 다른 언어를 공부했던 사람이라도 Rust의 열거자는 조금 차이가 있기 때문에 공부를 하고 가는게 좋다. 우리가 흔히 생각하는 열거보단 구조체에 훨씬 가깝기 때문이다.

이러한 형태는 F#,하스켈등 주로 함수형언어에서 볼 수 있는 형태이다.

열거자의 값

enum IpAddrKind{
	V4,
    V6,
}

정의 자체는 우리가 기존에 사용했던 열거형과 크게 다르지 않다. 실사용도 다음과 같으므로 거의 차이가 없다고 할 수 있다.


// 이렇게 선언한다.	
    let four = IpAddrKind::V4;
    let six  = IpAddrKind::V6;
    //매개변수로도 사용가능!
    fn route(ip_type:IpAddrKind){}

열거자의 값을 이용하면 구조체에 데이터를 다음과 같이 적용할 수 있다.

enum IpAddrKind{
	V4,
    V6,
}

struct IpAddr{
	kind: IpAddrKind,
    address: String,
}
//여기까지는 기존의 언어들과 큰차이가 없다.
let home = IpAddr{kind: IpAddrKind::V4, address: String:from("127.0.0.1")};
let loopback = IpAddr{kind: IpAddrKind::V6, address: String:from("::1")};

rust의 열거자가 가진 기능은 지금부터가 본격적인 시작이다. 먼저 rust는 문자열로도 열거값을 지정할 수 있다.

enum IpAddr{
	V4(String),
    V6(String),
}
//이전과 같은 코드 훨씬 간단해졌다.
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));

거기에다가 정수로 모든 값이 고정되었던 기존의 열거자들과 달리 열거값마다 타입을 다르게도 가능하다.

enum IpAddr{
//ipv4는 한자리가 0~255까지니까
	V4(u8,u8,u8,u8),
    V6(String),
}

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

사실 IPAddress에 관한 열거형이나 구조체는 표준 라이브러리에 모두 선언되어있기 때문에 이번에는 조금 다양한 데이터 타입들로 새로운 열거형을 선언해보자.

	enum Message{
    	Quit, //연관 데이터 값x
        Move{x:i32, y:i32},//익명의 구조체를 포함
        Write(String),//하나의 스트링
        ChangeColor(i32,i32,i32),//3개의 i32값
    }
	
    //위의 열거형을 구조체로 표현해보자
    
    struct QuitMessage;
    struct MoveMessage{
    	x: i32,
        y: i32,
    };
    struct WriteMessage(String);//튜플 구조체
    struct ChangeColor(i32,i32,i32);//튜플 구조체

슬슬 열거형이 어떠한곳에 사용되는지 감이 올거라고 생각한다. 만약 위의 코드에서 선언한 데이터를 모두 받아야하는 함수를 작성할 경우, 구조체를 이용하면 파라미터가 4개가 필요하지만 열거형은 1개의 파라미터로 4가지를 다 처리할 수 있는 장점이 있다.

거기다, 열거형도 함수와 method선언이 가능하기때문에 구조체와 기능이 거의 비슷하다.

	impl Message{
    	fn call(&self){
        	//여기에 메서드 본문을 작성한다.
        }
    
    }
    
    let m= Message::Wirte(String::from("hello"));
    
    m.call();

Option 열거자

표준 라이브러리에 존재하는 Option 열거자는 매우 범용적으로 많이 쓰인다. 왜냐하면 설계 자체가 데이터가 존재할 수도 있고, 존재 하지 않을 수도 있는 상황에 쓸 수 있도록 만들어졌기 때문이다.

이런 타입을 언어가 제공할 수 있다는건 코드가 모든 경우의 수를 처리하고 있는지를 컴파일러가 확인 할 수 있다는 것을 의미한다.

(이러한 특징때문에 Rust가 사랑을 받는게 아닐까? 컴파일 타임에 모든걸 검사할 수 있다니...)

Rust Null(아무런 값도 가지지않는)값이 존재하지 않는 프로그래밍 언어이다. 이 Option 열거자를 사용하면 rust에서도 Null의 개념을 사용할 수 있다.

정확히 말하면 Option 열거자를 통해 어떤값의 존재여부를 확인함으로 처리를 분기시킬 수 있다.

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

Option 열거자는 프렐류드(prelude)에 포함되어 있다. 여기서 말하는 프렐류드가 무엇이냐면

prelude

A prelude is a collection of names that are automatically brought into scope of every module in a crate.

모든 모듈이 만들어 질때 자동적으로 가져오게 되는 집합이라고 말한다. 별도의 선언이 없어도 우리가 예약어를 쓰는것처럼 쓸 수 있는 개념으로 생각하면 된다.

즉 우리는 Option 열거자를 사용하기 위해 라이브러리를 import 할 필요가 없으며, Some이나 None을 사용하기 위해 Option:: 을 명시할 필요도 없다.

(여기서 generic에 대한 개념이 나오지만 이부분은 따로 설명하지 않겠다.)

Option 열거자를 이용해 변수 선언을 해보도록 하겠다.

let some_number = some(5);
let some_number = some("a string");

let absent_number:Option<i32> = None;

여기서 중요한건 None의 선언이다. Some과 다르게 데이터 타입을 명시해준것을 확인 할 수 있다. 왜냐하면 None값만으로 Some에 어떤 값이 들어올지 알 수 없기때문이다(None은 열거자에서 별도의 타입을 갖지 않는다.)

그렇다면 Option을 사용하는것은 null을 쓰는것에 비해 어떠한 장점이 있을까?

    int a= 5;
    int* b = nullptr;

    int* c = a+b;

<cpp로 작성한 null코드>

let a  = 5;
let b : Option<i8> = 5;

let sum = a+b;

<Option을 사용한 rust코드>

이 두언어를 각각 compile해보면 cpp코드는 컴파일이 되지만, rust코드는 컴파일 단계에서 오류를 찾아내 사용자에게 알려준다.

rust에서 오류가 나는 이유는 a와 b의 타입이 다르기 때문이다.

즉, Option열거자를 우리가 아는 T타입으로 사용하려면 별도의 변환 과정이 필요하게 되고, 이 과정은 사용자가 자신을 가지고 프로그래밍 할 수 있도록 만들어준다.

이러한 T타입으로 변환하기위한 다양한 메서드를 Option열거자는 제공하고 있고, 이 메서드들을 이용하면 상황에 맞게 T타입으로 변경할 수 있게된다.

하지만 Option열거자도 한가지 단점이 있는데, 열거자에 나열된 개별 값들을 처리할 코드를 작성해 한다. 어떤 코드는 Some(T) 값을 가진 경우에만 실행해야 하며, 이때 이 코드는 열거자 안에 저장된 T의 값에 접근 할 수 있다. None값을 가진 경우에 실행될 코드가 있다면 이코드는 T값에 접근 할 수 없다.

이 부분을 쉽게 작성할 수 있는 연산자를 Rust에서 제공해주는데 바로 match 키워드이다.

match 흐름 제어 연산자

다른언어를 배워본 사람이라면 흐름제어 연산자가 무엇인지 알고 있을거라고 생각한다. (if, switch,for, while등...)

match는 타 언어에서 사용하는 switch-case와 매우 비슷하기 때문에 기존의 switch가 뭔지 알고 있는 분이라면 쉽게 이해할 수 있다.

enum Coin{
	Penny,
    Nickle,
    Dime,
    Quarter
}

fn value_in_cents(coin : Coin) -> u32{
//값에 따라 분류한다.
    match coin{
    	Coin::Penny => 1,
        Coin::Nickle => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

간단한 예제를 살펴 본것처럼, 열거의 값에 따라 분류하는걸 확인 할 수 있다. 참고로 단순히 함수에 대한 값을 리턴하는것 만아닌 다음과 같은 방법으로 함수를 호출해 실행할 수도 있다.

fn value_in_cents(coin : Coin) -> u32{
//값에 따라 분류한다.
    match coin{
    	Coin::Penny =>{
        	println!("Lucky Penny!);
            1
        },
        Coin::Nickle => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

그렇다면 열거자를 사용하는 예제를 봐보자

fn plush_one(x: Option<i32>)->Option<i32>{
	match x{
    	None => None,
        Some(i) => Some(i+1),
    }
    
    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);

}

<굳이 설명을 달지 않겠다 이 코드의 문제가 없는지 확인해 보자>

다만 Match를 사용 할때도 지켜야하는 조건이 있는데, 바로 모든 경우에 대해 처리가 되어야한다는것이다. 열거값이 6개면 6개 전부에 대한 처리가 구현되어있어야한다. 사용자가 이 모든걸 체크하면서 개발할 필요는없다. 우리가 무언가를 빼먹었어도 컴파일러가 친절하게 알려주기때문에 우리는 그걸 보고 수정하면된다.

하지만, 모든 걸 처리해야하는 특성때문에 괜히 불필요한 처리를 해야할 상황이 올수도 있다. 이런 상황을 피하기 위해 제공되는 기능이 있는데 바로 자리지정자 이다. match를 사용할때 처리를 하지않을 값에 대해 자리 지정자를 사용하면 된다. 단 자리 지정자는 모든 값에 일치함을 의미함으로 무언가 처리를 하게된다면 유의해야한다.


let some_u8_value = 0u8;

match some_u8_value {
	1 => println!("one"),
    3 => println!("three"),
    5 => println!("five"),
    7 => println!("seven"),
    _ => (),
}
//if let
if let Some(3) = some_u8_value{println!("three"), _=>(),}

<이러면 짝수 값들은 처리를 하지 않는다. ()는 단순한 유닛값이기 때문이다>

match를 쓰다보면 너무 코드가 장황해진다고 느낄수도 있다. 이런 느낌이 든다면 if let을 통해 코드를 더 간결하게 표현할 수있다.
단순히 if let으로 끝나는게 아닌 if let ~ else로 확장도 가능하니 필요할때 사용하면 매우 유용하게 작성이 가능하다.

다음시간 부터는

이제는 API를 지원하기위해 Rust에서 제공해주는 기능인 모듈에 대해 공부해볼 예정이다.

profile
다양한 경험을 하는걸 좋아하는 개발자입니다.

0개의 댓글