文档:Rust 程序设计语言 - Rust 程序设计语言 简体中文版 (bootcss.com)
墙裂推荐这个文档
第一章入门
入门指南 - Rust 程序设计语言 简体中文版
第二章猜猜看游戏
猜猜看游戏教程 - Rust 程序设计语言 简体中文版 (bootcss.com)
// 导入库
use std::io;
use std::cmp::Ordering;
use rand::Rng;fn main() {println!("Guess the number!");// 生成一个随机数,范围在[1, 100],,需要在配置文toml中加依赖let secret_number = rand::thread_rng().gen_range(1, 101);// loop循环loop {println!("Please input your guess.");// 定义一个可变变量,加mut是可变的,不加mut是不可变的let mut guess = String::new();// 读一行数据到guess中,返回值是Result(枚举,成员是Ok和Err)// Result 拥有expect方法// 如果 io::Result 实例的值是 Err,expect 会导致程序崩溃,并显示当做参数传递给 expect 的信息。// 如果 read_line 方法返回 Err,则可能是来源于底层操作系统错误的结果。如果 io::Result 实例的值是 Ok,expect 会获取 Ok 中的值并原样返回。io::stdin().read_line(&mut guess).expect("Failed to read line");// Rust 允许用一个新值来 隐藏 (shadow) guess 之前的值// String 实例的 trim 方法会去除字符串开头和结尾的空白字符。// 字符串的 parse 方法 将字符串解析成数字。// parse也是有返回值,成功的话就是返回num,失败则继续输入let guess: u32 = match guess.trim().parse() {Ok(num) => num,Err(_) => continue,};println!("You guessed: {}", guess);// 用match来匹配match guess.cmp(&secret_number) {Ordering::Less => println!("Too small!"), // guess < secret_numberOrdering::Greater => println!("Too big!"),Ordering::Equal => {println!("You win!"); // 匹配,跳出循环break;}}}
}
代码量不大,学了不少语法
测试
第三章常见编程概念
3.1 变量与可变性
变量不可改
fn main() {println!("Hello, world!");let i = 3;println!("i = {}", i);i = 23; // 变量不可以更改println!("i = {}", i);
}
结果
可变变量,可以修改
fn main() {println!("Hello, world!");let mut i = 3;println!("i = {}", i);i = 23;println!("i = {}", i);
}
变量与常量的区别
(1)不允许对常量使用mut;
(2)声明常量使用 const
关键字而不是 let
,并且 必须 注明值的类型;
(3)常量可以在任何作用域中声明;
(4)常量只能被设置为常量表达式,而不能是函数调用的结果,或任何其他只能在运行时计算出的值。
const MAX_POINTS : u32 = 100_100;
隐藏(Shadowing)
fn main() {let x = 5;let x = x + 1;let x = x * 2;println!("The value of x is: {}", x);
}
隐藏与将变量标记为 mut
是有区别的。当不小心尝试对变量重新赋值时,如果没有使用 let
关键字,就会导致编译时错误。通过使用 let
,我们可以用这个值进行一些计算,不过计算完之后变量仍然是不变的。
mut
与隐藏的另一个区别是,当再次使用 let
时,实际上创建了一个新变量,我们可以改变值的类型,但复用这个名字。
3.2 数据类型
标量类型 标量(scalar)类型代表一个单独的值。Rust 有四种基本的标量类型:整型、浮点型、布尔类型和字符类型。
整数
长度 | 有符号 | 无符号 |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
arch | isize | usize |
整形字面值
数字字面值 | 例子 |
---|---|
Decimal | 98_222 |
Hex | 0xff |
Octal | 0o77 |
Binary | 0b1111_0000 |
Byte (u8 only) | b'A' |
浮点型
Rust 也有两个原生的 浮点数(floating-point numbers)类型,它们是带小数点的数字。Rust 的浮点数类型是 f32
和 f64
,分别占 32 位和 64 位。默认类型是 f64
,因为在现代 CPU 中,它与 f32
速度几乎一样,不过精度更高。
Rust 中的所有数字类型都支持基本数学运算:加法、减法、乘法、除法和取余。
fn main() {// 加法let _sum = 5 + 10;// 减法let _difference = 95.5 - 4.3;// 乘法let _product = 4 * 30;// 除法let _quotient = 56.7 / 32.2;// 取余let _remainder = 43 % 5;
}
布尔型
正如其他大部分编程语言一样,Rust 中的布尔类型有两个可能的值:true
和 false
。Rust 中的布尔类型使用 bool
表示。
字符类型
在 Rust 中,拼音字母(Accented letters),中文、日文、韩文等字符,emoji(绘文字)以及零长度的空白字符都是有效的 char
值。Unicode 标量值包含从 U+0000
到 U+D7FF
和 U+E000
到 U+10FFFF
在内的值。不过,“字符” 并不是一个 Unicode 中的概念,所以人直觉上的 “字符” 可能与 Rust 中的 char
并不符合。
fn main() {let c = 'z';let z = 'ℤ';let heart_eyed_cat = '😻';
}
复合类型
复合类型(Compound types)可以将多个值组合成一个类型。Rust 有两个原生的复合类型:元组(tuple)和数组(array)
元组是一个将多个其他类型的值组合进一个复合类型的主要方式。元组长度固定:一旦声明,其长度不会增大或缩小。
使用包含在圆括号中的逗号分隔的值列表来创建一个元组。元组中的每一个位置都有一个类型,而且这些不同值的类型也不必是相同的。
fn main() {let tup: (i32, f64, u8) = (500, 6.4, 1);let tup = (500, 6.4, 1);let (x, y, z) = tup;println!("The value of y is: {}", y);let x: (i32, f64, u8) = (500, 6.4, 1);let _five_hundred = x.0; // 解构let _six_point_four = x.1; // 解构let _one = x.2; // 解构}
另一个包含多个值的方式是 数组(array)。与元组不同,数组中的每个元素的类型必须相同。Rust 中的数组与一些其他语言中的数组不同,因为 Rust 中的数组是固定长度的:一旦声明,它们的长度不能增长或缩小。
Rust 中,数组中的值位于中括号内的逗号分隔的列表中。
fn main() {let a = [1, 2, 3, 4, 5];let months = ["January", "February", "March", "April", "May", "June", "July","August", "September", "October", "November", "December"];let a: [i32; 5] = [1, 2, 3, 4, 5];let first = a[0];let second = a[1];let index = 3;let element = a[index];println!("The value of element is: {}", element);
}
3.3 函数如何工作
和C++大同小异,这里不同的是具有返回值的函数
fn five() -> i32 {// 没有return// 没有分号5
}fn main() {let x = five();println!("The value of x is: {}", x);
}
3.4 注释
不赘述
3.5 控制流
if-else
fn main() {let number = 6;// 注意没有括号if number % 4 == 0 {println!("number is divisible by 4");} else if number % 3 == 0 {println!("number is divisible by 3");} else if number % 2 == 0 {println!("number is divisible by 2");} else {println!("number is not divisible by 4, 3, or 2");}
}
在let语句中使用if
fn main() {let condition = true;let number = if condition {5} else {6};println!("The value of number is: {}", number);
}
if
的每个分支的可能的返回值都必须是相同类型
循环语句
Rust 有三种循环:loop
、while
和 for
。
loop
fn main() {let mut counter = 0;let result = loop {counter += 1;if counter == 10 {break counter * 2;}};println!("The result is {}", result);
}
while
fn main() {let mut number = 3;while number != 0 {println!("{}!", number);number = number - 1;}println!("LIFTOFF!!!");
}
for
fn main() {let a = [10, 20, 30, 40, 50];for element in a.iter() {println!("the value is: {}", element);}for number in (1..4).rev() {println!("{}!", number);}println!("LIFTOFF!!!");
}
第四章认识所有权
所有权(系统)是 Rust 最为与众不同的特性,它让 Rust 无需垃圾回收(garbage collector)即可保障内存安全。因此,理解 Rust 中所有权如何工作是十分重要的。
4.1 所有权
所有运行的程序都必须管理其使用计算机内存的方式。一些语言中具有垃圾回收机制,在程序运行时不断地寻找不再使用的内存;在另一些语言中,程序员必须亲自分配和释放内存。Rust 则选择了第三种方式:通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。在运行时,所有权系统的任何功能都不会减慢程序。
所有权规则
- Rust 中的每一个值都有一个被称为其 所有者(owner)的变量。
- 值有且只有一个所有者。
- 当所有者(变量)离开作用域,这个值将被丢弃。
字符串字面值不可变,这里以String为例
fn main() {println!("Hello, world!");// String在堆上// from 函数基于字符串字面值来创建 Stringlet mut str = String::from("hello");str.push_str(", world"); // push_str() 在字符串后追加字面值println!("{}", str);
}
结果
对于 String
类型,为了支持一个可变,可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容。这意味着:
- 必须在运行时向操作系统请求内存。
- 需要一个当我们处理完
String
时将内存返回给操作系统的方法。
第一部分由我们完成:当调用 String::from
时,它的实现 (implementation) 请求其所需的内存。这在编程语言中是非常通用的。
然而,第二部分实现起来就各有区别了。在有 垃圾回收(garbage collector,GC)的语言中, GC 记录并清除不再使用的内存,而我们并不需要关心它。没有 GC 的话,识别出不再使用的内存并调用代码显式释放就是我们的责任了,跟请求内存的时候一样。从历史的角度上说正确处理内存回收曾经是一个困难的编程问题。如果忘记回收了会浪费内存。如果过早回收了,将会出现无效变量。如果重复回收,这也是个 bug。我们需要精确的为一个
allocate
配对一个free
。
Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放。
fn main() {{let s = String::from("hello"); // 从此处起,s 是有效的// 使用 s} // 此作用域已结束,// s 不再有效
}
注意:在 C++ 中,这种 item 在生命周期结束时释放资源的模式有时被称作 资源获取即初始化(Resource Acquisition Is Initialization (RAII))。如果你使用过 RAII 模式的话应该对 Rust 的
drop
函数并不陌生。
变量与数据交互的方式(一):移动
fn main() {let x = 5;let y = x;
}
基本变量赋值,x和y都在栈中,且值为5。
那么String类型呢?
fn main() {let s1 = String::from("hello");let s2 = s1;
}
String
由三部分组成,如图左侧所示:一个指向存放字符串内容内存的指针,一个长度,和一个容量。这一组数据存储在栈上。右侧则是堆上存放内容的内存部分。
长度表示 String
的内容当前使用了多少字节的内存。容量是 String
从操作系统总共获取了多少字节的内存。长度与容量的区别是很重要的,不过在当前上下文中并不重要,所以现在可以忽略容量。
当将 s1
赋值给 s2
,String
的数据被复制了,这意味着我们从栈上拷贝了它的指针、长度和容量。我们并没有复制指针指向的堆上数据。
像C++中的浅拷贝
如果 Rust 也拷贝了堆上的数据,那么内存看起来就是这样的。如果 Rust 这么做了,那么操作 s2 = s1
在堆上数据比较大的时候会对运行时性能造成非常大的影响。
(浅拷贝)这就有了一个问题:当 s2
和 s1
离开作用域,他们都会尝试释放相同的内存。这是一个叫做 二次释放(double free)的错误,也是之前提到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。
为了确保内存安全,这种场景下 Rust 的处理有另一个细节值得注意。与其尝试拷贝被分配的内存,Rust 则认为 s1
不再有效,因此 Rust 不需要在 s1
离开作用域后清理任何东西。看看在 s2
被创建之后尝试使用 s1
会发生什么;这段代码不能运行:
fn main() {let s1 = String::from("hello");let s2 = s1;println!("{}, world!", s1);}
结果:
如果你在其他语言中听说过术语 浅拷贝(shallow copy)和 深拷贝(deep copy),那么拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为 Rust 同时使第一个变量无效了,这个操作被称为 移动(move),而不是浅拷贝。上面的例子可以解读为 s1
被 移动 到了 s2
中。那么具体发生了什么,如图
这样就解决了之前的问题!因为只有 s2
是有效的,当其离开作用域,它就释放自己的内存。
另外,这里还隐含了一个设计选择:Rust 永远也不会自动创建数据的 “深拷贝”。因此,任何 自动 的复制可以被认为对运行时性能影响较小。
变量与数据交互的方式(二):克隆
如果 确实 需要深度复制 String
中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 clone
的通用函数。
fn main() {let s1 = String::from("hello");let s2 = s1.clone();println!("{}, world!", s1);println!("{}, world!", s2);
}
这段代码的实际结果就是如下图
只在栈上的数据:拷贝
但这段代码似乎与我们刚刚学到的内容相矛盾:没有调用 clone
,不过 x
依然有效且没有被移动到 y
中。
fn main() {let x = 5;let y = x;println!("x = {}, y = {}", x, y);
}
原因是像整型这样的在编译时已知大小的类型被整个存储在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量 y
后使 x
无效。换句话说,这里没有深浅拷贝的区别,所以这里调用 clone
并不会与通常的浅拷贝有什么不同,可以不用管它。
所有权与函数
将值传递给函数在语义上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。
简单的案例,见注释
fn main() {let s = String::from("hello"); // s 进入作用域takes_ownership(s); // s 的值移动到函数里 ...// ... 所以到这里不再有效// println!("{}", 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 移出作用域。不会有特殊操作
复杂一点的
fn main() {let s = String::from("hello"); // s 进入作用域takes_ownership(s); // s 的值移动到函数里 ...// ... 所以到这里不再有效// println!("{}", s); // 报错
}
fn takes_ownership(some_string: String) { // some_string 进入作用域let str = String::from(some_string);println!("{}", some_string); // 报错
}
返回值与作用域
返回值也可以转移所有权
见注释
fn main() {let s1 = gives_ownership(); // gives_ownership 将返回值// 移给 s1let 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
被清理掉,除非数据被移动为另一个变量所有。
在每一个函数中都获取所有权并接着返回所有权有些啰嗦。如果想要函数使用一个值但不获取所有权该怎么办呢?如果还要接着使用它的话,每次都传进去再返回来就有点烦人了,除此之外,也可能想返回函数体中产生的一些数据。可以使用元组来返回多个值
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)
}
好
返回参数的所有权
见引用部分
4.4 引用与借用
下面是如何定义并使用一个calculate_length
函数,它以一个对象的引用作为参数而不是获取值的所有权:
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()
}
& 符号就是 引用,它允许你使用值但不获取其所有权。
注意:与使用
&
引用相反的操作是 解引用(dereferencing),它使用解引用运算符,*
。
变量 s
有效的作用域与函数参数的作用域一样,不过当引用离开作用域后并不丢弃它指向的数据,因为我们没有所有权。
将获取引用作为函数参数称为 借用(borrowing)。
正如变量默认是不可变的,引用也一样(默认)不允许修改引用的值。
fn main() {let s = String::from("hello");change(&s);
}fn change(some_string: &String) {some_string.push_str(", world");
}
结果
可变引用
fn main() {// s也必须是mut的let mut s = String::from("hello");change(&mut s);
}// 注意形参的形式
fn change(some_string: &mut String) {some_string.push_str(", world");
}
不过可变引用有一个很大的限制:在特定作用域中的特定数据有且只有一个可变引用。
fn main() {let mut s = String::from("hello");let r1 = &mut s;let r2 = &mut s; // 错误println!("{}, {}", r1, r2);}