Design Trade-offs
This section is about design trade-offs in Rust. To be an effective Rust engineer, it’s not enough just to know how Rust works. You have to decide which of Rust’s many tools are appropriate for a given job. In this section, we will give you a sequence of quizzes about your understanding of design trade-offs in Rust. After each quiz, we will explain in-depth our rationale for each question.
Here’s an example of what a question will look like. It will start out by describing a software case study with a space of designs:
Context: You are designing an application with a global configuration, e.g. containing command-line flags.
Functionality: The application needs to pass immutable references to this configuration throughout the application.
Designs: Below are several proposed designs to implement the functionality.
use std::rc::Rc; use std::sync::Arc; struct Config { flags: Flags, // .. more fields .. } // Option 1: use a reference struct ConfigRef<'a>(&'a Config); // Option 2: use a reference-counted pointer struct ConfigRef(Rc<Config>); // Option 3: use an atomic reference-counted pointer struct ConfigRef(Arc<Config>);
Given just the context and key functionality, all three designs are potential candidates. We need more information about the system goals to decide which ones make the most sense. Hence, we give a new requirement:
Select each design option that satisfies the following requirement:
Requirement: The configuration reference must be shareable between multiple threads.
Answer:
Option 1
Option 2
Option 3
In formal terms, this means that ConfigRef
implements Send
and Sync
.
Assuming Config: Send + Sync
, then both &Config
and Arc<Config>
satisfy this requirement,
but Rc
does not (because non-atomic reference-counted pointers are not thread-safe). So Option 2 does not satisfy the requirement, while Option 3 does.
We might also be tempted to conclude that Option 1 does not satisfy the requirement because functions like thread::spawn
require that all data moved into a thread can only contain references with a 'static
lifetime. However, that does not rule out Option 1 for two reasons:
- The
Config
could be stored as a global static variable (e.g., usingOnceLock
), so one could construct&'static Config
references. - Not all concurrency mechanisms require
'static
lifetimes, such asthread::scope
.
Therefore the requirement as-stated only rules out non-Send
types, and we consider Options 1 and 3 to be the correct answers.
Now you try with the questions below! Each section contains a quiz focused on a single scenario. Complete the quiz, and make sure to read the answer context after each quiz. These questions are both experimental and opinionated — please leave us feedback via the bug button 🐞 if you disagree with our answers.
Along with each quiz, we have also provided links to popular Rust crates that served as inspiration for the quiz.
References
Inspiration: Bevy assets, Petgraph node indices, Cargo units
Trait Trees
Inspiration: Yew components, Druid widgets
Dispatch
Inspiration: Bevy systems, Diesel queries, Axum handlers