Rust中与C++的shared_ptr相当的是什么?

8
为什么 Rust 不允许使用这种语法:
fn main() {
    let a = String::from("ping");
    let b = a;

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

当我尝试编译这段代码时,我得到了:
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
事实上,我们可以简单地创建一个引用 - 运行时并不相同。
fn main() {
    let a = String::from("ping");
    let b = &a;

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

它有效:

{ping, ping}
根据Rust Book的说明,这是为了避免双重释放错误,因为Rust的变量是按引用而不是按值复制的。Rust将简单地使第一个对象无效并使其无法使用...

enter image description here

我们必须做类似这样的事情:

enter image description here

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

可以使用不同的方法避免双重释放。例如,C++已经有了一个很好的工具来允许多个free调用...shared_ptr只在没有其他指针指向对象时才调用free - 这似乎与我们实际正在做的非常相似,不同之处在于shared_ptr有一个计数器。

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

但Rust是一种年轻的语言;也许他们还没有时间去实现类似的东西?Rust是否计划允许对对象进行第二次引用而不使第一个失效,还是应该习惯于仅使用引用的引用?

2个回答

29

无论是Rc还是Arc都可以替代shared_ptr。你选择哪一个取决于共享数据所需的线程安全级别;Rc用于非线程情况,Arc用于需要线程的情况:

use std::rc::Rc;

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

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

shared_ptr 类似,Rc 也不会复制 String 本身。它只在运行时通过增加引用计数器来实现克隆,并在每个副本超出范围时减少计数器。
shared_ptr 不同的是,RcArc 具有更好的线程语义。 shared_ptr 是半线程安全的shared_ptr 的引用计数器本身是线程安全的,但共享数据并没有 "神奇地" 变得线程安全。
如果您在多线程程序中使用 shared_ptr,仍然需要做更多工作来确保其安全。在非线程化程序中,您支付了一些不必要的线程安全性。
如果您希望允许修改共享值,则还需要切换到运行时借用检查。这由诸如 CellRefCellMutex 等类型提供。对于 StringRcRefCell 是适当的。
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());
}

在编译期间,我们可以计算每个对象的引用次数,并且仅在最后一个引用超出范围时调用free。这几乎就是Rust对引用的处理方式。它并不实际使用计数器,但只有在该值保证保持相同的内存地址时才允许您使用对该值的引用。C++的shared_ptr不会在编译时执行此操作。shared_ptr、Rc和Arc都是运行时结构,维护一个计数器。
是否可能在不使第一引用无效的情况下对对象进行引用?这正是Rust对引用的处理方式,也是你已经做过的事情。
fn main() {
    let a = String::from("ping");
    let b = &a;

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

更好的是,只要a不再有效,编译器就会阻止您使用b
因为Rust的变量是按引用而非按值复制的,所以这不是真的。在赋值时,该值的所有权被转移给了新变量。从语义上讲,变量的内存地址已经改变,因此读取该地址可能会导致内存不安全。
是的,在可能的情况下,使用引用是最惯用的选择。这些需要零运行时开销,并且编译器会告诉您错误,而不是在运行时遇到错误。
当存在循环数据结构时,Rc或Arc通常很有用。如果不能使纯引用工作,则不应感到难过。
这有点不足之处,因为额外的间接性是不幸的。如果您确实需要,可以将其减少。如果您不需要修改字符串,则可以切换到Rc<str>
use std::rc::Rc;

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

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

如果您需要保留修改 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);
}

另请参阅:


7

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

你走在了正确的路上!这就是 Rc 的作用。它是一种智能指针类型,非常类似于 C++ 中的 std::shared_ptr。它只有在最后一个指针实例超出范围后才释放内存:

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);
}

由于您只能获取Rc的不可变访问权限(毕竟它是共享的,而Rust禁止共享可变性),因此您需要内部可变性才能更改其内容,通过CellRefCell实现:

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!"
}

但大多数情况下,您不应该需要使用 Rc(或其线程安全的兄弟 Arc)。Rust 的所有权模型大多允许您通过在一个地方声明 String 实例并在其他所有地方使用引用来避免引用计数的开销,就像您在第二个片段中所做的那样。尝试专注于这一点,并仅在真正必要时使用 Rc,例如当您实现类似图形结构的数据结构时。

谢谢你对clone()的评论 :) - user4718768
是的,那是一种反模式,但这是一个很好的例子,可以看到a仍然可用。 - user4718768

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接