제네릭: 데이터 형식 & Trait & 수명
이해
- 제네릭 표현 방식 1: 데이터 형식
// 泛型声明 - struct struct Point<T> { x: T, y: T, } let point = Point { x: 5, y: 4 }; // 必须是同一类型 struct PointDiffType<T, U> { x: T, y: U, } let point_diff_type = PointDiffType{ x: 5, y: 4.0 }; // 不同类型 // 泛型声明 - impl impl<T> Point<T> { fn x(&self) -> &T { &self.x } } // 泛型声明 - enum enum Option<T> { Some(T), None, } enum Result<T, E> { Ok(T), Err(E), } // 一种 mix-up 实现 struct Point<T, U> { x: T, y: U, } impl<T, U> Point<T, U> { // 由于泛型的高灵活性,这里做了巧妙的 field 交叉混合,组合出第三个新泛型实例 // 这种语法可用于 middleware fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> { Point { x: self.x, y: other.y, } } } fn main() { 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); } - 제네릭 사용은 런타임 성능에 영향을 주지 않습니다.
컴파일 단계에서 Monomorphization process가 이미 제네릭을 구체적인 형식으로 변환하기 때문입니다.
// 比如使用了 Some 泛型代码 let integer = Some(5); let float = Some(5.0); // 编译期会 Monomorphization 为以下代码 enum Option_i32 { Some(i32), None, } enum Option_f64 { Some(f64), None, } fn main() { let integer = Option_i32::Some(5); let float = Option_f64::Some(5.0); } - 제네릭 표현 방식 2: Traits & Trait Bounds
Trait은 “특성”을 뜻하며, 기본적으로 struct, 함수 매개변수, 반환값에 특성을 부여할 수 있습니다.
struct의 경우 특성은 해당 struct에 선언된 method가 있음을 명확히 하며 Java의
interface와 비슷합니다.impl structA for traitAgeneric type에서는 대응하는 concrete type이 이러한 특성(trait bound)을 가질 때만 해당 함수 집합을 사용할 수 있음을 나타냅니다.impl<T: traitA + traitB + ...> structA<T> {...}함수 매개변수에서는 해당 특성을 가진 매개변수만 인자로 사용할 수 있도록 제한합니다(다형성).pub fn notify(item: &impl Summary) {...}함수 반환값에서는 반환값이 반드시 해당 특성을 가진다고 명시할 수 있습니다.// Defining a Trait pub trait Summary { fn summarize(&self) -> String; } // Implementing a Trait on a Type 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) } } // Default Implementations pub trait Summary { fn summarize(&self) -> String { String::from("(Read more...)") } } // Default Implementations 可以调用其它 impl pub trait Summary { fn summarize_author(&self) -> String; fn summarize(&self) -> String { format!("(Read more from {}...)", self.summarize_author()) } } // Traits as Parameters pub fn notify(item: &impl Summary) { println!("Breaking news! {}", item.summarize()); } // 可以使用 generic type 来指定 trait bounds pub fn notify<T: Summary>(item1: &T, item2: &T) {} // 多个 trait 的情况 1 - 使用 + 操作符 pub fn notify(item: &(impl Summary + Display)) { pub fn notify<T: Summary + Display>(item: &T) { // 多个 trait 的情况 2 - 使用 where fn some_function<T, U>(t: &T, u: &U) -> i32 where T: Display + Clone, U: Clone + Debug // Returning Types that Implement Traits 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, } } // 可以通过 trait bounds 特性,以实现:有对应 trait 的 generic type,才能有对应的 method use std::fmt::Display; struct Pair<T> { x: T, y: T, } impl<T> Pair<T> { fn new(x: T, y: T) -> Self { Self { x, y } } } impl<T: Display + PartialOrd> Pair<T> { fn cmp_display(&self) { if self.x >= self.y { println!("The largest member is x = {}", self.x); } else { println!("The largest member is y = {}", self.y); } } } - 제네릭 표현 방식 3: Lifetimes컴파일러는 명시적 주석이 없을 때 reference의 lifetime을 계산하기 위해 세 가지 규칙을 사용합니다. 첫 번째 규칙은 reference인 각 매개변수가 고유한 lifetime 매개변수를 갖는다는 것입니다. 첫 번째: 각 reference 입력 매개변수에는 자체 lifetime 매개변수가 있습니다. 두 번째 규칙은 입력 lifetime 매개변수가 정확히 하나라면 그 lifetime을 모든 출력 lifetime 매개변수에 할당한다는 것입니다. 두 번째: 입력 lifetime 매개변수가 하나뿐이면 이 매개변수를 모든 출력 lifetime 매개변수에 할당합니다. 세 번째 규칙은 입력 lifetime 매개변수가 여러 개이고 그중 하나가 method의
&i32 // a reference &'a i32 // a reference with an explicit lifetime &'a mut i32 // a mutable reference with an explicit lifetime // 一般用在 function 定义时 fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } // 可用于 struct struct ImportantExcerpt<'a> { part: &'a str, } // Lifetime Elision - 活用 3 条规则 fn first_word(s: &str) -> &str {...} // static 生命期,可以在整个程序运行时保持可用 let s: &'static str = "I have a static lifetime.";&self또는&mut self라면self의 lifetime을 모든 출력 lifetime 매개변수에 할당한다는 것입니다. 세 번째: 입력 lifetime 매개변수가 여러 개이고 그중 하나가&self또는&mut self(method)라면self의 lifetime 매개변수를 모든 출력 lifetime 매개변수에 할당합니다. - 세 가지 표현 방식의 조합 사용
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 } }
출처
…
제네릭을 사용하는 코드의 성능
generic type 매개변수를 사용할 때 런타임 비용이 있는지 궁금할 수 있습니다. 좋은 소식은 Rust가 제네릭을 구현하는 방식 덕분에 generic type을 사용해도 concrete type을 사용할 때보다 코드가 느려지지 않는다는 것입니다.
Rust는 컴파일 시 제네릭을 사용하는 코드를 monomorphization하여 이를 달성합니다. Monomorphization은 컴파일할 때 사용되는 concrete type을 채워 generic code를 구체적인 코드로 바꾸는 과정입니다.
…
컴파일러는 명시적 주석이 없을 때 reference의 lifetime을 알아내기 위해 세 규칙을 사용합니다. 첫 번째 규칙은 입력 lifetime에, 두 번째와 세 번째 규칙은 출력 lifetime에 적용됩니다. 세 규칙을 모두 적용한 뒤에도 lifetime을 알아낼 수 없는 reference가 남으면 컴파일러는 오류와 함께 중단합니다. 이 규칙은 fn 정의와 impl 블록 모두에 적용됩니다.
첫 번째 규칙은 reference인 각 매개변수가 고유한 lifetime 매개변수를 갖는다는 것입니다. In other words, a function with one parameter gets one lifetime parameter: fn foo<'a>(x: &'a i32); a function with two parameters gets two separate lifetime parameters: fn foo<'a, 'b>(x: &'a i32, y: &'b i32); and so on.
두 번째 규칙은 입력 lifetime 매개변수가 정확히 하나라면 그 lifetime을 모든 출력 lifetime 매개변수에 할당한다는 것입니다.: fn foo<'a>(x: &'a i32) -> &'a i32.
세 번째 규칙은 입력 lifetime 매개변수가 여러 개이고 그중 하나가 method의 &self 또는 &mut self라면 self의 lifetime을 모든 출력 lifetime 매개변수에 할당한다는 것입니다.This third rule makes methods much nicer to read and write because fewer symbols are necessary.