什么是C++的shared_ptr的Rust等价物?

tir*_*irz 2 scope shared-ptr ownership rust

为什么Rust中不允许使用此语法:

fn main() {
    let a = String::from("ping");
    let b = a;

    println!("{{{}, {}}}", a, b);
}
Run Code Online (Sandbox Code Playgroud)

当我尝试编译这段代码时,我得到了:

error[E0382]: use of moved value: `a`
 --> src/main.rs:5:28
  |
3 |     let b = a;
  |         - value moved here
4 | 
5 |     println!("{{{}, {}}}", a, b);
  |                            ^ value used here after move
  |
  = note: move occurs because `a` has type `std::string::String`, which does not implement the `Copy` trait
Run Code Online (Sandbox Code Playgroud)

实际上,我们可以简单地创建一个引用 - 在运行时它是不一样的:

fn main() {
    let a = String::from("ping");
    let b = &a;

    println!("{{{}, {}}}", a, b);
}
Run Code Online (Sandbox Code Playgroud)

它有效:

{ping, ping}
Run Code Online (Sandbox Code Playgroud)

根据Rust Book,它是为了避免双重免费错误,因为Rust的变量是通过引用而不是通过值复制的.Rust将使第一个对象无效并使其无法使用......

在此输入图像描述

我们必须做这样的事情:

在此输入图像描述

我喜欢通过引用复制的想法,但为什么会自动使第一个无效?

应该可以通过不同的方法避免双重自由.例如,C++已经有了一个很好的工具来允许多个空闲调用... shared_ptr只有当没有其他指针指向对象时,调用才会自由 - 它看起来与我们实际做的非常相似,区别在于shared_ptr有一个计数器.

例如,我们可以在编译期间计算每个对象的引用数,并free仅在最后一个引用超出范围时调用.

但鲁斯特是一种年轻的语言; 也许他们没有时间实施类似的东西?Rust是否计划允许第二次引用对象而不使第一个引用无效,或者我们应该习惯只使用引用的引用?

She*_*ter 14

无论是RcArc替代shared_ptr.您选择哪种方法取决于共享数据所需的线程安全级别; Rc适用于非线程情况,Arc是您需要线程的时候:

use std::rc::Rc;

fn main() {
    let a = Rc::new(String::from("ping"));
    let b = a.clone();

    println!("{{{}, {}}}", a, b);
}
Run Code Online (Sandbox Code Playgroud)

shared_ptr,这并不能复制String自身.它仅在clone调用时在运行时增加引用计数器,并在每个副本超出范围时减少计数器.

不同shared_ptr,Rc并且Arc有更好的线程语义.shared_ptr是半线程安全的.shared_ptr的引用计数器本身是线程安全的,但共享数据不是"神奇地"使线程安全.

如果您shared_ptr在线程程序中使用,您仍需要做更多工作以确保其安全.在非线程程序中,您需要支付一些您不需要的线程安全性.

如果您希望允许变更共享值,则还需要切换到运行时借用检查.这是通过提供类型等Cell,RefCell,MutexRefCell是适当的StringRc:

use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let a = Rc::new(RefCell::new(String::from("ping")));
    let b = a.clone();

    println!("{{{}, {}}}", a.borrow(), b.borrow());

    a.borrow_mut().push_str("pong");
    println!("{{{}, {}}}", a.borrow(), b.borrow());
}
Run Code Online (Sandbox Code Playgroud)

我们可以在编译期间计算每个对象的引用数,并且只有在最后一个引用超出范围时才调用.

这几乎就是Rust对引用的作用.它实际上并不使用计数器,但它只允许您使用对值的引用,同时保证该值保持在相同的内存地址.

C++的shared_ptr没有在编译时做到这一点.shared_ptr,RcArc都是维护计数器的运行时构造.

是否可以在不使第一个引用无效的情况下引用对象?

这正是Rust对引用所做的,以及你已经做过的事情:

fn main() {
    let a = String::from("ping");
    let b = &a;

    println!("{{{}, {}}}", a, b);
}
Run Code Online (Sandbox Code Playgroud)

更好的是,b一旦a不再有效,编译器将阻止您使用.

因为Rust的变量是通过引用而不是按值复制的

这不是真的.分配值时,值的所有权将传输到新变量.从语义上讲,变量的内存地址已经改变,因此读取该地址可能会导致内存不安全.

我们应该养成只参考的习惯

是的,尽可能使用参考是最惯用的选择.这些需要零运行时开销,编译器会告诉您有关错误的信息,而不是在运行时遇到错误.

肯定有时间RcArc有用.通常它们是循环数据结构所必需的.如果你不能得到明确的工作参考,你不应该对使用它们感到不舒服.

参考参考?

这有点不利,因为额外的间接是不幸的.如果你真的需要,你可以减少它.如果您不需要修改字符串,则可以切换到Rc<str>:

use std::rc::Rc;

fn main() {
    let a: Rc<str> = Rc::from("ping");
    let b = a.clone();

    println!("{{{}, {}}}", a, b);
}
Run Code Online (Sandbox Code Playgroud)

如果您需要保留String有时修改的能力,您还可以显式转换&Rc<T>&T:

use std::rc::Rc;

fn main() {
    let a = Rc::new(String::from("ping"));
    let b = a.clone();

    let a_s: &str = &*a;
    let b_s: &str = &*b;

    println!("{{{}, {}}}", a_s, b_s);
}
Run Code Online (Sandbox Code Playgroud)

也可以看看:


Fab*_*orr 5

也许我们可以简单地在编译期间计算对每个对象的引用数,并且仅在最后一个引用超出范围时才调用free。

您在正确的轨道上!这Rc是为了什么。这是一种智能指针类型,非常类似于std::shared_ptrC ++。仅在最后一个指针实例超出范围后才释放内存:

use std::rc::Rc;

fn main() {
    let a = Rc::new(String::from("ping"));

    // clone() here does not copy the string; it creates another pointer
    // and increments the reference count
    let b = a.clone();

    println!("{{{}, {}}}", *a, *b);
}
Run Code Online (Sandbox Code Playgroud)

由于您只能永久访问Rc的内容(毕竟是共享的,并且Rust中禁止共享可变性),因此您需要内部可变性才能更改其内容,可通过Cell或实现RefCell

use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    let a = Rc::new(RefCell::new(String::from("Hello")));
    let b = a.clone();

    a.borrow_mut() += ", World!";

    println!("{}", *b); // Prints "Hello, World!"
}
Run Code Online (Sandbox Code Playgroud)

但是大多数时候,您根本不需要使用Rc(或其线程安全的兄弟Arc)。Rust的所有权模型主要是通过String在一个地方声明该实例并在其他地方使用对该实例的引用来避免引用计数的开销,就像在第二个片段中所做的那样。尝试着重于此并Rc仅在确实必要时使用,例如在实现类似图的结构时。