It was very daunting for me initially to see Rc
, Arc
, being used in almost every open source repository I explored. I started reading books and tried to understand the underlying concepts behind Reference counting and Garbage collection, and it took a while for me to grasp the basics. This article is my attempt on explaining Rc
, a reference counted wrapper type in Rust with a few examples I wrote.
Before we dive deep into Rc types, let’s look at a simple rust program that tries to model a common real world scenario — An author can publish ultiple books.
This program tries to create two books , and have a common author for these books. I started writing the structure as if I were writing any other programming language to understand the errors.
struct Author {
name : String
}
impl Author {
fn new (name : String) -> Self {
return Author{name }
}
}
struct Book {
name : String,
author : Author
}
impl Book{
fn new (name : String, author : Author) -> Self{
Book{name , author}
}
}
fn main() {
let author = Author::new("J.K. Rowling".to_owned())
let chamber_of_secrets = Book::new(
"Harry Potter & The Chamber of secrets".to_owned()
, author);
let half_blood_prince = Book::new(
"Harry Potter & The Half Blood Prince".to_owned()
, author);
}
As you may have guessed it, this program doesn’t compile and throws an error. In the example above, we tried to assign the same author to two different books. This means that one of them would take ownership of the author type, and error out all other actions.
error[E0382]: use of moved value: `author`
--> src/main.rs:29:11
|
23 | let author = Author::new("J.K. Rowling".to_owned());
| ------ move occurs because `author` has type `Author`, which does not implement the `Copy` trait
...
26 | , author);
| ------ value moved here
...
29 | , author);
| ^^^^^^ value used here after move
|
note: consider changing this parameter type in method `new` to borrow instead if owning the value isn't necessary
--> src/main.rs:17:37
|
17 | fn new (name : String, author : Author) -> Self{
| --- in this method ^^^^^^ this parameter takes ownership of the value
Both books have the same author, so it makes sense to operate on the same author objects within the books. By default, Rust doesn’t allow multiple owners to an object ( refer ownership rules here. ).A possible solution to this problem would be to use a shared reference. Let’s say we used a shared reference, we would then have to deal with lifetimes. Add lifetimes for the struct because they now hold variable references. This would also mean that the lifetime of all members within the Book struct
is now constrained. It complicates our design and restricts us as our programs and structs grow.
As a solution to the above problem, Rust provides a few primitives that allow us to work with shared ownership semantics — Rc<T>
and Arc<T>
type. For this article, we’ll look at Rc type. Arc
is similar except that the reference counting can happen across multiple threads.
A RC type internally tracks the number of references to an underlying type. The primary goal of this is to keep track of how many owners an object in memory has. By doing so, it allows us to have multiple references to the underlying object, and Rust doesn’t free up the underlying object until no more references remain.
Imagine you live in a smart home where the air conditioning is turned on only when the household members are in the house. As long as someone in the family is in the house, the air conditioning stays on. But nobody is allowed to modify the temperature. As soon as all members leave the house, the air conditioner shuts down. This very similar to how Reference counting works, and is implemented in Rust.
The most common way of declaring an Rc Type is to use the associated function new(T)
.
In our example above, we wanted to share our author. We could just do the following to create an RC type.
let author = Rc::new(Author::new("J.K. Rowling".to_owned())) ;
This creates a new Rc type with the underlying author object. Note that **Rc**
takes ownership of any type we instantiate it.
Every time we need to have a reference to the object, we clone the RC type. This internally just clones the pointer and manages the count of references. This is extremely cheap as compared to cloning structs and large objects. As soon as the number of references to the underlying object become zero, it is dropped. Often, **Rc**
may be combined with **Box**
type to initialize an object on the heap and share it.
Let’s modify the program above to share our Author object to showcase both these concepts.
use std::rc::Rc;
struct Author {
name : String
}
impl Author {
fn new (name : String) -> Self {
return Author{name }
}
}
struct Book {
name : String,
author_ref : Rc<Box<Author>>
}
impl Book{
fn new (name : String, author_ref : Rc<Box<Author>>) -> Self{
Book{name , author_ref}
}
fn print_author(&self){
println!("{}", &self.author_ref.name)
}
}
fn main() {
let author = Box::new(Author::new("J.K. Rowling".to_owned()));
let author_ref = Rc::new(author);
let chamber_of_secrets = Book::new(
"Harry Potter & The Chamber of secrets".to_owned()
, author_ref.clone());
let half_blood_prince = Book::new(
"Harry Potter & The Half Blood Prince".to_owned()
, author_ref.clone());
chamber_of_secrets.print_author() ;
half_blood_prince.print_author();
}
We performed the following changes.
author_ref
Reference counted type that takes ownership of the author.Rc
type to get access to our data.Notice how we have only one author, but we’ve used that object in multiple structs owning an Rc
type wrapper.
The first thing that comes to my mind is whether values inside an Rc
type can be mutated. Below is an example of to attempt and see how can one mutate the author type to update the number of books published.
struct Author {
name : String,
books_published : u32
}
impl Author {
fn new (name : String) -> Self {
return Author{name,books_published:0 }
}
fn published_book(&mut self) {
self.books_published += 1 ;
}
}
We first update our author struct to add a new method which updates the number of books published by 1. Then we try to update the number of books published for the author from our earlier program using mutable references.
fn main() {
let author = Box::new(Author::new("J.K. Rowling".to_owned()));
let mut author_ref = Rc::new(author);
let chamber_of_secrets = Book::new(
"Harry Potter & The Chamber of secrets".to_owned()
, author_ref.clone());
author_ref.published_book();
let half_blood_prince = Book::new(
"Harry Potter & The Half Blood Prince".to_owned()
, author_ref.clone());
chamber_of_secrets.print_author() ;
half_blood_prince.print_author();
}
Our initial thoughts may be that this program will execute as expected. We’ll be disappointed pretty soon. This is because of another property which _Rc_
enforces. Let’s see the error first.
error[E0596]: cannot borrow data in an `Rc` as mutable
--> src/main.rs:41:5
|
41 | author_ref.published_book();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot borrow as mutable
|
= help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `Rc<Box<Author>>`
Like all types in Rust, **Rc**
is also immutable. Creating a mutable reference doesn’t mean we can modify the underlying type**. It is immutable.**
To mutate underlying types, we need to use interior mutability and wrapper types that allow such mutations. We saw such a type [RefCell](https://medium.com/@shanmukhsista/cell-t-and-refcell-t-wrapper-types-in-rust-in-depth-with-examples-6388ac1ce076)
and [Cell](https://medium.com/@shanmukhsista/cell-t-and-refcell-t-wrapper-types-in-rust-in-depth-with-examples-6388ac1ce076)
in one of the previous articles. A common practice may also be using something like Rc<RefCell<Author>>
. This can allow for mutating the values inside these wrapper types. ( Please read this article to understand the usage and risks of RefCell
.
In conclusion, use Rc<T>
type when you need shared access to variables and data in the same thread. Use Arc if you’re planning to share an oject across multiple threads. Rc<T>
doesn’t implement the Send
trait and cannot be shared across threads. You’ll see Rc being used in a lot of real world projects. I hope this article gave a good overview of the constructs and concepts that are behind Reference counted type in Rust. I’ll be writing more on Mutex
and interior mutability using locks in the next set of articles.