Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Тип среза

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

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

fn first_word(s: &String) -> ?

Функция first_word принимает параметр &String. Нам не нужно владение строкой, так что это нормально. Но что возвращать? У нас нет способа описать часть строки. Однако можно вернуть индекс конца слова, обозначенный пробелом. Попробуем так, как показано в Листинге 4-7.

Имя файла: src/main.rs

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() {}

Листинг 4-7: Функция first_word, возвращающая индекс байта в параметре String

Чтобы пройти по строке элемент за элементом и проверить, является ли значение пробелом, преобразуем String в массив байтов с помощью метода as_bytes:

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.

Имя файла: src/main.rs

Листинг 4-8: Сохранение результата вызова функции first_word и последующее изменение содержимого String

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

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

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

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

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

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

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

В отличие от ссылки на весь String (как s2), hello — это ссылка на часть String, указанная дополнительным фрагментом [0..5]. Мы создаём срезы, используя диапазон в квадратных скобках, указывая [начальный_индекс..конечный_индекс], где начальный_индекс — это первая позиция в срезе, а конечный_индекс — на единицу больше последней позиции в срезе.

Срезы — особые виды ссылок, потому что они являются “толстыми” указателями, или указателями с метаданными. Здесь метаданные — это длина среза. Мы можем увидеть эти метаданные, изменив визуализацию, чтобы заглянуть внутрь структур данных Rust:

Обратите внимание, что переменные hello и world имеют оба поля ptr и len, которые вместе определяют подчёркнутые области строки в куче. Вы также можете увидеть здесь, как на самом деле выглядит String: строка — это вектор байтов (Vec<u8>), который содержит длину len и буфер buf с указателем ptr и ёмкостью cap.

Поскольку срезы являются ссылками, они также изменяют права на referenced данные. Например, обратите внимание, что при создании hello как среза s, s теряет права на запись и владение:

Синтаксис диапазонов

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

#![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 находится в разделе “Хранение текста, закодированного в UTF-8, со строками” Главы 8.

Переписывание first_word со строковыми срезами

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

Имя файла: src/main.rs

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, ища первое вхождение пробела. Когда находим пробел, возвращаем строковый срез, используя начало строки и индекс пробела в качестве начального и конечного индексов.

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

Возврат среза также сработал бы для функции second_word:

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

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

Имя файла: src/main.rs

Вы видите, что вызов first_word теперь удаляет право на запись из s, что предотвращает вызов s.clear(). Вот ошибка компилятора:

$ 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(); // error!
   |     ^^^^^^^^^ mutable borrow occurs here
19 |
20 |     println!("the first word is: {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 {

Более опытный Rustacean написал бы сигнатуру, показанную в Листинге 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` works on slices of `String`s, whether partial or whole.
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` also works on references to `String`s, which are equivalent
    // to whole slices of `String`s.
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word` works on slices of string literals, whether partial or
    // whole.
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}

Листинг 4-9: Улучшение функции first_word использованием строкового среза для типа параметра s

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

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

Имя файла: src/main.rs

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` works on slices of `String`s, whether partial or whole.
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` also works on references to `String`s, which are equivalent
    // to whole slices of `String`s.
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word` works on slices of string literals, whether partial or
    // whole.
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    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]. Он работает так же, как и строковые срезы, храня ссылку на первый элемент и длину. Вы будете использовать этот вид среза для всех sorts других коллекций. Мы подробно обсудим эти коллекции, когда будем говорить о векторах в Главе 8.

Краткое содержание

Срезы — это особый вид ссылки, который ссылается на поддиапазоны последовательности, такие как строка или вектор. Во время выполнения срез представляется как “толстый указатель”, содержащий указатель на начало диапазона и длину диапазона. Одно из преимуществ срезов над диапазонами на основе индексов заключается в том, что срез не может быть недействительным, пока он используется.