Rust - chapter 10-1 generic


Rust 문서 챕터 10-1 제너릭을 읽고 정리한 글이다.(링크)

사실 이 문서를 보기로 했었을 때 금방 끝날 줄 알았는데 실제 실무에 쓰지 않고 이곳 저곳 기웃거리다 보니 속도가 안 나고 있는데 느리더라도 끝까지 읽고 다시 한 번 볼 수 있도록 노력해야겠다. 😓

Generic

제너릭 다른 프로그래밍 언어에도 대부분 있는 개념으로 인자의 타입을 받아서 해당 타입을 활용할 수 있도록 하는 타입이다.
간단하게 일반적으로 함수는 특정 타입만을 인자로 받게끔 작성되었었다. 제너릭 타입을 쓸 경우에는 인자가 정수로 들어오면 인자를 정수로, 문자로 들어오면 문자를 받을 수 있게 해준다.
간단한 예제를 살펴보자.

제너릭을 사용하지 않은 경우

fn largest_i32(list: &[i32]) -> i32 {
  let mut largest = list[0];

  for &item in list.iter() {
    if item > largest {
      largest = item;
    }
  }

  largest
}

fn largest_char(list: &[char]) -> char {
  let mut largest = list[0];

  for &item in list.iter() {
    if item > largest {
      largest = item;
    }
  }

  largest
}

fn main() {
  let numbers = vec![34, 50, 25, 100, 65];

  let result = largest_i32(&numbers);
  println!("The largest number is {}", result);
  assert_eq!(result, 100);

  let chars = vec!['y', 'm', 'a', 'q'];

  let result = largest_char(&chars);
  println!("The largest char is {}", result);
  assert_eq!(result, 'y');
}

위의 경우 사실 같은 기능을 하고 있는데 타입이 다르기에 2개의 함수를 작성한 경우다. 이 얼마나 불편한 일인가!! 그래서 사용하는게 제너릭이다.

제너릭 사용

fn largest<T>(list: &[T]) -> T {
  let mut largest = list[0];

  for &item in list.iter() {
    // 타입따라 아래의 비교가 성립여부가 다르다.
    if item > largest {
      largest = item;
    }
  }

  largest;
}

fn main() {
  let numbers = vec![34, 50, 25, 100, 65];
  let result = largest(&numbers);
  println!("The largest number is {}", result);

  let chars = vec!['y', 'm', 'a', 'q'];
  let result = largest(&chars);
  println!("The largest char is {}", result);
}

위 예제를 보면 제네릭 <T> 을 사용하여 중복된 기능을 하나의 함수로 통합한 것을 볼 수 있다. 일반적으로 T 를 사용하는데 Type 의 약자이며 다른 문자를 입력하여도 된다. 관습적으로 대문자 하나를 사용하여 표기한다.
그리고 사실 위 코드는 컴파일 시 에러가 난다. 주석에 작성하였지만 타입에 따라 저렇게 비교할 수 있는 경우가 있고 없기 때문이다.

사용법 기초

사실 다른 프로그래밍 언어의 제너릭과 동일하다고 봐도 무방할 것 같다. 그래도 일단 간단한 사용법에 대해서 살펴보자.

구조체 내에서 제너릭 사용

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

fn main() {
  let integer = Point { x: 5, y: 10 };
  let integer_float = Point { x: 5, y: 4.0 };
}

짐작하시겠지만 위의 경우는 당연히 에러가 발생한다. 타입을 하나(정수)만 받았는데 받은 2개의 타입이 다르기 때문이다.
그렇다면 타입을 하나 더 추가하면 된다!

struct Point<T, K> {
  x: T,
  y: K,
}

추가적으로 사실 우리는 이미 이전에 Option, Result 열거형에서 제너릭을 사용해왔다.

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

enum Result<T, E> {
  Ok(T),
  Err(E),
}

메소드 정의 내에서 제너릭 사용

🚧   impl 키워드 뒤에 제너릭 타입을 정의해야지만 메소드 구현 중에 사용할 수 있음을 주의하자.

struct Point<T, K> {
  x: T,
  y: K,
}

impl<T, K> Point<T, K> {
  fn mixup<U, V>(self, other: Point<U, V>) -> Point<T, V> {
    Point {
      x: self.x,
      y: other.y,
    }
  }
}

fn main() {
  let p1 = Point { x: 5, y: 10.4 };
  let p2 = Point { x: "Hello", y: 'c' };

  let p3 = p1.mixup(p2);

  println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
  // p3.x = 5, p3.y = c
}

제너릭을 이용한 코드의 성능

제너릭을 사용할 경우 런타임에 타입이 정해져서 런타임 비용이 발생하는지 궁금할 수 있다. 과연 어떨까?
놀랍게도 런타임 비용이 발생하지 않는다. 구체적인 타입을 명시하여 작성한 메소드를 사용한 것과 차이가 없다는 말이다.
이는 러스트가 컴파일 타임에 제너릭을 사용하는 코드에 대해 단형성화(monomorphization)를 사용하여 구체적인 타입으로 된 특정 함수의 코드를 생성하기 때문이다.


개인이 참고하고자 작성한 글이며, 잘못된 정보가 있을 수 있습니다. 잘못된 정보는 메일로 보내주시면 감사하겠습니다. 🙏