Rust - Basic - 10 - 제네릭 데이터 형식 & Trait & 수명

제네릭: 데이터 형식 & 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 traitA generic 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
    &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.";
    
    컴파일러는 명시적 주석이 없을 때 reference의 lifetime을 계산하기 위해 세 가지 규칙을 사용합니다. 첫 번째 규칙은 reference인 각 매개변수가 고유한 lifetime 매개변수를 갖는다는 것입니다. 첫 번째: 각 reference 입력 매개변수에는 자체 lifetime 매개변수가 있습니다. 두 번째 규칙은 입력 lifetime 매개변수가 정확히 하나라면 그 lifetime을 모든 출력 lifetime 매개변수에 할당한다는 것입니다. 두 번째: 입력 lifetime 매개변수가 하나뿐이면 이 매개변수를 모든 출력 lifetime 매개변수에 할당합니다. 세 번째 규칙은 입력 lifetime 매개변수가 여러 개이고 그중 하나가 method의 &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
        }
    }
    

출처

https://doc.rust-lang.org/book/ch10-00-generics.html

…

제네릭을 사용하는 코드의 성능

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.