zoukankan      html  css  js  c++  java
  • [易学易懂系列|rustlang语言|零基础|快速入门|(21)|智能指针]

    [易学易懂系列|rustlang语言|零基础|快速入门|(21)|智能指针]

    实用知识

    智能指针

    我们今天来讲讲Rust中的智能指针。

    什么是指针?

    在Rust,指针(普通指针),就是保存内存地址的值。这个值,指向堆heap的地址。

    什么是智能指针?

    在Rust,简单来说,相对普通指针,智能指针,除了保存内存地址外,还有额外的其他属性或元数据。

    在Rust中,因为有所有权和借用的概念,所以引用和智能指针,又有一点不一样。

    简单来说,智能指针,拥有数据所有权,而引用没有。

    智能指针分以下几种:

    1.Box,用于在堆里分配内存。

    2.Rc,引用计数类型,用于多线程中的多个所有权。

    3.Ref and RefMut, 用于强制让借用规则在运行时生效,一般通过RefCell访问。

    我们先来看看Box,来看看简单例子:

    fn main() {
        let b = Box::new(5);
        println!("b = {}", b);
    }
    

    这段代码很简单,定义一个Box智能指针,把它绑定到变量b,b就是智能指针(在栈stack里),指向堆内存地址的数据(数据在堆heap里)。

    结果打印:

    b = 5
    

    我们来看看一个复杂点的例子,我们想定义一个lisp语言中的cons list,这种类型,是个递归类型,

    简单来说,它是一个封装数据的容器,如图所示:

    An infinite Cons list

    我们来用Rust简单定义下这个数据结构,如下代码:

    enum List {
        Cons(i32, List),
        Nil,
    }
    fn main() {
        let list = Cons(1, Cons(2, Cons(3, Nil)));
    }
    

    用cargo run ,运行下,结果报错:

    error[E0072]: recursive type `List` has infinite size
     --> srcmain.rs:5:1
      |
    5 | enum List {
      | ^^^^^^^^^ recursive type has infinite size
    6 |     Cons(i32, List),
      |               ---- recursive without indirection
      |
      = help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to make `List` represxyentable
    

    编译器报告说,这是一个递归类型,不确定长度,没办法初始化

    怎么办?

    用Box,代码修改如下:

    enum List {
        Cons(i32, Box<List>),
        Nil,
    }
    
    use crate::List::{Cons, Nil};
    
    fn main() {
        let list = Cons(1,
            Box::new(Cons(2,
                Box::new(Cons(3,
                    Box::new(Nil))))));
    }
    

    现在一切正确。

    现在的数据结构,对Rust来说是这样的,如下图所示:

    Box类型因为实现了解引用特征Deref,所以它跟引用类型一样,同时,因为它实现了Drop特征,所以当它超出了作用域,它所占有的stack和heap空间会自动释放。

    我们现在再来看看普通引用和智能指针的不同。

    1.智能指针实现了Deref特征,所以它跟普通引用类似。

    我们来看看例子:

    fn main() {
        let x = 5;
        let y = &x;//y借用x,即y绑定到x的引用,y现在是个引用类型
    
        assert_eq!(5, x);
        assert_eq!(5, y);//error,错误,不能比较数据类型和引用类型
        
    }
    

    运行上面的代码,编译器报错:

    error[E0277]: can't compare `{integer}` with `&{integer}`
      --> srcmain.rs:28:5
       |
    28 |     assert_eq!(5, y);
       |     ^^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
       |
       = help: the trait `std::cmp::PartialEq<&{integer}>` is not implemented for `{integer}`
    

    怎么办?

    用解引用操作符*。

    我们来修改一下代码 :

    fn main() {
        let x = 5;
        let y = &x;
    
        assert_eq!(5, x);
        assert_eq!(5, *y);//用解引用操作符*,来取y指针指向的值
    }
    

    运行代码,一切正常。

    这个解引用操作符*,就是用来取引用(指针)指向的值。

    我们现在用Box类型来重写一下上面的代码:

    fn main() {
        let x = 5;
        let y = Box::new(x);
    
        assert_eq!(5, x);
        assert_eq!(5, *y);
    }
    

    运行代码,一切正常。

    说明 Box类型跟上面的普通引用(指针),是一样的效果。

    它们唯一 的区别就是,一个是智能指针,一个是普通指针。

    好理解。

    现在我们再来看定义一个自己的智能指针,开始设计:

    struct MyBox<T>(T);//tuple元组类型
    
    impl<T> MyBox<T> {
        fn new(x: T) -> MyBox<T> {
            MyBox(x)
        }
    }
    

    然后,我们同样的方式来用这个自定义的智能指针:

    fn main() {
        let x = 5;
        let y = MyBox::new(x);
    
        assert_eq!(5, x);
        assert_eq!(5, *y);
    }
    struct MyBox<T>(T);
    
    impl<T> MyBox<T> {
        fn new(x: T) -> MyBox<T> {
            MyBox(x)
        }
    }
    

    运行代码,报错了:

    error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
      --> src/main.rs:14:19
       |
    14 |     assert_eq!(5, *y);
       |                   ^^
    

    为什么?

    因为,我们的MyBox没有实现特征Deref。

    好,我们来实现Deref特征,代码更新如下:

    fn main() {
        let x = 5;
        let y = MyBox::new(x);
    
        assert_eq!(5, x);
        assert_eq!(5, *y);
    }
    struct MyBox<T>(T);////tuple类型
    
    impl<T> MyBox<T> {
        fn new(x: T) -> MyBox<T> {
            MyBox(x)
        }
    }
    use std::ops::Deref;
    ////必须实现Deref trait,否则不能使用*操作符
    impl<T> Deref for MyBox<T> {
        type Target = T;
    
        fn deref(&self) -> &T {
            &self.0////tuple索引
        }
    }
    

    运行代码,一切正常。

    我们来分析一下代码:

    type Target = T;语法是指定一个Deref特征的关联类型。

    而这段代码:

     fn deref(&self) -> &T {
            &self.0////tuple索引
        }
    

    则实现解引用方法,这里直接返回元组tuple第一个索引。

    当前我们也可以用官方标准写法:

    use std::ops::Deref;
    
    struct DerefExample<T> {
        value: T,
    }
    
    impl<T> Deref for DerefExample<T> {
        type Target = T;
    
        fn deref(&self) -> &Self::Target {
            &self.value
        }
    }
    
    fn main() {
        let x = DerefExample { value: 'a' };
        assert_eq!('a', *x);
        println!("{}", *x);
        let y = DerefExample {
            value: String::from("Good!"),
        };
        println!("{}", *y);
    }
    
    

    现在我们再看看把这个自定义智能指针作为传递参数:

    fn hello(name: &str) {
        println!("Hello, {}!", name);
    }
    

    我们先定义一个hello的方法,这个方法直接打印一条简单的问候信息。

    我们看看怎么调用:

    fn main() {
        let m = MyBox::new(String::from("Rust"));
        hello(&m);
    }
    

    完整代码如下:

    use std::ops::Deref;
    fn main() {
    
        let m = MyBox::new(String::from("Rust"));
        hello(&m);//这里直接用借用操作符&,不用再用解引用操作符*
    }
    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 hello(name: &str) {
        println!("Hello, {}!", name);
    }
    
    

    运行代码,打印结果信息:

    Hello, Rust!
    

    一切正常。

    因为Rust实现了强制解引用机制(deref coercion),所以我们不用再用解引用操作符访问:

    fn main() {
        let m = MyBox::new(String::from("Rust"));
        hello(&(*m)[..]);//解引用写法
    }
    

    我们再来看看特征Drop Trait

    简单来说,特征Drop是用来标记相关变量超出作用域后释放资源。

    在Rust所有智能指针都已经由编译器自动加入实现这个方法。

    当然,我们也可以定制一下这个方法,我们来看看简单例子:

    struct CustomSmartPointer {
        data: String,
    }
    
    impl Drop for CustomSmartPointer {
        fn drop(&mut self) {
            println!("Dropping CustomSmartPointer with data `{}`!", self.data);
        }
    }
    
    fn main() {
        let c = CustomSmartPointer { data: String::from("my stuff") };
        let d = CustomSmartPointer { data: String::from("other stuff") };
        println!("CustomSmartPointers created.");
    }//c,d在这里结束生命周期,这里Rust自动调用Drop实现方法
    

    我们在main函数创建了两个实例c,d,在最后一个大括号时,结束这两个实例的“生命”,自动调用相关Drop实现方法,打印结果为:

    CustomSmartPointers created.
    Dropping CustomSmartPointer with data `other stuff`!
    Dropping CustomSmartPointer with data `my stuff`!
    

    在这里,Drop特征实现,有点类似于java的finalize方法,是一个对象或资源的临终遗言。

    我们再来看看更复杂的智能指针,RC智能指针,也就是引用计数智能指针。

    为什么要有引用计数智能指针呢?

    因为有这样的情景,一个数据,可能有多个拥有者(当然这里的拥有者也就是线程)。这就是多所有权(multiple ownership)

    我们可以想象,这个多所有权,就像一台电视机,一个房间只有一台电视机,第一个人来了,打开电视机,第二个人,第三个人来了,就各加一个座位(这里就像引用加个计数器,每来一个人加1),有人离开了,就把座位拿开(计数器减1),直到最后一个人看完了电视,把电视关了。

    如果,中间有人a离开,但还有其他人在看电视,这个a直接把电视机关了,结果肯定会引起喧嚣!!!!

    我们回过头来看看之前提到过的cons list数据结构:

    Two lists that share ownership of a third list

    我现在用Box类型定义:

    enum List {
        Cons(i32, Box<List>),
        Nil,
    }
    
    use crate::List::{Cons, Nil};
    
    fn main() {
        let a = Cons(5,
            Box::new(Cons(10,
                Box::new(Nil))));
        let b = Cons(3, Box::new(a));
        let c = Cons(4, Box::new(a));
    }
    

    运行代码,编译错误:

    error[E0382]: use of moved value: `a`
      --> src/main.rs:13:30
       |
    12 |     let b = Cons(3, Box::new(a));
       |                              - value moved here
    13 |     let c = Cons(4, Box::new(a));
       |                              ^ value used here after move
       |
       = note: move occurs because `a` has type `List`, which does not implement
       the `Copy` trait
    

    怎么办?

    用RC类型,修改代码如下 :

    enum List {
        Cons(i32, Rc<List>),
        Nil,
    }
    
    use crate::List::{Cons, Nil};
    use std::rc::Rc;
    
    fn main() {
        let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
        let b = Cons(3, Rc::clone(&a));
        let c = Cons(4, Rc::clone(&a));
    }
    

    我们来看看引用计数智能指针的计数器,发生了什么,修改代码:

    enum List {
        Cons(i32, Rc<List>),
        Nil,
    }
    
    use crate::List::{Cons, Nil};
    use std::rc::Rc;
    
    fn main() {
        // let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
        // let b = Cons(3, Rc::clone(&a));
        // let c = Cons(4, Rc::clone(&a));
        // println!("{}", c);
        let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
        println!("count after creating a = {}", Rc::strong_count(&a));
        let b = Cons(3, Rc::clone(&a));
        println!("count after creating b = {}", Rc::strong_count(&a));
        {
            let c = Cons(4, Rc::clone(&a));
            println!("count after creating c = {}", Rc::strong_count(&a));
        }
        println!("count after c goes out of scope = {}", Rc::strong_count(&a))
    }
    
    

    运行代码,打印结果为:

    count after creating a = 1
    count after creating b = 2
    count after creating c = 3
    count after c goes out of scope = 2
    

    我们看到RC类型的引用计数器,是从1开始累加的。

    中间c的生命周期结束了,就减一。

    现在我们再来看看ReCell类型的智能指针。

    我们从上面的例子可在看到 ,因为Rust默认的变量绑定是不可变的。

    所以当我们要有一个变量,在运行时可变的。这时,就要用到ReCell类型。

    看下面简单代码:

    use std::cell::RefCell;
    fn main() {
        let c = RefCell::new(5);
    
        *c.borrow_mut() = 7;
        assert_eq!(*c.borrow(), 7);
    }
    
    

    我们可以让编译器通过,并且成功运行。

    为什么?

    我们再来看看另一个例子:

    use std::cell::RefCell;
    fn main() {
        // let c = RefCell::new(5);
    
        // *c.borrow_mut() = 7;
        // assert_eq!(*c.borrow(), 7);
    
        let x = RefCell::new(42);
    
        let y = x.borrow_mut();
        let z = x.borrow_mut();//每二次可变借用,已经违反了编译器的借用规则。但可以编译通过。
        
    }
    
    

    运行代码,编译通过。

    我们看到两次可变借用已经违反了借用规则。

    Rust的借用规则很简单:

    同一时间,同一数据

    1.允许一个或多个共享借用(不可变借用)

    2.只允许一个可变借用。

    上面的代码已经两个可变借用,但也可以通过。

    ReCell主要 作用就是用于运行时来检查借用规则。这就是内部可变性的设计模式。

    主要用途在哪里?

    我们再来看看例子:

    struct Point {
        x: i32,
        y: i32,
    }
    
    let mut a = Point { x: 5, y: 6 };
    
    a.x = 10;
    
    let b = Point { x: 5, y: 6 };
    
    b.x = 10; // Error: cannot assign to immutable field `b.x`.错误
    

    解决错误用Cell:

    use std::cell::Cell;
    
    struct Point {
        x: i32,
        y: Cell<i32>,
    }
    
    let point = Point { x: 5, y: Cell::new(6) };
    
    point.y.set(7);
    
    println!("y: {:?}", point.y);
    

    以上,希望对你有用。

    如果遇到什么问题,欢迎加入:rust新手群,在这里我可以提供一些简单的帮助,加微信:360369487,注明:博客园+rust
    

    参考文章:

    https://doc.rust-lang.org/stable/book/ch15-00-smart-pointers.html

    https://stackoverflow.com/questions/30831037/situations-where-cell-or-refcell-is-the-best-choice

  • 相关阅读:
    Kotlin中Range与异常体系剖析
    @RequestParam与@PathVariable的区别
    thymeleaf:局部变量 th:with
    关于thymeleaf th:replace th:include th:insert 的区别
    MockHttpServletRequestBuilder中content和param的区别
    使用spring的JavaMailSender发送邮件
    Spring的注解@Qualifier小结
    MySql 中 case when then else end 的用法
    @Transient 理解
    Vue生命周期-手动挂载理解
  • 原文地址:https://www.cnblogs.com/gyc567/p/12038826.html
Copyright © 2011-2022 走看看