[ Rust笔记 六] 基础-数组和字符串

数组

简介

数组本身所容纳的元素个数也必须是编译期确定的,执行阶段不可变。数组类型的表示方式为[T; n]​。其中T代表元素类型;n代表元素个数。

1
2
3
4
5
6
fn main() {
// 定长数组
let xs: [i32; 5] = [1, 2, 3, 4, 5];
// 所有的元素,如果初始化为同样的数据,可以使用如下语法
let ys: [i32; 500] = [0; 500];
}

在Rust中,对于两个数组类型,只有元素类型和元素个数都完全相同,这两个数组才是同类型的。

1
2
3
4
5
6
fn main() {
let mut xs: [i32; 5] = [1, 2, 3, 4, 5];
let ys: [i32; 5] = [6, 7, 8, 9, 10];
xs = ys; //同类型,正常赋值
println!("new array {:? }", xs);
}

把数组xs作为参数传给一个函数,这个数组并不会退化成一个指针。而是会将这个数组完整复制进这个函数。函数体内对数组的改动不会影响到外面的数组。

内置方法

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let v1 = [1, 2, 3];
let v2 = [1, 2, 4];
println!("{:? }", v1 < v2 );//比较大小
}

fn main() {
let v = [0_i32; 10];
for i in &v {//遍历切片
println!("{:? }", i);
}
}

数组本身没有实现IntoIterator trait,但是数组切片是实现了的。所以我们可以直接在for in循环中使用数组切片,而不能直接使用数组本身。

多维数组

1
2
3
4
5
6
fn main() {
let v : [[i32; 2]; 3] = [[0, 0], [0, 0], [0, 0]];
for i in &v {
println!("{:? }", i);
}
}

数组切片

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
fn mut_array(a : &mut [i32]) {
a[2] = 5;
}
println!("size of &[i32; 3] : {:? }", std::mem::size_of::<&[i32; 3]>());
println!("size of &[i32] : {:? }", std::mem::size_of::<&[i32]>());
let mut v : [i32; 3] = [1,2,3];
{
let s : &mut [i32; 3] = &mut v;
mut_array(s);
}
println!("{:? }", v);
}

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
2
3
4
5
6
fn main() {
let r = 1..10; // r是一个Range<i32>,中间是两个点,代表[1,10)这个区间
for i in r {
print! ("{:? }\t", i);
}
}

.. 实际是如下struct的语法糖

1
2
3
4
5
6
pub struct Range<Idx> {
/// The lower bound of the range (inclusive).
pub start: Idx,
/// The upper bound of the range (exclusive).
pub end: Idx,
}

这个类型实现了Iterator trait,因此它可以直接应用到循环语句中。比如,我们要实现从100递减到10,中间间隔为10的序列,可以这么做​:

1
2
3
4
5
6
7
8
fn main() {
use std::iter::Iterator;
// 先用rev方法把这个区间反过来,然后用map方法把每个元素乘以10
let r = (1i32..11).rev().map(|i| i * 10);
for i in r {
print! ("{:? }\t", i);
}
}

在Rust中,还有其他的几种Range,包括

❏ std::ops::RangeFrom代表只有起始没有结束的范围,语法为start..,含义是[start, +∞);

❏ std::ops::RangeTo代表没有起始只有结束的范围,语法为..end,对有符号数的含义是(-∞, end),对无符号数的含义是[0, end);

❏ std::ops::RangeFull代表没有上下限制的范围,语法为..,对有符号数的含义是(-∞, +∞),对无符号数的含义是[0, +∞)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn print_slice(arr: &[i32]) {
println!("Length: {}", arr.len());
for item in arr {
print! ("{}\t", item);
}
println!("");
}
fn main() {
let arr : [i32; 5] = [1, 2, 3, 4, 5];
print_slice(&arr[..]); // full range
let slice = &arr[2..]; // RangeFrom
print_slice(slice);
let slice2 = &slice[..2]; // RangeTo
print_slice(slice2);
}
1
2
3
4
5
6
Length: 5
1 2 3 4 5
Length: 3
3 4 5
Length: 2
3 4

虽然左闭右开区间是最常用的写法,然而,在有些情况下,这种语法不足以处理边界问题。
比如,我们希望产生一个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
2
3
4
5
6
fn main() {
let v = [10i32, 20, 30, 40, 50];
let first = v.get(0);
let tenth = v.get(10);
println!("{:? } {:? }", first, tenth);
}
1
2
3
4
5
6
7
8
9
10
11
fn main() {
use std::iter::Iterator;
let v = &[10i32, 20, 30, 40, 50];
// 如果我们同时需要index和内部元素的值,调用enumerate()方法
for (index, value) in v.iter().enumerate() {
println! ("{} {}", index, value);
}
// filter方法可以执行过滤,nth函数可以获取第n个元素
let item = v.iter().filter(|&x| *x % 2 == 0).nth(2);
println! ("{:? }", item);
}

字符串

&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
2
3
4
5
6
fn main() {
let mut s = String::from("Hello");
s.push(' ');
s.push_str("World.");
println!("{}", s);
}

String类型在堆上动态申请了一块内存空间,它有权对这块内存空间进行扩容,内部实现类似于std::Vec类型。所以我们可以把这个类型作为容纳字符串的容器使用。