From 98c377b373672f018d1b8c8a090820ffe7f86cbc Mon Sep 17 00:00:00 2001 From: Jason Walton Date: Tue, 25 Apr 2023 01:09:18 -0400 Subject: [PATCH] Some suggestions from ua-kxie and quinedot from the Rust forums. --- docs/ch02-guessing-game.md | 14 +++++++--- docs/ch03-common-programming-concepts.md | 33 +++++++++++++++++------- docs/ch04-ownership.md | 14 +++++++++- docs/ch10/ch10-03-lifetimes.md | 19 +++++++++++--- docs/ch14-more-about-cargo.md | 6 ++++- 5 files changed, 68 insertions(+), 18 deletions(-) diff --git a/docs/ch02-guessing-game.md b/docs/ch02-guessing-game.md index f5d262b..523d045 100644 --- a/docs/ch02-guessing-game.md +++ b/docs/ch02-guessing-game.md @@ -47,11 +47,13 @@ This creates new String and binds it to a mutable variable called `guess`. By de .expect("Failed to read line"); ``` -`read_line` reads some input from stdin, and stores it in `guess`. We pass in `&mut guess` instead of just `guess`. The `&` means we pass a reference to the object that the `guess` variable points to, and `mut` means that `read_line` is allowed to mutate that variable. Passing by reference works very similar to passing by reference in C++ or Go, or passing an object in Java or JavaScript - the called function/method can modify the passed in object, and those changes will be visible in the caller's scope. References also have a lot of implications for ownership. We'll go into references in much greater detail in [chapter 4][chap4]. +`read_line` reads some input from stdin, and stores it in `guess`. We pass in `&mut guess` instead of just `guess`. The `&` means we pass a reference to the object that the `guess` variable points to, and `mut` means that `read_line` is allowed to mutate that variable. + +Passing by reference here works very similar to passing by pointer in Go or C, or passing an object in Java or JavaScript - the called function/method can modify the passed in object, and those changes will be visible in the caller's scope. References also have a lot of implications for ownership. We'll go into references in much greater detail in [chapter 4][chap4]. :::info -If you're coming to Rust from a C++ background, you might assume that without the `&` Rust would pass a copy of `guess`, but this isn't true. When we pass a value without using a reference in Rust, we actually transfer ownership of the value to the called function. This is getting way ahead of ourselves though - again, we'll get there in [chapter 4][chap4]. +If you're coming from a C/C++ background, Rust references are a little bit like C++ references, and a little bit like C pointers. We'll go into this in more detail in chapter 4. You might also assume that without the `&` Rust would pass a copy of `guess`, but this isn't true. When we pass a value without using a reference in Rust, we actually transfer ownership of the value to the called function. This is getting way ahead of ourselves though - again, we'll get there in [chapter 4][chap4]. ::: @@ -81,7 +83,13 @@ The `{}` is a placeholder. You can place a single variable directly in the place ## Generating a Secret Number -We now have a program that asks you to guess a number, but we're not yet generating a secret number to guess. Since Rust has no random number generator in the standard library, we'll rely on the "rand" _crate_ from [crates.io](https://crates.io/). Open up _Cargo.toml_, and add "rand" as a dependency: +We now have a program that asks you to guess a number, but we're not yet generating a secret number to guess. Since Rust has no random number generator in the standard library, we'll rely on the "rand" _crate_ from [crates.io](https://crates.io/). To add `rand` to our project we can run: + +```sh +$ cargo add rand +``` + +Once you do this, if you open up _Cargo.toml_, you'll see rand listed as one of our dependencies: ```toml [dependencies] diff --git a/docs/ch03-common-programming-concepts.md b/docs/ch03-common-programming-concepts.md index 740e72d..ffcc93c 100644 --- a/docs/ch03-common-programming-concepts.md +++ b/docs/ch03-common-programming-concepts.md @@ -15,7 +15,7 @@ fn main() { } ``` -This is similar to `const` in JavaScript, or `final` in Java, although in Rust it also means the contents of the variable can't be modified either: +This is similar to `const` in JavaScript, or `final` in Java. In Rust `mut` also has some ownership implications which we'll talk about in [chapter 4][chap4]. The value the reference points to also can't be modified... mostly: ```rust fn main() { @@ -24,7 +24,15 @@ fn main() { } ``` -Here `clear` will try to empty the string, but will fail with the error `` cannot borrow `foo` as mutable, as it is not declared as mutable ``. This is ultimately because, if you go look at the source code for the `clear` method it is defined as requiring `self` to be mutable (`self` is a bit like `this` in other languages). +Here `clear` will try to empty the string, but will fail with the error `` cannot borrow `foo` as mutable, as it is not declared as mutable ``. This is ultimately because, if you go look at the source code for the `clear` method it is defined as requiring `self` to be a mutable reference (`self` is a bit like `this` in other languages). + +::: info + +You may have noticed that we said the value can't be changed "mostly". On a simple value like `String`, the underlying value can't be changed unless it's declared `mut`, but on other structs the definition of `mut` depends on the particular struct. A Rust mutex is an example of an object that is immutable, but you're allowed to change the value in it if you own the lock. + +Later on in [chapter 15][chap15] we're going to find out how you can modify parts of immutable objects through a concept call _interior mutability_, and that we can share mutable objects across multiple places in the code with smart pointers. + +::: As we saw in the [previous chapter][chap2], we can declare a variable as mutable with the `mut` keyword: @@ -49,7 +57,7 @@ Constants are stored directly in the program binary, and have a few restrictions The value of the constant has to be something that can be determined at compile time, not at runtime. The Rust Reference has a [section on constant evaluation](https://doc.rust-lang.org/stable/reference/const_eval.html) that lays out all the rules for what operators you're allowed to use and what you're not, but here the compiler can convert `60 * 60 * 3` into `10800` for us and store that in the executable. -Rust does have a concept of a global or _static variable_, but they are not often used and we'll talk about them in [chapter 19](./ch19/ch19-01-unsafe.md#accessing-or-modifying-a-mutable-static-variable). +Rust also has the a concept of a global or _static variable_. We'll talk about them in [chapter 19](./ch19/ch19-01-unsafe.md#accessing-or-modifying-a-mutable-static-variable). ### Shadowing @@ -110,10 +118,16 @@ Integer literals can be written using any of the methods below. Integer literals | Binary | 0b1111_0000 | | Byte (u8) | b'A' | -If you try to overflow an integer (e.g. you try to store 256 in a u8), what happens depends on whether you compiled your program with `--release` or not. In debug mode Rust adds runtime checks to ensure you don't overflow a value, so your program will panic and crash (see [chapter 9][chap9] for more about panics). With the --release flag, the integer will overflow as you would expect it to in another language like C or Java (the largest value a u8 can hold is 255, so 256 wraps to 0). +If you try to overflow an integer (e.g. you try to store 256 in a u8), what happens (by default) depends on whether you compiled your program with `--release` or not. In debug mode Rust adds runtime checks to ensure you don't overflow a value, so your program will panic and crash (see [chapter 9][chap9] for more about panics). With the --release flag, the integer will overflow as you would expect it to in another language like C or Java (the largest value a u8 can hold is 255, so 256 wraps to 0). The standard library has functions that let you explicitly define how you want to handle overflows if you don't want to panic. For example [`wrapping_add`](https://doc.rust-lang.org/std/intrinsics/fn.wrapping_add.html) will add two numbers and let them wrap around. There are `wrapping_*`, `checked_*`, `overflowing_*`, and `saturating_*` functions for integer arithmetic. +::: tip + +We can change how overflows are handled at runtime for development and release through [release profiles](./ch14-more-about-cargo.md#141---customizing-builds-with-release-profiles). + +::: + ### Floating-Point Types There are two floating point types, `f64` (the default) and `f32`. Floating-point numbers are stored using the IEEE-754 standard. @@ -133,7 +147,7 @@ let f: bool = false; ### Character Type -A `char` in Rust is a four-byte unicode code point. +A `char` in Rust is a four-byte unicode scalar value. ```rust let c = 'z'; @@ -144,11 +158,11 @@ let space_woman_zwj = '👩🏻‍🚀'; // <== This doesn't work! That last example doesn't work. Our female astronaut friend might look like a single character, but she's actually two emoji joined together with a zero-width-joiner (ZWJ). We'll talk a lot more about UTF-8 and Unicode in [chapter 8][chap8]. -### `str` and String +### `&str` and `String` -You'll see two different string types in Rust: `str` and `String`. `str` is a bit like an array - it's a list of characters with a fixed length known at compile time. `String` is more like a `Vector` - it's a data type that stores a list of strings in a variable-length chunk of memory on the heap. Any time you accept input from the user or read a string from a file, it's going to end up in a `String`. +You'll see two different string types in Rust: `str` and `String`. `String` is similar to a `Vector` - it's a data type that stores a list of characters in a variable-length chunk of memory on the heap. Any time you accept input from the user or read a string from a file, it's going to end up in a `String`. -The type `&str` - a reference to a `str` - is also known as a _string slice_ (which we'll learn more about in [the next chapter][chap4]), and is both a pointer to the string's data and a length for the string. Any string literal in Rust is a `&str`, since the actual string is stored somewhere in the executable and we just have an immutable reference to it. +The type `&str` (almost always seen in it's borrowed form) is also known as a _string slice_ (which we'll learn more about in [the next chapter][chap4]), and is both a pointer to the string's data and a length for the string. Any string literal in Rust is a `&str`, since the actual string is stored somewhere in the executable and we just have an immutable reference to it. A string slice can be used as a view into a `String`. ## Compound Types @@ -172,7 +186,7 @@ An empty tuple is written `()` and is called a "unit". This represents an empty ### Array Type -Every element in an array must be the same type, and arrays must be fixed length. If you're looking for a "variable length" array, you want a vector from the standard library (see [Chapter 8][chap8]). Note that arrays end up on the stack, where vectors store their contents on the heap. +Every element in an array must be the same type, and arrays must be fixed length. If you're looking for a "variable length" array, you want a vector from the standard library (see [Chapter 8][chap8]). If you declare a variable as an array in a function, then the contents of that variable will end up on the stack, while for a vector contents will end up on the heap. (Although you can put the contents of an array on the heap by using a smart pointer like a `Box` - see [chapter 15][chap15]). ```rust let a = [1, 2, 3, 4, 5]; @@ -349,4 +363,5 @@ Now that we know some basics, it's time to learn about [ownership][chap4]. [chap9]: ./ch09-error-handling.md "Chapter 9: Error Handling" [chap10]: ./ch10/ch10-01-generic-data-types.md "Chapter 10: Generic Types, Traits, and Lifetimes" [chap13]: ./ch13-functional-language-features.md "Chapter 13: Functional Language Features: Iterators and Closures" +[chap15]: ./ch15-smart-pointers.md "Chapter 15: Smart Pointers" [appb]: ./zz-appendix/appendix-02-operators.md diff --git a/docs/ch04-ownership.md b/docs/ch04-ownership.md index d827da8..1901258 100644 --- a/docs/ch04-ownership.md +++ b/docs/ch04-ownership.md @@ -22,6 +22,12 @@ From the original Rust Book: The _scope_ of a variable in Rust works much like it does in most other languages - inside a set of curly braces, any variables you declare can be accessed only after their declaration, and they go "out of scope" once we hit the closing brace. The key thing about Rust is that once a variable goes out of scope, if that variable currently owns some memory, then that memory will be freed. +::: info + +A variable can only have one owner at a time, but in [chapter 15][chap15] we'll talk about smart pointers like `Rc` that let us get around this restriction. + +::: + ### Memory and Allocation This is a trivial example demonstrating some memory being allocated on the heap and then freed: @@ -184,6 +190,12 @@ fn calculate_length(s: &String) -> usize { Two things to note here - when we call `calculate_length` instead of passing `s1` we're passing `&s1`, and in the signature for `calculate_length` we take a `&String` instead of a `String`. What we're passing here is a "reference to a string". Essentially `&s1` contains a pointer to the String held in `s1`, and we're passing that pointer to `calculate_length`. `calculate_length` doesn't take ownership of the `String`, it merely borrows it, so the `String` won't be dropped when `s` goes out of scope. +:::info + +The syntax for getting a reference to a value - `&x` - is exactly the same as getting a pointer to a value in C or Go, and references in Rust behave a lot like pointers. More so than C++ references. [This Stack Overflow answer](https://stackoverflow.com/questions/64167637/is-the-concept-of-reference-different-in-c-and-rust) talks about ways that Rust references compare to C/C++ pointers. + +::: + ### Mutable References As with variables, we can have both immutable references (the default) and mutable references: @@ -259,7 +271,7 @@ fn dangle() -> &String { Here `s` goes out of scope at the end of the function, so the String will be dropped. That means if Rust let us return a reference to the String, it would be a reference to memory that had already been reclaimed. -There's no `null` or `nil` in Rust. You can't have a nil reference like you could in Go. (Instead there's something called an `Option` which we'll talk about in [chapter 6][chap6].) +There's no `null` or `nil` in Rust. You can't have a nil pointer like you could in Go. (Instead there's something called an `Option` which we'll talk about in [chapter 6][chap6].) ### The Rules of References diff --git a/docs/ch10/ch10-03-lifetimes.md b/docs/ch10/ch10-03-lifetimes.md index 9e9758b..df9697b 100644 --- a/docs/ch10/ch10-03-lifetimes.md +++ b/docs/ch10/ch10-03-lifetimes.md @@ -112,7 +112,13 @@ fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { Think about this a bit like a generic function (the syntax is similar for a good reason). We're saying here there exists some lifetime which we're going to call `'a`, and the variables `x` and `y` both life at least as long as this hypothetical `'a`'. They don't have to both be the same lifetime, they just both have to be valid at the start and end of `'a`. Then in the case of this function we're making the claim that the value we return is going to be valid for this same lifetime. At compile time, the compiler will see how long the passed in `x` lives, how long the passed in `y` lives, and then it will verify that the result of this function isn't used anywhere outside of that lifetime. -Putting this a bit more succinctly, we're telling the compiler that the return value of `longest()` will live at least as long as the shorter lifetime of `x` and `y`. When the rust compiler analyzes a call to `longest()` it can now mark it as an error if the two parameters passed in don't adhere to this constraint. It's important to note that these annotations don't actually change the lifetime of the references passed in, it only gives the borrow checker enough information to work out whether a call is valid. +Putting this a bit more succinctly, we're telling the compiler that the return value of `longest()` will live at least as long as the shorter lifetime of `x` and `y`. When the rust compiler analyzes a call to `longest()` it can now mark it as an error if the two parameters passed in don't adhere to this constraint. + +:::info + +Lifetime annotations don't actually change the lifetime of the references passed in, it only gives the borrow checker enough information to work out whether a call is valid. + +::: Returning to this example: @@ -159,14 +165,19 @@ struct ImportantExcerpt<'a> { fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().expect("Could not find a '.'"); - let i = ImportantExcerpt { - part: first_sentence, - }; + + { + let i = ImportantExcerpt { + part: first_sentence, + }; + } } ``` Again, it's helpful to think about this like we would any other generic declaration. When we write `ImportantExcerpt<'a>` we are saying "there exists some lifetime which we'll call `'a`" - we don't know what that lifetime is yet, and we won't know until someone creates an actual instance of this struct. When we write `part: &'a str`, we are saying "when someone reads this ref, it has the lifetime `'a`" (and if someone later writes a new value to this ref, it must have a lifetime of at least `'a`). At compile time, the compiler will fill in the generic lifetimes with real lifetimes from your program, and then verify that the constraints hold. +In this particular example, take careful note that when we create our `ImportantExcerpt` in `main`, the lifetime annotation `'a` takes on the lifetime of `first_sentence`, which is longer than the lifetime of the `ImportantExcerpt`! + Here this struct has only a single reference, and so it might seem odd that we have to give an explicit lifetime for it. You might think the compiler could automatically figure out the lifetime here (and perhaps one day in this trivial example it will - Rust is evolving pretty rapidly). :::info diff --git a/docs/ch14-more-about-cargo.md b/docs/ch14-more-about-cargo.md index c40656a..ac4d57c 100644 --- a/docs/ch14-more-about-cargo.md +++ b/docs/ch14-more-about-cargo.md @@ -7,12 +7,16 @@ Cargo has four built-in release profiles called `dev`, `release`, `test`, and `b ```toml [profile.dev] opt-level = 0 +overflow-checks = true [profile.release] opt-level = 3 +overflow-checks = false ``` -In this example `opt-level` controls how much Rust tries to optimize your code and can be set from 0 to 3 (these are also the defaults - 0 for dev because you want the build to be fast, 3 for release because you want your program to be fast). For a full list of options see [the cargo documentation](https://doc.rust-lang.org/cargo/reference/profiles.html). +In this example `opt-level` controls how much Rust tries to optimize your code and can be set from 0 to 3 (these are also the defaults - 0 for dev because you want the build to be fast, 3 for release because you want your program to be fast). `overflow-checks` is used to determine whether or not Rust will add in runtime checks to see if integer arithmetic overflows any values. + +For a full list of options see [the cargo documentation](https://doc.rust-lang.org/cargo/reference/profiles.html). ## 14.2 - Publishing a Crate to Crates.io