【Rust学习之旅】泛型、Trait (十)

lxf2023-12-20 03:00:01

从这一期开始我们将真正的开始学习rust,泛型是大部分语言都有的东西,Trait与生命周期是rust特有的,虽然是他特有的,但也有别的编程语言概念的影子。

泛型

泛型这个东西对于使用过typescript的小伙伴来说都不陌生,泛型是具体类型或其他属性的抽象替代。可以把它简单的理解为占位符的意思。

展示了两个函数,它们的功能都是寻找 slice 中最大值。接着我们使用泛型将其合并为一个函数。

fn largest_i32(list: &[i32]) -> &i32 {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn largest_char(list: &[char]) -> &char {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest_i32(&number_list);
    println!("The largest number is {}", result);
    assert_eq!(*result, 100);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest_char(&char_list);
    println!("The largest char is {}", result);
    assert_eq!(*result, 'y');
}

接下来我们用泛型给他处理一下,我们将刚才的两个函数合并成一个,fn largest<T>(list: &[T]) -> T 使用类似函数签名。这里和typescript,完全一样。

fn largest<T>(list: &[T]) -> &T {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {}", result);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest(&char_list);
    println!("The largest char is {}", result);
}

函数在它的签名中使用了泛型,统一了两个实现。该示例也展示了如何调用 largest 函数,把 i32 值的 slice 或 char 值的 slice 传给它。请注意这些代码还不能编译,不过稍后会解决这个问题。

提示:泛型参数,导致下面无法比较两个泛型的大小,rust中需要实现 std::cmp::PartialOrd trait才可以比较两个变量大小。

结构体定义中的泛型

同样也可以用 <> 语法来定义结构体,它包含一个或多个泛型参数类型字段。

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 };
}

这里定义和typescript中函数的类型声明差不多就不细讲了。

枚举定义中的泛型

和结构体类似,枚举也可以在成员中存放泛型数据类型。前面我们学到的来个枚举OptionResult

enum Option<T> {
    Some(T),
    None,
}
enum Result<T, E> {
    Ok(T),
    Err(E),
}

当你意识到代码中定义了多个结构体或枚举,它们不一样的地方只是其中的值的类型的时候,不妨通过泛型类型来避免重复。

方法定义中的泛型

在为结构体和枚举实现方法时,一样也可以用泛型。

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
}

注意必须在 impl 后面声明 T,这样就可以在 Point<T> 上实现的方法中使用 T 了。

通过在 impl 之后声明泛型 T,Rust 就知道 Point 的尖括号中的类型是泛型而不是具体类型。

我们可以为泛型参数选择一个与结构体定义中声明的泛型参数所不同的名称,不过依照惯例使用了相同的名称。

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 main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
}

当然我们也可以仅为某个类型实现方法,这段代码意味着 Point<f32> 类型会有一个方法 distance_from_origin,而其他 T 不是 f32 类型的 Point<T> 实例则没有定义此方法。

泛型代码的性能

泛型并不会使程序比具体类型运行得慢。

Rust 通过在编译时进行泛型代码的 单态化monomorphization)来保证效率。

单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。

Trait:定义共同行为

trait 定义了某个特定类型拥有可能与其他类型共享的功能。可以通过 trait 以一种抽象的方式定义共享的行为。可以使用 trait bounds 指定泛型是任何拥有特定行为的类型。

trait 类似于typescript中的interfaces接口的功能,虽然有一些不同。

定义 trait

定义一个trait 与定义结构体、枚举这些数据类似,都是关键字+名字+{}

pub trait Summary {
    fn summarize(&self) -> String;
}

在方法签名后跟分号,而不是在大括号中提供其实现。接着每一个实现这个 trait 的类型都需要提供其自定义行为的方法体,编译器也会确保任何实现 Summary trait 的类型都拥有与这个签名的定义完全一致的 summarize 方法。

trait 体中可以有多个方法:一行一个方法签名且都以分号结尾。

为类型实现 trait

pub trait Summary {
    fn summarize(&self) -> String;
}

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)
    }
}

在类型上实现 trait 类似于实现与 trait 无关的方法。区别在于 impl 关键字之后,我们提供需要实现 trait 的名称,接着是 for 和需要实现 trait 的类型的名称。

默认实现

有时为 trait 中的某些或全部方法提供默认的行为,而不是在每个类型的每个实现中都定义自己的行为是很有用的。这样当为某个特定类型实现 trait 时,可以选择保留或重载每个方法的默认行为。

pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

如果想要对 NewsArticle 实例使用这个默认实现,可以通过 impl Summary for NewsArticle {} 指定一个空的 impl 块。

默认实现允许调用相同 trait 中的其他方法,哪怕这些方法没有默认实现。

pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

当使用这个版本的 Summary,只需在实现 trait 时定义 summarize_author 即可: 因为我们的summarize有默认实现

impl Summary for Tweet {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}

注意无法从相同方法的重载实现中调用默认方法。

trait 作为参数

我们可以将一个trait作为类型约束。这里参数就只能传递实现了Summary trait的类型

pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

Trait Bound 语法

我们也可以这样写,和上面的一样的。这种适用于多个的时候,看起来更简洁

pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

通过 + 指定多个 trait bound

如果 notify 需要显示 item 的格式化形式,同时也要使用 summarize 方法,那么 item 就需要同时实现两个不同的 trait:Display 和 Summary。这可以通过 + 语法实现:

pub fn notify(item: &(impl Summary + Display)) {...}

+ 语法也适用于泛型的 trait bound:

pub fn notify<T: Summary + Display>(item: &T) {...}

通过指定这两个 trait bound,notify 的函数体可以调用 summarize 并使用 {} 来格式化 item

通过 where 简化 trait bound

使用过多的 trait bound 也有缺点。看起来冗长,不清晰

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,
{
    unimplemented!()
}

where 跟在函数签名之后就可以了

返回实现了 trait 的类型

既然trait可以约束参数,当然也可以约束返回值。

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,但是返回了 NewsArticle 或 Tweet 就行不通:

fn returns_summarizable(switch: bool) -> impl Summary {
    if switch {
        NewsArticle {
            headline: String::from(
                "Penguins win the Stanley Cup Championship!",
            ),
            location: String::from("Pittsburgh, PA, USA"),
            author: String::from("Iceburgh"),
            content: String::from(
                "The Pittsburgh Penguins once again are the best \
                 hockey team in the NHL.",
            ),
        }
    } else {
        Tweet {
            username: String::from("horse_ebooks"),
            content: String::from(
                "of course, as you probably already know, people",
            ),
            reply: false,
            retweet: false,
        }
    }
}

这里尝试返回 NewsArticle 或 Tweet。这不能编译,因为 impl Trait 工作方式的限制。

使用 trait bound 有条件地实现方法

通过使用带有 trait bound 的泛型参数的 impl 块,可以有条件地只为那些实现了特定 trait 的类型实现方法。

impl<T: Display> ToString for T {
    // --snip--
}

结语

这一期我们学习了泛型与trait,都和typescript中的概念类似,只是trait有些区别。下面我会会讲解rust的重要概念生命周期,可能前端小伙伴经常听说,比如vue 就有生命周期的概念,我们下文,聊一聊他们之间是一样的吗?

本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!