Язык программирования Rust
Скачать 7.02 Mb.
|
Правила неявного выведения времени жизни Вы изучили, что у каждой ссылки есть время жизни и что нужно указывать параметры времени жизни для функций или структур, которые используют ссылки. Однако в Главе 4 у нас была функция в листинге 4-9, которая затем снова показана в листинге 10-25, в которой код скомпилировался без аннотаций времени жизни. Файл: src/lib.rs struct ImportantExcerpt < 'a > { part: & 'a str , } fn main () { let novel = String ::from( "Call me Ishmael. Some years ago..." ); let first_sentence = novel.split( '.' ).next().expect( "Could not find a '.'" ); let i = ImportantExcerpt { part: first_sentence, }; } 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[..] } Листинг 10-25: Функция, которую мы определили в листинге 4-9 компилируется без аннотаций времени жизни, несмотря на то, что входной и возвращаемый тип параметров являются ссылками Причина, по которой этот код компилируется — историческая. В ранних (до-1.0) версиях Rust этот код не скомпилировался бы, поскольку каждой ссылке нужно было явно назначать время жизни. В те времена, сигнатура функции была бы написана примерно так: После написания большого количества кода на Rust разработчики языка обнаружили, что в определённых ситуациях программисты описывают одни и те же аннотации времён жизни снова и снова. Эти ситуации были предсказуемы и следовали нескольким детерминированным шаблонным моделям. Команда Rust решила запрограммировать эти шаблоны в код компилятора Rust, чтобы анализатор заимствований мог вывести времена жизни в таких ситуациях без необходимости явного указания аннотаций программистами. Мы упоминаем этот фрагмент истории Rust, потому что возможно, что в будущем появится больше шаблонов для автоматического выведения времён жизни, которые будут добавлены в компилятор. Таким образом, в будущем может понадобится ещё меньшее количество аннотаций. Шаблоны, запрограммированные в анализаторе ссылок языка Rust, называются правилами неявного выведения времени жизни. Это не правила, которым должны следовать программисты; а набор частных случаев, которые рассмотрит компилятор, и, если ваш код попадает в эти случаи, вам не нужно будет указывать время жизни явно. Правила выведения не предоставляют полного заключения. Если Rust детерминировано применяет правила, но некоторая неясность относительно времён жизни ссылок все ещё остаётся, компилятор не будет догадываться, какими должны быть времена жизни оставшихся ссылок. В этом случае, вместо угадывания компилятор выдаст ошибку, которую вы можете устранить, добавив аннотации времени жизни. Времена жизни параметров функции или метода называются временем жизни ввода, а времена жизни возвращаемых значений называются временем жизни вывода. Компилятор использует три правила, чтобы выяснить времена жизни ссылок при отсутствии явных аннотаций. Первое правило относится ко времени жизни ввода, второе и третье правила применяются ко временам жизни вывода. Если компилятор доходит до конца проверки трёх правил и всё ещё есть ссылки, для которых он не может выяснить время жизни, компилятор остановится с ошибкой. Эти правила применяются к объявлениям fn , а также к блокам impl{/code1. Первое правило заключается в том, что каждый параметр являющийся ссылкой, получает свой собственный параметр времени жизни. Другими словами, функция с одним параметром получит один параметр времени жизни: fn foo<'a>(x: &'a i32) ; fn first_word < 'a >(s: & 'a str ) -> & 'a str { функция с двумя аргументами получит два отдельных параметра времени жизни: fn foo<'a, 'b>(x: &'a i32, y: &'b i32) , и так далее. Второе правило говорит, что если есть ровно один входной параметр времени жизни, то его время жизни назначается всем выходным параметрам: fn foo<'a>(x: &'a i32) -> &'a i32 Третье правило о том, что если есть множество входных параметров времени жизни, но один из них является ссылкой &self или &mut self , так как эта функция является методом, то время жизни self назначается временем жизни всем выходным параметрам. Это третье правило делает методы намного приятнее для чтения и записи, потому что требуется меньше символов. Представим, что мы компилятор и применим эти правила, чтобы вывести времена жизни ссылок в сигнатуре функции first_word листинга 10-25. Сигнатура этой функции начинается без объявления времён жизни ссылок: Теперь мы (в качестве компилятора) применим первое правило, утверждающее, что каждый параметр функции получает своё собственное время жизни. Как обычно, назовём его 'a и теперь сигнатура выглядит так: Далее применяем второе правило, поскольку в функции указан только один входной параметр времени жизни. Второе правило гласит, что время жизни единственного входного параметра назначается выходным параметрам, поэтому сигнатура теперь преобразуется таким образом: Теперь все ссылки в этой функции имеют параметры времени жизни и компилятор может продолжить свой анализ без необходимости просить у программиста указать аннотации времён жизни в сигнатуре этой функции. Давайте рассмотрим ещё один пример: на этот раз функцию longest , в которой не было параметров времени жизни, когда мы начали с ней работать в листинге 10-20: Применим первое правило: каждому параметру назначается собственное время жизни. На этот раз у функции есть два параметра, поэтому есть два времени жизни: fn first_word (s: & str ) -> & str { fn first_word < 'a >(s: & 'a str ) -> & str { fn first_word < 'a >(s: & 'a str ) -> & 'a str { fn longest (x: & str , y: & str ) -> & str { fn longest < 'a , 'b >(x: & 'a str , y: & 'b str ) -> & str { Можно заметить, что второе правило здесь не применимо, так как в сигнатуре указано больше одного входного параметра времени жизни. Третье правило также не применимо, так как longest — функция, а не метод, следовательно, в ней нет параметра self . Итак, мы прошли все три правила, но так и не смогли вычислить время жизни выходного параметра. Поэтому мы и получили ошибку при попытке скомпилировать код листинга 10-20: компилятор работал по правилам неявного выведения времён жизни, но не мог выяснить все времена жизни ссылок в сигнатуре. Так как третье правило применяется только к методам, далее мы рассмотрим времена жизни в этом контексте, чтобы понять, почему нам часто не требуется аннотировать времена жизни в сигнатурах методов. Аннотация времён жизни в определении методов Когда мы реализуем методы для структур с временами жизни, мы используем тот же синтаксис, который применялся для аннотаций обобщённых типов данных на листинге 10-11. Место, где мы объявляем и используем времена жизни, зависит от того, с чем они связаны — с полями структуры, либо с аргументами методов и возвращаемыми значениями. Имена параметров времени жизни для полей структур всегда описываются после ключевого слова impl и затем используются после имени структуры, поскольку эти времена жизни являются частью типа структуры. В сигнатурах методов внутри блока impl ссылки могут быть привязаны ко времени жизни ссылок в полях структуры, либо могут быть независимыми. Вдобавок, правила неявного выведения времён жизни часто делают так, что аннотации переменных времён жизни являются необязательными в сигнатурах методов. Рассмотрим несколько примеров, использующих структуру с названием ImportantExcerpt , которую мы определили в листинге 10-24. Сначала, воспользуемся методом level , чей единственный параметр является ссылкой на self , а возвращаемое значение i32 , не является ссылкой ни на что: Объявление параметра времени жизни после impl и его использование после имени типа является обязательным, но нам не нужно аннотировать время жизни ссылки на self , благодаря первому правилу неявного выведения времён жизни. Вот пример, где применяется третье правило неявного выведения времён жизни: impl < 'a > ImportantExcerpt< 'a > { fn level (& self ) -> i32 { 3 } } В этом методе имеется два входных параметра, поэтому Rust применит первое правило и назначит обоим параметрам &self и announcement собственные времена жизни. Далее, поскольку один из параметров является &self , то возвращаемое значение получает время жизни переменой &self и все времена жизни теперь выведены. Статическое время жизни Одно особенное время жизни, которое мы должны обсудить, называется 'static . Оно означает, что данная ссылка может жить всю продолжительность работы программы. Все строковые литералы по умолчанию имеют время жизни 'static , но мы можем указать его явным образом: Содержание этой строки сохраняется внутри бинарного файл программы и всегда доступно для использования. Следовательно, время жизни всех строковых литералов равно 'static Сообщения компилятора об ошибках в качестве решения проблемы могут предлагать вам использовать время жизни 'static . Но прежде чем указывать 'static как время жизни для ссылки, подумайте, на самом ли деле данная ссылка будет доступна во всё время работы программы. В большинстве случаев, сообщения об ошибках, предлагающие использовать время жизни 'static появляются при попытках создания недействительных ссылок или несовпадения имеющихся времён жизни. В таких случаях, решение заключается в исправлении таких проблем, а не в указании статического времени жизни 'static Обобщённые типы параметров, ограничения типажей и времена жизни вместе Давайте кратко рассмотрим синтаксис задания параметров обобщённых типов, ограничений типажа и времён жизни совместно в одной функции: impl < 'a > ImportantExcerpt< 'a > { fn announce_and_return_part (& self , announcement: & str ) -> & str { println! ( "Attention please: {}" , announcement); self .part } } let s: & 'static str = "I have a static lifetime." ; Это функция longest из листинга 10-21, которая возвращает наибольший из двух срезов строки. Но теперь у неё есть дополнительный параметр с именем ann обобщённого типа T , который может быть представлен любым типом, реализующим типаж Display , как указано в предложении where . Этот дополнительный параметр будет напечатан с использованием {} , поэтому ограничение типажа Display необходимо. Поскольку время жизни является обобщённым типом, то объявления параметра времени жизни 'a и параметра обобщённого типа T помещаются в один список внутри угловых скобок после имени функции. Итоги В этой главе мы рассмотрели много всего! Теперь вы знакомы с параметрами обобщённого типа, типажами и ограничениями типажа, обобщёнными параметрами времени жизни, вы готовы писать код без повторений, который будет работать во множестве различных ситуаций. Параметры обобщённого типа позволяют использовать код для различных типов данных. Типажи и ограничения типажа помогают убедиться, что, хотя типы и обобщённые, они будут вести себя, как этого требует ваш код. Вы изучили, как использовать аннотации времени жизни чтобы убедиться, что этот универсальный код не будет генерировать никаких повисших ссылок. И весь этот анализ происходит в момент компиляции и не влияет на производительность программы во время работы! Верите или нет, но в рамках этой темы всё есть ещё чему поучиться: в Главе 17 обсуждаются типажи-объекты, которые являются ещё одним способом использования типажей. Существуют также более сложные сценарии с аннотациями времени жизни, которые вам понадобятся только в очень сложных случаях; для этого вам следует прочитать Rust Reference . Далее вы узнаете, как писать тесты на Rust, чтобы убедиться, что ваш код работает так, как задумано. use std::fmt::Display; fn longest_with_an_announcement < 'a , T>( x: & 'a str , y: & 'a str , ann: T, ) -> & 'a str where T: Display, { println! ( "Announcement! {}" , ann); if x.len() > y.len() { x } else { y } } Написание автоматизированных тестов В своём эссе 1972 года “The Humble Programmer,” Edsger W. Dijkstra сказал, что «Тестирование программы может быть очень эффективным способом показать наличие ошибок, но это безнадёжно неадекватно для показа их отсутствия». Это не значит, что мы не должны пытаться тестировать столько, сколько мы можем! Корректностью программы считается то, в какой степени наш код выполняет именно то, что мы задумывали. Rust разработан с учётом большой озабоченности корректностью программ, но корректность сложна и нелегко доказуема. Система типизации Rust берет на себя огромную часть этого бремени, но она не может уловить абсолютно все проблемы. Поэтому в Rust предусмотрена возможность написания автотестов. Допустим, мы пишем функцию add_two , которая прибавляет 2 к любому переданному ей числу. Сигнатура этой функции принимает целое число в качестве параметра и возвращает целое число в качестве результата. Когда мы реализуем и компилируем эту функцию, Rust выполняет всю проверку типов и проверку заимствований, которую вы уже изучили, чтобы убедиться, что, например, мы не передаём значение String или недопустимую ссылку в эту функцию. Но Rust не способен проверить, что эта функция сделает именно то, что мы задумали, то есть вернёт параметр плюс 2, а не, скажем, параметр плюс 10 или параметр минус 50! Вот тут-то и приходят на помощь тесты. Мы можем написать тесты, которые утверждают, например, что когда мы передаём 3 в функцию add_two , возвращаемое значение будет 5 . Мы можем запускать эти тесты всякий раз, когда мы вносим изменения в наш код, чтобы убедиться, что любое существующее правильное поведение не изменилось. Тестирование - сложный навык: мы не сможем охватить все детали написания хороших тестов в одной главе, но мы обсудим основные подходы к тестированию в Rust. Мы поговорим об аннотациях и макросах, доступных вам для написания тестов, о поведении по умолчанию и параметрах, предусмотренных для запуска тестов, а также о том, как организовать тесты в модульные тесты и интеграционные тесты. Как писать тесты Тесты - это функции Rust, которые проверяют, что не тестовый код работает ожидаемым образом. Содержимое тестовых функций обычно выполняет следующие три действия: 1. Установка любых необходимых данных или состояния. 2. Запуск кода, который вы хотите проверить. 3. Утверждение, что результаты являются теми, которые вы ожидаете. Давайте рассмотрим функции предоставляемые в Rust специально для написания тестов, которые выполнят все эти действия, включая атрибут test , несколько макросов и атрибут should_panic Структура тестирующей функции В простейшем случае в Rust тест - это функция, аннотированная атрибутом test Атрибуты представляют собой метаданные о фрагментах кода Rust; один из примеров атрибут derive , который мы использовали со структурами в главе 5. Чтобы изменить функцию в тестирующую функцию добавьте #[test] в строку перед fn . Когда вы запускаете тесты командой cargo test , Rust создаёт бинарный модуль выполняющий функции аннотированные атрибутом test и сообщающий о том, прошла успешно или не прошла каждая тестирующая функция. Когда мы создаём новый проект библиотеки с помощью Cargo, то в нём автоматически генерируется тестовый модуль с тест функцией для нас. Этот модуль поможет вам начать написание ваших тестов, так что вам не нужно искать точную структуру и синтаксис тестовых функций каждый раз, когда вы начинаете новый проект. Вы можете добавить как большее количество дополнительных тестовых функций так и несколько тестовых модулей! Мы исследуем некоторые аспекты работы тестов, экспериментируя с шаблонным тестом сгенерированным для нас, без реального тестирования любого кода. Затем мы напишем некоторые реальные тесты, которые вызывают некоторый написанный код и убедимся в его правильном поведении. Давайте создадим новый проект библиотеки под названием adder : Содержимое файла src/lib.rs вашей библиотеки adder должно выглядеть как в листинге 11-1. Файл: src/lib.rs $ cargo new adder --lib Created library `adder` project $ cd adder Листинг 11-1: Тестовый модуль и функция, сгенерированные автоматически с помощью cargo new Сейчас проигнорируем первые две строчки кода и сосредоточимся на функции, чтобы увидеть как она работает. Обратите внимание на синтаксис аннотации #[test] перед ключевым словом fn . Этот атрибут сообщает компилятору, что это является заголовком тестирующей функции, так что функционал запускающий тесты на выполнение теперь знает, что это тестирующая функция. Также в составе модуля тестов tests могут быть вспомогательные функции, помогающие настроить и выполнить общие подготовительные операции, поэтому специальная аннотация важна для указания объявления функций тестами с использованием атрибута #[test] Тело функции использует макрос assert_eq! , чтобы утверждать, что 2 + 2 равно 4. Это утверждение служит примером формата для типичного теста. Давайте запустим, чтобы увидеть, что этот тест проходит. Команда cargo test выполнит все тесты в выбранном проекте и сообщит о результатах как в листинге 11-2: Листинг 11-2: Вывод информации о работе автоматически сгенерированных тестов Cargo скомпилировал и выполнил тест. После строк Compiling , Finished и Running мы видим строку running 1 test . Следующая строка показывает имя созданной тест функции с названием it_works и результат её выполнения - ok . Далее вы видите #[cfg(test)] mod tests { #[test] fn it_works () { let result = 2 + 2 ; assert_eq! (result, 4 ); } } $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.57s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test test tests::it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s обобщённую информацию о работе всех тестов. Текст test result: ok. означает, что все тесты пройдены успешно и часть вывода 1 passed; 0 failed сообщает общее количество тестов, которые прошли или были ошибочными. Поскольку у нас нет тестов, которые мы пометили как игнорируемые, в сводке отображается 0 ignored . Мы также не отфильтровывали тесты для выполнения, поэтому конец сводки пишет 0 filtered out . Мы поговорим про игнорирование и фильтрацию тестов в следующем разделе "Контролирование хода выполнения тестов" Статистика 0 measured предназначена для тестов производительности. На момент написания этой статьи такие тесты доступны только в ночной сборке Rust. Посмотрите документацию о тестах производительности , чтобы узнать больше. Следующая часть вывода тестов начинается с Doc-tests adder - это информация о тестах в документации. У нас пока нет тестов документации, но Rust может компилировать любые примеры кода, которые находятся в API документации. Такая возможность помогает поддерживать документацию и код в синхронизированном состоянии. Мы поговорим о написании тестов документации в секции "Комментарии документации как тесты" Главы 14. Пока просто проигнорируем часть Doc-tests вывода. Давайте поменяем название нашего теста и посмотрим что же измениться в строке вывода. Назовём нашу функцию it_works другим именем - exploration : Файл: src/lib.rs Снова выполним команду cargo test . Вывод показывает наименование нашей тест функции - exploration вместо it_works : #[cfg(test)] mod tests { #[test] fn exploration () { assert_eq! ( 2 + 2 , 4 ); } } Добавим ещё один тест, но в этот раз специально сделаем так, чтобы этот новый тест не отработал. Тест терпит неудачу, когда что-то паникует в тестируемой функции. Каждый тест запускается в новом потоке и когда главный поток видит, что тестовый поток упал, то помечает тест как завершившийся аварийно. Мы говорили о простейшем способе вызвать панику в главе 9, используя для этого известный макрос panic! . Введём код тест функции another , как в файле src/lib.rs из листинга 11-3. Файл: src/lib.rs |