Rust学习笔记3:类型、智能指针
Rust学习笔记3:类型、智能指针
空类型或称永远不返回类型
感叹号写在返回值的地方,返回该函数“永远不返回类型”,在函数内部,panic可以“返回”此类型。
fn main() {
let i = clac(5);
println!("i = {}", i); // i = 1
}
fn clac(n:i32) -> i32 {
match n {
0..=10 => 1,
_ => panic!("不合规定的值:{}", n)
}
}
永远不返回类型也是任何类型,第9行,这里期待返回一个i32类型,但使用panic也能通过编译。这个类型与JS中的void类型相似。
Sized特征与定长类型、不定长类型
出人意料的是,很多可以动态改变大小的类型,例如矢量(Vec)、字符串(String)、切片引用(&[T])、哈希字典(HashMap),都是定长类型。它们中的元素或内容可以改变,但在编译时却拥有一个确定的长度大小,所以它们是定长类型。
而不定长类型,指的是在编译器不能确定其长度大小的类型,也叫DST(dynamically sized types)类型,在Rust中这样的类型只有三种:str、[T]、dyn Trait。对于str,它的内存是分配在静态内存区,只有运行后才知道具体占了多少内存。对于[T],它是切片,它与切片引用(&[T])还不同,它也无法独立使用。对于dyn Tratit,大概因为特征的具体实现是不确定的,方法中使用的类型可能有不定长类型,所以它也是不定长类型。这三种类型要使用,必须用Box等智能指针包裹起来。
所有定长类型都实现了Sized特征,反之不定长类型实现了?Sized特征。我们知道,函数的参数和返回值都要求类型和长度是固定的,但是,?Sized却可以作为参数的类型:
use std::fmt::Debug;
fn main() {
let s1: Box<str> = "Hello!".into();
generic(&s1);
let s1: Box<String> = Box::new("Hello!".to_string());
generic(&s1);
}
fn generic<T: ?Sized+Debug>(t: &Box<T>) {
println!("t = {:?}", t);
}
第4行,str使用Box包裹了起来。第9行,T的类型约束就有?Sized。这个示例代码没有问题,可以运行。有人觉得String不是定长类型吗,为什么在这里仍然适用?原因在于?Sized这个特征,是一个模棱两可的特征,它代表定长类型可以,非定长类型同样也可以。
堆与栈的区别
与C、C++类似,使用Rust编写程序不得不考虑堆与栈。对于栈,Rust在大小上是有限制的,main函数大致限制为8MB,普通函数限制为2MB。这是栈的大小,栈并不是说可以随意取用的。一般情况下,我们只是将小数据,实现了Copy特征的基本数据,放在栈上进行分配,并且在函数执行完以后,立即就把他们释放掉。
对于堆,它的大小其实只受限于物理内存的大小。一般情况下,我们将中、大型数据存储在堆上,在栈上只存储指向该数据的指针。一般常识认为,对于内存的使用,堆相比于栈会更慢一些,但其实这不是一定的,因为无论是堆还是栈,本质上它们都是内存。而决定内存读取快慢的因素,主要是CPU缓存。一般情况下,我们认为栈的效率相对较高,那是因为栈一般存储的数据比较简单,并且存储的数据量比较小,取用比较便捷,这是由于一贯的使用方式给人们带来的一种假象。栈其实并不一定比堆上内存更快。
智能指针与String
String在Rust中其实是一种智能指针,它其实相当于是一个Box<&str>
,这在常识上有一些反直觉。
在Rust中,指针大致可以分为两种类型。
第一种类型是最简单、最常见的,叫做引用。例如:
let x = 1;
let y = &x;
在这里 y 就是一个引用,它是一个指针。
另外一种指针叫智能指针,也叫胖指针、大指针。它除了持有一个内存地址以外,还带有其他信息,例如内存地址(这是必不可少的)、长度、最大长度等。这些信息一般以结构体的形式进行组织,分配在栈上。
根据不同的智能指针类型,还有一些额外的特征。例如 String,它其实是一个智能指针,除了一般智能指针拥有的信息以外,它还要保证它的所有字符都是 utf-8 编码。又如 Rc<T>
,这是一个自动引用计数指针,这个东西与其他语言里的可以自动回收内存的对象类似。
Rust 是讲究所有权的,一般情况下一个值只有一个所有者。但是,这个 Rc<T>
不同,它允许有多个所有者,它是计数的,当所有的引用记数为 0 的时候,它才会被释放。还有 Arc<T>
,它是在 Rc<T>
的基础之上支持了多线程安全。
还有Ref<T>、RefMut<T>
,这两个也是智能指针,后者允许将借用规则检查从编译时延期至运行时进行,让一般不能通过编译检查的代码通过检查。
所以,我们看,Rust 的指针很丰富。Rust 是一个很严格的语言,它在默认情况下只允许程序员做极小权限的事情,但是它也提供了很多扩展手段,允许程序员做其他几乎所有的事情。
Rust像一个管家婆,充满了对程序员的不信任。
智能指针Box<T>
Box可以帮我们将对象分配在堆上,他有四个常见的应用场景。
第一个场景,就是简单的将一个本来要分配在栈上的变量分配在堆上,例如:
fn main() {
let y = Box::new(10);
println!("y (heap): {}", y);
}
第2行,这个数字10就分配到了堆内存空间。
第二个场景,可以避免大量的内存拷贝。假如我们有个数组,这个数组它有1000个元素,每个元素都是一个i32类型。如果分配在栈上的话,那么当我们将一个数组变量赋值给另外一个变量的时候,就会发生Copy,这会影响性能。在这种条件下,其实使用栈的效率未必比使用堆效率更高。而如果我们使用Box分配内存的话,在拷贝的时候,只是一个引用地址的赋值,就避免了大数据拷贝。示例:
fn main() {
let arr1 = [0; 1000]; // 将数组分配在栈上
let arr2 = arr1;
println!("arr2[0]: {}", arr2[0]); // 输出 arr2[0]: 0
println!("arr1[0]: {}", arr1[0]); // 输出 arr2[0]: 0
let arr1 = Box::new([0; 1000]); // 使用 Box 将数组分配在堆上
let arr2 = arr1;
println!("arr2[0]: {}", arr2[0]); // 输出 arr2[0]: 0
// println!("arr1[0]: {}", arr1[0]); // 这行代码会报错
}
第3行赋值时,发生了大量Copy。第8行却没有,arr1将所有权转移给了arr2,因此第10行代码不能正常运行。
第三个场景,可以将非定长类型转化为定长类型。举个例子:
struct List {
Cons(i32, List),
Nil
}
在Rust中直接定义这样的一个递归结构体是不被充许的,因为大小无法确定。下面用Rust的方法实现它:
// 定义一个枚举来表示链表
#[derive(Debug)]
enum List {
Cons(i32, Box<List>), // 使用 Box 将递归部分分配在堆上
Nil, // 链表结束标记
}
// 实现链表的方法
impl List {
// 创建一个空链表
fn new() -> Self {
List::Nil
}
// 在链表头部添加一个元素
fn prepend(self, value: i32) -> Self {
List::Cons(value, Box::new(self))
}
// 获取链表的长度
fn len(&self) -> usize {
match self {
List::Cons(_, tail) => 1 + tail.len(),
List::Nil => 0,
}
}
// 将链表转换为字符串(用于打印)
fn to_string(&self) -> String {
match self {
List::Cons(head, tail) => format!("{}, {}", head, tail.to_string()),
List::Nil => "Nil".to_string(),
}
}
}
fn main() {
// 创建一个空链表
let list = List::new();
// 在链表头部添加元素
let list = list.prepend(1);
let list = list.prepend(2);
let list = list.prepend(3);
// 打印链表
println!("List: {}", list.to_string()); // 输出: 3, 2, 1, Nil
println!("List length: {}", list.len()); // 输出: 3
}
第4行,我们用Box将List包裹了起来,这样List就有确定的大小了。
第四个场景,可以统一对象类型,然后作为元素集合中的元素使用。我们知道,集合中的元素,无论是矢量数组,还是数组,还是切片,都要求元素的类型是相同的,不同的类型,没有办法放在一起。对于不同的类型,我们可以使用特征对象,即Box<dyn Trait>
,让不同的类型实现同一个Trait,然后将它们放在一个统一的矢量数组中。另外一个类似的将不同类型放在一起的方式是枚举,但是枚举需要手动管理枚举项,其实不如矢量数组方便。这里有一个很常见的例子,就是UI组件的渲染,我们可以将实现了相同Component特征的UI组件,放进一个矢量数组中,然后在一个for循环里面,循环调用他们的draw方法,模拟UI绘制。示例:
// 定义 Component trait
trait Component {
fn draw(&self);
}
// 定义一个按钮组件
struct Button {
label: String,
}
impl Component for Button {
fn draw(&self) {
println!("Drawing button with label: {}", self.label);
}
}
// 定义一个文本框组件
struct TextBox {
text: String,
}
impl Component for TextBox {
fn draw(&self) {
println!("Drawing text box with text: {}", self.text);
}
}
// 定义一个图片组件
struct Image {
source: String,
}
impl Component for Image {
fn draw(&self) {
println!("Drawing image from source: {}", self.source);
}
}
fn main() {
// 创建一个矢量数组,存储实现了 Component trait 的组件
let mut components: Vec<Box<dyn Component>> = Vec::new();
// 创建一些 UI 组件
let button = Button {
label: "Click Me".to_string(),
};
let text_box = TextBox {
text: "Hello, World!".to_string(),
};
let image = Image {
source: "image.png".to_string(),
};
// 将组件添加到矢量数组中
components.push(Box::new(button));
components.push(Box::new(text_box));
components.push(Box::new(image));
// 模拟 UI 渲染:循环调用每个组件的 draw 方法
for component in components {
component.draw();
}
}
这是一个完整的示例。第41行,我们以Box<dyn Component>
作为元素类型,创建了一个矢量数组。dyn Component
是一个特征对象,指代所有实现了该特征的组件。
关联函数Box::leak
Box有一个关联函数叫Box::leak,
它有一个特别的用处,就是可以将Box分配的内存泄露出来,将堆上的对象主动脱去外壳,返回一个具有'static
生命周期的引用。这个功能,我们可以用于写全局的配置信息,让其与整个应用程序保持等长的生命周期。达到相同目的的另外一种方法,是我们可以用Arc或者Rc做到这一点,但是这个方法不够优雅,没有Box::leak
更简单,更优雅。下面是一个示例,我们利用Box::leak
返回一个static生命周期的&str
类型的字符字信息,这是一个全局的配置信息。
// 定义一个函数来创建全局配置信息
fn create_global_config() -> &'static str {
// 创建一个字符串并分配在堆上
let config = Box::new("This is a global configuration".to_string());
// 使用 Box::leak 将 Box 泄漏,返回一个 'static 生命周期的 &str
Box::leak(config)
}
fn main() {
// 调用函数获取全局配置信息
let global_config = create_global_config();
// 打印全局配置信息
println!("Global config: {}", global_config);
// 模拟其他函数使用全局配置
use_global_config(global_config);
}
// 一个函数,接受 'static 生命周期的 &str 作为参数
fn use_global_config(config: &'static str) {
println!("Using global config: {}", config);
}
第7行,使用Box::leak返回了'static生命周期的&str字符串。字符串是基础数据格式,可以用JSON或XML,进一步封装复杂的配置信息。
2025年03月11日