본문 바로가기

Rust/Basic

[Rust Tutorial] 10 - Generic

 

 

 


Intro

Generic이란? Type에 의존하지 않는 유연한 코드를 작성할 수 있도록 도와주는 기능입니다. Generic을 처음 접하게 되면 Code의 새로운 문법으로 인한 거부감이 있을 수 있습니다. 하지만 Rust에서 Generic은 꽤 이점이 많은 기능입니다.

 

  1. 코드 재사용성 향상 : 여러가지 Type에 대해서 동작이 필요할 때 각 Type별로 작성할 필요 없이 Generic을 통해 범용 코드를 작성할 수 있습니다.
  2. 타입 안전성 보장 : Generic을 사용하면 Compiler가 코드에서 type의 일관성을 검증합니다. 특히 Trait Bound (Generic Constraints) 로 특성이 포함되지 않은 Type은 Generic에 허용하지 않도록 Compiler가 검증합니다.
  3.  zero-cost abstraction : "사용하지 않는 기능에 대해서는 비용을 지불하지 않는다"는 철학입니다. Rust의 제네릭(Generic)은 컴파일 시점에 사용된 구체적인 타입으로 변환(모놀리틱 처리)되어, 각 타입에 대해 최적화된 코드를 생성합니다. 이 과정은 컴파일 타임에 이루어지며, 제네릭 사용으로 인한 추가적인 런타임 비용이 발생하지 않습니다.

 

generic은 그 효과성, 그리고 문법 둘 다 중요한 요소이기 때문에 다양한 예시를 담은 code를 준비해 보았습니다. 확인해 보겠습니다.

 


 

Code Example

 

Rust 1.79.0 (released 2024-06-13) 

#[allow(warnings)]
fn main(){
    // ============================================================================================
    // Generic : Type에 의존하지 않고 유연한 코드를 작성할 수 있는 기능
    // ============================================================================================

    
    /* Generic struct */

    // struct에서 사용되는 필드의 type을 generic 선언했다.
    struct Point<T>{
        x: T,
        y: T,
    }

    let p1 = Point{x: 12, y: 34};  //   정수값을 넣으면 기본 i32 Type로 추론된다.
    let p2: Point<u8> = Point{x: 12, y: 34};   //   특정 Type을 지정할 수도 있다.

    // 여기까지 Point를 Generic으로 사용했다면 Compile time에서 아래와 같은 code가 생성된다.
    // 컴파일 타임에 다음과 같이 구체적인 타입으로 변환된다 (Monomorphization).
    struct PointI32{
        x: i32,
        y: i32,
    }

    struct PointU8{
        x: u8,
        y: u8,
    }

    let p1 = PointI32{x:12, y: 34};
    let p2 = PointU8{x:12, y: 34};

    

    /* impl + Generic struct */

    impl<T> Point<T> {      // impl<T>로 작성해야 함. specific type과 문법적 구분 필요.
        fn new(x: T, y: T) -> Self {        
            Self{x, y}
        }

        fn x(&self) -> &T {
            &self.x
        }
    }



    /* impl + 특정 type struct. */

    // f64 type인 경우만 distance를 구하도록 했다.
    impl Point<f64> {
        fn distance(&self) -> f64 {
            (self.x.powi(2) + self.y.powi(2)).sqrt()
        }    
    }

    let p1: Point<i32> = Point::new(10, 20);
    // [Error] no method named `distance` found for struct `m_10_generic::example::Point<i32>` in the current scope
    // p1.distance();

    let p2: Point<f64> = Point::new(10.0, 20.0);
    println!("p2.distance : {}", p2.distance());



    /* 변수 개수만큼 Generic 사용 + method */

    #[derive(Debug)]
    struct GenericPoint<T, U>{
        x: T,
        y: U,
    }

    impl<T1, U1> GenericPoint<T1, U1> {
        // impl은 T1, U1을 사용, mix_up은 other의 type을 받은 후 self와 type을 합침
        fn mix_up<T2, U2>(self, other: GenericPoint<T2, U2>) -> GenericPoint<T1, U2>{
            GenericPoint{
                x: self.x,
                y: other.y,
            }
        }
    }

    let gp1: GenericPoint<u8, f64> = GenericPoint{x:1, y:2.2};
    let gp2: GenericPoint<f64, i32> = GenericPoint{x:3.3, y:4};
    let gp3 = gp1.mix_up(gp2);      // GenericPoint<u8, i32>



    /* Generic Constraints (Trait Bounds) : 특정 동작이 구현된 Generic type만 허용하도록 하는 방법 */
    // Display trait 예 : "print 동작이 구현된 Generic type만 허용"하려면 Display trait을 사용
    
    // 문법으로는 <T: Display>로 작성해서 T에 Display trait이 구현된 Type만 허용하도록 명시한다.
    use std::fmt::Display;
    fn display_item<T: Display>(item: T) {
        println!("{}", item);       // Display trait
        // println!("{:?}", item);  // [Error] "{:?}"은 Debug trait 이다.
    }

    
    // 2개 이상의 Trait이 필요할 때는 +로 표현한다.
    use std::fmt::Debug;
    fn debug_item<T: Display + Debug>(item: T){
        println!("{}", item);       // Display trait
        println!("{:?}", item);     // Debug trait
    }

    // PartialOrd trait 예 : PartialOrd는 크기를 비교할 수 있는 type에 대한 특성이다.
    struct Item<T> {
        value: T,
    }

    use std::cmp::Ordering;
    impl<T: PartialOrd> Item<T> {
        fn is_bigger(&self, other: &Item<T>) -> bool {
            self.value > other.value    // PartialOrd trait제한으로 크기 비교가 가능한 Type만 허용
        }
    }

    struct Dot<T>{
        x: T,
        y: T,
    }

    // no partialOrd type (custom type : Dot)
    let item_string1: Item<Dot<i32>> = Item{value: Dot{x:1, y: 2}};
    let item_string2: Item<Dot<i32>> = Item{value: Dot{x:2, y: 3}};
    // [Error] Dot에는 PartialOrd trait 구현이 안되어 있어 Compare할 수 없다.
    // item_string1.compare(&item_string2);

    // partialOrd type (i32 has partialOrd trait)
    let item_int1: Item<i32> = Item{value: 10};
    let item_int2: Item<i32> = Item{value: 20};
    println!("item_int1 is_bigger than item_int2 : {}", item_int1.is_bigger(&item_int2));


    
    /* Where keyword를 사용하여 Generic Constraints를 구현 */

    #[derive(PartialEq, PartialOrd)]
    struct Position<T> {
        x: T,
        y: T,
    }

    use std::ops::Add;
    // 아래와 같이 작성해도 된다.
    // impl<T: Add<Output = T> + PartialOrd + Copy> Position<T>{
    impl<T> Position<T>
    where
        T: Add<Output = T> + PartialOrd + Copy, // 크기 비교, Add가 가능한 T, Copy
        // U: ...
        // V: ...
        // 이와 같이 많은 type에 대한 다양한 trait을 작성해야 할 경우 where을 사용한다.
    {
        fn ge(&self, other: &Self) -> bool {
            (self.x + self.y) >= (other.x + other.y)
        }

        fn gt(&self, other: &Self) -> bool {
            (self.x + self.y) > (other.x + other.y)
        }

        fn le(&self, other: &Self) -> bool {
            (self.x + self.y) <= (other.x + other.y)
        }

        fn lt(&self, other: &Self) -> bool {
            (self.x + self.y) < (other.x + other.y)
        }

        fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
            let self_sum = self.x + self.y;
            let other_sum = other.x + other.y;

            if self_sum < other_sum {
                Some(Ordering::Less)
            } else if self_sum > other_sum {
                Some(Ordering::Greater)
            } else {
                Some(Ordering::Equal)
            }
        }
    }

    // Position<i32>
    let pos1 = Position{x: 10, y: 20};
    let pos2 = Position{x: 15, y: 5};
    if pos1 > pos2 {
        println!("pos1 is greater then pos2");
    } else {
        println!("pos1 is not greater then pos2");
    }

    // Position<f64>
    let pos3 = Position{x: 2.1, y: 3.4};
    let pos4 = Position{x: 1.2, y: 2.5};
    if pos3 > pos4 {
        println!("pos3 is greater then pos4");
    } else {
        println!("pos3 is not greater then pos4");
    }
}

 

 

'Rust > Basic' 카테고리의 다른 글

[Rust Tutorial] 11 - Option  (0) 2024.11.25
[Rust Tutorial] 9 - mod, use  (2) 2024.11.20
[Rust Tutorial] 8 - Struct, Impl, Trait  (0) 2024.11.18
[Rust Tutorial] 7 - Ownership  (0) 2024.11.17
[Rust Tutorial] 6 - Control Flow  (0) 2024.11.17