Chapter 3: Compound data types
The introduction explains why Rust’s structs and enums are useful for composing simpler values into compound data types.
After this chapter you will be able to
- Compound types as whole entities. Explain that structs and enums are compound data types that combine other types into a more useful whole, as a 2D point combines x and y coordinates.
- Plain Function File Sketch. Describe how the initial file API uses a String alias to stand in for a file, and explain why its plain functions are only a starting point.
- File struct and fields. Explain how the File struct represents a file using a name and zero or more bytes of data.
- Distinct types with newtype. Distinguish a type alias from the newtype pattern and explain the tradeoff illustrated by network hostnames.
- Methods and dot-call syntax. Explain how a method call makes its associated object implicit, contrasting f.read(buffer) with a function call that passes the object explicitly.
- Separate impl method definitions. Describe where Rust methods are defined relative to a struct or enum definition.
- Conventional new method for structs. Explain how a new() method simplifies creating a struct with reasonable defaults, and distinguish that convention from special language syntax.
- File operation error reporting. Explain why file operations need a way to report errors beyond a boolean success or failure, using examples from the section.
- Global error code convention. Explain how a caller can detect an error by checking a global variable after an operation, using the close() and errno example.
- Global error handling tradeoffs. Justify why global error codes are useful to recognize in systems programming but generally discouraged as an error-handling style in Rust.
- Result success and error cases. Explain how Result represents both the normal and error outcomes of a function, using Result<File, String> as the example.
- Unwrap and explicit failure. Describe what unwrap() does to each Result case and why the section cautions against relying on it for error handling.
- Known enum variants. Explain that an enum is a type whose value can be one of several known variants, such as the suits in a deck of cards.
- Event codes as enums. Justify using enum variants rather than strings for event codes such as UPDATE and DELETE. Explain that making the cases known to the compiler can help it warn when a case has been forgotten.
- File state as an enum. Explain that the File type can use an enum to represent whether a file is open or closed, allowing its represented state to change.
- Storage-independent file behavior. Explain why a definition of a file can focus on reading and writing streams of bytes rather than where those operations take place.
- Traits as behavior contracts. Describe a trait as rules a type must implement to claim a behavior, and identify #[derive(Debug)] as an example of implementing a trait.
- Shared behavior through traits. Explain that a trait signals that multiple types perform the same task, allowing code reuse; a Read trait can be implemented by multiple types.
- Trait definitions and implementations. Distinguish the trait keyword, which defines a trait, from impl, which attaches a trait to a particular type.
- Custom Display implementation. Describe how a type opts into Display using impl Trait for Type and a fmt method returning fmt::Result. Explain that fields formatted through Display also need Display implementations.
- Default privacy and public items. Explain why importing a crate containing only the code shown so far would provide no extra benefit, and how `pub` changes what the library makes available.
- Rust doc comment targets. Distinguish `///` from `//!` by explaining which item each documents and the usual convention for `//!`.
- Single-file rustdoc workflow. Explain that rustdoc turns inline documentation into HTML rather than executable code, and describe how to run it on a source file and find the generated page.
- Crate documentation workflow. Describe how to put the example source in a Cargo crate and render its HTML documentation from the project root with `cargo doc --open`.
What it builds on
- Defining functions (chapter 2) · The reader has already encountered defining functions.
- Loop: The basis for Rust’s looping constructs (chapter 2) · The reader has encountered loop as a looping construct.
- Vectors (chapter 2) · Uses vectors as growable lists of values.
- Using references (chapter 2) · Introduces using references.
- Match: Type-aware pattern matching (chapter 2) · Introduces Rust's type-aware pattern matching.
Section by section
Introduction
The introduction explains why Rust’s structs and enums are useful for composing simpler values into compound data types.
- Compound types as whole entities: Explain that structs and enums are compound data types that combine other types into a more useful whole, as a 2D point combines x and y coordinates.
Why it matters: Thinking of related values as one entity explains the purpose of the compound types introduced in this chapter.
Key terms: struct, enum, compound data types
3.1 Using plain functions to experiment with an API
The section sketches a file API with plain functions, identifies what that sketch cannot yet represent, and explains Rust’s unit and Never return types.
- Plain Function File Sketch: Describe how the initial file API uses a String alias to stand in for a file, and explain why its plain functions are only a starting point.
Why it matters: The sketch shows which needs motivate richer file types and APIs. - Unit Return Type: Explain what () means as a function or expression result, including the effect of a trailing semicolon.
Why it matters: Recognizing () helps make sense of return-type errors caused by an unintended semicolon. - Never Return Type: Explain what the ! return type says about a function, using the section’s examples.
Why it matters: Recognizing ! helps distinguish a function that returns no value from one that never returns at all.
Key terms: type alias, String, read(), bool, methods, unit type, (), zero-length tuple, semicolon, Never type
3.2 Modeling files with struct
The section uses a struct to model an in-memory file, shows how a newtype creates a distinct type, and identifies what the mock file still cannot do.
- File struct and fields: Explain how the File struct represents a file using a name and zero or more bytes of data.
Why it matters: This distinction separates modeling a file in memory from performing file I/O. - Struct instance and field access: Describe how the example creates a File instance and accesses its fields by reference.
Why it matters: It connects the File type definition to the values the example actually uses. - Distinct types with newtype: Distinguish a type alias from the newtype pattern and explain the tradeoff illustrated by network hostnames.
Why it matters: Distinct types help prevent values such as hostnames from being mistaken for ordinary strings. - Mock file reading limits: Describe what the revised File example simulates and which shortcomings remain.
Why it matters: It clarifies what this mock file demonstrates without mistaking it for finished file handling.
Key terms: struct, File, field, Vec<u8>, instance, literal syntax, field access, reference, borrowing, type
3.3 Adding methods to a struct with impl
The section introduces Rust methods, their dot-call syntax, and the separate impl blocks used to define them.
- Methods and dot-call syntax: Explain how a method call makes its associated object implicit, contrasting f.read(buffer) with a function call that passes the object explicitly.
Why it matters: This lets you recognize why a Rust method call looks different from an ordinary function call. - Separate impl method definitions: Describe where Rust methods are defined relative to a struct or enum definition.
Why it matters: It helps you find a type’s method definitions without expecting them inside its data definition. - Structs versus classes: Distinguish Rust structs from classes in languages that support inheritance.
Why it matters: This prevents familiar class terminology from creating the wrong expectation about Rust types.
Key terms: methods, functions, dot operator, impl block, struct, enum, class keyword, inheritance
3.3.1 Simplifying object creation by implementing new()
This subsection explains why Rust programmers commonly use a new() method to create structs with reasonable defaults instead of repeating struct literals.
- Conventional new method for structs: Explain how a new() method simplifies creating a struct with reasonable defaults, and distinguish that convention from special language syntax.
Why it matters: It explains why a struct may offer a convenient creation method even though Rust already supports struct literals.
Key terms: new(), struct, literal syntax, impl block
3.4 Returning errors
This section explains why file operations need to signal failures more informatively than a boolean result can.
- File operation error reporting: Explain why file operations need a way to report errors beyond a boolean success or failure, using examples from the section.
Why it matters: A useful error report can tell the caller what went wrong, not just that an operation failed.
Key terms: read(), open(), close(), bool, error message
3.4.1 Modifying a known global variable
This section explains global error codes as a systems-programming convention, why they are risky in Rust, and how const differs from an immutable let binding.
- Global error code convention: Explain how a caller can detect an error by checking a global variable after an operation, using the close() and errno example.
Why it matters: Recognizing this convention helps you understand error signals from operating-system interfaces. - Global error handling tradeoffs: Justify why global error codes are useful to recognize in systems programming but generally discouraged as an error-handling style in Rust.
Why it matters: It explains both why you may encounter this pattern and why you should handle it carefully. - Const and let distinction: Distinguish const from an immutable let binding using the section’s example of interior mutability and its explanation of aliasing.
Why it matters: It prevents treating an immutable binding as a guarantee that nothing associated with its data can change.
Key terms: errno, close(), EINTR, EIO, global variable, type safety, OS-defined global values, flags, const, let
3.4.2 Making use of the Result return type
This section shows how Result represents success or failure in file operations, what unwrap() does with each case, and why the example rebinds a File value after passing ownership.
- Result success and error cases: Explain how Result represents both the normal and error outcomes of a function, using Result<File, String> as the example.
Why it matters: It lets a function's return type represent failure as well as success. - Unwrap and explicit failure: Describe what unwrap() does to each Result case and why the section cautions against relying on it for error handling.
Why it matters: Extracting a successful value is not the same as handling an error helpfully. - File ownership and rebinding: Trace why the example rebinds f4 after calls to open() and close().
Why it matters: The rebinding keeps the example from trying to use a value after passing it to a function.
Key terms: Result, Ok, Err, Result<File, String>, unwrap(), ownership, File, f4, rebound
3.5 Defining and making use of an enum
Introduces Rust enums as types with known variants and explains why they are useful for representing event codes instead of strings.
- Known enum variants: Explain that an enum is a type whose value can be one of several known variants, such as the suits in a deck of cards.
Why it matters: This is the basic idea behind the enum examples that follow. - Event codes as enums: Justify using enum variants rather than strings for event codes such as UPDATE and DELETE. Explain that making the cases known to the compiler can help it warn when a case has been forgotten.
Why it matters: Named cases make event-handling code easier to check than strings alone. - Additional enum capabilities: State that enum variants can contain data and that enums, like structs, can have methods defined through impl blocks.
Why it matters: These capabilities explain why enums can model more than a list of names.
Key terms: enum, enumeration, variants, event codes, string comparisons, pattern-matching, impl blocks, methods
3.5.1 Using an enum to manage internal state
The section applies enums to a file’s open-or-closed state and recommends them over stringly-typed codes when modeling such distinctions.
- File state as an enum: Explain that the File type can use an enum to represent whether a file is open or closed, allowing its represented state to change.
Why it matters: It gives the file’s changing state an explicit representation. - Enums instead of string codes: Explain why the section recommends considering enums when data is represented by strings such as message codes.
Why it matters: It offers a cue for when an enum may be a better modeling choice.
Key terms: enum, File type, stringly-typed, message codes
3.6 Defining common behavior with traits
The section introduces traits as rules for shared behavior, using medium-independent file operations and the familiar Debug trait as examples.
- Storage-independent file behavior: Explain why a definition of a file can focus on reading and writing streams of bytes rather than where those operations take place.
Why it matters: Thinking in terms of capabilities makes it possible to describe common behavior across different kinds of files. - Traits as behavior contracts: Describe a trait as rules a type must implement to claim a behavior, and identify #[derive(Debug)] as an example of implementing a trait.
Why it matters: This connects the new term trait to both the file example and Rust syntax the reader has already used.
Key terms: file, reading, writing, streams of bytes, trait, Debug trait, #[derive(Debug)]
3.6.1 Creating a Read trait
This subsection explains how traits express shared behavior across types, distinguishes defining a trait from implementing it, and uses PartialEq as an example.
- Shared behavior through traits: Explain that a trait signals that multiple types perform the same task, allowing code reuse; a Read trait can be implemented by multiple types.
Why it matters: A trait lets you recognize common behavior even when different types provide it. - Trait definitions and implementations: Distinguish the trait keyword, which defines a trait, from impl, which attaches a trait to a particular type.
Why it matters: This distinction helps you read code that separates a shared behavior from the types providing it. - PartialEq comparison behavior: Describe how PartialEq enables == comparisons while allowing cases in which seemingly matching values are not treated as equal, such as NaN or SQL NULL.
Why it matters: PartialEq is a concrete example of how learning a trait’s meaning helps you recognize its behavior across types.
Key terms: trait, Read trait, code reuse, impl, PartialEq, ==, NAN, NULL
3.6.2 Implementing std::fmt::Display for your own types
This section explains how Rust’s formatting macros use traits and how to give a custom type its own printed representation by implementing Display.
- Formatting macros and traits: Explain that println!, print!, write!, writeln!, and format! use the Display and Debug traits to turn values into formatted text, with trait implementations supplying the behavior.
Why it matters: It connects printed output to behavior that a type provides. - Custom Display implementation: Describe how a type opts into Display using impl Trait for Type and a fmt method returning fmt::Result. Explain that fields formatted through Display also need Display implementations.
Why it matters: It lets you give your own types a deliberate, readable representation when formatting them.
Key terms: println!, Display, Debug, fmt, fmt::Result, impl Trait for Type
3.7 Exposing your types to the world
Introduces the value of hiding internal crate details and documenting public ones, then previews tooling covered later in the section.
3.7.1 Protecting private data
This subsection explains that Rust keeps items private by default and uses `pub` to expose them from a library.
- Default privacy and public items: Explain why importing a crate containing only the code shown so far would provide no extra benefit, and how `pub` changes what the library makes available.
Why it matters: A library needs public items for other code to use what it provides.
Key terms: private, pub, public, crate
3.8 Creating inline documentation for your projects
The section introduces inline documentation and explains how Rust’s two doc-comment forms attach documentation to different items.
- Rust doc comment targets: Distinguish `///` from `//!` by explaining which item each documents and the usual convention for `//!`.
Why it matters: The distinction lets you tell what a doc comment is documenting when you read Rust code.
Key terms: doc comments, ///, //!, module
3.8.1 Using rustdoc to render docs for a single source file
This subsection shows how to generate HTML documentation from a single Rust source file with rustdoc.
- Single-file rustdoc workflow: Explain that rustdoc turns inline documentation into HTML rather than executable code, and describe how to run it on a source file and find the generated page.
Why it matters: You can generate and open documentation for a source file without building an executable.
Key terms: rustdoc, inline documentation, HTML, doc/
3.8.2 Using cargo to render docs for a crate and its dependencies
This subsection shows how to render a crate’s documentation as HTML with Cargo and notes two ways to improve the output or build process.
- Crate documentation workflow: Describe how to put the example source in a Cargo crate and render its HTML documentation from the project root with `cargo doc --open`.
Why it matters: This lets you view your project’s documentation as a web page. - Dependency documentation shortcut: Explain when to use `cargo doc --no-deps` and what work it avoids.
Why it matters: It can make documentation builds less time-consuming when a crate has many dependencies. - Markdown documentation formatting: Describe the rich-text formatting rustdoc supports in documentation, including syntax-highlighted code snippets.
Why it matters: Markdown makes the generated documentation easier to structure and read.
Key terms: crate, cargo doc --open, HTML, cargo doc --no-deps, dependencies, rustdoc, Markdown, triple backticks, syntax highlighting
3.9 Summary
Where this is used later
- Encountering our first lifetime issue (chapter 4) · Uses a struct to model a value.
- Initializing the ActionKV struct (chapter 7) · Provides context for initializing a struct through a method rather than repeating its literal construction.
- Wrap data within specialty types (chapter 4) · Introduces interior mutability.
- Generating an HTTP GET request with reqwest (chapter 8) · Introduces the Result return type used by .text().
- Issue: Unable to return multiple error types (chapter 8) · Result as a way to return either a value or an error
- Cheating with unwrap() and expect() (chapter 8) · Revisits the drawbacks of using unwrap() and expect() for errors.
- How ownership moves (chapter 4) · Develops the ownership moves briefly illustrated here.
- Using a thread pool and task queue (chapter 10) · Enums as Rust types that can represent alternatives.
Concepts explained in this chapter
- impl blockThe impl keyword is Rust's reserved keyword for associating implementations, including trait implementations, with types.
- enumAn enum is a type whose value is one of a finite set of named variants, optionally carrying associated data.
- structA struct is a Rust data structure that defines a composite value with named fields and their types.
- result typeResult is Rust's standard result type that contains either a successful value or an error value.
- traitThe trait keyword is Rust's reserved keyword for declaring a trait.
- aliasingAliasing is the condition in which multiple references or access paths designate the same memory location at the same time.
- const qualifierA const declaration defines a compile-time constant whose value cannot be changed.
- errnoerrno is a C and POSIX process-global error indicator that system calls may set to identify a failure condition.
- global variableA global variable is a variable whose storage and scope make it accessible across multiple parts of a program.
- interior mutabilityInterior mutability is a design property allowing a value's internal state to change through an otherwise immutable interface.
- let keywordA let binding introduces a local Rust variable binding whose associated value may have internally mutable state even when the binding itself is immutable.
- static mutstatic mut declares a mutable Rust static variable with a program-wide lifetime and mutable access requiring unsafe handling.
- unsafe RustThe unsafe keyword marks Rust code that may perform operations requiring guarantees the compiler cannot verify automatically.
- unwrapThe unwrap method extracts a successful value from a Result or Option and panics when it contains an error or no value.
- attributeAn attribute is Rust syntax that attaches metadata to a program item and can control compiler behavior such as conditional inclusion.
- unit typeThe unit type is a type with exactly one value, written () in Rust, and represents the absence of a meaningful value.
- read traitA Read trait is a trait that specifies shared read-related behavior for implementing types.
- DisplayThe Display trait is Rust's standard formatting trait for producing a user-facing textual representation of a value.
- borrowingBorrowing is Rust's mechanism for temporarily accessing a value through a reference without taking ownership of it.
These guides are generated from the text of Manning books by Manning's knowledge graph and link to the sections they draw on. The full books are available on liveBook and at manning.com.
