Перейти к содержимому

Срезы в 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 не разрешит заимствовать его изменяемо.

Создай вектор 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(), код продолжит работать, если добавить или убрать числа. При нечётной длине лишний элемент попадает во вторую половину.

Напиши функцию 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]));
}

Вывод:

9
35
10
▶ Попробуй сам

При переборе среза ты получаешь ссылку на каждый элемент (&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