第六章 数组和字符串
数组
数组是一个容器,他在块连续空间内存中存储了一系列同样类型的数据. 数组中的元素的占用空间大小必须是编译器确定的, 数组本身所容纳元素的个数也必须是编译期确定的.执行阶段不可变.变长容器使用 Vec/LinkedList.
[T; n] // T是类型, n是数量
fn main() {
let xs: [i32; 5] = [1,2,3,4,5];
let ys: [i32; 500] = [0; 500]; // 所有元素相同时 的简化写法
println!("some {}", ys[100]);
}
在Rust中,对于两个数组类型,只有元素类型和元素个数都完全相同,这两个数组才是同类型的.数组与指针之间不能隐式转换.同类型的数组之间可以互相赋值.把数组xs作为参数传给一个函数,这个数组并不会退化成一个指针.而是会将这个数组完整复制进这个函数.函数体内对数组的改动不会影响到外面的数组.
多维数组
let v: [[i32; 3]; 3] = [[0,0], [0,0], [0,0]];
数组切片
对数组取借用borrow操作,可以生成一个“数组切片”(Slice).数组切片对数组没有“所有权”,我们可以把数组切片看作专门用于指向数组的指针.比如,我们有一个数组[T;n],它的借用指针的类型就是&[T;n].它可以通过编译器内部魔法转换为数组切片类型&[T].
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和胖指针
切片比单纯的指针多一个长度信息. 切片有一个别名叫胖指针.与这个概念相对应的概念是“动态大小类型”(Dynamic Sized Type,DST). 所谓的DST指的是编译阶段无法确定占用空间大小的类型.为了安全性,指向DST的指针一般是胖指针.
&[T] 类型占用了两个指针大小的内存空间, 一个是数组地址,一个是数组长度大小.
对于DST类型,Rust有如下限制:
- 只能通过指针来间接创建和操作DST类型,
&[T]Box<[T]>可以,[T]不可以; - 局部变量和函数参数的类型不能是DST类型,因为局部变量和函数参数必须在编译阶段知道它的大小因为目前
unsized rvalue功能还没有实现; - enum中不能包含DST类型,struct中只有最后一个元素可以是 DST,其他地方不行,如果包含有DST类型,那么这个结构体也就成了 DST类型
Range
代表一个区间, 一个范围. 内置支持 start..end(左闭右开)
fn main() {
let r = 1..10;
for i in r {
print!("{:?}\t", i);
}
}
..生成的是个std::ops::Range<_>类型的变量.标准库定义为(居然是个结构体)
pub struct Range<Idx> {
pub start: Idx,
pub end: Idx
}
这个类型是实现了 Iterator trait.因此可以直接用到循环中.
100 递减到 10
fn main() {
use std::iter::Iterator;
// 先用rev方法反转, 然后用map 乘以10. =>> 100 90 80 70
let r = (1i32..11).rev().map(|i| i * 10);
for i in r {
print!("{:?}\t", i);
}
}
其他的Range 类型(std::ops - Rust (rust-lang.org)):
std::ops::RangeFrom 只有起始值, 没有结束值. start.. ==> [start ....
std::ops::RangeTo 只有结束值, 没有起始值. ..end ==> ....end)
std::ops::RangeFull 使用全部值. .. ==> [start..end]
如果我们希望产生一个i32类型的从0到 i32::MAX的范围,为按语法,我们应该写0.. (i32::MAX+1),然而(i32::MAX+1)已经溢出了. 此时就需要一个闭区间, 使用..=.
对应的类型是
std::ops::RangeInclusive 语法为 start..=end
std::ops::RangeToInclusive语法为 ..=end
let arr = [0, 1, 2, 3, 4];
assert_eq!(arr[ .. ], [0, 1, 2, 3, 4]); // This is the `RangeFull` $$$
assert_eq!(arr[ .. 3], [0, 1, 2 ]); // This is a `RangeTo` $$$
assert_eq!(arr[ ..=3], [0, 1, 2, 3 ]); // This is a `RangeToInclusive`
assert_eq!(arr[1.. ], [ 1, 2, 3, 4]); // This is a `RangeFrom`
assert_eq!(arr[1.. 3], [ 1, 2 ]); // This is a `Range`
assert_eq!(arr[1..=3], [ 1, 2, 3 ]); // This is a `RangeInclusive` $$$
上面几个标记$$$的类型不能作为迭代器使用.
字符串
rust字符串使人困扰的原因
- rust倾向于暴露可能的错误
- 字符串数据结构复杂
- rust中的字符串是utf-8
什么是字符串
byte类型的集合, 并且提供一些方法,能将byte解析为文本.
在rust的核心语言层面,只有一个字符串类型:字符串切片str(或&str)
字符串切片: 对存储在其他地方, utf-8编码的字符串的引用. 字符串字面值: 存储在二进制文件中, 也是字符串切片.
这里要讲的是String 类型:
此类型来自标准库而不是核心语言层面.存储在堆上可以增长、可修改、可拥有.使用utf-8编码.
其他类型的字符串
rust标准库中包含了很多其他的字符串类型,例如: OsString、OsStr、CString、CStr
- 有String 和Str 后缀的,分别代表String拥有所有权或Str借用的变体
- 可以存储不同编码的文本或在内存中以不同的形式展现.
library crate针对存储字符串可提供更多的选项
字符串字面量是切片
let s: &str = "Hello, world!";
&str
字符串有两种类型, 一种是&str 一种是 String, String拥有所有权.
str是Rust的内置类型. &str是对str的借用.Rust的字符串内部默认是使用utf-8编码格式的. 字符串底层是一个用u8类型的数组.实际上str类型有一种方法: fn as_ptr(&self)-> *const u8. 它内部无须做任何计算,只需做一个强制 类型转换即可
self as *const str as *const u8
Rust 中的 字符 (注意是字符)是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是在字符串中不一样,字符串是 UTF-8 编码,也就是字符串中的字符所占的字节数是变化的(1 - 4) ,这样有助于大幅降低字符串所占用的内存空间.
rust 这种设计, 在查找字符串中的一个字符的时间是 O(n)的.并不是 O(1)的时间复杂度.他需要从头开始遍历找到所需的字符串.
&str 也是一个胖指针.
fn main() {
println!("Size of pointer:{}", std::mem::size_of::<*const ()>());
println!("Size of &str :{}", std::mem::size_of::<&str>());
}
String
String 的特点是它具有字符串的所有权, 有管理内存空间的权力.很多Vec<T>的操作都可用于String.创建方法
String::new() // 函数
使用初始值来创建String:
to_string() 方法 ,可用于实现了Display trait 的类型,包括字符串字面值.
fn main() {
let data = "initial contents";
let s = data.to_string();
let s1 = "initial contents".to_string();
}
String::from()函数,从字面值创建String
fn main() {
let s = String::from("initial contents")
}
&str的问题是无法直接修改字符串
let greeting: &str = "hello" // 无法添加字符串, 无法扩展他的存储空间.
fn main() {
let mut s = String::from("hello");
s.push(' '); // 可以添加字符
s.push_str("World.");
println!("{}", s);
}
fn capitalize(substr: &mut str) {
substr.make_ascii_uppercase(); // 可以改变字符串中的值
}
fn main() {
let mut s = String::from("Hello World");
capitalize(&mut s); // 将&mut String 转成了 &mut str
println!("{}", s);
}
String 与 &str的转换
&str ==> String
String::from("hello, world")
"hello, world".to_string()
String ==> &str
let s = String::from("hello, world!");
say_hello(&s);
say_hello(&s[..]);
say_hello(s.as_str());
字符串索引
就如上面说的, 对字符串的索引是对底层[u8] 数组的索引, 要十分注意每个字符的字节大小是不同的, 如果从中间切分一个字符, 会导致无法正确显示, 程序报错. 比如一个中文占三个字节, 取两个字节就会报错, (中英文混合就有问题...)
let s = "一个字符串";
let s2 = &s[..2]; // 报错!
字符串操作
追加
在字符串尾部可以使用 push() 方法追加字符 char,也可以使用 push_str() 方法追加字符串字面量. 这两个方法都是在原有字符串上追加, 不会返回新的字符串. 原字符串需要是mut.
fn main() {
let mut s = String::from("hello ");
s.push('r');
s.push_str("ust!");
}
插入
insert() 方法插入单个字符 char, insert_str() 方法插入字符串字面量. 需要输入两个参数, 第一个参数是插入的位置.
fn main() {
let mut s = String::from("hello rust!");
s.insert(5, ',');
s.insert_str(6, " I like");
}
替换
-
replace
适用于String 和 &str, 接收两个参数, 第一个是要被替换的字符串, 第二个是新的字符串. 该方法会替换所有匹配到的字符串, 返回新的字符串, 而不是修改原有的字符串.
fn main() { let string_replace = String::from("I like rust. Learning rust is my favorite!"); let new_string_replace = string_replace.replace("rust", "RUST"); } -
replacen
适用于String 和 &str, 接收三个参数, 第一个是要被替换的字符串, 第二个是新的字符串. 第三个是替换的个数.返回一个新的字符串,而不是操作原来的字符串.
fn main() { let string_replace = "I like rust. Learning rust is my favorite!"; let new_string_replacen = string_replace.replacen("rust", "RUST", 1); } -
replace_range
该方法仅适用于
String类型.replace_range接收两个参数,第一个参数是要替换字符串的范围(Range),第二个参数是新的字符串. 该方法是直接操作原来的字符串,不会返回新的字符串. 该方法需要使用mut关键字修饰.
删除
删除的方法有4个, pop(),remove(),truncate(),clear(). 这四个方法仅适用于 String 类型.
-
pop 删除并返回字符串的最后一个字符
返回值是一个
Option, 字符串为空时为None -
remove 删除并返回字符串中指定位置的字符(注意是字符)
指定的是一个字符的起始位置,比如一个中文字符串
let s = String::from("一个字符串"); s.remove(0); // 第一个字符的起始位置 s.remove(2); // 这里会报错, 因为2不是一个字符的起始位置 s.remove(3); // 第二个字符的起始位置 -
truncate 删除字符串中从指定位置开始到结尾的全部字符.
无返回值. 直接操作原字符串.
-
clear 清空字符串.
直接操作原字符串
拼接
使用 + 或者 += 连接字符串,要求右边的参数必须为字符串的切片引用(Slice)类型.其实当调用 + 的操作符时,相当于调用了 std::string 标准库中的 add() 方法,这里 add() 方法的第二个参数是一个引用的类型.因此我们在使用 +, 必须传递切片引用类型.不能直接传递 String 类型. + 和 += 都是返回一个新的字符串.所以变量声明可以不需要 mut 关键字修饰.
fn main() {
let string_append = String::from("hello ");
let string_rust = String::from("rust");
let result = string_append + &string_rust; // string_append的所有权被转移到result
let mut result = result + "!";
result += "!!!";
}
使用format!
适用于 String 和 &str
fn main() {
let s1 = "hello";
let s2 = String::from("rust");
let s = format!("{} {}!", s1, s2);
}
字符串转义
我们可以通过转义的方式 `` 输出 ASCII 和 Unicode 字符.
fn main() {
// 通过 \ + 字符的十六进制表示,转义输出一个字符
let byte_escape = "I'm writing \x52\x75\x73\x74!";
println!("What are you doing\x3F (\x3F means ?) {}", byte_escape);
// \u 可以输出一个 unicode 字符
let unicode_codepoint = "\u{211D}";
let character_name = ""DOUBLE-STRUCK CAPITAL R"";
println!(
"Unicode character {} (U+211D) is called {}",
unicode_codepoint, character_name
);
// 换行了也会保持之前的字符串格式
let long_string = "String literals
can span multiple lines.
The linebreak and indentation here ->\
<- can be escaped too!";
println!("{}", long_string);
}
保持字符串的原样,不要转义:
fn main() {
println!("{}", "hello \x52\x75\x73\x74");
let raw_str = r"Escapes don't work here: \x3F \u{211D}";
println!("{}", raw_str);
// 如果字符串包含双引号,可以在开头和结尾加 #
let quotes = r#"And then I said: "There is no escape!""#;
println!("{}", quotes);
// 如果还是有歧义,可以继续增加,没有限制
let longer_delimiter = r###"A string with "# in it. And even "##!"###;
println!("{}", longer_delimiter);
}
操作 UTF-8 字符串
如果你想要以 Unicode 字符的方式遍历字符串,最好的办法是使用 chars 方法,例如:
for c in "中国人".chars() {
println!("{}", c);
}
以字节的方式返回字符串
for b in "中国人".bytes() {
println!("{}", b);
}
想要准确的从 UTF-8 字符串中获取子串是较为复杂的事情,例如想要从 holla中国人नमस्ते 这种变长的字符串中取出某一个子串,使用标准库你是做不到的. 你需要在 crates.io 上搜索 utf8 来寻找想要的功能.
对String按索引的形式进行访问
按索引的语法访问String的某部分,会报错!!!!
&s1[0]
rust的字符串不支持索引语法的访问
String的内部表示: String是对Vec<u8>的包装,并且他有一个len方法,返回String的字节数.
fn main() {
let len = String::from("something").len()
println!("{}", len)
}
rust中看待字符串有三种方式 字节(bytes)、标量值(scalar values)、字形簇(grapheme clusters)
字节:查看的是字符串每一个字节存储的整数值.
标量值: 这是unicode中的概念,描述的是一个unicode字符的表示
字形簇:最接近我们看到的字符,但是rust 的标准库是不支持的.
fn main() {
let w = "一个字符串";
println!("{}", w.len()); // 15个字节
for b in w.bytes() {
println!("{}", b); // 228 184 128 228 184 170 229 173 151 231 172 166 228 184 178
// 每个中文字符占用3个字节
}
}
fn main() {
let w = "一个字符串";
for b in w.chars() {
println!("{}", b);
}
}
rust 不允许对String进行索引的最后一个原因是:
- 索引操作应该消耗常量的时间(O(1))
- 而String无法保证这一点,它需要遍历所有内容,来确定有多少合法的字符.