语句和表达式
1.语句与表达式
在 Rust 中,语法可以分为两大类: 语句 与 表达式,
语句是指要执行的一些操作以及产生副作用的表达式
表达式主要用于计算并返回一个值,表达式不带有分号
若一个表达式以分号结尾,那么它变为一个语句
例如:
x // 表达式
1 // 表达式
1 + 1 // 表达式
1 + x // 表达式
add(x, y) // 表达式
let x = 100; // 语句
x; // 语句
1 + x; // 语句
x = 1 + x; // 语句- 对于一个表达式,一定会得到一个结果
- 而对于一个语句,它往往是为了完成某些功能,比如赋值。
比如对于语句 x = 1 + x;,假设初始 x = 100。那么=右侧的1 + x就是一个表达式,它求出一个值 101,对于整个带分号的条目,叫做一个语句,它完成的是赋值的功能。
1.1 一切皆表达式
语句可以进一步分为两种:声明语句和表达式语句
1.声明语句:用于声明各种语言项,比如声明变量、声明结构体,函数,引入包和模块等
对于声明语句,它分号前不产生任何值,它用于完成某些 Rust 语言级别的功能,比如声明变量let x = 1;,引入模块 use std::collections;。
2.表达式语句:以分号结尾的表达式
对于表达式语句,由表达式产生一个值,在产生值的过程中可能导致副作用,在整个表达式末尾加一个分号形成语句,此时语句会把表达式产生的值丢弃,只保留副作用。
例如 x + 1,它是一个表达式,整体返回的是一个加法的结果值。如果我在后面添加一个分号 x + 1;,此时就构成一个语句,这个语句把计算结果丢掉了,但是它没有产生副作用,因此它没有任何意义。
再比如 printl!("hello!");,分号前面是一个函数调用表达式,它会产生一个单元值(),可以理解为一个空值,这个后续会讲解。加了一个分号后形成语句,此时把函数生成的值丢掉了。但是这个函数的副作用是往显示器上打印一个hello!,因此把表达式变成语句,可以只关注他的副作用,而忽略结果。
再比如 x = 1,这是一个赋值表达式,整体返回一个单元值(),由于分号,它被忽略了。但是它的副作用是把x赋值,这就是一个有意义的表达式语句。
在 Rust 中,一切皆表达式
对这句话的理解,将很大程度影响后续你对其它模块的学习,它是一切Rust语法的基石。
1.2 块表达式
如果你有其它语言的学习经验,那么你很可能听过 if 语句 这样的表述,但是在 Rust 中,if 是一个表达式,例如:
if x == 1 {
println!("hello!");
}也许你还没学到 if,以上代码的含义是:假如 x值为 1,那么打印一个hello!到屏幕上。你可以回看我刚才对声明语句的定义,请问if是一个声明语句吗?很明显不是,所以if是一个表达式。
此处的逻辑为:Rust只有两种语句:声明语句和表达式语句,只要不是声明语句,那么就是表达式语句,表达式语句前面一定是一个表达式,表达式一定会返回一个值。由此得出结论:if 是一个表达式,且会返回一个值!
因此你可以写出以下代码:
let ret = if x == 1 {
println!("hello!");
};看花眼了吧?这里其实就是在之前代码的前面加了一个 let ret = ,也就是进行赋值,用一个变量把if 的返回值接收起来。此处 if 返回的是一个单元值(),因此ret 就是 ()。
对于这种使用 {} 框起来的代码,Rust 把它视为一个表达式,称为块表达式,返回的值等于 { } 最后一个表达式的值。
比如说:
let ret = if x == 1 {
println!("hello!");
2025
};聪明的你肯定理解了,ret拿到的值是2025,因为它是 { } 内最后一个表达式。
现在有几个需要额外说明的小细节:
1.最后一个 2025 可以带分号吗?
不可以,如果2025;带上分号就变成一个表达式语句,此时 { } 最后一个值不是一个表达式,就会返回一个单元值()。
2.为什么 { } 内最后一行是语句的时候,会返回单元值()?
回看这个例子:
let ret = if x == 1 {
println!("hello!");
};我说ret会接受到一个(),可是为什么?
这是因为:当Rust解析代码时,会一条一条语句进行执行,每当看到;就视为一个语句结束,下一条语句开始,此时分为三种情况:
- 假如
;后面是一个表达式,就对表达式求值 - 假如
;后面是一个语句,就执行该语句 - 假如
;后面什么也没有,此时Rust自动补上一个()表达式
因此以上代码等效于:
let ret = if x == 1 {
println!("hello!");
()
};当Rust解析代码,发现println!("hello!");语句结束,开始解析下一条语句,但是后面啥也没有了,于是最后补上一个()。
这么做的目的,是因为表达式必须返回一个值,{ } 是一个表达式,它必须保证最后有一个值返回给外部。
包括我之前说把2025改成2025;的例子,修改后也会返回(),就是因为在;后面会偷偷加上一个()。
3.为什么if末尾没有分号
对于if,它可以像下面这样使用:
let x = 1;
if x == 1 {
x *= 10;
} // 这里没有分号
println!("world!");问题在于,为什么if末尾没有分号?如果它没有分号就是一个表达式,岂不是if 和 println 整体构成一个语句?
这里其实也是一个Rust底层的优化。在很多其它语言中,if本身就是一个语句,它末尾不需要;也会构成语句。因此大部分其他语言的习惯是if末尾不写分号。
对于Rust来说,如果强制所有用户if末尾写一个分号,可能会有些别扭。因此当if表达式单独出现的时候,Rust会在其末尾偷偷补一个分号,不需要用户自己写。从语法规则看,这是因为 Rust 允许控制流表达式直接作为语句出现,相当于结果被丢弃。
以上代码等效于:
let x = 1;
if x == 1 {
x *= 10;
}; // 这里有一个分号
println!("world!");但是以下写法是非法的:
let x = 1;
let ret = if x == 1 {
println!("hello!");
} // 这里没有分号
println!("world!");因为第二行整个区域已经不是一个if表达式了,if并不是单独出现的,而是一个声明,此时Rust不会在末尾补充;,导致println与let挤在一个语句内部,编译器报错。
总结:
Rust只有表达式和语句,语句也只分为声明语句和表达式语句
- 换个说法:
rust只由表达式、表达式语句、声明语句构成
- 换个说法:
一个表达式有
;就会转变为语句对于后续的
if、loop之类的特例,它们没有分号,是一个表达式。但是它们单独出现的时候,会被Rust偷偷补上;变成语句,这没有违背前两条规则
2.变量与绑定
在Rust中,使用let关键字来声明一个变量,这借鉴了其他的函数式语言风格,因为Rust也是带有函数式风格的一门语言。
例如:
let x = 1;就是声明了一个叫做 x 的变量,它指向的内存存储了1这个值。
默认情况下,你的绑定代表某个位置,但这个位置是只读的:
let a = 1;
a = 2; // error: a 是不可变绑定对于直接使用let绑定的变量,是不可修改的,称为不可变绑定。
使用let mut 声明一个可变绑定:
let mut b = 2;
b = 3; // 合法,可变绑定这其实是Rust安全性的一种体现,默认不可变。
对于大多数情况下,很多程序其实是在访问不会变化的数据,较少的情况才会对数据修改,也就是读的频率大于写的频率。
但是很多其它语言,声明一个不可变的值会比可变的值更复杂,例如C++:
int x = 1; // 可变
const int y = 2; // 不可变这会导致很多程序员不管值未来是否变化,都直接声明为可变的,从代码逻辑上没什么影响,但是这往往带来安全隐患。
所以Rust把不可变作为默认行为,如果你需要可变,需要付出额外写一个关键字的成本,从心理上就让程序员尽可能把变量声明为不可变,减少了安全隐患。
2.1 变量遮蔽
Rust允许用相同的名字声明新变量,新变量会遮蔽(shadow)之前的变量:
遮蔽是重新绑定,不是修改原变量。它常用在更小的作用域里用计算后的新值而不污染外层作用域。与可变不同,遮蔽可以改变类型。
let x = 5;
let x = x + 1; // 遮蔽前一个x,创建新变量
println!("{}", x); // 6
{
let x = x * 2; // 在内部作用域中遮蔽
println!("{}", x); // 12
}
println!("{}", x); // 6,外层作用域的x- 遮蔽:创建新变量,可以改变类型
- 可变性:修改同一变量的值,类型不能改变
// 遮蔽:可以改变类型
let spaces = " "; // &str 类型
let spaces = 10; // usize 类型
// 可变性:不能改变类型
let mut spaces = " ";
spaces = 10; // 错误:类型不匹配3.初始化
初始化一个变量与大多数语言一致,在声明时通过赋值操作符 = 给变量一个初始值。
let x = 1;在 Rust 中,一个变量必须进行初始化,否则编译不通过。
3.1 延迟初始化
Rust 允许用户延迟初始化一个变量。延迟初始化常用于先声明、后在分支或计算结果确定后再赋值的场景。
let x; // 只声明,不初始化
x = 42; // 稍后初始化
println!("x = {}", x); // 合法即使变量是不可变的(没有mut),延迟初始化仍然合法,变量的不可变性指的是初始化后不能重新赋值 。
这个延迟初始化可以延迟到同作用域内的任意位置,但是在多分支情况下,必须保证每个分支都会对该变量进行初始化。
示例:
let bl = false;
let x;
if bl {
x = 10;
} else {
x = 101;
}以上代码中,不论bl的值为多少,最后x一定会完成初始化,因此合法。
此外,对于延迟初始化,还要保证每一个分支的初始值类型是相同的。
if bl {
x = 10;
} else {
x = 3.14;
}以上代码就是非法的,因为编译器最后无法确定x的最终类型。10默认为数字类型,而3.14是浮点型,它们类型不一致。
4.类型系统与类型标注
与绝大多数语言一样,Rust也提供类型系统,每个变量都有指定的类型。Rust是静态类型语言,编译时必须知道所有变量的类型。 Rust提供了两种机制在编译期得知变量的类型:
- 类型推断:编译器智能推断类型
- 显式标注:程序员明确指定类型
4.1 类型推导
绑定变量时,编译器可以根据值和使用上下文推断类型:
let x = 5; // 推断为 i32(整数默认类型)
let y = 3.14; // 推断为 f64(浮点数默认类型)
let z = true; // 推断为 bool
let s = "hello"; // 推断为 &str这种推断语法,可以大部分类型简单的场景下简化编码,不用为每一个变量指明类型。
延迟初始化时,也可以触发类型推断:
let number; // 类型未知
number = 42; // 推断为 i32
let data; // 类型未知
data = String::from("hello"); // 推断为 StringRust 的推断是局部的:不会跨函数、跨模块进行全局推断。当上下文不足时,必须显式标注类型或使用类型后缀。
4.2 显式类型标注
在部分场景下,编译器无法很好的确定一个变量的最终类型,此时就需要程序员进行手动标注,语法如下:
let 变量名: 类型 = 值;示例:
let x: i32 = 5; // 明确指定为 i32
let y: f32 = 3.14; // 明确指定为 f32