对比学习 Go 和 Rust:原始类型

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

数据类型又称类型,是数据的一种属性,它决定了值的分类,如何解释保存值的内存,以及可以对值执行的操作(包括值具有的方法)。编程语言通常包含三类类型:内建的原始类型,如各种数值类型、布尔类型、字符串类型;基于原始类型构建的复合类型,如数组、切片、结构体等;与各种数据结构模型对应的抽象类型,如列表、堆栈、树、映射、图等。请注意这些划分并不是绝对的,如我们也可以认为字符串是切片,属于复合类型,同时认为映射也属于复合类型。语言的类型系统将类型与计算值相关联,通过检查这些值的流,保证不会发生类型错误。在编程时,必须充分了解语言的类型和类型系统。

Go 和 Rust 都是静态类型语言,从而在程序编译时需要知道所有变量的类型。本文将介绍除了字符串类型之外的其他原始类型,这些类型在类型系统中最为简单。同时由于他们都表示单个值,因此也可称为标量类型

标量类型列表

Go 和 Rust 中有如下表所示的标量类型:

Go 类型 Rust 类型 说明
uint8, byte u8 8 位无符号整数
uint16 u16 16 位无符号整数
uint32 u32 32 位无符号整数
uint64 u64 64 位无符号整数
u128 128 位无符号整数
int8 i8 8 位有符号整数
int16 i16 16 位有符号整数
int32, rune i32 32 位有符号整数
int64 i64 64 位有符号整数
i128 128 位有符号整数
float32 f32 32 位浮点数
float64 f64 64 位浮点数
complex64 64 位复数
complex128 128 位复数
uint usize 无符号整数,长度视架构而定
int isize 有符号整数,长度视架构而定
uintptr
char Unicode 字符
bool bool 布尔类型

说明:

  • 每个有符号整数类型的大小范围是 -(2n - 1 ) ~ 2n - 1 - 1,无符号整数类型的大小范围是 0 ~ 2n - 1,其中 n 是该定义形式的位长度。
  • 单精度 32 位浮点数至少有 6 位有效数字,数值范围为 -3.4×1038 ~3.4×1038 ;双精度 64 位浮点数至少有 15 位有效数字,数值范围为 -1.8×10308 ~1.8×10308
  • 一个 complex64 复数值的实部和虚部都是 float32 类型的值;一个 complex128 复数值的实部和虚部都是 float64 类型的值。
  • Go 中的 intuint 类型分别对应 Rust 中的 isizeusize,它们的长度依赖运行程序的计算机架构:64 位架构上它们是 64 位的, 32 位架构上它们是 32 位的。在 Rust 中,isizeusize 类型一般用作某些集合的索引。
  • 在 Go 中,uintptr 类型类似 uint 类型,不同之处是它总是能保存任意的内存地址,可以将其应用于指针相关的操作。

Go 和 Rust 的以上数据类型存在以下方面的重要区别:

  • 如果较难确定究竟该使用何种整数类型,在 Go 中一般使用 int 类型,而在 Rust 中则一般使用 i32 类型,这也分别是两种语言中整型的默认类型。
  • Rust 内建有 128 位的整型 i128u128,Go 没有。
  • Go 内建有复数类型complex64complex128,Rust 没有。

字面量表示

两种语言都用 truefalse 表示布尔类型字面量,这里就不再多做介绍了。两种语言在表示数值字面量时,都可以用下划线 _ 作为可视分隔符,如 10_0003.141_59

两种语言的一个重要区别是:Go 中的标量字面量是无类型的,而 Rust 中的标量字面量具有默认类型,这在前面变量与常量一文已经讨论过。在 Rust 中,除了字节字面量外,其他数字字面量都可以通过添加类型后缀显式指定其类型,如 10_000i323.141_59_f32,Go 不支持(也没有必要支持)这种方法。

整数字面量

字面量指值的字面形式。整数类型的字面量可以是十进制、二进制、八进制或十六进制的形式。

在 Go 中,各种进制字面量的表示方法为:

  • 十进制表示时,除了数字 0,其他整数不能以 0 开头,如 0、13、267;
  • 二进制表示时,以 0b0B 开头,如 0b0、0B1101、0B100001011;
  • 八进制表示时,以 00o0O 开头,如 0o0、015、0O413;
  • 十六进制表示时,以 0x0X 开头,如0x0、0Xd、0x10b。

在 Go 中,以下三行将全部输出 true

println(13 == 0b1101) // true
println(13 == 015)    // true
println(13 == 0Xd)    // true

在 Rust 中,各种进制字面量的表示方法为:

  • 十进制表示时,可以以 0 开头,如 0、013、267;
  • 二进制表示时,以 0b 开头,如 0b0、0b1101、0b100001011;
  • 八进制表示时,以 0o 开头,如 0o0、0o15、0o413;
  • 十六进制表示时,以 0x 开头,如0x0、0xd、0x10b。

在 Rust 中,以下三行将不会打印出错信息:

assert_eq!(13, 0b1101);
assert_eq!(13, 0o15);
assert_eq!(013, 0xd);

可以看出,Go 中可以以 0B00O0X 开头表示二进制、八进制及十六进制整数字面量,Rust 不支持这些。另外,在 Rust 中,以 0 开头的整数(如 013)不是 Go 中的八进制数,而是十进制数。

浮点数字面量

根据 Go 语言规范,Go 支持以下十进制形式的浮点数字面量:

0.
72.40
072.40       // == 72.40
2.71828
1.e+0
6.67428e-11
1E6
.25
.12345E+5
1_5.         // == 15.0
0.15e+0_2    // == 15.0

在 Rust 中,这些浮点字面量并没有获得全部的支持。其中 Rust 不支持 .25 形式的浮点字面量,要求必须有整数部分,即应写作 0.25,同样,.12345E+5 也应该写作 0.12345E+5;另外,1.e+0 中,e 前面不能紧临小数点,应写成 1.0e+01e+0

Go 还支持十六进制形式的浮点数表示。十六进制浮点字面量从左到右分别是:0x0X、十六进制的整数部分、小数点、十六进制的小数部分、指数部分(pP 紧跟一个可选的正负号及十进制数字)。整数部分和小数部分可以省略其一,小数点也可以省略,而指数部分不可省略。该浮点数的值由 pP 前面的有效数字乘以以 2 为底的指数函数 2exp 值,其中 exp 为指数部分的值。如下示例:

0x1p-2       // == 0.25
0x2.p10      // == 2048.0
0x1.Fp+0     // == 1.9375
0X.8p-0      // == 0.5
0X_1FFFP-16  // == 0.1249847412109375
0x15e-2      // == 0x15e - 2 (整数减法)

Rust 中还没有内建对十六进制浮点数的支持。

在 Go 中,以下浮点数字面量都是无效的(但有些在 Rust 中却是有效的):

0x.p1        // 无效:有效数字部分没有数字
1p-2         // 无效:p 指数前面应有十六进制的有效数字
0x1.5e-2     // 无效:十六进制的有效数字后应有 p 指数
1_.5         // 无效:_ 前后必须为数字
1._5         // 无效:_ 前后必须为数字
1.5_e1       // 无效:_ 前后必须为数字
1.5e_1       // 无效:_ 前后必须为数字
1.5e1_       // 无效:_ 前后必须为数字

字符字面量

Go 中的 rune 类型表示一个 Unicode 码点(或称码位),而 Rust 中的 char 类型表示一个 Unicode 标量值,因此两者是有些微区别的。Unicode 码点是在码空间中的任意值,其取值范围使 0x00000x10FFFF。而 Unicode 标量值是除了高代理码点和低代理码点之外的码点,范围是 0x00000xD7FF0xE0000x10FFFF。在 Rust 中,如果一个 char 值超过此范围,它将立刻变成未定义状态。而在 Go 中,如果一个 rune 值超过此范围,该值仍然有效,但作为 Unicode 字符,它实际上并不表示任何字符。

在 Go 中,表示 rune 类型的字面量时,最常见的方式是用一对单引号直接包括该字符,如 'a''我' 等。也可以用如下转义方式:

  • \x 后紧跟 2 个十六进制数字;
  • \u 后紧跟 4 个十六进制数字;
  • \U 后紧跟 8 个十六进制数字;
  • \ 后紧跟 3 个八进制数字,可表示的值范围为 [0, 255]。

根据以上规则,在 Go 中,'a'97'\x61''\u0061''\U00000061''\141' 都表示相同的字符,而 '我'25105'\u6211''\U00006211' 也表示相同的字符。如果通过以上 4 种转义方式表示字符,当值超出 Unicode 标量值范围时,将无法编译。如以下语句:

a := '\uD800'

将会导致编译错误:“escape is invalid Unicode code point U+D800”。不过以下语句却是有效的:

var a rune = 0xD800

在 Rust 中,同样可以用单引号包括字符来表示 char 类型的字面量,如 'a''我'。也可以用转义方式,方法为:

  • \x 后紧跟 2 个十六进制数字;
  • \u 后紧跟用花括号对 {} 包括的 1~6 个十六进制数字。

根据以上规则,在 Rust 中,'a''\x61''\u{61}''\u{061}''\u{0061}''\u{00061}''\u{000061}' 都表示相同的字符,而 '我''\u{6211}''\u{06211}''\u{006211}' 也表示相同的字符。同样,如下语句:

let a = '\u{D800}';

将会导致编译错误:“error: invalid unicode character escape”。

总的来说,Go 中的 rune 就是一个 32 位有符号整数 int32,可以很方便地参加一些数值运算,而 Rust 对 char 类型做了较多的封装,能始终确保其有效是有效的 Unicode 标量值。

在 Go 和 Rust 中,还有另外一些特殊值能用反斜杠转义表示:

\a   U+0007 警报或响铃
\b   U+0008 退格符
\f   U+000C 换页符
\n   U+000A 换行符
\r   U+000D 回车符
\t   U+0009 水平制表符
\v   U+000B 垂直制表符
\\   U+005C 反斜杠
\'   U+0027 单引号 Go 中仅在 rune 字面中有效Rust 中始终有效
\"   U+0022 双引号 Go 中仅在字符串字面中有效Rust 中始终有效

在 Rust 中,u8 类型的字节字面量也可以以 b 字符开头,后面紧跟一对单引号包含的单个合法的 ASCII 字符,如 b'a',该值与 97u8 是等价的。在 Go 中,不必要这样,'a' 本身就是无类型的整数,可以直接将其赋值给任何类型的整型变量,当然也包括 byte 类型:

var ch byte = 'a'

类型转换

在 Go 中,以下代码是无法运行的:

var a int = 11
var b int64 = 22
c := a + b
// c := a + int(b)

显示的错误信息位 invalid operation: a + b (mismatched types int and int64)

类似的 Rust 代码也无法运行:

let a: isize = 11;
let b: i64 = 22;
let c = a + b;
// let c = a + b as isize;

错误信息很丰富,大致意思是 mismatched types,对最后一行中的 bexpected isize, found i64

之所以这样,是因为两种语言均为强类型语言,并未事先定义针对不同类型的加法运算,且均未对原始类型提供隐式的类型转换功能。对于这种情况,可以使用显式的类型转换。Go 的类型转换的基本格式为 type_name(expression);而 Rust 则使用 as 关键字进行类型转换,其形式为 expression as type_name;正确的做法请参照以上两段代码中的注释行。

应该注意,以上给出的类型转换有时会造成精度损失,从而出现错误的结果。以下代码均尝试将一个超大的 32 位有符号整数转换为 8 位有符号整数,从而得到错误的结果,并且不报错:

Go:

var a int32 = 1_000_000_023
var b int8 = int8(a)
fmt.Println(b) //23

Rust:

let a: i32 = 1_000_000_023;
let b: i8 = a as i8;
println!("{}", b) // 23

———- 本节以下内容涉及更多知识,看不懂可以先跳过 ———-

Rust 的原始类型一般都针对各种类型实现了 std::convert::Fromstd::convert::TryFrom 特型(本学习笔记将 trait 翻译为特型),以及相应的反向对称的 std::convert::Intostd::convert::TryInto 特型,这些都是类型转换的通用特型,这样一般可以使用 fromintotry_fromtry_into 方法安全地进行类型转换。其中 frominto 方法总是预先确保转换能够成功,因而不需要返回可能出现的错误;而 try_fromtry_into 方法表示转换可能失败,因此需要返回 Result 枚举值应对可能的出错情况。以上将 i32 类型转换为 i8 类型,这种转换可能出错,因此 i8 类型没有相应的 from(v: i32) -> i8 方法可用(相应地 i32 类型也没有类似 into() -> i8 的方法);不过 i8 类型有 try_from(u: i32) -> Result<i8, <i8 as TryFrom<i32>>::Error> 方法(相应地 i32 类型自动获得了 try_into(self) -> Result<U, U::Error> 方法)。让我们修改以上 Rust 代码来应对可能得转换出错情况:

let a: i32 = 1_000_000_000;
let b: Option<i8> = match a.try_into() {
    Ok(v) => Some(v),
    Err(e) => {
        // out of range integral type conversion attempted
        println!("{}", e.to_string());
        None
    },
};
println!("{:?}", b) // None

在以上代码中,将变量 b 的类型从原来的 i8 转变为 Option<i8>。当转换成功时,设置其值为 Some(v)v 为转换成功的数值);否则打印错误信息(也可以不打印),并设置其值为 None

相关函数

两种语言都为操作原始类型提供了一些实用的函数或方法。在 Go 中,主要有 mathstringsstrconv 等包。在 Rust 中,同类的功能直接以方法的形式提供。以下分别是计算一个 f64 类型的浮点数的正切(sin)的示例。

Go:

f := math.Pi / 2
sin := math.Sin(f)
fmt.Printf("sin(%f) = %f", f, sin) // sin(1.570796) = 1.000000

Rust:

let f = std::f64::consts::FRAC_PI_2;
let sin = f.sin();
println!("sin({f}) = {sin}"); // sin(1.5707963267948966) = 1