ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 11.01.2024
Просмотров: 1172
Скачиваний: 5
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
Организационная проблема распределения ответственности за выполнение нескольких задач функции main является общей для многих выполняемых проектов. В результате
Rust сообщество разработало процесс для использования в качестве руководства по разделению ответственности бинарной программы, когда код в main начинает увеличиваться. Процесс имеет следующие шаги:
Разделите код программы на два файла main.rs и lib.rs. Перенесите всю логику работы программы в файл lib.rs.
Пока ваша логика синтаксического анализа командной строки мала, она может оставаться в файле main.rs.
Когда логика синтаксического анализа командной строки становится сложной,
извлеките её из main.rs и переместите в lib.rs.
Функциональные обязанности, которые остаются в функции main после этого процесса должно быть ограничено следующим:
Вызов логики разбора командной строки со значениями аргументов
Настройка любой другой конфигурации
Вызов функции run в lib.rs
Обработка ошибки, если run возвращает ошибку
Этот шаблон о разделении ответственности: main.rs занимается запуском программы, а
lib.rs обрабатывает всю логику задачи. Поскольку нельзя проверить функцию main напрямую, то такая структура позволяет проверить всю логику программы путём перемещения её в функции внутри lib.rs. Единственный код, который остаётся в main.rs
будет достаточно маленьким, чтобы проверить его корректность прочитав код. Давайте переработаем нашу программу, следуя этому процессу.
Извлечение парсера аргументов
Мы извлечём функциональность для разбора аргументов в функцию, которую вызовет main для подготовки к перемещению логики разбора командной строки в файл src/lib.rs.
Листинг 12-5 показывает новый запуск main
, который вызывает новую функцию parse_config
, которую мы определим сначала в src/main.rs.
Файл: src/main.rs
Листинг 12-5. Извлечение функции
parse_config
из
main
Мы все ещё собираем аргументы командной строки в вектор, но вместо присваивания значение аргумента с индексом 1 переменной query и значение аргумента с индексом 2
переменной с именем filename в функции main
, мы передаём весь вектор в функцию parse_config
. Функция parse_config затем содержит логику, которая определяет, какой аргумент идёт в какую переменную и передаёт значения обратно в main
. Мы все ещё
создаём переменные query и filename в main
, но main больше не несёт ответственности за определение соответствия аргумента командной строки и соответствующей переменной.
Эта доработка может показаться излишней для нашей маленькой программы, но мы проводим рефакторинг небольшими, постепенными шагами. После внесения этого изменения снова запустите программу и убедитесь, что анализ аргументов все ещё
работает. Также хорошо часто проверять прогресс, чтобы помочь определить причину проблем, когда они возникают.
Группировка конфигурационных переменных
Мы можем сделать ещё один маленький шаг для улучшения функции parse_config
. На данный момент мы возвращаем кортеж, но затем мы немедленно разделяем его снова на отдельные части. Это признак того, что, возможно, пока у нас нет правильной абстракции.
Ещё один индикатор, который показывает, что есть место для улучшения, это часть config из parse_config
, что подразумевает, что два значения, которые мы возвращаем,
связаны друг с другом и оба являются частью одного конфигурационного значения. В
настоящее время мы не отражаем этого смысла в структуре данных, кроме группировки двух значений в кортеж; мы могли бы поместить оба значения в одну структуру и дать каждому из полей структуры понятное имя. Это облегчит будущую поддержку этого кода,
чтобы понять, как различные значения относятся друг к другу и какое их назначение.
В листинге 12-6 показаны улучшения функции parse_config fn main
() { let args:
Vec
<
String
> = env::args().collect(); let
(query, file_path) = parse_config(&args);
// --snip--
} fn parse_config
(args: &[
String
]) -> (&
str
, &
str
) { let query = &args[
1
]; let file_path = &args[
2
];
(query, file_path)
}
Файл: 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 })
}
}
'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--