ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 11.01.2024
Просмотров: 1171
Скачиваний: 5
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
Листинг 12-12. Изменение функции
run
для возврата
Result
Здесь мы сделали три значительных изменения. Во-первых, мы изменили тип возвращаемого значения функции run на
Result<(), Box
. Эта функция ранее возвращала тип
()
и мы сохраняли его как значение, возвращаемое в случае
Ok
Для типа ошибки мы использовали объект типаж
Box
(и вверху мы подключили тип std::error::Error в область видимости с помощью оператора use
).
Мы рассмотрим типажи объектов в главе 17
. Сейчас просто знайте, что
Box
означает, что функция будет возвращать тип реализующий типаж
Error
, но не нужно указывать, какой именно будет тип возвращаемого значения. Это даёт возможность возвращать значения ошибок, которые могут быть разных типов в разных случаях.
Ключевое слово dyn сокращение для слова «динамический».
Во-вторых, мы убрали вызов expect в пользу использования оператора
?
, как мы обсудили в главе 9
. Скорее, чем вызывать panic!
в случае ошибки, оператор
?
вернёт значение ошибки из текущей функции для вызывающего, чтобы он её обработал.
В-третьих, функция run теперь возвращает значение
Ok в случае успеха. В сигнатуре функции run объявлен успешный тип как
()
, который означает, что нам нужно обернуть значение единичного типа в значение
Ok
. Данный синтаксис
Ok(())
поначалу может показаться немного странным, но использование
()
выглядит как идиоматический способ указать, что мы вызываем run для его побочных эффектов; он не возвращает значение, которое нам нужно.
Когда вы запустите этот код, он скомпилируется, но отобразит предупреждение:
use std::error::Error;
// --snip-- fn run
(config: Config) ->
Result
<(),
Box
<
dyn
Error>> { let contents = fs::read_to_string(config.file_path)?; println!
(
"With text:\n{contents}"
);
Ok
(())
}
Rust говорит, что наш код проигнорировал
Result значение и значение
Result может указывать на то, что произошла ошибка. Но мы не проверяем, была ли ошибка и компилятор напоминает нам, что мы, вероятно, хотели здесь выполнить некоторый код обработки ошибок! Давайте исправим эту проблему сейчас.
Обработка ошибок, возвращённых из run в main
Мы будем проверять и обрабатывать ошибки используя методику, аналогичную той,
которую мы использовали для
Config::new в листинге 12-10, но с небольшой разницей:
Файл: src/main.rs
$
cargo run the poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep) warning: unused `Result` that must be used
-->
src/main.rs:19:5
|
19 | run(config);
| ^^^^^^^^^^^^
|
= note: `#[warn(unused_must_use)]` on by default
= note: this `Result` may be an `Err` variant, which should be handled warning: `minigrep` (bin "minigrep") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.71s
Running `target/debug/minigrep the poem.txt`
Searching for the
In file poem.txt
With text:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.
How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog! fn main
() {
// --snip-- println!
(
"Searching for {}"
, config.query); println!
(
"In file {}"
, config.file_path); if let
Err
(e) = run(config) { println!
(
"Application error: {e}"
); process::exit(
1
);
}
}
Мы используем if let вместо unwrap_or_else чтобы проверить, возвращает ли run значение
Err и вызывается process::exit(1)
, если это так. Функция run не возвращает значение, которое мы хотим развернуть методом unwrap
, таким же образом как
Config::new возвращает экземпляр
Config
. Так как run возвращает
()
в случае успеха и мы заботимся только об обнаружении ошибки, то нам не нужно вызывать unwrap_or_else
, чтобы вернуть развёрнутое значение, потому что оно будет только
()
Тело функций if let и unwrap_or_else одинаковы в обоих случаях: мы печатаем ошибку и выходим.
Разделение кода на библиотечный крейт
Наш проект minigrep пока выглядит хорошо! Теперь мы разделим файл src/main.rs и поместим некоторый код в файл src/lib.rs, чтобы мы могли его тестировать и чтобы в файле src/main.rs было меньшее количество функциональных обязанностей.
Давайте перенесём весь код не относящийся к функции main из файла src/main.rs в новый файл src/lib.rs:
Определение функции run
Соответствующие инструкции use
Определение структуры
Config
Определение функции
Config::new
Содержимое src/lib.rs должно иметь сигнатуры, показанные в листинге 12-13 (мы опустили тела функций для краткости). Обратите внимание, что код не будет компилироваться пока мы не изменим src/main.rs в листинге 12-14.
Файл: src/lib.rs
Листинг 12-13. Перемещение
Config
и
run
в src/lib.rs
use std::error::Error; use std::fs; pub struct
Config
{ pub query:
String
, pub file_path:
String
,
} impl
Config { pub fn build
(args: &[
String
]) ->
Result
> {
// --snip--
}
} pub fn run
(config: Config) ->
Result
<(),
Box
<
dyn
Error>> {
// --snip--
}
Мы добавили спецификатор доступа pub к структуре
Config
, а также её полям, к методу new и функции run
. Теперь у нас есть API, функционал которого мы сможем протестировать.
Теперь нам нужно подключить код, который мы переместили в src/lib.rs, в область видимости бинарного крейта внутри src/main.rs, как показано в листинге 12-14.
Файл: src/main.rs
Листинг 12-14. Использование крейта библиотеки
minigrep
внутри src/main.rs
Мы добавляем use minigrep::Config для подключения типа
Config из крейта библиотеки в область видимости бинарного крейта и добавляем к имени функции run префикс нашего крейта. Теперь все функции должны быть подключены и должны работать. Запустите программу с cargo run и убедитесь, что все работает правильно.
Уф! Было много работы, но мы настроены на будущий успех. Теперь проще обрабатывать ошибки и мы сделали код более модульным. С этого момента почти вся наша работа будет выполняться внутри src/lib.rs.
Давайте воспользуемся этой новой модульностью, сделав что-то, что было бы трудно со старым кодом, но легко с новым кодом: мы напишем несколько тестов!
use std::env; use std::process; use minigrep::Config; fn main
() {
// --snip-- if let
Err
(e) = minigrep::run(config) {
// --snip--
}
}
1 ... 27 28 29 30 31 32 33 34 ... 62
Развитие функциональности библиотеки разработкой
на основе тестов
Теперь, когда мы извлекли логику в src/lib.rs и оставили разбор аргументов командной строки и обработку ошибок в src/main.rs, стало гораздо проще писать тесты для основной функциональности нашего кода. Мы можем вызывать функции напрямую с различными аргументами и проверить возвращаемые значения без необходимости вызова нашего двоичного файла из командной строки.
В этом разделе в программу minigrep мы добавим логику поиска с использованием процесса разработки через тестирование (TDD). Это техника разработки программного обеспечения следует этим шагам:
1. Напишите тест, который не прошёл и запустите его, чтобы убедиться, что он не прошёл по той причине, которую вы ожидаете.
2. Пишите или изменяйте ровно столько кода, чтобы успешно выполнился новый тест.
3. Модифицируйте код, который вы только что добавили или изменили и убедитесь,
что тесты продолжают проходить.
4. Повторите с шага 1!
Этот процесс является лишь одним из многих способов написания программного обеспечения, но TDD также может помочь дизайну кода. Написание теста перед написанием кода, который делает тестовый прогон помогает поддерживать высокое покрытие тестированием в течение всего процесса.
Мы протестируем реализацию функциональности, которая делает поиск строки запроса в содержимом файла и создание списка строк, соответствующих запросу. Мы добавим эту функциональность в функцию под названием search
Написание теста с ошибкой
Поскольку они нам больше не нужны, давайте удалим строки с println!
, которые мы использовали для проверки поведения программы в src/lib.rs и src/main.rs. Затем в
src/lib.rs мы добавим модуль tests с тестовой функцией, как делали это в главе 11
Тестовая функция определяет поведение, которое мы хотим проверить в функции search
. Она должна принимать запрос и текст для поиска, а возвращать только те строки из текста, которые содержат запрос. В листинге 12-15 показан этот тест, он пока не компилируется.
Файл: src/lib.rs
Листинг 12-15. Создание теста с ошибкой для функции
search
, которую мы хотели бы получить
Этот тест ищет строку "duct"
. Текст, который мы ищем состоит из трёх строк, только одна из которых содержит "duct"
(обратите внимание, что обратная косая черта после открывающей двойной кавычки говорит Rust не помещать символ новой строки в начало содержимого этого строкового литерала). Мы проверяем, что значение,
возвращаемое функцией search
, содержит только ожидаемую нами строку.
Мы не можем запустить этот тест и увидеть сбой, потому что тест даже не компилируется:
функции search ещё не существует! Так что мы добавим код, чтобы тест компилировался и запускался, написав определение функции search
, которая всегда возвращает пустой вектор, как показано в листинге 12-16. Потом тест должен скомпилироваться и потерпеть неудачу при запуске, потому что пустой вектор не равен вектору, содержащему строку "safe, fast, productive."
Файл: src/lib.rs
Листинг 12-16. Определение функции
search
, достаточное, чтобы тест скомпилировался
Заметьте, что в сигнатуре search нужно явно указать время жизни 'a для аргумента contents и возвращаемого значения. Напомним из
Главы 10
, что параметры времени жизни указывают с временем жизни какого аргумента связано время жизни возвращаемого значения. В данном случае мы говорим, что возвращаемый вектор должен содержать срезы строк, ссылающиеся на содержимое аргумента contents
(а не аргумента query
).
Другими словами, мы говорим Rust, что данные, возвращаемые функцией search
, будут жить до тех пор, пока живут данные, переданные в функцию search через аргумент contents
. Это важно! Чтобы ссылки были действительными, данные, на которые
#[cfg(test)]
mod tests { use super::*;
#[test]
fn one_result
() { let query =
"duct"
; let contents =
"\
Rust: safe, fast, productive.
Pick three."
; assert_eq!
(
vec!
[
"safe, fast, productive."
], search(query, contents));
}
} pub fn search
<
'a
>(query: &
str
, contents: &
'a str
) ->
Vec
<&
'a str
> { vec!
[]
}
ссылаются с помощью срезов тоже должны быть действительными; если компилятор предполагает, что мы делаем строковые срезы переменной query
, а не переменной contents
, он неправильно выполнит проверку безопасности.
Если мы забудем аннотации времени жизни и попробуем скомпилировать эту функцию,
то получим следующую ошибку:
Rust не может понять, какой из двух аргументов нам нужен, поэтому нужно сказать ему об этом. Так как contents является тем аргументом, который содержит весь наш текст, и мы хотим вернуть части этого текста, которые совпали при поиске, мы понимаем, что contents является аргументом, который должен быть связан с возвращаемым значением временем жизни.
Другие языки программирования не требуют от вас связывания в сигнатуре аргументов с возвращаемыми значениями. Хотя сейчас это может показаться странным, со временем станет понятнее. Можете сравнить этот пример с разделом
«Валидация ссылок при помощи времён жизни»
главы 10.
Запустим тест:
$
cargo build
Compiling minigrep v0.1.0 (file:///projects/minigrep) error[E0106]: missing lifetime specifier
-->
src/lib.rs:28:51
|
28 | pub fn search(query: &str, contents: &str) -> Vec<&str> {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `query` or `contents` help: consider introducing a named lifetime parameter
|
28 | pub fn search<'a>(query: &'a str, contents: &'a str) -> Vec<&'a str> {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`. error: could not compile `minigrep` due to previous error
, а не переменной contents
, он неправильно выполнит проверку безопасности.
Если мы забудем аннотации времени жизни и попробуем скомпилировать эту функцию,
то получим следующую ошибку:
Rust не может понять, какой из двух аргументов нам нужен, поэтому нужно сказать ему об этом. Так как contents является тем аргументом, который содержит весь наш текст, и мы хотим вернуть части этого текста, которые совпали при поиске, мы понимаем, что contents является аргументом, который должен быть связан с возвращаемым значением временем жизни.
Другие языки программирования не требуют от вас связывания в сигнатуре аргументов с возвращаемыми значениями. Хотя сейчас это может показаться странным, со временем станет понятнее. Можете сравнить этот пример с разделом
«Валидация ссылок при помощи времён жизни»
главы 10.
Запустим тест:
$
cargo build
Compiling minigrep v0.1.0 (file:///projects/minigrep) error[E0106]: missing lifetime specifier
-->
src/lib.rs:28:51
|
28 | pub fn search(query: &str, contents: &str) -> Vec<&str> {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `query` or `contents` help: consider introducing a named lifetime parameter
|
28 | pub fn search<'a>(query: &'a str, contents: &'a str) -> Vec<&'a str> {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`. error: could not compile `minigrep` due to previous error
Отлично. Наш тест не сработал, как мы и ожидали. Давайте сделаем так, чтобы он срабатывал!
Написание кода для прохождения теста
Сейчас наш тест не проходит, потому что мы всегда возвращаем пустой вектор. Чтобы исправить это и реализовать search
, наша программа должна выполнить следующие шаги:
Итерироваться по каждой строке содержимого.
Проверить, содержит ли данная строка искомую.
Если это так, добавить её в список значений, которые мы возвращаем.
Если это не так, ничего не делать.
Вернуть список результатов.
Давайте проработаем каждый шаг, начиная с перебора строк.
Перебор строк с помощью метода lines
В Rust есть полезный метод для построчной итерации строк, удобно названый lines
, как показано в листинге 12-17. Обратите внимание, код пока не компилируется.
Файл: src/lib.rs
$
cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished test [unoptimized + debuginfo] target(s) in 0.97s
Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 1 test test tests::one_result ... FAILED failures:
---- tests::one_result stdout ---- thread 'main' panicked at 'assertion failed: `(left == right)` left: `["safe, fast, productive."]`, right: `[]`', src/lib.rs:44:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::one_result test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass '--lib'