Rust之路(2)——数据类型 上篇
【未经书面同意,严禁转载】 -- 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:带符号 |
42, -5i8, 0x400u16, 0o100i16, 20_922_789_888_000u64, b'*' (u8 byte literal) |
isize, usize |
带符号/无符号 整型 |
137, -0b0101_0010isize, 0xffff_fc00usize |
f32, f64 |
IEEE标准的浮点数, 单精度/双精度 |
1.61803, 3.14f32, 6.0221e23f64 |
bool |
布尔型 |
true, false |
char |
Unicode字符 |
'*', '\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() |
&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内方法的对象 |
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之路(3)——数据类型 下篇
[未经书面同意,严禁转载] -- 2020-10-14 -- 架构是道,数据是术.道可道,非常道:术不名,不成术!道无常形,术却可循规. 学习与分析数据类型,最基本的方法就是搞清楚其存储原理,变量和对 ...
- Rust之路(4)——所有权
[未经书面同意,严禁转载] -- 2020-10-14 -- 所有权是Rust的重中之重(这口气咋像高中数学老师 WTF......). 所有权是指的对内存实际存储的数据的访问权(包括读取和修改),在 ...
- python之路:数据类型初识
python开发之路:数据类型初识 数据类型非常重要.不过我这么说吧,他不重要我还讲个屁? 好,既然有人对数据类型不了解,我就讲一讲吧.反正这东西不需要什么python代码. 数据类型我讲的很死板.. ...
- Scala进阶之路-高级数据类型之集合的使用
Scala进阶之路-高级数据类型之集合的使用 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. Scala 的集合有三大类:序列 Seq.集 Set.映射 Map,所有的集合都扩展自 ...
- Scala进阶之路-高级数据类型之数组的使用
Scala进阶之路-高级数据类型之数组的使用 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.数组的初始化方式 1>.长度不可变数组Array 注意:顾名思义,长度不可变数 ...
- Rust之路(0)
Rust--一个2012年出现,2015年推出1.0版本的"年轻"语言.在 2016 至 2018 年的 stack overflow 开发人员调查中,被评比为 "最受欢 ...
- Rust之路(1)
[未经书面许可,严禁转载]-- 2020-10-09 -- 正式开始Rust学习之路了! 思而不学则罔,学而不思则殆.边学边练才能快速上手,让我们先来个Hello World! 但前提是有Rust环境 ...
- Rust学习笔记一 数据类型
写在前面 我也不是什么特别厉害的大牛,学历也很低,只是对一些新语言比较感兴趣,接触过的语言不算多也不算少,大部分也都浅尝辄止,所以理解上可能会有一些偏差. 自学了Java.Kotlin.Python. ...
- Python之路-python数据类型(列表、字典、字符串、元祖)操作
一.列表: 列表的语法,以中括号开通和结尾,元素以逗号隔开.例如:name = [] 列表是以下标取值,第一个元素下标是0,第二个元素下标是1,最后一个元素下标是-1. 1.增加 #name = ...
随机推荐
- MyBatis的逆向工程、Example类
public void testFindUserByName(){ //通过criteria构造查询条件 UserExample userExample = new UserExample(); us ...
- Git 不能提交空目录?我也是醉了!
Git 不能提交空目录?我也是醉了! 背景 最近在提交文件时,因为是空的 Maven 项目结构,发现 Git 空目录死活不能提交,还以为是我自己在 .gitignore 文件中忽略了,在网上查了下,原 ...
- [Leetcode]225. 用队列实现栈 、剑指 Offer 09. 用两个栈实现队列
##225. 用队列实现栈 如题 ###题解 在push时候搞点事情:push时入队1,在把队2的元素一个个入队1,再交换队2和队1,保持队1除pushguocheng 始终为空. ###代码 cla ...
- HA切换失败原因分析
1. 问题描述 redhat在进行HA切换时,需要先停止service,并释放调当前主机占有的资源,比如说IP Address和Filesystem,但今天我在验证HA切换时,发现service一直停 ...
- 初识ABP vNext(10):ABP设置管理
Tips:本篇已加入系列文章阅读目录,可点击查看更多相关文章. 目录 前言 开始 定义设置 使用设置 最后 前言 上一篇介绍了ABP模块化开发的基本步骤,完成了一个简单的文件上传功能.通常的模块都有一 ...
- Cobalt Strike后渗透安装和初步使用
Cobalt Strike安装 系统要求 Cobalt Strike要求Java 1.8,Oracle Java ,或OpenJDK . 如果你的系统上装有防病毒产品,请确保在安装 Cobalt St ...
- Java并发包之Executors
概述 Executor.ExecutorService.ScheduledExecutorService.ThreadFactory.Callable的工厂和工具类. 方法 构造一个固定线程数目的线程 ...
- 关于微信小程序官网的使用
我们在看微信支付相关的东西的时候,会发现有些想找的地址不好找,,没看到入口,接下来我就是整理了一下 链接: https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa ...
- ssh 远程执行命令 nohup 无效问题
昨夜1:00多准备睡觉了,突然一哥们咨询了我一个问题. 他A机器上远程执行B机器(ssh user@ip "command")上的脚本,B上的服务并没有起来. 看了下截图,脚本确实 ...
- 容器云平台No.2~kubeadm创建高可用集群v1.19.1
通过kubernetes构建容器云平台第二篇,最近刚好官方发布了V1.19.0,本文就以最新版来介绍通过kubeadm安装高可用的kubernetes集群. 市面上安装k8s的工具很多,但是用于学习的 ...