Rust-程序设计语言

Hello World!

一般将函数体的左花括号与函数声明置于同一行并以空格分隔.

可以用 rustfmt 格式化代码.

Rust 的缩进风格是 4 个空格, 而不是一个 Tab.

!, 表明调用的是一个宏, 而不是一个函数, 如 println!.

编译

1
rustc main.rs

Hello Cargo

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

创建项目

1
cargo new hello_cargo

创建的项目的目录结构为:

1
2
3
4
5
.
└── hello_cargo
├── Cargo.toml
└── src
└── main.rs

若没有在 Git 仓库中, 其还会初始化一个 git 仓库以及一个 .gitignore 文件.

Cargo.toml 文件是 Cargo 的配置文件, 其内容如:

1
2
3
4
5
6
[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2021"

[dependencies]

构建

1
cargo build

可执行文件会生成在 target/debug/ 目录下.

其会在项目根目录创建 Cargo.lock 文件, 其会记录项目实际使用的以来以及其版本.

构建并运行

1
cargo run

检查代码但不运行

1
cargo check

项目发布

1
cargo build --release

其会开启优化编译, 并在 target/release 下生成可执行文件.

编写 “猜猜看” 游戏

导入包

导入标准 io 库:

1
use std::io;

默认情况下. Rust 会自动引入部分标准库中的模块.

函数声明

1
2
3
fn main() {
...
}

创建变量

1
let mut guess = String::new();

Rust 中, 变量默认不可变, 需加上 mut (mutable) 才可变.

这里将 guess 绑定到 String::new() 的返回值 (UTF-8 编码的可增长文本块).

调用方法

. 号:

1
2
io::stdin().read_line(&mut guess)
.expect("Failed to read line");

引用

&

Result 类型

这里 read_line 的返回值是一个 Result 类型, 其本质是 “枚举”, 即 enums.

Result 类型的成员有:

  • Ok
  • Err

crate

Rust 称外部的模块为 crate.

引入一个模块, 需要先修改 Cargo.toml 文件:

1
2
3
[dependencies]

rand = "0.8.3"

然后在 cargo build 时, 会自动拉取.

更新 crate

1
cargo update

其会忽略 Cargo.lock 文件. 并将新的版本信息写入其中.

cmp 方法

任何可以用于比较的值, 都可以调用 cmp 方法, 其获取一个用于比较的值的引用:

1
guess.cmp(&another_number);

其比较 guessanother_number. 其会返回 Ordering 类型的成员.

match 表达式

由:

  • 分支 (arms)
  • 模式 (pattern)

组成.

传递给 match 的值与模式向匹配, 则运行对应的代码:

1
2
3
4
5
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}

Rust 有一个静态强类型系统, 同时也有类型推断.

变量声明时指定类型

1
let guess: u32 = 10;

trim()parse() 方法

字符串的 trim() 方法用于去除字符串开头和结尾的空白字符.

parse() 方法用于将字符串解析为数字.

循环

loop 关键字:

1
2
3
4
5
6
7
8
9
10
11
12
loop {
println!("Please input your guess.");
// --snip--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!"),
break;
}
}
}

同样可以用 breakcontinue 处理.

常量

和普通不可变的变量类似, 但其值在编译期确定.

不能对常量使用 mut.

声明为:

1
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

命名约定为: 单词之间使用全大写加下划线.

隐藏 (Shadowing)

发生于重复声明时, 如:

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let x = 5;

let x = x + 1;

{
let x = x * 2;
println!("The value of x in the inner scope is: {}", x);
}

println!("The value of x is: {}", x);
}

数据类型

两类数据类型子集:

  • 标量, scalar
  • 复合, compound

Rust 编译器一般能推断出变量的类型, 但最好是加上类型标注:

1
let x: u32 = 10;

字符类型

Rust 中用:

  • 单引号声明 char (Unicode 字符, 4 个字节)
  • 双引号声明字符串字面量

复合类型

  • 元组 (tuple), 元素可类型不同
  • 数组 (array), 元素类型相同

元组

元组声明如:

1
2
3
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}

可用 . 加上索引来访问:

1
2
3
4
5
6
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
let first = tup.0;
let second = tup.1;
let third = tup.2;
}

没有值的元组 () 是一种特殊的类型 – 单元类型(unit type), 其值被称为单元值 (unit value).

数组

数组声明如:

1
2
3
fn main() {
let a = [1, 2, 3, 4, 5];
}

其长度固定. (vector 可变)

声明中包含类型和长度:

1
let a: [i32; 5] = [1, 2, 3, 4, 5];

包含初始值和长度:

1
let a: [3; 5];

访问如:

1
a[0]

函数

main 函数是 Rust 程序的入口点.

Rust 中的函数和变量名都遵循 snake case 规范, 即所有字母都是小写且用下划线分隔.

传递参数:

1
2
3
fn test_paras(num: i32) {
println!("The value of the num is {}", num);
}

语句和表达式

Rust 是基于表达式的语言.

区分语句和表达式:

  • 语句, 执行操作但不返回
  • 表达式, 计算并产生一个值

常见的, 函数调用是表达式, 宏调用也是表达式. 大括号创建的新的块作用域也是表达式.

如:

1
2
3
4
5
6
7
8
fn main() {
let x = 5;

let y = {
let x = 3;
x + 1
};
}

注意这里 x+1 之后没有分号 ;, 加上则变成语句了.

函数返回

-> 指定返回值的类型.

函数返回值为函数体中最后一个表达式的值, 也可以用 return 提前返回.

如:

1
2
3
fn testhh -> i32 {
5
}

控制流

if 表达式

如:

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let number = 3;

if number < 5 {
println!("condition was true");
} else if number == 6 {
println!("Hello")
} else {
println!("condition was false");

}
}

注意这里可以不加括号. 且条件结果必须为 bool 类型.

let 语句中使用 if

由于 if 是表达式, 因此可以有:

1
2
3
4
5
6
7
8
9
10
fn main() {
let condition = true;
let number = if condition {
5
} else {
6
};

println!("The value of number is: {}", number);
}

但需要注意, 每个分支的返回值需要为同种类型.

循环

loop 循环

可以用 break 关键字返回值:

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let mut counter = 0;

let result = loop {
counter += 1;

if counter == 10 {
break counter * 2;
}
};
println!("The result is {}", result);
}

while 循环

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let mut number = 3;

while number != 0 {
println!("{}!", number);

number = number - 1;
}

println!("LIFTOFF!!!");
}

for 循环

1
2
3
4
5
6
7
fn main() {
let a = [10, 20, 30, 40, 50];

for element in a.iter() {
println!("the value is: {}", element);
}
}

所有权 (ownership)

所有权的存在让 Rust 无需垃圾回收即可保障内存安全.

Rust 的编译器在编译时会根据一系列的规则进行检查, 在运行中, 所有权系统的任何功能都不会减慢程序.

什么是所有权

栈 (stack) 与堆 (Heap)

栈中的所有数据都必须占用已知且固定的大小, 大小未知或者大小可能变化的数据, 要改为存储在堆上.

在堆上分配内存的过程为: 请求一定大小的空间, 内存分配器在堆的某处找到一块足够大的空位, 将其标记为已使用, 并返回一个表示该位置地址的指针.

入栈比在堆上分配内存要快, 因为入栈时分配器无需为存储新数据去搜索内存空间, 其位置总是在栈顶.

访问堆上的数据比访问栈上的数据慢.

而当代码调用函数时, 传递给函数的值和函数的局部变量都是被压入栈中, 当函数结束时, 这些值被移出栈.

所有权的存在就是为了管理堆数据:

  • 追踪哪部分代码正在使用堆上的哪些数据
  • 减少堆上的重复数据的数量
  • 清理堆上不再使用的数据

所有权规则

  1. Rust 中的每一个值都有一个被称为其 所有者 (owner) 的变量
  2. 值在任意时刻都有且只有一个所有者
  3. 当所有者离开作用域时, 这个值将被丢弃

String 类型

字符串字面值的一个不方便之处在于不可变.

String 是 Rust 的第二个字符串类型, 其可以管理被分配到堆上的数据.

可以用 from 函数基于字符串字面值创建 String:

1
let mut s = String::from("hello");

此时可以修改 s 字符串:

1
2
3
s.push_str(", world");

println!("{}", s);

内存与分配

字符串字面值, 由于在编译时就知道其内容, 所以其会被直接硬编码进最终的可执行文件.

String 类型用 String::from 请求内存. 而在拥有这个内存的变量离开作用域时自动释放. (Rust 会自动调用 drop 函数)

移动

对于 Rust 而言, 以下代码的行为为:

1
2
let s1 = String::From("hello");
let s2 = s1;

s1 会被释放.

这种操作被称为 移动

克隆

1
2
let s1 = String::from("hello");
let s2 = s1.clone();

这里的 clone() 是一个通用函数.

此时数据被复制, s1 不会被释放.

拷贝

1
2
let x = 5;
let y = x;

此时 x 不会被释放.

原因为, 像整型这样的在编译时一直大小的类型被存储在栈上, 所以其拷贝是很快速的. 因此设计为可以直接拷贝.

究其根本在于: Rust 有一个叫做 Copy trait 的特殊注解, 可以用在类似整型这样的存储在栈上的类型上.

如果一个类型实现了 Copy trait, 那么一个旧的变量在将其赋值给其他变量后仍然可用.

Rust 不允许自身或其任何部分实现了 Drop trait 的类型使用 Copy trait.

一个通用的规则:

  • 任何一组简单标量值的组合都可以实现 Copy
  • 任何不需要分配内存或某种形式资源的类型都可以实现 Copy

所有权与函数

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
let s = String::from("hello"); // s 进入作用域

takes_ownership(s); // s 的值移动到函数里 ...

// ... 所以到这里不再有效
let x = 5; // x 进入作用域
makes_copy(x); // x 应该移动函数里,
// 但 i32 是 Copy 的,所以在后面可继续使用 x
}
// 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
// 所以不会有特殊操作

fn takes_ownership(some_string: String) { // some_string 进入作用域
println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放

fn makes_copy(some_integer: i32) { // some_integer 进入作用域
println!("{}", some_integer);
} // 这里,some_integer 移出作用域。不会有特殊操作

也就是说: 所有权在传递给函数时会转移.

返回值与所有权

返回值也可以转移所有权:

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
fn main() {
let s1 = gives_ownership(); // gives_ownership 将返回值
// 移给 s1
let s2 = String::from("hello"); // s2 进入作用域

let s3 = takes_and_gives_back(s2);
// s2 被移动到
// takes_and_gives_back 中,
// 它也将返回值移给 s3
}

// 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
// 所以什么也不会发生。s1 移出作用域并被丢弃

fn gives_ownership() -> String {
// gives_ownership 将返回值移动给
// 调用它的函数
let some_string = String::from("hello"); // some_string 进入作用域.
some_string
// 返回 some_string 并移出给调用的函数
}
// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
a_string
// 返回 a_string 并移出给调用的函数
}

引用与借用

在使用引用时, 允许使用值但不获取其所有权:

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let s1 = String::from("hello");

let len = calculate_length(&s1);

println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
s.len()
}

& 用于取引用. * 用于解引用.

Rust 将创建一个引用的行为称为 借用.

可变引用

1
2
3
4
5
6
7
8
9
fn main() {
let mut s = String::from("hello");

change(&mut s);
}

fn change(some_string: &mut String) {
some_string.push_str(", world");
}

一个限制, 同一时间只能有一个对某一特定数据的可变引用:

1
2
3
4
5
6
let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s;

println!("{}, {}", r1, r2);

会报错.

可以:

1
2
3
4
5
6
7
let mut s = String::from("hello");

{
let r1 = &mut s;
} // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用

let r2 = &mut s;

Slice 类型

slice 类型没有所有权. 其为 String 中一部分值的引用.

如:

1
2
3
4
5
6
7
let s = String::from("Hello World");

let hello = &s[0..5];
let world = &s[6..11];

let hello2 = &s[..5];
let world2 = &s[6..];

字符串字面值就是 slice

1
let s = "Hello World";

这里 s 的类型是 &str, 是一个不可变引用.

其他类型的 slice

引用数组的一部分:

1
2
3
let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

使用结构体组织相关联的数据

定义并实例化结构体

结构体和元组类似, 每一部分都可以是不同类型. 但结构体需要命名各部分数据以便清楚的表明其值的意义.

如:

1
2
3
4
5
6
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}

每一部分称为 字段 (field).

创建实例:

1
2
3
4
5
6
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
}

获取值:

1
println!("{}", user1.email);

注意 Rust 不允许仅仅是 struct 中的某一个字段可变, 而是全可变.

简写写法

如:

1
2
3
4
5
6
7
8
fn build_user(email: String, username: String) -> User {
User {
email: email,
username:username,
active: true,
sign_in_count: 1,
}
}

可以简写为:

1
2
3
4
5
6
7
8
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}

(需要变量名和字段名相同才行).

结构体更新语法

用旧实例中的大部分值但改变部分值来创建一个新的结构体实例.

1
2
3
4
5
6
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};

等价于:

1
2
3
4
let user2 = User {
email: String::from("another@example.com"),
..user1
};

注意这里移动了数据, 导致 user1 无效了.

元组结构体

没有具体的字段名, 只有字段的类型:

1
2
3
4
5
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

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

类单元结构体

没有任何字段:

1
2
3
struct AlwaysEqual;

let subject = AlwaysEqual;

通过派生 trait 增加实用功能

为结构体添加 Debug trait, 可以用于打印调试信息:

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

println!("rect1 is {:?}", rect1);
}

{:?} 的含义为使用 Debug 的输出格式. 也可以用 {:#?}.

也可以用 dbg! 宏, 其会输出文件和行号, 以及表达式的结果, 并返回该值的所有权. 其会打印至 stderr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};

dbg!(&rect1);
}

方法语法

方法和函数不同, 其定义在结构体上下文中.

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

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

fn main() {
let rect1 = Rectangle { width: 30, height: 50 };

println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
)
}

impl (implementation) 块, 表明其中所有内容都将与 Rectangle 类型相关联. self 指调用者本身.

这里的 &selfself: &Self 的缩写.

impl 块中, Self 类型就是其绑定类型的别名.

方法的第一个参数必须有一个名为 selfSelf 类型的参数.

若需要改动调用者, 则传入 &mut self

可以定义一个与结构中字段名称相同的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
impl Rectangle {
fn width(&self) -> bool {
self.width > 0
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};

if rect1.width() {
println!("The rectangle has a nonzero width; it is {}", rect1.width);
}
}

自动引用和解引用

如使用 object.something() 调用方法时, Rust 会自动为 object 添加 &, &mut* 以便使 object 与方法签名匹配.

以下代码等价:

1
2
p1.distance(&p2);
(&p1).distance(&p2);

关联函数

所有在 impl 块中定义的函数都被称为 关联函数 (associated function).

可以定义不以 self 为第一参数的关联函数 (此时就不是方法).

如用作一个构造函数:

1
2
3
4
5
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle { width: size, height: size }
}
}

使用结构体名以及 :: 即可调用关联函数. 如 let sq = Rectangle::square(3);
(这个函数位于 Rectangle 的命名空间中)

多个 impl

每个结构体都允许拥有多个 impl 块:

1
2
3
4
5
6
7
8
9
10
11
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}

impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}

枚举和模式匹配


Rust-程序设计语言
http://example.com/2023/10/17/Rust-程序设计语言/
作者
Jie
发布于
2023年10月17日
许可协议