홈강좌Rust 입문·중급 47편 중 32편째
Rust 제네릭 — 타입만 다른 코드를 하나로 쓴다
Rust 제네릭은 fn largest<T>(list: &[T]) 처럼 타입 자리에 이름 T 를 두고, 하나의 함수·구조체를 여러 타입에 쓰게 한다. T 로 무엇을 할 수 있는지는 T: PartialOrd 처럼 경계로 약속해야 해서, 빠뜨리면 E0369 가 난다. 경계는 컴파일러의 help 를 따라 하나씩 채운다.
01 타입만 다른 함수가 둘 생긴다
목록에서 가장 큰 값을 찾는 함수를 정수용·실수용으로 만들어 보자.
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]));
} 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 를 받는다" 는 선언이다. 그런데 그대로 바꾸면 막힌다.
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]));
}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 {- 무엇 —
>연산을 타입T에 쓸 수 없다. - 어디 — 4번째 줄
n > big. 양쪽이 다T라고 밑줄로 적었다. - 고치는 법 —
help:가T에PartialOrd를 붙여 제한하라고 한다.
T 는 아무 타입이나 될 수 있다. 크기를 비교할 수 없는 타입(예: 22편의 Member)도 들어올 수 있으니, 컴파일러는 > 를 허락하지 않는다. "T 는 크기를 비교할 수 있는 타입만" 이라고 약속해야 한다. 이 약속이 경계(bound)이고, PartialOrd 가 "크기를 비교할 수 있다" 는 뜻이다.
help 대로 PartialOrd 만 붙이면 다음 오류가 나온다.
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` traitlist[0] 의 값을 big 으로 옮기려는데(move), T 가 Copy 가 아니라서 슬라이스 안에서 꺼낼 수 없다는 뜻이다(16편). 정수·실수처럼 복사되는 타입만 받기로 하고 Copy 도 붙인다. 경계 여럿은 + 로 잇는다.
03 경계를 채우면 하나로 세 타입에
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']));
} Compiling largest v0.1.0 (~/projects/largest)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.16s
Running `target/debug/largest`
9
7.25
uT: PartialOrd + Copy 는 "크기를 비교할 수 있고 복사되는 타입" 이다. 정수·실수·char 가 모두 그렇다. 함수 하나로 세 타입을 처리했다. 부를 때 T 를 적지 않아도 넘긴 값을 보고 컴파일러가 정한다. 컴파일러는 실제로 쓰인 타입마다 함수를 따로 만들어 두므로, 직접 두 벌을 쓴 것과 실행 속도가 같다.
04 실습 — 제네릭 구조체
구조체도 칸의 타입을 T 로 비울 수 있다.
#[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());
} 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" } -> pearstruct 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 같은 "약속" 은 트레이트라고 부른다. 다음 편에서 트레이트를 직접 만든다.