Skip to content

语句和表达式

1.语句与表达式

Rust 中,语法可以分为两大类: 语句表达式

  • 语句是指要执行的一些操作以及产生副作用的表达式

  • 表达式主要用于计算并返回一个值,表达式不带有分号

  • 若一个表达式以分号结尾,那么它变为一个语句

例如:

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 是一个表达式,例如:

rust
if x == 1 {
	println!("hello!");
}

也许你还没学到 if,以上代码的含义是:假如 x值为 1,那么打印一个hello!到屏幕上。你可以回看我刚才对声明语句的定义,请问if是一个声明语句吗?很明显不是,所以if是一个表达式。

此处的逻辑为:Rust只有两种语句:声明语句和表达式语句,只要不是声明语句,那么就是表达式语句,表达式语句前面一定是一个表达式,表达式一定会返回一个值。由此得出结论:if 是一个表达式,且会返回一个值!

因此你可以写出以下代码:

rust
let ret = if x == 1 { 
	println!("hello!"); 
};

看花眼了吧?这里其实就是在之前代码的前面加了一个 let ret = ,也就是进行赋值,用一个变量把if 的返回值接收起来。此处 if 返回的是一个单元值(),因此ret 就是 ()

对于这种使用 {} 框起来的代码,Rust 把它视为一个表达式,称为块表达式,返回的值等于 { } 最后一个表达式的值。

比如说:

rust
let ret = if x == 1 {
	println!("hello!"); 
	2025
};

聪明的你肯定理解了,ret拿到的值是2025,因为它是 { } 内最后一个表达式。

现在有几个需要额外说明的小细节:

1.最后一个 2025 可以带分号吗?

不可以,如果2025;带上分号就变成一个表达式语句,此时 { } 最后一个值不是一个表达式,就会返回一个单元值()

2.为什么 { } 内最后一行是语句的时候,会返回单元值()

回看这个例子:

rust
let ret = if x == 1 { 
	println!("hello!"); 
};

我说ret会接受到一个(),可是为什么?

这是因为:当Rust解析代码时,会一条一条语句进行执行,每当看到;就视为一个语句结束,下一条语句开始,此时分为三种情况:

  • 假如;后面是一个表达式,就对表达式求值
  • 假如;后面是一个语句,就执行该语句
  • 假如;后面什么也没有,此时Rust自动补上一个()表达式

因此以上代码等效于:

rust
let ret = if x == 1 { 
	println!("hello!"); 
	()
};

Rust解析代码,发现println!("hello!");语句结束,开始解析下一条语句,但是后面啥也没有了,于是最后补上一个()

这么做的目的,是因为表达式必须返回一个值,{ } 是一个表达式,它必须保证最后有一个值返回给外部。

包括我之前说把2025改成2025;的例子,修改后也会返回(),就是因为在;后面会偷偷加上一个()

3.为什么if末尾没有分号

对于if,它可以像下面这样使用:

rust
let x = 1;

if x == 1 {
	x *= 10;
} // 这里没有分号

println!("world!");

问题在于,为什么if末尾没有分号?如果它没有分号就是一个表达式,岂不是ifprintln 整体构成一个语句?

这里其实也是一个Rust底层的优化。在很多其它语言中,if本身就是一个语句,它末尾不需要;也会构成语句。因此大部分其他语言的习惯是if末尾不写分号。

对于Rust来说,如果强制所有用户if末尾写一个分号,可能会有些别扭。因此当if表达式单独出现的时候,Rust会在其末尾偷偷补一个分号,不需要用户自己写。从语法规则看,这是因为 Rust 允许控制流表达式直接作为语句出现,相当于结果被丢弃。

以上代码等效于:

rust
let x = 1;

if x == 1 {
	x *= 10;
}; // 这里有一个分号

println!("world!");

但是以下写法是非法的:

rust
let x = 1;

let ret = if x == 1 { 
	println!("hello!"); 
} // 这里没有分号

println!("world!");

因为第二行整个区域已经不是一个if表达式了,if并不是单独出现的,而是一个声明,此时Rust不会在末尾补充;,导致printlnlet挤在一个语句内部,编译器报错。

总结:

  1. Rust只有表达式和语句,语句也只分为声明语句和表达式语句

    • 换个说法: rust只由表达式表达式语句声明语句构成
  2. 一个表达式有;就会转变为语句

  3. 对于后续的 ifloop 之类的特例,它们没有分号,是一个表达式。但是它们单独出现的时候,会被Rust偷偷补上;变成语句,这没有违背前两条规则

2.变量与绑定

Rust中,使用let关键字来声明一个变量,这借鉴了其他的函数式语言风格,因为Rust也是带有函数式风格的一门语言。

例如:

rust
let x = 1;

就是声明了一个叫做 x 的变量,它指向的内存存储了1这个值。

默认情况下,你的绑定代表某个位置,但这个位置是只读的:

rust
let a = 1;
a = 2; // error: a 是不可变绑定

对于直接使用let绑定的变量,是不可修改的,称为不可变绑定

使用let mut 声明一个可变绑定

rust
let mut b = 2; 
b = 3; // 合法,可变绑定

这其实是Rust安全性的一种体现,默认不可变。

对于大多数情况下,很多程序其实是在访问不会变化的数据,较少的情况才会对数据修改,也就是读的频率大于写的频率。

但是很多其它语言,声明一个不可变的值会比可变的值更复杂,例如C++

cpp
int x = 1; // 可变
const int y = 2; // 不可变

这会导致很多程序员不管值未来是否变化,都直接声明为可变的,从代码逻辑上没什么影响,但是这往往带来安全隐患。

所以Rust把不可变作为默认行为,如果你需要可变,需要付出额外写一个关键字的成本,从心理上就让程序员尽可能把变量声明为不可变,减少了安全隐患。


2.1 变量遮蔽

Rust允许用相同的名字声明新变量,新变量会遮蔽(shadow)之前的变量:

遮蔽是重新绑定,不是修改原变量。它常用在更小的作用域里用计算后的新值而不污染外层作用域。与可变不同,遮蔽可以改变类型

rust
let x = 5;
let x = x + 1; // 遮蔽前一个x,创建新变量
println!("{}", x); // 6

{
    let x = x * 2; // 在内部作用域中遮蔽
    println!("{}", x); // 12
}

println!("{}", x); // 6,外层作用域的x
  • 遮蔽:创建新变量,可以改变类型
  • 可变性:修改同一变量的值,类型不能改变
rust
// 遮蔽:可以改变类型
let spaces = "   "; // &str 类型
let spaces = 10; // usize 类型

// 可变性:不能改变类型
let mut spaces = "   ";
spaces = 10; // 错误:类型不匹配

3.初始化

初始化一个变量与大多数语言一致,在声明时通过赋值操作符 = 给变量一个初始值。

rust
let x = 1;

Rust 中,一个变量必须进行初始化,否则编译不通过。


3.1 延迟初始化

Rust 允许用户延迟初始化一个变量。延迟初始化常用于先声明、后在分支或计算结果确定后再赋值的场景。

rust
let x;        // 只声明,不初始化
x = 42;       // 稍后初始化
println!("x = {}", x); // 合法

即使变量是不可变的(没有mut),延迟初始化仍然合法,变量的不可变性指的是初始化后不能重新赋值

这个延迟初始化可以延迟到同作用域内的任意位置,但是在多分支情况下,必须保证每个分支都会对该变量进行初始化。

示例:

rust
let bl = false;
let x;

if bl {
	x = 10;
} else {
	x = 101;
}

以上代码中,不论bl的值为多少,最后x一定会完成初始化,因此合法。

此外,对于延迟初始化,还要保证每一个分支的初始值类型是相同的。

rust
if bl {
	x = 10;
} else {
	x = 3.14;
}

以上代码就是非法的,因为编译器最后无法确定x的最终类型。10默认为数字类型,而3.14是浮点型,它们类型不一致。

4.类型系统与类型标注

与绝大多数语言一样,Rust也提供类型系统,每个变量都有指定的类型。Rust是静态类型语言,编译时必须知道所有变量的类型。 Rust提供了两种机制在编译期得知变量的类型:

  1. 类型推断:编译器智能推断类型
  2. 显式标注:程序员明确指定类型

4.1 类型推导

绑定变量时,编译器可以根据使用上下文推断类型:

rust
let x = 5;          // 推断为 i32(整数默认类型)
let y = 3.14;       // 推断为 f64(浮点数默认类型)
let z = true;       // 推断为 bool
let s = "hello";    // 推断为 &str

这种推断语法,可以大部分类型简单的场景下简化编码,不用为每一个变量指明类型。

延迟初始化时,也可以触发类型推断:

rust
let number;         // 类型未知
number = 42;        // 推断为 i32

let data;           // 类型未知
data = String::from("hello"); // 推断为 String

Rust 的推断是局部的:不会跨函数、跨模块进行全局推断。当上下文不足时,必须显式标注类型或使用类型后缀。


4.2 显式类型标注

在部分场景下,编译器无法很好的确定一个变量的最终类型,此时就需要程序员进行手动标注,语法如下:

rust
let 变量名: 类型 = 值;

示例:

rust
let x: i32 = 5;    // 明确指定为 i32
let y: f32 = 3.14; // 明确指定为 f32

MIT Licensed