Avatar ·

Почему в Rust не рекомендуется принимать &String, &Vec и &Box в качестве аргументов функций?

Всем привет! Я недавно начал изучать Rust и заметил странную вещь: во многих руководствах и на Stack Overflow пишут, что не стоит писать функции, которые принимают `&String`, `&Vec` или `&Box`. Вместо этого советуют использовать `&str`, `&[T]` и просто `&T` (или `&T` для Box). Но мне не совсем понятно, в чём проблема. Вот простой пример, который я написал для работы с текстом: ```rust fn print_length(s: &String) { println!("Длина строки: {}", s.len()); } fn main() { let my_string = String::from("Привет, мир!"); print_length(&my_string); } ``` Этот код работает. Почему же его считают плохим стилем? Чем `&String` хуже `&str`? И то же самое с векторами: чем `&Vec` хуже `&[i32]`? Ведь в обоих случаях я передаю ссылку, а не владение. Меня смущает, что если я напишу функцию с `&str`, то при вызове с `String` мне придётся каждый раз явно приводить тип: `print_length(&my_string[..])` или `print_length(my_string.as_str())`. А с `&String` всё работает напрямую. В чём подвох? Есть ли разница в производительности или это просто вопрос идиоматичности кода? Буду благодарен за подробное объяснение с примерами. <<>>

Почему &String, &Vec и &Box — это плохой выбор для аргументов функций

Короткий ответ: принимая &String, &Vec или &Box, вы навязываете вызывающему коду конкретный тип, который требует дополнительной аллокации и не позволяет использовать более общие типы данных. Правильный подход — принимать ссылки на срез (&str для строк, &[T] для векторов) или просто &T для упакованных значений.

Проблема с &String против &str

Когда вы пишете:

fn print_length(s: &String) {
    println!("{}", s.len());
}

Вы говорите: «Эта функция может работать только со ссылкой на String». Но что, если у вас есть &str (строковый литерал) или Cow<str>? Вы не сможете передать их без преобразования. Например:

fn main() {
    let s1 = String::from("hello");
    let s2 = "world"; // это &str
    
    print_length(&s1); // работает
    // print_length(&s2); // ОШИБКА: expected &String, found &&str
}

Вам пришлось бы делать print_length(&s2.to_string()) — это лишняя аллокация!

С &str такого ограничения нет:

fn print_length(s: &str) {
    println!("{}", s.len());
}

fn main() {
    let s1 = String::from("hello");
    let s2 = "world";
    
    print_length(&s1); // работает — автоматическая ссылка &String -> &str
    print_length(s2);  // работает — &str
}

Почему это работает? Потому что String реализует типаж Deref<Target=str>, и Rust автоматически выполняет приведение при заимствовании. То есть &String неявно становится &str. Это называется Deref coercion.

Проблема с &Vec против &[T]

Аналогичная ситуация с векторами:

// Плохо
fn sum_vec(v: &Vec<i32>) -> i32 {
    v.iter().sum()
}

// Хорошо
fn sum_slice(v: &[i32]) -> i32 {
    v.iter().sum()
}

С &[i32] вы можете передать:

  • &Vec<i32> — через Deref
  • Срез массива &[1, 2, 3]
  • Срез части вектора: &my_vec[1..3]
  • Любой другой тип, реализующий AsRef<[i32]>

Проблема с &Box против &T

С Box то же самое: Box<T> реализует Deref<Target=T>. Принимая &Box<T>, вы заставляете вызывающего иметь именно Box. А если у него просто T на стеке? Нельзя передать. Правильно — принимать &T.

// Плохо
fn process_box(b: &Box<dyn SomeTrait>) { ... }

// Хорошо
fn process_trait(b: &dyn SomeTrait) { ... }
// или даже обобщённо:
fn process_generic<T: SomeTrait>(b: &T) { ... }

Когда это всё же может быть оправдано?

Есть редкие случаи, когда вам нужно именно &String или &Vec:

  • Когда вы вызываете метод, который есть только у String (например, as_mut_str) — но это почти всегда говорит о плохом дизайне.
  • Когда вы работаете с Rc или Arc и вам нужно сохранить ссылку на конкретный тип для подсчёта ссылок.

Практическое правило

Запомните простое правило:

  • &String → &str
  • &Vec<T> → &[T]
  • &Box<T> → &T

Это делает ваши функции более гибкими, не теряя производительности. Deref coercion работает автоматически, так что вызывающему коду не нужно ничего менять.

<<>>

Альтернативный взгляд: когда можно использовать &String и &Vec

Хотя общая рекомендация — избегать &String, &Vec и &Box в сигнатурах, есть несколько ситуаций, где это может быть оправдано:

1. Когда вам нужен доступ к методам, специфичным для конкретного типа

Например, String имеет метод capacity(), которого нет у &str. Если ваша функция действительно использует такие методы, то &String может быть уместен. Но лучше пересмотреть дизайн: возможно, стоит принимать &mut String или использовать обобщённый подход с трейтами.

2. Внутренние функции в модуле

Если функция не является частью публичного API и вызывается только с String внутри одного модуля, то использование &String не навредит. Однако даже в этом случае лучше привыкнуть к &str — это улучшит читаемость и упростит рефакторинг.

3. Производительность при очень частых вызовах

Теоретически, Deref coercion добавляет крошечные накладные расходы при каждом вызове. На практике это настолько ничтожно, что не стоит беспокоиться. Только если вы пишете высоконагруженный код с миллионами вызовов в секунду, можно задуматься о микрооптимизациях, но сначала измерьте!

Вывод

Правило «не принимайте &String, &Vec, &Box» — это рекомендация, а не жёсткое правило. Следуйте ей в 95% случаев, но если есть веская причина отступить — сделайте это осознанно и задокументируйте причину в комментарии.