Web Rust基础三

内容纲要

模块化

函数

函数的参数类型和返回值类型不能省略:

fn a_plus_b(a: i32, b: i32) -> i32 {
    return a + b;
}

fn main() {
    println!("{}", a_plus_b(1, 2));
}

函数的返回值可以像用 return 返回。更常见的是,函数的最后一个表达式如果不以分号结尾,它的值就是这个函数的返回值,例如:

fn a_plus_b(a: i32, b: i32) -> i32 {
    a + b
}

给函数传参时,对于字符串或者复杂 struct ,为了避免复制,通常应传递引用:

fn concat_str(a: &str, b: &str) -> String {
    format!("{}{}", a, b)
}F
fn main() {
    let a: String = format!("A");
    let b: String = format!("BC");
    println!("{}", concat_str(&a, &b));
}

关联函数

函数可以关联在 struct 或 enum 上,这样就可以像面向对象编程语言的对象方法那样调用,例如:

struct Rectangle {
    width: f32,
    height: f32,
}

// Rectangle 的关联函数列表
impl Rectangle {
    fn area(&self) -> f32 { // &self 表示取 Rectangle 的不可变引用
        self.width * self.height
    }

    fn extend(
        &mut self, // &mut self 表示取 Rectangle 的可变引用
        extend_width: f32,
        extend_height: f32,
    ) {
        self.width += extend_width;
        self.height += extend_height;
    }

    fn new( // 也可以不取 Rectangle 的引用
        initial_width: f32,
        initial_height: f32,
    ) -> Self {
        Self { // Self 指代 Rectangle (也可以直接写成 Rectangle )
            width: initial_width,
            height: initial_height,
        }
    }
}

fn main() {
    // 关联函数可以用 :: 形式调用
    let mut rect = Rectangle::new(2.0, 3.5);
    // 如果关联函数的第一个参数是 self 的话,还可以这样调用
    rect.extend(1.0, -2.0);
    println!("{}", rect.area()); // 输出 4.5
}

一般的,很多 struct 都有个命名为 new 的方法,这个方法返回 Self ,类似于面向对象编程语言中的构造器;如果 struct 不应该被直接构造,那它就没有 new 方法。

子模块

当项目较大的时候,可以将部分 struct 、 enum 和函数等拆分到子模块中。

子模块写法有很多种。它可以直接用 mod … { } 定义,例如:

// 定义一个子模块
mod utils {
    pub fn a_plus_b(a: i32, b: i32) -> i32 { // pub 表示可以在子模块外访问
        a + b
    }
}

fn main() {
    let sum = utils::a_plus_b(1, 2); // 使用 :: 访问其中的函数
    println!("{}", sum); // 输出 3
}

更常见的做法是,将子模块的内容单独写在另一个文件里。例如,与 main.rs 同一个目录中还有一个文件 utils.rs 或在 utils 子目录下有 mod.rs,内容是:

// utils.rs 或 utils/mod.rs
pub fn a_plus_b(a: i32, b: i32) -> i32 {
    a + b
}

在 main.rs 中就可以引入:

// 将 utils 的内容作为子模块引入
mod utils;

fn main() {
    let sum = utils::a_plus_b(1, 2); // 使用 :: 访问其中的函数
    println!("{}", sum); // 输出 3
}

在使用子模块中的 struct 、 enum 和函数等时,可以使用 :: 来查找,必要时还可以借助 super 和 crate 关键字:

// utils 子模块中的 MyStruct
utils::MyStruct
// 父模块的 utils 子模块中的 MyStruct
super::utils::MyStruct
// crate 根模块的 utils 子模块中的 MyStruct
crate::utils::MyStruct

公开私有访问

子模块内部的 struct 、 enum 和 fn 等内容不能被子模块外部访问到,使用 pub 可以改变这些内容的可见性。可见性可以被设定为任何一个模块层级,例如:

// 这个函数只能在子模块内使用
fn my_private_function() { }
// 这个函数在父模块中可访问
pub(super) fn my_super_public_function() { }
// 这个函数在整个 crate 内部可访问
pub(crate) fn my_crate_public_function() { }
// 这个函数在所有地方都可访问(如果有别的 crate 引用这个 crate ,那它也能访问)
pub fn my_public_function() { }

use

在子模块层级很多时,每次都使用 :: 来访问的话代码会很长,通常可以用 use 来简化代码。这样相当于为 struct 、 enum 和 fn 等创建了一个较短的别名:

mod utils;
use utils::a_plus_b;

fn main() {
    let sum = a_plus_b(1, 2);
    println!("{}", sum);
}

use 前还可以用 pub(…) 来修饰,使这个别名在其他文件中也能访问:

// 这样可以使其他文件中都可以访问 crate::a_plus_b
pub use utils::a_plus_b;

还可以结合 as 关键字来改名:

// 将引入的 a_plus_b 改名为 plus
use utils::a_plus_b as plus;

还可以直接将一个子模块内的所有项目全部引入:

// 引入所有 utils 中的项目
use utils::*;

struct 内部可见性

默认情况下,即使 struct 是外部可访问的,它内部的字段仍不可被外部访问。可以使用 pub 关键字单独指定每个字段的可见性:

pub struct Rectangle {
    pub(super) width: f32, // 这个字段在父模块可访问
    height: f32, // 这个字段仅子模块内可访问
}

如果有任何一个字段在父模块中不可访问,父模块就不能直接创建这个 struct 的数据实例,即:

fn new() -> Rectangle {
    // 只能写在子模块内,因为 height 只有子模块内可见
    Rectangle {
        width: 0.0,
        height: 0.0,
    }
}

因此,控制内部字段的可见性可以防止 struct 被外部代码以错误的方式创建。大多数情况下, struct 内部字段都不应设为 pub ,而是暴露 pub 的关联函数供外部访问。

另外, enum 的每个分支的可见性跟随 enum 本身,不能为 enum 的每个分支单独指定可见性。

接口

不写例子了。

trait

不同于一些常用的编程语言, rust 没有继承、 interface 等机制。取而代之的是 trait 。

trait 可以用于表达 struct 和 enum 所具有的抽象特征。例如,一个商城内有多个商品,其中水果有保质期等数据,书有作者、出版社等数据,但他们都是商品、有各自的售价。这样,你可以用一个“商品” trait 将他们归为一类:

trait Product {
    // trait 中可以包含若干个函数声明(以分号结尾,没有具体实现)
    fn price(&self) -> u32;
}

然后,为水果实现 Product trait :

struct Fruit {
    durability_days: u32,
    current_price: u32,
}

// 实现 Product trait ,此时需要为其中的函数添加实现
impl Product for Fruit {
    fn price(&self) -> u32 {
        self.current_price
    }
}

为书也实现 Product trait :

struct Book {
    author: String,
    publisher: String,
    original_price: u32,
    discount: f32,
}

impl Product for Book {
    fn price(&self) -> u32 {
        let price = self.original_price as f32 * self.discount;
        price.round() as u32
    }
}

在实现购物车的时候,并不需要关心具体是水果还是书,而是针对 Product trait 进行实现,具体函数中可以使用一个 impl Product 来代替:

struct Cart {
    total_price: u32,
}
impl Cart {
    fn new() -> Self {
        Self { total_price: 0 }
    }
    // 函数参数可以不是一个具体的类型,而是一个 trait
    fn add_product(&mut self, product: & impl Product) {
        self.total_price += product.price();
    }
    fn checkout(self) -> u32 {
        self.total_price
    }
}

为 impl Product 传参时,可以传任何实现了 Product trait 的类型:

fn main() {
    let apple = Fruit {
        durability_days: 30,
        current_price: 5,
    };
    let taocp = Book {
        author: format!("Knuth"),
        publisher: format!("AW"),
        original_price: 100,
        discount: 0.5,
    };
    let mut cart = Cart::new();
    cart.add_product(&apple);
    cart.add_product(&taocp);
    let total_price = cart.checkout();
    println!("Total price: {}", total_price);
}

像上例这样, trait 的主要作用就是使得 Cart 的实现并不需要关心各种 Product 的具体实现,解耦代码。

在 trait 中实现函数

在 trait 定义里,除了未实现的函数,还可以包含带有具体实现的函数。但在函数实现中只能调用这个 trait 里其他函数,不能访问实现 trait 的 struct (或 enum )里面的具体字段。例如,可以添加一个计算多个相同商品总价的函数:

trait Product {
    fn price(&self) -> u32;
    fn price_of_multiple_items(&self, count: u32) -> u32 {
        self.price() * count
    }
}

注意,在 trait 中实现了的函数,依旧可以被 struct 或 enum 在实现这个 trait 时覆盖(虽然实践中很少这么做)。

关联类型

如果 trait 中的函数中包含一些在 trait 中还不确定的类型,可以在 trait 里用 type 定义出来。例如,如果水果是按重量计价,书是按数量计价的,就要改成下面这样的价格函数:

trait Product {
    type AmountType; // 定义一个关联类型
    fn price_of_amount(&self, amount: Self::AmountType) -> u32;
}

其中的 amount 可能是重量数值 f32 或者数量 u32 ,具体是哪个类型,需要在 impl Product 中明确指定:

struct Fruit {
    durability_days: u32,
    unit_price: u32,
}
impl Product for Fruit {
    type AmountType = f32; // 指定关联类型的实际类型
    fn price_of_amount(&self, amount: Self::AmountType) -> u32 {
        let price = self.unit_price as f32 * amount;
        price.round() as u32
    }
}
struct Book {
    author: String,
    publisher: String,
    original_price: u32,
    discount: f32,
}
impl Product for Book {
    type AmountType = u32; // 指定关联类型的实际类型
    fn price_of_amount(&self, amount: Self::AmountType) -> u32 {
        let price = self.original_price as f32 * self.discount;
        price.round() as u32 * amount
    }
}

在实际使用 price_of_amount 方法时,传入值的类型需要与对应的关联类型一致:

fn main() {
    let apple = Fruit {
        durability_days: 30,
        unit_price: 5,
    };
    let taocp = Book {
        author: format!("Knuth"),
        publisher: format!("AW"),
        original_price: 100,
        discount: 0.5,
    };
    let mut total_price = 0;
    total_price += apple.price_of_amount(1.5);
    total_price += taocp.price_of_amount(2);
    println!("Total price: {}", total_price);
}

内置 trait

rust 语言本身也有一些内置的 trait ,如 Debug 、 Display 等,实现这些内置 trait 可以改变一些 rust 编译器的行为。

Debug trait 可以改变 struct 或 enum 在 println! 中被 {:?} 输出时的具体显示内容。例如,普通的 struct 会产生固定格式的输出:

struct Rect {
    width: f32,
    height: f32,
}
fn main() {
    let rect = Rect {
        width: 2.,
        height: 3.,
    };
    println!("{:?}", rect);
    // 输出:Rect { width: 2.0, height: 3.0 }
}

通过 impl Debug 就可以自定义这个输出格式:

use std::fmt::{Debug, Formatter};
struct Rect {
    width: f32,
    height: f32,
}
impl Debug for Rect {
    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
        write!(f, "Rect({}x{})", self.width, self.height)
    }
}
fn main() {
    let rect = Rect {
        width: 2.,
        height: 3.,
    };
    println!("{:?}", rect);
    // 输出:Rect(2x3)
}

在 println! 中被 {} 输出时的具体显示内容可以通过 impl Display 来改变,具体方法类似。

rust 提供了很多这样的类似 trait ,可以改变语言本身的基础功能,包括让 struct 或者 enum 支持运算符(运算符重载)也可以通过实现一些 std::ops 里面的一些 trait 来做到。

范型

struct 和 enum 的类型参数

有时,在 struct 或 enum 定义时,无法确定其中某项的具体类型,此时可以先用“类型参数”来代替具体的类型。

例如,长方形包含长、宽两项,每一项的数值既可能是整数类型 u32 也可能是小数类型 f32 ,此时可以将它们的类型用参数 T 和 U 来代替:

struct Rectangle<T, U> {
    width: T,
    height: U,
}

注意,上面这种定义方式将允许长和宽是不同的类型(可以一个是 u32 、另一个是 f32 ),如果要求长和宽的类型相同,应当只使用一个参数 T :

struct Rectangle<T> {
    width: T,
    height: T,
}

创建它的实例时, T 需要有一个确定的类型,可以写成:

let rect = Rectangle::<u32> {
    width: 2,
    height: 3,
};

其实可以不写明具体类型,编译器会根据具体值的类型来推断出 T 的实际类型:

let rect = Rectangle {
    width: 2,
    height: 3,
};

泛型约束

实践中,泛型的实际类型往往不能是任意类型。在定义时,常常会要求 T 的实际类型必须是实现了某些 trait 的类型,例如:

use std::fmt::Debug;
// 要求 T 的实际类型必须实现 Debug
struct Rectangle<T: Debug> {
    width: T,
    height: T,
}
impl<T: Debug> Rectangle<T> {
    fn debug_info(&self) -> String {
        // 因为 T 实现了 Debug ,所以可以用 {:?} 占位
        format!("{:?}x{:?}", self.width, self.height)
    }
}
fn main() {
    let rect = Rectangle::<u32> {
        width: 2,
        height: 3,
    };
    println!("{}", rect.debug_info()); // 输出 2x3
}

上面这种方式可以将 T 限制为实现了 Debug 的类型,但在实践中这种限制还不够精确。如果需要更精确的限制,比如将 T 限制为 u32 、 f32 二者之一,可以单独定一个新的 trait ,并对 u32 和 f32 实现这个新的 trait :

use std::fmt::Debug;
trait Length {
    fn multiply(&self, rhs: &Self) -> Self;
}
// 使 u32 实现 Length
impl Length for u32 {
    fn multiply(&self, rhs: &u32) -> u32 {
        self * rhs
    }
}
// 使 f32 实现 Length
impl Length for f32 {
    fn multiply(&self, rhs: &f32) -> f32 {
        self * rhs
    }
}
// T 的实际类型必须实现 Debug 和 Length
// 因为只有 u32 和 f32 实现了 Length ,所以实际只能是它们之一
struct Rectangle<T: Debug + Length> {
    width: T,
    height: T,
}
impl<T: Debug + Length> Rectangle<T> {
    fn area(&self) -> T {
        // 可以调用 Length 中的 multiply 方法了
        self.width.multiply(&self.height)
    }
}
fn main() {
    let rect = Rectangle::<f32> {
        width: 1.5,
        height: 2.0,
    };
    println!("{}", rect.area()); // 输出 3
}

在函数上使用泛型

除了 struct 和 enum ,在函数和 trait 上也可以定义类型参数。其中,在函数上的应用也是比较常见的。具体用法也比较类似,例如,上面的 Length 可以用在函数泛型约束中:

fn rectangle_area<T: Length>(width: T, height: T) -> T {
    width.multiply(&height)
}

不过,为了更加直观,通常 T 的约束会使用 where 附加在后面,例如:

fn rectangle_area<T>(width: T, height: T) -> T
where T: Length {
    width.multiply(&height)
}

另外,如果 T 只在参数列表中使用一次,那可以使用 impl Trait 的写法简化,即以下两种写法等价:

fn print_square_area<T>(edge_len: T)
where T: Debug + Length {
    println!("{:?}", edge_len.multiply(&edge_len));
}
// 等价于
fn print_square_area(edge_len: impl Debug + Length) {
    println!("{:?}", edge_len.multiply(&edge_len));
}

在实践中泛型是很常见的,使用空类型、错误处理时都会涉及泛型。

code enjoy! 🐜🐜🐜

作者:indeex

链接:https://indeex.club

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

发表评论

您的电子邮箱地址不会被公开。