Недействительные ссылки В языках с указателями легко ошибочно создать висячий указатель — указатель, ссылающийся на место в памяти, которое могло быть передано кому-то другому после освобождения этой части памяти, сохраняя при этом указатель на неё. В Rust, напротив, компилятор гарантирует, что ссылки никогда не будут висячими: если у вас есть ссылка на какие-то данные, компилятор убедится, что данные не выйдут за пределы области видимости до того, как это сделает ссылка на них. Давайте попробуем создать висячую ссылку, чтобы увидеть, как Rust предотвращает их появление с помощью ошибки во время компиляции: Файл: src/main.rs Здесь ошибка: Это сообщение об ошибке относится к особенности языка, которую мы ещё не рассмотрели: времени жизни. Мы подробно обсудим времена жизни в главе 10. Но если вы не обращаете внимания на части, касающиеся времени жизни, сообщение будет содержать ключ к тому, почему этот код является проблемой: fn main () { let reference_to_nothing = dangle(); } fn dangle () -> & String { let s = String ::from( "hello" ); &s } $ cargo run Compiling ownership v0.1.0 (file:///projects/ownership) error[E0106]: missing lifetime specifier --> src/main.rs:5:16 | 5 | fn dangle() -> &String { | ^ expected named lifetime parameter | = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from help: consider using the `'static` lifetime | 5 | fn dangle() -> &'static String { |
For more information about this error, try `rustc --explain E0106`. error: could not compile `ownership` due to previous error this function's return type contains a borrowed value, but there is no value for it to be borrowed from Давайте подробнее рассмотрим, что именно происходит на каждом этапе нашего кода dangle : Файл: src/main.rs Поскольку s создаётся внутри dangle , когда код dangle будет завершён, s будет освобождена. Но мы попытались вернуть ссылку на неё. Это означает, что эта ссылка будет указывать на недопустимую String . Это нехорошо! Rust не позволит нам сделать это. Решением будет вернуть непосредственно String : Это работает без проблем. Владение перемещено, и ничего не освобождено. Правила работы с ссылкамиДавайте повторим все, что мы обсудили про ссылки: В один момент времени, может существовать либо одна изменяемая ссылочная переменная, либо любое количество неизменяемых ссылочных переменных, Все ссылки должны быть действительными. В следующей главе мы рассмотрим другой тип ссылочных переменных — срезы. fn dangle () -> & String { // dangle returns a reference to a String let s = String ::from( "hello" ); // s is a new String &s // we return a reference to the String, s } // Here, s goes out of scope, and is dropped. Its memory goes away. // Danger! fn no_dangle () -> String { let s = String ::from( "hello" ); s } СрезыСрезы позволяют ссылаться на непрерывную последовательность элементов в коллекции, а не на всю коллекцию. Срез является своего рода ссылкой, поэтому он не имеет права владения. Вот небольшая проблема программирования: напишите функцию, которая принимает строку слов, разделённых пробелами, и возвращает первое слово, которое она находит в этой строке. Если функция не находит пробела в строке, вся строка должна состоять из одного слова, поэтому должна быть возвращена вся строка. Давайте рассмотрим, как бы мы написали сигнатуру этой функции без использования срезов, чтобы понять проблему, которую решат срезы: Функция first_word имеет &String в качестве параметра. Мы не хотим владения, так что всё в порядке. Но что мы должны вернуть? На самом деле у нас нет способа говорить о части строки. Однако мы могли бы вернуть индекс конца слова, обозначенного пробелом. Давайте попробуем, как показано в Листинге 4-7. Файл: src/main.rs Листинг 4-7. Функция first_word , возвращающая значение индекса байта в параметр String Для того, чтобы найти пробел в строке, мы превратим String в массив байт, используя метод as_bytes и пройдём по String элемент за элементом, проверяя является ли значение пробелом. Далее, мы создаём итератор по массиву байт используя метод iter : Мы обсудим итераторы более подробно в Главе 13 . На данный момент знайте, что iter — это метод, который возвращает каждый элемент в коллекции, а enumerate fn first_word (s: & String ) -> ? 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() } let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { оборачивает результат iter и вместо этого возвращает каждый элемент как часть кортежа. Первый элемент кортежа, возвращаемый из enumerate , является индексом, а второй элемент — ссылкой на элемент. Это немного удобнее, чем вычислять индекс самостоятельно. Поскольку метод enumerate возвращает кортеж, мы можем использовать шаблоны для деструктурирования этого кортежа. Мы подробнее обсудим шаблоны в Главе 6. . В цикле for мы указываем шаблон, имеющий i для индекса в кортеже и &item для одного байта в кортеже. Поскольку мы получаем ссылку на элемент из .iter().enumerate() , мы используем & в шаблоне. Внутри цикла for , ищем байт представляющий пробел используя синтаксис байт литерала. Если пробел найден, возвращается его позиция. Иначе, возвращается длина строки s.len() : Теперь у нас есть способ узнать индекс байта указывающего на конец первого слова в строке, но есть проблема. Мы возвращаем сам usize , но это число имеет значение только в контексте &String . Другими словами, поскольку это значение отдельное от String , то нет гарантии, что оно все ещё будет действительным в будущем. Рассмотрим программу из листинга 4-8, которая использует функцию first_word листинга 4-7. Файл: src/main.rs Листинг 4-8. Сохранение результата вызова функции first_word и последующего изменения содержимого String Данная программа компилируется без ошибок и будет успешно работать, даже после того как мы воспользуемся переменной word после вызова s.clear() . Так как значение word совсем не связано с состоянием переменной s , то word сохраняет своё значение 5 без изменений. Мы могли бы использовать 5 вместе с переменной s и попытаться извлечь первое слово из строки, но это приведёт к ошибке, потому что содержимое s if item == b' ' { return i; } } s.len() fn main () { let mut s = String ::from( "hello world" ); let word = first_word(&s); // word will get the value 5 s.clear(); // this empties the String, making it equal to "" // word still has the value 5 here, but there's no more string that // we could meaningfully use the value 5 with. word is now totally invalid! } изменилось после того как мы сохранили 5 в переменной word (стало пустой строкой в вызове s.clear() ). Необходимость беспокоиться о том, что индекс в переменной word не синхронизируется с данными в переменной s является утомительной и подверженной ошибкам! Управление этими индексами становится ещё более хрупким, если мы напишем функцию second_word . Её сигнатура могла бы выглядеть так: Теперь мы отслеживаем начальный и конечный индекс, и у нас есть ещё больше значений, которые были рассчитаны на основе данных в определённом состоянии, но вообще не привязаны к этому состоянию. У нас есть три несвязанные переменные, которые необходимо синхронизировать. К счастью в Rust есть решение данной проблемы: строковые срезы. Строковые срезы Строковый срез - это ссылка на часть строки String и он выглядит следующим образом: Вместо ссылки на всю String hello является ссылкой на часть String , указанную в дополнительном бите [0..5] . Мы создаём срезы, используя диапазон в квадратных скобках, указав [starting_index..ending_index] , где starting_index — это первая позиция в срезе, а ending_index — на единицу больше последней позиции в срезе. Внутри структура данных среза хранит начальную позицию и длину среза, что соответствует ending_index минус starting_index . Таким образом, в случае let world = &s[6..11]; , world будет срезом, содержащим указатель на байт с индексом 6 s со значением длины 5. Рисунок 4-6 отображает это на диаграмме. fn second_word (s: & String ) -> ( usize , usize ) { let s = String ::from( "hello world" ); let hello = &s[ 0 5 ]; let world = &s[ 6 11 ];
world name value ptr len 5 index value 0 h 1 e 2 l 3 l 4 o 5 6 w 7 o 8 r 9 l 10 d s name value ptr len 11 capacity 11 Рисунок 4-6: Фрагмент строки, относящийся к части String С синтаксисом диапазона в Rust, если вы хотите начать с нулевого индекса, вы можете отбросить значение перед двумя точками. Другими словами, они равны:
Таким же образом, если срез включает последний байт строки String , можно убрать завершающее число. Это эквивалентно: Вы также можете отбросить оба значения, чтобы получить часть всей строки. Итак, они равны: Примечание. Индексы диапазона срезов строк должны располагаться на допустимых границах символов UTF-8. Если вы попытаетесь создать фрагмент строки в середине много байтового символа, ваша программа завершится с ошибкой. В целях изучения срезов строк мы предполагаем, что в этом разделе используется только ASCII; более подробное обсуждение обработки UTF-8 находится в разделе «Сохранение закодированного текста UTF-8 со строками» . Главы 8. Давайте используем полученную информацию и перепишем метод first_word так, чтобы он возвращал срез. Для обозначения типа "срез строки" существует запись &str : Файл: src/main.rs let s = String ::from( "hello" ); let slice = &s[ 0 2 ]; let slice = &s[.. 2 ]; let s = String ::from( "hello" ); let len = s.len(); let slice = &s[ 3 ..len]; let slice = &s[ 3 ..]; let s = String ::from( "hello" ); let len = s.len(); let slice = &s[ 0 ..len]; let slice = &s[..];
Мы получаем индекс конца слова способом аналогичным тому, как мы это делали в листинге 4-7: ищем индекс первого вхождения пробела, когда пробел найден, возвращается строковый срез, используя начало строки в качестве начального индекса и индекс пробела в качестве конечного индекса среза. Теперь, вызвав метод first_word , мы получим одно единственное значение, которое привязано к нижележащим данным. Значение, которое составлено из ссылки на начальную точку среза и количества элементов в срезе. Аналогичным образом можно переписать и второй метод second_word : Теперь есть простое API, работу которого гораздо сложнее испортить, потому что компилятор обеспечивает нам то, что ссылки на String останутся действительными. Помните ошибку в программе листинга 4-8, когда мы получили индекс конца первого слова, но затем очистили строку, так что она стала недействительной? Тот код был логически некорректным, хотя не показывал никаких ошибок. Проблемы возникли бы позже, если бы мы попытались использовать индекс первого слова для пустой строки. Срезы делают невозможной данную ошибку и позволяют понять о наличии проблемы гораздо раньше. Так, использование версии метода first_word со срезом вернёт ошибку компиляции: Файл: 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 second_word (s: & String ) -> & str { fn main () { let mut s = String ::from( "hello world" ); let word = first_word(&s); s.clear(); // error! println! ( "the first word is: {}" , word); } Напомним из правил заимствования, что если у нас есть неизменяемая ссылка на что-то, мы не можем также взять изменяемую ссылку. Поскольку для clear необходимо обрезать String , необходимо получить изменяемую ссылку. println! после вызова clear использует ссылку в word , поэтому неизменяемая ссылка в этот момент всё ещё должна быть активной. Rust запрещает одновременное существование изменяемой ссылки в формате clear и неизменяемой ссылки в word , и компиляция завершается ошибкой. Rust не только упростил использование нашего API, но и устранил целый класс ошибок во время компиляции! Строковые литералы это срезы Напомним, что мы говорили о строковых литералах, хранящихся внутри бинарного файла. Теперь, когда мы знаем чем являются срезы, мы правильно понимаем что такое строковые литералы: Тип s здесь является &str срезом, указывающим на конкретное место в бинарном файле программы. Это также объясняет, почему строковый литерал является неизменяемым, потому что тип &str это неизменяемая ссылка. Строковые срезы как параметры Знание о том, что можно брать срезы строковых литералов и String строк приводит к ещё одному улучшению метода first_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` due to previous error let s = "Hello, world!" ; fn first_word (s: & String ) -> & str {
Более опытные разработчики Rust написали бы сигнатуру из листинга 4-9, потому что она позволяет использовать одну функцию для значений обоих типов &String и &str Листинг 4-9: Улучшение функции first_word с помощью среза строки для типа параметра s Если у нас есть фрагмент строки, мы можем передать его напрямую. Если у нас есть String , мы можем передать часть String или ссылку на String . Эта гибкость использует преимущества разыменованного приведения, функции, которую мы рассмотрим в разделе «Неявные разыменованные приведения с функциями и методами». Главы 15. Определение функции, принимающей фрагмент строки вместо ссылки на String , делает наш API более общим и полезным без потери какой-либо функциональности: Файл: src/main.rs Другие срезы Как вы могли бы представить, строковые срезы относятся к строкам. Но также есть более общий тип среза. Рассмотрим массив: Подобно тому как мы хотели бы ссылаться на часть строки, мы можем захотеть ссылаться на часть массива. Мы можем делать это вот так: fn first_word (s: & str ) -> & str { 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); } let a = [ 1 , 2 , 3 , 4 , 5 ];
Данный срез имеет тип &[i32] . Он работает таким же образом, как и строковый срез, сохраняя ссылку на первый элемент и длину. Вы будете использовать данную разновидность среза для всех видов коллекций. Мы обсудим коллекции детально, когда будем говорить про векторы в Главе 8. Итоги Концепции владения, заимствования и срезов обеспечивают защиту использования памяти в Rust. Rust даёт вам возможность контролировать использование памяти тем же способом, как другие языки системного программирования, но дополнительно предоставляет возможность автоматической очистки данных, когда их владелец покидает область видимости функции. Это означает, что не нужно писать и отлаживать дополнительный код, чтобы добиться такого контроля. Владение влияет на множество других частей и концепций языка Rust. Мы будем говорить об этих концепциях на протяжении оставшихся частей книги. Давайте перейдём к Главе 5 и рассмотрим группировку частей данных в структуры struct let a = [ 1 , 2 , 3 , 4 , 5 ]; let slice = &a[ 1 3 ]; assert_eq! (slice, &[ 2 , 3 ]);
Использование структур дляструктурирования связанных данныхСтруктура (struct) — это пользовательский тип данных, позволяющий назвать и упаковать вместе несколько связанных значений, составляющих значимую логическую группу. Если вы знакомы с объектно-ориентированными языками, структура похожа на атрибуты данных объекта. В этой главе мы сравним и сопоставим кортежи со структурами, чтобы опираться на то, что вы уже знаете, и продемонстрируем, когда структуры являются лучшим способом группировки данных. Мы продемонстрируем, как определять структуры и создавать их экземпляры. Мы обсудим, как определить ассоциированные функции, особенно ассоциированные функции, называемые методами, для указания поведения, ассоциированного с типом структуры. Структуры и перечисления (обсуждаемые в главе 6) являются строительными блоками для создания новых типов в предметной области вашей программы. Они дают возможность в полной мере воспользоваться преимуществами проверки типов во время компиляции Rust. |