Введение: Ловушка, в которую попадают все новички в Rust
Вы учите Rust. Вы уже разобрались с концепцией владения и заимствования. Вы пишете свою первую серьёзную функцию и думаете: «Ага, чтобы не перемещать строку, я передам ссылку!». И вы пишете:
fn print_string(s: &String) {
println!("{}", s);
}
Компилятор счастлив. Код работает. Вы счастливы. Но вот беда: опытный Rust-разработчик, глядя на ваш код, поморщится. Почему? Ведь всё работает!
Этот вопрос — один из самых популярных на StackOverflow. Тысячи разработчиков спотыкаются об эту, казалось бы, незначительную деталь. Сегодня мы разберёмся, почему &String, &Vec и &Box в сигнатуре функции — это красный флаг, и как писать правильно.
Проблема №1: Неявное ограничение типов (Type Coercion)
Rust обладает мощной системой автоматического приведения типов, но только в одну сторону. Это называется Deref Coercion (приведение через разыменование).
Суть проста: если у вас есть ссылка на String, Rust автоматически может превратить её в ссылку на &str. То же самое с &Vec<T> → &[T] и &Box<T> → &T.
Посмотрите на разницу:
// Плохо: принимает только &String
fn bad(s: &String) {
println!("{}", s);
}
// Хорошо: принимает &str
fn good(s: &str) {
println!("{}", s);
}
fn main() {
let owned = String::from("Привет, мир!");
let slice = "Привет, слайс!";
bad(&owned); // Работает
// bad(slice); // ОШИБКА! slice — это &str, а не &String
good(&owned); // Работает (автоматическое приведение)
good(slice); // Работает (это уже &str)
}
Видите? Функция good принимает любую строку: и владельца String, и строковый литерал &str, и даже часть строки. Функция bad — только String. Это ненужное ограничение.
Практический пример: поиск подстроки
// Ограниченный поиск
fn find_in_string(haystack: &String, needle: &str) -> bool {
haystack.contains(needle)
}
// Универсальный поиск
fn find_in_str(haystack: &str, needle: &str) -> bool {
haystack.contains(needle)
}
fn main() {
let my_string = String::from("Rust is awesome");
let my_str = "Rust is awesome";
// Работает только с String
find_in_string(&my_string, "Rust");
// find_in_str(my_str, "Rust"); // Ошибка компиляции!
// Работает и с тем, и с другим
find_in_str(&my_string, "Rust");
find_in_str(my_str, "Rust");
}
Проблема №2: Производительность и лишние косвенные обращения
Давайте заглянем «под капот». Что такое String в памяти? Это структура из трёх полей:
- Указатель на данные в куче
- Длина строки
- Вместимость (capacity)
Когда вы передаёте &String, вы передаёте ссылку на эту структуру. Чтобы добраться до данных, компилятору нужно:
- Разыменовать
&String→ получить структуруString - Из структуры взять указатель на данные в куче
- Разыменовать этот указатель → получить байты строки
Когда вы передаёте &str, вы передаёте fat pointer (жирный указатель), который состоит всего из двух полей:
- Указатель на данные
- Длина
Компилятору нужно сделать всего один шаг — разыменовать указатель. Меньше косвенности — быстрее код. Разница микроскопическая, но в горячих циклах или при передаче в глубоко вложенные функции она накапливается.
Проблема №3: Игнорирование возможностей слайсов
Принимая &Vec<T> вместо &[T], вы лишаете вызывающую сторону возможности передать часть вектора (срез) или массив фиксированного размера.
fn sum_vec(v: &Vec<i32>) -> i32 {
v.iter().sum()
}
fn sum_slice(v: &[i32]) -> i32 {
v.iter().sum()
}
fn main() {
let vec = vec![1, 2, 3, 4, 5];
let arr = [1, 2, 3, 4, 5];
sum_vec(&vec); // Работает
// sum_vec(&arr[..]); // Ошибка! &[i32] не приводится к &Vec<i32>
sum_slice(&vec); // Работает
sum_slice(&arr); // Работает
sum_slice(&vec[1..4]); // Работает! Передаём только часть данных
}
Слайсы — это мощный инструмент Rust. Они позволяют работать с любыми последовательными данными без копирования. Используя &Vec, вы выбрасываете эту возможность.
Исключение: Когда всё-таки можно?
Как и в любом правиле, есть исключения. Иногда &String или &Vec оправданы:
- Изменение содержимого с сохранением типа: если ваша функция должна изменить строку или вектор, но не заменять его целиком (например,
push,clear). Для этого нужен&mut Stringили&mut Vec. - API, специфичный для типа: если вы работаете с методами, которые есть только у
String, но нет у&str(например,capacity()илиreserve()). - FFI (Foreign Function Interface): при взаимодействии с C-кодом, где ожидается конкретный тип.
Но даже в этих случаях стоит спросить себя: «Может, я могу принять &mut str или &mut [T]?»
Как переписать свой код: практические советы
1. Замена &String → &str
// Было
fn