复合类型

复合类型是由其他类型组合而成的,最典型的就是结构体struct和枚举enum,例如平面上的一个点point(x,y),由两个数值类型的值 x, y 组合而来。

看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#![allow(unused_variables)]
type File = String;

fn open(f: &mut File) -> bool {
true
}

fn close(f: &mut File) {
true
}

#[allow(dead_code)]
fn read(f: &mut File, save_to: &mut Vec<u8>) -> !{
unimplemented!()
}

fn main() {
let mut f1 = File::from("f1.txt");
open(&mut f1);
close(&mut f1);
}

有些方法只提供API结构,不提供具体实现。 此外,有的变量在声明之后并未使用,因此在这个阶段我们需要排除一些编译器噪音。

read函数也很有意思,这个地方返回 ! , 因为 unimplemented告诉编译器这个代码尚未实现。类似的还有 todo!()

字符串

首先,rust里面的字符串复杂很多,接下来的内容一定仔细看。

先看一段简单的代码:

1
2
3
4
5
6
7
8
fn main() {
let my_name = "Pascal";
greet(my_name);
}

fn greet(name: String){
println!("Hello, {}!", name);
}

greet接受name参数,然后打印出来。

而事实上是,这段代码无法通过编译,因为我们实际上传进去的是&str类型的字符串。

切片 slice

对于字符串而言,切片就是对 String 类型中的某一部分的引用

1
2
3
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];

使用方括号包括的一个序列:[ 开始索引..终止索引]

开始索引是第一个元素的索引位置,终止索引是最后一个元素的索引位置的下一个位置。右半开区间。

切片数据结构内部都会保存开始的位置和切片的长度,其中长度是通过终止索引 - 开始索引的方式计算得来的

使用Rust的 .. range序列语法的时候,可以使用以下的方式

1
2
3
4
5
let s = String::from("hello world");
let slice = &s[3..]; // 从索引3开始到结尾
let slice = &s[..3]; // 从开头到索引3(不包含索引3)
let slice = &s[..]; // 从开头到结尾
let slice = &s[0..2]; // 从索引0到索引2(不包含索引2)

如果想要切片包含String的最后一个字节

1
2
3
4
5
6
let s = String::from("hello");

let len = s.len();

let slice = &s[3..len]; // 从索引3到结尾
let slice = &s[0..]; // 从开头到结尾

在对字符串使用切片语法时需要格外小心,切片的索引必须落在字符之间的边界位置,也就是 UTF-8 字符的边界,例如中文在 UTF-8 中占用三个字节,下面的代码就会崩溃:

1
2
3
let s = "中国人";
let a = &s[0..2];
println!("{}",a);

因为我们只取 s 字符串的前两个字节,但是本例中每个汉字占用三个字节,因此没有落在边界处,也就是连 中 字都取不完整,此时程序会直接崩溃退出,如果改成 &s[0..3],则可以正常通过编译。 因此,当你需要对字符串做切片索引操作时,需要格外小心这一点

看看下面这个代码

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let mut s = String::from("hello world");

let word = first_word(&s);

s.clear(); // error!

println!("the first word is: {}", word);
}
fn first_word(s: &String) -> &str {
&s[..1]
}

这个函数会报错。

因为 let word 这个地方,对于传到函数里面的s是一个不可变引用,而s.clear()是一个可变引用。

当我们有了可变的借用的时候,Rust不允许我们再有不可变的借用。因为rust承诺这个不可变借用的引用在整个生命周期内都是有效的。

而后面的println!() 又使用了不可变借用。也是就是在 s.clear()处可变借用和不可变借用试图同时生效。

其他切片

1
2
3
4
5
6

let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

assert_eq!(slice, &[2, 3]);

字符串字面量是切片

1
let s: &str = "Hello, world!";

该切片指向了程序中可执行文件中的某一个点,这也是为什么字符串字面量是不可变的,因为它们被硬编码进了程序中。

什么是字符串

字符串是由字符组成的连续集合。 Rust中的字符是Unicode类型,每个字符占据4个字节的内存空间,但是在字符串中不一样,字符串是UTF-8编码,也就是字符串中的字符所占的字节数是变化的(1-4).

Rust 在语言级别,只有一种字符串类型: str,它通常是以引用类型出现 &str,也就是上文提到的字符串切片。虽然语言级别只有上述的 str 类型,但是在标准库里,还有多种不同用途的字符串类型,其中使用最广的即是 String 类型。

str 类型是硬编码进可执行文件,也无法被修改,但是 String 则是一个可增长、可改变且具有所有权的 UTF-8 编码字符串,当 Rust 用户提到字符串时,往往指的就是 String 类型和 &str 字符串切片类型,这两个类型都是 UTF-8 编码。

String和 &str的转换

之前的代码中已经见到过好几种从 &str 类型生成 String类型的操作:

  • String::from("hello")
  • "hello".to_string()

那么如何将String 转换为 &str呢,取得引用就可以了。

1
2
3
4
5
6
7
8
9
10
11

fn main() {
let s = String::from("hello,world!");
say_hello(&s);
say_hello(&s[..]);
say_hello(s.as_str());

fn say_hello(s: &str) {
println!("Hello, {}!", s);
}
}

字符串索引

在其他语言中用索引的方式访问字符串的某个字符或者子串是很正常的,但是在rust中这就会报错。

1
2
3

let s1 = String::from("hello");
let h = s1[0]; // error

字符串内部

字符串底层的数据存储格式实际上是 [u8],一个字节数组。

例如Hola 的长度是4个字节,因为Hola的每个字母在 UTF-8 编码中占用一个字节。

而中国人的长度是9个字节,因为每个汉字在 UTF-8 编码中占用三个字节。

这种情况下对hello进行索引,访问 &hello[0] 没有任何意义。

还有一个原因导致了 Rust 不允许去索引字符串:因为索引操作,我们总是期望它的性能表现是 O(1),然而对于 String 类型来说,无法保证这一点,因为 Rust 可能需要从 0 开始去遍历字符串来定位合法的字符。

字符串切片

前文提到过,字符串切片很危险,因为切片的索引是按照字节来的,但是字符串优势UTF-8来的,无法保证字节刚好落在字符的边界上

1
2
3
4

let hello = "中国人";

let s = &hello[0..2]; // error

操作字符串

由于String是可变字符串,下面介绍Rust字符串的修改\添加\山出

追加

push()方法追加char

push_str()方法追加字符串字面量.

两个方法都是在原有的字符串上追加内容,而不是创建一个新的字符串。

1
2
3
4
5
6
7
8
9
10

fn main() {
let mus s = String::from("hello");

s.push_str(",rust");
println!("{}", s); // hello,rust

s.push('!');
println!("{}", s); // hello,rust!
}

插入(Insert)

可以使用 insert() 方法插入单个字符char , 也可以使用insert_str() 方法插入字符串字面量,与push()方法不同,这俩方法要传入插入位置的索引。二要传入插入的字符串。

由于插入要修改原来的字符串,字符串必须是可变的

1
2
3
4
5
6
7
8
9
10
11
fn main(){
let mut s = String::from("Hello rust!");

s.insert(5, ',');
println!("插入字符 insert() -> {}",s);

s.insert_str(6, " I like");

println!("插入字符串 insert_str() -> {}",s);
}

结果

1
2
插入字符 insert() -> Hello, rust!
插入字符串 insert_str() -> Hello, I like rust!

替换

  1. replace

适用于 String 类型和 &str类型。 replace方法接受两个参数,第一个是要被替换的字符串,第二个是替换成的字符串。该方法会返回一个新的字符串,而不会修改原来的字符串。

1
2
3
4
5
6
7
fn main(){
let string_replace = String::from("I like rust. Learning rust is my favorite.");

let new_string = string_replace.replace("rust", "Rust");

dbg!(new_string);
}
  1. replacen

接受三个参数,前两个一样,第三个表示替换的次数。该方法会返回一个新的字符串,而不会修改原来的字符串。

1
2
3
4
5
6
7

fn main(){
let string_replace = "I like rust. Learning rust is my favorite.";

let new_string = string_replace.replacen("rust", "Rust", 1);
dbg!(new_string);
}
  1. replace_range

仅适用于String类型。replace_range方法接受两个参数,第一个是要被替换的范围,第二个是替换成的字符串。该方法会修改原来的字符串,而不会返回一个新的字符串。

1
2
3
4
5
6
fn main(){
let mut string_replace = String::from("I like rust!");

string_replace.replace_range(7..11, "Rust");
dbg!(string_replace);
}

山出

  1. pop

删除并返回最后一个字符。 直接操作原来的字符串。但是存在返回值.如果字符串为空,则返回None

1
2
3
4
5
6
7
8
9
10
11

fn main() {
let mut string_pop = String::from("rust pop 中文!");

let p1 = string_pop.pop();
let p2 = string_pop.pop();
dbg!(p1);
dbg!(p2);
dbg!(string_pop);
}

1
2
3
4
5
6
7
p1 = Some(
'!',
)
p2 = Some(
'文',
)
string_pop = "rust pop 中"
  1. remove

删除并返回字符串中指定位置的字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14

fn main() {
let mut string_remove = String::from("测试remove方法");

println!("string_remove 占 {} 字节",std::mem::size_of_val(string_remove.as_str()));


//删除第一个汉字
string_remove.remove(0);
//下面代码会报错
//string_remove.remove(1);

dbg!(string_remove);
}
  1. truncate

删除字符串中指定位置开始到结尾的全部字符

直接操作原始字符串,无返回值

1
2
3
4
5
6
7

fn main(){
let mut string_truncate = String::from("测试truncate方法");

string_truncate.truncate(6);
dbg!(string_truncate);
}
1
string_truncate = "测试t"
  1. clear

直接清空字符串,原始字符串变成空字符串

1
2
3
4
5
6
fn main(){
let mut string_clear = String::from("测试clear方法");

string_clear.clear();
dbg!(string_clear);
}

concatenate

  1. + , +=

使用 + 或者 += 连接字符串,要求右边的参数必须为字符串的切片引用(Slice)类型。

调用+相当于调用了 std::string里面的标准库 add()方法。这里add()方法的第二个参数是引用类型。

  • 返回一个新的字符串,原来的字符串不变。 += 直接修改原来的字符串。
1
2
3
4
5
6
7
8
9
10
11
12
fn main(){
let string_append = String::from("Hello");
let string_append2 = String::from(" Rust");

let result = string_append + &string_append2; // 注意这里的 & 符号
let mut resutl = resutl + "!";

result += "!!!";

println!("result = {}", result);
}

1
result = Hello Rust!!!!
1
2
3
4
5
6
7
8
9
fn main() {
let s1 = String::from("hello,");
let s2 = String::from("world!");
// 在下句中,s1的所有权被转移走了,因此后面不能再使用s1
let s3 = s1 + &s2;
assert_eq!(s3,"hello,world!");
// 下面的语句如果去掉注释,就会报错
// println!("{}",s1);
}
1
2
3
4
5
6
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

// String = String + &str + &str + &str + &str
let s = s1 + "-" + &s2 + "-" + &s3;

String + &str返回一个 String,然后再继续跟一个 &str 进行 + 操作,返回一个 String 类型,不断循环,最终生成一个 s,也是 String 类型。

s1调用add()方法之后,所有权被转移给了add(),而add()调用之后就被释放了,s1也没了。

  1. format!宏
1
2
3
4
5
6
7
fn main() {
let s1 = "hello";
let s2 = String::from("rust");
let s = format!("{} {}!", s1, s2);
println!("{}", s);
}

字符串转义

可以通过转义 \ 的方式输出 ASC II or Unicode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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);
}

当然,在某些情况下,可能你会希望保持字符串的原样,不要转义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

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);

// 如果字符串中包含 # 号,可以在开头和结尾加多个 # 号,最多加255个,只需保证与字符串中连续 # 号的个数不超过开头和结尾的 # 号的个数即可
let longer_delimiter = r###"A string with "# in it. And even "##!"###;
println!("{}", longer_delimiter);
}

操作UTF8

字符

如果想用Unicode方式遍历,最好使用 chars方法

1
2
3
for c in "中国人".chars() {
println!("{}", c);
}

输出

1
2
3
中
国
人

字节

返回底层字节数组表现形式。

1
2
3
4

for b in "中国人".bytes() {
println!("{}", b);
}

output:

1
2
3
4
5
6
7
8
9
228
184
173
229
155
189
228
186
186

获取子串

想要准确的从 UTF-8 字符串中获取子串是较为复杂的事情,例如想要从 holla中国人नमस्ते 这种变长的字符串中取出某一个子串,使用标准库你是做不到的。 你需要在 crates.io 上搜索 utf8 来寻找想要的功能。

深度剖析

为啥 String 可变,而字符串字面值 str 却不可以?

就字符串字面值来说,我们在编译时就知道其内容,最终字面值文本被直接硬编码进可执行文件中,这使得字符串字面值快速且高效,这主要得益于字符串字面值的不可变性。不幸的是,我们不能为了获得这种性能,而把每一个在编译时大小未知的文本都放进内存,因为有的字符串是在程序运行的过程中动态生成的。

对于string类型,为了支持可变长可增长的文本,需要在堆上面分配一块在编译的时候未知大小的内存。

  • 首先向操作系统request memory for string object
  • 用完了之后release还给它

其中第一部分由 String::from 完成,它创建了一个全新的 String。

有垃圾回收 GC 的语言中,GC 来负责标记并清除这些不再使用的内存对象,这个过程都是自动完成,无需开发者关心。

而没有gc的话,就要自己手动释放。

rust的话,就是通过变量在离开作用域时自动调用 drop 方法来释放内存。

与其它系统编程语言的 free 函数相同,Rust 也提供了一个释放内存的函数: drop,但是不同的是,其它语言要手动调用 free 来释放每一个变量占用的内存,而 Rust 则在变量离开作用域时,自动调用 drop 函数