Rust-base-learning-2

Rust基础学习–(二)

四、所有权与借用

1、所有权原则

  1. Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
  2. 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
  3. 当所有者(变量)离开作用域(rust的作用域与c相同)范围时,这个值将被丢弃(drop)

2、引用(借用)与解引用

& 符号即是引用,它们允许你使用值,但是不获取所有权,因此,不可通过解引用来修改变量值。

1
2
3
4
5
6
7
fn main() {
let x = 5;
let y = &x;

assert_eq!(5, x);
assert_eq!(5, *y);//与上一行相同,必须使用解引用符
}

这就是不可变引用,我们可以通过加上mut关键字的方式改为可变引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
let s = String::from("hello");

change(&s);
}

fn change(some_string: &String) {
some_string.push_str(", world");
}//报错,这是不可变引用
fn main() {
let mut s = String::from("hello");

change(&mut s);
}

fn change(some_string: &mut String) {
some_string.push_str(", world");
}//可以运行,这是可变引用

不过可变引用并不是随心所欲、想用就用的,它有一个很大的限制: 同一作用域,特定数据只能有一个可变引用

另外,可变引用与不可变引用不能同时存在

Non-Lexical Lifetimes(NLL):新版rust的一个特性,用于找到某个引用在作用域(})结束前就不再被使用的代码位置,见下例。

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

let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// 新编译器中,r1,r2作用域在这里结束

let r3 = &mut s;
println!("{}", r3);
} // 老编译器中,r1、r2、r3作用域在这里结束
// 新编译器中,r3作用域在这里结束

【垂悬引用导致的报错:

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let reference_to_nothing = dangle();
}

fn dangle() -> &String { // dangle 返回一个字符串的引用

let s = String::from("hello"); // s 是一个新字符串

&s // 返回字符串 s 的引用,换为s即可避免报错,最终String的所有权被转移给外面的调用者。
} // 这里 s 离开作用域并被丢弃。其内存被释放。
// 危险!

总的来说,借用规则如下:

  • 同一时刻,你只能拥有要么一个可变引用,要么任意多个不可变引用
  • 引用必须总是有效的

五、复合类型

1、字符串与切片

切片:

它允许你引用集合中部分连续的元素序列,而不是引用整个集合。

对于字符串而言,切片就是对 String 类型中某一部分的引用,是使用方括号包括的一个序列:[开始索引..终止索引],它看起来像这样:

1
2
3
4
5
let s = String::from("hello world");
let hello = &s[0..5];
//let hello = &s[..5];与上面那行等效
let world = &s[6..11];
//let world = &s[6..len];与上面那行等效

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

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

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

字符串:(String)

<1>简介:Rust 中的字符是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是在字符串中不一样,字符串是 UTF-8 编码,也就是字符串中的字符所占的字节数是变化的(1 - 4),这样有助于大幅降低字符串所占用的内存空间。

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

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

除了 String 类型的字符串,Rust 的标准库还提供了其他类型的字符串,例如 OsStringOsStrCsStringCsStr 等,注意到这些名字都以 String 或者 Str 结尾,分别对应的是具有所有权和被借用的变量。

<2>String与&str的转化:

1
2
3
4
5
6
7
8
9
10
11
12
String::from("hello,world")
"hello,world".to_string()//&str转String的两种方式
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!("{}",s);
}//String转&str,解引用即可

<3>rust不允许通过索引访问字符串某一个元素,因为Rust 提供了不同的字符串展现方式,不同语言符号占用的空间不同。但是允许字符串切片,但仅在切片恰好有意义的时候允许,否则会崩溃。

<4>字符串操作:修改,添加,删除

追加:在字符串尾部可以使用 push() 方法追加字符 char,也可以使用 push_str() 方法追加字符串字面量。这两个方法都是在原有的字符串上追加,并不会返回新的字符串。由于字符串追加操作要修改原来的字符串,则该字符串必须是可变的,即字符串变量必须由 mut 关键字修饰。eg:

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

s.push_str("rust");
println!("追加字符串 push_str() -> {}", s);

s.push('!');
println!("追加字符 push() -> {}", s);
}

插入:可以使用 insert() 方法插入单个字符 char,也可以使用 insert_str() 方法插入字符串字面量,与 push() 方法不同,这俩方法需要传入两个参数,第一个参数是字符(串)插入位置的索引,第二个参数是要插入的字符(串),索引从 0 开始计数,如果越界则会发生错误。由于字符串插入操作要修改原来的字符串,则该字符串必须是可变的,即字符串变量必须由 mut 关键字修饰

1
2
3
4
5
6
7
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)replace

该方法可适用于 String&str 类型。replace() 方法接收两个参数,第一个参数是要被替换的字符串,第二个参数是新的字符串。该方法会替换所有匹配到的字符串。该方法是返回一个新的字符串,而不是操作原来的字符串

1
2
3
4
5
fn main() {
let string_replace = String::from("I like rust. Learning rust is my favorite!");
let new_string_replace = string_replace.replace("rust", "RUST");
dbg!(new_string_replace);
}//运行结果:new_string_replace = "I like RUST. Learning RUST is my favorite!"

2)replacen

该方法可适用于 String&str 类型。replacen() 方法接收三个参数,前两个参数与 replace() 方法一样,第三个参数则表示替换的个数。该方法是返回一个新的字符串,而不是操作原来的字符串

1
2
3
4
5
fn main() {
let string_replace = "I like rust. Learning rust is my favorite!";
let new_string_replacen = string_replace.replacen("rust", "RUST", 1);
dbg!(new_string_replacen);
}//运行结果:new_string_replacen = "I like RUST. Learning rust is my favorite!"

3)replace_range

该方法仅适用于 String 类型。replace_range 接收两个参数,第一个参数是要替换字符串的范围(Range),第二个参数是新的字符串。该方法是直接操作原来的字符串,不会返回新的字符串。该方法需要使用 mut 关键字修饰

1
2
3
4
5
fn main() {
let mut string_replace_range = String::from("I like rust!");
string_replace_range.replace_range(7..8, "R");
dbg!(string_replace_range);
}//运行结果:new_string_replacen = "I like Rust!"

删除:共四个方法

1)pop —— 删除并返回字符串的最后一个字符

该方法是直接操作原来的字符串。但是存在返回值,其返回值是一个 Option 类型,如果字符串为空,则返回 None

1
2
3
4
5
6
7
8
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);
}

2)remove —— 删除并返回字符串中指定位置的字符

该方法是直接操作原来的字符串。但是存在返回值,其返回值是删除位置的字符串,只接收一个参数,表示该字符起始索引位置。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);
// 直接删除第二个汉字
// string_remove.remove(3);
dbg!(string_remove);
}

3)truncate —— 删除字符串中从指定位置开始到结尾的全部字符

该方法是直接操作原来的字符串。无返回值。该方法 truncate() 方法是按照字节来处理字符串的,如果参数所给的位置不是合法的字符边界,则会发生错误。

1
2
3
4
5
fn main() {
let mut string_truncate = String::from("测试truncate");
string_truncate.truncate(3);
dbg!(string_truncate);
}

4)clear —— 清空字符串

该方法是直接操作原来的字符串。调用后,删除字符串中的所有字符,相当于 truncate() 方法参数为 0 的时候。

1
2
3
4
5
fn main() {
let mut string_clear = String::from("string clear");
string_clear.clear();
dbg!(string_clear);
}

连接:共三个方法

1)使用 + 或者 += 连接字符串

使用 + 或者 += 连接字符串,要求右边的参数必须为字符串的切片引用(Slice)类型。其实当调用 + 的操作符时,相当于调用了 std::string 标准库中的 add() 方法,这里 add() 方法的第二个参数是一个引用的类型。因此我们在使用 + 时, 必须传递切片引用类型。不能直接传递 String 类型。+ 是返回一个新的字符串,所以变量声明可以不需要 mut 关键字修饰

1
2
3
4
5
6
7
8
9
10
fn main() {
let string_append = String::from("hello ");
let string_rust = String::from("rust");
// &string_rust会自动解引用为&str
let result = string_append + &string_rust;
let mut result = result + "!"; // `result + "!"` 中的 `result` 是不可变的
result += "!!!";

println!("连接字符串 + -> {}", result);
}

2)add()

以下是add方法的定义:

1
fn add(self, s: &str) -> String
1
2
3
4
5
6
7
8
9
10
//使用
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);
}

3)使用 format! 连接字符串

format! 这种方式适用于 String&strformat! 的用法与 print! 的用法类似。

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

<5>字符串转义:

我们可以通过转义的方式 \ 输出 ASCII 和 Unicode 字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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);

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

<6>操作UTF-8字符

char()

1
2
3
4
5
6
7
for c in "中国人".chars() {
println!("{}", c);
}
//输出;
//中
//国
//人

bytes()

1
2
3
4
5
6
7
8
9
10
11
12
13
for b in "中国人".bytes() {
println!("{}", b);
}
//输出:
//228
//184
//173
//229
//155
//189
//228
//186
//186

2、元组

元组是由多种类型组合到一起形成的,因此它是复合类型,元组的长度是固定的,元组中元素的顺序也是固定的。

创建:

1
2
3
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}

可以使用模式匹配或者 . 操作符来获取元组中的值。

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {}", y);
}//模式匹配
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
}//用.访问

3、结构体

<1>和c语言语法基本相同

1
2
3
4
5
6
7
8
9
10
11
12
13
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
let mut user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com");//只有使用mut关键字才能修改

(简化:构建结构体的时候,当函数参数和结构体字段同名时,可以直接使用缩略的方式进行初始化。

eg:

1
2
3
4
5
6
7
8
9
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}

更新结构体时,与原结构体相同的部分可以的通过结构体更新语法 ..user1(原结构体名) 即可完成。

1
2
3
4
5
let user2 = User {
email: String::from("another@example.com"),
..user1
};

<2>结构体的内存排列与访问

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let f1 = File {
name: String::from("f1.txt"),
data: Vec::new(),
};
let name = f1.name; // 转移name字段的所有权
println!("{}", name); // 可以正常访问name
// println!("{:?}", f1); // 错误!f1的部分所有权已被转移
// println!("{}", f1.name); // 错误!name字段的所有权已被转移
// 但仍可访问未被转移所有权的字段
println!("data length: {}", f1.data.len()); // 正确!data的所有权未被转移
}

<3>元组结构体

结构体必须要有名称,但是结构体的字段可以没有名称,这种结构体长得很像元组,因此被称为元组结构体,这种方式在希望有一个整体名称,但是又不关心里面字段的名称时将非常有用。

1
2
3
4
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

<4>单元结构体

如果你定义一个类型,但是不关心该类型的内容,只关心它的行为时,就可以使用 单元结构体,即没有内含变量的结构体。

1
2
3
4
5
struct AlwaysEqual;
let subject = AlwaysEqual;
// 我们不关心 AlwaysEqual 的字段数据,只关心它的行为,因此将它声明为单元结构体,然后再为它实现某个特征
impl SomeTrait for AlwaysEqual {
}

<5>结构体数据所有权

结构体要么使用具有自身所有权的变量,要么在从其他对象借用数据时加上生命周期符(见后续生命周期部分)

<6>使用#[derive(Debug)]来打印结构体的信息

结构体没有实现Display特征,因此不可以直接使用{}打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#[derive(Debug)]//手动实现Debug特征,而不是Display特征
struct Rectangle {
width: u32,
height: u32,
}

fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};

println!("rect1 is {:?}", rect1);
}//输出:
//$ cargo run
//rect1 is Rectangle { width: 30, height: 50 }
//也可以用{:#?}{:?},输出结果是:
//rect1 is Rectangle {
// width: 30,
// height: 50,
//}

除了使用println!()外,还可以使用dbg!宏进行输出。

dbg! 输出到标准错误输出 stderr,而 println! 输出到标准输出 stdout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};

dbg!(&rect1);
}
//输出:
//$ cargo run
//[src/main.rs:10] 30 * scale = 60
//[src/main.rs:14] &rect1 = Rectangle {
// width: 60,
// height: 50,
//}

4、枚举

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum PokerSuit {
Clubs,
Spades,
Diamonds,
Hearts,
}
fn main() {
let heart = PokerSuit::Hearts;//创建两个实例
let diamond = PokerSuit::Diamonds;
print_suit(heart);
print_suit(diamond);
}

fn print_suit(card: PokerSuit) {
// 需要在定义 enum PokerSuit 的上面添加上 #[derive(Debug)],否则会报 card 没有实现 Debug
println!("{:?}",card);
}

枚举成员可以包含各种数据,如:

1
2
3
4
5
6
7
8
9
10
11
12
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}

fn main() {
let m1 = Message::Quit;
let m2 = Message::Move{x:1,y:1};
let m3 = Message::ChangeColor(255,255,0);
}
  • Quit 没有任何关联数据
  • Move 包含一个匿名结构体
  • Write 包含一个 String 字符串
  • ChangeColor 包含三个 i32

【用Option枚举处理空值:

(代替其他语言中的NULL)

定义:

1
2
3
4
enum Option<T> {
Some(T),
None,
}

其中 T 是泛型参数,Some(T)表示该枚举成员的数据类型是 T,换句话说,Some 可以包含任何类型的数据。

Option<T> 枚举是如此有用以至于它被包含在了prelude(prelude 属于 Rust 标准库,Rust 会将最常用的类型、函数等提前引入其中,省得我们再手动引入)之中,你不需要将其显式引入作用域。另外,它的成员 SomeNone 也是如此,无需使用 Option:: 前缀就可直接使用 SomeNone

option的匹配:

1
2
3
4
5
6
7
8
9
10
11
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}

let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);

plus_one 接受一个 Option<i32> 类型的参数,同时返回一个 Option<i32> 类型的值(这种形式的函数在标准库内随处所见),在该函数的内部处理中,如果传入的是一个 None ,则返回一个 None 且不做任何处理;如果传入的是一个 Some(i32),则通过模式绑定,把其中的值绑定到变量 i 上,然后返回 i+1 的值,同时用 Some 进行包裹。

5、数组

在日常开发中,使用最广的数据结构之一就是数组,在 Rust 中,最常用的数组有两种,第一种是速度很快但是长度固定的 array,第二种是可动态增长的但是有性能损耗的 Vector,在本书中,我们称 array 为数组,Vector 为动态数组。

<1>array:

三要素:

  • 长度固定
  • 元素必须有相同的类型
  • 依次线性排列

创建:

1
2
3
4
5
6
7
fn main() {
let a = [1, 2, 3, 4, 5];
let a: [i32; 5] = [1, 2, 3, 4, 5];//声明类型
let a = [3; 5];//声明重复出现的数据,5个元素,均为3,当数组元素为非基础类型时,不可以使用
let array: [String; 8] = std::array::from_fn(|_i| String::from("rust is good!"));//当数组元素为重复的非基础类型时,这么写
println!("{:#?}", array);
}

访问:

1
2
3
4
5
6
7
8
fn main() {
let a = [9, 8, 7, 6, 5];

let first = a[0]; // 获取a数组第一个元素
let second = a[1]; // 获取第二个元素
// let sixth = a[5];运行此行时程序会崩溃,由于数组访问越界
}

数组切片:

1
2
3
let a: [i32; 5] = [1, 2, 3, 4, 5];
let slice: &[i32] = &a[1..3];
assert_eq!(slice, &[2, 3]);
  • 切片的长度可以与数组不同,并不是固定的,而是取决于你使用时指定的起始和结束位置
  • 创建切片的代价非常小,因为切片只是针对底层数组的一个引用
  • 切片类型 [T] 拥有不固定的大小,而切片引用类型 &[T] 则具有固定的大小,因为 Rust 很多时候都需要固定大小数据类型,因此 &[T] 更有用,&str 字符串切片也同理

<2>vector:

动态数组(具体见集合类型,须先学习其他知识)

To Be Continue…


Rust-base-learning-2
http://example.com/2025/06/28/Rust-base-learning-2/
作者
oxygen
发布于
2025年6月28日
许可协议