It's a decent way, overall. For the ownership / borrowing sections specifically though, I'll throw out some heads-ups.
Almost all Rust learning material presents a "mutable vs immutable" world view, at least in the beginning. A more accurate dichotomy is "exclusive vs shared". &mut _s must be exclusive, even if you don't actually mutate something. And once you get to chapter 15, the book will introduce types that facilitate shared ownership and shared mutation -- the ability to mutate through a shared reference (&_).
You don't have to master these immediately, but it's good to know they exist up-front, so that you aren't surprised later.
In terms of how borrow-checking works, the Brown book presents a more accurate mental model than the official book. Rust lifetimes -- those '_ things -- are about the duration of borrows, and not directly about the liveness of values. They are not based on lexical blocks, though this is commonly used as an illustration. The compiler uses lifetimes to statically analyze when and how variables and other places are borrowed. It then checks every use of every place and makes sure there is no conflict with being borrowed.
The Brown book presents this as sort of permission system. It's probably a lot to take in at first, but it's also a lot more accurate that the official book. They also cover concepts like reborrowing which the official book does not.
There's probably a better approach some place between how the two books do it.
A few examples about borrow-returning functions
Here I'll talk about function signatures where a borrow in the input is also present in the output. (The bodies aren't important for what I'll be emphasizing.) This is a borrow-checking topic which is talked about a bit later in the Book (chapter 10).
You could perhaps skip this or come back to it after reading Chapter 10, it may be a bit to targeted as a reply to your general question.
//fn ex1<'a>(slice: &'a [String]) -> Option<&'a str> {
fn ex1(slice: &[String]) -> Option<&str> {
slice.get(0).map(|s| &**s)
}
//fn ex2<'a>(slice: &'a mut [String]) -> Option<&'a mut String> {
fn ex2(slice: &mut [String]) -> Option<&mut String> {
slice.get_mut(0)
}
//fn ex3<'a, S: ToString>(vec: &'a mut Vec<String>, s: S) -> &'a str {
fn ex3<S: ToString>(vec: &mut Vec<String>, s: S) -> &str {
vec.push(s.to_string());
vec.last().unwrap()
}
fn ex4<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1 < s2 { s2 } else { s1 }
}
Recall that Rust lifetimes represent the duration of borrows (not the liveness of values).
The meaning of ex1 and ex2 is that the slice (the [String]) remains borrowed so long as the returned value is in use. With ex1 the borrow is shared; with ex2 it's exclusive. This is enforced at the call site, no matter what the bodies of the functions are.
With ex3, the Vec<String> remains exclusively borrowed while the return value is in use. The exclusive borrow of the input does not "downgrade" even though the return type is a shared borrow. That kind of API would sometimes be nice, but Rust doesn't have it yet.
With ex4, both str from the inputs remain borrowed so long as the return value is in use. The use of the return value determines the duration of the borrows (the 'a lifetime) at the call site.
This last example corresponds to an example in Chapter 10 of the official book. Chapter 10 talks about borrow checking and lifetime annotations. Its overview of how borrow checking works is quite inaccurate; it talks about the liveness of input parameters a lot, but again, Rust lifetimes are about borrow durations.
The way the borrow checking of ex4 works -- longest in the book -- is pretty much exactly the opposite of what they say. The borrow checker does not look at the lifetimes of the references passed in a choose the shorter for the return value at the call site. Instead, the use of the return value determines how long the borrow of referents of the references passed in must be -- and, as per the signature, that lifetime is the same for both inputs.
Where does value liveness come in (example 10-23)? Being destructed conflicts with being borrowed. The borrow checker didn't see the liveness scope and assign a lifetime based on that. The borrow checker determined that string2 was still borrowed at the point it went out of scope
Both the Brown book and the official book over-emphasize "stack versus heap". But heap allocated memory is not the only motivation for the ownership system, and there are other misguiding statements as well. For example the book presents Copy as a trait for "stack-only data". But that is inaccurate in multiple ways:
- Types don't care where their instances are stored. All their examples of "stack-only data" --
Copy types -- can be moved to the heap (e.g. put in a Vec<_>).
Copy types can also refer to the heap (or static memory, etc).
- There are types that can definitely not be
Copy which can also reside entirely on the stack.
In actuality, Copy is mostly about resource management. Types are only eligible to be Copy if they do not have a destructor. Types which need to deallocate some heap memory when destructed are not Copy, but that is not the only kind of resource management.
E.g. File cannot be Copy, even through from Rust's perspective, it's just an integer (an identifier from the OS). And though you probably won't run into this type for some time, Ref<'_, T> is a type with a destructor that can reside entirely on the stack.
Much of the official ownership chapter could be rewritten with File as the example instead of String, without going into all the stack-and-heap details.
Borrow-checking exists for reasons other than preventing dangling references, too. The Brown book has a related problem: they settled on some (unspecified) subset of what is undefined behavior in Rust, and then ask the user to judge if various examples would trigger UB or not in their quizes. But in most cases, they are really trying to point out one specific memory safety issue which (safe) Rust makes impossible. And when I last reviewed some chapters, they had some questions where they said something was not UB when it actually was -- for reasons outside the point they were trying to make.
So my advice here is to interpret those parts of the Brown book as examples of specific, definitely-bad things that could go wrong if Rust didn't have the protections that it does have. Do not take them as authoritative examples of what isn't UB in Rust. In particular, they are not suitable illustrations of how to write sound unsafe code.
Finally, if either book leaves you with questions, feel welcome to bring those questions to this forum.