Rust에 존재하는 제네릭 (Generic), 트레이트 (Trait), 라이프타임 (Lifetime) 개념에 대해 공부하고, 정리해 보겠습니다.
교재에 나와 있는 제네릭에 대한 설명입니다. 다시 요약하자면, 러스트에서는 타입을 추상화하는 도구로 제네릭을 사용합니다.
반복되는 코드 블록을 여러 번 반복해서 작성하는 대신 ‘함수’로 만들어서 재사용하는 개념을 떠올려보면 제네릭을 이해하는 데 도움이 될 것 같습니다.
제네릭을 사용하면 타입과 기능을 일반화하여 코드 중복을 줄이고 유지보수성을 향상시킬 수 있습니다.
제네릭은 타입 매개변수를 사용하여 구현되며, 이는 코드가 사용될 때 구체적인 타입으로 지정될 placeholder 역할을 합니다.
함수의 매개변수나 반환 타입, 구조체, 열거형, 메서드 등에서 경우에 따라 여러 타입을 사용해야 할 수도 있습니다. 이때, 제네릭이 빛을 발하게 됩니다.
// 제네릭 함수 정의
fn largest<T>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
// 제네릭 구조체 정의
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
}
// 제네릭 열거형 정의 - Option<T>, Result<T, E>
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
// 제네릭 메서드 정의
struct Point<X1, Y1> {
x: X1,
y: Y1,
}
impl<X1, Y1> Point<X1, Y1> {
fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
Point {
x: self.x,
y: other.y,
}
}
}
제네릭을 사용할 때는 <> 꺾쇠괄호 안에 타입 매개변수의 이름을 작성해 줘야 합니다. (관행적으로 Type의 첫 글자를 딴 T를 사용합니다.)
함수나 구조체, 열거형, impl 키워드 바로 뒤에 <T> 를 붙여 타입 매개변수 이름을 선언하고 각각의 정의 내에서 구체적인 타입 대신 타입 매개변수 이름을 활용하면 됩니다!
(Note: 약간의 차이는 있으나, 트레이트는 다른 언어에서 흔히 인터페이스 (interface) 라고 부르는 기능과 유사합니다.)
// Summary 트레이트 정의
pub trait Summary {
fn summarize(&self) -> String;
}
우리는 러스트에서 트레이트를 활용해서, 다양한 타입들이 공통적으로 갖는 동작에 대해 추상화할 수 있습니다. 마치 객체지향 프로그래밍의 인터페이스처럼 말이죠!
트레이트를 통해 우리는 코드의 재사용성, 유연성, 안전성을 높일 수 있습니다.
위의 트레이트 정의 예시에서, 메서드 시그니처 뒤에는 중괄호로 시작하여 메서드를 구현하는 대신 세미콜론을 집어넣었습니다. 이 트레이트를 구현하는 각 타입이 메서드에 맞는 동작을 직접 제공해야 합니다.
컴파일러는 Summary 트레이트가 있는 모든 타입에 정확히 이와 같은 시그니처의 summarize 메서드를 가지고 있도록 강제할 것입니다.
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
이 예시에서는 NewsArticle 과 Tweet 타입에 각각 Summary 트레이트를 구현하였습니다.
특정 타입에 트레이트를 구현하는 문법은 위 예시에서 확인할 수 있듯이,
impl Trait for Type { }
형태로 명시한 다음, 트레이트 정의에 정의된 메서드 시그니처에 대한 메서드 본문을 작성해주면 됩니다.
앞서 소개한 제네릭과 트레이트를 함께 사용하는 방법으로 트레이트 바운드가 있습니다.
트레이트 바운드는 제네릭에 제약을 걸어, 특정 동작을 보장하는 방법입니다. 즉, 제네릭 타입이 특정 트레이트를 구현한 타입이어야 한다는 조건을 명시해둔 것입니다.
이를 통해, 다양한 이점을 얻을 수 있는데, 아래와 같습니다:
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
트레이트는, 꺾쇠괄호 안의 제네릭 타입 매개변수 선언 뒤에 콜론(:)과 함께 위치하게 됩니다.
이제, notify 함수의 item 매개변수로 올 수 있는 타입은 Summary 트레이트를 구현한 타입으로 제한됩니다.
pub fn notify<T: Summary + Display>(item: &T) {
위와 같이 + 구문으로 여러 개의 트레이트를 모두 구현하도록 바운드를 설정할 수도 있습니다.
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
위 예시는 where 조항입니다.
트레이트 바운드가 복잡해지면 자칫 코드의 가독성을 해칠 수 있습니다. 이때, 트레이트 바운드의 내용은 where 조항으로 따로 빼 버리면 함수 시그니처를 읽기가 더욱 쉬워질 것입니다.
읽어주셔서 감사합니다 🙇♂️
https://doc.rust-kr.org/ch10-01-syntax.html