对比学习 Go 和 Rust:语句和表达式

2022年1月13日 // Go Rust 编程

语句和表达式是所有编程语言都要涉及的概念。Go 和 Rust 在语句和表达式的功能和使用要求方面存在微妙的差异,本文将就此主题进行简单的讨论,以便为后续的学习打下必要的基础。

简单来说,语句是程序要执行的操作,而表达式则代表一个可以通过计算而求得的值。也就是说,语句是用来执行的,而表达式是用来求值的。在多数编程语言中,语句和表达式的主要区别在于语句不返回结果,而表达式总是返回结果。另外,表达式通常处于语句中。

Go 的语句和表达式

Go 的语句和表达式与其他语言差别不大。因此,即便你跳过本节下面的内容,也不会有什么大的损失。

Go 的表达式

这里是 Go 中对表达式的严格定义:表达式通过对操作数应用运算符函数来指定值的计算。要理解什么是表达式,关键是理解什么是操作数。操作数是表达式中的各个基本值,就像是表达式中的子表达式,它包括:

操作数       = 字面量 | 具名操作数 | "(" 表达式 ")" .
字面量       = 基本字面量 | 复合字面量 | 函数字面量 .
基本字面量    = 整型字面量 | 浮点型字面量 | 虚数字面量 | rune 字面量 | 字符串字面量 .
具名操作数    = 标识符 | 限定符 .

从上可以看出,操作数可以是各种值的字面量,表示常量、变量、函数的非空白标识符,选择器(值.方法,或 值.字段),以及用圆括号包括的表达式。这些操作数本身也可以单独出现成为一个表达式。很显然,如果函数(或方法)调用没有返回值,就不能放在表达式中,因此这样的函数调用不是表达式,也不可以作为表达式的操作数。

大部分时候,表达式只表示一个值,即称为单值表达式。不过,Go 中的函数(或方法)调用能返回两个或两个以上的值,这样的函数调用可被称为多值表达式。另外,无赋值部分的信道接收运算(如 <-ch)也是表达式,该也可能是单值的或多值的。

Go 的语句

Go 的语句包括以下分类:声明语句(不包括短变量声明语句)、标签语句、简单语句、go 语句、return 语句、break 语句、continue 语句goto 语句、fallthrough 语句、块、if 语句、switch 语句、select 语句、for 语句、defer 语句。

其中简单语句包括空语句、表达式语句、发送语句、自增减语句、赋值语句和短变量声明语句。简单语句在一些地方有特殊的用途,主要是其中的短变量声明语句可以用于 ifforswitch 语句中进行状态的初始化。

表达式语句指的是整个语句由一个表达式构成,如下所示:

func main() {
	sayHello()
}

func sayHello() int {
	fmt.Println("Hello, world!")
	return 0
}

其中 sayHello() 是一个有返回值的表达式,但这里舍弃了此表达式的值,而只进行函数调用而使用其副作用,该语句就是表达式语句。

同样,无赋值部分的信道接收运算也可以用于语句上下文而成为表达式语句,如单独占一行的 <-ch

值得注意的是,自增(i++)和自减(i--)是语句而不是表达式,不能将他们当作表达式使用。

Rust 的语句和表达式

Rust 关于语句和表达式的定义和其他语言基本相似。但 Rust 是一种基于表达式的语言,它只有少数种类的语句,其他大多东西属于表达式。其中最特别的是,Rust 中的代码块属于表达式(即块表达式)而非语句,代码块、语句和表达式可以递归地彼此嵌套到任意深度。

Rust 中只有声明语句和表达式语句两种语句。其中声明语句用来声明一个名称,该名称可以表示变量(用 let 声明)和程序项,后者包括函数、结构体、类型、静态变量、trait、实现或模块等。表达式语句与 Go 中表达式语句相似,即对表达式求值并忽略其结果。表达式语句必须以分号 ; 结尾。不同于 Go 中只有函数调用和信道接收运算两种表达式语句,大多数表达式加上分号都可以变为表达式语句。但当忽略表达式值的时候,会收到编译器的警告。在 Go 中,语句结尾的分号 ; 可以省略,并且经常省略;Rust 语句末尾的分号则不可省略。

要弄清 Rust 中语句和表达式间的关系,理解代码块(块表达式)至为关键。Rust 允许如下形式的代码块:

let _v = {
    let x = 5;
    x * 6
};

上述变量 _v 前面的下划线允许我们不使用该变量。其中代码块的值就是其最后的不带分号行的表达式的值,即为 30,该值也将被赋给 _v

接下来我们把代码块中最后一行表达式加个分号,即:

let _v = {
    let x = 5;
    x * 6;
};

该代码块仍然能够执行,只是编译器会对 x * 6; 行产生警告:the arithmetic operation produces a value,这是因为在加分号后,原来的表达式变成了表达式语句,从而舍弃了该行中表达式的值。

那么,既然已经舍弃了 x + 6 的值,为什么又将代码块赋值给 _v 呢?这是因为该块表达式没有显式地返回一个值,那么将默认返回单元类型的单元值 ()。这相当于编译器自动在最后一个语句后面添加了单元值 () 实现的。因此,以上代码等价于:

let _v = {
    let x = 5;
    x * 6;
    ()
};

看到这里,我们自然会想到,函数、各种控制流语句中都有大括号限定的代码块,这些代码块都会返回值吗?没错的,他们都会返回值。不仅如此,其中很多带有块的东西还是表达式,如 if 表达式、if let 表达式、match 表达式、各种循环表达式,另外还有一些特殊的块,如异步块和非安全块。这些东西,在 Go 语言中都属于语句,但在 Rust 中却变成了表达式。既然是表达式,我们就可以将他们灵活地嵌套在表达式和语句中,例如:

let x = 11;
let s = if x % 2 == 0 { "偶数" } else { "奇数" };

该段代码将一个 if 表达式赋值给了 s

基于同样的规则,函数体的大括号也属于块表达式,块表达式的值就是该函数的返回值。即便函数没有显式地返回任何值,它也隐式地返回了一个单元值 ()

运算符及其优先级

这里仅针对原始类型,讨论运算符的功能。

Go 和 Rust 的运算符非常相似,这

算术运算符

运算 Go 运算符 Rust 运算符 示例 结果
+ + 2 + 3 5
- - 3.2 - 1.0 2.2
* *
/ / -5.0 / 3.05 / 3 -5 / 3 -1.66666666666666671-1
取余 % %

要点:

  • 没有取幂运算,因此要计算 33 ,应该使用 math.Pow(3.0, 3.0)(Go)、3.0.powi(3.0)(Rust)或 3.0.powf(3.0)(Rust)。

https://go.dev/ref/spec#Operators

https://doc.rust-lang.org/reference/expressions/operator-expr.html

位运算符

运算 Go 运算符 Rust 运算符 示例 结果
按位(AND) & & 0b1010 & 0b1100 0b1000
按位(OR) | | 0b1010 | 0b1100 0b1110
按位异或(XOR) ^ ^ 0b1010 ^ 0b1100 0b0110
按位清除(与非) &^ 0b1010 &^ 0b1100 0b0010
左移位 « « 0b1010 « 2 0b101000
右移位 » » 0b1010 » 1 0b0101

以上各个位运算符中,除了按位清除(与非)之外,其他的都是我们熟悉的。这里仅就按位清除(与非)进行举例说明。在 Linux 操作系统中,文件和目录的绝对权限(读、写、执行)可用最多 4 位的 8 进制数来表示,而日常一般用 3 位八进制数表示,这三位从左到右分别表示用户、用户组、其他用户操作文件或目录的权限,可以使用 chmod 命令更改这些权限。另外,Linux 可以使用 umask 命令设置或显示文件或目录的默认权限模式。umask 是一个二进制值(经常以八进制形式呈现),它通过二进制的按位清除(与非)运算,结合文件的八进制值 0666 和目录的 0777 来定义其默认保护模式。以下代码模拟文件权限模式的计算过程(仅模拟用户、用户组、其他用户的权限):

const (
    OtherRead = 1 << iota
    OtherWrite
    OtherExecute
    GroupRead
    GroupWrite
    GroupExecute
    UserRead
    UserWrite
    UserExecute
)
// 对应 Linux 系统中 umask 常用预设值 022(八进制)
umask := GroupWrite | OtherWrite
// 对应文件的初始权限值 666(八进制)
file_mod_init := UserExecute | UserWrite | GroupExecute | GroupWrite | OtherExecute | OtherWrite
// 对应目录的初始权限值 777(八进制)
dir_mod_init := UserExecute | UserWrite | UserRead | GroupExecute | GroupRead | GroupWrite | OtherExecute | OtherWrite | OtherRead
file_mod := file_mod_init &^ umask // 644
dir_mod := dir_mod_init &^ umask   // 755
fmt.Printf("File mod:\n   %03o\n&^ %03o\n = %03o\n\n", file_mod_init, umask, file_mod)
fmt.Printf("Dir mod:\n   %03o\n&^ %03o\n = %03o\n", dir_mod_init, umask, dir_mod)

运行结果:

File mod:
   666
&^ 022
 = 644

Dir mod:
   777
&^ 022
 = 755

赋值运算符

运算 Go 运算符 Rust 运算符 示例 结果
简单的赋值运算 == == 32 == 32 true
加赋值 != != 32 != 32 false
减赋值 < < 23.4 < 36.7 true
乘赋值 <= <= 23.4 <= 36.7 true
除赋值 > > 23.4 > 36.7 false
取余数赋值 >= >= 23.4 >= 23.2 true

字符串拼接

n := 2
s := "我有 " + strconv.Itoa(n) + " 个手机。"
fmt.Println(s)

比较运算符

运算 Go 运算符 Rust 运算符 示例 结果
等于 == == 32 == 32 true
不等于 != != 32 != 32 false
小于 < < 23.4 < 36.7 true
小于或等于 <= <= 23.4 <= 36.7 true
大于 > > 23.4 > 36.7 false
大于或等于 >= >= 23.4 >= 23.2 true

逻辑运算符