Срезы в Rust
Срез (slice) позволяет заимствовать часть массива или вектора — «окно» в его фрагмент — без копирования данных и без перехода владения. Если ты уже пользовался заимствованием для одного значения, срез — та же идея, только для диапазона элементов.
Срез массива
Заголовок раздела «Срез массива»fn main() { let numbers = [10, 20, 30, 40, 50];
let middle = &numbers[1..4]; // элементы с индексами 1, 2, 3 println!("{:?}", middle);}Вывод:
[20, 30, 40]1..4 — это такой же диапазон, как в цикле for: он начинается с индекса 1 и останавливается перед индексом 4, так что ты получаешь индексы 1, 2 и 3.
Сокращённая запись диапазонов
Заголовок раздела «Сокращённая запись диапазонов»Несколько сокращений, которые встречаются часто:
fn main() { let numbers = [10, 20, 30, 40, 50];
println!("{:?}", &numbers[..3]); // от начала до индекса 3 -> [10, 20, 30] println!("{:?}", &numbers[2..]); // от индекса 2 до конца -> [30, 40, 50] println!("{:?}", &numbers[..]); // всё целиком -> [10, 20, 30, 40, 50]}Строковые срезы
Заголовок раздела «Строковые срезы»На самом деле ты уже пользовался срезами: &str — это срез String (или другой строки)! Поэтому эти два типа так хорошо работают вместе:
fn main() { let sentence = String::from("The quick brown fox");
let first_word = &sentence[0..3]; println!("{first_word}"); // "The"}Строка здесь оставлена на английском не случайно: диапазон в строковом срезе считается в байтах, а каждая русская буква занимает в UTF-8 два байта. С кириллицей &sentence[0..3] разрезал бы букву пополам, и программа запаниковала бы. Подробнее — в уроке Строки.
Почему срезы, а не копирование?
Заголовок раздела «Почему срезы, а не копирование?»Можно представить, что задача решается копированием нужного фрагмента в новый Vec или String. Срезы этого полностью избегают: они просто указывают на часть уже существующих данных — так же, как ссылка (&) указывает на одно значение. Значит, нет лишнего выделения памяти, нет копирования, а правила заимствования, которые ты знаешь из урока Заимствование, по-прежнему действуют.
Функция, которая принимает срез
Заголовок раздела «Функция, которая принимает срез»Срезы особенно удобны как параметры функций, потому что принимают и массивы, и векторы:
fn sum(numbers: &[i32]) -> i32 { let mut total = 0; for n in numbers { total += n; } total}
fn main() { let array = [1, 2, 3]; let vector = vec![4, 5, 6];
println!("{}", sum(&array)); println!("{}", sum(&vector));}&[i32] принимает срез и массива, и Vec — одна функция, и оба вызова работают.
Меняем данные через изменяемый срез
Заголовок раздела «Меняем данные через изменяемый срез»До сих пор наши срезы только читали данные. Изменяемый срез &mut [T] позволяет функции менять переданные ей элементы — но только их:
fn double_all(numbers: &mut [i32]) { for i in 0..numbers.len() { numbers[i] *= 2; }}
fn main() { let mut scores = vec![1, 2, 3, 4, 5];
double_all(&mut scores[1..3]); // только индексы 1 и 2 println!("{:?}", scores);
double_all(&mut scores); // весь вектор println!("{:?}", scores);}Вывод:
[1, 4, 6, 4, 5][2, 8, 12, 8, 10]Первый вызов передаёт &mut scores[1..3], поэтому double_all видит только индексы 1 и 2. Всё, что за пределами диапазона, от неё защищено. Внутри функции срез снова начинается с индекса 0: для double_all это просто список из двух чисел.
Это безопасно благодаря правилу, которое ты как следует разберёшь в уроке Заимствование: пока функция держит &mut-срез, никто другой не может пользоваться этими элементами, так что две части программы никогда не изменят их одновременно. Сам вектор тоже должен быть объявлен с mut, иначе Rust не разрешит заимствовать его изменяемо.
Практика
Заголовок раздела «Практика»1. Делим пополам
Заголовок раздела «1. Делим пополам»Создай вектор vec![3, 6, 9, 12, 15, 18]. Выведи его первую и вторую половины как два отдельных среза. Подсказка: вычисли let mid = numbers.len() / 2; и используй ..mid и mid...
Показать решение
fn main() { let numbers = vec![3, 6, 9, 12, 15, 18]; let mid = numbers.len() / 2;
let first_half = &numbers[..mid]; let second_half = &numbers[mid..];
println!("Первая половина: {:?}", first_half); println!("Вторая половина: {:?}", second_half);}Вывод:
Первая половина: [3, 6, 9]Вторая половина: [12, 15, 18]Поскольку mid вычисляется из len(), код продолжит работать, если добавить или убрать числа. При нечётной длине лишний элемент попадает во вторую половину.
2. Наибольшее в любом списке
Заголовок раздела «2. Наибольшее в любом списке»Напиши функцию largest(numbers: &[i32]) -> i32, которая возвращает наибольшее число. Вызови её с массивом, с вектором и со срезом из одного только первого элемента вектора. Подсказка: начни с numbers[0] и перебери остальные, как сделал бы с массивом.
Показать решение
fn largest(numbers: &[i32]) -> i32 { let mut biggest = numbers[0]; for n in numbers { if *n > biggest { biggest = *n; } } biggest}
fn main() { let array = [7, 2, 9, 4]; let vector = vec![10, 35, 21];
println!("{}", largest(&array)); println!("{}", largest(&vector)); println!("{}", largest(&vector[..1]));}Вывод:
93510При переборе среза ты получаешь ссылку на каждый элемент (&i32), а не само число. *n значит «значение, на которое указывает n», поэтому *n > biggest сравнивает два обычных числа. На пустом срезе эта версия запаникует, потому что numbers[0] не существует. Позже ты научишься возвращать в таком случае Option.
- Срез (
&[T]) заимствует часть массива или вектора без копирования. 1..4выбирает индексы с 1 по 3; если опустить одну из сторон.., диапазон пойдёт от начала или до конца.&str— это тоже срез, только текста, поэтому у строк и срезов так много общего.- Используй
&[T]как тип параметра, когда хочешь принимать и массивы, и векторы. - Изменяемый срез (
&mut [T]) позволяет функции менять только переданные ей элементы.
Проверь себя
Заголовок раздела «Проверь себя»Проверь себя
1. Что позволяет сделать срез с частью массива или вектора?
2. Какие индексы входят в &numbers[1..4]?
3. Почему &[i32] — хороший выбор для типа параметра функции?
Результат: 0 / 3
Что почитать
Заголовок раздела «Что почитать»- Тип среза — книга «Язык программирования Rust»: шаг за шагом подводит к срезам на примере «первого слова строки» (перевод на русский).
- Массивы и срезы — Rust на примерах: запускаемые примеры заимствования массивов как срезов (перевод на русский).
slice— документация стандартной библиотеки (на английском): множество методов срезов, напримерfirst,split_at,containsиsort.- Slice types — The Rust Reference (на английском): точное определение — если хочешь знать, что именно значат
[T]и&[T].