Today was an intense deep dive into two core Rust concepts:
- Fixing Ownership and Borrowing Errors
- Understanding and Using Slices
π Fixing Ownership Errors in Rust
Learning how to fix ownership errors is a vital Rust skill. When the borrow checker rejects your code, understanding why and how to respond is key.
π Returning a Reference to the Stack
Trying to return a reference to a value that goes out of scope (e.g., inside a function) causes an error because the value is dropped when the function ends.
Fixes include:
- Returning the owned String instead of &String
- Returning a string literal with a 'static lifetime
- Using reference-counted pointers like Rc
- Letting the caller provide a mutable reference to populate
β Not Enough Permissions (Immutable vs Mutable)
Attempting to mutate an immutable reference causes a compile error. For example, using .push() on a &Vec is not allowed.
Fixes include:
- Using a mutable reference &mut Vec (not ideal for non-mutating APIs)
- Taking ownership of the data (not idiomatic for Vec or String)
- Cloning the vector before mutation
- Avoiding mutation entirely and appending the suffix to the final result instead
π Aliasing and Mutating a Data Structure
Holding a reference to part of a structure while trying to mutate the structure can cause conflicts. This is due to the possibility that mutation may invalidate previous references.
Fixes include:
- Cloning the data before mutation
- Splitting mutation and reference usage into separate scopes
- Extracting only the necessary data (like length) instead of the reference
π€ Copying vs Moving from Collections
With types like String (non-Copy), you cannot move the value out of a shared reference. Rust prevents you from accidentally causing double-free bugs.
Fixes include:
- Using .clone() to explicitly duplicate the data
- Borrowing the value immutably with &String
- Using Vec::remove() to safely move data out
β οΈ Safe Program Getting Rejected
Sometimes Rust rejects perfectly safe code due to limitations in borrow tracking. For example, borrowing a tuple field through a helper function may make the entire tuple seem borrowed.
Fixes include:
- Inlining the borrow instead of using a function
- Splitting the data structure or using cells if needed (e.g., RefCell)
π Mutating Different Array Elements
Rust sees array indices like a[1] and a[2] as possibly overlapping and disallows simultaneous mutable and immutable borrows.
Fixes include:
- Using methods like slice::split_at_mut()
- Ensuring mutations and reads happen in separate scopes
- Avoiding raw pointer and unsafe unless absolutely necessary
βοΈ The Slice Type in Rust
Slices let you reference a part of a collection (like a string or array) without copying it. They are non-owning references and include length metadata β making them "fat pointers".
β¨ Motivation for Slices
Functions like first_word return an index into a String, but that index can become invalid if the string is modified. This creates bugs even in safe-looking code.
β Slice to the Rescue
With slices, you return a reference to a portion of the string:
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
This ties the returned reference to the actual string and ensures that the string cannot be mutated while the slice exists.
π§ Borrowing Rules with Slices
Creating a slice from a string borrows it immutably. If you later try to mutate the original string, Rust will produce an error.
let mut s = String::from("hello");
let slice = &s[..];
s.push_str(" world"); // β Error
π Range Syntax Shortcuts
&s[..] // whole string
&s[3..] // from index 3 to end
&s[..5] // from start to index 5
π String Slices as Parameters
Instead of &String, it's idiomatic to use &str as the input type:
fn first_word(s: &str) -> &str
This makes the function work with:
- &String
- string slices
- string literals
π§© Other Slice Types
Slices also work with arrays and vectors:
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
This is useful when you want to access subranges without copying data.
β Summary
π Fixing Ownership Errors:
- Understand why a program is rejected: Is it unsafe or safe but rejected?
- Use cloning, ownership transfer, or slice-based strategies to fix issues.
- Avoid unnecessary mutations or unexpected side effects in function APIs.
- Respect the borrow checkerβs rules on lifetimes and permissions.
βοΈ Working with Slices:
- Slices are safer and easier to use than index tracking.
- They prevent bugs by tying the lifetime of the reference to the data.
- Slices work with strings, arrays, and more.
- Using &str makes functions more flexible and idiomatic.
That's a wrap for Day 4! Understanding these subtle but powerful concepts really builds the foundation for safe, performant Rust code. See you in Day 5! πͺ
Follow my journey on Twitter @subeshDev
Top comments (0)