Rust的问题还是很多,主要是一些关键的使用方法太多了,需要知道什么时候用,为什么用是非常关键的,下面罗列的是基本上贯穿《Rust程序设计 - 第二版》的所有难点理解内容。为了将这些内容融会贯通,需要非常多的例子总结才能理解。下面内容就开始对其中的内容一个一个来梳理。
dyn、deref、deref_mut、mut、Drop
Vec<T> 是一个存储在堆中的动态数组,可以在运行时改变大小。
[T] 是一个切片,表示一段连续的内存区域,存储零个或多个类型为 T 的元素。用于引用一个数组或 Vec 的一部分。
str 是一个字符串切片,表示一段连续的内存区域,存储UTF-8编码的字符串。用于引用字符串的部分或整个字符串。
String 是一个可变的、拥有所有权的字符串类型。用于需要动态构建或修改字符串的场景。
但与普通字节数组不同的是,String强制它所存储的内容是UTF-8编码,而普通的字节数组可以是任何内容.String对字符串具有读取,修改,乃至销毁的权力,而&str只能读取字符串。
String的内部是一个Vec<u8>,进一步展开,String拥有三个字段一个指向一块内存的指针,一个表示字符串实际长度的usize,一个表示内存大小的usize而&str的内部,是一个指向一块内存的指针,一个表示字符串长度的usize值。
我们一般将String与&str组合使用,两者之间可以轻易互相转换
fn main() {
let s1: String = String::from("s1");
let s1: &str = s1.as_str();
let s2: String = String::from("s2");
let s2: &str = s2.as_str();
}
而&str转换成String的开销大了,如前文所述,String对内存具有所有权,而&str不具有,所以&str转换成String时需要将内存上的内容复制一遍,作为新的String。
由于&str的设计,使得String能够轻易地被切成多片&str
fn main() {
let s = String::from("Hello world !");
for slice in s.split_whitespace() {
println!("{}", slice);
}
}
OsString与String相对应,&OsStr与&str相对应。它们都是没有编码限制的字符串,当与系统交互时,可以使用它们。
dyn关键字在Rust中用于动态派发,即在运行时决定调用哪个具体的方法实现。它通常与特征对象(trait objects)一起使用。特征对象允许你在运行时处理不同类型的对象,只要这些对象实现了某个特征(trait)。
静态派发:在编译时决定调用哪个具体的方法实现。通常通过泛型和特征约束来实现。静态派发的优点是性能更高,因为编译器可以内联方法调用。例如函数声明。
动态派发:在运行时决定调用哪个具体的方法实现。通过特征对象和虚函数表(vtable)来实现。动态派发的优点是灵活性更高,可以处理不同类型的对象,而不需要在编译时知道具体类型。例如代码执行里dyn。
trait Shape {
fn area(&self) -> f64;
fn draw(&self);
}
struct Circle {
radius:f64
}
impl Shape for Circle{
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
fn draw(&self) {
println!("Drawing a circle with radius {}", self.radius);
}
}
struct Rectangle {
width: f64,
height: f64,
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
fn draw(&self) {
println!("Drawing a rectangle with width {} and height {}", self.width, self.height);
}
}
#[test]
pub fn test(){
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 5.0 }),
Box::new(Rectangle { width: 10.0, height: 20.0 }),
];
for shape in shapes {
shape.draw();
println!("Area:{}", shape.area());
}
}
也就是类比java的话,如果是抽象接口,需要手动声明dyn。 Circle 和 Rectangle 的大小在编译时是已知的,但 dyn Shape 的大小是未知的,因为 dyn Shape 可以表示任何实现了 Shape 特征的类型。编译器无法在编译时确定 dyn Shape 实例的大小。因此,必须通过指针来处理这些特征对象,指针可以用&,如此:
let x :&dyn Shape = &Circle { radius: 5.0 };
let shapes: Vec<&dyn Shape> = vec![
&Circle { radius: 5.0 },
&Rectangle { width: 10.0, height: 20.0 },
];
上面的代码都是能work的,但为什么要使用Box?
让我们详细讨论一下使用引用(&dyn Shape)和使用 Box<dyn Shape> 的区别,以及为什么在某些情况下 Box 更合适。
这种方法使用了引用来存储特征对象。主要的特点和限制如下:
生命周期问题:
例如,如果 Circle 和 Rectangle 是在函数内部创建的局部变量,那么它们在函数结束时会被销毁,从而导致悬挂引用。
fn create_shapes() -> Vec<&dyn Shape> {
let circle = Circle { radius: 5.0 };
let rectangle = Rectangle { width: 10.0, height: 20.0 };
// 这里我们尝试返回对局部变量的引用
vec![&circle, &rectangle]
}
fn main() {
let shapes = create_shapes();
for shape in shapes {
println!("Area: {}", shape.area());
}
}
上面的代码就会发生问题:
error[E0515]: cannot return reference to local variable `circle`
--> src/main.rs:24:12
|
24 | vec![&circle, &rectangle]
| ^^^^^^ returns a reference to data owned by the current function
error[E0515]: cannot return reference to local variable `rectangle`
--> src/main.rs:24:21
|
24 | vec![&circle, &rectangle]
| ^^^^^^^^ returns a reference to data owned by the current function
所有权问题:
不可变性:
引用默认是不可变的(&dyn Shape),如果你需要修改被引用的对象,你需要使用可变引用(&mut dyn Shape),这会带来额外的复杂性。
使用 Box<dyn Shape> 的主要特点和优势如下:
所有权管理:
例如,你可以在函数内部创建对象并将它们存储在 Box 中,而不需要担心对象的生命周期问题。
堆分配:
灵活性:
使用 Box 可以更灵活地管理对象的生命周期和内存。例如,你可以将 Box<dyn Shape> 从一个函数返回,而引用则不能轻易做到这一点。
为了上面的问题,我们可以使用 Box<dyn Shape> 来存储形状对象的所有权,而不是引用。这样,形状对象会在堆上分配内存,并且 Box 会负责管理它们的生命周期。
fn create_shapes() -> Vec<Box<dyn Shape>> {
let circle = Box::new(Circle { radius: 5.0 });
let rectangle = Box::new(Rectangle { width: 10.0, height: 20.0 });
// 返回包含 Box<dyn Shape> 的向量
vec![circle, rectangle]
}
fn main() {
let shapes = create_shapes();
for shape in shapes {
println!("Area: {}", shape.area());
}
}
在这个修正后的代码中,create_shapes 函数返回一个包含 Box<dyn Shape> 的向量。由于 Box 获取了对象的所有权,并且对象在堆上分配内存,这样就避免了悬挂引用的问题。Rust 的所有权系统会确保这些对象在合适的时间被释放,避免内存泄漏和未定义行为。
fn print_shape_info<T: Shape>(shape: &T) {
shape.draw();
println!("Area: {}", shape.area());
}
print_shape_info<T: Shape>(shape: &T) 是一个泛型函数,接受任何实现了 Shape 特征的类型 T。编译器在编译时知道具体的类型,因此可以进行内联优化和其他优化。
使用 &dyn Trait 作为函数参数
fn print_shape_info(shape: &dyn Shape) {
shape.draw();
println!("Area: {}", shape.area());
}
let circle = Circle { radius: 5.0 };
let rectangle = Rectangle { width: 10.0, height: 20.0 };
print_shape_info(&circle);
print_shape_info(&rectangle);
注意细节:这里没有使用Box,是因为这里的方法调用,不涉及所有权。
使用 Box<dyn Trait> 作为函数参数
fn print_shape_info(shape: Box<dyn Shape>) {
shape.draw();
println!("Area: {}", shape.area());
}
fn main() {
let circle = Box::new(Circle { radius: 5.0 });
let rectangle = Box::new(Rectangle { width: 10.0, height: 20.0 });
print_shape_info(circle);
print_shape_info(rectangle);
}
使用 Rc<dyn Trait> 或 Arc<dyn Trait> 作为函数参数
use std::rc::Rc;
use std::sync::Arc;
fn print_shape_info(shape: Rc<dyn Shape>) {
shape.draw();
println!("Area: {}", shape.area());
}
fn main() {
let circle = Rc::new(Circle { radius: 5.0 });
let rectangle = Rc::new(Rectangle { width: 10.0, height: 20.0 });
print_shape_info(circle.clone());
print_shape_info(rectangle.clone());
}
或者使用 Arc 进行线程安全的共享:
fn print_shape_info(shape: Arc<dyn Shape>) {
shape.draw();
println!("Area: {}", shape.area());
}
fn main() {
let circle = Arc::new(Circle { radius: 5.0 });
let rectangle = Arc::new(Rectangle { width: 10.0, height: 20.0 });
print_shape_info(circle.clone());
print_shape_info(rectangle.clone());
}
通过实现 std::ops::Deref 特型和 std::ops::DerefMut 特型,可以指定像 * 和 . 这样的解引用运算符在你的类型上的行为。像 Box<T> 和 Rc<T> 这样的指针类型就实现了这些特型,因此它们可以像 Rust 的内置指针类型那样用。
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y); // 通过实现 Deref,使得 MyBox<T> 可以像引用一样被解引用
}
上面代码直接对结构体解引用了。
如果上下文对引用目标进行了赋值或借用了可变引用,那么 Rust 就会使用 DerefMut(解可变引用)特型,否则,只要通过 Deref 进行只读 访问就够了。
如果你学习The Book时不仔细,你可能会疑惑,有时候明明参数是&str,但是传进&String依然不会报错,这得益于 Deref trait,我们为类型A实现了对String的Deref,那么在我们使用A的时候,编译器自动尝试将&A转换为&String
use std::ops::Deref;
struct A {
s: String,
}
impl Deref for A {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.s
}
}
fn say(s: &String) {
println!("Hello {s} !");
}
fn main() {
let s = A {
s: String::from("world"),
};
say(&s);
}
// Output
// Hello world !
智能指针类型(如 Box, Rc, 和 Arc)通过实现 Deref 特性,使得它们能够与普通引用类型进行互操作。例如,你可以将 Box<T> 传递给期望 &T 的函数。
use std::rc::Rc;
fn main() {
let s = Rc::new(String::from("Hello"));
let s_ref: &str = &s; // 自动解引用 Rc<String> -> &String -> &str
println!("{}", s_ref);
}
Deref 强制转换是 Rust 中的一种便捷特性,它允许在特定情况下自动将 &T 类型转换为 &U 类型,只要 T 实现了 Deref<Target = U>。这在编写更简洁的代码时非常有用。
use std::rc::Rc;
fn greet(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
let name = Rc::new(String::from("Alice"));
greet(&name); // 自动解引用 Rc<String> -> &String -> &str
}
impl<T> DerefMut for MyBox<T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.0
}
}
可以解引用修改数据
fn main() {
let mut x = MyBox::new(5);
*x += 1; // 通过 DerefMut,使得 MyBox<T> 可以像可变引用一样被解引用并修改内部数据
println!("x: {}", *x);
}
一个值的拥有者消失时,Rust 会 (drop)该值。丢弃一个值 就必须释放该值拥有的任何其他值、堆存储和系统资源。丢弃可能发 生在多种情况下:当变量超出作用域时;在表达式语句的末尾;当截 断一个向量时,会从其末尾移除元素;等等。
如果一个类型实现了 Drop,就不能再实现 Copy 特型了。如果类型 是 Copy 类型,就表示简单的逐字节复制足以生成该值的独立副本。 但是,对同一份数据多次调用同一个 drop 方法显然是错误的。
impl<T> Drop for MyBox<T> {
fn drop(&mut self) {
println!("Dropping MyBox with data: {:?}", self.0);
}
}
fn main() {
let x = MyBox::new(5);
let y = MyBox::new(String::from("Hello"));
// 当 x 和 y 离开作用域时,drop 方法会被自动调用
}
Rust 保证在值离开作用域时会自动调用 drop 方法,无需手动调用。事实上,手动调用 drop 方法是被禁止的,因为 Rust 设计上希望确保资源的安全释放。