동작하는 코드
지금까지 Vec<i32>, Option<String>, Result<String, io::Error>를 써왔어요. 꺾쇠 괄호 안에 타입을 넣는 이 문법, 사실 전부 제네릭이었어요!
먼저, 제네릭이 없다면 어떤 일이 생기는지 볼게요.
fn print_i32(value: i32) {
println!("값: {value}");
}
fn print_str(value: &str) {
println!("값: {value}");
}
fn print_f64(value: f64) {
println!("값: {value}");
}
fn main() {
print_i32(42);
print_str("안녕");
print_f64(3.14);
}
같은 일을 하는 함수를 타입마다 하나씩 만들어야 해요. 타입이 10개면 함수도 10개?!
직접 해보기
print_bool(value: bool)함수를 추가해보세요. 점점 귀찮아지죠?- 아래 제네릭 버전과 비교해보세요 — 함수 하나로 전부 처리할 수 있어요!
제네릭 함수 — 한 번만 쓰고, 모든 타입에
use std::fmt::Display;
fn print_value<T: Display>(value: T) {
println!("값: {value}");
}
fn main() {
print_value(42);
print_value("안녕");
print_value(3.14);
print_value(true);
}
<T: Display>는 "T라는 타입 변수를 쓸게요. 단, Display 자격증이 있는 타입만요"라는 뜻이에요. 모듈 20에서 배운 트레이트 바운드가 여기서 쓰이죠!
T는 관례적인 이름이에요. Type의 첫 글자를 딴 거예요. U, V 등도 자주 쓰여요.
타입 추론 — 대부분 생략 가능
Rust가 타입을 알아서 추론해줘요. 명시적으로 쓸 수도 있지만, 보통 생략해요.
use std::fmt::Display;
fn print_value<T: Display>(value: T) {
println!("값: {value}");
}
fn main() {
print_value::<i32>(42); // 명시적 — 보통 불필요
print_value(42); // 추론 — 이렇게 쓰세요
}
여러 제네릭 타입
제네릭 타입을 여러 개 쓸 수도 있어요.
use std::fmt::Display;
fn print_pair<T: Display, U: Display>(first: T, second: U) {
println!("{first}, {second}");
}
fn main() {
print_pair(1, "사과");
print_pair(3.14, 42);
}
제네릭 구조체
구조체에도 제네릭을 쓸 수 있어요.
#[derive(Debug)]
struct Pair<T> {
first: T,
second: T,
}
impl<T> Pair<T> {
fn new(first: T, second: T) -> Self {
Pair { first, second }
}
}
fn main() {
let integers = Pair::new(1, 2);
let strings = Pair::new("안녕", "세상");
println!("{:?}", integers); // Pair { first: 1, second: 2 }
println!("{:?}", strings); // Pair { first: "안녕", second: "세상" }
}
Pair<T>에서 first와 second는 같은 타입 T여야 해요. 다른 타입을 넣고 싶다면?
#[derive(Debug)]
struct MixedPair<T, U> {
first: T,
second: U,
}
fn main() {
let mix = MixedPair { first: 42, second: "안녕" };
println!("{:?}", mix);
}
이미 써본 제네릭들
사실 지금까지 제네릭을 계속 써왔어요!
fn main() {
// Vec<T> — 어떤 타입이든 담는 벡터
let numbers: Vec<i32> = vec![1, 2, 3];
let names: Vec<&str> = vec!["지수", "민수"];
// Option<T> — 값이 있거나 없거나
let some_number: Option<i32> = Some(42);
let no_number: Option<i32> = None;
// Result<T, E> — 성공이거나 실패이거나
let ok: Result<i32, String> = Ok(42);
let err: Result<i32, String> = Err("에러!".into());
println!("{:?}, {:?}", numbers, names);
println!("{:?}, {:?}", some_number, no_number);
println!("{:?}, {:?}", ok, err);
}
Vec, Option, Result 모두 제네릭 타입이에요. 꺾쇠 괄호 안에 넣는 i32, String 등이 바로 타입 인자예요.
where 절 — 트레이트 바운드를 깔끔하게
트레이트 바운드가 길어지면 where 절로 분리할 수 있어요.
꺾쇠 괄호 안에 전부 쓰면 복잡해요
use std::fmt::{Display, Debug};
fn compare_and_print<T: Display + PartialOrd + Debug>(a: T, b: T) {
if a > b {
println!("{a} > {b}");
} else {
println!("{a} <= {b}");
}
}
where 절로 분리하면 깔끔해요
use std::fmt::{Display, Debug};
fn compare_and_print<T>(a: T, b: T)
where
T: Display + PartialOrd + Debug,
{
if a > b {
println!("{a} > {b}");
} else {
println!("{a} <= {b}");
}
}
fn main() {
compare_and_print(10, 20);
compare_and_print("사과", "바나나");
}
기능은 완전히 같아요. where는 읽기 편하게 만드는 도구예요.
더 알아보기: 단형화(monomorphization)
"제네릭을 쓰면 느려지지 않나요?" 걱정하지 마세요! Rust는 단형화라는 기법을 써요.
컴파일할 때 제네릭 함수를 실제 사용된 타입별로 복사해요.
fn print_value<T: std::fmt::Display>(value: T) {
println!("값: {value}");
}
fn main() {
print_value(42); // i32 버전이 생성됨
print_value("안녕"); // &str 버전이 생성됨
}
컴파일러가 내부적으로 이렇게 만들어요.
fn print_value_i32(value: i32) {
println!("값: {value}");
}
fn print_value_str(value: &str) {
println!("값: {value}");
}
제네릭은 코드를 줄이면서도 성능은 그대로예요. 런타임 비용이 전혀 없어요.
연습 1 (쉬움). 아래 제네릭 함수를 완성하세요. 두 값 중 큰 값을 반환해야 해요.
fn max_of<T: PartialOrd>(a: T, b: T) -> T {
// 여기를 완성하세요
}
fn main() {
println!("{}", max_of(10, 20)); // 20
println!("{}", max_of("apple", "banana")); // banana
}
연습 2 (보통). 제네릭 구조체 Stack<T>을 구현하세요.
struct Stack<T> {
items: Vec<T>,
}
impl<T> Stack<T> {
fn new() -> Self {
// 여기를 완성하세요
}
fn push(&mut self, item: T) {
// 여기를 완성하세요
}
fn pop(&mut self) -> Option<T> {
// 여기를 완성하세요
// 힌트: Vec의 pop() 메서드를 쓰세요
}
fn is_empty(&self) -> bool {
// 여기를 완성하세요
}
}
fn main() {
let mut stack = Stack::new();
stack.push(1);
stack.push(2);
stack.push(3);
while let Some(value) = stack.pop() {
println!("{value}"); // 3, 2, 1 순서로 출력
}
}
연습 3 (도전). where 절을 사용해서 Display + Clone 바운드가 있는 제네릭 함수를 작성하세요. 값을 복제해서 두 번 출력해야 해요.
use std::fmt::Display;
fn print_cloned<T>(value: T)
where
// 여기에 바운드를 작성하세요
{
let cloned = value.clone();
println!("원본: {value}");
println!("복제: {cloned}");
}
fn main() {
print_cloned(String::from("Rust"));
print_cloned(42);
}
Q1. 제네릭의 주요 목적은?
- A) 프로그램을 더 빠르게 만든다
- B) 하나의 코드로 여러 타입을 처리할 수 있게 한다
- C) 메모리 사용량을 줄인다
- D) 에러 처리를 자동화한다
Q2. 다음 중 fn foo<T: Display + Clone>(x: T)와 같은 의미인 것은?
- A)
fn foo<T>(x: T) where T: Display - B)
fn foo<T>(x: T) where T: Display + Clone - C)
fn foo<T>(x: T) where T: Clone - D)
fn foo(x: Display + Clone)
Q3. Rust의 제네릭이 런타임 성능에 미치는 영향은?
- A) 약간 느려진다
- B) 비용이 없다 (단형화로 컴파일 시점에 구체 타입으로 변환)
- C) 메모리를 2배 사용한다
- D) 가비지 컬렉션이 필요해진다