点击我的视频网校,学习编程课程,或者关注我的微信公众号“编程之路从0到1”,了解课程更新

Rust 语法进阶(三)

语法进阶

面向对象编程

Rust语言并不支持传统意义上的面向对象编程,但是我们可以利用Rust语言的一些特性,来实现一些面向对象编程的思想。

结构体

Rust提供三种结构体:

  • 命名结构体(Named-Field Struct):结构体包含字段名称

  • 元组结构体(Tuple-Like Struct):特点是,字段没有名称,只有类型

  • 基元结构体(Unit-Like Struct):相当于定义了一个新的类型

命名结构体:

struct Point {x: i32, y: i32}
let p = Point {x: 10, y: 11};

元组结构体:

struct Point(i32, i32);
let p = Point(10, 11);

基元结构体:

基元元结构是一种没有任何字段的结构,通过完全省略字段列表来定义。这样的结构隐含地定义了一个同名类型的常量。例如:

struct Cookie;
let c = [Cookie, Cookie {}];

等价于以下代码

struct Cookie {}
const Cookie: Cookie = Cookie {};
let c = [Cookie, Cookie {}];

定义方法

可以给任意结构体类型定义方法

struct People {
    name: &'static str,
    age: u32,
}

// 定义方法
impl People {
    // 方法的参数并没有 &self
    fn new(name: &'static str, age: u32) -> Self {
        return People { name, age };
    }

    //  &self 不可变引用
    fn print_name(&self) {
        println!("People name is {}", self.name);
    }

    // 第一个参数是 &mut self 可变引用
    fn set_name(&mut self, name: &'static str) {
        self.name = name;
    }
}

fn main() {
    // 默认是不可变
    let p = People::new("Bob", 18);
    p.print_name();

    // 创建可变结构体
    let mut p2 = People::new("Alice", 28);
    p2.set_name("zhangsan");
    p2.print_name();
}

impl 块用于定义结构体的方法集合。 这里先定义了一个结构体 People,接着又给它定义了两个普通方法print_nameset_name 和一个静态方法new

作为第一个参数传入并且必须有一个特殊的名字:self self 的具体类型明显就是与之关联的那个结构体或者对该结构体的引用的类型,所以 Rust 允许省略它的类型声明。可简写成self&self&mut self ,实际上它对应的类型是Self,显式的类型分别为 self: Selfself: &Selfself: &mut Self。如果你愿意,也可以使用完整类型声明。

注意,Selfself都是关键字。函数的此参数被称为接收者(receiver),具有该参数的函数被称为方法,可以通过结构体实例变量来调用;不具有此参数的函数可以称为静态方法,这类方法是结构体类型本身而非该类型实例的函数。静态方法通常用于定义构造函数。遵循Rust 的惯例,通常将这种构造方法命名为new方法。但实际上Rust结构体并没有构造方法一说,所谓构造方法只是一个普通的静态方法,此处是模仿面向对象编程的传统概念。

引用静态方法的格式是:类型名+双冒号+方法名。例如,People::new("Bob", 18)

注意几种self参数之间的区别:

&self方法,只对实例的成员有读取权限;&mut self方法具有对成员的可变访问权限,如果方法要借用一个可修改的引用,则应使用&mut self;如果方法需要取得 self 的所有权,则参数应为 self

泛型

泛型(Generics)是指把类型抽象成一种“参数”,数据和算法都针对这种抽象的类型参数来实现,而不针对具体类型。当我们需要真正使用的时候,再具体化、实例化类型参数。

简单说,泛型可以将“类型”作为参数,在函数或者数据结构中使用,从而减少重复工作量,即一套代码可以应用于多种类型。

泛型可以在函数中使用,在impl块中使用,在trait中和数据结构(包含结构体、枚举、特征以及各种数据容器)中使用。

泛型函数:

fn give_me<T>(val:T){
    let _ = val;
}

fn main() {
    give_me(19);
    give_me("xxx");
}

泛型枚举:

enum Transmission<T>{
    Signal(T),
    NoSignal
}

泛型与结构体:

struct Tuple<T>(T);

struct Container<T,E>{
    item:T,
    other:E
}

// 实现泛型
impl <T,E> Container<T,E> {
    
    fn new(item:T) -> Self{
        Container{item}
    }
}

注意实现泛型的impl块和普通impl块的区别,impl后多了一对尖括号。

泛型与容器:

// 指定类型
let v1: Vec<u16> = Vec::new();

// 另一种是使用turbofish运算符
let v2 = Vec::<i32>::new();

小结:

  • 泛型函数是一种提供多态代码错觉的简易方法。实际上会根据调用的类型生成多个对应的函数。
  • 泛型的缺点是会生成重复代码导致编译后文件大小增加。
  • 泛型实现的多态是一种静态多态,它没有运行时开销,性能更好,是首选方案。

Trait

Trait通常翻译为特征特型。简单说,Trait类似于Rust 中对接口或抽象基类的一种实现。它主要有3种用法:

  • 接口抽象

  • 泛型约束

  • 抽象类型

接口抽象

最基础的用法就是进行接口抽象,是对类型行为的统一约束。

使用关键字trait定义一个特征,在其后的花括号内,我们可以提供零个或多个方法,任何可实现特征的类型都应该对这些方法提供具体实现。还可以在特征中定义常量,所有实现者都可以共享它们。实现者可以是任何结构体、枚举、基元类型、函数及闭包,甚至其他特征。

// 定义特征
trait Phone {
    fn call(&self,number:&str);
    fn send_message(&self,number:&str);
}

struct IPhone {}

// 该impl块用于实现特征中的方法
impl Phone for IPhone{
    fn call(&self,number:&str){
        println!("call number is {}",number);
    }

    fn send_message(&self,number:&str){
        println!("send message number is {}",number);
    }
}

// 单独的impl块定义结构体特有方法
impl IPhone{
    fn greet_siri(&self){
        println!("Hello,siri!");
    }
}

fn main() {
    let phone = IPhone{};
    phone.call("911");
    phone.send_message("112");
    phone.greet_siri();
}

注意,Trait中可以包含默认方法和静态方法的实现,还可以使用Self类型作返回值,声明一个构造方法:

trait Test{
    // 静态方法
    fn greet(name :&str){
        println!("Hello, {}", name);
    }

    // 默认方法。已经有了方法体,当具体类型实现此特征时,可以不用重写
    fn test(&self){
        println!("test run ...");
    }

    // 构造方法(签名)
    fn new()->Self;
}

可以将一个特征声明为另一个特征的扩展,这相当于Trait 的继承:

trait Foo {
    fn foo(&self);
}

// 继承另一个特征
trait FooBar : Foo {
    fn foobar(&self);
}

struct Baz{};

// 结构体必须分别实现这两个trait
impl Foo for Baz {
    fn foo(&self) { println!("foo..."); }
}

impl FooBar for Baz {
    fn foobar(&self) { println!("foobar..."); }
}

还可以利用trait给其他的类型添加方法:

trait Double{
    fn double(&self)->Self;
}

// 为内置类型i32添加一个方法
impl Double for i32{
    fn double(&self) -> Self {
        (*self) * 2
    }
}

fn main() {
    let r = 15.double();
    println!("{}",r);
}

为其他类型扩展方法时要注意,Rust规定了一个一致性规则(Coherence Rule),即impl块要么与trait的声明在同一个的crate中,要么与类型的声明在同一个crate中。也就是说,如果trait来自于外部crate,而且类型也来自于外部crate,编译器不允许你为该类型impltrait。它们之中必须至少有一个是在当前crate中定义的。

小结:

  • 作为接口的特征,可以定义方法,并支持默认实现
  • 特征之间可以继承。冒号后面跟父trait,如果需要继承多个trait,则使用+号相连,例如:trait FooBar : Foo+Bird{}
  • 使用impl关键字实现特征方法。语法格式: impl 特征名 for 类型名
  • trait和其他语言中的interfaceprotocol相似,但也存在一些差异。有些类型的大小是在编译阶段可以确定的,有些类型的大小是编译阶段无法确定的。目前Rust规定,在函数参数传递、返回值传递等地方,都要求这个类型在编译阶段有确定的大小。否则,编译器就不知道该如何生成代码。而trait本身既不是具体类型,也不是指针类型,它只是定义了针对某类型的抽象“约束”。不同的类型可以实现同一个trait,而这些类型可能具有不同的大小。因此,trait在编译阶段没有固定大小,目前不能直接使用trait声明实例变量、参数、返回值。

泛型约束

trait可以将泛型的行为限定在更有限的范围内。

当我们想要复用代码时,会考虑使用泛型,如下例求和函数:

fn sum<T>(a:T,b:T)->T{
    a + b
}

编译会产生一个错误,因为并不是任意类型都能支持加法操作,并且相加后返回一个相同类型的结果。此时就需要使用泛型约束:

use std::ops::Add;

fn sum<T:Add<Output=T>>(a:T,b:T)->T{
    a + b
}

使用<T:Add<Output=T>>对泛型进行了约束,表示sum函数的参数必须实现标准库定义的Add 特征(即对加法的抽象),并且加号两边的类型必须一致。上例中的写法使得函数可读性变差,Rust提供了一个关键字where,可以将泛型的限定放到后面:

fn sum<T>(a:T,b:T)->T where T:Add<Output=T>{
    a + b
}

抽象类型

trait 可以用作抽象类型,动态地分发给具体的类型,实现类似于面向对象中的多态。

首先看一个例子:

trait Phone {
    fn call(&self,number:&str);
}

struct IPhone;

impl Phone for IPhone{
    fn call(&self,number:&str){
        println!("IPhone call number: {}",number);
    }
}

struct XiaoMi;

impl Phone for XiaoMi{
    fn call(&self, number: &str) {
        println!("XiaoMi call number: {}",number);
    }
}

这里定义了一个描述手机的trait,分别有两个结构实现了它,IPhoneXiaoMi,此时我们希望将Phone 类型作为函数test_call的参数,在运行时才确定具体子类型,以实现面向对象中的多态:

fn test_call(phone:Phone){
    phone.call("10086")
}

此函数无法通过编译,编译器并不允许我们在函数参数中直接使用trait类型。这便是上文提到的,Rust中的trait与其他语言中的interface的差别。

此时,有两种方式来解决问题,从而实现面向对象中的多态。一种是静态分派,另一种称为动态分派。

静态分派

利用泛型约束:

// 函数参数类型是泛型
fn test_call<T:Phone>(phone:T){
    phone.call("10086")
}

// 调用
test_call(IPhone);

此时函数参数的类型被指定为泛型,且该泛型被trait Phone所约束。这要求参数必须是一个实现了Phone特征的类型。实际上,编译器会根据实际调用参数的具体类型不同,直接生成不同的函数版本。Rust中通过泛型实现的“多态”,是在编译阶段就已经确定好了调用哪个版本的函数,因此被称为静态分派

如上例,直接使用泛型,会显得麻烦,还会使函数的可读性变差。为此,Rust提供了一种impl Trait语法来简化静态分派:

fn call(phone: impl Phone){
    phone.call("10086")
}

需注意,目前impl Trait语法只能在函数的参数和返回值处使用,不能用于let声明等地方。

动态分派

虽然trait是DST(Dynamically Sized Types,即编译时大小不确定)类型,但是指向trait的指针不是DST。如果把trait隐藏到指针的后面,那它就是一个trait对象,而trait对象是可以作为参数和返回值类型的。

fn call(phone: &dyn Phone){
    phone.call("10086")
}

这里dyn是一个关键字,参数类型还可以写成&mut dyn PhoneBox<dyn Phone>trait对象。

注意,指向trait的指针就是trait对象,这个指针不是一个普通的指针,而是一个胖指针。上例中的Phone只是一个trait的名字,符合此trait的具体类型可能有很多种,这些类型并不具备相同的大小,因此使用dyn Phone来明确表示这是一个DST类型,即动态类型。

trait对象可以等价于如下结构体:

pub struct TraitObject{
    pub data: *mut (),
    pub vtable: *mut ()
}

可以看到内部包含两个指针,此处*mut ()类型相当于C语言中的void *指针。data指针指向trait对象所保存的具体数据类型 T,vtable 指针指向包含为具体类型 T 实现的 Phone的虚函数表 (VirtualTable)。在编译期,编译器只知道trait对象包含指针的信息,指针的大小也是确定的,但并不知道要调用哪个方法。在运行期,当phone.call("10086")方法被调用时,trait对象才会根据vtable指针从虚函数表中查出正确的函数指针,然后再进行动态调用,因此称之为动态分派

注意事项

使用动态分派时要注意,并不是每个trait都可以作为trait对象被使用,以下情况是无法构造trait对象的:

  1. trait具有Self:Sized约束时不行。

    一般情况下,trait不满足Sized条件,因为trait大小是不确定的。Self关键字代表的类型是实现该trait的具体类型,在impl的时候,针对不同的类型,有不同的具体化实现。如果给Self加上Sized约束:

    trait Foo where Self:Sized{
        fn foo(&self);
    }
    

    或者Foo继承自Sized,这表明要为某类型实现Foo,必须先实现Sized。所以Foo中的隐式Self也必然是Sized的

    trait Foo :Sized{
        fn foo(&self);
    }
    
  2. 当函数中Self类型出现在除第一个参数之外的地方,包括返回值中时不行。例如:

    trait Class{
        fn get_class(&self)->Self;
    }
    
  3. 当函数第一个参数不是self时不行。即trait中不能包含静态方法。

  4. 当函数有泛型参数时不行。

对于第2、3两种情况,可以将不符合要求的函数从虚函数表中移除来构造trait对象,当然,被移除的函数就不能通过这种多态方式调用了。要将不符合要求的函数从虚函数表移除,只需要给该函数加上Self: Sized约束即可。如下例:

trait Class{
    fn get_class(&self)->Self where Self:Sized;
    // 静态方法
    fn object() where Self:Sized;
    fn from_str(&self);
}

则该trait对象只能调用from_str方法,其他两个被虚函数表移除。

小结:

  • 静态派发在编译时就确定了被调用的函数,因此没有额外的性能损耗,但是它会对不同类型进行展开导致编译后产物体积膨胀。
  • 动态派发在编译时不能确定被调用的函数,在运行时才会去虚函数表中查询到被调用的函数指针,因此有一定性能开销,但是编译后产物会比静态派发更小。

特征的其他形式

标签trait

目前Rust一共提供了9个标签trait,都被定义在标准库std::marker模块中,其中有4个还是实验性的,以下5个是已经稳定的:

  • Sized ,用于标识编译期可确定大小的类型
  • Copy ,用于标识可以按位复制其值的类型
  • Send ,用于标识可以跨线程安全通信的类型
  • Sync ,用于标识可以在线程间安全共享引用的类型
  • Unpin,用于被固定后可以安全移动的类型

它们用于简单地将类型标记为属于特定的组群,以获得一定程度的编译期保障。

泛型trait
trait From<T>{
    fn from<T>()->Self;
}
关联类型trait
trait Foo{
    type Out;

    fn get_val(self) -> Self::Out;
}

函数式编程

闭包

Rust也支持闭包语法,Rust的闭包通常由参数列表(在两条竖线中给出)和表达式组成 :

    let result = |a,b| a+b;
    println!("{}",result(1,2));

Rust 可以推断参数类型和返回类型。当然也可以明确地指定,就跟定义函数一样。如果指定了返回类型,那么闭包体必须是一个代码块:

let result = |a:u32, b:u32| -> u32 {a+b};

变量捕获

闭包类似于一种匿名函数,但它并不是函数,它与函数最大区别在于,闭包能够捕获外部作用域中的变量,而函数不能:

fn main() {
    let n = 10;

    let add = |a| a + n;
    println!("{}",add(3));

    fn sub(a:i32)->i32{
        n - a
    }
}

如上例,add是闭包,而sub是函数,运行代码会报错,注释掉sub函数则正常执行。说明函数是不能捕获外部作用域中的变量的。

闭包也是通过三种方式使用外部作用域中的变量,因此闭包有三种类型(即三种trait):

  • Fn:通过引用(&)方式捕获。调用参数为&self,对方法接收者进行不可变借用。这种闭包可以被调用多次。
  • FnMut:通过可变引用(&mut)方式捕获。用参数为&mut self,对方法接收者进行可变借用。
  • FnOnce:通过值的方式捕获,即通过转移所有权来捕获。调用参数为self,因此也会转移方法接收者的所有权,所以这种闭包只能被调用一次。
pub trait Fn<Args>: FnMut<Args> {
    extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}

pub trait FnMut<Args>: FnOnce<Args> {
    extern "rust-call" fn call_mut(&mut self,args: Args) -> Self::Output;
}

pub trait FnOnce<Args> {
    type Output;
    extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}

在保证能编译通过的情况下,编译器会自动选择一种对外部影响最小的类型,这个选择顺序是: Fn > FnMut > FnOnce

总之,编译器最终是根据所捕获变量在闭包里的使用情况决定捕获方式的。

闭包作为参数

与函数一样,Rust中的闭包也被当作值来使用,因此闭包也能作为函数的参数被传递,但它的类型必须使用泛型来声明:

fn calc<F>(f :F) where F: Fn(i32,i32)->i32{
    println!("{}",f(3,3));
}

fn main() {
    let add = |a,b| a + b;
    calc(add);
}

注意,这里Fn是一个trait ,它既包括函数类型,也包括闭包类型,因此我们也可以给它传递函数:

fn calc<F>(f :F) where F: Fn(i32,i32)->i32{
    println!("{}",f(3,3));
}

fn sub(a:i32,b:i32)->i32{
    a - b
}

fn main() {
    calc(sub);
}

再看一个示例:

struct Data {
    data: String,
}

impl Drop for Data {
    fn drop(&mut self) {
        println!("destroyed struct Data!");
    }
}

fn greet<F>(f:F) where F:FnOnce(){
    f();
}

fn main() {
    let d = Data{ data: "Alex".to_string() };

    greet(||{
        println!("hello,{}",d.data);
    });
}

上例运行正常,但是当我们将闭包调用两次时,就会发生错误:

fn greet<F>(f:F) where F:FnOnce(){
    f();
    f();
}

报错:

​```rust
2 | fn greet<F>(f:F) where F:FnOnce(){
  |             - move occurs because `f` has type `F`, which does not implement the `Copy` trait
3 |     f();
  |     --- `f` moved due to this call
4 |     f();
  |     ^ value used here after move

看到FnOnce源码中的定义:

pub trait FnOnce<Args> {
    type Output;
    extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}

参数类型是self,所以在f第一次执行后,这个闭包的生命周期就已经结束了,它只能被调用一次。

有时候,闭包的生命周期可能会超过一个函数的范围。比如,我们可以将此闭包存储到某个数据结构中,在当前函数返回之后继续使用。这样一来,闭包被创建时,它通过引用的方式捕获了某些局部变量,而在闭包被调用的时候,它所指向的外部变量已经被释放。幸运的是,这种情况在Rust中无法通过编译。要解决这个问题,可以使用move关键字修饰一个闭包:

fn main() {
    let d = Data{ data: "Alex".to_string() };

    greet(move||{
        println!("hello,{}",d.data);
    });
    // println!("hello,{}",d.data);
}

使用move修饰闭包后,被捕获变量的所有权将强制转移到闭包中。这样一来,打开上例中的注释代码将报错,因为d变量已经转移了所有权,不能再被访问。

闭包作为返回值

闭包是基于trait实现的,因此闭包也不能直接作为函数返回值。前面在介绍trait的时候已经讲过,可以使用两种方案解决问题:静态分派或动态分派。

fn test<F:Fn(i32)->i32>()->F{
    return |a|a+1;
}

fn main() {
    let f = test();
    println!("{}",f(10));
}

上例代码运行会报错,我们先使用静态分派来修复:

// 静态分派
fn test()-> impl Fn(i32)->i32{
    return |a|a+1;
}

使用动态分派修复:

// 动态分派
fn test()-> Box<dyn Fn(i32) -> i32> {
    return Box::new(|a|a+1);
}

小结

  • 如果闭包中没有捕获任何外部变量,则默认自动实现Fn

  • 如果不需要修改捕获的变量:

    • 当捕获的变量是复制语义类型时,无论是否使用move修饰,均自动实现Fn
    • 当捕获的变量是移动语义类型时,若使用move修饰,则自动实现Fn;反之自动实现FnOnce
  • 如果需要修改捕获的变量,无论捕获的变量是复制语义还是移动语义,都会自动实现FnMut

  • 闭包在使用了move关键字,如果捕获变量是复制语义类型的,则闭包自动实现Copy/Clone;如果是移动语义类型,则闭包不会自动实现Copy/Clone

  • 编译器会为每一个闭包生成一个匿名结构体类型,即使两个闭包的参数和返回值一致,它们也是两个完全不同的类型,只是都实现了同一个trait而已。以下代码无法通过编译:

    fn main() {
        let mut f = |a:i32|->i32 {a+1};
        f = |a:i32|->i32 {a-1};
        println!("{}",f(10));
    }
    
  • 闭包依靠trait来实现,跟普通trait一样,我们不能直接用FnFnMutFnOnce作为变量、函数参数、函数返回值的类型

Rust 的闭包与其他大多数语言中的闭包不同。最大的区别是在有垃圾回收的语言中,可以在闭包中使用局部变量而无须考虑生命期或所有权。如果没有垃圾回收,情况就不同了。 Java、 C# 和 JavaScript 中常见的设计模式无法原封不动照搬到 Rust 里来。

智能指针

什么是智能指针?

智能指针是一种指针,它提供了超越传统指针的额外功能,例如跟踪智能指针所指向的内存。智能指针也可以用来管理其他资源,如网络连接。

智能指针还为引用提供了额外的功能。一个常见的例子是 “引用计数的智能指针类型”,它允许我们的数据有多个所有者,通过跟踪他们,一旦数据没有所有者了,就清理掉。智能指针拥有他们所指向的数据,而引用只借用数据。

智能指针的类型

Rust中的五种智能指针:

智能指针类型 描述
Box<T> 该智能指针指向在堆上分配的T类型的数据。它被用来在堆上存储数据,而不是在栈上。
Deref<T> 该智能指针允许我们自定义解引用运算符的行为。
Drop<T> 当变量超出作用域时,此智能指针从内存中释放在堆上分配的空间。
Rc<T> 该智能指针是引用计数指针。 它记录了对堆中存储的值的引用数量。
RefCell<T> 该智能指针允许我们借用可变数据,即使数据是不可变的,也被称为内部可变性

使用Box<T>

Box<T>被用来在堆上存储数据。Box智能指针本身被存储在栈中,而它所指向的数据将被存储在堆中。使用new()创建一个Box智能指针,并在括号中加入我们想要存储在堆上的值:

fn main() {
    let a = Box::new(5);
    println!("The value of a is : {}", a);
}

上例中,变量a包含Box的值,它指向数据值5。Box被存储在栈中,它所指向的数据5被存储在堆中。当程序结束时,Box被从内存中释放。除了在堆上存储数据之外,Box智能指针没有任何性能上的开销。

使用Deref<T>

我们知道,一个指针存储着另一个变量在内存中的地址。为了得到一个变量的内存地址,我们使用&引用运算符。该引用操作符也被称为取地址运算符。一旦我们得到了变量的内存地址,我们就可以得到存储在该地址的值。要做到这一点,需要用解引用运算符*解除对内存地址的引用。解引用运算符也称为间接寻址运算符。

简而言之,解引用运算符使我们能够获取指针所指向的内存中的值。

在Rust中,使用Deref特性来定制解引用运算符的行为。当我们实现Deref时,智能指针可以被当作引用来处理,任何在引用上工作的代码也可以在智能指针上使用。

fn main() {
    let a = 5;
    let b = &a;

    if a == *b {
        println!("Equal");
    } else {
        println!("Not equal");
    }
}

上例中,a包含5的值,b包含a的引用。当我们在if语句中使用*b时,它代表与a相同的值5,所以我们可以比较这两个值。如果我们将if语句改为&b,编译器将引发错误。

fn main() {
    let a = 5;
    let b = &a;

    if a == &b {
        println!("Equal");
    } else {
        println!("Not equal");
    }
}

编译器将引发一个错误,即它们不能被比较。这是因为&得到的是内存中a变量的地址,而不是实际值。我们也可以使用Box<T>作为引用,但结果是一样的。

// 自定义智能指针
struct CustomBox<T> {
    a : T,
}

// 使用标准库中的Deref
use :: std::ops::Deref;

// 实现Deref
impl<T> Deref for CustomBox<T> {
    type Target = T;

    fn deref(&self) -> &T{
        &self.a
    }
}

fn main() {
    let b = CustomBox{ a : 5 };

    // 调用deref方法,用*号对返回的引用进行解引用
    println!("{}", *b.deref());
}

使用Drop

当不再使用一个资源时,我们不希望它在内存中占用空间。Drop trait将在内存中删除指针指向的空间。我们不需要明确地调用Drop trait,当一个值超出作用域时,Rust会自动为我们这样做。

struct Example {
    a: i32
}

// 一个实例超出作用域时,使用这个自定义的drop()方法
impl Drop for Example {

    fn drop(&mut self) {
        println!("Custom Drop: Dropping the instance of Example: {}", self.a);
    }
}

fn main() {
    let var_1 = Example{a: 1};
    let var_2 = Example{a: 5};

    println!("Example instances created");
}

上例中,我们为 Example 结构实现了 Drop 特征。Drop 特征用于实现drop()方法,该方法需要一个对自身的可变引用。在drop()方法中,我们添加了一些自定义代码,在本例中,这些代码会向控制台打印一条简单的信息。当Example的一个实例超出作用域并在内存中被删除时,这个消息将被打印出来。

main()中,创建了两个example结构的实例,并向控制台打印了一条信息,说明它们已经被创建。当这些实例超出作用域时,自定义的 drop() 方法将被触发,它应该向控制台打印一条消息。

小结:

  • 指针是一个变量,它存储着内存中另一个变量的地址。
  • 一个智能指针是一个具有额外功能的指针。
  • Box<T>用于在堆上而不是在栈上存储数据。
  • Deref<T> 特征允许我们自定义解引用运算符的行为。
  • Drop特征允许我们自定义实例的释放行为。

关注公众号:编程之路从0到1

了解更多技术干货

编程之路从0到1

# Rust  

评论

公众号:编程之路从0到1

公众号

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×