Editor’s note: This Rust guide was updated on 3 Aug, 2022 to include information about doubly linked lists and borrowing things that aren’t static
in async code.
As a moderator of the Rust subreddit, I regularly happen upon posts about developers’ attempts to transpose their respective language paradigms to Rust, with mixed results and varying degrees of success.
In this guide, I’ll describe some of the issues developers encounter when transposing other language paradigms to Rust and propose some alternative solutions to help you work around Rust’s limitations. Let’s get started!
static
in an async fn
Arguably the most asked about missing feature coming from object-oriented (OO) languages is inheritance. Why wouldn’t Rust let a struct inherit from another? Well, Rust inheritance is interesting.
You could surely argue that even in the OO world, inheritance has a bad reputation and practitioners usually favor composition if they can. But you could also argue that allowing a type to perform a method differently might improve performance and is thus desirable for those specific instances.
Here’s a classic example taken from Java:
interface Animal { void tell(); void pet(); void feed(Food food); } class Cat implements Animal { public void tell() { System.out.println("Meow"); } public void pet() { System.out.println("purr"); } public void feed(Food food) { System.out.println("lick"); } } // this implementation is probably too optimistic... class Lion extends Cat { public void tell() { System.out.println("Roar"); } }
The first part can be implemented with traits:
trait Animal { fn tell(&self); fn pet(&mut self); fn feed(&mut self, food: Food); } struct Cat; impl Animal for Cat { fn tell(&self) { println!("Meow"); } fn pet(&mut self) { println!("purr"); fn feed(&mut self, food: Food) { println!("lick"); } }
But the second part is not so easy:
struct Lion; impl Animal for Lion { fn tell(&self) { println!("Roar"); } // Error: Missing methods pet and feed }
The simplest way is, obviously, to duplicate the methods. Yes, duplication is bad. So is complexity. Create a free method and call that from the Cat
and Lion
impl
if you need to deduplicate the code.
But wait, what about the polymorphism part of the equation? That’s where it gets complicated. Where OO languages usually give you dynamic dispatch, Rust makes you choose between static and dynamic dispatch, and both have their costs and benefits.
// static dispatch let cat = Cat; cat.tell(); let lion = Lion; lion.tell(); // dynamic dispatch via enum enum AnyAnimal { Cat(Cat), Lion(Lion), } // `impl Animal for AnyAnimal` left as an exercise for the reader let animals = [AnyAnimal::Cat(cat), AnyAnimal::Lion(lion)]; for animal in animals.iter() { animal.tell(); } // dynamic dispatch via "fat" pointer including vtable let animals = [&cat as &dyn Animal, &lion as &dyn Animal]; for animal in animals.iter() { animal.tell(); }
Unlike in garbage collected languages, each variable has to have a single concrete type at compile time. Also, for the enum case, delegating the implementation of the trait is tedious, but crates such as ambassador
can help.
A rather hacky way to delegate functions to a member is by using the Deref
trait for polymorphism. Functions defined on the deref
target can be called on the derefee directly. Note, however, that this is often considered an antipattern.
Finally, it’s possible to implement a trait for all classes that implement one of a number of other traits. It requires specialization, which is a nightly feature for now (though there is a workaround available, even packed in a macro crate if you don’t want to write out all the boilerplate required). Traits may very well inherit from each other, though they only prescribe behavior, not data.
Many folks coming from C++ to Rust will at first want to implement a “simple” doubly linked list. They quickly learn that it’s actually far from simple.
That’s because Rust wants to be clear about ownership, and thus doubly linked lists require quite complex handling of pointers vs. references.
A newcomer might try to write the following structure:
struct MyLinkedList { value: T previous_node: Option<Box<T>>, next_node: Option<Box<T>>, }
Well, they’ll add the Option
and Box
when they note that this otherwise fails. But once they try to implement insertion, they’re in for an unpleasant surprise:
impl MyLinkedList { fn insert(&mut self, value: T) { let next_node = self.next_node.take(); self.next_node = Some(Box::new(MyLinkedList { value, previous_node: Some(Box::new(*self)), // Ouch next_node, })); } }
Of course, the borrow checker won’t allow this. The ownership of values is completely muddled. Box
owns the data it contains, and thus each node in the list would be owned by the previous and next node in the list. Rust only ever allows one owner per data, so this will at least require a Rc
or Arc
to work. But even this becomes cumbersome quickly, not to mention the overhead from reference counts.
Luckily, you don’t have to write a doubly linked list because the standard library already contains one (std::collections::LinkedList
). Also, it is quite rare that this will give you good performance compared to simple Vec
s, so you may want to measure accordingly.
If you really want to write a doubly linked list, you can refer to “Learning Rust With Entirely Too Many Linked Lists”, which may help you both write linked lists and learn a lot about unsafe Rust in the process.
N.B., Singly-linked lists are absolutely fine to build out of a chain of boxes. In fact, the Rust compiler contains an implementation. Note that linked lists still are often bad for performance.
The same mostly applies to graph structures, although you’ll likely need a dependency for handling graph data structures. petgraph
is the most popular at the moment, providing both the data structure and a number of graph algorithms. If you don’t want to incur another dependency, using a Vec
or arena for allocating the nodes and using indices instead of references is always a workable approach.
When faced with the concept of self-referencing types, it’s fair to ask, “Who owns this?” Again, this is a wrinkle in the ownership story that the borrow checker isn’t usually happy with.
You’ll encounter this problem when you have an ownership relation and want to store both the owning and owned object within one struct. Try this naïvely and you’ll have a bad time trying to get the lifetimes to work.
We can only guess that many Rustaceans have turned to unsafe code, which is subtle and really easy to get wrong. Of course, using a plain pointer instead of a reference will remove your lifetime worries, as pointers carry no lifetime. However, this is taking up the responsibility of managing the lifetime manually.
Luckily there are some crates that take the solution and present a safe interface, such as the ouroboros
, self_cell
, and one_self_cell
crates.
static
in an async fn
Classic Rust leans a lot on borrowing stuff. It’s cheaper than passing around by value and you can share with multiple parties within the code. However, once you go async and expect everything working the same, you’re in for a bad time.
See, each borrow has an associated lifetime, and our famed and feared borrowck
will tell you off if you try to borrow something longer than it’s alive. On the other hand, with async code, you’re producing state machines that, by design, need to contain all the data they use during their execution and live and die at the behest of the async runtime. So when that data contains a borrow, the borrowed value must live until the end of the program (which implies the static
lifetime).
When async Rust was introduced, many of us tried to just slap an async
here and an .await
there, only to find out that the borrow checker was having none of it. So we either .clone
or reach for Arc
s that handle the lifetime of their contents at runtime. Some things we want to share between tasks we just put in a static
OnceCell
.
Similarly, async code is meant to be able to span multiple threads, so data contained in an async task must be Send
because the runtime might push our task to another thread at will. Again, Rustaceans will either change their data to be Send
or wrap it in a Mutex
(they’ll now have to make sure they don’t hold it across an await
lest they cause deadlocks, an easy mistake to make. Luckily there’s a clippy lint against that).
People coming from C and/or C++ — or, less often, from dynamic languages — are sometimes accustomed to creating and modifying global state throughout their code. For example, one Redditor ranted that “It’s completely safe and yet Rust doesn’t let you do it.”
Here is a slightly simplified example:
#include int i = 1; int main() { std::cout << i; i = 2; std::cout << i; }
In Rust, that would translate roughly to:
static I: u32 = 1; fn main() { print!("{}", I); I = 2; // <- Error: Cannot mutate global state print!("{}", I); }
Many Rustaceans will tell you that you just don’t need that state to be global. Of course, in such a simple example, this is true. But for a good number of use cases, you really do need global mutable state — e.g., in some embedded applications.
There is, of course a way to do it, using unsafe
. But before you reach for that, depending on your use case, you may just want to use a Mutex
instead to be really sure. Or, if the mutation is only needed once for initialization, a OnceCell
or lazy_static
will solve the problem neatly. Or if you really only need integers, the std::sync::Atomic*
types have you covered.
With that said, especially in the embedded world where every byte counts and resources are often mapped into memory, having a mutable static
is often the preferred solution. So if you really must do it and can ensure that no race conditions can cause undefined behavior (in general this would either involve a lock or ensuring that the code is single-threaded), it would look like this:
static mut DATA_RACE_COUNTER: u32 = 1; fn main() { print!("{}", DATA_RACE_COUNTER); // I solemny swear that I'm up to no good, and also single threaded. unsafe { DATA_RACE_COUNTER = 2; } print!("{}", DATA_RACE_COUNTER); }
Again, you shouldn’t do this unless you really need to. And if you need to ask whether it’s a good idea, the answer is no.
A neophyte may be tempted to declare an array as follows:
let array: [usize; 512]; for i in 0..512 { array[i] = i; }
This fails because the array was never initialized. We then try to assign values into it, but without telling the compiler, it won’t even reserve a place for us to write on the stack. Rust is picky like that; it distinguishes the array from its contents. Furthermore, it requires both to be initialized before we can read them.
By initializing let array = [0usize; 512];
, we solve this problem at the cost of a double initialization, which may or may not get optimized out — or, depending on the type, may even be impossible. See “Unsafe Rust: How and when (not) to use it” for a solution.
Since Rust 1.63.0, we also have the std::array::from_fn
function that can be used to completely safely initialize arrays:
// the closure `|i| i` will be called for each index from 0..512 // and can do arbitrary computation let array: [usize; 512] = std::array::from_fn(|i| i);
And this concludes our tour of things that you cannot (easily) do in Rust.
In this article, we talked about inheritance in Rust, doubly linked lists, self-referencing types, borrowing things that aren’t static
in an async fn
, global mutable state, and just initializing an array.
While there are surely other things that Rust makes hard, listing them all would take up too much time for both me and you, dear reader.
However, for all the hassle Rust also gives us a number of powerful abstractions that can be and have been used to get us out of the problems we encounter.
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust application. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Modernize how you debug your Rust apps — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
2 Replies to "Understanding inheritance and other limitations in Rust"
“You could surely argue that even in the OO world, inheritance has a bad reputation and practitioners usually favor composition if they can.”
I’ve been programming with OO for more than 25 years and I’ve never heard it had bad reputation, or that one should favor composition (which doesn’t offer the benefits of inheritance).
When programming UI there is simply no alternative, and the lack of inheritance in Rust excludes it from any form of efficient UI framework. The default implementation on undefined types that you mention is not inheritance:
– it cannot act on the object since it’s generic
– a custom implementation can replace the default one, but not call it
– there is only one level (replacing the default implementation)
In general like any other feature, inheritance shouldn’t be abused, maybe that’s what you’re referring to, though without an example it is hard to tell for sure.
You wrote, about code duplication due to the lack of inheritance: “The simplest way is, obviously, to duplicate the methods. Yes, duplication is bad. So is complexity. Create a free method and call that from the Cat and Lion impl if you need to deduplicate the code.”
But how do you manage to access the struct’s (or worse, enum’s) fields from a free function, for several different types? Polymorphism doesn’t stretch that far, so it doesn’t look like this is possible and that’s the whole point about reusing methods with inheritance.
That’s why I generally avoid using fake code with animals to talk about OOP, they don’t reflect the actual complexity and the interdependencies of the actual code.
Currently one option is to use macros, like delegate for instance, but macros are not standard, harder to read and even harder to use with refactoring. That could be an acceptable temporary work-around if the macro was part of the core library, but it isn’t, which makes it a hack at best.