Appendix A: Habits and Their Replacements
This appendix is a lookup table for the reflexes you brought with you. Each entry names a habit, says why it was correct where you learned it, and gives the Rust move that replaces it, with the chapter that explains why.
The habits are not mistakes. Every one of them was the right answer to a real constraint in the language it came from, and knowing which constraint it answered is what makes it possible to put down.
From Java and C#
| Habit | Why it worked | In Rust | Chapter |
|---|---|---|---|
Check for null before use | Any reference might be null | The type says whether a value can be absent; handle None where it appears | 7 |
| Extend an abstract base class | Bundled interface, reuse and shared fields | Trait for the interface, default methods for shared behavior, a field for shared data | 9, 11 |
| Copy a collection defensively before passing it | Callee might mutate it | A &T cannot mutate; nothing to defend against | 3 |
| Getters and setters on every field | Fields are the class’s business | Public fields where the value is just data; methods where an invariant needs upholding | 5, 6 |
try/finally to close resources | Exceptions can skip the close | Drop runs on every path, including panic | 1, 17 |
| Declare a variable, assign it later | Fields have default values | Build the value and return it; there is no partly built state | 5 |
| Interface with a single implementing class | Dependency injection wants one | A trait with one implementor is usually not paying; use the concrete type | 9 |
| Throw on invalid input | Exceptions are the error channel | Return Result; panic only for defects in your own code | 8 |
instanceof then cast | Recovering a subtype | match on an enum, which binds the data as it tests | 7 |
A status String or int field | Enums were weak | An enum with data per variant, checked exhaustively | 6, 7 |
| Synchronize on a lock object | Lock and data are separate | Mutex<T> owns the data; access requires locking | 14 |
equals and hashCode by hand | Inherited defaults compare identity | #[derive(PartialEq, Eq, Hash)], or write them when field-by-field is wrong | 5 |
The largest single adjustment is the first one. Null checking is not replaced by more careful null checking; it is replaced by the observation that a nullable reference was two types pretending to be one.
From Python and Ruby
| Habit | Why it worked | In Rust | Chapter |
|---|---|---|---|
| Duck typing | Runtime dispatch on whatever is there | Trait bounds: say which capability you need and the compiler checks | 9 |
| Dictionaries as ad-hoc records | No cost to a dict | A struct, which is faster and names its fields | 5 |
None as a sentinel returned from anything | Dynamic types absorb it | Option<T> in the signature, so callers cannot ignore it | 7 |
| Mutating a list while iterating, carefully | It usually works | iter_mut to change elements, retain to remove, collect-then-append to add | 3, 13 |
try/except around anything that might fail | Exceptions are cheap and pervasive | Result and ?, which propagate without hiding the path | 8 |
| Monkey-patching a library class | Classes are open | An extension trait, which is scoped and checked | 9, 17 |
| Passing dicts or tuples between functions | Structure is implicit | A struct or enum, so that adding a field is checked | 5, 6 |
| Everything is a reference and lives forever | The collector deals with it | Decide what owns each value; most designs are trees | 2 |
The adjustment here is less about safety and more about when you find out. Almost everything on this list moves a discovery from runtime to build time, and the cost is having to be specific earlier than you are used to.
From Go
| Habit | Why it worked | In Rust | Chapter |
|---|---|---|---|
if err != nil after every call | Errors are values, checked by convention | ?, which is the same idea with the boilerplate removed and the check enforced | 8 |
interface{} or generics with any | Escape hatch for heterogeneous data | An enum when the set is closed, Box<dyn Trait> when it is open | 7, 10 |
| A struct with an embedded struct | Go’s promotion gives delegation free | An explicit field and delegating methods; there is no promotion | 11 |
| Return a zero value and a bool | No sum types | Option<T> or Result<T, E> | 7, 8 |
| Share memory by communicating | Channels are the idiomatic primitive | Channels work the same way, and moving a value into one really does hand it over | 14 |
defer for cleanup | Cleanup is attached to the function | Drop attaches it to the value instead, so it cannot be forgotten at a new return | 1, 17 |
| Nil pointer receivers as an option | Zero values are useful | Option<T>, or a type that cannot be constructed invalid | 6 |
| Small interfaces | Go’s culture already says this | Keep it: small traits compose, large ones force stub implementations | 9 |
Go is the closest starting point for the error handling and concurrency chapters, and the furthest for anything involving ownership, since Go’s collector makes the same design question optional.
From C and C++
| Habit | Why it worked | In Rust | Chapter |
|---|---|---|---|
Manual free, or a comment saying who owns it | Nothing else tracked it | Ownership is in the type; destruction is scheduled by scope | 2 |
const& parameters to avoid copies | Copies could be deep and expensive | Moves are cheap; pass by value freely, borrow when you need the caller to keep it | 2 |
| Raw pointers for optional values | No standard optional until C++17 | Option<T>, which is the same size as a pointer for reference types | 7 |
| Rule of three, five, zero | Copy and move semantics are yours to define | Clone when duplication means something, Drop when destruction does, and nothing otherwise | 5 |
| Header and implementation split | The compilation model demands it | Modules and pub; there is no header | 15 |
| Templates for compile-time polymorphism | Zero-cost abstraction | Generics, with bounds checked at definition rather than at instantiation | 10 |
std::shared_ptr for shared ownership | Reference counting is the general answer | Rc or Arc, deliberately, after considering single ownership | 12 |
| Iterator invalidation rules memorized | The standard documents them per container | The compiler enforces them; the rules do not need memorizing | 3 |
C++ programmers usually find the model familiar and the enforcement unfamiliar. The most useful adjustment is the second row: the instinct to avoid copies is imported from a language where a copy could be arbitrarily expensive, and it produces borrowed designs in Rust that would have been simpler owned.
The habits with no replacement
Three things you may be looking for do not exist, and looking for a substitute wastes more time than accepting the absence.
There is no inheritance. Not a hidden version, not a crate that provides it. Composition and traits cover what it was doing, and chapter 11 works through the translation.
There is no reflection. No runtime type inspection, no dynamic class loading,
no annotation scanning. What frameworks in other languages do with reflection,
Rust does with derive macros at compile time — which is why serde needs
#[derive(Serialize)] on your type rather than discovering its fields at
runtime.
There are no exceptions. panic! is not one, catch_unwind is not a
substitute, and building control flow on either is a way of writing a different
language badly.