当前位置:   article > 正文

Rust 入门教程_rust教程

rust教程

文章目录


前言

欢迎来到 Rust 语言之旅。本教程旨在循序渐进地介绍 Rust 编程语言的特性,大家通常认为 Rust 是一门学习曲线陡峭的语言。本文适用于有一定编程基础的同学学习,以代码实例来演示 Rust 编程。如果你不会 Rust 或者想要加深 Rust 的印象的话,那么就跟着我一起来学习吧!


1. 使用cargo创建项目

cargo 是Rust 的包管理器和构建系统

创建项目的命令如下:

cargo new 项目名	
  • 1

编译:

cargo build
  • 1

运行:

cargo run
  • 1

2. 编程语言类型

静态类型?动态类型?强类型?弱类型?

  • JavaScript是什么类型的语言? 动态,弱类型
  • C是什么类型的语言? 静态,弱类型
  • Rust是什么类型的语言? 静态,强类型

1. 整数

  • 无符号整数:u8,u16,u32(推荐),u64,usize
  • 有符号整数:i8,i16,i32(推荐),i64,isize

2. 浮点数

  • 32位浮点数:f32
  • 64位浮点数:f64(推荐)

3. 字符

Char 是Unicode码,并且总是4bytes大小

3. 常量与不可变变量的区别

既然不可变变量是不可变的,那不就是常量吗?为什么叫变量?

变量和常量还是有区别的。在Rust中,以下程序是合法的:

let a = 123;   // 可以编译,但可能有警告,因为该变量没有被使用
let a = 456;
  • 1
  • 2

4. 重影(Shadowing)

重影的概念与其他面向对象语言里的"重写"(Override)或"重载"(Overload)是不一样的。重影就是刚才讲述的所谓"重新绑定",之所以加引号就是为了在没有介绍这个概念的时候代替一下概念。

重影就是指变量的名称可以被重新使用的机制:

实例

fn main() {
    let x = 5;
    let x = x + 1;
    let x = x * 2;
    println!("The value of x is: {}", x);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这段程序的运行结果:
The value of x is: 12

重影与可变变量的赋值不是一个概念,重影是指用同一个名字重新代表另一个变量实体,其类型、可变属性和值都可以变化。但可变变量赋值仅能发生值的变化。

let mut s = "123";
s = s.len();
  • 1
  • 2

这段程序会出错:不能给字符串变量赋整型值。

5. 两个重要的泛型类型

  • Option,代表有或无
  • Result<T,E>,代表成功或失败

实例

// 两个重要的泛型类型
enum Option<T> {
    Some(T),
    None,
}

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

fn main() {
    // 查看当前用户目录—>返回Option
    match std::env::home_dir() {
        Some(data) => println!("option i some, data = {:?}", data),
        // 如果为None执行
        None => println!("option is none"),
    }
    // 查看当前系统的环境变量—>返回Result
    match std::env::var("MYSQL_HOME") {
        Ok(data) => println!("ok! {:?}", data),
        Err(err) => println!("err {}", err),
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

运行结果:
option i some, data = “C:\Users\1”
ok! “D:\Program Files\MySQL\MySQL Server 8.0”

6. 常见的内存管理方式

  • C语言的malloc和free(手动管理,Bug制造机)
  • GC:Golang,Java等语言(自动管理)
  • 基于生命周期的半自动管理:Rust

7. 如何理解生命周期?

在C语言中需要直接手动调用free去释放内存

Rust在编译间计算变量的使用范围

当变量不再被使用时编译自动在源码中插入free代码

8. 条件语句

1. if实例

fn main() {
    let a = 3;
    let number = if a > 0 { 1 } else { -1 };
    println!("number 为 {}", number);
}
  • 1
  • 2
  • 3
  • 4
  • 5

2. while循环实例

常用来循环外部条件,条件不成立结束循环

let mut i = 0;
while i < 10 {
    // 循环体
    i += 1;
}
  • 1
  • 2
  • 3
  • 4
  • 5

3. for循环实例

常用来遍历一个线性数据据结构(比如数组)

fn main() {
    let a = [10, 20, 30, 40, 50];
    for i in a.iter() {
        println!("值为 : {}", i);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

4. loop循环实例

常用来循环内部条件,使用 break 结束循环

fn main() {
    let s = ['R', 'U', 'N', 'O', 'O', 'B'];
    let mut i = 0;
    loop {
        let ch = s[i];
        if ch == 'O' {
            break;
        }
        println!("\'{}\'", ch);
        i += 1;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

福利时刻,玩个游戏放松一下吧!————猜谜游戏

extern crate rand;
use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Guess the number!");
    let secret_number = rand::thread_rng().gen_range(1..101);
    loop {
        println!("Please input your guess.");
        let mut guess = String::new();
        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");
        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };
        println!("You guessed: {}", guess);
        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

运行结果:
Guess the number!
Please input your guess.
60
You guessed: 60
Too big!
Please input your guess.
30
You guessed: 30
Too small!
Please input your guess.
45
You guessed: 45
Too small!
Please input your guess.
55
You guessed: 55
Too small!
Please input your guess.
58
You guessed: 58
Too small!
Please input your guess.
59
You guessed: 59
You win!

9. 变量与数据交互的方式

变量与数据交互方式主要有移动(Move)和克隆(Clone)两种

10. 引用的一些规制

不会获取所有权(所有权稍后会详细介绍),默认情况下是不可变的,同一时间最多只能存在一个可变引用

1. 引用实例(实质上"引用"是变量的间接访问方式)

fn main() {
    let s1 = String::from("hello");
    let s2 = &s1;
    println!("s1 is {}, s2 is {}", s1, s2);
}
  • 1
  • 2
  • 3
  • 4
  • 5

可变引用与不可变引用相比除了权限不同以外,可变引用不允许多重引用,但不可变引用可以

2. "垂悬引用"实例

在 Rust 语言里不允许出现,如果有,编译器会发现它

fn main() {
    let reference_to_nothing = dangle();
}
fn dangle() -> &String {
    let s = String::from("hello");
    &s
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

很显然,伴随着 dangle 函数的结束,其局部变量的值本身没有被当作返回值,被释放了。但它的引用却被返回,这个引用所指向的值已经不能确定的存在,故不允许其出现。

11. Slice(切片)类型实例

fn main() {
    let s = String::from("broadcast");
    let part1 = &s[0..5];
    let part2 = &s[5..9];
    println!("{}={}+{}", s, part1, part2);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

运行结果:
broadcast=broad+cast

  • …y 等价于 0…y
  • x… 等价于位置 x 到数据结束
  • … 等价于位置 0 到结束

注意:到目前为止,尽量不要在字符串中使用非英文字符,因为编码的问题。

实际上,到目前为止你一定疑惑为什么每一次使用字符串都要这样写String::from(“runoob”) ,直接写 “runoob” 不行吗?

事已至此我们必须分辨这两者概念的区别了。在 Rust 中有两种常用的字符串类型:str 和 String。str 是 Rust 核心语言类型,字符串切片(String Slice),常常以引用的形式出现(&str)。

凡是用双引号包括的字符串常量整体的类型性质都是 &str:

let s = "hello";
  • 1

这里的 s 就是一个 &str 类型的变量。

String 类型是 Rust 标准公共库提供的一种数据类型,它的功能更完善——它支持字符串的追加、清空等实用的操作。String 和 str 除了同样拥有一个字符开始位置属性和一个字符串长度属性以外还有一个容量(capacity)属性。

String 和 str 都支持切片,切片的结果是 &str 类型的数据。

注意:切片结果必须是引用类型,但开发者必须自己明示这一点:

let slice = &s[0..3];
  • 1

有一个快速的办法可以将 String 转换成 &str:

let s1 = String::from("hello");
let s2 = &s1[..];
  • 1
  • 2

12. 非字符串切片实例

除了字符串以外,其他一些线性数据结构也支持切片操作,例如数组

fn main() {
    let arr = [1, 3, 5, 7, 9];
    let part = &arr[0..3];
    for i in part.iter() {
        println!("{}", i);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

运行结果:
1
3
5

13. 结构体实例

let domain = String::from("www.runoob.com");
let name = String::from("RUNOOB");
let runoob = Site {
    domain,  // 等同于 domain : domain,
    name,    // 等同于 name : name,
    nation: String::from("China"),
    traffic: 2013
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

有这样一种情况:你想要新建一个结构体的实例,其中大部分属性需要被设置成与现存的一个结构体属性一样,仅需更改其中的一两个字段的值,可以使用结构体更新语法:

let site = Site {
    domain: String::from("www.runoob.com"),
    name: String::from("RUNOOB"),
    ..runoob
};
  • 1
  • 2
  • 3
  • 4
  • 5

注意:…runoob 后面不可以有逗号。这种语法不允许一成不变的复制另一个结构体实例,意思就是说至少重新设定一个字段的值才能引用其他实例的值。

14. 元组结构体实例

"颜色"和"点坐标"是常用的两种数据类型,但如果实例化时写个大括号再写上两个名字就为了可读性牺牲了便捷性,Rust 不会遗留这个问题。元组结构体对象的使用方式和元组一样,通过 . 和下标来进行访问:

fn main() {
    struct Color(u8, u8, u8);
    struct Point(f64, f64);

    let black = Color(0, 0, 0);
    let origin = Point(0.0, 0.0);

    println!("black = ({}, {}, {})", black.0, black.1, black.2);
    println!("origin = ({}, {})", origin.0, origin.1);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

运行结果:
black = (0, 0, 0)
origin = (0, 0)

15. 输出结构体实例

调试中,完整地显示出一个结构体实例是非常有用的。但如果我们手动的书写一个格式会非常的不方便。所以 Rust 提供了一个方便地输出一整个结构体的方法:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}
fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };
    println!("rect1 is {:?}", rect1);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

如第一行所示:一定要导入调试库 #[derive(Debug)] ,之后在 printlnprint 宏中就可以用 {:?} 占位符输出一整个结构体:

rect1 is Rectangle { width: 30, height: 50 }
  • 1

如果属性较多的话可以使用另一个占位符 {:#?}

输出结果:
rect1 is Rectangle {
width: 30,
height: 50
}

16. 结构体方法实例

结构体方法的第一个参数必须是 &self ,不需声明类型,因为 self 不是一种风格而是关键字。

struct Rectangle {
    width: u32,
    height: u32,
}
impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn wider(&self, rect: &Rectangle) -> bool {
        self.width > rect.width
    }
}
fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };
    let rect2 = Rectangle { width: 40, height: 20 };

    println!("{}", rect1.wider(&rect2));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

运行结果:
false

17. 结构体关联函数实例

之所以"结构体方法"不叫"结构体函数"是因为"函数"这个名字留给了这种函数:它在 impl 块中却没有 &self 参数。

这种函数不依赖实例,但是使用它需要声明是在哪个 impl 块中的。

一直使用的 String::from 函数就是一个"关联函数"。

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}
impl Rectangle {
    fn create(width: u32, height: u32) -> Rectangle {
        Rectangle { width, height }
    }
}
fn main() {
    let rect = Rectangle::create(30, 50);
    println!("{:?}", rect);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

运行结果:
Rectangle { width: 30, height: 50 }

贴士:结构体 impl 块可以写几次,效果相当于它们内容的拼接!

18. 单元结构体

结构体可以只作为一种象征而无需任何成员:

struct UnitStruct;
我们称这种没有身体的结构体为单元结构体(Unit Struct)。

19. 枚举类

1. match语法实例

fn main() {
    enum Book {
        Papery {index: u32},
        Electronic {url: String},
    }
    let book = Book::Papery{index: 1001};
    let ebook = Book::Electronic{url: String::from("url...")};
    match book {
        Book::Papery { index } => {
            println!("Papery book {}", index);
        },
        Book::Electronic { url } => {
            println!("E-book {}", url);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

运行结果:
Papery book 1001

match 块也可以当作函数表达式来对待,它也是可以有返回值的:

match 枚举类实例 {
    分类1 => 返回值表达式,
    分类2 => 返回值表达式,
    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5

match 除了能够对枚举类进行分支选择以外,还可以对整数、浮点数、字符和字符串切片引用(&str)类型的数据进行分支选择。其中,浮点数类型被分支选择虽然合法,但不推荐这样使用,因为精度问题可能会导致分支错误。

对非枚举类进行分支选择时必须注意处理例外情况,即使在例外情况下没有任何要做的事,例外情况用下划线 _ 表示:

实例

fn main() {
    let t = "abc";
    match t {
        "abc" => println!("Yes"),
        _ => {},
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

2. Option 枚举类

Option 是 Rust 标准库中的枚举类,这个类用于填补 Rust 不支持 null 引用的空白。

Rust 在语言层面彻底不允许空值 null 的存在,但无奈null 可以高效地解决少量的问题,所以 Rust 引入了 Option 枚举类:

enum Option<T> {
    Some(T),
    None,
}
  • 1
  • 2
  • 3
  • 4

如果你想定义一个可以为空值的类,你可以这样:

let opt = Option::Some("Hello");
  • 1

如果你想针对 opt 执行某些操作,你必须先判断它是否是 Option::None

实例

fn main() {
    let opt = Option::Some("Hello");
    match opt {
        Option::Some(something) => {
            println!("{}", something);
        },
        Option::None => {
            println!("opt is nothing");
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

运行结果:
Hello

如果你的变量刚开始是空值,你体谅一下编译器,它怎么知道值不为空的时候变量是什么类型的呢?

所以初始值为空的 Option 必须明确类型:

实例

fn main() {
    let opt: Option<&str> = Option::None;
    match opt {
        Option::Some(something) => {
            println!("{}", something);
        },
        Option::None => {
            println!("opt is nothing");
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

运行结果:
opt is nothing

这种设计会让空值编程变得不容易,但这正是构建一个稳定高效的系统所需要的。由于 Option 是 Rust 编译器默认引入的,在使用时可以省略 Option:: 直接写 None 或者 Some()

Option 是一种特殊的枚举类,它可以含值分支选择:

实例

fn main() {
    let t = Some(64);
    match t {
        Some(64) => println!("Yes"),
        _ => println!("No"),
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

20. if let 语法实例

let i = 0;
match i {
    0 => println!("zero"),
    _ => {},
}
  • 1
  • 2
  • 3
  • 4
  • 5

放入主函数运行结果:
zero

这段程序的目的是判断 i 是否是数字 0,如果是就打印 zero。

现在用 if let 语法缩短这段代码:

let i = 0;
if let 0 = i {
    println!("zero");
}
  • 1
  • 2
  • 3
  • 4

if let 语法格式如下:

if let 匹配值 = 源变量 {
    语句块
}
  • 1
  • 2
  • 3

可以在之后添加一个 else 块来处理例外情况。

if let 语法可以认为是只区分两种情况的 match 语句的"语法糖"(语法糖指的是某种语法的原理相同的便捷替代品)。

对于枚举类依然适用:

实例

fn main() {
    enum Book {
        Papery(u32),
        Electronic(String)
    }
    let book = Book::Electronic(String::from("url"));
    if let Book::Papery(index) = book {
        println!("Papery {}", index);
    } else {
        println!("Not papery book");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

21. Rust 组织管理

Rust 中有三个重要的组织概念:模块

1. 箱(Crate)

"箱"是二进制程序文件或者库文件,存在于"包"中。

"箱"是树状结构的,它的树根是编译器开始运行时编译的源文件所编译的程序。

注意:“二进制程序文件"不一定是"二进制可执行文件”,只能确定是是包含目标机器语言的文件,文件格式随编译环境的不同而不同。

2. 包(Package)

当我们使用 Cargo 执行 new 命令创建 Rust 工程时,工程目录下会建立一个 Cargo.toml 文件。工程的实质就是一个包,包必须由一个 Cargo.toml 文件来管理,该文件描述了包的基本信息以及依赖项。

一个包最多包含一个库"箱",可以包含任意数量的二进制"箱",但是至少包含一个"箱"(不管是库还是二进制"箱")。

当使用 cargo new 命令创建完包之后,src 目录下会生成一个 main.rs 源文件,Cargo 默认这个文件为二进制箱的根,编译之后的二进制箱将与包名相同。

3. 模块(Module)

对于一个软件工程来说,我们往往按照所使用的编程语言的组织规范来进行组织,组织模块的主要结构往往是树。Java 组织功能模块的主要单位是类,而 JavaScript 组织模块的主要方式是 function。

这些先进的语言的组织单位可以层层包含,就像文件系统的目录结构一样。Rust 中的组织单位是模块(Module)。

22. 访问权限

Rust 中有两种简单的访问权:公共(public)和私有(private)。

默认情况下,如果不加修饰符,模块中的成员访问权将是私有的。

如果想使用公共权限,需要使用 pub 关键字。

对于私有的模块,只有在与其平级的位置或下级的位置才能访问,不能从其外部访问。

use 关键字

use 关键字能够将模块标识符引入当前作用域:

实例

mod nation {
    pub mod government {
        pub fn govern() {}
    }
}

use crate::nation::government::govern;
fn main() {
    govern();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

这段程序能够通过编译。

因为 use 关键字把 govern 标识符导入到了当前的模块下,可以直接使用。

这样就解决了局部模块路径过长的问题。

当然,有些情况下存在两个相同的名称,且同样需要导入,我们可以使用 as 关键字为标识符添加别名:

实例

mod nation {
    pub mod government {
        pub fn govern() {}
    }
    pub fn govern() {}
} 

use crate::nation::government::govern;
use crate::nation::govern as nation_govern;
fn main() {
    nation_govern();
    govern();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

这里有两个 govern 函数,一个是 nation 下的,一个是 government 下的,我们用 as 将 nation 下的取别名 nation_govern。两个名称可以同时使用。

use 关键字可以与 pub 关键字配合使用:

实例

mod nation {
    pub mod government {
        pub fn govern() {}
    }
    pub use government::govern;
}

fn main() {
    nation::govern();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

2. 引用标准库

Rust 官方标准库字典:https://doc.rust-lang.org/stable/std/all.html

在学习了组织管理概念之后,我们可以轻松的导入系统库来方便的开发程序了:

实例

use std::f64::consts::PI;
fn main() {
    println!("{}", (PI / 2.0).sin());
}
  • 1
  • 2
  • 3
  • 4

运行结果:
1

所有的系统库模块都是被默认导入的,所以在使用的时候只需要使用 use 关键字简化路径就可以方便的使用了。

23. 格式化输出

打印操作由 std::fmt 里面所定义的一系列宏来处理,包括:

  • format!:将格式化文本写到字符串。
  • print!:与 format! 类似,但将文本输出到控制台(io::stdout)。
  • println!: 与 print! 类似,但输出结果追加一个换行符。
  • eprint!:与 print! 类似,但将文本输出到标准错误(io::stderr)。
  • eprintln!:与 eprint! 类似,但输出结果追加一个换行符。

24. 特性

特性(trait)概念接近于 Java 中的接口(Interface),但两者不完全相同。特性与接口相同的地方在于它们都是一种行为规范,可以用于标识哪些类有哪些方法。

特性在 Rust 中用 trait 表示:

trait Descriptive {
    fn describe(&self) -> String;
}
  • 1
  • 2
  • 3

Descriptive 规定了实现者必需有 describe(&self) -> String 方法。

我们用它实现一个结构体:

实例

struct Person {
    name: String,
    age: u8
}
impl Descriptive for Person {
    fn describe(&self) -> String {
        format!("{} {}", self.name, self.age)
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

格式是:

impl <特性名> for <所实现的类型名>

Rust 同一个类可以实现多个特性,每个 impl 块只能实现一个。

1. 默认特性

这是特性与接口的不同点:接口只能规范方法而不能定义方法,但特性可以定义方法作为默认方法,因为是"默认",所以对象既可以重新定义方法,也可以不重新定义方法使用默认的方法:
实例

trait Descriptive {
    fn describe(&self) -> String {
        String::from("[Object]")
    }
}
struct Person {
    name: String,
    age: u8
}
impl Descriptive for Person {
    fn describe(&self) -> String {
        format!("{} {}", self.name, self.age)
    }
}
fn main() {
    let cali = Person {
        name: String::from("Cali"),
        age: 24
    };
    println!("{}", cali.describe());
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

运行结果:
Cali 24

如果我们将 impl Descriptive for Person 块中的内容去掉,那么运行结果就是:
[Object]

2. 特性做参数

很多情况下我们需要传递一个函数做参数,例如回调函数、设置按钮事件等。在 Java 中函数必须以接口实现的类实例来传递,在 Rust 中可以通过传递特性参数来实现:

fn output(object: impl Descriptive) {
    println!("{}", object.describe());
}
  • 1
  • 2
  • 3

任何实现了 Descriptive 特性的对象都可以作为这个函数的参数,这个函数没必要了解传入对象有没有其他属性或方法,只需要了解它一定有 Descriptive 特性规范的方法就可以了。当然,此函数内也无法使用其他的属性与方法。

特性参数还可以用这种等效语法实现:

fn output<T: Descriptive>(object: T) {
    println!("{}", object.describe());
}
  • 1
  • 2
  • 3

这是一种风格类似泛型的语法糖,这种语法糖在有多个参数类型均是特性的情况下十分实用:

fn output_two<T: Descriptive>(arg1: T, arg2: T) {
    println!("{}", arg1.describe());
    println!("{}", arg2.describe());
}
  • 1
  • 2
  • 3
  • 4

特性作类型表示时如果涉及多个特性,可以用 + 符号表示,例如:

fn notify(item: impl Summary + Display)
fn notify<T: Summary + Display>(item: T)
  • 1
  • 2

注意:仅用于表示类型的时候,并不意味着可以在 impl 块中使用。

复杂的实现关系可以使用 where `关键字简化,例如:

fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U)
  • 1

可以简化成:

fn some_function<T, U>(t: T, u: U) -> i32
    where T: Display + Clone,
          U: Clone + Debug
  • 1
  • 2
  • 3

在了解这个语法之后,泛型章节中的"取最大值"案例就可以真正实现了:
实例

trait Comparable {
    fn compare(&self, object: &Self) -> i8;
}
fn max<T: Comparable>(array: &[T]) -> &T {
    let mut max_index = 0;
    let mut i = 1;
    while i < array.len() {
        if array[i].compare(&array[max_index]) > 0 {
            max_index = i;
        }
        i += 1;
    }
    &array[max_index]
}
impl Comparable for f64 {
    fn compare(&self, object: &f64) -> i8 {
        if &self > &object { 1 }
        else if &self == &object { 0 }
        else { -1 }
    }
}
fn main() {
    let arr = [1.0, 3.0, 5.0, 4.0, 2.0];
    println!("maximum of arr is {}", max(&arr));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

运行结果:
maximum of arr is 5

Tip: 由于需要声明 compare 函数的第二参数必须与实现该特性的类型相同,所以 Self (注意大小写)关键字就代表了当前类型(不是实例)本身。

3. 特性做返回值

特性做返回值格式如下:

实例

fn person() -> impl Descriptive {
    Person {
        name: String::from("Cali"),
        age: 24
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

但是有一点,特性做返回值只接受实现了该特性的对象做返回值且在同一个函数中所有可能的返回值类型必须完全一样。比如结构体 A 与结构体 B 都实现了特性 Trait,下面这个函数就是错误的:

实例

fn some_function(bool bl) -> impl Descriptive {
    if bl {
        return A {};
    } else {
        return B {};
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

4. 有条件实现方法

impl 功能十分强大,我们可以用它实现类的方法。但对于泛型类来说,有时我们需要区分一下它所属的泛型已经实现的方法来决定它接下来该实现的方法:

struct A<T> {}
impl<T: B + C> A<T> {
    fn d(&self) {}
}
  • 1
  • 2
  • 3
  • 4

这段代码声明了 A 类型必须在 T 已经实现 B 和 C 特性的前提下才能有效实现此 impl 块。

25. 优雅地错误处理

Result 如此常见以至于 Rust 有个强大的操作符 ? 来与之配合。 以下两个表达式是等价的:

do_something_that_might_fail()?

match do_something_that_might_fail() {
    Ok(v) => v,
    Err(e) => return Err(e),
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

26. 丑陋的 Option/Result 处理

当你只是试图快速地写一些代码时,Option/Result 对付起来可能比较无聊。 Option 和 Result 都有一个名为 unwrap 的函数:这个函数可以简单粗暴地获取其中的值。

unwrap 会:获取 Option/Result 内部的值

如果枚举的类型是 None/Err, 则会 panic!

这两段代码是等价的:

my_option.unwrap()

match my_option {
    Some(v) => v,
    None => panic!("some error message generated by Rust!"),
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

类似的:

my_result.unwrap()

match my_result {
    Ok(v) => v,
    Err(e) => panic!("some error message generated by Rust!"),
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

不过啊,做个好 Rustacean,正确地使用 match!

27. Vectors

一些经常使用的泛型是集合类型。一个 vector 是可变长度的元素集合,以 Vec 结构表示。

比起手动构建,宏 vec! 让我们可以轻松地创建 vector。

Vec 有一个形如 iter() 的方法可以为一个 vector 创建迭代器,这允许我们可以轻松地将 vector 用到 for 循环中去。

内存细节:
Vec 是一个结构体,但是内部其实保存了在堆上固定长度数据的引用。

一个 vector 开始有默认大小容量,当更多的元素被添加进来后,它会重新在堆上分配一个新的并具有更大容量的定长列表。(类似 C++ 的 vector)

实例

fn main() {
    // 我们可以显式确定类型
    let mut i32_vec = Vec::<i32>::new(); // turbofish <3
    i32_vec.push(1);
    i32_vec.push(2);
    i32_vec.push(3);

    // 但是看看 Rust 是多么聪明的自动检测类型啊
    let mut float_vec = Vec::new();
    float_vec.push(1.3);
    float_vec.push(2.3);
    float_vec.push(3.4);

    // 这是个漂亮的宏!
    let string_vec = vec![String::from("Hello"), String::from("World")];

    for word in string_vec.iter() {
        println!("{}", word);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

28. 解引用

使用 &mut 引用时, 你可以通过 * 操作符来修改其指向的值。 你也可以使用 * 操作符来对所拥有的值进行拷贝(前提是该值可以被拷贝)。
实例

fn main() {
    let mut foo = 42;
    let f = &mut foo;
    let bar = *f; // 取得所有者值的拷贝
    *f = 13;      // 设置引用所有者的值
    println!("{}", bar);
    println!("{}", foo);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

运行结果:
42
13

29. 生命周期

1. 显式生命周期

尽管 Rust 不总是在代码中将它展示出来,但编译器会理解每一个变量的生命周期并进行验证以确保一个引用不会有长于其所有者的存在时间。

同时,函数可以通过使用一些符号来参数化函数签名,以帮助界定哪些参数和返回值共享同一生命周期。 生命周期注解总是以 ’ 开头,例如 'a,'b 以及 'c。

2. 多个生命周期

生命周期注解可以通过区分函数签名中不同部分的生命周期,来允许我们显式地明确某些编译器靠自己无法解决的场景。

3. 静态生命周期

一个静态变量是一个在编译期间即被创建并存在于整个程序始末的内存资源。他们必须被明确指定类型。

一个静态生命周期是指一段内存资源无限期地延续到程序结束。需要注意的一点是,在此定义之下,一些静态生命周期的资源也可以在运行时被创建。

拥有静态生命周期的资源会拥有一个特殊的生命周期注解 'static。 'static 资源永远也不会被 drop 释放。

如果静态生命周期资源包含了引用,那么这些引用的生命周期也一定是 'static 的。(任何缺少了此注解的引用都不会达到同样长的存活时间)

内存细节:
因为静态变量可以全局性地被任何人访问读取而潜在地引入数据争用,所以修改它具有内在的危险性。

Rust 允许使用 unsafe { ... } 代码块来进行一些无法被编译器担保的内存操作。The R̸͉̟͈͔̄͛̾̇͜U̶͓͖͋̅Ṡ̴͉͇̃̉̀T̵̻̻͔̟͉́͆Ơ̷̥̟̳̓͝N̶̨̼̹̲͛Ö̵̝͉̖̏̾̔M̶̡̠̺̠̐͜Î̷̛͓̣̃̐̏C̸̥̤̭̏͛̎͜O̶̧͚͖͔̊͗̇͠N̸͇̰̏̏̽̃(常见的中文翻译为:Rust 死灵书)在讨论时应该被严肃地看待。

实例

static PI: f64 = 3.1415;

fn main() {
    // 静态变量的范围也可以被限制在一个函数内
    static mut SECRET: &'static str = "swordfish";

    // 字符串字面值拥有 'static 生命周期
    let msg: &'static str = "Hello World!";
    let p: &'static f64 = &PI;
    println!("{} {}", msg, p);

    // 你可以打破一些规则,但是必须是显式地
    unsafe {
        // 我们可以修改 SECRET 到一个字符串字面值因为其同样是 'static 的
        SECRET = "abracadabra";
        println!("{}", SECRET);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

运行结果:
Hello World! 3.1415
abracadabra

30. 数据类型中的生命周期

和函数相同,数据类型也可以用生命周期注解来参数化其成员。 Rust 会验证引用所包含的数据结构永远也不会比引用指向的所有者存活周期更长。 我们不能在运行中拥有一个包括指向虚无的引用结构存在!

实例

struct Foo<'a> {
    i:&'a i32
}

fn main() {
    let x = 42;
    let foo = Foo {
        i: &x
    };
    println!("{}",foo.i);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

运行结果:
42

31. 原始字符串常量

原始字符串支持写入原始的文本而无需为特殊字符转义,因而不会导致可读性下降(如双引号与反斜杠无需写为 " 和 \),只需以 r#" 开头,以 "# 结尾。
实例

fn main() {
    let a: &'static str = r#"
        <div class="advice">
            原始字符串在一些情景下非常有用。
        </div>
        "#;
    println!("{}", a);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

32. 文件中的字符串常量

如果你需要使用大量文本,可以尝试用宏 include_str! 来从本地文件中导入文本到程序中:

let hello_html = include_str!("hello.html");
  • 1

33. 重温字符串片段(String Slice)

字符串片段是对内存中字节序列的引用,而且这段字节序列必须是合法的 utf-8 字节序列。

str 片段的字符串片段(子片段),也必须是合法的 utf-8 字节序列。

&str 的常用方法:

  • len 获取字符串常量的字节长度(不是字符长度)。
  • starts_with/ends_with 用于基础测试。
  • is_empty 长度为 0 时返回 true。
  • find 返回 Option,其中的 usize 为匹配到的第一个对应文本的索引值。
    实例
fn main() {
    let a = "你好 
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/205253
推荐阅读