hyunsu blog

홈강좌Rust 입문·중급 47편 중 32편째

Rust 제네릭 — 타입만 다른 코드를 하나로 쓴다

목차 · 4
  1. 01타입만 다른 함수가 둘 생긴다
  2. 02타입 자리에 T 를 둔다
  3. 03경계를 채우면 하나로 세 타입에
  4. 04실습 — 제네릭 구조체

Rust 제네릭은 fn largest<T>(list: &[T]) 처럼 타입 자리에 이름 T 를 두고, 하나의 함수·구조체를 여러 타입에 쓰게 한다. T 로 무엇을 할 수 있는지는 T: PartialOrd 처럼 경계로 약속해야 해서, 빠뜨리면 E0369 가 난다. 경계는 컴파일러의 help 를 따라 하나씩 채운다.

01 타입만 다른 함수가 둘 생긴다

목록에서 가장 큰 값을 찾는 함수를 정수용·실수용으로 만들어 보자.

rust
fn largest_i32(list: &[i32]) -> i32 {
    let mut big = list[0];
    for &n in list {
        if n > big {
            big = n;
        }
    }
    big
}

fn largest_f64(list: &[f64]) -> f64 {
    let mut big = list[0];
    for &n in list {
        if n > big {
            big = n;
        }
    }
    big
}

fn main() {
    println!("{}", largest_i32(&[3, 9, 4]));
    println!("{}", largest_f64(&[1.5, 0.2, 7.25]));
}
text
   Compiling largest v0.1.0 (~/projects/largest)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.16s
     Running `target/debug/largest`
9
7.25
  • 매개변수 list: &[i32] 는 "i32 들의 슬라이스" 다(21편). 배열이든 Vec 이든 빌려서 넘길 수 있다.
  • for &n in list 의 &n 은 참조를 풀어 값을 n 에 담는다는 뜻이다. 정수·실수는 Copy(16편)라 복사된다.

두 함수는 이름과 타입(i32·f64)만 다르고 몸체가 글자 하나 다르지 않다. char 용이 필요하면 또 복사해야 한다.

02 타입 자리에 T 를 둔다

타입을 이름으로 비워 두고, 부를 때 정해지게 하는 것이 제네릭이다. 함수 이름 뒤 <T> 가 "이 함수는 타입 T 를 받는다" 는 선언이다. 그런데 그대로 바꾸면 막힌다.

rust
fn largest<T>(list: &[T]) -> T {
    let mut big = list[0];
    for &n in list {
        if n > big {
            big = n;
        }
    }
    big
}

fn main() {
    println!("{}", largest(&[3, 9, 4]));
}
text
error[E0369]: binary operation `>` cannot be applied to type `T`
 --> src/main.rs:4:14
  |
4 |         if n > big {
  |            - ^ --- T
  |            |
  |            T
  |
help: consider restricting type parameter `T` with trait `PartialOrd`
  |
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> T {
  1. 무엇 — > 연산을 타입 T 에 쓸 수 없다.
  2. 어디 — 4번째 줄 n > big. 양쪽이 다 T 라고 밑줄로 적었다.
  3. 고치는 법 — help: 가 T 에 PartialOrd 를 붙여 제한하라고 한다.

T 는 아무 타입이나 될 수 있다. 크기를 비교할 수 없는 타입(예: 22편의 Member)도 들어올 수 있으니, 컴파일러는 > 를 허락하지 않는다. "T 는 크기를 비교할 수 있는 타입만" 이라고 약속해야 한다. 이 약속이 경계(bound)이고, PartialOrd 가 "크기를 비교할 수 있다" 는 뜻이다.

help 대로 PartialOrd 만 붙이면 다음 오류가 나온다.

text
error[E0508]: cannot move out of type `[T]`, a non-copy slice
 --> src/main.rs:2:19
  |
2 |     let mut big = list[0];
  |                   ^^^^^^^
  |                   |
  |                   cannot move out of here
  |                   move occurs because `list[_]` has type `T`, which does not implement the `Copy` trait

list[0] 의 값을 big 으로 옮기려는데(move), T 가 Copy 가 아니라서 슬라이스 안에서 꺼낼 수 없다는 뜻이다(16편). 정수·실수처럼 복사되는 타입만 받기로 하고 Copy 도 붙인다. 경계 여럿은 + 로 잇는다.

03 경계를 채우면 하나로 세 타입에

rust
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut big = list[0];
    for &n in list {
        if n > big {
            big = n;
        }
    }
    big
}

fn main() {
    println!("{}", largest(&[3, 9, 4]));
    println!("{}", largest(&[1.5, 0.2, 7.25]));
    println!("{}", largest(&['r', 'u', 's', 't']));
}
text
   Compiling largest v0.1.0 (~/projects/largest)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.16s
     Running `target/debug/largest`
9
7.25
u

T: PartialOrd + Copy 는 "크기를 비교할 수 있고 복사되는 타입" 이다. 정수·실수·char 가 모두 그렇다. 함수 하나로 세 타입을 처리했다. 부를 때 T 를 적지 않아도 넘긴 값을 보고 컴파일러가 정한다. 컴파일러는 실제로 쓰인 타입마다 함수를 따로 만들어 두므로, 직접 두 벌을 쓴 것과 실행 속도가 같다.

04 실습 — 제네릭 구조체

구조체도 칸의 타입을 T 로 비울 수 있다.

rust
#[derive(Debug)]
struct Pair<T> {
    left: T,
    right: T,
}

impl<T: PartialOrd> Pair<T> {
    fn bigger(&self) -> &T {
        if self.left > self.right { &self.left } else { &self.right }
    }
}

fn main() {
    let nums = Pair { left: 3, right: 8 };
    let words = Pair { left: "apple", right: "pear" };
    println!("{nums:?} -> {}", nums.bigger());
    println!("{words:?} -> {}", words.bigger());
}
text
   Compiling pair v0.1.0 (~/projects/pair)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
     Running `target/debug/pair`
Pair { left: 3, right: 8 } -> 8
Pair { left: "apple", right: "pear" } -> pear
  • struct Pair<T> 의 두 칸은 같은 타입 T 다. 정수 둘이든 문자열 둘이든 된다.
  • impl<T: PartialOrd> Pair<T> 는 "T 가 비교 가능할 때만 이 메서드들이 있다" 는 뜻이다.
  • bigger 는 큰 쪽의 참조(&T)를 돌려준다. 값을 꺼내지 않으니 Copy 가 필요 없다.

Pair { left: 3, right: "pear" } 처럼 두 칸 타입을 섞어 보자. 같은 T 여야 하니 오류가 난다. 26편의 Option<T>, 28편의 Vec<T> 도 이렇게 만든 제네릭이다. PartialOrd 와 Copy 같은 "약속" 은 트레이트라고 부른다. 다음 편에서 트레이트를 직접 만든다.