Rust-Learning-1

Rust学习篇,介绍Rust的基础知识。

基本概念

变量

变量默认是不可改变的(immutable)

1
let x = 5;

可变:

1
let mut x = 5;

不允许对常量使用 mut。常量不光默认不能变,它总是不能变。声明常量使用 const 关键字而不是 let,并且 必须 注明值的类型。

1
const MAX_POINTS: u32 = 100_000;

新变量会 隐藏 之前的变量

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

变量类型

标量类型

标量scalar)类型代表一个单独的值。Rust 有四种基本的标量类型:整型、浮点型、布尔类型和字符类型。你可能在其他语言中见过它们。让我们深入了解它们在 Rust 中是如何工作的。

整型

整数 是一个没有小数部分的数字。我们在第二章使用过 u32 整数类型。该类型声明表明,它关联的值应该是一个占据 32 比特位的无符号整数(有符号整数类型以 i 开头而不是 u)。

Rust 中的整型

长度 有符号 无符号
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
arch isize usize

Rust 中的整型字面值

数字字面值 例子
Decimal 98_222
Hex 0xff
Octal 0o77
Binary 0b1111_0000
Byte (u8 only) b'A'

浮点类型

Rust 也有两个原生的 浮点数floating-point numbers)类型,它们是带小数点的数字。Rust 的浮点数类型是 f32f64。默认类型是 f64

bool类型

正如其他大部分编程语言一样,Rust 中的布尔类型有两个可能的值:truefalse。Rust 中的布尔类型使用 bool 表示。

字符类型

目前为止只使用到了数字,不过 Rust 也支持字母。Rust 的 char 类型是语言中最原生的字母类型,如下代码展示了如何使用它。(注意 char 由单引号指定,不同于字符串使用双引号。)

1
2
3
4
5
fn main() {
let c = 'z';
let z = 'ℤ';
let heart_eyed_cat = '😻';
}

复合类型

元组

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

tup 变量绑定到整个元组上,因为元组是一个单独的复合元素。为了从元组中获取单个值,可以使用模式匹配(pattern matching)来解构(destructure)元组值,像这样:

1
2
3
4
5
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {}", y);
}

访问元组元素使用.

1
2
3
4
5
6
7
8
9
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);

let five_hundred = x.0;

let six_point_four = x.1;

let one = x.2;
}

数组

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

与元组不同,数组中的每个元素的类型必须相同。Rust 中的数组与一些其他语言中的数组不同,因为 Rust 中的数组是固定长度的:一旦声明,它们的长度不能增长或缩小。

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

[type:number]用于表示数组的类型

访问方式同C,用中括号

String 类型

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

可以 修改此类字符串 :

1
2
3
4
5
6

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

s.push_str(", world!"); // push_str() 在字符串后追加字面值

println!("{}", s); // 将打印 `hello, world!`

函数

fn 关键字,它用来声明新函数

例子:

1
2
3
4
5
6
7
fn main() {
another_function(5);
}

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

语句和表达式

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

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

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

分号视作是语句的结束,如果没有分号结尾,我们默认认为最后的语句用于作为返回值。

函数如果有返回值的话我们需要使用->用于表示返回值类型。

1
2
3
4
5
6
7
8
9
fn five() -> i32 {
5
}

fn main() {
let x = five();

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

控制流

分支

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

if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
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

1
2
3
4
5
fn main() {
loop {
println!("again!");
}
}

loop用于执行死循环,可以用break跳出循环

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

let result = loop {
counter += 1;

if counter == 10 {
break counter * 2;
}
};

assert_eq!(result, 20);
}

break的用法不同与我们的C,有返回值

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
fn main() {
let a = [10, 20, 30, 40, 50];

for element in a.iter() {
println!("the value is: {}", element);
}
1
2
3
4
5
6
fn main() {
for number in (1..4).rev() {
println!("{}!", number);
}
println!("LIFTOFF!!!");
}

所有权

作为Rust独有的新概念

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

作用域scope

这个变量从声明的点开始直到当前 作用域 结束时都是有效的。

1
2
3
4
5
{                      // s 在这里无效, 它尚未声明
let s = "hello"; // 从此处起,s 是有效的

// 使用 s
} // 此作用域已结束,s 不再有效

变量与数据交互

移动

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

此时s1失效,不再有用

我们称作s1被移到s2中

克隆

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

println!("s1 = {}, s2 = {}", s1, s2);

此时s1和s2都是有效的

拷贝

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

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

没有调用 clone,不过 x 依然有效且没有被移动到 y

Rust 有一个叫做 Copy trait 的特殊注解,可以用在类似整型这样的存储在栈上的类型上(第十章详细讲解 trait)。如果一个类型拥有 Copy trait,一个旧的变量在将其赋值给其他变量后仍然可用。

如下是一些 Copy 的类型:

  • 所有整数类型,比如 u32
  • 布尔类型,bool,它的值是 truefalse
  • 所有浮点数类型,比如 f64
  • 字符类型,char
  • 元组,当且仅当其包含的类型也都是 Copy 的时候。比如,(i32, i32)Copy 的,但 (i32, String) 就不是。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
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 并移出给调用的函数
}

变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 drop 被清理掉,除非数据被移动为另一个变量所有。

在每一个函数中都获取所有权并接着返回所有权有些啰嗦。如果我们想要函数使用一个值但不获取所有权该怎么办呢?如果我们还要接着使用它的话,每次都传进去再返回来就有点烦人了,除此之外,我们也可能想返回函数体中产生的一些数据。我们可以使用元组来返回多个值。

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

let (s2, len) = calculate_length(s1);

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

fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() 返回字符串的长度

(s, length)
}

引用和借用

一般引用

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()
}

注意我们传递 &s1calculate_length,同时在函数定义中,我们获取 &String 而不是 String

这些 & 符号就是 引用,它们允许你使用值但不获取其所有权。

1
2
3
4
fn calculate_length(s: &String) -> usize { // s 是对 String 的引用
s.len()
} // 这里,s 离开了作用域。但因为它并不拥有引用值的所有权,
// 所以什么也不会发生

我们将获取引用作为函数参数称为 借用borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完毕,必须还回去。我们尝试修改借用的变量是不可能的。

可变引用

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");
}

不过可变引用有一个很大的限制:在特定作用域中的特定数据有且只有一个可变引用。

这个限制的好处是 Rust 可以在编译时就避免数据竞争。数据竞争data race)类似于竞态条件,它可由这三个行为造成:

  • 两个或更多指针同时访问同一数据。
  • 至少有一个指针被用来写入数据。
  • 没有同步数据访问的机制。

一如既往,可以使用大括号来创建一个新的作用域,以允许拥有多个可变引用,只是不能 同时 拥有.

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

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

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

类似的规则也存在于同时使用可变与不可变引用中。我们不能同时有可变和不可变的引用

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

let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM

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

悬垂引用

在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个 悬垂指针dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。

1
2
3
4
5
6
7
8
9
fn main() {
let reference_to_nothing = dangle();
}

fn dangle() -> &String {
let s = String::from("hello");

&s
}

错误信息引用了一个我们还未介绍的功能:生命周期(lifetimes)。

因为 s 是在 dangle 函数内创建的,当 dangle 的代码执行完毕后,s 将被释放。不过我们尝试返回它的引用。这意味着这个引用会指向一个无效的 String,这可不对!Rust 不会允许我们这么做。

这里的解决方法是直接返回 String

1
2
3
4
5
fn no_dangle() -> String {
let s = String::from("hello");

s
}

引用的规则

让我们概括一下之前对引用的讨论:

  • 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
  • 引用必须总是有效。

Slice类型

字符串 slicestring slice)是 String 中一部分值的引用,它看起来像这样:

1
2
3
4
5

let s = String::from("hello world");

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

这类似于引用整个 String 不过带有额外的 [0..5] 部分。它不是对整个 String 的引用,而是对部分 String 的引用。start..end 语法代表一个以 start 开头并一直持续到但不包含 end 的 range。如果需要包含 end,可以使用 ..= 而不是 ..

1
2
3
4
5

let s = String::from("hello world");

let hello = &s[0..=4];
let world = &s[6..=10];

一个真正获取 部分 字符串的办法。不过,我们可以返回单词结尾的索引。

如果想要从第一个索引(0)开始,可以不写两个点号之前的值

如果 slice 包含 String 的最后一个字节,也可以舍弃尾部的数字

在记住所有这些知识后,让我们重写 first_word 来返回一个 slice。“字符串 slice” 的类型声明写作 &str

文件名: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12

fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();

for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}

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

let word = first_word(&s);

s.clear(); // error!

println!("the first word is: {}", word);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn main() {
let my_string = String::from("hello world");

// first_word 中传入 `String` 的 slice
let word = first_word(&my_string[..]);

let my_string_literal = "hello world";

// first_word 中传入字符串字面值的 slice
let word = first_word(&my_string_literal[..]);

// 因为字符串字面值 **就是** 字符串 slice,
// 这样写也可以,即不使用 slice 语法!
let word = first_word(my_string_literal);
}
1
2
3
let a = [1, 2, 3, 4, 5];

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

这个 slice 的类型是 &[i32]。它跟字符串 slice 的工作方式一样,通过存储第一个集合元素的引用和一个集合总长度。你可以对其他所有集合使用这类 slice。


Rust-Learning-1
http://blog.chivier.site/2019-04-05/2021/Rust-Learning-1/
Author
Chivier Humber
Posted on
April 5, 2019
Licensed under