This entire comment is about the articles in the first link.
These articles are pretty good on the whole. In general I was more satisfied the further I went. Their presentation of ownership is a bit overgeneralized without noting that it's generalized (which is common). They present references as immutable/mutable (in contrast with shared/exclusive) without pointing out that shared mutability is possible (also common). They have some bigger misconceptions about reference lifetimes or perhaps lifetimes generally.
Almost all these gripes are from part 1. By the time I finished reading I got the impression that the author is aware of at least some of the overgeneralizations. Personally I would prefer if they were explicit about generalizing upfront, preferably some note about the exceptions are -- I have a negative reaction to counterfactual statements and strongly dislike having to go back and relearn things after I found out I wasn't given the whole truth.
I also think this was written from a particular perspective (without calling it out), coming from Java maybe? Some of their points would probably be better served by being explicit about the comparison / POV assumptions.
More detailed notes follow. Everything after part 1 is a nit or clarification.
Part 1
A core aspect of Rust is that every piece of data has one explicit owner.
[...]
the information that a garbage collector tracks at runtime is known to Rust at compile time
Shared ownership can be modeled in Rust. Not everything is known at compile time.
Arguably static
and leaked data has no owner.
A borrow might be implemented using a reference, but it might not.
I mean, if you say use a reference, it's going to use a reference. Maybe this note was here due to coming from a language where "reference" meant something else than a Rust reference, and the author wanted to warn against assuming a Rust reference is the as what "reference" means in some other language.
Similarly, Rust makes no guarantees about move's implementation.
I don't know what they're getting at with this either. There are no move
constructors. A move is notionally a memcpy
:
It’s important to note that in these two examples, the only difference is whether you are allowed to access x
after the assignment. Under the hood, both a copy and a move can result in bits being copied in memory, although this is sometimes optimized away.
(The above quote is from here.)
If we intend to mutate data we must declare our variables using mut
mut
bindings allow directly overwriting and taking a &mut
to the bound variable, to be precise. There are various ways to mutate things even if there is no mut
binding (see "interior mutability" below for one way).
They do a good job covering how you can just move to a mut
binding in part 2.
They do a good job of covering how the lack of a mut
binding don't mean the underlying data has some immutable property in part 3.
Borrows, mutable or not, exist for the lifetime of their scope. In all of the above cases, the borrows exist until the end of main
, which is why we violate the requirements of multiple readers OR one writer.
No, that's incorrect. References haven't been limited to scopes since NLL landed over 5 years ago. The requirements are violated in the examples because they try to interleave the &mut
borrows with the &
borrows and/or use of the borrowed struct.
Shared borrows and exclusive borrows are better names than immutable borrows and mutable borrows, despite the mut
keyword. In particular, mutating through a &
is possible in some cases. This is called interior mutability in the documentation, but another description is shared mutability.
Here's their "limited" code block, with no function boundaries
fn main() {
let a1 = 1;
let a2 = &a1;
let a3 = &a1;
println!("{a1:?} {a2:?} {a3:?}");
let mut b1 = 1;
let b2 = &mut b1;
println!("{b2}");
let b3 = &mut b1;
println!("{b3}");
println!("{b1}");
let mut c1 = 1;
let c2 = &c1;
println!("{c1} {c2}");
let c3 = &mut c1;
println!("{c3}");
let mut d1 = 1;
let d2 = &mut d1;
println!("{d2}");
let d3 = &d1;
println!("{d3} {d1}",)
}
Playground.
Part 2
I didn't have any nits here worth writing out.
Part 3
Most my nits here would be repeats from part 1 (e.g. shared ownership). Exactly when deallocations occur is sometimes determined at run-time. Rust does determine where values do or may possibly drop at compile time.
Maybe they're getting around to pointing these things out...
Everything we've discussed here is the default behavior. It's the set of rules that most of your Rust code will live under. It provides the strongest safety guarantees and the lowest runtime overhead. However, Rust provides constructs to change these rules on a case by case basis. This often involves deferring compile-time checks to the runtime. Hopefully we'll get to talk about these more advanced cases soon.
Destructors are more general than Drop
.
Part 4
Literal &str
s are &'static str
s.
Not the point of the code but here's how I might write the word counter.
A &mut str
on the other hand, is something you'll rarely, if ever, use. It doesn't own the underlying data so it can't really change it.
It definitely can, there's just relatively few cases where you can validly mutate UTF8 data (like a str
is) in-place, since it's a variable-width encoding. (ASCII bytes are always one UTF8 byte, which is why &mut str
can be used to perform the lowercasing.)
Technically, str
is the slice and &str
is the slice with an added length value. But str
is so infrequently used that people just refer to &str
as a slice.
More generally, a str
is basically a [u8]
with more invariants. Slices like str
and [u8]
and any other [T]
are dynamically sized types (DSTs). References to slices like &[T]
and &mut [T]
are very often also just called slices. So unless the material you're reading is being very explicit/pedantic/careful, you just have to figure out from context whether a "slice" means the DST or some sort of pointer to the DST.
Since it doesn't need ownership of the parameter, &str
is the correct choice.
&String
doesn't give ownership either. It's more that
But since this is so common, the Rust compiler will also let us call this function with a &String
:
The technical term is "deref coercion". (String
dereferences to str
; &String
deref-coerces to &str
.)