제네릭 데이터 타입
제네릭을 사용하면 함수 시그니처나 구조체의 아이템에 다양한 데이터 타입을 사용할 수 있다.
fn largest<T>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
largest
함수는 어떤 타입의 벡터가 들어와도 받아들일 수 있다. 타입 매개변수 T
가 알아서 변신하는 느낌이다.
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 };
}
구조체에도 제네릭 타입을 사용할 수 있다. 위와 같은 경우 x
와 y
는 동일한 타입 T
로 지정되어 있으므로 x와 y는 서로 동일한 타입이어야 한다. 서로 다른 타입으로 지정하려면
struct Point<T, U> {
x: T,
y: U,
}
이처럼 다른 매개변수를 포함하면 된다.
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
메서드를 구현할 때는 impl
뒤에 제네릭 타입을 명시해야 한다.
단형성화(monomorphization)
제네릭 코드를 구체적인 타입으로 변환하는 과정을 말한다. 컴파일러가 제네릭 코드를 변경해주기 때문에 별도로 타입마다 중복된 코드를 작성할 필요가 없다.
트레이트(trait)
트레이트는 특정한 타입이 가지고 있으면서 다른 타입과 공유할 수 있는 기능을 정의한다.
트레이트 정의
src/lib.rs
pub trait Summary {
fn summarize(&self) -> String;
}
트레이트는 trait
키워드로 정의한다. 중괄호 안에는 트레이트를 구현할 타입의 동작을 묘사하는 메서드 시그니처를 선언하고, 세미콜론으로 마무리해 실제 구현은 각 타입에서 만든다.
src/lib.rs
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
impl Summary for Tweet{
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
트레이트 구현은 메서드 구현과 유사하다. impl
뒤에 구현하고자 하는 트레이트 이름을 적고, 어떤 타입에 트레이트를 구현할지 for
로 명시한다.
위 예제에서는 신문 기사와 트위터 글을 나타내는 NewsAritcle
과 Tweet
에 대해 트레이트를 구현했다.
사용자는 트레이트를 스코프로 가져와야 사용할 수 있다.
use aggregator::{Summary, Tweet};
외부 타입에 외부 트레이트를 구현할 수는 없다. 이는 orphan rule(부모 타입이 존재하지 않음)에서 나온다.
기본 구현
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
세미콜론으로 마무리해 구현을 각 타입에 맡긴다고 했는데, 트레이트를 정의할 때 기본 동작을 미리 구현해둘 수도 있다.
매개변수 트레이트
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
item
매개변수의 타입을 지정하지 않고 impl Summary
로 트레이트 이름을 명시했다. 해당 트레이트를 구현한 타입이라면 모두 전달받을 수 있다.
트레이트 바운드
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
트레이트 바운드는 impl trait
와 동일하지만, 조금 더 장황하다. 즉 impl trait
가 syntax sugar이다. 트레이트 바운드로는 더 복잡한 상황을 표현할 수 있다.
pub fn notify(item1: &impl Summary, item2: &impl Summary) {}
pub fn notify<T: Summary>(item1: &T, item2: &T) {}
1번 코드의 경우 impl trait
을 사용했고, 2번 코드의 경우 트레이트 바운드를 사용했다.
item1
과 item2
의 매개변수가 같은 타입으로 강제되어야 한다면, 2번 코드를 사용해야 한다.
pub fn notify(item: &(impl Summary + Display)) {}
pub fn notify<T: Summary + Display> (item: &T) {}
- 문법을 사용하면 트레이트를 여러 개 지정할 수 있다.
제네릭마다 트레이트 바운드를 갖게 되면 너무 많은 정보를 갖게될 수도 있다. 가독성을 위해 where
절을 사용할 수 있다.
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {}
//where를 사용하기
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{}
트레이트 구현 타입 반환
fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
}
}
반환 타입에 impl Summary
를 써서 Summary trait를 구현한 타입을 반환한다고 명시했다.
함수를 호출하는 쪽에서는 Tweet
과 NewsArticle
중 어떤 타입을 반환해야 할지 몰라도 된다.
그렇다고 이 함수 자체가 반환값을 모르는 건 불가능하다. 반환 타입은 Tweet
만 있거나 NewsArticle
만 있어야 하지, if
문으로 경우에 따라 둘 중 하나가 반환되도록 하면 한된다.
조건부 메서드 구현
타입이 특정 트레이트를 구현하는 경우에만 해당 타입에 트레이트를 구현할 수도 있다.
트레이트 바운드를 만족하는 모든 타입에 대해 트레이트를 구현하는 것을 포괄 구현9blanket implementation)이라 한다.
impl<T: Display> ToString for T {}
Display
트레이트를 구현하는 모든 타입에 ToString
트레이트도 구현한다.
라이프타임
러스트의 모든 참조자는 참조자의 유효성을 보장하는 라이프타임이라는 범위를 갖는다. 암묵적으로 추론할 수 있지만, 명시해야 하는 경우도 있다.
dangling reference
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("r: {}", r);
}
x
는 내부 블록 내에 선언되어 블록이 끝나면 소멸하지만, r
은 사라진 x
의 참조값을 가지고 있다.
따라서 'x' does not live long enough
라는 오류가 발생한다.
대여 검사기
러스트에서는 대여 검사기(borrow checker)를 이용해 대여의 유효성을 판단한다.
r
은 2번째 라인부터 선언되어 있는데, x
는 5번째 줄에서 생성되어 6번째 줄에서 종료되어버리니 라이프타임의 크기가 r
보다 작다. 컴파일러는 참조 대상이 참조자보다 오래 살지 못하니 컴파일하지 않는다.
라이프타임 명시 문법
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
라이프타임은 아포스트로피(’)로 명시하고, 일반적으로 소문자 a를 많이 사용한다.
위에서는 두 매개변수의 참조자 모두가 유효한 동안에는 반환된 참조자도 유효할 것이라는 점을 표현한다. 두 매개변수와 반환 값 적어도 라이프타임 'a
만큼 살아있다고 알려주는 것이다.
실제로는 x
와 y
중 더 짧은 라이프타임이 'a
의 라이프타임이 된다.
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {}", result);
}
string2
는 블록이 종료될 떄 사라지지만 result
는 출력될 떄까지 살아있고 string2
에 대한 정보를 담아야 하므로, 문제가 있는 코드이다. 라이프타임을 명시함으로써 컴파일러가 문제를 명확하게 인식한다.
라이프타임 문법은 함수의 매개변수와 반환값의 라이프타임을 서로 연결시켜준다. 러스트는 연결된 라이프타임 정보를 이용해 댕글링 포인터 생성을 방지하고 메모리를 보호한다.
몇몇 경우에 있어서는 똑같은 라이프타임을 수도 없이 반복해서 명시해야 하는데, 이런 일반적인 패턴에 한해서는 라이프타임을 생략할 수도 있다.
컴파일러가 라이프타임을 알아내는데 사용하는 규칙은 세 가지다.
- 컴파일러는 참조자인 매개변수 각각에 라이프타임 매개변수를 할당한다.
- 입력 라이프타임 매개변수가 딱 하나라면, 해당 라이프타임이 모든 출력 라이프타임에 대입된다.
- 입력 라이프타임 매개변수가 여러 개인데, 그중 하나가
&self
나&mut self
라면(메서드라면)self
의 라이프타임이 모든 출력 라이프타임 매개변수에 대입된다.
요약하자면, 컴파일러가 시그니처 안에있는 모든 참조자의 라이프타임을 알아내지 못할 때 오류가 발생한다.
'프로그래밍 > Rust' 카테고리의 다른 글
[Rust] 클로저 (0) | 2024.05.19 |
---|---|
[Rust] 테스트 (0) | 2024.05.19 |
[Rust] 오류 처리 (0) | 2024.05.18 |
[Rust] 컬렉션 (0) | 2024.05.18 |
[Rust]프로젝트 모듈 관리 (0) | 2024.05.17 |