Rust 제네릭
사실 여러분은 이미 이름도 모른 채 제네릭(generics)을 써 왔어요. Vec<i32>도, Option<T>도 제네릭을 사용하거든요. 이 페이지에서는 직접 제네릭 함수와 구조체를 만드는 방법을 알아볼게요.
제네릭이 해결하는 문제
섹션 제목: “제네릭이 해결하는 문제”숫자 목록에서 가장 큰 값을 돌려주는 함수가 필요하고, 이 함수를 i32와 f64 모두에 쓰고 싶다고 해 볼게요. 제네릭이 없다면 똑같은 함수를 두 번 써야 해요.
fn largest_i32(numbers: &[i32]) -> i32 { let mut largest = numbers[0]; for &n in numbers { if n > largest { largest = n; } } largest}
fn largest_f64(numbers: &[f64]) -> f64 { let mut largest = numbers[0]; for &n in numbers { if n > largest { largest = n; } } largest}로직은 똑같은데 타입마다 복사해서 붙여 넣었어요. 제네릭을 쓰면 한 번만 쓰면 돼요.
제네릭 함수
섹션 제목: “제네릭 함수”fn largest<T: PartialOrd + Copy>(numbers: &[T]) -> T { let mut largest = numbers[0]; for &n in numbers { if n > largest { largest = n; } } largest}
fn main() { let integers = [10, 25, 3, 47, 8]; let floats = [1.5, 2.8, 0.3];
println!("{}", largest(&integers)); println!("{}", largest(&floats));}실행 결과:
472.8<T: PartialOrd + Copy>는 “비교할 수 있고(PartialOrd) 복사할 수 있는(Copy) 타입이라면 어떤 타입 T든”이라고 읽으면 돼요. T는 자리를 맡아 두는 이름이에요. 여러분이 실제로 함수를 호출할 때 쓴 구체적인 타입(i32, f64 등)마다 Rust가 함수를 하나씩 만들어 줘요.
제네릭 구조체
섹션 제목: “제네릭 구조체”구조체도 제네릭으로 만들 수 있어요.
struct Pair<T> { first: T, second: T,}
fn main() { let numbers = Pair { first: 5, second: 10 }; let words = Pair { first: "안녕", second: "세상" };
println!("{} {}", numbers.first, numbers.second); println!("{} {}", words.first, words.second);}실행 결과:
5 10안녕 세상Pair<T> 정의 하나로 숫자든 문자열이든 어떤 타입이든 담을 수 있어요. numbers는 Pair<i32>이고 words는 Pair<&str>이지만, 구조체는 한 번만 썼죠.
실행할 때 비용이 들까요?
섹션 제목: “실행할 때 비용이 들까요?”아니요. Rust는 실제로 사용한 구체적인 타입마다 함수나 구조체의 전용 버전을 따로 만들어요. 이것을 단형성화(monomorphization)라고 해요. 프로그램이 실행될 때는 제네릭 코드가 하나도 남아 있지 않고, 타입이 모두 정해진 평범한 함수만 있어요. 그래서 타입마다 손으로 함수를 쓴 것과 똑같이 빨라요.
제네릭 구조체의 메서드
섹션 제목: “제네릭 구조체의 메서드”구조체 페이지에서 한 것처럼 impl 블록으로 제네릭 구조체에 메서드를 추가할 수 있어요. 새로운 점은 impl에도 <T>를 붙여야 한다는 것 하나뿐이에요.
struct Pair<T> { first: T, second: T,}
impl<T> Pair<T> { fn new(first: T, second: T) -> Self { Pair { first, second } }
fn swap(self) -> Pair<T> { Pair { first: self.second, second: self.first } }}
impl Pair<f64> { fn average(&self) -> f64 { (self.first + self.second) / 2.0 }}
fn main() { let words = Pair::new("왼쪽", "오른쪽").swap(); println!("{} {}", words.first, words.second);
let scores = Pair::new(7.0, 9.0); println!("{}", scores.average());}실행 결과:
오른쪽 왼쪽8여기에는 두 종류의 impl 블록이 있어요.
impl<T> Pair<T>: “어떤 타입의Pair든 이 메서드를 쓸 수 있다”는 뜻이에요. 앞의<T>는 “어떤 타입T든”이라고 읽으면 돼요. 이게 없으면 Rust는T를 실제로 있는 타입 이름으로 생각하고, 그런 타입을 찾을 수 없다고 불평해요.impl Pair<f64>:<T>가 없어요. 이 블록의 메서드는 오직Pair<f64>에서만 쓸 수 있어요. 그래서average에서+와/ 2.0을 쓸 수 있는 거예요.words.average()를 호출해 보면, 컴파일러가Pair<&str>에는 그런 메서드가 없다고 알려 줄 거예요.
두 필드의 타입이 서로 다를 수 있다면 struct Pair<T, U> { first: T, second: U }처럼 타입 매개변수를 여러 개 둘 수도 있어요. 또 트레이트 바운드가 길어지면 where 절로 옮겨서 시그니처를 읽기 쉽게 만들 수 있어요. 이 내용은 고급 트레이트 페이지에서 다뤄요.
연습 문제
섹션 제목: “연습 문제”1. 제네릭으로 가장 작은 값 찾기
섹션 제목: “1. 제네릭으로 가장 작은 값 찾기”가장 작은 항목을 돌려주는 fn smallest<T: PartialOrd + Copy>(items: &[T]) -> T를 만들어 보세요. [8, 3, 5]와 문자 ['r', 'u', 's', 't']로 테스트해 보세요. 힌트: 이 페이지의 largest 함수에서 비교 하나만 뒤집으면 돼요.
정답 보기
fn smallest<T: PartialOrd + Copy>(items: &[T]) -> T { let mut smallest = items[0]; for &item in items { if item < smallest { smallest = item; } } smallest}
fn main() { println!("{}", smallest(&[8, 3, 5])); println!("{}", smallest(&['r', 'u', 's', 't']));}실행 결과:
3r2. Pair에서 더 큰 쪽 고르기
섹션 제목: “2. Pair에서 더 큰 쪽 고르기”Pair<T>에 두 필드 중 더 큰 쪽의 참조자를 돌려주는 larger(&self) -> &T 메서드를 추가해 보세요. 숫자 Pair와 단어 Pair로 테스트해 보세요. 힌트: 비교하려면 PartialOrd가 필요하니까, 바운드를 impl 자체에 붙이세요. impl<T: PartialOrd> Pair<T>처럼요.
정답 보기
struct Pair<T> { first: T, second: T,}
impl<T: PartialOrd> Pair<T> { fn larger(&self) -> &T { if self.first > self.second { &self.first } else { &self.second } }}
fn main() { let numbers = Pair { first: 3, second: 9 }; let words = Pair { first: "pear", second: "apple" };
println!("{}", numbers.larger()); println!("{}", words.larger());}실행 결과:
9pear문자열은 알파벳 순서(정확히는 바이트 순서)로 비교돼요. "pear"가 "apple"보다 뒤에 오니까 더 “큰” 쪽이에요. 이 예제는 순서가 헷갈리지 않도록 영어 단어를 그대로 두었어요.
- 제네릭을 쓰면 코드를 복사하지 않고도, 함수나 구조체 하나로 여러 타입을 다룰 수 있어요.
<T>는 자리를 맡아 두는 타입이고, 트레이트 바운드(T: PartialOrd)는T가 될 수 있는 타입을 제한해요.impl<T> Pair<T>는 모든T에 메서드를 추가하고,impl Pair<f64>는 하나의 구체적인 타입에만 메서드를 추가해요.- 제네릭 코드는 타입마다 전용 버전으로 컴파일되기 때문에, 실행할 때 성능 비용이 없어요.
이해도 점검
섹션 제목: “이해도 점검”이해도 점검
1. 제네릭은 어떤 문제를 해결할까요?
2. fn largest<T: PartialOrd + Copy>(...)에서 PartialOrd + Copy는 무슨 역할을 할까요?
3. 손으로 쓴 코드와 비교했을 때, 제네릭을 쓰면 실행 시간에 비용이 들까요?
4. struct Pair<T> 정의 하나로 무엇을 할 수 있을까요?
점수: 0 / 4
더 알아보기
섹션 제목: “더 알아보기”- 제네릭 데이터 타입 — The Rust Programming Language 한국어판: 함수, 구조체, 열거형, 메서드에서 제네릭을 쓰는 법을 다루고,
largest예제도 나와요. - Generics — Rust by Example (영어): 제네릭 함수와 구조체, 그 문법을 짧은 예제로 실행해 볼 수 있어요.
- Implementation — Rust by Example (영어):
impl<T>블록과 구체적인 타입 하나에만 메서드를 구현하는 법을 더 자세히 보여 줘요. - Bounds — Rust by Example (영어): 트레이트 바운드가 제네릭 타입을 어떻게 제한하는지, 왜 필요한지 설명해요.
- Generic parameters — The Rust Reference (영어): 자세한 내용까지 알고 싶을 때 보는 정확한 규칙이에요.