Document not found (404)
-This URL is invalid, sorry. Please use the navigation bar or search to continue.
- -diff --git a/404.html b/404.html new file mode 100644 index 0000000..03a9d32 --- /dev/null +++ b/404.html @@ -0,0 +1,11 @@ + + +
+ +This URL is invalid, sorry. Please use the navigation bar or search to continue.
+ + diff --git a/README.md b/README.md new file mode 100644 index 0000000..40a044f --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# Dewey's Rustbook Build + +All redundant assets (CSS files, JS bundles, custom fonts, themes, and favicons) have been removed. The HTML pages have been cleaned up in place to keep only the structural content, organized into folders, and their relative links updated. + +## Setup and Cleanup Script + +The cleanup and folder organization are powered by [clean_book.py](file:///home/sortedcord/Projects/docs-rust/clean_book.py). + +### What the script does: +1. **HTML Clean-up**: For each HTML file, it extracts only the main content inside the `The following sections contain reference material you may find useful in your +Rust journey.
+ + diff --git a/appendix/appendix-01-keywords.html b/appendix/appendix-01-keywords.html new file mode 100644 index 0000000..8cbe3d3 --- /dev/null +++ b/appendix/appendix-01-keywords.html @@ -0,0 +1,123 @@ + + + + +The following lists contain keywords that are reserved for current or future +use by the Rust language. As such, they cannot be used as identifiers (except +as raw identifiers, as we discuss in the “Raw +Identifiers” section). Identifiers are names +of functions, variables, parameters, struct fields, modules, crates, constants, +macros, static values, attributes, types, traits, or lifetimes.
+The following is a list of keywords currently in use, with their functionality +described.
+as: Perform primitive casting, disambiguate the specific trait
+containing an item, or rename items in use statements.async: Return a Future instead of blocking the current thread.await: Suspend execution until the result of a Future is ready.break: Exit a loop immediately.const: Define constant items or constant raw pointers.continue: Continue to the next loop iteration.crate: In a module path, refers to the crate root.dyn: Dynamic dispatch to a trait object.else: Fallback for if and if let control flow constructs.enum: Define an enumeration.extern: Link an external function or variable.false: Boolean false literal.fn: Define a function or the function pointer type.for: Loop over items from an iterator, implement a trait, or specify a
+higher ranked lifetime.if: Branch based on the result of a conditional expression.impl: Implement inherent or trait functionality.in: Part of for loop syntax.let: Bind a variable.loop: Loop unconditionally.match: Match a value to patterns.mod: Define a module.move: Make a closure take ownership of all its captures.mut: Denote mutability in references, raw pointers, or pattern bindings.pub: Denote public visibility in struct fields, impl blocks, or
+modules.ref: Bind by reference.return: Return from function.Self: A type alias for the type we are defining or implementing.self: Method subject or current module.static: Global variable or lifetime lasting the entire program
+execution.struct: Define a structure.super: Parent module of the current module.trait: Define a trait.true: Boolean true literal.type: Define a type alias or associated type.union: Define a union; is a keyword only when
+used in a union declaration.unsafe: Denote unsafe code, functions, traits, or implementations.use: Bring symbols into scope.where: Denote clauses that constrain a type.while: Loop conditionally based on the result of an expression.The following keywords do not yet have any functionality but are reserved by +Rust for potential future use:
+abstractbecomeboxdofinalgenmacrooverrideprivtrytypeofunsizedvirtualyieldRaw identifiers are the syntax that lets you use keywords where they wouldn’t
+normally be allowed. You use a raw identifier by prefixing a keyword with r#.
For example, match is a keyword. If you try to compile the following function
+that uses match as its name:
Filename: src/main.rs
+fn match(needle: &str, haystack: &str) -> bool {
+ haystack.contains(needle)
+}
+you’ll get this error:
+error: expected identifier, found keyword `match`
+ --> src/main.rs:4:4
+ |
+4 | fn match(needle: &str, haystack: &str) -> bool {
+ | ^^^^^ expected identifier, found keyword
+
+The error shows that you can’t use the keyword match as the function
+identifier. To use match as a function name, you need to use the raw
+identifier syntax, like this:
Filename: src/main.rs
+fn r#match(needle: &str, haystack: &str) -> bool {
+ haystack.contains(needle)
+}
+
+fn main() {
+ assert!(r#match("foo", "foobar"));
+}
+This code will compile without any errors. Note the r# prefix on the function
+name in its definition as well as where the function is called in main.
Raw identifiers allow you to use any word you choose as an identifier, even if
+that word happens to be a reserved keyword. This gives us more freedom to choose
+identifier names, as well as lets us integrate with programs written in a
+language where these words aren’t keywords. In addition, raw identifiers allow
+you to use libraries written in a different Rust edition than your crate uses.
+For example, try isn’t a keyword in the 2015 edition but is in the 2018, 2021,
+and 2024 editions. If you depend on a library that is written using the 2015
+edition and has a try function, you’ll need to use the raw identifier syntax,
+r#try in this case, to call that function from your code on later editions.
+See Appendix E for more information on editions.
This appendix contains a glossary of Rust’s syntax, including operators and +other symbols that appear by themselves or in the context of paths, generics, +trait bounds, macros, attributes, comments, tuples, and brackets.
+Table B-1 contains the operators in Rust, an example of how the operator would +appear in context, a short explanation, and whether that operator is +overloadable. If an operator is overloadable, the relevant trait to use to +overload that operator is listed.
+Table B-1: Operators
+| Operator | Example | Explanation | Overloadable? |
|---|---|---|---|
! | ident!(...), ident!{...}, ident![...] | Macro expansion | |
! | !expr | Bitwise or logical complement | Not |
!= | expr != expr | Nonequality comparison | PartialEq |
% | expr % expr | Arithmetic remainder | Rem |
%= | var %= expr | Arithmetic remainder and assignment | RemAssign |
& | &expr, &mut expr | Borrow | |
& | &type, &mut type, &'a type, &'a mut type | Borrowed pointer type | |
& | expr & expr | Bitwise AND | BitAnd |
&= | var &= expr | Bitwise AND and assignment | BitAndAssign |
&& | expr && expr | Short-circuiting logical AND | |
* | expr * expr | Arithmetic multiplication | Mul |
*= | var *= expr | Arithmetic multiplication and assignment | MulAssign |
* | *expr | Dereference | Deref |
* | *const type, *mut type | Raw pointer | |
+ | trait + trait, 'a + trait | Compound type constraint | |
+ | expr + expr | Arithmetic addition | Add |
+= | var += expr | Arithmetic addition and assignment | AddAssign |
, | expr, expr | Argument and element separator | |
- | - expr | Arithmetic negation | Neg |
- | expr - expr | Arithmetic subtraction | Sub |
-= | var -= expr | Arithmetic subtraction and assignment | SubAssign |
-> | fn(...) -> type, |…| -> type | Function and closure return type | |
. | expr.ident | Field access | |
. | expr.ident(expr, ...) | Method call | |
. | expr.0, expr.1, and so on | Tuple indexing | |
.. | .., expr.., ..expr, expr..expr | Right-exclusive range literal | PartialOrd |
..= | ..=expr, expr..=expr | Right-inclusive range literal | PartialOrd |
.. | ..expr | Struct literal update syntax | |
.. | variant(x, ..), struct_type { x, .. } | “And the rest” pattern binding | |
... | expr...expr | (Deprecated, use ..= instead) In a pattern: inclusive range pattern | |
/ | expr / expr | Arithmetic division | Div |
/= | var /= expr | Arithmetic division and assignment | DivAssign |
: | pat: type, ident: type | Constraints | |
: | ident: expr | Struct field initializer | |
: | 'a: loop {...} | Loop label | |
; | expr; | Statement and item terminator | |
; | [...; len] | Part of fixed-size array syntax | |
<< | expr << expr | Left-shift | Shl |
<<= | var <<= expr | Left-shift and assignment | ShlAssign |
< | expr < expr | Less than comparison | PartialOrd |
<= | expr <= expr | Less than or equal to comparison | PartialOrd |
= | var = expr, ident = type | Assignment/equivalence | |
== | expr == expr | Equality comparison | PartialEq |
=> | pat => expr | Part of match arm syntax | |
> | expr > expr | Greater than comparison | PartialOrd |
>= | expr >= expr | Greater than or equal to comparison | PartialOrd |
>> | expr >> expr | Right-shift | Shr |
>>= | var >>= expr | Right-shift and assignment | ShrAssign |
@ | ident @ pat | Pattern binding | |
^ | expr ^ expr | Bitwise exclusive OR | BitXor |
^= | var ^= expr | Bitwise exclusive OR and assignment | BitXorAssign |
| | pat | pat | Pattern alternatives | |
| | expr | expr | Bitwise OR | BitOr |
|= | var |= expr | Bitwise OR and assignment | BitOrAssign |
|| | expr || expr | Short-circuiting logical OR | |
? | expr? | Error propagation |
The following tables contain all symbols that don’t function as operators; that +is, they don’t behave like a function or method call.
+Table B-2 shows symbols that appear on their own and are valid in a variety of +locations.
+Table B-2: Stand-alone Syntax
+| Symbol | Explanation |
|---|---|
'ident | Named lifetime or loop label |
Digits immediately followed by u8, i32, f64, usize, and so on | Numeric literal of specific type |
"..." | String literal |
r"...", r#"..."#, r##"..."##, and so on | Raw string literal; escape characters not processed |
b"..." | Byte string literal; constructs an array of bytes instead of a string |
br"...", br#"..."#, br##"..."##, and so on | Raw byte string literal; combination of raw and byte string literal |
'...' | Character literal |
b'...' | ASCII byte literal |
|…| expr | Closure |
! | Always-empty bottom type for diverging functions |
_ | “Ignored” pattern binding; also used to make integer literals readable |
Table B-3 shows symbols that appear in the context of a path through the module +hierarchy to an item.
+Table B-3: Path-Related Syntax
+| Symbol | Explanation |
|---|---|
ident::ident | Namespace path |
::path | Path relative to the crate root (that is, an explicitly absolute path) |
self::path | Path relative to the current module (that is, an explicitly relative path) |
super::path | Path relative to the parent of the current module |
type::ident, <type as trait>::ident | Associated constants, functions, and types |
<type>::... | Associated item for a type that cannot be directly named (for example, <&T>::..., <[T]>::..., and so on) |
trait::method(...) | Disambiguating a method call by naming the trait that defines it |
type::method(...) | Disambiguating a method call by naming the type for which it’s defined |
<type as trait>::method(...) | Disambiguating a method call by naming the trait and type |
Table B-4 shows symbols that appear in the context of using generic type +parameters.
+Table B-4: Generics
+| Symbol | Explanation |
|---|---|
path<...> | Specifies parameters to a generic type in a type (for example, Vec<u8>) |
path::<...>, method::<...> | Specifies parameters to a generic type, function, or method in an expression; often referred to as turbofish (for example, "42".parse::<i32>()) |
fn ident<...> ... | Define generic function |
struct ident<...> ... | Define generic structure |
enum ident<...> ... | Define generic enumeration |
impl<...> ... | Define generic implementation |
for<...> type | Higher ranked lifetime bounds |
type<ident=type> | A generic type where one or more associated types have specific assignments (for example, Iterator<Item=T>) |
Table B-5 shows symbols that appear in the context of constraining generic type +parameters with trait bounds.
+Table B-5: Trait Bound Constraints
+| Symbol | Explanation |
|---|---|
T: U | Generic parameter T constrained to types that implement U |
T: 'a | Generic type T must outlive lifetime 'a (meaning the type cannot transitively contain any references with lifetimes shorter than 'a) |
T: 'static | Generic type T contains no borrowed references other than 'static ones |
'b: 'a | Generic lifetime 'b must outlive lifetime 'a |
T: ?Sized | Allow generic type parameter to be a dynamically sized type |
'a + trait, trait + trait | Compound type constraint |
Table B-6 shows symbols that appear in the context of calling or defining +macros and specifying attributes on an item.
+Table B-6: Macros and Attributes
+| Symbol | Explanation |
|---|---|
#[meta] | Outer attribute |
#![meta] | Inner attribute |
$ident | Macro substitution |
$ident:kind | Macro metavariable |
$(...)... | Macro repetition |
ident!(...), ident!{...}, ident![...] | Macro invocation |
Table B-7 shows symbols that create comments.
+Table B-7: Comments
+| Symbol | Explanation |
|---|---|
// | Line comment |
//! | Inner line doc comment |
/// | Outer line doc comment |
/*...*/ | Block comment |
/*!...*/ | Inner block doc comment |
/**...*/ | Outer block doc comment |
Table B-8 shows the contexts in which parentheses are used.
+Table B-8: Parentheses
+| Symbol | Explanation |
|---|---|
() | Empty tuple (aka unit), both literal and type |
(expr) | Parenthesized expression |
(expr,) | Single-element tuple expression |
(type,) | Single-element tuple type |
(expr, ...) | Tuple expression |
(type, ...) | Tuple type |
expr(expr, ...) | Function call expression; also used to initialize tuple structs and tuple enum variants |
Table B-9 shows the contexts in which curly brackets are used.
+Table B-9: Curly Brackets
+| Context | Explanation |
|---|---|
{...} | Block expression |
Type {...} | Struct literal |
Table B-10 shows the contexts in which square brackets are used.
+Table B-10: Square Brackets
+| Context | Explanation |
|---|---|
[...] | Array literal |
[expr; len] | Array literal containing len copies of expr |
[type; len] | Array type containing len instances of type |
expr[expr] | Collection indexing; overloadable (Index, IndexMut) |
expr[..], expr[a..], expr[..b], expr[a..b] | Collection indexing pretending to be collection slicing, using Range, RangeFrom, RangeTo, or RangeFull as the “index” |
In various places in the book, we’ve discussed the derive attribute, which
+you can apply to a struct or enum definition. The derive attribute generates
+code that will implement a trait with its own default implementation on the
+type you’ve annotated with the derive syntax.
In this appendix, we provide a reference of all the traits in the standard
+library that you can use with derive. Each section covers:
derive doesIf you want different behavior from that provided by the derive attribute,
+consult the standard library documentation
+for each trait for details on how to manually implement them.
The traits listed here are the only ones defined by the standard library that
+can be implemented on your types using derive. Other traits defined in the
+standard library don’t have sensible default behavior, so it’s up to you to
+implement them in the way that makes sense for what you’re trying to accomplish.
An example of a trait that can’t be derived is Display, which handles
+formatting for end users. You should always consider the appropriate way to
+display a type to an end user. What parts of the type should an end user be
+allowed to see? What parts would they find relevant? What format of the data
+would be most relevant to them? The Rust compiler doesn’t have this insight, so
+it can’t provide appropriate default behavior for you.
The list of derivable traits provided in this appendix is not comprehensive:
+Libraries can implement derive for their own traits, making the list of
+traits you can use derive with truly open ended. Implementing derive
+involves using a procedural macro, which is covered in the “Custom derive
+Macros” section in Chapter 20.
Debug for Programmer OutputThe Debug trait enables debug formatting in format strings, which you
+indicate by adding :? within {} placeholders.
The Debug trait allows you to print instances of a type for debugging
+purposes, so you and other programmers using your type can inspect an instance
+at a particular point in a program’s execution.
The Debug trait is required, for example, in the use of the assert_eq!
+macro. This macro prints the values of instances given as arguments if the
+equality assertion fails so that programmers can see why the two instances
+weren’t equal.
PartialEq and Eq for Equality ComparisonsThe PartialEq trait allows you to compare instances of a type to check for
+equality and enables use of the == and != operators.
Deriving PartialEq implements the eq method. When PartialEq is derived on
+structs, two instances are equal only if all fields are equal, and the
+instances are not equal if any fields are not equal. When derived on enums,
+each variant is equal to itself and not equal to the other variants.
The PartialEq trait is required, for example, with the use of the
+assert_eq! macro, which needs to be able to compare two instances of a type
+for equality.
The Eq trait has no methods. Its purpose is to signal that for every value of
+the annotated type, the value is equal to itself. The Eq trait can only be
+applied to types that also implement PartialEq, although not all types that
+implement PartialEq can implement Eq. One example of this is floating-point
+number types: The implementation of floating-point numbers states that two
+instances of the not-a-number (NaN) value are not equal to each other.
An example of when Eq is required is for keys in a HashMap<K, V> so that
+the HashMap<K, V> can tell whether two keys are the same.
PartialOrd and Ord for Ordering ComparisonsThe PartialOrd trait allows you to compare instances of a type for sorting
+purposes. A type that implements PartialOrd can be used with the <, >,
+<=, and >= operators. You can only apply the PartialOrd trait to types
+that also implement PartialEq.
Deriving PartialOrd implements the partial_cmp method, which returns an
+Option<Ordering> that will be None when the values given don’t produce an
+ordering. An example of a value that doesn’t produce an ordering, even though
+most values of that type can be compared, is the NaN floating point value.
+Calling partial_cmp with any floating-point number and the NaN
+floating-point value will return None.
When derived on structs, PartialOrd compares two instances by comparing the
+value in each field in the order in which the fields appear in the struct
+definition. When derived on enums, variants of the enum declared earlier in the
+enum definition are considered less than the variants listed later.
The PartialOrd trait is required, for example, for the gen_range method
+from the rand crate that generates a random value in the range specified by a
+range expression.
The Ord trait allows you to know that for any two values of the annotated
+type, a valid ordering will exist. The Ord trait implements the cmp method,
+which returns an Ordering rather than an Option<Ordering> because a valid
+ordering will always be possible. You can only apply the Ord trait to types
+that also implement PartialOrd and Eq (and Eq requires PartialEq). When
+derived on structs and enums, cmp behaves the same way as the derived
+implementation for partial_cmp does with PartialOrd.
An example of when Ord is required is when storing values in a BTreeSet<T>,
+a data structure that stores data based on the sort order of the values.
Clone and Copy for Duplicating ValuesThe Clone trait allows you to explicitly create a deep copy of a value, and
+the duplication process might involve running arbitrary code and copying heap
+data. See the “Variables and Data Interacting with
+Clone” section in
+Chapter 4 for more information on Clone.
Deriving Clone implements the clone method, which when implemented for the
+whole type, calls clone on each of the parts of the type. This means all the
+fields or values in the type must also implement Clone to derive Clone.
An example of when Clone is required is when calling the to_vec method on a
+slice. The slice doesn’t own the type instances it contains, but the vector
+returned from to_vec will need to own its instances, so to_vec calls
+clone on each item. Thus, the type stored in the slice must implement Clone.
The Copy trait allows you to duplicate a value by only copying bits stored on
+the stack; no arbitrary code is necessary. See the “Stack-Only Data:
+Copy” section in Chapter 4 for more
+information on Copy.
The Copy trait doesn’t define any methods to prevent programmers from
+overloading those methods and violating the assumption that no arbitrary code
+is being run. That way, all programmers can assume that copying a value will be
+very fast.
You can derive Copy on any type whose parts all implement Copy. A type that
+implements Copy must also implement Clone because a type that implements
+Copy has a trivial implementation of Clone that performs the same task as
+Copy.
The Copy trait is rarely required; types that implement Copy have
+optimizations available, meaning you don’t have to call clone, which makes
+the code more concise.
Everything possible with Copy you can also accomplish with Clone, but the
+code might be slower or have to use clone in places.
Hash for Mapping a Value to a Value of Fixed SizeThe Hash trait allows you to take an instance of a type of arbitrary size and
+map that instance to a value of fixed size using a hash function. Deriving
+Hash implements the hash method. The derived implementation of the hash
+method combines the result of calling hash on each of the parts of the type,
+meaning all fields or values must also implement Hash to derive Hash.
An example of when Hash is required is in storing keys in a HashMap<K, V>
+to store data efficiently.
Default for Default ValuesThe Default trait allows you to create a default value for a type. Deriving
+Default implements the default function. The derived implementation of the
+default function calls the default function on each part of the type,
+meaning all fields or values in the type must also implement Default to
+derive Default.
The Default::default function is commonly used in combination with the struct
+update syntax discussed in the “Creating Instances from Other Instances with
+Struct Update
+Syntax” section in Chapter 5. You can customize a few fields of a struct and
+then set and use a default value for the rest of the fields by using
+..Default::default().
The Default trait is required when you use the method unwrap_or_default on
+Option<T> instances, for example. If the Option<T> is None, the method
+unwrap_or_default will return the result of Default::default for the type
+T stored in the Option<T>.
In this appendix, we talk about some useful development tools that the Rust +project provides. We’ll look at automatic formatting, quick ways to apply +warning fixes, a linter, and integrating with IDEs.
+rustfmtThe rustfmt tool reformats your code according to the community code style.
+Many collaborative projects use rustfmt to prevent arguments about which
+style to use when writing Rust: Everyone formats their code using the tool.
Rust installations include rustfmt by default, so you should already have the
+programs rustfmt and cargo-fmt on your system. These two commands are
+analogous to rustc and cargo in that rustfmt allows finer grained control
+and cargo-fmt understands conventions of a project that uses Cargo. To format
+any Cargo project, enter the following:
$ cargo fmt
+
+Running this command reformats all the Rust code in the current crate. This
+should only change the code style, not the code semantics. For more information
+on rustfmt, see its documentation.
rustfixThe rustfix tool is included with Rust installations and can automatically
+fix compiler warnings that have a clear way to correct the problem that’s
+likely what you want. You’ve probably seen compiler warnings before. For
+example, consider this code:
Filename: src/main.rs
+fn main() {
+ let mut x = 42;
+ println!("{x}");
+}
+Here, we’re defining the variable x as mutable, but we never actually mutate
+it. Rust warns us about that:
$ cargo build
+ Compiling myprogram v0.1.0 (file:///projects/myprogram)
+warning: variable does not need to be mutable
+ --> src/main.rs:2:9
+ |
+2 | let mut x = 0;
+ | ----^
+ | |
+ | help: remove this `mut`
+ |
+ = note: `#[warn(unused_mut)]` on by default
+
+The warning suggests that we remove the mut keyword. We can automatically
+apply that suggestion using the rustfix tool by running the command cargo fix:
$ cargo fix
+ Checking myprogram v0.1.0 (file:///projects/myprogram)
+ Fixing src/main.rs (1 fix)
+ Finished dev [unoptimized + debuginfo] target(s) in 0.59s
+
+When we look at src/main.rs again, we’ll see that cargo fix has changed the
+code:
Filename: src/main.rs
+fn main() {
+ let x = 42;
+ println!("{x}");
+}
+The variable x is now immutable, and the warning no longer appears.
You can also use the cargo fix command to transition your code between
+different Rust editions. Editions are covered in Appendix E.
The Clippy tool is a collection of lints to analyze your code so that you can +catch common mistakes and improve your Rust code. Clippy is included with +standard Rust installations.
+To run Clippy’s lints on any Cargo project, enter the following:
+$ cargo clippy
+
+For example, say you write a program that uses an approximation of a +mathematical constant, such as pi, as this program does:
+fn main() {
+ let x = 3.1415;
+ let r = 8.0;
+ println!("the area of the circle is {}", x * r * r);
+}
+Running cargo clippy on this project results in this error:
error: approximate value of `f{32, 64}::consts::PI` found
+ --> src/main.rs:2:13
+ |
+2 | let x = 3.1415;
+ | ^^^^^^
+ |
+ = note: `#[deny(clippy::approx_constant)]` on by default
+ = help: consider using the constant directly
+ = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#approx_constant
+
+This error lets you know that Rust already has a more precise PI constant
+defined, and that your program would be more correct if you used the constant
+instead. You would then change your code to use the PI constant.
The following code doesn’t result in any errors or warnings from Clippy:
+fn main() {
+ let x = std::f64::consts::PI;
+ let r = 8.0;
+ println!("the area of the circle is {}", x * r * r);
+}
+For more information on Clippy, see its documentation.
+rust-analyzerTo help with IDE integration, the Rust community recommends using
+rust-analyzer. This tool is a set of
+compiler-centric utilities that speak Language Server Protocol, which is a specification for IDEs and programming languages to
+communicate with each other. Different clients can use rust-analyzer, such as
+the Rust analyzer plug-in for Visual Studio Code.
Visit the rust-analyzer project’s home page
+for installation instructions, then install the language server support in your
+particular IDE. Your IDE will gain capabilities such as autocompletion, jump to
+definition, and inline errors.
In Chapter 1, you saw that cargo new adds a bit of metadata to your
+Cargo.toml file about an edition. This appendix talks about what that means!
The Rust language and compiler have a six-week release cycle, meaning users get +a constant stream of new features. Other programming languages release larger +changes less often; Rust releases smaller updates more frequently. After a +while, all of these tiny changes add up. But from release to release, it can be +difficult to look back and say, “Wow, between Rust 1.10 and Rust 1.31, Rust has +changed a lot!”
+Every three years or so, the Rust team produces a new Rust edition. Each +edition brings together the features that have landed into a clear package with +fully updated documentation and tooling. New editions ship as part of the usual +six-week release process.
+Editions serve different purposes for different people:
+At the time of this writing, four Rust editions are available: Rust 2015, Rust +2018, Rust 2021, and Rust 2024. This book is written using Rust 2024 edition +idioms.
+The edition key in Cargo.toml indicates which edition the compiler should
+use for your code. If the key doesn’t exist, Rust uses 2015 as the edition
+value for backward compatibility reasons.
Each project can opt in to an edition other than the default 2015 edition. +Editions can contain incompatible changes, such as including a new keyword that +conflicts with identifiers in code. However, unless you opt in to those +changes, your code will continue to compile even as you upgrade the Rust +compiler version you use.
+All Rust compiler versions support any edition that existed prior to that +compiler’s release, and they can link crates of any supported editions +together. Edition changes only affect the way the compiler initially parses +code. Therefore, if you’re using Rust 2015 and one of your dependencies uses +Rust 2018, your project will compile and be able to use that dependency. The +opposite situation, where your project uses Rust 2018 and a dependency uses +Rust 2015, works as well.
+To be clear: Most features will be available on all editions. Developers using +any Rust edition will continue to see improvements as new stable releases are +made. However, in some cases, mainly when new keywords are added, some new +features might only be available in later editions. You will need to switch +editions if you want to take advantage of such features.
+For more details, see The Rust Edition Guide. This is a
+complete book that enumerates the differences between editions and explains how
+to automatically upgrade your code to a new edition via cargo fix.
For resources in languages other than English. Most are still in progress; see +the Translations label to help or let us know about a new translation!
+This appendix is about how Rust is made and how that affects you as a Rust +developer.
+As a language, Rust cares a lot about the stability of your code. We want +Rust to be a rock-solid foundation you can build on, and if things were +constantly changing, that would be impossible. At the same time, if we can’t +experiment with new features, we may not find out important flaws until after +their release, when we can no longer change things.
+Our solution to this problem is what we call “stability without stagnation”, +and our guiding principle is this: you should never have to fear upgrading to a +new version of stable Rust. Each upgrade should be painless, but should also +bring you new features, fewer bugs, and faster compile times.
+Rust development operates on a train schedule. That is, all development is +done in the main branch of the Rust repository. Releases follow a software +release train model, which has been used by Cisco IOS and other software +projects. There are three release channels for Rust:
+Most Rust developers primarily use the stable channel, but those who want to +try out experimental new features may use nightly or beta.
+Here’s an example of how the development and release process works: let’s +assume that the Rust team is working on the release of Rust 1.5. That release +happened in December of 2015, but it will provide us with realistic version +numbers. A new feature is added to Rust: a new commit lands on the main +branch. Each night, a new nightly version of Rust is produced. Every day is a +release day, and these releases are created by our release infrastructure +automatically. So as time passes, our releases look like this, once a night:
+nightly: * - - * - - *
+
+Every six weeks, it’s time to prepare a new release! The beta branch of the
+Rust repository branches off from the main branch used by nightly. Now,
+there are two releases:
nightly: * - - * - - *
+ |
+beta: *
+
+Most Rust users do not use beta releases actively, but test against beta in +their CI system to help Rust discover possible regressions. In the meantime, +there’s still a nightly release every night:
+nightly: * - - * - - * - - * - - *
+ |
+beta: *
+
+Let’s say a regression is found. Good thing we had some time to test the beta
+release before the regression snuck into a stable release! The fix is applied
+to the main branch, so that nightly is fixed, and then the fix is backported to
+the beta branch, and a new release of beta is produced:
nightly: * - - * - - * - - * - - * - - *
+ |
+beta: * - - - - - - - - *
+
+Six weeks after the first beta was created, it’s time for a stable release! The
+stable branch is produced from the beta branch:
nightly: * - - * - - * - - * - - * - - * - * - *
+ |
+beta: * - - - - - - - - *
+ |
+stable: *
+
+Hooray! Rust 1.5 is done! However, we’ve forgotten one thing: because the six
+weeks have gone by, we also need a new beta of the next version of Rust, 1.6.
+So after stable branches off of beta, the next version of beta branches
+off of nightly again:
nightly: * - - * - - * - - * - - * - - * - * - *
+ | |
+beta: * - - - - - - - - * *
+ |
+stable: *
+
+This is called the “train model” because every six weeks, a release “leaves the +station”, but still has to take a journey through the beta channel before it +arrives as a stable release.
+Rust releases every six weeks, like clockwork. If you know the date of one Rust +release, you can know the date of the next one: it’s six weeks later. A nice +aspect of having releases scheduled every six weeks is that the next train is +coming soon. If a feature happens to miss a particular release, there’s no need +to worry: another one is happening in a short time! This helps reduce pressure +to sneak possibly unpolished features in close to the release deadline.
+Thanks to this process, you can always check out the next build of Rust and
+verify for yourself that it’s easy to upgrade to: if a beta release doesn’t
+work as expected, you can report it to the team and get it fixed before the
+next stable release happens! Breakage in a beta release is relatively rare, but
+rustc is still a piece of software, and bugs do exist.
The Rust project supports the most recent stable version. When a new stable +version is released, the old version reaches its end of life (EOL). This means +each version is supported for six weeks.
+There’s one more catch with this release model: unstable features. Rust uses a +technique called “feature flags” to determine what features are enabled in a +given release. If a new feature is under active development, it lands on the +main branch, and therefore, in nightly, but behind a feature flag. If you, as +a user, wish to try out the work-in-progress feature, you can, but you must be +using a nightly release of Rust and annotate your source code with the +appropriate flag to opt in.
+If you’re using a beta or stable release of Rust, you can’t use any feature +flags. This is the key that allows us to get practical use with new features +before we declare them stable forever. Those who wish to opt into the bleeding +edge can do so, and those who want a rock-solid experience can stick with +stable and know that their code won’t break. Stability without stagnation.
+This book only contains information about stable features, as in-progress +features are still changing, and surely they’ll be different between when this +book was written and when they get enabled in stable builds. You can find +documentation for nightly-only features online.
+Rustup makes it easy to change between different release channels of Rust, on a +global or per-project basis. By default, you’ll have stable Rust installed. To +install nightly, for example:
+$ rustup toolchain install nightly
+
+You can see all of the toolchains (releases of Rust and associated
+components) you have installed with rustup as well. Here’s an example on one
+of your authors’ Windows computer:
> rustup toolchain list
+stable-x86_64-pc-windows-msvc (default)
+beta-x86_64-pc-windows-msvc
+nightly-x86_64-pc-windows-msvc
+
+As you can see, the stable toolchain is the default. Most Rust users use stable
+most of the time. You might want to use stable most of the time, but use
+nightly on a specific project, because you care about a cutting-edge feature.
+To do so, you can use rustup override in that project’s directory to set the
+nightly toolchain as the one rustup should use when you’re in that directory:
$ cd ~/projects/needs-nightly
+$ rustup override set nightly
+
+Now, every time you call rustc or cargo inside of
+~/projects/needs-nightly, rustup will make sure that you are using nightly
+Rust, rather than your default of stable Rust. This comes in handy when you
+have a lot of Rust projects!
So how do you learn about these new features? Rust’s development model follows +a Request For Comments (RFC) process. If you’d like an improvement in Rust, +you can write up a proposal, called an RFC.
+Anyone can write RFCs to improve Rust, and the proposals are reviewed and +discussed by the Rust team, which is comprised of many topic subteams. There’s +a full list of the teams on Rust’s website, which includes teams for +each area of the project: language design, compiler implementation, +infrastructure, documentation, and more. The appropriate team reads the +proposal and the comments, writes some comments of their own, and eventually, +there’s consensus to accept or reject the feature.
+If the feature is accepted, an issue is opened on the Rust repository, and +someone can implement it. The person who implements it very well may not be the +person who proposed the feature in the first place! When the implementation is +ready, it lands on the main branch behind a feature gate, as we discussed in +the “Unstable Features” section.
+After some time, once Rust developers who use nightly releases have been able +to try out the new feature, team members will discuss the feature, how it’s +worked out on nightly, and decide if it should make it into stable Rust or not. +If the decision is to move forward, the feature gate is removed, and the +feature is now considered stable! It rides the trains into a new stable release +of Rust.
+ + diff --git a/build/404.html b/build/404.html deleted file mode 100644 index 644824b..0000000 --- a/build/404.html +++ /dev/null @@ -1,233 +0,0 @@ - - - - - -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
-This URL is invalid, sorry. Please use the navigation bar or search to continue.
- -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
-The following sections contain reference material you may find useful in your -Rust journey.
- -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
-The following lists contain keywords that are reserved for current or future -use by the Rust language. As such, they cannot be used as identifiers (except -as raw identifiers, as we discuss in the “Raw -Identifiers” section). Identifiers are names -of functions, variables, parameters, struct fields, modules, crates, constants, -macros, static values, attributes, types, traits, or lifetimes.
-The following is a list of keywords currently in use, with their functionality -described.
-as: Perform primitive casting, disambiguate the specific trait
-containing an item, or rename items in use statements.async: Return a Future instead of blocking the current thread.await: Suspend execution until the result of a Future is ready.break: Exit a loop immediately.const: Define constant items or constant raw pointers.continue: Continue to the next loop iteration.crate: In a module path, refers to the crate root.dyn: Dynamic dispatch to a trait object.else: Fallback for if and if let control flow constructs.enum: Define an enumeration.extern: Link an external function or variable.false: Boolean false literal.fn: Define a function or the function pointer type.for: Loop over items from an iterator, implement a trait, or specify a
-higher ranked lifetime.if: Branch based on the result of a conditional expression.impl: Implement inherent or trait functionality.in: Part of for loop syntax.let: Bind a variable.loop: Loop unconditionally.match: Match a value to patterns.mod: Define a module.move: Make a closure take ownership of all its captures.mut: Denote mutability in references, raw pointers, or pattern bindings.pub: Denote public visibility in struct fields, impl blocks, or
-modules.ref: Bind by reference.return: Return from function.Self: A type alias for the type we are defining or implementing.self: Method subject or current module.static: Global variable or lifetime lasting the entire program
-execution.struct: Define a structure.super: Parent module of the current module.trait: Define a trait.true: Boolean true literal.type: Define a type alias or associated type.union: Define a union; is a keyword only when
-used in a union declaration.unsafe: Denote unsafe code, functions, traits, or implementations.use: Bring symbols into scope.where: Denote clauses that constrain a type.while: Loop conditionally based on the result of an expression.The following keywords do not yet have any functionality but are reserved by -Rust for potential future use:
-abstractbecomeboxdofinalgenmacrooverrideprivtrytypeofunsizedvirtualyieldRaw identifiers are the syntax that lets you use keywords where they wouldn’t
-normally be allowed. You use a raw identifier by prefixing a keyword with r#.
For example, match is a keyword. If you try to compile the following function
-that uses match as its name:
Filename: src/main.rs
-fn match(needle: &str, haystack: &str) -> bool {
- haystack.contains(needle)
-}
-you’ll get this error:
-error: expected identifier, found keyword `match`
- --> src/main.rs:4:4
- |
-4 | fn match(needle: &str, haystack: &str) -> bool {
- | ^^^^^ expected identifier, found keyword
-
-The error shows that you can’t use the keyword match as the function
-identifier. To use match as a function name, you need to use the raw
-identifier syntax, like this:
Filename: src/main.rs
-fn r#match(needle: &str, haystack: &str) -> bool {
- haystack.contains(needle)
-}
-
-fn main() {
- assert!(r#match("foo", "foobar"));
-}
-This code will compile without any errors. Note the r# prefix on the function
-name in its definition as well as where the function is called in main.
Raw identifiers allow you to use any word you choose as an identifier, even if
-that word happens to be a reserved keyword. This gives us more freedom to choose
-identifier names, as well as lets us integrate with programs written in a
-language where these words aren’t keywords. In addition, raw identifiers allow
-you to use libraries written in a different Rust edition than your crate uses.
-For example, try isn’t a keyword in the 2015 edition but is in the 2018, 2021,
-and 2024 editions. If you depend on a library that is written using the 2015
-edition and has a try function, you’ll need to use the raw identifier syntax,
-r#try in this case, to call that function from your code on later editions.
-See Appendix E for more information on editions.
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
-This appendix contains a glossary of Rust’s syntax, including operators and -other symbols that appear by themselves or in the context of paths, generics, -trait bounds, macros, attributes, comments, tuples, and brackets.
-Table B-1 contains the operators in Rust, an example of how the operator would -appear in context, a short explanation, and whether that operator is -overloadable. If an operator is overloadable, the relevant trait to use to -overload that operator is listed.
-Table B-1: Operators
-| Operator | Example | Explanation | Overloadable? |
|---|---|---|---|
! | ident!(...), ident!{...}, ident![...] | Macro expansion | |
! | !expr | Bitwise or logical complement | Not |
!= | expr != expr | Nonequality comparison | PartialEq |
% | expr % expr | Arithmetic remainder | Rem |
%= | var %= expr | Arithmetic remainder and assignment | RemAssign |
& | &expr, &mut expr | Borrow | |
& | &type, &mut type, &'a type, &'a mut type | Borrowed pointer type | |
& | expr & expr | Bitwise AND | BitAnd |
&= | var &= expr | Bitwise AND and assignment | BitAndAssign |
&& | expr && expr | Short-circuiting logical AND | |
* | expr * expr | Arithmetic multiplication | Mul |
*= | var *= expr | Arithmetic multiplication and assignment | MulAssign |
* | *expr | Dereference | Deref |
* | *const type, *mut type | Raw pointer | |
+ | trait + trait, 'a + trait | Compound type constraint | |
+ | expr + expr | Arithmetic addition | Add |
+= | var += expr | Arithmetic addition and assignment | AddAssign |
, | expr, expr | Argument and element separator | |
- | - expr | Arithmetic negation | Neg |
- | expr - expr | Arithmetic subtraction | Sub |
-= | var -= expr | Arithmetic subtraction and assignment | SubAssign |
-> | fn(...) -> type, |…| -> type | Function and closure return type | |
. | expr.ident | Field access | |
. | expr.ident(expr, ...) | Method call | |
. | expr.0, expr.1, and so on | Tuple indexing | |
.. | .., expr.., ..expr, expr..expr | Right-exclusive range literal | PartialOrd |
..= | ..=expr, expr..=expr | Right-inclusive range literal | PartialOrd |
.. | ..expr | Struct literal update syntax | |
.. | variant(x, ..), struct_type { x, .. } | “And the rest” pattern binding | |
... | expr...expr | (Deprecated, use ..= instead) In a pattern: inclusive range pattern | |
/ | expr / expr | Arithmetic division | Div |
/= | var /= expr | Arithmetic division and assignment | DivAssign |
: | pat: type, ident: type | Constraints | |
: | ident: expr | Struct field initializer | |
: | 'a: loop {...} | Loop label | |
; | expr; | Statement and item terminator | |
; | [...; len] | Part of fixed-size array syntax | |
<< | expr << expr | Left-shift | Shl |
<<= | var <<= expr | Left-shift and assignment | ShlAssign |
< | expr < expr | Less than comparison | PartialOrd |
<= | expr <= expr | Less than or equal to comparison | PartialOrd |
= | var = expr, ident = type | Assignment/equivalence | |
== | expr == expr | Equality comparison | PartialEq |
=> | pat => expr | Part of match arm syntax | |
> | expr > expr | Greater than comparison | PartialOrd |
>= | expr >= expr | Greater than or equal to comparison | PartialOrd |
>> | expr >> expr | Right-shift | Shr |
>>= | var >>= expr | Right-shift and assignment | ShrAssign |
@ | ident @ pat | Pattern binding | |
^ | expr ^ expr | Bitwise exclusive OR | BitXor |
^= | var ^= expr | Bitwise exclusive OR and assignment | BitXorAssign |
| | pat | pat | Pattern alternatives | |
| | expr | expr | Bitwise OR | BitOr |
|= | var |= expr | Bitwise OR and assignment | BitOrAssign |
|| | expr || expr | Short-circuiting logical OR | |
? | expr? | Error propagation |
The following tables contain all symbols that don’t function as operators; that -is, they don’t behave like a function or method call.
-Table B-2 shows symbols that appear on their own and are valid in a variety of -locations.
-Table B-2: Stand-alone Syntax
-| Symbol | Explanation |
|---|---|
'ident | Named lifetime or loop label |
Digits immediately followed by u8, i32, f64, usize, and so on | Numeric literal of specific type |
"..." | String literal |
r"...", r#"..."#, r##"..."##, and so on | Raw string literal; escape characters not processed |
b"..." | Byte string literal; constructs an array of bytes instead of a string |
br"...", br#"..."#, br##"..."##, and so on | Raw byte string literal; combination of raw and byte string literal |
'...' | Character literal |
b'...' | ASCII byte literal |
|…| expr | Closure |
! | Always-empty bottom type for diverging functions |
_ | “Ignored” pattern binding; also used to make integer literals readable |
Table B-3 shows symbols that appear in the context of a path through the module -hierarchy to an item.
-Table B-3: Path-Related Syntax
-| Symbol | Explanation |
|---|---|
ident::ident | Namespace path |
::path | Path relative to the crate root (that is, an explicitly absolute path) |
self::path | Path relative to the current module (that is, an explicitly relative path) |
super::path | Path relative to the parent of the current module |
type::ident, <type as trait>::ident | Associated constants, functions, and types |
<type>::... | Associated item for a type that cannot be directly named (for example, <&T>::..., <[T]>::..., and so on) |
trait::method(...) | Disambiguating a method call by naming the trait that defines it |
type::method(...) | Disambiguating a method call by naming the type for which it’s defined |
<type as trait>::method(...) | Disambiguating a method call by naming the trait and type |
Table B-4 shows symbols that appear in the context of using generic type -parameters.
-Table B-4: Generics
-| Symbol | Explanation |
|---|---|
path<...> | Specifies parameters to a generic type in a type (for example, Vec<u8>) |
path::<...>, method::<...> | Specifies parameters to a generic type, function, or method in an expression; often referred to as turbofish (for example, "42".parse::<i32>()) |
fn ident<...> ... | Define generic function |
struct ident<...> ... | Define generic structure |
enum ident<...> ... | Define generic enumeration |
impl<...> ... | Define generic implementation |
for<...> type | Higher ranked lifetime bounds |
type<ident=type> | A generic type where one or more associated types have specific assignments (for example, Iterator<Item=T>) |
Table B-5 shows symbols that appear in the context of constraining generic type -parameters with trait bounds.
-Table B-5: Trait Bound Constraints
-| Symbol | Explanation |
|---|---|
T: U | Generic parameter T constrained to types that implement U |
T: 'a | Generic type T must outlive lifetime 'a (meaning the type cannot transitively contain any references with lifetimes shorter than 'a) |
T: 'static | Generic type T contains no borrowed references other than 'static ones |
'b: 'a | Generic lifetime 'b must outlive lifetime 'a |
T: ?Sized | Allow generic type parameter to be a dynamically sized type |
'a + trait, trait + trait | Compound type constraint |
Table B-6 shows symbols that appear in the context of calling or defining -macros and specifying attributes on an item.
-Table B-6: Macros and Attributes
-| Symbol | Explanation |
|---|---|
#[meta] | Outer attribute |
#![meta] | Inner attribute |
$ident | Macro substitution |
$ident:kind | Macro metavariable |
$(...)... | Macro repetition |
ident!(...), ident!{...}, ident![...] | Macro invocation |
Table B-7 shows symbols that create comments.
-Table B-7: Comments
-| Symbol | Explanation |
|---|---|
// | Line comment |
//! | Inner line doc comment |
/// | Outer line doc comment |
/*...*/ | Block comment |
/*!...*/ | Inner block doc comment |
/**...*/ | Outer block doc comment |
Table B-8 shows the contexts in which parentheses are used.
-Table B-8: Parentheses
-| Symbol | Explanation |
|---|---|
() | Empty tuple (aka unit), both literal and type |
(expr) | Parenthesized expression |
(expr,) | Single-element tuple expression |
(type,) | Single-element tuple type |
(expr, ...) | Tuple expression |
(type, ...) | Tuple type |
expr(expr, ...) | Function call expression; also used to initialize tuple structs and tuple enum variants |
Table B-9 shows the contexts in which curly brackets are used.
-Table B-9: Curly Brackets
-| Context | Explanation |
|---|---|
{...} | Block expression |
Type {...} | Struct literal |
Table B-10 shows the contexts in which square brackets are used.
-Table B-10: Square Brackets
-| Context | Explanation |
|---|---|
[...] | Array literal |
[expr; len] | Array literal containing len copies of expr |
[type; len] | Array type containing len instances of type |
expr[expr] | Collection indexing; overloadable (Index, IndexMut) |
expr[..], expr[a..], expr[..b], expr[a..b] | Collection indexing pretending to be collection slicing, using Range, RangeFrom, RangeTo, or RangeFull as the “index” |
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
-In various places in the book, we’ve discussed the derive attribute, which
-you can apply to a struct or enum definition. The derive attribute generates
-code that will implement a trait with its own default implementation on the
-type you’ve annotated with the derive syntax.
In this appendix, we provide a reference of all the traits in the standard
-library that you can use with derive. Each section covers:
derive doesIf you want different behavior from that provided by the derive attribute,
-consult the standard library documentation
-for each trait for details on how to manually implement them.
The traits listed here are the only ones defined by the standard library that
-can be implemented on your types using derive. Other traits defined in the
-standard library don’t have sensible default behavior, so it’s up to you to
-implement them in the way that makes sense for what you’re trying to accomplish.
An example of a trait that can’t be derived is Display, which handles
-formatting for end users. You should always consider the appropriate way to
-display a type to an end user. What parts of the type should an end user be
-allowed to see? What parts would they find relevant? What format of the data
-would be most relevant to them? The Rust compiler doesn’t have this insight, so
-it can’t provide appropriate default behavior for you.
The list of derivable traits provided in this appendix is not comprehensive:
-Libraries can implement derive for their own traits, making the list of
-traits you can use derive with truly open ended. Implementing derive
-involves using a procedural macro, which is covered in the “Custom derive
-Macros” section in Chapter 20.
Debug for Programmer OutputThe Debug trait enables debug formatting in format strings, which you
-indicate by adding :? within {} placeholders.
The Debug trait allows you to print instances of a type for debugging
-purposes, so you and other programmers using your type can inspect an instance
-at a particular point in a program’s execution.
The Debug trait is required, for example, in the use of the assert_eq!
-macro. This macro prints the values of instances given as arguments if the
-equality assertion fails so that programmers can see why the two instances
-weren’t equal.
PartialEq and Eq for Equality ComparisonsThe PartialEq trait allows you to compare instances of a type to check for
-equality and enables use of the == and != operators.
Deriving PartialEq implements the eq method. When PartialEq is derived on
-structs, two instances are equal only if all fields are equal, and the
-instances are not equal if any fields are not equal. When derived on enums,
-each variant is equal to itself and not equal to the other variants.
The PartialEq trait is required, for example, with the use of the
-assert_eq! macro, which needs to be able to compare two instances of a type
-for equality.
The Eq trait has no methods. Its purpose is to signal that for every value of
-the annotated type, the value is equal to itself. The Eq trait can only be
-applied to types that also implement PartialEq, although not all types that
-implement PartialEq can implement Eq. One example of this is floating-point
-number types: The implementation of floating-point numbers states that two
-instances of the not-a-number (NaN) value are not equal to each other.
An example of when Eq is required is for keys in a HashMap<K, V> so that
-the HashMap<K, V> can tell whether two keys are the same.
PartialOrd and Ord for Ordering ComparisonsThe PartialOrd trait allows you to compare instances of a type for sorting
-purposes. A type that implements PartialOrd can be used with the <, >,
-<=, and >= operators. You can only apply the PartialOrd trait to types
-that also implement PartialEq.
Deriving PartialOrd implements the partial_cmp method, which returns an
-Option<Ordering> that will be None when the values given don’t produce an
-ordering. An example of a value that doesn’t produce an ordering, even though
-most values of that type can be compared, is the NaN floating point value.
-Calling partial_cmp with any floating-point number and the NaN
-floating-point value will return None.
When derived on structs, PartialOrd compares two instances by comparing the
-value in each field in the order in which the fields appear in the struct
-definition. When derived on enums, variants of the enum declared earlier in the
-enum definition are considered less than the variants listed later.
The PartialOrd trait is required, for example, for the gen_range method
-from the rand crate that generates a random value in the range specified by a
-range expression.
The Ord trait allows you to know that for any two values of the annotated
-type, a valid ordering will exist. The Ord trait implements the cmp method,
-which returns an Ordering rather than an Option<Ordering> because a valid
-ordering will always be possible. You can only apply the Ord trait to types
-that also implement PartialOrd and Eq (and Eq requires PartialEq). When
-derived on structs and enums, cmp behaves the same way as the derived
-implementation for partial_cmp does with PartialOrd.
An example of when Ord is required is when storing values in a BTreeSet<T>,
-a data structure that stores data based on the sort order of the values.
Clone and Copy for Duplicating ValuesThe Clone trait allows you to explicitly create a deep copy of a value, and
-the duplication process might involve running arbitrary code and copying heap
-data. See the “Variables and Data Interacting with
-Clone” section in
-Chapter 4 for more information on Clone.
Deriving Clone implements the clone method, which when implemented for the
-whole type, calls clone on each of the parts of the type. This means all the
-fields or values in the type must also implement Clone to derive Clone.
An example of when Clone is required is when calling the to_vec method on a
-slice. The slice doesn’t own the type instances it contains, but the vector
-returned from to_vec will need to own its instances, so to_vec calls
-clone on each item. Thus, the type stored in the slice must implement Clone.
The Copy trait allows you to duplicate a value by only copying bits stored on
-the stack; no arbitrary code is necessary. See the “Stack-Only Data:
-Copy” section in Chapter 4 for more
-information on Copy.
The Copy trait doesn’t define any methods to prevent programmers from
-overloading those methods and violating the assumption that no arbitrary code
-is being run. That way, all programmers can assume that copying a value will be
-very fast.
You can derive Copy on any type whose parts all implement Copy. A type that
-implements Copy must also implement Clone because a type that implements
-Copy has a trivial implementation of Clone that performs the same task as
-Copy.
The Copy trait is rarely required; types that implement Copy have
-optimizations available, meaning you don’t have to call clone, which makes
-the code more concise.
Everything possible with Copy you can also accomplish with Clone, but the
-code might be slower or have to use clone in places.
Hash for Mapping a Value to a Value of Fixed SizeThe Hash trait allows you to take an instance of a type of arbitrary size and
-map that instance to a value of fixed size using a hash function. Deriving
-Hash implements the hash method. The derived implementation of the hash
-method combines the result of calling hash on each of the parts of the type,
-meaning all fields or values must also implement Hash to derive Hash.
An example of when Hash is required is in storing keys in a HashMap<K, V>
-to store data efficiently.
Default for Default ValuesThe Default trait allows you to create a default value for a type. Deriving
-Default implements the default function. The derived implementation of the
-default function calls the default function on each part of the type,
-meaning all fields or values in the type must also implement Default to
-derive Default.
The Default::default function is commonly used in combination with the struct
-update syntax discussed in the “Creating Instances from Other Instances with
-Struct Update
-Syntax” section in Chapter 5. You can customize a few fields of a struct and
-then set and use a default value for the rest of the fields by using
-..Default::default().
The Default trait is required when you use the method unwrap_or_default on
-Option<T> instances, for example. If the Option<T> is None, the method
-unwrap_or_default will return the result of Default::default for the type
-T stored in the Option<T>.
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
-In this appendix, we talk about some useful development tools that the Rust -project provides. We’ll look at automatic formatting, quick ways to apply -warning fixes, a linter, and integrating with IDEs.
-rustfmtThe rustfmt tool reformats your code according to the community code style.
-Many collaborative projects use rustfmt to prevent arguments about which
-style to use when writing Rust: Everyone formats their code using the tool.
Rust installations include rustfmt by default, so you should already have the
-programs rustfmt and cargo-fmt on your system. These two commands are
-analogous to rustc and cargo in that rustfmt allows finer grained control
-and cargo-fmt understands conventions of a project that uses Cargo. To format
-any Cargo project, enter the following:
$ cargo fmt
-
-Running this command reformats all the Rust code in the current crate. This
-should only change the code style, not the code semantics. For more information
-on rustfmt, see its documentation.
rustfixThe rustfix tool is included with Rust installations and can automatically
-fix compiler warnings that have a clear way to correct the problem that’s
-likely what you want. You’ve probably seen compiler warnings before. For
-example, consider this code:
Filename: src/main.rs
-fn main() {
- let mut x = 42;
- println!("{x}");
-}
-Here, we’re defining the variable x as mutable, but we never actually mutate
-it. Rust warns us about that:
$ cargo build
- Compiling myprogram v0.1.0 (file:///projects/myprogram)
-warning: variable does not need to be mutable
- --> src/main.rs:2:9
- |
-2 | let mut x = 0;
- | ----^
- | |
- | help: remove this `mut`
- |
- = note: `#[warn(unused_mut)]` on by default
-
-The warning suggests that we remove the mut keyword. We can automatically
-apply that suggestion using the rustfix tool by running the command cargo fix:
$ cargo fix
- Checking myprogram v0.1.0 (file:///projects/myprogram)
- Fixing src/main.rs (1 fix)
- Finished dev [unoptimized + debuginfo] target(s) in 0.59s
-
-When we look at src/main.rs again, we’ll see that cargo fix has changed the
-code:
Filename: src/main.rs
-fn main() {
- let x = 42;
- println!("{x}");
-}
-The variable x is now immutable, and the warning no longer appears.
You can also use the cargo fix command to transition your code between
-different Rust editions. Editions are covered in Appendix E.
The Clippy tool is a collection of lints to analyze your code so that you can -catch common mistakes and improve your Rust code. Clippy is included with -standard Rust installations.
-To run Clippy’s lints on any Cargo project, enter the following:
-$ cargo clippy
-
-For example, say you write a program that uses an approximation of a -mathematical constant, such as pi, as this program does:
-fn main() {
- let x = 3.1415;
- let r = 8.0;
- println!("the area of the circle is {}", x * r * r);
-}
-Running cargo clippy on this project results in this error:
error: approximate value of `f{32, 64}::consts::PI` found
- --> src/main.rs:2:13
- |
-2 | let x = 3.1415;
- | ^^^^^^
- |
- = note: `#[deny(clippy::approx_constant)]` on by default
- = help: consider using the constant directly
- = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#approx_constant
-
-This error lets you know that Rust already has a more precise PI constant
-defined, and that your program would be more correct if you used the constant
-instead. You would then change your code to use the PI constant.
The following code doesn’t result in any errors or warnings from Clippy:
-fn main() {
- let x = std::f64::consts::PI;
- let r = 8.0;
- println!("the area of the circle is {}", x * r * r);
-}
-For more information on Clippy, see its documentation.
-rust-analyzerTo help with IDE integration, the Rust community recommends using
-rust-analyzer. This tool is a set of
-compiler-centric utilities that speak Language Server Protocol, which is a specification for IDEs and programming languages to
-communicate with each other. Different clients can use rust-analyzer, such as
-the Rust analyzer plug-in for Visual Studio Code.
Visit the rust-analyzer project’s home page
-for installation instructions, then install the language server support in your
-particular IDE. Your IDE will gain capabilities such as autocompletion, jump to
-definition, and inline errors.
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
-In Chapter 1, you saw that cargo new adds a bit of metadata to your
-Cargo.toml file about an edition. This appendix talks about what that means!
The Rust language and compiler have a six-week release cycle, meaning users get -a constant stream of new features. Other programming languages release larger -changes less often; Rust releases smaller updates more frequently. After a -while, all of these tiny changes add up. But from release to release, it can be -difficult to look back and say, “Wow, between Rust 1.10 and Rust 1.31, Rust has -changed a lot!”
-Every three years or so, the Rust team produces a new Rust edition. Each -edition brings together the features that have landed into a clear package with -fully updated documentation and tooling. New editions ship as part of the usual -six-week release process.
-Editions serve different purposes for different people:
-At the time of this writing, four Rust editions are available: Rust 2015, Rust -2018, Rust 2021, and Rust 2024. This book is written using Rust 2024 edition -idioms.
-The edition key in Cargo.toml indicates which edition the compiler should
-use for your code. If the key doesn’t exist, Rust uses 2015 as the edition
-value for backward compatibility reasons.
Each project can opt in to an edition other than the default 2015 edition. -Editions can contain incompatible changes, such as including a new keyword that -conflicts with identifiers in code. However, unless you opt in to those -changes, your code will continue to compile even as you upgrade the Rust -compiler version you use.
-All Rust compiler versions support any edition that existed prior to that -compiler’s release, and they can link crates of any supported editions -together. Edition changes only affect the way the compiler initially parses -code. Therefore, if you’re using Rust 2015 and one of your dependencies uses -Rust 2018, your project will compile and be able to use that dependency. The -opposite situation, where your project uses Rust 2018 and a dependency uses -Rust 2015, works as well.
-To be clear: Most features will be available on all editions. Developers using -any Rust edition will continue to see improvements as new stable releases are -made. However, in some cases, mainly when new keywords are added, some new -features might only be available in later editions. You will need to switch -editions if you want to take advantage of such features.
-For more details, see The Rust Edition Guide. This is a
-complete book that enumerates the differences between editions and explains how
-to automatically upgrade your code to a new edition via cargo fix.
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
-For resources in languages other than English. Most are still in progress; see -the Translations label to help or let us know about a new translation!
-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
-This appendix is about how Rust is made and how that affects you as a Rust -developer.
-As a language, Rust cares a lot about the stability of your code. We want -Rust to be a rock-solid foundation you can build on, and if things were -constantly changing, that would be impossible. At the same time, if we can’t -experiment with new features, we may not find out important flaws until after -their release, when we can no longer change things.
-Our solution to this problem is what we call “stability without stagnation”, -and our guiding principle is this: you should never have to fear upgrading to a -new version of stable Rust. Each upgrade should be painless, but should also -bring you new features, fewer bugs, and faster compile times.
-Rust development operates on a train schedule. That is, all development is -done in the main branch of the Rust repository. Releases follow a software -release train model, which has been used by Cisco IOS and other software -projects. There are three release channels for Rust:
-Most Rust developers primarily use the stable channel, but those who want to -try out experimental new features may use nightly or beta.
-Here’s an example of how the development and release process works: let’s -assume that the Rust team is working on the release of Rust 1.5. That release -happened in December of 2015, but it will provide us with realistic version -numbers. A new feature is added to Rust: a new commit lands on the main -branch. Each night, a new nightly version of Rust is produced. Every day is a -release day, and these releases are created by our release infrastructure -automatically. So as time passes, our releases look like this, once a night:
-nightly: * - - * - - *
-
-Every six weeks, it’s time to prepare a new release! The beta branch of the
-Rust repository branches off from the main branch used by nightly. Now,
-there are two releases:
nightly: * - - * - - *
- |
-beta: *
-
-Most Rust users do not use beta releases actively, but test against beta in -their CI system to help Rust discover possible regressions. In the meantime, -there’s still a nightly release every night:
-nightly: * - - * - - * - - * - - *
- |
-beta: *
-
-Let’s say a regression is found. Good thing we had some time to test the beta
-release before the regression snuck into a stable release! The fix is applied
-to the main branch, so that nightly is fixed, and then the fix is backported to
-the beta branch, and a new release of beta is produced:
nightly: * - - * - - * - - * - - * - - *
- |
-beta: * - - - - - - - - *
-
-Six weeks after the first beta was created, it’s time for a stable release! The
-stable branch is produced from the beta branch:
nightly: * - - * - - * - - * - - * - - * - * - *
- |
-beta: * - - - - - - - - *
- |
-stable: *
-
-Hooray! Rust 1.5 is done! However, we’ve forgotten one thing: because the six
-weeks have gone by, we also need a new beta of the next version of Rust, 1.6.
-So after stable branches off of beta, the next version of beta branches
-off of nightly again:
nightly: * - - * - - * - - * - - * - - * - * - *
- | |
-beta: * - - - - - - - - * *
- |
-stable: *
-
-This is called the “train model” because every six weeks, a release “leaves the -station”, but still has to take a journey through the beta channel before it -arrives as a stable release.
-Rust releases every six weeks, like clockwork. If you know the date of one Rust -release, you can know the date of the next one: it’s six weeks later. A nice -aspect of having releases scheduled every six weeks is that the next train is -coming soon. If a feature happens to miss a particular release, there’s no need -to worry: another one is happening in a short time! This helps reduce pressure -to sneak possibly unpolished features in close to the release deadline.
-Thanks to this process, you can always check out the next build of Rust and
-verify for yourself that it’s easy to upgrade to: if a beta release doesn’t
-work as expected, you can report it to the team and get it fixed before the
-next stable release happens! Breakage in a beta release is relatively rare, but
-rustc is still a piece of software, and bugs do exist.
The Rust project supports the most recent stable version. When a new stable -version is released, the old version reaches its end of life (EOL). This means -each version is supported for six weeks.
-There’s one more catch with this release model: unstable features. Rust uses a -technique called “feature flags” to determine what features are enabled in a -given release. If a new feature is under active development, it lands on the -main branch, and therefore, in nightly, but behind a feature flag. If you, as -a user, wish to try out the work-in-progress feature, you can, but you must be -using a nightly release of Rust and annotate your source code with the -appropriate flag to opt in.
-If you’re using a beta or stable release of Rust, you can’t use any feature -flags. This is the key that allows us to get practical use with new features -before we declare them stable forever. Those who wish to opt into the bleeding -edge can do so, and those who want a rock-solid experience can stick with -stable and know that their code won’t break. Stability without stagnation.
-This book only contains information about stable features, as in-progress -features are still changing, and surely they’ll be different between when this -book was written and when they get enabled in stable builds. You can find -documentation for nightly-only features online.
-Rustup makes it easy to change between different release channels of Rust, on a -global or per-project basis. By default, you’ll have stable Rust installed. To -install nightly, for example:
-$ rustup toolchain install nightly
-
-You can see all of the toolchains (releases of Rust and associated
-components) you have installed with rustup as well. Here’s an example on one
-of your authors’ Windows computer:
> rustup toolchain list
-stable-x86_64-pc-windows-msvc (default)
-beta-x86_64-pc-windows-msvc
-nightly-x86_64-pc-windows-msvc
-
-As you can see, the stable toolchain is the default. Most Rust users use stable
-most of the time. You might want to use stable most of the time, but use
-nightly on a specific project, because you care about a cutting-edge feature.
-To do so, you can use rustup override in that project’s directory to set the
-nightly toolchain as the one rustup should use when you’re in that directory:
$ cd ~/projects/needs-nightly
-$ rustup override set nightly
-
-Now, every time you call rustc or cargo inside of
-~/projects/needs-nightly, rustup will make sure that you are using nightly
-Rust, rather than your default of stable Rust. This comes in handy when you
-have a lot of Rust projects!
So how do you learn about these new features? Rust’s development model follows -a Request For Comments (RFC) process. If you’d like an improvement in Rust, -you can write up a proposal, called an RFC.
-Anyone can write RFCs to improve Rust, and the proposals are reviewed and -discussed by the Rust team, which is comprised of many topic subteams. There’s -a full list of the teams on Rust’s website, which includes teams for -each area of the project: language design, compiler implementation, -infrastructure, documentation, and more. The appropriate team reads the -proposal and the comments, writes some comments of their own, and eventually, -there’s consensus to accept or reject the feature.
-If the feature is accepted, an issue is opened on the Rust repository, and -someone can implement it. The person who implements it very well may not be the -person who proposed the feature in the first place! When the implementation is -ready, it lands on the main branch behind a feature gate, as we discussed in -the “Unstable Features” section.
-After some time, once Rust developers who use nightly releases have been able -to try out the new feature, team members will discuss the feature, how it’s -worked out on nightly, and decide if it should make it into stable Rust or not. -If the decision is to move forward, the feature gate is removed, and the -feature is now considered stable! It rides the trains into a new stable release -of Rust.
- -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
-Note: This edition of the book is the same as The Rust Programming -Language available in print and ebook format from No Starch -Press.
-Welcome to The Rust Programming Language, an introductory book about Rust. -The Rust programming language helps you write faster, more reliable software. -High-level ergonomics and low-level control are often at odds in programming -language design; Rust challenges that conflict. Through balancing powerful -technical capacity and a great developer experience, Rust gives you the option -to control low-level details (such as memory usage) without all the hassle -traditionally associated with such control.
-Rust is ideal for many people for a variety of reasons. Let’s look at a few of -the most important groups.
-Rust is proving to be a productive tool for collaborating among large teams of -developers with varying levels of systems programming knowledge. Low-level code -is prone to various subtle bugs, which in most other languages can only be -caught through extensive testing and careful code review by experienced -developers. In Rust, the compiler plays a gatekeeper role by refusing to -compile code with these elusive bugs, including concurrency bugs. By working -alongside the compiler, the team can spend its time focusing on the program’s -logic rather than chasing down bugs.
-Rust also brings contemporary developer tools to the systems programming world:
-rustfmt formatting tool ensures a consistent coding style across
-developers.By using these and other tools in the Rust ecosystem, developers can be -productive while writing systems-level code.
-Rust is for students and those who are interested in learning about systems -concepts. Using Rust, many people have learned about topics like operating -systems development. The community is very welcoming and happy to answer -students’ questions. Through efforts such as this book, the Rust teams want to -make systems concepts more accessible to more people, especially those new to -programming.
-Hundreds of companies, large and small, use Rust in production for a variety of -tasks, including command line tools, web services, DevOps tooling, embedded -devices, audio and video analysis and transcoding, cryptocurrencies, -bioinformatics, search engines, Internet of Things applications, machine -learning, and even major parts of the Firefox web browser.
-Rust is for people who want to build the Rust programming language, community, -developer tools, and libraries. We’d love to have you contribute to the Rust -language.
-Rust is for people who crave speed and stability in a language. By speed, we -mean both how quickly Rust code can run and the speed at which Rust lets you -write programs. The Rust compiler’s checks ensure stability through feature -additions and refactoring. This is in contrast to the brittle legacy code in -languages without these checks, which developers are often afraid to modify. By -striving for zero-cost abstractions—higher-level features that compile to -lower-level code as fast as code written manually—Rust endeavors to make safe -code be fast code as well.
-The Rust language hopes to support many other users as well; those mentioned -here are merely some of the biggest stakeholders. Overall, Rust’s greatest -ambition is to eliminate the trade-offs that programmers have accepted for -decades by providing safety and productivity, speed and ergonomics. Give -Rust a try, and see if its choices work for you.
-This book assumes that you’ve written code in another programming language, but -it doesn’t make any assumptions about which one. We’ve tried to make the -material broadly accessible to those from a wide variety of programming -backgrounds. We don’t spend a lot of time talking about what programming is -or how to think about it. If you’re entirely new to programming, you would be -better served by reading a book that specifically provides an introduction to -programming.
-In general, this book assumes that you’re reading it in sequence from front to -back. Later chapters build on concepts in earlier chapters, and earlier -chapters might not delve into details on a particular topic but will revisit -the topic in a later chapter.
-You’ll find two kinds of chapters in this book: concept chapters and project -chapters. In concept chapters, you’ll learn about an aspect of Rust. In project -chapters, we’ll build small programs together, applying what you’ve learned so -far. Chapter 2, Chapter 12, and Chapter 21 are project chapters; the rest are -concept chapters.
-Chapter 1 explains how to install Rust, how to write a “Hello, world!” -program, and how to use Cargo, Rust’s package manager and build tool. Chapter -2 is a hands-on introduction to writing a program in Rust, having you build -up a number-guessing game. Here, we cover concepts at a high level, and later -chapters will provide additional detail. If you want to get your hands dirty -right away, Chapter 2 is the place for that. If you’re a particularly -meticulous learner who prefers to learn every detail before moving on to the -next, you might want to skip Chapter 2 and go straight to Chapter 3, which -covers Rust features that are similar to those of other programming languages; -then, you can return to Chapter 2 when you’d like to work on a project applying -the details you’ve learned.
-In Chapter 4, you’ll learn about Rust’s ownership system. Chapter 5
-discusses structs and methods. Chapter 6 covers enums, match expressions,
-and the if let and let...else control flow constructs. You’ll use structs
-and enums to make custom types.
In Chapter 7, you’ll learn about Rust’s module system and about privacy -rules for organizing your code and its public application programming interface -(API). Chapter 8 discusses some common collection data structures that the -standard library provides: vectors, strings, and hash maps. Chapter 9 -explores Rust’s error-handling philosophy and techniques.
-Chapter 10 digs into generics, traits, and lifetimes, which give you the
-power to define code that applies to multiple types. Chapter 11 is all
-about testing, which even with Rust’s safety guarantees is necessary to ensure
-that your program’s logic is correct. In Chapter 12, we’ll build our own
-implementation of a subset of functionality from the grep command line tool
-that searches for text within files. For this, we’ll use many of the concepts
-we discussed in the previous chapters.
Chapter 13 explores closures and iterators: features of Rust that come from -functional programming languages. In Chapter 14, we’ll examine Cargo in -more depth and talk about best practices for sharing your libraries with -others. Chapter 15 discusses smart pointers that the standard library -provides and the traits that enable their functionality.
-In Chapter 16, we’ll walk through different models of concurrent -programming and talk about how Rust helps you program in multiple threads -fearlessly. In Chapter 17, we build on that by exploring Rust’s async and -await syntax, along with tasks, futures, and streams, and the lightweight -concurrency model they enable.
-Chapter 18 looks at how Rust idioms compare to object-oriented programming -principles you might be familiar with. Chapter 19 is a reference on -patterns and pattern matching, which are powerful ways of expressing ideas -throughout Rust programs. Chapter 20 contains a smorgasbord of advanced -topics of interest, including unsafe Rust, macros, and more about lifetimes, -traits, types, functions, and closures.
-In Chapter 21, we’ll complete a project in which we’ll implement a -low-level multithreaded web server!
-Finally, some appendixes contain useful information about the language in a -more reference-like format. Appendix A covers Rust’s keywords, Appendix -B covers Rust’s operators and symbols, Appendix C covers derivable traits -provided by the standard library, Appendix D covers some useful development -tools, and Appendix E explains Rust editions. In Appendix F, you can -find translations of the book, and in Appendix G we’ll cover how Rust is -made and what nightly Rust is.
-There is no wrong way to read this book: If you want to skip ahead, go for it! -You might have to jump back to earlier chapters if you experience any -confusion. But do whatever works for you.
--
An important part of the process of learning Rust is learning how to read the -error messages the compiler displays: These will guide you toward working code. -As such, we’ll provide many examples that don’t compile along with the error -message the compiler will show you in each situation. Know that if you enter -and run a random example, it may not compile! Make sure you read the -surrounding text to see whether the example you’re trying to run is meant to -error. In most situations, we’ll lead you to the correct version of any code -that doesn’t compile. Ferris will also help you distinguish code that isn’t -meant to work:
-| Ferris | Meaning |
|---|---|
| This code does not compile! | |
| This code panics! | |
| This code does not produce the desired behavior. |
In most situations, we’ll lead you to the correct version of any code that -doesn’t compile.
-The source files from which this book is generated can be found on -GitHub.
- -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
-Let’s start your Rust journey! There’s a lot to learn, but every journey starts -somewhere. In this chapter, we’ll discuss:
-Hello, world!cargo, Rust’s package manager and build systemPress ← or → to navigate between chapters
-Press S or / to search in the book
-Press ? to show this help
-Press Esc to hide this help
-The first step is to install Rust. We’ll download Rust through rustup, a
-command line tool for managing Rust versions and associated tools. You’ll need
-an internet connection for the download.
Note: If you prefer not to use rustup for some reason, please see the
-Other Rust Installation Methods page for more options.
The following steps install the latest stable version of the Rust compiler. -Rust’s stability guarantees ensure that all the examples in the book that -compile will continue to compile with newer Rust versions. The output might -differ slightly between versions because Rust often improves error messages and -warnings. In other words, any newer, stable version of Rust you install using -these steps should work as expected with the content of this book.
-In this chapter and throughout the book, we’ll show some commands used in the
-terminal. Lines that you should enter in a terminal all start with $. You
-don’t need to type the $ character; it’s the command line prompt shown to
-indicate the start of each command. Lines that don’t start with $ typically
-show the output of the previous command. Additionally, PowerShell-specific
-examples will use > rather than $.
rustup on Linux or macOSIf you’re using Linux or macOS, open a terminal and enter the following command:
-$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
-
-The command downloads a script and starts the installation of the rustup
-tool, which installs the latest stable version of Rust. You might be prompted
-for your password. If the install is successful, the following line will appear:
Rust is installed now. Great!
-
-You will also need a linker, which is a program that Rust uses to join its -compiled outputs into one file. It is likely you already have one. If you get -linker errors, you should install a C compiler, which will typically include a -linker. A C compiler is also useful because some common Rust packages depend on -C code and will need a C compiler.
-On macOS, you can get a C compiler by running:
-$ xcode-select --install
-
-Linux users should generally install GCC or Clang, according to their
-distribution’s documentation. For example, if you use Ubuntu, you can install
-the build-essential package.
rustup on WindowsOn Windows, go to https://www.rust-lang.org/tools/install and follow the instructions for installing Rust. At some point in the -installation, you’ll be prompted to install Visual Studio. This provides a -linker and the native libraries needed to compile programs. If you need more -help with this step, see -https://rust-lang.github.io/rustup/installation/windows-msvc.html.
-The rest of this book uses commands that work in both cmd.exe and PowerShell. -If there are specific differences, we’ll explain which to use.
-To check whether you have Rust installed correctly, open a shell and enter this -line:
-$ rustc --version
-
-You should see the version number, commit hash, and commit date for the latest -stable version that has been released, in the following format:
-rustc x.y.z (abcabcabc yyyy-mm-dd)
-
-If you see this information, you have installed Rust successfully! If you don’t
-see this information, check that Rust is in your %PATH% system variable as
-follows.
In Windows CMD, use:
-> echo %PATH%
-
-In PowerShell, use:
-> echo $env:Path
-
-In Linux and macOS, use:
-$ echo $PATH
-
-If that’s all correct and Rust still isn’t working, there are a number of -places you can get help. Find out how to get in touch with other Rustaceans (a -silly nickname we call ourselves) on the community page.
-Once Rust is installed via rustup, updating to a newly released version is
-easy. From your shell, run the following update script:
$ rustup update
-
-To uninstall Rust and rustup, run the following uninstall script from your
-shell:
$ rustup self uninstall
-
-
-
-The installation of Rust also includes a local copy of the documentation so
-that you can read it offline. Run rustup doc to open the local documentation
-in your browser.
Any time a type or function is provided by the standard library and you’re not -sure what it does or how to use it, use the application programming interface -(API) documentation to find out!
- - -This book makes no assumptions about what tools you use to author Rust code. -Just about any text editor will get the job done! However, many text editors and -integrated development environments (IDEs) have built-in support for Rust. You -can always find a fairly current list of many editors and IDEs on the tools -page on the Rust website.
-In several examples, we will use Rust packages beyond the standard library. To
-work through those examples, you will either need to have an internet connection
-or to have downloaded those dependencies ahead of time. To download the
-dependencies ahead of time, you can run the following commands. (We’ll explain
-what cargo is and what each of these commands does in detail later.)
$ cargo new get-dependencies
-$ cd get-dependencies
-$ cargo add rand@0.8.5 trpl@0.2.0
-
-This will cache the downloads for these packages so you will not need to
-download them later. Once you have run this command, you do not need to keep the
-get-dependencies folder. If you have run this command, you can use the
---offline flag with all cargo commands in the rest of the book to use these
-cached versions instead of attempting to use the network.
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
-Now that you’ve installed Rust, it’s time to write your first Rust program.
-It’s traditional when learning a new language to write a little program that
-prints the text Hello, world! to the screen, so we’ll do the same here!
Note: This book assumes basic familiarity with the command line. Rust makes
-no specific demands about your editing or tooling or where your code lives, so
-if you prefer to use an IDE instead of the command line, feel free to use your
-favorite IDE. Many IDEs now have some degree of Rust support; check the IDE’s
-documentation for details. The Rust team has been focusing on enabling great
-IDE support via rust-analyzer. See Appendix D
-for more details.
You’ll start by making a directory to store your Rust code. It doesn’t matter -to Rust where your code lives, but for the exercises and projects in this book, -we suggest making a projects directory in your home directory and keeping all -your projects there.
-Open a terminal and enter the following commands to make a projects directory -and a directory for the “Hello, world!” project within the projects directory.
-For Linux, macOS, and PowerShell on Windows, enter this:
-$ mkdir ~/projects
-$ cd ~/projects
-$ mkdir hello_world
-$ cd hello_world
-
-For Windows CMD, enter this:
-> mkdir "%USERPROFILE%\projects"
-> cd /d "%USERPROFILE%\projects"
-> mkdir hello_world
-> cd hello_world
-
-
-
-Next, make a new source file and call it main.rs. Rust files always end with -the .rs extension. If you’re using more than one word in your filename, the -convention is to use an underscore to separate them. For example, use -hello_world.rs rather than helloworld.rs.
-Now open the main.rs file you just created and enter the code in Listing 1-1.
-fn main() {
- println!("Hello, world!");
-}
-Hello, world!Save the file and go back to your terminal window in the -~/projects/hello_world directory. On Linux or macOS, enter the following -commands to compile and run the file:
-$ rustc main.rs
-$ ./main
-Hello, world!
-
-On Windows, enter the command .\main instead of ./main:
> rustc main.rs
-> .\main
-Hello, world!
-
-Regardless of your operating system, the string Hello, world! should print to
-the terminal. If you don’t see this output, refer back to the
-“Troubleshooting” part of the Installation
-section for ways to get help.
If Hello, world! did print, congratulations! You’ve officially written a Rust
-program. That makes you a Rust programmer—welcome!
Let’s review this “Hello, world!” program in detail. Here’s the first piece of -the puzzle:
-fn main() {
-
-}
-These lines define a function named main. The main function is special: It
-is always the first code that runs in every executable Rust program. Here, the
-first line declares a function named main that has no parameters and returns
-nothing. If there were parameters, they would go inside the parentheses (()).
The function body is wrapped in {}. Rust requires curly brackets around all
-function bodies. It’s good style to place the opening curly bracket on the same
-line as the function declaration, adding one space in between.
Note: If you want to stick to a standard style across Rust projects, you can
-use an automatic formatter tool called rustfmt to format your code in a
-particular style (more on rustfmt in
-Appendix D). The Rust team has included this tool
-with the standard Rust distribution, as rustc is, so it should already be
-installed on your computer!
The body of the main function holds the following code:
#![allow(unused)]
-fn main() {
-println!("Hello, world!");
-}
-This line does all the work in this little program: It prints text to the -screen. There are three important details to notice here.
-First, println! calls a Rust macro. If it had called a function instead, it
-would be entered as println (without the !). Rust macros are a way to write
-code that generates code to extend Rust syntax, and we’ll discuss them in more
-detail in Chapter 20. For now, you just need to
-know that using a ! means that you’re calling a macro instead of a normal
-function and that macros don’t always follow the same rules as functions.
Second, you see the "Hello, world!" string. We pass this string as an argument
-to println!, and the string is printed to the screen.
Third, we end the line with a semicolon (;), which indicates that this
-expression is over, and the next one is ready to begin. Most lines of Rust code
-end with a semicolon.
You’ve just run a newly created program, so let’s examine each step in the -process.
-Before running a Rust program, you must compile it using the Rust compiler by
-entering the rustc command and passing it the name of your source file, like
-this:
$ rustc main.rs
-
-If you have a C or C++ background, you’ll notice that this is similar to gcc
-or clang. After compiling successfully, Rust outputs a binary executable.
On Linux, macOS, and PowerShell on Windows, you can see the executable by
-entering the ls command in your shell:
$ ls
-main main.rs
-
-On Linux and macOS, you’ll see two files. With PowerShell on Windows, you’ll -see the same three files that you would see using CMD. With CMD on Windows, you -would enter the following:
-> dir /B %= the /B option says to only show the file names =%
-main.exe
-main.pdb
-main.rs
-
-This shows the source code file with the .rs extension, the executable file -(main.exe on Windows, but main on all other platforms), and, when using -Windows, a file containing debugging information with the .pdb extension. -From here, you run the main or main.exe file, like this:
-$ ./main # or .\main on Windows
-
-If your main.rs is your “Hello, world!” program, this line prints Hello, world! to your terminal.
If you’re more familiar with a dynamic language, such as Ruby, Python, or -JavaScript, you might not be used to compiling and running a program as -separate steps. Rust is an ahead-of-time compiled language, meaning you can -compile a program and give the executable to someone else, and they can run it -even without having Rust installed. If you give someone a .rb, .py, or -.js file, they need to have a Ruby, Python, or JavaScript implementation -installed (respectively). But in those languages, you only need one command to -compile and run your program. Everything is a trade-off in language design.
-Just compiling with rustc is fine for simple programs, but as your project
-grows, you’ll want to manage all the options and make it easy to share your
-code. Next, we’ll introduce you to the Cargo tool, which will help you write
-real-world Rust programs.
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
-Cargo is Rust’s build system and package manager. Most Rustaceans use this tool -to manage their Rust projects because Cargo handles a lot of tasks for you, -such as building your code, downloading the libraries your code depends on, and -building those libraries. (We call the libraries that your code needs -dependencies.)
-The simplest Rust programs, like the one we’ve written so far, don’t have any -dependencies. If we had built the “Hello, world!” project with Cargo, it would -only use the part of Cargo that handles building your code. As you write more -complex Rust programs, you’ll add dependencies, and if you start a project -using Cargo, adding dependencies will be much easier to do.
-Because the vast majority of Rust projects use Cargo, the rest of this book -assumes that you’re using Cargo too. Cargo comes installed with Rust if you -used the official installers discussed in the -“Installation” section. If you installed Rust -through some other means, check whether Cargo is installed by entering the -following in your terminal:
-$ cargo --version
-
-If you see a version number, you have it! If you see an error, such as command not found, look at the documentation for your method of installation to
-determine how to install Cargo separately.
Let’s create a new project using Cargo and look at how it differs from our -original “Hello, world!” project. Navigate back to your projects directory -(or wherever you decided to store your code). Then, on any operating system, -run the following:
-$ cargo new hello_cargo
-$ cd hello_cargo
-
-The first command creates a new directory and project called hello_cargo. -We’ve named our project hello_cargo, and Cargo creates its files in a -directory of the same name.
-Go into the hello_cargo directory and list the files. You’ll see that Cargo -has generated two files and one directory for us: a Cargo.toml file and a -src directory with a main.rs file inside.
-It has also initialized a new Git repository along with a .gitignore file.
-Git files won’t be generated if you run cargo new within an existing Git
-repository; you can override this behavior by using cargo new --vcs=git.
Note: Git is a common version control system. You can change cargo new to
-use a different version control system or no version control system by using
-the --vcs flag. Run cargo new --help to see the available options.
Open Cargo.toml in your text editor of choice. It should look similar to the -code in Listing 1-2.
-[package]
-name = "hello_cargo"
-version = "0.1.0"
-edition = "2024"
-
-[dependencies]
-
-cargo newThis file is in the TOML (Tom’s Obvious, Minimal -Language) format, which is Cargo’s configuration format.
-The first line, [package], is a section heading that indicates that the
-following statements are configuring a package. As we add more information to
-this file, we’ll add other sections.
The next three lines set the configuration information Cargo needs to compile
-your program: the name, the version, and the edition of Rust to use. We’ll talk
-about the edition key in Appendix E.
The last line, [dependencies], is the start of a section for you to list any
-of your project’s dependencies. In Rust, packages of code are referred to as
-crates. We won’t need any other crates for this project, but we will in the
-first project in Chapter 2, so we’ll use this dependencies section then.
Now open src/main.rs and take a look:
-Filename: src/main.rs
-fn main() {
- println!("Hello, world!");
-}
-Cargo has generated a “Hello, world!” program for you, just like the one we -wrote in Listing 1-1! So far, the differences between our project and the -project Cargo generated are that Cargo placed the code in the src directory, -and we have a Cargo.toml configuration file in the top directory.
-Cargo expects your source files to live inside the src directory. The -top-level project directory is just for README files, license information, -configuration files, and anything else not related to your code. Using Cargo -helps you organize your projects. There’s a place for everything, and -everything is in its place.
-If you started a project that doesn’t use Cargo, as we did with the “Hello,
-world!” project, you can convert it to a project that does use Cargo. Move the
-project code into the src directory and create an appropriate Cargo.toml
-file. One easy way to get that Cargo.toml file is to run cargo init, which
-will create it for you automatically.
Now let’s look at what’s different when we build and run the “Hello, world!” -program with Cargo! From your hello_cargo directory, build your project by -entering the following command:
-$ cargo build
- Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
- Finished dev [unoptimized + debuginfo] target(s) in 2.85 secs
-
-This command creates an executable file in target/debug/hello_cargo (or -target\debug\hello_cargo.exe on Windows) rather than in your current -directory. Because the default build is a debug build, Cargo puts the binary in -a directory named debug. You can run the executable with this command:
-$ ./target/debug/hello_cargo # or .\target\debug\hello_cargo.exe on Windows
-Hello, world!
-
-If all goes well, Hello, world! should print to the terminal. Running cargo build for the first time also causes Cargo to create a new file at the top
-level: Cargo.lock. This file keeps track of the exact versions of
-dependencies in your project. This project doesn’t have dependencies, so the
-file is a bit sparse. You won’t ever need to change this file manually; Cargo
-manages its contents for you.
We just built a project with cargo build and ran it with
-./target/debug/hello_cargo, but we can also use cargo run to compile the
-code and then run the resultant executable all in one command:
$ cargo run
- Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
- Running `target/debug/hello_cargo`
-Hello, world!
-
-Using cargo run is more convenient than having to remember to run cargo build and then use the whole path to the binary, so most developers use cargo run.
Notice that this time we didn’t see output indicating that Cargo was compiling
-hello_cargo. Cargo figured out that the files hadn’t changed, so it didn’t
-rebuild but just ran the binary. If you had modified your source code, Cargo
-would have rebuilt the project before running it, and you would have seen this
-output:
$ cargo run
- Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
- Finished dev [unoptimized + debuginfo] target(s) in 0.33 secs
- Running `target/debug/hello_cargo`
-Hello, world!
-
-Cargo also provides a command called cargo check. This command quickly checks
-your code to make sure it compiles but doesn’t produce an executable:
$ cargo check
- Checking hello_cargo v0.1.0 (file:///projects/hello_cargo)
- Finished dev [unoptimized + debuginfo] target(s) in 0.32 secs
-
-Why would you not want an executable? Often, cargo check is much faster than
-cargo build because it skips the step of producing an executable. If you’re
-continually checking your work while writing the code, using cargo check will
-speed up the process of letting you know if your project is still compiling! As
-such, many Rustaceans run cargo check periodically as they write their
-program to make sure it compiles. Then, they run cargo build when they’re
-ready to use the executable.
Let’s recap what we’ve learned so far about Cargo:
-cargo new.cargo build.cargo run.cargo check.An additional advantage of using Cargo is that the commands are the same no -matter which operating system you’re working on. So, at this point, we’ll no -longer provide specific instructions for Linux and macOS versus Windows.
-When your project is finally ready for release, you can use cargo build --release to compile it with optimizations. This command will create an
-executable in target/release instead of target/debug. The optimizations
-make your Rust code run faster, but turning them on lengthens the time it takes
-for your program to compile. This is why there are two different profiles: one
-for development, when you want to rebuild quickly and often, and another for
-building the final program you’ll give to a user that won’t be rebuilt
-repeatedly and that will run as fast as possible. If you’re benchmarking your
-code’s running time, be sure to run cargo build --release and benchmark with
-the executable in target/release.
With simple projects, Cargo doesn’t provide a lot of value over just using
-rustc, but it will prove its worth as your programs become more intricate.
-Once programs grow to multiple files or need a dependency, it’s much easier to
-let Cargo coordinate the build.
Even though the hello_cargo project is simple, it now uses much of the real
-tooling you’ll use in the rest of your Rust career. In fact, to work on any
-existing projects, you can use the following commands to check out the code
-using Git, change to that project’s directory, and build:
$ git clone example.org/someproject
-$ cd someproject
-$ cargo build
-
-For more information about Cargo, check out its documentation.
-You’re already off to a great start on your Rust journey! In this chapter, you -learned how to:
-rustup.rustc directly.This is a great time to build a more substantial program to get used to reading -and writing Rust code. So, in Chapter 2, we’ll build a guessing game program. -If you would rather start by learning how common programming concepts work in -Rust, see Chapter 3 and then return to Chapter 2.
- -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
-This chapter covers concepts that appear in almost every programming language -and how they work in Rust. Many programming languages have much in common at -their core. None of the concepts presented in this chapter are unique to Rust, -but we’ll discuss them in the context of Rust and explain the conventions -around using them.
-Specifically, you’ll learn about variables, basic types, functions, comments, -and control flow. These foundations will be in every Rust program, and learning -them early will give you a strong core to start from.
-The Rust language has a set of keywords that are reserved for use by the -language only, much as in other languages. Keep in mind that you cannot use -these words as names of variables or functions. Most of the keywords have -special meanings, and you’ll be using them to do various tasks in your Rust -programs; a few have no current functionality associated with them but have -been reserved for functionality that might be added to Rust in the future. You -can find the list of the keywords in Appendix A.
-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
-As mentioned in the “Storing Values with -Variables” section, by default, -variables are immutable. This is one of many nudges Rust gives you to write -your code in a way that takes advantage of the safety and easy concurrency that -Rust offers. However, you still have the option to make your variables mutable. -Let’s explore how and why Rust encourages you to favor immutability and why -sometimes you might want to opt out.
-When a variable is immutable, once a value is bound to a name, you can’t change
-that value. To illustrate this, generate a new project called variables in
-your projects directory by using cargo new variables.
Then, in your new variables directory, open src/main.rs and replace its -code with the following code, which won’t compile just yet:
-Filename: src/main.rs
-fn main() {
- let x = 5;
- println!("The value of x is: {x}");
- x = 6;
- println!("The value of x is: {x}");
-}
-Save and run the program using cargo run. You should receive an error message
-regarding an immutability error, as shown in this output:
$ cargo run
- Compiling variables v0.1.0 (file:///projects/variables)
-error[E0384]: cannot assign twice to immutable variable `x`
- --> src/main.rs:4:5
- |
-2 | let x = 5;
- | - first assignment to `x`
-3 | println!("The value of x is: {x}");
-4 | x = 6;
- | ^^^^^ cannot assign twice to immutable variable
- |
-help: consider making this binding mutable
- |
-2 | let mut x = 5;
- | +++
-
-For more information about this error, try `rustc --explain E0384`.
-error: could not compile `variables` (bin "variables") due to 1 previous error
-
-This example shows how the compiler helps you find errors in your programs. -Compiler errors can be frustrating, but really they only mean your program -isn’t safely doing what you want it to do yet; they do not mean that you’re -not a good programmer! Experienced Rustaceans still get compiler errors.
-You received the error message cannot assign twice to immutable variable `x` because you tried to assign a second value to the immutable x variable.
It’s important that we get compile-time errors when we attempt to change a -value that’s designated as immutable, because this very situation can lead to -bugs. If one part of our code operates on the assumption that a value will -never change and another part of our code changes that value, it’s possible -that the first part of the code won’t do what it was designed to do. The cause -of this kind of bug can be difficult to track down after the fact, especially -when the second piece of code changes the value only sometimes. The Rust -compiler guarantees that when you state that a value won’t change, it really -won’t change, so you don’t have to keep track of it yourself. Your code is thus -easier to reason through.
-But mutability can be very useful and can make code more convenient to write.
-Although variables are immutable by default, you can make them mutable by
-adding mut in front of the variable name as you did in Chapter
-2. Adding mut also conveys
-intent to future readers of the code by indicating that other parts of the code
-will be changing this variable’s value.
For example, let’s change src/main.rs to the following:
-Filename: src/main.rs
-fn main() {
- let mut x = 5;
- println!("The value of x is: {x}");
- x = 6;
- println!("The value of x is: {x}");
-}
-When we run the program now, we get this:
-$ cargo run
- Compiling variables v0.1.0 (file:///projects/variables)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
- Running `target/debug/variables`
-The value of x is: 5
-The value of x is: 6
-
-We’re allowed to change the value bound to x from 5 to 6 when mut is
-used. Ultimately, deciding whether to use mutability or not is up to you and
-depends on what you think is clearest in that particular situation.
Like immutable variables, constants are values that are bound to a name and -are not allowed to change, but there are a few differences between constants -and variables.
-First, you aren’t allowed to use mut with constants. Constants aren’t just
-immutable by default—they’re always immutable. You declare constants using the
-const keyword instead of the let keyword, and the type of the value must
-be annotated. We’ll cover types and type annotations in the next section,
-“Data Types”, so don’t worry about the details
-right now. Just know that you must always annotate the type.
Constants can be declared in any scope, including the global scope, which makes -them useful for values that many parts of code need to know about.
-The last difference is that constants may be set only to a constant expression, -not the result of a value that could only be computed at runtime.
-Here’s an example of a constant declaration:
-#![allow(unused)]
-fn main() {
-const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
-}
-The constant’s name is THREE_HOURS_IN_SECONDS, and its value is set to the
-result of multiplying 60 (the number of seconds in a minute) by 60 (the number
-of minutes in an hour) by 3 (the number of hours we want to count in this
-program). Rust’s naming convention for constants is to use all uppercase with
-underscores between words. The compiler is able to evaluate a limited set of
-operations at compile time, which lets us choose to write out this value in a
-way that’s easier to understand and verify, rather than setting this constant
-to the value 10,800. See the Rust Reference’s section on constant
-evaluation for more information on what operations can be used
-when declaring constants.
Constants are valid for the entire time a program runs, within the scope in -which they were declared. This property makes constants useful for values in -your application domain that multiple parts of the program might need to know -about, such as the maximum number of points any player of a game is allowed to -earn, or the speed of light.
-Naming hardcoded values used throughout your program as constants is useful in -conveying the meaning of that value to future maintainers of the code. It also -helps to have only one place in your code that you would need to change if the -hardcoded value needed to be updated in the future.
-As you saw in the guessing game tutorial in Chapter
-2, you can declare a
-new variable with the same name as a previous variable. Rustaceans say that the
-first variable is shadowed by the second, which means that the second
-variable is what the compiler will see when you use the name of the variable.
-In effect, the second variable overshadows the first, taking any uses of the
-variable name to itself until either it itself is shadowed or the scope ends.
-We can shadow a variable by using the same variable’s name and repeating the
-use of the let keyword as follows:
Filename: src/main.rs
-fn main() {
- let x = 5;
-
- let x = x + 1;
-
- {
- let x = x * 2;
- println!("The value of x in the inner scope is: {x}");
- }
-
- println!("The value of x is: {x}");
-}
-This program first binds x to a value of 5. Then, it creates a new variable
-x by repeating let x =, taking the original value and adding 1 so that
-the value of x is 6. Then, within an inner scope created with the curly
-brackets, the third let statement also shadows x and creates a new
-variable, multiplying the previous value by 2 to give x a value of 12.
-When that scope is over, the inner shadowing ends and x returns to being 6.
-When we run this program, it will output the following:
$ cargo run
- Compiling variables v0.1.0 (file:///projects/variables)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
- Running `target/debug/variables`
-The value of x in the inner scope is: 12
-The value of x is: 6
-
-Shadowing is different from marking a variable as mut because we’ll get a
-compile-time error if we accidentally try to reassign to this variable without
-using the let keyword. By using let, we can perform a few transformations
-on a value but have the variable be immutable after those transformations have
-completed.
The other difference between mut and shadowing is that because we’re
-effectively creating a new variable when we use the let keyword again, we can
-change the type of the value but reuse the same name. For example, say our
-program asks a user to show how many spaces they want between some text by
-inputting space characters, and then we want to store that input as a number:
fn main() {
- let spaces = " ";
- let spaces = spaces.len();
-}
-The first spaces variable is a string type, and the second spaces variable
-is a number type. Shadowing thus spares us from having to come up with
-different names, such as spaces_str and spaces_num; instead, we can reuse
-the simpler spaces name. However, if we try to use mut for this, as shown
-here, we’ll get a compile-time error:
fn main() {
- let mut spaces = " ";
- spaces = spaces.len();
-}
-The error says we’re not allowed to mutate a variable’s type:
-$ cargo run
- Compiling variables v0.1.0 (file:///projects/variables)
-error[E0308]: mismatched types
- --> src/main.rs:3:14
- |
-2 | let mut spaces = " ";
- | ----- expected due to this value
-3 | spaces = spaces.len();
- | ^^^^^^^^^^^^ expected `&str`, found `usize`
-
-For more information about this error, try `rustc --explain E0308`.
-error: could not compile `variables` (bin "variables") due to 1 previous error
-
-Now that we’ve explored how variables work, let’s look at more data types they -can have.
- -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
-Every value in Rust is of a certain data type, which tells Rust what kind of -data is being specified so that it knows how to work with that data. We’ll look -at two data type subsets: scalar and compound.
-Keep in mind that Rust is a statically typed language, which means that it
-must know the types of all variables at compile time. The compiler can usually
-infer what type we want to use based on the value and how we use it. In cases
-when many types are possible, such as when we converted a String to a numeric
-type using parse in the “Comparing the Guess to the Secret
-Number” section in
-Chapter 2, we must add a type annotation, like this:
#![allow(unused)]
-fn main() {
-let guess: u32 = "42".parse().expect("Not a number!");
-}
-If we don’t add the : u32 type annotation shown in the preceding code, Rust
-will display the following error, which means the compiler needs more
-information from us to know which type we want to use:
$ cargo build
- Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
-error[E0284]: type annotations needed
- --> src/main.rs:2:9
- |
-2 | let guess = "42".parse().expect("Not a number!");
- | ^^^^^ ----- type must be known at this point
- |
- = note: cannot satisfy `<_ as FromStr>::Err == _`
-help: consider giving `guess` an explicit type
- |
-2 | let guess: /* Type */ = "42".parse().expect("Not a number!");
- | ++++++++++++
-
-For more information about this error, try `rustc --explain E0284`.
-error: could not compile `no_type_annotations` (bin "no_type_annotations") due to 1 previous error
-
-You’ll see different type annotations for other data types.
-A scalar type represents a single value. Rust has four primary scalar types: -integers, floating-point numbers, Booleans, and characters. You may recognize -these from other programming languages. Let’s jump into how they work in Rust.
-An integer is a number without a fractional component. We used one integer
-type in Chapter 2, the u32 type. This type declaration indicates that the
-value it’s associated with should be an unsigned integer (signed integer types
-start with i instead of u) that takes up 32 bits of space. Table 3-1 shows
-the built-in integer types in Rust. We can use any of these variants to declare
-the type of an integer value.
Table 3-1: Integer Types in Rust
-| Length | Signed | Unsigned |
|---|---|---|
| 8-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| 128-bit | i128 | u128 |
| Architecture-dependent | isize | usize |
Each variant can be either signed or unsigned and has an explicit size. -Signed and unsigned refer to whether it’s possible for the number to be -negative—in other words, whether the number needs to have a sign with it -(signed) or whether it will only ever be positive and can therefore be -represented without a sign (unsigned). It’s like writing numbers on paper: When -the sign matters, a number is shown with a plus sign or a minus sign; however, -when it’s safe to assume the number is positive, it’s shown with no sign. -Signed numbers are stored using two’s complement representation.
-Each signed variant can store numbers from −(2n − 1) to 2n −
-1 − 1 inclusive, where n is the number of bits that variant uses. So, an
-i8 can store numbers from −(27) to 27 − 1, which equals
-−128 to 127. Unsigned variants can store numbers from 0 to 2n − 1,
-so a u8 can store numbers from 0 to 28 − 1, which equals 0 to 255.
Additionally, the isize and usize types depend on the architecture of the
-computer your program is running on: 64 bits if you’re on a 64-bit architecture
-and 32 bits if you’re on a 32-bit architecture.
You can write integer literals in any of the forms shown in Table 3-2. Note
-that number literals that can be multiple numeric types allow a type suffix,
-such as 57u8, to designate the type. Number literals can also use _ as a
-visual separator to make the number easier to read, such as 1_000, which will
-have the same value as if you had specified 1000.
Table 3-2: Integer Literals in Rust
-| Number literals | Example |
|---|---|
| Decimal | 98_222 |
| Hex | 0xff |
| Octal | 0o77 |
| Binary | 0b1111_0000 |
Byte (u8 only) | b'A' |
So how do you know which type of integer to use? If you’re unsure, Rust’s
-defaults are generally good places to start: Integer types default to i32.
-The primary situation in which you’d use isize or usize is when indexing
-some sort of collection.
Let’s say you have a variable of type u8 that can hold values between 0 and
-255. If you try to change the variable to a value outside that range, such as
-256, integer overflow will occur, which can result in one of two behaviors.
-When you’re compiling in debug mode, Rust includes checks for integer overflow
-that cause your program to panic at runtime if this behavior occurs. Rust
-uses the term panicking when a program exits with an error; we’ll discuss
-panics in more depth in the “Unrecoverable Errors with
-panic!” section in Chapter
-9.
When you’re compiling in release mode with the --release flag, Rust does
-not include checks for integer overflow that cause panics. Instead, if
-overflow occurs, Rust performs two’s complement wrapping. In short, values
-greater than the maximum value the type can hold “wrap around” to the minimum
-of the values the type can hold. In the case of a u8, the value 256 becomes
-0, the value 257 becomes 1, and so on. The program won’t panic, but the
-variable will have a value that probably isn’t what you were expecting it to
-have. Relying on integer overflow’s wrapping behavior is considered an error.
To explicitly handle the possibility of overflow, you can use these families -of methods provided by the standard library for primitive numeric types:
-wrapping_* methods, such as wrapping_add.None value if there is overflow with the checked_* methods.overflowing_* methods.saturating_*
-methods.Rust also has two primitive types for floating-point numbers, which are
-numbers with decimal points. Rust’s floating-point types are f32 and f64,
-which are 32 bits and 64 bits in size, respectively. The default type is f64
-because on modern CPUs, it’s roughly the same speed as f32 but is capable of
-more precision. All floating-point types are signed.
Here’s an example that shows floating-point numbers in action:
-Filename: src/main.rs
-fn main() {
- let x = 2.0; // f64
-
- let y: f32 = 3.0; // f32
-}
-Floating-point numbers are represented according to the IEEE-754 standard.
-Rust supports the basic mathematical operations you’d expect for all the number
-types: addition, subtraction, multiplication, division, and remainder. Integer
-division truncates toward zero to the nearest integer. The following code shows
-how you’d use each numeric operation in a let statement:
Filename: src/main.rs
-fn main() {
- // addition
- let sum = 5 + 10;
-
- // subtraction
- let difference = 95.5 - 4.3;
-
- // multiplication
- let product = 4 * 30;
-
- // division
- let quotient = 56.7 / 32.2;
- let truncated = -5 / 3; // Results in -1
-
- // remainder
- let remainder = 43 % 5;
-}
-Each expression in these statements uses a mathematical operator and evaluates -to a single value, which is then bound to a variable. Appendix -B contains a list of all operators that Rust -provides.
-As in most other programming languages, a Boolean type in Rust has two possible
-values: true and false. Booleans are one byte in size. The Boolean type in
-Rust is specified using bool. For example:
Filename: src/main.rs
-fn main() {
- let t = true;
-
- let f: bool = false; // with explicit type annotation
-}
-The main way to use Boolean values is through conditionals, such as an if
-expression. We’ll cover how if expressions work in Rust in the “Control
-Flow” section.
Rust’s char type is the language’s most primitive alphabetic type. Here are
-some examples of declaring char values:
Filename: src/main.rs
-fn main() {
- let c = 'z';
- let z: char = 'ℤ'; // with explicit type annotation
- let heart_eyed_cat = '😻';
-}
-Note that we specify char literals with single quotation marks, as opposed to
-string literals, which use double quotation marks. Rust’s char type is 4
-bytes in size and represents a Unicode scalar value, which means it can
-represent a lot more than just ASCII. Accented letters; Chinese, Japanese, and
-Korean characters; emojis; and zero-width spaces are all valid char values in
-Rust. Unicode scalar values range from U+0000 to U+D7FF and U+E000 to
-U+10FFFF inclusive. However, a “character” isn’t really a concept in Unicode,
-so your human intuition for what a “character” is may not match up with what a
-char is in Rust. We’ll discuss this topic in detail in “Storing UTF-8
-Encoded Text with Strings” in Chapter 8.
Compound types can group multiple values into one type. Rust has two -primitive compound types: tuples and arrays.
-A tuple is a general way of grouping together a number of values with a -variety of types into one compound type. Tuples have a fixed length: Once -declared, they cannot grow or shrink in size.
-We create a tuple by writing a comma-separated list of values inside -parentheses. Each position in the tuple has a type, and the types of the -different values in the tuple don’t have to be the same. We’ve added optional -type annotations in this example:
-Filename: src/main.rs
-fn main() {
- let tup: (i32, f64, u8) = (500, 6.4, 1);
-}
-The variable tup binds to the entire tuple because a tuple is considered a
-single compound element. To get the individual values out of a tuple, we can
-use pattern matching to destructure a tuple value, like this:
Filename: src/main.rs
-fn main() {
- let tup = (500, 6.4, 1);
-
- let (x, y, z) = tup;
-
- println!("The value of y is: {y}");
-}
-This program first creates a tuple and binds it to the variable tup. It then
-uses a pattern with let to take tup and turn it into three separate
-variables, x, y, and z. This is called destructuring because it breaks
-the single tuple into three parts. Finally, the program prints the value of
-y, which is 6.4.
We can also access a tuple element directly by using a period (.) followed by
-the index of the value we want to access. For example:
Filename: src/main.rs
-fn main() {
- let x: (i32, f64, u8) = (500, 6.4, 1);
-
- let five_hundred = x.0;
-
- let six_point_four = x.1;
-
- let one = x.2;
-}
-This program creates the tuple x and then accesses each element of the tuple
-using their respective indices. As with most programming languages, the first
-index in a tuple is 0.
The tuple without any values has a special name, unit. This value and its
-corresponding type are both written () and represent an empty value or an
-empty return type. Expressions implicitly return the unit value if they don’t
-return any other value.
Another way to have a collection of multiple values is with an array. Unlike -a tuple, every element of an array must have the same type. Unlike arrays in -some other languages, arrays in Rust have a fixed length.
-We write the values in an array as a comma-separated list inside square -brackets:
-Filename: src/main.rs
-fn main() {
- let a = [1, 2, 3, 4, 5];
-}
-Arrays are useful when you want your data allocated on the stack, the same as -the other types we have seen so far, rather than the heap (we will discuss the -stack and the heap more in Chapter 4) or when -you want to ensure that you always have a fixed number of elements. An array -isn’t as flexible as the vector type, though. A vector is a similar collection -type provided by the standard library that is allowed to grow or shrink in -size because its contents live on the heap. If you’re unsure whether to use an -array or a vector, chances are you should use a vector. Chapter -8 discusses vectors in more detail.
-However, arrays are more useful when you know the number of elements will not -need to change. For example, if you were using the names of the month in a -program, you would probably use an array rather than a vector because you know -it will always contain 12 elements:
-#![allow(unused)]
-fn main() {
-let months = ["January", "February", "March", "April", "May", "June", "July",
- "August", "September", "October", "November", "December"];
-}
-You write an array’s type using square brackets with the type of each element, -a semicolon, and then the number of elements in the array, like so:
-#![allow(unused)]
-fn main() {
-let a: [i32; 5] = [1, 2, 3, 4, 5];
-}
-Here, i32 is the type of each element. After the semicolon, the number 5
-indicates the array contains five elements.
You can also initialize an array to contain the same value for each element by -specifying the initial value, followed by a semicolon, and then the length of -the array in square brackets, as shown here:
-#![allow(unused)]
-fn main() {
-let a = [3; 5];
-}
-The array named a will contain 5 elements that will all be set to the value
-3 initially. This is the same as writing let a = [3, 3, 3, 3, 3]; but in a
-more concise way.
An array is a single chunk of memory of a known, fixed size that can be -allocated on the stack. You can access elements of an array using indexing, -like this:
-Filename: src/main.rs
-fn main() {
- let a = [1, 2, 3, 4, 5];
-
- let first = a[0];
- let second = a[1];
-}
-In this example, the variable named first will get the value 1 because that
-is the value at index [0] in the array. The variable named second will get
-the value 2 from index [1] in the array.
Let’s see what happens if you try to access an element of an array that is past -the end of the array. Say you run this code, similar to the guessing game in -Chapter 2, to get an array index from the user:
-Filename: src/main.rs
-use std::io;
-
-fn main() {
- let a = [1, 2, 3, 4, 5];
-
- println!("Please enter an array index.");
-
- let mut index = String::new();
-
- io::stdin()
- .read_line(&mut index)
- .expect("Failed to read line");
-
- let index: usize = index
- .trim()
- .parse()
- .expect("Index entered was not a number");
-
- let element = a[index];
-
- println!("The value of the element at index {index} is: {element}");
-}
-This code compiles successfully. If you run this code using cargo run and
-enter 0, 1, 2, 3, or 4, the program will print out the corresponding
-value at that index in the array. If you instead enter a number past the end of
-the array, such as 10, you’ll see output like this:
thread 'main' panicked at src/main.rs:19:19:
-index out of bounds: the len is 5 but the index is 10
-note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
-
-The program resulted in a runtime error at the point of using an invalid
-value in the indexing operation. The program exited with an error message and
-didn’t execute the final println! statement. When you attempt to access an
-element using indexing, Rust will check that the index you’ve specified is less
-than the array length. If the index is greater than or equal to the length,
-Rust will panic. This check has to happen at runtime, especially in this case,
-because the compiler can’t possibly know what value a user will enter when they
-run the code later.
This is an example of Rust’s memory safety principles in action. In many -low-level languages, this kind of check is not done, and when you provide an -incorrect index, invalid memory can be accessed. Rust protects you against this -kind of error by immediately exiting instead of allowing the memory access and -continuing. Chapter 9 discusses more of Rust’s error handling and how you can -write readable, safe code that neither panics nor allows invalid memory access.
- -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
-Functions are prevalent in Rust code. You’ve already seen one of the most
-important functions in the language: the main function, which is the entry
-point of many programs. You’ve also seen the fn keyword, which allows you to
-declare new functions.
Rust code uses snake case as the conventional style for function and variable -names, in which all letters are lowercase and underscores separate words. -Here’s a program that contains an example function definition:
-Filename: src/main.rs
-fn main() {
- println!("Hello, world!");
-
- another_function();
-}
-
-fn another_function() {
- println!("Another function.");
-}
-We define a function in Rust by entering fn followed by a function name and a
-set of parentheses. The curly brackets tell the compiler where the function
-body begins and ends.
We can call any function we’ve defined by entering its name followed by a set
-of parentheses. Because another_function is defined in the program, it can be
-called from inside the main function. Note that we defined another_function
-after the main function in the source code; we could have defined it before
-as well. Rust doesn’t care where you define your functions, only that they’re
-defined somewhere in a scope that can be seen by the caller.
Let’s start a new binary project named functions to explore functions
-further. Place the another_function example in src/main.rs and run it. You
-should see the following output:
$ cargo run
- Compiling functions v0.1.0 (file:///projects/functions)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.28s
- Running `target/debug/functions`
-Hello, world!
-Another function.
-
-The lines execute in the order in which they appear in the main function.
-First the “Hello, world!” message prints, and then another_function is called
-and its message is printed.
We can define functions to have parameters, which are special variables that -are part of a function’s signature. When a function has parameters, you can -provide it with concrete values for those parameters. Technically, the concrete -values are called arguments, but in casual conversation, people tend to use -the words parameter and argument interchangeably for either the variables -in a function’s definition or the concrete values passed in when you call a -function.
-In this version of another_function we add a parameter:
Filename: src/main.rs
-fn main() {
- another_function(5);
-}
-
-fn another_function(x: i32) {
- println!("The value of x is: {x}");
-}
-Try running this program; you should get the following output:
-$ cargo run
- Compiling functions v0.1.0 (file:///projects/functions)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.21s
- Running `target/debug/functions`
-The value of x is: 5
-
-The declaration of another_function has one parameter named x. The type of
-x is specified as i32. When we pass 5 in to another_function, the
-println! macro puts 5 where the pair of curly brackets containing x was
-in the format string.
In function signatures, you must declare the type of each parameter. This is -a deliberate decision in Rust’s design: Requiring type annotations in function -definitions means the compiler almost never needs you to use them elsewhere in -the code to figure out what type you mean. The compiler is also able to give -more-helpful error messages if it knows what types the function expects.
-When defining multiple parameters, separate the parameter declarations with -commas, like this:
-Filename: src/main.rs
-fn main() {
- print_labeled_measurement(5, 'h');
-}
-
-fn print_labeled_measurement(value: i32, unit_label: char) {
- println!("The measurement is: {value}{unit_label}");
-}
-This example creates a function named print_labeled_measurement with two
-parameters. The first parameter is named value and is an i32. The second is
-named unit_label and is type char. The function then prints text containing
-both the value and the unit_label.
Let’s try running this code. Replace the program currently in your functions
-project’s src/main.rs file with the preceding example and run it using cargo run:
$ cargo run
- Compiling functions v0.1.0 (file:///projects/functions)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
- Running `target/debug/functions`
-The measurement is: 5h
-
-Because we called the function with 5 as the value for value and 'h' as
-the value for unit_label, the program output contains those values.
Function bodies are made up of a series of statements optionally ending in an -expression. So far, the functions we’ve covered haven’t included an ending -expression, but you have seen an expression as part of a statement. Because -Rust is an expression-based language, this is an important distinction to -understand. Other languages don’t have the same distinctions, so let’s look at -what statements and expressions are and how their differences affect the bodies -of functions.
-Let’s look at some examples.
-We’ve actually already used statements and expressions. Creating a variable and
-assigning a value to it with the let keyword is a statement. In Listing 3-1,
-let y = 6; is a statement.
fn main() {
- let y = 6;
-}
-main function declaration containing one statementFunction definitions are also statements; the entire preceding example is a -statement in itself. (As we’ll see shortly, calling a function is not a -statement, though.)
-Statements do not return values. Therefore, you can’t assign a let statement
-to another variable, as the following code tries to do; you’ll get an error:
Filename: src/main.rs
-fn main() {
- let x = (let y = 6);
-}
-When you run this program, the error you’ll get looks like this:
-$ cargo run
- Compiling functions v0.1.0 (file:///projects/functions)
-error: expected expression, found `let` statement
- --> src/main.rs:2:14
- |
-2 | let x = (let y = 6);
- | ^^^
- |
- = note: only supported directly in conditions of `if` and `while` expressions
-
-warning: unnecessary parentheses around assigned value
- --> src/main.rs:2:13
- |
-2 | let x = (let y = 6);
- | ^ ^
- |
- = note: `#[warn(unused_parens)]` on by default
-help: remove these parentheses
- |
-2 - let x = (let y = 6);
-2 + let x = let y = 6;
- |
-
-warning: `functions` (bin "functions") generated 1 warning
-error: could not compile `functions` (bin "functions") due to 1 previous error; 1 warning emitted
-
-The let y = 6 statement does not return a value, so there isn’t anything for
-x to bind to. This is different from what happens in other languages, such as
-C and Ruby, where the assignment returns the value of the assignment. In those
-languages, you can write x = y = 6 and have both x and y have the value
-6; that is not the case in Rust.
Expressions evaluate to a value and make up most of the rest of the code that
-you’ll write in Rust. Consider a math operation, such as 5 + 6, which is an
-expression that evaluates to the value 11. Expressions can be part of
-statements: In Listing 3-1, the 6 in the statement let y = 6; is an
-expression that evaluates to the value 6. Calling a function is an
-expression. Calling a macro is an expression. A new scope block created with
-curly brackets is an expression, for example:
Filename: src/main.rs
-fn main() {
- let y = {
- let x = 3;
- x + 1
- };
-
- println!("The value of y is: {y}");
-}
-This expression:
-{
- let x = 3;
- x + 1
-}
-is a block that, in this case, evaluates to 4. That value gets bound to y
-as part of the let statement. Note the x + 1 line without a semicolon at
-the end, which is unlike most of the lines you’ve seen so far. Expressions do
-not include ending semicolons. If you add a semicolon to the end of an
-expression, you turn it into a statement, and it will then not return a value.
-Keep this in mind as you explore function return values and expressions next.
Functions can return values to the code that calls them. We don’t name return
-values, but we must declare their type after an arrow (->). In Rust, the
-return value of the function is synonymous with the value of the final
-expression in the block of the body of a function. You can return early from a
-function by using the return keyword and specifying a value, but most
-functions return the last expression implicitly. Here’s an example of a
-function that returns a value:
Filename: src/main.rs
-fn five() -> i32 {
- 5
-}
-
-fn main() {
- let x = five();
-
- println!("The value of x is: {x}");
-}
-There are no function calls, macros, or even let statements in the five
-function—just the number 5 by itself. That’s a perfectly valid function in
-Rust. Note that the function’s return type is specified too, as -> i32. Try
-running this code; the output should look like this:
$ cargo run
- Compiling functions v0.1.0 (file:///projects/functions)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
- Running `target/debug/functions`
-The value of x is: 5
-
-The 5 in five is the function’s return value, which is why the return type
-is i32. Let’s examine this in more detail. There are two important bits:
-First, the line let x = five(); shows that we’re using the return value of a
-function to initialize a variable. Because the function five returns a 5,
-that line is the same as the following:
#![allow(unused)]
-fn main() {
-let x = 5;
-}
-Second, the five function has no parameters and defines the type of the
-return value, but the body of the function is a lonely 5 with no semicolon
-because it’s an expression whose value we want to return.
Let’s look at another example:
-Filename: src/main.rs
-fn main() {
- let x = plus_one(5);
-
- println!("The value of x is: {x}");
-}
-
-fn plus_one(x: i32) -> i32 {
- x + 1
-}
-Running this code will print The value of x is: 6. But what happens if we
-place a semicolon at the end of the line containing x + 1, changing it from
-an expression to a statement?
Filename: src/main.rs
-fn main() {
- let x = plus_one(5);
-
- println!("The value of x is: {x}");
-}
-
-fn plus_one(x: i32) -> i32 {
- x + 1;
-}
-Compiling this code will produce an error, as follows:
-$ cargo run
- Compiling functions v0.1.0 (file:///projects/functions)
-error[E0308]: mismatched types
- --> src/main.rs:7:24
- |
-7 | fn plus_one(x: i32) -> i32 {
- | -------- ^^^ expected `i32`, found `()`
- | |
- | implicitly returns `()` as its body has no tail or `return` expression
-8 | x + 1;
- | - help: remove this semicolon to return this value
-
-For more information about this error, try `rustc --explain E0308`.
-error: could not compile `functions` (bin "functions") due to 1 previous error
-
-The main error message, mismatched types, reveals the core issue with this
-code. The definition of the function plus_one says that it will return an
-i32, but statements don’t evaluate to a value, which is expressed by (),
-the unit type. Therefore, nothing is returned, which contradicts the function
-definition and results in an error. In this output, Rust provides a message to
-possibly help rectify this issue: It suggests removing the semicolon, which
-would fix the error.
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
-All programmers strive to make their code easy to understand, but sometimes -extra explanation is warranted. In these cases, programmers leave comments in -their source code that the compiler will ignore but that people reading the -source code may find useful.
-Here’s a simple comment:
-#![allow(unused)]
-fn main() {
-// hello, world
-}
-In Rust, the idiomatic comment style starts a comment with two slashes, and the
-comment continues until the end of the line. For comments that extend beyond a
-single line, you’ll need to include // on each line, like this:
#![allow(unused)]
-fn main() {
-// So we're doing something complicated here, long enough that we need
-// multiple lines of comments to do it! Whew! Hopefully, this comment will
-// explain what's going on.
-}
-Comments can also be placed at the end of lines containing code:
-Filename: src/main.rs
-fn main() {
- let lucky_number = 7; // I'm feeling lucky today
-}
-But you’ll more often see them used in this format, with the comment on a -separate line above the code it’s annotating:
-Filename: src/main.rs
-fn main() {
- // I'm feeling lucky today
- let lucky_number = 7;
-}
-Rust also has another kind of comment, documentation comments, which we’ll -discuss in the “Publishing a Crate to Crates.io” -section of Chapter 14.
- -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
-Ownership is Rust’s most unique feature and has deep implications for the rest -of the language. It enables Rust to make memory safety guarantees without -needing a garbage collector, so it’s important to understand how ownership -works. In this chapter, we’ll talk about ownership as well as several related -features: borrowing, slices, and how Rust lays data out in memory.
- -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
-The issue with the tuple code in Listing 4-5 is that we have to return the
-String to the calling function so that we can still use the String after
-the call to calculate_length, because the String was moved into
-calculate_length. Instead, we can provide a reference to the String value.
-A reference is like a pointer in that it’s an address we can follow to access
-the data stored at that address; that data is owned by some other variable.
-Unlike a pointer, a reference is guaranteed to point to a valid value of a
-particular type for the life of that reference.
Here is how you would define and use a calculate_length function that has a
-reference to an object as a parameter instead of taking ownership of the value:
fn main() {
- let s1 = String::from("hello");
-
- let len = calculate_length(&s1);
-
- println!("The length of '{s1}' is {len}.");
-}
-
-fn calculate_length(s: &String) -> usize {
- s.len()
-}
-First, notice that all the tuple code in the variable declaration and the
-function return value is gone. Second, note that we pass &s1 into
-calculate_length and, in its definition, we take &String rather than
-String. These ampersands represent references, and they allow you to refer to
-some value without taking ownership of it. Figure 4-6 depicts this concept.
Figure 4-6: A diagram of &String s pointing at
-String s1
Note: The opposite of referencing by using & is dereferencing, which is
-accomplished with the dereference operator, *. We’ll see some uses of the
-dereference operator in Chapter 8 and discuss details of dereferencing in
-Chapter 15.
Let’s take a closer look at the function call here:
-fn main() {
- let s1 = String::from("hello");
-
- let len = calculate_length(&s1);
-
- println!("The length of '{s1}' is {len}.");
-}
-
-fn calculate_length(s: &String) -> usize {
- s.len()
-}
-The &s1 syntax lets us create a reference that refers to the value of s1
-but does not own it. Because the reference does not own it, the value it points
-to will not be dropped when the reference stops being used.
Likewise, the signature of the function uses & to indicate that the type of
-the parameter s is a reference. Let’s add some explanatory annotations:
fn main() {
- let s1 = String::from("hello");
-
- let len = calculate_length(&s1);
-
- println!("The length of '{s1}' is {len}.");
-}
-
-fn calculate_length(s: &String) -> usize { // s is a reference to a String
- s.len()
-} // Here, s goes out of scope. But because s does not have ownership of what
- // it refers to, the String is not dropped.
-The scope in which the variable s is valid is the same as any function
-parameter’s scope, but the value pointed to by the reference is not dropped
-when s stops being used, because s doesn’t have ownership. When functions
-have references as parameters instead of the actual values, we won’t need to
-return the values in order to give back ownership, because we never had
-ownership.
We call the action of creating a reference borrowing. As in real life, if a -person owns something, you can borrow it from them. When you’re done, you have -to give it back. You don’t own it.
-So, what happens if we try to modify something we’re borrowing? Try the code in -Listing 4-6. Spoiler alert: It doesn’t work!
-fn main() {
- let s = String::from("hello");
-
- change(&s);
-}
-
-fn change(some_string: &String) {
- some_string.push_str(", world");
-}
-Here’s the error:
-$ cargo run
- Compiling ownership v0.1.0 (file:///projects/ownership)
-error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
- --> src/main.rs:8:5
- |
-8 | some_string.push_str(", world");
- | ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
- |
-help: consider changing this to be a mutable reference
- |
-7 | fn change(some_string: &mut String) {
- | +++
-
-For more information about this error, try `rustc --explain E0596`.
-error: could not compile `ownership` (bin "ownership") due to 1 previous error
-
-Just as variables are immutable by default, so are references. We’re not -allowed to modify something we have a reference to.
-We can fix the code from Listing 4-6 to allow us to modify a borrowed value -with just a few small tweaks that use, instead, a mutable reference:
-fn main() {
- let mut s = String::from("hello");
-
- change(&mut s);
-}
-
-fn change(some_string: &mut String) {
- some_string.push_str(", world");
-}
-First, we change s to be mut. Then, we create a mutable reference with
-&mut s where we call the change function and update the function signature
-to accept a mutable reference with some_string: &mut String. This makes it
-very clear that the change function will mutate the value it borrows.
Mutable references have one big restriction: If you have a mutable reference to
-a value, you can have no other references to that value. This code that
-attempts to create two mutable references to s will fail:
fn main() {
- let mut s = String::from("hello");
-
- let r1 = &mut s;
- let r2 = &mut s;
-
- println!("{r1}, {r2}");
-}
-Here’s the error:
-$ cargo run
- Compiling ownership v0.1.0 (file:///projects/ownership)
-error[E0499]: cannot borrow `s` as mutable more than once at a time
- --> src/main.rs:5:14
- |
-4 | let r1 = &mut s;
- | ------ first mutable borrow occurs here
-5 | let r2 = &mut s;
- | ^^^^^^ second mutable borrow occurs here
-6 |
-7 | println!("{r1}, {r2}");
- | -- first borrow later used here
-
-For more information about this error, try `rustc --explain E0499`.
-error: could not compile `ownership` (bin "ownership") due to 1 previous error
-
-This error says that this code is invalid because we cannot borrow s as
-mutable more than once at a time. The first mutable borrow is in r1 and must
-last until it’s used in the println!, but between the creation of that
-mutable reference and its usage, we tried to create another mutable reference
-in r2 that borrows the same data as r1.
The restriction preventing multiple mutable references to the same data at the -same time allows for mutation but in a very controlled fashion. It’s something -that new Rustaceans struggle with because most languages let you mutate -whenever you’d like. The benefit of having this restriction is that Rust can -prevent data races at compile time. A data race is similar to a race -condition and happens when these three behaviors occur:
-Data races cause undefined behavior and can be difficult to diagnose and fix -when you’re trying to track them down at runtime; Rust prevents this problem by -refusing to compile code with data races!
-As always, we can use curly brackets to create a new scope, allowing for -multiple mutable references, just not simultaneous ones:
-fn main() {
- let mut s = String::from("hello");
-
- {
- let r1 = &mut s;
- } // r1 goes out of scope here, so we can make a new reference with no problems.
-
- let r2 = &mut s;
-}
-Rust enforces a similar rule for combining mutable and immutable references. -This code results in an error:
-fn main() {
- let mut s = String::from("hello");
-
- let r1 = &s; // no problem
- let r2 = &s; // no problem
- let r3 = &mut s; // BIG PROBLEM
-
- println!("{r1}, {r2}, and {r3}");
-}
-Here’s the error:
-$ cargo run
- Compiling ownership v0.1.0 (file:///projects/ownership)
-error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
- --> src/main.rs:6:14
- |
-4 | let r1 = &s; // no problem
- | -- immutable borrow occurs here
-5 | let r2 = &s; // no problem
-6 | let r3 = &mut s; // BIG PROBLEM
- | ^^^^^^ mutable borrow occurs here
-7 |
-8 | println!("{r1}, {r2}, and {r3}");
- | -- immutable borrow later used here
-
-For more information about this error, try `rustc --explain E0502`.
-error: could not compile `ownership` (bin "ownership") due to 1 previous error
-
-Whew! We also cannot have a mutable reference while we have an immutable one -to the same value.
-Users of an immutable reference don’t expect the value to suddenly change out -from under them! However, multiple immutable references are allowed because no -one who is just reading the data has the ability to affect anyone else’s -reading of the data.
-Note that a reference’s scope starts from where it is introduced and continues
-through the last time that reference is used. For instance, this code will
-compile because the last usage of the immutable references is in the println!,
-before the mutable reference is introduced:
fn main() {
- let mut s = String::from("hello");
-
- let r1 = &s; // no problem
- let r2 = &s; // no problem
- println!("{r1} and {r2}");
- // Variables r1 and r2 will not be used after this point.
-
- let r3 = &mut s; // no problem
- println!("{r3}");
-}
-The scopes of the immutable references r1 and r2 end after the println!
-where they are last used, which is before the mutable reference r3 is
-created. These scopes don’t overlap, so this code is allowed: The compiler can
-tell that the reference is no longer being used at a point before the end of
-the scope.
Even though borrowing errors may be frustrating at times, remember that it’s -the Rust compiler pointing out a potential bug early (at compile time rather -than at runtime) and showing you exactly where the problem is. Then, you don’t -have to track down why your data isn’t what you thought it was.
-In languages with pointers, it’s easy to erroneously create a dangling -pointer—a pointer that references a location in memory that may have been -given to someone else—by freeing some memory while preserving a pointer to that -memory. In Rust, by contrast, the compiler guarantees that references will -never be dangling references: If you have a reference to some data, the -compiler will ensure that the data will not go out of scope before the -reference to the data does.
-Let’s try to create a dangling reference to see how Rust prevents them with a -compile-time error:
-fn main() {
- let reference_to_nothing = dangle();
-}
-
-fn dangle() -> &String {
- let s = String::from("hello");
-
- &s
-}
-Here’s the error:
-$ cargo run
- Compiling ownership v0.1.0 (file:///projects/ownership)
-error[E0106]: missing lifetime specifier
- --> src/main.rs:5:16
- |
-5 | fn dangle() -> &String {
- | ^ expected named lifetime parameter
- |
- = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
-help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
- |
-5 | fn dangle() -> &'static String {
- | +++++++
-help: instead, you are more likely to want to return an owned value
- |
-5 - fn dangle() -> &String {
-5 + fn dangle() -> String {
- |
-
-For more information about this error, try `rustc --explain E0106`.
-error: could not compile `ownership` (bin "ownership") due to 1 previous error
-
-This error message refers to a feature we haven’t covered yet: lifetimes. We’ll -discuss lifetimes in detail in Chapter 10. But, if you disregard the parts -about lifetimes, the message does contain the key to why this code is a problem:
-this function's return type contains a borrowed value, but there is no value
-for it to be borrowed from
-
-Let’s take a closer look at exactly what’s happening at each stage of our
-dangle code:
fn main() {
- let reference_to_nothing = dangle();
-}
-
-fn dangle() -> &String { // dangle returns a reference to a String
-
- let s = String::from("hello"); // s is a new String
-
- &s // we return a reference to the String, s
-} // Here, s goes out of scope and is dropped, so its memory goes away.
- // Danger!
-Because s is created inside dangle, when the code of dangle is finished,
-s will be deallocated. But we tried to return a reference to it. That means
-this reference would be pointing to an invalid String. That’s no good! Rust
-won’t let us do this.
The solution here is to return the String directly:
fn main() {
- let string = no_dangle();
-}
-
-fn no_dangle() -> String {
- let s = String::from("hello");
-
- s
-}
-This works without any problems. Ownership is moved out, and nothing is -deallocated.
-Let’s recap what we’ve discussed about references:
-Next, we’ll look at a different kind of reference: slices.
- -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
-A struct, or structure, is a custom data type that lets you package -together and name multiple related values that make up a meaningful group. If -you’re familiar with an object-oriented language, a struct is like an object’s -data attributes. In this chapter, we’ll compare and contrast tuples with -structs to build on what you already know and demonstrate when structs are a -better way to group data.
-We’ll demonstrate how to define and instantiate structs. We’ll discuss how to -define associated functions, especially the kind of associated functions called -methods, to specify behavior associated with a struct type. Structs and enums -(discussed in Chapter 6) are the building blocks for creating new types in your -program’s domain to take full advantage of Rust’s compile-time type checking.
- -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
-Structs are similar to tuples, discussed in “The Tuple Type” section, in that both hold multiple related values. Like tuples, the -pieces of a struct can be different types. Unlike with tuples, in a struct -you’ll name each piece of data so it’s clear what the values mean. Adding these -names means that structs are more flexible than tuples: You don’t have to rely -on the order of the data to specify or access the values of an instance.
-To define a struct, we enter the keyword struct and name the entire struct. A
-struct’s name should describe the significance of the pieces of data being
-grouped together. Then, inside curly brackets, we define the names and types of
-the pieces of data, which we call fields. For example, Listing 5-1 shows a
-struct that stores information about a user account.
struct User {
- active: bool,
- username: String,
- email: String,
- sign_in_count: u64,
-}
-
-fn main() {}
-User struct definitionTo use a struct after we’ve defined it, we create an instance of that struct
-by specifying concrete values for each of the fields. We create an instance by
-stating the name of the struct and then add curly brackets containing key: value pairs, where the keys are the names of the fields and the values are the
-data we want to store in those fields. We don’t have to specify the fields in
-the same order in which we declared them in the struct. In other words, the
-struct definition is like a general template for the type, and instances fill
-in that template with particular data to create values of the type. For
-example, we can declare a particular user as shown in Listing 5-2.
struct User {
- active: bool,
- username: String,
- email: String,
- sign_in_count: u64,
-}
-
-fn main() {
- let user1 = User {
- active: true,
- username: String::from("someusername123"),
- email: String::from("someone@example.com"),
- sign_in_count: 1,
- };
-}
-User structTo get a specific value from a struct, we use dot notation. For example, to
-access this user’s email address, we use user1.email. If the instance is
-mutable, we can change a value by using the dot notation and assigning into a
-particular field. Listing 5-3 shows how to change the value in the email
-field of a mutable User instance.
struct User {
- active: bool,
- username: String,
- email: String,
- sign_in_count: u64,
-}
-
-fn main() {
- let mut user1 = User {
- active: true,
- username: String::from("someusername123"),
- email: String::from("someone@example.com"),
- sign_in_count: 1,
- };
-
- user1.email = String::from("anotheremail@example.com");
-}
-email field of a User instanceNote that the entire instance must be mutable; Rust doesn’t allow us to mark -only certain fields as mutable. As with any expression, we can construct a new -instance of the struct as the last expression in the function body to -implicitly return that new instance.
-Listing 5-4 shows a build_user function that returns a User instance with
-the given email and username. The active field gets the value true, and the
-sign_in_count gets a value of 1.
struct User {
- active: bool,
- username: String,
- email: String,
- sign_in_count: u64,
-}
-
-fn build_user(email: String, username: String) -> User {
- User {
- active: true,
- username: username,
- email: email,
- sign_in_count: 1,
- }
-}
-
-fn main() {
- let user1 = build_user(
- String::from("someone@example.com"),
- String::from("someusername123"),
- );
-}
-build_user function that takes an email and username and returns a User instanceIt makes sense to name the function parameters with the same name as the struct
-fields, but having to repeat the email and username field names and
-variables is a bit tedious. If the struct had more fields, repeating each name
-would get even more annoying. Luckily, there’s a convenient shorthand!
Because the parameter names and the struct field names are exactly the same in
-Listing 5-4, we can use the field init shorthand syntax to rewrite
-build_user so that it behaves exactly the same but doesn’t have the
-repetition of username and email, as shown in Listing 5-5.
struct User {
- active: bool,
- username: String,
- email: String,
- sign_in_count: u64,
-}
-
-fn build_user(email: String, username: String) -> User {
- User {
- active: true,
- username,
- email,
- sign_in_count: 1,
- }
-}
-
-fn main() {
- let user1 = build_user(
- String::from("someone@example.com"),
- String::from("someusername123"),
- );
-}
-build_user function that uses field init shorthand because the username and email parameters have the same name as struct fieldsHere, we’re creating a new instance of the User struct, which has a field
-named email. We want to set the email field’s value to the value in the
-email parameter of the build_user function. Because the email field and
-the email parameter have the same name, we only need to write email rather
-than email: email.
It’s often useful to create a new instance of a struct that includes most of -the values from another instance of the same type, but changes some of them. -You can do this using struct update syntax.
-First, in Listing 5-6 we show how to create a new User instance in user2 in
-the regular way, without the update syntax. We set a new value for email but
-otherwise use the same values from user1 that we created in Listing 5-2.
struct User {
- active: bool,
- username: String,
- email: String,
- sign_in_count: u64,
-}
-
-fn main() {
- // --snip--
-
- let user1 = User {
- email: String::from("someone@example.com"),
- username: String::from("someusername123"),
- active: true,
- sign_in_count: 1,
- };
-
- let user2 = User {
- active: user1.active,
- username: user1.username,
- email: String::from("another@example.com"),
- sign_in_count: user1.sign_in_count,
- };
-}
-User instance using all but one of the values from user1Using struct update syntax, we can achieve the same effect with less code, as
-shown in Listing 5-7. The syntax .. specifies that the remaining fields not
-explicitly set should have the same value as the fields in the given instance.
struct User {
- active: bool,
- username: String,
- email: String,
- sign_in_count: u64,
-}
-
-fn main() {
- // --snip--
-
- let user1 = User {
- email: String::from("someone@example.com"),
- username: String::from("someusername123"),
- active: true,
- sign_in_count: 1,
- };
-
- let user2 = User {
- email: String::from("another@example.com"),
- ..user1
- };
-}
-email value for a User instance but to use the rest of the values from user1The code in Listing 5-7 also creates an instance in user2 that has a
-different value for email but has the same values for the username,
-active, and sign_in_count fields from user1. The ..user1 must come last
-to specify that any remaining fields should get their values from the
-corresponding fields in user1, but we can choose to specify values for as
-many fields as we want in any order, regardless of the order of the fields in
-the struct’s definition.
Note that the struct update syntax uses = like an assignment; this is because
-it moves the data, just as we saw in the “Variables and Data Interacting with
-Move” section. In this example, we can no longer use
-user1 after creating user2 because the String in the username field of
-user1 was moved into user2. If we had given user2 new String values for
-both email and username, and thus only used the active and sign_in_count
-values from user1, then user1 would still be valid after creating user2.
-Both active and sign_in_count are types that implement the Copy trait, so
-the behavior we discussed in the “Stack-Only Data: Copy”
-section would apply. We can also still use user1.email in this example,
-because its value was not moved out of user1.
Rust also supports structs that look similar to tuples, called tuple structs. -Tuple structs have the added meaning the struct name provides but don’t have -names associated with their fields; rather, they just have the types of the -fields. Tuple structs are useful when you want to give the whole tuple a name -and make the tuple a different type from other tuples, and when naming each -field as in a regular struct would be verbose or redundant.
-To define a tuple struct, start with the struct keyword and the struct name
-followed by the types in the tuple. For example, here we define and use two
-tuple structs named Color and Point:
struct Color(i32, i32, i32);
-struct Point(i32, i32, i32);
-
-fn main() {
- let black = Color(0, 0, 0);
- let origin = Point(0, 0, 0);
-}
-Note that the black and origin values are different types because they’re
-instances of different tuple structs. Each struct you define is its own type,
-even though the fields within the struct might have the same types. For
-example, a function that takes a parameter of type Color cannot take a
-Point as an argument, even though both types are made up of three i32
-values. Otherwise, tuple struct instances are similar to tuples in that you can
-destructure them into their individual pieces, and you can use a . followed
-by the index to access an individual value. Unlike tuples, tuple structs
-require you to name the type of the struct when you destructure them. For
-example, we would write let Point(x, y, z) = origin; to destructure the
-values in the origin point into variables named x, y, and z.
You can also define structs that don’t have any fields! These are called
-unit-like structs because they behave similarly to (), the unit type that
-we mentioned in “The Tuple Type” section. Unit-like
-structs can be useful when you need to implement a trait on some type but don’t
-have any data that you want to store in the type itself. We’ll discuss traits
-in Chapter 10. Here’s an example of declaring and instantiating a unit struct
-named AlwaysEqual:
struct AlwaysEqual;
-
-fn main() {
- let subject = AlwaysEqual;
-}
-To define AlwaysEqual, we use the struct keyword, the name we want, and
-then a semicolon. No need for curly brackets or parentheses! Then, we can get
-an instance of AlwaysEqual in the subject variable in a similar way: using
-the name we defined, without any curly brackets or parentheses. Imagine that
-later we’ll implement behavior for this type such that every instance of
-AlwaysEqual is always equal to every instance of any other type, perhaps to
-have a known result for testing purposes. We wouldn’t need any data to
-implement that behavior! You’ll see in Chapter 10 how to define traits and
-implement them on any type, including unit-like structs.
In the User struct definition in Listing 5-1, we used the owned String
-type rather than the &str string slice type. This is a deliberate choice
-because we want each instance of this struct to own all of its data and for
-that data to be valid for as long as the entire struct is valid.
It’s also possible for structs to store references to data owned by something -else, but to do so requires the use of lifetimes, a Rust feature that we’ll -discuss in Chapter 10. Lifetimes ensure that the data referenced by a struct -is valid for as long as the struct is. Let’s say you try to store a reference -in a struct without specifying lifetimes, like the following in -src/main.rs; this won’t work:
-struct User {
- active: bool,
- username: &str,
- email: &str,
- sign_in_count: u64,
-}
-
-fn main() {
- let user1 = User {
- active: true,
- username: "someusername123",
- email: "someone@example.com",
- sign_in_count: 1,
- };
-}
-The compiler will complain that it needs lifetime specifiers:
-$ cargo run
- Compiling structs v0.1.0 (file:///projects/structs)
-error[E0106]: missing lifetime specifier
- --> src/main.rs:3:15
- |
-3 | username: &str,
- | ^ expected named lifetime parameter
- |
-help: consider introducing a named lifetime parameter
- |
-1 ~ struct User<'a> {
-2 | active: bool,
-3 ~ username: &'a str,
- |
-
-error[E0106]: missing lifetime specifier
- --> src/main.rs:4:12
- |
-4 | email: &str,
- | ^ expected named lifetime parameter
- |
-help: consider introducing a named lifetime parameter
- |
-1 ~ struct User<'a> {
-2 | active: bool,
-3 | username: &str,
-4 ~ email: &'a str,
- |
-
-For more information about this error, try `rustc --explain E0106`.
-error: could not compile `structs` (bin "structs") due to 2 previous errors
-
-In Chapter 10, we’ll discuss how to fix these errors so that you can store
-references in structs, but for now, we’ll fix errors like these using owned
-types like String instead of references like &str.
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
-To understand when we might want to use structs, let’s write a program that -calculates the area of a rectangle. We’ll start by using single variables and -then refactor the program until we’re using structs instead.
-Let’s make a new binary project with Cargo called rectangles that will take -the width and height of a rectangle specified in pixels and calculate the area -of the rectangle. Listing 5-8 shows a short program with one way of doing -exactly that in our project’s src/main.rs.
-fn main() {
- let width1 = 30;
- let height1 = 50;
-
- println!(
- "The area of the rectangle is {} square pixels.",
- area(width1, height1)
- );
-}
-
-fn area(width: u32, height: u32) -> u32 {
- width * height
-}
-Now, run this program using cargo run:
$ cargo run
- Compiling rectangles v0.1.0 (file:///projects/rectangles)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
- Running `target/debug/rectangles`
-The area of the rectangle is 1500 square pixels.
-
-This code succeeds in figuring out the area of the rectangle by calling the
-area function with each dimension, but we can do more to make this code clear
-and readable.
The issue with this code is evident in the signature of area:
fn main() {
- let width1 = 30;
- let height1 = 50;
-
- println!(
- "The area of the rectangle is {} square pixels.",
- area(width1, height1)
- );
-}
-
-fn area(width: u32, height: u32) -> u32 {
- width * height
-}
-The area function is supposed to calculate the area of one rectangle, but the
-function we wrote has two parameters, and it’s not clear anywhere in our
-program that the parameters are related. It would be more readable and more
-manageable to group width and height together. We’ve already discussed one way
-we might do that in “The Tuple Type” section
-of Chapter 3: by using tuples.
Listing 5-9 shows another version of our program that uses tuples.
-fn main() {
- let rect1 = (30, 50);
-
- println!(
- "The area of the rectangle is {} square pixels.",
- area(rect1)
- );
-}
-
-fn area(dimensions: (u32, u32)) -> u32 {
- dimensions.0 * dimensions.1
-}
-In one way, this program is better. Tuples let us add a bit of structure, and -we’re now passing just one argument. But in another way, this version is less -clear: Tuples don’t name their elements, so we have to index into the parts of -the tuple, making our calculation less obvious.
-Mixing up the width and height wouldn’t matter for the area calculation, but if
-we want to draw the rectangle on the screen, it would matter! We would have to
-keep in mind that width is the tuple index 0 and height is the tuple
-index 1. This would be even harder for someone else to figure out and keep in
-mind if they were to use our code. Because we haven’t conveyed the meaning of
-our data in our code, it’s now easier to introduce errors.
We use structs to add meaning by labeling the data. We can transform the tuple -we’re using into a struct with a name for the whole as well as names for the -parts, as shown in Listing 5-10.
-struct Rectangle {
- width: u32,
- height: u32,
-}
-
-fn main() {
- let rect1 = Rectangle {
- width: 30,
- height: 50,
- };
-
- println!(
- "The area of the rectangle is {} square pixels.",
- area(&rect1)
- );
-}
-
-fn area(rectangle: &Rectangle) -> u32 {
- rectangle.width * rectangle.height
-}
-Rectangle structHere, we’ve defined a struct and named it Rectangle. Inside the curly
-brackets, we defined the fields as width and height, both of which have
-type u32. Then, in main, we created a particular instance of Rectangle
-that has a width of 30 and a height of 50.
Our area function is now defined with one parameter, which we’ve named
-rectangle, whose type is an immutable borrow of a struct Rectangle
-instance. As mentioned in Chapter 4, we want to borrow the struct rather than
-take ownership of it. This way, main retains its ownership and can continue
-using rect1, which is the reason we use the & in the function signature and
-where we call the function.
The area function accesses the width and height fields of the Rectangle
-instance (note that accessing fields of a borrowed struct instance does not
-move the field values, which is why you often see borrows of structs). Our
-function signature for area now says exactly what we mean: Calculate the area
-of Rectangle, using its width and height fields. This conveys that the
-width and height are related to each other, and it gives descriptive names to
-the values rather than using the tuple index values of 0 and 1. This is a
-win for clarity.
It’d be useful to be able to print an instance of Rectangle while we’re
-debugging our program and see the values for all its fields. Listing 5-11 tries
-using the println! macro as we have used in
-previous chapters. This won’t work, however.
struct Rectangle {
- width: u32,
- height: u32,
-}
-
-fn main() {
- let rect1 = Rectangle {
- width: 30,
- height: 50,
- };
-
- println!("rect1 is {rect1}");
-}
-Rectangle instanceWhen we compile this code, we get an error with this core message:
-error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
-
-The println! macro can do many kinds of formatting, and by default, the curly
-brackets tell println! to use formatting known as Display: output intended
-for direct end user consumption. The primitive types we’ve seen so far
-implement Display by default because there’s only one way you’d want to show
-a 1 or any other primitive type to a user. But with structs, the way
-println! should format the output is less clear because there are more
-display possibilities: Do you want commas or not? Do you want to print the
-curly brackets? Should all the fields be shown? Due to this ambiguity, Rust
-doesn’t try to guess what we want, and structs don’t have a provided
-implementation of Display to use with println! and the {} placeholder.
If we continue reading the errors, we’ll find this helpful note:
- | |`Rectangle` cannot be formatted with the default formatter
- | required by this formatting parameter
-
-Let’s try it! The println! macro call will now look like println!("rect1 is {rect1:?}");. Putting the specifier :? inside the curly brackets tells
-println! we want to use an output format called Debug. The Debug trait
-enables us to print our struct in a way that is useful for developers so that
-we can see its value while we’re debugging our code.
Compile the code with this change. Drat! We still get an error:
-error[E0277]: `Rectangle` doesn't implement `Debug`
-
-But again, the compiler gives us a helpful note:
- | required by this formatting parameter
- |
-
-Rust does include functionality to print out debugging information, but we
-have to explicitly opt in to make that functionality available for our struct.
-To do that, we add the outer attribute #[derive(Debug)] just before the
-struct definition, as shown in Listing 5-12.
#[derive(Debug)]
-struct Rectangle {
- width: u32,
- height: u32,
-}
-
-fn main() {
- let rect1 = Rectangle {
- width: 30,
- height: 50,
- };
-
- println!("rect1 is {rect1:?}");
-}
-Debug trait and printing the Rectangle instance using debug formattingNow when we run the program, we won’t get any errors, and we’ll see the -following output:
-$ cargo run
- Compiling rectangles v0.1.0 (file:///projects/rectangles)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
- Running `target/debug/rectangles`
-rect1 is Rectangle { width: 30, height: 50 }
-
-Nice! It’s not the prettiest output, but it shows the values of all the fields
-for this instance, which would definitely help during debugging. When we have
-larger structs, it’s useful to have output that’s a bit easier to read; in
-those cases, we can use {:#?} instead of {:?} in the println! string. In
-this example, using the {:#?} style will output the following:
$ cargo run
- Compiling rectangles v0.1.0 (file:///projects/rectangles)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
- Running `target/debug/rectangles`
-rect1 is Rectangle {
- width: 30,
- height: 50,
-}
-
-Another way to print out a value using the Debug format is to use the dbg!
-macro, which takes ownership of an expression (as opposed
-to println!, which takes a reference), prints the file and line number of
-where that dbg! macro call occurs in your code along with the resultant value
-of that expression, and returns ownership of the value.
Note: Calling the dbg! macro prints to the standard error console stream
-(stderr), as opposed to println!, which prints to the standard output
-console stream (stdout). We’ll talk more about stderr and stdout in the
-“Redirecting Errors to Standard Error” section in Chapter
-12.
Here’s an example where we’re interested in the value that gets assigned to the
-width field, as well as the value of the whole struct in rect1:
#[derive(Debug)]
-struct Rectangle {
- width: u32,
- height: u32,
-}
-
-fn main() {
- let scale = 2;
- let rect1 = Rectangle {
- width: dbg!(30 * scale),
- height: 50,
- };
-
- dbg!(&rect1);
-}
-We can put dbg! around the expression 30 * scale and, because dbg!
-returns ownership of the expression’s value, the width field will get the
-same value as if we didn’t have the dbg! call there. We don’t want dbg! to
-take ownership of rect1, so we use a reference to rect1 in the next call.
-Here’s what the output of this example looks like:
$ cargo run
- Compiling rectangles v0.1.0 (file:///projects/rectangles)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
- Running `target/debug/rectangles`
-[src/main.rs:10:16] 30 * scale = 60
-[src/main.rs:14:5] &rect1 = Rectangle {
- width: 60,
- height: 50,
-}
-
-We can see the first bit of output came from src/main.rs line 10 where we’re
-debugging the expression 30 * scale, and its resultant value is 60 (the
-Debug formatting implemented for integers is to print only their value). The
-dbg! call on line 14 of src/main.rs outputs the value of &rect1, which is
-the Rectangle struct. This output uses the pretty Debug formatting of the
-Rectangle type. The dbg! macro can be really helpful when you’re trying to
-figure out what your code is doing!
In addition to the Debug trait, Rust has provided a number of traits for us
-to use with the derive attribute that can add useful behavior to our custom
-types. Those traits and their behaviors are listed in Appendix C. We’ll cover how to implement these traits with custom behavior as
-well as how to create your own traits in Chapter 10. There are also many
-attributes other than derive; for more information, see the “Attributes”
-section of the Rust Reference.
Our area function is very specific: It only computes the area of rectangles.
-It would be helpful to tie this behavior more closely to our Rectangle struct
-because it won’t work with any other type. Let’s look at how we can continue to
-refactor this code by turning the area function into an area method
-defined on our Rectangle type.
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
-Methods are similar to functions: We declare them with the fn keyword and a
-name, they can have parameters and a return value, and they contain some code
-that’s run when the method is called from somewhere else. Unlike functions,
-methods are defined within the context of a struct (or an enum or a trait
-object, which we cover in Chapter 6 and Chapter
-18, respectively), and their first parameter is
-always self, which represents the instance of the struct the method is being
-called on.
Let’s change the area function that has a Rectangle instance as a parameter
-and instead make an area method defined on the Rectangle struct, as shown
-in Listing 5-13.
#[derive(Debug)]
-struct Rectangle {
- width: u32,
- height: u32,
-}
-
-impl Rectangle {
- fn area(&self) -> u32 {
- self.width * self.height
- }
-}
-
-fn main() {
- let rect1 = Rectangle {
- width: 30,
- height: 50,
- };
-
- println!(
- "The area of the rectangle is {} square pixels.",
- rect1.area()
- );
-}
-area method on the Rectangle structTo define the function within the context of Rectangle, we start an impl
-(implementation) block for Rectangle. Everything within this impl block
-will be associated with the Rectangle type. Then, we move the area function
-within the impl curly brackets and change the first (and in this case, only)
-parameter to be self in the signature and everywhere within the body. In
-main, where we called the area function and passed rect1 as an argument,
-we can instead use method syntax to call the area method on our Rectangle
-instance. The method syntax goes after an instance: We add a dot followed by
-the method name, parentheses, and any arguments.
In the signature for area, we use &self instead of rectangle: &Rectangle.
-The &self is actually short for self: &Self. Within an impl block, the
-type Self is an alias for the type that the impl block is for. Methods must
-have a parameter named self of type Self for their first parameter, so Rust
-lets you abbreviate this with only the name self in the first parameter spot.
-Note that we still need to use the & in front of the self shorthand to
-indicate that this method borrows the Self instance, just as we did in
-rectangle: &Rectangle. Methods can take ownership of self, borrow self
-immutably, as we’ve done here, or borrow self mutably, just as they can any
-other parameter.
We chose &self here for the same reason we used &Rectangle in the function
-version: We don’t want to take ownership, and we just want to read the data in
-the struct, not write to it. If we wanted to change the instance that we’ve
-called the method on as part of what the method does, we’d use &mut self as
-the first parameter. Having a method that takes ownership of the instance by
-using just self as the first parameter is rare; this technique is usually
-used when the method transforms self into something else and you want to
-prevent the caller from using the original instance after the transformation.
The main reason for using methods instead of functions, in addition to
-providing method syntax and not having to repeat the type of self in every
-method’s signature, is for organization. We’ve put all the things we can do
-with an instance of a type in one impl block rather than making future users
-of our code search for capabilities of Rectangle in various places in the
-library we provide.
Note that we can choose to give a method the same name as one of the struct’s
-fields. For example, we can define a method on Rectangle that is also named
-width:
#[derive(Debug)]
-struct Rectangle {
- width: u32,
- height: u32,
-}
-
-impl Rectangle {
- fn width(&self) -> bool {
- self.width > 0
- }
-}
-
-fn main() {
- let rect1 = Rectangle {
- width: 30,
- height: 50,
- };
-
- if rect1.width() {
- println!("The rectangle has a nonzero width; it is {}", rect1.width);
- }
-}
-Here, we’re choosing to make the width method return true if the value in
-the instance’s width field is greater than 0 and false if the value is
-0: We can use a field within a method of the same name for any purpose. In
-main, when we follow rect1.width with parentheses, Rust knows we mean the
-method width. When we don’t use parentheses, Rust knows we mean the field
-width.
Often, but not always, when we give a method the same name as a field we want -it to only return the value in the field and do nothing else. Methods like this -are called getters, and Rust does not implement them automatically for struct -fields as some other languages do. Getters are useful because you can make the -field private but the method public and thus enable read-only access to that -field as part of the type’s public API. We will discuss what public and private -are and how to designate a field or method as public or private in Chapter -7.
--> Operator?In C and C++, two different operators are used for calling methods: You use
-. if you’re calling a method on the object directly and -> if you’re
-calling the method on a pointer to the object and need to dereference the
-pointer first. In other words, if object is a pointer,
-object->something() is similar to (*object).something().
Rust doesn’t have an equivalent to the -> operator; instead, Rust has a
-feature called automatic referencing and dereferencing. Calling methods is
-one of the few places in Rust with this behavior.
Here’s how it works: When you call a method with object.something(), Rust
-automatically adds in &, &mut, or * so that object matches the
-signature of the method. In other words, the following are the same:
#![allow(unused)]
-fn main() {
-#[derive(Debug,Copy,Clone)]
-struct Point {
- x: f64,
- y: f64,
-}
-
-impl Point {
- fn distance(&self, other: &Point) -> f64 {
- let x_squared = f64::powi(other.x - self.x, 2);
- let y_squared = f64::powi(other.y - self.y, 2);
-
- f64::sqrt(x_squared + y_squared)
- }
-}
-let p1 = Point { x: 0.0, y: 0.0 };
-let p2 = Point { x: 5.0, y: 6.5 };
-p1.distance(&p2);
-(&p1).distance(&p2);
-}
-The first one looks much cleaner. This automatic referencing behavior works
-because methods have a clear receiver—the type of self. Given the receiver
-and name of a method, Rust can figure out definitively whether the method is
-reading (&self), mutating (&mut self), or consuming (self). The fact
-that Rust makes borrowing implicit for method receivers is a big part of
-making ownership ergonomic in practice.
Let’s practice using methods by implementing a second method on the Rectangle
-struct. This time we want an instance of Rectangle to take another instance
-of Rectangle and return true if the second Rectangle can fit completely
-within self (the first Rectangle); otherwise, it should return false.
-That is, once we’ve defined the can_hold method, we want to be able to write
-the program shown in Listing 5-14.
fn main() {
- let rect1 = Rectangle {
- width: 30,
- height: 50,
- };
- let rect2 = Rectangle {
- width: 10,
- height: 40,
- };
- let rect3 = Rectangle {
- width: 60,
- height: 45,
- };
-
- println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
- println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
-}
-can_hold methodThe expected output would look like the following because both dimensions of
-rect2 are smaller than the dimensions of rect1, but rect3 is wider than
-rect1:
Can rect1 hold rect2? true
-Can rect1 hold rect3? false
-
-We know we want to define a method, so it will be within the impl Rectangle
-block. The method name will be can_hold, and it will take an immutable borrow
-of another Rectangle as a parameter. We can tell what the type of the
-parameter will be by looking at the code that calls the method:
-rect1.can_hold(&rect2) passes in &rect2, which is an immutable borrow to
-rect2, an instance of Rectangle. This makes sense because we only need to
-read rect2 (rather than write, which would mean we’d need a mutable borrow),
-and we want main to retain ownership of rect2 so that we can use it again
-after calling the can_hold method. The return value of can_hold will be a
-Boolean, and the implementation will check whether the width and height of
-self are greater than the width and height of the other Rectangle,
-respectively. Let’s add the new can_hold method to the impl block from
-Listing 5-13, shown in Listing 5-15.
#[derive(Debug)]
-struct Rectangle {
- width: u32,
- height: u32,
-}
-
-impl Rectangle {
- fn area(&self) -> u32 {
- self.width * self.height
- }
-
- fn can_hold(&self, other: &Rectangle) -> bool {
- self.width > other.width && self.height > other.height
- }
-}
-
-fn main() {
- let rect1 = Rectangle {
- width: 30,
- height: 50,
- };
- let rect2 = Rectangle {
- width: 10,
- height: 40,
- };
- let rect3 = Rectangle {
- width: 60,
- height: 45,
- };
-
- println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
- println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
-}
-can_hold method on Rectangle that takes another Rectangle instance as a parameterWhen we run this code with the main function in Listing 5-14, we’ll get our
-desired output. Methods can take multiple parameters that we add to the
-signature after the self parameter, and those parameters work just like
-parameters in functions.
All functions defined within an impl block are called associated functions
-because they’re associated with the type named after the impl. We can define
-associated functions that don’t have self as their first parameter (and thus
-are not methods) because they don’t need an instance of the type to work with.
-We’ve already used one function like this: the String::from function that’s
-defined on the String type.
Associated functions that aren’t methods are often used for constructors that
-will return a new instance of the struct. These are often called new, but
-new isn’t a special name and isn’t built into the language. For example, we
-could choose to provide an associated function named square that would have
-one dimension parameter and use that as both width and height, thus making it
-easier to create a square Rectangle rather than having to specify the same
-value twice:
Filename: src/main.rs
-#[derive(Debug)]
-struct Rectangle {
- width: u32,
- height: u32,
-}
-
-impl Rectangle {
- fn square(size: u32) -> Self {
- Self {
- width: size,
- height: size,
- }
- }
-}
-
-fn main() {
- let sq = Rectangle::square(3);
-}
-The Self keywords in the return type and in the body of the function are
-aliases for the type that appears after the impl keyword, which in this case
-is Rectangle.
To call this associated function, we use the :: syntax with the struct name;
-let sq = Rectangle::square(3); is an example. This function is namespaced by
-the struct: The :: syntax is used for both associated functions and
-namespaces created by modules. We’ll discuss modules in Chapter
-7.
impl BlocksEach struct is allowed to have multiple impl blocks. For example, Listing
-5-15 is equivalent to the code shown in Listing 5-16, which has each method in
-its own impl block.
#[derive(Debug)]
-struct Rectangle {
- width: u32,
- height: u32,
-}
-
-impl Rectangle {
- fn area(&self) -> u32 {
- self.width * self.height
- }
-}
-
-impl Rectangle {
- fn can_hold(&self, other: &Rectangle) -> bool {
- self.width > other.width && self.height > other.height
- }
-}
-
-fn main() {
- let rect1 = Rectangle {
- width: 30,
- height: 50,
- };
- let rect2 = Rectangle {
- width: 10,
- height: 40,
- };
- let rect3 = Rectangle {
- width: 60,
- height: 45,
- };
-
- println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
- println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
-}
-impl blocksThere’s no reason to separate these methods into multiple impl blocks here,
-but this is valid syntax. We’ll see a case in which multiple impl blocks are
-useful in Chapter 10, where we discuss generic types and traits.
Structs let you create custom types that are meaningful for your domain. By
-using structs, you can keep associated pieces of data connected to each other
-and name each piece to make your code clear. In impl blocks, you can define
-functions that are associated with your type, and methods are a kind of
-associated function that let you specify the behavior that instances of your
-structs have.
But structs aren’t the only way you can create custom types: Let’s turn to -Rust’s enum feature to add another tool to your toolbox.
- -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
-In this chapter, we’ll look at enumerations, also referred to as enums.
-Enums allow you to define a type by enumerating its possible variants. First
-we’ll define and use an enum to show how an enum can encode meaning along with
-data. Next, we’ll explore a particularly useful enum, called Option, which
-expresses that a value can be either something or nothing. Then, we’ll look at
-how pattern matching in the match expression makes it easy to run different
-code for different values of an enum. Finally, we’ll cover how the if let
-construct is another convenient and concise idiom available to handle enums in
-your code.
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
-match Control Flow ConstructRust has an extremely powerful control flow construct called match that
-allows you to compare a value against a series of patterns and then execute
-code based on which pattern matches. Patterns can be made up of literal values,
-variable names, wildcards, and many other things; Chapter
-19 covers all the different kinds of patterns
-and what they do. The power of match comes from the expressiveness of the
-patterns and the fact that the compiler confirms that all possible cases are
-handled.
Think of a match expression as being like a coin-sorting machine: Coins slide
-down a track with variously sized holes along it, and each coin falls through
-the first hole it encounters that it fits into. In the same way, values go
-through each pattern in a match, and at the first pattern the value “fits,”
-the value falls into the associated code block to be used during execution.
Speaking of coins, let’s use them as an example using match! We can write a
-function that takes an unknown US coin and, in a similar way as the counting
-machine, determines which coin it is and returns its value in cents, as shown
-in Listing 6-3.
enum Coin {
- Penny,
- Nickel,
- Dime,
- Quarter,
-}
-
-fn value_in_cents(coin: Coin) -> u8 {
- match coin {
- Coin::Penny => 1,
- Coin::Nickel => 5,
- Coin::Dime => 10,
- Coin::Quarter => 25,
- }
-}
-
-fn main() {}
-match expression that has the variants of the enum as its patternsLet’s break down the match in the value_in_cents function. First, we list
-the match keyword followed by an expression, which in this case is the value
-coin. This seems very similar to a conditional expression used with if, but
-there’s a big difference: With if, the condition needs to evaluate to a
-Boolean value, but here it can be any type. The type of coin in this example
-is the Coin enum that we defined on the first line.
Next are the match arms. An arm has two parts: a pattern and some code. The
-first arm here has a pattern that is the value Coin::Penny and then the =>
-operator that separates the pattern and the code to run. The code in this case
-is just the value 1. Each arm is separated from the next with a comma.
When the match expression executes, it compares the resultant value against
-the pattern of each arm, in order. If a pattern matches the value, the code
-associated with that pattern is executed. If that pattern doesn’t match the
-value, execution continues to the next arm, much as in a coin-sorting machine.
-We can have as many arms as we need: In Listing 6-3, our match has four arms.
The code associated with each arm is an expression, and the resultant value of
-the expression in the matching arm is the value that gets returned for the
-entire match expression.
We don’t typically use curly brackets if the match arm code is short, as it is
-in Listing 6-3 where each arm just returns a value. If you want to run multiple
-lines of code in a match arm, you must use curly brackets, and the comma
-following the arm is then optional. For example, the following code prints
-“Lucky penny!” every time the method is called with a Coin::Penny, but it
-still returns the last value of the block, 1:
enum Coin {
- Penny,
- Nickel,
- Dime,
- Quarter,
-}
-
-fn value_in_cents(coin: Coin) -> u8 {
- match coin {
- Coin::Penny => {
- println!("Lucky penny!");
- 1
- }
- Coin::Nickel => 5,
- Coin::Dime => 10,
- Coin::Quarter => 25,
- }
-}
-
-fn main() {}
-Another useful feature of match arms is that they can bind to the parts of the -values that match the pattern. This is how we can extract values out of enum -variants.
-As an example, let’s change one of our enum variants to hold data inside it.
-From 1999 through 2008, the United States minted quarters with different
-designs for each of the 50 states on one side. No other coins got state
-designs, so only quarters have this extra value. We can add this information to
-our enum by changing the Quarter variant to include a UsState value
-stored inside it, which we’ve done in Listing 6-4.
#[derive(Debug)] // so we can inspect the state in a minute
-enum UsState {
- Alabama,
- Alaska,
- // --snip--
-}
-
-enum Coin {
- Penny,
- Nickel,
- Dime,
- Quarter(UsState),
-}
-
-fn main() {}
-Coin enum in which the Quarter variant also holds a UsState valueLet’s imagine that a friend is trying to collect all 50 state quarters. While -we sort our loose change by coin type, we’ll also call out the name of the -state associated with each quarter so that if it’s one our friend doesn’t have, -they can add it to their collection.
-In the match expression for this code, we add a variable called state to the
-pattern that matches values of the variant Coin::Quarter. When a
-Coin::Quarter matches, the state variable will bind to the value of that
-quarter’s state. Then, we can use state in the code for that arm, like so:
#[derive(Debug)]
-enum UsState {
- Alabama,
- Alaska,
- // --snip--
-}
-
-enum Coin {
- Penny,
- Nickel,
- Dime,
- Quarter(UsState),
-}
-
-fn value_in_cents(coin: Coin) -> u8 {
- match coin {
- Coin::Penny => 1,
- Coin::Nickel => 5,
- Coin::Dime => 10,
- Coin::Quarter(state) => {
- println!("State quarter from {state:?}!");
- 25
- }
- }
-}
-
-fn main() {
- value_in_cents(Coin::Quarter(UsState::Alaska));
-}
-If we were to call value_in_cents(Coin::Quarter(UsState::Alaska)), coin
-would be Coin::Quarter(UsState::Alaska). When we compare that value with each
-of the match arms, none of them match until we reach Coin::Quarter(state). At
-that point, the binding for state will be the value UsState::Alaska. We can
-then use that binding in the println! expression, thus getting the inner
-state value out of the Coin enum variant for Quarter.
Option<T> match PatternIn the previous section, we wanted to get the inner T value out of the Some
-case when using Option<T>; we can also handle Option<T> using match, as
-we did with the Coin enum! Instead of comparing coins, we’ll compare the
-variants of Option<T>, but the way the match expression works remains the
-same.
Let’s say we want to write a function that takes an Option<i32> and, if
-there’s a value inside, adds 1 to that value. If there isn’t a value inside,
-the function should return the None value and not attempt to perform any
-operations.
This function is very easy to write, thanks to match, and will look like
-Listing 6-5.
fn main() {
- fn plus_one(x: Option<i32>) -> Option<i32> {
- match x {
- None => None,
- Some(i) => Some(i + 1),
- }
- }
-
- let five = Some(5);
- let six = plus_one(five);
- let none = plus_one(None);
-}
-match expression on an Option<i32>Let’s examine the first execution of plus_one in more detail. When we call
-plus_one(five), the variable x in the body of plus_one will have the
-value Some(5). We then compare that against each match arm:
fn main() {
- fn plus_one(x: Option<i32>) -> Option<i32> {
- match x {
- None => None,
- Some(i) => Some(i + 1),
- }
- }
-
- let five = Some(5);
- let six = plus_one(five);
- let none = plus_one(None);
-}
-The Some(5) value doesn’t match the pattern None, so we continue to the
-next arm:
fn main() {
- fn plus_one(x: Option<i32>) -> Option<i32> {
- match x {
- None => None,
- Some(i) => Some(i + 1),
- }
- }
-
- let five = Some(5);
- let six = plus_one(five);
- let none = plus_one(None);
-}
-Does Some(5) match Some(i)? It does! We have the same variant. The i
-binds to the value contained in Some, so i takes the value 5. The code in
-the match arm is then executed, so we add 1 to the value of i and create a
-new Some value with our total 6 inside.
Now let’s consider the second call of plus_one in Listing 6-5, where x is
-None. We enter the match and compare to the first arm:
fn main() {
- fn plus_one(x: Option<i32>) -> Option<i32> {
- match x {
- None => None,
- Some(i) => Some(i + 1),
- }
- }
-
- let five = Some(5);
- let six = plus_one(five);
- let none = plus_one(None);
-}
-It matches! There’s no value to add to, so the program stops and returns the
-None value on the right side of =>. Because the first arm matched, no other
-arms are compared.
Combining match and enums is useful in many situations. You’ll see this
-pattern a lot in Rust code: match against an enum, bind a variable to the
-data inside, and then execute code based on it. It’s a bit tricky at first, but
-once you get used to it, you’ll wish you had it in all languages. It’s
-consistently a user favorite.
There’s one other aspect of match we need to discuss: The arms’ patterns must
-cover all possibilities. Consider this version of our plus_one function,
-which has a bug and won’t compile:
fn main() {
- fn plus_one(x: Option<i32>) -> Option<i32> {
- match x {
- Some(i) => Some(i + 1),
- }
- }
-
- let five = Some(5);
- let six = plus_one(five);
- let none = plus_one(None);
-}
-We didn’t handle the None case, so this code will cause a bug. Luckily, it’s
-a bug Rust knows how to catch. If we try to compile this code, we’ll get this
-error:
$ cargo run
- Compiling enums v0.1.0 (file:///projects/enums)
-error[E0004]: non-exhaustive patterns: `None` not covered
- --> src/main.rs:3:15
- |
-3 | match x {
- | ^ pattern `None` not covered
- |
-note: `Option<i32>` defined here
- --> /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:593:1
- ::: /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:597:5
- |
- = note: not covered
- = note: the matched value is of type `Option<i32>`
-help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
- |
-4 ~ Some(i) => Some(i + 1),
-5 ~ None => todo!(),
- |
-
-For more information about this error, try `rustc --explain E0004`.
-error: could not compile `enums` (bin "enums") due to 1 previous error
-
-Rust knows that we didn’t cover every possible case and even knows which
-pattern we forgot! Matches in Rust are exhaustive: We must exhaust every last
-possibility in order for the code to be valid. Especially in the case of
-Option<T>, when Rust prevents us from forgetting to explicitly handle the
-None case, it protects us from assuming that we have a value when we might
-have null, thus making the billion-dollar mistake discussed earlier impossible.
_ PlaceholderUsing enums, we can also take special actions for a few particular values, but
-for all other values take one default action. Imagine we’re implementing a game
-where, if you roll a 3 on a dice roll, your player doesn’t move but instead
-gets a fancy new hat. If you roll a 7, your player loses a fancy hat. For all
-other values, your player moves that number of spaces on the game board. Here’s
-a match that implements that logic, with the result of the dice roll
-hardcoded rather than a random value, and all other logic represented by
-functions without bodies because actually implementing them is out of scope for
-this example:
fn main() {
- let dice_roll = 9;
- match dice_roll {
- 3 => add_fancy_hat(),
- 7 => remove_fancy_hat(),
- other => move_player(other),
- }
-
- fn add_fancy_hat() {}
- fn remove_fancy_hat() {}
- fn move_player(num_spaces: u8) {}
-}
-For the first two arms, the patterns are the literal values 3 and 7. For
-the last arm that covers every other possible value, the pattern is the
-variable we’ve chosen to name other. The code that runs for the other arm
-uses the variable by passing it to the move_player function.
This code compiles, even though we haven’t listed all the possible values a
-u8 can have, because the last pattern will match all values not specifically
-listed. This catch-all pattern meets the requirement that match must be
-exhaustive. Note that we have to put the catch-all arm last because the
-patterns are evaluated in order. If we had put the catch-all arm earlier, the
-other arms would never run, so Rust will warn us if we add arms after a
-catch-all!
Rust also has a pattern we can use when we want a catch-all but don’t want to
-use the value in the catch-all pattern: _ is a special pattern that matches
-any value and does not bind to that value. This tells Rust we aren’t going to
-use the value, so Rust won’t warn us about an unused variable.
Let’s change the rules of the game: Now, if you roll anything other than a 3 or
-a 7, you must roll again. We no longer need to use the catch-all value, so we
-can change our code to use _ instead of the variable named other:
fn main() {
- let dice_roll = 9;
- match dice_roll {
- 3 => add_fancy_hat(),
- 7 => remove_fancy_hat(),
- _ => reroll(),
- }
-
- fn add_fancy_hat() {}
- fn remove_fancy_hat() {}
- fn reroll() {}
-}
-This example also meets the exhaustiveness requirement because we’re explicitly -ignoring all other values in the last arm; we haven’t forgotten anything.
-Finally, we’ll change the rules of the game one more time so that nothing else
-happens on your turn if you roll anything other than a 3 or a 7. We can express
-that by using the unit value (the empty tuple type we mentioned in “The Tuple
-Type” section) as the code that goes with the _ arm:
fn main() {
- let dice_roll = 9;
- match dice_roll {
- 3 => add_fancy_hat(),
- 7 => remove_fancy_hat(),
- _ => (),
- }
-
- fn add_fancy_hat() {}
- fn remove_fancy_hat() {}
-}
-Here, we’re telling Rust explicitly that we aren’t going to use any other value -that doesn’t match a pattern in an earlier arm, and we don’t want to run any -code in this case.
-There’s more about patterns and matching that we’ll cover in Chapter
-19. For now, we’re going to move on to the
-if let syntax, which can be useful in situations where the match expression
-is a bit wordy.
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
-if let and let...elseThe if let syntax lets you combine if and let into a less verbose way to
-handle values that match one pattern while ignoring the rest. Consider the
-program in Listing 6-6 that matches on an Option<u8> value in the
-config_max variable but only wants to execute code if the value is the Some
-variant.
fn main() {
- let config_max = Some(3u8);
- match config_max {
- Some(max) => println!("The maximum is configured to be {max}"),
- _ => (),
- }
-}
-match that only cares about executing code when the value is SomeIf the value is Some, we print out the value in the Some variant by binding
-the value to the variable max in the pattern. We don’t want to do anything
-with the None value. To satisfy the match expression, we have to add _ => () after processing just one variant, which is annoying boilerplate code to
-add.
Instead, we could write this in a shorter way using if let. The following
-code behaves the same as the match in Listing 6-6:
fn main() {
- let config_max = Some(3u8);
- if let Some(max) = config_max {
- println!("The maximum is configured to be {max}");
- }
-}
-The syntax if let takes a pattern and an expression separated by an equal
-sign. It works the same way as a match, where the expression is given to the
-match and the pattern is its first arm. In this case, the pattern is
-Some(max), and the max binds to the value inside the Some. We can then
-use max in the body of the if let block in the same way we used max in
-the corresponding match arm. The code in the if let block only runs if the
-value matches the pattern.
Using if let means less typing, less indentation, and less boilerplate code.
-However, you lose the exhaustive checking match enforces that ensures that
-you aren’t forgetting to handle any cases. Choosing between match and if let depends on what you’re doing in your particular situation and whether
-gaining conciseness is an appropriate trade-off for losing exhaustive checking.
In other words, you can think of if let as syntax sugar for a match that
-runs code when the value matches one pattern and then ignores all other values.
We can include an else with an if let. The block of code that goes with the
-else is the same as the block of code that would go with the _ case in the
-match expression that is equivalent to the if let and else. Recall the
-Coin enum definition in Listing 6-4, where the Quarter variant also held a
-UsState value. If we wanted to count all non-quarter coins we see while also
-announcing the state of the quarters, we could do that with a match
-expression, like this:
#[derive(Debug)]
-enum UsState {
- Alabama,
- Alaska,
- // --snip--
-}
-
-enum Coin {
- Penny,
- Nickel,
- Dime,
- Quarter(UsState),
-}
-
-fn main() {
- let coin = Coin::Penny;
- let mut count = 0;
- match coin {
- Coin::Quarter(state) => println!("State quarter from {state:?}!"),
- _ => count += 1,
- }
-}
-Or we could use an if let and else expression, like this:
#[derive(Debug)]
-enum UsState {
- Alabama,
- Alaska,
- // --snip--
-}
-
-enum Coin {
- Penny,
- Nickel,
- Dime,
- Quarter(UsState),
-}
-
-fn main() {
- let coin = Coin::Penny;
- let mut count = 0;
- if let Coin::Quarter(state) = coin {
- println!("State quarter from {state:?}!");
- } else {
- count += 1;
- }
-}
-let...elseThe common pattern is to perform some computation when a value is present and
-return a default value otherwise. Continuing with our example of coins with a
-UsState value, if we wanted to say something funny depending on how old the
-state on the quarter was, we might introduce a method on UsState to check the
-age of a state, like so:
#[derive(Debug)] // so we can inspect the state in a minute
-enum UsState {
- Alabama,
- Alaska,
- // --snip--
-}
-
-impl UsState {
- fn existed_in(&self, year: u16) -> bool {
- match self {
- UsState::Alabama => year >= 1819,
- UsState::Alaska => year >= 1959,
- // -- snip --
- }
- }
-}
-
-enum Coin {
- Penny,
- Nickel,
- Dime,
- Quarter(UsState),
-}
-
-fn describe_state_quarter(coin: Coin) -> Option<String> {
- if let Coin::Quarter(state) = coin {
- if state.existed_in(1900) {
- Some(format!("{state:?} is pretty old, for America!"))
- } else {
- Some(format!("{state:?} is relatively new."))
- }
- } else {
- None
- }
-}
-
-fn main() {
- if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
- println!("{desc}");
- }
-}
-Then, we might use if let to match on the type of coin, introducing a state
-variable within the body of the condition, as in Listing 6-7.
#[derive(Debug)] // so we can inspect the state in a minute
-enum UsState {
- Alabama,
- Alaska,
- // --snip--
-}
-
-impl UsState {
- fn existed_in(&self, year: u16) -> bool {
- match self {
- UsState::Alabama => year >= 1819,
- UsState::Alaska => year >= 1959,
- // -- snip --
- }
- }
-}
-
-enum Coin {
- Penny,
- Nickel,
- Dime,
- Quarter(UsState),
-}
-
-fn describe_state_quarter(coin: Coin) -> Option<String> {
- if let Coin::Quarter(state) = coin {
- if state.existed_in(1900) {
- Some(format!("{state:?} is pretty old, for America!"))
- } else {
- Some(format!("{state:?} is relatively new."))
- }
- } else {
- None
- }
-}
-
-fn main() {
- if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
- println!("{desc}");
- }
-}
-if letThat gets the job done, but it has pushed the work into the body of the if let statement, and if the work to be done is more complicated, it might be
-hard to follow exactly how the top-level branches relate. We could also take
-advantage of the fact that expressions produce a value either to produce the
-state from the if let or to return early, as in Listing 6-8. (You could do
-something similar with a match, too.)
#[derive(Debug)] // so we can inspect the state in a minute
-enum UsState {
- Alabama,
- Alaska,
- // --snip--
-}
-
-impl UsState {
- fn existed_in(&self, year: u16) -> bool {
- match self {
- UsState::Alabama => year >= 1819,
- UsState::Alaska => year >= 1959,
- // -- snip --
- }
- }
-}
-
-enum Coin {
- Penny,
- Nickel,
- Dime,
- Quarter(UsState),
-}
-
-fn describe_state_quarter(coin: Coin) -> Option<String> {
- let state = if let Coin::Quarter(state) = coin {
- state
- } else {
- return None;
- };
-
- if state.existed_in(1900) {
- Some(format!("{state:?} is pretty old, for America!"))
- } else {
- Some(format!("{state:?} is relatively new."))
- }
-}
-
-fn main() {
- if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
- println!("{desc}");
- }
-}
-if let to produce a value or return earlyThis is a bit annoying to follow in its own way, though! One branch of the if let produces a value, and the other one returns from the function entirely.
To make this common pattern nicer to express, Rust has let...else. The
-let...else syntax takes a pattern on the left side and an expression on the
-right, very similar to if let, but it does not have an if branch, only an
-else branch. If the pattern matches, it will bind the value from the pattern
-in the outer scope. If the pattern does not match, the program will flow into
-the else arm, which must return from the function.
In Listing 6-9, you can see how Listing 6-8 looks when using let...else in
-place of if let.
#[derive(Debug)] // so we can inspect the state in a minute
-enum UsState {
- Alabama,
- Alaska,
- // --snip--
-}
-
-impl UsState {
- fn existed_in(&self, year: u16) -> bool {
- match self {
- UsState::Alabama => year >= 1819,
- UsState::Alaska => year >= 1959,
- // -- snip --
- }
- }
-}
-
-enum Coin {
- Penny,
- Nickel,
- Dime,
- Quarter(UsState),
-}
-
-fn describe_state_quarter(coin: Coin) -> Option<String> {
- let Coin::Quarter(state) = coin else {
- return None;
- };
-
- if state.existed_in(1900) {
- Some(format!("{state:?} is pretty old, for America!"))
- } else {
- Some(format!("{state:?} is relatively new."))
- }
-}
-
-fn main() {
- if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
- println!("{desc}");
- }
-}
-let...else to clarify the flow through the functionNotice that it stays on the “happy path” in the main body of the function this
-way, without having significantly different control flow for two branches the
-way the if let did.
If you have a situation in which your program has logic that is too verbose to
-express using a match, remember that if let and let...else are in your
-Rust toolbox as well.
We’ve now covered how to use enums to create custom types that can be one of a
-set of enumerated values. We’ve shown how the standard library’s Option<T>
-type helps you use the type system to prevent errors. When enum values have
-data inside them, you can use match or if let to extract and use those
-values, depending on how many cases you need to handle.
Your Rust programs can now express concepts in your domain using structs and -enums. Creating custom types to use in your API ensures type safety: The -compiler will make certain your functions only get values of the type each -function expects.
-In order to provide a well-organized API to your users that is straightforward -to use and only exposes exactly what your users will need, let’s now turn to -Rust’s modules.
- -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
-As you write large programs, organizing your code will become increasingly -important. By grouping related functionality and separating code with distinct -features, you’ll clarify where to find code that implements a particular -feature and where to go to change how a feature works.
-The programs we’ve written so far have been in one module in one file. As a -project grows, you should organize code by splitting it into multiple modules -and then multiple files. A package can contain multiple binary crates and -optionally one library crate. As a package grows, you can extract parts into -separate crates that become external dependencies. This chapter covers all -these techniques. For very large projects comprising a set of interrelated -packages that evolve together, Cargo provides workspaces, which we’ll cover in -“Cargo Workspaces” in Chapter 14.
-We’ll also discuss encapsulating implementation details, which lets you reuse -code at a higher level: Once you’ve implemented an operation, other code can -call your code via its public interface without having to know how the -implementation works. The way you write code defines which parts are public for -other code to use and which parts are private implementation details that you -reserve the right to change. This is another way to limit the amount of detail -you have to keep in your head.
-A related concept is scope: The nested context in which code is written has a -set of names that are defined as “in scope.” When reading, writing, and -compiling code, programmers and compilers need to know whether a particular -name at a particular spot refers to a variable, function, struct, enum, module, -constant, or other item and what that item means. You can create scopes and -change which names are in or out of scope. You can’t have two items with the -same name in the same scope; tools are available to resolve name conflicts.
-Rust has a number of features that allow you to manage your code’s -organization, including which details are exposed, which details are private, -and what names are in each scope in your programs. These features, sometimes -collectively referred to as the module system, include:
-In this chapter, we’ll cover all these features, discuss how they interact, and -explain how to use them to manage scope. By the end, you should have a solid -understanding of the module system and be able to work with scopes like a pro!
- -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
-The first parts of the module system we’ll cover are packages and crates.
-A crate is the smallest amount of code that the Rust compiler considers at a
-time. Even if you run rustc rather than cargo and pass a single source code
-file (as we did all the way back in “Rust Program Basics” in Chapter 1), the compiler considers that file to be a crate. Crates can
-contain modules, and the modules may be defined in other files that get
-compiled with the crate, as we’ll see in the coming sections.
A crate can come in one of two forms: a binary crate or a library crate.
-Binary crates are programs you can compile to an executable that you can run,
-such as a command line program or a server. Each must have a function called
-main that defines what happens when the executable runs. All the crates we’ve
-created so far have been binary crates.
Library crates don’t have a main function, and they don’t compile to an
-executable. Instead, they define functionality intended to be shared with
-multiple projects. For example, the rand crate we used in Chapter
-2 provides functionality that generates random numbers.
-Most of the time when Rustaceans say “crate,” they mean library crate, and they
-use “crate” interchangeably with the general programming concept of a “library.”
The crate root is a source file that the Rust compiler starts from and makes -up the root module of your crate (we’ll explain modules in depth in “Control -Scope and Privacy with Modules”).
-A package is a bundle of one or more crates that provides a set of -functionality. A package contains a Cargo.toml file that describes how to -build those crates. Cargo is actually a package that contains the binary crate -for the command line tool you’ve been using to build your code. The Cargo -package also contains a library crate that the binary crate depends on. Other -projects can depend on the Cargo library crate to use the same logic the Cargo -command line tool uses.
-A package can contain as many binary crates as you like, but at most only one -library crate. A package must contain at least one crate, whether that’s a -library or binary crate.
-Let’s walk through what happens when we create a package. First, we enter the
-command cargo new my-project:
$ cargo new my-project
- Created binary (application) `my-project` package
-$ ls my-project
-Cargo.toml
-src
-$ ls my-project/src
-main.rs
-
-After we run cargo new my-project, we use ls to see what Cargo creates. In
-the my-project directory, there’s a Cargo.toml file, giving us a package.
-There’s also a src directory that contains main.rs. Open Cargo.toml in
-your text editor and note that there’s no mention of src/main.rs. Cargo
-follows a convention that src/main.rs is the crate root of a binary crate
-with the same name as the package. Likewise, Cargo knows that if the package
-directory contains src/lib.rs, the package contains a library crate with the
-same name as the package, and src/lib.rs is its crate root. Cargo passes the
-crate root files to rustc to build the library or binary.
Here, we have a package that only contains src/main.rs, meaning it only
-contains a binary crate named my-project. If a package contains src/main.rs
-and src/lib.rs, it has two crates: a binary and a library, both with the same
-name as the package. A package can have multiple binary crates by placing files
-in the src/bin directory: Each file will be a separate binary crate.
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
-In this section, we’ll talk about modules and other parts of the module system,
-namely paths, which allow you to name items; the use keyword that brings a
-path into scope; and the pub keyword to make items public. We’ll also discuss
-the as keyword, external packages, and the glob operator.
Before we get to the details of modules and paths, here we provide a quick
-reference on how modules, paths, the use keyword, and the pub keyword work
-in the compiler, and how most developers organize their code. We’ll be going
-through examples of each of these rules throughout this chapter, but this is a
-great place to refer to as a reminder of how modules work.
mod garden;. The compiler will look
-for the module’s code in these places:
-mod gardenmod vegetables; in
-src/garden.rs. The compiler will look for the submodule’s code within the
-directory named for the parent module in these places:
-mod vegetables, within curly brackets instead
-of the semicolonAsparagus type in the garden vegetables module would be found at
-crate::garden::vegetables::Asparagus.pub mod
-instead of mod. To make items within a public module public as well, use
-pub before their declarations.use keyword: Within a scope, the use keyword creates shortcuts to
-items to reduce repetition of long paths. In any scope that can refer to
-crate::garden::vegetables::Asparagus, you can create a shortcut with use crate::garden::vegetables::Asparagus;, and from then on you only need to
-write Asparagus to make use of that type in the scope.Here, we create a binary crate named backyard that illustrates these rules.
-The crate’s directory, also named backyard, contains these files and
-directories:
backyard
-├── Cargo.lock
-├── Cargo.toml
-└── src
- ├── garden
- │ └── vegetables.rs
- ├── garden.rs
- └── main.rs
-
-The crate root file in this case is src/main.rs, and it contains:
-use crate::garden::vegetables::Asparagus;
-
-pub mod garden;
-
-fn main() {
- let plant = Asparagus {};
- println!("I'm growing {plant:?}!");
-}
-The pub mod garden; line tells the compiler to include the code it finds in
-src/garden.rs, which is:
pub mod vegetables;
-Here, pub mod vegetables; means the code in src/garden/vegetables.rs is
-included too. That code is:
#[derive(Debug)]
-pub struct Asparagus {}
-Now let’s get into the details of these rules and demonstrate them in action!
-Modules let us organize code within a crate for readability and easy reuse. -Modules also allow us to control the privacy of items because code within a -module is private by default. Private items are internal implementation details -not available for outside use. We can choose to make modules and the items -within them public, which exposes them to allow external code to use and depend -on them.
-As an example, let’s write a library crate that provides the functionality of a -restaurant. We’ll define the signatures of functions but leave their bodies -empty to concentrate on the organization of the code rather than the -implementation of a restaurant.
-In the restaurant industry, some parts of a restaurant are referred to as front -of house and others as back of house. Front of house is where customers are; -this encompasses where the hosts seat customers, servers take orders and -payment, and bartenders make drinks. Back of house is where the chefs and -cooks work in the kitchen, dishwashers clean up, and managers do administrative -work.
-To structure our crate in this way, we can organize its functions into nested
-modules. Create a new library named restaurant by running cargo new restaurant --lib. Then, enter the code in Listing 7-1 into src/lib.rs to
-define some modules and function signatures; this code is the front of house
-section.
mod front_of_house {
- mod hosting {
- fn add_to_waitlist() {}
-
- fn seat_at_table() {}
- }
-
- mod serving {
- fn take_order() {}
-
- fn serve_order() {}
-
- fn take_payment() {}
- }
-}
-front_of_house module containing other modules that then contain functionsWe define a module with the mod keyword followed by the name of the module
-(in this case, front_of_house). The body of the module then goes inside curly
-brackets. Inside modules, we can place other modules, as in this case with the
-modules hosting and serving. Modules can also hold definitions for other
-items, such as structs, enums, constants, traits, and as in Listing 7-1,
-functions.
By using modules, we can group related definitions together and name why -they’re related. Programmers using this code can navigate the code based on the -groups rather than having to read through all the definitions, making it easier -to find the definitions relevant to them. Programmers adding new functionality -to this code would know where to place the code to keep the program organized.
-Earlier, we mentioned that src/main.rs and src/lib.rs are called crate
-roots. The reason for their name is that the contents of either of these two
-files form a module named crate at the root of the crate’s module structure,
-known as the module tree.
Listing 7-2 shows the module tree for the structure in Listing 7-1.
-crate
- └── front_of_house
- ├── hosting
- │ ├── add_to_waitlist
- │ └── seat_at_table
- └── serving
- ├── take_order
- ├── serve_order
- └── take_payment
-
-This tree shows how some of the modules nest inside other modules; for example,
-hosting nests inside front_of_house. The tree also shows that some modules
-are siblings, meaning they’re defined in the same module; hosting and
-serving are siblings defined within front_of_house. If module A is
-contained inside module B, we say that module A is the child of module B and
-that module B is the parent of module A. Notice that the entire module tree
-is rooted under the implicit module named crate.
The module tree might remind you of the filesystem’s directory tree on your -computer; this is a very apt comparison! Just like directories in a filesystem, -you use modules to organize your code. And just like files in a directory, we -need a way to find our modules.
- -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
-To show Rust where to find an item in a module tree, we use a path in the same -way we use a path when navigating a filesystem. To call a function, we need to -know its path.
-A path can take two forms:
-crate.self, super, or
-an identifier in the current module.Both absolute and relative paths are followed by one or more identifiers
-separated by double colons (::).
Returning to Listing 7-1, say we want to call the add_to_waitlist function.
-This is the same as asking: What’s the path of the add_to_waitlist function?
-Listing 7-3 contains Listing 7-1 with some of the modules and functions removed.
We’ll show two ways to call the add_to_waitlist function from a new function,
-eat_at_restaurant, defined in the crate root. These paths are correct, but
-there’s another problem remaining that will prevent this example from compiling
-as is. We’ll explain why in a bit.
The eat_at_restaurant function is part of our library crate’s public API, so
-we mark it with the pub keyword. In the “Exposing Paths with the pub
-Keyword” section, we’ll go into more detail about pub.
mod front_of_house {
- mod hosting {
- fn add_to_waitlist() {}
- }
-}
-
-pub fn eat_at_restaurant() {
- // Absolute path
- crate::front_of_house::hosting::add_to_waitlist();
-
- // Relative path
- front_of_house::hosting::add_to_waitlist();
-}
-add_to_waitlist function using absolute and relative pathsThe first time we call the add_to_waitlist function in eat_at_restaurant,
-we use an absolute path. The add_to_waitlist function is defined in the same
-crate as eat_at_restaurant, which means we can use the crate keyword to
-start an absolute path. We then include each of the successive modules until we
-make our way to add_to_waitlist. You can imagine a filesystem with the same
-structure: We’d specify the path /front_of_house/hosting/add_to_waitlist to
-run the add_to_waitlist program; using the crate name to start from the
-crate root is like using / to start from the filesystem root in your shell.
The second time we call add_to_waitlist in eat_at_restaurant, we use a
-relative path. The path starts with front_of_house, the name of the module
-defined at the same level of the module tree as eat_at_restaurant. Here the
-filesystem equivalent would be using the path
-front_of_house/hosting/add_to_waitlist. Starting with a module name means
-that the path is relative.
Choosing whether to use a relative or absolute path is a decision you’ll make
-based on your project, and it depends on whether you’re more likely to move
-item definition code separately from or together with the code that uses the
-item. For example, if we moved the front_of_house module and the
-eat_at_restaurant function into a module named customer_experience, we’d
-need to update the absolute path to add_to_waitlist, but the relative path
-would still be valid. However, if we moved the eat_at_restaurant function
-separately into a module named dining, the absolute path to the
-add_to_waitlist call would stay the same, but the relative path would need to
-be updated. Our preference in general is to specify absolute paths because it’s
-more likely we’ll want to move code definitions and item calls independently of
-each other.
Let’s try to compile Listing 7-3 and find out why it won’t compile yet! The -errors we get are shown in Listing 7-4.
-$ cargo build
- Compiling restaurant v0.1.0 (file:///projects/restaurant)
-error[E0603]: module `hosting` is private
- --> src/lib.rs:9:28
- |
-9 | crate::front_of_house::hosting::add_to_waitlist();
- | ^^^^^^^ --------------- function `add_to_waitlist` is not publicly re-exported
- | |
- | private module
- |
-note: the module `hosting` is defined here
- --> src/lib.rs:2:5
- |
-2 | mod hosting {
- | ^^^^^^^^^^^
-
-error[E0603]: module `hosting` is private
- --> src/lib.rs:12:21
- |
-12 | front_of_house::hosting::add_to_waitlist();
- | ^^^^^^^ --------------- function `add_to_waitlist` is not publicly re-exported
- | |
- | private module
- |
-note: the module `hosting` is defined here
- --> src/lib.rs:2:5
- |
- 2 | mod hosting {
- | ^^^^^^^^^^^
-
-For more information about this error, try `rustc --explain E0603`.
-error: could not compile `restaurant` (lib) due to 2 previous errors
-
-The error messages say that module hosting is private. In other words, we
-have the correct paths for the hosting module and the add_to_waitlist
-function, but Rust won’t let us use them because it doesn’t have access to the
-private sections. In Rust, all items (functions, methods, structs, enums,
-modules, and constants) are private to parent modules by default. If you want
-to make an item like a function or struct private, you put it in a module.
Items in a parent module can’t use the private items inside child modules, but -items in child modules can use the items in their ancestor modules. This is -because child modules wrap and hide their implementation details, but the child -modules can see the context in which they’re defined. To continue with our -metaphor, think of the privacy rules as being like the back office of a -restaurant: What goes on in there is private to restaurant customers, but -office managers can see and do everything in the restaurant they operate.
-Rust chose to have the module system function this way so that hiding inner
-implementation details is the default. That way, you know which parts of the
-inner code you can change without breaking the outer code. However, Rust does
-give you the option to expose inner parts of child modules’ code to outer
-ancestor modules by using the pub keyword to make an item public.
pub KeywordLet’s return to the error in Listing 7-4 that told us the hosting module is
-private. We want the eat_at_restaurant function in the parent module to have
-access to the add_to_waitlist function in the child module, so we mark the
-hosting module with the pub keyword, as shown in Listing 7-5.
mod front_of_house {
- pub mod hosting {
- fn add_to_waitlist() {}
- }
-}
-
-// -- snip --
-pub fn eat_at_restaurant() {
- // Absolute path
- crate::front_of_house::hosting::add_to_waitlist();
-
- // Relative path
- front_of_house::hosting::add_to_waitlist();
-}
-hosting module as pub to use it from eat_at_restaurantUnfortunately, the code in Listing 7-5 still results in compiler errors, as -shown in Listing 7-6.
-$ cargo build
- Compiling restaurant v0.1.0 (file:///projects/restaurant)
-error[E0603]: function `add_to_waitlist` is private
- --> src/lib.rs:10:37
- |
-10 | crate::front_of_house::hosting::add_to_waitlist();
- | ^^^^^^^^^^^^^^^ private function
- |
-note: the function `add_to_waitlist` is defined here
- --> src/lib.rs:3:9
- |
- 3 | fn add_to_waitlist() {}
- | ^^^^^^^^^^^^^^^^^^^^
-
-error[E0603]: function `add_to_waitlist` is private
- --> src/lib.rs:13:30
- |
-13 | front_of_house::hosting::add_to_waitlist();
- | ^^^^^^^^^^^^^^^ private function
- |
-note: the function `add_to_waitlist` is defined here
- --> src/lib.rs:3:9
- |
- 3 | fn add_to_waitlist() {}
- | ^^^^^^^^^^^^^^^^^^^^
-
-For more information about this error, try `rustc --explain E0603`.
-error: could not compile `restaurant` (lib) due to 2 previous errors
-
-What happened? Adding the pub keyword in front of mod hosting makes the
-module public. With this change, if we can access front_of_house, we can
-access hosting. But the contents of hosting are still private; making the
-module public doesn’t make its contents public. The pub keyword on a module
-only lets code in its ancestor modules refer to it, not access its inner code.
-Because modules are containers, there’s not much we can do by only making the
-module public; we need to go further and choose to make one or more of the
-items within the module public as well.
The errors in Listing 7-6 say that the add_to_waitlist function is private.
-The privacy rules apply to structs, enums, functions, and methods as well as
-modules.
Let’s also make the add_to_waitlist function public by adding the pub
-keyword before its definition, as in Listing 7-7.
mod front_of_house {
- pub mod hosting {
- pub fn add_to_waitlist() {}
- }
-}
-
-// -- snip --
-pub fn eat_at_restaurant() {
- // Absolute path
- crate::front_of_house::hosting::add_to_waitlist();
-
- // Relative path
- front_of_house::hosting::add_to_waitlist();
-}
-pub keyword to mod hosting and fn add_to_waitlist lets us call the function from eat_at_restaurant.Now the code will compile! To see why adding the pub keyword lets us use
-these paths in eat_at_restaurant with respect to the privacy rules, let’s
-look at the absolute and the relative paths.
In the absolute path, we start with crate, the root of our crate’s module
-tree. The front_of_house module is defined in the crate root. While
-front_of_house isn’t public, because the eat_at_restaurant function is
-defined in the same module as front_of_house (that is, eat_at_restaurant
-and front_of_house are siblings), we can refer to front_of_house from
-eat_at_restaurant. Next is the hosting module marked with pub. We can
-access the parent module of hosting, so we can access hosting. Finally, the
-add_to_waitlist function is marked with pub, and we can access its parent
-module, so this function call works!
In the relative path, the logic is the same as the absolute path except for the
-first step: Rather than starting from the crate root, the path starts from
-front_of_house. The front_of_house module is defined within the same module
-as eat_at_restaurant, so the relative path starting from the module in which
-eat_at_restaurant is defined works. Then, because hosting and
-add_to_waitlist are marked with pub, the rest of the path works, and this
-function call is valid!
If you plan to share your library crate so that other projects can use your -code, your public API is your contract with users of your crate that determines -how they can interact with your code. There are many considerations around -managing changes to your public API to make it easier for people to depend on -your crate. These considerations are beyond the scope of this book; if you’re -interested in this topic, see the Rust API Guidelines.
-We mentioned that a package can contain both a src/main.rs binary crate -root as well as a src/lib.rs library crate root, and both crates will have -the package name by default. Typically, packages with this pattern of -containing both a library and a binary crate will have just enough code in the -binary crate to start an executable that calls code defined in the library -crate. This lets other projects benefit from the most functionality that the -package provides because the library crate’s code can be shared.
-The module tree should be defined in src/lib.rs. Then, any public items can -be used in the binary crate by starting paths with the name of the package. -The binary crate becomes a user of the library crate just like a completely -external crate would use the library crate: It can only use the public API. -This helps you design a good API; not only are you the author, but you’re -also a client!
-In Chapter 12, we’ll demonstrate this organizational -practice with a command line program that will contain both a binary crate -and a library crate.
-superWe can construct relative paths that begin in the parent module, rather than
-the current module or the crate root, by using super at the start of the
-path. This is like starting a filesystem path with the .. syntax that means
-to go to the parent directory. Using super allows us to reference an item
-that we know is in the parent module, which can make rearranging the module
-tree easier when the module is closely related to the parent but the parent
-might be moved elsewhere in the module tree someday.
Consider the code in Listing 7-8 that models the situation in which a chef
-fixes an incorrect order and personally brings it out to the customer. The
-function fix_incorrect_order defined in the back_of_house module calls the
-function deliver_order defined in the parent module by specifying the path to
-deliver_order, starting with super.
fn deliver_order() {}
-
-mod back_of_house {
- fn fix_incorrect_order() {
- cook_order();
- super::deliver_order();
- }
-
- fn cook_order() {}
-}
-superThe fix_incorrect_order function is in the back_of_house module, so we can
-use super to go to the parent module of back_of_house, which in this case
-is crate, the root. From there, we look for deliver_order and find it.
-Success! We think the back_of_house module and the deliver_order function
-are likely to stay in the same relationship to each other and get moved
-together should we decide to reorganize the crate’s module tree. Therefore, we
-used super so that we’ll have fewer places to update code in the future if
-this code gets moved to a different module.
We can also use pub to designate structs and enums as public, but there are a
-few extra details to the usage of pub with structs and enums. If we use pub
-before a struct definition, we make the struct public, but the struct’s fields
-will still be private. We can make each field public or not on a case-by-case
-basis. In Listing 7-9, we’ve defined a public back_of_house::Breakfast struct
-with a public toast field but a private seasonal_fruit field. This models
-the case in a restaurant where the customer can pick the type of bread that
-comes with a meal, but the chef decides which fruit accompanies the meal based
-on what’s in season and in stock. The available fruit changes quickly, so
-customers can’t choose the fruit or even see which fruit they’ll get.
mod back_of_house {
- pub struct Breakfast {
- pub toast: String,
- seasonal_fruit: String,
- }
-
- impl Breakfast {
- pub fn summer(toast: &str) -> Breakfast {
- Breakfast {
- toast: String::from(toast),
- seasonal_fruit: String::from("peaches"),
- }
- }
- }
-}
-
-pub fn eat_at_restaurant() {
- // Order a breakfast in the summer with Rye toast.
- let mut meal = back_of_house::Breakfast::summer("Rye");
- // Change our mind about what bread we'd like.
- meal.toast = String::from("Wheat");
- println!("I'd like {} toast please", meal.toast);
-
- // The next line won't compile if we uncomment it; we're not allowed
- // to see or modify the seasonal fruit that comes with the meal.
- // meal.seasonal_fruit = String::from("blueberries");
-}
-Because the toast field in the back_of_house::Breakfast struct is public,
-in eat_at_restaurant we can write and read to the toast field using dot
-notation. Notice that we can’t use the seasonal_fruit field in
-eat_at_restaurant, because seasonal_fruit is private. Try uncommenting the
-line modifying the seasonal_fruit field value to see what error you get!
Also, note that because back_of_house::Breakfast has a private field, the
-struct needs to provide a public associated function that constructs an
-instance of Breakfast (we’ve named it summer here). If Breakfast didn’t
-have such a function, we couldn’t create an instance of Breakfast in
-eat_at_restaurant, because we couldn’t set the value of the private
-seasonal_fruit field in eat_at_restaurant.
In contrast, if we make an enum public, all of its variants are then public. We
-only need the pub before the enum keyword, as shown in Listing 7-10.
mod back_of_house {
- pub enum Appetizer {
- Soup,
- Salad,
- }
-}
-
-pub fn eat_at_restaurant() {
- let order1 = back_of_house::Appetizer::Soup;
- let order2 = back_of_house::Appetizer::Salad;
-}
-Because we made the Appetizer enum public, we can use the Soup and Salad
-variants in eat_at_restaurant.
Enums aren’t very useful unless their variants are public; it would be annoying
-to have to annotate all enum variants with pub in every case, so the default
-for enum variants is to be public. Structs are often useful without their
-fields being public, so struct fields follow the general rule of everything
-being private by default unless annotated with pub.
There’s one more situation involving pub that we haven’t covered, and that is
-our last module system feature: the use keyword. We’ll cover use by itself
-first, and then we’ll show how to combine pub and use.
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
-use KeywordHaving to write out the paths to call functions can feel inconvenient and
-repetitive. In Listing 7-7, whether we chose the absolute or relative path to
-the add_to_waitlist function, every time we wanted to call add_to_waitlist
-we had to specify front_of_house and hosting too. Fortunately, there’s a
-way to simplify this process: We can create a shortcut to a path with the use
-keyword once and then use the shorter name everywhere else in the scope.
In Listing 7-11, we bring the crate::front_of_house::hosting module into the
-scope of the eat_at_restaurant function so that we only have to specify
-hosting::add_to_waitlist to call the add_to_waitlist function in
-eat_at_restaurant.
mod front_of_house {
- pub mod hosting {
- pub fn add_to_waitlist() {}
- }
-}
-
-use crate::front_of_house::hosting;
-
-pub fn eat_at_restaurant() {
- hosting::add_to_waitlist();
-}
-useAdding use and a path in a scope is similar to creating a symbolic link in
-the filesystem. By adding use crate::front_of_house::hosting in the crate
-root, hosting is now a valid name in that scope, just as though the hosting
-module had been defined in the crate root. Paths brought into scope with use
-also check privacy, like any other paths.
Note that use only creates the shortcut for the particular scope in which the
-use occurs. Listing 7-12 moves the eat_at_restaurant function into a new
-child module named customer, which is then a different scope than the use
-statement, so the function body won’t compile.
mod front_of_house {
- pub mod hosting {
- pub fn add_to_waitlist() {}
- }
-}
-
-use crate::front_of_house::hosting;
-
-mod customer {
- pub fn eat_at_restaurant() {
- hosting::add_to_waitlist();
- }
-}
-use statement only applies in the scope it’s in.The compiler error shows that the shortcut no longer applies within the
-customer module:
$ cargo build
- Compiling restaurant v0.1.0 (file:///projects/restaurant)
-error[E0433]: failed to resolve: use of unresolved module or unlinked crate `hosting`
- --> src/lib.rs:11:9
- |
-11 | hosting::add_to_waitlist();
- | ^^^^^^^ use of unresolved module or unlinked crate `hosting`
- |
- = help: if you wanted to use a crate named `hosting`, use `cargo add hosting` to add it to your `Cargo.toml`
-help: consider importing this module through its public re-export
- |
-10 + use crate::hosting;
- |
-
-warning: unused import: `crate::front_of_house::hosting`
- --> src/lib.rs:7:5
- |
-7 | use crate::front_of_house::hosting;
- | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- |
- = note: `#[warn(unused_imports)]` on by default
-
-For more information about this error, try `rustc --explain E0433`.
-warning: `restaurant` (lib) generated 1 warning
-error: could not compile `restaurant` (lib) due to 1 previous error; 1 warning emitted
-
-Notice there’s also a warning that the use is no longer used in its scope! To
-fix this problem, move the use within the customer module too, or reference
-the shortcut in the parent module with super::hosting within the child
-customer module.
use PathsIn Listing 7-11, you might have wondered why we specified use crate::front_of_house::hosting and then called hosting::add_to_waitlist in
-eat_at_restaurant, rather than specifying the use path all the way out to
-the add_to_waitlist function to achieve the same result, as in Listing 7-13.
mod front_of_house {
- pub mod hosting {
- pub fn add_to_waitlist() {}
- }
-}
-
-use crate::front_of_house::hosting::add_to_waitlist;
-
-pub fn eat_at_restaurant() {
- add_to_waitlist();
-}
-add_to_waitlist function into scope with use, which is unidiomaticAlthough both Listing 7-11 and Listing 7-13 accomplish the same task, Listing
-7-11 is the idiomatic way to bring a function into scope with use. Bringing
-the function’s parent module into scope with use means we have to specify the
-parent module when calling the function. Specifying the parent module when
-calling the function makes it clear that the function isn’t locally defined
-while still minimizing repetition of the full path. The code in Listing 7-13 is
-unclear as to where add_to_waitlist is defined.
On the other hand, when bringing in structs, enums, and other items with use,
-it’s idiomatic to specify the full path. Listing 7-14 shows the idiomatic way
-to bring the standard library’s HashMap struct into the scope of a binary
-crate.
use std::collections::HashMap;
-
-fn main() {
- let mut map = HashMap::new();
- map.insert(1, 2);
-}
-HashMap into scope in an idiomatic wayThere’s no strong reason behind this idiom: It’s just the convention that has -emerged, and folks have gotten used to reading and writing Rust code this way.
-The exception to this idiom is if we’re bringing two items with the same name
-into scope with use statements, because Rust doesn’t allow that. Listing 7-15
-shows how to bring two Result types into scope that have the same name but
-different parent modules, and how to refer to them.
use std::fmt;
-use std::io;
-
-fn function1() -> fmt::Result {
- // --snip--
- Ok(())
-}
-
-fn function2() -> io::Result<()> {
- // --snip--
- Ok(())
-}
-As you can see, using the parent modules distinguishes the two Result types.
-If instead we specified use std::fmt::Result and use std::io::Result, we’d
-have two Result types in the same scope, and Rust wouldn’t know which one we
-meant when we used Result.
as KeywordThere’s another solution to the problem of bringing two types of the same name
-into the same scope with use: After the path, we can specify as and a new
-local name, or alias, for the type. Listing 7-16 shows another way to write
-the code in Listing 7-15 by renaming one of the two Result types using as.
use std::fmt::Result;
-use std::io::Result as IoResult;
-
-fn function1() -> Result {
- // --snip--
- Ok(())
-}
-
-fn function2() -> IoResult<()> {
- // --snip--
- Ok(())
-}
-as keywordIn the second use statement, we chose the new name IoResult for the
-std::io::Result type, which won’t conflict with the Result from std::fmt
-that we’ve also brought into scope. Listing 7-15 and Listing 7-16 are
-considered idiomatic, so the choice is up to you!
pub useWhen we bring a name into scope with the use keyword, the name is private to
-the scope into which we imported it. To enable code outside that scope to refer
-to that name as if it had been defined in that scope, we can combine pub and
-use. This technique is called re-exporting because we’re bringing an item
-into scope but also making that item available for others to bring into their
-scope.
Listing 7-17 shows the code in Listing 7-11 with use in the root module
-changed to pub use.
mod front_of_house {
- pub mod hosting {
- pub fn add_to_waitlist() {}
- }
-}
-
-pub use crate::front_of_house::hosting;
-
-pub fn eat_at_restaurant() {
- hosting::add_to_waitlist();
-}
-pub useBefore this change, external code would have to call the add_to_waitlist
-function by using the path
-restaurant::front_of_house::hosting::add_to_waitlist(), which also would have
-required the front_of_house module to be marked as pub. Now that this pub use has re-exported the hosting module from the root module, external code
-can use the path restaurant::hosting::add_to_waitlist() instead.
Re-exporting is useful when the internal structure of your code is different
-from how programmers calling your code would think about the domain. For
-example, in this restaurant metaphor, the people running the restaurant think
-about “front of house” and “back of house.” But customers visiting a restaurant
-probably won’t think about the parts of the restaurant in those terms. With pub use, we can write our code with one structure but expose a different structure.
-Doing so makes our library well organized for programmers working on the library
-and programmers calling the library. We’ll look at another example of pub use
-and how it affects your crate’s documentation in “Exporting a Convenient Public
-API” in Chapter 14.
In Chapter 2, we programmed a guessing game project that used an external
-package called rand to get random numbers. To use rand in our project, we
-added this line to Cargo.toml:
rand = "0.8.5"
-
-Adding rand as a dependency in Cargo.toml tells Cargo to download the
-rand package and any dependencies from crates.io and
-make rand available to our project.
Then, to bring rand definitions into the scope of our package, we added a
-use line starting with the name of the crate, rand, and listed the items we
-wanted to bring into scope. Recall that in “Generating a Random
-Number” in Chapter 2, we brought the Rng trait into
-scope and called the rand::thread_rng function:
use std::io;
-
-use rand::Rng;
-
-fn main() {
- println!("Guess the number!");
-
- let secret_number = rand::thread_rng().gen_range(1..=100);
-
- println!("The secret number is: {secret_number}");
-
- println!("Please input your guess.");
-
- let mut guess = String::new();
-
- io::stdin()
- .read_line(&mut guess)
- .expect("Failed to read line");
-
- println!("You guessed: {guess}");
-}
-Members of the Rust community have made many packages available at
-crates.io, and pulling any of them into your package
-involves these same steps: listing them in your package’s Cargo.toml file and
-using use to bring items from their crates into scope.
Note that the standard std library is also a crate that’s external to our
-package. Because the standard library is shipped with the Rust language, we
-don’t need to change Cargo.toml to include std. But we do need to refer to
-it with use to bring items from there into our package’s scope. For example,
-with HashMap we would use this line:
#![allow(unused)]
-fn main() {
-use std::collections::HashMap;
-}
-This is an absolute path starting with std, the name of the standard library
-crate.
use ListsIf we’re using multiple items defined in the same crate or same module, listing
-each item on its own line can take up a lot of vertical space in our files. For
-example, these two use statements we had in the guessing game in Listing 2-4
-bring items from std into scope:
use rand::Rng;
-// --snip--
-use std::cmp::Ordering;
-use std::io;
-// --snip--
-
-fn main() {
- println!("Guess the number!");
-
- let secret_number = rand::thread_rng().gen_range(1..=100);
-
- println!("The secret number is: {secret_number}");
-
- println!("Please input your guess.");
-
- let mut guess = String::new();
-
- io::stdin()
- .read_line(&mut guess)
- .expect("Failed to read line");
-
- println!("You guessed: {guess}");
-
- match guess.cmp(&secret_number) {
- Ordering::Less => println!("Too small!"),
- Ordering::Greater => println!("Too big!"),
- Ordering::Equal => println!("You win!"),
- }
-}
-Instead, we can use nested paths to bring the same items into scope in one -line. We do this by specifying the common part of the path, followed by two -colons, and then curly brackets around a list of the parts of the paths that -differ, as shown in Listing 7-18.
-use rand::Rng;
-// --snip--
-use std::{cmp::Ordering, io};
-// --snip--
-
-fn main() {
- println!("Guess the number!");
-
- let secret_number = rand::thread_rng().gen_range(1..=100);
-
- println!("The secret number is: {secret_number}");
-
- println!("Please input your guess.");
-
- let mut guess = String::new();
-
- io::stdin()
- .read_line(&mut guess)
- .expect("Failed to read line");
-
- let guess: u32 = guess.trim().parse().expect("Please type a number!");
-
- println!("You guessed: {guess}");
-
- match guess.cmp(&secret_number) {
- Ordering::Less => println!("Too small!"),
- Ordering::Greater => println!("Too big!"),
- Ordering::Equal => println!("You win!"),
- }
-}
-In bigger programs, bringing many items into scope from the same crate or
-module using nested paths can reduce the number of separate use statements
-needed by a lot!
We can use a nested path at any level in a path, which is useful when combining
-two use statements that share a subpath. For example, Listing 7-19 shows two
-use statements: one that brings std::io into scope and one that brings
-std::io::Write into scope.
use std::io;
-use std::io::Write;
-use statements where one is a subpath of the otherThe common part of these two paths is std::io, and that’s the complete first
-path. To merge these two paths into one use statement, we can use self in
-the nested path, as shown in Listing 7-20.
use std::io::{self, Write};
-use statementThis line brings std::io and std::io::Write into scope.
If we want to bring all public items defined in a path into scope, we can
-specify that path followed by the * glob operator:
#![allow(unused)]
-fn main() {
-use std::collections::*;
-}
-This use statement brings all public items defined in std::collections into
-the current scope. Be careful when using the glob operator! Glob can make it
-harder to tell what names are in scope and where a name used in your program
-was defined. Additionally, if the dependency changes its definitions, what
-you’ve imported changes as well, which may lead to compiler errors when you
-upgrade the dependency if the dependency adds a definition with the same name
-as a definition of yours in the same scope, for example.
The glob operator is often used when testing to bring everything under test into
-the tests module; we’ll talk about that in “How to Write
-Tests” in Chapter 11. The glob operator is also
-sometimes used as part of the prelude pattern: See the standard library
-documentation for more
-information on that pattern.
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
-So far, all the examples in this chapter defined multiple modules in one file. -When modules get large, you might want to move their definitions to a separate -file to make the code easier to navigate.
-For example, let’s start from the code in Listing 7-17 that had multiple -restaurant modules. We’ll extract modules into files instead of having all the -modules defined in the crate root file. In this case, the crate root file is -src/lib.rs, but this procedure also works with binary crates whose crate root -file is src/main.rs.
-First, we’ll extract the front_of_house module to its own file. Remove the
-code inside the curly brackets for the front_of_house module, leaving only
-the mod front_of_house; declaration, so that src/lib.rs contains the code
-shown in Listing 7-21. Note that this won’t compile until we create the
-src/front_of_house.rs file in Listing 7-22.
mod front_of_house;
-
-pub use crate::front_of_house::hosting;
-
-pub fn eat_at_restaurant() {
- hosting::add_to_waitlist();
-}
-front_of_house module whose body will be in src/front_of_house.rsNext, place the code that was in the curly brackets into a new file named
-src/front_of_house.rs, as shown in Listing 7-22. The compiler knows to look
-in this file because it came across the module declaration in the crate root
-with the name front_of_house.
pub mod hosting {
- pub fn add_to_waitlist() {}
-}
-front_of_house module in src/front_of_house.rsNote that you only need to load a file using a mod declaration once in your
-module tree. Once the compiler knows the file is part of the project (and knows
-where in the module tree the code resides because of where you’ve put the mod
-statement), other files in your project should refer to the loaded file’s code
-using a path to where it was declared, as covered in the “Paths for Referring
-to an Item in the Module Tree” section. In other words,
-mod is not an “include” operation that you may have seen in other
-programming languages.
Next, we’ll extract the hosting module to its own file. The process is a bit
-different because hosting is a child module of front_of_house, not of the
-root module. We’ll place the file for hosting in a new directory that will be
-named for its ancestors in the module tree, in this case src/front_of_house.
To start moving hosting, we change src/front_of_house.rs to contain only
-the declaration of the hosting module:
pub mod hosting;
-Then, we create a src/front_of_house directory and a hosting.rs file to
-contain the definitions made in the hosting module:
pub fn add_to_waitlist() {}
-If we instead put hosting.rs in the src directory, the compiler would
-expect the hosting.rs code to be in a hosting module declared in the crate
-root and not declared as a child of the front_of_house module. The
-compiler’s rules for which files to check for which modules’ code mean the
-directories and files more closely match the module tree.
So far we’ve covered the most idiomatic file paths the Rust compiler uses,
-but Rust also supports an older style of file path. For a module named
-front_of_house declared in the crate root, the compiler will look for the
-module’s code in:
For a module named hosting that is a submodule of front_of_house, the
-compiler will look for the module’s code in:
If you use both styles for the same module, you’ll get a compiler error. -Using a mix of both styles for different modules in the same project is -allowed but might be confusing for people navigating your project.
-The main downside to the style that uses files named mod.rs is that your -project can end up with many files named mod.rs, which can get confusing -when you have them open in your editor at the same time.
-We’ve moved each module’s code to a separate file, and the module tree remains
-the same. The function calls in eat_at_restaurant will work without any
-modification, even though the definitions live in different files. This
-technique lets you move modules to new files as they grow in size.
Note that the pub use crate::front_of_house::hosting statement in
-src/lib.rs also hasn’t changed, nor does use have any impact on what files
-are compiled as part of the crate. The mod keyword declares modules, and Rust
-looks in a file with the same name as the module for the code that goes into
-that module.
Rust lets you split a package into multiple crates and a crate into modules so
-that you can refer to items defined in one module from another module. You can
-do this by specifying absolute or relative paths. These paths can be brought
-into scope with a use statement so that you can use a shorter path for
-multiple uses of the item in that scope. Module code is private by default, but
-you can make definitions public by adding the pub keyword.
In the next chapter, we’ll look at some collection data structures in the -standard library that you can use in your neatly organized code.
- -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
-Rust’s standard library includes a number of very useful data structures called -collections. Most other data types represent one specific value, but -collections can contain multiple values. Unlike the built-in array and tuple -types, the data that these collections point to is stored on the heap, which -means the amount of data does not need to be known at compile time and can grow -or shrink as the program runs. Each kind of collection has different -capabilities and costs, and choosing an appropriate one for your current -situation is a skill you’ll develop over time. In this chapter, we’ll discuss -three collections that are used very often in Rust programs:
-String type
-previously, but in this chapter, we’ll talk about it in depth.To learn about the other kinds of collections provided by the standard library, -see the documentation.
-We’ll discuss how to create and update vectors, strings, and hash maps, as well -as what makes each special.
- -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
-The first collection type we’ll look at is Vec<T>, also known as a vector.
-Vectors allow you to store more than one value in a single data structure that
-puts all the values next to each other in memory. Vectors can only store values
-of the same type. They are useful when you have a list of items, such as the
-lines of text in a file or the prices of items in a shopping cart.
To create a new, empty vector, we call the Vec::new function, as shown in
-Listing 8-1.
fn main() {
- let v: Vec<i32> = Vec::new();
-}
-i32Note that we added a type annotation here. Because we aren’t inserting any
-values into this vector, Rust doesn’t know what kind of elements we intend to
-store. This is an important point. Vectors are implemented using generics;
-we’ll cover how to use generics with your own types in Chapter 10. For now,
-know that the Vec<T> type provided by the standard library can hold any type.
-When we create a vector to hold a specific type, we can specify the type within
-angle brackets. In Listing 8-1, we’ve told Rust that the Vec<T> in v will
-hold elements of the i32 type.
More often, you’ll create a Vec<T> with initial values, and Rust will infer
-the type of value you want to store, so you rarely need to do this type
-annotation. Rust conveniently provides the vec! macro, which will create a
-new vector that holds the values you give it. Listing 8-2 creates a new
-Vec<i32> that holds the values 1, 2, and 3. The integer type is i32
-because that’s the default integer type, as we discussed in the “Data
-Types” section of Chapter 3.
fn main() {
- let v = vec![1, 2, 3];
-}
-Because we’ve given initial i32 values, Rust can infer that the type of v
-is Vec<i32>, and the type annotation isn’t necessary. Next, we’ll look at how
-to modify a vector.
To create a vector and then add elements to it, we can use the push method,
-as shown in Listing 8-3.
fn main() {
- let mut v = Vec::new();
-
- v.push(5);
- v.push(6);
- v.push(7);
- v.push(8);
-}
-push method to add values to a vectorAs with any variable, if we want to be able to change its value, we need to
-make it mutable using the mut keyword, as discussed in Chapter 3. The numbers
-we place inside are all of type i32, and Rust infers this from the data, so
-we don’t need the Vec<i32> annotation.
There are two ways to reference a value stored in a vector: via indexing or by
-using the get method. In the following examples, we’ve annotated the types of
-the values that are returned from these functions for extra clarity.
Listing 8-4 shows both methods of accessing a value in a vector, with indexing
-syntax and the get method.
fn main() {
- let v = vec![1, 2, 3, 4, 5];
-
- let third: &i32 = &v[2];
- println!("The third element is {third}");
-
- let third: Option<&i32> = v.get(2);
- match third {
- Some(third) => println!("The third element is {third}"),
- None => println!("There is no third element."),
- }
-}
-get method to access an item in a vectorNote a few details here. We use the index value of 2 to get the third element
-because vectors are indexed by number, starting at zero. Using & and []
-gives us a reference to the element at the index value. When we use the get
-method with the index passed as an argument, we get an Option<&T> that we can
-use with match.
Rust provides these two ways to reference an element so that you can choose how -the program behaves when you try to use an index value outside the range of -existing elements. As an example, let’s see what happens when we have a vector -of five elements and then we try to access an element at index 100 with each -technique, as shown in Listing 8-5.
-fn main() {
- let v = vec![1, 2, 3, 4, 5];
-
- let does_not_exist = &v[100];
- let does_not_exist = v.get(100);
-}
-When we run this code, the first [] method will cause the program to panic
-because it references a nonexistent element. This method is best used when you
-want your program to crash if there’s an attempt to access an element past the
-end of the vector.
When the get method is passed an index that is outside the vector, it returns
-None without panicking. You would use this method if accessing an element
-beyond the range of the vector may happen occasionally under normal
-circumstances. Your code will then have logic to handle having either
-Some(&element) or None, as discussed in Chapter 6. For example, the index
-could be coming from a person entering a number. If they accidentally enter a
-number that’s too large and the program gets a None value, you could tell the
-user how many items are in the current vector and give them another chance to
-enter a valid value. That would be more user-friendly than crashing the program
-due to a typo!
When the program has a valid reference, the borrow checker enforces the -ownership and borrowing rules (covered in Chapter 4) to ensure that this -reference and any other references to the contents of the vector remain valid. -Recall the rule that states you can’t have mutable and immutable references in -the same scope. That rule applies in Listing 8-6, where we hold an immutable -reference to the first element in a vector and try to add an element to the -end. This program won’t work if we also try to refer to that element later in -the function.
-fn main() {
- let mut v = vec![1, 2, 3, 4, 5];
-
- let first = &v[0];
-
- v.push(6);
-
- println!("The first element is: {first}");
-}
-Compiling this code will result in this error:
-$ cargo run
- Compiling collections v0.1.0 (file:///projects/collections)
-error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
- --> src/main.rs:6:5
- |
-4 | let first = &v[0];
- | - immutable borrow occurs here
-5 |
-6 | v.push(6);
- | ^^^^^^^^^ mutable borrow occurs here
-7 |
-8 | println!("The first element is: {first}");
- | ----- immutable borrow later used here
-
-For more information about this error, try `rustc --explain E0502`.
-error: could not compile `collections` (bin "collections") due to 1 previous error
-
-The code in Listing 8-6 might look like it should work: Why should a reference -to the first element care about changes at the end of the vector? This error is -due to the way vectors work: Because vectors put the values next to each other -in memory, adding a new element onto the end of the vector might require -allocating new memory and copying the old elements to the new space, if there -isn’t enough room to put all the elements next to each other where the vector -is currently stored. In that case, the reference to the first element would be -pointing to deallocated memory. The borrowing rules prevent programs from -ending up in that situation.
-Note: For more on the implementation details of the Vec<T> type, see “The
-Rustonomicon”.
To access each element in a vector in turn, we would iterate through all of the
-elements rather than use indices to access one at a time. Listing 8-7 shows how
-to use a for loop to get immutable references to each element in a vector of
-i32 values and print them.
fn main() {
- let v = vec![100, 32, 57];
- for i in &v {
- println!("{i}");
- }
-}
-for loopWe can also iterate over mutable references to each element in a mutable vector
-in order to make changes to all the elements. The for loop in Listing 8-8
-will add 50 to each element.
fn main() {
- let mut v = vec![100, 32, 57];
- for i in &mut v {
- *i += 50;
- }
-}
-To change the value that the mutable reference refers to, we have to use the
-* dereference operator to get to the value in i before we can use the +=
-operator. We’ll talk more about the dereference operator in the “Following the
-Reference to the Value” section of Chapter 15.
Iterating over a vector, whether immutably or mutably, is safe because of the
-borrow checker’s rules. If we attempted to insert or remove items in the for
-loop bodies in Listing 8-7 and Listing 8-8, we would get a compiler error
-similar to the one we got with the code in Listing 8-6. The reference to the
-vector that the for loop holds prevents simultaneous modification of the
-whole vector.
Vectors can only store values that are of the same type. This can be -inconvenient; there are definitely use cases for needing to store a list of -items of different types. Fortunately, the variants of an enum are defined -under the same enum type, so when we need one type to represent elements of -different types, we can define and use an enum!
-For example, say we want to get values from a row in a spreadsheet in which -some of the columns in the row contain integers, some floating-point numbers, -and some strings. We can define an enum whose variants will hold the different -value types, and all the enum variants will be considered the same type: that -of the enum. Then, we can create a vector to hold that enum and so, ultimately, -hold different types. We’ve demonstrated this in Listing 8-9.
-fn main() {
- enum SpreadsheetCell {
- Int(i32),
- Float(f64),
- Text(String),
- }
-
- let row = vec![
- SpreadsheetCell::Int(3),
- SpreadsheetCell::Text(String::from("blue")),
- SpreadsheetCell::Float(10.12),
- ];
-}
-Rust needs to know what types will be in the vector at compile time so that it
-knows exactly how much memory on the heap will be needed to store each element.
-We must also be explicit about what types are allowed in this vector. If Rust
-allowed a vector to hold any type, there would be a chance that one or more of
-the types would cause errors with the operations performed on the elements of
-the vector. Using an enum plus a match expression means that Rust will ensure
-at compile time that every possible case is handled, as discussed in Chapter 6.
If you don’t know the exhaustive set of types a program will get at runtime to -store in a vector, the enum technique won’t work. Instead, you can use a trait -object, which we’ll cover in Chapter 18.
-Now that we’ve discussed some of the most common ways to use vectors, be sure
-to review the API documentation for all of the many
-useful methods defined on Vec<T> by the standard library. For example, in
-addition to push, a pop method removes and returns the last element.
Like any other struct, a vector is freed when it goes out of scope, as
-annotated in Listing 8-10.
fn main() {
- {
- let v = vec![1, 2, 3, 4];
-
- // do stuff with v
- } // <- v goes out of scope and is freed here
-}
-When the vector gets dropped, all of its contents are also dropped, meaning the -integers it holds will be cleaned up. The borrow checker ensures that any -references to contents of a vector are only used while the vector itself is -valid.
-Let’s move on to the next collection type: String!
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
-The last of our common collections is the hash map. The type HashMap<K, V>
-stores a mapping of keys of type K to values of type V using a hashing
-function, which determines how it places these keys and values into memory.
-Many programming languages support this kind of data structure, but they often
-use a different name, such as hash, map, object, hash table,
-dictionary, or associative array, just to name a few.
Hash maps are useful when you want to look up data not by using an index, as -you can with vectors, but by using a key that can be of any type. For example, -in a game, you could keep track of each team’s score in a hash map in which -each key is a team’s name and the values are each team’s score. Given a team -name, you can retrieve its score.
-We’ll go over the basic API of hash maps in this section, but many more goodies
-are hiding in the functions defined on HashMap<K, V> by the standard library.
-As always, check the standard library documentation for more information.
One way to create an empty hash map is to use new and to add elements with
-insert. In Listing 8-20, we’re keeping track of the scores of two teams whose
-names are Blue and Yellow. The Blue team starts with 10 points, and the
-Yellow team starts with 50.
fn main() {
- use std::collections::HashMap;
-
- let mut scores = HashMap::new();
-
- scores.insert(String::from("Blue"), 10);
- scores.insert(String::from("Yellow"), 50);
-}
-Note that we need to first use the HashMap from the collections portion of
-the standard library. Of our three common collections, this one is the least
-often used, so it’s not included in the features brought into scope
-automatically in the prelude. Hash maps also have less support from the
-standard library; there’s no built-in macro to construct them, for example.
Just like vectors, hash maps store their data on the heap. This HashMap has
-keys of type String and values of type i32. Like vectors, hash maps are
-homogeneous: All of the keys must have the same type, and all of the values
-must have the same type.
We can get a value out of the hash map by providing its key to the get
-method, as shown in Listing 8-21.
fn main() {
- use std::collections::HashMap;
-
- let mut scores = HashMap::new();
-
- scores.insert(String::from("Blue"), 10);
- scores.insert(String::from("Yellow"), 50);
-
- let team_name = String::from("Blue");
- let score = scores.get(&team_name).copied().unwrap_or(0);
-}
-Here, score will have the value that’s associated with the Blue team, and the
-result will be 10. The get method returns an Option<&V>; if there’s no
-value for that key in the hash map, get will return None. This program
-handles the Option by calling copied to get an Option<i32> rather than an
-Option<&i32>, then unwrap_or to set score to zero if scores doesn’t
-have an entry for the key.
We can iterate over each key-value pair in a hash map in a similar manner as we
-do with vectors, using a for loop:
fn main() {
- use std::collections::HashMap;
-
- let mut scores = HashMap::new();
-
- scores.insert(String::from("Blue"), 10);
- scores.insert(String::from("Yellow"), 50);
-
- for (key, value) in &scores {
- println!("{key}: {value}");
- }
-}
-This code will print each pair in an arbitrary order:
-Yellow: 50
-Blue: 10
-
-
-
-For types that implement the Copy trait, like i32, the values are copied
-into the hash map. For owned values like String, the values will be moved and
-the hash map will be the owner of those values, as demonstrated in Listing 8-22.
fn main() {
- use std::collections::HashMap;
-
- let field_name = String::from("Favorite color");
- let field_value = String::from("Blue");
-
- let mut map = HashMap::new();
- map.insert(field_name, field_value);
- // field_name and field_value are invalid at this point, try using them and
- // see what compiler error you get!
-}
-We aren’t able to use the variables field_name and field_value after
-they’ve been moved into the hash map with the call to insert.
If we insert references to values into the hash map, the values won’t be moved -into the hash map. The values that the references point to must be valid for at -least as long as the hash map is valid. We’ll talk more about these issues in -“Validating References with -Lifetimes” in Chapter 10.
-Although the number of key and value pairs is growable, each unique key can
-only have one value associated with it at a time (but not vice versa: For
-example, both the Blue team and the Yellow team could have the value 10
-stored in the scores hash map).
When you want to change the data in a hash map, you have to decide how to -handle the case when a key already has a value assigned. You could replace the -old value with the new value, completely disregarding the old value. You could -keep the old value and ignore the new value, only adding the new value if the -key doesn’t already have a value. Or you could combine the old value and the -new value. Let’s look at how to do each of these!
-If we insert a key and a value into a hash map and then insert that same key
-with a different value, the value associated with that key will be replaced.
-Even though the code in Listing 8-23 calls insert twice, the hash map will
-only contain one key-value pair because we’re inserting the value for the Blue
-team’s key both times.
fn main() {
- use std::collections::HashMap;
-
- let mut scores = HashMap::new();
-
- scores.insert(String::from("Blue"), 10);
- scores.insert(String::from("Blue"), 25);
-
- println!("{scores:?}");
-}
-This code will print {"Blue": 25}. The original value of 10 has been
-overwritten.
It’s common to check whether a particular key already exists in the hash map -with a value and then to take the following actions: If the key does exist in -the hash map, the existing value should remain the way it is; if the key -doesn’t exist, insert it and a value for it.
-Hash maps have a special API for this called entry that takes the key you
-want to check as a parameter. The return value of the entry method is an enum
-called Entry that represents a value that might or might not exist. Let’s say
-we want to check whether the key for the Yellow team has a value associated
-with it. If it doesn’t, we want to insert the value 50, and the same for the
-Blue team. Using the entry API, the code looks like Listing 8-24.
fn main() {
- use std::collections::HashMap;
-
- let mut scores = HashMap::new();
- scores.insert(String::from("Blue"), 10);
-
- scores.entry(String::from("Yellow")).or_insert(50);
- scores.entry(String::from("Blue")).or_insert(50);
-
- println!("{scores:?}");
-}
-entry method to only insert if the key does not already have a valueThe or_insert method on Entry is defined to return a mutable reference to
-the value for the corresponding Entry key if that key exists, and if not, it
-inserts the parameter as the new value for this key and returns a mutable
-reference to the new value. This technique is much cleaner than writing the
-logic ourselves and, in addition, plays more nicely with the borrow checker.
Running the code in Listing 8-24 will print {"Yellow": 50, "Blue": 10}. The
-first call to entry will insert the key for the Yellow team with the value
-50 because the Yellow team doesn’t have a value already. The second call to
-entry will not change the hash map, because the Blue team already has the
-value 10.
Another common use case for hash maps is to look up a key’s value and then
-update it based on the old value. For instance, Listing 8-25 shows code that
-counts how many times each word appears in some text. We use a hash map with
-the words as keys and increment the value to keep track of how many times we’ve
-seen that word. If it’s the first time we’ve seen a word, we’ll first insert
-the value 0.
fn main() {
- use std::collections::HashMap;
-
- let text = "hello world wonderful world";
-
- let mut map = HashMap::new();
-
- for word in text.split_whitespace() {
- let count = map.entry(word).or_insert(0);
- *count += 1;
- }
-
- println!("{map:?}");
-}
-This code will print {"world": 2, "hello": 1, "wonderful": 1}. You might see
-the same key-value pairs printed in a different order: Recall from “Accessing
-Values in a Hash Map” that iterating over a hash map
-happens in an arbitrary order.
The split_whitespace method returns an iterator over subslices, separated by
-whitespace, of the value in text. The or_insert method returns a mutable
-reference (&mut V) to the value for the specified key. Here, we store that
-mutable reference in the count variable, so in order to assign to that value,
-we must first dereference count using the asterisk (*). The mutable
-reference goes out of scope at the end of the for loop, so all of these
-changes are safe and allowed by the borrowing rules.
By default, HashMap uses a hashing function called SipHash that can provide
-resistance to denial-of-service (DoS) attacks involving hash
-tables1. This is not the fastest hashing algorithm
-available, but the trade-off for better security that comes with the drop in
-performance is worth it. If you profile your code and find that the default
-hash function is too slow for your purposes, you can switch to another function
-by specifying a different hasher. A hasher is a type that implements the
-BuildHasher trait. We’ll talk about traits and how to implement them in
-Chapter 10. You don’t necessarily have to implement
-your own hasher from scratch; crates.io
-has libraries shared by other Rust users that provide hashers implementing many
-common hashing algorithms.
Vectors, strings, and hash maps will provide a large amount of functionality -necessary in programs when you need to store, access, and modify data. Here are -some exercises you should now be equipped to solve:
-The standard library API documentation describes methods that vectors, strings, -and hash maps have that will be helpful for these exercises!
-We’re getting into more complex programs in which operations can fail, so it’s -a perfect time to discuss error handling. We’ll do that next!
-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
-Errors are a fact of life in software, so Rust has a number of features for -handling situations in which something goes wrong. In many cases, Rust requires -you to acknowledge the possibility of an error and take some action before your -code will compile. This requirement makes your program more robust by ensuring -that you’ll discover errors and handle them appropriately before deploying your -code to production!
-Rust groups errors into two major categories: recoverable and unrecoverable -errors. For a recoverable error, such as a file not found error, we most -likely just want to report the problem to the user and retry the operation. -Unrecoverable errors are always symptoms of bugs, such as trying to access a -location beyond the end of an array, and so we want to immediately stop the -program.
-Most languages don’t distinguish between these two kinds of errors and handle
-both in the same way, using mechanisms such as exceptions. Rust doesn’t have
-exceptions. Instead, it has the type Result<T, E> for recoverable errors and
-the panic! macro that stops execution when the program encounters an
-unrecoverable error. This chapter covers calling panic! first and then talks
-about returning Result<T, E> values. Additionally, we’ll explore
-considerations when deciding whether to try to recover from an error or to stop
-execution.
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
-panic!Sometimes bad things happen in your code, and there’s nothing you can do about
-it. In these cases, Rust has the panic! macro. There are two ways to cause a
-panic in practice: by taking an action that causes our code to panic (such as
-accessing an array past the end) or by explicitly calling the panic! macro.
-In both cases, we cause a panic in our program. By default, these panics will
-print a failure message, unwind, clean up the stack, and quit. Via an
-environment variable, you can also have Rust display the call stack when a
-panic occurs to make it easier to track down the source of the panic.
By default, when a panic occurs, the program starts unwinding, which means -Rust walks back up the stack and cleans up the data from each function it -encounters. However, walking back and cleaning up is a lot of work. Rust -therefore allows you to choose the alternative of immediately aborting, -which ends the program without cleaning up.
-Memory that the program was using will then need to be cleaned up by the
-operating system. If in your project you need to make the resultant binary as
-small as possible, you can switch from unwinding to aborting upon a panic by
-adding panic = 'abort' to the appropriate [profile] sections in your
-Cargo.toml file. For example, if you want to abort on panic in release mode,
-add this:
[profile.release]
-panic = 'abort'
-
-Let’s try calling panic! in a simple program:
fn main() {
- panic!("crash and burn");
-}
-When you run the program, you’ll see something like this:
-$ cargo run
- Compiling panic v0.1.0 (file:///projects/panic)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s
- Running `target/debug/panic`
-
-thread 'main' panicked at src/main.rs:2:5:
-crash and burn
-note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
-
-The call to panic! causes the error message contained in the last two lines.
-The first line shows our panic message and the place in our source code where
-the panic occurred: src/main.rs:2:5 indicates that it’s the second line,
-fifth character of our src/main.rs file.
In this case, the line indicated is part of our code, and if we go to that
-line, we see the panic! macro call. In other cases, the panic! call might
-be in code that our code calls, and the filename and line number reported by
-the error message will be someone else’s code where the panic! macro is
-called, not the line of our code that eventually led to the panic! call.
We can use the backtrace of the functions the panic! call came from to figure
-out the part of our code that is causing the problem. To understand how to use
-a panic! backtrace, let’s look at another example and see what it’s like when
-a panic! call comes from a library because of a bug in our code instead of
-from our code calling the macro directly. Listing 9-1 has some code that
-attempts to access an index in a vector beyond the range of valid indexes.
fn main() {
- let v = vec![1, 2, 3];
-
- v[99];
-}
-panic!Here, we’re attempting to access the 100th element of our vector (which is at
-index 99 because indexing starts at zero), but the vector has only three
-elements. In this situation, Rust will panic. Using [] is supposed to return
-an element, but if you pass an invalid index, there’s no element that Rust
-could return here that would be correct.
In C, attempting to read beyond the end of a data structure is undefined -behavior. You might get whatever is at the location in memory that would -correspond to that element in the data structure, even though the memory -doesn’t belong to that structure. This is called a buffer overread and can -lead to security vulnerabilities if an attacker is able to manipulate the index -in such a way as to read data they shouldn’t be allowed to that is stored after -the data structure.
-To protect your program from this sort of vulnerability, if you try to read an -element at an index that doesn’t exist, Rust will stop execution and refuse to -continue. Let’s try it and see:
-$ cargo run
- Compiling panic v0.1.0 (file:///projects/panic)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
- Running `target/debug/panic`
-
-thread 'main' panicked at src/main.rs:4:6:
-index out of bounds: the len is 3 but the index is 99
-note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
-
-This error points at line 4 of our main.rs where we attempt to access index
-99 of the vector in v.
The note: line tells us that we can set the RUST_BACKTRACE environment
-variable to get a backtrace of exactly what happened to cause the error. A
-backtrace is a list of all the functions that have been called to get to this
-point. Backtraces in Rust work as they do in other languages: The key to
-reading the backtrace is to start from the top and read until you see files you
-wrote. That’s the spot where the problem originated. The lines above that spot
-are code that your code has called; the lines below are code that called your
-code. These before-and-after lines might include core Rust code, standard
-library code, or crates that you’re using. Let’s try to get a backtrace by
-setting the RUST_BACKTRACE environment variable to any value except 0.
-Listing 9-2 shows output similar to what you’ll see.
$ RUST_BACKTRACE=1 cargo run
-thread 'main' panicked at src/main.rs:4:6:
-index out of bounds: the len is 3 but the index is 99
-stack backtrace:
- 0: rust_begin_unwind
- at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/std/src/panicking.rs:692:5
- 1: core::panicking::panic_fmt
- at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:75:14
- 2: core::panicking::panic_bounds_check
- at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:273:5
- 3: <usize as core::slice::index::SliceIndex<[T]>>::index
- at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/slice/index.rs:274:10
- 4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
- at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/slice/index.rs:16:9
- 5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
- at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/alloc/src/vec/mod.rs:3361:9
- 6: panic::main
- at ./src/main.rs:4:6
- 7: core::ops::function::FnOnce::call_once
- at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
-note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
-
-panic! displayed when the environment variable RUST_BACKTRACE is setThat’s a lot of output! The exact output you see might be different depending
-on your operating system and Rust version. In order to get backtraces with this
-information, debug symbols must be enabled. Debug symbols are enabled by
-default when using cargo build or cargo run without the --release flag,
-as we have here.
In the output in Listing 9-2, line 6 of the backtrace points to the line in our -project that’s causing the problem: line 4 of src/main.rs. If we don’t want -our program to panic, we should start our investigation at the location pointed -to by the first line mentioning a file we wrote. In Listing 9-1, where we -deliberately wrote code that would panic, the way to fix the panic is to not -request an element beyond the range of the vector indexes. When your code -panics in the future, you’ll need to figure out what action the code is taking -with what values to cause the panic and what the code should do instead.
-We’ll come back to panic! and when we should and should not use panic! to
-handle error conditions in the “To panic! or Not to
-panic!” section later in this
-chapter. Next, we’ll look at how to recover from an error using Result.
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
-panic! or Not to panic!So, how do you decide when you should call panic! and when you should return
-Result? When code panics, there’s no way to recover. You could call panic!
-for any error situation, whether there’s a possible way to recover or not, but
-then you’re making the decision that a situation is unrecoverable on behalf of
-the calling code. When you choose to return a Result value, you give the
-calling code options. The calling code could choose to attempt to recover in a
-way that’s appropriate for its situation, or it could decide that an Err
-value in this case is unrecoverable, so it can call panic! and turn your
-recoverable error into an unrecoverable one. Therefore, returning Result is a
-good default choice when you’re defining a function that might fail.
In situations such as examples, prototype code, and tests, it’s more
-appropriate to write code that panics instead of returning a Result. Let’s
-explore why, then discuss situations in which the compiler can’t tell that
-failure is impossible, but you as a human can. The chapter will conclude with
-some general guidelines on how to decide whether to panic in library code.
When you’re writing an example to illustrate some concept, also including
-robust error-handling code can make the example less clear. In examples, it’s
-understood that a call to a method like unwrap that could panic is meant as a
-placeholder for the way you’d want your application to handle errors, which can
-differ based on what the rest of your code is doing.
Similarly, the unwrap and expect methods are very handy when you’re
-prototyping and you’re not yet ready to decide how to handle errors. They leave
-clear markers in your code for when you’re ready to make your program more
-robust.
If a method call fails in a test, you’d want the whole test to fail, even if
-that method isn’t the functionality under test. Because panic! is how a test
-is marked as a failure, calling unwrap or expect is exactly what should
-happen.
It would also be appropriate to call expect when you have some other logic
-that ensures that the Result will have an Ok value, but the logic isn’t
-something the compiler understands. You’ll still have a Result value that you
-need to handle: Whatever operation you’re calling still has the possibility of
-failing in general, even though it’s logically impossible in your particular
-situation. If you can ensure by manually inspecting the code that you’ll never
-have an Err variant, it’s perfectly acceptable to call expect and document
-the reason you think you’ll never have an Err variant in the argument text.
-Here’s an example:
fn main() {
- use std::net::IpAddr;
-
- let home: IpAddr = "127.0.0.1"
- .parse()
- .expect("Hardcoded IP address should be valid");
-}
-We’re creating an IpAddr instance by parsing a hardcoded string. We can see
-that 127.0.0.1 is a valid IP address, so it’s acceptable to use expect
-here. However, having a hardcoded, valid string doesn’t change the return type
-of the parse method: We still get a Result value, and the compiler will
-still make us handle the Result as if the Err variant is a possibility
-because the compiler isn’t smart enough to see that this string is always a
-valid IP address. If the IP address string came from a user rather than being
-hardcoded into the program and therefore did have a possibility of failure,
-we’d definitely want to handle the Result in a more robust way instead.
-Mentioning the assumption that this IP address is hardcoded will prompt us to
-change expect to better error-handling code if, in the future, we need to get
-the IP address from some other source instead.
It’s advisable to have your code panic when it’s possible that your code could -end up in a bad state. In this context, a bad state is when some assumption, -guarantee, contract, or invariant has been broken, such as when invalid values, -contradictory values, or missing values are passed to your code—plus one or -more of the following:
-If someone calls your code and passes in values that don’t make sense, it’s
-best to return an error if you can so that the user of the library can decide
-what they want to do in that case. However, in cases where continuing could be
-insecure or harmful, the best choice might be to call panic! and alert the
-person using your library to the bug in their code so that they can fix it
-during development. Similarly, panic! is often appropriate if you’re calling
-external code that is out of your control and returns an invalid state that you
-have no way of fixing.
However, when failure is expected, it’s more appropriate to return a Result
-than to make a panic! call. Examples include a parser being given malformed
-data or an HTTP request returning a status that indicates you have hit a rate
-limit. In these cases, returning a Result indicates that failure is an
-expected possibility that the calling code must decide how to handle.
When your code performs an operation that could put a user at risk if it’s
-called using invalid values, your code should verify the values are valid first
-and panic if the values aren’t valid. This is mostly for safety reasons:
-Attempting to operate on invalid data can expose your code to vulnerabilities.
-This is the main reason the standard library will call panic! if you attempt
-an out-of-bounds memory access: Trying to access memory that doesn’t belong to
-the current data structure is a common security problem. Functions often have
-contracts: Their behavior is only guaranteed if the inputs meet particular
-requirements. Panicking when the contract is violated makes sense because a
-contract violation always indicates a caller-side bug, and it’s not a kind of
-error you want the calling code to have to explicitly handle. In fact, there’s
-no reasonable way for calling code to recover; the calling programmers need
-to fix the code. Contracts for a function, especially when a violation will
-cause a panic, should be explained in the API documentation for the function.
However, having lots of error checks in all of your functions would be verbose
-and annoying. Fortunately, you can use Rust’s type system (and thus the type
-checking done by the compiler) to do many of the checks for you. If your
-function has a particular type as a parameter, you can proceed with your code’s
-logic knowing that the compiler has already ensured that you have a valid
-value. For example, if you have a type rather than an Option, your program
-expects to have something rather than nothing. Your code then doesn’t have
-to handle two cases for the Some and None variants: It will only have one
-case for definitely having a value. Code trying to pass nothing to your
-function won’t even compile, so your function doesn’t have to check for that
-case at runtime. Another example is using an unsigned integer type such as
-u32, which ensures that the parameter is never negative.
Let’s take the idea of using Rust’s type system to ensure that we have a valid -value one step further and look at creating a custom type for validation. -Recall the guessing game in Chapter 2 in which our code asked the user to guess -a number between 1 and 100. We never validated that the user’s guess was -between those numbers before checking it against our secret number; we only -validated that the guess was positive. In this case, the consequences were not -very dire: Our output of “Too high” or “Too low” would still be correct. But it -would be a useful enhancement to guide the user toward valid guesses and have -different behavior when the user guesses a number that’s out of range versus -when the user types, for example, letters instead.
-One way to do this would be to parse the guess as an i32 instead of only a
-u32 to allow potentially negative numbers, and then add a check for the
-number being in range, like so:
use rand::Rng;
-use std::cmp::Ordering;
-use std::io;
-
-fn main() {
- println!("Guess the number!");
-
- let secret_number = rand::thread_rng().gen_range(1..=100);
-
- loop {
- // --snip--
-
- println!("Please input your guess.");
-
- let mut guess = String::new();
-
- io::stdin()
- .read_line(&mut guess)
- .expect("Failed to read line");
-
- let guess: i32 = match guess.trim().parse() {
- Ok(num) => num,
- Err(_) => continue,
- };
-
- if guess < 1 || guess > 100 {
- println!("The secret number will be between 1 and 100.");
- continue;
- }
-
- match guess.cmp(&secret_number) {
- // --snip--
- Ordering::Less => println!("Too small!"),
- Ordering::Greater => println!("Too big!"),
- Ordering::Equal => {
- println!("You win!");
- break;
- }
- }
- }
-}
-The if expression checks whether our value is out of range, tells the user
-about the problem, and calls continue to start the next iteration of the loop
-and ask for another guess. After the if expression, we can proceed with the
-comparisons between guess and the secret number knowing that guess is
-between 1 and 100.
However, this is not an ideal solution: If it were absolutely critical that the -program only operated on values between 1 and 100, and it had many functions -with this requirement, having a check like this in every function would be -tedious (and might impact performance).
-Instead, we can make a new type in a dedicated module and put the validations
-in a function to create an instance of the type rather than repeating the
-validations everywhere. That way, it’s safe for functions to use the new type
-in their signatures and confidently use the values they receive. Listing 9-13
-shows one way to define a Guess type that will only create an instance of
-Guess if the new function receives a value between 1 and 100.
#![allow(unused)]
-fn main() {
-pub struct Guess {
- value: i32,
-}
-
-impl Guess {
- pub fn new(value: i32) -> Guess {
- if value < 1 || value > 100 {
- panic!("Guess value must be between 1 and 100, got {value}.");
- }
-
- Guess { value }
- }
-
- pub fn value(&self) -> i32 {
- self.value
- }
-}
-}
-Guess type that will only continue with values between 1 and 100Note that this code in src/guessing_game.rs depends on adding a module
-declaration mod guessing_game; in src/lib.rs that we haven’t shown here.
-Within this new module’s file, we define a struct named Guess that has a
-field named value that holds an i32. This is where the number will be
-stored.
Then, we implement an associated function named new on Guess that creates
-instances of Guess values. The new function is defined to have one
-parameter named value of type i32 and to return a Guess. The code in the
-body of the new function tests value to make sure it’s between 1 and 100.
-If value doesn’t pass this test, we make a panic! call, which will alert
-the programmer who is writing the calling code that they have a bug they need
-to fix, because creating a Guess with a value outside this range would
-violate the contract that Guess::new is relying on. The conditions in which
-Guess::new might panic should be discussed in its public-facing API
-documentation; we’ll cover documentation conventions indicating the possibility
-of a panic! in the API documentation that you create in Chapter 14. If
-value does pass the test, we create a new Guess with its value field set
-to the value parameter and return the Guess.
Next, we implement a method named value that borrows self, doesn’t have any
-other parameters, and returns an i32. This kind of method is sometimes called
-a getter because its purpose is to get some data from its fields and return
-it. This public method is necessary because the value field of the Guess
-struct is private. It’s important that the value field be private so that
-code using the Guess struct is not allowed to set value directly: Code
-outside the guessing_game module must use the Guess::new function to
-create an instance of Guess, thereby ensuring that there’s no way for a
-Guess to have a value that hasn’t been checked by the conditions in the
-Guess::new function.
A function that has a parameter or returns only numbers between 1 and 100 could
-then declare in its signature that it takes or returns a Guess rather than an
-i32 and wouldn’t need to do any additional checks in its body.
Rust’s error-handling features are designed to help you write more robust code.
-The panic! macro signals that your program is in a state it can’t handle and
-lets you tell the process to stop instead of trying to proceed with invalid or
-incorrect values. The Result enum uses Rust’s type system to indicate that
-operations might fail in a way that your code could recover from. You can use
-Result to tell code that calls your code that it needs to handle potential
-success or failure as well. Using panic! and Result in the appropriate
-situations will make your code more reliable in the face of inevitable problems.
Now that you’ve seen useful ways that the standard library uses generics with
-the Option and Result enums, we’ll talk about how generics work and how you
-can use them in your code.
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
-Every programming language has tools for effectively handling the duplication -of concepts. In Rust, one such tool is generics: abstract stand-ins for -concrete types or other properties. We can express the behavior of generics or -how they relate to other generics without knowing what will be in their place -when compiling and running the code.
-Functions can take parameters of some generic type, instead of a concrete type
-like i32 or String, in the same way they take parameters with unknown
-values to run the same code on multiple concrete values. In fact, we already
-used generics in Chapter 6 with Option<T>, in Chapter 8 with Vec<T> and
-HashMap<K, V>, and in Chapter 9 with Result<T, E>. In this chapter, you’ll
-explore how to define your own types, functions, and methods with generics!
First, we’ll review how to extract a function to reduce code duplication. We’ll -then use the same technique to make a generic function from two functions that -differ only in the types of their parameters. We’ll also explain how to use -generic types in struct and enum definitions.
-Then, you’ll learn how to use traits to define behavior in a generic way. You -can combine traits with generic types to constrain a generic type to accept -only those types that have a particular behavior, as opposed to just any type.
-Finally, we’ll discuss lifetimes: a variety of generics that give the -compiler information about how references relate to each other. Lifetimes allow -us to give the compiler enough information about borrowed values so that it can -ensure that references will be valid in more situations than it could without -our help.
-Generics allow us to replace specific types with a placeholder that represents -multiple types to remove code duplication. Before diving into generics syntax, -let’s first look at how to remove duplication in a way that doesn’t involve -generic types by extracting a function that replaces specific values with a -placeholder that represents multiple values. Then, we’ll apply the same -technique to extract a generic function! By looking at how to recognize -duplicated code you can extract into a function, you’ll start to recognize -duplicated code that can use generics.
-We’ll begin with the short program in Listing 10-1 that finds the largest -number in a list.
-fn main() {
- let number_list = vec![34, 50, 25, 100, 65];
-
- let mut largest = &number_list[0];
-
- for number in &number_list {
- if number > largest {
- largest = number;
- }
- }
-
- println!("The largest number is {largest}");
- assert_eq!(*largest, 100);
-}
-We store a list of integers in the variable number_list and place a reference
-to the first number in the list in a variable named largest. We then iterate
-through all the numbers in the list, and if the current number is greater than
-the number stored in largest, we replace the reference in that variable.
-However, if the current number is less than or equal to the largest number seen
-so far, the variable doesn’t change, and the code moves on to the next number
-in the list. After considering all the numbers in the list, largest should
-refer to the largest number, which in this case is 100.
We’ve now been tasked with finding the largest number in two different lists of -numbers. To do so, we can choose to duplicate the code in Listing 10-1 and use -the same logic at two different places in the program, as shown in Listing 10-2.
-fn main() {
- let number_list = vec![34, 50, 25, 100, 65];
-
- let mut largest = &number_list[0];
-
- for number in &number_list {
- if number > largest {
- largest = number;
- }
- }
-
- println!("The largest number is {largest}");
-
- let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
-
- let mut largest = &number_list[0];
-
- for number in &number_list {
- if number > largest {
- largest = number;
- }
- }
-
- println!("The largest number is {largest}");
-}
-Although this code works, duplicating code is tedious and error-prone. We also -have to remember to update the code in multiple places when we want to change -it.
-To eliminate this duplication, we’ll create an abstraction by defining a -function that operates on any list of integers passed in as a parameter. This -solution makes our code clearer and lets us express the concept of finding the -largest number in a list abstractly.
-In Listing 10-3, we extract the code that finds the largest number into a
-function named largest. Then, we call the function to find the largest number
-in the two lists from Listing 10-2. We could also use the function on any other
-list of i32 values we might have in the future.
fn largest(list: &[i32]) -> &i32 {
- let mut largest = &list[0];
-
- for item in list {
- if item > largest {
- largest = item;
- }
- }
-
- largest
-}
-
-fn main() {
- let number_list = vec![34, 50, 25, 100, 65];
-
- let result = largest(&number_list);
- println!("The largest number is {result}");
- assert_eq!(*result, 100);
-
- let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
-
- let result = largest(&number_list);
- println!("The largest number is {result}");
- assert_eq!(*result, 6000);
-}
-The largest function has a parameter called list, which represents any
-concrete slice of i32 values we might pass into the function. As a result,
-when we call the function, the code runs on the specific values that we pass
-in.
In summary, here are the steps we took to change the code from Listing 10-2 to -Listing 10-3:
-Next, we’ll use these same steps with generics to reduce code duplication. In
-the same way that the function body can operate on an abstract list instead
-of specific values, generics allow code to operate on abstract types.
For example, say we had two functions: one that finds the largest item in a
-slice of i32 values and one that finds the largest item in a slice of char
-values. How would we eliminate that duplication? Let’s find out!
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
-In his 1972 essay “The Humble Programmer,” Edsger W. Dijkstra said that “program -testing can be a very effective way to show the presence of bugs, but it is -hopelessly inadequate for showing their absence.” That doesn’t mean we shouldn’t -try to test as much as we can!
-Correctness in our programs is the extent to which our code does what we -intend it to do. Rust is designed with a high degree of concern about the -correctness of programs, but correctness is complex and not easy to prove. -Rust’s type system shoulders a huge part of this burden, but the type system -cannot catch everything. As such, Rust includes support for writing automated -software tests.
-Say we write a function add_two that adds 2 to whatever number is passed to
-it. This function’s signature accepts an integer as a parameter and returns an
-integer as a result. When we implement and compile that function, Rust does all
-the type checking and borrow checking that you’ve learned so far to ensure
-that, for instance, we aren’t passing a String value or an invalid reference
-to this function. But Rust can’t check that this function will do precisely
-what we intend, which is return the parameter plus 2 rather than, say, the
-parameter plus 10 or the parameter minus 50! That’s where tests come in.
We can write tests that assert, for example, that when we pass 3 to the
-add_two function, the returned value is 5. We can run these tests whenever
-we make changes to our code to make sure any existing correct behavior has not
-changed.
Testing is a complex skill: Although we can’t cover in one chapter every detail -about how to write good tests, in this chapter we will discuss the mechanics of -Rust’s testing facilities. We’ll talk about the annotations and macros -available to you when writing your tests, the default behavior and options -provided for running your tests, and how to organize tests into unit tests and -integration tests.
- -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
-Just as cargo run compiles your code and then runs the resultant binary,
-cargo test compiles your code in test mode and runs the resultant test
-binary. The default behavior of the binary produced by cargo test is to run
-all the tests in parallel and capture output generated during test runs,
-preventing the output from being displayed and making it easier to read the
-output related to the test results. You can, however, specify command line
-options to change this default behavior.
Some command line options go to cargo test, and some go to the resultant test
-binary. To separate these two types of arguments, you list the arguments that
-go to cargo test followed by the separator -- and then the ones that go to
-the test binary. Running cargo test --help displays the options you can use
-with cargo test, and running cargo test -- --help displays the options you
-can use after the separator. These options are also documented in the “Tests”
-section of The rustc Book.
When you run multiple tests, by default they run in parallel using threads, -meaning they finish running more quickly and you get feedback sooner. Because -the tests are running at the same time, you must make sure your tests don’t -depend on each other or on any shared state, including a shared environment, -such as the current working directory or environment variables.
-For example, say each of your tests runs some code that creates a file on disk -named test-output.txt and writes some data to that file. Then, each test -reads the data in that file and asserts that the file contains a particular -value, which is different in each test. Because the tests run at the same time, -one test might overwrite the file in the time between when another test is -writing and reading the file. The second test will then fail, not because the -code is incorrect but because the tests have interfered with each other while -running in parallel. One solution is to make sure each test writes to a -different file; another solution is to run the tests one at a time.
-If you don’t want to run the tests in parallel or if you want more fine-grained
-control over the number of threads used, you can send the --test-threads flag
-and the number of threads you want to use to the test binary. Take a look at
-the following example:
$ cargo test -- --test-threads=1
-
-We set the number of test threads to 1, telling the program not to use any
-parallelism. Running the tests using one thread will take longer than running
-them in parallel, but the tests won’t interfere with each other if they share
-state.
By default, if a test passes, Rust’s test library captures anything printed to
-standard output. For example, if we call println! in a test and the test
-passes, we won’t see the println! output in the terminal; we’ll see only the
-line that indicates the test passed. If a test fails, we’ll see whatever was
-printed to standard output with the rest of the failure message.
As an example, Listing 11-10 has a silly function that prints the value of its -parameter and returns 10, as well as a test that passes and a test that fails.
-fn prints_and_returns_10(a: i32) -> i32 {
- println!("I got the value {a}");
- 10
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn this_test_will_pass() {
- let value = prints_and_returns_10(4);
- assert_eq!(value, 10);
- }
-
- #[test]
- fn this_test_will_fail() {
- let value = prints_and_returns_10(8);
- assert_eq!(value, 5);
- }
-}
-println!When we run these tests with cargo test, we’ll see the following output:
$ cargo test
- Compiling silly-function v0.1.0 (file:///projects/silly-function)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
- Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166)
-
-running 2 tests
-test tests::this_test_will_fail ... FAILED
-test tests::this_test_will_pass ... ok
-
-failures:
-
----- tests::this_test_will_fail stdout ----
-I got the value 8
-
-thread 'tests::this_test_will_fail' panicked at src/lib.rs:19:9:
-assertion `left == right` failed
- left: 10
- right: 5
-note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
-
-
-failures:
- tests::this_test_will_fail
-
-test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-error: test failed, to rerun pass `--lib`
-
-Note that nowhere in this output do we see I got the value 4, which is
-printed when the test that passes runs. That output has been captured. The
-output from the test that failed, I got the value 8, appears in the section
-of the test summary output, which also shows the cause of the test failure.
If we want to see printed values for passing tests as well, we can tell Rust to
-also show the output of successful tests with --show-output:
$ cargo test -- --show-output
-
-When we run the tests in Listing 11-10 again with the --show-output flag, we
-see the following output:
$ cargo test -- --show-output
- Compiling silly-function v0.1.0 (file:///projects/silly-function)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.60s
- Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166)
-
-running 2 tests
-test tests::this_test_will_fail ... FAILED
-test tests::this_test_will_pass ... ok
-
-successes:
-
----- tests::this_test_will_pass stdout ----
-I got the value 4
-
-
-successes:
- tests::this_test_will_pass
-
-failures:
-
----- tests::this_test_will_fail stdout ----
-I got the value 8
-
-thread 'tests::this_test_will_fail' panicked at src/lib.rs:19:9:
-assertion `left == right` failed
- left: 10
- right: 5
-note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
-
-
-failures:
- tests::this_test_will_fail
-
-test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-error: test failed, to rerun pass `--lib`
-
-Running a full test suite can sometimes take a long time. If you’re working on
-code in a particular area, you might want to run only the tests pertaining to
-that code. You can choose which tests to run by passing cargo test the name
-or names of the test(s) you want to run as an argument.
To demonstrate how to run a subset of tests, we’ll first create three tests for
-our add_two function, as shown in Listing 11-11, and choose which ones to run.
pub fn add_two(a: u64) -> u64 {
- a + 2
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn add_two_and_two() {
- let result = add_two(2);
- assert_eq!(result, 4);
- }
-
- #[test]
- fn add_three_and_two() {
- let result = add_two(3);
- assert_eq!(result, 5);
- }
-
- #[test]
- fn one_hundred() {
- let result = add_two(100);
- assert_eq!(result, 102);
- }
-}
-If we run the tests without passing any arguments, as we saw earlier, all the -tests will run in parallel:
-$ cargo test
- Compiling adder v0.1.0 (file:///projects/adder)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.62s
- Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
-
-running 3 tests
-test tests::add_three_and_two ... ok
-test tests::add_two_and_two ... ok
-test tests::one_hundred ... ok
-
-test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Doc-tests adder
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-
-We can pass the name of any test function to cargo test to run only that test:
$ cargo test one_hundred
- Compiling adder v0.1.0 (file:///projects/adder)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.69s
- Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
-
-running 1 test
-test tests::one_hundred ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
-
-
-Only the test with the name one_hundred ran; the other two tests didn’t match
-that name. The test output lets us know we had more tests that didn’t run by
-displaying 2 filtered out at the end.
We can’t specify the names of multiple tests in this way; only the first value
-given to cargo test will be used. But there is a way to run multiple tests.
We can specify part of a test name, and any test whose name matches that value
-will be run. For example, because two of our tests’ names contain add, we can
-run those two by running cargo test add:
$ cargo test add
- Compiling adder v0.1.0 (file:///projects/adder)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
- Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
-
-running 2 tests
-test tests::add_three_and_two ... ok
-test tests::add_two_and_two ... ok
-
-test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
-
-
-This command ran all tests with add in the name and filtered out the test
-named one_hundred. Also note that the module in which a test appears becomes
-part of the test’s name, so we can run all the tests in a module by filtering
-on the module’s name.
Sometimes a few specific tests can be very time-consuming to execute, so you
-might want to exclude them during most runs of cargo test. Rather than
-listing as arguments all tests you do want to run, you can instead annotate the
-time-consuming tests using the ignore attribute to exclude them, as shown
-here:
Filename: src/lib.rs
-pub fn add(left: u64, right: u64) -> u64 {
- left + right
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn it_works() {
- let result = add(2, 2);
- assert_eq!(result, 4);
- }
-
- #[test]
- #[ignore]
- fn expensive_test() {
- // code that takes an hour to run
- }
-}
-After #[test], we add the #[ignore] line to the test we want to exclude.
-Now when we run our tests, it_works runs, but expensive_test doesn’t:
$ cargo test
- Compiling adder v0.1.0 (file:///projects/adder)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.60s
- Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
-
-running 2 tests
-test tests::expensive_test ... ignored
-test tests::it_works ... ok
-
-test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Doc-tests adder
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-
-The expensive_test function is listed as ignored. If we want to run only
-the ignored tests, we can use cargo test -- --ignored:
$ cargo test -- --ignored
- Compiling adder v0.1.0 (file:///projects/adder)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
- Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
-
-running 1 test
-test tests::expensive_test ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
-
- Doc-tests adder
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-
-By controlling which tests run, you can make sure your cargo test results
-will be returned quickly. When you’re at a point where it makes sense to check
-the results of the ignored tests and you have time to wait for the results,
-you can run cargo test -- --ignored instead. If you want to run all tests
-whether they’re ignored or not, you can run cargo test -- --include-ignored.
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
-As mentioned at the start of the chapter, testing is a complex discipline, and -different people use different terminology and organization. The Rust community -thinks about tests in terms of two main categories: unit tests and integration -tests. Unit tests are small and more focused, testing one module in isolation -at a time, and can test private interfaces. Integration tests are entirely -external to your library and use your code in the same way any other external -code would, using only the public interface and potentially exercising multiple -modules per test.
-Writing both kinds of tests is important to ensure that the pieces of your -library are doing what you expect them to, separately and together.
-The purpose of unit tests is to test each unit of code in isolation from the
-rest of the code to quickly pinpoint where code is and isn’t working as
-expected. You’ll put unit tests in the src directory in each file with the
-code that they’re testing. The convention is to create a module named tests
-in each file to contain the test functions and to annotate the module with
-cfg(test).
tests Module and #[cfg(test)]The #[cfg(test)] annotation on the tests module tells Rust to compile and
-run the test code only when you run cargo test, not when you run cargo build. This saves compile time when you only want to build the library and
-saves space in the resultant compiled artifact because the tests are not
-included. You’ll see that because integration tests go in a different
-directory, they don’t need the #[cfg(test)] annotation. However, because unit
-tests go in the same files as the code, you’ll use #[cfg(test)] to specify
-that they shouldn’t be included in the compiled result.
Recall that when we generated the new adder project in the first section of
-this chapter, Cargo generated this code for us:
Filename: src/lib.rs
-pub fn add(left: u64, right: u64) -> u64 {
- left + right
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn it_works() {
- let result = add(2, 2);
- assert_eq!(result, 4);
- }
-}
-On the automatically generated tests module, the attribute cfg stands for
-configuration and tells Rust that the following item should only be included
-given a certain configuration option. In this case, the configuration option is
-test, which is provided by Rust for compiling and running tests. By using the
-cfg attribute, Cargo compiles our test code only if we actively run the tests
-with cargo test. This includes any helper functions that might be within this
-module, in addition to the functions annotated with #[test].
There’s debate within the testing community about whether or not private
-functions should be tested directly, and other languages make it difficult or
-impossible to test private functions. Regardless of which testing ideology you
-adhere to, Rust’s privacy rules do allow you to test private functions.
-Consider the code in Listing 11-12 with the private function internal_adder.
pub fn add_two(a: u64) -> u64 {
- internal_adder(a, 2)
-}
-
-fn internal_adder(left: u64, right: u64) -> u64 {
- left + right
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn internal() {
- let result = internal_adder(2, 2);
- assert_eq!(result, 4);
- }
-}
-Note that the internal_adder function is not marked as pub. Tests are just
-Rust code, and the tests module is just another module. As we discussed in
-“Paths for Referring to an Item in the Module Tree”,
-items in child modules can use the items in their ancestor modules. In this
-test, we bring all of the items belonging to the tests module’s parent into
-scope with use super::*, and then the test can call internal_adder. If you
-don’t think private functions should be tested, there’s nothing in Rust that
-will compel you to do so.
In Rust, integration tests are entirely external to your library. They use your -library in the same way any other code would, which means they can only call -functions that are part of your library’s public API. Their purpose is to test -whether many parts of your library work together correctly. Units of code that -work correctly on their own could have problems when integrated, so test -coverage of the integrated code is important as well. To create integration -tests, you first need a tests directory.
-We create a tests directory at the top level of our project directory, next -to src. Cargo knows to look for integration test files in this directory. We -can then make as many test files as we want, and Cargo will compile each of the -files as an individual crate.
-Let’s create an integration test. With the code in Listing 11-12 still in the -src/lib.rs file, make a tests directory, and create a new file named -tests/integration_test.rs. Your directory structure should look like this:
-adder
-├── Cargo.lock
-├── Cargo.toml
-├── src
-│ └── lib.rs
-└── tests
- └── integration_test.rs
-
-Enter the code in Listing 11-13 into the tests/integration_test.rs file.
-use adder::add_two;
-
-#[test]
-fn it_adds_two() {
- let result = add_two(2);
- assert_eq!(result, 4);
-}
-adder crateEach file in the tests directory is a separate crate, so we need to bring our
-library into each test crate’s scope. For that reason, we add use adder::add_two; at the top of the code, which we didn’t need in the unit tests.
We don’t need to annotate any code in tests/integration_test.rs with
-#[cfg(test)]. Cargo treats the tests directory specially and compiles files
-in this directory only when we run cargo test. Run cargo test now:
$ cargo test
- Compiling adder v0.1.0 (file:///projects/adder)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 1.31s
- Running unittests src/lib.rs (target/debug/deps/adder-1082c4b063a8fbe6)
-
-running 1 test
-test tests::internal ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Running tests/integration_test.rs (target/debug/deps/integration_test-1082c4b063a8fbe6)
-
-running 1 test
-test it_adds_two ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Doc-tests adder
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-
-The three sections of output include the unit tests, the integration test, and -the doc tests. Note that if any test in a section fails, the following sections -will not be run. For example, if a unit test fails, there won’t be any output -for integration and doc tests, because those tests will only be run if all unit -tests are passing.
-The first section for the unit tests is the same as we’ve been seeing: one line
-for each unit test (one named internal that we added in Listing 11-12) and
-then a summary line for the unit tests.
The integration tests section starts with the line Running tests/integration_test.rs. Next, there is a line for each test function in
-that integration test and a summary line for the results of the integration
-test just before the Doc-tests adder section starts.
Each integration test file has its own section, so if we add more files in the -tests directory, there will be more integration test sections.
-We can still run a particular integration test function by specifying the test
-function’s name as an argument to cargo test. To run all the tests in a
-particular integration test file, use the --test argument of cargo test
-followed by the name of the file:
$ cargo test --test integration_test
- Compiling adder v0.1.0 (file:///projects/adder)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.64s
- Running tests/integration_test.rs (target/debug/deps/integration_test-82e7799c1bc62298)
-
-running 1 test
-test it_adds_two ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-
-This command runs only the tests in the tests/integration_test.rs file.
-As you add more integration tests, you might want to make more files in the -tests directory to help organize them; for example, you can group the test -functions by the functionality they’re testing. As mentioned earlier, each file -in the tests directory is compiled as its own separate crate, which is useful -for creating separate scopes to more closely imitate the way end users will be -using your crate. However, this means files in the tests directory don’t -share the same behavior as files in src do, as you learned in Chapter 7 -regarding how to separate code into modules and files.
-The different behavior of tests directory files is most noticeable when you
-have a set of helper functions to use in multiple integration test files, and
-you try to follow the steps in the “Separating Modules into Different
-Files” section of Chapter 7 to
-extract them into a common module. For example, if we create tests/common.rs
-and place a function named setup in it, we can add some code to setup that
-we want to call from multiple test functions in multiple test files:
Filename: tests/common.rs
-pub fn setup() {
- // setup code specific to your library's tests would go here
-}
-When we run the tests again, we’ll see a new section in the test output for the
-common.rs file, even though this file doesn’t contain any test functions nor
-did we call the setup function from anywhere:
$ cargo test
- Compiling adder v0.1.0 (file:///projects/adder)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.89s
- Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
-
-running 1 test
-test tests::internal ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Running tests/common.rs (target/debug/deps/common-92948b65e88960b4)
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Running tests/integration_test.rs (target/debug/deps/integration_test-92948b65e88960b4)
-
-running 1 test
-test it_adds_two ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Doc-tests adder
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-
-Having common appear in the test results with running 0 tests displayed for
-it is not what we wanted. We just wanted to share some code with the other
-integration test files. To avoid having common appear in the test output,
-instead of creating tests/common.rs, we’ll create tests/common/mod.rs. The
-project directory now looks like this:
├── Cargo.lock
-├── Cargo.toml
-├── src
-│ └── lib.rs
-└── tests
- ├── common
- │ └── mod.rs
- └── integration_test.rs
-
-This is the older naming convention that Rust also understands that we mentioned
-in “Alternate File Paths” in Chapter 7. Naming the
-file this way tells Rust not to treat the common module as an integration test
-file. When we move the setup function code into tests/common/mod.rs and
-delete the tests/common.rs file, the section in the test output will no longer
-appear. Files in subdirectories of the tests directory don’t get compiled as
-separate crates or have sections in the test output.
After we’ve created tests/common/mod.rs, we can use it from any of the
-integration test files as a module. Here’s an example of calling the setup
-function from the it_adds_two test in tests/integration_test.rs:
Filename: tests/integration_test.rs
-use adder::add_two;
-
-mod common;
-
-#[test]
-fn it_adds_two() {
- common::setup();
-
- let result = add_two(2);
- assert_eq!(result, 4);
-}
-Note that the mod common; declaration is the same as the module declaration
-we demonstrated in Listing 7-21. Then, in the test function, we can call the
-common::setup() function.
If our project is a binary crate that only contains a src/main.rs file and
-doesn’t have a src/lib.rs file, we can’t create integration tests in the
-tests directory and bring functions defined in the src/main.rs file into
-scope with a use statement. Only library crates expose functions that other
-crates can use; binary crates are meant to be run on their own.
This is one of the reasons Rust projects that provide a binary have a
-straightforward src/main.rs file that calls logic that lives in the
-src/lib.rs file. Using that structure, integration tests can test the
-library crate with use to make the important functionality available. If the
-important functionality works, the small amount of code in the src/main.rs
-file will work as well, and that small amount of code doesn’t need to be tested.
Rust’s testing features provide a way to specify how code should function to -ensure that it continues to work as you expect, even as you make changes. Unit -tests exercise different parts of a library separately and can test private -implementation details. Integration tests check that many parts of the library -work together correctly, and they use the library’s public API to test the code -in the same way external code will use it. Even though Rust’s type system and -ownership rules help prevent some kinds of bugs, tests are still important to -reduce logic bugs having to do with how your code is expected to behave.
-Let’s combine the knowledge you learned in this chapter and in previous -chapters to work on a project!
- -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
-This chapter is a recap of the many skills you’ve learned so far and an -exploration of a few more standard library features. We’ll build a command line -tool that interacts with file and command line input/output to practice some of -the Rust concepts you now have under your belt.
-Rust’s speed, safety, single binary output, and cross-platform support make it
-an ideal language for creating command line tools, so for our project, we’ll
-make our own version of the classic command line search tool grep
-(globally search a regular expression and print). In the
-simplest use case, grep searches a specified file for a specified string. To
-do so, grep takes as its arguments a file path and a string. Then, it reads
-the file, finds lines in that file that contain the string argument, and prints
-those lines.
Along the way, we’ll show how to make our command line tool use the terminal
-features that many other command line tools use. We’ll read the value of an
-environment variable to allow the user to configure the behavior of our tool.
-We’ll also print error messages to the standard error console stream (stderr)
-instead of standard output (stdout) so that, for example, the user can
-redirect successful output to a file while still seeing error messages onscreen.
One Rust community member, Andrew Gallant, has already created a fully
-featured, very fast version of grep, called ripgrep. By comparison, our
-version will be fairly simple, but this chapter will give you some of the
-background knowledge you need to understand a real-world project such as
-ripgrep.
Our grep project will combine a number of concepts you’ve learned so far:
We’ll also briefly introduce closures, iterators, and trait objects, which -Chapter 13 and Chapter 18 will -cover in detail.
- -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
-Let’s create a new project with, as always, cargo new. We’ll call our project
-minigrep to distinguish it from the grep tool that you might already have
-on your system:
$ cargo new minigrep
- Created binary (application) `minigrep` project
-$ cd minigrep
-
-The first task is to make minigrep accept its two command line arguments: the
-file path and a string to search for. That is, we want to be able to run our
-program with cargo run, two hyphens to indicate the following arguments are
-for our program rather than for cargo, a string to search for, and a path to
-a file to search in, like so:
$ cargo run -- searchstring example-filename.txt
-
-Right now, the program generated by cargo new cannot process arguments we
-give it. Some existing libraries on crates.io can help
-with writing a program that accepts command line arguments, but because you’re
-just learning this concept, let’s implement this capability ourselves.
To enable minigrep to read the values of command line arguments we pass to
-it, we’ll need the std::env::args function provided in Rust’s standard
-library. This function returns an iterator of the command line arguments passed
-to minigrep. We’ll cover iterators fully in Chapter 13. For now, you only need to know two details about iterators: Iterators
-produce a series of values, and we can call the collect method on an iterator
-to turn it into a collection, such as a vector, which contains all the elements
-the iterator produces.
The code in Listing 12-1 allows your minigrep program to read any command
-line arguments passed to it and then collect the values into a vector.
use std::env;
-
-fn main() {
- let args: Vec<String> = env::args().collect();
- dbg!(args);
-}
-First, we bring the std::env module into scope with a use statement so that
-we can use its args function. Notice that the std::env::args function is
-nested in two levels of modules. As we discussed in Chapter
-7, in cases where the desired function is
-nested in more than one module, we’ve chosen to bring the parent module into
-scope rather than the function. By doing so, we can easily use other functions
-from std::env. It’s also less ambiguous than adding use std::env::args and
-then calling the function with just args, because args might easily be
-mistaken for a function that’s defined in the current module.
args Function and Invalid UnicodeNote that std::env::args will panic if any argument contains invalid
-Unicode. If your program needs to accept arguments containing invalid
-Unicode, use std::env::args_os instead. That function returns an iterator
-that produces OsString values instead of String values. We’ve chosen to
-use std::env::args here for simplicity because OsString values differ per
-platform and are more complex to work with than String values.
On the first line of main, we call env::args, and we immediately use
-collect to turn the iterator into a vector containing all the values produced
-by the iterator. We can use the collect function to create many kinds of
-collections, so we explicitly annotate the type of args to specify that we
-want a vector of strings. Although you very rarely need to annotate types in
-Rust, collect is one function you do often need to annotate because Rust
-isn’t able to infer the kind of collection you want.
Finally, we print the vector using the debug macro. Let’s try running the code -first with no arguments and then with two arguments:
-$ cargo run
- Compiling minigrep v0.1.0 (file:///projects/minigrep)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
- Running `target/debug/minigrep`
-[src/main.rs:5:5] args = [
- "target/debug/minigrep",
-]
-
-$ cargo run -- needle haystack
- Compiling minigrep v0.1.0 (file:///projects/minigrep)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.57s
- Running `target/debug/minigrep needle haystack`
-[src/main.rs:5:5] args = [
- "target/debug/minigrep",
- "needle",
- "haystack",
-]
-
-Notice that the first value in the vector is "target/debug/minigrep", which
-is the name of our binary. This matches the behavior of the arguments list in
-C, letting programs use the name by which they were invoked in their execution.
-It’s often convenient to have access to the program name in case you want to
-print it in messages or change the behavior of the program based on what
-command line alias was used to invoke the program. But for the purposes of this
-chapter, we’ll ignore it and save only the two arguments we need.
The program is currently able to access the values specified as command line -arguments. Now we need to save the values of the two arguments in variables so -that we can use the values throughout the rest of the program. We do that in -Listing 12-2.
-use std::env;
-
-fn main() {
- let args: Vec<String> = env::args().collect();
-
- let query = &args[1];
- let file_path = &args[2];
-
- println!("Searching for {query}");
- println!("In file {file_path}");
-}
-As we saw when we printed the vector, the program’s name takes up the first
-value in the vector at args[0], so we’re starting arguments at index 1. The
-first argument minigrep takes is the string we’re searching for, so we put a
-reference to the first argument in the variable query. The second argument
-will be the file path, so we put a reference to the second argument in the
-variable file_path.
We temporarily print the values of these variables to prove that the code is
-working as we intend. Let’s run this program again with the arguments test
-and sample.txt:
$ cargo run -- test sample.txt
- Compiling minigrep v0.1.0 (file:///projects/minigrep)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
- Running `target/debug/minigrep test sample.txt`
-Searching for test
-In file sample.txt
-
-Great, the program is working! The values of the arguments we need are being -saved into the right variables. Later we’ll add some error handling to deal -with certain potential erroneous situations, such as when the user provides no -arguments; for now, we’ll ignore that situation and work on adding file-reading -capabilities instead.
- -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
-Now we’ll add functionality to read the file specified in the file_path
-argument. First, we need a sample file to test it with: We’ll use a file with a
-small amount of text over multiple lines with some repeated words. Listing 12-3
-has an Emily Dickinson poem that will work well! Create a file called
-poem.txt at the root level of your project, and enter the poem “I’m Nobody!
-Who are you?”
I'm nobody! Who are you?
-Are you nobody, too?
-Then there's a pair of us - don't tell!
-They'd banish us, you know.
-
-How dreary to be somebody!
-How public, like a frog
-To tell your name the livelong day
-To an admiring bog!
-
-With the text in place, edit src/main.rs and add code to read the file, as -shown in Listing 12-4.
-use std::env;
-use std::fs;
-
-fn main() {
- // --snip--
- let args: Vec<String> = env::args().collect();
-
- let query = &args[1];
- let file_path = &args[2];
-
- println!("Searching for {query}");
- println!("In file {file_path}");
-
- let contents = fs::read_to_string(file_path)
- .expect("Should have been able to read the file");
-
- println!("With text:\n{contents}");
-}
-First, we bring in a relevant part of the standard library with a use
-statement: We need std::fs to handle files.
In main, the new statement fs::read_to_string takes the file_path, opens
-that file, and returns a value of type std::io::Result<String> that contains
-the file’s contents.
After that, we again add a temporary println! statement that prints the value
-of contents after the file is read so that we can check that the program is
-working so far.
Let’s run this code with any string as the first command line argument (because -we haven’t implemented the searching part yet) and the poem.txt file as the -second argument:
-$ cargo run -- the poem.txt
- Compiling minigrep v0.1.0 (file:///projects/minigrep)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
- Running `target/debug/minigrep the poem.txt`
-Searching for the
-In file poem.txt
-With text:
-I'm nobody! Who are you?
-Are you nobody, too?
-Then there's a pair of us - don't tell!
-They'd banish us, you know.
-
-How dreary to be somebody!
-How public, like a frog
-To tell your name the livelong day
-To an admiring bog!
-
-
-Great! The code read and then printed the contents of the file. But the code
-has a few flaws. At the moment, the main function has multiple
-responsibilities: Generally, functions are clearer and easier to maintain if
-each function is responsible for only one idea. The other problem is that we’re
-not handling errors as well as we could. The program is still small, so these
-flaws aren’t a big problem, but as the program grows, it will be harder to fix
-them cleanly. It’s a good practice to begin refactoring early on when
-developing a program because it’s much easier to refactor smaller amounts of
-code. We’ll do that next.
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
-Now that we have the search logic in src/lib.rs separate from the main
-function, it’s much easier to write tests for the core functionality of our
-code. We can call functions directly with various arguments and check return
-values without having to call our binary from the command line.
In this section, we’ll add the searching logic to the minigrep program using
-the test-driven development (TDD) process with the following steps:
Though it’s just one of many ways to write software, TDD can help drive code -design. Writing the test before you write the code that makes the test pass -helps maintain high test coverage throughout the process.
-We’ll test-drive the implementation of the functionality that will actually do
-the searching for the query string in the file contents and produce a list of
-lines that match the query. We’ll add this functionality in a function called
-search.
In src/lib.rs, we’ll add a tests module with a test function, as we did in
-Chapter 11. The test function specifies the
-behavior we want the search function to have: It will take a query and the
-text to search, and it will return only the lines from the text that contain
-the query. Listing 12-15 shows this test.
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
- unimplemented!();
-}
-
-// --snip--
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn one_result() {
- let query = "duct";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.";
-
- assert_eq!(vec!["safe, fast, productive."], search(query, contents));
- }
-}
-search function for the functionality we wish we hadThis test searches for the string "duct". The text we’re searching is three
-lines, only one of which contains "duct" (note that the backslash after the
-opening double quote tells Rust not to put a newline character at the beginning
-of the contents of this string literal). We assert that the value returned from
-the search function contains only the line we expect.
If we run this test, it will currently fail because the unimplemented! macro
-panics with the message “not implemented”. In accordance with TDD principles,
-we’ll take a small step of adding just enough code to get the test to not panic
-when calling the function by defining the search function to always return an
-empty vector, as shown in Listing 12-16. Then, the test should compile and fail
-because an empty vector doesn’t match a vector containing the line "safe, fast, productive.".
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
- vec![]
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn one_result() {
- let query = "duct";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.";
-
- assert_eq!(vec!["safe, fast, productive."], search(query, contents));
- }
-}
-search function so that calling it won’t panicNow let’s discuss why we need to define an explicit lifetime 'a in the
-signature of search and use that lifetime with the contents argument and
-the return value. Recall in Chapter 10 that
-the lifetime parameters specify which argument lifetime is connected to the
-lifetime of the return value. In this case, we indicate that the returned
-vector should contain string slices that reference slices of the argument
-contents (rather than the argument query).
In other words, we tell Rust that the data returned by the search function
-will live as long as the data passed into the search function in the
-contents argument. This is important! The data referenced by a slice needs
-to be valid for the reference to be valid; if the compiler assumes we’re making
-string slices of query rather than contents, it will do its safety checking
-incorrectly.
If we forget the lifetime annotations and try to compile this function, we’ll -get this error:
-$ cargo build
- Compiling minigrep v0.1.0 (file:///projects/minigrep)
-error[E0106]: missing lifetime specifier
- --> src/lib.rs:1:51
- |
-1 | pub fn search(query: &str, contents: &str) -> Vec<&str> {
- | ---- ---- ^ expected named lifetime parameter
- |
- = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `query` or `contents`
-help: consider introducing a named lifetime parameter
- |
-1 | pub fn search<'a>(query: &'a str, contents: &'a str) -> Vec<&'a str> {
- | ++++ ++ ++ ++
-
-For more information about this error, try `rustc --explain E0106`.
-error: could not compile `minigrep` (lib) due to 1 previous error
-
-Rust can’t know which of the two parameters we need for the output, so we need
-to tell it explicitly. Note that the help text suggests specifying the same
-lifetime parameter for all the parameters and the output type, which is
-incorrect! Because contents is the parameter that contains all of our text
-and we want to return the parts of that text that match, we know contents is
-the only parameter that should be connected to the return value using the
-lifetime syntax.
Other programming languages don’t require you to connect arguments to return -values in the signature, but this practice will get easier over time. You might -want to compare this example with the examples in the “Validating References -with Lifetimes” section -in Chapter 10.
-Currently, our test is failing because we always return an empty vector. To fix
-that and implement search, our program needs to follow these steps:
Let’s work through each step, starting with iterating through lines.
-lines MethodRust has a helpful method to handle line-by-line iteration of strings,
-conveniently named lines, that works as shown in Listing 12-17. Note that
-this won’t compile yet.
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
- for line in contents.lines() {
- // do something with line
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn one_result() {
- let query = "duct";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.";
-
- assert_eq!(vec!["safe, fast, productive."], search(query, contents));
- }
-}
-contentsThe lines method returns an iterator. We’ll talk about iterators in depth in
-Chapter 13. But recall that you saw this way
-of using an iterator in Listing 3-5, where we used a
-for loop with an iterator to run some code on each item in a collection.
Next, we’ll check whether the current line contains our query string.
-Fortunately, strings have a helpful method named contains that does this for
-us! Add a call to the contains method in the search function, as shown in
-Listing 12-18. Note that this still won’t compile yet.
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
- for line in contents.lines() {
- if line.contains(query) {
- // do something with line
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn one_result() {
- let query = "duct";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.";
-
- assert_eq!(vec!["safe, fast, productive."], search(query, contents));
- }
-}
-queryAt the moment, we’re building up functionality. To get the code to compile, we -need to return a value from the body as we indicated we would in the function -signature.
-To finish this function, we need a way to store the matching lines that we want
-to return. For that, we can make a mutable vector before the for loop and
-call the push method to store a line in the vector. After the for loop,
-we return the vector, as shown in Listing 12-19.
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
- let mut results = Vec::new();
-
- for line in contents.lines() {
- if line.contains(query) {
- results.push(line);
- }
- }
-
- results
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn one_result() {
- let query = "duct";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.";
-
- assert_eq!(vec!["safe, fast, productive."], search(query, contents));
- }
-}
-Now the search function should return only the lines that contain query,
-and our test should pass. Let’s run the test:
$ cargo test
- Compiling minigrep v0.1.0 (file:///projects/minigrep)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 1.22s
- Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
-
-running 1 test
-test tests::one_result ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Doc-tests minigrep
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-
-Our test passed, so we know it works!
-At this point, we could consider opportunities for refactoring the -implementation of the search function while keeping the tests passing to -maintain the same functionality. The code in the search function isn’t too bad, -but it doesn’t take advantage of some useful features of iterators. We’ll -return to this example in Chapter 13, where -we’ll explore iterators in detail, and look at how to improve it.
-Now the entire program should work! Let’s try it out, first with a word that -should return exactly one line from the Emily Dickinson poem: frog.
-$ cargo run -- frog poem.txt
- Compiling minigrep v0.1.0 (file:///projects/minigrep)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.38s
- Running `target/debug/minigrep frog poem.txt`
-How public, like a frog
-
-Cool! Now let’s try a word that will match multiple lines, like body:
-$ cargo run -- body poem.txt
- Compiling minigrep v0.1.0 (file:///projects/minigrep)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
- Running `target/debug/minigrep body poem.txt`
-I'm nobody! Who are you?
-Are you nobody, too?
-How dreary to be somebody!
-
-And finally, let’s make sure that we don’t get any lines when we search for a -word that isn’t anywhere in the poem, such as monomorphization:
-$ cargo run -- monomorphization poem.txt
- Compiling minigrep v0.1.0 (file:///projects/minigrep)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
- Running `target/debug/minigrep monomorphization poem.txt`
-
-Excellent! We’ve built our own mini version of a classic tool and learned a lot -about how to structure applications. We’ve also learned a bit about file input -and output, lifetimes, testing, and command line parsing.
-To round out this project, we’ll briefly demonstrate how to work with -environment variables and how to print to standard error, both of which are -useful when you’re writing command line programs.
- -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
-At the moment, we’re writing all of our output to the terminal using the
-println! macro. In most terminals, there are two kinds of output: standard
-output (stdout) for general information and standard error (stderr) for
-error messages. This distinction enables users to choose to direct the
-successful output of a program to a file but still print error messages to the
-screen.
The println! macro is only capable of printing to standard output, so we have
-to use something else to print to standard error.
First, let’s observe how the content printed by minigrep is currently being
-written to standard output, including any error messages we want to write to
-standard error instead. We’ll do that by redirecting the standard output stream
-to a file while intentionally causing an error. We won’t redirect the standard
-error stream, so any content sent to standard error will continue to display on
-the screen.
Command line programs are expected to send error messages to the standard error -stream so that we can still see error messages on the screen even if we -redirect the standard output stream to a file. Our program is not currently -well behaved: We’re about to see that it saves the error message output to a -file instead!
-To demonstrate this behavior, we’ll run the program with > and the file path,
-output.txt, that we want to redirect the standard output stream to. We won’t
-pass any arguments, which should cause an error:
$ cargo run > output.txt
-
-The > syntax tells the shell to write the contents of standard output to
-output.txt instead of the screen. We didn’t see the error message we were
-expecting printed to the screen, so that means it must have ended up in the
-file. This is what output.txt contains:
Problem parsing arguments: not enough arguments
-
-Yup, our error message is being printed to standard output. It’s much more -useful for error messages like this to be printed to standard error so that -only data from a successful run ends up in the file. We’ll change that.
-We’ll use the code in Listing 12-24 to change how error messages are printed.
-Because of the refactoring we did earlier in this chapter, all the code that
-prints error messages is in one function, main. The standard library provides
-the eprintln! macro that prints to the standard error stream, so let’s change
-the two places we were calling println! to print errors to use eprintln!
-instead.
use std::env;
-use std::error::Error;
-use std::fs;
-use std::process;
-
-use minigrep::{search, search_case_insensitive};
-
-fn main() {
- let args: Vec<String> = env::args().collect();
-
- let config = Config::build(&args).unwrap_or_else(|err| {
- eprintln!("Problem parsing arguments: {err}");
- process::exit(1);
- });
-
- if let Err(e) = run(config) {
- eprintln!("Application error: {e}");
- process::exit(1);
- }
-}
-
-pub struct Config {
- pub query: String,
- pub file_path: String,
- pub ignore_case: bool,
-}
-
-impl Config {
- fn build(args: &[String]) -> Result<Config, &'static str> {
- if args.len() < 3 {
- return Err("not enough arguments");
- }
-
- let query = args[1].clone();
- let file_path = args[2].clone();
-
- let ignore_case = env::var("IGNORE_CASE").is_ok();
-
- Ok(Config {
- query,
- file_path,
- ignore_case,
- })
- }
-}
-
-fn run(config: Config) -> Result<(), Box<dyn Error>> {
- let contents = fs::read_to_string(config.file_path)?;
-
- let results = if config.ignore_case {
- search_case_insensitive(&config.query, &contents)
- } else {
- search(&config.query, &contents)
- };
-
- for line in results {
- println!("{line}");
- }
-
- Ok(())
-}
-eprintln!Let’s now run the program again in the same way, without any arguments and
-redirecting standard output with >:
$ cargo run > output.txt
-Problem parsing arguments: not enough arguments
-
-Now we see the error onscreen and output.txt contains nothing, which is the -behavior we expect of command line programs.
-Let’s run the program again with arguments that don’t cause an error but still -redirect standard output to a file, like so:
-$ cargo run -- to poem.txt > output.txt
-
-We won’t see any output to the terminal, and output.txt will contain our -results:
-Filename: output.txt
-Are you nobody, too?
-How dreary to be somebody!
-
-This demonstrates that we’re now using standard output for successful output -and standard error for error output as appropriate.
-This chapter recapped some of the major concepts you’ve learned so far and
-covered how to perform common I/O operations in Rust. By using command line
-arguments, files, environment variables, and the eprintln! macro for printing
-errors, you’re now prepared to write command line applications. Combined with
-the concepts in previous chapters, your code will be well organized, store data
-effectively in the appropriate data structures, handle errors nicely, and be
-well tested.
Next, we’ll explore some Rust features that were influenced by functional -languages: closures and iterators.
- -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
-Rust’s design has taken inspiration from many existing languages and -techniques, and one significant influence is functional programming. -Programming in a functional style often includes using functions as values by -passing them in arguments, returning them from other functions, assigning them -to variables for later execution, and so forth.
-In this chapter, we won’t debate the issue of what functional programming is or -isn’t but will instead discuss some features of Rust that are similar to -features in many languages often referred to as functional.
-More specifically, we’ll cover:
-We’ve already covered some other Rust features, such as pattern matching and -enums, that are also influenced by the functional style. Because mastering -closures and iterators is an important part of writing fast, idiomatic, Rust -code, we’ll devote this entire chapter to them.
- -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
-The iterator pattern allows you to perform some task on a sequence of items in -turn. An iterator is responsible for the logic of iterating over each item and -determining when the sequence has finished. When you use iterators, you don’t -have to reimplement that logic yourself.
-In Rust, iterators are lazy, meaning they have no effect until you call
-methods that consume the iterator to use it up. For example, the code in
-Listing 13-10 creates an iterator over the items in the vector v1 by calling
-the iter method defined on Vec<T>. This code by itself doesn’t do anything
-useful.
fn main() {
- let v1 = vec![1, 2, 3];
-
- let v1_iter = v1.iter();
-}
-The iterator is stored in the v1_iter variable. Once we’ve created an
-iterator, we can use it in a variety of ways. In Listing 3-5, we iterated over
-an array using a for loop to execute some code on each of its items. Under
-the hood, this implicitly created and then consumed an iterator, but we glossed
-over how exactly that works until now.
In the example in Listing 13-11, we separate the creation of the iterator from
-the use of the iterator in the for loop. When the for loop is called using
-the iterator in v1_iter, each element in the iterator is used in one
-iteration of the loop, which prints out each value.
fn main() {
- let v1 = vec![1, 2, 3];
-
- let v1_iter = v1.iter();
-
- for val in v1_iter {
- println!("Got: {val}");
- }
-}
-for loopIn languages that don’t have iterators provided by their standard libraries, -you would likely write this same functionality by starting a variable at index -0, using that variable to index into the vector to get a value, and -incrementing the variable value in a loop until it reached the total number of -items in the vector.
-Iterators handle all of that logic for you, cutting down on repetitive code you -could potentially mess up. Iterators give you more flexibility to use the same -logic with many different kinds of sequences, not just data structures you can -index into, like vectors. Let’s examine how iterators do that.
-Iterator Trait and the next MethodAll iterators implement a trait named Iterator that is defined in the
-standard library. The definition of the trait looks like this:
#![allow(unused)]
-fn main() {
-pub trait Iterator {
- type Item;
-
- fn next(&mut self) -> Option<Self::Item>;
-
- // methods with default implementations elided
-}
-}
-Notice that this definition uses some new syntax: type Item and Self::Item,
-which are defining an associated type with this trait. We’ll talk about
-associated types in depth in Chapter 20. For now, all you need to know is that
-this code says implementing the Iterator trait requires that you also define
-an Item type, and this Item type is used in the return type of the next
-method. In other words, the Item type will be the type returned from the
-iterator.
The Iterator trait only requires implementors to define one method: the
-next method, which returns one item of the iterator at a time, wrapped in
-Some, and, when iteration is over, returns None.
We can call the next method on iterators directly; Listing 13-12 demonstrates
-what values are returned from repeated calls to next on the iterator created
-from the vector.
#[cfg(test)]
-mod tests {
- #[test]
- fn iterator_demonstration() {
- let v1 = vec![1, 2, 3];
-
- let mut v1_iter = v1.iter();
-
- assert_eq!(v1_iter.next(), Some(&1));
- assert_eq!(v1_iter.next(), Some(&2));
- assert_eq!(v1_iter.next(), Some(&3));
- assert_eq!(v1_iter.next(), None);
- }
-}
-next method on an iteratorNote that we needed to make v1_iter mutable: Calling the next method on an
-iterator changes internal state that the iterator uses to keep track of where
-it is in the sequence. In other words, this code consumes, or uses up, the
-iterator. Each call to next eats up an item from the iterator. We didn’t need
-to make v1_iter mutable when we used a for loop, because the loop took
-ownership of v1_iter and made it mutable behind the scenes.
Also note that the values we get from the calls to next are immutable
-references to the values in the vector. The iter method produces an iterator
-over immutable references. If we want to create an iterator that takes
-ownership of v1 and returns owned values, we can call into_iter instead of
-iter. Similarly, if we want to iterate over mutable references, we can call
-iter_mut instead of iter.
The Iterator trait has a number of different methods with default
-implementations provided by the standard library; you can find out about these
-methods by looking in the standard library API documentation for the Iterator
-trait. Some of these methods call the next method in their definition, which
-is why you’re required to implement the next method when implementing the
-Iterator trait.
Methods that call next are called consuming adapters because calling them
-uses up the iterator. One example is the sum method, which takes ownership of
-the iterator and iterates through the items by repeatedly calling next, thus
-consuming the iterator. As it iterates through, it adds each item to a running
-total and returns the total when iteration is complete. Listing 13-13 has a
-test illustrating a use of the sum method.
#[cfg(test)]
-mod tests {
- #[test]
- fn iterator_sum() {
- let v1 = vec![1, 2, 3];
-
- let v1_iter = v1.iter();
-
- let total: i32 = v1_iter.sum();
-
- assert_eq!(total, 6);
- }
-}
-sum method to get the total of all items in the iteratorWe aren’t allowed to use v1_iter after the call to sum, because sum takes
-ownership of the iterator we call it on.
Iterator adapters are methods defined on the Iterator trait that don’t
-consume the iterator. Instead, they produce different iterators by changing
-some aspect of the original iterator.
Listing 13-14 shows an example of calling the iterator adapter method map,
-which takes a closure to call on each item as the items are iterated through.
-The map method returns a new iterator that produces the modified items. The
-closure here creates a new iterator in which each item from the vector will be
-incremented by 1.
fn main() {
- let v1: Vec<i32> = vec![1, 2, 3];
-
- v1.iter().map(|x| x + 1);
-}
-map to create a new iteratorHowever, this code produces a warning:
-$ cargo run
- Compiling iterators v0.1.0 (file:///projects/iterators)
-warning: unused `Map` that must be used
- --> src/main.rs:4:5
- |
-4 | v1.iter().map(|x| x + 1);
- | ^^^^^^^^^^^^^^^^^^^^^^^^
- |
- = note: iterators are lazy and do nothing unless consumed
- = note: `#[warn(unused_must_use)]` on by default
-help: use `let _ = ...` to ignore the resulting value
- |
-4 | let _ = v1.iter().map(|x| x + 1);
- | +++++++
-
-warning: `iterators` (bin "iterators") generated 1 warning
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.47s
- Running `target/debug/iterators`
-
-The code in Listing 13-14 doesn’t do anything; the closure we’ve specified -never gets called. The warning reminds us why: Iterator adapters are lazy, and -we need to consume the iterator here.
-To fix this warning and consume the iterator, we’ll use the collect method,
-which we used with env::args in Listing 12-1. This method consumes the
-iterator and collects the resultant values into a collection data type.
In Listing 13-15, we collect the results of iterating over the iterator that’s
-returned from the call to map into a vector. This vector will end up
-containing each item from the original vector, incremented by 1.
fn main() {
- let v1: Vec<i32> = vec![1, 2, 3];
-
- let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
-
- assert_eq!(v2, vec![2, 3, 4]);
-}
-map method to create a new iterator, and then calling the collect method to consume the new iterator and create a vectorBecause map takes a closure, we can specify any operation we want to perform
-on each item. This is a great example of how closures let you customize some
-behavior while reusing the iteration behavior that the Iterator trait
-provides.
You can chain multiple calls to iterator adapters to perform complex actions in -a readable way. But because all iterators are lazy, you have to call one of the -consuming adapter methods to get results from calls to iterator adapters.
- - -Many iterator adapters take closures as arguments, and commonly the closures -we’ll specify as arguments to iterator adapters will be closures that capture -their environment.
-For this example, we’ll use the filter method that takes a closure. The
-closure gets an item from the iterator and returns a bool. If the closure
-returns true, the value will be included in the iteration produced by
-filter. If the closure returns false, the value won’t be included.
In Listing 13-16, we use filter with a closure that captures the shoe_size
-variable from its environment to iterate over a collection of Shoe struct
-instances. It will return only shoes that are the specified size.
#[derive(PartialEq, Debug)]
-struct Shoe {
- size: u32,
- style: String,
-}
-
-fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
- shoes.into_iter().filter(|s| s.size == shoe_size).collect()
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn filters_by_size() {
- let shoes = vec![
- Shoe {
- size: 10,
- style: String::from("sneaker"),
- },
- Shoe {
- size: 13,
- style: String::from("sandal"),
- },
- Shoe {
- size: 10,
- style: String::from("boot"),
- },
- ];
-
- let in_my_size = shoes_in_size(shoes, 10);
-
- assert_eq!(
- in_my_size,
- vec![
- Shoe {
- size: 10,
- style: String::from("sneaker")
- },
- Shoe {
- size: 10,
- style: String::from("boot")
- },
- ]
- );
- }
-}
-filter method with a closure that captures shoe_sizeThe shoes_in_size function takes ownership of a vector of shoes and a shoe
-size as parameters. It returns a vector containing only shoes of the specified
-size.
In the body of shoes_in_size, we call into_iter to create an iterator that
-takes ownership of the vector. Then, we call filter to adapt that iterator
-into a new iterator that only contains elements for which the closure returns
-true.
The closure captures the shoe_size parameter from the environment and
-compares the value with each shoe’s size, keeping only shoes of the size
-specified. Finally, calling collect gathers the values returned by the
-adapted iterator into a vector that’s returned by the function.
The test shows that when we call shoes_in_size, we get back only shoes that
-have the same size as the value we specified.
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
-To determine whether to use loops or iterators, you need to know which
-implementation is faster: the version of the search function with an explicit
-for loop or the version with iterators.
We ran a benchmark by loading the entire contents of The Adventures of
-Sherlock Holmes by Sir Arthur Conan Doyle into a String and looking for the
-word the in the contents. Here are the results of the benchmark on the
-version of search using the for loop and the version using iterators:
test bench_search_for ... bench: 19,620,300 ns/iter (+/- 915,700)
-test bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200)
-
-The two implementations have similar performance! We won’t explain the -benchmark code here because the point is not to prove that the two versions -are equivalent but to get a general sense of how these two implementations -compare performance-wise.
-For a more comprehensive benchmark, you should check using various texts of
-various sizes as the contents, different words and words of different lengths
-as the query, and all kinds of other variations. The point is this:
-Iterators, although a high-level abstraction, get compiled down to roughly the
-same code as if you’d written the lower-level code yourself. Iterators are one
-of Rust’s zero-cost abstractions, by which we mean that using the abstraction
-imposes no additional runtime overhead. This is analogous to how Bjarne
-Stroustrup, the original designer and implementor of C++, defines
-zero-overhead in his 2012 ETAPS keynote presentation “Foundations of C++”:
--In general, C++ implementations obey the zero-overhead principle: What you -don’t use, you don’t pay for. And further: What you do use, you couldn’t hand -code any better.
-
In many cases, Rust code using iterators compiles to the same assembly you’d -write by hand. Optimizations such as loop unrolling and eliminating bounds -checking on array access apply and make the resultant code extremely efficient. -Now that you know this, you can use iterators and closures without fear! They -make code seem like it’s higher level but don’t impose a runtime performance -penalty for doing so.
-Closures and iterators are Rust features inspired by functional programming -language ideas. They contribute to Rust’s capability to clearly express -high-level ideas at low-level performance. The implementations of closures and -iterators are such that runtime performance is not affected. This is part of -Rust’s goal to strive to provide zero-cost abstractions.
-Now that we’ve improved the expressiveness of our I/O project, let’s look at
-some more features of cargo that will help us share the project with the
-world.
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
-So far, we’ve used only the most basic features of Cargo to build, run, and -test our code, but it can do a lot more. In this chapter, we’ll discuss some of -its other, more advanced features to show you how to do the following:
-Cargo can do even more than the functionality we cover in this chapter, so for -a full explanation of all its features, see its documentation.
- -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
-In Rust, release profiles are predefined, customizable profiles with -different configurations that allow a programmer to have more control over -various options for compiling code. Each profile is configured independently of -the others.
-Cargo has two main profiles: the dev profile Cargo uses when you run cargo build, and the release profile Cargo uses when you run cargo build --release. The dev profile is defined with good defaults for development,
-and the release profile has good defaults for release builds.
These profile names might be familiar from the output of your builds:
- -$ cargo build
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
-$ cargo build --release
- Finished `release` profile [optimized] target(s) in 0.32s
-
-The dev and release are these different profiles used by the compiler.
Cargo has default settings for each of the profiles that apply when you haven’t
-explicitly added any [profile.*] sections in the project’s Cargo.toml file.
-By adding [profile.*] sections for any profile you want to customize, you
-override any subset of the default settings. For example, here are the default
-values for the opt-level setting for the dev and release profiles:
Filename: Cargo.toml
-[profile.dev]
-opt-level = 0
-
-[profile.release]
-opt-level = 3
-
-The opt-level setting controls the number of optimizations Rust will apply to
-your code, with a range of 0 to 3. Applying more optimizations extends
-compiling time, so if you’re in development and compiling your code often,
-you’ll want fewer optimizations to compile faster even if the resultant code
-runs slower. The default opt-level for dev is therefore 0. When you’re
-ready to release your code, it’s best to spend more time compiling. You’ll only
-compile in release mode once, but you’ll run the compiled program many times,
-so release mode trades longer compile time for code that runs faster. That is
-why the default opt-level for the release profile is 3.
You can override a default setting by adding a different value for it in -Cargo.toml. For example, if we want to use optimization level 1 in the -development profile, we can add these two lines to our project’s Cargo.toml -file:
-Filename: Cargo.toml
-[profile.dev]
-opt-level = 1
-
-This code overrides the default setting of 0. Now when we run cargo build,
-Cargo will use the defaults for the dev profile plus our customization to
-opt-level. Because we set opt-level to 1, Cargo will apply more
-optimizations than the default, but not as many as in a release build.
For the full list of configuration options and defaults for each profile, see -Cargo’s documentation.
- -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
-In Chapter 12, we built a package that included a binary crate and a library -crate. As your project develops, you might find that the library crate -continues to get bigger and you want to split your package further into -multiple library crates. Cargo offers a feature called workspaces that can -help manage multiple related packages that are developed in tandem.
-A workspace is a set of packages that share the same Cargo.lock and output
-directory. Let’s make a project using a workspace—we’ll use trivial code so
-that we can concentrate on the structure of the workspace. There are multiple
-ways to structure a workspace, so we’ll just show one common way. We’ll have a
-workspace containing a binary and two libraries. The binary, which will provide
-the main functionality, will depend on the two libraries. One library will
-provide an add_one function and the other library an add_two function.
-These three crates will be part of the same workspace. We’ll start by creating
-a new directory for the workspace:
$ mkdir add
-$ cd add
-
-Next, in the add directory, we create the Cargo.toml file that will
-configure the entire workspace. This file won’t have a [package] section.
-Instead, it will start with a [workspace] section that will allow us to add
-members to the workspace. We also make a point to use the latest and greatest
-version of Cargo’s resolver algorithm in our workspace by setting the
-resolver value to "3":
Filename: Cargo.toml
-[workspace]
-resolver = "3"
-
-Next, we’ll create the adder binary crate by running cargo new within the
-add directory:
$ cargo new adder
- Created binary (application) `adder` package
- Adding `adder` as member of workspace at `file:///projects/add`
-
-Running cargo new inside a workspace also automatically adds the newly created
-package to the members key in the [workspace] definition in the workspace
-Cargo.toml, like this:
[workspace]
-resolver = "3"
-members = ["adder"]
-
-At this point, we can build the workspace by running cargo build. The files
-in your add directory should look like this:
├── Cargo.lock
-├── Cargo.toml
-├── adder
-│ ├── Cargo.toml
-│ └── src
-│ └── main.rs
-└── target
-
-The workspace has one target directory at the top level that the compiled
-artifacts will be placed into; the adder package doesn’t have its own
-target directory. Even if we were to run cargo build from inside the
-adder directory, the compiled artifacts would still end up in add/target
-rather than add/adder/target. Cargo structures the target directory in a
-workspace like this because the crates in a workspace are meant to depend on
-each other. If each crate had its own target directory, each crate would have
-to recompile each of the other crates in the workspace to place the artifacts
-in its own target directory. By sharing one target directory, the crates
-can avoid unnecessary rebuilding.
Next, let’s create another member package in the workspace and call it
-add_one. Generate a new library crate named add_one:
$ cargo new add_one --lib
- Created library `add_one` package
- Adding `add_one` as member of workspace at `file:///projects/add`
-
-The top-level Cargo.toml will now include the add_one path in the members
-list:
Filename: Cargo.toml
-[workspace]
-resolver = "3"
-members = ["adder", "add_one"]
-
-Your add directory should now have these directories and files:
-├── Cargo.lock
-├── Cargo.toml
-├── add_one
-│ ├── Cargo.toml
-│ └── src
-│ └── lib.rs
-├── adder
-│ ├── Cargo.toml
-│ └── src
-│ └── main.rs
-└── target
-
-In the add_one/src/lib.rs file, let’s add an add_one function:
Filename: add_one/src/lib.rs
-pub fn add_one(x: i32) -> i32 {
- x + 1
-}
-Now we can have the adder package with our binary depend on the add_one
-package that has our library. First, we’ll need to add a path dependency on
-add_one to adder/Cargo.toml.
Filename: adder/Cargo.toml
-[dependencies]
-add_one = { path = "../add_one" }
-
-Cargo doesn’t assume that crates in a workspace will depend on each other, so -we need to be explicit about the dependency relationships.
-Next, let’s use the add_one function (from the add_one crate) in the
-adder crate. Open the adder/src/main.rs file and change the main
-function to call the add_one function, as in Listing 14-7.
fn main() {
- let num = 10;
- println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
-}
-add_one library crate from the adder crateLet’s build the workspace by running cargo build in the top-level add
-directory!
$ cargo build
- Compiling add_one v0.1.0 (file:///projects/add/add_one)
- Compiling adder v0.1.0 (file:///projects/add/adder)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.22s
-
-To run the binary crate from the add directory, we can specify which package
-in the workspace we want to run by using the -p argument and the package name
-with cargo run:
$ cargo run -p adder
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
- Running `target/debug/adder`
-Hello, world! 10 plus one is 11!
-
-This runs the code in adder/src/main.rs, which depends on the add_one crate.
Notice that the workspace has only one Cargo.lock file at the top level,
-rather than having a Cargo.lock in each crate’s directory. This ensures that
-all crates are using the same version of all dependencies. If we add the rand
-package to the adder/Cargo.toml and add_one/Cargo.toml files, Cargo will
-resolve both of those to one version of rand and record that in the one
-Cargo.lock. Making all crates in the workspace use the same dependencies
-means the crates will always be compatible with each other. Let’s add the
-rand crate to the [dependencies] section in the add_one/Cargo.toml file
-so that we can use the rand crate in the add_one crate:
Filename: add_one/Cargo.toml
-[dependencies]
-rand = "0.8.5"
-
-We can now add use rand; to the add_one/src/lib.rs file, and building the
-whole workspace by running cargo build in the add directory will bring in
-and compile the rand crate. We will get one warning because we aren’t
-referring to the rand we brought into scope:
$ cargo build
- Updating crates.io index
- Downloaded rand v0.8.5
- --snip--
- Compiling rand v0.8.5
- Compiling add_one v0.1.0 (file:///projects/add/add_one)
-warning: unused import: `rand`
- --> add_one/src/lib.rs:1:5
- |
-1 | use rand;
- | ^^^^
- |
- = note: `#[warn(unused_imports)]` on by default
-
-warning: `add_one` (lib) generated 1 warning (run `cargo fix --lib -p add_one` to apply 1 suggestion)
- Compiling adder v0.1.0 (file:///projects/add/adder)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.95s
-
-The top-level Cargo.lock now contains information about the dependency of
-add_one on rand. However, even though rand is used somewhere in the
-workspace, we can’t use it in other crates in the workspace unless we add
-rand to their Cargo.toml files as well. For example, if we add use rand;
-to the adder/src/main.rs file for the adder package, we’ll get an error:
$ cargo build
- --snip--
- Compiling adder v0.1.0 (file:///projects/add/adder)
-error[E0432]: unresolved import `rand`
- --> adder/src/main.rs:2:5
- |
-2 | use rand;
- | ^^^^ no external crate `rand`
-
-To fix this, edit the Cargo.toml file for the adder package and indicate
-that rand is a dependency for it as well. Building the adder package will
-add rand to the list of dependencies for adder in Cargo.lock, but no
-additional copies of rand will be downloaded. Cargo will ensure that every
-crate in every package in the workspace using the rand package will use the
-same version as long as they specify compatible versions of rand, saving us
-space and ensuring that the crates in the workspace will be compatible with
-each other.
If crates in the workspace specify incompatible versions of the same -dependency, Cargo will resolve each of them but will still try to resolve as -few versions as possible.
-For another enhancement, let’s add a test of the add_one::add_one function
-within the add_one crate:
Filename: add_one/src/lib.rs
-pub fn add_one(x: i32) -> i32 {
- x + 1
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn it_works() {
- assert_eq!(3, add_one(2));
- }
-}
-Now run cargo test in the top-level add directory. Running cargo test in
-a workspace structured like this one will run the tests for all the crates in
-the workspace:
$ cargo test
- Compiling add_one v0.1.0 (file:///projects/add/add_one)
- Compiling adder v0.1.0 (file:///projects/add/adder)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.20s
- Running unittests src/lib.rs (target/debug/deps/add_one-93c49ee75dc46543)
-
-running 1 test
-test tests::it_works ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Running unittests src/main.rs (target/debug/deps/adder-3a47283c568d2b6a)
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Doc-tests add_one
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-The first section of the output shows that the it_works test in the add_one
-crate passed. The next section shows that zero tests were found in the adder
-crate, and then the last section shows that zero documentation tests were found
-in the add_one crate.
We can also run tests for one particular crate in a workspace from the
-top-level directory by using the -p flag and specifying the name of the crate
-we want to test:
$ cargo test -p add_one
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.00s
- Running unittests src/lib.rs (target/debug/deps/add_one-93c49ee75dc46543)
-
-running 1 test
-test tests::it_works ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Doc-tests add_one
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-This output shows cargo test only ran the tests for the add_one crate and
-didn’t run the adder crate tests.
If you publish the crates in the workspace to
-crates.io, each crate in the workspace
-will need to be published separately. Like cargo test, we can publish a
-particular crate in our workspace by using the -p flag and specifying the
-name of the crate we want to publish.
For additional practice, add an add_two crate to this workspace in a similar
-way as the add_one crate!
As your project grows, consider using a workspace: It enables you to work with -smaller, easier-to-understand components than one big blob of code. -Furthermore, keeping the crates in a workspace can make coordination between -crates easier if they are often changed at the same time.
- -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
-cargo installThe cargo install command allows you to install and use binary crates
-locally. This isn’t intended to replace system packages; it’s meant to be a
-convenient way for Rust developers to install tools that others have shared on
-crates.io. Note that you can only install
-packages that have binary targets. A binary target is the runnable program
-that is created if the crate has a src/main.rs file or another file specified
-as a binary, as opposed to a library target that isn’t runnable on its own but
-is suitable for including within other programs. Usually, crates have
-information in the README file about whether a crate is a library, has a
-binary target, or both.
All binaries installed with cargo install are stored in the installation
-root’s bin folder. If you installed Rust using rustup.rs and don’t have any
-custom configurations, this directory will be $HOME/.cargo/bin. Ensure that
-this directory is in your $PATH to be able to run programs you’ve installed
-with cargo install.
For example, in Chapter 12 we mentioned that there’s a Rust implementation of
-the grep tool called ripgrep for searching files. To install ripgrep, we
-can run the following:
$ cargo install ripgrep
- Updating crates.io index
- Downloaded ripgrep v14.1.1
- Downloaded 1 crate (213.6 KB) in 0.40s
- Installing ripgrep v14.1.1
---snip--
- Compiling grep v0.3.2
- Finished `release` profile [optimized + debuginfo] target(s) in 6.73s
- Installing ~/.cargo/bin/rg
- Installed package `ripgrep v14.1.1` (executable `rg`)
-
-The second-to-last line of the output shows the location and the name of the
-installed binary, which in the case of ripgrep is rg. As long as the
-installation directory is in your $PATH, as mentioned previously, you can
-then run rg --help and start using a faster, Rustier tool for searching files!
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
-Cargo is designed so that you can extend it with new subcommands without having
-to modify it. If a binary in your $PATH is named cargo-something, you can
-run it as if it were a Cargo subcommand by running cargo something. Custom
-commands like this are also listed when you run cargo --list. Being able to
-use cargo install to install extensions and then run them just like the
-built-in Cargo tools is a super-convenient benefit of Cargo’s design!
Sharing code with Cargo and crates.io is -part of what makes the Rust ecosystem useful for many different tasks. Rust’s -standard library is small and stable, but crates are easy to share, use, and -improve on a timeline different from that of the language. Don’t be shy about -sharing code that’s useful to you on crates.io; it’s likely that it will be useful to someone else as well!
- -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
-A pointer is a general concept for a variable that contains an address in
-memory. This address refers to, or “points at,” some other data. The most
-common kind of pointer in Rust is a reference, which you learned about in
-Chapter 4. References are indicated by the & symbol and borrow the value they
-point to. They don’t have any special capabilities other than referring to
-data, and they have no overhead.
Smart pointers, on the other hand, are data structures that act like a -pointer but also have additional metadata and capabilities. The concept of -smart pointers isn’t unique to Rust: Smart pointers originated in C++ and exist -in other languages as well. Rust has a variety of smart pointers defined in the -standard library that provide functionality beyond that provided by references. -To explore the general concept, we’ll look at a couple of different examples of -smart pointers, including a reference counting smart pointer type. This -pointer enables you to allow data to have multiple owners by keeping track of -the number of owners and, when no owners remain, cleaning up the data.
-In Rust, with its concept of ownership and borrowing, there is an additional -difference between references and smart pointers: While references only borrow -data, in many cases smart pointers own the data they point to.
-Smart pointers are usually implemented using structs. Unlike an ordinary
-struct, smart pointers implement the Deref and Drop traits. The Deref
-trait allows an instance of the smart pointer struct to behave like a reference
-so that you can write your code to work with either references or smart
-pointers. The Drop trait allows you to customize the code that’s run when an
-instance of the smart pointer goes out of scope. In this chapter, we’ll discuss
-both of these traits and demonstrate why they’re important to smart pointers.
Given that the smart pointer pattern is a general design pattern used -frequently in Rust, this chapter won’t cover every existing smart pointer. Many -libraries have their own smart pointers, and you can even write your own. We’ll -cover the most common smart pointers in the standard library:
-Box<T>, for allocating values on the heapRc<T>, a reference counting type that enables multiple ownershipRef<T> and RefMut<T>, accessed through RefCell<T>, a type that enforces
-the borrowing rules at runtime instead of compile timeIn addition, we’ll cover the interior mutability pattern where an immutable -type exposes an API for mutating an interior value. We’ll also discuss -reference cycles: how they can leak memory and how to prevent them.
-Let’s dive in!
- -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
-Box<T> to Point to Data on the HeapThe most straightforward smart pointer is a box, whose type is written
-Box<T>. Boxes allow you to store data on the heap rather than the stack.
-What remains on the stack is the pointer to the heap data. Refer to Chapter 4
-to review the difference between the stack and the heap.
Boxes don’t have performance overhead, other than storing their data on the -heap instead of on the stack. But they don’t have many extra capabilities -either. You’ll use them most often in these situations:
-We’ll demonstrate the first situation in “Enabling Recursive Types with -Boxes”. In the second -case, transferring ownership of a large amount of data can take a long time -because the data is copied around on the stack. To improve performance in this -situation, we can store the large amount of data on the heap in a box. Then, -only the small amount of pointer data is copied around on the stack, while the -data it references stays in one place on the heap. The third case is known as a -trait object, and “Using Trait Objects to Abstract over Shared -Behavior” in Chapter 18 is devoted to that -topic. So, what you learn here you’ll apply again in that section!
- - -Before we discuss the heap storage use case for Box<T>, we’ll cover the
-syntax and how to interact with values stored within a Box<T>.
Listing 15-1 shows how to use a box to store an i32 value on the heap.
fn main() {
- let b = Box::new(5);
- println!("b = {b}");
-}
-i32 value on the heap using a boxWe define the variable b to have the value of a Box that points to the
-value 5, which is allocated on the heap. This program will print b = 5; in
-this case, we can access the data in the box similarly to how we would if this
-data were on the stack. Just like any owned value, when a box goes out of
-scope, as b does at the end of main, it will be deallocated. The
-deallocation happens both for the box (stored on the stack) and the data it
-points to (stored on the heap).
Putting a single value on the heap isn’t very useful, so you won’t use boxes by
-themselves in this way very often. Having values like a single i32 on the
-stack, where they’re stored by default, is more appropriate in the majority of
-situations. Let’s look at a case where boxes allow us to define types that we
-wouldn’t be allowed to define if we didn’t have boxes.
A value of a recursive type can have another value of the same type as part of -itself. Recursive types pose an issue because Rust needs to know at compile time -how much space a type takes up. However, the nesting of values of recursive -types could theoretically continue infinitely, so Rust can’t know how much space -the value needs. Because boxes have a known size, we can enable recursive types -by inserting a box in the recursive type definition.
-As an example of a recursive type, let’s explore the cons list. This is a data -type commonly found in functional programming languages. The cons list type -we’ll define is straightforward except for the recursion; therefore, the -concepts in the example we’ll work with will be useful anytime you get into -more complex situations involving recursive types.
- - -A cons list is a data structure that comes from the Lisp programming language
-and its dialects, is made up of nested pairs, and is the Lisp version of a
-linked list. Its name comes from the cons function (short for construct
-function) in Lisp that constructs a new pair from its two arguments. By
-calling cons on a pair consisting of a value and another pair, we can
-construct cons lists made up of recursive pairs.
For example, here’s a pseudocode representation of a cons list containing the
-list 1, 2, 3 with each pair in parentheses:
(1, (2, (3, Nil)))
-
-Each item in a cons list contains two elements: the value of the current item
-and of the next item. The last item in the list contains only a value called
-Nil without a next item. A cons list is produced by recursively calling the
-cons function. The canonical name to denote the base case of the recursion is
-Nil. Note that this is not the same as the “null” or “nil” concept discussed
-in Chapter 6, which is an invalid or absent value.
The cons list isn’t a commonly used data structure in Rust. Most of the time
-when you have a list of items in Rust, Vec<T> is a better choice to use.
-Other, more complex recursive data types are useful in various situations,
-but by starting with the cons list in this chapter, we can explore how boxes
-let us define a recursive data type without much distraction.
Listing 15-2 contains an enum definition for a cons list. Note that this code
-won’t compile yet, because the List type doesn’t have a known size, which
-we’ll demonstrate.
enum List {
- Cons(i32, List),
- Nil,
-}
-
-fn main() {}
-i32 valuesNote: We’re implementing a cons list that holds only i32 values for the
-purposes of this example. We could have implemented it using generics, as we
-discussed in Chapter 10, to define a cons list type that could store values of
-any type.
Using the List type to store the list 1, 2, 3 would look like the code in
-Listing 15-3.
enum List {
- Cons(i32, List),
- Nil,
-}
-
-// --snip--
-
-use crate::List::{Cons, Nil};
-
-fn main() {
- let list = Cons(1, Cons(2, Cons(3, Nil)));
-}
-List enum to store the list 1, 2, 3The first Cons value holds 1 and another List value. This List value is
-another Cons value that holds 2 and another List value. This List value
-is one more Cons value that holds 3 and a List value, which is finally
-Nil, the non-recursive variant that signals the end of the list.
If we try to compile the code in Listing 15-3, we get the error shown in -Listing 15-4.
-$ cargo run
- Compiling cons-list v0.1.0 (file:///projects/cons-list)
-error[E0072]: recursive type `List` has infinite size
- --> src/main.rs:1:1
- |
-1 | enum List {
- | ^^^^^^^^^
-2 | Cons(i32, List),
- | ---- recursive without indirection
- |
-help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
- |
-2 | Cons(i32, Box<List>),
- | ++++ +
-
-error[E0391]: cycle detected when computing when `List` needs drop
- --> src/main.rs:1:1
- |
-1 | enum List {
- | ^^^^^^^^^
- |
- = note: ...which immediately requires computing when `List` needs drop again
- = note: cycle used when computing whether `List` needs drop
- = note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information
-
-Some errors have detailed explanations: E0072, E0391.
-For more information about an error, try `rustc --explain E0072`.
-error: could not compile `cons-list` (bin "cons-list") due to 2 previous errors
-
-The error shows this type “has infinite size.” The reason is that we’ve defined
-List with a variant that is recursive: It holds another value of itself
-directly. As a result, Rust can’t figure out how much space it needs to store a
-List value. Let’s break down why we get this error. First, we’ll look at how
-Rust decides how much space it needs to store a value of a non-recursive type.
Recall the Message enum we defined in Listing 6-2 when we discussed enum
-definitions in Chapter 6:
enum Message {
- Quit,
- Move { x: i32, y: i32 },
- Write(String),
- ChangeColor(i32, i32, i32),
-}
-
-fn main() {}
-To determine how much space to allocate for a Message value, Rust goes
-through each of the variants to see which variant needs the most space. Rust
-sees that Message::Quit doesn’t need any space, Message::Move needs enough
-space to store two i32 values, and so forth. Because only one variant will be
-used, the most space a Message value will need is the space it would take to
-store the largest of its variants.
Contrast this with what happens when Rust tries to determine how much space a
-recursive type like the List enum in Listing 15-2 needs. The compiler starts
-by looking at the Cons variant, which holds a value of type i32 and a value
-of type List. Therefore, Cons needs an amount of space equal to the size of
-an i32 plus the size of a List. To figure out how much memory the List
-type needs, the compiler looks at the variants, starting with the Cons
-variant. The Cons variant holds a value of type i32 and a value of type
-List, and this process continues infinitely, as shown in Figure 15-1.
Figure 15-1: An infinite List consisting of infinite
-Cons variants
Because Rust can’t figure out how much space to allocate for recursively -defined types, the compiler gives an error with this helpful suggestion:
- -help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
- |
-2 | Cons(i32, Box<List>),
- | ++++ +
-
-In this suggestion, indirection means that instead of storing a value -directly, we should change the data structure to store the value indirectly by -storing a pointer to the value instead.
-Because a Box<T> is a pointer, Rust always knows how much space a Box<T>
-needs: A pointer’s size doesn’t change based on the amount of data it’s
-pointing to. This means we can put a Box<T> inside the Cons variant instead
-of another List value directly. The Box<T> will point to the next List
-value that will be on the heap rather than inside the Cons variant.
-Conceptually, we still have a list, created with lists holding other lists, but
-this implementation is now more like placing the items next to one another
-rather than inside one another.
We can change the definition of the List enum in Listing 15-2 and the usage
-of the List in Listing 15-3 to the code in Listing 15-5, which will compile.
enum List {
- Cons(i32, Box<List>),
- Nil,
-}
-
-use crate::List::{Cons, Nil};
-
-fn main() {
- let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
-}
-List that uses Box<T> in order to have a known sizeThe Cons variant needs the size of an i32 plus the space to store the box’s
-pointer data. The Nil variant stores no values, so it needs less space on the
-stack than the Cons variant. We now know that any List value will take up
-the size of an i32 plus the size of a box’s pointer data. By using a box,
-we’ve broken the infinite, recursive chain, so the compiler can figure out the
-size it needs to store a List value. Figure 15-2 shows what the Cons
-variant looks like now.
Figure 15-2: A List that is not infinitely sized,
-because Cons holds a Box
Boxes provide only the indirection and heap allocation; they don’t have any -other special capabilities, like those we’ll see with the other smart pointer -types. They also don’t have the performance overhead that these special -capabilities incur, so they can be useful in cases like the cons list where the -indirection is the only feature we need. We’ll look at more use cases for boxes -in Chapter 18.
-The Box<T> type is a smart pointer because it implements the Deref trait,
-which allows Box<T> values to be treated like references. When a Box<T>
-value goes out of scope, the heap data that the box is pointing to is cleaned
-up as well because of the Drop trait implementation. These two traits will be
-even more important to the functionality provided by the other smart pointer
-types we’ll discuss in the rest of this chapter. Let’s explore these two traits
-in more detail.
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
-Drop TraitThe second trait important to the smart pointer pattern is Drop, which lets
-you customize what happens when a value is about to go out of scope. You can
-provide an implementation for the Drop trait on any type, and that code can
-be used to release resources like files or network connections.
We’re introducing Drop in the context of smart pointers because the
-functionality of the Drop trait is almost always used when implementing a
-smart pointer. For example, when a Box<T> is dropped, it will deallocate the
-space on the heap that the box points to.
In some languages, for some types, the programmer must call code to free memory -or resources every time they finish using an instance of those types. Examples -include file handles, sockets, and locks. If the programmer forgets, the system -might become overloaded and crash. In Rust, you can specify that a particular -bit of code be run whenever a value goes out of scope, and the compiler will -insert this code automatically. As a result, you don’t need to be careful about -placing cleanup code everywhere in a program that an instance of a particular -type is finished with—you still won’t leak resources!
-You specify the code to run when a value goes out of scope by implementing the
-Drop trait. The Drop trait requires you to implement one method named
-drop that takes a mutable reference to self. To see when Rust calls drop,
-let’s implement drop with println! statements for now.
Listing 15-14 shows a CustomSmartPointer struct whose only custom
-functionality is that it will print Dropping CustomSmartPointer! when the
-instance goes out of scope, to show when Rust runs the drop method.
struct CustomSmartPointer {
- data: String,
-}
-
-impl Drop for CustomSmartPointer {
- fn drop(&mut self) {
- println!("Dropping CustomSmartPointer with data `{}`!", self.data);
- }
-}
-
-fn main() {
- let c = CustomSmartPointer {
- data: String::from("my stuff"),
- };
- let d = CustomSmartPointer {
- data: String::from("other stuff"),
- };
- println!("CustomSmartPointers created");
-}
-CustomSmartPointer struct that implements the Drop trait where we would put our cleanup codeThe Drop trait is included in the prelude, so we don’t need to bring it into
-scope. We implement the Drop trait on CustomSmartPointer and provide an
-implementation for the drop method that calls println!. The body of the
-drop method is where you would place any logic that you wanted to run when an
-instance of your type goes out of scope. We’re printing some text here to
-demonstrate visually when Rust will call drop.
In main, we create two instances of CustomSmartPointer and then print
-CustomSmartPointers created. At the end of main, our instances of
-CustomSmartPointer will go out of scope, and Rust will call the code we put
-in the drop method, printing our final message. Note that we didn’t need to
-call the drop method explicitly.
When we run this program, we’ll see the following output:
-$ cargo run
- Compiling drop-example v0.1.0 (file:///projects/drop-example)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.60s
- Running `target/debug/drop-example`
-CustomSmartPointers created
-Dropping CustomSmartPointer with data `other stuff`!
-Dropping CustomSmartPointer with data `my stuff`!
-
-Rust automatically called drop for us when our instances went out of scope,
-calling the code we specified. Variables are dropped in the reverse order of
-their creation, so d was dropped before c. This example’s purpose is to
-give you a visual guide to how the drop method works; usually you would
-specify the cleanup code that your type needs to run rather than a print
-message.
Unfortunately, it’s not straightforward to disable the automatic drop
-functionality. Disabling drop isn’t usually necessary; the whole point of the
-Drop trait is that it’s taken care of automatically. Occasionally, however,
-you might want to clean up a value early. One example is when using smart
-pointers that manage locks: You might want to force the drop method that
-releases the lock so that other code in the same scope can acquire the lock.
-Rust doesn’t let you call the Drop trait’s drop method manually; instead,
-you have to call the std::mem::drop function provided by the standard library
-if you want to force a value to be dropped before the end of its scope.
Trying to call the Drop trait’s drop method manually by modifying the
-main function from Listing 15-14 won’t work, as shown in Listing 15-15.
struct CustomSmartPointer {
- data: String,
-}
-
-impl Drop for CustomSmartPointer {
- fn drop(&mut self) {
- println!("Dropping CustomSmartPointer with data `{}`!", self.data);
- }
-}
-
-fn main() {
- let c = CustomSmartPointer {
- data: String::from("some data"),
- };
- println!("CustomSmartPointer created");
- c.drop();
- println!("CustomSmartPointer dropped before the end of main");
-}
-drop method from the Drop trait manually to clean up earlyWhen we try to compile this code, we’ll get this error:
-$ cargo run
- Compiling drop-example v0.1.0 (file:///projects/drop-example)
-error[E0040]: explicit use of destructor method
- --> src/main.rs:16:7
- |
-16 | c.drop();
- | ^^^^ explicit destructor calls not allowed
- |
-help: consider using `drop` function
- |
-16 - c.drop();
-16 + drop(c);
- |
-
-For more information about this error, try `rustc --explain E0040`.
-error: could not compile `drop-example` (bin "drop-example") due to 1 previous error
-
-This error message states that we’re not allowed to explicitly call drop. The
-error message uses the term destructor, which is the general programming term
-for a function that cleans up an instance. A destructor is analogous to a
-constructor, which creates an instance. The drop function in Rust is one
-particular destructor.
Rust doesn’t let us call drop explicitly, because Rust would still
-automatically call drop on the value at the end of main. This would cause a
-double free error because Rust would be trying to clean up the same value twice.
We can’t disable the automatic insertion of drop when a value goes out of
-scope, and we can’t call the drop method explicitly. So, if we need to force
-a value to be cleaned up early, we use the std::mem::drop function.
The std::mem::drop function is different from the drop method in the Drop
-trait. We call it by passing as an argument the value we want to force-drop.
-The function is in the prelude, so we can modify main in Listing 15-15 to
-call the drop function, as shown in Listing 15-16.
struct CustomSmartPointer {
- data: String,
-}
-
-impl Drop for CustomSmartPointer {
- fn drop(&mut self) {
- println!("Dropping CustomSmartPointer with data `{}`!", self.data);
- }
-}
-
-fn main() {
- let c = CustomSmartPointer {
- data: String::from("some data"),
- };
- println!("CustomSmartPointer created");
- drop(c);
- println!("CustomSmartPointer dropped before the end of main");
-}
-std::mem::drop to explicitly drop a value before it goes out of scopeRunning this code will print the following:
-$ cargo run
- Compiling drop-example v0.1.0 (file:///projects/drop-example)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
- Running `target/debug/drop-example`
-CustomSmartPointer created
-Dropping CustomSmartPointer with data `some data`!
-CustomSmartPointer dropped before the end of main
-
-The text Dropping CustomSmartPointer with data `some data`! is printed
-between the CustomSmartPointer created and CustomSmartPointer dropped before the end of main text, showing that the drop method code is called to drop
-c at that point.
You can use code specified in a Drop trait implementation in many ways to
-make cleanup convenient and safe: For instance, you could use it to create your
-own memory allocator! With the Drop trait and Rust’s ownership system, you
-don’t have to remember to clean up, because Rust does it automatically.
You also don’t have to worry about problems resulting from accidentally
-cleaning up values still in use: The ownership system that makes sure
-references are always valid also ensures that drop gets called only once when
-the value is no longer being used.
Now that we’ve examined Box<T> and some of the characteristics of smart
-pointers, let’s look at a few other smart pointers defined in the standard
-library.
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
-Rc<T>, the Reference-Counted Smart PointerIn the majority of cases, ownership is clear: You know exactly which variable -owns a given value. However, there are cases when a single value might have -multiple owners. For example, in graph data structures, multiple edges might -point to the same node, and that node is conceptually owned by all of the edges -that point to it. A node shouldn’t be cleaned up unless it doesn’t have any -edges pointing to it and so has no owners.
-You have to enable multiple ownership explicitly by using the Rust type
-Rc<T>, which is an abbreviation for reference counting. The Rc<T> type
-keeps track of the number of references to a value to determine whether or not
-the value is still in use. If there are zero references to a value, the value
-can be cleaned up without any references becoming invalid.
Imagine Rc<T> as a TV in a family room. When one person enters to watch TV,
-they turn it on. Others can come into the room and watch the TV. When the last
-person leaves the room, they turn off the TV because it’s no longer being used.
-If someone turns off the TV while others are still watching it, there would be
-an uproar from the remaining TV watchers!
We use the Rc<T> type when we want to allocate some data on the heap for
-multiple parts of our program to read and we can’t determine at compile time
-which part will finish using the data last. If we knew which part would finish
-last, we could just make that part the data’s owner, and the normal ownership
-rules enforced at compile time would take effect.
Note that Rc<T> is only for use in single-threaded scenarios. When we discuss
-concurrency in Chapter 16, we’ll cover how to do reference counting in
-multithreaded programs.
Let’s return to our cons list example in Listing 15-5. Recall that we defined
-it using Box<T>. This time, we’ll create two lists that both share ownership
-of a third list. Conceptually, this looks similar to Figure 15-3.
Figure 15-3: Two lists, b and c, sharing ownership of
-a third list, a
We’ll create list a that contains 5 and then 10. Then, we’ll make two
-more lists: b that starts with 3 and c that starts with 4. Both the b
-and c lists will then continue on to the first a list containing 5 and
-10. In other words, both lists will share the first list containing 5 and
-10.
Trying to implement this scenario using our definition of List with Box<T>
-won’t work, as shown in Listing 15-17.
enum List {
- Cons(i32, Box<List>),
- Nil,
-}
-
-use crate::List::{Cons, Nil};
-
-fn main() {
- let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
- let b = Cons(3, Box::new(a));
- let c = Cons(4, Box::new(a));
-}
-Box<T> that try to share ownership of a third listWhen we compile this code, we get this error:
-$ cargo run
- Compiling cons-list v0.1.0 (file:///projects/cons-list)
-error[E0382]: use of moved value: `a`
- --> src/main.rs:11:30
- |
- 9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
- | - move occurs because `a` has type `List`, which does not implement the `Copy` trait
-10 | let b = Cons(3, Box::new(a));
- | - value moved here
-11 | let c = Cons(4, Box::new(a));
- | ^ value used here after move
- |
-note: if `List` implemented `Clone`, you could clone the value
- --> src/main.rs:1:1
- |
- 1 | enum List {
- | ^^^^^^^^^ consider implementing `Clone` for this type
-...
-10 | let b = Cons(3, Box::new(a));
- | - you could clone this value
-
-For more information about this error, try `rustc --explain E0382`.
-error: could not compile `cons-list` (bin "cons-list") due to 1 previous error
-
-The Cons variants own the data they hold, so when we create the b list, a
-is moved into b and b owns a. Then, when we try to use a again when
-creating c, we’re not allowed to because a has been moved.
We could change the definition of Cons to hold references instead, but then
-we would have to specify lifetime parameters. By specifying lifetime
-parameters, we would be specifying that every element in the list will live at
-least as long as the entire list. This is the case for the elements and lists
-in Listing 15-17, but not in every scenario.
Instead, we’ll change our definition of List to use Rc<T> in place of
-Box<T>, as shown in Listing 15-18. Each Cons variant will now hold a value
-and an Rc<T> pointing to a List. When we create b, instead of taking
-ownership of a, we’ll clone the Rc<List> that a is holding, thereby
-increasing the number of references from one to two and letting a and b
-share ownership of the data in that Rc<List>. We’ll also clone a when
-creating c, increasing the number of references from two to three. Every time
-we call Rc::clone, the reference count to the data within the Rc<List> will
-increase, and the data won’t be cleaned up unless there are zero references to
-it.
enum List {
- Cons(i32, Rc<List>),
- Nil,
-}
-
-use crate::List::{Cons, Nil};
-use std::rc::Rc;
-
-fn main() {
- let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
- let b = Cons(3, Rc::clone(&a));
- let c = Cons(4, Rc::clone(&a));
-}
-List that uses Rc<T>We need to add a use statement to bring Rc<T> into scope because it’s not
-in the prelude. In main, we create the list holding 5 and 10 and store it
-in a new Rc<List> in a. Then, when we create b and c, we call the
-Rc::clone function and pass a reference to the Rc<List> in a as an
-argument.
We could have called a.clone() rather than Rc::clone(&a), but Rust’s
-convention is to use Rc::clone in this case. The implementation of
-Rc::clone doesn’t make a deep copy of all the data like most types’
-implementations of clone do. The call to Rc::clone only increments the
-reference count, which doesn’t take much time. Deep copies of data can take a
-lot of time. By using Rc::clone for reference counting, we can visually
-distinguish between the deep-copy kinds of clones and the kinds of clones that
-increase the reference count. When looking for performance problems in the
-code, we only need to consider the deep-copy clones and can disregard calls to
-Rc::clone.
Let’s change our working example in Listing 15-18 so that we can see the
-reference counts changing as we create and drop references to the Rc<List> in
-a.
In Listing 15-19, we’ll change main so that it has an inner scope around list
-c; then, we can see how the reference count changes when c goes out of
-scope.
enum List {
- Cons(i32, Rc<List>),
- Nil,
-}
-
-use crate::List::{Cons, Nil};
-use std::rc::Rc;
-
-// --snip--
-
-fn main() {
- let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
- println!("count after creating a = {}", Rc::strong_count(&a));
- let b = Cons(3, Rc::clone(&a));
- println!("count after creating b = {}", Rc::strong_count(&a));
- {
- let c = Cons(4, Rc::clone(&a));
- println!("count after creating c = {}", Rc::strong_count(&a));
- }
- println!("count after c goes out of scope = {}", Rc::strong_count(&a));
-}
-At each point in the program where the reference count changes, we print the
-reference count, which we get by calling the Rc::strong_count function. This
-function is named strong_count rather than count because the Rc<T> type
-also has a weak_count; we’ll see what weak_count is used for in “Preventing
-Reference Cycles Using Weak<T>”.
This code prints the following:
-$ cargo run
- Compiling cons-list v0.1.0 (file:///projects/cons-list)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.45s
- Running `target/debug/cons-list`
-count after creating a = 1
-count after creating b = 2
-count after creating c = 3
-count after c goes out of scope = 2
-
-We can see that the Rc<List> in a has an initial reference count of 1;
-then, each time we call clone, the count goes up by 1. When c goes out of
-scope, the count goes down by 1. We don’t have to call a function to decrease
-the reference count like we have to call Rc::clone to increase the reference
-count: The implementation of the Drop trait decreases the reference count
-automatically when an Rc<T> value goes out of scope.
What we can’t see in this example is that when b and then a go out of scope
-at the end of main, the count is 0, and the Rc<List> is cleaned up
-completely. Using Rc<T> allows a single value to have multiple owners, and
-the count ensures that the value remains valid as long as any of the owners
-still exist.
Via immutable references, Rc<T> allows you to share data between multiple
-parts of your program for reading only. If Rc<T> allowed you to have multiple
-mutable references too, you might violate one of the borrowing rules discussed
-in Chapter 4: Multiple mutable borrows to the same place can cause data races
-and inconsistencies. But being able to mutate data is very useful! In the next
-section, we’ll discuss the interior mutability pattern and the RefCell<T>
-type that you can use in conjunction with an Rc<T> to work with this
-immutability restriction.
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
-Handling concurrent programming safely and efficiently is another of Rust’s -major goals. Concurrent programming, in which different parts of a program -execute independently, and parallel programming, in which different parts of -a program execute at the same time, are becoming increasingly important as more -computers take advantage of their multiple processors. Historically, -programming in these contexts has been difficult and error-prone. Rust hopes to -change that.
-Initially, the Rust team thought that ensuring memory safety and preventing -concurrency problems were two separate challenges to be solved with different -methods. Over time, the team discovered that the ownership and type systems are -a powerful set of tools to help manage memory safety and concurrency -problems! By leveraging ownership and type checking, many concurrency errors -are compile-time errors in Rust rather than runtime errors. Therefore, rather -than making you spend lots of time trying to reproduce the exact circumstances -under which a runtime concurrency bug occurs, incorrect code will refuse to -compile and present an error explaining the problem. As a result, you can fix -your code while you’re working on it rather than potentially after it has been -shipped to production. We’ve nicknamed this aspect of Rust fearless -concurrency. Fearless concurrency allows you to write code that is free of -subtle bugs and is easy to refactor without introducing new bugs.
-Note: For simplicity’s sake, we’ll refer to many of the problems as -concurrent rather than being more precise by saying concurrent and/or -parallel. For this chapter, please mentally substitute concurrent and/or -parallel whenever we use concurrent. In the next chapter, where the -distinction matters more, we’ll be more specific.
-Many languages are dogmatic about the solutions they offer for handling -concurrent problems. For example, Erlang has elegant functionality for -message-passing concurrency but has only obscure ways to share state between -threads. Supporting only a subset of possible solutions is a reasonable -strategy for higher-level languages because a higher-level language promises -benefits from giving up some control to gain abstractions. However, lower-level -languages are expected to provide the solution with the best performance in any -given situation and have fewer abstractions over the hardware. Therefore, Rust -offers a variety of tools for modeling problems in whatever way is appropriate -for your situation and requirements.
-Here are the topics we’ll cover in this chapter:
-Sync and Send traits, which extend Rust’s concurrency guarantees to
-user-defined types as well as types provided by the standard libraryPress ← or → to navigate between chapters
-Press S or / to search in the book
-Press ? to show this help
-Press Esc to hide this help
-In most current operating systems, an executed program’s code is run in a -process, and the operating system will manage multiple processes at once. -Within a program, you can also have independent parts that run simultaneously. -The features that run these independent parts are called threads. For -example, a web server could have multiple threads so that it can respond to -more than one request at the same time.
-Splitting the computation in your program into multiple threads to run multiple -tasks at the same time can improve performance, but it also adds complexity. -Because threads can run simultaneously, there’s no inherent guarantee about the -order in which parts of your code on different threads will run. This can lead -to problems, such as:
-Rust attempts to mitigate the negative effects of using threads, but -programming in a multithreaded context still takes careful thought and requires -a code structure that is different from that in programs running in a single -thread.
-Programming languages implement threads in a few different ways, and many -operating systems provide an API the programming language can call for creating -new threads. The Rust standard library uses a 1:1 model of thread -implementation, whereby a program uses one operating system thread per one -language thread. There are crates that implement other models of threading that -make different trade-offs to the 1:1 model. (Rust’s async system, which we will -see in the next chapter, provides another approach to concurrency as well.)
-spawnTo create a new thread, we call the thread::spawn function and pass it a
-closure (we talked about closures in Chapter 13) containing the code we want to
-run in the new thread. The example in Listing 16-1 prints some text from a main
-thread and other text from a new thread.
use std::thread;
-use std::time::Duration;
-
-fn main() {
- thread::spawn(|| {
- for i in 1..10 {
- println!("hi number {i} from the spawned thread!");
- thread::sleep(Duration::from_millis(1));
- }
- });
-
- for i in 1..5 {
- println!("hi number {i} from the main thread!");
- thread::sleep(Duration::from_millis(1));
- }
-}
-Note that when the main thread of a Rust program completes, all spawned threads -are shut down, whether or not they have finished running. The output from this -program might be a little different every time, but it will look similar to the -following:
- -hi number 1 from the main thread!
-hi number 1 from the spawned thread!
-hi number 2 from the main thread!
-hi number 2 from the spawned thread!
-hi number 3 from the main thread!
-hi number 3 from the spawned thread!
-hi number 4 from the main thread!
-hi number 4 from the spawned thread!
-hi number 5 from the spawned thread!
-
-The calls to thread::sleep force a thread to stop its execution for a short
-duration, allowing a different thread to run. The threads will probably take
-turns, but that isn’t guaranteed: It depends on how your operating system
-schedules the threads. In this run, the main thread printed first, even though
-the print statement from the spawned thread appears first in the code. And even
-though we told the spawned thread to print until i is 9, it only got to 5
-before the main thread shut down.
If you run this code and only see output from the main thread, or don’t see any -overlap, try increasing the numbers in the ranges to create more opportunities -for the operating system to switch between the threads.
- - -The code in Listing 16-1 not only stops the spawned thread prematurely most of -the time due to the main thread ending, but because there is no guarantee on -the order in which threads run, we also can’t guarantee that the spawned thread -will get to run at all!
-We can fix the problem of the spawned thread not running or of it ending
-prematurely by saving the return value of thread::spawn in a variable. The
-return type of thread::spawn is JoinHandle<T>. A JoinHandle<T> is an
-owned value that, when we call the join method on it, will wait for its
-thread to finish. Listing 16-2 shows how to use the JoinHandle<T> of the
-thread we created in Listing 16-1 and how to call join to make sure the
-spawned thread finishes before main exits.
use std::thread;
-use std::time::Duration;
-
-fn main() {
- let handle = thread::spawn(|| {
- for i in 1..10 {
- println!("hi number {i} from the spawned thread!");
- thread::sleep(Duration::from_millis(1));
- }
- });
-
- for i in 1..5 {
- println!("hi number {i} from the main thread!");
- thread::sleep(Duration::from_millis(1));
- }
-
- handle.join().unwrap();
-}
-JoinHandle<T> from thread::spawn to guarantee the thread is run to completionCalling join on the handle blocks the thread currently running until the
-thread represented by the handle terminates. Blocking a thread means that
-thread is prevented from performing work or exiting. Because we’ve put the call
-to join after the main thread’s for loop, running Listing 16-2 should
-produce output similar to this:
hi number 1 from the main thread!
-hi number 2 from the main thread!
-hi number 1 from the spawned thread!
-hi number 3 from the main thread!
-hi number 2 from the spawned thread!
-hi number 4 from the main thread!
-hi number 3 from the spawned thread!
-hi number 4 from the spawned thread!
-hi number 5 from the spawned thread!
-hi number 6 from the spawned thread!
-hi number 7 from the spawned thread!
-hi number 8 from the spawned thread!
-hi number 9 from the spawned thread!
-
-The two threads continue alternating, but the main thread waits because of the
-call to handle.join() and does not end until the spawned thread is finished.
But let’s see what happens when we instead move handle.join() before the
-for loop in main, like this:
use std::thread;
-use std::time::Duration;
-
-fn main() {
- let handle = thread::spawn(|| {
- for i in 1..10 {
- println!("hi number {i} from the spawned thread!");
- thread::sleep(Duration::from_millis(1));
- }
- });
-
- handle.join().unwrap();
-
- for i in 1..5 {
- println!("hi number {i} from the main thread!");
- thread::sleep(Duration::from_millis(1));
- }
-}
-The main thread will wait for the spawned thread to finish and then run its
-for loop, so the output won’t be interleaved anymore, as shown here:
hi number 1 from the spawned thread!
-hi number 2 from the spawned thread!
-hi number 3 from the spawned thread!
-hi number 4 from the spawned thread!
-hi number 5 from the spawned thread!
-hi number 6 from the spawned thread!
-hi number 7 from the spawned thread!
-hi number 8 from the spawned thread!
-hi number 9 from the spawned thread!
-hi number 1 from the main thread!
-hi number 2 from the main thread!
-hi number 3 from the main thread!
-hi number 4 from the main thread!
-
-Small details, such as where join is called, can affect whether or not your
-threads run at the same time.
move Closures with ThreadsWe’ll often use the move keyword with closures passed to thread::spawn
-because the closure will then take ownership of the values it uses from the
-environment, thus transferring ownership of those values from one thread to
-another. In “Capturing References or Moving Ownership” in Chapter 13, we discussed move in the context of closures. Now we’ll
-concentrate more on the interaction between move and thread::spawn.
Notice in Listing 16-1 that the closure we pass to thread::spawn takes no
-arguments: We’re not using any data from the main thread in the spawned
-thread’s code. To use data from the main thread in the spawned thread, the
-spawned thread’s closure must capture the values it needs. Listing 16-3 shows
-an attempt to create a vector in the main thread and use it in the spawned
-thread. However, this won’t work yet, as you’ll see in a moment.
use std::thread;
-
-fn main() {
- let v = vec![1, 2, 3];
-
- let handle = thread::spawn(|| {
- println!("Here's a vector: {v:?}");
- });
-
- handle.join().unwrap();
-}
-The closure uses v, so it will capture v and make it part of the closure’s
-environment. Because thread::spawn runs this closure in a new thread, we
-should be able to access v inside that new thread. But when we compile this
-example, we get the following error:
$ cargo run
- Compiling threads v0.1.0 (file:///projects/threads)
-error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
- --> src/main.rs:6:32
- |
-6 | let handle = thread::spawn(|| {
- | ^^ may outlive borrowed value `v`
-7 | println!("Here's a vector: {v:?}");
- | - `v` is borrowed here
- |
-note: function requires argument type to outlive `'static`
- --> src/main.rs:6:18
- |
-6 | let handle = thread::spawn(|| {
- | __________________^
-7 | | println!("Here's a vector: {v:?}");
-8 | | });
- | |______^
-help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
- |
-6 | let handle = thread::spawn(move || {
- | ++++
-
-For more information about this error, try `rustc --explain E0373`.
-error: could not compile `threads` (bin "threads") due to 1 previous error
-
-Rust infers how to capture v, and because println! only needs a reference
-to v, the closure tries to borrow v. However, there’s a problem: Rust can’t
-tell how long the spawned thread will run, so it doesn’t know whether the
-reference to v will always be valid.
Listing 16-4 provides a scenario that’s more likely to have a reference to v
-that won’t be valid.
use std::thread;
-
-fn main() {
- let v = vec![1, 2, 3];
-
- let handle = thread::spawn(|| {
- println!("Here's a vector: {v:?}");
- });
-
- drop(v); // oh no!
-
- handle.join().unwrap();
-}
-v from a main thread that drops vIf Rust allowed us to run this code, there’s a possibility that the spawned
-thread would be immediately put in the background without running at all. The
-spawned thread has a reference to v inside, but the main thread immediately
-drops v, using the drop function we discussed in Chapter 15. Then, when the
-spawned thread starts to execute, v is no longer valid, so a reference to it
-is also invalid. Oh no!
To fix the compiler error in Listing 16-3, we can use the error message’s -advice:
- -help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
- |
-6 | let handle = thread::spawn(move || {
- | ++++
-
-By adding the move keyword before the closure, we force the closure to take
-ownership of the values it’s using rather than allowing Rust to infer that it
-should borrow the values. The modification to Listing 16-3 shown in Listing
-16-5 will compile and run as we intend.
use std::thread;
-
-fn main() {
- let v = vec![1, 2, 3];
-
- let handle = thread::spawn(move || {
- println!("Here's a vector: {v:?}");
- });
-
- handle.join().unwrap();
-}
-move keyword to force a closure to take ownership of the values it usesWe might be tempted to try the same thing to fix the code in Listing 16-4 where
-the main thread called drop by using a move closure. However, this fix will
-not work because what Listing 16-4 is trying to do is disallowed for a
-different reason. If we added move to the closure, we would move v into the
-closure’s environment, and we could no longer call drop on it in the main
-thread. We would get this compiler error instead:
$ cargo run
- Compiling threads v0.1.0 (file:///projects/threads)
-error[E0382]: use of moved value: `v`
- --> src/main.rs:10:10
- |
- 4 | let v = vec![1, 2, 3];
- | - move occurs because `v` has type `Vec<i32>`, which does not implement the `Copy` trait
- 5 |
- 6 | let handle = thread::spawn(move || {
- | ------- value moved into closure here
- 7 | println!("Here's a vector: {v:?}");
- | - variable moved due to use in closure
-...
-10 | drop(v); // oh no!
- | ^ value used here after move
- |
-help: consider cloning the value before moving it into the closure
- |
- 6 ~ let value = v.clone();
- 7 ~ let handle = thread::spawn(move || {
- 8 ~ println!("Here's a vector: {value:?}");
- |
-
-For more information about this error, try `rustc --explain E0382`.
-error: could not compile `threads` (bin "threads") due to 1 previous error
-
-Rust’s ownership rules have saved us again! We got an error from the code in
-Listing 16-3 because Rust was being conservative and only borrowing v for the
-thread, which meant the main thread could theoretically invalidate the spawned
-thread’s reference. By telling Rust to move ownership of v to the spawned
-thread, we’re guaranteeing to Rust that the main thread won’t use v anymore.
-If we change Listing 16-4 in the same way, we’re then violating the ownership
-rules when we try to use v in the main thread. The move keyword overrides
-Rust’s conservative default of borrowing; it doesn’t let us violate the
-ownership rules.
Now that we’ve covered what threads are and the methods supplied by the thread -API, let’s look at some situations in which we can use threads.
- -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
-One increasingly popular approach to ensuring safe concurrency is message -passing, where threads or actors communicate by sending each other messages -containing data. Here’s the idea in a slogan from the Go language documentation: -“Do not communicate by sharing memory; instead, share memory by communicating.”
-To accomplish message-sending concurrency, Rust’s standard library provides an -implementation of channels. A channel is a general programming concept by -which data is sent from one thread to another.
-You can imagine a channel in programming as being like a directional channel of -water, such as a stream or a river. If you put something like a rubber duck -into a river, it will travel downstream to the end of the waterway.
-A channel has two halves: a transmitter and a receiver. The transmitter half is -the upstream location where you put the rubber duck into the river, and the -receiver half is where the rubber duck ends up downstream. One part of your -code calls methods on the transmitter with the data you want to send, and -another part checks the receiving end for arriving messages. A channel is said -to be closed if either the transmitter or receiver half is dropped.
-Here, we’ll work up to a program that has one thread to generate values and -send them down a channel, and another thread that will receive the values and -print them out. We’ll be sending simple values between threads using a channel -to illustrate the feature. Once you’re familiar with the technique, you could -use channels for any threads that need to communicate with each other, such as -a chat system or a system where many threads perform parts of a calculation and -send the parts to one thread that aggregates the results.
-First, in Listing 16-6, we’ll create a channel but not do anything with it. -Note that this won’t compile yet because Rust can’t tell what type of values we -want to send over the channel.
-use std::sync::mpsc;
-
-fn main() {
- let (tx, rx) = mpsc::channel();
-}
-tx and rxWe create a new channel using the mpsc::channel function; mpsc stands for
-multiple producer, single consumer. In short, the way Rust’s standard library
-implements channels means a channel can have multiple sending ends that
-produce values but only one receiving end that consumes those values. Imagine
-multiple streams flowing together into one big river: Everything sent down any
-of the streams will end up in one river at the end. We’ll start with a single
-producer for now, but we’ll add multiple producers when we get this example
-working.
The mpsc::channel function returns a tuple, the first element of which is the
-sending end—the transmitter—and the second element of which is the receiving
-end—the receiver. The abbreviations tx and rx are traditionally used in
-many fields for transmitter and receiver, respectively, so we name our
-variables as such to indicate each end. We’re using a let statement with a
-pattern that destructures the tuples; we’ll discuss the use of patterns in
-let statements and destructuring in Chapter 19. For now, know that using a
-let statement in this way is a convenient approach to extract the pieces of
-the tuple returned by mpsc::channel.
Let’s move the transmitting end into a spawned thread and have it send one -string so that the spawned thread is communicating with the main thread, as -shown in Listing 16-7. This is like putting a rubber duck in the river upstream -or sending a chat message from one thread to another.
-use std::sync::mpsc;
-use std::thread;
-
-fn main() {
- let (tx, rx) = mpsc::channel();
-
- thread::spawn(move || {
- let val = String::from("hi");
- tx.send(val).unwrap();
- });
-}
-tx to a spawned thread and sending "hi"Again, we’re using thread::spawn to create a new thread and then using move
-to move tx into the closure so that the spawned thread owns tx. The spawned
-thread needs to own the transmitter to be able to send messages through the
-channel.
The transmitter has a send method that takes the value we want to send. The
-send method returns a Result<T, E> type, so if the receiver has already
-been dropped and there’s nowhere to send a value, the send operation will
-return an error. In this example, we’re calling unwrap to panic in case of an
-error. But in a real application, we would handle it properly: Return to
-Chapter 9 to review strategies for proper error handling.
In Listing 16-8, we’ll get the value from the receiver in the main thread. This -is like retrieving the rubber duck from the water at the end of the river or -receiving a chat message.
-use std::sync::mpsc;
-use std::thread;
-
-fn main() {
- let (tx, rx) = mpsc::channel();
-
- thread::spawn(move || {
- let val = String::from("hi");
- tx.send(val).unwrap();
- });
-
- let received = rx.recv().unwrap();
- println!("Got: {received}");
-}
-"hi" in the main thread and printing itThe receiver has two useful methods: recv and try_recv. We’re using recv,
-short for receive, which will block the main thread’s execution and wait
-until a value is sent down the channel. Once a value is sent, recv will
-return it in a Result<T, E>. When the transmitter closes, recv will return
-an error to signal that no more values will be coming.
The try_recv method doesn’t block, but will instead return a Result<T, E>
-immediately: an Ok value holding a message if one is available and an Err
-value if there aren’t any messages this time. Using try_recv is useful if
-this thread has other work to do while waiting for messages: We could write a
-loop that calls try_recv every so often, handles a message if one is
-available, and otherwise does other work for a little while until checking
-again.
We’ve used recv in this example for simplicity; we don’t have any other work
-for the main thread to do other than wait for messages, so blocking the main
-thread is appropriate.
When we run the code in Listing 16-8, we’ll see the value printed from the main -thread:
- -Got: hi
-
-Perfect!
- - -The ownership rules play a vital role in message sending because they help you
-write safe, concurrent code. Preventing errors in concurrent programming is the
-advantage of thinking about ownership throughout your Rust programs. Let’s do
-an experiment to show how channels and ownership work together to prevent
-problems: We’ll try to use a val value in the spawned thread after we’ve
-sent it down the channel. Try compiling the code in Listing 16-9 to see why
-this code isn’t allowed.
use std::sync::mpsc;
-use std::thread;
-
-fn main() {
- let (tx, rx) = mpsc::channel();
-
- thread::spawn(move || {
- let val = String::from("hi");
- tx.send(val).unwrap();
- println!("val is {val}");
- });
-
- let received = rx.recv().unwrap();
- println!("Got: {received}");
-}
-val after we’ve sent it down the channelHere, we try to print val after we’ve sent it down the channel via tx.send.
-Allowing this would be a bad idea: Once the value has been sent to another
-thread, that thread could modify or drop it before we try to use the value
-again. Potentially, the other thread’s modifications could cause errors or
-unexpected results due to inconsistent or nonexistent data. However, Rust gives
-us an error if we try to compile the code in Listing 16-9:
$ cargo run
- Compiling message-passing v0.1.0 (file:///projects/message-passing)
-error[E0382]: borrow of moved value: `val`
- --> src/main.rs:10:27
- |
- 8 | let val = String::from("hi");
- | --- move occurs because `val` has type `String`, which does not implement the `Copy` trait
- 9 | tx.send(val).unwrap();
- | --- value moved here
-10 | println!("val is {val}");
- | ^^^ value borrowed here after move
- |
- = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
-
-For more information about this error, try `rustc --explain E0382`.
-error: could not compile `message-passing` (bin "message-passing") due to 1 previous error
-
-Our concurrency mistake has caused a compile-time error. The send function
-takes ownership of its parameter, and when the value is moved the receiver
-takes ownership of it. This stops us from accidentally using the value again
-after sending it; the ownership system checks that everything is okay.
The code in Listing 16-8 compiled and ran, but it didn’t clearly show us that -two separate threads were talking to each other over the channel.
-In Listing 16-10, we’ve made some modifications that will prove the code in -Listing 16-8 is running concurrently: The spawned thread will now send multiple -messages and pause for a second between each message.
-use std::sync::mpsc;
-use std::thread;
-use std::time::Duration;
-
-fn main() {
- let (tx, rx) = mpsc::channel();
-
- thread::spawn(move || {
- let vals = vec![
- String::from("hi"),
- String::from("from"),
- String::from("the"),
- String::from("thread"),
- ];
-
- for val in vals {
- tx.send(val).unwrap();
- thread::sleep(Duration::from_secs(1));
- }
- });
-
- for received in rx {
- println!("Got: {received}");
- }
-}
-This time, the spawned thread has a vector of strings that we want to send to
-the main thread. We iterate over them, sending each individually, and pause
-between each by calling the thread::sleep function with a Duration value of
-one second.
In the main thread, we’re not calling the recv function explicitly anymore:
-Instead, we’re treating rx as an iterator. For each value received, we’re
-printing it. When the channel is closed, iteration will end.
When running the code in Listing 16-10, you should see the following output -with a one-second pause in between each line:
- -Got: hi
-Got: from
-Got: the
-Got: thread
-
-Because we don’t have any code that pauses or delays in the for loop in the
-main thread, we can tell that the main thread is waiting to receive values from
-the spawned thread.
Earlier we mentioned that mpsc was an acronym for multiple producer, single
-consumer. Let’s put mpsc to use and expand the code in Listing 16-10 to
-create multiple threads that all send values to the same receiver. We can do so
-by cloning the transmitter, as shown in Listing 16-11.
use std::sync::mpsc;
-use std::thread;
-use std::time::Duration;
-
-fn main() {
- // --snip--
-
- let (tx, rx) = mpsc::channel();
-
- let tx1 = tx.clone();
- thread::spawn(move || {
- let vals = vec![
- String::from("hi"),
- String::from("from"),
- String::from("the"),
- String::from("thread"),
- ];
-
- for val in vals {
- tx1.send(val).unwrap();
- thread::sleep(Duration::from_secs(1));
- }
- });
-
- thread::spawn(move || {
- let vals = vec![
- String::from("more"),
- String::from("messages"),
- String::from("for"),
- String::from("you"),
- ];
-
- for val in vals {
- tx.send(val).unwrap();
- thread::sleep(Duration::from_secs(1));
- }
- });
-
- for received in rx {
- println!("Got: {received}");
- }
-
- // --snip--
-}
-This time, before we create the first spawned thread, we call clone on the
-transmitter. This will give us a new transmitter we can pass to the first
-spawned thread. We pass the original transmitter to a second spawned thread.
-This gives us two threads, each sending different messages to the one receiver.
When you run the code, your output should look something like this:
- -Got: hi
-Got: more
-Got: from
-Got: messages
-Got: for
-Got: the
-Got: thread
-Got: you
-
-You might see the values in another order, depending on your system. This is
-what makes concurrency interesting as well as difficult. If you experiment with
-thread::sleep, giving it various values in the different threads, each run
-will be more nondeterministic and create different output each time.
Now that we’ve looked at how channels work, let’s look at a different method of -concurrency.
- -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
-Message passing is a fine way to handle concurrency, but it’s not the only way. -Another method would be for multiple threads to access the same shared data. -Consider this part of the slogan from the Go language documentation again: “Do -not communicate by sharing memory.”
-What would communicating by sharing memory look like? In addition, why would -message-passing enthusiasts caution not to use memory sharing?
-In a way, channels in any programming language are similar to single ownership -because once you transfer a value down a channel, you should no longer use that -value. Shared-memory concurrency is like multiple ownership: Multiple threads -can access the same memory location at the same time. As you saw in Chapter 15, -where smart pointers made multiple ownership possible, multiple ownership can -add complexity because these different owners need managing. Rust’s type system -and ownership rules greatly assist in getting this management correct. For an -example, let’s look at mutexes, one of the more common concurrency primitives -for shared memory.
- - -Mutex is an abbreviation for mutual exclusion, as in a mutex allows only -one thread to access some data at any given time. To access the data in a -mutex, a thread must first signal that it wants access by asking to acquire the -mutex’s lock. The lock is a data structure that is part of the mutex that -keeps track of who currently has exclusive access to the data. Therefore, the -mutex is described as guarding the data it holds via the locking system.
-Mutexes have a reputation for being difficult to use because you have to -remember two rules:
-For a real-world metaphor for a mutex, imagine a panel discussion at a -conference with only one microphone. Before a panelist can speak, they have to -ask or signal that they want to use the microphone. When they get the -microphone, they can talk for as long as they want to and then hand the -microphone to the next panelist who requests to speak. If a panelist forgets to -hand the microphone off when they’re finished with it, no one else is able to -speak. If management of the shared microphone goes wrong, the panel won’t work -as planned!
-Management of mutexes can be incredibly tricky to get right, which is why so -many people are enthusiastic about channels. However, thanks to Rust’s type -system and ownership rules, you can’t get locking and unlocking wrong.
-Mutex<T>As an example of how to use a mutex, let’s start by using a mutex in a -single-threaded context, as shown in Listing 16-12.
-use std::sync::Mutex;
-
-fn main() {
- let m = Mutex::new(5);
-
- {
- let mut num = m.lock().unwrap();
- *num = 6;
- }
-
- println!("m = {m:?}");
-}
-Mutex<T> in a single-threaded context for simplicityAs with many types, we create a Mutex<T> using the associated function new.
-To access the data inside the mutex, we use the lock method to acquire the
-lock. This call will block the current thread so that it can’t do any work
-until it’s our turn to have the lock.
The call to lock would fail if another thread holding the lock panicked. In
-that case, no one would ever be able to get the lock, so we’ve chosen to
-unwrap and have this thread panic if we’re in that situation.
After we’ve acquired the lock, we can treat the return value, named num in
-this case, as a mutable reference to the data inside. The type system ensures
-that we acquire a lock before using the value in m. The type of m is
-Mutex<i32>, not i32, so we must call lock to be able to use the i32
-value. We can’t forget; the type system won’t let us access the inner i32
-otherwise.
The call to lock returns a type called MutexGuard, wrapped in a
-LockResult that we handled with the call to unwrap. The MutexGuard type
-implements Deref to point at our inner data; the type also has a Drop
-implementation that releases the lock automatically when a MutexGuard goes
-out of scope, which happens at the end of the inner scope. As a result, we
-don’t risk forgetting to release the lock and blocking the mutex from being
-used by other threads because the lock release happens automatically.
After dropping the lock, we can print the mutex value and see that we were able
-to change the inner i32 to 6.
Mutex<T>Now let’s try to share a value between multiple threads using Mutex<T>. We’ll
-spin up 10 threads and have them each increment a counter value by 1, so the
-counter goes from 0 to 10. The example in Listing 16-13 will have a compiler
-error, and we’ll use that error to learn more about using Mutex<T> and how
-Rust helps us use it correctly.
use std::sync::Mutex;
-use std::thread;
-
-fn main() {
- let counter = Mutex::new(0);
- let mut handles = vec![];
-
- for _ in 0..10 {
- let handle = thread::spawn(move || {
- let mut num = counter.lock().unwrap();
-
- *num += 1;
- });
- handles.push(handle);
- }
-
- for handle in handles {
- handle.join().unwrap();
- }
-
- println!("Result: {}", *counter.lock().unwrap());
-}
-Mutex<T>We create a counter variable to hold an i32 inside a Mutex<T>, as we did
-in Listing 16-12. Next, we create 10 threads by iterating over a range of
-numbers. We use thread::spawn and give all the threads the same closure: one
-that moves the counter into the thread, acquires a lock on the Mutex<T> by
-calling the lock method, and then adds 1 to the value in the mutex. When a
-thread finishes running its closure, num will go out of scope and release the
-lock so that another thread can acquire it.
In the main thread, we collect all the join handles. Then, as we did in Listing
-16-2, we call join on each handle to make sure all the threads finish. At
-that point, the main thread will acquire the lock and print the result of this
-program.
We hinted that this example wouldn’t compile. Now let’s find out why!
-$ cargo run
- Compiling shared-state v0.1.0 (file:///projects/shared-state)
-error[E0382]: borrow of moved value: `counter`
- --> src/main.rs:21:29
- |
- 5 | let counter = Mutex::new(0);
- | ------- move occurs because `counter` has type `std::sync::Mutex<i32>`, which does not implement the `Copy` trait
-...
- 8 | for _ in 0..10 {
- | -------------- inside of this loop
- 9 | let handle = thread::spawn(move || {
- | ------- value moved into closure here, in previous iteration of loop
-...
-21 | println!("Result: {}", *counter.lock().unwrap());
- | ^^^^^^^ value borrowed here after move
- |
-help: consider moving the expression out of the loop so it is only moved once
- |
- 8 ~ let mut value = counter.lock();
- 9 ~ for _ in 0..10 {
-10 | let handle = thread::spawn(move || {
-11 ~ let mut num = value.unwrap();
- |
-
-For more information about this error, try `rustc --explain E0382`.
-error: could not compile `shared-state` (bin "shared-state") due to 1 previous error
-
-The error message states that the counter value was moved in the previous
-iteration of the loop. Rust is telling us that we can’t move the ownership of
-lock counter into multiple threads. Let’s fix the compiler error with the
-multiple-ownership method we discussed in Chapter 15.
In Chapter 15, we gave a value to multiple owners by using the smart pointer
-Rc<T> to create a reference-counted value. Let’s do the same here and see
-what happens. We’ll wrap the Mutex<T> in Rc<T> in Listing 16-14 and clone
-the Rc<T> before moving ownership to the thread.
use std::rc::Rc;
-use std::sync::Mutex;
-use std::thread;
-
-fn main() {
- let counter = Rc::new(Mutex::new(0));
- let mut handles = vec![];
-
- for _ in 0..10 {
- let counter = Rc::clone(&counter);
- let handle = thread::spawn(move || {
- let mut num = counter.lock().unwrap();
-
- *num += 1;
- });
- handles.push(handle);
- }
-
- for handle in handles {
- handle.join().unwrap();
- }
-
- println!("Result: {}", *counter.lock().unwrap());
-}
-Rc<T> to allow multiple threads to own the Mutex<T>Once again, we compile and get… different errors! The compiler is teaching us -a lot:
-$ cargo run
- Compiling shared-state v0.1.0 (file:///projects/shared-state)
-error[E0277]: `Rc<std::sync::Mutex<i32>>` cannot be sent between threads safely
- --> src/main.rs:11:36
- |
-11 | let handle = thread::spawn(move || {
- | ------------- ^------
- | | |
- | ______________________|_____________within this `{closure@src/main.rs:11:36: 11:43}`
- | | |
- | | required by a bound introduced by this call
-12 | | let mut num = counter.lock().unwrap();
-13 | |
-14 | | *num += 1;
-15 | | });
- | |_________^ `Rc<std::sync::Mutex<i32>>` cannot be sent between threads safely
- |
- = help: within `{closure@src/main.rs:11:36: 11:43}`, the trait `Send` is not implemented for `Rc<std::sync::Mutex<i32>>`
-note: required because it's used within this closure
- --> src/main.rs:11:36
- |
-11 | let handle = thread::spawn(move || {
- | ^^^^^^^
-note: required by a bound in `spawn`
- --> /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/std/src/thread/mod.rs:723:1
-
-For more information about this error, try `rustc --explain E0277`.
-error: could not compile `shared-state` (bin "shared-state") due to 1 previous error
-
-Wow, that error message is very wordy! Here’s the important part to focus on:
-`Rc<Mutex<i32>>` cannot be sent between threads safely. The compiler is
-also telling us the reason why: the trait `Send` is not implemented for `Rc<Mutex<i32>>`. We’ll talk about Send in the next section: It’s one of
-the traits that ensures that the types we use with threads are meant for use in
-concurrent situations.
Unfortunately, Rc<T> is not safe to share across threads. When Rc<T>
-manages the reference count, it adds to the count for each call to clone and
-subtracts from the count when each clone is dropped. But it doesn’t use any
-concurrency primitives to make sure that changes to the count can’t be
-interrupted by another thread. This could lead to wrong counts—subtle bugs that
-could in turn lead to memory leaks or a value being dropped before we’re done
-with it. What we need is a type that is exactly like Rc<T>, but that makes
-changes to the reference count in a thread-safe way.
Arc<T>Fortunately, Arc<T> is a type like Rc<T> that is safe to use in
-concurrent situations. The a stands for atomic, meaning it’s an atomically
-reference-counted type. Atomics are an additional kind of concurrency
-primitive that we won’t cover in detail here: See the standard library
-documentation for std::sync::atomic for more
-details. At this point, you just need to know that atomics work like primitive
-types but are safe to share across threads.
You might then wonder why all primitive types aren’t atomic and why standard
-library types aren’t implemented to use Arc<T> by default. The reason is that
-thread safety comes with a performance penalty that you only want to pay when
-you really need to. If you’re just performing operations on values within a
-single thread, your code can run faster if it doesn’t have to enforce the
-guarantees atomics provide.
Let’s return to our example: Arc<T> and Rc<T> have the same API, so we fix
-our program by changing the use line, the call to new, and the call to
-clone. The code in Listing 16-15 will finally compile and run.
use std::sync::{Arc, Mutex};
-use std::thread;
-
-fn main() {
- let counter = Arc::new(Mutex::new(0));
- let mut handles = vec![];
-
- for _ in 0..10 {
- let counter = Arc::clone(&counter);
- let handle = thread::spawn(move || {
- let mut num = counter.lock().unwrap();
-
- *num += 1;
- });
- handles.push(handle);
- }
-
- for handle in handles {
- handle.join().unwrap();
- }
-
- println!("Result: {}", *counter.lock().unwrap());
-}
-Arc<T> to wrap the Mutex<T> to be able to share ownership across multiple threadsThis code will print the following:
- -Result: 10
-
-We did it! We counted from 0 to 10, which may not seem very impressive, but it
-did teach us a lot about Mutex<T> and thread safety. You could also use this
-program’s structure to do more complicated operations than just incrementing a
-counter. Using this strategy, you can divide a calculation into independent
-parts, split those parts across threads, and then use a Mutex<T> to have each
-thread update the final result with its part.
Note that if you are doing simple numerical operations, there are types simpler
-than Mutex<T> types provided by the std::sync::atomic module of the
-standard library. These types provide safe, concurrent,
-atomic access to primitive types. We chose to use Mutex<T> with a primitive
-type for this example so that we could concentrate on how Mutex<T> works.
RefCell<T>/Rc<T> and Mutex<T>/Arc<T>You might have noticed that counter is immutable but that we could get a
-mutable reference to the value inside it; this means Mutex<T> provides
-interior mutability, as the Cell family does. In the same way we used
-RefCell<T> in Chapter 15 to allow us to mutate contents inside an Rc<T>, we
-use Mutex<T> to mutate contents inside an Arc<T>.
Another detail to note is that Rust can’t protect you from all kinds of logic
-errors when you use Mutex<T>. Recall from Chapter 15 that using Rc<T> came
-with the risk of creating reference cycles, where two Rc<T> values refer to
-each other, causing memory leaks. Similarly, Mutex<T> comes with the risk of
-creating deadlocks. These occur when an operation needs to lock two resources
-and two threads have each acquired one of the locks, causing them to wait for
-each other forever. If you’re interested in deadlocks, try creating a Rust
-program that has a deadlock; then, research deadlock mitigation strategies for
-mutexes in any language and have a go at implementing them in Rust. The
-standard library API documentation for Mutex<T> and MutexGuard offers
-useful information.
We’ll round out this chapter by talking about the Send and Sync traits and
-how we can use them with custom types.
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
-Send and SyncInterestingly, almost every concurrency feature we’ve talked about so far in -this chapter has been part of the standard library, not the language. Your -options for handling concurrency are not limited to the language or the -standard library; you can write your own concurrency features or use those -written by others.
-However, among the key concurrency concepts that are embedded in the language
-rather than the standard library are the std::marker traits Send and Sync.
The Send marker trait indicates that ownership of values of the type
-implementing Send can be transferred between threads. Almost every Rust type
-implements Send, but there are some exceptions, including Rc<T>: This
-cannot implement Send because if you cloned an Rc<T> value and tried to
-transfer ownership of the clone to another thread, both threads might update
-the reference count at the same time. For this reason, Rc<T> is implemented
-for use in single-threaded situations where you don’t want to pay the
-thread-safe performance penalty.
Therefore, Rust’s type system and trait bounds ensure that you can never
-accidentally send an Rc<T> value across threads unsafely. When we tried to do
-this in Listing 16-14, we got the error the trait `Send` is not implemented for `Rc<Mutex<i32>>`. When we switched to Arc<T>, which does implement
-Send, the code compiled.
Any type composed entirely of Send types is automatically marked as Send as
-well. Almost all primitive types are Send, aside from raw pointers, which
-we’ll discuss in Chapter 20.
The Sync marker trait indicates that it is safe for the type implementing
-Sync to be referenced from multiple threads. In other words, any type T
-implements Sync if &T (an immutable reference to T) implements Send,
-meaning the reference can be sent safely to another thread. Similar to Send,
-primitive types all implement Sync, and types composed entirely of types that
-implement Sync also implement Sync.
The smart pointer Rc<T> also doesn’t implement Sync for the same reasons
-that it doesn’t implement Send. The RefCell<T> type (which we talked about
-in Chapter 15) and the family of related Cell<T> types don’t implement
-Sync. The implementation of borrow checking that RefCell<T> does at runtime
-is not thread-safe. The smart pointer Mutex<T> implements Sync and can be
-used to share access with multiple threads, as you saw in “Shared Access to
-Mutex<T>”.
Send and Sync Manually Is UnsafeBecause types composed entirely of other types that implement the Send and
-Sync traits also automatically implement Send and Sync, we don’t have to
-implement those traits manually. As marker traits, they don’t even have any
-methods to implement. They’re just useful for enforcing invariants related to
-concurrency.
Manually implementing these traits involves implementing unsafe Rust code.
-We’ll talk about using unsafe Rust code in Chapter 20; for now, the important
-information is that building new concurrent types not made up of Send and
-Sync parts requires careful thought to uphold the safety guarantees. “The
-Rustonomicon” has more information about these guarantees and how to
-uphold them.
This isn’t the last you’ll see of concurrency in this book: The next chapter -focuses on async programming, and the project in Chapter 21 will use the -concepts in this chapter in a more realistic situation than the smaller -examples discussed here.
-As mentioned earlier, because very little of how Rust handles concurrency is -part of the language, many concurrency solutions are implemented as crates. -These evolve more quickly than the standard library, so be sure to search -online for the current, state-of-the-art crates to use in multithreaded -situations.
-The Rust standard library provides channels for message passing and smart
-pointer types, such as Mutex<T> and Arc<T>, that are safe to use in
-concurrent contexts. The type system and the borrow checker ensure that the
-code using these solutions won’t end up with data races or invalid references.
-Once you get your code to compile, you can rest assured that it will happily
-run on multiple threads without the kinds of hard-to-track-down bugs common in
-other languages. Concurrent programming is no longer a concept to be afraid of:
-Go forth and make your programs concurrent, fearlessly!
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
-Many operations we ask the computer to do can take a while to finish. It would -be nice if we could do something else while we’re waiting for those -long-running processes to complete. Modern computers offer two techniques for -working on more than one operation at a time: parallelism and concurrency. Our -programs’ logic, however, is written in a mostly linear fashion. We’d like to -be able to specify the operations a program should perform and points at which -a function could pause and some other part of the program could run instead, -without needing to specify up front exactly the order and manner in which each -bit of code should run. Asynchronous programming is an abstraction that lets -us express our code in terms of potential pausing points and eventual results -that takes care of the details of coordination for us.
-This chapter builds on Chapter 16’s use of threads for parallelism and
-concurrency by introducing an alternative approach to writing code: Rust’s
-futures, streams, and the async and await syntax that let us express how
-operations could be asynchronous, and the third-party crates that implement
-asynchronous runtimes: code that manages and coordinates the execution of
-asynchronous operations.
Let’s consider an example. Say you’re exporting a video you’ve created of a -family celebration, an operation that could take anywhere from minutes to -hours. The video export will use as much CPU and GPU power as it can. If you -had only one CPU core and your operating system didn’t pause that export until -it completed—that is, if it executed the export synchronously—you couldn’t do -anything else on your computer while that task was running. That would be a -pretty frustrating experience. Fortunately, your computer’s operating system -can, and does, invisibly interrupt the export often enough to let you get other -work done simultaneously.
-Now say you’re downloading a video shared by someone else, which can also take -a while but does not take up as much CPU time. In this case, the CPU has to -wait for data to arrive from the network. While you can start reading the data -once it starts to arrive, it might take some time for all of it to show up. -Even once the data is all present, if the video is quite large, it could take -at least a second or two to load it all. That might not sound like much, but -it’s a very long time for a modern processor, which can perform billions of -operations every second. Again, your operating system will invisibly interrupt -your program to allow the CPU to perform other work while waiting for the -network call to finish.
-The video export is an example of a CPU-bound or compute-bound operation. -It’s limited by the computer’s potential data processing speed within the CPU -or GPU, and how much of that speed it can dedicate to the operation. The video -download is an example of an I/O-bound operation, because it’s limited by the -speed of the computer’s input and output; it can only go as fast as the data -can be sent across the network.
-In both of these examples, the operating system’s invisible interrupts provide -a form of concurrency. That concurrency happens only at the level of the entire -program, though: the operating system interrupts one program to let other -programs get work done. In many cases, because we understand our programs at a -much more granular level than the operating system does, we can spot -opportunities for concurrency that the operating system can’t see.
-For example, if we’re building a tool to manage file downloads, we should be -able to write our program so that starting one download won’t lock up the UI, -and users should be able to start multiple downloads at the same time. Many -operating system APIs for interacting with the network are blocking, though; -that is, they block the program’s progress until the data they’re processing is -completely ready.
-Note: This is how most function calls work, if you think about it. However, -the term blocking is usually reserved for function calls that interact with -files, the network, or other resources on the computer, because those are the -cases where an individual program would benefit from the operation being -non-blocking.
-We could avoid blocking our main thread by spawning a dedicated thread to -download each file. However, the overhead of the system resources used by those -threads would eventually become a problem. It would be preferable if the call -didn’t block in the first place, and instead we could define a number of tasks -that we’d like our program to complete and allow the runtime to choose the best -order and manner in which to run them.
-That is exactly what Rust’s async (short for asynchronous) abstraction -gives us. In this chapter, you’ll learn all about async as we cover the -following topics:
-async and await syntax and execute asynchronous
-functions with a runtimeBefore we see how async works in practice, though, we need to take a short -detour to discuss the differences between parallelism and concurrency.
-We’ve treated parallelism and concurrency as mostly interchangeable so far. Now -we need to distinguish between them more precisely, because the differences -will show up as we start working.
-Consider the different ways a team could split up work on a software project. -You could assign a single member multiple tasks, assign each member one task, -or use a mix of the two approaches.
-When an individual works on several different tasks before any of them is -complete, this is concurrency. One way to implement concurrency is similar to -having two different projects checked out on your computer, and when you get -bored or stuck on one project, you switch to the other. You’re just one person, -so you can’t make progress on both tasks at the exact same time, but you can -multitask, making progress on one at a time by switching between them (see -Figure 17-1).
-When the team splits up a group of tasks by having each member take one task -and work on it alone, this is parallelism. Each person on the team can make -progress at the exact same time (see Figure 17-2).
-In both of these workflows, you might have to coordinate between different -tasks. Maybe you thought the task assigned to one person was totally -independent from everyone else’s work, but it actually requires another person -on the team to finish their task first. Some of the work could be done in -parallel, but some of it was actually serial: it could only happen in a -series, one task after the other, as in Figure 17-3.
-Likewise, you might realize that one of your own tasks depends on another of -your tasks. Now your concurrent work has also become serial.
-Parallelism and concurrency can intersect with each other, too. If you learn -that a colleague is stuck until you finish one of your tasks, you’ll probably -focus all your efforts on that task to “unblock” your colleague. You and your -coworker are no longer able to work in parallel, and you’re also no longer able -to work concurrently on your own tasks.
-The same basic dynamics come into play with software and hardware. On a machine -with a single CPU core, the CPU can perform only one operation at a time, but -it can still work concurrently. Using tools such as threads, processes, and -async, the computer can pause one activity and switch to others before -eventually cycling back to that first activity again. On a machine with -multiple CPU cores, it can also do work in parallel. One core can be performing -one task while another core performs a completely unrelated one, and those -operations actually happen at the same time.
-Running async code in Rust usually happens concurrently. Depending on the -hardware, the operating system, and the async runtime we are using (more on -async runtimes shortly), that concurrency may also use parallelism under the -hood.
-Now, let’s dive into how async programming in Rust actually works.
- -Redirecting to... ch18-00-oop.html.
- - - - diff --git a/build/ch17-01-what-is-oo.html b/build/ch17-01-what-is-oo.html deleted file mode 100644 index cbca098..0000000 --- a/build/ch17-01-what-is-oo.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - -Redirecting to... ch18-01-what-is-oo.html.
- - - - diff --git a/build/ch17-02-trait-objects.html b/build/ch17-02-trait-objects.html deleted file mode 100644 index e7cd192..0000000 --- a/build/ch17-02-trait-objects.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - -Redirecting to... ch18-02-trait-objects.html.
- - - - diff --git a/build/ch17-03-more-futures.html b/build/ch17-03-more-futures.html deleted file mode 100644 index 8651644..0000000 --- a/build/ch17-03-more-futures.html +++ /dev/null @@ -1,626 +0,0 @@ - - - - - -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
-Recall from the “Our First Async Program” -section that at each await point, Rust gives a runtime a chance to pause the -task and switch to another one if the future being awaited isn’t ready. The -inverse is also true: Rust only pauses async blocks and hands control back to -a runtime at an await point. Everything between await points is synchronous.
-That means if you do a bunch of work in an async block without an await point, -that future will block any other futures from making progress. You may sometimes -hear this referred to as one future starving other futures. In some cases, -that may not be a big deal. However, if you are doing some kind of expensive -setup or long-running work, or if you have a future that will keep doing some -particular task indefinitely, you’ll need to think about when and where to hand -control back to the runtime.
-Let’s simulate a long-running operation to illustrate the starvation problem,
-then explore how to solve it. Listing 17-14 introduces a slow function.
extern crate trpl; // required for mdbook test
-
-use std::{thread, time::Duration};
-
-fn main() {
- trpl::block_on(async {
- // We will call `slow` here later
- });
-}
-
-fn slow(name: &str, ms: u64) {
- thread::sleep(Duration::from_millis(ms));
- println!("'{name}' ran for {ms}ms");
-}
-thread::sleep to simulate slow operationsThis code uses std::thread::sleep instead of trpl::sleep so that calling
-slow will block the current thread for some number of milliseconds. We can
-use slow to stand in for real-world operations that are both long-running and
-blocking.
In Listing 17-15, we use slow to emulate doing this kind of CPU-bound work in
-a pair of futures.
extern crate trpl; // required for mdbook test
-
-use std::{thread, time::Duration};
-
-fn main() {
- trpl::block_on(async {
- let a = async {
- println!("'a' started.");
- slow("a", 30);
- slow("a", 10);
- slow("a", 20);
- trpl::sleep(Duration::from_millis(50)).await;
- println!("'a' finished.");
- };
-
- let b = async {
- println!("'b' started.");
- slow("b", 75);
- slow("b", 10);
- slow("b", 15);
- slow("b", 350);
- trpl::sleep(Duration::from_millis(50)).await;
- println!("'b' finished.");
- };
-
- trpl::select(a, b).await;
- });
-}
-
-fn slow(name: &str, ms: u64) {
- thread::sleep(Duration::from_millis(ms));
- println!("'{name}' ran for {ms}ms");
-}
-slow function to simulate slow operationsEach future hands control back to the runtime only after carrying out a bunch -of slow operations. If you run this code, you will see this output:
- -'a' started.
-'a' ran for 30ms
-'a' ran for 10ms
-'a' ran for 20ms
-'b' started.
-'b' ran for 75ms
-'b' ran for 10ms
-'b' ran for 15ms
-'b' ran for 350ms
-'a' finished.
-
-As with Listing 17-5 where we used trpl::select to race futures fetching two
-URLs, select still finishes as soon as a is done. There’s no interleaving
-between the calls to slow in the two futures, though. The a future does all
-of its work until the trpl::sleep call is awaited, then the b future does
-all of its work until its own trpl::sleep call is awaited, and finally the
-a future completes. To allow both futures to make progress between their slow
-tasks, we need await points so we can hand control back to the runtime. That
-means we need something we can await!
We can already see this kind of handoff happening in Listing 17-15: if we
-removed the trpl::sleep at the end of the a future, it would complete
-without the b future running at all. Let’s try using the trpl::sleep
-function as a starting point for letting operations switch off making progress,
-as shown in Listing 17-16.
extern crate trpl; // required for mdbook test
-
-use std::{thread, time::Duration};
-
-fn main() {
- trpl::block_on(async {
- let one_ms = Duration::from_millis(1);
-
- let a = async {
- println!("'a' started.");
- slow("a", 30);
- trpl::sleep(one_ms).await;
- slow("a", 10);
- trpl::sleep(one_ms).await;
- slow("a", 20);
- trpl::sleep(one_ms).await;
- println!("'a' finished.");
- };
-
- let b = async {
- println!("'b' started.");
- slow("b", 75);
- trpl::sleep(one_ms).await;
- slow("b", 10);
- trpl::sleep(one_ms).await;
- slow("b", 15);
- trpl::sleep(one_ms).await;
- slow("b", 350);
- trpl::sleep(one_ms).await;
- println!("'b' finished.");
- };
-
- trpl::select(a, b).await;
- });
-}
-
-fn slow(name: &str, ms: u64) {
- thread::sleep(Duration::from_millis(ms));
- println!("'{name}' ran for {ms}ms");
-}
-trpl::sleep to let operations switch off making progressWe’ve added trpl::sleep calls with await points between each call to slow.
-Now the two futures’ work is interleaved:
'a' started.
-'a' ran for 30ms
-'b' started.
-'b' ran for 75ms
-'a' ran for 10ms
-'b' ran for 10ms
-'a' ran for 20ms
-'b' ran for 15ms
-'a' finished.
-
-The a future still runs for a bit before handing off control to b, because
-it calls slow before ever calling trpl::sleep, but after that the futures
-swap back and forth each time one of them hits an await point. In this case, we
-have done that after every call to slow, but we could break up the work in
-whatever way makes the most sense to us.
We don’t really want to sleep here, though: we want to make progress as fast
-as we can. We just need to hand back control to the runtime. We can do that
-directly, using the trpl::yield_now function. In Listing 17-17, we replace
-all those trpl::sleep calls with trpl::yield_now.
extern crate trpl; // required for mdbook test
-
-use std::{thread, time::Duration};
-
-fn main() {
- trpl::block_on(async {
- let a = async {
- println!("'a' started.");
- slow("a", 30);
- trpl::yield_now().await;
- slow("a", 10);
- trpl::yield_now().await;
- slow("a", 20);
- trpl::yield_now().await;
- println!("'a' finished.");
- };
-
- let b = async {
- println!("'b' started.");
- slow("b", 75);
- trpl::yield_now().await;
- slow("b", 10);
- trpl::yield_now().await;
- slow("b", 15);
- trpl::yield_now().await;
- slow("b", 350);
- trpl::yield_now().await;
- println!("'b' finished.");
- };
-
- trpl::select(a, b).await;
- });
-}
-
-fn slow(name: &str, ms: u64) {
- thread::sleep(Duration::from_millis(ms));
- println!("'{name}' ran for {ms}ms");
-}
-yield_now to let operations switch off making progressThis code is both clearer about the actual intent and can be significantly
-faster than using sleep, because timers such as the one used by sleep often
-have limits on how granular they can be. The version of sleep we are using,
-for example, will always sleep for at least a millisecond, even if we pass it a
-Duration of one nanosecond. Again, modern computers are fast: they can do a
-lot in one millisecond!
This means that async can be useful even for compute-bound tasks, depending on -what else your program is doing, because it provides a useful tool for -structuring the relationships between different parts of the program (but at a -cost of the overhead of the async state machine). This is a form of -cooperative multitasking, where each future has the power to determine when -it hands over control via await points. Each future therefore also has the -responsibility to avoid blocking for too long. In some Rust-based embedded -operating systems, this is the only kind of multitasking!
-In real-world code, you won’t usually be alternating function calls with await -points on every single line, of course. While yielding control in this way is -relatively inexpensive, it’s not free. In many cases, trying to break up a -compute-bound task might make it significantly slower, so sometimes it’s better -for overall performance to let an operation block briefly. Always -measure to see what your code’s actual performance bottlenecks are. The -underlying dynamic is important to keep in mind, though, if you are seeing a -lot of work happening in serial that you expected to happen concurrently!
-We can also compose futures together to create new patterns. For example, we can
-build a timeout function with async building blocks we already have. When
-we’re done, the result will be another building block we could use to create
-still more async abstractions.
Listing 17-18 shows how we would expect this timeout to work with a slow
-future.
extern crate trpl; // required for mdbook test
-
-use std::time::Duration;
-
-fn main() {
- trpl::block_on(async {
- let slow = async {
- trpl::sleep(Duration::from_secs(5)).await;
- "Finally finished"
- };
-
- match timeout(slow, Duration::from_secs(2)).await {
- Ok(message) => println!("Succeeded with '{message}'"),
- Err(duration) => {
- println!("Failed after {} seconds", duration.as_secs())
- }
- }
- });
-}
-timeout to run a slow operation with a time limitLet’s implement this! To begin, let’s think about the API for timeout:
Duration,
-that will make it easy to pass along to trpl::sleep.Result. If the future completes successfully, the
-Result will be Ok with the value produced by the future. If the timeout
-elapses first, the Result will be Err with the duration that the timeout
-waited for.Listing 17-19 shows this declaration.
- -extern crate trpl; // required for mdbook test
-
-use std::time::Duration;
-
-fn main() {
- trpl::block_on(async {
- let slow = async {
- trpl::sleep(Duration::from_secs(5)).await;
- "Finally finished"
- };
-
- match timeout(slow, Duration::from_secs(2)).await {
- Ok(message) => println!("Succeeded with '{message}'"),
- Err(duration) => {
- println!("Failed after {} seconds", duration.as_secs())
- }
- }
- });
-}
-
-async fn timeout<F: Future>(
- future_to_try: F,
- max_time: Duration,
-) -> Result<F::Output, Duration> {
- // Here is where our implementation will go!
-}
-timeoutThat satisfies our goals for the types. Now let’s think about the behavior we
-need: we want to race the future passed in against the duration. We can use
-trpl::sleep to make a timer future from the duration, and use trpl::select
-to run that timer with the future the caller passes in.
In Listing 17-20, we implement timeout by matching on the result of awaiting
-trpl::select.
extern crate trpl; // required for mdbook test
-
-use std::time::Duration;
-
-use trpl::Either;
-
-// --snip--
-
-fn main() {
- trpl::block_on(async {
- let slow = async {
- trpl::sleep(Duration::from_secs(5)).await;
- "Finally finished"
- };
-
- match timeout(slow, Duration::from_secs(2)).await {
- Ok(message) => println!("Succeeded with '{message}'"),
- Err(duration) => {
- println!("Failed after {} seconds", duration.as_secs())
- }
- }
- });
-}
-
-async fn timeout<F: Future>(
- future_to_try: F,
- max_time: Duration,
-) -> Result<F::Output, Duration> {
- match trpl::select(future_to_try, trpl::sleep(max_time)).await {
- Either::Left(output) => Ok(output),
- Either::Right(_) => Err(max_time),
- }
-}
-timeout with select and sleepThe implementation of trpl::select is not fair: it always polls arguments in
-the order in which they are passed (other select implementations will
-randomly choose which argument to poll first). Thus, we pass future_to_try to
-select first so it gets a chance to complete even if max_time is a very
-short duration. If future_to_try finishes first, select will return Left
-with the output from future_to_try. If timer finishes first, select will
-return Right with the timer’s output of ().
If the future_to_try succeeds and we get a Left(output), we return
-Ok(output). If the sleep timer elapses instead and we get a Right(()), we
-ignore the () with _ and return Err(max_time) instead.
With that, we have a working timeout built out of two other async helpers. If
-we run our code, it will print the failure mode after the timeout:
Failed after 2 seconds
-
-Because futures compose with other futures, you can build really powerful tools -using smaller async building blocks. For example, you can use this same -approach to combine timeouts with retries, and in turn use those with -operations such as network calls (such as those in Listing 17-5).
-In practice, you’ll usually work directly with async and await, and
-secondarily with functions such as select and macros such as the join!
-macro to control how the outermost futures are executed.
We’ve now seen a number of ways to work with multiple futures at the same time. -Up next, we’ll look at how we can work with multiple futures in a sequence over -time with streams.
- -Redirecting to... ch18-03-oo-design-patterns.html.
- - - - diff --git a/build/ch17-04-streams.html b/build/ch17-04-streams.html deleted file mode 100644 index 7e48194..0000000 --- a/build/ch17-04-streams.html +++ /dev/null @@ -1,358 +0,0 @@ - - - - - -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
-Recall how we used the receiver for our async channel earlier in this chapter
-in the “Message Passing” section. The async
-recv method produces a sequence of items over time. This is an instance of a
-much more general pattern known as a stream. Many concepts are naturally
-represented as streams: items becoming available in a queue, chunks of data
-being pulled incrementally from the filesystem when the full data set is too
-large for the computer’s memory, or data arriving over the network over time.
-Because streams are futures, we can use them with any other kind of future and
-combine them in interesting ways. For example, we can batch up events to avoid
-triggering too many network calls, set timeouts on sequences of long-running
-operations, or throttle user interface events to avoid doing needless work.
We saw a sequence of items back in Chapter 13, when we looked at the Iterator
-trait in “The Iterator Trait and the next Method” section, but there are two differences between iterators and the
-async channel receiver. The first difference is time: iterators are
-synchronous, while the channel receiver is asynchronous. The second difference
-is the API. When working directly with Iterator, we call its synchronous
-next method. With the trpl::Receiver stream in particular, we called an
-asynchronous recv method instead. Otherwise, these APIs feel very similar,
-and that similarity isn’t a coincidence. A stream is like an asynchronous form
-of iteration. Whereas the trpl::Receiver specifically waits to receive
-messages, though, the general-purpose stream API is much broader: it provides
-the next item the way Iterator does, but asynchronously.
The similarity between iterators and streams in Rust means we can actually
-create a stream from any iterator. As with an iterator, we can work with a
-stream by calling its next method and then awaiting the output, as in Listing
-17-21, which won’t compile yet.
extern crate trpl; // required for mdbook test
-
-fn main() {
- trpl::block_on(async {
- let values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
- let iter = values.iter().map(|n| n * 2);
- let mut stream = trpl::stream_from_iter(iter);
-
- while let Some(value) = stream.next().await {
- println!("The value was: {value}");
- }
- });
-}
-We start with an array of numbers, which we convert to an iterator and then
-call map on to double all the values. Then we convert the iterator into a
-stream using the trpl::stream_from_iter function. Next, we loop over the
-items in the stream as they arrive with the while let loop.
Unfortunately, when we try to run the code, it doesn’t compile but instead
-reports that there’s no next method available:
error[E0599]: no method named `next` found for struct `tokio_stream::iter::Iter` in the current scope
- --> src/main.rs:10:40
- |
-10 | while let Some(value) = stream.next().await {
- | ^^^^
- |
- = help: items from traits can only be used if the trait is in scope
-help: the following traits which provide `next` are implemented but not in scope; perhaps you want to import one of them
- |
-1 + use crate::trpl::StreamExt;
- |
-1 + use futures_util::stream::stream::StreamExt;
- |
-1 + use std::iter::Iterator;
- |
-1 + use std::str::pattern::Searcher;
- |
-help: there is a method `try_next` with a similar name
- |
-10 | while let Some(value) = stream.try_next().await {
- | ~~~~~~~~
-
-As this output explains, the reason for the compiler error is that we need the
-right trait in scope to be able to use the next method. Given our discussion
-so far, you might reasonably expect that trait to be Stream, but it’s
-actually StreamExt. Short for extension, Ext is a common pattern in the
-Rust community for extending one trait with another.
The Stream trait defines a low-level interface that effectively combines the
-Iterator and Future traits. StreamExt supplies a higher-level set of APIs
-on top of Stream, including the next method as well as other utility
-methods similar to those provided by the Iterator trait. Stream and
-StreamExt are not yet part of Rust’s standard library, but most ecosystem
-crates use similar definitions.
The fix to the compiler error is to add a use statement for
-trpl::StreamExt, as in Listing 17-22.
extern crate trpl; // required for mdbook test
-
-use trpl::StreamExt;
-
-fn main() {
- trpl::block_on(async {
- let values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
- // --snip--
- let iter = values.iter().map(|n| n * 2);
- let mut stream = trpl::stream_from_iter(iter);
-
- while let Some(value) = stream.next().await {
- println!("The value was: {value}");
- }
- });
-}
-With all those pieces put together, this code works the way we want! What’s
-more, now that we have StreamExt in scope, we can use all of its utility
-methods, just as with iterators.
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
-As we saw in Chapter 16, threads provide one approach to -concurrency. We’ve seen another approach in this chapter: using async with -futures and streams. If you’re wondering when to choose one method over the other, -the answer is: it depends! And in many cases, the choice isn’t threads or -async but rather threads and async.
-Many operating systems have supplied threading-based concurrency models for -decades now, and many programming languages support them as a result. However, -these models are not without their tradeoffs. On many operating systems, they -use a fair bit of memory for each thread. Threads are also only an option when -your operating system and hardware support them. Unlike mainstream desktop and -mobile computers, some embedded systems don’t have an OS at all, so they also -don’t have threads.
-The async model provides a different—and ultimately complementary—set of
-tradeoffs. In the async model, concurrent operations don’t require their own
-threads. Instead, they can run on tasks, as when we used trpl::spawn_task to
-kick off work from a synchronous function in the streams section. A task is
-similar to a thread, but instead of being managed by the operating system, it’s
-managed by library-level code: the runtime.
There’s a reason the APIs for spawning threads and spawning tasks are so -similar. Threads act as a boundary for sets of synchronous operations; -concurrency is possible between threads. Tasks act as a boundary for sets of -asynchronous operations; concurrency is possible both between and within -tasks, because a task can switch between futures in its body. Finally, futures -are Rust’s most granular unit of concurrency, and each future may represent a -tree of other futures. The runtime—specifically, its executor—manages tasks, -and tasks manage futures. In that regard, tasks are similar to lightweight, -runtime-managed threads with added capabilities that come from being managed by -a runtime instead of by the operating system.
-This doesn’t mean that async tasks are always better than threads (or vice
-versa). Concurrency with threads is in some ways a simpler programming model
-than concurrency with async. That can be a strength or a weakness. Threads are
-somewhat “fire and forget”; they have no native equivalent to a future, so they
-simply run to completion without being interrupted except by the operating
-system itself.
And it turns out that threads and tasks often work
-very well together, because tasks can (at least in some runtimes) be moved
-around between threads. In fact, under the hood, the runtime we’ve been
-using—including the spawn_blocking and spawn_task functions—is multithreaded
-by default! Many runtimes use an approach called work stealing to
-transparently move tasks around between threads, based on how the threads are
-currently being utilized, to improve the system’s overall performance. That
-approach actually requires threads and tasks, and therefore futures.
When thinking about which method to use when, consider these rules of thumb:
-And if you need both parallelism and concurrency, you don’t have to choose -between threads and async. You can use them together freely, letting each -play the part it’s best at. For example, Listing 17-25 shows a fairly common -example of this kind of mix in real-world Rust code.
-extern crate trpl; // for mdbook test
-
-use std::{thread, time::Duration};
-
-fn main() {
- let (tx, mut rx) = trpl::channel();
-
- thread::spawn(move || {
- for i in 1..11 {
- tx.send(i).unwrap();
- thread::sleep(Duration::from_secs(1));
- }
- });
-
- trpl::block_on(async {
- while let Some(message) = rx.recv().await {
- println!("{message}");
- }
- });
-}
-We begin by creating an async channel, then spawning a thread that takes
-ownership of the sender side of the channel using the move keyword. Within
-the thread, we send the numbers 1 through 10, sleeping for a second between
-each. Finally, we run a future created with an async block passed to
-trpl::block_on just as we have throughout the chapter. In that future, we
-await those messages, just as in the other message-passing examples we have
-seen.
To return to the scenario we opened the chapter with, imagine running a set of -video encoding tasks using a dedicated thread (because video encoding is -compute-bound) but notifying the UI that those operations are done with an -async channel. There are countless examples of these kinds of combinations in -real-world use cases.
-This isn’t the last you’ll see of concurrency in this book. The project in -Chapter 21 will apply these concepts in a more realistic -situation than the simpler examples discussed here and compare problem-solving -with threading versus tasks and futures more directly.
-No matter which of these approaches you choose, Rust gives you the tools you -need to write safe, fast, concurrent code—whether for a high-throughput web -server or an embedded operating system.
-Next, we’ll talk about idiomatic ways to model problems and structure solutions -as your Rust programs get bigger. In addition, we’ll discuss how Rust’s idioms -relate to those you might be familiar with from object-oriented programming.
- -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
-Object-oriented programming (OOP) is a way of modeling programs. Objects as a -programmatic concept were introduced in the programming language Simula in the -1960s. Those objects influenced Alan Kay’s programming architecture in which -objects pass messages to each other. To describe this architecture, he coined -the term object-oriented programming in 1967. Many competing definitions -describe what OOP is, and by some of these definitions Rust is object oriented -but by others it is not. In this chapter, we’ll explore certain characteristics -that are commonly considered object oriented and how those characteristics -translate to idiomatic Rust. We’ll then show you how to implement an -object-oriented design pattern in Rust and discuss the trade-offs of doing so -versus implementing a solution using some of Rust’s strengths instead.
- -Redirecting to... ch19-00-patterns.html.
- - - - diff --git a/build/ch18-01-all-the-places-for-patterns.html b/build/ch18-01-all-the-places-for-patterns.html deleted file mode 100644 index 600a349..0000000 --- a/build/ch18-01-all-the-places-for-patterns.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - -Redirecting to... ch19-01-all-the-places-for-patterns.html.
- - - - diff --git a/build/ch18-01-what-is-oo.html b/build/ch18-01-what-is-oo.html deleted file mode 100644 index 0d4582f..0000000 --- a/build/ch18-01-what-is-oo.html +++ /dev/null @@ -1,398 +0,0 @@ - - - - - -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
-There is no consensus in the programming community about what features a -language must have to be considered object oriented. Rust is influenced by many -programming paradigms, including OOP; for example, we explored the features -that came from functional programming in Chapter 13. Arguably, OOP languages -share certain common characteristics—namely, objects, encapsulation, and -inheritance. Let’s look at what each of those characteristics means and whether -Rust supports it.
-The book Design Patterns: Elements of Reusable Object-Oriented Software by -Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides (Addison-Wesley, -1994), colloquially referred to as The Gang of Four book, is a catalog of -object-oriented design patterns. It defines OOP in this way:
---Object-oriented programs are made up of objects. An object packages both -data and the procedures that operate on that data. The procedures are -typically called methods or operations.
-
Using this definition, Rust is object oriented: Structs and enums have data,
-and impl blocks provide methods on structs and enums. Even though structs and
-enums with methods aren’t called objects, they provide the same
-functionality, according to the Gang of Four’s definition of objects.
Another aspect commonly associated with OOP is the idea of encapsulation, -which means that the implementation details of an object aren’t accessible to -code using that object. Therefore, the only way to interact with an object is -through its public API; code using the object shouldn’t be able to reach into -the object’s internals and change data or behavior directly. This enables the -programmer to change and refactor an object’s internals without needing to -change the code that uses the object.
-We discussed how to control encapsulation in Chapter 7: We can use the pub
-keyword to decide which modules, types, functions, and methods in our code
-should be public, and by default everything else is private. For example, we
-can define a struct AveragedCollection that has a field containing a vector
-of i32 values. The struct can also have a field that contains the average of
-the values in the vector, meaning the average doesn’t have to be computed on
-demand whenever anyone needs it. In other words, AveragedCollection will
-cache the calculated average for us. Listing 18-1 has the definition of the
-AveragedCollection struct.
pub struct AveragedCollection {
- list: Vec<i32>,
- average: f64,
-}
-AveragedCollection struct that maintains a list of integers and the average of the items in the collectionThe struct is marked pub so that other code can use it, but the fields within
-the struct remain private. This is important in this case because we want to
-ensure that whenever a value is added or removed from the list, the average is
-also updated. We do this by implementing add, remove, and average methods
-on the struct, as shown in Listing 18-2.
pub struct AveragedCollection {
- list: Vec<i32>,
- average: f64,
-}
-
-impl AveragedCollection {
- pub fn add(&mut self, value: i32) {
- self.list.push(value);
- self.update_average();
- }
-
- pub fn remove(&mut self) -> Option<i32> {
- let result = self.list.pop();
- match result {
- Some(value) => {
- self.update_average();
- Some(value)
- }
- None => None,
- }
- }
-
- pub fn average(&self) -> f64 {
- self.average
- }
-
- fn update_average(&mut self) {
- let total: i32 = self.list.iter().sum();
- self.average = total as f64 / self.list.len() as f64;
- }
-}
-add, remove, and average on AveragedCollectionThe public methods add, remove, and average are the only ways to access
-or modify data in an instance of AveragedCollection. When an item is added to
-list using the add method or removed using the remove method, the
-implementations of each call the private update_average method that handles
-updating the average field as well.
We leave the list and average fields private so that there is no way for
-external code to add or remove items to or from the list field directly;
-otherwise, the average field might become out of sync when the list
-changes. The average method returns the value in the average field,
-allowing external code to read the average but not modify it.
Because we’ve encapsulated the implementation details of the struct
-AveragedCollection, we can easily change aspects, such as the data structure,
-in the future. For instance, we could use a HashSet<i32> instead of a
-Vec<i32> for the list field. As long as the signatures of the add,
-remove, and average public methods stayed the same, code using
-AveragedCollection wouldn’t need to change. If we made list public instead,
-this wouldn’t necessarily be the case: HashSet<i32> and Vec<i32> have
-different methods for adding and removing items, so the external code would
-likely have to change if it were modifying list directly.
If encapsulation is a required aspect for a language to be considered object
-oriented, then Rust meets that requirement. The option to use pub or not for
-different parts of code enables encapsulation of implementation details.
Inheritance is a mechanism whereby an object can inherit elements from -another object’s definition, thus gaining the parent object’s data and behavior -without you having to define them again.
-If a language must have inheritance to be object oriented, then Rust is not -such a language. There is no way to define a struct that inherits the parent -struct’s fields and method implementations without using a macro.
-However, if you’re used to having inheritance in your programming toolbox, you -can use other solutions in Rust, depending on your reason for reaching for -inheritance in the first place.
-You would choose inheritance for two main reasons. One is for reuse of code:
-You can implement particular behavior for one type, and inheritance enables you
-to reuse that implementation for a different type. You can do this in a limited
-way in Rust code using default trait method implementations, which you saw in
-Listing 10-14 when we added a default implementation of the summarize method
-on the Summary trait. Any type implementing the Summary trait would have
-the summarize method available on it without any further code. This is
-similar to a parent class having an implementation of a method and an
-inheriting child class also having the implementation of the method. We can
-also override the default implementation of the summarize method when we
-implement the Summary trait, which is similar to a child class overriding the
-implementation of a method inherited from a parent class.
The other reason to use inheritance relates to the type system: to enable a -child type to be used in the same places as the parent type. This is also -called polymorphism, which means that you can substitute multiple objects for -each other at runtime if they share certain characteristics.
-To many people, polymorphism is synonymous with inheritance. But it’s -actually a more general concept that refers to code that can work with data of -multiple types. For inheritance, those types are generally subclasses.
-Rust instead uses generics to abstract over different possible types and -trait bounds to impose constraints on what those types must provide. This is -sometimes called bounded parametric polymorphism.
-Rust has chosen a different set of trade-offs by not offering inheritance. -Inheritance is often at risk of sharing more code than necessary. Subclasses -shouldn’t always share all characteristics of their parent class but will do so -with inheritance. This can make a program’s design less flexible. It also -introduces the possibility of calling methods on subclasses that don’t make -sense or that cause errors because the methods don’t apply to the subclass. In -addition, some languages will only allow single inheritance (meaning a -subclass can only inherit from one class), further restricting the flexibility -of a program’s design.
-For these reasons, Rust takes the different approach of using trait objects -instead of inheritance to achieve polymorphism at runtime. Let’s look at how -trait objects work.
- -Redirecting to... ch19-02-refutability.html.
- - - - diff --git a/build/ch18-02-trait-objects.html b/build/ch18-02-trait-objects.html deleted file mode 100644 index 6abb97a..0000000 --- a/build/ch18-02-trait-objects.html +++ /dev/null @@ -1,572 +0,0 @@ - - - - - -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
-In Chapter 8, we mentioned that one limitation of vectors is that they can
-store elements of only one type. We created a workaround in Listing 8-9 where
-we defined a SpreadsheetCell enum that had variants to hold integers, floats,
-and text. This meant we could store different types of data in each cell and
-still have a vector that represented a row of cells. This is a perfectly good
-solution when our interchangeable items are a fixed set of types that we know
-when our code is compiled.
However, sometimes we want our library user to be able to extend the set of
-types that are valid in a particular situation. To show how we might achieve
-this, we’ll create an example graphical user interface (GUI) tool that iterates
-through a list of items, calling a draw method on each one to draw it to the
-screen—a common technique for GUI tools. We’ll create a library crate called
-gui that contains the structure of a GUI library. This crate might include
-some types for people to use, such as Button or TextField. In addition,
-gui users will want to create their own types that can be drawn: For
-instance, one programmer might add an Image, and another might add a
-SelectBox.
At the time of writing the library, we can’t know and define all the types
-other programmers might want to create. But we do know that gui needs to keep
-track of many values of different types, and it needs to call a draw method
-on each of these differently typed values. It doesn’t need to know exactly what
-will happen when we call the draw method, just that the value will have that
-method available for us to call.
To do this in a language with inheritance, we might define a class named
-Component that has a method named draw on it. The other classes, such as
-Button, Image, and SelectBox, would inherit from Component and thus
-inherit the draw method. They could each override the draw method to define
-their custom behavior, but the framework could treat all of the types as if
-they were Component instances and call draw on them. But because Rust
-doesn’t have inheritance, we need another way to structure the gui library to
-allow users to create new types compatible with the library.
To implement the behavior that we want gui to have, we’ll define a trait
-named Draw that will have one method named draw. Then, we can define a
-vector that takes a trait object. A trait object points to both an instance
-of a type implementing our specified trait and a table used to look up trait
-methods on that type at runtime. We create a trait object by specifying some
-sort of pointer, such as a reference or a Box<T> smart pointer, then the
-dyn keyword, and then specifying the relevant trait. (We’ll talk about the
-reason trait objects must use a pointer in “Dynamically Sized Types and the
-Sized Trait” in Chapter 20.) We can use
-trait objects in place of a generic or concrete type. Wherever we use a trait
-object, Rust’s type system will ensure at compile time that any value used in
-that context will implement the trait object’s trait. Consequently, we don’t
-need to know all the possible types at compile time.
We’ve mentioned that, in Rust, we refrain from calling structs and enums
-“objects” to distinguish them from other languages’ objects. In a struct or
-enum, the data in the struct fields and the behavior in impl blocks are
-separated, whereas in other languages, the data and behavior combined into one
-concept is often labeled an object. Trait objects differ from objects in other
-languages in that we can’t add data to a trait object. Trait objects aren’t as
-generally useful as objects in other languages: Their specific purpose is to
-allow abstraction across common behavior.
Listing 18-3 shows how to define a trait named Draw with one method named
-draw.
pub trait Draw {
- fn draw(&self);
-}
-Draw traitThis syntax should look familiar from our discussions on how to define traits
-in Chapter 10. Next comes some new syntax: Listing 18-4 defines a struct named
-Screen that holds a vector named components. This vector is of type
-Box<dyn Draw>, which is a trait object; it’s a stand-in for any type inside a
-Box that implements the Draw trait.
pub trait Draw {
- fn draw(&self);
-}
-
-pub struct Screen {
- pub components: Vec<Box<dyn Draw>>,
-}
-Screen struct with a components field holding a vector of trait objects that implement the Draw traitOn the Screen struct, we’ll define a method named run that will call the
-draw method on each of its components, as shown in Listing 18-5.
pub trait Draw {
- fn draw(&self);
-}
-
-pub struct Screen {
- pub components: Vec<Box<dyn Draw>>,
-}
-
-impl Screen {
- pub fn run(&self) {
- for component in self.components.iter() {
- component.draw();
- }
- }
-}
-run method on Screen that calls the draw method on each componentThis works differently from defining a struct that uses a generic type
-parameter with trait bounds. A generic type parameter can be substituted with
-only one concrete type at a time, whereas trait objects allow for multiple
-concrete types to fill in for the trait object at runtime. For example, we
-could have defined the Screen struct using a generic type and a trait bound,
-as in Listing 18-6.
pub trait Draw {
- fn draw(&self);
-}
-
-pub struct Screen<T: Draw> {
- pub components: Vec<T>,
-}
-
-impl<T> Screen<T>
-where
- T: Draw,
-{
- pub fn run(&self) {
- for component in self.components.iter() {
- component.draw();
- }
- }
-}
-Screen struct and its run method using generics and trait boundsThis restricts us to a Screen instance that has a list of components all of
-type Button or all of type TextField. If you’ll only ever have homogeneous
-collections, using generics and trait bounds is preferable because the
-definitions will be monomorphized at compile time to use the concrete types.
On the other hand, with the method using trait objects, one Screen instance
-can hold a Vec<T> that contains a Box<Button> as well as a
-Box<TextField>. Let’s look at how this works, and then we’ll talk about the
-runtime performance implications.
Now we’ll add some types that implement the Draw trait. We’ll provide the
-Button type. Again, actually implementing a GUI library is beyond the scope
-of this book, so the draw method won’t have any useful implementation in its
-body. To imagine what the implementation might look like, a Button struct
-might have fields for width, height, and label, as shown in Listing 18-7.
pub trait Draw {
- fn draw(&self);
-}
-
-pub struct Screen {
- pub components: Vec<Box<dyn Draw>>,
-}
-
-impl Screen {
- pub fn run(&self) {
- for component in self.components.iter() {
- component.draw();
- }
- }
-}
-
-pub struct Button {
- pub width: u32,
- pub height: u32,
- pub label: String,
-}
-
-impl Draw for Button {
- fn draw(&self) {
- // code to actually draw a button
- }
-}
-Button struct that implements the Draw traitThe width, height, and label fields on Button will differ from the
-fields on other components; for example, a TextField type might have those
-same fields plus a placeholder field. Each of the types we want to draw on
-the screen will implement the Draw trait but will use different code in the
-draw method to define how to draw that particular type, as Button has here
-(without the actual GUI code, as mentioned). The Button type, for instance,
-might have an additional impl block containing methods related to what
-happens when a user clicks the button. These kinds of methods won’t apply to
-types like TextField.
If someone using our library decides to implement a SelectBox struct that has
-width, height, and options fields, they would implement the Draw trait
-on the SelectBox type as well, as shown in Listing 18-8.
use gui::Draw;
-
-struct SelectBox {
- width: u32,
- height: u32,
- options: Vec<String>,
-}
-
-impl Draw for SelectBox {
- fn draw(&self) {
- // code to actually draw a select box
- }
-}
-
-fn main() {}
-gui and implementing the Draw trait on a SelectBox structOur library’s user can now write their main function to create a Screen
-instance. To the Screen instance, they can add a SelectBox and a Button
-by putting each in a Box<T> to become a trait object. They can then call the
-run method on the Screen instance, which will call draw on each of the
-components. Listing 18-9 shows this implementation.
use gui::Draw;
-
-struct SelectBox {
- width: u32,
- height: u32,
- options: Vec<String>,
-}
-
-impl Draw for SelectBox {
- fn draw(&self) {
- // code to actually draw a select box
- }
-}
-
-use gui::{Button, Screen};
-
-fn main() {
- let screen = Screen {
- components: vec![
- Box::new(SelectBox {
- width: 75,
- height: 10,
- options: vec![
- String::from("Yes"),
- String::from("Maybe"),
- String::from("No"),
- ],
- }),
- Box::new(Button {
- width: 50,
- height: 10,
- label: String::from("OK"),
- }),
- ],
- };
-
- screen.run();
-}
-When we wrote the library, we didn’t know that someone might add the
-SelectBox type, but our Screen implementation was able to operate on the
-new type and draw it because SelectBox implements the Draw trait, which
-means it implements the draw method.
This concept—of being concerned only with the messages a value responds to
-rather than the value’s concrete type—is similar to the concept of duck
-typing in dynamically typed languages: If it walks like a duck and quacks like
-a duck, then it must be a duck! In the implementation of run on Screen in
-Listing 18-5, run doesn’t need to know what the concrete type of each
-component is. It doesn’t check whether a component is an instance of a Button
-or a SelectBox, it just calls the draw method on the component. By
-specifying Box<dyn Draw> as the type of the values in the components
-vector, we’ve defined Screen to need values that we can call the draw
-method on.
The advantage of using trait objects and Rust’s type system to write code -similar to code using duck typing is that we never have to check whether a -value implements a particular method at runtime or worry about getting errors -if a value doesn’t implement a method but we call it anyway. Rust won’t compile -our code if the values don’t implement the traits that the trait objects need.
-For example, Listing 18-10 shows what happens if we try to create a Screen
-with a String as a component.
use gui::Screen;
-
-fn main() {
- let screen = Screen {
- components: vec![Box::new(String::from("Hi"))],
- };
-
- screen.run();
-}
-We’ll get this error because String doesn’t implement the Draw trait:
$ cargo run
- Compiling gui v0.1.0 (file:///projects/gui)
-error[E0277]: the trait bound `String: Draw` is not satisfied
- --> src/main.rs:5:26
- |
-5 | components: vec![Box::new(String::from("Hi"))],
- | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not implemented for `String`
- |
- = help: the trait `Draw` is implemented for `Button`
- = note: required for the cast from `Box<String>` to `Box<dyn Draw>`
-
-For more information about this error, try `rustc --explain E0277`.
-error: could not compile `gui` (bin "gui") due to 1 previous error
-
-This error lets us know that either we’re passing something to Screen that we
-didn’t mean to pass and so should pass a different type, or we should implement
-Draw on String so that Screen is able to call draw on it.
Recall in “Performance of Code Using -Generics” in Chapter 10 our -discussion on the monomorphization process performed on generics by the -compiler: The compiler generates nongeneric implementations of functions and -methods for each concrete type that we use in place of a generic type -parameter. The code that results from monomorphization is doing static -dispatch, which is when the compiler knows what method you’re calling at -compile time. This is opposed to dynamic dispatch, which is when the compiler -can’t tell at compile time which method you’re calling. In dynamic dispatch -cases, the compiler emits code that at runtime will know which method to call.
-When we use trait objects, Rust must use dynamic dispatch. The compiler doesn’t -know all the types that might be used with the code that’s using trait objects, -so it doesn’t know which method implemented on which type to call. Instead, at -runtime, Rust uses the pointers inside the trait object to know which method to -call. This lookup incurs a runtime cost that doesn’t occur with static dispatch. -Dynamic dispatch also prevents the compiler from choosing to inline a method’s -code, which in turn prevents some optimizations, and Rust has some rules about -where you can and cannot use dynamic dispatch, called dyn compatibility. Those -rules are beyond the scope of this discussion, but you can read more about them -in the reference. However, we did get extra -flexibility in the code that we wrote in Listing 18-5 and were able to support -in Listing 18-9, so it’s a trade-off to consider.
- -Redirecting to... ch19-03-pattern-syntax.html.
- - - - diff --git a/build/ch19-00-advanced-features.html b/build/ch19-00-advanced-features.html deleted file mode 100644 index 2032304..0000000 --- a/build/ch19-00-advanced-features.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - -Redirecting to... ch20-00-advanced-features.html.
- - - - diff --git a/build/ch19-00-patterns.html b/build/ch19-00-patterns.html deleted file mode 100644 index 75a3ad7..0000000 --- a/build/ch19-00-patterns.html +++ /dev/null @@ -1,268 +0,0 @@ - - - - - -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
-Patterns are a special syntax in Rust for matching against the structure of
-types, both complex and simple. Using patterns in conjunction with match
-expressions and other constructs gives you more control over a program’s
-control flow. A pattern consists of some combination of the following:
Some example patterns include x, (a, 3), and Some(Color::Red). In the
-contexts in which patterns are valid, these components describe the shape of
-data. Our program then matches values against the patterns to determine whether
-it has the correct shape of data to continue running a particular piece of code.
To use a pattern, we compare it to some value. If the pattern matches the
-value, we use the value parts in our code. Recall the match expressions in
-Chapter 6 that used patterns, such as the coin-sorting machine example. If the
-value fits the shape of the pattern, we can use the named pieces. If it
-doesn’t, the code associated with the pattern won’t run.
This chapter is a reference on all things related to patterns. We’ll cover the -valid places to use patterns, the difference between refutable and irrefutable -patterns, and the different kinds of pattern syntax that you might see. By the -end of the chapter, you’ll know how to use patterns to express many concepts in -a clear way.
- -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
-Patterns pop up in a number of places in Rust, and you’ve been using them a lot -without realizing it! This section discusses all the places where patterns are -valid.
-match ArmsAs discussed in Chapter 6, we use patterns in the arms of match expressions.
-Formally, match expressions are defined as the keyword match, a value to
-match on, and one or more match arms that consist of a pattern and an
-expression to run if the value matches that arm’s pattern, like this:
match VALUE {
- PATTERN => EXPRESSION,
- PATTERN => EXPRESSION,
- PATTERN => EXPRESSION,
-}
-
-For example, here’s the match expression from Listing 6-5 that matches on an
-Option<i32> value in the variable x:
match x {
- None => None,
- Some(i) => Some(i + 1),
-}
-The patterns in this match expression are the None and Some(i) to the
-left of each arrow.
One requirement for match expressions is that they need to be exhaustive in
-the sense that all possibilities for the value in the match expression must
-be accounted for. One way to ensure that you’ve covered every possibility is to
-have a catch-all pattern for the last arm: For example, a variable name
-matching any value can never fail and thus covers every remaining case.
The particular pattern _ will match anything, but it never binds to a
-variable, so it’s often used in the last match arm. The _ pattern can be
-useful when you want to ignore any value not specified, for example. We’ll
-cover the _ pattern in more detail in “Ignoring Values in a
-Pattern” later in this chapter.
let StatementsPrior to this chapter, we had only explicitly discussed using patterns with
-match and if let, but in fact, we’ve used patterns in other places as well,
-including in let statements. For example, consider this straightforward
-variable assignment with let:
#![allow(unused)]
-fn main() {
-let x = 5;
-}
-Every time you’ve used a let statement like this you’ve been using patterns,
-although you might not have realized it! More formally, a let statement looks
-like this:
-let PATTERN = EXPRESSION;
-
-
-In statements like let x = 5; with a variable name in the PATTERN slot, the
-variable name is just a particularly simple form of a pattern. Rust compares
-the expression against the pattern and assigns any names it finds. So, in the
-let x = 5; example, x is a pattern that means “bind what matches here to
-the variable x.” Because the name x is the whole pattern, this pattern
-effectively means “bind everything to the variable x, whatever the value is.”
To see the pattern-matching aspect of let more clearly, consider Listing
-19-1, which uses a pattern with let to destructure a tuple.
fn main() {
- let (x, y, z) = (1, 2, 3);
-}
-Here, we match a tuple against a pattern. Rust compares the value (1, 2, 3)
-to the pattern (x, y, z) and sees that the value matches the pattern—that is,
-it sees that the number of elements is the same in both—so Rust binds 1 to
-x, 2 to y, and 3 to z. You can think of this tuple pattern as nesting
-three individual variable patterns inside it.
If the number of elements in the pattern doesn’t match the number of elements -in the tuple, the overall type won’t match and we’ll get a compiler error. For -example, Listing 19-2 shows an attempt to destructure a tuple with three -elements into two variables, which won’t work.
-fn main() {
- let (x, y) = (1, 2, 3);
-}
-Attempting to compile this code results in this type error:
-$ cargo run
- Compiling patterns v0.1.0 (file:///projects/patterns)
-error[E0308]: mismatched types
- --> src/main.rs:2:9
- |
-2 | let (x, y) = (1, 2, 3);
- | ^^^^^^ --------- this expression has type `({integer}, {integer}, {integer})`
- | |
- | expected a tuple with 3 elements, found one with 2 elements
- |
- = note: expected tuple `({integer}, {integer}, {integer})`
- found tuple `(_, _)`
-
-For more information about this error, try `rustc --explain E0308`.
-error: could not compile `patterns` (bin "patterns") due to 1 previous error
-
-To fix the error, we could ignore one or more of the values in the tuple using
-_ or .., as you’ll see in the “Ignoring Values in a
-Pattern” section. If the problem
-is that we have too many variables in the pattern, the solution is to make the
-types match by removing variables so that the number of variables equals the
-number of elements in the tuple.
if let ExpressionsIn Chapter 6, we discussed how to use if let expressions mainly as a shorter
-way to write the equivalent of a match that only matches one case.
-Optionally, if let can have a corresponding else containing code to run if
-the pattern in the if let doesn’t match.
Listing 19-3 shows that it’s also possible to mix and match if let, else if, and else if let expressions. Doing so gives us more flexibility than a
-match expression in which we can express only one value to compare with the
-patterns. Also, Rust doesn’t require that the conditions in a series of if let, else if, and else if let arms relate to each other.
The code in Listing 19-3 determines what color to make your background based on -a series of checks for several conditions. For this example, we’ve created -variables with hardcoded values that a real program might receive from user -input.
-fn main() {
- let favorite_color: Option<&str> = None;
- let is_tuesday = false;
- let age: Result<u8, _> = "34".parse();
-
- if let Some(color) = favorite_color {
- println!("Using your favorite color, {color}, as the background");
- } else if is_tuesday {
- println!("Tuesday is green day!");
- } else if let Ok(age) = age {
- if age > 30 {
- println!("Using purple as the background color");
- } else {
- println!("Using orange as the background color");
- }
- } else {
- println!("Using blue as the background color");
- }
-}
-if let, else if, else if let, and elseIf the user specifies a favorite color, that color is used as the background. -If no favorite color is specified and today is Tuesday, the background color is -green. Otherwise, if the user specifies their age as a string and we can parse -it as a number successfully, the color is either purple or orange depending on -the value of the number. If none of these conditions apply, the background -color is blue.
-This conditional structure lets us support complex requirements. With the
-hardcoded values we have here, this example will print Using purple as the background color.
You can see that if let can also introduce new variables that shadow existing
-variables in the same way that match arms can: The line if let Ok(age) = age
-introduces a new age variable that contains the value inside the Ok variant,
-shadowing the existing age variable. This means we need to place the if age > 30 condition within that block: We can’t combine these two conditions into if let Ok(age) = age && age > 30. The new age we want to compare to 30 isn’t
-valid until the new scope starts with the curly bracket.
The downside of using if let expressions is that the compiler doesn’t check
-for exhaustiveness, whereas with match expressions it does. If we omitted the
-last else block and therefore missed handling some cases, the compiler would
-not alert us to the possible logic bug.
while let Conditional LoopsSimilar in construction to if let, the while let conditional loop allows a
-while loop to run for as long as a pattern continues to match. In Listing
-19-4, we show a while let loop that waits on messages sent between threads,
-but in this case checking a Result instead of an Option.
fn main() {
- let (tx, rx) = std::sync::mpsc::channel();
- std::thread::spawn(move || {
- for val in [1, 2, 3] {
- tx.send(val).unwrap();
- }
- });
-
- while let Ok(value) = rx.recv() {
- println!("{value}");
- }
-}
-while let loop to print values for as long as rx.recv() returns OkThis example prints 1, 2, and then 3. The recv method takes the first
-message out of the receiver side of the channel and returns an Ok(value). When
-we first saw recv back in Chapter 16, we unwrapped the error directly, or
-we interacted with it as an iterator using a for loop. As Listing 19-4 shows,
-though, we can also use while let, because the recv method returns an Ok
-each time a message arrives, as long as the sender exists, and then produces an
-Err once the sender side disconnects.
for LoopsIn a for loop, the value that directly follows the keyword for is a
-pattern. For example, in for x in y, the x is the pattern. Listing 19-5
-demonstrates how to use a pattern in a for loop to destructure, or break
-apart, a tuple as part of the for loop.
fn main() {
- let v = vec!['a', 'b', 'c'];
-
- for (index, value) in v.iter().enumerate() {
- println!("{value} is at index {index}");
- }
-}
-for loop to destructure a tupleThe code in Listing 19-5 will print the following:
-$ cargo run
- Compiling patterns v0.1.0 (file:///projects/patterns)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.52s
- Running `target/debug/patterns`
-a is at index 0
-b is at index 1
-c is at index 2
-
-We adapt an iterator using the enumerate method so that it produces a value
-and the index for that value, placed into a tuple. The first value produced is
-the tuple (0, 'a'). When this value is matched to the pattern (index, value), index will be 0 and value will be 'a', printing the first line of
-the output.
Function parameters can also be patterns. The code in Listing 19-6, which
-declares a function named foo that takes one parameter named x of type
-i32, should by now look familiar.
fn foo(x: i32) {
- // code goes here
-}
-
-fn main() {}
-The x part is a pattern! As we did with let, we could match a tuple in a
-function’s arguments to the pattern. Listing 19-7 splits the values in a tuple
-as we pass it to a function.
fn print_coordinates(&(x, y): &(i32, i32)) {
- println!("Current location: ({x}, {y})");
-}
-
-fn main() {
- let point = (3, 5);
- print_coordinates(&point);
-}
-This code prints Current location: (3, 5). The values &(3, 5) match the
-pattern &(x, y), so x is the value 3 and y is the value 5.
We can also use patterns in closure parameter lists in the same way as in -function parameter lists because closures are similar to functions, as -discussed in Chapter 13.
-At this point, you’ve seen several ways to use patterns, but patterns don’t -work the same in every place we can use them. In some places, the patterns must -be irrefutable; in other circumstances, they can be refutable. We’ll discuss -these two concepts next.
- -Redirecting to... ch20-01-unsafe-rust.html.
- - - - diff --git a/build/ch19-02-refutability.html b/build/ch19-02-refutability.html deleted file mode 100644 index 824cecd..0000000 --- a/build/ch19-02-refutability.html +++ /dev/null @@ -1,350 +0,0 @@ - - - - - -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
-Patterns come in two forms: refutable and irrefutable. Patterns that will match
-for any possible value passed are irrefutable. An example would be x in the
-statement let x = 5; because x matches anything and therefore cannot fail
-to match. Patterns that can fail to match for some possible value are
-refutable. An example would be Some(x) in the expression if let Some(x) = a_value because if the value in the a_value variable is None rather than
-Some, the Some(x) pattern will not match.
Function parameters, let statements, and for loops can only accept
-irrefutable patterns because the program cannot do anything meaningful when
-values don’t match. The if let and while let expressions and the
-let...else statement accept refutable and irrefutable patterns, but the
-compiler warns against irrefutable patterns because, by definition, they’re
-intended to handle possible failure: The functionality of a conditional is in
-its ability to perform differently depending on success or failure.
In general, you shouldn’t have to worry about the distinction between refutable -and irrefutable patterns; however, you do need to be familiar with the concept -of refutability so that you can respond when you see it in an error message. In -those cases, you’ll need to change either the pattern or the construct you’re -using the pattern with, depending on the intended behavior of the code.
-Let’s look at an example of what happens when we try to use a refutable pattern
-where Rust requires an irrefutable pattern and vice versa. Listing 19-8 shows a
-let statement, but for the pattern, we’ve specified Some(x), a refutable
-pattern. As you might expect, this code will not compile.
fn main() {
- let some_option_value: Option<i32> = None;
- let Some(x) = some_option_value;
-}
-letIf some_option_value were a None value, it would fail to match the pattern
-Some(x), meaning the pattern is refutable. However, the let statement can
-only accept an irrefutable pattern because there is nothing valid the code can
-do with a None value. At compile time, Rust will complain that we’ve tried to
-use a refutable pattern where an irrefutable pattern is required:
$ cargo run
- Compiling patterns v0.1.0 (file:///projects/patterns)
-error[E0005]: refutable pattern in local binding
- --> src/main.rs:3:9
- |
-3 | let Some(x) = some_option_value;
- | ^^^^^^^ pattern `None` not covered
- |
- = note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
- = note: for more information, visit https://doc.rust-lang.org/book/ch19-02-refutability.html
- = note: the matched value is of type `Option<i32>`
-help: you might want to use `let else` to handle the variant that isn't matched
- |
-3 | let Some(x) = some_option_value else { todo!() };
- | ++++++++++++++++
-
-For more information about this error, try `rustc --explain E0005`.
-error: could not compile `patterns` (bin "patterns") due to 1 previous error
-
-Because we didn’t cover (and couldn’t cover!) every valid value with the
-pattern Some(x), Rust rightfully produces a compiler error.
If we have a refutable pattern where an irrefutable pattern is needed, we can
-fix it by changing the code that uses the pattern: Instead of using let, we
-can use let...else. Then, if the pattern doesn’t match, the code in the curly
-brackets will handle the value. Listing 19-9 shows how to fix the code in
-Listing 19-8.
fn main() {
- let some_option_value: Option<i32> = None;
- let Some(x) = some_option_value else {
- return;
- };
-}
-let...else and a block with refutable patterns instead of letWe’ve given the code an out! This code is perfectly valid, although it means we
-cannot use an irrefutable pattern without receiving a warning. If we give
-let...else a pattern that will always match, such as x, as shown in Listing
-19-10, the compiler will give a warning.
fn main() {
- let x = 5 else {
- return;
- };
-}
-let...elseRust complains that it doesn’t make sense to use let...else with an
-irrefutable pattern:
$ cargo run
- Compiling patterns v0.1.0 (file:///projects/patterns)
-warning: irrefutable `let...else` pattern
- --> src/main.rs:2:5
- |
-2 | let x = 5 else {
- | ^^^^^^^^^
- |
- = note: this pattern will always match, so the `else` clause is useless
- = help: consider removing the `else` clause
- = note: `#[warn(irrefutable_let_patterns)]` on by default
-
-warning: `patterns` (bin "patterns") generated 1 warning
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.39s
- Running `target/debug/patterns`
-
-For this reason, match arms must use refutable patterns, except for the last
-arm, which should match any remaining values with an irrefutable pattern. Rust
-allows us to use an irrefutable pattern in a match with only one arm, but
-this syntax isn’t particularly useful and could be replaced with a simpler
-let statement.
Now that you know where to use patterns and the difference between refutable -and irrefutable patterns, let’s cover all the syntax we can use to create -patterns.
- -Redirecting to... ch20-02-advanced-traits.html.
- - - - diff --git a/build/ch19-04-advanced-types.html b/build/ch19-04-advanced-types.html deleted file mode 100644 index 3937ec4..0000000 --- a/build/ch19-04-advanced-types.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - -Redirecting to... ch20-03-advanced-types.html.
- - - - diff --git a/build/ch19-05-advanced-functions-and-closures.html b/build/ch19-05-advanced-functions-and-closures.html deleted file mode 100644 index c3008b4..0000000 --- a/build/ch19-05-advanced-functions-and-closures.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - -Redirecting to... ch20-04-advanced-functions-and-closures.html.
- - - - diff --git a/build/ch19-06-macros.html b/build/ch19-06-macros.html deleted file mode 100644 index ff406de..0000000 --- a/build/ch19-06-macros.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - -Redirecting to... ch20-05-macros.html.
- - - - diff --git a/build/ch20-00-advanced-features.html b/build/ch20-00-advanced-features.html deleted file mode 100644 index 8b07eb5..0000000 --- a/build/ch20-00-advanced-features.html +++ /dev/null @@ -1,262 +0,0 @@ - - - - - -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
-By now, you’ve learned the most commonly used parts of the Rust programming -language. Before we do one more project, in Chapter 21, we’ll look at a few -aspects of the language you might run into every once in a while but may not -use every day. You can use this chapter as a reference for when you encounter -any unknowns. The features covered here are useful in very specific situations. -Although you might not reach for them often, we want to make sure you have a -grasp of all the features Rust has to offer.
-In this chapter, we’ll cover:
-It’s a panoply of Rust features with something for everyone! Let’s dive in!
- -Redirecting to... ch21-00-final-project-a-web-server.html.
- - - - diff --git a/build/ch20-01-single-threaded.html b/build/ch20-01-single-threaded.html deleted file mode 100644 index 1ac1006..0000000 --- a/build/ch20-01-single-threaded.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - -Redirecting to... ch21-01-single-threaded.html.
- - - - diff --git a/build/ch20-02-multithreaded.html b/build/ch20-02-multithreaded.html deleted file mode 100644 index 173a9b4..0000000 --- a/build/ch20-02-multithreaded.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - -Redirecting to... ch21-02-multithreaded.html.
- - - - diff --git a/build/ch20-03-advanced-traits.html b/build/ch20-03-advanced-traits.html deleted file mode 100644 index 9d60e3e..0000000 --- a/build/ch20-03-advanced-traits.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - -Redirecting to... ch20-02-advanced-traits.html.
- - - - diff --git a/build/ch20-03-advanced-types.html b/build/ch20-03-advanced-types.html deleted file mode 100644 index d3c7837..0000000 --- a/build/ch20-03-advanced-types.html +++ /dev/null @@ -1,585 +0,0 @@ - - - - - -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
-The Rust type system has some features that we’ve so far mentioned but haven’t
-yet discussed. We’ll start by discussing newtypes in general as we examine why
-they are useful as types. Then, we’ll move on to type aliases, a feature
-similar to newtypes but with slightly different semantics. We’ll also discuss
-the ! type and dynamically sized types.
This section assumes you’ve read the earlier section “Implementing External
-Traits with the Newtype Pattern”. The newtype pattern
-is also useful for tasks beyond those we’ve discussed so far, including
-statically enforcing that values are never confused and indicating the units of
-a value. You saw an example of using newtypes to indicate units in Listing
-20-16: Recall that the Millimeters and Meters structs wrapped u32 values
-in a newtype. If we wrote a function with a parameter of type Millimeters, we
-wouldn’t be able to compile a program that accidentally tried to call that
-function with a value of type Meters or a plain u32.
We can also use the newtype pattern to abstract away some implementation -details of a type: The new type can expose a public API that is different from -the API of the private inner type.
-Newtypes can also hide internal implementation. For example, we could provide a
-People type to wrap a HashMap<i32, String> that stores a person’s ID
-associated with their name. Code using People would only interact with the
-public API we provide, such as a method to add a name string to the People
-collection; that code wouldn’t need to know that we assign an i32 ID to names
-internally. The newtype pattern is a lightweight way to achieve encapsulation
-to hide implementation details, which we discussed in the “Encapsulation that
-Hides Implementation
-Details”
-section in Chapter 18.
Rust provides the ability to declare a type alias to give an existing type
-another name. For this we use the type keyword. For example, we can create
-the alias Kilometers to i32 like so:
fn main() {
- type Kilometers = i32;
-
- let x: i32 = 5;
- let y: Kilometers = 5;
-
- println!("x + y = {}", x + y);
-}
-Now the alias Kilometers is a synonym for i32; unlike the Millimeters
-and Meters types we created in Listing 20-16, Kilometers is not a separate,
-new type. Values that have the type Kilometers will be treated the same as
-values of type i32:
fn main() {
- type Kilometers = i32;
-
- let x: i32 = 5;
- let y: Kilometers = 5;
-
- println!("x + y = {}", x + y);
-}
-Because Kilometers and i32 are the same type, we can add values of both
-types and can pass Kilometers values to functions that take i32
-parameters. However, using this method, we don’t get the type-checking benefits
-that we get from the newtype pattern discussed earlier. In other words, if we
-mix up Kilometers and i32 values somewhere, the compiler will not give us
-an error.
The main use case for type synonyms is to reduce repetition. For example, we -might have a lengthy type like this:
-Box<dyn Fn() + Send + 'static>
-Writing this lengthy type in function signatures and as type annotations all -over the code can be tiresome and error-prone. Imagine having a project full of -code like that in Listing 20-25.
-fn main() {
- let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi"));
-
- fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) {
- // --snip--
- }
-
- fn returns_long_type() -> Box<dyn Fn() + Send + 'static> {
- // --snip--
- Box::new(|| ())
- }
-}
-A type alias makes this code more manageable by reducing the repetition. In
-Listing 20-26, we’ve introduced an alias named Thunk for the verbose type and
-can replace all uses of the type with the shorter alias Thunk.
fn main() {
- type Thunk = Box<dyn Fn() + Send + 'static>;
-
- let f: Thunk = Box::new(|| println!("hi"));
-
- fn takes_long_type(f: Thunk) {
- // --snip--
- }
-
- fn returns_long_type() -> Thunk {
- // --snip--
- Box::new(|| ())
- }
-}
-Thunk, to reduce repetitionThis code is much easier to read and write! Choosing a meaningful name for a -type alias can help communicate your intent as well (thunk is a word for code -to be evaluated at a later time, so it’s an appropriate name for a closure that -gets stored).
-Type aliases are also commonly used with the Result<T, E> type for reducing
-repetition. Consider the std::io module in the standard library. I/O
-operations often return a Result<T, E> to handle situations when operations
-fail to work. This library has a std::io::Error struct that represents all
-possible I/O errors. Many of the functions in std::io will be returning
-Result<T, E> where the E is std::io::Error, such as these functions in
-the Write trait:
use std::fmt;
-use std::io::Error;
-
-pub trait Write {
- fn write(&mut self, buf: &[u8]) -> Result<usize, Error>;
- fn flush(&mut self) -> Result<(), Error>;
-
- fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>;
- fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<(), Error>;
-}
-The Result<..., Error> is repeated a lot. As such, std::io has this type
-alias declaration:
use std::fmt;
-
-type Result<T> = std::result::Result<T, std::io::Error>;
-
-pub trait Write {
- fn write(&mut self, buf: &[u8]) -> Result<usize>;
- fn flush(&mut self) -> Result<()>;
-
- fn write_all(&mut self, buf: &[u8]) -> Result<()>;
- fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
-}
-Because this declaration is in the std::io module, we can use the fully
-qualified alias std::io::Result<T>; that is, a Result<T, E> with the E
-filled in as std::io::Error. The Write trait function signatures end up
-looking like this:
use std::fmt;
-
-type Result<T> = std::result::Result<T, std::io::Error>;
-
-pub trait Write {
- fn write(&mut self, buf: &[u8]) -> Result<usize>;
- fn flush(&mut self) -> Result<()>;
-
- fn write_all(&mut self, buf: &[u8]) -> Result<()>;
- fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
-}
-The type alias helps in two ways: It makes code easier to write and it gives
-us a consistent interface across all of std::io. Because it’s an alias, it’s
-just another Result<T, E>, which means we can use any methods that work on
-Result<T, E> with it, as well as special syntax like the ? operator.
Rust has a special type named ! that’s known in type theory lingo as the
-empty type because it has no values. We prefer to call it the never type
-because it stands in the place of the return type when a function will never
-return. Here is an example:
fn bar() -> ! {
- // --snip--
- panic!();
-}
-This code is read as “the function bar returns never.” Functions that return
-never are called diverging functions. We can’t create values of the type !,
-so bar can never possibly return.
But what use is a type you can never create values for? Recall the code from -Listing 2-5, part of the number-guessing game; we’ve reproduced a bit of it -here in Listing 20-27.
-use std::cmp::Ordering;
-use std::io;
-
-use rand::Rng;
-
-fn main() {
- println!("Guess the number!");
-
- let secret_number = rand::thread_rng().gen_range(1..=100);
-
- println!("The secret number is: {secret_number}");
-
- loop {
- println!("Please input your guess.");
-
- let mut guess = String::new();
-
- // --snip--
-
- io::stdin()
- .read_line(&mut guess)
- .expect("Failed to read line");
-
- let guess: u32 = match guess.trim().parse() {
- Ok(num) => num,
- Err(_) => continue,
- };
-
- println!("You guessed: {guess}");
-
- // --snip--
-
- match guess.cmp(&secret_number) {
- Ordering::Less => println!("Too small!"),
- Ordering::Greater => println!("Too big!"),
- Ordering::Equal => {
- println!("You win!");
- break;
- }
- }
- }
-}
-match with an arm that ends in continueAt the time, we skipped over some details in this code. In “The match
-Control Flow Construct”
-section in Chapter 6, we discussed that match arms must all return the same
-type. So, for example, the following code doesn’t work:
fn main() {
- let guess = "3";
- let guess = match guess.trim().parse() {
- Ok(_) => 5,
- Err(_) => "hello",
- };
-}
-The type of guess in this code would have to be an integer and a string,
-and Rust requires that guess have only one type. So, what does continue
-return? How were we allowed to return a u32 from one arm and have another arm
-that ends with continue in Listing 20-27?
As you might have guessed, continue has a ! value. That is, when Rust
-computes the type of guess, it looks at both match arms, the former with a
-value of u32 and the latter with a ! value. Because ! can never have a
-value, Rust decides that the type of guess is u32.
The formal way of describing this behavior is that expressions of type ! can
-be coerced into any other type. We’re allowed to end this match arm with
-continue because continue doesn’t return a value; instead, it moves control
-back to the top of the loop, so in the Err case, we never assign a value to
-guess.
The never type is useful with the panic! macro as well. Recall the unwrap
-function that we call on Option<T> values to produce a value or panic with
-this definition:
enum Option<T> {
- Some(T),
- None,
-}
-
-use crate::Option::*;
-
-impl<T> Option<T> {
- pub fn unwrap(self) -> T {
- match self {
- Some(val) => val,
- None => panic!("called `Option::unwrap()` on a `None` value"),
- }
- }
-}
-In this code, the same thing happens as in the match in Listing 20-27: Rust
-sees that val has the type T and panic! has the type !, so the result
-of the overall match expression is T. This code works because panic!
-doesn’t produce a value; it ends the program. In the None case, we won’t be
-returning a value from unwrap, so this code is valid.
One final expression that has the type ! is a loop:
fn main() {
- print!("forever ");
-
- loop {
- print!("and ever ");
- }
-}
-Here, the loop never ends, so ! is the value of the expression. However, this
-wouldn’t be true if we included a break, because the loop would terminate
-when it got to the break.
Sized TraitRust needs to know certain details about its types, such as how much space to -allocate for a value of a particular type. This leaves one corner of its type -system a little confusing at first: the concept of dynamically sized types. -Sometimes referred to as DSTs or unsized types, these types let us write -code using values whose size we can know only at runtime.
-Let’s dig into the details of a dynamically sized type called str, which
-we’ve been using throughout the book. That’s right, not &str, but str on
-its own, is a DST. In many cases, such as when storing text entered by a user,
-we can’t know how long the string is until runtime. That means we can’t create
-a variable of type str, nor can we take an argument of type str. Consider
-the following code, which does not work:
fn main() {
- let s1: str = "Hello there!";
- let s2: str = "How's it going?";
-}
-Rust needs to know how much memory to allocate for any value of a particular
-type, and all values of a type must use the same amount of memory. If Rust
-allowed us to write this code, these two str values would need to take up the
-same amount of space. But they have different lengths: s1 needs 12 bytes of
-storage and s2 needs 15. This is why it’s not possible to create a variable
-holding a dynamically sized type.
So, what do we do? In this case, you already know the answer: We make the type
-of s1 and s2 string slice (&str) rather than str. Recall from the
-“String Slices” section in Chapter 4 that the
-slice data structure only stores the starting position and the length of the
-slice. So, although &T is a single value that stores the memory address of
-where the T is located, a string slice is two values: the address of the
-str and its length. As such, we can know the size of a string slice value at
-compile time: It’s twice the length of a usize. That is, we always know the
-size of a string slice, no matter how long the string it refers to is. In
-general, this is the way in which dynamically sized types are used in Rust:
-They have an extra bit of metadata that stores the size of the dynamic
-information. The golden rule of dynamically sized types is that we must always
-put values of dynamically sized types behind a pointer of some kind.
We can combine str with all kinds of pointers: for example, Box<str> or
-Rc<str>. In fact, you’ve seen this before but with a different dynamically
-sized type: traits. Every trait is a dynamically sized type we can refer to by
-using the name of the trait. In the “Using Trait Objects to Abstract over
-Shared Behavior” section in Chapter 18, we mentioned that to use traits as trait
-objects, we must put them behind a pointer, such as &dyn Trait or Box<dyn Trait> (Rc<dyn Trait> would work too).
To work with DSTs, Rust provides the Sized trait to determine whether or not
-a type’s size is known at compile time. This trait is automatically implemented
-for everything whose size is known at compile time. In addition, Rust
-implicitly adds a bound on Sized to every generic function. That is, a
-generic function definition like this:
fn generic<T>(t: T) {
- // --snip--
-}
-is actually treated as though we had written this:
-fn generic<T: Sized>(t: T) {
- // --snip--
-}
-By default, generic functions will work only on types that have a known size at -compile time. However, you can use the following special syntax to relax this -restriction:
-fn generic<T: ?Sized>(t: &T) {
- // --snip--
-}
-A trait bound on ?Sized means “T may or may not be Sized,” and this
-notation overrides the default that generic types must have a known size at
-compile time. The ?Trait syntax with this meaning is only available for
-Sized, not any other traits.
Also note that we switched the type of the t parameter from T to &T.
-Because the type might not be Sized, we need to use it behind some kind of
-pointer. In this case, we’ve chosen a reference.
Next, we’ll talk about functions and closures!
- -Redirecting to... ch21-03-graceful-shutdown-and-cleanup.html.
- - - - diff --git a/build/ch20-04-advanced-functions-and-closures.html b/build/ch20-04-advanced-functions-and-closures.html deleted file mode 100644 index 8384dfb..0000000 --- a/build/ch20-04-advanced-functions-and-closures.html +++ /dev/null @@ -1,443 +0,0 @@ - - - - - -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
-This section explores some advanced features related to functions and closures, -including function pointers and returning closures.
-We’ve talked about how to pass closures to functions; you can also pass regular
-functions to functions! This technique is useful when you want to pass a
-function you’ve already defined rather than defining a new closure. Functions
-coerce to the type fn (with a lowercase f), not to be confused with the
-Fn closure trait. The fn type is called a function pointer. Passing
-functions with function pointers will allow you to use functions as arguments
-to other functions.
The syntax for specifying that a parameter is a function pointer is similar to
-that of closures, as shown in Listing 20-28, where we’ve defined a function
-add_one that adds 1 to its parameter. The function do_twice takes two
-parameters: a function pointer to any function that takes an i32 parameter
-and returns an i32, and one i32 value. The do_twice function calls the
-function f twice, passing it the arg value, then adds the two function call
-results together. The main function calls do_twice with the arguments
-add_one and 5.
fn add_one(x: i32) -> i32 {
- x + 1
-}
-
-fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
- f(arg) + f(arg)
-}
-
-fn main() {
- let answer = do_twice(add_one, 5);
-
- println!("The answer is: {answer}");
-}
-fn type to accept a function pointer as an argumentThis code prints The answer is: 12. We specify that the parameter f in
-do_twice is an fn that takes one parameter of type i32 and returns an
-i32. We can then call f in the body of do_twice. In main, we can pass
-the function name add_one as the first argument to do_twice.
Unlike closures, fn is a type rather than a trait, so we specify fn as the
-parameter type directly rather than declaring a generic type parameter with one
-of the Fn traits as a trait bound.
Function pointers implement all three of the closure traits (Fn, FnMut, and
-FnOnce), meaning you can always pass a function pointer as an argument for a
-function that expects a closure. It’s best to write functions using a generic
-type and one of the closure traits so that your functions can accept either
-functions or closures.
That said, one example of where you would want to only accept fn and not
-closures is when interfacing with external code that doesn’t have closures: C
-functions can accept functions as arguments, but C doesn’t have closures.
As an example of where you could use either a closure defined inline or a named
-function, let’s look at a use of the map method provided by the Iterator
-trait in the standard library. To use the map method to turn a vector of
-numbers into a vector of strings, we could use a closure, as in Listing 20-29.
fn main() {
- let list_of_numbers = vec![1, 2, 3];
- let list_of_strings: Vec<String> =
- list_of_numbers.iter().map(|i| i.to_string()).collect();
-}
-map method to convert numbers to stringsOr we could name a function as the argument to map instead of the closure.
-Listing 20-30 shows what this would look like.
fn main() {
- let list_of_numbers = vec![1, 2, 3];
- let list_of_strings: Vec<String> =
- list_of_numbers.iter().map(ToString::to_string).collect();
-}
-String::to_string function with the map method to convert numbers to stringsNote that we must use the fully qualified syntax that we talked about in the
-“Advanced Traits” section because there are
-multiple functions available named to_string.
Here, we’re using the to_string function defined in the ToString trait,
-which the standard library has implemented for any type that implements
-Display.
Recall from the “Enum Values” section in Chapter -6 that the name of each enum variant that we define also becomes an initializer -function. We can use these initializer functions as function pointers that -implement the closure traits, which means we can specify the initializer -functions as arguments for methods that take closures, as seen in Listing 20-31.
-fn main() {
- enum Status {
- Value(u32),
- Stop,
- }
-
- let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect();
-}
-map method to create a Status instance from numbersHere, we create Status::Value instances using each u32 value in the range
-that map is called on by using the initializer function of Status::Value.
-Some people prefer this style and some people prefer to use closures. They
-compile to the same code, so use whichever style is clearer to you.
Closures are represented by traits, which means you can’t return closures
-directly. In most cases where you might want to return a trait, you can instead
-use the concrete type that implements the trait as the return value of the
-function. However, you can’t usually do that with closures because they don’t
-have a concrete type that is returnable; you’re not allowed to use the function
-pointer fn as a return type if the closure captures any values from its
-scope, for example.
Instead, you will normally use the impl Trait syntax we learned about in
-Chapter 10. You can return any function type, using Fn, FnOnce, and FnMut.
-For example, the code in Listing 20-32 will compile just fine.
#![allow(unused)]
-fn main() {
-fn returns_closure() -> impl Fn(i32) -> i32 {
- |x| x + 1
-}
-}
-impl Trait syntaxHowever, as we noted in the “Inferring and Annotating Closure -Types” section in Chapter 13, each closure is -also its own distinct type. If you need to work with multiple functions that -have the same signature but different implementations, you will need to use a -trait object for them. Consider what happens if you write code like that shown -in Listing 20-33.
-fn main() {
- let handlers = vec![returns_closure(), returns_initialized_closure(123)];
- for handler in handlers {
- let output = handler(5);
- println!("{output}");
- }
-}
-
-fn returns_closure() -> impl Fn(i32) -> i32 {
- |x| x + 1
-}
-
-fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
- move |x| x + init
-}
-Vec<T> of closures defined by functions that return impl Fn typesHere we have two functions, returns_closure and returns_initialized_closure,
-which both return impl Fn(i32) -> i32. Notice that the closures that they
-return are different, even though they implement the same type. If we try to
-compile this, Rust lets us know that it won’t work:
$ cargo build
- Compiling functions-example v0.1.0 (file:///projects/functions-example)
-error[E0308]: mismatched types
- --> src/main.rs:2:44
- |
- 2 | let handlers = vec![returns_closure(), returns_initialized_closure(123)];
- | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected opaque type, found a different opaque type
-...
- 9 | fn returns_closure() -> impl Fn(i32) -> i32 {
- | ------------------- the expected opaque type
-...
-13 | fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
- | ------------------- the found opaque type
- |
- = note: expected opaque type `impl Fn(i32) -> i32`
- found opaque type `impl Fn(i32) -> i32`
- = note: distinct uses of `impl Trait` result in different opaque types
-
-For more information about this error, try `rustc --explain E0308`.
-error: could not compile `functions-example` (bin "functions-example") due to 1 previous error
-
-The error message tells us that whenever we return an impl Trait, Rust
-creates a unique opaque type, a type where we cannot see into the details of
-what Rust constructs for us, nor can we guess the type Rust will generate to
-write ourselves. So, even though these functions return closures that implement
-the same trait, Fn(i32) -> i32, the opaque types Rust generates for each are
-distinct. (This is similar to how Rust produces different concrete types for
-distinct async blocks even when they have the same output type, as we saw in
-“The Pin Type and the Unpin Trait” in
-Chapter 17.) We have seen a solution to this problem a few times now: We can
-use a trait object, as in Listing 20-34.
fn main() {
- let handlers = vec![returns_closure(), returns_initialized_closure(123)];
- for handler in handlers {
- let output = handler(5);
- println!("{output}");
- }
-}
-
-fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
- Box::new(|x| x + 1)
-}
-
-fn returns_initialized_closure(init: i32) -> Box<dyn Fn(i32) -> i32> {
- Box::new(move |x| x + init)
-}
-Vec<T> of closures defined by functions that return Box<dyn Fn> so that they have the same typeThis code will compile just fine. For more about trait objects, refer to the -section “Using Trait Objects To Abstract over Shared -Behavior” in Chapter 18.
-Next, let’s look at macros!
- -Redirecting to... ch20-03-advanced-types.html.
- - - - diff --git a/build/ch20-05-advanced-functions-and-closures.html b/build/ch20-05-advanced-functions-and-closures.html deleted file mode 100644 index c3008b4..0000000 --- a/build/ch20-05-advanced-functions-and-closures.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - -Redirecting to... ch20-04-advanced-functions-and-closures.html.
- - - - diff --git a/build/ch20-06-macros.html b/build/ch20-06-macros.html deleted file mode 100644 index ff406de..0000000 --- a/build/ch20-06-macros.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - -Redirecting to... ch20-05-macros.html.
- - - - diff --git a/build/ch21-00-final-project-a-web-server.html b/build/ch21-00-final-project-a-web-server.html deleted file mode 100644 index 9549ade..0000000 --- a/build/ch21-00-final-project-a-web-server.html +++ /dev/null @@ -1,276 +0,0 @@ - - - - - -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
-It’s been a long journey, but we’ve reached the end of the book. In this -chapter, we’ll build one more project together to demonstrate some of the -concepts we covered in the final chapters, as well as recap some earlier -lessons.
-For our final project, we’ll make a web server that says “Hello!” and looks like -Figure 21-1 in a web browser.
-Here is our plan for building the web server:
-
-Figure 21-1: Our final shared project
-Before we get started, we should mention two details. First, the method we’ll -use won’t be the best way to build a web server with Rust. Community members -have published a number of production-ready crates available at -crates.io that provide more complete web server and -thread pool implementations than we’ll build. However, our intention in this -chapter is to help you learn, not to take the easy route. Because Rust is a -systems programming language, we can choose the level of abstraction we want to -work with and can go to a lower level than is possible or practical in other -languages.
-Second, we will not be using async and await here. Building a thread pool is a -big enough challenge on its own, without adding in building an async runtime! -However, we will note how async and await might be applicable to some of the -same problems we will see in this chapter. Ultimately, as we noted back in -Chapter 17, many async runtimes use thread pools for managing their work.
-We’ll therefore write the basic HTTP server and thread pool manually so that -you can learn the general ideas and techniques behind the crates you might use -in the future.
- -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
-The Rust programming language has come a long way in a few short years, from -its creation and incubation by a small and nascent community of enthusiasts, to -becoming one of the most loved and in-demand programming languages in the -world. Looking back, it was inevitable that the power and promise of Rust would -turn heads and gain a foothold in systems programming. What was not inevitable -was the global growth in interest and innovation that permeated through open -source communities and catalyzed wide-scale adoption across industries.
-At this point in time, it is easy to point to the wonderful features that Rust -has to offer to explain this explosion in interest and adoption. Who doesn’t -want memory safety, and fast performance, and a friendly compiler, and -great tooling, among a host of other wonderful features? The Rust language you -see today combines years of research in systems programming with the practical -wisdom of a vibrant and passionate community. This language was designed with -purpose and crafted with care, offering developers a tool that makes it easier -to write safe, fast, and reliable code.
-But what makes Rust truly special is its roots in empowering you, the user, to -achieve your goals. This is a language that wants you to succeed, and the -principle of empowerment runs through the core of the community that builds, -maintains, and advocates for this language. Since the previous edition of this -definitive text, Rust has further developed into a truly global and trusted -language. The Rust Project is now robustly supported by the Rust Foundation, -which also invests in key initiatives to ensure that Rust is secure, stable, -and sustainable.
-This edition of The Rust Programming Language is a comprehensive update, -reflecting the language’s evolution over the years and providing valuable new -information. But it is not just a guide to syntax and libraries—it’s an -invitation to join a community that values quality, performance, and thoughtful -design. Whether you’re a seasoned developer looking to explore Rust for the -first time or an experienced Rustacean looking to refine your skills, this -edition offers something for everyone.
-The Rust journey has been one of collaboration, learning, and iteration. The -growth of the language and its ecosystem is a direct reflection of the vibrant, -diverse community behind it. The contributions of thousands of developers, from -core language designers to casual contributors, are what make Rust such a -unique and powerful tool. By picking up this book, you’re not just learning a -new programming language—you’re joining a movement to make software better, -safer, and more enjoyable to work with.
-Welcome to the Rust community!
-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
-by Steve Klabnik, Carol Nichols, and Chris Krycho, with contributions from the -Rust Community
-This version of the text assumes you’re using Rust 1.90.0 (released 2025-09-18)
-or later with edition = "2024" in the Cargo.toml file of all projects to
-configure them to use Rust 2024 Edition idioms. See the “Installation” section
-of Chapter 1 for instructions on installing or
-updating Rust, and see Appendix E for information
-on editions.
The HTML format is available online at
-https://doc.rust-lang.org/stable/book/
-and offline with installations of Rust made with rustup; run rustup doc --book to open.
Several community translations are also available.
-This text is available in paperback and ebook format from No Starch -Press.
--- -🚨 Want a more interactive learning experience? Try out a different version -of the Rust Book, featuring: quizzes, highlighting, visualizations, and -more: https://rust-book.cs.brown.edu
-