1 变量与常量
使用 let
关键字声明变量,变量名后面为变量类型:
let x: u32 = 1;
Rust 是静态类型(statically typed)语言,也就是说在编译时就必须知道所有变量的类型。根据值及其使用方式,编译器通常可以推断出我们想要用的类型:
let x = 1;
变量默认是不可改变的,可以在变量名之前加 mut
来使其可变:
let mut x = 1;
x = 2;
如果你创建了一个变量却不在任何地方使用, Rust 通常会警告。但是有时创建一个还未使用的变量是有用的,为此可以用下划线作为变量名的开头:
let _x = 5;
使用 const
关键字声明常量,Rust 对常量的命名约定是在单词之间使用全大写加下划线:
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
常量有以下特性:
- 常量可以在任何作用域中声明,包括全局作用域;
- 常量只能被设置为常量表达式,而不可以是其他任何只能在运行时计算出的值;
2 数据类型
在 Rust 中,每一个值都属于某一个数据类型(data type)。Rust 内置的原生类型(primitive types)有以下几类:
- 布尔类型:有两个值 true 和 false。
- 字符类型:表示单个 Unicode 字符,存储为4个字节。
- 数值类型:分为有符号整数(i8,i16,i32,i64,isize)、无符号整数(u8,u16,u32,u64,usize)以及浮点数(f32,f64)。
- 字符串类型:最底层的是不定长类型 str,更常用的是字符串切片 &str 和堆分配字符串 String, 其中字符串切片是静态分配的,有固定的大小,并且不可变,而堆分配字符串是可变的。
- 数组:具有固定大小,并且元素都是同种类型,可表示为 [T; N]。
- 切片:引用一个数组的部分数据并且不需要拷贝,可表示为 &[T]。
- 元组:具有固定大小的有序列表,每个元素都有自己的类型,通过解构或者索引来获得每个元素的值。
- 指针:最底层的是裸指针 *const T 和 *mut T,但解引用它们是不安全的,必须放到 unsafe 块里。
- 函数:具有函数类型的变量实质上是一个函数指针。
- 元类型:即(),其唯一的值也是()。
Rust 数据类型可分为两类:标量(scalar)和复合(compound)。标量类型代表一个单独的值,包括整型、浮点型、布尔类型和字符类型;复合类型可以将多个值组合成一个类型,包括元组和数组。
3 函数
使用 fn
关键字声明函数:
fn plus_one(x: i32) -> i32 {
x + 1
}
Rust 有一个叫做 !
的特殊类型,被称为 empty type 或 never type,因为它没有值,在函数从不返回的时候充当返回值:
fn bar() -> ! {
// --snip--
}
这读 “函数 bar
从不返回”,而从不返回的函数被称为发散函数(diverging functions)。不能创建 !
类型的值,所以 bar
也不可能返回值。
4 控制流
4.1 if
if condition1 {
// do something
} else if condition2 {
// do something
} else {
// do something
}
4.2 while
while condition {
// do something
}
4.3 loop
loop {
// do something
}
4.4 for
for element in iter {
// do something
}
4.5 match
let some_u8_value = 0u8;
match some_u8_value {
1 => println!("one"),
3 => println!("three"),
5 => println!("five"),
7 => println!("seven"),
_ => (),
}
其中,下划线 _
作为匹配但不绑定任何值。
另外可以使用 |
语法匹配多个模式:
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
可以使用 ..=
语法匹配一个闭区间范围内的值:
let x = 'c';
match x {
'a'..='j' => println!("early ASCII letter"),
'k'..='z' => println!("late ASCII letter"),
_ => println!("something else"),
}
使用 ..
语法来只使用部分并忽略其它值:
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, .., last) => {
println!("Some numbers: {}, {}", first, last);
},
}
at 运算符(@)允许在创建一个存放值的变量的同时测试其值是否匹配模式:
enum Message {
Hello { id: i32 },
}
let msg = Message::Hello { id: 5 };
match msg {
Message::Hello { id: id_variable @ 3..=7 } => {
println!("Found an id in range: {}", id_variable)
},
Message::Hello { id: 10..=12 } => {
println!("Found an id in another range")
},
Message::Hello { id } => {
println!("Found some other id: {}", id)
},
}
4.6 if let
if let Some(3) = some_u8_value {
println!("three");
}
if let
获取通过等号分隔的一个模式和一个表达式,工作方式与 match 相同,这里的表达式对应 match 而模式则对应第一个分支。
使用 if let 意味着编写更少代码,更少的缩进和更少的样板代码,这样会失去 match 强制要求的穷尽性检查。match 和 if let 之间的选择依赖特定的环境以及增加简洁度和失去穷尽性检查的权衡取舍。
5 结构体
使用 struct
关键字声明结构体,并在大括号中定义每个字段的名字和类型:
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
参数名与字段名都完全相同,可以使用字段初始化简写语法(field init shorthand):
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
结构体更新语法(struct update syntax)进行结构体构造:
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};
..
语法指定了剩余未显式设置值的字段应有与给定实例对应字段相同的值:
let user2 = User {
email: String::from("another@example.com"),
..user1
};
类单元结构体(unit-like structs):
类单元结构体没有任何字段,通常在某个类型上实现 trait 但不需要在类型中存储数据的时候发挥作用:
struct AlwaysEqual;
let subject = AlwaysEqual; // no need '()' or '{}'
方法与函数类似,使用 fn
关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码。不过方法与函数是不同的,因为它们在结构体的上下文中被定义(或者是枚举或 trait 对象的上下文),并且它们第一个参数总是 self,代表调用该方法的结构体实例:
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
块(impl 是 implementation 的缩写),这个 impl 块中的所有内容都将与 Rectangle 类型相关联。所有在 impl 块中定义的函数被称为关联函数(associated functions),因为它们与 impl 后面命名的类型相关,每个结构体都允许拥有多个 impl 块。
另外也可以定义不以 self 为第一参数的关联函数(因此不是方法),因为它们并不作用于一个结构体的实例,不是方法的关联函数经常被用作返回一个结构体新实例的构造函数:
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle { width: size, height: size }
}
}
6 枚举
使用 enum
关键字声明枚举,枚举值后可声明此枚举值的类型,作为枚举的一部分:
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
Option
是标准库定义的一个枚举,
enum Option<T> {
Some(T),
None,
}
7 特性
特性(trait)告诉 Rust 编译器某个特定类型拥有可能与其他类型共享的功能。使用 trait
定义一个特性,特性中包含该特性的实现函数:
pub trait Summary {
fn summarize(&self) -> String;
}
一个类型的行为由其可供调用的方法构成,如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合:
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)
}
}
需要注意的是,不能为外部类型实现外部 trait。这个限制是被称为相干性(coherence) 的程序属性的一部分,或者更具体的说是孤儿规则(orphan rule),其得名于不存在父类型。这条规则确保了其他人编写的代码不会破坏你代码,反之亦然。没有这条规则的话,两个 crate 可以分别对相同类型实现相同的 trait,而 Rust 将无从得知应该使用哪一个实现。
有时为 trait 中的某些或全部方法提供默认的行为,而不是在每个类型的每个实现中都定义自己的行为是很有用的。这样当为某个特定类型实现 trait 时,可以选择保留或重载每个方法的默认行为:
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
如果执行默认实现,只需 impl Summary for NewsArticle {}
指定一个空的 impl 块,另外,重载一个默认实现的语法与实现没有默认实现的 trait 方法的语法相同。
trait 作为参数如下,该参数是实现了 trait 的某种类型:
pub fn notify(item: impl Summary) {
println!("Breaking news! {}", item.summarize());
}
上述还可以用一种成为 trait bound
的方法实现,它实际上是一种较长形式语法的语法糖:
pub fn notify<T: Summary>(item: T) {
println!("Breaking news! {}", item.summarize());
}
指定多个 trait bound
:
pub fn notify<T: Summary + Display>(item: T)
通过 where
简化 trait bound
:
fn some_function<T, U>(t: T, u: U) -> i32
where T: Display + Clone,
U: Clone + Debug
{
使用 trait bound
有条件的实现方法:
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}
8 闭包
闭包(closures)是可以保存进变量或作为参数传递给其他函数的匿名函数。可以在一个地方创建闭包,然后在不同的上下文中执行闭包运算。不同于函数,闭包允许捕获调用者作用域中的值。
定义一个闭包并储存到变量中:
let expensive_closure = |num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
闭包的定义以一对竖线 |
开始,在竖线中指定闭包的参数,如果有多于一个参数,可以使用逗号分隔,比如 |param1, param2|
;参数之后是存放闭包体的大括号,其中最后一行的返回值作为调用闭包时的返回值。
这个语句意味着 expensive_closure 包含一个匿名函数的定义,不是调用匿名函数的返回值。
闭包不要求像 fn 函数那样在参数和返回值上注明类型,通常很短,并只关联于小范围的上下文而非任意情境,在这些有限制的上下文中,编译器能可靠的推断参数和返回值的类型:
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 x = 4;
let equal_to_x = |z| z == x;
let y = 4;
assert!(equal_to_x(y));
}
闭包可以通过三种方式捕获其环境,他们直接对应函数的三种获取参数的方式:获取所有权,可变借用和不可变借用。这三种捕获值的方式被编码为如下三个 Fn
trait:
-
FnOnce
从周围作用域捕获的变量,闭包周围的作用域被称为其环境(environment)。为了消费捕获到的变量,闭包必须获取其所有权并在定义闭包时将其移动进闭包。其名称的 Once 部分代表了闭包不能多次获取相同变量的所有权的事实,所以它只能被调用一次。 -
FnMut
获取可变的借用值所以可以改变其环境。 -
Fn
从其环境获取不可变的借用值。
当创建一个闭包时,Rust 根据其如何使用环境中变量来推断我们希望如何引用环境。由于所有闭包都可以被调用至少一次,所以所有闭包都实现了FnOnce
。
如果你希望强制闭包获取其使用的环境值的所有权,可以在参数列表前使用 move
关键字:
fn main() {
let x = vec![1, 2, 3];
let equal_to_x = move |z| z == x;
println!("can't use x here: {:?}", x);
let y = vec![1, 2, 3];
assert!(equal_to_x(y));
}
9 函数指针
fn
被称为函数指针(function pointer):
fn add_one(x: i32) -> i32 {
x + 1
}
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}
fn main() {
let answer = do_twice(add_one, 5);
println!("The answer is: {}", answer);
}
函数指针实现了所有三个闭包 trait(Fn
、FnMut
和 FnOnce
)。所以总是可以在调用期望闭包的函数时传递函数指针作为参数。
一个只期望接受 fn
而不接受闭包的情况的例子是与不存在闭包的外部代码交互时:C 语言的函数可以接受函数作为参数,但 C 语言没有闭包。
网友评论