Мы используем 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
'static str
> {
// --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
Отлично. Наш тест не сработал, как мы и ожидали. Давайте сделаем так, чтобы он срабатывал!
Написание кода для прохождения теста
Сейчас наш тест не проходит, потому что мы всегда возвращаем пустой вектор. Чтобы исправить это и реализовать 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'
Листинг 12-17: Итерация по каждой строке из
contents
Метод lines возвращает итератор. Мы подробно поговорим об итераторах в
Главе 13
,
но вспомните, что вы видели этот способ использования итератора в
Листинге 3-5
, где мы использовали цикл for с итератором, чтобы выполнить некоторый код для каждого элемента в коллекции.
Поиск в каждой строке текста запроса
Далее мы проверяем, содержит ли текущая строка нашу искомую строку. К счастью, у строк есть полезный метод contains
, который именно это и делает! Добавьте вызов метода contains в функции search
, как показано в листинге 12-18. Обратите внимание,
что это все ещё не компилируется.
Файл: src/lib.rs
Листинг 12-18. Добавление проверки, содержится ли
query
в строке
Сохранение совпавшей строки
Нам также нужен способ хранить строки, содержащие искомую строку. Для этого мы можем создать изменяемый вектор перед циклом for и вызывать метод push для сохранения line в векторе. После цикла for мы возвращаем вектор, как показано в листинге 12-19.
Файл: src/lib.rs pub fn search
<
'a
>(query: &
str
, contents: &
'a str
) ->
Vec
<&
'a str
> { for line in contents.lines() {
// do something with line
}
} pub fn search
<
'a
>(query: &
str
, contents: &
'a str
) ->
Vec
<&
'a str
> { for line in contents.lines() { if line.contains(query) {
// do something with line
}
}
}
Листинг 12-19. Сохранение совпадающих строк, чтобы вернуть их
Теперь функция search должна возвратить только строки, содержащие query
, и тест должен пройти. Запустим его:
Наш тест пройден, значит он работает!
На этом этапе мы могли бы рассмотреть возможности изменения реализации функции поиска, сохраняя прохождение тестов и поддерживая имеющуюся функциональность.
Код в функции поиска не так уж плох, но он не использует некоторые полезные функции итераторов. Вернёмся к этому примеру в главе 13
, где будем исследовать итераторы подробно, и посмотрим как его улучшить.
Использование функции search в функции run pub fn search
<
'a
>(query: &
str
, contents: &
'a str
) ->
Vec
<&
'a str
> { let mut results =
Vec
::new(); for line in contents.lines() { if line.contains(query) { results.push(line);
}
} results
}
$
cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished test [unoptimized + debuginfo] target(s) in 1.22s
Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 1 test test tests::one_result ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests minigrep running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s