blog
blog copied to clipboard
Rust
快速入门
$ vim main.rs # 新建文件
$ rustc main.rs # 编译
$ ls
main.rs # 源文件
main # 可执行文件
$ ./main # 执行
Hello, world!
- 入口
main
函数,它是每个可执行的 Rust 程序首先执行的代码 -
rustfmt
自动格式化工具(后续会将它包含在标准 Rust 发行版中) - 风格
- 文件名以下划线 _ 分隔
- 缩进,4个空格,而不是1个tab
- 大部分代码以
;
结尾
- 输出
-
println!
调用了一个 Rust 宏 -
println
是调用函数 -
记住:当看到
!
时,就是宏,而不是普通函数
-
- 编译和运行,是彼此独立的
- 编译:
rustc
Rust 的编译器(类似于C/C++的gcc
/clang
) - 运行:编译成功后,Rust 会输出一个二进制的可执行文件
- 在 Windows 下是
.ext
后缀的,其它平台,无后缀 -
.pdb
后缀是包含调试信息的
- 在 Windows 下是
- Rust 预编译静态类型的语言,ahead-of-time compiled
- 即编译后,将可执行的二进制的文件发送给第三个人,他们就可以运行了(不需要安装 Rust),类似C语言
- 编译:
Cargo.toml
[dependencies]
rand = "0.5.5"
main.rs
// 使用外部依赖
extern crate rand; // 这会调用 use rand,所以下方就可以使用 rand:: 来调用 crate 里的内容了
use std::io; // 把io库引入到当前作用域
use std::cmp::Ordering; // Ordering 是个枚举类,成员有 Less|Greater|Equal
use rand::Rng; // rand 是个 crate,Rng 是个 trait(随机数生成器的方法)
fn main() {
println!("Guess the number!"); // 打印字符串的宏
let secret_number = rand::thread_rng().gen_range(1,101); // 1-100的随机数
//println!("The secret number is: {}", secret_number);
loop {
println!("Please input your guess.");
let mut guess = String::new(); // 创建了一个可变变量,并将它绑定到一个新的 String 空实例上
/*
* 在 Rust 中,变量默认是不可变的。加 mut,让变量可变
* String::new() 会返回一个 String 的新实例
* String 是标准库提供的字符串类型
* :: 表示是关联函数,关联函数-静态方法,是针对类型的
*/
io::stdin().read_line(&mut guess)
.expect("Failed to read line");
/*
* io::stdin() ~ std::io::stdio (当不写上面那行use时)终端标准输入句柄的类型
* .read_line()
* & 表示参数是一个引用(Rust可以安全简单的操作引用)
* 传参 &mut guess
* 返回 io::Result(值可能是 Ok 或 Err)
* io::Result 的实例有 expect 方法
*/
// guess 被绑定到 guess.trim().parse() 表达式
// let guess: u32 = guess.trim().parse()
// .expect("Please type a number!");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num, // 若成功将字符串转换成数字,则会匹配第一个分支的模式
Err(_) => continue // _ 是个通配符
};
/**
* 创建了一个变量 guess(目前,有两个同名的 guess 变量)
* Rust 允许 shadow,用一个新值来隐藏之前的值
* eg.场景,类型转换
* Rust 有静态强类型系统,也有类型推断
* 当类型不匹配的时候,会报错 mismatched types
* 所以需要把字符串转成数字
* : 告诉 Rust 指定了变量类型
*
* 将 expect 换成 match 语句,遇到转换错误不崩溃,而是正常捕获自行处理
*/
println!("You guessed: {}", guess); // {} 占位符
match guess.cmp(&secret_number) { // match 表达式
Ordering::Less => println!("Too small!"), // 分支1
Ordering::Greater => println!("Too big!"), // 分支2
Ordering::Equal => {
println!("You win!");
break;
}
};
/*
* match 表达式(match 的分支和模式是Rust的特色之一,对多种情形的处理)
* ~ 有点 switch-case 的影子
* cmd 方法用来比较两个值,可在任何可比较的值上调用。它获取一个被比较值的引用
* 它返回一个 Ordering 枚举的成员
*/
}
}
编程概念
变量
- 不可变与可变
- 默认是不可变的(immutable),方便利用 Rust 安全和简单并发的优势
- 但也可声明成可变的
mut
- 权衡
- bug
- 大型数据结构,适当地用可变变量,可能比复制和返回新分配的实例更快
- 较小数据结构,总是创建新实例,采用更偏向函数式的风格编程,可能会使代码更易理解
- 不可变的变量(variables) vs. 常量(constants)
- 常量不能用
mut
- 声明的关键字
- 常量
const
,且必须注明类型 - 变量
let
- 常量
- 范围:常量,只能用于常量表达式,不能用于函数返回的结果/运行时的计算值
- 作用域:常量,可以在任何作用域,eg.全局
- 命名规范:常量,大写字母 _分隔
- eg.
const MAX_POINTS: u32 = 100000;
- 常量不能用
- 隐藏(shadowing)
- 即,多个重名的变量时
- 它与将
mut
变量的区别- 隐藏,当再次使用
let
时,是创建了一个新变量(改变了值的类型,从而复用了名字) - 而
mut
若第二次赋值时类型变了,会有个编译错误
- 隐藏,当再次使用
数据类型
Rust 是一种静态类型的语言,任何值都属于一种明确的类型。内置的数据类型有这两类:标量(scalar)、复合(compound)
标量类型代表一个单独的值,四种基本的标量类型:
- 整型
- u8, u16, u32, u64, usize
- i8, i16, i32, i64, isize
- 浮点型:f32, f64
- 布尔类型:bool
- 字符类型:char
-
char
由单引号括起来 - 字符串,由双引号括起来
-
两个原生的复合类型:元组(tuple)、数组(array)
- 元组:每个元素的类型可以不同
- 数组:每个元素的类型必须相同,且是长度固定(一旦声明了,长度就不能变了)
fn main() {
// let tup = (500, 6.4, 1);
let tup: (i32, f64, u8) = (500, 6.4, 1);
// 获取值,可以用模式匹配来解构元组
let (x, y, z) = tup;
// 获取值,可以用.
let x = tup.0;
let y = tup.1;
let z = tup.2;
}
fn main() {
let a = [1, 2, 3, 4, 5]; // 数组是分配在栈上的一整块内存
let first = a[0]; // 使用索引来访问
let second = a[1];
let element = a[10]; // 访问越界
/*
* 编译时不会报错,
* thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 10', src/main.rs:5:19
* note: Run with `RUST_BACKTRACE=1` for a backtrace.
* 但会有一个运行时错误,Rust 会立即退出(而不会继续访问,导致访问位置内存
* 更多信息,见 Rust 的错误处理
*/
}
vector 类型是标准库提供的一种集合类型,类似数组,但允许增长和缩小长度
函数
Rust 不关心函数定义于何处,只要它们被定义了。定义和使用的顺序,无所谓
fn main() {
println!("This is in main()");
function_a(10, 1);
}
fn function_a(x: i32, y: i8) {
println!("This is in function_a(), x = {}, y = {}", x, y);
}
/*
* 关键字 fn
* 函数定义时,必须指定参数类型
* 命名规范是 snake case(小写字母加_),函数名、变量名
*/
- 函数体:由一系列语句和一个可选的表达式组成
- 语句(statement):执行一些操作,但不返回值的指令
- 表达式(expression):计算并产生一个值
- Rust 是一个基于表达式的语言,expression-based
fn main() {
// 6 是一个表达式
let a = 6; // 整体是一个语句,用 let 关键字创建变量并绑定一个值。语句并不返回值
let b = (let a = 6); // 报错:语句并不返回值,所以,不能把 let 语句赋值给另一个变量
let b = a = 10; // 报错(这不同于某些语言,因为赋值语句并不返回值)
let d = 6 + 5; // 6+5 是一个表达式,它返回11。表达式可以作为语句的一部分
let e = {
let f = 3;
f + 1 // 注意,结尾没有分号
}; // 代码块 {} 是个表达式,它返回 f+1
// 报错
let e = {
let f = 3;
f + 1; // 当加了分号,那它就变成了语句。会导致 {} 不返回值了
};
println!("a={}, d={}, e={}", a, d, e);
}
函数的返回值
fn main() {
let x = my_func_five(); // 使用函数的返回值来初始化了一个变量
println!("x = {}", x);
}
fn my_func_five() -> i32 {
5
}
// 有效的 Rust 函数,返回值是 5
// 函数的返回值,等于函数体最后一个表达式的值
// Rust 并不对返回值命名,但会用`->` 声明返回值的类型
控制流
if 表达式
fn main() {
let number = 15;
if number < 10 {
println!("condition is true");
} else {
println!("condition is false");
}
// 如果有多于一个 else if,最好重构代码。可用 match(强大的分支结构)
// 报错,因为条件必须是 bool 类型
if number {
println!("condition is true");
}
}
/*
* if 表达式
* else if 表达式
* else 表达式
*/
fn main() {
let condition = true;
let number = if condition {
5
} else {
6
}; // if 的每个分支的可能返回值,都必须是相同类型。若不同,则会报错
println!("number = {}", number);
}
/*
* if 是一个表达式,所以,可以在赋值语句的右侧用它
* {} 的值,是最后一个表达式的值
* 数字本身,也是一个表达式
*/
循环
fn main() {
loop {
println!("loop");
}
}
// loop 关键字
// 告诉 Rust 一遍又一遍地执行一段代码,直到你明确要求停止(在控制台 ctrl+c,或者 break)
fn main() {
let mut number = 4;
while number != 0 {
println!("number = {}", number);
number = number - 1;
}
}
// while 内建的语言结构
// ~ 简写了一堆 (loop, if, else, break)
// 还有一种更好的写法,用 for
fn main() {
for number in (1..5).rev() {
println!("number = {}", number);
}
}
// Range,是标准库提供的用来生成从一个数字开始到另一个数字之前结束的所有数字序列
// .rev() 翻转 Range
fn main() {
let a = [10, 20, 30, 40, 50];
for item in a.iter() {
println!("{}", item);
}
}
// for 循环
// 遍历数组,更高效。因为不用担心数组下标
// 安全、简洁
所有权(ownership)
所有权是 Rust 最独特的功能,是它让 Rust 无需垃圾回收就能保障内存安全。
一旦理解了所有权,你就不需要经常考虑栈和堆了。但是,理解所有权需要知道栈和堆,以及哪些类型的数据结构在哪存着。
所有权的规则
- Rust 中的每个值都有一个称之为它的所有者(owner)的变量
- 值有且只能有一个所有者
- 当所有者(变量)离开作用域,这个值将被丢弃
变量作用域
- 存在栈上的数据,eg.在“数据类型”中提到的类型都存在栈上,因为长度已知。当离开作用域时被移出栈
- 存在堆上的数据,eg.标准库提供的类型(String)或自己创建的复杂数据类型。当离开作用域时被释放
let s1 = "hello"; // 变量 s 绑定到了一个字符串字面值,这个字符串值是硬编码进程序代码中的。栈
let s2 = String::from("hello"); // 存在堆上,长度要可变,eg.用户输入
let mut s = String::from("hello");
s.push_str(", world!"); // 将一个字面量添加到 String
println!("{}", s); // hello, world!
内存与分配
垃圾回收:
- 有些语言,会在程序运行过程中,时刻寻找不再被使用的内存
- 有些语言,须程序员亲自分配和释放内存
- Rust 则是第三种方式:内存由所有权系统管理 (机制类似 C++)
- 所有权系统有一系列规则,使编译器在编译时进行检查
- 任何所有权系统的功能,都不会导致运行时开销
所有权系统要处理:
- 最小化堆上的冗余数据的数量
- 清理堆上不再使用的数据以致不至于耗尽空间
1. 变量与数据交互的方式:移动
let x = 5;
let y = x;
/**
* 将 5 绑定到 x;生成一个值 x 的拷贝,并绑定到 y
* 现在,有了两个变量 x 和 y,都等于 5
* 因为正数是有已知固定大小的简单值,所以这两个 5 被放入了栈中。
*/
let s1 = String::from("hello"); // 在栈上: 变量s1=(指针,长度,容量) 在堆上:存放实际的字符串内容
let s2 = s1; // Rust 认为 s1 不再有效
/**
* 当对堆上的内存,拷贝的时候
* - 复制指针+堆内存,这样会导致重复 (类似“深拷贝” deep copy)
* - 只复制指针,此时同一段内存会有两处引用。牵扯到内存的释放次数 (类似“浅拷贝” shallow copy)
* - 而 Rust 是:当尝试拷贝的时候,它就认为之前的变量无效了(被称为移动 move)
*
* 此外,还隐含了个设计选择:Rust 永远也不会自动创建数据的 “深拷贝”
*/
2. 变量与数据交互的方式:克隆
let s1 = String::from("Hello");
let s2 = s1.clone(); // 显式 clone,比较消耗资源的代码被执行
所有权与函数
将值传递给函数,在语义上和给变量赋值相似。可能会移动,也可能复制。
返回值与作用域
返回值也可以转移作用域。
变量的所有权总是遵循相同的模式:
- 将值赋值给另一个变量时移动它
- 当持有堆中数据值的变量离开作用域时,其值将通过
drop
清理掉,除非数据被移动为另一个变量所有
在函数中传值/返回值,如果每次都不停地转移所有权,就有点麻烦了。 所以,接着来,引用(reference)
引用
把对象的引用作为参数,而不是转移参数的所有权。
借用
fn main() {
let s1 = String::from("hello");
let len = calulate_length(&s1); // 创建一个指向s1的引用
println!("string is {}, len is {}", s1, len);
}
fn calulate_length(s: &String) -> usize {
s.len()
} // 此时,s 出了它的作用域,s 自己被释放(同函数参与一样)。
// 但因为 s 并不拥有它指向内容的所有权,所以对它的指向什么也不做。
/**
* 传参,&s1
* 接参,&String
*/
& 引用,可以使用值,但不获取其所有权。所以在函数里,是不能修改 s 的值的。正如,变量默认是不可变的,引用也一样。默认,不允许修改引用的值。
fn main() {
let s1 = String::from("hello");
change(&s1);
}
fn change(s: &String) {
s.push_str(", world!"); // 编译报错
}
可变引用
fn main() {
let mut s1 = String::from("hello"); // mut
change(&mut s1); // &mut
println!("{}", s1);
}
fn change(s: &mut String) { // &mut String
s.push_str(", world!");
}
然而,可变引用有个限制:在特定的作用域中,特定的数据,有且只有一个,可变引用。
fn main() {
let mut s1 = String::from("hello");
let r1 = &mut s1;
let r2 = &mut s1; // 报错
println!("{}", r1);
}
/**
* 这个限制,可以让 Rust 在编译时就避免数据竞争
*
* 数据竞争(data race)是一种特定类型的竞争状态,它可由这三个行为造成:
* 1. 两个或更多指针同时访问同一数据
* 2. 至少有一个这样的指针被用来写入数据
* 3. 没有同步数据访问的机制
*
* 数据竞争会导致未定义行为,难以在运行时追踪,并且难以诊断和修复;Rust 避免了这种情况的发生,因为它甚至不会编译存在数据竞争的代码!
*/
当然,可以用块级作用域来拥有多个可变引用,只是不能同时拥有。
fn main() {
let mut s1 = String::from("hello");
{
let r1 = &mut s1;
println!("{}", r1);
}
let r2 = &mut s1;
println!("{}", r2);
}
同时有多个不可变引用,没问题。然而,不能在拥有不可变引用的同时,拥有可变引用
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &mut s; // 报错
}
垂悬引用
垂悬指针,就是指向的内存可能已经被分配给其它人了。而 Rust 编译器,会确保引用永远不会变成垂悬状态:当我们拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
/**
* s 当 dangle() 的代码执行完毕后,s 将被释放。
* 如果我们返回 &s,那就会让这个引用指向一个无效的 String
*/
修改成,当子函数执行完毕之后,把所有权也转移出去。就没问题了。
fn main() {
let get_ownership = no_dangle();
println!("{}", get_ownership);
}
fn no_dangle() -> String {
let s = String::from("hello");
s
}
小节
引用的规则
- 在任意给定的时间,只能拥有下面的一个引用
- 任意数量的不可变引用
- 一个可变引用
- 引用必须总是有效的
还有一种不同类型的引用 slice。接着往下看
字符串 slice
另一个没有所有权的数据类型是 slice。允许引用集合中一段连续的元素序列,而不是整个集合。
eg. 寻找字符串里的第一个单词。先找到第一个出现的空格
fn main() {
let my_sentence = String::from("hello world");
let word = first_word(&my_sentence);
println!("{}", word); // 下标 5
// my_sentence.clear(); // 将字符串变成 ""
}
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes(); // 将 String 转化成字节数组(因为要一个一个元素检查字符是否是空格)
for(i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
// .iter() 返回集合中的每个元素
// .enumerate() 包装 iter 的结果,并返回一个元组
// 元组的第一个元素是索引
// 元组的第二个元素是集合中元素的引用
// 比自己计算索引要方便
s.len()
}
以上代码有个问题,返回的下标和原始字符串是分离的。
字符串 slice
字符串 slice 是 String 中一部分值的引用。
[start..end] 语法表示,从 start 开始,并持续到 end,但不包含 end,也就是区间 [start, end)
fn main() {
let mut my_sentence = String::from("hello world");
let word = first_word(&my_sentence);
println!("第一个单词是 {}", word);
// my_sentence.clear(); // 此时,就会报错
}
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[..]
}
Rust 不仅使得我们的 API 简单易用,也在编译时就消除了一整类的错误!
字符串字面值就是 slice
fn main() {
let s = "Hello, world";
/**
* s 的类型是 &str
* 它是一个指向二进制程序特定位置的 slice
* 这也就是为什么
* - 字符串字面值是不可变的
* - &str 是一个不可变引用
*/
}
所以,可以再改进:
fn main() {
let my_sentence = String::from("hello world");
let word = first_word(&my_sentence[..]);
println!("1.第一个单词是 {}", word);
let my_string_literal = "hello world";
let word = first_word(&my_string_literal[..]);
println!("2.第一个单词是 {}", word);
let word = first_word(my_string_literal);
println!("3.第一个单词是 {}", word);
}
fn first_word(s: &str) -> &str { // (s: &str) -> &str
let bytes = s.as_bytes();
for(i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
其他类型的 slice
eg. 数组
fn main() {
let a = [1, 2, 3, 4, 5];
let arr_slice = &a[1..3]; // 2,3
for item in arr_slice.iter() {
println!("{}", item);
}
}
eg. 其它所有类型的集合,可查看 vector 章节
总结
所有权、借用、引用、slice 这些概念是 Rust 在编译时保证内存安全的关键所在。Rust 可以在拥有数据的 owner 在离开作用域后自动清除其数据,这意味着你无须额外编写和调试相关的控制代码。
所有权系统影响了 Rust 中很多其他部分的工作方式,后面慢慢聊。