JS, TS랑 비교하며 Rust 공부하기 (7)
제네릭 타입
TS의 제네릭을 알고 있다면 어렵지않게 이해할 수 있다.
제네릭을 통해 함수 시그니처나 구조체의 아이템에 다양한 구체적 데이터 타입을 사용할 수 있도록 정의할 수 있다.
제네릭은 구체 타입이 한 번 정해지면 해당 인스턴스에서는 다른 타입을 받을 수 없다.
러스트에서는 타입 이름을 지어줄 때는 대문자로 시작하는 낙타 표기법을 따르고, 타입 매개변수의 이름은 짧게 짓는 것이 관례이기 때문에, 대부분의 러스트 프로그래머는 T를 사용한다.
러스트는 컴파일 타임에 제네릭을 사용하는 코드를 구체 타입으로 채워진 코드로 바꾸기 때문에 제네릭을 사용한다고 해서 느려지지 않는다.
제네릭 함수 정의
함수 시그니처와 본문에 제네릭 타입을 갖도록 정의할 수 있다.
fn largest<T>(list: &[T]) -> &T { ... }
제네릭 구조체 정의
구조체 필드에 제네릭 타입을 갖도록 정의할 수 있다.
struct Point<T, U> {
x: T,
y: U,
}
제네릭 열거형 정의
열거형의 배리언트에 제네릭 타입을 갖도록 정의할 수 있다.
enum Option<T> {
Some(T),
None,
}
제네릭 메서드 정의
구조체나 열거형에 메서드를 구현할 때도 제네릭 타입을 갖도록 정의할 수 있다.
구조체 정의에 선언된 제네릭 매개변수명과는 다른 제네릭 매개변수명을 사용할 수 있지만 같은 이름을 사용하는 것이 관례이다.
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
특정 타입의 메서드를 정의할 때 제네릭 타입에 대한 제약을 지정할 수 있다.
특정 타입으로 정의한다고 해서 기존 메서드와 동일한 이름을 가진 메서드는 정의할 수 없다.
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
fn x(&self) -> &f32 { // error
&self.x
}
}
구조체 정의에 사용한 제네릭 타입 매개변수와 구조체의 메서드 시그니처에 사용하는 제네릭 타입 매개변수는 다르게 지정할 수 있다.
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,
}
}
}
let p1 = Point { x: 5, y: 10.4 };
let p2 = Point { x: "Hello", y: 'c' };
let p3 = p1.mixup(p2);
println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
라이프타임
라이프타임은 참조자가 얼마 동안 유효한지를 나타내기 위해 사용된다.
주된 목적은 댕글링 참조를 컴파일 타임에 방지하는 것이다.
라이프타임을 명시한다고 해서 참조자의 실제 수명 자체가 늘어나거나 줄어들진 않으며, 여러 참조자 간에 어떤 게 더 오래 살아야 하는지 관계를 명확히 해주는 역할을 한다.
라이프타임 매개변수는 '로 시작하며 보통 제네릭 타입처럼 짧은 소문자(’a, ‘b)로 쓴다. 그다음 참조자의 &뒤에 위치시킨다.
&i32 // 참조자
&'a i32 // 명시적인 라이프타임이 있는 참조자
&'a mut i32 // 명시적인 라이프타임이 있는 가변 참조자
라이프타임의 주요 역할
라이프타임 문법은 주로 다음과 같은 상황에서 사용된다.
- 함수의 여러 참조자 매개변수와 반환값 간의 관계를 명확히 할 때
- 구조체가 참조자를 필드로 가질 때
- 라이프타임 추론이 어려워 명시적으로 표현이 필요할 때
라이프타임 생략
러스트는 자주 쓰이는 경우에 대해 라이프타임을 자동 추론해준다.
라이프타임 생략 규칙은 다음과 같으며 이 규칙에 포함되어 있지 않으면 라이프타임을 명시해야 한다.
- 컴파일러가 참조자인 매개변수 각각에게 라이프타임 매개변수를 할당한다. ex)
fn foo<'a, 'b>(x: &'a i32, y: &'b i32) - 만약 입력 라이프타임 매개변수가 하나면, 출력 라이프타임에 대입한다.
- 만약 입력 라이프타임 매개변수가 여러 개인데, 그중 하나가
&self나&mut self라면self의 라이프타임이 모든 출력 라이프타임 매개변수에 대입한다.
함수 시그니처에 라이프타임 명시
라이프타임 명시를 함수 시그니처에 사용하기 위해서는 제네릭 타입 매개변수를 사용할 때와 같다.
참조자를 반환하는 함수를 작성할 때는 반환 타입의 라이프타임 매개변수가 함수 매개변수 중 하나와 일치해야 한다.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
구조체 정의에 라이프타임 명시
구조체가 참조자를 포함한다면 구조체 정의 내 모든 참조자에 라이프타임을 명시해야 한다.
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl 블록에 라이프타임 명시
구조체가 라이프타임을 가진다면 impl 블록도 동일한 라이프타임을 선언해야 한다.
연관 함수 내에서만 라이프타임이 필요하다면, 함수 시그니처에만 라이프타임 매개변수를 명시하면 된다.
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}
제네릭 + 라이프타임
제네릭과 라이프타임을 함께 사용할 수도 있다.
use std::fmt::Display;
fn longest_with_an_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display,
{
println!("Announcement! {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}
정적 라이프타임
‘static 라이프타임은 해당 참조자가 프로그램의 전체 생애주기 동안 살아있음을 의미하며 모든 문자열 리터럴이 해당 라이프타임을 가진다.
let s: &'static str = "I have a static lifetime.";