[Rust] study Generic

Rust Generic 자료형

Posted by lim.Chuck on August 20, 2024

[Rust]

  1. [Rust] Rust 설치
  2. [Rust] Hello Cargo
  3. [Rust] VSCode 설정
  4. [Rust] Rust 공부 시작
  5. [Rust] Rust 기초
  6. [Rust] Rust 제어문
  7. [Rust] Rust 자료형
  8. [Rust] Rust 제네릭
  9. [Rust] Rust 소유권, 대여, 데이터
  10. [Rust] study Text
  11. [Rust] Rust 객체지향 프로그래밍
  12. [Rust] Rust 스마트 포인터
  13. [Rust] Rust 프로젝트 구성 및 구조

참고문서
《 Tour of Rust 》
《 Rust_Book 》
《 프로그래밍 언어 러스트를 배웁시다! 》

Rust Study Generic

generic 자료형은 Rust에서 엄청나게 중요합니다. 널 허용(nullable) 값을 표현할 때에도 쓰이며 (i.e. 아직 값이 없을 수도 있는 변수), 오류 처리, collection, 등등에도 쓰입니다! 이 장에서는 언제든 사용될만한 기본적인 generic 자료형을 살펴보겠습니다.

1. Generic 자료형이란?

generic 자료형은 structenum을 부분적으로 정의하여, 컴파일러가 컴파일 타임에 코드 사용을 기반으로 완전히 정의된 버전을 만들 수 있게 해줍니다.

Rust는 일반적으로 인스턴스화 하는 것을 보고 최종 자료형을 유추할 수 있으나, 여러분의 도움이 필요한 경우에는 turbofish (내 친한 친구입니다!)로도 알려진 ::<T> 연산자를 사용해 언제든 명시적으로 지정할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 일부만 정의된 struct 자료형
struct BagOfHolding<T> {
    item: T,
}

fn main() {
    // 중요: 여기서 generic 자료형을 쓰면, 컴파일 타임에 생성된 자료형을 생성하게 됨.
    // Turbofish를 쓰면 명시적일 수 있다.
    let i32_bag = BagOfHolding::<i32> { item: 42 };
    let bool_bag = BagOfHolding::<bool> { item: true };

    // Rust는 generic에도 자료형을 유추할 수 있다!
    let float_bag = BagOfHolding { item: 3.14 };

    // 중요: 실생활에서는 가방 속에 가방을 넣지 마시오
    let bag_in_bag = BagOfHolding {
        item: BagOfHolding { item: "쾅!" },
    };

    println!(
        "{} {} {} {}",
        i32_bag.item, bool_bag.item, float_bag.item, bag_in_bag.item.item
    );
}
$ 42 true 3.14 쾅!

2. 아무 것도 없는 것을 표현하기

다른 언어에서 null 키워드는 값이 없음을 나타내기 위해 사용됩니다. 이는 프로그래밍 언어에 어려움을 유발하는데, 변수나 field를 처리하는 중에 프로그램이 실패할 수도 있기 때문입니다.

Rust에는 null이 없습니다만, 아무 것도 없음을 표현하는 것의 중요성을 무시하는 것은 아닙니다! 우리가 이미 아는 도구를 사용한 순수한 표현법을 고려해봅시다.

이렇게 한 개 이상의 선택 가능한 값에 대해 None 선택지를 제공하는 방법은 null 값이 없는 Rust에서 매우 흔한 패턴입니다. generic 자료형은 이러한 어려움을 해결하는 데에 도움을 줍니다.

1
2
3
4
5
6
7
8
9
enum Item {
    Inventory(String),
    // None은 항목의 부재를 나타냄
    None,
}

struct BagOfHolding {
    item: Item,
}

3.옵션

Rust에는 null을 쓰지 않고도 nullable한 값을 표현할 수 있는 Option이라 불리는 내장된 generic enum이 있습니다.

1
2
3
4
enum Option<T> {
None,
Some(T),
}

이 enum은 매우 흔해서, 어디서나 SomeNone으로 enum을 인스턴스화 할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 일부만 정의된 struct 자료형
struct BagOfHolding<T> {
    // 자료형 T의 인자는 다른 곳으로 넘겨질 수 있음
    item: Option<T>,
}

fn main() {
    // 중요: i32를 위한 가방에 아무 것도 안들었다!
    // Rust가 무슨 자료형의 가방인지 알 수가 없으므로 자료형을 지정해야 함.
    let i32_bag = BagOfHolding::<i32> { item: None };

    if i32_bag.item.is_none() {
        println!("가방에 아무 것도 없다!")
    } else {
        println!("가방에 뭔가 있다!")
    }

    let i32_bag = BagOfHolding::<i32> { item: Some(42) };

    if i32_bag.item.is_some() {
        println!("가방에 뭔가 있다!")
    } else {
        println!("가방에 아무 것도 없다!")
    }

    // match는 Option을 우아하게 분해하고, 모든 케이스를 처리하도록 해준다!
    match i32_bag.item {
        Some(v) => println!("가방에서 {}를 찾았다!", v),
        None => println!("아무 것도 찾지 못했다"),
    }
}
$ 가방에 아무 것도 없다!
$ 가방에 뭔가 있다!
$ 가방에서 42를 찾았다!

4. 결과

Rust에는 실패할 가능성이 있는 값을 리턴할 수 있도록 해주는 Result라 불리는 내장된 generic enum이 있습니다. 이는 Rust에서 오류 처리를 하는 관용적인 방법입니다.

1
2
3
4
enum Result<T, E> {
    Ok(T),
    Err(E),
}

우리의 generic 자료형이 쉼표로 구분된 여러개의 매개화된 자료형(parameterized types)을 갖고 있는 것을 기억해 두기 바랍니다.

이 enum은 매우 흔해서, 어디서나 OkErr로 enum을 인스턴스화 할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn do_something_that_might_fail(i: i32) -> Result<f32, String> {
    if i == 42 {
        Ok(13.0)
    } else {
        Err(String::from("맞는 숫자가 아닙니다"))
    }
}

fn main() {
    let result = do_something_that_might_fail(12);

    // match는 Result를 우아하게 분해하고, 모든 케이스를 처리하도록 해준다!
    match result {
        Ok(v) => println!("{} 발견", v),
        Err(e) => println!("오류: {}", e),
    }
}
$ 오류: 맞는 숫자가 아닙니다

5. 실패할 수 있는 메인

mainResult를 리턴할 수 있다!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
fn do_something_that_might_fail(i: i32) -> Result<f32, String> {
    if i == 42 {
        Ok(13.0)
    } else {
        Err(String::from("맞는 숫자가 아닙니다"))
    }
}

// Main은 아무 값도 리턴하지 않지만, 오류를 리턴할 수 있다!
fn main() -> Result<(), String> {
    let result = do_something_that_might_fail(12);

    match result {
        Ok(v) => println!("{} 발견", v),
        Err(_e) => {
            // 이 오류를 우아하게 처리한다

            // main으로부터 무슨 일이 발생했는지 새 오류를 리턴한다!
            return Err(String::from("main에서 뭔가 잘못 되었습니다!"));
        }
    }

    // 모든 일이 잘 끝났음을 표현하기 위해
    // Result Ok 안에 unit 값을 쓰고 있는걸 잘 봐두십시오
    Ok(())
}

6. 우아한 오류 처리

Rust는 매우 흔히 사용되는 Result와 함께 쓸 수 있는 강력한 연산자 ?를 갖고 있습니다. 다음 두 구문은 동일합니다:

1
2
3
4
5
6
7
//줄였을때
do_something_that_might_fail()?
//풀어쓸때
match do_something_that_might_fail() {
    Ok(v) => v,
    Err(e) => return Err(e),
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn do_something_that_might_fail(i: i32) -> Result<f32, String> {
    if i == 42 {
        Ok(13.0)
    } else {
        Err(String::from("맞는 숫자가 아닙니다"))
    }
}

fn main() -> Result<(), String> {
    // 얼마나 코드를 줄였는지 보세요!
    let v = do_something_that_might_fail(42)?;
    println!("{} 발견", v);
    Ok(())
}
$ 13 발견

7.추한 옵션/결과 처리

간단한 코드를 짤 때에도 Option/Result를 쓰는 것은 귀찮은 일일 수 있습니다. OptionResult 둘 다 빠르고 더러운 방식으로 값을 가져오는데 유용한 unwrap이라는 함수를 갖고 있습니다. unwrap은:

  1. Option/Result 내부의 값을 꺼내오고
  2. enum이 None/Err인 경우에는 panic!

다음 두 코드는 동일합니다:

1
2
3
4
5
6
7
8
9
10
11
12
//unwrap Option
my_option.unwrap()
match my_option {
    Some(v) => v,
    None => panic!("some error message generated by Rust!"),
}
//unwrap Result
my_result.unwrap()
match my_result {
    Ok(v) => v,
    Err(e) => panic!("some error message generated by Rust!"),
}

좋은 러스타시안(rustacean)이 되어 가능한 제대로 match를 사용하세요!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn do_something_that_might_fail(i: i32) -> Result<f32, String> {
    if i == 42 {
        Ok(13.0)
    } else {
        Err(String::from("맞는 숫자가 아닙니다"))
    }
}

fn main() -> Result<(), String> {
    // 간결하지만 가정적이고 빠르게 지저분 해짐
    let v = do_something_that_might_fail(42).unwrap();
    println!("{} 발견", v);

    // panic! 될 것임
    let v = do_something_that_might_fail(1).unwrap();
    println!("{} 발견", v);

    Ok(())
}
$ 13 발견

8. 벡터

가장 유용한 generic 자료형들 중 하나가 collection 자료형입니다. vector는 Vec struct로 표현되는 가변 크기의 리스트입니다.

vec! macro는 vector를 수동으로 일일이 만드는 대신, 손쉽게 생성할 수 있게 해줍니다.

Vec는 vector를 for 반복문에 손쉽게 넣을 수 있도록 vector로부터 반복자를 생성할 수 있는 iter() 메소드를 갖고 있습니다.

메모리 상세:

  • Vec은 struct이나, 내부적으로는 내용물이 heap에 있는 고정 리스트에 대한 참조를 포함하고 있습니다.
  • vector는 기본 용량을 갖고 시작하는데, 용량보다 많은 내용물이 추가될 경우, 큰 용량을 가진 새 고정 리스트를 위해 heap에 데이터를 재할당합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
    // 자료형을 명시적으로 할 수 있음
    let mut i32_vec = Vec::<i32>::new(); // turbofish <3
    i32_vec.push(1);
    i32_vec.push(2);
    i32_vec.push(3);

    // 하지만 Rust가 얼마나 똑똑하게 자료형을 자동으로 결정하는지 보십시오
    let mut float_vec = Vec::new();
    float_vec.push(1.3);
    float_vec.push(2.3);
    float_vec.push(3.4);

    // 아름다운 macro입니다!
    let string_vec = vec![String::from("Hello"), String::from("World")];

    for word in string_vec.iter() {
        println!("{}", word);
    }
}
$ Hello
$ World

마무리

rust를 공부하다보니 jstry/catch문을 rust에서는 match로 잡아내는거 같다. unwrap()사용하기도 하지만 좋지않은방법 match를 사용하자.
그러면서 match를 사용 안하고 함수뒤에 ?를 붙여 에러처리를 하기도하네 신기방기
다음 장에서는 Rust의 중요한 개념인 데이터 소유권(data ownership)에 대해 레스기릿.