Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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#

HabitWhy it workedIn RustChapter
Check for null before useAny reference might be nullThe type says whether a value can be absent; handle None where it appears7
Extend an abstract base classBundled interface, reuse and shared fieldsTrait for the interface, default methods for shared behavior, a field for shared data9, 11
Copy a collection defensively before passing itCallee might mutate itA &T cannot mutate; nothing to defend against3
Getters and setters on every fieldFields are the class’s businessPublic fields where the value is just data; methods where an invariant needs upholding5, 6
try/finally to close resourcesExceptions can skip the closeDrop runs on every path, including panic1, 17
Declare a variable, assign it laterFields have default valuesBuild the value and return it; there is no partly built state5
Interface with a single implementing classDependency injection wants oneA trait with one implementor is usually not paying; use the concrete type9
Throw on invalid inputExceptions are the error channelReturn Result; panic only for defects in your own code8
instanceof then castRecovering a subtypematch on an enum, which binds the data as it tests7
A status String or int fieldEnums were weakAn enum with data per variant, checked exhaustively6, 7
Synchronize on a lock objectLock and data are separateMutex<T> owns the data; access requires locking14
equals and hashCode by handInherited defaults compare identity#[derive(PartialEq, Eq, Hash)], or write them when field-by-field is wrong5

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

HabitWhy it workedIn RustChapter
Duck typingRuntime dispatch on whatever is thereTrait bounds: say which capability you need and the compiler checks9
Dictionaries as ad-hoc recordsNo cost to a dictA struct, which is faster and names its fields5
None as a sentinel returned from anythingDynamic types absorb itOption<T> in the signature, so callers cannot ignore it7
Mutating a list while iterating, carefullyIt usually worksiter_mut to change elements, retain to remove, collect-then-append to add3, 13
try/except around anything that might failExceptions are cheap and pervasiveResult and ?, which propagate without hiding the path8
Monkey-patching a library classClasses are openAn extension trait, which is scoped and checked9, 17
Passing dicts or tuples between functionsStructure is implicitA struct or enum, so that adding a field is checked5, 6
Everything is a reference and lives foreverThe collector deals with itDecide what owns each value; most designs are trees2

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

HabitWhy it workedIn RustChapter
if err != nil after every callErrors are values, checked by convention?, which is the same idea with the boilerplate removed and the check enforced8
interface{} or generics with anyEscape hatch for heterogeneous dataAn enum when the set is closed, Box<dyn Trait> when it is open7, 10
A struct with an embedded structGo’s promotion gives delegation freeAn explicit field and delegating methods; there is no promotion11
Return a zero value and a boolNo sum typesOption<T> or Result<T, E>7, 8
Share memory by communicatingChannels are the idiomatic primitiveChannels work the same way, and moving a value into one really does hand it over14
defer for cleanupCleanup is attached to the functionDrop attaches it to the value instead, so it cannot be forgotten at a new return1, 17
Nil pointer receivers as an optionZero values are usefulOption<T>, or a type that cannot be constructed invalid6
Small interfacesGo’s culture already says thisKeep it: small traits compose, large ones force stub implementations9

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++

HabitWhy it workedIn RustChapter
Manual free, or a comment saying who owns itNothing else tracked itOwnership is in the type; destruction is scheduled by scope2
const& parameters to avoid copiesCopies could be deep and expensiveMoves are cheap; pass by value freely, borrow when you need the caller to keep it2
Raw pointers for optional valuesNo standard optional until C++17Option<T>, which is the same size as a pointer for reference types7
Rule of three, five, zeroCopy and move semantics are yours to defineClone when duplication means something, Drop when destruction does, and nothing otherwise5
Header and implementation splitThe compilation model demands itModules and pub; there is no header15
Templates for compile-time polymorphismZero-cost abstractionGenerics, with bounds checked at definition rather than at instantiation10
std::shared_ptr for shared ownershipReference counting is the general answerRc or Arc, deliberately, after considering single ownership12
Iterator invalidation rules memorizedThe standard documents them per containerThe compiler enforces them; the rules do not need memorizing3

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.