399 lines
23 KiB
HTML
399 lines
23 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>Structs, enums, and packages — from zero</title>
|
|
<link rel="stylesheet" href="../assets/style.css" />
|
|
</head>
|
|
<body>
|
|
<h1>Structs, enums, and packages</h1>
|
|
<p class="subtitle">Lesson 0004 · read this before <a href="0003-build-a-task-cli.html">0003</a> · reading, not typing · ~25 minutes</p>
|
|
|
|
<p>Three ideas, taught from nothing. Every code block below was run in a real project and every output and error message on this page is copied from that run — none of it is written from memory.</p>
|
|
|
|
<p>The domain is a café, deliberately. <a href="0003-build-a-task-cli.html">Lesson 0003</a> is a task CLI, so nothing here can be pasted into it. You will have to translate, and translating is where the understanding happens.</p>
|
|
|
|
<div class="callout">
|
|
<strong>Read this one. Do not type it.</strong> This is the knowledge half. 0003 is the skill half — that is where your hands go on the keyboard. Reading is cheap and this page is short; the project is where it sticks.
|
|
</div>
|
|
|
|
<h2>Part 1 — Structs</h2>
|
|
|
|
<p>A struct is a <strong>named bundle of fields</strong>. That is genuinely all it is. If you have used an object, a record, or a dictionary with fixed keys, you already have the idea.</p>
|
|
|
|
<pre><code>struct Drink {
|
|
name: String,
|
|
shots: u32,
|
|
iced: bool,
|
|
}</code></pre>
|
|
|
|
<p>This defines a <em>type</em>. It creates nothing and allocates nothing — it tells the compiler that a thing called <code>Drink</code> has exactly these three fields with exactly these types.</p>
|
|
|
|
<h3>Building one</h3>
|
|
|
|
<pre><code>let latte = Drink {
|
|
name: String::from("latte"),
|
|
shots: 1,
|
|
iced: false,
|
|
};
|
|
|
|
println!("{} / {} shots / iced={}", latte.name, latte.shots, latte.iced);</code></pre>
|
|
<pre><code>1. latte / 1 shots / iced=false</code></pre>
|
|
|
|
<p>Two rules that catch people coming from other languages:</p>
|
|
<ul>
|
|
<li><strong>Every field must be given a value.</strong> There are no defaults and no null. If you want "no value", the type has to say so — that is what <code>Option</code> is for, and we get to it in Part 2.</li>
|
|
<li>Field order in the literal does not matter. Names do.</li>
|
|
</ul>
|
|
|
|
<h3>Methods: the <code>impl</code> block</h3>
|
|
|
|
<p>Functions that belong to a type live in a separate block. The struct says <em>what it is</em>; the <code>impl</code> block says <em>what it can do</em>.</p>
|
|
|
|
<pre><code>impl Drink {
|
|
fn new(name: &str, shots: u32) -> Drink { // no self
|
|
Drink { name: name.to_string(), shots, iced: false }
|
|
}
|
|
|
|
fn price(&self) -> u32 { // &self
|
|
250 + self.shots * 50
|
|
}
|
|
|
|
fn add_shot(&mut self) { // &mut self
|
|
self.shots += 1;
|
|
}
|
|
|
|
fn into_name(self) -> String { // self
|
|
self.name
|
|
}
|
|
}</code></pre>
|
|
|
|
<p>The first parameter is the whole lesson here. There are four possibilities and they mean four different things:</p>
|
|
|
|
<table>
|
|
<tr><th align="left">First parameter</th><th align="left">Name</th><th align="left">Called as</th><th align="left">Means</th></tr>
|
|
<tr><td>none</td><td>associated function</td><td><code>Drink::new(..)</code></td><td>Related to the type, but there is no instance yet. This is how you make one.</td></tr>
|
|
<tr><td><code>&self</code></td><td>method</td><td><code>drink.price()</code></td><td>Borrows to <strong>read</strong>. Cannot change anything.</td></tr>
|
|
<tr><td><code>&mut self</code></td><td>method</td><td><code>drink.add_shot()</code></td><td>Borrows to <strong>change</strong>. Requires the variable be <code>mut</code>.</td></tr>
|
|
<tr><td><code>self</code></td><td>consuming method</td><td><code>drink.into_name()</code></td><td><strong>Takes ownership.</strong> The caller cannot use the value afterwards.</td></tr>
|
|
</table>
|
|
|
|
<p>Rust has no <code>constructor</code> keyword. <code>new</code> is an ordinary associated function that people agreed to call <code>new</code>. Nothing enforces the name.</p>
|
|
|
|
<p>Real output from all four:</p>
|
|
<pre><code>2. price = 300
|
|
3. after add_shot: 2 shots, price = 350
|
|
4. Drink::new gave: espresso / 2 shots / iced=false
|
|
5. into_name took ownership, returned: espresso</code></pre>
|
|
|
|
<h3>What the compiler enforces</h3>
|
|
|
|
<p>Call <code>add_shot</code> on a binding that is not <code>mut</code>:</p>
|
|
<pre><code>let d = Drink::new("mocha", 1);
|
|
d.add_shot();</code></pre>
|
|
<pre><code>error[E0596]: cannot borrow `d` as mutable, as it is not declared as mutable
|
|
help: consider changing this to be mutable</code></pre>
|
|
|
|
<p>Use a value after a consuming method took it:</p>
|
|
<pre><code>let e = Drink::new("espresso", 2);
|
|
let name = e.into_name();
|
|
println!("{} {}", name, e.shots);</code></pre>
|
|
<pre><code>error[E0382]: borrow of moved value: `e`
|
|
| ----------- `e` moved due to this method call
|
|
note: `Drink::into_name` takes ownership of the receiver `self`, which moves `e`</code></pre>
|
|
|
|
<p>That second message is ownership from chapter 4, showing up in the design of your own API. The choice between <code>&self</code> and <code>self</code> is a promise to your callers about whether they keep their value. It is a design decision, not a syntax detail.</p>
|
|
|
|
<h3>Private fields — the point of structs in a library</h3>
|
|
|
|
<p><code>pub struct</code> makes the <em>type</em> visible. It does <strong>not</strong> make the fields visible. Each field needs its own <code>pub</code>, and often you want none of them:</p>
|
|
|
|
<pre><code>pub struct Menu {
|
|
items: Vec<String>, // private: no `pub`
|
|
}
|
|
|
|
impl Menu {
|
|
pub fn new() -> Menu { Menu { items: Vec::new() } }
|
|
pub fn add(&mut self, name: &str) { self.items.push(name.to_string()); }
|
|
pub fn items(&self) -> &[String] { &self.items }
|
|
}</code></pre>
|
|
|
|
<p>From outside the module, reaching for the field directly fails:</p>
|
|
<pre><code>error[E0616]: field `items` of struct `Menu` is private
|
|
| ^^^^^ private field
|
|
help: a method `items` also exists, call it with parentheses</code></pre>
|
|
|
|
<p>Note what <code>items()</code> hands back: <code>&[String]</code>, a borrowed <em>view</em>, not the <code>Vec</code> itself. Callers may read every item and cannot push, clear, or reorder. You decided what outsiders can do, and the compiler enforces it with no runtime check.</p>
|
|
|
|
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch05-01-defining-structs.html">5.1 Defining Structs</a> · <a href="https://doc.rust-lang.org/stable/book/ch05-03-method-syntax.html">5.3 Method Syntax</a> · <a href="https://doc.rust-lang.org/stable/book/ch07-03-paths-for-referring-to-an-item-in-the-module-tree.html">7.3 Paths and privacy</a></p>
|
|
|
|
<h2>Part 2 — Enums</h2>
|
|
|
|
<p>An enum lists <strong>every value this type is allowed to be</strong>. A value is exactly one of them at a time.</p>
|
|
|
|
<pre><code>enum Size {
|
|
Small,
|
|
Medium,
|
|
Large,
|
|
}</code></pre>
|
|
|
|
<p>A <code>Size</code> is small, medium, or large. Not <code>"smal"</code>, not <code>"venti"</code>, not empty, not null. Where a <code>String</code> has billions of possible values and three that you meant, <code>Size</code> has three. <strong>The illegal states no longer exist</strong>, so you never write code to check for them.</p>
|
|
|
|
<h3>Matching on yourself</h3>
|
|
|
|
<p>The most common thing an enum does is answer a question about which variant it is:</p>
|
|
|
|
<pre><code>impl Size {
|
|
fn ml(&self) -> u32 {
|
|
match self {
|
|
Size::Small => 240,
|
|
Size::Medium => 350,
|
|
Size::Large => 470,
|
|
}
|
|
}
|
|
}</code></pre>
|
|
|
|
<p><code>match</code> compares a value against patterns top to bottom and runs the first arm that fits. It is an <em>expression</em> — it produces a value, which is why there is no <code>return</code> above.</p>
|
|
|
|
<pre><code>1. Large is 470 ml
|
|
2. is it large? true</code></pre>
|
|
|
|
<h3>Exhaustiveness — the reason enums are worth it</h3>
|
|
|
|
<p>Delete one arm:</p>
|
|
<pre><code>error[E0004]: non-exhaustive patterns: `&Size::Large` not covered
|
|
| ^^^^ pattern `&Size::Large` not covered
|
|
note: `Size` defined here
|
|
= note: the matched value is of type `&Size`
|
|
help: ensure that all possible cases are being handled by adding a match arm
|
|
with a wildcard pattern or an explicit pattern as shown</code></pre>
|
|
|
|
<p>Not a warning. The program does not build. Now the version that actually pays you back — add a fourth variant and change <strong>nothing else</strong>:</p>
|
|
|
|
<pre><code>enum Size { Small, Medium, Large, ExtraLarge }</code></pre>
|
|
<pre><code>error[E0004]: non-exhaustive patterns: `&Size::ExtraLarge` not covered</code></pre>
|
|
|
|
<p>The compiler now walks you to every single place in the codebase that has to think about the new case. In a language with string constants or integer flags, adding a case is silent, and you find the places you forgot in production.</p>
|
|
|
|
<p>This is why <code>_ => {}</code> as a catch-all arm should make you pause. It silences that help forever. Use it when you genuinely mean "everything else", not to shut the compiler up.</p>
|
|
|
|
<h3>Variants that carry data</h3>
|
|
|
|
<p>This is the part with no equivalent in most languages, and the part worth slowing down for. <strong>Each variant can carry different data of a different shape.</strong></p>
|
|
|
|
<pre><code>enum Payment {
|
|
Cash { received: u32 }, // named fields, like a struct
|
|
Card(String), // one unnamed field, like a tuple
|
|
Voucher { code: String, off: u32 }, // several named fields
|
|
OnTheHouse, // nothing at all
|
|
}</code></pre>
|
|
|
|
<p>A <code>Payment</code> is one of four things, and the data it carries depends on which. A cash payment has an amount received. A voucher has a code and a discount. A free drink has nothing. There is no <code>Payment</code> that has a voucher code but is cash — that state cannot be constructed.</p>
|
|
|
|
<p>Building them:</p>
|
|
<pre><code>Payment::Cash { received: 500 }
|
|
Payment::Card(String::from("4242"))
|
|
Payment::Voucher { code: String::from("FREE10"), off: 100 }
|
|
Payment::OnTheHouse</code></pre>
|
|
|
|
<p>And matching pulls the data back out, binding it to names you can use in that arm:</p>
|
|
|
|
<pre><code>match payment {
|
|
Payment::Cash { received } => format!("cash, {received} received"),
|
|
Payment::Card(last4) => format!("card ending {last4}"),
|
|
Payment::Voucher { code, off } => format!("voucher {code}, {off} off"),
|
|
Payment::OnTheHouse => String::from("free"),
|
|
}</code></pre>
|
|
|
|
<pre><code>5. Cash { received: 500 } -> cash, 500 received
|
|
5. Card("4242") -> card ending 4242
|
|
5. Voucher { code: "FREE10", off: 100 } -> voucher FREE10, 100 off
|
|
5. OnTheHouse -> free</code></pre>
|
|
|
|
<p>Inside <code>Payment::Cash { received }</code>, the name <code>received</code> becomes a variable holding that variant's value. You cannot reach it any other way — the data is sealed inside the variant, and <code>match</code> is the key. That sealing is exactly why the compiler can promise you never read a voucher code off a cash payment.</p>
|
|
|
|
<h3>You have been using enums the whole time</h3>
|
|
|
|
<pre><code>enum Option<T> { Some(T), None }
|
|
enum Result<T, E> { Ok(T), Err(E) }</code></pre>
|
|
|
|
<p>That is their real definition — ordinary enums with data-carrying variants, no special compiler magic. Everything you learned about <code>Result</code> in <a href="../reference/rust-syntax.html">the Result pattern</a> is just this:</p>
|
|
|
|
<pre><code>match found {
|
|
Some(s) => println!("Option::Some carried a {:?}", s),
|
|
None => println!("Option::None carried nothing"),
|
|
}</code></pre>
|
|
<pre><code>6. Option::Some carried a Small
|
|
7. if let pulled out Large</code></pre>
|
|
|
|
<p>And <code>if let</code> is the shortcut for when you care about one variant and want to ignore the rest:</p>
|
|
<pre><code>if let Some(s) = Size::parse("large") {
|
|
println!("if let pulled out {:?}", s);
|
|
}</code></pre>
|
|
|
|
<h3>Option vs Result, when you write your own function</h3>
|
|
|
|
<pre><code>fn parse(text: &str) -> Option<Size> {
|
|
match text {
|
|
"small" => Some(Size::Small),
|
|
"medium" => Some(Size::Medium),
|
|
"large" => Some(Size::Large),
|
|
_ => None,
|
|
}
|
|
}</code></pre>
|
|
<pre><code>3. parse("medium") = Some(Medium)
|
|
4. parse("venti") = None</code></pre>
|
|
|
|
<p>Why <code>Option</code> and not <code>Result</code> here? Because there is nothing useful to say about the failure. "That is not a size" is the whole story, and the absence itself carries it. Reach for <code>Result</code> when the caller needs to know <em>why</em> — a file that was missing versus one you lacked permission to read.</p>
|
|
|
|
<h3>Struct or enum?</h3>
|
|
|
|
<table>
|
|
<tr><th align="left">Question</th><th align="left">Use</th></tr>
|
|
<tr><td>Is it <strong>this AND this AND this</strong>?</td><td><code>struct</code></td></tr>
|
|
<tr><td>Is it <strong>this OR this OR this</strong>?</td><td><code>enum</code></td></tr>
|
|
</table>
|
|
|
|
<p>A drink has a name <em>and</em> a shot count <em>and</em> a size → struct. A size is small <em>or</em> medium <em>or</em> large → enum. They nest freely: the struct holds a field whose type is the enum, which is precisely what <code>Drink { size: Size }</code> means and what you will build in 0003.</p>
|
|
|
|
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch06-01-defining-an-enum.html">6.1 Defining an Enum</a> · <a href="https://doc.rust-lang.org/stable/book/ch06-02-match.html">6.2 The match Control Flow Construct</a> · <a href="https://doc.rust-lang.org/stable/book/ch06-03-if-let.html">6.3 Concise Control Flow with if let</a></p>
|
|
|
|
<h2>Part 3 — Packages, crates, modules</h2>
|
|
|
|
<p>Four words that get used interchangeably and should not be. From outside in:</p>
|
|
|
|
<table>
|
|
<tr><th align="left">Word</th><th align="left">What it is</th><th align="left">Where you see it</th></tr>
|
|
<tr><td><strong>Package</strong></td><td>What Cargo manages. One <code>Cargo.toml</code>. Can hold up to one library crate and any number of binary crates.</td><td><code>cargo new cafe</code></td></tr>
|
|
<tr><td><strong>Crate</strong></td><td>What the compiler compiles, in one go. A tree of modules with a single root file.</td><td><code>src/lib.rs</code>, <code>src/main.rs</code></td></tr>
|
|
<tr><td><strong>Module</strong></td><td>A namespace inside a crate. Controls what is visible to whom.</td><td><code>pub mod menu;</code></td></tr>
|
|
<tr><td><strong>Path</strong></td><td>How you name an item: <code>cafe::menu::Menu</code>.</td><td><code>use</code> lines</td></tr>
|
|
</table>
|
|
|
|
<p>The one that matters for your project: <strong>a package can contain two crates, and they are as separate as if a stranger wrote one of them.</strong></p>
|
|
|
|
<h3>The layout</h3>
|
|
|
|
<pre><code>cafe/
|
|
├── Cargo.toml
|
|
├── src/
|
|
│ ├── lib.rs ← root of the LIBRARY crate, named `cafe`
|
|
│ ├── menu.rs ← a module in that crate
|
|
│ ├── order.rs ← another module in that crate
|
|
│ └── main.rs ← root of the BINARY crate
|
|
└── tests/
|
|
└── spec.rs ← integration tests: a separate crate again</code></pre>
|
|
|
|
<p>Cargo finds all of this by filename. Here is the entire <code>Cargo.toml</code> for the working demo, unchanged from what <code>cargo new</code> produced:</p>
|
|
|
|
<pre><code>[package]
|
|
name = "cafe"
|
|
version = "0.1.0"
|
|
edition = "2024"
|
|
|
|
[dependencies]</code></pre>
|
|
|
|
<p>No <code>[lib]</code>. No <code>[[bin]]</code>. Convention over configuration: <code>src/lib.rs</code> means "library crate", <code>src/main.rs</code> means "binary crate", and both are picked up automatically.</p>
|
|
|
|
<h3>Wiring it, one error at a time</h3>
|
|
|
|
<p>This is the sequence I actually ran, and it is the sequence you will hit.</p>
|
|
|
|
<p><strong>Step 1.</strong> <code>src/menu.rs</code> exists and contains <code>pub struct Menu</code>. <code>src/lib.rs</code> is empty.</p>
|
|
<pre><code>error[E0433]: cannot find `menu` in `cafe`</code></pre>
|
|
<p>The file existing is not enough. <strong>A module does not exist until its parent declares it.</strong> <code>src/menu.rs</code> is an unread file on disk until something says <code>mod menu;</code>.</p>
|
|
|
|
<p><strong>Step 2.</strong> Put <code>mod menu;</code> in <code>lib.rs</code>.</p>
|
|
<pre><code>error[E0603]: module `menu` is private
|
|
| private module
|
|
note: the module `menu` is defined here</code></pre>
|
|
<p>Now it exists, and it is invisible from outside. <strong>Everything in Rust is private by default</strong>, including modules. <code>mod menu;</code> means "this module is part of my crate". <code>pub mod menu;</code> means "and outsiders may use it".</p>
|
|
|
|
<p><strong>Step 3.</strong> <code>pub mod menu;</code></p>
|
|
<pre><code>["latte"]</code></pre>
|
|
<p>Working. Three states, two error messages, and each message named exactly what was wrong.</p>
|
|
|
|
<h3><code>crate::</code> versus the package name</h3>
|
|
|
|
<p>The single most common stumble, and the one to memorise:</p>
|
|
|
|
<pre><code>// src/order.rs — INSIDE the library crate, reaching a sibling module
|
|
use crate::menu::Menu;
|
|
|
|
// src/main.rs — a DIFFERENT crate, so use the library's name
|
|
use cafe::menu::Menu;
|
|
|
|
// tests/spec.rs — also a different crate, same as main.rs
|
|
use cafe::menu::Menu;</code></pre>
|
|
|
|
<p><code>crate</code> means "the root of the crate I am compiling right now". In <code>main.rs</code>, that root is <code>main.rs</code> — which has no <code>menu</code> module, so:</p>
|
|
<pre><code>error[E0432]: unresolved import `crate::menu`</code></pre>
|
|
|
|
<p>The question to ask whenever a path will not resolve: <strong>which crate am I in right now?</strong> If the file is <code>main.rs</code> or anything under <code>tests/</code>, you are outside the library and must use its name.</p>
|
|
|
|
<p>The working version, run for real:</p>
|
|
<pre><code>all items: ["latte", "mocha"]
|
|
first item: Some("latte")</code></pre>
|
|
|
|
<h3>Why bother with a library crate at all?</h3>
|
|
|
|
<p>You could put everything in <code>main.rs</code>. Two concrete reasons not to:</p>
|
|
|
|
<ol>
|
|
<li><strong>Integration tests can only reach a library.</strong> Files in <code>tests/</code> compile as separate crates and can only <code>use</code> public items from the library. They cannot see inside <code>main.rs</code> at all. If your logic lives in <code>main.rs</code>, it is untestable from <code>tests/</code>.</li>
|
|
<li><strong>It forces you to design a real boundary.</strong> Your <code>main.rs</code> becomes just another consumer, so anything awkward about your API you feel immediately — the same as an outside user would.</li>
|
|
</ol>
|
|
|
|
<pre><code>test cannot_touch_private_field ... ok
|
|
test menu_starts_empty ... ok
|
|
test result: ok. 2 passed; 0 failed</code></pre>
|
|
|
|
<h3>The privacy rules, complete</h3>
|
|
|
|
<ul>
|
|
<li>Everything is <strong>private by default</strong>: modules, structs, fields, functions, enum variants' containing type.</li>
|
|
<li><code>pub</code> is needed at <strong>every level of the path</strong>. A <code>pub fn</code> inside a private <code>mod</code> is unreachable from outside.</li>
|
|
<li><code>pub struct</code> does <strong>not</strong> make fields public. Each field needs its own <code>pub</code>.</li>
|
|
<li><code>pub enum</code> <strong>does</strong> make all its variants public. Enums are the exception — a variant you cannot name is useless.</li>
|
|
<li>Child modules can always see their ancestors' private items. Privacy points outward, not inward.</li>
|
|
</ul>
|
|
|
|
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch07-01-packages-and-crates.html">7.1 Packages and Crates</a> · <a href="https://doc.rust-lang.org/stable/book/ch07-02-defining-modules-to-control-scope-and-privacy.html">7.2 Defining Modules</a> · <a href="https://doc.rust-lang.org/stable/book/ch07-05-separating-modules-into-different-files.html">7.5 Separating Modules into Files</a> · <a href="https://doc.rust-lang.org/stable/book/ch11-03-test-organization.html">11.3 Test Organization</a></p>
|
|
|
|
<h2>Derives, briefly</h2>
|
|
|
|
<p>You will need these in 0003 and they look like magic, so: <code>#[derive(..)]</code> asks the compiler to write an obvious implementation for you.</p>
|
|
|
|
<table>
|
|
<tr><th align="left">Derive</th><th align="left">Gives you</th><th align="left">Needed when</th></tr>
|
|
<tr><td><code>Debug</code></td><td><code>{:?}</code> printing</td><td>Any test that prints your type on failure</td></tr>
|
|
<tr><td><code>PartialEq</code></td><td><code>==</code> and <code>!=</code></td><td><code>assert_eq!</code> on your type</td></tr>
|
|
<tr><td><code>Clone</code></td><td><code>.clone()</code></td><td>You need a second copy explicitly</td></tr>
|
|
<tr><td><code>Copy</code></td><td>Assignment copies instead of moving</td><td>Small types with no heap data</td></tr>
|
|
</table>
|
|
|
|
<pre><code>#[derive(Debug, Clone, Copy, PartialEq)]
|
|
enum Size { Small, Medium, Large }</code></pre>
|
|
|
|
<p><code>Copy</code> has a hard limit: a type containing a <code>String</code> or <code>Vec</code> <strong>cannot</strong> be <code>Copy</code>, because those own heap memory and copying the pointer twice would mean freeing it twice. That is the move-versus-copy split from chapter 4, now constraining your own types. A three-variant enum with no data is a single byte and copies happily; a struct with a <code>String</code> title does not.</p>
|
|
|
|
<p>If you forget one, the compiler names the exact trait and the exact type. Read that message rather than guessing from this table.</p>
|
|
|
|
<h2>The five sentences worth keeping</h2>
|
|
|
|
<ol>
|
|
<li>A <strong>struct</strong> is this AND this AND this. An <strong>enum</strong> is this OR this OR this.</li>
|
|
<li><code>impl</code> holds the behaviour; the first parameter (<code>&self</code>, <code>&mut self</code>, <code>self</code>, or nothing) decides what the caller keeps.</li>
|
|
<li><code>match</code> on an enum must cover every variant — which is why adding a variant produces a to-do list instead of a bug.</li>
|
|
<li>A <strong>package</strong> holds <strong>crates</strong>; <code>src/lib.rs</code> and <code>src/main.rs</code> are two separate crates, so <code>main.rs</code> says <code>use tasks::..</code>, never <code>use crate::..</code>.</li>
|
|
<li>Everything is private until you write <code>pub</code>, and a file is not a module until a parent declares <code>mod</code>.</li>
|
|
</ol>
|
|
|
|
<footer>
|
|
<p><strong>Primary source:</strong> <a href="https://doc.rust-lang.org/stable/book/ch05-00-structs.html">The Rust Book, ch. 5 (structs)</a>, <a href="https://doc.rust-lang.org/stable/book/ch06-00-enums.html">ch. 6 (enums)</a>, <a href="https://doc.rust-lang.org/stable/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html">ch. 7 (packages and modules)</a>. Chapter 6 is the highest-value read of the three — enums with data are the idea most worth having properly.</p>
|
|
<p><strong>Next:</strong> <a href="0003-build-a-task-cli.html">0003 — Build a task CLI</a> · <strong>Reference:</strong> <a href="../reference/rust-syntax.html">Rust syntax reference</a></p>
|
|
<p>If any single paragraph here did not land, say which one. Vague explanations are my fault, not yours, and it is much cheaper to fix one now than to hit it as a compiler error in the middle of the project.</p>
|
|
</footer>
|
|
</body>
|
|
</html>
|