类型分类

Rust 是静态类型statically typed)语言,即编译时需要明确所有变量类型。

Rust内置的原生类型 (primitive types) 包含了标量和复合两种数据类型子集。

Rust中有 标量复合 两类数据类型子集:
标量(scalar):【基本数据类型】仅一个值。
四种基本标量类型:整型 Integer浮点型 Floating布尔类型 Boolean字符类型 Character。
复合(compound):多个值组合成一个类型。
两种原生复合类型:元组 tuple数组 array。

数据类型 Data Types - 图1

原始类型 Primitive Types

【点击链接可到达rust标准库 The Rust Standard Library下的特定章节】

array
数组
固定大小的数组,用[ T ; N ]表示;其中 T 表示元素类型, N 表示非负整数,编译时的常量大小
A fixed-size array, denoted [T; N], for the element type, T, and the non-negative compile-time constant size, N.
bool The boolean type.
char A character type.
f32 A 32-bit floating point type (specifically, the “binary32” type defined in IEEE 754-2008).
f64 A 64-bit floating point type (specifically, the “binary64” type defined in IEEE 754-2008).
fn Function pointers, like fn(usize) -> bool.
i8 The 8-bit signed integer type.
i16 The 16-bit signed integer type.
i32 The 32-bit signed integer type.
i64 The 64-bit signed integer type.
i128 The 128-bit signed integer type.
isize The pointer-sized signed integer type.
pointer Raw, unsafe pointers, *const T, and *mut T.
reference References, both shared and mutable.
slice A dynamically-sized view into a contiguous sequence, [T]. Contiguous here means that elements are laid out so that every element is the same distance from its neighbors.
str String slices.
tuple A finite heterogeneous sequence, (T, U, ..).
u8 The 8-bit unsigned integer type.
u16 The 16-bit unsigned integer type.
u32 The 32-bit unsigned integer type.
u64 The 64-bit unsigned integer type.
u128 The 128-bit unsigned integer type.
unit The () type, also called “unit”.
usize The pointer-sized unsigned integer type.

Rust标准库std::collections提供了4种通用的容器类型,其中包含8种数据结构。根据应用场景划分,动态数组可细分为普通动态数组Vec和双端队列VecDeque,映射包括HashMap,字符串包括String等类型。
数据类型是rust强类型的基础。有了数据类型,rust才能对不同的类型抽象出不同的运算,开发者才能在更高的层次上操作数据。

整数类型

  1. 整型按整型长度【存储大小】,整型分为1字节(8位),2字节,4字节,8字节,16字节。

长度是8位,有符号:i8;无符号:u8。
—— i8的数值范围:-27 ~27-1,即 -128 ~ 127;u8的数值范围:0~28-1,即0~255。
长度是16位,有符号:i16;无符号:u16。
—— i16的数值范围:-215 ~215-1;u16的数值范围:0 ~216-1
长度是32位,有符号:i32;无符号:u32。
——rust默认类型为i32。有符号的数值范围:-2n-1 ~2n-1-1;无符号的数值范围:0 ~2n-1 。
长度是64位,有符号:i64;无符号:u64。
—— i64的数值范围:-263 ~263-1;u64的数值范围:-264 ~264-1 。
长度是arch,有符号:isize;无符号:usize。
——isize,usize 主要作为数组或集合的索引类型使用,其长度依赖于运行程序的电脑系统。

整数类型 有符号 有符号数值范围 无符号 无符号数值范围
8 bits i8 -27 ~27-1 u8 0~28-1
16 bits i16 -215 ~215-1 u16 0 ~216-1
32 bits i32 -231 ~231-1 u32 0 ~232-1
64 bits i64 -263 ~263-1 u64 0 ~264-1
128 bits i128 -2127 ~2127-1 u128 0~2128-1
Pointer size isize -2n-1 ~2n-1-1 usize 0 ~2n-1

浮点数类型

浮点数分为f32和f64两类。Rust默认为 f64位。

可读性分隔符 _

为了提升长数字的可读性,数字之间可以增加 ‘‘ 下划线符号,Rust编译时自动去除可读性分隔符 ‘‘。
比如120030004.5600078000090000 可以表示为 120030_004.560_007_800_009_000_0,便于人阅读,机器还是自动去掉可读性分隔符,直接识别 120030004.5600078000090000 。
比如十六进制数字0x1234ABCD 写为 0x_1234_ABCD,不影响语义,但极大的提升了阅读体验。
比如类型后缀,也可以用可读性分隔符隔开:
let x = 0x_ff_u8; 表示 x 是 u8 类型(0~255的十六进制数字);
let y = 0o_55_i64; 表示 y 是 i64 类型 (-263 ~263-1范围内的八进制数字);
let z = 0b_1001;表示 z 是 i32(默认)类型(二进制数字)

标准库 std::f32 和 std::f64提供了 IEEE 所需的特殊常量值,比如

  • INFINITY (无穷大)
  • NEG_INFINITY (负无穷大)
  • NAN (非数字值)
  • MIN (最小有限值)
  • MAX (最大有限值)

字符类型 char

Rust使用 UTF-8 作为底层编码。字符类型代表的是一个Unicode标量值。(Unicode Scalar Value),包括数字,字母,Unicode和其他特殊字符。每个字符占用4个字节。字符类型 char 由单引号来定义。其声明语法如下: let z = 'z';

这里有不同!在 windows learn 的Rust系列课程中:
某些语言将其 char 类型视为 8 位无符号整数,这等效于 Rust 中的 u8 类型。 Rust 中的 char 类型包含 unicode 码位,但不会使用 utf-8 编码。 Rust 中的 char 是一个 21 位整数系统会将其宽度填充为 32 位。 char 直接包含纯码位值。

String类型在堆上分配!

布尔类型 bool

true 和 false(全小写)。

  1. fn main() {
  2. let x = true;
  3. let y = !x;
  4. println!("{},{}",x,y);
  5. }
  6. //true,false

范围类型

范围类型常用来生成从一个整数开始到另一个整数结束的整数序列。有左闭右开和全闭两种形式。
(1..5)表示 0,1,2,3,4,不包含5,是左闭右开;
(1..=5)表示 0,1,2,3,4,5,包含5,是全闭。


RustPrimer

原生类型

Rust内置的原生类型 (primitive types) 有以下几类:

  • 布尔类型:有两个值truefalse
  • 字符类型:表示单个Unicode字符,存储为4个字节。
  • 数值类型:分为有符号整数 (i8, i16, i32, i64, isize)、 无符号整数 (u8, u16, u32, u64, usize) 以及浮点数 (f32, f64)。
  • 字符串类型:最底层的是不定长类型str,更常用的是字符串切片&str和堆分配字符串String, 其中字符串切片是静态分配的,有固定的大小,并且不可变,而堆分配字符串是可变的。
  • 数组:具有固定大小,并且元素都是同种类型,可表示为[T; N]
  • 切片:引用一个数组的部分数据并且不需要拷贝,可表示为&[T]
  • 元组:具有固定大小的有序列表,每个元素都有自己的类型,通过解构或者索引来获得每个元素的值。
  • 指针:最底层的是裸指针*const T*mut T,但解引用它们是不安全的,必须放到unsafe块里。
  • 函数:具有函数类型的变量实质上是一个函数指针。
  • 元类型:即(),其唯一的值也是()

    1. // boolean type
    2. let t = true;
    3. let f: bool = false;
    4. // char type
    5. let c = 'c';
    6. // numeric types
    7. let x = 42;
    8. let y: u32 = 123_456;
    9. let z: f64 = 1.23e+2;
    10. let zero = z.abs_sub(123.4);
    11. let bin = 0b1111_0000;
    12. let oct = 0o7320_1546;
    13. let hex = 0xf23a_b049;
    14. // string types
    15. let str = "Hello, world!";
    16. let mut string = str.to_string();
    17. // arrays and slices
    18. let a = [0, 1, 2, 3, 4];
    19. let middle = &a[1..4];
    20. let mut ten_zeros: [i64; 10] = [0; 10];
    21. // tuples
    22. let tuple: (i32, &str) = (50, "hello");
    23. let (fifty, _) = tuple;
    24. let hello = tuple.1;
    25. // raw pointers
    26. let x = 5;
    27. let raw = &x as *const i32;
    28. let points_at = unsafe { *raw };
    29. // functions
    30. fn foo(x: i32) -> i32 { x }
    31. let bar: fn(i32) -> i32 = foo;

    有几点是需要特别注意的:

  • 数值类型可以使用_分隔符来增加可读性。

  • Rust还支持单字节字符b'H'以及单字节字符串b"Hello",仅限制于ASCII字符。 此外,还可以使用r#"..."#标记来表示原始字符串,不需要对特殊字符进行转义。
  • 使用&符号将String类型转换成&str类型很廉价, 但是使用to_string()方法将&str转换到String类型涉及到分配内存, 除非很有必要否则不要这么做。
  • 数组的长度是不可变的,动态的数组称为Vec (vector),可以使用宏vec!创建。
  • 元组可以使用==!=运算符来判断是否相同。
  • 不多于32个元素的数组和不多于12个元素的元组在值传递时是自动复制的。
  • Rust不提供原生类型之间的隐式转换,只能使用as关键字显式转换。
  • 可以使用type关键字定义某个类型的别名,并且应该采用驼峰命名法。
    1. // explicit conversion
    2. let decimal = 65.4321_f32;
    3. let integer = decimal as u8;
    4. let character = integer as char;
    5. // type aliases
    6. type NanoSecond = u64;
    7. type Point = (u8, u8);

Rust programming book

标量类型

标量scalar)类型代表一个单独的值。Rust 有四种基本的标量类型:整型、浮点型、布尔类型和字符类型。你可能在其他语言中见过它们。让我们深入了解它们在 Rust 中是如何工作的。

整型

整数 是一个没有小数部分的数字。我们在第二章使用过 u32 整数类型。该类型声明表明,它关联的值应该是一个占据 32 比特位的无符号整数(有符号整数类型以 i 开头而不是 u)。表格 3-1 展示了 Rust 内建的整数类型。在有符号列和无符号列中的每一个变体(例如,i16)都可以用来声明整数值的类型。
表格 3-1: Rust 中的整型

长度 有符号 无符号
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
128-bit i128 u128
arch isize usize

每一个变体都可以是有符号或无符号的,并有一个明确的大小。有符号无符号 代表数字能否为负值,换句话说,数字是否需要有一个符号(有符号数),或者永远为正而不需要符号(无符号数)。这有点像在纸上书写数字:当需要考虑符号的时候,数字以加号或减号作为前缀;然而,可以安全地假设为正数时,加号前缀通常省略。有符号数以补码形式(two’s complement representation) 存储。
每一个有符号的变体可以储存包含从 -(2n - 1) 到 2n - 1 - 1 在内的数字,这里 n 是变体使用的位数。所以 i8 可以储存从 -(27) 到 27 - 1 在内的数字,也就是从 -128 到 127。无符号的变体可以储存从 0 到 2n - 1 的数字,所以 u8 可以储存从 0 到 28 - 1 的数字,也就是从 0 到 255。
另外,isizeusize 类型依赖运行程序的计算机架构:64 位架构上它们是 64 位的, 32 位架构上它们是 32 位的。
可以使用表格 3-2 中的任何一种形式编写数字字面值。注意除 byte 以外的所有数字字面值允许使用类型后缀,例如 57u8,同时也允许使用 _ 做为分隔符以方便读数,例如1_000
表格 3-2: Rust 中的整型字面值

数字字面值 例子
Decimal (十进制) 98_222
Hex (十六进制) 0xff
Octal (八进制) 0o77
Binary (二进制) 0b1111_0000
Byte (单字节字符)(仅限于u8
)
b'A'

那么该使用哪种类型的数字呢?如果拿不定主意,Rust 的默认类型通常就很好,数字类型默认是 i32:它通常是最快的,甚至在 64 位系统上也是。isizeusize 主要作为某些集合的索引。

整型溢出

比方说有一个 u8 ,它可以存放从零到 255 的值。那么当你将其修改为 256 时会发生什么呢?这被称为 “整型溢出”(“integer overflow” ),关于这一行为 Rust 有一些有趣的规则。当在 debug 模式编译时,Rust 检查这类问题并使程序 panic,这个术语被 Rust 用来表明程序因错误而退出。第九章 [panic!](https://kaisery.github.io/trpl-zh-cn/ch09-01-unrecoverable-errors-with-panic.html) 与不可恢复的错误” 部分会详细介绍 panic。 在 release 构建中,Rust 不检测溢出,相反会进行一种被称为二进制补码包装(two’s complement wrapping)的操作。简而言之,256 变成 0257 变成 1,依此类推。依赖整型溢出被认为是一种错误,即便可能出现这种行为。如果你确实需要这种行为,标准库中有一个类型显式提供此功能,[Wrapping](https://kaisery.github.io/std/num/struct.Wrapping.html)

  1. fn main(){
  2. let i = -126_i8;
  3. let j = 125_i8;
  4. //checked *系列函数返回的类型是 Option< >, 当出现溢出的时候,返回值是 None ;
  5. println!("checked {:?}",i.checked_add(-2));
  6. println!("checked {:?}",i.checked_add(-3));
  7. println!("checked {:?}",j.checked_add(2));
  8. println!("checked {:?}",j.checked_add(3));
  9. println!();
  10. //saturating *系列函数返回类型是整数, 如果溢出,则给出该类型可表示范 围的“最大/最小”值;
  11. println!("saturating {:?}",i.saturating_add(-2));
  12. println!("saturating {:?}",i.saturating_add(-3));
  13. println!("saturating {:?}",j.saturating_add(2));
  14. println!("saturating {:?}",j.saturating_add(3));
  15. println!();
  16. //wrapping_*系列函数则是直接抛弃已经溢出的最高位,将剩下的部分返回。
  17. println!("wrapping {:?}",i.wrapping_add(-2));
  18. println!("wrapping {:?}",i.wrapping_add(-3));
  19. println!("wrapping {:?}",j.wrapping_add(2));
  20. println!("wrapping {:?}",j.wrapping_add(3));
  21. println!();
  22. }
  23. //checked Some(-128)
  24. //checked None
  25. //checked Some(127)
  26. //checked None
  27. //saturating -128
  28. //saturating -128
  29. //saturating 127
  30. //saturating 127
  31. //wrapping -128
  32. //wrapping 127
  33. //wrapping 127
  34. //wrapping -128

浮点型

Rust 也有两个原生的 浮点数floating-point numbers)类型,它们是带小数点的数字。Rust 的浮点数类型是 f32f64,分别占 32 位和 64 位。默认类型是 f64,因为在现代 CPU 中,它与 f32 速度几乎一样,不过精度更高。
这是一个展示浮点数的实例:
文件名: src/main.rs

fn main() { let x = 2.0; // f64 let y: f32 = 3.0; // f32 }
浮点数采用 IEEE-754 标准表示。f32 是单精度浮点数,f64 是双精度浮点数。

另外注意,整数对零求余,会出现panic,但是浮点数不会。它会返回特殊值,表明发生了问题。这一点和JavaScript一样。

  1. fn main(){
  2. let i = 100.0_f32;
  3. println!("{}",i/0.0);
  4. }
  5. //inf
  1. fn main(){
  2. let i = 100;
  3. println!("{}",i/0);
  4. }
  5. //error: this operation will panic at runtime
  6. // --> main.rs:3:18
  7. // 3 | println!("{}",i/0);
  8. // | ^^^ attempt to divide `100_i32` by zero
  9. // = note: `#[deny(unconditional_panic)]` on by default
  10. // error: aborting due to previous error

获取数值类型大小

  1. use std::num::Wrapping;
  2. fn main(){
  3. let i8_big = Wrapping(std::i8::MAX);
  4. let i8_small = Wrapping(std::i8::MIN);
  5. println!("i8 type is from {0} to {1}",i8_small,i8_big);
  6. let f32_big = Wrapping(std::f32::MAX);
  7. let f32_small = Wrapping(std::f32::MIN);
  8. println!("f32 type is from {small} to {big}",small = f32_small, big = f32_big);
  9. }
  10. // i8 type is from -128 to 127
  11. // f32 type is from -340282350000000000000000000000000000000 to 340282350000000000000000000000000000000

数值运算

Rust 中的所有数字类型都支持基本数学运算:加法、减法、乘法、除法和取余。下面的代码展示了如何在 let 语句中使用它们:
文件名: src/main.rs

fn main() { // 加法 let sum = 5 + 10; // 减法 let difference = 95.5 - 4.3; // 乘法 let product = 4 * 30; // 除法 let quotient = 56.7 / 32.2; // 取余 let remainder = 43 % 5; }
这些语句中的每个表达式使用了一个数学运算符并计算出了一个值,然后绑定给一个变量。附录 B 包含 Rust 提供的所有运算符的列表。

数值类型转换

使用 as 转换数字类型

  1. fn main(){
  2. let x = 100;
  3. let y = 200f32;
  4. let z:f64 = 5.5;
  5. println!("{}",add_func(x,y));
  6. // 注意z转换无需括号,否则会有提醒
  7. println!("{}",add_func(x,z as f32));
  8. }
  9. fn add_func(a:i32,b:f32) -> f64{
  10. return (a as f64)+(b as f64);
  11. }
  12. // 300
  13. // 105.5

布尔型

正如其他大部分编程语言一样,Rust 中的布尔类型有两个可能的值:truefalse。Rust 中的布尔类型使用 bool 表示。例如:
文件名: src/main.rs

fn main() { let t = true; let f: bool = false; // 显式指定类型注解 }
使用布尔值的主要场景是条件表达式,例如 if 表达式。在 “控制流”(“Control Flow”) 部分将介绍 if 表达式在 Rust 中如何工作。

字符类型

目前为止只使用到了数字,不过 Rust 也支持字母。Rust 的 char 类型是语言中最原生的字母类型,如下代码展示了如何使用它。(注意 char 由单引号指定,不同于字符串使用双引号。)
文件名: src/main.rs

fn main() { let c = 'z'; let z = 'ℤ'; let heart_eyed_cat = '😻'; }
Rust 的 char 类型的大小为四个字节(four bytes),并代表了一个 Unicode 标量值(Unicode Scalar Value),这意味着它可以比 ASCII 表示更多内容。在 Rust 中,拼音字母(Accented letters),中文、日文、韩文等字符,emoji(绘文字)以及零长度的空白字符都是有效的 char 值。Unicode 标量值包含从 U+0000U+D7FFU+E000U+10FFFF 在内的值。不过,“字符” 并不是一个 Unicode 中的概念,所以人直觉上的 “字符” 可能与 Rust 中的 char 并不符合。第八章的 “使用字符串存储 UTF-8 编码的文本” 中将详细讨论这个主题。

复合类型

复合类型Compound types)可以将多个值组合成一个类型。Rust 有两个原生的复合类型:元组(tuple)和数组(array);还有结构体和枚举。

元组类型

元组是一个将多个其他类型的值组合进一个复合类型的主要方式。元组长度固定:一旦声明,其长度不会增大或缩小。
我们使用包含在圆括号中的逗号分隔的值列表来创建一个元组。元组中的每一个位置都有一个类型,而且这些不同值的类型也不必是相同的。这个例子中使用了可选的类型注解:

  1. //文件名: src/main.rs
  2. fn main() {
  3. let tup: (i32, f64, u8) = (500, 6.4, 1);
  4. }

tup 变量绑定到整个元组上,因为元组是一个单独的复合元素。为了从元组中获取单个值,可以使用模式匹配(pattern matching)来解构(destructure)元组值,像这样:

  1. //文件名: src/main.rs
  2. fn main() {
  3. let tup = (500, 6.4, 1);
  4. let (x, y, z) = tup;
  5. println!("The value of y is: {}", y);
  6. }
  7. //6.4

程序首先创建了一个元组并绑定到 tup 变量上。接着使用了 let 和一个模式将 tup 分成了三个不同的变量,xyz。这叫做 解构destructuring),因为它将一个元组拆成了三个部分。最后,程序打印出了 y 的值,也就是 6.4
除了使用模式匹配解构外,也可以使用点号(.)后跟值的索引来直接访问它们。例如:

  1. //文件名: src/main.rs
  2. fn main() {
  3. let x: (i32, f64, u8) = (500, 6.4, 1);
  4. let five_hundred = x.0;
  5. let six_point_four = x.1;
  6. let one = x.2

这个程序创建了一个元组,x,并接着使用索引为每个元素创建新变量。跟大多数编程语言一样,元组的第一个索引值是 0。

  1. fn main(){
  2. let tup1:(i8,f32,bool) = (-20,5.67,false);
  3. let tup2 = (2.34,(22,2.2,true));
  4. let tup3 = (50,);
  5. println!("{2},{1},{0}",tup3.0,tup2.1.2,tup1.1); //5.67,true,50
  6. println!("{:?}",tup2.1); //(22, 2.2, true)
  7. // println!("{}",tup3);
  8. // 报错`({integer},)` cannot be formatted with the default formatter
  9. // 需要用{:?}格式化
  10. }

取值注意:
元组 . 索引值【 tuple . index】;
数组 [ 索引值 ] 【array [ index ]】;

数组类型

另一个包含多个值的方式是 数组array)。与元组不同,数组中的每个元素的类型必须相同。Rust 中的数组与一些其他语言中的数组不同,因为 Rust 中的数组是固定长度的:一旦声明,它们的长度不能增长或缩小。
Rust 中,数组中的值位于中括号内的逗号分隔的列表中:
文件名: src/main.rs

fn main() { let a = [1, 2, 3, 4, 5]; }
当你想要在栈(stack)而不是在堆(heap)上为数据分配空间(第四章将讨论栈与堆的更多内容),或者是想要确保总是有固定数量的元素时,数组非常有用。但是数组并不如 vector 类型灵活。vector 类型是标准库提供的一个 允许 增长和缩小长度的类似数组的集合类型。当不确定是应该使用数组还是 vector 的时候,你可能应该使用 vector。第八章会详细讨论 vector。
一个你可能想要使用数组而不是 vector 的例子是,当程序需要知道一年中月份的名字时。程序不大可能会去增加或减少月份。这时你可以使用数组,因为我们知道它总是包含 12 个元素:

let months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
可以像这样编写数组的类型:在方括号中包含每个元素的类型,后跟分号,再后跟数组元素的数量。

let a: [i32; 5] = [1, 2, 3, 4, 5];
这里,i32 是每个元素的类型。分号之后,数字 5 表明该数组包含五个元素。
以这种方式编写数组的类型看起来类似于初始化数组的另一种语法:如果要为每个元素创建包含相同值的数组,可以指定初始值,后跟分号,然后在方括号中指定数组的长度,如下所示:

let a = [3; 5];
变量名为 a 的数组将包含 5 个元素,这些元素的值最初都将被设置为 3。这种写法与 let a = [3, 3, 3, 3, 3]; 效果相同,但更简洁。

访问数组元素

数组是一整块分配在栈上的内存。可以使用索引来访问数组的元素,像这样:
文件名: src/main.rs

fn main() { let a = [1, 2, 3, 4, 5]; let first = a[0]; let second = a[1]; }
在这个例子中,叫做 first 的变量的值是 1,因为它是数组索引 [0] 的值。变量 second 将会是数组索引 [1] 的值 2

无效的数组元素访问

如果我们访问数组结尾之后的元素会发生什么呢?比如你将上面的例子改成下面这样,这可以编译通过,不过在运行时会因错误而退出:
文件名: src/main.rs

fn main() { let a = [1, 2, 3, 4, 5]; let index = 10; let element = a[index]; println!("The value of element is: {}", element); }
使用 cargo run 运行代码后会产生如下结果:

$ cargo run Compiling arrays v0.1.0 (file:///projects/arrays) Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs Runningtarget/debug/arraysthread 'main' panicked at 'index out of bounds: the len is 5 but the index is 10', src/main.rs:5:19 note: Run withRUSTBACKTRACE=1for a backtrace.
编译并没有产生任何错误,不过程序会出现一个 运行时(_runtime
)错误并且不会成功退出。当尝试用索引访问一个元素时,Rust 会检查指定的索引是否小于数组的长度。如果索引超出了数组长度,Rust 会 panic,这是 Rust 术语,它用于程序因为错误而退出的情况。
这是第一个在实战中遇到的 Rust 安全原则的例子。在很多底层语言中,并没有进行这类检查,这样当提供了一个不正确的索引时,就会访问无效的内存。通过立即退出而不是允许内存访问并继续执行,Rust 让你避开此类错误。第九章会讨论更多 Rust 的错误处理。