【零基础 Rust 入门 03】类型 - 怎么做体操 🤸

111 阅读6分钟

这是【零基础 Rust 入门】系列的第 3 章。本系列由前端技术专家零弌分享。想要探索前端技术的无限可能,就请关注我们吧!🤗

Scalar Types

长度有符号无符号
8biti8u8
16biti16u16
32biti32u32
64biti64u64
128biti128u128
arch(平台相关)isizeusize
32bitf32-
64bitf64-
8bitboolean-
4 bytes(32 bit)char(Unicode)
let a = 10u8; // 这个是 u8 类型
let a = 10u64; // 这个是 u64 类型

let heart_eyed_cat = '😻'; // 这是一个 char

Rust 冷知识:溢出

关于溢出的问题,在 debug 模式下会有 panic 产生,release 模式下还是和其他语言一样的溢出,如果要依赖溢出这个特性,应该要用标准库中的 wrapping.

Compound Type

tuple

tuple 类似数组,长度是固定的,又有些类似 struct,每一位都有独立的类型。

具体写法如下

// 可以声明类型,也可以不声明
let tup: (i32, f64, u8) = (500, 6.4, 1);

// 可以直接访问第一位
let five_hundred = tup.0;

// 也可以解构
let (a, b, c) = tup;

在 ts 里也可以实现类似的效果。

let a: [ Number, String ] = [ 2333, '2333' ];

array

数组是固定长度的,并且是每一个元素都必须是相同类型的。并且数组的内存是分配在栈上的。

数组有两种声明的方式:

  • let a = [1, 2, 3, 4, 5];  字面的形式。
  • let a = [0; 3]; 代表数组的长度为 3,每个元素为 0。要写这种形式的话,要保证元素是实现 Copy 的。

Rust 冷知识:不同长度的数组类型其实是不同的。(以下来自 Rust 1.76.0)

  • [expr; 0] 长度为 0 的数组是允许的,并且 expr 还会被执行
  • 长度为 [0, 32] 的数组默认实现了以下 Trait
    • Copy
    • Clone
    • Debug
    • IntoIterator (implemented for [T; N], &[T; N] and &mut [T; N])
    • PartialEq, PartialOrd, Eq, Ord
    • Hash
    • AsRef, AsMut
    • Borrow, BorrowMut
  • 长度为 [1, 12] 的数组默认实现了 From<Tuple>

Struct

定义

struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: boolean,
}

初始化

let user = User {
    email: String::from("zhubin.gzb@antgroup.com"),
    username: String::from("zhubin.gzb"),
    active: true,
    sign_in_count: 1,
};

let user2 = User {
    ...user,
    sign_in_count: 2,
};

let email = String::from("zhubin.gzb@antgroup.com");
let username = String::from("zhubin.gzb");
let active = true;
let sign_in_count = 1;

User {
    email,
    username,
    active,
    sign_in_count,
}

Debug

给 Struct 增加 derive attribute 来使用 Rust 实现的默认 debug 实现。

所有支持的 derivable traits 可以参考 doc.rust-lang.org/book/append…

#[derive(Debug)]
struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: boolean,
}

Function&Method

  • method: 在 impl 上下文中实现的 fn
  • function: 没有在 impl 上下文中实现的 fn
impl User {
    // Method
    fn sign_in(&mut self) -> u32 {
        self.sign_in_count +=1;
    }
}

// Function
fn sign_in_user(user: &mut User) {
    self.sign_in_count +=1;
}

sign_in_user(user);
user.sign_in();

大家一定有一个疑惑构造函数去哪了?rust 中并没有构造函数这种概念,只有 struct 的实例化,一般通过一个 new 方法来表示构造函数。

impl User {
    fn new(email: String, user_name: String) -> Self {
        User {
            email,
            username,
            active: true,
            sign_in_count: 1,
        }
    }
}

Enum

rust 中的 enum 非常强大,到处都会使用,加上模式匹配,真香。match 大概比 switch 强 100 倍。

最基础的 enum 使用方法和传统的 enum 没啥区别。这里定义了 Message enum,其有不同的枚举值。

enum Message {
    Quit,
    Move,
    Write,
    ChangeColor,
}

这里将变量和枚举值关联在了一起,拿到一个 enum 之后就可以通过 match 来做不同情况的处理。

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

演示一个经典操作。rust 为了避免经典的 billion-dollar mistake ,不提供 null 这个特性,提供了 Option 这个枚举。

pub enum Option<T> {
    None,
    Some(T),
}

模式匹配

下面是一个模式匹配的实例

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        // 这样就获取到了 enum 中的值
        // 要注意值的类型,这里会 copy/move
        Some(i) => Some(i + 1),
    }
}

let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);

match 时也可以匹配具体的值

fn print_only_three(x: Options<i32>) {
    match x {
        Some(3) => println("val is 3"),
        _ => (),
    };
}

也可以通过 if 来实现

fn print_only_three(x: Options<i32>) {
    if let Some(3) = x {
        print("val is 3");
    }
}

错误处理

和 Option 同等地位的一个枚举。

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

可以让程序直接挂掉。

use std::fs::File;

fn main() {
    let f = File::open("hello.txt").unwrap();
    let f = File::open("hello.txt").expect("Failed to open hello.txt");
}

可以转换为另一个错误。

use std::io;
use std::io::Read;
use std::fs::File;

// 定义一个自定义错误
enum MyError {
    IoError(std::io::Error)
}

// 实现 IoError 到 MyError 的转换逻辑
impl From<std::io::Error> for MyError {
    fn from(e: std::io::Error) -> Self {
        MyError::IoError(e)
    }
}

type Result<T> = std::result::Result<T, MyError>;

fn read_username_from_file() -> Result<String> {
    // ? 会调用 From::from 方法
    let mut f = File::open("hello.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}

问:为什么不能实现 impl Into<MyError> for std::io::Error? 答:不可以实现自己项目之外的类型。

Trait & Generic

Trait 定义了一个特定的类型具备的能力,和 interface 是类似的。

定义 Trait

定义了一个 trait Summary 具有一个 method summarize。

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

实现 Trait

impl Summary for User {
    fn summarize(&self) -> String {
        format!("{}@{}", self.user_name, self.email)
    }
}

默认实现 Trait

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

Trait Bound

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

可以将多个 Trait 绑在一起

pub fn print<T: Summary + Display>(item: &T) {
    println!("show {}", item.summarize());
}

在范型很多的时候可以写的更加优雅

pub fn print<T>(item: &T) 
where T : Summary + Display{
    println!("show {}", item.summarize());
}

Associated Type

Associated Type 可以在范型中定义类型占位符,这些类型占位符可以在 trait 中的函数签名中使用。

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}

struct Counter {
    num: u32
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        let num = self.num
        self.num += 1
        Some(num)
    }
}

在举个使用范型的例子,这里用 T 去替换了 Associated Type

pub trait Iterator<T> {
    fn next(&mut self) -> Option<T>;
}

struct Counter {
    num: u32
}

impl Iterator<u32> for Counter {
    fn next(&mut self) -> Option<u32> {
        let num = self.num
        self.num += 1
        Some(num)
    }
}
  • Associated Type 使范型使用的时候不用声明其参数,比如 Counter 这里的例子明显是没必要的。特别是 Associated Type 多的时候,如果用范型就会很麻烦。
  • Associated Type 只能有一个,在 impl 中就指定了。不像范型可以指定多个实现。

操作符重载与默认范型

Point 实现了 + 操作。

use std::ops::Add;

#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

impl Add for Point {
    type Output = Point;

    fn add(self, other: Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

fn main() {
    assert_eq!(
        Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
        Point { x: 3, y: 3 }
    );
}

其实 Add 还有范型,默认类型是其自己,也可以为其他类型实现。

trait Add<Rhs=Self> {
    type Output;

    fn add(self, rhs: Rhs) -> Self::Output;
}

Generic Associated Type

在 GAT 面世之前,Associated Type是不可以加范型的,而上一章也提过 'a 也是一个范型。blog.rust-lang.org/2022/10/28/…

trait LendingIterator {
    type Item<'a> where Self: 'a;

    fn next<'a>(&'a mut self) -> Self::Item<'a>;
}

补充阅读

  • type alias: type Result<T> = std::result::Result<T, MyError>;
  • never type: ! fn bar() -> !,比如说 execunimplement 就需要这样的类型。
  • Dynamically Sized Types: 前一章提到 rust 必需知道所有类型的大小,而 str 是没有大小的,只能使用 &str 类型。哪如果要用到 str 怎么办?fn generic<T: ?Sized>(t: &T)。注意: ?只能对 Sized 可以用。

本章习题

定义一个 Iterator 范型,并为 Apple 实现,支持遍历 Apple 的切片。