前言
承接前文,本文主要对Rust基础概念中关于表达式、语句、控制流、注释、函数的知识点进行梳理。
表达式
- Rust is primarily an expression language
- Rust的表达式包括字面量表达式、方法调用表达式、数组表达式、 索引表达式、单目运算符表达式、双目运算符表达式等。
- Rust表达式又 可以分为“左值”(lvalue)和“右值”(rvalue)两类。所谓左值,意思是 这个表达式可以表达一个内存地址。因此,它们可以放到赋值运算符左边使用。其他的都是右值。
- 算术运算符包括:加(+)、减(-)、乘(*)、除(/)、 求余(%);比较运算符包括:等于(==)、不等于(!=)、小于 (<)、大于(>)、小于等于(<=)、大于等于(>=);位运算符包括:按位取反(!)、按位与(&)、按位或(|)、按位异或(^)、左移(<<)、右移(>>);逻辑运算符包括:逻辑取反(!)、逻辑与(&&)、逻辑或(||)。
- 编译器会禁止此形式的连续比较:a==b==c。连续比较运算符必须加上括号。
- 逻辑与、逻辑或有短路功能。按位与、按位或不会短路。
- 比较运算符的两边必须是同类型的,并满足PartialEq约束。比较表达式的类型是bool
- 赋值表达式:一个左值表达式、赋值运算符(=)和右值表达式,可以构成一个赋值表达式。赋值表达式具有“副作用”:当它执行的时候,会把右边表达式的 值“复制或者移动”(copy or move)到左边的表达式中。赋值号左右两边表达式的类型必 须一致,否则是编译错误。
- 赋值表达式也有对应的类型和值。Rust规定,赋值表达式的类型为unit,即空的tuple()。如下例,输出是()。Rust这么设计是有原因的,比如说可以防止连续赋值。
#![allow(unused)] fn main() { let x = 1; let mut y = 2; // 注意这里专门用括号括起来了 let z = (y = x); println!("{:?}", z); }
- Rust也支持组合赋值表达式。但不支持++、--运算符,请使用+=1、-=1替代。
- 语句块表达式:在Rust中,语句块也可以是表达式的一部分。语句和表达式的区分 方式是后面带不带分号(;)。如果带了分号,意味着这是一条语句, 它的类型是();如果不带分号,它的类型就是表达式的类型。所以在函数中,我们也可以利用这样的特点来写返回值。这种写法用在后面讲到的闭包closure中,会方便轻量(无需写return)。
// 语句块可以是表达式,注意后面有分号结尾,x的类型是() let x : () = { println!("Hello."); }; // Rust将按顺序执行语句块内的语句,并将最后一个表达式类型返回,y的类型是 i32 let y : i32 = { println!("Hello."); 5 };
注:
1,查了下PartialEq,可参考:https://segmentfault.com/a/1190000011929777。 简言之:PartialEq定义了部分相等,且这种相等具有对称性(a==b则b==a)、传递性(a==b且b==c,则a==c)。但它没有反身性(即a==a不成立。作为对比,Eq具有反身性)。回想前文的浮点数,NAN!=NAN,所以浮点数只实现了PartialEq。
2,rust的运算符,实现了一些trait。要进行自定义的运算符重载,也是实现这些trait。可参考阅读:https://blog.youkuaiyun.com/u010824081/article/details/78447453
语句
- 一个表达式总是会产生一个值,因此它必然有类型;语句不产生值,它的类型永远是()。如果把一个表达式加上分号,那么它就变成 了一个语句;如果把语句放到一个语句块中包起来,那么它就可以被当 成一个表达式使用。
- 语句(Statements)是执行一些操作但不返回值的指令。表达式(Expressions)计算并产生一个值。
- 使用
let
关键字创建变量并绑定一个值是一个语句。函数定义也是语句。//函数定义本身就是一个语句 fn main() { let y = 6; //这是一个语句 }
-
语句不返回值。 看下面的示例:
fn main() { let x = (let y = 6); //编译错误。let y = 6 是语句,并不返回值。 }
-
函数调用是一个表达式;宏调用是一个表达式;我们用来创建新作用域的大括号(代码块),
{}
,也是一个表达式。 -
表达式的结尾没有分号。再看个例子:
fn main() { let x = 5; let y = { let x = 3; x + 1 //这是一个表达式。如果在结尾加上分号,它就变成了语句,而语句不会返回值。 }; println!("The value of y is: {}", y); }
控制流
条件分支:if-else、if-else if-...-else。其中else可以省略。
- 条件必须是bool值。else if 如果比较多,可以考虑用match。以下是个示例:
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"); } }
- 在if语句中,后续的结果语句块要求一定要用大括号包起来,不能省略,以便明确指出该if语句块的作用范围。这个规定是为了避免“悬空 else”导致的bug。
- 条件表达式并未强制要求用小括号包起来;如果加上小括 号,编译器反而会认为这是一个多余的小括号,给出警告。(试了下的确如此)
- if-else结构还可以当表达式使用。
fn main() { let condition = true; let number = if condition { 5 } else { 6 }; println!("The value of number is: {}", number); }
if
分支和else
分支的结果,其类型必须匹配。
循环:loop
、while
和 for
- loop是个无限循环。可以用break或者continue跳出。与if结构一样,loop结构也可以作为表达式的一部分。以下是示例:
fn main() { let mut counter = 0; let result = loop { counter += 1; if counter == 10 { break counter * 2; //跳出时可以返回值 } }; println!("The result is {}", result); //输出20 }
-
break语句和continue语句还可以在多重循环中选择跳出到哪 一层的循环。我们可以在loop while for循环前面加上“生命周期标识符”。该标识符以单引号开头,在内部的循环中可以使用break语句选择跳出到哪一层。以下是示例。
fn main() { // A counter variable let mut m = 1; let n = 1; 'a: loop { if m < 100 { m += 1; } else { 'b: loop { if m + n > 50 { println!("break"); break 'a; } else { continue 'a; } } } } }
-
如果一个loop永远不返回,那么它的类型就是“发散类型”。示例如下:
fn main() { let v = loop {}; println!("{}", v); }
-
while语句中也可以使用continue和break来控制循环流程。
-
既然有while,为何还要loop?书中这么解释的:“主要原因在于,相比于其他的许多语言,Rust语言要 做更多的静态分析。loop和while true语句在运行时没有什么区别,它们主要是会影响编译器内部的静态分析结果”。看如下示例:
//编译可以通过 let x; loop { x = 1; break; } println!("{}", x) //编译通过不了。因为编译器会觉得while语句的执行跟条件表达式在运行阶段的值 有关,因此它不确定x是否一定会初始化 let x; while true { x = 1; break; } println!("{}", x);
注:
坦白讲,即使看了这样的解释,还没想明白哪个应用场景非用loop不可。感觉还真有点多余。后面如果有了新的理解,再更新这里。
- Rust中的for循环实际上是许多其他语言中的for-each循环。Rust中没 有类似C/C++的三段式for循环语句。以下是个示例。
fn main() { let a = [10, 20, 30, 40, 50]; for element in a.iter() { println!("the value is: {}", element); } }
- for循环的主要用处是利用迭代器对包含同样类型的多个元素的容器 执行遍历,如数组、链表、HashMap、HashSet等。在Rust中,我们可以 轻松地定制自己的容器和迭代器,因此也很容易使for循环也支持自定义 类型。
- for循环内部也可以使用continue和break控制执行流程。
注释
- 分为代码层面的注释和文档注释。
- 代码注释的格式:一种是用//开头的,是行注释。一种是/**/,是块注释。
- 文档注释的格式:///、//!、/**…*/、/*!…*/,
- 用///开头的文档被视为是给它后面的那个元素做的说明;//!开头的文档被视为是给包含这块文档的元素做的说 明。/**…*/和/*!…*/也是类似的。
mod foo { //! 这块文档是给 `foo` 模块做的说明 /// 这块文档是给函数 `f` 做的说明 fn f() { // 这块注释不是文档的一部分 } }
-
文档内部支持markdown格式。可以使用#作为一级标题。比如标准 库中常用的几种标题:
/// # Panics /// # Errors /// # Safety /// # Examples
-
文档中的代码部分要用``符号把它们括起来。代码块应该用```括起 来。示例如下。Rust文档里面的代码块,在使用cargo test命令时,也是会被当做测 试用例执行的。这个设计可以在很多时候检查出文档和代码不对应的情况。
/// ``` /// let mut vec = Vec::with_capacity(10); /// /// // The vector contains no items, even though it has capacity for more /// assert_eq!(vec.len(), 0); /// ```
-
如果文档太长,也可以写在单独的markdown文件中。如果在单独 的文件中写文档,就不需要再用///或者//!开头了,直接写内容就可 以。然后再用一个attribute来指定给对应的元素:
#![feature(external_doc)] #[doc(include = "external-doc.md")] pub struct MyAwesomeType;
注:
利用cargo 创建文档注释的html、文档测试等相关知识点,这里跳过。后面和cargo一起说明。列个TODO项。