对比学习 Go 和 Rust:变量和常量

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

变量

变量的声明

直接通过示例来看 Go 和 Rust 中声明变量的方法吧。

Go:

var n int
var n1, n2 int
var n3 = 33
var n4 int = 33
var n5, n6 = 33, 44
n7 := 33
n8, n9 := 33, 44
var (
    n10 int
    n11 int
)
var (
    n12 = 33
    n13 = 44
)
var (
    n14 int = 33
    n15 int = 44
)

Rust:

let m = 33;
let n: i32 = 33;
let (n1, n2) = (33, 44);
let x;      // 尚未初始化变量,这时变量尚不可用
x = 55;     // 初始化变量
let y: i32; // 尚未初始化变量,这时变量尚不可用
y = 55;     // 初始化变量

在定义变量时,两种语言都可以不显式指定类型,这时编译器通过自动推导确定类型。

区别:

  • 最明显的区别,Go 声明变量用 var 关键字,而 Rust 则用 let
  • 当显式指定类型时,Go 的变量名和类型名称间用空格分隔,而 Rust 则用冒号分隔。
  • Go 可以在包的全局作用域中定义变量(即该变量不在任何函数中),而 Rust 则不可以在全局作用域中定义变量(但可以定义常量)。
  • Go 中可以通过省略 var 而使用 name := expression 的形式进行短变量声明(临时使用的局部变量常采用这种声明方法),而 Rust 则不能省略 let
  • Go 可以通过 var n5, n6 = 33, 44n8, n9 := 33, 44 的形式声明一个变量列表。Rust 表面上是不支持的,但能简单地通过元组来实现此功能,实际就是用圆括号把多个变量或表达式括起来形成一个元组,如 let (n1, n2) = (33, 44)
  • 当通过自动推导得出变量类型,且赋予变量的值是一个未显式标记类型的整数时,Go 默认类型为 int 类型,而 Rust 则默认类型为 i32 类型。
  • 未给 Go 中的变量赋值时,其具有默认的零值,如布尔变量的零值为 false,整数类型变量为 0,浮点数类型变量为 0.0,字符串为 "",切片、指针、函数、接口、映射、信道都为 nil,数组和结构体则根据其元素的具体类型初始化为零值;Rust 中,如果未初始化而使用变量,则将不能通过编译。相对来说,Go 的这种默认赋零值的做法简单实用,但不能应对所有的情况;而 Rust 则可以进一步结合 Option 枚举系统性地解决了空值的问题。

变量的可变性

在 Rust 中,有点让人不可思议的地方在于,默认情况下变量是不可变的。不可变变量看起来有点像常量,但却可以在运行过程中对其进行动态赋值。变量默认的不可变性有利于提高程序的安全性,尤其是并发安全性。想要使变量可变,需要在声明时在变量名前加 mut 关键字。

let a = 12.5;      // a 是不可变变量
// a = 14.0;       // 该行不能编译
let mut b = 2.5;   // b 是可变变量
b = 3.0;           // 该行没问题

变量遮蔽

在 Rust 中,同一名称的变量是可以反复声明的,并且可以改变类型。不,这只是变量遮蔽(shadow)的小把戏,其方法是通过用 let 关键字再次声明变量将上一个同名变量遮蔽起来。

let spaces = "   ";
let spaces = spaces.len();

在我看来,变量遮蔽会造成一定的混乱,这似乎是为那些不想花心思构想相似应用场景的多个变量名的懒人而设计的,除了个别的场合,还是应尽量避免滥用该功能。

常量

常量的声明和使用

Go 中声明常量和声明变量差不多,只不过要使用 const 关键字:

const IPv4Len = 4
const MaxNum uint = 100000 // 有类型常量
const m, n = "hello", 33
const (
    Ln10   = 2.30258509299404568401799145468436420760110148862877297603332790
    Log10E = 1 / Ln10
)
const (
    a = 1
    b        // 1
    c = 2
    d        // 2
)

Rust 中常量的定义就没有这样多样化了:

const IPV4_LEN: i32 = 4;
const MAX_NUM: u32 = 10_0000;
const LN10: f64 = 2.30258509299404568401799145468436420760110148862877297603332790;
const LOG10E: f64 = 1.0 / LN10;

Go 和 Rust 中的常量都使用 const 关键字声明,都是在编译期就计算出所赋值的常量表达式(可被编译器求解的表达式)的值。但两者之间又存在很大的区别,主要是 Go 支持无类型常量,而 Rust 不支持。在 Go 中,若常量出现时不指定类型,该常量为无类型常量。无类型常量仅仅是一个值,而没有一个明确的类型。无类型常量可以使我们非常自由地使用常量。以下各行在 Go 中都是合法的:

f := 44 + 2.0
var n int = 2.0
s := math.Sin(2)

但类似的代码在 Rust 中却全部无法编译通过:

let f = 44 + 2.0;
let n: i32 = 2.0;
let s1 = 3.sin();
let s2 = 3.0.sin();

有人可能奇怪以上 let s2 = 3.0.sin(); 为何无法编译通过,这是因为并不确定浮点值 3.0 是 f32 还是 f64 类型,将该行变为 let s2 = 3.0_f64.sin(); 就可以通过编译了。

Go 的无类型常量就如同生活在理想空间中,他们更少地受到类型系统的束缚,因而如我们直觉预期那样自由地参与运算。但在使用这些无类型常量时,最终都会将他们赋值给变量,这时他们就降落凡尘了,从而变成为指定类型或默认类型。这时如果大小超限,将会产生编译错误,如 fmt.Println(1e1000) 语句中,就存在着将 1e1000 赋值给函数参数的过程,因此会因溢出而无法编译。以下是有关 Go 和 Rust 常量的一些具体对比:

  • Go 的无类型数值常量的大小和精度可以比基本类型更高,如前面的 Ln10,这会使算术运算更加精确;Rust 中,声明常量时,必须标注其类型,上面 LN10 的精度将是 f64 所能表示的最大精度。
  • 在 Go 中,00.00i'x'true 的类型分别是无类型整数、无类型浮点数、无类型复数、无类型字符和无类型布尔值;在 Rust 中00.0'x'true 的类型将分别是 i32f64charbool

常量生成器 iota

在 Go 中,还可以通过常量生成器 iota 声明一系列相关的常量,iota 从 0 开始取值,逐项加 1。因为 Go 中没有枚举类型,因此常使用 iota 模拟创建枚举值,iota 也被称为枚举符。如下是Go 语言规范中所给出的一些示例:

type Weekday int

const (
    Sunday Weekday = iota
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
)

const (
    c0 = iota  // c0 == 0
    c1 = iota  // c1 == 1
    c2 = iota  // c2 == 2
)

const (
    a = 1 << iota  // a == 1  (iota == 0)
    b = 1 << iota  // b == 2  (iota == 1)
    c = 3          // c == 3  (iota == 2, unused)
    d = 1 << iota  // d == 8  (iota == 3)
)

const (
    u         = iota * 42  // u == 0     (untyped integer constant)
    v float64 = iota * 42  // v == 42.0  (float64 constant)
    w         = iota * 42  // w == 84    (untyped integer constant)
)

const x = iota  // x == 0
const y = iota  // y == 0

const (
    bit0, mask0 = 1 << iota, 1<<iota - 1  // bit0 == 1, mask0 == 0  (iota == 0)
    bit1, mask1                           // bit1 == 2, mask1 == 1  (iota == 1)
    _, _                                  //                        (iota == 2, unused)
    bit3, mask3                           // bit3 == 8, mask3 == 7  (iota == 3)
)

const (
    One   = 1
    Two   = 2
    Three = iota + 1 // 3
    Four             // 4
)

有时候,程序中或设置一些标记(flag)选项,这也可以通过 iota 枚举符配合位运算实现。以下是一个完整的例子:

package main

import "fmt"

const (
    Tall   = 1 << iota // 个子高吗?
    Fat                // 胖吗?
    Strong             // 强壮吗?
    Smooth             // 圆滑吗?
    Clever             // 聪明吗?
)

type Person struct {
    Name     string
    Features int
}

func main() {
    fmt.Printf("项目\t十进制\t二进制\n")
    fmt.Printf("身高:\t%d\t%b\n", Tall, Tall)
    fmt.Printf("胖瘦:\t%d\t%b\n", Fat, Fat)
    fmt.Printf("体格:\t%d\t%b\n", Strong, Strong)
    fmt.Printf("情商:\t%d\t%b\n", Smooth, Smooth)
    fmt.Printf("智商:\t%d\t%b\n", Clever, Clever)

    fmt.Println()
    p := new(Person)
    p.Name = "王小二"
    p.Features = Tall | Smooth | Clever
    fmt.Printf("过去的 %s 的特征值:\n\t%d\t%b\n", p.Name, p.Features, p.Features)

    fmt.Printf("过去的 %s 胖吗?\n", p.Name)
    checkFat(p.Features)
    fmt.Println()

    p.Features |= Fat // 无论原来胖瘦如何,现在都是胖子了
    fmt.Printf("现在 %s 的特征值:\n\t%d\t%b\n", p.Name, p.Features, p.Features)
    fmt.Printf("现在的 %s 胖吗?\n", p.Name)
    checkFat(p.Features)
    fmt.Println()

    p.Features &^= Fat // 无论原来胖瘦如何,现在都不再是胖子了
    fmt.Printf("将来 %s 的特征值:\n\t%d\t%b\n", p.Name, p.Features, p.Features)
    fmt.Printf("将来的 %s 胖吗?\n", p.Name)
    checkFat(p.Features)
}

func checkFat(features int) {
	if features&Fat == Fat {
        fmt.Println("是的,他是一个大胖子。")
    } else {
        fmt.Println("不,他一点都不胖。")
    }
}

以下是上述代码的运行结果:

项目    十进制  二进制
身高:  1       1
胖瘦:  2       10
体格:  4       100
情商:  8       1000
智商:  16      10000

过去的 王小二 的特征值:
        25      11001
过去的 王小二 胖吗?
不,他一点都不胖。

现在 王小二 的特征值:
        27      11011r
现在的 王小二 胖吗?
是的,他是一个大胖子。

将来 王小二 的特征值:
        25      11001
将来的 王小二 胖吗?
不,他一点都不胖。