Файл: src/main.rs
Листинг 12-6: Рефакторинг функции
parse_config
, чтобы возвращать экземпляр структуры
Config
Мы добавили структуру с именем
Config объявленную с полями назваными как query и filename
. Сигнатура parse_config теперь указывает, что она возвращает значение
Config
. В теле parse_config
, где мы возвращали срезы строк, которые ссылаются на значения
String в args
, теперь мы определяем
Config как содержащие собственные
String значения. Переменная args в main является владельцем значений аргумента и позволяют функции parse_config только одалживать их, что означает, что мы бы нарушили правила заимствования Rust, если бы
Config попытался бы взять во владение значения в args
Мы можем управлять данными
String разным количеством способов, но самый простой, хотя и отчасти неэффективный это вызвать метод clone у значений. Он сделает полную копию данных для экземпляра
Config для владения, что занимает больше времени и памяти, чем сохранение ссылки на строку данных. Однако клонирование данных также делает наш код очень простым, потому что нам не нужно управлять временем жизни ссылок; в этом обстоятельстве, отказ от небольшой производительности, чтобы получить простоту, стоит небольшого компромисса.
Компромиссы при использовании метода clone fn main
() { let args:
Vec
<
String
> = env::args().collect(); let config = parse_config(&args); println!
(
"Searching for {}"
, config.query); println!
(
"In file {}"
, config.file_path); let contents = fs::read_to_string(config.file_path)
.expect(
"Should have been able to read the file"
);
// --snip--
} struct
Config
{ query:
String
, file_path:
String
,
} fn parse_config
(args: &[
String
]) -> Config { let query = args[
1
].clone(); let file_path = args[
2
].clone();
Config { query, file_path }
}
Существует тенденция в среде программистов Rust избегать использования clone
,
т.к. это понижает эффективность работы кода. В
Главе 13
, вы изучите более эффективные методы, которые могут подойти в подобной ситуации. Но сейчас можно копировать несколько строк, чтобы продолжить работу, потому что вы сделаете эти копии только один раз, а ваше имя файла и строка запроса будут очень маленькими. Лучше иметь работающую программу, которая немного неэффективна, чем пытаться заранее оптимизировать код при первом написании.
По мере приобретения опыта работы с Rust вам будет проще начать с наиболее эффективного решения, но сейчас вполне приемлемо вызвать clone
Мы обновили код в main поэтому он помещает экземпляр
Config возвращённый из parse_config в переменную с именем config
, и мы обновили код, в котором ранее использовались отдельные переменные query и filename
, так что теперь он использует вместо этого поля в структуре
Config
Теперь наш код более чётко передаёт то, что query и filename связаны и что их назначение - настроить работу программы. Любой код, который использует эти значения знает, что может найти их в именованных полях экземпляра config по их назначению.
1 ... 26 27 28 29 30 31 32 33 ... 62
Создание конструктора для структуры 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
'static str
> { 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
);
}
}