Срезы
Slices let you reference a contiguous sequence of elements in a collection. A slice is a kind of reference, so it does not have ownership.
Here’s a small programming problem: Write a function that takes a string of words separated by spaces and returns the first word it finds in that string. If the function doesn’t find a space in the string, the whole string must be one word, so the entire string should be returned.
Note: For the purposes of introducing slices, we are assuming ASCII only in this section; a more thorough discussion of UTF-8 handling is in the “Storing UTF-8 Encoded Text with Strings” section of Chapter 8.
Давайте посмотрим, как бы мы написали сигнатуру этой функции без использования срезов, чтобы понять их смысл:
fn first_word(s: &String) -> ?
The first_word function has a parameter of type &String. 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() {}
first_word function that returns a byte index value into the String parameterBecause 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 still has the value 5 here, but s no longer has any content that we
// could meaningfully use with the value 5, so word is now totally invalid!
}
first_word function and then changing the String contentsДанная программа компилируется без ошибок и будет успешно работать даже после того как мы воспользуемся переменной word после вызова s.clear(). Поскольку значение word совсем не связано с переменной s, то word сохраняет своё значение 5. Мы бы могли воспользоваться значением 5, чтобы получить первое слово из переменной s, но это приведёт к ошибке, потому что содержимое s изменилось с того момента, как мы сохраняли 5 в переменной word (s стала пустой строкой после вызова s.clear()).
Having to worry about the index in word getting out of sync with the data in s is tedious and error-prone! Managing these indices is even more brittle if we write a second_word function. Its signature would have to look like this:
fn second_word(s: &String) -> (usize, usize) {
Теперь мы отслеживаем как начальный, так и конечный индекс. Теперь у нас есть ещё больше информации, вычислительно связанной с некоторыми данными, но фактически никак не реагирующими на их изменение. У нас есть три отдельные переменные, и все их необходимо держать действительными.
К счастью, в Rust есть решение данной проблемы: строковые срезы.
Строковые срезы
A string slice is a reference to a contiguous sequence of the elements of a String, and it looks like this:
fn main() {
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
}
Rather than a reference to the entire String, hello is a reference to a portion of the String, specified in the extra [0..5] bit. We create slices using a range within square brackets by specifying [starting_index..ending_index], where starting_index is the first position in the slice and ending_index is one more than the last position in the slice. Internally, the slice data structure stores the starting position and the length of the slice, which corresponds to ending_index minus starting_index. So, in the case of let world = &s[6..11];, world would be a slice that contains a pointer to the byte at index 6 of s with a length value of 5.
Рисунок 4-7 показывает это.
Figure 4-7: A string slice referring to part of a 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..];
}
You can also drop both values to take a slice of the entire string. So, these are equal:
#![allow(unused)]
fn main() {
let s = String::from("hello");
let len = s.len();
let slice = &s[0..len];
let slice = &s[..];
}
Note: String slice range indices must occur at valid UTF-8 character boundaries. If you attempt to create a string slice in the middle of a multibyte character, your program will exit with an error.
Давайте используем полученную информацию и перепишем 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 {
We now have a straightforward API that’s much harder to mess up because the compiler will ensure that the references into the String remain valid. Remember the bug in the program in Listing 4-8, when we got the index to the end of the first word but then cleared the string so our index was invalid? That code was logically incorrect but didn’t show any immediate errors. The problems would show up later if we kept trying to use the first word index with an emptied string. Slices make this bug impossible and let us know much sooner that we have a problem with our code. Using the slice version of first_word will throw a compile-time error:
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(); // 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!";
}
The type of s here is &str: It’s a slice pointing to that specific point of the binary. This is also why string literals are immutable; &str is an immutable reference.
Строковые срезы как параметры
Зная, что мы можем брать срезы литералов и значений типа 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);
}
first_word function by using a string slice for the type of the s parameterIf we have a string slice, we can pass that directly. If we have a String, we can pass a slice of the String or a reference to the String. This flexibility takes advantage of deref coercions, a feature we will cover in the “Using Deref Coercions in Functions and Methods” section of Chapter 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.
Подведём итоги
The concepts of ownership, borrowing, and slices ensure memory safety in Rust programs at compile time. The Rust language gives you control over your memory usage in the same way as other systems programming languages. But having the owner of data automatically clean up that data when the owner goes out of scope means you don’t have to write and debug extra code to get this control.
Владение влияет на множество других аспектов Rust. Мы будем говорить об этих концепциях на протяжении оставшихся частей книги. Давайте перейдём к Главе 5 и рассмотрим группировку данных в структуры.