数据类型又称类型,是数据的一种属性,它决定了值的分类,如何解释保存值的内存,以及可以对值执行的操作(包括值具有的方法)。编程语言通常包含三类类型:内建的原始类型,如各种数值类型、布尔类型、字符串类型;基于原始类型构建的复合类型,如数组、切片、结构体等;与各种数据结构模型对应的抽象类型,如列表、堆栈、树、映射、图等。请注意这些划分并不是绝对的,如我们也可以认为字符串是切片,属于复合类型,同时认为映射也属于复合类型。语言的类型系统将类型与计算值相关联,通过检查这些值的流,保证不会发生类型错误。在编程时,必须充分了解语言的类型和类型系统。
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 中的
int
和uint
类型分别对应 Rust 中的isize
和usize
,它们的长度依赖运行程序的计算机架构:64 位架构上它们是 64 位的, 32 位架构上它们是 32 位的。在 Rust 中,isize
和usize
类型一般用作某些集合的索引。 - 在 Go 中,
uintptr
类型类似uint
类型,不同之处是它总是能保存任意的内存地址,可以将其应用于指针相关的操作。
Go 和 Rust 的以上数据类型存在以下方面的重要区别:
- 如果较难确定究竟该使用何种整数类型,在 Go 中一般使用
int
类型,而在 Rust 中则一般使用i32
类型,这也分别是两种语言中整型的默认类型。 - Rust 内建有 128 位的整型
i128
和u128
,Go 没有。 - Go 内建有复数类型
complex64
和complex128
,Rust 没有。
字面量表示
两种语言都用 true
和 false
表示布尔类型字面量,这里就不再多做介绍了。两种语言在表示数值字面量时,都可以用下划线 _
作为可视分隔符,如 10_000
、3.141_59
。
两种语言的一个重要区别是:Go 中的标量字面量是无类型的,而 Rust 中的标量字面量具有默认类型,这在前面变量与常量一文已经讨论过。在 Rust 中,除了字节字面量外,其他数字字面量都可以通过添加类型后缀显式指定其类型,如 10_000i32
、3.141_59_f32
,Go 不支持(也没有必要支持)这种方法。
整数字面量
字面量指值的字面形式。整数类型的字面量可以是十进制、二进制、八进制或十六进制的形式。
在 Go 中,各种进制字面量的表示方法为:
- 十进制表示时,除了数字 0,其他整数不能以 0 开头,如 0、13、267;
- 二进制表示时,以
0b
或0B
开头,如 0b0、0B1101、0B100001011; - 八进制表示时,以
0
、0o
或0O
开头,如 0o0、015、0O413; - 十六进制表示时,以
0x
或0X
开头,如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 中可以以 0B
、0
或 0O
、0X
开头表示二进制、八进制及十六进制整数字面量,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+0
或 1e+0
。
Go 还支持十六进制形式的浮点数表示。十六进制浮点字面量从左到右分别是:0x
或 0X
、十六进制的整数部分、小数点、十六进制的小数部分、指数部分(p
或 P
紧跟一个可选的正负号及十进制数字)。整数部分和小数部分可以省略其一,小数点也可以省略,而指数部分不可省略。该浮点数的值由 p
或 P
前面的有效数字乘以以 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 码点是在码空间中的任意值,其取值范围使 0x0000
~0x10FFFF
。而 Unicode 标量值是除了高代理码点和低代理码点之外的码点,范围是 0x0000
~0xD7FF
及 0xE000
~0x10FFFF
。在 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
,对最后一行中的 b
的 expected 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::From
和 std::convert::TryFrom
特型(本学习笔记将 trait 翻译为特型),以及相应的反向对称的 std::convert::Into
和 std::convert::TryInto
特型,这些都是类型转换的通用特型,这样一般可以使用 from
、into
、try_from
、try_into
方法安全地进行类型转换。其中 from
和into
方法总是预先确保转换能够成功,因而不需要返回可能出现的错误;而 try_from
和 try_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 中,主要有 math
、strings
、strconv
等包。在 Rust 中,同类的功能直接以方法的形式提供,如 f64
类型。以下分别是计算一个 64 位浮点数的正切(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