【Rust】所有权

目录

  • 所有权
    • 基本概念
      • 所有权介绍
      • 栈与堆
      • 变量作用域
    • 字符串
      • 字符串字面值(&str)
      • String 类型
      • 相互转换
      • 所有权 + 内存结构对比
      • 注意事项和常见坑
      • 使用场景
    • 内存与分配
      • 变量与数据交互的方式(一):移动
      • 变量与数据交互的方式(二):克隆
    • 所有权与函数
    • 引用
      • 基本使用
      • 可变引用
      • 悬垂引用
      • Slice 类型

所有权

所有权(系统)是 Rust 最为与众不同的特性,对语言的其他部分有着深刻含义。它让 Rust 无需垃圾回收(garbage collector)即可保障内存安全,因此理解 Rust 中所有权如何工作是十分重要的。

基本概念

所有权介绍

所有权ownership)是 Rust 用于如何管理内存的一组规则。所有程序都必须管理其运行时使用计算机内存的方式。一些语言中具有垃圾回收机制,在程序运行时有规律地寻找不再使用的内存;在另一些语言中,程序员必须亲自分配和释放内存。Rust 则选择了第三种方式:通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。如果违反了任何这些规则,程序都不能编译。在运行时,所有权系统的任何功能都不会减慢程序。

所有权规则

  1. Rust 中的每一个值都有一个 所有者(owner)。
  2. 值在任一时刻有且只有一个所有者。
  3. 当所有者(变量)离开作用域,这个值将被丢弃。

栈与堆

在 Rust 中,**栈(Stack)堆(Heap)**是内存管理的重要概念,理解它们对于掌握 Rust 的所有权(Ownership)、借用(Borrowing)和生命周期(Lifetime)机制非常关键。

栈(Stack):

  • 特点:

    • 后进先出(LIFO):像叠盘子,最后放上去的最先被取出。
    • 分配速度快:因为只需要移动一个指针。
    • 存储固定大小的数据:如基本数据类型(i32, bool)、元组((i32, bool))等。
  • 在 Rust 中的表现:

    fn main() {let x = 5;     // x 被分配在栈上let y = true;  // y 也在栈上
    }
    

​ 这里的 x 和 y 都是已知大小的基本类型,会直接分配在栈上。

堆(Heap):

  • 特点:

    • 动态分配内存:用于大小在编译时不确定的值(比如 Vec、String)。
    • 分配慢于栈:需要操作操作系统请求内存。
    • 需要手动释放(Rust 使用所有权机制自动释放,无需开发者手动调用 free)。
  • 在 Rust 中的表现:

    fn main() {let s = String::from("hello"); // String 的数据部分存在堆上
    }
    

    这里:s 是一个变量,保存在栈上,里面包含一个指向堆中实际字符串内容的指针。字符串 “hello” 的内容是动态分配的,存在堆上。

栈与堆的关系图解:

在这里插入图片描述

总结对比:

特性栈(Stack)堆(Heap)
分配速度非常快较慢
管理方式编译器自动Rust 自动通过所有权管理
数据大小编译期已知编译期未知,运行时决定
访问速度
示例类型i32, bool, charString, Vec<T>, Box<T>

变量作用域

Rust 中的变量作用域是理解程序生命周期、内存管理和所有权系统的一个重要概念。它决定了变量的有效范围、生命周期以及如何管理内存。

作用域(Scope)是指变量、函数、结构体等在程序中有效的范围。Rust 使用静态分析来确保变量在其作用域结束时被销毁,并且会在作用域结束时自动回收内存(这是 Rust 所独有的所有权机制的一部分)。

变量作用域的基础:

  1. 作用域定义:
    变量通常在代码块(block)内定义,代码块是由 {} 包围的区域。只要变量处于该块内,它就是有效的(也就是“可见”的)。
  2. 生命周期:
    Rust 使用 所有权(ownership) 机制来确保每个变量只有一个有效的“所有者”,并且当该变量超出作用域时会被自动销毁。
fn main() {let x = 5;  // 变量 x 在这里被声明并初始化println!("x is {}", x);  // x 在作用域内有效
}  // 这里 x 超出作用域,内存被释放

作用域规则:

  • 变量 x 的作用域从它被创建的地方(let x = 5)开始,一直到所在的代码块结束(在这里是 main 函数的结束)。
  • 当作用域结束时,变量 x 会被自动销毁,并释放相关内存。

嵌套作用域:

Rust 允许作用域嵌套,外层作用域的变量可以在内层作用域中使用,但内层作用域结束时,外层作用域的变量不会受到影响。

fn main() {let x = 5;{let y = 10;  // y 的作用域在这个内部作用域内println!("x = {}, y = {}", x, y); // x 和 y 都有效}  // y 超出作用域,被销毁println!("x = {}", x); // x 仍然有效,因为它在外层作用域,而 y 在内层作用域结束后被销毁
}

输出结果如下:

x = 5, y = 10
x = 5

字符串

字符串字面值(&str)

示例:

let s: &str = "hello";// let s = "hello";

特点:

  • &str 是字符串切片类型,表示某段 UTF-8 编码的字符串引用。
  • 字面值 "hello"静态存在于程序的只读数据段 中。
  • "hello" 的类型实际上是 &'static str,它有 'static 生命周期,即整个程序运行期间都有效。

内存结构:

&str = {指针:指向字符串首地址,长度:5(字节数)
}

特性:

特性说明
长度固定不能动态添加字符
不可变不支持修改内容
存储位置静态分配的只读内存或堆中其他 String 的切片
无所有权不能拥有资源,只是“借用”
生命周期相关经常和 &'static str 或函数参数一起使用

String 类型

示例:

let mut s: String = String::from("hello");
// let s = String::from("hello");
s.push_str(" world");

特点:

  • String 是拥有所有权的、可变的、堆分配的 UTF-8 字符串。
  • 支持动态增长和修改,常用于处理来自用户输入、文件等动态数据。

内部结构:

String = {指针:指向堆上数据,长度:已用字节数,容量:分配的总字节数
}

特性总结:

特性说明
可变可以添加、替换、删除内容
拥有所有权当变量离开作用域自动释放内存
堆分配内容存储在堆中
灵活适合处理动态、拼接的字符串

相互转换

&str → String(借用到拥有):

let s1 = "hello"; // &str
let s2 = s1.to_string(); // String
let s3 = String::from(s1); // 等价写法

String → &str(拥有到借用):

let s1 = String::from("hello");
let s2: &str = &s1; // 自动解引用为 &str

所有权 + 内存结构对比

维度&strString
是否拥有数据否(只借用)是(拥有)
是否可变
存储位置只读段或堆(借用)
生命周期有生命周期限制生命周期随变量作用域自动管理
内部结构指针 + 长度(胖指针)指针 + 长度 + 容量(结构体)

let s = "hello"; 这里 s 中的并不是"hello",而是指针指向 "hello" 的地址值和值的长度,也就是说 s 内部是一个胖指针而不是值的本身,String 也是同理,变量内部是一个结构体而不是值的本身。

注意事项和常见坑

字节边界问题:

let s = "你好";  // UTF-8 每个汉字占 3 字节
// println!("{}", &s[0..1]); // panic:不是合法 UTF-8 边界
println!("{}", &s[0..3]); // 输出:你

String 的按索引访问:

let s = String::from("hello");
// println!("{}", s[1]); // 编译错误,不能用索引访问 String
let ch = s.chars().nth(1); // 正确方式

使用场景

场景推荐类型
只读字面值,固定内容&str
动态构建、修改字符串String
需要拥有字符串(函数返回值)String
接收字符串参数&str (更通用)
拼接多个字符串String

&str 是借用的只读 UTF-8 字符串切片,适合轻量访问;String 是拥有堆内存的可变字符串,适合需要修改或管理生命周期的场景。

内存与分配

就字符串字面值来说,在编译时就知道其内容,所以文本被直接硬编码进最终的可执行文件中。这使得字符串字面值快速且高效。不过这些特性都只得益于字符串字面值的不可变性。不幸的是,不能为了每一个在编译时大小未知的文本而将一块内存放入二进制文件中,并且它的大小还可能随着程序运行而改变。

对于 String 类型,为了支持一个可变,可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容。这意味着:

  • 必须在运行时向内存分配器(memory allocator)请求内存。
  • 需要一个当我们处理完 String 时将内存返回给分配器的方法。

第一部分由开发者完成:当调用 String::from 时,它的实现请求其所需的内存。

第二部分,Rust 采取了一个策略:内存在拥有它的变量离开作用域后就被自动释放。当变量离开作用域,Rust 为我们调用一个特殊的函数,这个函数叫做 drop,Rust 在结尾的 } 处自动调用 drop

变量与数据交互的方式(一):移动

在 Rust 中,多个变量可以采取不同的方式与同一数据进行交互。

let x = 5;
let y = x;

将 5 赋值给 x,然后生成 x 的副本并将其拷贝赋值给 y。现在 xy,都等于 5,因为整数是有已知固定大小的简单值,所以这两个 5 被放入了栈中。

类似的过程,放在 String 类型上结果完全不一样。

let s1 = String::from("hello");
let s2 = s1;

前面提到过 String 类型的变量内部是一个结构体而不是值的本身。

在这里插入图片描述

s1 赋值给 s2String 的数据被复制了,这意味着从栈上拷贝了它的指针、长度和容量。我们并没有复制指针指向的堆上数据。

在这里插入图片描述

之前提到过当变量离开作用域后,Rust 自动调用 drop 函数并清理变量的堆内存。不过图中展示了两个数据指针指向了同一位置。这就有了一个问题:当 s2s1 离开作用域,它们都会尝试释放相同的内存。这是一个叫做二次释放的错误,也是之前提到过的内存安全性 bug 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。

为了确保内存安全,在 let s2 = s1; 之后,Rust 认为 s1 不再有效,因此 Rust 不需要在 s1离开作用域后清理任何东西。

将拷贝指针、长度和容量而不拷贝数据并使第一个变量无效的操作在 Rust 中称为移动,有一点类似于其他编程语言中的浅拷贝。这样就解决了二次释放的错误,因为只有 s2 是有效的,当其离开作用域,它就释放自己的内存。

Rust 永远也不会自动创建数据的 “深拷贝”。因此,任何 自动的复制都可以被认为是对运行时性能影响较小的。

变量与数据交互的方式(二):克隆

如果确实需要深度复制 String 中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 clone 的通用函数。

let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {s1}, s2 = {s2}");

这就相当于把堆上的值也复制了一份给 s2

在这里插入图片描述

输出结果如下:

s1 = hello, s2 = hello

回到一开始讲解移动中的代码

let x = 5;
let y = x;

这段代码明明没有用 clone 是如何实现的数据拷贝?

Rust 有一个叫做 Copy trait 的特殊注解,可以用在类似整型这样的存储在栈上的类型上。如果一个类型实现了 Copy trait,那么一个旧的变量在将其赋值给其他变量后仍然可用。

Rust 中的 Copy 是一种轻量级、无资源所有权的按位拷贝,它只在栈上发生,不会去复制堆内存或执行任何逻辑代码(比如 Drop)。

Rust 不允许自身或其任何部分实现了 Drop trait 的类型使用 Copy trait。如下是一些 Copy 的类型:

  • 所有整数类型,比如 u32
  • 布尔类型,bool,它的值是 truefalse
  • 所有浮点数类型,比如 f64
  • 字符类型,char
  • 元组,当且仅当其包含的类型也都实现 Copy 的时候。比如,(i32, i32) 实现了 Copy,但 (i32, String) 就没有。

所有权与函数

将值传递给函数与给变量赋值的原理相似。向函数传递值可能会移动或者复制,就像赋值语句一样。

fn main() {let s = String::from("hello");  // s 进入作用域takes_ownership(s);             // s 的值移动到函数里 ...// ... 所以到这里不再有效println!("{}", s);let x = 5;                      // x 进入作用域makes_copy(x);                  // x 应该移动函数里,// 但 i32 是 Copy 的,// 所以在后面可继续使用 x} // 这里,x 先移出了作用域,然后是 s。但因为 s 的值已被移走,// 没有特殊之处fn takes_ownership(some_string: String) { // some_string 进入作用域println!("{some_string}");
} // 这里,some_string 移出作用域并调用 `drop` 方法。// 占用的内存被释放fn makes_copy(some_integer: i32) { // some_integer 进入作用域println!("{some_integer}");
} // 这里,some_integer 移出作用域。没有特殊之处

该代码会产生以下错误:

error[E0382]: borrow of moved value: `s`--> src/main.rs:6:20|
2  |     let s = String::from("hello");  // s 进入作用域|         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
3  |
4  |     takes_ownership(s);             // s 的值移动到函数里 ...|                     - value moved here
5  |     // ... 所以到这里不再有效
6  |     println!("{}", s);|                    ^ value borrowed here after move|
note: consider changing this parameter type in function `takes_ownership` to borrow instead if owning the value isn't necessary--> src/main.rs:17:33|
17 | fn takes_ownership(some_string: String) { // some_string 进入作用域|    ---------------              ^^^^^^ this parameter takes ownership of the value|    ||    in this function= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable|
4  |     takes_ownership(s.clone());             // s 的值移动到函数里 ...|                      ++++++++

这是由于 s 传给 takes_ownership 之后,所有权转移到 takes_ownership 里,随着 takes_ownership 的结束一起被释放了,导致后面无法继续使用 s

这种情况可以通过函数返回值转移所有权来解决。

fn main() {let s1 = gives_ownership();         // gives_ownership 将返回值// 转移给 s1let s2 = String::from("hello");     // s2 进入作用域let s3 = takes_and_gives_back(s2);  // s2 被移动到// takes_and_gives_back 中,// 它也将返回值移给 s3
} // 这里,s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,// 所以什么也不会发生。s1 离开作用域并被丢弃fn gives_ownership() -> String {             // gives_ownership 会将// 返回值移动给// 调用它的函数let some_string = String::from("yours"); // some_string 进入作用域。some_string                              // 返回 some_string // 并移出给调用的函数// 
}// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域// a_string  // 返回 a_string 并移出给调用的函数
}

变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 drop 被清理掉,除非数据被移动为另一个变量所有。

虽然这样是可以的,但是在每一个函数中都获取所有权并接着返回所有权有些繁琐。幸运的是,Rust 对此提供了一个不用获取所有权就可以使用值的功能,叫做引用

引用

基本使用

为了能在调用函数后仍能使用变量,就可以使用引用。引用像一个指针,因为它是一个地址,可以由此访问储存于该地址的属于其他变量的数据。 与指针不同,引用确保指向某个特定类型的有效值。使用符号 & 进行引用。

fn main() {let s1 = String::from("hello");let len = calculate_length(&s1);println!("The length of '{s1}' is {len}.");
}fn calculate_length(s: &String) -> usize {s.len()
}

引用的原理图如下:

在这里插入图片描述

&s1 语法创建一个 指向s1 的引用,但是并不拥有它。因为并不拥有这个值,所以当引用停止使用时,它所指向的值也不会被丢弃。

同理,函数签名使用 & 来表明参数 s 的类型是一个引用。

变量 s 有效的作用域与函数参数的作用域一样,不过当 s 停止使用时并不丢弃引用指向的数据,因为 s 并没有所有权。当函数使用引用而不是实际值作为参数,无需返回值来交还所有权,因为就不曾拥有所有权。

输出结果如下:

The length of 'hello' is 5.

小提一下:与使用 & 引用相反的操作是 解引用,它使用解引用运算符,*

将创建一个引用的行为称为借用。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完后,必须还回去。因为你并不拥有它的所有权。

如果尝试修改借用的变量是行不通的。

fn main() {let s = String::from("hello");change(&s);
}fn change(some_string: &String) {some_string.push_str(", world");
}

会产生一个错误:

error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference--> src/main.rs:8:5|
8 |     some_string.push_str(", world");|     ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable|
help: consider changing this to be a mutable reference|
7 | fn change(some_string: &mut String) {|                         +++

正如变量默认是不可变的,引用也一样。(默认)不允许修改引用的值。

可变引用

通过一个小调整就能修复上述代码中的错误,允许修改一个借用的值,这就是可变引用

fn main() {let mut s = String::from("hello");change(&mut s);
}fn change(some_string: &mut String) {some_string.push_str(", world");
}

必须将 s 改为 mut。然后在调用 change 函数的地方创建一个可变引用 &mut s,并更新函数签名以接受一个可变引用 some_string: &mut String。这就非常清楚地表明,change 函数将改变它所借用的值。

可变引用有一个很大的限制:如果你有一个对该变量的可变引用,你就不能再创建对该变量的引用。这些尝试创建两个 s 的可变引用的代码会失败:

fn main() {let mut s = String::from("hello");let r1 = &mut s;let r2 = &mut s;println!("{}, {}", r1, r2);
}

错误如下:

error[E0499]: cannot borrow `s` as mutable more than once at a time--> src/main.rs:5:14|
4 |     let r1 = &mut s;|              ------ first mutable borrow occurs here
5 |     let r2 = &mut s;|              ^^^^^^ second mutable borrow occurs here
6 |
7 |     println!("{}, {}", r1, r2);|                        -- first borrow later used here

这个报错说这段代码是无效的,因为不能在同一时间多次将 s 作为可变变量借用。第一个可变的借入在 r1 中,并且必须持续到在 println! 中使用它,但是在那个可变引用的创建和它的使用之间,又尝试在 r2 中创建另一个可变引用,该引用借用与 r1 相同的数据。

这一限制以一种非常小心谨慎的方式允许可变性,防止同一时间对同一数据存在多个可变引用。这个限制的好处是 Rust 可以在编译时就避免数据竞争。数据竞争类似于竞态条件,它可由这三个行为造成:

  • 两个或更多指针同时访问同一数据。
  • 至少有一个指针被用来写入数据。
  • 没有同步数据访问的机制。

数据竞争会导致未定义行为,难以在运行时追踪,并且难以诊断和修复;Rust 避免了这种情况的发生,因为它甚至不会编译存在数据竞争的代码。

一如既往,可以使用大括号来创建一个新的作用域,以允许拥有多个可变引用,只是不能同时拥有:

fn main() {let mut s = String::from("hello");{let r1 = &mut s;} // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用let r2 = &mut s;
}

Rust 在同时使用可变与不可变引用时也采用的类似的规则。这些代码会导致一个错误:

fn main() {let mut s = String::from("hello");let r1 = &s; // 没问题let r2 = &s; // 没问题let r3 = &mut s; // 大问题println!("{}, {}, and {}", r1, r2, r3);
}

错误如下:

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable--> src/main.rs:6:14|
4 |     let r1 = &s; // 没问题|              -- immutable borrow occurs here
5 |     let r2 = &s; // 没问题
6 |     let r3 = &mut s; // 大问题|              ^^^^^^ mutable borrow occurs here
7 |
8 |     println!("{}, {}, and {}", r1, r2, r3);|                                -- immutable borrow later used here

不可变引用的借用者可不希望在借用时值会突然发生改变!然而,多个不可变引用是可以的,因为没有哪个只能读取数据的引用者能够影响其他引用者读取到的数据。

注意一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。例如,因为最后一次使用不可变引用(println!),发生在声明可变引用之前,所以如下代码是可以编译的:

fn main() {let mut s = String::from("hello");let r1 = &s; // 没问题let r2 = &s; // 没问题println!("{r1} and {r2}");// 此位置之后 r1 和 r2 不再使用let r3 = &mut s; // 没问题println!("{r3}");
}

不可变引用 r1r2 的作用域在 println! 最后一次使用之后结束,这也是创建可变引用 r3 的地方。因为它们的作用域没有重叠,所以代码是可以编译的。编译器可以在作用域结束之前判断不再使用的引用。

悬垂引用

在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个悬垂指针,所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。

这里尝试创建一个悬垂引用,Rust 会通过一个编译时错误来避免:

fn main() {let reference_to_nothing = dangle();
}fn dangle() -> &String {let s = String::from("hello");&s
}

错误如下:

error[E0106]: missing lifetime specifier--> src/main.rs:5:16|
5 | fn dangle() -> &String {|                ^ expected named lifetime parameter|= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`|
5 | fn dangle() -> &'static String {|                 +++++++
help: instead, you are more likely to want to return an owned value|
5 - fn dangle() -> &String {
5 + fn dangle() -> String {|

因为 s 是在 dangle 函数内创建的,当 dangle 的代码执行完毕后,s 将被释放。当尝试返回它的引用时,意味着这个引用会指向一个无效的 String,Rust 不会允许这么做。

这里的解决方法是直接返回 String

fn no_dangle() -> String {let s = String::from("hello");s
}
  • 在任意给定时间,要么只能有一个可变引用,要么只能有多个不可变引用。
  • 引用必须总是有效的。

Slice 类型

什么是 Slice 类型?

Slice 是对一块连续内存区域的借用,它本身不拥有数据,只是引用。比如:数组、字符串,都可以通过切片来引用一部分内容。

一句话理解:Slice = 指向连续元素的一段引用 + 长度信息。

let arr = [1, 2, 3, 4, 5];
let slice = &arr[1..4];  // 包括arr[1], arr[2], arr[3],不包括arr[4]

这里的 slice 是一个 &[i32] 类型的切片引用,指向数组的一部分。

为什么 Rust 要引入 Slice 类型(切片)?

  • 为了在不复制数据的情况下,安全、灵活地访问连续内存的一部分。
  • Slice 是一种只借用部分数据的机制,能零拷贝地、安全地处理数据片段。

Slice 类型有什么优势?

  1. 避免复制,提高性能
    • 如果没有 Slice,每次想操作一部分数据(比如一个字符串的一小段),就要复制一份,非常浪费内存和时间。
    • Slice 只借用原数据的一部分,不分配新内存,零拷贝。
  2. 安全访问内存
    • Slice 包含长度信息,Rust 在访问时能自动检查边界,避免访问越界、野指针等内存错误(C 语言就容易出问题)。
  3. 通用性和灵活性
    • Slice 设计得很通用,不管是 StringVec、数组 [T; N],都可以切成 Slice 来操作。
    • 比如:&[T] 是对数组或向量的借用,&str 是对字符串的借用。
  4. 支持共享只读和可变借用
    • &[T] 是不可变切片(只读访问);
    • &mut [T] 是可变切片(可以修改内容)。
  5. Rust 借用检查器可以追踪
    • Slice 是一个标准的借用,符合 Rust 的所有权和生命周期系统,可以被编译器静态检查,保证不会悬垂引用。

Slice 的基本使用:

fn main() {let arr = [10, 20, 30, 40, 50];let slice = &arr[1..4]; // 取arr[1], arr[2], arr[3]println!("slice: {:?}", slice);  // 输出: slice: [20, 30, 40]for x in slice {println!("{}", x);}
}

slice 只是引用,不占用额外空间。切片里保存了两个信息:指针 + 长度。

切片语法:

&array[start..end]   // 从start开始,到end前结束(左闭右开区间)

&array[..]:整个数组,&array[start..]:从start到末尾,&array[..end]:从开头到end前,&array[start..=end]:从start到end,包括end(注意用 ..=)。

let arr = [1, 2, 3, 4, 5];// 从2开始到4前
let a = &arr[1..3];   // [2, 3]// 从0到2
let b = &arr[..2];    // [1, 2]// 从2到最后
let c = &arr[2..];    // [3, 4, 5]// 包括索引2到4
let d = &arr[2..=4];  // [3, 4, 5]

&[T] 是不可变切片,有时需要可变的情况,跟前面将不可变改为可变一样,加上 mut 关键字,例如:

fn main() {let mut arr = [1, 2, 3, 4, 5];// 不可变let slice = &arr[1..4];println!("{:?}", slice);// 可变let slice_mut = &mut arr[1..4];slice_mut[0] = 99;println!("{:?}", arr);  // arr变成了 [1, 99, 3, 4, 5];
}

结果输出如下:

[2, 3, 4]
[1, 99, 3, 4, 5]

Slice 类型是特殊的引用,所以可变切片不能跟不可变引用同时存在,Rust 的借用检查器保证这一点。

字符串切片的用法跟数组切片的用法一样,字符串切片其实就是之前提到过的字符串字面值 &strString&str 是不同的:

  • String:堆上的可变字符串

  • &str:字符串切片,不可变的引用(指向 UTF-8 编码的一段字节序列)

fn main() {let s = String::from("hello world");let hello = &s[0..5];    // "hello"let world = &s[6..];     // "world"println!("{}, {}", hello, world);
}

可以对字符串进行切片,但要注意是按照字节切,不是按字符数
(如果切到中间的UTF-8字符,就会 panic)。来看下面例子:

fn main() {let s = String::from("你好"); // "你"和"好"在UTF-8下每个字占3个字节!println!("{:?}", s.as_bytes());// 输出: [228, 189, 160, 229, 165, 189]
}

如果写成 let slice = &s[0..1]; 这是错的,只有一个字节,UTF-8 破坏了,程序直接 panic

warning: unused variable: `slice`--> src/main.rs:4:9|
4 |     let slice = &s[0..1];|         ^^^^^ help: if this is intentional, prefix it with an underscore: `_slice`|= note: `#[warn(unused_variables)]` on by defaultwarning: `hello_cargo` (bin "hello_cargo") generated 1 warningFinished `dev` profile [unoptimized + debuginfo] target(s) in 0.00sRunning `target/debug/hello_cargo`thread 'main' panicked at src/main.rs:4:19:
byte index 1 is not a char boundary; it is inside '你' (bytes 0..3) of `你好`
stack backtrace:0: rust_begin_unwindat /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/std/src/panicking.rs:695:51: core::panicking::panic_fmtat /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/core/src/panicking.rs:75:142: core::str::slice_error_fail_rt3: core::str::slice_error_failat /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/core/src/str/mod.rs:68:54: core::str::traits::<impl core::slice::index::SliceIndex<str> for core::ops::range::Range<usize>>::indexat /Users/huangruibang/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/str/traits.rs:240:215: <alloc::string::String as core::ops::index::Index<I>>::indexat /Users/huangruibang/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/alloc/src/string.rs:2669:96: hello_cargo::mainat ./src/main.rs:4:197: core::ops::function::FnOnce::call_onceat /Users/huangruibang/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

正确切法应该是按照字符来切到完整的 3 个字节:

fn main() {let s = String::from("你好"); // "你"和"好"在UTF-8下每个字占3个字节!let slice1 = &s[0..3]; // 切到完整的3个字节,才是"你"let slice2 = &s[3..]; // 切到完整的3个字节,才是"好"println!("{}", slice1);println!("{}", slice2);
}

接下来讲讲 Slice 类型的底层原理,以便更好地理解:

编译器的角度看,切片是一个胖指针(fat pointer)

它包含两部分信息:

  • 一个指针(指向数据的起始地址)
  • 一个长度(告诉你有多少个元素)

可以理解成这样的结构(简化版伪代码):

struct Slice<T> {ptr: *const T, // 指向第一个元素的地址len: usize,    // 有多少个元素
}

所以:&[T] 是对 Slice<T>不可变引用&mut [T] 是对 Slice<T>可变引用

let arr = [10, 20, 30, 40, 50];
let slice = &arr[1..4];

内存大概是这样(数字是地址举例):

地址内容
0x100010
0x100420
0x100830
0x100C40
0x101050

然后:slice.ptr 指向 0x1004(也就是20所在位置),slice.len3(指向3个元素:20, 30, 40)。所以 slice 能正确访问 [20, 30, 40]

切片本身是独立的小结构体,占据一点栈空间,但它指向的数据是在原数组里。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/78896.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

4月29日日记

终于是考完解析几何了&#xff0c;今天昨天突击了一下&#xff0c;感觉确实学会了很多之前不会的东西&#xff0c;但是可能距离高分还差很多。这次考试不太理想。大部分原因是前期没学&#xff0c;吸取教训&#xff0c;早点开始复习微积分。明天还有一节微积分&#xff0c;但是…

【深度对比】Google Play与IOS 马甲包处理差异分析

在移动应用发布与推广过程中&#xff0c;马甲包&#xff08;Cloned App / Alternate Version&#xff09; 曾被广泛用于流量测试、风险隔离、多品牌运营等场景中。随着 Google Play 与 Apple App Store 审核政策不断收紧&#xff0c;开发者们越来越关注两个平台对“马甲包”的态…

MCP 架构全解析:Host、Client 与 Server 的协同机制

目录 &#x1f3d7;️ MCP 架构全解析&#xff1a;Host、Client 与 Server 的协同机制 &#x1f4cc; 引言 &#x1f9e9; 核心架构组件 1. Host&#xff08;主机&#xff09; 2. Client&#xff08;客户端&#xff09; 3. Server&#xff08;服务器&#xff09; &#…

记录一次无界微前端的简单使用

记录一次无界微前端使用 无界微前端主应用子应用nginx配置 无界微前端 https://wujie-micro.github.io/doc/ 因为使用的是vue项目主应用和次应用都是 所以用的封装的。 https://wujie-micro.github.io/doc/pack/ 主应用 安装 选择对应的版本 # vue2 框架 npm i wujie-vue2…

LLM应用于自动驾驶方向相关论文整理(大模型在自动驾驶方向的相关研究)

1、《HILM-D: Towards High-Resolution Understanding in Multimodal Large Language Models for Autonomous Driving》 2023年9月发表的大模型做自动驾驶的论文&#xff0c;来自香港科技大学和人华为诺亚实验室&#xff08;代码开源&#xff09;。 论文简介&#xff1a; 本文…

FTP-网络文件服务器

部署思路 单纯上传下载ftp系统集成间的共享 samba网络存储服务器 NFS 网络文件服务器&#xff1a;通过网络共享文件或文件夹&#xff0c;实现数据共享 NAS &#xff08; network append storage):共享的是文件夹 FTP&#xff1a;文件服务器samba&#xff1a;不同系统间的文件…

在 Ubuntu 22.04 x64 系统安装/卸载 1Panel 面板

一、 1Panel 是什么&#xff1f; 1Panel 是一款基于 Go 语言开发的现代化开源服务器管理面板&#xff08;类似宝塔面板&#xff09;&#xff0c;专注于容器化&#xff08;Docker&#xff09;和云原生环境管理&#xff0c;提供可视化界面简化服务器运维操作。 1. 1Panel主要功…

Redis | Redis集群模式技术原理介绍

关注&#xff1a;CodingTechWork Redis 集群模式概述 Redis 集群&#xff08;Cluster&#xff09;模式是 Redis 官方提供的分布式解决方案&#xff0c;旨在解决单机 Redis 在数据量和性能上的限制。它通过数据分片、高可用性和自动故障转移等特性&#xff0c;提供了水平扩展和…

Servlet小结

视频链接&#xff1a;黑马servlet视频全套视频教程&#xff0c;快速入门servlet原理servlet实战 什么是Servlet&#xff1f; 菜鸟教程&#xff1a;Java Servlet servlet&#xff1a; server applet Servlet是一个运行在Web服务器&#xff08;如Tomcat、Jetty&#xff09;或应用…

数据库进阶之MySQL 程序

1.目标 1> 了解mysqlId服务端程序 2> 掌握mysql客户端程序的使用 3> 了解工具包中的其他程序 2. MySQL程序简介 本章介绍 MySQL 命令⾏程序以及在运⾏这些程序时指定选项的⼀般语法(如:mysql -uroot -p)。 对常⽤程序进⾏详细的讲解(实用工具的使用方法)&#xf…

VS2022 设置 Qt Project Settings方法

本文解决的问题&#xff1a;创建完成后&#xff0c;如需要用到Sql或者Socket等技术&#xff0c;需要设置Qt Project Settings&#xff1b; 1、打开VS2022编译器&#xff0c;创建QT项目工程 2、创建完成后&#xff0c;点击 解决方案 →右键属性 3、选择 Qt Project Settings →…

React:封装一个评论回复组件

分析 用户想要一个能够显示评论列表&#xff0c;并且允许用户进行回复的组件。可能还需要支持多级回复&#xff0c;也就是对回复进行再回复。然后&#xff0c;我要考虑组件的结构和功能。 首先&#xff0c;数据结构方面&#xff0c;评论应该包含id、内容、作者、时间&#xf…

wx读书某sign算法详解

未加固 版本&#xff1a;9.2.3 前置知识&#xff1a; (v41 & 0xFFFFFFFFFFFFFFFELL) 是一种高效的奇偶检查方法&#xff0c;用于判断数值 v41 是否为奇数。 std::sort<std::lessstd::string,std::string &,std::string>(a1, v6, s); 排序算法 # 完全等价的字…

Django的异步任务队列管理_Celery

1 基本原理 Celery 是一个异步任务队列&#xff0c;能够将耗时操作&#xff08;如发邮件、处理图片、网络爬虫等&#xff09;从 Django 主线程中分离出来&#xff0c;由后台的 worker 处理&#xff0c;避免阻塞请求。Celery 作为独立运行的后台进程&#xff08;Worker&#xf…

【计算机网络】Linux网络的几个常用命令

&#x1f4da; 博主的专栏 &#x1f427; Linux | &#x1f5a5;️ C | &#x1f4ca; 数据结构 | &#x1f4a1;C 算法 | &#x1f152; C 语言 | &#x1f310; 计算机网络 相关文章&#xff1a;计算机网络专栏 目录 ping&#xff08;检测网络连通性&#xff09;…

全开源、私有化部署!轻量级用户行为分析系统-ClkLog

ClkLog是一款支持私有化部署的全开源埋点数据采集与分析系统&#xff0c;兼容Web、App、小程序多端埋点&#xff0c;快速洞察用户访问路径、行为轨迹&#xff0c;并生成多维用户画像。助力中小团队搭建轻量灵活的用户行为分析平台。 为什么需要一款私有化的埋点分析系统&#x…

golang定时器的精度

以 go1.23.3 linux/amd64 为例。 定时器示例代码&#xff1a; package mainimport ("context""fmt""time" )var ctx context.Contextfunc main() {timeout : 600 * time.Secondctx, _ context.WithTimeout(context.Background(), timeout)dea…

svn 远程服务搜索功能

svn服务器没有远程搜索功能&#xff0c;靠人工检索耗时耗力&#xff0c;当服务器文件过多时&#xff0c;全部checkout到本地检索&#xff0c;耗时太久。 1. TortoiseSVN 安装注意事项 下载官网地址&#xff1a;https://tortoisesvn.en.softonic.com/download 安装时选中 co…

uniapp-商城-39-shop 购物车 选好了 进行订单确认4 配送方式2 地址页面

上面讲基本的样式和地址信息&#xff0c;但是如果没有地址就需要添加地址&#xff0c;如果有不同的地址就要选地址。 来看看处理方式&#xff0c; 1、回顾 在delivery-layout中 methods:{goAddress(){uni.navigateTo({url:"/pagesub/pageshop/address/addrlist"})…

Linux命令-iostat

iostat 命令介绍 iostat 是一个用于监控 Linux 系统输入/输出设备加载情况的工具。它可以显示 CPU 的使用情况以及设备和分区的输入/输出统计信息&#xff0c;对于诊断系统性能瓶颈&#xff08;如磁盘或网络活动缓慢&#xff09;特别有用。 语法&#xff1a; iostat [options…