【Rust】——高级类型

Y小夜 2024-09-06 10:35:01 阅读 64

💻博主现有专栏:

                C51单片机(STC89C516),c语言,c++,离散数学,算法设计与分析,数据结构,Python,Java基础,MySQL,linux,基于HTML5的网页设计及应用,Rust(官方文档重点总结),jQuery,前端vue.js,Javaweb开发,Python机器学习等

🥏主页链接:

                Y小夜-CSDN博客

目录

🎯为了类型安全和抽象而使用的newtype模式

🎯类型别名用来创建类型同义词

🎯不返回的never type

🎯动态大小类型和Sized trait

        Rust 的类型系统有一些我们曾经提到但没有讨论过的功能。首先我们从一个关于为什么 newtype 与类型一样有用的更宽泛的讨论开始。接着会转向类型别名(type aliases),一个类似于 newtype 但有着稍微不同的语义的功能。我们还会讨论 <code>! 类型和动态大小类型。

🎯为了类型安全和抽象而使用的newtype模式

        newtype 模式也可以用于一些其他我们还未讨论的功能,包括静态的确保某值不被混淆,和用来表示一个值的单位。Millimeters 和 Meters 结构体都在 newtype 中封装了 u32 值。如果编写了一个有 Millimeters 类型参数的函数,不小心使用 Meters 或普通的 u32 值来调用该函数的程序是不能编译的。

        newtype 模式也可以用于抽象掉一些类型的实现细节:例如,封装类型可以暴露出与直接使用其内部私有类型时所不同的公有 API。

        newtype 也可以隐藏其内部的泛型类型。例如,可以提供一个封装了 HashMap<i32, String> 的 People 类型,用来储存人名以及相应的 ID。

🎯类型别名用来创建类型同义词

        Rust 提供了声明 类型别名type alias)的能力,使用 type 关键字来给予现有类型另一个名字。例如,可以像这样创建 i32 的别名 Kilometers

type Kilometers = i32;

这意味着 Kilometers 是 i32 的 同义词synonym);创建的 Millimeters 和 Meters 类型。Kilometers 不是一个新的、单独的类型。Kilometers 类型的值将被完全当作 i32 类型值来对待:

type Kilometers = i32;

let x: i32 = 5;

let y: Kilometers = 5;

println!("x + y = {}", x + y);

        因为 Kilometers 是 i32 的别名,它们是同一类型,可以将 i32 与 Kilometers 相加,也可以将 Kilometers 传递给获取 i32 参数的函数。但通过这种手段无法获得上一部分讨论的 newtype 模式所提供的类型检查的好处。换句话说,如果在哪里混用 Kilometers 和 i32 的值,编译器也不会给出一个错误。

        类型别名的主要用途是减少重复。例如,可能会有这样很长的类型:

Box<dyn Fn() + Send + 'static>

        在函数签名或类型注解中每次都书写这个类型将是枯燥且易于出错的。

let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi"));

fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) {

// --snip--

}

fn returns_long_type() -> Box<dyn Fn() + Send + 'static> {

// --snip--

}

        类型别名通过减少项目中重复代码的数量来使其更加易于控制。这里我们为这个冗长的类型引入了一个叫做 Thunk 的别名,这样就可以如示例 19-25 所示将所有使用这个类型的地方替换为更短的 Thunk

type Thunk = Box<dyn Fn() + Send + 'static>;

let f: Thunk = Box::new(|| println!("hi"));

fn takes_long_type(f: Thunk) {

// --snip--

}

fn returns_long_type() -> Thunk {

// --snip--

}

        这样读写起来就容易多了!为类型别名选择一个好名字也可以帮助你表达意图(单词 thunk 表示会在之后被计算的代码,所以这是一个存放闭包的合适的名字)。

🎯不返回的never type

        Rust 有一个叫做 ! 的特殊类型。在类型理论术语中,它被称为 empty type,因为它没有值。我们更倾向于称之为 never type。这个名字描述了它的作用:在函数从不返回的时候充当返回值。例如:

fn bar() -> ! {

// --snip--

}

        这读 “函数 bar 从不返回”,而从不返回的函数被称为 发散函数diverging functions)。不能创建 ! 类型的值,所以 bar 也不可能返回值。

let guess: u32 = match guess.trim().parse() {

Ok(num) => num,

Err(_) => continue,

};

当时我们忽略了代码中的一些细节。

let guess = match guess.trim().parse() {

Ok(_) => 5,

Err(_) => "hello",

};

        这里的 guess 必须既是整型 也是 字符串,而 Rust 要求 guess 只能是一个类型。那么 continue 返回了什么呢?为什么示例 19-26 中会允许一个分支返回 u32 而另一个分支却以 continue 结束呢?

        正如你可能猜到的,continue 的值是 !。也就是说,当 Rust 要计算 guess 的类型时,它查看这两个分支。前者是 u32 值,而后者是 ! 值。因为 ! 并没有一个值,Rust 决定 guess 的类型是 u32

        描述 ! 的行为的正式方式是 never type 可以强转为任何其他类型。允许 match 的分支以 continue 结束是因为 continue 并不真正返回一个值;相反它把控制权交回上层循环,所以在 Err 的情况,事实上并未对 guess 赋值。

impl<T> Option<T> {

pub fn unwrap(self) -> T {

match self {

Some(val) => val,

None => panic!("called `Option::unwrap()` on a `None` value"),

}

}

}

        Rust 知道 val 是 T 类型,panic! 是 ! 类型,所以整个 match 表达式的结果是 T 类型。这能工作是因为 panic! 并不产生一个值;它会终止程序。对于 None 的情况,unwrap 并不返回一个值,所以这些代码是有效的。

最后一个有着 ! 类型的表达式是 loop

print!("forever ");

loop {

print!("and ever ");

}

        这里,循环永远也不结束,所以此表达式的值是 !。但是如果引入 break 这就不为真了,因为循环在执行到 break 后就会终止。

🎯动态大小类型和Sized trait

        Rust 需要知道有关类型的某些细节,例如为特定类型的值需要分配多少空间。这便是起初留下的一个类型系统中令人迷惑的角落:即 动态大小类型dynamically sized types)。这有时被称为 “DST” 或 “unsized types”,这些类型允许我们处理只有在运行时才知道大小的类型。

        让我们深入研究一个贯穿本书都在使用的动态大小类型的细节:str。没错,不是 &str,而是 str 本身。str 是一个 DST;直到运行时我们都不知道字符串有多长。因为直到运行时都不能知道其大小,也就意味着不能创建 str 类型的变量,也不能获取 str 类型的参数。考虑一下这些代码,它们不能工作:

let s1: str = "Hello there!";

let s2: str = "How's it going?";

        Rust 需要知道应该为特定类型的值分配多少内存,同时所有同一类型的值必须使用相同数量的内存。如果允许编写这样的代码,也就意味着这两个 str 需要占用完全相同大小的空间,不过它们有着不同的长度。这也就是为什么不可能创建一个存放动态大小类型的变量的原因。

        为了处理 DST,Rust 提供了 Sized trait 来决定一个类型的大小是否在编译时可知。这个 trait 自动为编译器在编译时就知道大小的类型实现。另外,Rust 隐式的为每一个泛型函数增加了 Sized bound。也就是说,对于如下泛型函数定义:

fn generic<T>(t: T) {

// --snip--

}

实际上被当作如下处理:

fn generic<T: Sized>(t: T) {

// --snip--

}

        泛型函数默认只能用于在编译时已知大小的类型。然而可以使用如下特殊语法来放宽这个限制:

fn generic<T: ?Sized>(t: &T) {

// --snip--

}

  ?Sized 上的 trait bound 意味着 “T 可能是也可能不是 Sized” 同时这个注解会覆盖泛型类型必须在编译时拥有固定大小的默认规则。这种意义的 ?Trait 语法只能用于 Sized ,而不能用于任何其他 trait。

        另外注意我们将 t 参数的类型从 T 变为了 &T:因为其类型可能不是 Sized 的,所以需要将其置于某种指针之后。在这个例子中选择了引用。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。