IS 4010: Application Development with Artificial Intelligence
Week 10: The soul of Rust, ownership and borrowing
Session 1: who owns this value?
We will follow one small piece of text: "Homer".
Who owns it?
Can we still use it after an assignment or function call?
When is its memory cleaned up?
Our routine: Predict → run → explain.
The next four slides are background: why memory cleanup is a problem at all, how Python handles it, and where values live. They set up ownership as Rust’s answer rather than an arbitrary rule. Do not assume students already know String, references, or memory allocation. Allow 30–60 seconds for each prediction and 2–3 minutes for each repair exercise. Answers and demonstration steps are in these notes.
Who cleans up the memory?
Every program borrows memory to store values. Someone has to give it back.
Manual (C, C++)
You call free
Forget, free twice, or free too early
Garbage collector (Python, Java, Go)
A helper running alongside your program
Extra runtime work, occasional pauses
Ownership (Rust)
Rules the compiler checks before the program runs
Learning the rules (this week!)
A library analogy works well. Manual memory is returning every book yourself: forget one and it is lost forever; return it twice or while a friend is still reading it and chaos follows. A garbage collector is a librarian who periodically walks the building collecting books nobody seems to be reading; convenient, but the librarian costs time and sometimes everyone waits. Rust puts one name on each checkout card, and the book goes back automatically the moment that person leaves. Ownership checks happen at compile time; allocating and freeing a String still happen at runtime. We will focus on ordinary owned values and references, leaving shared ownership and interior mutability for later study.
Why this matters: memory bugs are expensive
Microsoft : about 70% of the vulnerabilities it assigns a CVE each year are memory-safety issues.
Chromium : around 70% of its high-severity security bugs are memory-safety problems.
Rust’s pitch: Turn many of those bugs into compiler errors you fix before shipping.
A CVE is a publicly catalogued security vulnerability. Both figures come from large C and C++ codebases, which use manual memory management. Safe Rust prevents many memory-safety errors, but it does not guarantee correct answers or prevent every security bug; logic errors are still the programmer’s job.
Remember Python?
name = ["Homer" ]
guest = name
guest.append("Simpson" )
print (name)
Predict: What does this print?
Answer: [‘Homer’, ‘Simpson’]. In Week 03 we said Python variables point to objects. Here name and guest point to one list, so a change through guest is visible through name. Python counts how many names refer to each object and frees it when the count reaches zero, with an extra collector for cycles. Students never think about cleanup. Close with the question for today: when Rust runs let guest = name, what should happen to name? Python’s answer is “share it.” Rust’s answer is different.
Two places to store values: stack and heap
Stack: a stack of plates
Fixed, known size
Last on, first off
Fast
Cleared automatically when a function ends
let score = 5;
Heap: a restaurant host
Any size, can grow
The host finds space and hands you a table number (a pointer )
Slower to set up and to reach
Someone has to say “we’re done”
The text inside a String
The stack cleans itself up. Heap memory needs an owner.
Both analogies come from Rust Book 4.1, the assigned reading. A function’s local values sit on the stack and disappear together when it returns, like clearing a whole stack of plates. Text that can grow needs heap space; the host analogy covers finding room for a party of any size and telling friends the table number so they can find you. Ownership exists mainly to answer one question: who is responsible for giving each piece of heap memory back? Students do not need to predict where every value lives; we will draw one String’s layout shortly.
Meet an owned string
fn main() {
let name = String :: from("Homer" );
println! ("{}" , name);
}
String stores owned text that can grow.
String::from("Homer") creates a String containing Homer.
Here, the variable name is the owner of that String.
Predict: What will this print?
Try this example in Rust Playground
Answer: Homer. Run the example in Playground or a scratch Cargo project, separate from the lab. Read String::from as “make an owned String from this text.” The double colon selects the from function associated with String. Recall fn main and println! from Week 09. println! borrows this value for display; it does not take ownership. Explain the distinction from a string literal more fully when we introduce &str in session 2.
Braces show where a variable is available
fn main() {
{
let name = String :: from("Homer" );
println! ("{}" , name);
} // name leaves scope; its String is cleaned up
// println!("{}", name); // Try uncommenting this
}
Scope is the region of code where a variable can be used.
Predict: Can the final line use name outside its block?
Try this example in Rust Playground
Answer: no. Uncomment the final line and read E0425: cannot find value name in this scope. The inner braces define a block; the name binding is available inside that block. When its owner leaves scope, the String is dropped and its text buffer is freed. Cleanup is automatic; students do not add a manual free call. Distinguish this name-resolution error from the moved-value error coming next.
Assignment can move ownership
fn main() {
let name = String :: from("Homer" );
let guest = name; // The String moves to guest
println! ("{}" , guest);
// println!("{}", name); // Can we still use name?
}
Predict: Which variable owns the String after the assignment?
Try this example in Rust Playground
Answer: guest owns the String, and this program prints Homer. Pause before revealing it. The assignment does not create a second text buffer. name is no longer usable unless it is assigned a new value; we do not need that detail for this example. Point out that mut would allow mutation but would not undo a move. Next, try the commented line.
Try using the old owner
This example intentionally fails to compile.
fn main() {
let name = String :: from("Homer" );
let guest = name;
println! ("{}" , guest);
println! ("{}" , name); // name no longer owns the String
}
Predict: Which earlier line caused the problem?
Try this example in Rust Playground
Answer: let guest = name moved the String. Run this version and ask students to find both the move and the later attempted use in the diagnostic. The program does not print anything: compilation fails before execution. The final println! tries to borrow name for display, which explains “borrow of moved value.” We fix this particular example by using guest. We will learn borrowing when the original owner needs to keep using its data.
Why the old owner stops working
A String is a small header on the stack that points to its text on the heap:
Before: let guest = name;
name: [pointer, length, capacity] ──→ [ Homer ]
stack heap
After the move:
name: unavailable
guest: [pointer, length, capacity] ──→ [ Homer ]
same text
If name and guest both stayed valid, both would free the same text when main ends. Freeing twice is a classic C bug, so Rust retires name instead.
Walk through the arrows slowly. A pointer records where the text is, length counts the used bytes, and capacity records how much space is available. Students do not need to calculate sizes. Moving a String copies the header without duplicating the text; the compiler may optimize even that away. Connect back to the first slide’s table: “free twice” was one of the manual-memory failures. One owner means exactly one cleanup. Python avoids the problem by counting references at runtime; Rust avoids it by allowing only one owner, checked at compile time.
Integers copy instead
fn main() {
let score = 5 ;
let saved_score = score;
println! ("{} and {}" , score, saved_score);
}
Integers implement Copy, which marks a type as safe to duplicate automatically. Assignment copies the value.
A number has no heap text to clean up, so duplicating it is cheap and safe.
Both variables remain usable. Output: 5 and 5.
bool, char, and floating-point numbers also implement Copy.
Check: Does String behave the same way?
Try this example in Rust Playground
Answer: String does not implement Copy; its assignment moves ownership. Copy is a trait, a type’s promise that implicit duplication is allowed. We will study traits later. Copy does not mean “stored only on the stack”: integers can also be stored inside heap-allocated values. Keep the distinction about assignment behavior, not a universal rule about storage location.
Clone when you need independent text
push_str appends text. mut allows the owned string to change.
fn main() {
let name = String :: from("Homer" );
let mut guest = name. clone();
guest. push_str(" Simpson" );
println! ("{} and {}" , name, guest);
}
Predict: Does changing guest also change name?
name: [pointer, length, capacity] ──→ [ Homer ]
guest: [pointer, length, capacity] ──→ [ Homer Simpson ]
Try this example in Rust Playground
Answer: no; output is Homer and Homer Simpson. String::clone copies the text into a separate owned buffer. Both variables are valid because each owns a different String. Cloning a String allocates and copies data, so its cost depends on the text length. This independence is specific to the type’s clone behavior; do not promise that Clone always makes deep copies for every Rust type. Ask students to replace name.clone() with name: the final print now fails because name moved.
Function arguments can move too
fn welcome(guest: String ) {
println! ("Welcome, {}!" , guest);
} // guest's String is dropped here
fn main() {
let name = String :: from("Homer" );
welcome(name); // Ownership moves into the function
// println!("{}", name); // Try uncommenting this
}
Predict: Who owns the String while welcome runs?
Try this example in Rust Playground
Answer: the parameter guest owns it. The program prints Welcome, Homer!. Passing this String to a String parameter moves it, just like assignment did. Uncomment the last line to see E0382. println! inside welcome only borrows guest for display; cleanup occurs when guest leaves scope at the end of the function. Students may ask why a read-only greeting needs ownership. It does not: we deliberately revise this exact example to borrow in session 2.
Returning an owned value
fn make_name() -> String {
let name = String :: from("Homer" );
name // No semicolon: return this value
}
fn main() {
let guest = make_name();
println! ("{}" , guest);
}
Predict: Is the text cleaned up when make_name finishes?
Try this example in Rust Playground
Answer: no. Ownership moves out as the return value, and guest becomes the owner. Output: Homer. The local binding name leaves scope, but it no longer owns the returned String. The String is dropped when guest leaves scope in main. Recall Week 09’s final-expression syntax. If students add a semicolon after name, discuss the resulting type mismatch, then restore the example. No lifetime annotations are needed to return an owned String.
Checkpoint: move or copy?
fn main() {
let visits = 2 ;
let saved_visits = visits;
let name = String :: from("Homer" );
let guest = name;
println! ("{} and {}" , visits, saved_visits);
println! ("{}" , guest);
// println!("{}", name);
}
Explain: Which assignment copies? Which moves? What happens if we uncomment the last line?
Try this example in Rust Playground
Give students a minute to discuss with a neighbor before running. Answers: saved_visits = visits copies the integer, so both are usable; guest = name moves the String. Output is 2 and 2, then Homer. Uncommenting the last line causes E0382 and prevents the whole program from running. Ask for the owner of each String, rather than only whether a line compiles.
Ownership recap
Assign or pass a String
Ownership moves
Assign or pass an integer
Its value copies
Call .clone() on a String
Independent text is created
Return a String
Ownership moves to the caller
Owner leaves scope
Its value is dropped
Next question: What if a function needs to use text while the caller keeps ownership?
Use this as the session 1 stopping point. Ask students to explain “move” in one sentence. A strong answer: ownership transfers and the previous binding can no longer use that value. Session 2 provides the alternative for read-only use or temporary mutation: borrowing.
Session 2: using a value without owning it
In session 1, passing name to welcome moved it. main lost the text.
Can a function use name without taking it?
Can a function change name while main stays the owner?
What stops two parts of a program from getting in each other’s way?
Our routine: Predict → run → explain.
Briefly revisit welcome(name) from session 1. Ask why it is inconvenient if main wants to print the name afterward. Do not introduce any new syntax yet; the next slide shows the failure, and the slide after it introduces references with a picture before any code uses them.
We only wanted to read the name
This version intentionally fails to compile.
fn welcome(guest: String ) {
println! ("Welcome, {}!" , guest);
}
fn main() {
let name = String :: from("Homer" );
welcome(name);
println! ("See you soon, {}!" , name);
}
Discuss: Does welcome need to own the text just to display it?
Try this example in Rust Playground
Answer: no. The function only reads the text, but its String parameter takes ownership. The diagnostic is E0382 at the later print. Ask students to identify the intended behavior before choosing a fix. Cloning would make a second String, but we do not need independent text here. What we want instead is a way to point at main’s String without taking it. That is a reference.
A reference points to a value someone else owns
guest ──→ name: [pointer, length, capacity] ──→ [ Homer ]
(reference) (owner, lives in main) heap
A reference lets code use a value without owning it.
Making a reference is called borrowing .
In Rust, the & symbol (an ampersand) means “a reference to.”
The owner keeps ownership and still handles cleanup. Nothing is copied.
Connect this to the String diagram from session 1. The reference is just another arrow: it records where the owner’s String is. Library analogy: borrowing a book means you can read it, but it is still checked out to its owner, and you have to give it back before the owner leaves. These are checked references: the compiler verifies that the borrowed data remains valid and that conflicting uses do not overlap. Students do not need to know how a reference is represented in memory beyond “it points at the owner’s value.”
& shows up in two places
guest: String
Parameter type
This parameter takes ownership of a String
guest: &String
Parameter type
This parameter accepts a reference to a String
welcome(name)
Function call
Pass the owned value; it moves
welcome(&name)
Function call
Make a reference to name and pass that
In a type, & describes what is expected. In an expression, & creates the reference.
The first and third rows describe the original owned version of welcome; the second and fourth describe the borrowed version. Ask students which of these appears in a function definition and which appears at a call site. The next slide applies both rows to the failing example. Method calls such as guest.len() can use references directly; no manual dereferencing is needed in today’s String examples.
Borrow for the function call
fn welcome(guest: & String ) {
println! ("Welcome, {}!" , guest);
}
fn main() {
let name = String :: from("Homer" );
welcome(& name);
println! ("See you soon, {}!" , name);
}
Only two changes from the failing version: &String in the parameter and &name in the call.
Predict: Why can main still use name?
Try this example in Rust Playground
Answer: name remains the owner; welcome borrows the String. Output: Welcome, Homer! followed by See you soon, Homer!. The borrowed String is not dropped when welcome returns, because the function never owned it. Use &String here to match Lab 10’s string_length signature. Later in this session, explain why &str is a more flexible read-only text parameter; Clippy may recommend it for this example.
Predict: which of these compile?
Here input is a String in main.
A
name: String
welcome(input)
B
name: &String
welcome(&input)
C
name: String
welcome(&input)
D
name: &String
welcome(input)
A compiles: ownership moves into welcome.
B compiles: welcome borrows, and main keeps input.
C and D fail with E0308, mismatched types.
The argument must match the parameter type. Rust will not add or remove the & for you.
Give students a minute to choose before revealing. For C the compiler says expected String, found &String, and suggests removing the borrow; for D it says expected &String, found String, and suggests borrowing here. Point out that E0308 is a type error (“wrong kind of thing”), unlike E0382 or E0502, where the type is right but the timing breaks a rule. The call does not always need a literal &: if a variable already holds a reference, as in let r = &input; welcome(r);, it can be passed directly. Method calls such as input.len() do borrow automatically; ordinary function arguments do not. The &str slides later show one convenience: &input is accepted where &str is expected.
Borrow to change the string
A plain & reference can only read. &mut is a reference that is allowed to change the value.
fn add_surname(guest: & mut String ) {
guest. push_str(" Simpson" );
}
fn main() {
let mut name = String :: from("Homer" );
add_surname(& mut name);
println! ("{}" , name);
}
let mut name
The owner allows its String to change (Week 09)
&mut name
Make a reference that can change name
guest: &mut String
This parameter accepts a changing reference
Predict: What prints? Which variable owns the changed text?
Why: guest points at main’s String; it has no text of its own. Changing through guest changes name.
Try this example in Rust Playground
Answer: Homer Simpson; name remains the owner. Walk through the table: all three mut keywords are needed. You cannot hand out a changing reference to something the owner did not declare changeable. Remove one mut at a time, read the compiler feedback, then restore it. The function changes the original String through the reference. It does not create a second text buffer or take ownership. The push_str method may allocate if the existing buffer needs more capacity.
Two readers are fine
fn main() {
let name = String :: from("Homer" );
let first_reader = & name;
let second_reader = & name;
println! ("{} and {}" , first_reader, second_reader);
println! ("Owner: {}" , name);
}
Predict: Does either reference take ownership or change the text?
Try this example in Rust Playground
Answer: neither. Output: Homer and Homer, then Owner: Homer. Both references read the same String. name remains the owner. Shared references do not create independent text. Ask students to point to the two borrow expressions and to the variable responsible for eventual cleanup. Contrast this with name.clone(), which created independent storage.
Two overlapping mutable borrows fail
This example intentionally fails to compile.
fn main() {
let mut name = String :: from("Homer" );
let first_editor = & mut name;
let second_editor = & mut name; // Overlaps first borrow
first_editor. push_str(" J." );
second_editor. push_str(" Simpson" );
}
error[E0499]: cannot borrow `name` as mutable
more than once at a time
Why: first_editor is used again on a later line, so its borrow is still active when second_editor tries to start one.
Try this example in Rust Playground
Ask students to identify which borrowing rule is violated. first_editor must remain usable through its later push_str, so the second mutable borrow overlaps it. The issue is exclusive access to the same String, even though this code has only one thread. Do not describe this example as an actual data race. No output is produced because compilation fails. Predict a repair by finishing the first editor’s work before borrowing for the second editor.
Finish one edit before starting the next
fn main() {
let mut name = String :: from("Homer" );
let first_editor = & mut name;
first_editor. push_str(" J." ); // Last use of first_editor
let second_editor = & mut name;
second_editor. push_str(" Simpson" ); // Last use of second_editor
println! ("{}" , name);
}
Predict: Why do these two mutable borrows work now?
let first_editor = &mut name; ┐ first borrow
first_editor.push_str(" J."); ┘ ends at its last use
let second_editor = &mut name; ┐ second borrow
second_editor.push_str(...); ┘ ends at its last use
println!("{}", name); no borrows active
A borrow lasts until its last use, not until the closing brace.
Try this example in Rust Playground
Answer: their uses no longer overlap. Output: Homer J. Simpson. The compiler can end each borrow after its final use here. We did not add another text buffer or silence the error; we changed the sequence of accesses. The variable names remain in lexical scope, but the borrows no longer need to be active. Distinguish borrow duration from where a binding’s name is in scope. Ask what would happen if first_editor were used again after the second mutable borrow: that would make the borrows overlap again.
A reader and an editor can conflict
This example intentionally fails to compile.
fn main() {
let mut name = String :: from("Homer" );
let reader = & name;
let editor = & mut name;
editor. push_str(" Simpson" );
println! ("{}" , reader); // reader is still needed here
}
error[E0502]: cannot borrow `name` as mutable
because it is also borrowed as immutable
A shared reference comes with a promise: the value won’t change while you’re looking at it.
Why: reader is used on the last line, so editor changing the text would break that promise.
Try this example in Rust Playground
Answer to “which rule?”: shared and exclusive borrows of the same data cannot overlap. Trace reader from its creation to its final println!. editor is created within that interval. The compiler rejects the pattern even though the operations would run one after another: the later use of reader keeps the shared borrow active across the mutable borrow. Ask students to move the read before the mutable borrow and predict the result.
A borrow can end after its last use
fn main() {
let mut name = String :: from("Homer" );
let reader = & name;
println! ("Before: {}" , reader); // Last use of reader
let editor = & mut name;
editor. push_str(" Simpson" );
println! ("After: {}" , name);
}
Why it works: reader’s borrow ends at its last use, before editor starts.
Ending a borrow does not drop the String. Only the owner’s scope decides when the text is cleaned up.
Try: Add println!("{}", reader); at the end. Will it still compile?
Try this example in Rust Playground
Output: Before: Homer, then After: Homer Simpson. The read finishes before the mutable borrow begins. Adding the suggested final print makes the example fail with E0502: reader would now be used after the edit, extending the shared borrow across the mutable borrow. Run both versions. This behavior is often called non-lexical lifetimes; students do not need the term to use the rule. Ending a borrow does not drop the owned String. name still owns its String until the end of main.
Declare now, assign later
So far every let gave the variable a value right away. You can also split that into two steps:
fn main() {
let total; // Declare total now
{
let score = 5 ;
total = score * 2 ; // Give total its value
}
println! ("{}" , total);
}
let total; creates the variable but gives it no value yet.
Rust works out the type from the later assignment, here an integer.
Reading total before it has a value is a compile error.
Without mut, the value can be assigned only once.
Why do this? total is declared outside the braces, so it is still available after them. This prints 10.
Try this example in Rust Playground
This is new syntax; Week 09 only showed let with a value. Keep the example free of borrowing so the let form is the only new idea. Optional demos: move the println! above the inner block to see E0381 (used binding isn’t initialized), or add a second assignment to see E0384 (cannot assign twice to immutable variable). It works here because total holds its own integer, copied out of the block. The next slide uses the same shape with a reference instead, and that changes the answer. Python has no equivalent, since a Python name only exists once it is assigned.
Borrowed data must still exist
This example intentionally fails to compile.
fn main() {
let reader; // Declared now, given a value below
{
let name = String :: from("Homer" );
reader = & name;
} // name's String is dropped here
println! ("{}" , reader); // Too late to read that String
}
Same shape as the last slide, but reader holds a reference to name, not its own value.
error[E0597]: `name` does not live long enough
Why: name’s text is cleaned up at the closing brace, so reader would point at text that no longer exists (a dangling reference ).
Try this example in Rust Playground
Stress that let reader; does not create a null reference. Ask students to identify the owner, the reference, and the closing brace where the String is dropped. The reference would point to data that no longer exists, a dangling reference, so Rust rejects the program before it runs. This is the second borrowing rule. We do not need any named lifetime syntax to understand the error.
Keep the owner available for the read
fn main() {
let name = String :: from("Homer" );
let reader;
{
reader = & name;
}
println! ("{}" , reader);
} // name's String is dropped here
What changed: The let name line now sits above the braces. name still owns the String, and it now lives past the final read.
Try this example in Rust Playground
Output: Homer. If students say we “moved” name, clarify that relocating a line of code is not a Rust ownership move; no ownership transferred. The braces remain to make the minimal edit visible. The lifetime of a reference is the region where it must be valid; it cannot require access after the borrowed data is gone. This is enough lifetime reasoning for the required lesson and Lab 10’s scope puzzle.
Return owned data when you create it
fn make_name() -> String {
String :: from("Homer" )
}
fn main() {
let name = make_name();
println! ("{}" , name);
}
The function creates text and transfers ownership to its caller.
Returning a reference to a local String would leave no owner to keep it alive.
Try this example in Rust Playground
Answer: this prints Homer and is the shorter form of session 1’s owned return example. The caller owns the returned String; it stays valid after the function returns. Explain the alternative in words rather than adding several new errors to one slide. A function can return a reference borrowed from caller-owned data; it cannot return a reference into its own local String that is dropped on return. Adding a lifetime annotation would not keep a local value alive.
Two useful text types: String and &str
fn main() {
let name = String :: from("Homer" );
let borrowed_text: & str = name. as_str();
let literal: & str = "Simpson" ;
println! ("{} {}" , borrowed_text, literal);
}
String: owned text that can grow.
&str: a shared view of text, called a string slice .
str is the text itself. You always reach it through a reference, so you will see it written &str.
.as_str() borrows the text in a String; it does not copy it.
Try this example in Rust Playground
Output: Homer Simpson. A slice can view all or part of some text; we use the whole text here and avoid introducing slicing indexes. The string literal has type &str and refers to text stored in the program, rather than to a local owned String. Neither borrowed_text nor literal owns a String buffer. We introduced String::from at the start; now students can distinguish its input literal from the owned String it creates. The explicit &str annotations are for teaching; Rust can infer these types.
&str accepts borrowed text and literals
fn show_size(text: & str ) {
println! ("{} bytes" , text. len());
}
fn main() {
let name = String :: from("Homer" );
show_size(name. as_str());
show_size("Simpson" );
println! ("Still owned here: {}" , name);
}
Predict: Can the caller still use name?
Try this example in Rust Playground
Answer: yes. Output: 5 bytes, then 7 bytes, then Still owned here: Homer. String and str len count UTF-8 bytes, not necessarily characters; these ASCII examples have one byte per character. Read-only text functions usually prefer &str to &String because &str works with both String text and literals. Rust also converts &name to &str automatically when the function expects &str; demonstrate changing name.as_str() to &name after students understand the explicit version. Keep Lab 10’s supplied &String signature unchanged: it is deliberate borrowing practice. This example displays a size instead of implementing the graded string_length function.
Choose access to match the job
Own the text
String
name
Read an existing String
&String
&name
Read text from different sources
&str
name.as_str() or "Homer"
Change the caller’s String
&mut String
&mut name
Ask first: Do I need to own, read, or change this value?
Have students select a type for each situation: a function that stores an owned name for later (String), displays a message (&str), or adds a surname to the caller’s string (&mut String). The &String row matches the introductory example and the fixed lab contract; prefer &str when designing a general read-only text interface. These types communicate permissions and ownership. &mut String is needed when the function must grow the String, as push_str does.
Borrow or clone?
let name = String :: from("Homer" );
let reader = & name; // Points at name's text
let copy = name. clone(); // Makes new text
reader ──→ name: [pointer, length, capacity] ──→ [ Homer ]
copy: [pointer, length, capacity] ──→ [ Homer ]
Creates new text?
No
Yes, a full copy
Who owns it?
name
copy owns its own
Usable after name is dropped?
No
Yes
Cost
Nearly free
Grows with the text’s length
Borrow by default. Clone when you truly need independent text.
Both let main keep using name, which is why students confuse them. AI assistants often suggest .clone() to silence a borrow error; that compiles, but it changes the program’s meaning when the goal was to update or share the original. Ask: if a function needs to change the caller’s text, which one works? Only &mut; a clone would change a copy. Connect the diagram to the clone slide from session 1.
Your turn: fix the access
This example intentionally fails to compile.
fn add_surname(guest: & String ) {
guest. push_str(" Simpson" );
}
fn main() {
let name = String :: from("Homer" );
add_surname(& name);
println! ("{}" , name);
}
Repair: Change the original string through a borrow. Explain each edit.
Try this example in Rust Playground
Give students 2–3 minutes. Expected initial diagnostic: E0596, cannot borrow through a shared reference as mutable. Three edits: parameter guest: &mut String, binding let mut name, and argument &mut name. Output after repair: Homer Simpson. This recreates the earlier working example for retrieval practice. Ask students why making only the binding mutable is insufficient: the function’s parameter still permits only shared access. Do not solve this by cloning: the goal is to update the original String.
Lab 10: apply these access patterns
Part 1: Repair seven borrow-checker puzzles and run them.
Part 2: Implement the four supplied functions; keep their signatures.
to_uppercase_owned
String → owned String
string_length
&String → usize
append_suffix
&mut String and &str → change the original
concat_strings
Two &str inputs → new owned String
Follow the Lab 10 instructions . Leave the supplied tests unchanged.
The public lab repository is authoritative. Part 1 is practice and is not graded automatically. Part 2 contains the graded implementations. Have students describe ownership at each function boundary before coding. Returning a newly created String from concat_strings is different from returning a borrowed input, so it needs no named lifetime parameter. Do not provide implementations of these four functions in the deck. The classroom examples prepare students to choose and explain access patterns without supplying their submissions.
Check and submit Lab 10
Run these inside your fork’s week10/ directory:
cargo test
cargo fmt
cargo clippy -- -D warnings
CI checks formatting with cargo fmt --check.
Review git diff, commit week10/, and push.
Confirm that the Week 10 README badge is green.
These commands and the submission workflow match week10/lab10.md. cargo fmt applies formatting; cargo fmt –check verifies without rewriting. A green Week 10 badge represents the complete 10-point lab grade. Run the checks after your final edits, and investigate diagnostics rather than changing supplied tests to make them pass.
Explain it in your own words
Move: Who owns the value now?
Copy or clone: Did we create a separate value?
Borrow: What access does the function need?
Validity: Will the borrowed data still exist when it is used?
Next week: Use these ideas with structs, enums, and pattern matching.
Ask each student to explain one repair using the actual variable names. Revisit the example if they cannot identify the owner or conflicting uses yet. Encourage small experiments: changing one line and predicting the result is useful practice. Students can stop after the resources slide; the remaining slides are an optional preview of named lifetime annotations, not required for Lab 10.
Resources and questions
Start here:
Practice: Rust Playground and Rustlings .
Optional reading: Rust Book: validating references with lifetimes .
Pause for questions and end the required lesson here. Rust Book Chapter 4 is the primary reading. Rustlings is additional practice. The next section can be skipped or revisited after students have successfully explained the core borrowing examples. Named lifetime parameters and reference-containing structs are not required by this week’s lab.
Optional extension: named lifetimes
We already checked that borrowed data stays valid through each use.
Sometimes a function signature must also describe which inputs a returned reference can borrow from .
That is when a name such as 'a can help.
This section is optional; Lab 10 does not require named lifetimes.
Continue only if the main ownership and borrowing examples are comfortable. A named lifetime is a label for a validity relationship, not a timer or an instruction to allocate data. The earlier owned-return functions need no such relationship because they return ownership. Avoid introducing structs containing references or generic trait bounds here.
Optional: give the relationship a name
fn longest< 'a > (first: & 'a str , second: & 'a str ) -> & 'a str {
if first. len() >= second. len() {
first
} else {
second
}
}
fn main() {
println! ("{}" , longest("Homer" , "Simpson" ));
}
<'a> after the function name declares the label, much like declaring a variable before using it.
'a says: the result can be used only while both inputs still exist .
Annotations describe a relationship; they do not keep data alive.
Try this example in Rust Playground
Output: Simpson. Read apostrophe-a as a lifetime label declared after the function name. It is not the single-quoted char ‘a’. Both input references must be valid for the selected lifetime, and the output reference is usable for that lifetime. The original values need not have identical lifetimes; callers are limited to their common valid region, often described as the shorter input lifetime. The annotation does not extend either value’s life, create a String, or choose the returned input. The if expression still chooses the input at runtime.