数组
简介
数组本身所容纳的元素个数也必须是编译期确定的,执行阶段不可变。数组类型的表示方式为[T; n]。其中T代表元素类型;n代表元素个数。
1 | fn main() { |
在Rust中,对于两个数组类型,只有元素类型和元素个数都完全相同,这两个数组才是同类型的。
1 | fn main() { |
把数组xs作为参数传给一个函数,这个数组并不会退化成一个指针。而是会将这个数组完整复制进这个函数。函数体内对数组的改动不会影响到外面的数组。
内置方法
1 | fn main() { |
数组本身没有实现IntoIterator trait,但是数组切片是实现了的。所以我们可以直接在for in循环中使用数组切片,而不能直接使用数组本身。
多维数组
1 | fn main() { |
数组切片
1 | fn main() { |
DST和胖指针
Slice与普通的指针是不同的,它有一个非常形象的名字:胖指针(fat pointer)。与这个概念相对应的概念是“动态大小类型”(Dynamic Sized Type, DST)。所谓的DST指的是编译阶段无法确定占用空间大小的类型。为了安全性,指向DST的指针一般是胖指针。
胖指针内部的数据既包含了指向源数组的地址,又包含了该切片的长度。对于DST类型,Rust有如下限制:
❏ 只能通过指针来间接创建和操作DST类型,&[T] Box<[T]>可以,[T]不可以;
❏ 局部变量和函数参数的类型不能是DST类型,因为局部变量和函数参数必须在编译阶段知道它的大小因为目前unsized rvalue功能还没有实现;
❏ enum中不能包含DST类型,struct中只有最后一个元素可以是DST,其他地方不行,如果包含有DST类型,那么这个结构体也就成了DST类型。
这一设计的好处有:
❏ 首先,DST类型虽然有一些限制条件,但我们依然可以把它当成合法的类型看待,比如,可以为这样的类型实现trait、添加方法、用在泛型参数中等;
❏ 胖指针的设计,避免了数组类型作为参数传递时自动退化为裸指针类型,丢失了长度信息的问题,保证了类型安全;
❏ 这一设计依然保持了与“所有权”“生命周期”等概念相容的特点。
range
1 | fn main() { |
.. 实际是如下struct的语法糖
1 | pub struct Range<Idx> { |
这个类型实现了Iterator trait,因此它可以直接应用到循环语句中。比如,我们要实现从100递减到10,中间间隔为10的序列,可以这么做:
1 | fn main() { |
在Rust中,还有其他的几种Range,包括
❏ std::ops::RangeFrom代表只有起始没有结束的范围,语法为start..,含义是[start, +∞);
❏ std::ops::RangeTo代表没有起始只有结束的范围,语法为..end,对有符号数的含义是(-∞, end),对无符号数的含义是[0, end);
❏ std::ops::RangeFull代表没有上下限制的范围,语法为..,对有符号数的含义是(-∞, +∞),对无符号数的含义是[0, +∞)。
1 | fn print_slice(arr: &[i32]) { |
1 | Length: 5 |
虽然左闭右开区间是最常用的写法,然而,在有些情况下,这种语法不足以处理边界问题。
比如,我们希望产生一个i32类型的从0到i32::MAX的范围,就无法表示。因为按语法,我们应该写0..(i32::MAX + 1),然而(i32::MAX+1)已经溢出了。
所以,Rust还提供了一种左闭右闭区间的语法,它使用这种语法来表示..=。
闭区间对应的标准库中的类型是:
❏ std::ops::RangeInclusive,语法为start..=end,含义是[start, end]
❏ std::ops::RangeToInclusive,语法为..=end,对有符号数的含义是(-∞,end],对无符号数的含义是[0, end]
边界检查
如果index超过了数组的真实长度范围,会执行panic!操作,导致线程abort。使用Range等类型做Index操作的执行流程与此类似。
为了防止索引操作导致程序崩溃,如果我们不确定使用的“索引”是否合法,应该使用get()方法调用来获取数组中的元素,这个方法不会引起panic!,它的返回类型是Option
1 | fn main() { |
1 | fn main() { |
字符串
&str
str是Rust的内置类型。&str是对str的借用。Rust的字符串内部默认是使用utf-8编码格式的。而内置的char类型是4字节长度的,存储的内容是Unicode Scalar Value。所以,Rust里面的字符串不能视为char类型的数组,而更接近u8类型的数组。实际上str类型有一种方法:fn as_ptr(&self) -> *const u8。它内部无须做任何计算,只需做一个强制类型转换即可:
1 | self as *const str as *const u8 |
这样设计有一个缺点,就是不能支持O(1)时间复杂度的索引操作。如果我们要找一个字符串s内部的第n个字符,不能直接通过s[n]得到,这一点跟其他许多语言不一样。在Rust中,这样的需求可以通过下面的语句实现:
1 | s.chars().nth(n) |
它的时间复杂度是O(n),因为utf-8是变长编码,如果我们不从头开始过一遍,根本不知道第n个字符的地址在什么地方。
String
&str类型的主要区别是,它有管理内存空间的权力。&str类型是对一块字符串区间的借用,它对所指向的内存空间没有所有权,哪怕&mut str也一样。
1 | let greeting : &str = "Hello"; |
1 | fn main() { |
String类型在堆上动态申请了一块内存空间,它有权对这块内存空间进行扩容,内部实现类似于std::Vec