String против &str и срезы

String владеет буфером и умеет расти, &str — только заимствованный вид на кусок UTF-8, и почти все функции должны принимать именно его.

Slice — заимствованный вид на непрерывный участок данных: пара «указатель + длина», без владения и без копирования.

Вопрос: «В чём разница между String и &str?»

Самый частый вопрос по строкам, и отвечать на него надо через раскладку в памяти, а не через «один изменяемый, другой нет».

let owned: String = String::from("привет");   // ptr + len + capacity, буфер в куче
let literal: &'static str = "привет";         // ptr + len, данные в бинарнике
let view: &str = &owned[..];                  // ptr + len, смотрит в буфер owned

String — владеющий, растущий буфер в куче: три слова на стеке (указатель, длина, ёмкость) и сами байты в куче. При push_str буфер может перевыделиться, и все старые указатели стали бы невалидными — поэтому заимствования и владение здесь строго разделены.

&str — это fat pointer: указатель и длина, без capacity, потому что менять размер чужих данных нельзя. Он может смотреть куда угодно: в буфер String, в статические данные бинарника, в кусок массива байтов.

Что на самом деле проверяет интервьюер

  • Различаете ли владение и заимствование на уровне типов, а не на уровне «mut / не mut».
  • Понимаете ли, почему у &str нет capacity и почему его нельзя дописать.
  • Знаете ли, что литерал имеет тип &'static str — это связывает тему со срезами и лайфтаймами из прошлого урока.

Вопрос: «Что лучше принимать в параметрах функции?»

Ответ: &str, если функции не нужно владение. Работает это благодаря deref coercion — компилятор автоматически превращает &String в &str.

fn shout(s: &str) -> String {
    s.to_uppercase()
}

fn main() {
    let name = String::from("rust");
    println!("{}", shout(&name));   // &String -> &str автоматически
    println!("{}", shout("cargo")); // литерал уже &str
}

Вывод:

RUST
CARGO

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

fn main() {
    let name = String::from("rust");
    println!("{}", shout(name));
}

Ошибка компиляции:

error[E0308]: mismatched types
 --> src/main.rs:7:26
  |
7 |     println!("{}", shout(name));
  |                    ----- ^^^^ expected `&str`, found `String`
  |                    |
  |                    arguments to this function are incorrect
  |
note: function defined here
 --> src/main.rs:1:4
  |
1 | fn shout(s: &str) -> String {
  |    ^^^^^ -------
help: consider borrowing here
  |
7 |     println!("{}", shout(&name));
  |                          +

Правило по умолчанию: принимать &str и &[T], возвращать String и Vec<T>. Владение в параметре запрашивают только тогда, когда функция действительно оставляет значение себе — например, кладёт его в структуру.

Вопрос: «Что такое срез и почему s[0] не работает для строк?»

Срез — это заимствованный вид на непрерывный участок: &v[1..3] для векторов и &s[0..4] для строк. Копирования не происходит, создаётся только пара «указатель + длина».

let v = vec![10, 20, 30, 40];
let part: &[i32] = &v[1..3];
println!("{part:?} len={}", part.len());

Вывод:

[20, 30] len=2

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

fn main() {
    let s = String::from("Привет");
    let c = s[0];
    println!("{c}");
}

Ошибка компиляции:

error[E0277]: the type `str` cannot be indexed by `{integer}`
 --> src/main.rs:3:13
  |
3 |     let c = s[0];
  |             ^^^^ string indices are ranges of `usize`
  |
  = help: the trait `SliceIndex<str>` is not implemented for `{integer}`
  = note: you can use `.chars().nth()` or `.bytes().nth()` instead
  = note: required for `String` to implement `Index<{integer}>`

Причина в UTF-8: один символ занимает от одного до четырёх байтов, поэтому «нулевой элемент» неоднозначен — байт это или символ? Rust отказывается угадывать.

let s = String::from("Привет");
println!("{}", s.len());
println!("{}", s.chars().count());
println!("{:?}", s.chars().nth(0));

Вывод:

12
6
Some('П')

.len() считает байты, а не символы. Диапазонный срез разрешён, но границы обязаны попадать на границу символа, иначе будет паника уже во время выполнения:

let s = String::from("Привет");
let bad = &s[0..1];

Вывод:

thread 'main' panicked at src/main.rs:2:16:
byte index 1 is not a char boundary; it is inside 'П' (bytes 0..2) of `Привет`

Таблица-памятка

ТипВладеет даннымиЧто лежит на стекеГде применять
Stringда, буфер можно раститьptr + len + capacityполя структур, возврат из функций
&strнет, заимствованиеptr + lenпараметры функций, чтение подстрок
Vec<T>да, буфер можно раститьptr + len + capacityвладеющий список, возврат из функций
&[T]нет, заимствованиеptr + lenпараметры функций, работа с частью данных

Пары читаются одинаково: String относится к &str ровно так же, как Vec<T> к &[T]. Это удобная формулировка для устного ответа.

Типичные ошибки кандидатов

  • Объяснять разницу как «String изменяемый, &str нет» — дело во владении, а &mut str существует и почти бесполезен.
  • Объявлять параметр как &String: функция теряет возможность принимать литералы и срезы.
  • Звать .to_string() или .clone() на каждом шаге вместо заимствования.
  • Считать, что .len() возвращает число символов.
  • Резать строку по произвольным байтовым индексам и получать панику на кириллице или эмодзи.
  • Не знать про deref coercion и объяснять shout(&name) магией.

Как ответить кратко

String — владеющий растущий буфер в куче: указатель, длина, ёмкость. &str — заимствованный вид на UTF-8 байты, только указатель и длина, поэтому расти он не может; литерал имеет тип &'static str. В параметрах почти всегда берём &str: благодаря deref coercion туда подходит и &String, и литерал, и срез. Индексация s[0] запрещена, потому что в UTF-8 символ занимает от одного до четырёх байтов; есть только диапазонные срезы по границам символов плюс .chars(), а .len() считает байты. Пара String/&str устроена так же, как Vec<T>/&[T].

Проверьте себя
1. Что лежит на стеке для String и для &str?
AУ обоих только указатель на кучу
BУ String — ptr, len и capacity; у &str — ptr и len
CУ String — ptr и len; у &str — ptr, len и capacity
DУ String вся строка целиком, у &str — индекс в глобальной таблице строк
2. Почему параметр функции лучше объявлять как &str, а не как &String?
AПотому что &String вообще не является валидным типом в Rust
BПотому что &str быстрее копируется при передаче
CПотому что благодаря deref coercion в &str подходит и &String, и литерал, и срез
DПотому что &String требует явной аннотации лайфтайма, а &str — нет
3. Что напечатает s.len() для let s = String::from("Привет");?
A6
B7
C12
DКод не скомпилируется