Ownership is a property that allows values to be moved (aka transfer of ownership) or borrowed (aka shared, aka "passed by reference"). Without either moving or borrowing, I would say, ownership doesn't really mean anything.
Let's go over some samples, so we are on the same page;
fn main() {
let a = vec![1, 2, 3];
let b = a;
println!("{:?} {:?}", a, b);
}
In this example, we have two variables, a
, and b
. First, a
is given ownership over a Vec<integer>
, and the vector is given ownership over three integer
s. Second, ownership of the vector is transferred to b
. Third, both a
and b
are printed. This does not compile, of course, because the vector was moved from a
, which is now unusable. This demonstrates ownership with move semantics.
fn main() {
let a = "Hello world";
let b = a;
println!("{:?} {:?}", a, b);
}
This is slightly different, now we have given a
ownership over a &'static str
, and copied it to b
. This time the program builds successfully. The assignment on line 3 does not transfer ownership because string slices are Copy
types. Types implementing Copy
are cheap to copy around, in this case it is pointer-sized. This demonstrates ownership with copy semantics.
Some types are expensive to copy, but they may implement an alternative called Clone
which is usually a memcpy
under the hood. Cloning requires an explicit call, e.g. T::clone()
.
With move
and Copy
out of the way, there is still another use case to cover for ownership, and that is what most people will initially struggle with; borrowing. The borrowing rules are really simple, but I won't repeat them here. But I will show more code to demonstrates the rules.
// This is fine
fn foo() {
let mut a = String::from("Hello world");
a.push('!');
let b = &a;
println!("{:?} {:?}", a, b);
}
// This is not
fn bar() {
let mut a = String::from("Hello world");
let b = &a;
a.push('!');
println!("{:?} {:?}", a, b);
}
In function foo
, we have mutated a
by pushing a '!'
character to the end of the string, followed by b
borrowing a
, and printing both. This is fine because no mutation is occurring after the borrow is taken.
In function bar
, we have swapped the borrow and the push. In this case, the function fails to compile because it is in violation of the borrowing rules; namely that you cannot take an exclusive reference (mutable borrow) while a shared reference (immutable borrow) is alive. String::push()
takes &mut self
, which conflicts with b
.
In both functions, there is only a single owner! a
owns the String
(and the string owns its chars
, etc...)
This mental model of exclusive vs shared references is very fitting, much more than "mutable and immutable" anyway.
The last bit to cover, then, is shared ownership! The borrow rules can be too restrictive, in many cases. The standard library provides a means of sharing ownership in multiple places. This is done with smart pointers, which chapter 15 in The Book covers in more detail. Some smart pointers offer compile time guarantees (like the static borrow checker), and others only offer runtime guarantees.
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let a = Rc::new(RefCell::new(String::from("Hello world")));
let b = Rc::clone(&a);
a.borrow_mut().push('!');
b.borrow_mut().push('?');
println!("{:?} {:?}", a, b);
}
Now this is an interesting example! We have a non-Copy
type with shared ownership and interior mutability; both a
and b
share ownership of this value. This works because each variable owns a unique clone of a smart pointer that references the value. Both smart pointers can be used to mutate the interior value while ownership is shared. And both pointers can be used to print the value.
RefCell
is a smart pointer that allows interior mutability by guaranteeing safety at runtime. With a minor adjustment, this can be made to panic, by violating the borrowing rules:
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let a = Rc::new(RefCell::new(String::from("Hello world")));
let b = Rc::clone(&a);
let c = a.borrow_mut();
let d = b.borrow_mut();
println!("{:?} {:?} {:?} {:?}", a, b, c, d);
}
This example compiles, but panics at runtime on line 8, in which d
attempts to take an exclusive reference to the inner value while c
already has an exclusive reference.
Well, ok, I guess I do have one final thought on the discussion of ownership, and it has to do with lifetimes. This was implied in previous posts by Drop
. Non-Lexical Lifetimes made some problems with ownership much easier to reason about. E.g. this example compiles and runs just fine, even though I used an example almost identical earlier to demonstrate how the borrow on line 3 violates the borrowing rules:
fn main() {
let mut a = String::from("Hello world");
let b = &a;
println!("{:?}", b);
a.push('!');
println!("{:?}", a);
}
In this case, it works because NLL drops b
right after it is last used. This allows the exclusive reference on line 5 to be valid.
I hope that answers your questions about "what is ownership". I think if I had to succinctly describe ownership, I would explain it as something like a complex interplay between move semantics, copy semantics, borrowing, and smart pointers. A value can only ever have a single owner. But ownership can be transferred, some values can be copied (some others may be cloned), almost all values can be borrowed, and in more tricky situations shared ownership can be employed with smart pointers. Together, all of these things culminate as memory safety in Rust.