【未经书面同意,严禁转载】 -- 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(仅限于元组内的元素也是值类型)。在这里,所谓的基本类型,有以下特点:

  1. 数据分布在栈上,在参数传递的过程中会复制一个值用于传递,本身不会受影响;
  2. 数据在编译时即可知道占用多大空间,比如i32占据4字节;
  3. 因为上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
*/

总结一下:

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

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

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

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

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

Rust之路(2)——数据类型 上篇的更多相关文章

  1. Rust之路(3)——数据类型 下篇

    [未经书面同意,严禁转载] -- 2020-10-14 -- 架构是道,数据是术.道可道,非常道:术不名,不成术!道无常形,术却可循规. 学习与分析数据类型,最基本的方法就是搞清楚其存储原理,变量和对 ...

  2. Rust之路(4)——所有权

    [未经书面同意,严禁转载] -- 2020-10-14 -- 所有权是Rust的重中之重(这口气咋像高中数学老师 WTF......). 所有权是指的对内存实际存储的数据的访问权(包括读取和修改),在 ...

  3. python之路:数据类型初识

    python开发之路:数据类型初识 数据类型非常重要.不过我这么说吧,他不重要我还讲个屁? 好,既然有人对数据类型不了解,我就讲一讲吧.反正这东西不需要什么python代码. 数据类型我讲的很死板.. ...

  4. Scala进阶之路-高级数据类型之集合的使用

    Scala进阶之路-高级数据类型之集合的使用 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. Scala 的集合有三大类:序列 Seq.集 Set.映射 Map,所有的集合都扩展自 ...

  5. Scala进阶之路-高级数据类型之数组的使用

    Scala进阶之路-高级数据类型之数组的使用 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.数组的初始化方式 1>.长度不可变数组Array 注意:顾名思义,长度不可变数 ...

  6. Rust之路(0)

    Rust--一个2012年出现,2015年推出1.0版本的"年轻"语言.在 2016 至 2018 年的 stack overflow 开发人员调查中,被评比为 "最受欢 ...

  7. Rust之路(1)

    [未经书面许可,严禁转载]-- 2020-10-09 -- 正式开始Rust学习之路了! 思而不学则罔,学而不思则殆.边学边练才能快速上手,让我们先来个Hello World! 但前提是有Rust环境 ...

  8. Rust学习笔记一 数据类型

    写在前面 我也不是什么特别厉害的大牛,学历也很低,只是对一些新语言比较感兴趣,接触过的语言不算多也不算少,大部分也都浅尝辄止,所以理解上可能会有一些偏差. 自学了Java.Kotlin.Python. ...

  9. Python之路-python数据类型(列表、字典、字符串、元祖)操作

    一.列表: 列表的语法,以中括号开通和结尾,元素以逗号隔开.例如:name = [] 列表是以下标取值,第一个元素下标是0,第二个元素下标是1,最后一个元素下标是-1.   1.增加 #name = ...

随机推荐

  1. ZT:C/C++ 字符串与数字相互转换

    转载地址:https://www.cnblogs.com/happygirl-zjj/p/4633789.html 一.利用stringstream类 1. 字符串到整数     stringstre ...

  2. BIO应用-RPC框架

    为什么要有RPC?  我们最开始开发的时候,一个应用一台机器,将所有功能都写在一起,比如说比较常见的电商场景. 随着我们业务的发展,我们需要提示性能了,我们会怎么做?将不同的业务功能放到线程里来实现异 ...

  3. 从一知半解到揭晓Java高级语法—泛型

    目录 前言 探讨 泛型解决了什么问题? 扩展 引入泛型 什么是泛型? 泛型类 泛型接口 泛型方法 类型擦除 擦除的问题 边界 通配符 上界通配符 下界通配符 通配符和向上转型 泛型约束 实践总结 泛型 ...

  4. oracle之数据限定与排序

    数据限定与排序 6.1 简单查询语句执行顺序 from, where, group by, having, order by, select where限定from后面的表或视图,限定的选项只能是表的 ...

  5. Java常见重构技巧 - 去除不必要的!=null判断空的5种方式,很少有人知道后两种

    常见重构技巧 - 去除不必要的!= 项目中会存在大量判空代码,多么丑陋繁冗!如何避免这种情况?我们是否滥用了判空呢?@pdai 常见重构技巧 - 去除不必要的!= 场景一:null无意义之常规判断空 ...

  6. 认识一下python

    python 目录 python 1.python创始人 2.python的设计目标 3.为什么使用python 4.python的特点 5.python的优缺点 1.python创始人 1.1989 ...

  7. MaaS系统概述

    摘要:共享经济正改变着人们的生活方式,城市公共交通系统应该顺应共享经济的潮流进行转型.近年来,西方国家提出的“出行即服务(MaaS)”理念为我国解决日益严重的城市交通拥堵问题提供了新的思路.基于Maa ...

  8. hystrix(2) metrics

    上一节讲到了hystrix提供的五个功能,这一节我们首先来讲hystrix中提供实时执行metrics信息的实现.为什么先讲metrics,因为很多功能都是基于metrics的数据来实现的,它是很多功 ...

  9. hystrix熔断器之HystrixRequestLog

    HystrixRequestLog会记录所有执行过的命令.

  10. Flutter学习五之网络请求和轮播图的实现

    上期讲到了,怎样实现一个下拉刷新和加载更多的列表,数据更新,需要使用到网络请求,在flutter中,怎样实现一个网络请求呢?官方使用的是dart io中的HttpClient发起的请求,但HttpCl ...