rust随笔
# 第二章 语言精要
好读书,不求甚解;每有会意,便欣然忘食。
**动手,动手,动手!!!**
## 语句与表达式
Rust 中语法可以分成两大类:语句 statement 和表达式 expression。语句是指要执行的一些操作和产生副作用的表达式。
表达式主要用于计算求值。
语句又分为两种:声明语句 Declaration statment 和表达式语句 Expression statement
- 声明语句,用于声明各种语言项,包括声明变量、静态变量、常量、结构体、函数等,以及通过extern 和use关键字引入包和模块。
- 表达式语句,特指以分号结尾的表达式。此类表达式求值结果将会被舍弃,并总是返回单元类型()。
```rust
//extern crate std;
// use std::prelude::v1::*;
fn main(){
pub fn answer() -> () {
let a = 40;
let b = 2;
assert_eq!(sum(a,b), 42);
}
pub fn sum(a: i32, b: i32) -> i32{
a + b
}
answer();
}
```
Extern cate std; 和 use std::prelude::v1::*; 是声明语句,他们并不需要求值,只是用来引入标准库包以及prelude模块。
rust 会为每个crate都自动引入标准库模块,除非使用#[no_std]属性明确指定了不需要标准库。
使用fn关键子定义了函数,函数answer没有输入参数,并且返回类型为**单元类型()**。单元类型拥有唯一的值,就是它本身,为了描述方便,将该值称为**单元值**。单元类型的概念来自Ocmal,他表示“没有什么特殊的值”。所以,这里将单元类型作为函数返回值,就是表示该函数无返回值。通常没有返回值的函数默认是不需要在函数签名中指定返回类型的。
使用了let声明变量assert_eq! 则是宏语句,踏实rust提供的断言,允许判断给定的两个表达式求值结果是否相同。像这种名字以叹号结尾,并且可以像函数一样被调用的语句,在rust中叫做**宏**。
rust编译器在解析代码的时候,如果碰到分号,就会继续往后面执行;如果碰到语句,则执行语句;如果碰到表达式,则会对表达式求值,如果分号后面什么都没有,就会补上单元值()。
当遇到函数的时候,会将函数体的花括号识别为块表达式 Block expression。块表达式是由一对花括号和系列表达式组成的。他总是返回块中最后一个表达式的值。
从这个角度看,可以讲rust看作一切皆表达式。由于当分号后面什么都没有时自动补单元值()的特点。我们可以将rust中的语句看作计算结果均为()的特殊表达式。而对于普通表达式来说,则会得到正常的求值结果。
## 变量与绑定
通过let关键字来创建变量,这是rust函数从函数式语言中借鉴的语法形式。let创建的变量一般称为绑定 binding。它表明了标识符 identifier 和值value 之间建立的一种关联关系。
### 位置表达式和值表达式
rust中的表达式一般分为位置表达式 place expression 和值表达式 value expression。在其他语言中,一般叫做左值 lvalue和右值rvalue。
位置表达式就是表示内存位置的表达式。分类有:
- 本地变量
- 静态变量
- 解引用 (*epr)
- 数组索引(expr[expr])
- 字段引用(exp.field)
- 位置表达式组合
通过位置表达式可以对某个数据单元的内存进行读写。主要进行写操作,这也是位置表达式可以被赋值的原因。
值表达式一般只引用了某个存储单元地址中的数据。它相当于数据值,只能进行读操作。
从语义角度来说,位置表达式代表了持久性数据,值表达式代表了临时数据。位置表达式一般有持久的状态,值表达式要么是自面量,要么是表达式求值过程中创建的临时值。
表达式的求值过程在不同的上下文中会有不同的结果。求值上下文也分为位置上下文 place context 和值上下文 value context。
下面几种表达式属于位置上下文:
- 赋值或者复合赋值语句左侧的操作数。
- 一元引用表达式的独立操作数。
- 包含隐式借用(引用)的操作数。
- match 判别式或者let绑定右侧在使用ref模式匹配的时候也是位置上下文。
除了上述几种情况,其余表达式都属于值上下文。值表达式不能出现在位置上下文中。
```rust
pub fn temp() -> i32{
return 1;
}
fn main() {
let x = &temp();
temp() = *x; //e0070: invalid left-hand size expression
}
```
在main函数中,使用temp函数调用放到赋值语句左边的位置上下文中,此时编译器会报错。因为temp函数调用是一个无效的位置表达式,它是值表达式。
### 不可变绑定与可变绑定
使用let关键字声明的位置表达式默认不可变,为不可变绑定。
```rust
fn main() {
let a = 1;
// a = 2; //immytable and error
let mut b = 2;
b = 3; // mutable
}
```
变量a默认是不可变绑定,对其重新复制后编译器会报错。通过mut关键字,可以声明可变位置表达式,即可变绑定。可变绑定可以正常修改和赋值。
从语义上来说,let声明的不可变绑定只能对相应的存储单元进行读取,而let mut 声明的可变绑定则是可以对相应的存储单元进行写入。
### 所有权与引用
当位置表达式出现在值上下文中,该位置表达式将会把内存地址转移到另一个位置表达式,这是所有权的转移。
```rust
fn main(){
let place1 = "hello";
let place2 = "hello".to_string();
let other = place1;//Copy
println!("{:?}", place1);
let other = place2;// Move
println!("{:?}", place2); //place2 value used here after move
}
```
place1是绑定的是一个字符串字面量,对于字符串字面量来说,和基本类型都一样都有Copy trait,在赋值操作时执行的是Copy操作,不回所有权的移动,现在出现在赋值操作符右侧,这里let other = place1; 会执行place1的拷贝操作,在执行之后,打印place1仍然是可以的。
而place2是String类型,没有Copy triat,这里赋值后,会发生所有权的移动。接着打印place2会出现错误,place2 value used here after move
**在语义上,每个变量实际上都拥有该存储单元的所有权,这种转移所有权地址的行为就是所有权ownership的转移,在rust中称为移动 move 语义。那种不转移的情况。实际是上是一种复制copy语义。Rust没有GC, 所以完全依靠所有权来管理内存管理。**
在日常开发中,有时并不需要转移所有权。Rust提供**引用操作符&**, 可以直接获取表达式的存储单元地址,即内存地址。可以通过该内存位置对存储进行读取。
```rust
fn main(){
let a = [1,2,3];
let b = &a;
println!("{:p}", b); // 0x7fffcbc067704
let mut c = vec![1, 2, 3];
let d = &mut c;
d.push(4);
println!("{:?}", d);// [1, 2, 3, 4]
let e = &42;
assert_eq!(42, *e);
}
```
上面定义了固定长度数组a,并使用引用操作符&取得a的内存地址,赋值给b。这种方式不会引起所有权的转移,因为使用引用操作符已经将赋值表达式右侧变成了位置上下文,他只是共享内存地址。通过println! 宏指定{:p}格式,可以打印b的指针地址。也就是内存地址。
通过let mut 声明了动态长度数组c,然后通过&mut 获取c的可变引用,赋值给d。
**注意,要获取可变引用,必须先声明可变绑定。**
对于字面量42来说,其本省属于值表达式。通过引用操作符,相当于值表达式在位置上下文中进行求值,所以编译器会为&42创建一个临时值。
最后,通过解引用操作符*将引用e中的值取出来,以供assert_eq!宏使用。
从语义上来说,不管&a还是&mut c;都相当于对a和c所有权的借用,因为a和c还依旧保留他们的所有权,所以引用也被称为借用。
## 函数与闭包
### 函数定义
main函数,它代表程序的入口。对于二进制可执行文件来说,main函数必不可少。对于库函数来说,mian函数就没有必要。
函数是通过关键字fn定义的。
```rust
pub fn fizz_buzz(num: i32) -> String {
if num % 15 == 0 {
return "fizzbuzz".to_string();
}else if num % 3 == 0 {
return "fizz".to_string();
}else if num % 5 == 0 {
return "buzz".to_stirng();
}else{
return num.to_string();
}
}
fn main(){
assert_eq!(fizz_buzz(15), "fizzbuzz".to_string());
assert_eq!(fizz_buzz(3), "fizz".to_string());
assert_eq!(fizz_buzz(5), "buzz".to_string());
assert_eq!(fizz_buzz(13), "13".to_string());
}
```
使用fn关键字定义了fizz_buzz函数,其函数签名pub fn fizz_buzz(num: i32) -> String 清晰地反映了函数的类型约定:传入i32类型,返回Srtring类型。**Rust编译器会严格遵守此类型的契约,如果传入或返回的不是约定好的类型,则会编译时出错。**
return表达式用于退出一个函数,并返回一个值。但是如果return后买呢没有值,就会默认返回单元值。
使用了to_string 方法,他将表达式的求值结果转换成Siting类型。
### 作用域与生命周期
Rust语言的作用域是静态作用域,即词法作用域 Lexical scope。由一对花括号来开辟作用域,其作用域在词法分析阶段就已经确定了,不回动态该改变。
```rust
fn main(){
let v = "hello world!";
assert_eq!(v, "hello world!");
let v = "hello Rsut!";
assert_eq!(v, "hello Rsut!");
{
let v = "hello Wolrd!";
assert_eq!(v, "hello World!");
}
assert_eq!(v, "hello Rust!");
}
```
先定义了变量绑定v,赋值为hello world!,然后通过断言验证其值。再次通过let声明变量绑定v,赋值为hello Rust!这种定义同名变量的做法叫做**变量遮蔽 variable shadow**。但是最终的变量v的值是由第二个变量定义所决定的。
在词法作用域内部使用花括号开辟新的词法作用域后,两个作用域是相互独立的。在不同的词法作用域内声明的变量绑定,拥有不同的生命周期 lifetime。 **变量绑定的生命周期总是遵循这样的规律:从使用let声明创建变量绑定开始,到超出词法作用域的范围时结束。**
### 函数指针
在rust 中,**函数为一等公民**。这意味,函数自身既可以作为函数的参数和返回值使用。
```rust
pub fn math(op: fn(i32, i32) -> i32, a: i32, b: i32) -> i32 {
op(a, b)
}
fn sum(a: i32, b: i32) -> i32 {
a + b
}
fn product(a: i32, b: i32) -> i32 {
a * b
}
fn main() {
let a = 2;
let b = 3;
assert_eq!(math(sum, a, b), 5);
assert_eq!(math(product, a, b), 6);
}
```
定义了函数math, 其函数签名的第一个参数为fn(i32, i32) -> i32 类型, 这在rust中时函数指针 fn pointer类型。
这里直接使用函数的名字作为函数指针。
函数也可以作为返回值使用。
```rust
fn is_true() -> bool {
true
}
fn true_maker() -> fn() -> bool {
is_true
}
fn main(){
assert_eq!(true_maker()(), true);
}
```
定义了函数true_maker,返回fn() -> bool 类型,其函数体内直接将is_true函数指针返回。注意此处也使用了函数名字作为函数指针,如果加上括号,就会调用该函数。
在main函数的断言中,true_maker()()调用相当于(true_maker() ) ()。 首先调用true_maker(), 会返回is_true函数指针;然后再调用is_true()函数,最终得到true。
### CTEE机制
rust编译器可以像C++,拥有编译时函数执行 compile - time function execution , CTTE的能力。
```rust
#![feature(const_fn)]
const fn init_len() -> usize {
return 5;
}
fn main(){
let arr = [0; init_len()];
}
```
使用了const fn 来定义函数init_len, 该函数返回一个固定值5. 并且在main函数中,通过[0; N]这种形式来初始化初始值为0, 长度为N的数组,其中N时由调用函数init_len来求得的。
Rust中固定的数组必须在编译期就知道长度,否则会编译出错。所以函数init_len必须在编译器求值。这就是CTTE的能力。
使用const fn 定义的函数,必须可以确定值,不能存在歧义。与定义fn函数的区别在于, const fn 可以强制编译器在编译期执行函数。其中关键字const一般用于定义全局常量。
除了const fn,官方还在实现const generics特性。 支持const generics 特性,将可以实现类似impel < T, const N: usize> Foo for [T:N] {... } 的代码。可以为所有长度的数组实现trait Foo.
Rust中的CTTE是由miri来执行的。miri是一个MIR解释器,目前已经被集成到Rust编译器rustc中。rust 编译器目前可以支持的常量表达式有:字面量、元组、数组、字段结构体、枚举、只包含单行代码的块表达式、范围等。
### 闭包
闭包也叫做匿名函数。闭包的特点:
- 可以像函数一种被调用。
- 可以捕获上下文环境中的自由变量。
- 可以自动推断输入和返回的类型。
```rust
fn main() {
let out = 42;
// fn add(i:i32, j: i32) -> i32 { i + j + out }
fn add(i: i32, j: i32) -> i32 { i + j}
let closure_annotated = |i: i32, j:i32| -> i32 { i + j + out };
let closure_inferred = |i, j| i + j + out;
let i = 1;
let j = 2;
assert_eq!(3, add(i, j));
assert_eq!(45, closure_annotated(i,j));
assert_eq!(45, closure_inferrend(i,j));
}
```
闭包函数有一个重要的区别,就是闭包可以捕获外部变量,而函数不可以。
闭包也可以作为函数参数和返回值。
```rust
fn closure_math<F: Fn() -> i32 >(op: F) -> i32 {
op()
}
fn main() {
let a = 2;
let b = 3;
assert_eq!(clousre_math(|| a + b), 5);
assert_eq!(clousre_math(|| a * b), 6);
}
```
定义了函数closure_math,其参数是一个泛型F, 并且泛型受Fn() -> i32 trait的限定,代表函数只允许实现Fn()->i32 trait的类型作为参数。
**Rust中闭包实际上就是一个匿名结构体和trait来组合实现的。**
闭包也可以作为返回值,
```rust
fn two_times_impl() -> impl Fn(i32) -> i32 {
let i = 2;
move |j| j * i
}
fn main() {
let result = two_times_impl();
assert_eq!(result(2), 4);
}
```
在函数定义时并不知道具体的返回类型,但是在函数调用时,编译器会推断出来。这个过程也是零成本抽象的,一切都发生在编译器。
**在函数two_times_imple中最后返回闭包时使用了move关键字,这是因为在一般情况下,闭包默认会岸引用捕获变量。如果将此闭包返回,则引用也会跟着返回变量i的引用,也将成为悬垂指针。使用move关键字,将捕获变量i的所有权转移到闭包中,就不会按引用进行捕获变量,这样闭包才可以安全地返回。**
## 流程控制
流程控制表达式
### 条件表达式
表达式一定会有值,所以if表达式的分支必须返回一个类型的值才可以。if表达式的求值规则和块表达式一致。
```rust
fn main() {
let n = 13;
let big_n = if ( n < 10 && n > -10 ){
10 * n
}else {
n / 2;
};
assert_eq!(big_n, 6);
}
```
### 循环表达式
rust中包括三种循环表达式: while, loop和for ... in 表达式。
```rust
fn main() {
for n in 1..101 {
if n % 15 == 0 {
println!("fizzbuzz");
}else if n % 3 == 0{
println!("fizz");
}else if n % 5 == 0 {
println!("buzz");
}else {
println!("{}", n);
}
}
}
```
For .. in 表达式本质上是一个迭代器,其中1..101 是一个range类型,它是一个迭代器。for的每一次循环都从迭代器中取值,当迭代器中没有值的时候,for循环结束。
```rust
fn while_true(x: i32) -> i32 {
while true {
return x + 1;
}
}
fn main() {
let y = while_true(5);
assert_eq!(y, 6);
}
```
其中while循环条件使用了硬编码true,目的是实现无限循环。这种看似非常正确的代码会引起rust编译器报错。
错误提示称while true 循环块返回的是单元值,而函数while_true返回值是i32, 所以不匹配。但是在while true 循环中使用了return关键字,应该返回i32类型才行。为什么会报错呢?
这是因为rust编译器在对while循环做流分析 Flow sensitive 的时候,不会检查循环条件,编译器会认为while循环条件可真可假,所以循环体里的表达式也会被忽略,此时编译器只知道while true 循环返回的是单元值,而函数返回的是i32。其他情况一概不知。这一切都是因为CTEE功能的限制,while条件表达式无法作为编译器常量来使用。
```rust
fn while_true(x: i32) -> i32 {
while true {
return x + 1;
}
x
}
```
在while_true函数的最后一行加了x变量,这是为了让编译器以为返回的类型是i32类型,但实际上,程序在运行以后,将永远在while true 循环汇中执行。
### match 表达式与模式匹配
```rust
fn main() {
let number = 42;
match number {
0 => println!("Origin"),
1...3 => println!("All"),
|5|7|13 => println!("Bad luck"),
n @ 42 => println!("Answer is {}", n),
_ => println!("Common"),
}
}
```
在rust语言中,match分支使用了模式匹配 pattern matching 技术。模式匹配在数据结构字符串中经常出现,比如在某个字符串中找出与该字符串相同的所有子串。在编程语言中,模式匹配用于判断类型或者值是否存在可以匹配的模式。模式匹配在很多函数式语言中已经被广泛应用。
在rust语言中,match分支左边就是模式,右边就是执行代码。模式匹配同时也是一个表达式,与if表达式类似,所有分支必须返回一个类型。但是左侧的模式可以是不同的。
使用操作符@可以将模式中的值绑定给一个变量。供分支右侧的代码使用,这类匹配叫做绑定模式 Bindingmode。match表达式必须穷尽每一种可能,所以一般情况下会使用通配符_来处理剩余的情况。