(This page has no text content)
RUST PROGR AMMING Ef fective Rust linkedin.com/company/oreilly-media youtube.com/oreillymedia Rust’s popularity is growing, due in part to features like memory safety, type safety, and thread safety. But these same elements can also make learning Rust a challenge, even for experienced programmers. This practical guide helps you make the transition to writing idiomatic Rust. In the process, you’ll also make full use of Rust’s type system, safety guarantees, and burgeoning ecosystem. If you’re a software engineer who has experience with an existing compiled language, or if you’ve struggled to convert a basic understanding of Rust syntax into working programs, this book is for you. Effective Rust focuses on the conceptual differences between Rust and other compiled languages, and provides specific recommendations that programmers can easily follow. Author David Drysdale will soon have you writing fluent Rust, rather than badly translated C++. Effective Rust will help you: • Understand the structure of Rust’s type system • Learn Rust idioms for error handling, iteration, and more • Discover how to work with Rust’s crate ecosystem • Use Rust’s type system to express your design • Win fights with the borrow checker • Build a robust project that takes full advantage of the Rust tooling ecosystem David Drysdale is a Google staff software engineer who has worked in Rust since 2019, primarily in security-related areas. He led the Rust rewrite of Android’s hardware cryptography subsystem and authored the Rust port of the Tink cryptography library. He has also worked in C/C++ and Go and on projects as diverse as the Linux kernel and video conferencing mobile apps. 9 7 8 1 0 9 8 1 5 1 4 0 9 5 9 9 9 9 US $59.99 CAN $74.99 ISBN: 978-1-098-15140-9 “Ef fective Rust is an excellent collection of real-world Rust knowledge beyond the basics. The advice in this book will help you become a conf ident and well-rounded Rustacean.” —Carol Nichols Coauthor of The Rust Programming Language “Ef fective Rust dives deep into most of the recommendations I give people on how to improve their projects. It’s a great resource to level up your Rust.” —Pietro Albini Former member of the Rust Core Team
David Drysdale Effective Rust 35 Specific Ways to Improve Your Rust Code Boston Farnham Sebastopol TokyoBeijing
978-1-098-15140-9 [LSI] Effective Rust by David Drysdale Copyright © 2024 Galloglass Consulting Limited. All rights reserved. Printed in the United States of America. Published by O’Reilly Media, Inc., 1005 Gravenstein Highway North, Sebastopol, CA 95472. O’Reilly books may be purchased for educational, business, or sales promotional use. Online editions are also available for most titles (http://oreilly.com). For more information, contact our corporate/institutional sales department: 800-998-9938 or corporate@oreilly.com. Acquisitions Editor: Brian Guerin Development Editor: Jeff Bleiel Production Editor: Katherine Tozer Copyeditor: Piper Editorial Consulting, LLC Proofreader: Dwight Ramsey Indexer: Ellen Troutman-Zaig Interior Designer: David Futato Cover Designer: Karen Montgomery Illustrator: Kate Dullea April 2024: First Edition Revision History for the First Edition 2024-04-01: First Release See http://oreilly.com/catalog/errata.csp?isbn=9781098151409 for release details. The O’Reilly logo is a registered trademark of O’Reilly Media, Inc. Effective Rust, the cover image, and related trade dress are trademarks of O’Reilly Media, Inc. The views expressed in this work are those of the author and do not represent the publisher’s views. While the publisher and the author have used good faith efforts to ensure that the information and instructions contained in this work are accurate, the publisher and the author disclaim all responsibility for errors or omissions, including without limitation responsibility for damages resulting from the use of or reliance on this work. Use of the information and instructions contained in this work is at your own risk. If any code samples or other technology this work contains or describes is subject to open source licenses or the intellectual property rights of others, it is your responsibility to ensure that your use thereof complies with such licenses and/or rights.
Table of Contents Preface. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . v 1. Types. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 Item 1: Use the type system to express your data structures 2 Item 2: Use the type system to express common behavior 10 Item 3: Prefer Option and Result transforms over explicit match expressions 20 Item 4: Prefer idiomatic Error types 25 Item 5: Understand type conversions 34 Item 6: Embrace the newtype pattern 40 Item 7: Use builders for complex types 45 Item 8: Familiarize yourself with reference and pointer types 51 Item 9: Consider using iterator transforms instead of explicit loops 64 2. Traits. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 Item 10: Familiarize yourself with standard traits 77 Item 11: Implement the Drop trait for RAII patterns 89 Item 12: Understand the trade-offs between generics and trait objects 93 Item 13: Use default implementations to minimize required trait methods 103 3. Concepts. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 Item 14: Understand lifetimes 106 Item 15: Understand the borrow checker 123 Item 16: Avoid writing unsafe code 142 Item 17: Be wary of shared-state parallelism 145 Item 18: Don’t panic 159 Item 19: Avoid reflection 162 Item 20: Avoid the temptation to over-optimize 169 iii
4. Dependencies. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175 Item 21: Understand what semantic versioning promises 176 Item 22: Minimize visibility 181 Item 23: Avoid wildcard imports 186 Item 24: Re-export dependencies whose types appear in your API 188 Item 25: Manage your dependency graph 191 Item 26: Be wary of feature creep 197 5. Tooling. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203 Item 27: Document public interfaces 203 Item 28: Use macros judiciously 209 Item 29: Listen to Clippy 223 Item 30: Write more than unit tests 227 Item 31: Take advantage of the tooling ecosystem 235 Item 32: Set up a continuous integration (CI) system 237 6. Beyond Standard Rust. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243 Item 33: Consider making library code no_std compatible 243 Item 34: Control what crosses FFI boundaries 249 Item 35: Prefer bindgen to manual FFI mappings 261 Afterword. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265 Index. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267 iv | Table of Contents
Preface The code is more what you’d call guidelines than actual rules. —Hector Barbossa In the crowded landscape of modern programming languages, Rust is different. Rust offers the speed of a compiled language, the efficiency of a non-garbage-collected lan‐ guage, and the type safety of a functional language—as well as a unique solution to memory safety problems. As a result, Rust regularly polls as the most loved program‐ ming language. The strength and consistency of Rust’s type system means that if a Rust program compiles, there is already a decent chance that it will work—a phenomenon previ‐ ously observed only with more academic, less accessible languages such as Haskell. If a Rust program compiles, it will also work safely. This safety—both type safety and memory safety—does come with a cost, though. Despite the quality of the basic documentation, Rust has a reputation for having a steep on-ramp, where newcomers have to go through the initiation rituals of fighting the borrow checker, redesigning their data structures, and being befuddled by life‐ times. A Rust program that compiles may have a good chance of working the first time, but the struggle to get it to compile is real—even with the Rust compiler’s remarkably helpful error diagnostics. Who This Book Is For This book tries to help with these areas where programmers struggle, even if they already have experience with an existing compiled language like C++. As such—and in common with other Effective <Language> books—this book is intended to be the second book that a newcomer to Rust might need, after they have already encountered the basics elsewhere—for example, in The Rust Programming Language (Steve Klabnik and Carol Nichols, No Starch Press) or Programming Rust (Jim Blandy et al., O’Reilly). v
However, Rust’s safety leads to a slightly different slant to the Items here, particularly when compared to Scott Meyers’s original Effective C++ series. The C++ language was (and is) full of footguns, so Effective C++ focused on a collection of advice for avoiding those footguns, based on real-world experience creating software in C++. Significantly, it contained guidelines not rules, because guidelines have exceptions— providing the detailed rationale for a guideline allows readers to decide for them‐ selves whether their particular scenario warranted breaking the rule. The general style of giving advice together with the reasons for that advice is pre‐ served here. However, since Rust is remarkably free of footguns, the Items here con‐ centrate more on the concepts that Rust introduces. Many Items have titles like “Understand…” and “Familiarize yourself with…”, and help on the journey toward writing fluent, idiomatic Rust. Rust’s safety also leads to a complete absence of Items titled “Never…”. If you really should never do something, the compiler will generally prevent you from doing it. Rust Version The text is written for the 2018 edition of Rust, using the stable toolchain. Rust’s back-compatibility promises mean that any later edition of Rust, including the 2021 edition, will still support code written for the 2018 edition, even if that later edition introduces breaking changes. Rust is now also stable enough that the differences between the 2018 and 2021 editions are minor; none of the code in the book needs altering to be 2021-edition compliant (but Item 19 includes one exception in which a later version of Rust allows new behavior that wasn’t previously possible). The Items here do not cover any aspects of Rust’s async functionality, as this involves more advanced concepts and less stable toolchain support—there’s already enough ground to cover with synchronous Rust. Perhaps an Effective Async Rust will emerge in the future… The specific rustc version used for code fragments and error messages is 1.70. The code fragments are unlikely to need changes for later versions, but the error messages may vary with your particular compiler version. The error messages included in the text have also been manually edited to fit within the width constraints of the book but are otherwise as produced by the compiler. vi | Preface
The text has a number of references to and comparisons with other statically typed languages, such as Java, Go, and C++, to help readers with experience in those lan‐ guages orient themselves. (C++ is probably the closest equivalent language, particu‐ larly when C++11’s move semantics come into play.) Navigating This Book The Items that make up the book are divided into six chapters: Chapter 1, “Types” Suggestions that revolve around Rust’s core type system Chapter 2, “Traits” Suggestions for working with Rust’s traits Chapter 3, “Concepts” Core ideas that form the design of Rust Chapter 4, “Dependencies” Advice for working with Rust’s package ecosystem Chapter 5, “Tooling” Suggestions for improving your codebase by going beyond just the Rust compiler Chapter 6, “Beyond Standard Rust” Suggestions for when you have to work beyond Rust’s standard, safe environment Although the “Concepts” chapter is arguably more fundamental than the “Types” and “Traits” chapters, it is deliberately placed later in the book so that readers who are reading from beginning to end can build up some confidence first. Conventions Used in This Book The following typographical conventions are used in this book: Italic Indicates new terms, URLs, email addresses, filenames, and file extensions. Constant width Used for program listings, as well as within paragraphs to refer to program ele‐ ments such as variable or function names, databases, data types, environment variables, statements, and keywords. Preface | vii
DOES NOT COMPILE // Marks code samples that do not compile UNDESIRED BEHAVIOR // Marks code samples that exhibit undesired behavior O’Reilly Online Learning For more than 40 years, O’Reilly Media has provided technol‐ ogy and business training, knowledge, and insight to help companies succeed. Our unique network of experts and innovators share their knowledge and expertise through books, articles, and our online learning platform. O’Reilly’s online learning platform gives you on-demand access to live training courses, in-depth learning paths, interactive coding environments, and a vast collection of text and video from O’Reilly and 200+ other publishers. For more information, visit https://oreilly.com. How to Contact Us Please address comments and questions concerning this book to the publisher: O’Reilly Media, Inc. 1005 Gravenstein Highway North Sebastopol, CA 95472 800-889-8969 (in the United States or Canada) 707-827-7019 (international or local) 707-829-0104 (fax) support@oreilly.com https://www.oreilly.com/about/contact.html We have a web page for this book, where we list errata, examples, and any additional information. You can access this page at https://oreil.ly/effective-rust. For news and information about our books and courses, visit https://oreilly.com. Find us on LinkedIn: https://linkedin.com/company/oreilly-media. Watch us on YouTube: https://youtube.com/oreillymedia. viii | Preface
Acknowledgments My thanks go to the people who helped make this book possible: • The technical reviewers who gave expert and detailed feedback on all aspects of the text: Pietro Albini, Jess Males, Mike Capp, and especially Carol Nichols. • My editors at O’Reilly: Jeff Bleiel, Brian Guerin, and Katie Tozer. • Tiziano Santoro, from whom I originally learned many things about Rust. • Danny Elfanbaum, who provided vital technical assistance for dealing with the AsciiDoc formatting of the book. • Diligent readers of the original web version of the book, in particular: — Julian Rosse, who spotted dozens of typos and other errors in the online text. — Martin Disch, who pointed out potential improvements and inaccuracies in several Items. — Chris Fleetwood, Sergey Kaunov, Clifford Matthews, Remo Senekowitsch, Kirill Zaborsky, and an anonymous Proton Mail user, who pointed out mis‐ takes in the text. • My family, who coped with many weekends when I was distracted by writing. Preface | ix
(This page has no text content)
CHAPTER 1 Types This first chapter of this book covers advice that revolves around Rust’s type system. This type system is more expressive than that of other mainstream languages; it has more in common with “academic” languages such as OCaml or Haskell. One core part of this is Rust’s enum type, which is considerably more expressive than the enumeration types in other languages and which allows for algebraic data types. The Items in this chapter cover the fundamental types that the language provides and how to combine them into data structures that precisely express the semantics of your program. This concept of encoding behavior into the type system helps to reduce the amount of checking and error path code that’s required, because invalid states are rejected by the toolchain at compile time rather than by the program at run‐ time. This chapter also describes some of the ubiquitous data structures that are provided by Rust’s standard library: Options, Results, Errors and Iterators. Familiarity with these standard tools will help you write idiomatic Rust that is efficient and compact— in particular, they allow use of Rust’s question mark operator, which supports error handling that is unobtrusive but still type-safe. Note that Items that involve Rust traits are covered in the following chapter, but there is necessarily a degree of overlap with the Items in this chapter, because traits describe the behavior of types. 1
Item 1: Use the type system to express your data structures who called them programers and not type writers —@thingskatedid This Item provides a quick tour of Rust’s type system, starting with the fundamental types that the compiler makes available, then moving on to the various ways that val‐ ues can be combined into data structures. Rust’s enum type then takes a starring role. Although the basic version is equivalent to what other languages provide, the ability to combine enum variants with data fields allows for enhanced flexibility and expressivity. Fundamental Types The basics of Rust’s type system are pretty familiar to anyone coming from another statically typed programming language (such as C++, Go, or Java). There’s a collec‐ tion of integer types with specific sizes, both signed (i8, i16, i32, i64, i128) and unsigned (u8, u16, u32, u64, u128). There are also signed (isize) and unsigned (usize) integers whose sizes match the pointer size on the target system. However, you won’t be doing much in the way of converting between pointers and integers with Rust, so that size equivalence isn’t really relevant. However, standard collections return their size as a usize (from .len()), so collection indexing means that usize values are quite common— which is obviously fine from a capacity perspective, as there can’t be more items in an in-memory collection than there are memory addresses on the system. The integral types do give us the first hint that Rust is a stricter world than C++. In Rust, attempting to put a larger integer type (i32) into a smaller integer type (i16) generates a compile-time error: DOES NOT COMPILE let x: i32 = 42; let y: i16 = x; error[E0308]: mismatched types --> src/main.rs:18:18 | 18 | let y: i16 = x; | --- ^ expected `i16`, found `i32` | | | expected due to this 2 | Chapter 1: Types
| help: you can convert an `i32` to an `i16` and panic if the converted value doesn't fit | 18 | let y: i16 = x.try_into().unwrap(); | ++++++++++++++++++++ This is reassuring: Rust is not going to sit there quietly while the programmer does things that are risky. Although we can see that the values involved in this particular conversion would be just fine, the compiler has to allow for the possibility of values where the conversion is not fine: DOES NOT COMPILE let x: i32 = 66_000; let y: i16 = x; // What would this value be? The error output also gives an early indication that while Rust has stronger rules, it also has helpful compiler messages that point the way to how to comply with the rules. The suggested solution raises the question of how to handle situations where the conversion would have to alter the value to fit, and we’ll have more to say on both error handling (Item 4) and using panic! (Item 18) later. Rust also doesn’t allow some things that might appear “safe,” such as putting a value from a smaller integer type into a larger integer type: DOES NOT COMPILE let x = 42i32; // Integer literal with type suffix let y: i64 = x; error[E0308]: mismatched types --> src/main.rs:36:18 | 36 | let y: i64 = x; | --- ^ expected `i64`, found `i32` | | | expected due to this | help: you can convert an `i32` to an `i64` | 36 | let y: i64 = x.into(); | +++++++ Here, the suggested solution doesn’t raise the specter of error handling, but the con‐ version does still need to be explicit. We’ll discuss type conversions in more detail later (Item 5). Item 1: Use the type system to express your data structures | 3
1 The situation gets muddier still if the filesystem is involved, since filenames on popular platforms are some‐ where in between arbitrary bytes and UTF-8 sequences: see the std::ffi::OsString documentation. 2 Technically, a Unicode scalar value rather than a code point. Continuing with the unsurprising primitive types, Rust has a bool type, floating point types (f32, f64), and a unit type () (like C’s void). More interesting is the char character type, which holds a Unicode value (similar to Go’s rune type). Although this is stored as four bytes internally, there are again no silent conversions to or from a 32-bit integer. This precision in the type system forces you to be explicit about what you’re trying to express—a u32 value is different from a char, which in turn is different from a sequence of UTF-8 bytes, which in turn is different from a sequence of arbitrary bytes, and it’s up to you to specify exactly which you mean.1 Joel Spolsky’s famous blog post can help you understand which you need. Of course, there are helper methods that allow you to convert between these different types, but their signatures force you to handle (or explicitly ignore) the possibility of failure. For example, a Unicode code point can always be represented in 32 bits,2 so 'a' as u32 is allowed, but the other direction is trickier (as there are some u32 val‐ ues that are not valid Unicode code points): char::from_u32 Returns an Option<char>, forcing the caller to handle the failure case. char::from_u32_unchecked Makes the assumption of validity but has the potential to result in undefined behavior if that assumption turns out not to be true. The function is marked unsafe as a result, forcing the caller to use unsafe too (Item 16). Aggregate Types Moving on to aggregate types, Rust has a variety of ways to combine related values. Most of these are familiar equivalents to the aggregation mechanisms available in other languages: Arrays Hold multiple instances of a single type, where the number of instances is known at compile time. For example, [u32; 4] is four 4-byte integers in a row. Tuples Hold instances of multiple heterogeneous types, where the number of elements and their types are known at compile time, for example, (WidgetOffset, Widget Size, WidgetColor). If the types in the tuple aren’t distinctive—for example, 4 | Chapter 1: Types
(i32, i32, &'static str, bool)—it’s better to give each element a name and use a struct. Structs Also hold instances of heterogeneous types known at compile time but allow both the overall type and the individual fields to be referred to by name. Rust also includes the tuple struct, which is a crossbreed of a struct and a tuple: there’s a name for the overall type but no names for the individual fields—they are referred to by number instead: s.0, s.1, and so on: /// Struct with two unnamed fields. struct TextMatch(usize, String); // Construct by providing the contents in order. let m = TextMatch(12, "needle".to_owned()); // Access by field number. assert_eq!(m.0, 12); enums This brings us to the jewel in the crown of Rust’s type system, the enum. With the basic form of an enum, it’s hard to see what there is to get excited about. As with other lan‐ guages, the enum allows you to specify a set of mutually exclusive values, possibly with a numeric value attached: enum HttpResultCode { Ok = 200, NotFound = 404, Teapot = 418, } let code = HttpResultCode::NotFound; assert_eq!(code as i32, 404); Because each enum definition creates a distinct type, this can be used to improve read‐ ability and maintainability of functions that take bool arguments. Instead of: print_page(/* both_sides= */ true, /* color= */ false); a version that uses a pair of enums: pub enum Sides { Both, Single, } pub enum Output { BlackAndWhite, Color, Item 1: Use the type system to express your data structures | 5
} pub fn print_page(sides: Sides, color: Output) { // ... } is more type-safe and easier to read at the point of invocation: print_page(Sides::Both, Output::BlackAndWhite); Unlike the bool version, if a library user were to accidentally flip the order of the arguments, the compiler would immediately complain: error[E0308]: arguments to this function are incorrect --> src/main.rs:104:9 | 104 | print_page(Output::BlackAndWhite, Sides::Single); | ^^^^^^^^^^ --------------------- ------------- expected `enums::Output`, | | found `enums::Sides` | | | expected `enums::Sides`, found `enums::Output` | note: function defined here --> src/main.rs:145:12 | 145 | pub fn print_page(sides: Sides, color: Output) { | ^^^^^^^^^^ ------------ ------------- help: swap these arguments | 104 | print_page(Sides::Single, Output::BlackAndWhite); | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Using the newtype pattern—see Item 6—to wrap a bool also achieves type safety and maintainability; it’s generally best to use the newtype pattern if the semantics will always be Boolean, and to use an enum if there’s a chance that a new alternative—e.g., Sides::BothAlternateOrientation—could arise in the future. The type safety of Rust’s enums continues with the match expression: DOES NOT COMPILE let msg = match code { HttpResultCode::Ok => "Ok", HttpResultCode::NotFound => "Not found", // forgot to deal with the all-important "I'm a teapot" code }; error[E0004]: non-exhaustive patterns: `HttpResultCode::Teapot` not covered --> src/main.rs:44:21 | 6 | Chapter 1: Types
3 The need to consider all possibilities also means that adding a new variant to an existing enum in a library is a breaking change (Item 21): library clients will need to change their code to cope with the new variant. If an enum is really just a C-like list of related numerical values, this behavior can be avoided by marking it as a non_exhaustive enum; see Item 21. 44 | let msg = match code { | ^^^^ pattern `HttpResultCode::Teapot` not covered | note: `HttpResultCode` defined here --> src/main.rs:10:5 | 7 | enum HttpResultCode { | -------------- ... 10 | Teapot = 418, | ^^^^^^ not covered = note: the matched value is of type `HttpResultCode` help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown | 46 ~ HttpResultCode::NotFound => "Not found", 47 ~ HttpResultCode::Teapot => todo!(), | The compiler forces the programmer to consider all of the possibilities that are repre‐ sented by the enum,3 even if the result is just to add a default arm _ => {}. (Note that modern C++ compilers can and do warn about missing switch arms for enums as well.) enums with Fields The true power of Rust’s enum feature comes from the fact that each variant can have data that comes along with it, making it an aggregate type that acts as an algebraic data type (ADT). This is less familiar to programmers of mainstream languages; in C/C++ terms, it’s like a combination of an enum with a union—only type-safe. This means that the invariants of the program’s data structures can be encoded into Rust’s type system; states that don’t comply with those invariants won’t even compile. A well-designed enum makes the creator’s intent clear to humans as well as to the compiler: use std::collections::{HashMap, HashSet}; pub enum SchedulerState { Inert, Pending(HashSet<Job>), Running(HashMap<CpuId, Vec<Job>>), } Item 1: Use the type system to express your data structures | 7
Just from the type definition, it’s reasonable to guess that Jobs get queued up in the Pending state until the scheduler is fully active, at which point they’re assigned to some per-CPU pool. This highlights the central theme of this Item, which is to use Rust’s type system to express the concepts that are associated with the design of your software. A dead giveaway for when this is not happening is a comment that explains when some field or parameter is valid: UNDESIRED BEHAVIOR pub struct DisplayProps { pub x: u32, pub y: u32, pub monochrome: bool, // `fg_color` must be (0, 0, 0) if `monochrome` is true. pub fg_color: RgbColor, } This is a prime candidate for replacement with an enum holding data: pub enum Color { Monochrome, Foreground(RgbColor), } pub struct DisplayProps { pub x: u32, pub y: u32, pub color: Color, } This small example illustrates a key piece of advice: make invalid states inexpressible in your types. Types that support only valid combinations of values mean that whole classes of errors are rejected by the compiler, leading to smaller and safer code. Ubiquitous enum Types Returning to the power of the enum, there are two concepts that are so common that Rust’s standard library includes built-in enum types to express them; these types are ubiquitous in Rust code. 8 | Chapter 1: Types
Comments 0
Loading comments...
Reply to Comment
Edit Comment