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].