Rust之路(2)——数据类型 上篇

2023-06-14,,

【未经书面同意,严禁转载】 -- 2020-10-13 --

Rust是系统编程语言。什么意思呢?其主要领域是编写贴近操作系统的软件,文件操作、办公工具、网络系统,日常用的各种客户端、浏览器、记事本、聊天工具等,还包括硬件驱动、板载程序,甚至写操作系统。但和python、Java等注重应用型语言不同。系统编程语言最主要的要求就是执行效率高、运行快!其次是可以访问硬件,直接操作内存和各种端口。当前系统编程语言当推C和C++为老大,相对来说,C在更底层的驱动、嵌入式,C++侧重在应用程序层。

这也注定了Rust的语法规则会比较多。另外,Rust站在诸多语言巨人的肩膀上,糅合了多家功力,为了解决现有语言的问题,提出了一些新的概念,可能有些规则让学C++、Java等传统语言的人大跌眼镜。所以,我赞同一代宗师张三丰的方法:

《倚天屠龙记》

张三丰:“还记得吗?”

张无忌:“全都记得。”

张三丰:“现在呢?”

张无忌:“已经忘了一小半。”

张三丰: “现在呢?”

张无忌: “啊,已经忘了一大半。”

张三丰:“不坏不坏,忘得真快,那么现在呢?”

张无忌:  “已经全都忘了,忘得干干净净。”

好了,放空自己,放下过往,开始Rust,你会进步Fast!

从易到难,从根基到大厦。语言首先关注的,就应该是数据类型了。

Rust的数据类型特点:安全、高效、简洁。除了类型的使用规范,编译器的功能之强大,是保证这些特点的功臣。编译器的首要任务是检查类型使用正确与否,还具有类型推断和支持泛型的特点,这使得Rust在高度限制的前提下又很灵活。

数据类型分类

Rust的基本类型(Primitive Types)有整型interger、字节byte、字符char、浮点型float、布尔bool、数组array、元组tuple(仅限于元组内的元素也是值类型)。在这里,所谓的基本类型,有以下特点:

    数据分布在栈上,在参数传递的过程中会复制一个值用于传递,本身不会受影响;
    数据在编译时即可知道占用多大空间,比如i32占据4字节;
    因为上2条的原因,数据存取特别快,执行效率高,但是栈空间比较小,不能存储特别大的值。

后面要说的指针pointer、字符段str、切片slice、引用reference、单元unit(代码中写作一对小括号())、空never(在代码中写做叹号!),也属于基本类型,但是说起来比前面几类复杂,本篇中讲一部分,后面章节的内容还会融合这些数据类型。

除基本类型外最常用的类型是字符串String、结构体struct、枚举enum、向量Vector和字典HashMap(也叫哈希图)。string、struct、enum、vector、HashMap的数据都是在堆内存上分配空间,然后在栈空间分配指向堆内存的指针信息。函数也可以算是一种类型,此外还有闭包、trait。这些类型各有实现方式,复杂度也高。

这些数据的用法,就构成了Rust的语法规则。

下表是Rust的基本类型、常用的std库内的类型和自定义类型。

类型写法 描述 值举例
i8, i16, i32, i64,
u8, u16, u32, u64

i:带符号
u:无符号
数字代表存储位数

42,
-5i8, 0x400u16, 0o100i16,
20_922_789_888_000u64,
b'*' (u8 byte literal)
isize, usize

带符号/无符号 整型
存储位数与系统位数相同
(32或64 位整数)

137,
-0b0101_0010isize,
0xffff_fc00usize
f32, f64

IEEE标准的浮点数,

单精度/双精度

1.61803, 3.14f32,
6.0221e23f64
bool

布尔型

true, false
char

Unicode字符
存储空间固定为4字符

'*', '\n', '字', '\x7f', '\u{CA0}'
(char, u8, i32)

元组tuple:可以存储多种类型

 ('%', 0x7f, -1)
()

单元类型,实际上是空tuple

()
struct S { x: f32, y:
f32 }

命名元素结构体,数据成员有变量名的结构体

struct S { x: 120.0, y: 209.0 }
struct T(i32, char)

元组型结构体,数据成员无名称,形如元组,有点像python里的namedtuple
注意不可与元组混淆

struct T(120, 'X')
struct E

单元型结构体,没有数据成员

E
enum Attend {
OnTime, Late(u32)
}

枚举类型,例如一个Attend类型的值,要么取值OnTime,要么取值Late(u32)

与其他语言不通,枚举类型默认没有比较是否相等的运算,更没有比较大小

Attend::Late(5),
Attend::OnTime
Box<Attend>

Box指针类型,指向堆内存中的一个泛型值

Box::new(Late(15))
&i32, &mut i32

只读引用和可变引用,物所有权,生命周期不能超过所指向的值。
只读引用也叫共享引用,因为可以建立多个指向同一个值的只读引用

&s.y, &mut v
String

字符串,UTF-8格式存储,长度可变

"编程".to_string()
to_string函数返回一个字符串类型

&str

str的引用,指向UTF-8文本的指针,无所有权

"そば: soba", &s[0..12]
[f64; 4], [u8; 256]

数组,固定长度,内部数据类型必须一致

[1.0, 0.0, 0.0, 1.0],
[b' '; 256]
Vec<f64>

Vector向量,可变长度,内部数据类型必须一致

vec![0.367, 2.718, 7.389]
&[u8..u8],
&mut [u8..u8]

切片引用,通过起始索引和长度指向数组或向量的一部分连续元素

&v[10..20], &mut a[..]
&Any, &mut Read

traid对象:实现了某trait内方法的对象
示例中Any、Read都是trait

value as &Any,
&mut file as &mut Read
fn(&str, usize) ->
isize

函数类型,可以理解为函数指针

i32::saturating_add
闭包

闭包

|a, b| a*a + b*b

上表中没有byte类型,是因为Rust压根就没有byte类型,实际上等于u8,在一般计算中认为是u8,在文件或网络中读写数据时经常称为byte流。

整型

Rust的带符号整型,使用最高一位(bit)表示为符号,0为正数,1为负数,其他位是数值,用补码表示。比如0b0000 0100i8,是正数,值为4,而0b1000 0100i8是负数,用补码换算出来是-252。在此解释一下这个数值的写法,0b00000100i8,分成三部分来看:第一部分0b表示这个值是用2进制书写的,0o开头是8进制,0x开头是16进制;第二部分00000100是数值;第三部分i8是类型,Rust中用数值后面直接跟类型(中间不能有空格),来表明这个数值是什么类型。另外,为了方便阅读,数值中间或数值和类型中间可以加下划线,例如123_456i32, 5_1234u64, 8_i8,下划线只是为了便于人类阅读,编译时会忽略掉。

各整数类型的取值范围:

u8: 0 至 28 –1 (0 至 255)
u16: 0 至 216 −1 (0 至 65,535)
u32: 0 至 232 −1 (0 至 4,294,967,295)
u64: 0 至 264 −1 (0 至 18,446,744,073,709,551,615,约1.8千亿亿)
usize: 0 至 232 −1 或 264 −1

i8: −27 至 27 −1 (−128 至 127)
i16: −215 至 215 −1 (−32,768 至 32,767)
i32: −231 至 231 −1 (−2,147,483,648 至 2,147,483,647)
i64: −263 至 263 −1 (−9,223,372,036,854,775,808 至 9,223,372,036,854,775,807)
isize: −231 至 231 −1, or −263 至 263 −1

因为带符号整型的最高位是符号位,而无符号整型没有符号位,所以能表示的最大正整数更大。

上面说过Rust没有byte类型,而是u8类型。我们认为的byte型应该叫byte字面量,指的是ASCII字符,在本文中,暂且仍然称为byte类型,书写方式是b'x',b表示是byte,内容用单引号引起来。ASCII码值在0至127(128至255也有ASCII字符,但是没有统一标准,暂且不论)。一旦提到ASCII字符,就会想到一些不可见字符,比如回车、换行、制表符,就需要用一种可见的形式来书写,我们称之为转义。byte字面量是用单引号引起来的,所以单引号也需要转义,转义是在一个字母或符号前加一个反斜杠,所以反斜杠自身也需要转义。

ASCII字符

byte字面量的书写

相当于的数值

单引号  '

b'\''

39u8

反斜杠 \

b'\\'

92u8

换行

b'\n'

10u8

回车

b'\r'

13u8

制表符Tab

b'\t'

9u8

对于没有转义或转义难以阅读的byte字符,建议用16进制的数字表示,形如b'0xHH',其中HH是两位的16进制数字来表示其ASCII码。下面是ASCII码速查表。

题内话:

Rust中,char类型和byte类型完全不一样,char类型和上述所有的整型都不相同,不要混淆!

usize和isize类似于C语言中的size_t,它们的精度与平台的位数相同,在32位体系结构中长32位,在64位体系结构中长64位。那有什么用?Rust要求数组的索引必须是usize类型,在一些数据结构中,数组和向量的元素数也是usize型。

在debug模式下,Rust会对整型的计算溢出做检查:

let big_val = std::i32::MAX; //MAX 是std::i32中定义的常量,表示i32型的最大值,即231-1
let x = big_val + 1; // 发生异常 panic: arithmetic operation overflowed

但是在release中,不会出现异常,而是返回二进制计算的结果。具体地说:

std::i32::MAX的二进制: 0111 1111 1111 1111 1111 1111 1111 1111
加1操作后      : 1000 0000 0000 0000 0000 0000 0000 0000

x读取这个二进制数,补码翻译的结果就是一个负数,正最大值加1操作后,编程了负最小值。如果正最大值加2,就等于负最小值加1,依次类推。这就像一个环形跑道,跑完一圈回到原点,然后继续往前跑。这称为wrapping arithmetic,回环算术。但绝不应该用这种溢出的方式进行回环运算,而是应该用整型的内置函数wrapping_add():

let x = big_val.wrapping_add(1); // 结果是i32类型的负最小值,完美回到起点~~

这种回环运算在需要模运算的时候很有用,例如hash算法、加密、清零等。

as是一个运算符,可以从一种整型转换为另一种整型,例如:

assert_eq!(  10_i8 as u16,    10_u16); // 正数值由少位数转入多位数
assert_eq!( 2525_u16 as i16, 2525_i16); // 正数值同位数转换 assert_eq!( -1_i16 as i32, -1_i32); // 负数少位转多位执行符号位扩展
assert_eq!(65535_u16 as i32, 65535_i32); // 正数少位转多位执行0位扩展(也可以理解为符号位扩展)

//由多位数转少位数,会截掉多位数的高位,相当于多位数除以2^N的取模,其中N是少位数的位数
assert_eq!( 1000_i16 as u8, 232_u8); //1000的二进制是0000 0011 1110 1000,截掉左侧8位,留下右侧8位,是232
assert_eq!(65535_u32 as i16, -1_i16); //65535的二进制,16个0和16个1,截掉高位的16个0,剩下的全是1,全1的有符号补码是-1

//同位数的带符号和无符号相互转化,存储的数字并不动,只是解释的方法不一样
//无符号数,就是这个值;而有符号数,需要用补码来翻译
assert_eq!(-1_i8 as u8, 255_u8); //有符号转无符号
assert_eq!(255_u8 as i8, -1_i8); //无符号转有符号

有以上例子可以看出,as运算符在整型之间转换时,对存储的数字并不改动,只是把数读出来的时候进行截取、扩展、或决定是否采用补码翻译。

同样在整型和浮点型之间转换时,也是不会改动存储的数字,而是用不同类型的方式去解释翻译。

浮点型

Rust的浮点型有单精度浮点型f32和双精度浮点型f64,浮点数全部是有符号,没有无符号浮点数这一说。

根据IEEE 754-2008规范,浮点类型包括正无穷大和负无穷大、不同的正负零值(即有正0和负0两种0)以及非数字值(NaN)。

f32的存储空间占4个字节,有6位有效数字,表示的范围大概介于 –3.4 × 1038 和 +3.4 × 1038之间。

f64的存储空间占8个字节,有15位有效数字,表示的范围大概介于 –1.8 × 10308 和 +1.8 × 10308 之间。

浮点数的书写方式有123.4567和89.012e-2两种写法,后者叫科学技术法,89.012叫基数,-2叫尾数,e作为分隔,其值等于89.012× 10-2

5.和.5都是合法的写法。

如果一个浮点数缺少类型后缀,Rust会从上下文中推断它是f32还是f64,如果两者都可能,则默认为f64。在现代计算机中,对浮点数的计算做了优化,使用f64比f32的运算效率低不了太多。

但不会把一个整数推断为浮点数。例如35是一个整数,35f32才是浮点数。

f32和f64类型都定义了一些特殊值常量: INFINITY(无穷大)、 NEG_INFINITY (负无穷)、NAN (非数字)、MIN(最小值)、MAX (最大值)。std::f32::consts和std::f64::consts模块定义了一些常量:E(自然对数)、PI(圆周率)、SQRT_2(2的平方根)等等。

题外话:

上面这段使用了两种说法:

f32类型定义了……

意思是这些常量是在f32类型中定义的,f32在Rust的核心中,使用方法是f32::INFINITY、 f32::NEG_INFINITY、f32::MAX……

std::f32::consts模块定义了……

  意思是这些常量是在std::f32::consts模块定义的,这个模块不属于Rust核心,而是属于std库,使用方法:std::f32::consts::E……。或者使用后面讲到的use std::f32::consts路径导入,然后使用consts::E。

浮点数常用的一些方法:

assert_eq!(5f32.sqrt() * 5f32.sqrt(), 5.); // 平方根,此外还有sin()、ln()等诸多数学计算方法
assert_eq!(-3.7f64.floor(), -4.0); //向下取整,还有ceil()方法是向上取整,round()方法是四舍五入)
assert_eq!(1.2f32.max(2.2), 2,2); //比较返回最大值,min()方法是取最小值
assert_eq!(-3.7f64.trunc(), -3.0); //删除小数部分,注意和floor、ceil的区别
assert!((-1. / std::f32::INFINITY).is_sign_negative()); //是否为负值,注意-0.0也算负值

题内话:

代码中通常不需要带类型后缀,但在使用浮点类型的方法时,如果不标明类型,就会报编译错误,例如:

println!("{}", (2.0).sqrt());
//编译错误,错误提示:
// error[E0689]: can't call method `sqrt` on ambiguous numeric type `{float}`

这种情况就需要标明类型,或者是使用关联函数的方式调用:

println!("{}", (2.0_f64).sqrt());  //标明类型
println!("{}", f64::sqrt(2.0)); //浮点类型关联函数的使用方法

与C和C++不同,RUST几乎不执行数字隐式转换。如果函数需要f64参数,则传递i32值作为参数是错误的。事实上,Rust甚至不会隐式地将i16值转换为i32值,即使每个i16值可以无损扩展为i32值。这种情况的传参需要用关键字as显式地写下:i as f64、 x as i32。缺乏隐式转换有时会使Rust表达式比类似的C或C++代码更冗长,但是隐式整数转换极有可能导致错误和安全漏洞。

布尔型

我认为布尔型是最简单的数据类型,只有true和false两个值,所以只说几个注意事项即可:

Rust的判断语句if,循环语句while的判断条件,以及逻辑运算符&&和| |的表达式必须是布尔值,不能像C语言那样 if 1{……},而是要写成if 1!=0{……};
as运算符可以将bool值转换为整数类型,false转换为0,true转换为1;
但是,as不会从数值类型转换为bool。你必须写出一个像这样的显式比较x != 0;
尽管布尔值只需要1个位来表示它,但值的存储使用整个字节(我相信任何一种有布尔类型的语言不会用1位来表示布尔型的,计算机寻址的方式就是按字节寻址)

就这些吧!

字符(char)

再次强调,不要把Rust中的字符char类型和byte混淆,更不要把字符串string认为是字符(char)的数组或序列!

原因是:Rust固定的用4字节来存储char类型,表示一个Unicode字符。

而文本流(byte文本)和string是utf-8编码的序列,UTF-8编码是变长的。何为变长?就是一个Unicode字符,可能占1个字节,也可能占2、3个字节,例如英文字母a,Unicode码是0x61,占1个字节,而中文“我”,Unicode码是0xE68891,占3个字节。所以:

//string的len()方法返回字符串占据的字节数
String::from("a").len() //等于1
String::from("我").len() //等于3
String::from("a我").len() //等于4

和byte一样,有些字符需要用反斜杠转义。

char类型的书写是用单引号引起来,字符串是用双引号引起来:

‘C’——char类型;“C”——字符串;b'C'——byte型(u8型)

三者的存储方式也不同:

char类型在栈内存上开辟4字节空间,把字母C的Unicode码 0x 00 00 00 43存入;

byte型在栈内存上开辟1字节空间,把字母C的ASCII码 0x43 存入;

字符串型在堆内存上开辟N字节空间(N一般是字母C的字节数1),然后在栈内存上开辟12字节空间(此处以32位平台为例),4个字节存放堆内存放置数据的指针,4个字节存放字符串在内存中开辟的空间N,4个字节存放字符串当前使用的空间。关于后两个4字节的区别在string类型中叙述。

char类型的值包含范围为0x0000到0xD7FF或0xE000到0x10FFFF的Unicode码位。对于其他数值,Rust会认为是无效的char类型,出现编译异常。

char不能和任何其他类型之间隐式转换。但可以使用as运算符将字符转换为整数类型;对于小于32位的类型,字符值的高位将被截断:

assert_eq!('*' as i32, 42); 
assert_eq!('ಠ' as u16, 0xca0);
assert_eq!('ಠ' as i8, -0x60); // U+0CA0 被截断为8位带符号整型

所有的整数类型中,只有u8能用as转换为char。如果想用u32位转换为char,可以用std库里的std::char::from_32()函数,返回值是Option<char>类型,关于这个类型我们后面会重点讲述。

char类型和std库的std::char模块中有很多有用的char方法/函数,例如:

assert_eq!('*'.is_alphabetic(), false);  //检查是否是字母
assert_eq!('β'.is_alphabetic(), true);
assert_eq!('8'.to_digit(10), Some(8)); //检查是否数字
assert_eq!('ಠ'.len_utf8(), 3); //用utf-8格式表示的话,占据几个字节
assert_eq!(std::char::from_digit(2, 10), Some('2')); //数字转换为char,第二个参数是进制

但是char类型使用的场景不多,我们应该更多关注相关的string类型。

元组 tuple

元组是若干个其他类型的数据,用逗号隔开,再用一对小括号包裹起来。例如(“巴西”, 1985,  29)。

首先,元组不是一种类型,我们只能说元组是一种格式,比如(“巴西”, 1985,  29)的类型是(&str, i32, i32),('p', 99)的类型是(char, i32),这两者是不同的类型,明白这一点很重要,不同类型意味着不能直接判断大小或是否相等。所以元组是无数个类型的统称。

元组内的各个元素,可以用“.”来访问,类似访问对象中的成员:

let t = ("巴西", 1985,  29);
let x = t.0 //"巴西"
let y = t.1 //1985

由于元组的类型和各元素的类型相关,所以以下代码是不对的

let mut t = ("巴西", 1985,  29);
t.1 = 'A' //错误!!,t的类型是(&str, i32, i32),所以t.1赋值必须赋i32类型

通常使用元组类型从函数返回多个值:

let text = "I see the eigenvalue in thine eye";  //str型字符串
let (head, tail) = text.split_at(21); //此方法将一个str字符串分割为两个str字符串,这两个字符串就可以用一个二元的元组来接收
assert_eq!(head, "I see the eigenvalue ");
assert_eq!(tail, "in thine eye");

我们经常将相关联的几个值以元组形式表示,比如一个坐标点,可以用(i64, i64)表示,三维坐标点可以用(i64, i64, i64)表示。

元组的一个特殊类型是空元组(), 也叫零元组(zero-tuple)或单元类型(unit),因为它只有一个值,就是()。Rust使用空元祖表示没有有意义的值,但是上下文仍然需要某种类型的类型。例如,没有显式返回值的函数的返回类型为(),这在某些返回Result<>类型函数的时候,会有用。

元组的各元素用逗号分隔,而且在最末尾的元素后面,也可以加上逗号,例如(1, 2, 3,)。当元组中只有一个元素的时候,(1)就会有歧义了,编译器会认为这是个括号表达式,而不是元组,而(1,)则明明白白是个元组。在Rust中,末尾元素可以加逗号的规则不但适合元组中,函数参数、数组、结构和枚举定义等等场合也可以。

指针(Pointer)

作为系统编程语言,指针肯定是不能缺席。但是Rust为了实现安全的目的,对指针做了多个包装类型,其中有安全指针(不会造成内存泄漏,Rust自动回收),也有不安全指针(由程序员负责分配和释放)。Rust为了维护自己的尊严,声称大多数程序使用安全指针可以满足。并对不安全指针也做了一些限制。

下面看一下两种安全指针:引用(reference)和Box,和不安全指针(也叫裸指针,Raw Pointer)。

引用

引用的概念被广泛使用,在Rust中,可以理解为指向某块内存的指针。目标可以使堆空间也可以是栈空间。

目标值是某种类型,那指向目标的引用,也是有类型的。指向字符串string类型的引用是&string类型,指向i32类型的引用是&i32类型,即在目标值的类型前加&。

&不但用在引用类型的写法上,而且用做引用运算符:

let a: i32 = 90;
let ref_a: &i32 = &a; //&a就是a的引用
let ref_a2: &i32 = &a; //可以声明多个引用
let b = *ref_a; // *是解引用运算符,即获取某个引用的原始值

&和*运算符的作用和C语言中很像,但是Rust中引用不会是null,必须有目标值。

和普通变量相似,默认引用是不可变的,加上mut才能是可变。

和C语言指针的另一个主要的区别是,Rust跟踪目标值的所有权和生命周期,引用不负责目标值的内存分配和释放,只是一种借用关系,目标值根据自己的生命周期产生和销毁,引用必须在目标值的生命周期内产生和使用,因此在编译时可以排除悬空指针、多次释放和指针无效等错误。

Box

用代码在堆内存中分配空间的最简单方法就是用Box::new

let t = (12, "eggs");
let b = Box::new(t); // 在堆内存中分配空间,容纳一个元组,然后返回一个指向该内存段的Box型指针b

Box是一种泛型,t的类型是(i32,&str),因此b的类型是Box<(i32,&str)>。Box::new()分配足够的内存来包含堆上的元组。这段堆空间的声明周期就由变量b来控制,当b超出作用域时,内存将立即释放。

裸指针 Raw Pointers

Rust也有裸指针类型 *mut  T和 *const  t。裸指针就像C++中的指针一样。使用裸指针是不安全的,因为Rust不会追踪它指向的内存。例如,裸指针可能为null,也可能指向已释放的内存或包含不同类型值的内存。这些都是C++中典型的内存泄漏问题。

但是,只能在不安全代码段中解引用裸指针。一个不安全代码段是Rust针对特殊场合使用而加入机制,其安全性不能保证。

题外话:

Rust的内存安全依赖于强大的类型系统和编译检测,不过它并不能适应所有的场景。 首先,所有的编程语言都需要跟外部的“不安全”接口打交道,调用外部库等,在“安全”的Rust下是无法实现的; 其次,“安全”的Rust无法高效表示复杂的数据结构,特别是数据结构内部有各种指针互相引用的时候;再次, 事实上还存在着一些操作,这些操作是安全的,但不能通过编译器的验证。

因此在安全的Rust背后,还需要unsafe代码。它可以做三件事:

解引用裸指针*const T*mut T
读写可变的静态变量static mut
调用不安全函数

unsafe代码用unsafe{……}包括起来。

unsafe{
…… //unsafe 代码
}

序列类型(数组Array、向量Vector、切片Slice)

Rust有三种类型用于表示内存中的序列值:

类型 [T;n] 表示一个由n个值组成的数组,每个值都是T类型。数组的大小是在编译时确定的常量,是类型的一部分;数组的元素数量是固定的,不能增减。
类型 Vec<T> 称为T的vector,是动态分配的、可增长的T类型值序列。vector的元素位于堆中,可以随意调整vector的大小,增删元素。
类型 &[T] 和 &mut[T] 称为类型T的只读切片和T的可变切片,是对序列中元素的引用,这些元素是序列的一部分,序列可以是数组或向量。切片相当于包含了指向此切片第一个元素的指针,以及切片元素数的计数。可变切片&mut[T]修改和增删元素,但一个序列同时只能声明一个切片;只读切片&[T]不允许修改元素,但可以同时在一个序列上声明多个只读切片。

三种类型的值都有一个len()方法,返回这个序列的元素数;都可以用下标的方式访问,形如v[0]……。Rust会检查i是否在有效范围内;如果没有会产生异常。i必须是一个usize类型的值,不能使用任何其他整数类型作为索引。另外,它们的长度也可以是零。

数组Array

数组的初始化可以有形式:

let lazy_caterer: [u32; 6] = [1, 2, 4, 7, 11, 16];  //元素枚举法
let taxonomy = ["Animalia", "Arthropoda", "Insecta"];
assert_eq!(lazy_caterer[3], 7);
assert_eq!(taxonomy.len(), 3);
let mut sieve = [true; 10000]; //通项法,一共1000个元素,值都是true
for i in 2..100 {
if sieve[i] {
  let mut j = i * i;
  while j < 10000 {
    sieve[j] = false;
    j += i;
  }
}
}
assert!(sieve[211]);
assert!(!sieve[9876]);

和元组一样,数组的长度是其类型的一部分,在编译时固定。不同长度的数组,不属于同一类型。如果n是一个变量,则[true;n]不能表示数组。如果需要在运行时长度可变的数组,需要用到vector。

在数组上迭代元素时,常用的方法——迭代,搜索、排序、填充、筛选等——都以切片的方式出现,而不是数组。但是Rust在使用这些方法时隐式地将对数组的引用转换为切片,因此可以在数组上直接调用任何切片的方法:

let mut chaos = [3, 5, 4, 1, 2];
chaos.sort();
assert_eq!(chaos, [1, 2, 3, 4, 5]);

sort方法实际上是在切片上定义的,但是由于sort通过引用获取其操作数,所以我们可以直接在chaos中使用它:隐式地生成&mut[i32]切片。实际上前面提到的len方法也是一种切片方法。

Vector

Vec<T>是一个可调整大小的T类型元素序列,分配在堆上

创建Vector的方式有:

let mut v = vec![2, 3, 5, 7];  //用vec!宏声明
let mut w = vec![0; 1024]; //用通项法声明
let mut x = Vec::new(); //用Vec的new函数(在Rust中,struct的new()方法相当于其他语言中的类构造函数)
x.push("step"); //vector可以添加元素
x.push("on");
x.push("no");
x.push("pets");
assert_eq!(v, vec!["step", "on", "no", "pets"]);
let y: Vec<i32> = (0..5).collect(); //根据迭代器创建,因为collect()方法可构建多种类型序列的值,所以变量y必须声明类型
assert_eq!(v, [0, 1, 2, 3, 4]);

像数组一样,vector可以使用切片的方法:

let mut v = vec!["a man", "a plan", "a canal", "panama"];
v.reverse();
assert_eq!(v, vec!["panama", "a canal", "a plan", "a man"]); //元素顺序翻转

在这里,reverse方法实际上是在切片类型上定义的,但是vector被隐式地引用了,变为施加在&mut[&str]切片上的方法。

Vec是一种非常常用、非常重要的类型,它几乎可以用于任何需要动态长度的地方,因此还有许多其他方法可以创建向量或扩展现有的向量。

Vec<T>由三个值组成:指向分配给堆内存缓冲区的指针;缓冲区有能力存储的元素数量;以及它现在实际包含的数量。随着长度增加,当缓冲区达到,向向量添加另一个元素需要分配一个更大的缓冲区,将当前内容复制到其中,更新向量的指针和容量以描述新的缓冲区,最后释放旧的缓冲区。

由于这个原理,假设建一个空vector,然后不断往里添加元素。如果用 Vec::new()或者vec![]创建,将会频繁调整vector的大小,因此就会在内存中不断迁移。这时候,最好的办法是能够预估总的vector有多大,一次性申请空间,再添加元素的时候就尽量不重新分配空间,或者少重分配。方法Vec::with_capacity(capacity: usize)能够创建一个有初始化容量的vector,参数capacity代表能存放多少元素。(当然当达到这个数字时,并不是存不进去,而是会找块更大的内存)

let mut v = Vec::with_capacity(2); 
assert_eq!(v.len(), 0);
assert_eq!(v.capacity(), 2); //初始化就有2个空座位
v.push(1);
v.push(2);
assert_eq!(v.len(), 2);
assert_eq!(v.capacity(), 2);
v.push(3); //空座位不够时,再添加元素会重新分配空间
assert_eq!(v.len(), 3);
assert_eq!(v.capacity(), 4); //一般是按空间翻倍分配,这是通常情况的最优算法
//在测试以上代码环境中,也可能不是这个结果,Vec和系统的堆分配器可能会对请求进行取

vector的功能还有:

let mut v = vec![10, 30, 50];

// 在索引2处插入35
v.insert(2, 35);
assert_eq!(v, [10, 30, 35, 50]); // 移除索引1的元素
v.remove(1);
assert_eq!(v, [10, 35, 50]); let mut v = vec!["carmen", "miranda"];

//pop方法移除最后的元素并返回一个Option值,有值时返回Option::Some(val),无值时返回Option::None
assert_eq!(v.pop(), Some(50));
assert_eq!(v.pop(), Some(35));
assert_eq!(v.pop(), Some(10));
assert_eq!(v.pop(), None);

题外话:

Option是一个枚举(Enum)类型,在Rust中非常常用,致力于严格的数据安全规则。有两个元素Some(T)和None,定义是(其中T是指任意一种数据类型):

enum Option<T> {
    Some(T),
    None,
}

Option枚举可以包装一个值,例如一个i32类型的变量a,在一些情况下,可能有个整数值,在一些情况下可能是空值,但又不能用0来表达。Rust没有其他语言中的null或None来表示。这时候,就可以使a赋值为Option枚举类型。在空值的情况下,a = Option::None;在有值时,a=Option::Some(1024),数值写在Some后的括号内。

不用null而是用option类型,是为了严格的数据类型安全,可以避免很多bug。

Option<T> 枚举非常有用,以至于Option作用域已经在语言中内置了,你不需要将其显式引入作用域。它的成员也是如此,可以不需要 Option:: 前缀来直接使用 Some 和 None。今后我们将直接用Some(xxx)和None。

vector 的元素遍历可以用for ... in语句:

for i in vec!["C", "C++", "Python", "Go"] {
println!("语言:{}", i);
}
/* 输出:
  语言:C
  语言:C++
  语言:Python
  语言:Go
*/

总结一下:

    vector的数据分布在堆上,栈上保存其指针;
    可以用new方法或vec!宏或一些其他方法创建;
    如果能预估总长度,最好使用Vec::with_capacity()方法创建;
    有增删改查元素以及排序、翻转、迭代等等方法;
    各种命名(注意大小写):Vec是向量的类型名,就像i32、char类似;vec!是创建向量的宏;vector只是向量的英文单词,不是关键字。

数据类型的上篇到此为止吧,下篇说一下切片Slice、字符串String和文本字符串str。另外,函数在语句篇介绍,trait和闭包有专门的篇章。

说实话,Rust的入门内容挺多,很多特点和其他语言不同,这就是所谓的学习路线陡峭吧。就感觉处处都是知识点,没这些知识点做铺垫,连一个小小的demo都写不了。

但这些都是基础的点,虽然多,学起来还是很容易的。关键还是我在本系列文章的一开始说的:要有空杯心态,把其他语言的习性放一放,不强行混为一谈。

对于喜欢学习的人,有这么一门新鲜的语言,也算是一种福气。

Rust之路(2)——数据类型 上篇的相关教程结束。

《Rust之路(2)——数据类型 上篇.doc》

下载本文的Word格式文档,以方便收藏与打印。