Generics, 제네릭

개인적으로 러스트에서 중요하게 생각하는 개념이 있는데 오너쉽, 트레이트, 그리고 제네릭 3가지라 생각한다. 그 중 제네릭에 대해 알아보자. 예제만 잘 숙달하여도 이해하는데 큰 문제는 없다고 생각한다.

말 그대로 제네릭은 포괄적인 타입을 말한다. 그래서 제네릭 타입을 나타낼 때에도 타입을 특정하지 않고 camel case 방식으로 <Aaa, Bbb> 로 표현한다.

Functions & Implementation

  • struct 에 제네릭 타입을 사용할 때에는 이름 뒤에 를 사용한다.
  • impl 에 사용할 떄에는 impl, Struct 뒤에 사용한다.

하나의 struct 으로 여러 타입의 struct 인스턴스 생성이 가능하다.



#[derive(Debug)]
struct SGen<T> {
    random : T,
}

impl<T> SGen<T> {
    fn get_value(&self) -> &T {
            &self.random
    }
}


fn main() {  
       let gen = SGen { random : 3}; 
        println!("{:?}", &gen);
       let gen2 = SGen { random : 3.3};
        println!("{:?}", &gen2);
       let gen3 = SGen { random : "hello".to_string()};
        println!("{:?}", &gen3);
       let gen4 = SGen { random : "Hi"};
        println!("{:?}", &gen4);
}

Traits

trait 도 제네릭으로 활용할 수 있다. Drop trait 를 통해 알아보자.



#[derive(Debug)]
struct Empty;

#[derive(Debug)]
struct Null;

trait DoubleDrop<T>  {
    fn double_drop(self, _: T);
}

// impl 할 struct 을 제네릭 타입 U 로 추가하였다. 현재 crate 에선 모든 타입이
// double_drop 을 사용할 수 있게 된다. 

impl<T, U> DoubleDrop<T> for U {
    fn double_drop(self, _: T) {}
}

fn main() { 
    let e = Empty;
    println!("{:?}", e);
    
    let n = Null; 
    println!("{:?}", n);

    // double_drop 함수 실행 후 더 이상 n, e는 사용할 수 없게 된다.
    n.double_drop(e);
    // println!("{:?}", n);
}

Bounds

제네릭의 타입을 정하고 제한하는 것을 바운드라고 한다.

바운드 표현 방식에 대해 알아보자

  • 타입 뒤에 : 후 바운드
  • 리턴 값 지정하기 전에 where 구문을 뒤에 바운드


use std::fmt::Display;

// 타입 뒤에 바로 바운드
fn printer<T: Display>(t: &T) {
    println!("{}", t);
}

// 타입 지정 후 리턴 값 넣기 전에 where T : 방법으로 바운드
fn printer_<T>(t: &T)
where
    T: Display,
{
    println!("{}", t);
}


fn main() {
   
    //struct 자체에는 display 가 되지 않기에 따로 구현 필요. 
    struct NoDisplayTrait(i32);

    //생성 시점 부터 display 를 구현한 타입만 받을 수 있게 바운드 처리.
    struct NeedDisplayTrait<T: Display>(T);

    let string_p = "tony".to_string();


    printer(&string_p);


    printer_(&string_p);


    let nodisplay =NoDisplayTrait(3);
    
    // struct 은 display trait 이 구현되지 않았기에 printer 함수 실행 불가.
    
    // printer(&nodisplay);
   
    // vec 타입은 display trait 을 구현하지 않았기에 아예 생성 조차 불가능.

    // let vec_is_not_impl_display = NeedDisplayTrait(vec![1,2,3]);
   


}

예제를 통해 자세히 알아보자.


struct Rectangle {
    length: f64,
    height: f64,
}

struct Triangle {
    length: f64,
    height: f64,
}

//trait HasArea 생성 후 함수만 생성
trait HasArea {

    fn area(&self) -> f64;

}


// HasArea 를 impl 한 Rec 
impl HasArea for Rectangle {

	// 함수식 구현
    fn area(&self) -> f64 {

        println!("this is rectangle");
        self.length * self.height
    
    }
}

// HasArea 를 impl 한 Tri 
impl HasArea for Triangle {

	// 함수식 구현
    fn area(&self) -> f64 {

        println!("this is triangle");
        (self.length * self.height) / 2.0
    
    }
}


// where 를 이용한 바운드
// 하나의 함수를 만들고 제네릭 타입을 받고 바운드 처리 후 
// 해당 trait 를 구현한 타입에 한해서 함수 실행.

fn get_area<T>(objec: &T) -> f64
where
    T: HasArea,
{
    objec.area()
}

fn main() {
    let rec = Rectangle {
        length: 3.0,
        height: 3.0,
    };

    let tri = Triangle {
        length: 3.0,
        height: 3.0,
    };

    let rec_area = get_area(&rec);
    println!("{:?}", rec_area);

    let tri_area = get_area(&tri);
    println!("{:?}", tri_area);
}

Testcase: empty bounds

trait 에 함수가 없어도 유용하게 쓰일 수 있다. 예로 Copy 와 Eq trait 이 그러하다.

예제를 통해 어떻게 타입을 바운드 하는지 알아보자.


#[derive(Debug)]
struct Rectangle {
    length: f64,
    height: f64,
}

struct Korea;
struct China;

trait Hanbok {}

// 이렇게 Korea 란 struct 을 impl 하는 것으로 해당 struct 의 타입은 Hanbok 으로 바운드 된다.
impl Hanbok for Korea {}


// 해당 함수는 Hanbok 이란 타입으로 제한하고 있다. 
fn ownership<T>(_: &T) -> &'static str
where
    T: Hanbok,
{
    "한복"
}

fn main() {
    let k = Korea;
    let c = China;


	// 함수 호출 불가능
    // println!("{:?}", ownership(&c));

	// 호출 가능    
    println!("{:?}", ownership(&k));
}