Язык программирования Rust
Скачать 7.02 Mb.
|
Создание конструктора для структуры Config Пока что мы извлекли логику, отвечающую за синтаксический анализ аргументов командной строки из main и поместили его в функцию parse_config . Это помогло нам увидеть, что значения query и filename были связаны и что их отношения должны быть отражены в нашем коде. Затем мы добавили структуру Config в качестве названия связанных общей целью query и filename и чтобы иметь возможность вернуть именованные значения как имена полей структуры из функции parse_config Итак, теперь целью функции parse_config является создание экземпляра Config , мы можем изменить parse_config из простой функции на функцию названную new , которая связана со структурой Config . Выполняя это изменение мы сделаем код более идиоматичным. Можно создавать экземпляры типов в стандартной библиотеке, такие как String с помощью вызова String::new . Точно так же изменив название parse_config на название функции new , связанную с Config , мы будем уметь создавать экземпляры Config , вызывая Config::new . Листинг 12-7 показывает изменения, которые мы должны сделать. Файл: src/main.rs Листинг 12-7. Изменение имени с parse_config на Config::new Мы обновили main где вызывали parse_config , чтобы вместо этого вызывалась Config::new . Мы изменили имя parse_config на new и перенесли его внутрь блока impl , который связывает функцию new с Config . Попробуйте снова скомпилировать код, чтобы убедиться, что он работает. Исправление ошибок обработки Теперь мы поработаем над исправлением обработки ошибок. Напомним, что попытки получить доступ к значениям в векторе args с индексом 1 или индексом 2 приведут к панике, если вектор содержит менее трёх элементов. Попробуйте запустить программу без каких-либо аргументов; это будет выглядеть так: Строка index out of bounds: the len is 1 but the index is 1 является сообщением об ошибке предназначенной для программистов. Она не поможет нашим конечным пользователям понять, что случилось и что они должны сделать вместо этого. Давайте исправим это сейчас. Улучшение сообщения об ошибке fn main () { let args: Vec < String > = env::args().collect(); let config = Config::new(&args); // --snip-- } // --snip-- impl Config { fn new (args: &[ String ]) -> Config { let query = args[ 1 ].clone(); let file_path = args[ 2 ].clone(); Config { query, file_path } } } $ cargo run Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep` thread 'main' panicked at 'index out of bounds: the len is 1 but the index is 1', src/main.rs:27:21 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace В листинге 12-8 мы добавляем проверку в функцию new , которая будет проверять, что срез достаточно длинный, перед попыткой доступа по индексам 1 и 2. Если срез не достаточно длинный, программа паникует и отображает улучшенное сообщение об ошибке, чем сообщение index out of bounds Файл: src/main.rs Листинг 12-8. Добавление проверки на число аргументы Этот код похож на функцию Guess::new написанную в листинге 9-13 , где мы вызывали panic! , когда value аргумента вышло за пределы допустимых значений. Здесь вместо проверки на диапазон значений, мы проверяем, что длина args не менее 3 и остальная часть функции может работать при условии, что это условие было выполнено. Если в args меньше трёх элементов, это условие будет истинным и мы вызываем макрос panic! для немедленного завершения программы. Имея нескольких лишних строк кода в new , давайте запустим программу снова без аргументов, чтобы увидеть, как выглядит ошибка: Этот вывод лучше: у нас теперь есть разумное сообщение об ошибке. Тем не менее, мы также имеем постороннюю информацию, которую мы не хотим предоставлять нашим пользователям. Возможно, использованная техника, которую мы использовали в листинге 9-13, не является лучшей для использования: вызов panic! больше подходит для программирования проблемы, чем решения проблемы, как обсуждалось в главе 9 Вместо этого мы можем использовать другую технику, о которой вы узнали в главе 9 возвращая Result , которая указывает либо на успех, либо на ошибку. Возвращение Result из new вместо вызова panic! Мы можем вернуть значение Result , которое будет содержать экземпляр Config в успешном случае и опишет проблему в случае ошибки. Когда Config::new взаимодействует с main , мы можем использовать тип Result как сигнал возникновения проблемы. Затем мы можем изменить main , чтобы преобразовать вариант Err в более // --snip-- fn new (args: &[ String ]) -> Config { if args.len() < 3 { panic! ( "not enough arguments" ); } // --snip-- $ cargo run Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep` thread 'main' panicked at 'not enough arguments', src/main.rs:26:13 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace практичную ошибку для наших пользователей без окружающего текста вроде thread 'main' и RUST_BACKTRACE , что происходит при вызове panic! Листинг 12-9 показывает изменения, которые нужно внести для возвращения значения из Config::new и в тело функции, необходимые для возврата типа Result . Заметьте, что этот код не скомпилируется, пока мы не обновим main , что мы и сделаем в следующем листинге. Файл: src/main.rs Листинг 12-9. Возвращение типа Result из Config::new Наша функция new теперь возвращает Result с экземпляром Config в случае успеха и &'static str в случае ошибки. Значения ошибок всегда будут строковыми литералами, которые имеют время жизни 'static Мы внесли два изменения в тело функции new : вместо вызова panic! , когда пользователь не передаёт достаточно аргументов, мы теперь возвращаем значение Err и мы завернули возвращаемое значение Config в Ok . Эти изменения заставят функцию соответствовать своей новой сигнатуре типа. Возвращение значения Err из Config::new позволяет функции main обработать значение Result возвращённое из функции new и выйти из процесса более чисто в случае ошибки. Вызов Config::new и обработка ошибок Чтобы обработать ошибку и вывести более дружественное сообщение об ошибке, нам нужно обновить код main для обработки Result , возвращаемого из Config::new как показано в листинге 12-10. Мы также возьмём на себя ответственность за выход из программы командной строки с ненулевым кодом ошибки panic! и реализуем это вручную. Не нулевой статус выхода - это соглашение, которое сигнализирует процессу, который вызывает нашу программу, что программа завершилась с ошибкой. Файл: src/main.rs impl Config { fn build (args: &[ String ]) -> Result > { if args.len() < 3 { return Err ( "not enough arguments" ); } let query = args[ 1 ].clone(); let file_path = args[ 2 ].clone(); Ok (Config { query, file_path }) } } Листинг 12-10. Выход с кодом ошибки если создание новой Config терпит неудачу В этом листинге мы использовали метод, который мы ещё не рассматривали детально: unwrap_or_else , который в стандартной библиотеке определён как Result Использование unwrap_or_else позволяет нам определить некоторые пользовательские ошибка обработки, не содержащие panic! . Если Result является значением Ok , поведение этого метода аналогично unwrap : возвращает внутреннее значение из обёртки Ok . Однако, если значение значение Err , то этот метод вызывает код замыкания, которое является анонимной функцией определённую заранее и передаваемую в качестве аргумента в unwrap_or_else . Мы рассмотрим замыкания более подробно в главе 13 . В данный момент, вам просто нужно знать, что unwrap_or_else передаст внутреннее значение Err , которое в этом случае является статической строкой not enough arguments , которое мы добавили в листинге 12-9, в наше замыкание как аргумент err указанное между вертикальными линиями. Код в замыкании может затем использовать значение err при выполнении. Мы добавили новую строку use , чтобы подключить process из стандартной библиотеки в область видимости. Код в замыкании, который будет запущен в случае ошибки содержит только две строчки: мы печатаем значение err и затем вызываем process::exit . Функция process::exit немедленно остановит программу и вернёт номер, который был передан в качестве кода состояния выхода. Это похоже на обработку с помощью макроса panic! , которую мы использовали в листинге 12-8, но мы больше не получаем весь дополнительный вывод. Давай попробуем: Замечательно! Этот вывод намного дружелюбнее для наших пользователей. Извлечение логики из main use std::process; fn main () { let args: Vec < String > = env::args().collect(); let config = Config::build(&args).unwrap_or_else(|err| { println! ( "Problem parsing arguments: {err}" ); process::exit( 1 ); }); // --snip-- $ cargo run Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.48s Running `target/debug/minigrep` Problem parsing arguments: not enough arguments Теперь, когда мы закончили рефакторинг разбора конфигурации, давайте обратимся к логике программы. Как мы указали в разделе «Разделение ответственности в бинарных проектах» , мы извлечём функцию с именем run , которая будет содержать всю логику, присутствующую в настоящее время в функции main и которая не связана с настройкой конфигурации или обработкой ошибок. Когда мы закончим, то main будет краткой, легко проверяемой и мы сможем написать тесты для всей остальной логики. Код 12-11 демонстрирует извлечённую логику в функцию run . Мы делаем маленькое, инкрементальное приближение к извлечению функции. Код всё ещё сосредоточен в файле src/main.rs: Файл: src/main.rs Листинг 12-11. Извлечение функции run , содержащей остальную логику программы Функция run теперь содержит всю оставшуюся логику из main , начиная от чтения файла. Функция run принимает экземпляр Config как аргумент. Возврат ошибок из функции run Оставшаяся логика программы выделена в функцию run , где мы можем улучшить обработку ошибок как мы уже делали с Config::new в листинге 12-9. Вместо того, чтобы позволить программе паниковать с помощью вызова expect , функция run вернёт Result , если что-то пойдёт не так. Это позволит далее консолидировать логику обработки ошибок в main удобным способом. Листинг 12-12 показывает изменения, которые мы должны внести в сигнатуру и тело run Файл: src/main.rs fn main () { // --snip-- println! ( "Searching for {}" , config.query); println! ( "In file {}" , config.file_path); run(config); } fn run (config: Config) { let contents = fs::read_to_string(config.file_path) .expect( "Should have been able to read the file" ); println! ( "With text:\n{contents}" ); } // --snip-- Листинг 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-- } } |