Use borrowed types for arguments
Khi viết functions trong Rust, prefer sử dụng borrowed types (&str, &[T], &Path) thay vì owned types (String, Vec
Vấn đề với Owned Types
// ❌ Không tốt - chỉ nhận String
fn print_message(message: String) {
println!("{}", message);
}
fn main() {
let msg = "Hello";
// Phải convert từ &str sang String
print_message(msg.to_string()); // Allocate bộ nhớ không cần thiết
// Hoặc
print_message(String::from(msg));
}
Giải pháp: Borrowed Types
// ✅ Tốt - nhận &str (borrowed)
fn print_message(message: &str) {
println!("{}", message);
}
fn main() {
// Có thể truyền &str
print_message("Hello");
// Hoặc String (tự động coerce sang &str)
let owned = String::from("World");
print_message(&owned);
}
String vs &str
Owned: String
// ❌ Hạn chế - chỉ nhận String
fn process(s: String) {
println!("{}", s.to_uppercase());
}
fn main() {
let owned = String::from("hello");
process(owned); // OK
// String literal phải convert
process("world".to_string()); // Phải allocate
// Không thể dùng owned sau khi pass
// println!("{}", owned); // ❌ Error: value moved
}
Borrowed: &str
// ✅ Linh hoạt - nhận cả &str và &String
fn process(s: &str) {
println!("{}", s.to_uppercase());
}
fn main() {
let owned = String::from("hello");
process(&owned); // OK, borrow
// String literal - không cần allocate
process("world"); // OK, zero-cost
// Có thể dùng owned sau đó
println!("{}", owned); // ✅ OK
}
Vec vs &[T]
Owned: Vec
// ❌ Hạn chế - chỉ nhận Vec
fn sum(numbers: Vec<i32>) -> i32 {
numbers.iter().sum()
}
fn main() {
let vec = vec![1, 2, 3];
let total = sum(vec);
// ❌ Error: vec đã bị moved
// println!("{:?}", vec);
}
Borrowed: &[T]
// ✅ Linh hoạt - nhận slice
fn sum(numbers: &[i32]) -> i32 {
numbers.iter().sum()
}
fn main() {
// Với Vec
let vec = vec![1, 2, 3];
let total = sum(&vec);
println!("{:?}", vec); // ✅ OK, vẫn dùng được
// Với array
let arr = [4, 5, 6];
let total2 = sum(&arr);
// Với slice
let total3 = sum(&vec[1..]);
println!("{}, {}, {}", total, total2, total3);
}
PathBuf vs &Path
Owned: PathBuf
use std::path::PathBuf;
// ❌ Hạn chế
fn read_config(path: PathBuf) -> std::io::Result<String> {
std::fs::read_to_string(path)
}
fn main() -> std::io::Result<()> {
let path = PathBuf::from("config.toml");
read_config(path)?;
// ❌ path đã moved, không dùng được nữa
Ok(())
}
Borrowed: &Path
use std::path::Path;
// ✅ Linh hoạt
fn read_config(path: &Path) -> std::io::Result<String> {
std::fs::read_to_string(path)
}
fn main() -> std::io::Result<()> {
// Với PathBuf
let path = std::path::PathBuf::from("config.toml");
read_config(&path)?;
// Với &str
read_config(Path::new("config.toml"))?;
// path vẫn dùng được
println!("{:?}", path);
Ok(())
}
Bảng so sánh
| Owned Type | Borrowed Type | Use Case |
|---|---|---|
String | &str | Text processing, không cần ownership |
Vec<T> | &[T] | Read-only access to collection |
PathBuf | &Path | File path operations |
OsString | &OsStr | OS strings |
CString | &CStr | C strings |
Khi nào dùng Owned Types?
1. Function cần ownership
#![allow(unused)]
fn main() {
// ✅ Cần owned để store hoặc modify
fn store_message(message: String) -> Message {
Message { content: message } // Move vào struct
}
struct Message {
content: String,
}
}
2. Function modify data
// ✅ Cần owned để modify
fn uppercase(mut s: String) -> String {
s.make_ascii_uppercase();
s
}
fn main() {
let msg = "hello".to_string();
let upper = uppercase(msg);
println!("{}", upper); // HELLO
}
3. Builder pattern
struct Request {
url: String,
method: String,
}
impl Request {
fn new(url: String) -> Self {
Self {
url,
method: "GET".to_string(),
}
}
// Builder methods take ownership
fn method(mut self, method: String) -> Self {
self.method = method;
self
}
}
fn main() {
let req = Request::new("https://example.com".to_string())
.method("POST".to_string());
}
Generic over Borrowed Types
Sử dụng generic bounds để accept cả owned và borrowed:
use std::borrow::Borrow;
// Accept cả String và &str
fn print<S: AsRef<str>>(s: S) {
println!("{}", s.as_ref());
}
fn main() {
print("hello"); // &str
print(String::from("world")); // String
print(&String::from("rust")); // &String
}
With collections
use std::borrow::Borrow;
fn contains<T, Q>(slice: &[T], item: &Q) -> bool
where
T: Borrow<Q>,
Q: Eq + ?Sized,
{
slice.iter().any(|x| x.borrow() == item)
}
fn main() {
let strings = vec![
String::from("hello"),
String::from("world"),
];
// Tìm với &str, không cần String
println!("{}", contains(&strings, "hello")); // true
}
Deref Coercion
Rust tự động convert owned → borrowed trong một số cases:
fn print_str(s: &str) {
println!("{}", s);
}
fn main() {
let owned = String::from("hello");
// Auto deref: &String → &str
print_str(&owned);
// Tương đương với:
// print_str(&*owned);
// print_str(owned.as_str());
}
Ví dụ thực tế
File operations
use std::path::Path;
use std::fs;
// ✅ Linh hoạt với &Path
fn file_exists(path: &Path) -> bool {
path.exists()
}
fn read_file(path: &Path) -> std::io::Result<String> {
fs::read_to_string(path)
}
fn main() -> std::io::Result<()> {
// Nhiều cách gọi
println!("{}", file_exists(Path::new("config.toml")));
let path_buf = std::path::PathBuf::from("data.txt");
println!("{}", file_exists(&path_buf));
Ok(())
}
API design
use std::collections::HashMap;
struct UserService {
users: HashMap<String, User>,
}
struct User {
name: String,
email: String,
}
impl UserService {
// ✅ Borrowed - không cần own the key
fn get_user(&self, username: &str) -> Option<&User> {
self.users.get(username)
}
// ✅ Owned - cần store the user
fn add_user(&mut self, username: String, user: User) {
self.users.insert(username, user);
}
}
fn main() {
let mut service = UserService {
users: HashMap::new(),
};
// Add với owned
service.add_user(
"alice".to_string(),
User {
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
},
);
// Get với borrowed
if let Some(user) = service.get_user("alice") {
println!("{}", user.name);
}
}
Performance Considerations
Zero-cost với borrowed types
// ✅ Zero allocations
fn process(s: &str) {
for c in s.chars() {
// Process character
}
}
fn main() {
process("hello"); // Không allocate memory
}
Unnecessary allocations với owned
// ❌ Allocates mỗi lần gọi
fn process(s: String) {
for c in s.chars() {
// Process character
}
}
fn main() {
process("hello".to_string()); // Allocate memory
}
Best Practices
1. Default to borrowed
#![allow(unused)]
fn main() {
// ✅ Bắt đầu với borrowed
fn process(data: &str) { }
// Chỉ dùng owned nếu thực sự cần
fn store(data: String) -> Storage {
Storage { data }
}
}
2. Use AsRef/Borrow traits
#![allow(unused)]
fn main() {
// ✅ Generic over borrowed/owned
fn process<S: AsRef<str>>(s: S) {
println!("{}", s.as_ref());
}
}
3. Document ownership requirements
#![allow(unused)]
fn main() {
/// Process data without taking ownership
fn process(data: &[u8]) { }
/// Takes ownership and stores the data
fn store(data: Vec<u8>) -> DataStore {
DataStore { data }
}
}
4. Consider &mut for in-place modification
#![allow(unused)]
fn main() {
// ✅ Modify in-place
fn uppercase_in_place(s: &mut String) {
s.make_ascii_uppercase();
}
// ❌ Unnecessary allocation
fn uppercase(s: String) -> String {
s.to_uppercase()
}
}
Common Patterns
Accepting both owned and borrowed
#![allow(unused)]
fn main() {
// Pattern 1: AsRef
fn print1<S: AsRef<str>>(s: S) {
println!("{}", s.as_ref());
}
// Pattern 2: Into
fn print2(s: impl Into<String>) {
let s: String = s.into();
println!("{}", s);
}
// Pattern 3: Borrow
use std::borrow::Borrow;
fn print3<S: Borrow<str>>(s: S) {
println!("{}", s.borrow());
}
}
Tổng kết
Use borrowed types for arguments:
- ✅ More flexible (accept owned và borrowed)
- ✅ Better performance (avoid clones)
- ✅ Clearer ownership semantics
- ✅ Idiomatic Rust
Guidelines:
- Default to borrowed (&str, &[T], &Path)
- Use owned khi cần ownership (store, modify, return)
- Use AsRef/Borrow cho flexible APIs
- Document ownership requirements
Common borrowed types:
&strinstead ofString&[T]instead ofVec<T>&Pathinstead ofPathBuf&Tinstead ofT(when possible)