String vs str в Rust: почему новички путаются и как разобраться раз и навсегда
Если вы только начинаете изучать Rust, то наверняка уже столкнулись с ситуацией: компилятор ругается, вы пытаетесь передать String туда, где ожидается &str, или наоборот. Знакомо? Это один из самых популярных вопросов на Stack Overflow (тысячи просмотров!), и не зря — тема действительно фундаментальная для понимания работы с памятью в Rust.
В этой статье мы разберёмся, чем отличаются String и &str, когда какой тип использовать, и как избежать типичных ошибок. Поехали!
Короткий ответ для тех, кто спешит
Если вам нужно прямо сейчас — запомните главное:
String— это владеющий, изменяемый, динамический тип данных, который хранится в куче (heap).&str— это неизменяемый срез строки (slice), который ссылается на данные, принадлежащие кому-то другому. Чаще всего встречается как&str.
Теперь давайте копнём глубже.
Что такое String в Rust?
String — это тип, который владеет своими данными. Когда вы создаёте String, память для неё выделяется в куче, и строка может расти, уменьшаться или изменяться.
let mut s = String::from("Привет");
s.push_str(", мир!");
println!("{}", s); // Выведет: Привет, мир!
Здесь s — владелец строки. Когда s выходит из области видимости, память автоматически освобождается (спасибо RAII — Resource Acquisition Is Initialization).
Основные характеристики String:
- Владеет данными (owning type)
- Изменяемый (mutable)
- Хранится в куче
- Может быть создан из строкового литерала:
String::from("текст") - Поддерживает конкатенацию, вставку, удаление символов
Что такое &str в Rust?
&str (произносится как "стр-срез" или "стринг-слайс") — это ссылка на последовательность UTF-8 символов, которая принадлежит кому-то другому. Сам по себе &str — это просто указатель на данные и длина.
let greeting: &str = "Привет";
println!("{}", greeting);
В этом примере greeting — это ссылка на строковый литерал, который хранится прямо в бинарном файле программы (в сегменте read-only памяти). greeting не владеет этими данными — он просто на них смотрит.
Основные характеристики &str:
- Не владеет данными (borrowed type)
- Неизменяемый (immutable)
- Хранится либо в бинарнике (строковые литералы), либо в куче (как срез от
String) - Легковесный — состоит из двух полей: указатель + длина (16 байт на 64-битной системе)
- Идеален для передачи в функции, когда не нужно изменять строку
Ключевые отличия String и &str (таблица)
Давайте сведём всё в наглядную таблицу:
| Характеристика | String | &str |
|---|---|---|
| Владение | Владеет данными | Заимствует данные |
| Изменяемость | Да (mut) | Нет |
| Где хранится | Куча (heap) | Куча, стек или бинарник |
| Размер в стеке | 24 байта (ptr + len + cap) | 16 байт (ptr + len) |
| Создание | String::from("...") |
"..." или &s[..] |
| Основное применение | Когда нужно изменять или владеть строкой | Когда нужно только читать строку |
Когда использовать String, а когда &str?
Золотое правило Rust: всегда используйте &str для параметров функций, если вам не нужно владеть строкой или изменять её. Это делает ваш код более гибким.
Пример: правильный подход
// Плохо: принимает только String
fn greet_owned(name: String) {
println!("Привет, {}!", name);
}
// Хорошо: принимает и String, и &str
fn greet(name: &str) {
println!("Привет, {}!", name);
}
fn main() {
let s = String::from("Анна");
greet(&s); // Работает!
greet("Пётр"); // Работает! (литерал — &str)
}
Обратите внимание: мы передаём &s — ссылку на String, которая автоматически приводится к &str благодаря Deref-преобразованию.
Когда без String не обойтись
fn build_greeting(name: &str) -> String {
let mut result = String::from("Здравствуйте, ");
result.push_str(name);
result.push('!');
result // Возвращаем владение строкой
}
fn main() {
let msg = build_greeting("Мария");
println!("{}", msg);
}
Здесь мы создаём новую строку, поэтому String — единственный правильный выбор.
Типичные ошибки новичков
Ошибка 1: Путаница с конкатенацией
fn main() {
let s1 = String::from("Hello");
let s2 = String::from("World");
let s3 = s1 + &s2; // s1 перемещается, s2 заимствуется
// println!("{}", s1); // Ошибка! s1 больше недействителен
println!("{}", s3);
}
Оператор + использует метод add(self, s: &str) -> String. Он потребляет self (первый операнд) и принимает &str для второго.
Ошибка 2: Возврат ссылки на локальную строку
fn bad() -> &str {
let s = String::from("временная строка