Главная страница
Навигация по странице:

  • Листинг 12-7. Изменение имени с parse_config на

  • Исправление ошибок обработки

  • Улучшение сообщения об ошибке

  • Листинг 12-8. Добавление проверки на число аргументы

  • Возвращение

  • Листинг 12-9. Возвращение типа Result из

  • Вызов

  • Листинг 12-10. Выход с кодом ошибки если создание новой Config терпит неудачу

  • Листинг 12-11. Извлечение функции run, содержащей остальную логику программы

  • Возврат ошибок из функции

  • Листинг 12-12. Изменение функции run для возврата

  • Обработка ошибок, возвращённых из run в

  • Листинг 12-13. Перемещение Config и run в src/lib.rs

  • Листинг 12-14. Использование крейта библиотеки minigrep внутри src/main.rs

  • Язык программирования Rust


    Скачать 7.02 Mb.
    НазваниеЯзык программирования Rust
    Дата12.04.2023
    Размер7.02 Mb.
    Формат файлаpdf
    Имя файлаThe Rust Programming Language_ru.pdf
    ТипУчебник
    #1056301
    страница31 из 62
    1   ...   27   28   29   30   31   32   33   34   ...   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
    );
    }
    }

    Мы используем 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


    написать администратору сайта