Avatar ·

В чем разница между String и str в Rust?

Привет! Я только начал изучать Rust и столкнулся с путаницей между `String` и `str`. В других языках, например в Python или JavaScript, строки — это просто строки, а тут какой-то зоопарк. Я написал такой код: ```rust fn main() { let greeting = "Hello"; // Это str? let name = String::from("World"); // А это String? println!("{} {}", greeting, name); // Попробовал изменить greeting // greeting.push_str("!"); // Ошибка! // А name можно изменить name.push_str("!"); // Работает println!("{}", name); } ``` Я понимаю, что `String` можно изменять, а `str` — нет. Но почему? Когда какой тип использовать? И что такое `&str`? Я видел, что в функциях часто пишут `&str`, но не понимаю, зачем. Еще я заметил, что `str` не имеет известного размера во время компиляции, а `String` — имеет. Это как-то связано с тем, что строки хранятся в куче? Помогите разобраться, а то я уже голову сломал. <<>>

Ключевое различие: владение и расположение в памяти

Главное отличие 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. Это просто ссылка на данные, которыми вы не владеете. Со временем вы привыкнете к этой модели.