В чем разница между String и str в Rust?
Ключевое различие: владение и расположение в памяти
Главное отличие String от str в Rust — это модель владения. String — это владеющий тип, который хранит данные в куче (heap) и может изменять свой размер. str — это срез строки (string slice), неизменяемый по размеру, который всегда ссылается на какие-то существующие данные.
String — владелец данных
String состоит из трех частей: указатель на кучу, длина и емкость. Когда вы создаете String, память выделяется в куче, и вы становитесь ее владельцем. Это позволяет:
- Изменять строку (добавлять, удалять символы)
- Передавать владение другой функции
- Освобождать память при выходе из области видимости
let mut s = String::from("Hello");
s.push_str(" World"); // Работает — мы владеем памятью
println!("{}", s); // "Hello World"
str и &str — заимствованные данные
str — это сырой тип с неизвестным размером (DST, Dynamically Sized Type). Вы не можете создать переменную типа str напрямую, только через ссылку &str. &str — это указатель на какой-то участок памяти, где хранятся UTF-8 байты. Этот участок может быть:
- Внутри
String - Внутри статической памяти программы (строковые литералы)
- Внутри массива байт на стеке
let literal: &str = "Hello"; // Ссылка на статические данные
let string: String = String::from("World");
let slice: &str = &string[..3]; // Ссылка на часть String
println!("{}", slice); // "Wor"
Когда что использовать?
- String — когда вам нужно владеть строкой, изменять ее, передавать между потоками или возвращать из функции
- &str — когда вам нужно только прочитать данные, не изменяя их. Это предпочтительный тип для параметров функций
fn greet(name: &str) {
println!("Привет, {}!", name);
}
fn main() {
let my_name = String::from("Алексей");
greet(&my_name); // Передаем ссылку на String
greet("Гость"); // Или строковый литерал
}
Почему &str предпочтительнее в параметрах?
Используя &str, вы позволяете функции принимать как &String, так и строковые литералы. Это делает API более гибким. Если бы вы написали fn greet(name: String), то пришлось бы копировать или клонировать данные, что неэффективно.
Преобразование между типами
// String -> &str
let s = String::from("привет");
let slice: &str = &s; // или &s[..]
// &str -> String
let slice = "мир";
let string: String = slice.to_string(); // или String::from(slice)
Запомните золотое правило: String — владеет, &str — заимствует. Это основа работы с памятью в Rust.
Практические советы по выбору между String и &str
Добавлю несколько практических рекомендаций, которые помогут новичкам не ошибаться:
1. Строковые литералы — это &str
Когда вы пишете let x = "hello", тип x — &str, потому что литералы встраиваются в бинарник программы. Их нельзя изменить.
2. Используйте String для накопления данных
Если вы строите строку динамически (читаете из файла, собираете из частей), используйте String:
let mut result = String::new();
result.push_str("Строка ");
result.push_str("номер ");
result.push_str("1");
3. В структурах чаще используйте String
Если структура должна владеть данными (например, имя пользователя), используйте String. &str требует указания времени жизни (lifetime), что усложняет код:
struct User {
name: String, // Просто и понятно
// name: &str, // Потребует lifetime: &'a str
}
4. Для параметров — &str, для возврата — String
Функции, которые принимают строку, почти всегда должны брать &str. Функции, которые создают новую строку, возвращают String. Это стандартная практика в Rust.
Главное — не бойтесь &str. Это просто ссылка на данные, которыми вы не владеете. Со временем вы привыкнете к этой модели.