对比学习 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      11011
现在的 王小二 胖吗?
是的,他是一个大胖子。

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