Срезы

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

Вот небольшая программистская задачка: напишите функцию, которая принимает строку слов, разделённых пробелами, и возвращает первое слово в этой строке. Если функция не находит пробела в строке, то вся строка состоит из одного слова, а потому должна быть возвращена вся строка целиком.

Давайте посмотрим, как бы мы написали сигнатуру этой функции без использования срезов, чтобы понять их смысл:

fn first_word(s: &String) -> ?

The first_word function has a &String as a parameter. We don’t need ownership, so this is fine. (In idiomatic Rust, functions do not take ownership of their arguments unless they need to, and the reasons for that will become clear as we keep going!) But what should we return? We don’t really have a way to talk about part of a string. However, we could return the index of the end of the word, indicated by a space. Let’s try that, as shown in Listing 4-7.

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {}

Because we need to go through the String element by element and check whether a value is a space, we’ll convert our String to an array of bytes using the as_bytes method.

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {}

Далее, мы создаём итератор по массиву байтов используя метод iter:

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {}

Мы обсудим итераторы более подробно в Главе 13. На данный момент знайте, что iter — это метод, который возвращает каждый элемент в коллекции, а enumerate оборачивает результат iter и вместо этого возвращает каждое значение как элемент кортежа. Первый элемент кортежа, возвращаемый из enumerate, является индексом, а второй элемент — ссылкой на само значение. Это немного удобнее, чем вычислять индекс самостоятельно.

Поскольку метод enumerate возвращает кортеж, мы можем использовать шаблоны для деконструирования этого кортежа. Мы подробнее обсудим шаблоны в Главе 6.В цикле for мы указываем шаблон, состоящий из i для индекса в кортеже и &item для отдельного байта в кортеже. Поскольку мы из .iter().enumerate() получаем лишь ссылку на элемент, мы в шаблоне используем &.

Внутри цикла for мы ищем байт, являющийся пробелос, используя синтаксис байтового литерала. Если мы находим пробел, мы возвращаем его позицию. В противном случае, мы возвращаем длину строки с помощью s.len().

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {}

Теперь у нас есть способ узнать индекс байта, указывающего на конец первого слова в строке; но есть проблема. Мы возвращаем лишь usize, но это число имеет смысл только в свя́зи со &String. Другими словами, поскольку это значение отделено от String, то нет гарантии, что оно будет действительным в будущем. Рассмотрим программу из Листинга 4-8, которая использует функцию first_word из Листинга 4-7.

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s); // word будет связано со значением 5

    s.clear(); // здесь String очищается и становится равным ""

    // `word` здесь всё ещё имеет значение `5` но `s` теперь пуста, а значение
    // `5` не имеет с ней никакого смысла, так что `word` теперь
    // полностью бессмысленна!
}

Данная программа компилируется без ошибок и будет успешно работать даже после того как мы воспользуемся переменной word после вызова s.clear(). Поскольку значение word совсем не связано с переменной s, то word сохраняет своё значение 5. Мы бы могли воспользоваться значением 5, чтобы получить первое слово из переменной s, но это приведёт к ошибке, потому что содержимое s изменилось с того момента, как мы сохраняли 5 в переменной word (s стала пустой строкой после вызова s.clear()).

Необходимость беспокоиться о том, что индекс в переменной word не синхронизируется с содержимым переменным s — это утомительно и потенциально опасно! Управление этими индексами становится ещё более хрупким в функциях вроде second_word. Её сигнатура могла бы выглядеть так:

fn second_word(s: &String) -> (usize, usize) {

Теперь мы отслеживаем как начальный, так и конечный индекс. Теперь у нас есть ещё больше информации, вычислительно связанной с некоторыми данными, но фактически никак не реагирующими на их изменение. У нас есть три отдельные переменные, и все их необходимо держать действительными.

К счастью, в Rust есть решение данной проблемы: строковые срезы.

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

Строковый срез — это ссылка на часть строки String. Он выглядит вот так:

fn main() {
    let s = String::from("hello world");

    let hello = &s[0..5];
    let world = &s[6..11];
}

Вместо того, чтобы ссылаться на всю String, hello ссылается лишь на часть String, определяемую приписанным справа промежутком [0..5]. Срезы создаются указанием диапазона в квадратных скобках: [starting_index..ending_index], где starting_index — это первая позиция в срезе, а ending_index — это на единицу больше, чем последняя позиция в срезе. Внутри себя срез хранит начальную позицию и длину среза, что соответствует ending_index минус starting_index. Итак, в строчке let world = &s[6..11];, world будет срезом, содержащим указатель на байт строки s по индексу 6, и значение 5 длины среза.

Рисунок 4-7 показывает это.

Три таблицы: таблица, представляющая данные s на стеке, которая указывает
на байт по индексу 0 в таблице с текстом "hello world" в
куче. Третья таблица пред-ставляет данные среза world на стеке, которые
включают в себя его длину 5 и указание на байт по индексу 6 в таблице данных в куче.

Рисунок 4-7: Строковый срез ссылается на часть String

Синтаксис .. позволяет вам опустить первое число, если вы хотите взять промежуток, начиная с индекса 0. Другими словами, эти две записи равны:

#![allow(unused)]
fn main() {
let s = String::from("hello");

let slice = &s[0..2];
let slice = &s[..2];
}

Таким же образом, если ваш срез включает последний байт String, вы можете опустить второе число. Эти две записи тоже равны:

#![allow(unused)]
fn main() {
let s = String::from("hello");

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..];
}

Вы также можете опустить оба числа, чтобы получить срез из всей строки. Эти две записи тоже будут равны между собой:

#![allow(unused)]
fn main() {
let s = String::from("hello");

let len = s.len();

let slice = &s[0..len];
let slice = &s[..];
}

Примечание: Индексы диапазона среза строки должны соблюдать байтовое членение символов в кодировке UTF-8. Если вы попытаетесь создать срез строки, в котором начало или конец будет указывать на кусок символа, программа завершится с ошибкой. В целях объяснения работы срезов строк мы предполагаем, что в этом разделе используется только кодировка ASCII; более подробное обработка текста в UTF-8 обсуждается в разделе Главы 8 "Хранение текста в кодировке UTF-8 с помощью строк".

Давайте используем полученную информацию и перепишем first_word так, чтобы она возвращала срез. Тип "срез строки" обозначается как &str:

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {}

Мы получаем индекс конца слова так же, как в Листинге 4-7, ища первое вхождение пробела. Когда мы находим пробел, мы возвращаем срез строки, используя 0 и индекс пробела в качестве начального и конечного индексов.

Теперь, когда мы вызываем first_word, мы возвращаем единственное значение, привязанное к обрабатываемым данным. Значение состоит из ссылки на первый символ строки, с которого начинается срез, и количества элементов в срезе.

Аналогичным образом можно переписать и функцмю second_word:

fn second_word(s: &String) -> &str {

Теперь у нас есть простой API, который гораздо сложнее испортить, поскольку что компилятор гарантирует, что ссылки на String останутся действительными. Помните ошибку в программе Листинга 4-8, когда мы получили индекс конца первого слова, но затем очистили строку, так что наш индекс стал недействительным? Этот код был неправильным логически, но не формально. Его проблемы могли проявиться позже, если бы мы попытались использовать индекс первого слова с пустой строкой. Срезы делают эту ошибку невозможной и сообщают нам о проблеме с нашим кодом гораздо раньше. Например, код ниже не скомпилируется:

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s);

    s.clear(); // ошибка!

    println!("первое слово: {word}");
}

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

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  --> src/main.rs:18:5
   |
16 |     let word = first_word(&s);
   |                           -- immutable borrow occurs here
17 |
18 |     s.clear(); // ошибка!
   |     ^^^^^^^^^ mutable borrow occurs here
19 |
20 |     println!("первое слово: {word}");
   |                                  ------ immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

Напомним из правил заимствования, что если у нас есть неизменяемая ссылка на что-то, мы не можем также взять изменяемую ссылку. Поскольку методу clear необходимо очистить String, необходимо получить изменяемую ссылку на неё. Макрос println! (после вызова clear) использует ссылку, сохранённую в word, а потому неизменяемая ссылка в этот момент всё ещё должна быть действительной. Rust запрещает одновременное существование изменяемой ссылки в clear и неизменяемой ссылки в word, и компиляция завершается ошибкой. Rust не только упростил использование нашего API, но и устранил целый класс ошибок сразу на этапе компиляции!

Строковые литералы как срезы

Напомним: мы когда-то говорили о строковых литералах, записанных напрямую в файлах. Теперь, когда мы знаем чем являются срезы, мы можем правильно понять, что такое строковые литералы:

#![allow(unused)]
fn main() {
let s = "Hello, world!";
}

Тип переменной s — это &str, то есть срез, указывающий на конкретную точку бинарного файла. Вот почему строковые литералы неизменяемы; почему &str — ссылка именно неизменяемая.

Строковые срезы как параметры

Зная, что мы можем брать срезы литералов и значений типа String, мы можем прийти к ещё одному улучшению first_word. Вот её сигнатура:

fn first_word(s: &String) -> &str {

Более опытный программист на Rust вместо этого написал бы сигнатуру, показанную в Листинге 4-9, потому что это позволяет нам использовать одну и ту же функцию как для значений &String, так и для значений &str.

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {
    let my_string = String::from("hello world");

    // `first_word` работает на любых срезах значений типа `String`: полных или частичных
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` также работает на ссылках на значения типа `String`:
    // такие ссылки равны срезам из всей `String` целиком.
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word` работает на любых срезах литералов строк: полных или
    // частичных.
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // Поскольку строковые литералы *эквивалентны* срезам строк,
    // это тоже сработает, без необходимости брать срез!
    let word = first_word(my_string_literal);
}

Если у нас есть срез строки, мы можем передать его напрямую. Если у нас есть String, мы можем передать часть String или ссылку на String. Эта гибкость работает за счёт функциональности приведения ссылок при разыменовывании, что мы рассмотрим в разделе "Неявное приведение при разыменовании с функциями и методами" Главы 15.

Определение функции на срезе строки, а не на ссылке на String, делает наш API более общим и шире применимым без потери какой-либо функциональности:

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {
    let my_string = String::from("hello world");

    // `first_word` работает на любых срезах значений типа `String`: полных или частичных
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` также работает на ссылках на значения типа `String`:
    // такие ссылки равны срезам из всей `String` целиком.
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word` работает на любых срезах литералов строк: полных или
    // частичных.
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // Поскольку литералы строк *эквивалентны* срезам строк,
    // это тоже сработает, без необходимости брать срез!
    let word = first_word(my_string_literal);
}

Другие срезы

Срезы строк, как вы можете понять, работают именно со строками. Но есть и более общий тип среза. Рассмотрим вот этот массив:

#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];
}

Точно так же, как мы можем захотеть сослаться на часть строки, мы можем захотеть сослаться на часть массива. Мы можем сделать это так:

#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

assert_eq!(slice, &[2, 3]);
}

Этот срез имеет тип &[i32]. Он работает так же, как и срезы строк: сохраняет ссылку на первый элемент и его длину. Вам понадобится этот вид среза для всех видов других коллекций. Мы подробно обсудим различные коллекции, когда будем говорить о векторах в Главе 8.

Подведём итоги

Концепции владения, заимствования и срезов обеспечивают безопасность памяти в программах на Rust без их замедления — все необходимые проверки происходят на этапе компиляции. Язык Rust даёт вам контроль над использованием памяти так же, как и другие языки системного программирования, но то, что владелец данных автоматически очищает эти данные, когда выходит из области видимости, означает, что вам не нужно писать и отлаживать дополнительный код, чтобы делать это вручную.

Владение влияет на множество других аспектов Rust. Мы будем говорить об этих концепциях на протяжении оставшихся частей книги. Давайте перейдём к Главе 5 и рассмотрим группировку данных в структуры.