仅记录Rust特有的表达方式
基本的编程要素
数据类型
元祖类型
元祖有着固定的长度,一旦声明,元祖类型将不能改变size
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
元祖元素的访问是通过.
操作符号来进行的
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;
}
空的元祖()
有一个特殊的名称叫做unit,它类似于golang中的struct {}
数组类型
数组类型中的元素必须拥有同样的类型,并且在Rust中,数组类型是不可变长度的,并且数组的内存是在栈上申请的
数组的类型还可以表示为[data type; length]
或者[initial value; length]
let a: [i32; 5] = [1, 2, 3, 4, 5];
let a = [3; 5]; // same as let a = [3, 3, 3, 3, 3]
函数
在Rust的函数定义中,必须给出传入参数的数据类型
Rust中,函数定义由一系列的statement构成,并且可选择的会包含一个expression
- Statements是一系列指令,执行某些动作而没有返回值
- Expressions则计算并得出一个结果值
例如使用let定义一个变量并且初始化就是一个statement,所以在Rust中不存在let x = (let y = 1)
这样的语法,函数定义也是一个statement
expression可以是statement的一部分:调用一个函数是一个expression,调用一个宏是一个expression,一个新的花括号括起来的代码块是一个expression
fn main() {
let y = {
let x = 3;
x + 1
};
println!("The value of y is: {y}");
}
expression没有分号,如果你加上分号,那么它就会变成一个statement
Rust中,函数的返回值没有名字,但是必须要使用->
指定返回值的类型,返回值和函数体花括号的最后一个expression是同义的,同时你也可以使用return
在函数中间返回一个值,如果没有显示的返回一个值,例如最后是一个statement,则Rust默认返回一个()
基本控制流
If
if语句中的condition必须是一个bool类型的expression
三元表达式
fn main() {
let condition = true;
// else两边的值类型必须相同
let number = if condition { 5 } else { 6 };
println!("The value of number is: {number}");
}
循环
Rust使用loop
语法创建一个死循环,同时loop
语句可以使用continue
和break
跳出,并且可以在break
后面跟上loop
语句的返回值
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {result}");
}
在有嵌套循环时,如果不想让break
或者continue
跳转到外一层的循环中,可以使用label,label必须以一个单引号开头命名
fn main() {
let mut count = 0;
'counting_up: loop {
println!("count = {count}");
let mut remaining = 10;
loop {
println!("remaining = {remaining}");
if remaining == 9 {
break;
}
if count == 2 {
break 'counting_up;
}
remaining -= 1;
}
count += 1;
}
println!("End count = {count}");
}
Rust使用while
语法创建一个条件循环
Rust使用for in
语法遍历一个可遍历的对象
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {element}");
}
}
同时for
常常结合内置的Range
语法使用,Range
会产生一个范围内的所有整数
fn main() {
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!!!");
}
所有权
- Rust不会在运行时(Runtime)的时候检测一个变量是否在使用之前就已经定义了,而是在编译阶段完成检测,Rust的一个宗旨就是将Undifined Behavior在编译阶段检测出来。
- 如果一个变量拥有(Owns) 一个box(heap 对象),当 Rust 释放变量的帧(frame)时(通常在函数返回的时候释放函数体stack frame上的所有变量,Rust 释放box的堆内存。
- 如果变量 x 将堆数据的所有权(ownership)移动(move) 到另一个变量 y,则在移动后不能使用 x。
- 数据不应该在同一时间被借用和声明为可变。
考虑一个变量在且仅在编译阶段有以下三种权限:
- Read:变量可以被拷贝到另一个位置
- Write:数据可以被原地修改
- Own:数据可以被移动或者删除
变量默认拥有RO权限,当被声明为mut之后,则拥有了W权限,编译器Borrow Checker的一个关键是引用可以使得变量暂时的失去某些权限,并且在引用的生命周期结束后重新获得这些权限
注意这里的num
和*num
是拥有不同权限的,这是因为实际上权限是根据Path赋予的,Path包括以下几个方面:
- 变量 例如
a
- 解引用,比如
*a
- 数组访问,如
a[0]
- 路径字段,比如元组中的
a.0
,结构体中的a.field
- 以上的任何组合
可变引用提供对数据的唯一且非拥有的访问
- 当
num
是不可变引用(immutable)时,v
仍然拥有 R 权限。既然num
是一个可变引用(mutable reference),那么在num
in use 时,v
就失去了所有的权限。 - 当
num
是不可变引用时,路径*num
只有 R 权限。既然num
是一个可变引用,*num
也获得了 W 权限。 v
和num
没有W权限, 不能重新分配给不同的可变引用,但是可以暂时“降级”为只读引用(read-only references)
所有权总结
- Rust跟踪每个变量的读取(R)、写入(W)和拥有(O)权限。Rust要求变量具有执行给定操作所需的适当权限。如果一个变量没有声明为let mut,则它缺少写入权限(W permission),不能被改变。
- 变量的权限可以发生改变如果被移动(moved) 或者被借用(borrowed) ,对一个非拷贝类型(例如
Box<T>
或者String
)进行移动需要RO权限,移动之后会将该变量的所有权限都取消掉,这个规则保证了无法使用已经移动的变量:
fn main() {
let s = String::from("Hello world");
consume_a_string(s);
println!("{s}"); // COMPILE ERROR: can't read `s` after moving it
}
fn consume_a_string(_s: String) {
// om nom nom
}
- 一个不可变的借用(immutable borrow)创建一个不可变引用(immutable reference),同时不再允许被借用的数据被改变或者移动(移除W和O权限)
- 打印(Read)一个不可变的引用是可行的
- 改变(Write)一个不可变的引用是不可行的
- 改变一个被不可变借用(immutably borrowed)的数据是不可行的
- 将数据从引用中移出是不可行的
fn main() {
let mut s = String::from("Hello");
let s_ref = &s;
s.push_str(" world"); // COMPILE ERROR
println!("{s_ref}");
}
fn main() {
let mut s = String::from("Hello");
let s_ref = &s;
let s2 = *s_ref; // COMPILE ERROR
println!("{s}");
}
- 一个可变的借用(mutable borrow)创建一个可变的引用(mutable reference),将不允许被借用的数据被读,写或者移动 (剥夺RWO权限)
- 改变(Write)一个可变引用是可行的
- 访问(Read)一个被可变借用(mutably borrowed)的数据是不可行的
fn main() {
let mut s = String::from("Hello");
let s_ref = &mut s;
println!("{s}"); // COMPILE ERROR
s_ref.push_str(" world");
}
切片类型
切片类型是一个非拥有类型的引用,切片属于一种富引用,即拥有元数据的指针
fn main() {
let s = String::from("hello world");
let hello: &str = &s[0..5];
let world: &str = &s[6..11];
let s2: &String = &s;
}
同时支持省略区间表达,例如下面的写法是等价的:
let s = String::from("hello");
let len = s.len();
let slice = &s[0..len];
let slice = &s[..];
对String类型进行切片返回的是一个&str,同时Rust会隐式的转换&String和&str
fn main() {
let my_string = String::from("hello world");
// `first_word` works on slices of `String`s, whether partial or whole
let word = first_word(&my_string[0..6]);
let word = first_word(&my_string[..]);
// `first_word` also works on references to `String`s, which are equivalent
// to whole slices of `String`s
let word = first_word(&my_string);
let my_string_literal = "hello world";
// `first_word` works on slices of string literals, whether partial or whole
let word = first_word(&my_string_literal[0..6]);
let word = first_word(&my_string_literal[..]);
// Because string literals *are* string slices already,
// this works too, without the slice syntax!
let word = first_word(my_string_literal);
}
Struct
简写初始化
可以使用简写来初始化一个struct对象
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
// 因为build_user的参数和struct要求的一样,所以可以直接用email来替代email:email
fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1,
}
}
省略初始化
可以使用省略语法来从利用一个对象中的成员初始化另一个对象,而只更新部分参数,注意这里的初始化使用的是=运算符,这里对于non-copyable对象是直接move,因此会移交所有权,因此在移交了non-copyable对象之后user1将不再可用,但若只利用copyable对象进行初始化则两者都将继续可用(例如只使用active
和sign_in_count
进行初始化,而更新email
和username
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
let user1 = User {
email: String::from("[email protected]"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
// --snip--
let user2 = User {
email: String::from("[email protected]"),
..user1 // 必须写在最后
};
}
匿名struct
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
Unit-Like Struct
tuple
里面有unit类型()
,struct也有类似的空结构体,称为Unit-Like Struct
,当需要在某种类型上实现一个trait,但又没有要存储在类型本身中的任何数据时,类似单元的结构体可以很有用
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
Struct的借用
struct的借用和tuple类似,也分为filed-level和struct-level,例如如果借用了Point p
中的某一个filed x
,则p
和p.x
都会失去某些权限,而p.y
却不会
struct Point { x: i32, y: i32 }
fn print_point(p: &Point) {
println!("{}, {}", p.x, p.y);
}
fn main() {
let mut p = Point { x: 0, y: 0 };
let x = &mut p.x;
// COMPILE ERROR: can't READ p
print_point(&p);
*x += 1;
}
struct Point { x: i32, y: i32 }
fn print_point(y: &mut i32) {
*y += 1;
}
fn main() {
let mut p = Point { x: 0, y: 0 };
let x = &mut p.x;
// CHECK PASS: even we can write p.y!
print_point(&mut p.y);
*x += 1;
}
Struct 成员函数(method)
一个基础的成员函数实现是将函数搬到impl
块中,并且第一个参数一定是self
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}
注意这里的&self
是self: &Self
的缩写,其意义与rectangle: &Rectangle
一样,表达self
为一个借用,且可以被声明为mutable
的: area(&mut self)
,而通过直接使用 self
作为第一个参数来传递onwership的方式很少见; 这种技术通常用于当method将 self 转换成其他东西并且想阻止调用者在转换后使用原始实例时。
Rust允许struct中将成员变量和成员函数作相同命名,编译器可以通过是否使用括号来判断使用的是变量还是函数,但大多数情况下,与成员变量同名的函数被用作返回该成员变量。
Associated functions是指的在impl
中定义的,但是没有使用self
作为其第一个参数的函数,通常将函数的构造器作为Associated functions定义为new
方法,但是new
也不是规定的名字
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
调用Associated functions时,我们使用::
语法,例如String::from()
Enum和模式匹配
定义enum可以给每个枚举量绑定一个类型,可以是简单类型,可以是struct甚至也可以是enum
fn main() {
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V61");
}
并且可以给每个变量赋予有名的字段
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
impl Message {
fn call(&self) {
// method body would be defined here
}
}
let m = Message::Writefrom("hello");
m.call();
一个特殊的emun——option
null值的一个特点是当你试图去使用一个null值的时候,你会引发一个错误,null 试图表达的概念仍然是一个有用的概念:null 是当前无效或由于某种原因不存在的值。
Rust标准库定义了一个Option<T>
的emun类型
enum Option<T> {
None,
Some(T),
}
并且你可以省去Option::
而直接使用None
和Some
fn main() {
let some_number = Some(5);
let some_char = Some('e');
let absent_number: Option<i32> = None;
}
注意absent_number
,编译器无法仅通过查看 None 值来推断相应 Some 变体将持有的类型。在这里,我们告诉 Rust,我们的意思是 absent_number
是 Option<i32>
类型。
为什么Option<T>
有效的原因是T
和Option<T>
是两个不同的类型,他们之间不能够混淆使用,当我们面临一个Option<T>
类型的变量的时候,我们要主动去思考(判断)它是否是一个有效值
模式匹配是以match
开头的一个expression
,匹配过程是进入第一个match的arm,执行其中的代码块并返回,模式匹配语句与代码块之间用=>
隔开,如果没有{}
形成的代码块,则将单个expression作为返回值
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
模式匹配时可以进行变量绑定
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => { // 这里state就绑定了UsState的值
println!("State quarter from {:?}!", state);
25
}
}
}
对Option<T>
做模式匹配是代码中非常常见的方式
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
模式匹配不仅可以用于emun,也可以用为常规值,但常规值通常具有无限的可能,这种情况下必须要注意的是,模式匹配必须是完备的(match表达式的arm囊括了所有可能的情况),即编译器不允许发生完全匹配不上的情况,且这个检查是在编译期执行的
- 作为最后一个分支,不提供匹配分支,而仅仅提供一个绑定的变量
- 作为最后一个分支,表示为默认分支,不需要使用绑定的变量,则使用
_
表达
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(other),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(), // 返回()表示什么也不执行
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}
对于上述第二种情况,还有一个更加方便的语法糖可以达到同样的效果,就是if let
和if let - else
语句
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {}", max),
_ => (),
}
等价于
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {}", max);
}
let mut count = 0;
match coin {
Coin::Quarter(state) => println!("State quarter from {:?}!", state),
_ => count += 1,
}
等价于
let mut count = 0;
if let Coin::Quarter(state) = coin {
println!("State quarter from {:?}!", state);
} else {
count += 1;
}
对于一个非copyable
的变量的模式匹配,必须要注意其生命周期
fn main() {
let opt: Option<String> =
Somefrom("Hello world");
match opt {
// _ became s
Some(s) => println!("Some: {}", s), // opt moves to s
None => println!("None!")
};
println!("{:?}", opt); // PANIC: opt lost read and own
}
这个时候可以使用&
来匹配
fn main() {
let opt: Option<String> =
Somefrom("Hello world");
// opt became &opt
match &opt {
Some(s) => println!("Some: {}", s),
None => println!("None!")
};
println!("{:?}", opt);
}
packages、Crates And Modules
Crate是Rust编译器考虑的最小单位,通常一个.rs
文件就是一个Crate,Crate可以是binary,也可以是library
Package是一个或者多个crates的集合,一个package包含一个Cargo.toml
文件,用于描述如何编译代码,一个package可以包含任意数量的binary crates,但是最多只能有一个library crate,一个package可以将多个binary crate的文件放在src/bin
中,每一个文件被视为一个单独的binary crate
Cargo约定src/main.rs
是与package名称相同的binary cate的create root,src/lib.rs
则是与package名称相同的library crate
下面是编译器构建一个项目的过程:
- 从crate root开始:党编译一个crate的时候,编译器首先在crate root文件中查找(通常是
src/lib.rs
或者src/main.rs
- 定义module:在crate root文件中,你可以定义一个新的module:
mod garden
,编译器会在以下位置寻找这个module的代码:- 内联方式,在
mod garden
后面利用花括号代替分号中的代码块 - 在文件
src/garden.rs
中 - 在文件
src/garden/mod.rs
中
- 内联方式,在
- 定义submodules:在除了crate root外的其他任何文件中,通过
mod
定义的被视作submodule,例如在src/garden.rs
中定义的mod vegetables
,编译器会在这些地方寻找submodule的代码- 内联方式,同module
- 在文件
src/garden/vegetables.rs
中 - 在文件
src/garden/vegetables/mod.rs
中
- module中的代码路径:当一个module作为crate的一部分的时候,当权限允许时,在任何地方都可以通过路径引用module中的代码,例如,garden vegetables module 中的
Asparagus
可以通过crate::garden::vegetables::Asparagus
访问 - private和public:默认情况下,module中的代码默认情况下对其parent modules是私有的,可以通过
pub mod
代替mod
声明来使其public,父module不能够使用子module中的私有代码,但是子module中的代码可以使用它们祖先module中的代码mod
上的pub
关键字只允许其祖先模块中的代码引用它,而不能访问其内部代码,可以把mod
想象成一个容器,仅仅把容器设为pub
无济于事,需要进一步把其中定义的项设置为pub
:structs, enums, functions, methods, modules。注意struct的各成员的可见性也是单独定义的,这种情况下struct是一个容器
use
关键字:为了简化代码路径而存在的,如果使用use crate::garden::vegetables::Asparagus;
,则可以直接通过Asparagus
来使用,并且可以通过as
关键字来起别名
Path
Rust代码中形成的module tree支持像文件系统一样通过路径来访问定义的对象:
- 绝对路径:以
crate
开头 - 相对路径:以当前module的标识符或者
self
或者super
开头 - 分隔符是
::
Re-exporting
当我们使用use
关键字将一个名称引入作用域时,该名称在新的作用域中是私有的。为了使调用我们代码的代码能够像在其自身作用域中定义了该名称一样引用它,我们可以结合使用pub
和use
。
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
在这个改变之前,外部代码必须通过使用路径 restaurant::front_of_house::hosting::add_to_waitlist()
来调用add_to_waitlist
函数。现在,由于 pub use 从根模块re-exporting了 hosting 模块,外部代码可以使用路径 restaurant::hosting::add_to_waitlist()
来调用该函数。
容器
Vector
Rust内置了一个宏vec!
来初始化vector
let v = vec![100, 32, 57];
String
String是实现在vec
上的,并且是以utf-8
编码的
拼接字符串可以使用以下的方式:
push_str(&mut self, string: &str)
,向末尾添加string字符串push(&mut self, ch: char)
,向末尾添加一个字符ch
- 运算符+
format!
宏调用
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // note s1 has been moved here and can no longer be used
+
的函数定义是fn add(self, s: &str) -> String
,注意第一个参数是非借用的,调用之后第一个字符串的生命周期结束,第二个操作单位是借用单位
对于更加复杂的拼接,可以使用和println
类似的宏调用format!
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{s1}-{s2}-{s3}");
遍历字符串
.chars()
可以遍历字符.bytes()
可以遍历字节序列
for c in "Зд".chars() {
println!("{c}");
}
for b in "Зд".bytes() {
println!("{b}");
}
Hash Map
要在Rust中使用Hash Map,首先要使用use
将其导入进来,因为它没有被默认引入
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insertfrom("Blue"), 10;
scores.insertfrom("Yellow"), 50;
insert
的参数如果是可拷贝对象,则会将值复制一份,如果是所有权对象,则Hash Map会接管对象的所有权
使用Hash Map
get
方法返回一个Option<&V>
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insertfrom("Blue"), 10;
scores.insertfrom("Yellow"), 50;
let team_name = String::from("Blue");
let score = scores.get(&team_name).copied().unwrap_or(0);
- 遍历一个Hash Map
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insertfrom("Blue"), 10;
scores.insertfrom("Yellow"), 50;
for (key, value) in &scores {
println!("{key}: {value}");
}
- 更新Hash Map
entry
方法返回一个对应参数健的Entry
对象,Entry
对象的or_insert
方法会检查这个Entry
是Occupied
还是Vacant
,如果是有旧值的则直接返回一个值类型的可变引用,否则将参数作为值插入Hash Map,并返回该值类型的可变引用
use std::collections::HashMap;
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}
println!("{:?}", map);
错误处理
通过Result
来处理可恢复(容忍)的错误
Result
是一个枚举类型,T
表示成功时需要返回的类型,E
表示失败时需要返回的类型
enum Result<T, E> {
Ok(T),
Err(E),
}
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => panic!("Problem opening the file: {:?}", error),
};
}
Result
及其变量是默认引入的,因此不需要加Result::
处理不同类型的错误
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error => {
panic!("Problem opening the file: {:?}", other_error);
}
},
};
}
Panic on Error的简写
unwrap()
:如果Result
的值是Ok
,则会返回其包含的值,如果是Err
,则会调用panic!
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt").unwrap();
}
except()
:用法与unwrap()
类似,但支持自定义输出的错误信息
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")
.expect("hello.txt should be included in this project");
}
错误传递的简写
当一个函数的实现调用可能失败的函数时,不在函数本身内部处理错误,而是将错误返回给调用者,以便它可以决定如何处理。这被称为传播错误,并且将更多的控制权交给了调用代码,在那里可能有更多信息或逻辑来决定如何处理错误,而你在代码上下文中可用的信息则可能较少。
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let username_file_result = File::open("hello.txt");
let mut username_file = match username_file_result {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut username = String::new();
match username_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}
错误传递在Rust中太常见了,以至于有一个简写?
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username_file = File::open("hello.txt")?;
let mut username = String::new();
username_file.read_to_string(&mut username)?;
Ok(username)
}
?
的实现与match基本一致,如果Result
中是Ok
,则把其中包含的值当作表达式的返回值,否则直接让整个函数返回错误
范型、Trait(接口)
Rust编译器要求事先声明泛型类型的预期功能,例如如果范型函数中使用了<
,则一定需要将T
的限制传递给范型函数,否则编译无法通过
fn largest<T>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {}", result);
}
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `&T`
--> src/main.rs:5:17
|
5 | if item > largest {
| ---- ^ ------- &T
| |
| &T
|
help: consider restricting type parameter `T`
|
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
| ++++++++++++++++++++++
For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10` due to previous error
如果没有限制,泛型类型 T 就没有能力: 它不能被print、clone或mutate(尽管它可以被删除)。
struct 和 enum 的定义也可以采用范型
struct Point<T, U> {
x: T,
y: U,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let both_integer = Point { x: 5, y: 10 };
let both_float = Point { x: 1.0, y: 4.0 };
let integer_and_float = Point { x: 5, y: 4.0 };
println!("p.x = {}", p.x());
}
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
在方法定义中使用范型
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("p.x = {}", p.x());
}
你也可以单独为某个类型定义方法,这样该类型就具有此方法,而范型的其他类型则不具有此方法,并且,不能同时为某个类型和所有范型定义同名的方法
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
Trait
类似于其他编程语言中的接口
pub trait Summary { fn summarize(&self) -> String; }
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
如果pub trait Summary { fn summarize(&self) -> String; }
中的summarize
不是以分号结尾而是通过大括号实现,则将作为该Trait的默认实现,通过空的impl
块可以使用默认实现impl Summary for NewsArticle {}
将Trait作为参数
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
任何实现了Summary
的类型都可以作为该函数的参数,该语法还存在一个语法糖,称为Trait Bound
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
// 该写法与上面的程序段等价
// 以下两个函数声明等价
pub fn notify(item1: &impl Summary, item2: &impl Summary) {
pub fn notify<T: Summary>(item1: &T, item2: &T) {
还可以同时声明多个Trait作为函数参数传递
// 以下两者等价
pub fn notify(item: &(impl Summary + Display)) {
pub fn notify<T: Summary + Display>(item: &T) {
为了避免函数声明繁琐,保证其可读性,引入了where
关键字辅助声明函数
// 以下两者等价
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
返回Trait
可以将impl Trait
作为返回值,但是不能够返回两种都实现了Trait
的类型其中之一
fn returns_summarizable(switch: bool) -> impl Summary {
if switch {
NewsArticle {
headline: String::from(
"Penguins win the Stanley Cup Championship!",
),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best \
hockey team in the NHL.",
),
}
} else {
Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
}
}
}
// 编译无法通过
条件性的实现方法
可以条件性的为实现了某种Trait的类型实现另一种Trait
impl<T: Display> ToString for T {
// --snip--
}
引用的Lifetime
Lifetime标注旨在告诉 Rust 多个引用的通用生命周期参数如何相互关联
采用以下的语法给变量标注lifetime
&i32 // a reference
&'a i32 // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime
怎么使用Lifetime标注:
以下的代码无法通过编译:
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
--> src/main.rs:9:33
|
9 | fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
|
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10` due to previous error
而进行了Lifetime标注之后即可通过编译:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
函数签名现在告诉 Rust,对于某个生命周期 'a,该函数采用两个参数,这两个参数都是字符串切片,其生命周期至少与生命周期 'a 一样长。函数签名还告诉 Rust,从函数返回的字符串切片的生命周期至少与生命周期 'a 一样长。实际上,这意味着longest
函数返回的引用的生命周期与函数参数引用的值的较小生命周期相同。这些关系是我们希望 Rust 在分析这段代码时使用的。
当一个函数返回引用的时候,返回类型的生命周期参数需要与参数之一的生命周期参数匹配,因为如果不是这样的话,那么这个返回的引用一定指向函数体中新建的变量,那么这个引用在函数结束之后就一定会成为一个悬空引用(因为函数体中新建的变量一定会在函数结束的时候被free掉)
函数编程特性
闭包
Rust 的闭包是匿名函数,可以保存在变量中,也可以作为参数传递给其他函数。您可以在一个地方创建闭包,然后在其他地方调用闭包,以便在不同的上下文中对其进行评估。与函数不同,闭包可以从定义它们的作用域中捕获值。
#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
Red,
Blue,
}
struct Inventory {
shirts: Vec<ShirtColor>,
}
impl Inventory {
fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
user_preference.unwrap_or_else(|| self.most_stocked())
}
fn most_stocked(&self) -> ShirtColor {
let mut num_red = 0;
let mut num_blue = 0;
for color in &self.shirts {
match color {
ShirtColor::Red => num_red += 1,
ShirtColor::Blue => num_blue += 1,
}
}
if num_red > num_blue {
ShirtColor::Red
} else {
ShirtColor::Blue
}
}
}
fn main() {
let store = Inventory {
shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
};
let user_pref1 = SomeRed;
let giveaway1 = store.giveaway(user_pref1);
println!(
"The user with preference {:?} gets {:?}",
user_pref1, giveaway1
);
let user_pref2 = None;
let giveaway2 = store.giveaway(user_pref2);
println!(
"The user with preference {:?} gets {:?}",
user_pref2, giveaway2
);
}
unwarp_or_else
接收一个没有参数的函数闭包,该闭包返回一个和unwarp_or_else
调用者Option<T>
的T
相同的类型,如果对一个None
使用,则调用传入的函数闭包获取一个值,对一个Some(T)
使用则包含的返回T
类型的值
闭包通常在一个小的上下文环境中定义和使用,因此编译器可以像推断变量类型那样推断闭包的参数类型和返回值类型,当然也可以自行指定,以下比较了函数定义和闭包定义:
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
需要注意的是,编译器对于闭包变量类型的推断是一次性的,这注定了闭包不能够像范型一样使用,能够接受多种类型的参数
闭包对于引用和所有权的作用
闭包会根据函数体决定要通过哪种形式捕获引用
fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {:?}", list);
let only_borrows = || println!("From closure: {:?}", list);
println!("Before calling closure: {:?}", list);
only_borrows();
println!("After calling closure: {:?}", list);
}
闭包的函数体仅仅打印了list
,所以其在定义的时候会申请一个其不可变的引用,而在使用完毕之后释放这个引用,由于不可变引用可以在同时申请多个,因此在定义之前,定义之后使用之前,使用之后都可以正常的申请和使用list
的不可变引用
fn main() {
let mut list = vec![1, 2, 3];
println!("Before defining closure: {:?}", list);
let mut borrows_mutably = || list.push(7);
borrows_mutably();
println!("After calling closure: {:?}", list);
}
这里闭包的定义必须申请一个可变引用,因此只有在使用完毕之后才能再申请一个引用,在定义和使用的这段时间内可以视为闭包持有一个list
的可变引用
如果你想要强制闭包在使用环境中的值时获取所有权,即使闭包体并不严格需要所有权,你可以在参数列表之前使用move
关键字。这通常在新建一个线程,需要将某些数据的所有权交给新线程的时候使用。
use std::thread;
fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {:?}", list);
thread::spawn(move || println!("From thread: {:?}", list))
.join()
.unwrap();
}
闭包对待捕获变量的方式决定了其实现的Trait
FnOnce
表示那些可以被调用一次的闭包,所有的闭包都实现了这个Trait,因为所有的闭包都可以被调用,如果一个闭包会将它捕获的值移出它的函数体,那么它就只实现了FnOnce
FnMut
表示那些不会将捕获的值移出函数体,但是会对某些值进行修改的闭包实现,这些闭包可以调用多次Fn
的实现代表了那些不会将值移出函数体,也不会对值进行修改的闭包,同时包含了那些不从环境中捕获任何值的闭包,这些闭包可以被调用多次
例如unwrap_or_else
的定义就是指定了传递的闭包必须是FnOnce
的实现:
impl<T> Option<T> {
pub fn unwrap_or_else<F>(self, f: F) -> T
where
F: FnOnce() -> T
{
match self {
Some(x) => x,
None => f(),
}
}
}
例如接收FnMut
的sort_by_key
不能够接受FnOnce
而顺利通过编译
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
let mut sort_operations = vec![];
let value = String::from("by key called");
list.sort_by_key(|r| {
sort_operations.push(value);
r.width
});
println!("{:#?}", list);
}
这里传递的闭包获取了value
并将它的所有权转交给了sort_operations
,这个闭包显然只能运行一次,因此其实现的是FnOnce
,从而不能够作为sort_by_key
的参数
fn can_we_return_ptr(x: String) -> &str {
let y = String::from("we cant");
&y
}