Срезы и время жизни

Срезы — ссылки на часть коллекции; на их примере мягко знакомимся со временем жизни (lifetimes).

Срез (slice) — ссылка на непрерывный участок коллекции (строки или массива), которая не владеет данными, а только указывает на них.

Строковые срезы &str

Срез — это ссылка на часть данных. Строковый срез &str указывает на участок строки, не копируя его. Диапазон задаётся через [начало..конец] (конец не включается).

fn main() {
    let s = String::from("привет мир");
    let hello = &s[0..12];   // срез: первое слово (12 байт в UTF-8)
    let world = &s[13..];    // от 13-го байта до конца
    println!("{hello}|{world}");
}

Вывод:

привет|мир

Срез не владеет строкой — он лишь ссылается на её часть. Поэтому он подчиняется тем же правилам заимствования: пока живёт срез, нельзя изменить исходную строку.

String против &str

Это различие сбивает с толку всех новичков. String — владеющая строка, растущая, хранится в куче. &str — заимствованный взгляд на строку, ничем не владеет. Строковые литералы вроде "привет" уже имеют тип &str.

String&str
Владеет данными?данет (заимствует)
Можно менять?да (если mut)нет
Откуда берётсяString::from(...)литерал или срез

Практическое правило: функции лучше принимать &str — тогда им можно передать и String (через &), и литерал. Так функция становится универсальнее.

fn first_word(s: &str) -> &str {     // принимаем &str — самый гибкий вариант
    match s.find(' ') {
        Some(i) => &s[..i],
        None => s,
    }
}

fn main() {
    let owned = String::from("привет мир");
    println!("{}", first_word(&owned)); // String -> &str через &
    println!("{}", first_word("один два")); // литерал — уже &str
}

Вывод:

привет
один

Время жизни (lifetimes) — мягкое введение

Раз срез не владеет данными, возникает вопрос: что если исходные данные удалят, а срез останется? Это была бы висячая ссылка. Чтобы такого не случилось, Rust отслеживает время жизни каждой ссылки — как долго она действительна — и гарантирует, что ссылка не переживёт свои данные.

Чаще всего компилятор выводит время жизни сам, и вы его даже не пишете. Но иногда, когда функция возвращает ссылку, ему нужна подсказка — какая из входных ссылок «связана» с результатом. Тогда появляется аннотация вида &'a str, где 'a — имя времени жизни.

// 'a говорит: результат живёт ровно столько, сколько оба входных среза
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() > b.len() { a } else { b }
}

fn main() {
    let s1 = String::from("длинная строка");
    let s2 = String::from("строка");
    println!("{}", longest(&s1, &s2));
}

Вывод:

длинная строка

Аннотации времени жизни ничего не меняют в рантайме — это лишь контракт для компилятора: «возвращаемая ссылка действительна не дольше, чем входные». Благодаря ему вернуть висячую ссылку невозможно. На старте достаточно понимать идею; подробности приходят с практикой.

Итог

  • Срез (&str, &[T]) — ссылка на часть коллекции, без владения.
  • String владеет строкой, &str — заимствует; функции удобнее принимать &str.
  • Время жизни гарантирует, что ссылка не переживёт данные; обычно выводится автоматически.
Проверьте себя
1. Владеет ли срез (&str) данными, на которые указывает?
AДа, полностью
BНет, он только ссылается на часть данных
CТолько если объявлен как mut
DТолько для чисел
2. В чём разница между String и &str?
AЭто синонимы
BString владеет данными и растёт, &str — заимствованный неизменяемый взгляд на строку
C&str владеет данными, String — нет
DString быстрее всегда
3. Что задаёт аннотация времени жизни вроде 'a?
AСкорость выполнения
BКонтракт для компилятора: как долго ссылка должна оставаться действительной
CОбъём выделяемой памяти
DВерсию языка
Поддержать проект