📘 PHẦN 2: OWNERSHIP VÀ BORROWING
🎯 Mục tiêu tổng quát
- Hiểu sâu về ownership - đặc trưng quan trọng nhất của Rust
- Nắm vững move semantics và copy trait
- Biết cách sử dụng references và borrowing đúng cách
- Làm việc với slices hiệu quả
- Hiểu lifetimes và cách compiler kiểm tra
- Sử dụng smart pointers cơ bản: Box, Rc, RefCell
- Xây dựng ứng dụng memory-safe mà không có garbage collector
🧑🏫 Bài 1: Ownership - Khái niệm cốt lõi của Rust
Memory management models
Các cách quản lý memory:
Manual memory management (C/C++)
c// C code int* ptr = malloc(sizeof(int)); *ptr = 42; free(ptr); // Phải nhớ free- Ưu: Hiệu suất cao, kiểm soát hoàn toàn
- Nhược: Dễ gây memory leaks, dangling pointers, double free
Garbage Collection (Java, Python, Go)
java// Java code String s = new String("hello"); // GC tự động thu hồi memory- Ưu: Dễ sử dụng, an toàn
- Nhược: Runtime overhead, pause times, không predictable
Ownership (Rust)
rustfn main() { let s = String::from("hello"); // Compiler tự động drop khi out of scope } // s dropped here- Ưu: Memory safe + Zero cost
- Nhược: Learning curve cao
Ownership rules
Ba quy tắc vàng của Ownership:
- Each value in Rust has a variable that's called its owner
- There can only be one owner at a time
- When the owner goes out of scope, the value will be dropped
fn main() {
{
let s = String::from("hello"); // s is owner
// s is valid here
} // s goes out of scope, memory freed automatically
// println!("{}", s); // ERROR: s no longer exists
}Stack vs Heap:
fn main() {
// Stack allocated (known size, fast)
let x = 5; // Copy on stack
let y = x; // Copy on stack
println!("x={}, y={}", x, y); // Both valid
// Heap allocated (unknown size, slower)
let s1 = String::from("hello"); // Heap allocated
let s2 = s1; // Move (not copy!)
// println!("{}", s1); // ERROR: s1 moved to s2
println!("{}", s2); // OK
}Visualizing memory:
Stack allocated:
x: 5 -> copied to y
y: 5
Heap allocated:
s1: [ptr, len, capacity] -> moved to s2
s2: [ptr, len, capacity] -> [h,e,l,l,o] on heapMove semantics
Move khi assign:
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 moved to s2
// println!("{}", s1); // ERROR: value borrowed after move
println!("{}", s2); // OK
}Tại sao move?
- Prevent double free
- Ensure single owner
- Zero cost abstraction
Move with functions:
fn main() {
let s = String::from("hello");
takes_ownership(s); // s moved into function
// println!("{}", s); // ERROR: s moved
}
fn takes_ownership(some_string: String) {
println!("{}", some_string);
} // some_string dropped hereReturn ownership:
fn main() {
let s1 = gives_ownership(); // Function gives ownership
println!("{}", s1);
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2); // s2 moved in, s3 gets it back
// println!("{}", s2); // ERROR
println!("{}", s3); // OK
}
fn gives_ownership() -> String {
let some_string = String::from("hello");
some_string // Ownership moved to caller
}
fn takes_and_gives_back(a_string: String) -> String {
a_string // Return ownership
}Copy trait
Types that implement Copy:
fn main() {
// All these types implement Copy trait
let x = 5; // i32
let y = 3.14; // f64
let c = 'a'; // char
let b = true; // bool
let t = (1, 2); // tuple (if all elements implement Copy)
let arr = [1, 2, 3]; // array (if element type implements Copy)
// Copy happens
let x2 = x;
println!("x={}, x2={}", x, x2); // Both valid
// Tuple with Copy
let point = (10, 20);
let point2 = point;
println!("point: {:?}, point2: {:?}", point, point2);
}Types that DON'T implement Copy:
fn main() {
let s1 = String::from("hello"); // String
let v1 = vec![1, 2, 3]; // Vec
let b1 = Box::new(5); // Box
// These are moved, not copied
let s2 = s1; // s1 moved
let v2 = v1; // v1 moved
let b2 = b1; // b1 moved
}Rule: A type can't implement Copy if it or any of its parts implement Drop
Clone
Explicit deep copy:
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // Explicit deep copy
println!("s1={}, s2={}", s1, s2); // Both valid
// Clone can be expensive!
let v1 = vec![1, 2, 3, 4, 5];
let v2 = v1.clone(); // Copies all elements
println!("v1: {:?}", v1);
println!("v2: {:?}", v2);
}Clone vs Copy:
| Copy | Clone | |
|---|---|---|
| Implicit/Explicit | Implicit | Explicit (clone()) |
| Cost | Cheap (stack copy) | Can be expensive |
| Trait | Copy trait | Clone trait |
| Usage | Automatic | Manual call |
When to clone:
fn main() {
let s1 = String::from("expensive data");
// Avoid unnecessary clones
process_string(&s1); // Borrow instead
println!("{}", s1);
// Clone when needed
let s2 = s1.clone();
std::thread::spawn(move || {
println!("{}", s2); // s2 moved into thread
});
println!("{}", s1); // s1 still valid
}
fn process_string(s: &String) {
println!("{}", s);
}Ownership và functions
Complete example:
fn main() {
let s = String::from("hello");
// Move into function
takes_ownership(s);
// println!("{}", s); // ERROR: moved
let x = 5;
makes_copy(x);
println!("{}", x); // OK: x implements Copy
// Get ownership back
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("String '{}' has length {}", s2, len);
}
fn takes_ownership(some_string: String) {
println!("{}", some_string);
} // some_string dropped
fn makes_copy(some_integer: i32) {
println!("{}", some_integer);
} // nothing special happens
fn calculate_length(s: String) -> (String, usize) {
let length = s.len();
(s, length) // Return ownership
}Problem with ownership:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(s1);
// Can't use s1 anymore!
// println!("Length of '{}' is {}", s1, len); // ERROR
}
fn calculate_length(s: String) -> usize {
s.len()
} // s droppedSolution: Use references! (next lesson)
🧑🏫 Bài 2: References và Borrowing
Immutable references
Borrowing without taking ownership:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // Borrow s1
println!("Length of '{}' is {}", s1, len); // s1 still valid!
}
fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope, but doesn't drop (not owner)Visual representation:
s1 (owner) ──────> [String data on heap]
↑
│
&s1 (reference) ─┘Multiple immutable references:
fn main() {
let s = String::from("hello");
let r1 = &s; // OK
let r2 = &s; // OK
let r3 = &s; // OK
println!("{}, {}, {}", r1, r2, r3); // All valid
}References are immutable by default:
fn main() {
let s = String::from("hello");
let r = &s;
// r.push_str(", world"); // ERROR: cannot mutate through immutable reference
println!("{}", r);
}Mutable references
Borrow mutably:
fn main() {
let mut s = String::from("hello");
change(&mut s); // Mutable borrow
println!("{}", s); // "hello, world"
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}Only ONE mutable reference at a time:
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
// let r2 = &mut s; // ERROR: cannot borrow as mutable more than once
println!("{}", r1);
}Why this restriction?
- Prevent data races at compile time
- Data race occurs when:
- Two or more pointers access same data
- At least one is writing
- No synchronization mechanism
Can create multiple mutable references in different scopes:
fn main() {
let mut s = String::from("hello");
{
let r1 = &mut s;
r1.push_str(" world");
} // r1 goes out of scope
let r2 = &mut s;
r2.push_str("!");
println!("{}", s); // "hello world!"
}Borrowing rules
The Rules:
- You can have EITHER:
- One mutable reference
- Any number of immutable references
- References must always be valid
Cannot mix mutable and immutable:
fn main() {
let mut s = String::from("hello");
let r1 = &s; // OK
let r2 = &s; // OK
// let r3 = &mut s; // ERROR: cannot borrow as mutable
println!("{}, {}", r1, r2);
}Why?
// If this was allowed:
let mut s = String::from("hello");
let r1 = &s;
let r2 = &mut s; // Hypothetically allowed
r2.push_str(" world");
println!("{}", r1); // r1 would see unexpected change!Valid pattern:
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{}, {}", r1, r2); // r1, r2 last used here
let r3 = &mut s; // OK: r1, r2 no longer used
r3.push_str(" world");
println!("{}", r3);
}Dangling references
Rust prevents dangling references:
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // ERROR: returns a reference to data owned by function
let s = String::from("hello");
&s // s will be dropped, reference would be dangling
} // s goes out of scope and is droppedCorrect version:
fn main() {
let string = no_dangle();
println!("{}", string);
}
fn no_dangle() -> String {
let s = String::from("hello");
s // Transfer ownership
}Another dangling example:
fn main() {
let r;
{
let x = 5;
// r = &x; // ERROR: x doesn't live long enough
}
// println!("{}", r); // x dropped, r would be dangling
}Reference scope
Non-Lexical Lifetimes (NLL):
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// r1 and r2 are no longer used after this point
let r3 = &mut s; // OK: no overlap with r1, r2
println!("{}", r3);
}Before NLL (Rust 2015):
// This would error in older Rust
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// let r3 = &mut s; // ERROR in Rust 2015
// Scope of r1, r2 extends to end of block
}Practical example:
fn main() {
let mut data = vec![1, 2, 3];
// Read-only access
let first = &data[0];
println!("First element: {}", first);
// Modify
data.push(4); // OK: first no longer used
println!("Data: {:?}", data);
}🧑🏫 Bài 3: Slices
String slices
String slice type: &str
fn main() {
let s = String::from("hello world");
let hello = &s[0..5]; // "hello"
let world = &s[6..11]; // "world"
println!("{} {}", hello, world);
}Slice syntax:
fn main() {
let s = String::from("hello world");
let slice1 = &s[0..5]; // "hello"
let slice2 = &s[..5]; // Same as [0..5]
let slice3 = &s[6..11]; // "world"
let slice4 = &s[6..]; // Same as [6..len]
let slice5 = &s[..]; // Entire string
let slice6 = &s[0..s.len()]; // Same as [..]
println!("{}", slice1);
}Why slices?
// Without slices
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {
let mut s = String::from("hello world");
let word_length = first_word(&s);
s.clear(); // s is now empty
// word_length still has value 5, but s is empty!
// Compiler can't catch this bug
}With slices:
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[..]
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
// s.clear(); // ERROR: can't borrow mutably while immutable borrow exists
println!("First word: {}", word);
}Array slices
Slice type: &[T]
fn main() {
let arr = [1, 2, 3, 4, 5];
let slice = &arr[1..3]; // [2, 3]
println!("Slice: {:?}", slice);
println!("Slice length: {}", slice.len());
}Working with slices:
fn main() {
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let first_half = &numbers[..5];
let second_half = &numbers[5..];
println!("First half: {:?}", first_half);
println!("Second half: {:?}", second_half);
print_slice(first_half);
print_slice(second_half);
}
fn print_slice(slice: &[i32]) {
print!("[");
for (i, &item) in slice.iter().enumerate() {
print!("{}", item);
if i < slice.len() - 1 {
print!(", ");
}
}
println!("]");
}Slice as parameters
Better function signatures:
fn main() {
let s = String::from("hello world");
let literal = "hello world";
let arr = [1, 2, 3, 4, 5];
// Works with both String and string literal
let word1 = first_word(&s);
let word2 = first_word(literal);
println!("{}, {}", word1, word2);
// Works with both array and slice
let sum1 = sum_slice(&arr);
let sum2 = sum_slice(&arr[2..4]);
println!("sum1={}, sum2={}", sum1, sum2);
}
// Use &str instead of &String for flexibility
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
// Use &[T] instead of &Vec<T> or &[T; N]
fn sum_slice(slice: &[i32]) -> i32 {
let mut sum = 0;
for &item in slice {
sum += item;
}
sum
}Why &str is better than &String:
// Bad: only accepts &String
fn process_bad(s: &String) {
println!("{}", s);
}
// Good: accepts both &String and &str
fn process_good(s: &str) {
println!("{}", s);
}
fn main() {
let string = String::from("hello");
let literal = "world";
// process_bad(&string); // OK
// process_bad(literal); // ERROR!
process_good(&string); // OK
process_good(literal); // OK
}String literals as slices
String literals are slices:
fn main() {
// Type is &str
let s: &str = "Hello, world!";
// Stored in binary, immutable
// &str is a reference to that immutable data
println!("{}", s);
}String vs &str:
fn main() {
// String: owned, mutable, heap-allocated
let mut s1 = String::from("hello");
s1.push_str(" world");
// &str: borrowed, immutable, can be stack or binary
let s2: &str = "hello";
// s2.push_str(" world"); // ERROR: can't modify
// Convert String to &str
let s3: &str = &s1;
let s4: &str = s1.as_str();
// Convert &str to String
let s5: String = s2.to_string();
let s6: String = String::from(s2);
println!("{}, {}", s1, s2);
}Other slice types
Generic slices:
fn main() {
let numbers: Vec<i32> = vec![1, 2, 3, 4, 5];
let slice: &[i32] = &numbers[1..4];
let chars: Vec<char> = vec!['a', 'b', 'c'];
let char_slice: &[char] = &chars[..];
println!("{:?}", slice);
println!("{:?}", char_slice);
}Mutable slices:
fn main() {
let mut numbers = [1, 2, 3, 4, 5];
let slice = &mut numbers[1..4];
slice[0] = 10;
slice[1] = 20;
println!("{:?}", numbers); // [1, 10, 20, 4, 5]
}
fn double_values(slice: &mut [i32]) {
for item in slice {
*item *= 2;
}
}🧑🏫 Bài 4: Lifetimes cơ bản
Lifetime concepts
Why lifetimes?
fn main() {
let string1 = String::from("long string");
let result;
{
let string2 = String::from("short");
result = longest(string1.as_str(), string2.as_str());
} // string2 dropped here
// println!("{}", result); // result might reference dropped data!
}
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}Compiler error:
error[E0106]: missing lifetime specifier
--> src/main.rs
|
| fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameterLifetime annotations
Syntax:
&i32 // Reference
&'a i32 // Reference with explicit lifetime
&'a mut i32 // Mutable reference with explicit lifetimeAnnotating longest:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}What this means:
- Return value lives as long as the shorter of x or y
- Compiler will enforce this
Valid usage:
fn main() {
let string1 = String::from("long string");
let string2 = String::from("short");
let result = longest(string1.as_str(), string2.as_str());
println!("Longest: {}", result);
} // Both string1 and string2 still validInvalid usage:
fn main() {
let string1 = String::from("long string");
let result;
{
let string2 = String::from("short");
result = longest(string1.as_str(), string2.as_str());
} // ERROR: string2 doesn't live long enough
println!("{}", result);
}Multiple lifetimes:
fn main() {
let s1 = String::from("hello");
let s2 = String::from("world");
let result = first_word_or_second(&s1, &s2);
println!("{}", result);
}
// Return has lifetime of first parameter only
fn first_word_or_second<'a, 'b>(first: &'a str, second: &'b str) -> &'a str {
if first.len() > 0 {
first
} else {
second // This would error if we try to return second
}
}Lifetime elision
Compiler can infer lifetimes in simple cases:
Before elision rules:
fn first_word<'a>(s: &'a str) -> &'a str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}After elision rules (can omit):
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}Elision rules:
- Each parameter gets its own lifetime
- If one input lifetime, assign to output
- If multiple input lifetimes and one is
&selfor&mut self, assign self's lifetime to output
Examples:
// Rule 1 + 2
fn example1(s: &str) -> &str { s }
// Becomes: fn example1<'a>(s: &'a str) -> &'a str { s }
// Rule 1 only (can't infer output)
// fn example2(x: &str, y: &str) -> &str { x } // ERROR: can't infer
// With explicit lifetime
fn example2<'a>(x: &'a str, y: &str) -> &'a str { x }
// Rule 3 (methods)
impl MyStruct {
fn get_data(&self) -> &str {
&self.data
}
// Becomes: fn get_data<'a>(&'a self) -> &'a str
}Struct lifetimes
Struct holding references:
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find '.'");
let excerpt = ImportantExcerpt {
part: first_sentence,
};
println!("Excerpt: {}", excerpt.part);
} // excerpt must not outlive novelInvalid usage:
fn main() {
let excerpt;
{
let novel = String::from("Short story.");
excerpt = ImportantExcerpt {
part: &novel,
};
} // ERROR: novel doesn't live long enough
println!("{}", excerpt.part);
}Methods with lifetimes:
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention: {}", announcement);
self.part
}
}
fn main() {
let novel = String::from("Once upon a time...");
let first = novel.split(' ').next().unwrap();
let excerpt = ImportantExcerpt { part: first };
println!("Level: {}", excerpt.level());
println!("Part: {}", excerpt.announce_and_return_part("Breaking news"));
}Common lifetime patterns
The static lifetime:
fn main() {
// String literals have 'static lifetime
let s: &'static str = "I have a static lifetime.";
// Lives for entire duration of program
// Stored in binary
println!("{}", s);
}Don't overuse 'static:
// Bad: unnecessarily restrictive
fn bad_example(x: &'static str) -> &'static str {
x
}
// Good: more flexible
fn good_example<'a>(x: &'a str) -> &'a str {
x
}Multiple lifetime parameters:
fn longest_with_announcement<'a, 'b>(
x: &'a str,
y: &'a str,
ann: &'b str,
) -> &'a str {
println!("Announcement: {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}🧑🏫 Bài 5: Smart Pointers cơ bản
Box<T>
Heap allocation:
fn main() {
// Store i32 on heap
let b = Box::new(5);
println!("b = {}", b);
// Automatically dereferenced
let x = *b + 10;
println!("x = {}", x);
} // b dropped, heap memory freedWhen to use Box:
- Unknown size at compile time
- Transfer ownership of large data without copying
- Trait objects
Recursive types:
// ERROR: infinite size
// enum List {
// Cons(i32, List),
// Nil,
// }
// OK: Box has known size
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
print_list(&list);
}
fn print_list(list: &List) {
match list {
Cons(value, next) => {
print!("{} -> ", value);
print_list(next);
}
Nil => println!("Nil"),
}
}Large data on heap:
fn main() {
// Stack allocated (expensive to move)
let large_array = [0; 1000000];
// Heap allocated (cheap to move, just pointer)
let boxed_array = Box::new([0; 1000000]);
// Moving boxed_array just moves the pointer
let moved = boxed_array;
}Rc<T> - Reference Counting
Multiple ownership:
use std::rc::Rc;
fn main() {
let a = Rc::new(5);
println!("Count: {}", Rc::strong_count(&a)); // 1
let b = Rc::clone(&a);
println!("Count: {}", Rc::strong_count(&a)); // 2
{
let c = Rc::clone(&a);
println!("Count: {}", Rc::strong_count(&a)); // 3
}
println!("Count: {}", Rc::strong_count(&a)); // 2
} // All cleaned up when count reaches 0Rc with list:
use std::rc::Rc;
enum List {
Cons(i32, Rc<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
println!("Count after creating a: {}", Rc::strong_count(&a));
let b = Cons(3, Rc::clone(&a));
println!("Count after creating b: {}", Rc::strong_count(&a));
{
let c = Cons(4, Rc::clone(&a));
println!("Count after creating c: {}", Rc::strong_count(&a));
}
println!("Count after c goes out of scope: {}", Rc::strong_count(&a));
}Rc limitations:
- Single-threaded only (use
Arcfor multi-threaded) - Immutable only (use
RefCellfor mutability)
RefCell<T> - Interior Mutability
Mutate through immutable reference:
use std::cell::RefCell;
fn main() {
let x = RefCell::new(5);
// Borrow immutably
{
let borrowed = x.borrow();
println!("Value: {}", *borrowed);
} // borrowed dropped
// Borrow mutably
{
let mut borrowed_mut = x.borrow_mut();
*borrowed_mut += 10;
} // borrowed_mut dropped
println!("New value: {}", x.borrow());
}Runtime borrowing rules:
use std::cell::RefCell;
fn main() {
let x = RefCell::new(5);
let a = x.borrow();
let b = x.borrow(); // OK: multiple immutable borrows
// let c = x.borrow_mut(); // PANIC! Can't borrow mutably while immutably borrowed
drop(a);
drop(b);
let d = x.borrow_mut(); // OK: no other borrows
}When to use RefCell:
- Need mutability through shared reference
- Testing/mocking
- Cache/memoization
- Complex data structures
Combining Rc và RefCell
Multiple ownership with mutability:
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
let value = Rc::new(RefCell::new(5));
let a = Rc::clone(&value);
let b = Rc::clone(&value);
*value.borrow_mut() += 10;
println!("value: {}", value.borrow());
println!("a: {}", a.borrow());
println!("b: {}", b.borrow());
}Shared mutable list:
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
let value = Rc::new(RefCell::new(5));
let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));
*value.borrow_mut() += 10;
println!("a: {:?}", a);
println!("b: {:?}", b);
println!("c: {:?}", c);
}Memory leaks và prevention
Reference cycles cause leaks:
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
next: RefCell<Option<Rc<Node>>>,
}
fn main() {
let a = Rc::new(Node {
value: 5,
next: RefCell::new(None),
});
let b = Rc::new(Node {
value: 10,
next: RefCell::new(Some(Rc::clone(&a))),
});
// Create cycle
*a.next.borrow_mut() = Some(Rc::clone(&b));
// Memory leak! a and b reference each other
// Neither will be dropped
}Prevention with Weak:
use std::rc::{Rc, Weak};
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
next: RefCell<Option<Rc<Node>>>,
prev: RefCell<Weak<Node>>, // Weak reference
}
fn main() {
let a = Rc::new(Node {
value: 5,
next: RefCell::new(None),
prev: RefCell::new(Weak::new()),
});
let b = Rc::new(Node {
value: 10,
next: RefCell::new(Some(Rc::clone(&a))),
prev: RefCell::new(Weak::new()),
});
// Set prev with weak reference
*a.prev.borrow_mut() = Rc::downgrade(&b);
// No cycle! b -> a (strong), a -> b (weak)
// When b dropped, a can be dropped too
}🧪 BÀI TẬP LỚN CUỐI PHẦN: Hệ thống quản lý sinh viên
Mô tả bài toán
Xây dựng hệ thống quản lý sinh viên với các chức năng:
- Thêm, sửa, xóa sinh viên
- Quản lý điểm số (thêm, sửa, xóa môn học)
- Tính điểm trung bình
- Xếp loại sinh viên
- Tìm kiếm và lọc
- Lưu/load từ file
- Undo/Redo operations
Yêu cầu đặc biệt về Ownership:
- Sử dụng
Rc<RefCell<>>cho shared mutable state - Implement proper borrowing trong các methods
- Avoid memory leaks (no reference cycles)
- Use lifetimes cho các references
- Implement custom Drop trait
Yêu cầu
1. Data structures:
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug, Clone)]
struct Student {
id: u32,
name: String,
age: u8,
grades: Vec<Grade>,
}
#[derive(Debug, Clone)]
struct Grade {
subject: String,
score: f32,
}
struct StudentManager {
students: Vec<Rc<RefCell<Student>>>,
next_id: u32,
history: Vec<Operation>, // For undo/redo
}
enum Operation {
AddStudent(Rc<RefCell<Student>>),
RemoveStudent(u32),
UpdateStudent(u32, Student),
}2. Core methods:
impl StudentManager {
fn new() -> Self;
fn add_student(&mut self, name: String, age: u8) -> Rc<RefCell<Student>>;
fn remove_student(&mut self, id: u32) -> Result<(), String>;
fn find_student(&self, id: u32) -> Option<Rc<RefCell<Student>>>;
fn update_student(&mut self, id: u32, name: Option<String>, age: Option<u8>);
}
impl Student {
fn add_grade(&mut self, subject: String, score: f32);
fn update_grade(&mut self, subject: &str, score: f32) -> Result<(), String>;
fn remove_grade(&mut self, subject: &str) -> Result<(), String>;
fn average_grade(&self) -> f32;
fn classification(&self) -> &str; // Excellent/Good/Average/Poor
}3. Advanced features:
- Iterator for students
- Filter by grade range
- Sort by name/grade
- Export to JSON
- Lifetime annotations cho search results
Template code:
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug, Clone)]
struct Student {
id: u32,
name: String,
age: u8,
grades: Vec<Grade>,
}
#[derive(Debug, Clone)]
struct Grade {
subject: String,
score: f32,
}
impl Student {
fn new(id: u32, name: String, age: u8) -> Self {
Student {
id,
name,
age,
grades: Vec::new(),
}
}
fn add_grade(&mut self, subject: String, score: f32) {
// TODO: Validate score (0-100)
// TODO: Check if subject already exists
self.grades.push(Grade { subject, score });
}
fn average_grade(&self) -> f32 {
if self.grades.is_empty() {
return 0.0;
}
let sum: f32 = self.grades.iter().map(|g| g.score).sum();
sum / self.grades.len() as f32
}
fn classification(&self) -> &str {
let avg = self.average_grade();
match avg {
90.0..=100.0 => "Excellent",
80.0..=89.9 => "Good",
70.0..=79.9 => "Average",
60.0..=69.9 => "Below Average",
_ => "Poor",
}
}
fn display(&self) {
println!("ID: {}", self.id);
println!("Name: {}", self.name);
println!("Age: {}", self.age);
println!("Grades:");
for grade in &self.grades {
println!(" {}: {:.2}", grade.subject, grade.score);
}
println!("Average: {:.2}", self.average_grade());
println!("Classification: {}", self.classification());
}
}
struct StudentManager {
students: Vec<Rc<RefCell<Student>>>,
next_id: u32,
}
impl StudentManager {
fn new() -> Self {
StudentManager {
students: Vec::new(),
next_id: 1,
}
}
fn add_student(&mut self, name: String, age: u8) -> Rc<RefCell<Student>> {
let student = Rc::new(RefCell::new(Student::new(self.next_id, name, age)));
self.next_id += 1;
self.students.push(Rc::clone(&student));
student
}
fn find_student(&self, id: u32) -> Option<Rc<RefCell<Student>>> {
// TODO: Implement binary search if sorted
for student in &self.students {
if student.borrow().id == id {
return Some(Rc::clone(student));
}
}
None
}
fn remove_student(&mut self, id: u32) -> Result<(), String> {
// TODO: Implement
let index = self.students.iter()
.position(|s| s.borrow().id == id)
.ok_or("Student not found")?;
self.students.remove(index);
Ok(())
}
fn list_all(&self) {
println!("\n=== All Students ===");
for student in &self.students {
student.borrow().display();
println!("---");
}
}
fn find_by_name<'a>(&'a self, name: &str) -> Vec<Rc<RefCell<Student>>> {
// TODO: Use lifetime annotations
self.students.iter()
.filter(|s| s.borrow().name.contains(name))
.map(|s| Rc::clone(s))
.collect()
}
fn save_to_file(&self, filename: &str) {
// TODO: Implement serialization
unimplemented!()
}
fn load_from_file(&mut self, filename: &str) {
// TODO: Implement deserialization
unimplemented!()
}
}
fn main() {
let mut manager = StudentManager::new();
// Add students
let alice = manager.add_student(String::from("Alice"), 20);
let bob = manager.add_student(String::from("Bob"), 21);
// Add grades
alice.borrow_mut().add_grade(String::from("Math"), 95.0);
alice.borrow_mut().add_grade(String::from("Physics"), 88.0);
alice.borrow_mut().add_grade(String::from("Chemistry"), 92.0);
bob.borrow_mut().add_grade(String::from("Math"), 78.0);
bob.borrow_mut().add_grade(String::from("Physics"), 85.0);
// Display all
manager.list_all();
// Find student
if let Some(student) = manager.find_student(1) {
println!("\nFound student:");
student.borrow().display();
}
// Search by name
let results = manager.find_by_name("Alice");
println!("\nSearch results for 'Alice': {} students", results.len());
// TODO: Implement menu system with all features
}Yêu cầu mở rộng:
Undo/Redo:
- Lưu lại history của operations
- Implement undo/redo stack
Filtering & Sorting:
- Filter by grade range
- Sort by multiple criteria
- Iterator pattern
Validation:
- Age range (15-100)
- Score range (0-100)
- Name không rỗng
- Không duplicate subject trong grades
Advanced ownership:
- Implement course sharing (nhiều students cùng course)
- Use
Weak<>để tránh cycles - Custom Drop trait để cleanup
Performance:
- Use HashMap thay vì Vec để O(1) lookup
- Lazy evaluation cho statistics
- Benchmark các operations
Persistence:
- Serialize với
serde_json - Auto-save every N operations
- Import/Export CSV
- Serialize với
