Why the "Garbage Collector" in Rust is Not What You Think
\n\nIf you come from the world of Python, Java, or C#, the term "garbage collector" evokes a clear image: a background environment periodically finds and removes objects that are no longer referenced. In Rust, it's different. There is no classical GC here, but there is a system of ownership and borrowing that solves the same problem — managing object lifetimes — at compile time.
\n\nHowever, life (and code) is more complex than ideal schemes. Sometimes you need to share ownership of one object between multiple parts of a program, create cyclic references, or work with mutable data through immutable pointers. This is where garbage-collected objects in Rust come onto the scene: Rc, Arc, Weak, and RefCell. Let's figure out how to manage their lifetimes, avoid memory leaks, and not break our heads.
This article is a detailed guide for those who want to understand how managing object lifetimes works in Rust in practice. We won't just list the types, but will show real scenarios, including a popular Stack Overflow question (yes, that one with thousands of views).
\n\nKey Concepts: Ownership, Borrowing, and Reference Counting
\n\nBefore diving deep, let's recall the basics:
\n\n- \n
- Ownership — each value has exactly one owner. When the owner goes out of scope, the value is dropped. \n
- Borrowing — you can temporarily "borrow" a reference to a value without becoming the owner. Restrictions: either one mutable reference or any number of immutable references. \n
- Reference Counting — this is a mechanism that allows having multiple owners of one value. In Rust, this is implemented via
Rc(for single-threaded programs) andArc(for multi-threaded programs). \n
When we talk about "garbage-collected objects" in the context of Rust, we mean precisely objects managed by a reference counter. They are not dropped immediately after going out of scope, but wait until the counter drops to zero. This is lifetime management — you control when memory is freed.
\n\nRc and Arc: The Foundation of Lifetime Management
\n\nRc (Reference Counted) is a smart pointer that allows sharing ownership of one value between multiple parts of code in a single thread. Each time you clone an Rc, the counter increases. When a clone goes out of scope, it decreases. When the counter reaches zero, the value is dropped.
Example of simple Rc usage:
use std::rc::Rc;\n\nfn main() {\n let data = Rc::new(vec![1, 2, 3]);\n let a = Rc::clone(&data);\n let b = Rc::clone(&data);\n\n println!("Reference count: {}", Rc::strong_count(&data)); // 3\n\n drop(a);\n println!("After dropping a: {}", Rc::strong_count(&data)); // 2\n}\n\n\nHere, data, a, and b are three owners of one vector. As long as at least one of them is alive, the vector remains in memory. This is ideal for situations where you cannot determine which module will finish last.
Arc (Atomic Reference Counted) works the same way but uses atomic operations, making it safe for multi-threaded programs. The cost is a slight performance decrease.
use std::sync::Arc;\nuse std::thread;\n\nfn main() {\n let data = Arc::new(vec![1, 2, 3]);\n let mut handles = vec![];\n\n for _ in 0..3 {\n let data_clone = Arc::clone(&data);\n handles.push(thread::spawn(move || {\n println!("{:?}", data_clone);\n }));\n }\n\n for handle in handles {\n handle.join().unwrap();\n }\n}\n\n\n