Почему &String, &Vec и &Box — плохой тон в Rust?

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Ловушка, в которую попадают все новички в 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, вы передаёте ссылку на эту структуру. Чтобы добраться до данных, компилятору нужно:

  1. Разыменовать &String → получить структуру String
  2. Из структуры взять указатель на данные в куче
  3. Разыменовать этот указатель → получить байты строки

Когда вы передаёте &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                

Блоги

Рекомендаций по книгам