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

  • Листинг 12-1: Собираем аргументы командной строки в вектор и выводим их на печать

  • Функция

  • Сохранения значений аргументов в переменные

  • Листинг 12-2: Создание переменных для хранения значений аргументов искомой подстроки и пути к файлу

  • Листинг 12-3: Стихотворение Эмили Дикинсон "I’m nobody! Who are you"

  • Листинг 12-4: Чтение содержимого файла указанного во втором аргументе

  • Рефакторинг для улучшения модульности и обработки ошибок

  • Разделение ответственности для бинарных проектов

  • Извлечение парсера аргументов

  • Листинг 12-5. Извлечение функции parse_config из

  • Группировка конфигурационных переменных

  • Листинг 12-6: Рефакторинг функции parse_config, чтобы возвращать экземпляр структуры

  • Компромиссы при использовании метода

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


    Скачать 7.02 Mb.
    НазваниеЯзык программирования Rust
    Дата12.04.2023
    Размер7.02 Mb.
    Формат файлаpdf
    Имя файлаThe Rust Programming Language_ru.pdf
    ТипУчебник
    #1056301
    страница30 из 62
    1   ...   26   27   28   29   30   31   32   33   ...   62
    Чтение значений аргументов
    Чтобы minigrep мог воспринимать значения аргументов командной строки, которые мы ему передаём, нам понадобится функция std::env::args
    , входящая в стандартную библиотеку Rust. Эта функция возвращает итератор аргументов командной строки,
    переданных в minigrep
    . Мы подробно рассмотрим итераторы в главе 13
    . Пока вам достаточно знать две вещи об итераторах: итераторы генерируют серию значений, и мы можем вызвать метод collect у итератора, чтобы создать из него коллекцию, например вектор, который будет содержать все элементы, произведённые итератором.
    Код представленный в Листинге 12-1 позволяет вашей программе minigrep читать любые переданные ей аргументы командной строки, а затем собирать значения в вектор.
    Файл: src/main.rs
    $
    cargo new minigrep
    Created binary (application) `minigrep` project
    $
    cd minigrep
    $
    cargo run -- searchstring example-filename.txt use std::env; fn main
    () { let args:
    Vec
    <
    String
    > = env::args().collect(); dbg!(args);
    }

    Листинг 12-1: Собираем аргументы командной строки в вектор и выводим их на печать
    Сначала мы вводим модуль std::env в область видимости с помощью оператора use
    ,
    чтобы мы могли использовать его функцию args
    . Обратите внимание, что функция std::env::args вложена в два уровня модулей. Как мы обсуждали в главе 7
    , в случаях,
    когда нужная функция оказывается вложенной в более чем один модуль, рекомендуется выносить в область видимости родительский модуль, а не функцию. Таким образом, мы можем легко использовать другие функции из std::env
    . Это менее двусмысленно, чем добавление use std::env::args и последующий вызов функции только с args
    , потому что args может быть легко принят за функцию, определённую в текущем модуле.
    Функция args и недействительный Юникод символ (Unicode)
    Обратите внимание, что std::env::args вызовет панику, если какой-либо аргумент содержит недопустимый символ Юникода. Если вашей программе необходимо принимать аргументы, содержащие недопустимые символы Unicode, используйте вместо этого std::env::args_os
    . Эта функция возвращает итератор, который выдаёт значения
    OsString вместо значений
    String
    . Мы решили использовать std::env::args здесь для простоты, потому что значения
    OsString отличаются для каждой платформы и с ними сложнее работать, чем со значениями
    String
    В первой строке кода функции main мы вызываем env::args и сразу используем метод collect
    , чтобы превратить итератор в вектор содержащий все полученные значения.
    Мы можем использовать функцию collect для создания многих видов коллекций,
    поэтому мы явно аннотируем тип args чтобы указать, что мы хотим вектор строк. Хотя нам очень редко нужно аннотировать типы в Rust, collect
    - это одна из функций, с которой вам часто нужна аннотация типа, потому что Rust не может сам вывести какую коллекцию вы хотите.
    И в заключение мы печатаем вектор с помощью отладочного макроса. Попробуем запустить код сначала без аргументов, а затем с двумя аргументами:
    $
    cargo run
    Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.61s
    Running `target/debug/minigrep`
    [src/main.rs:5] args = [
    "target/debug/minigrep",
    ]

    Обратите внимание, что первое значение в векторе "target/debug/minigrep"
    является названием нашего двоичного файла. Это соответствует поведению списка аргументов в
    Си, позволяя программам использовать название из которой они были вызваны при выполнении. Часто бывает удобно иметь доступ к имени программы, если вы хотите распечатать его в сообщениях или изменить поведение программы в зависимости от того, какой псевдоним командной строки был использован для вызова программы. Но для целей этой главы, мы проигнорируем его и сохраним только два аргумента, которые нам нужны.
    Сохранения значений аргументов в переменные
    На текущий момент программа может получить доступ к значениям, указанным в качестве аргументов командной строки. Теперь нам требуется сохранять значения этих двух аргументов в переменных, чтобы мы могли использовать их в остальных частях программы. Мы сделаем это в листинге 12-2.
    Файл: src/main.rs
    Листинг 12-2: Создание переменных для хранения значений аргументов искомой подстроки и пути к файлу
    Как видно из распечатки вектора, имя программы занимает первое значение в векторе по адресу args[0]
    , значит, аргументы начинаются с индекса
    1
    . Первый аргумент minigrep
    - это строка, которую мы ищем, поэтому мы помещаем ссылку на первый аргумент в переменную query
    . Вторым аргументом является путь к файлу, поэтому мы помещаем ссылку на второй аргумент в переменную file_path
    $
    cargo run -- needle haystack
    Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 1.57s
    Running `target/debug/minigrep needle haystack`
    [src/main.rs:5] args = [
    "target/debug/minigrep",
    "needle",
    "haystack",
    ] use std::env; fn main
    () { let args:
    Vec
    <
    String
    > = env::args().collect(); let query = &args[
    1
    ]; let file_path = &args[
    2
    ]; println!
    (
    "Searching for {}"
    , query); println!
    (
    "In file {}"
    , file_path);
    }

    Для проверки корректности работы нашей программы, значения переменных выводятся в консоль. Далее, запустим нашу программу со следующими аргументами: test и sample.txt
    :
    Отлично, программа работает! Нам нужно чтобы значения аргументов были сохранены в правильных переменных. Позже мы добавим обработку ошибок с некоторыми потенциальными ошибочными ситуациями, например, когда пользователь не предоставляет аргументы; сейчас мы проигнорируем эту ситуацию и поработаем над добавлением возможности чтения файла.
    $
    cargo run -- test sample.txt
    Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
    Running `target/debug/minigrep test sample.txt`
    Searching for test
    In file sample.txt

    Чтение файла
    Теперь добавим возможность чтения файла, указанного как аргумент командной строки filename
    . Во-первых, нам нужен пример файла для тестирования: лучший тип файла для проверки работы minigrep это файл с небольшим количеством текста в несколько строк с несколькими повторяющимися словами. В листинге 12-3 представлено стихотворение
    Эмили Дикинсон, которое будет хорошо работать! Создайте файл с именем poem.txt в корне вашего проекта и введите стихотворение "I’m nobody! Who are you?"
    Файл: poem.txt
    Листинг 12-3: Стихотворение Эмили Дикинсон "I’m nobody! Who are you?"
    Текст на месте, отредактируйте src/main.rs и добавьте код для чтения файла, как показано в листинге 12-4.
    Файл: src/main.rs
    Листинг 12-4: Чтение содержимого файла указанного во втором аргументе
    Во-первых, мы добавляем ещё одно объявление use чтобы подключить соответствующую часть стандартной библиотеки: нам нужен std::fs для обработки файлов.
    В main мы добавили новый оператор: функция fs::read_to_string принимает filename
    , открывает этот файл и возвращает содержимое файла как
    Result
    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! use std::env; use std::fs; fn main
    () {
    // --snip-- println!
    (
    "In file {}"
    , file_path); let contents = fs::read_to_string(file_path)
    .expect(
    "Should have been able to read the file"
    ); println!
    (
    "With text:\n{contents}"
    );
    }

    После этого выражения мы снова добавили временный вывод println!
    для печати значения contents после чтения файла, поэтому мы можем проверить, что программа работает.
    Давайте запустим этот код с любой строкой в качестве первого аргумента командной строки (потому что мы ещё не реализовали поисковую часть) и файл poem.txt как второй аргумент:
    Отлично! Этот код прочитал и затем печатал содержимое файла. Хотя наша программа решает поставленную задачу, она не лишена недостатков. Прежде всего, функция main решает множество задач. Такую функцию неудобно тестировать. Далее, не отслеживаются возможные ошибки ввода данных. Пока наша программа небольшая, то данными недочётами можно пренебречь. При увеличении размеров программы, такую программу будет всё сложнее и сложнее поддерживать. Хорошей практикой программирования является ранний рефакторинг кода по мере усложнения. Поэтому,
    далее мы улучшим наш код с помощью улучшения его структуры.
    $
    cargo run -- the poem.txt
    Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
    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!

    Рефакторинг для улучшения модульности и обработки
    ошибок
    Для улучшения программы мы исправим 4 имеющихся проблемы, связанных со структурой программы и тем как обрабатываются потенциальные ошибки.
    Во-первых, функция main на данный момент решает две задачи - анализирует переменные командной строки и читает файлы. Для небольшой функции это не является проблемой. Тем не менее, при увеличении функционала внутри main
    , количество отдельных обрабатываемых задач в функции main будет расти. Поскольку эта функция получает больше обязанностей, то становится все труднее понимать, труднее тестировать и труднее изменять, не сломав одну из её частей. Лучше всего разделить функциональность, чтобы каждая функция отвечала за одну задачу.
    Эта проблема также связана со второй проблемой: хотя переменные query и filename являются переменными конфигурации нашей программы, переменные типа contents используются для выполнения логики программы. Чем длиннее main становится, тем больше переменных нам нужно будет добавить в область видимости; чем больше у нас переменных, тем сложнее будет отслеживать назначение каждой переменной. Лучше всего сгруппировать переменные конфигурации в одну структуру, чтобы сделать их назначение понятным.
    Третья проблема заключается в том, что мы используем expect для вывода информации об ошибке при проблеме с чтением файла, но сообщение об ошибке просто выведет текст
    Something went wrong reading the file
    . Чтение файла может не сработать по разным причинам, например: файл не найден или у нас может не быть разрешения на его чтение. В существующем коде, независимо от ситуации, мы напечатаем
    Something went wrong reading the file
    , что не даст пользователю никакой информации!
    В четвёртых, мы используем expect неоднократно для обработки различных ошибок и если пользователь запускает нашу программу без указания достаточного количества аргументов он получит ошибку index out of bounds из Rust, что не совсем понятно описывает проблему. Было бы лучше, если бы весь код обработки ошибок был в одном месте. Это позволило бы тем, кто будет поддерживать наш код в дальнейшем, при необходимости изменения логики обработки ошибок, вносить нужные изменения только в одном месте. Наличие всего кода обработки ошибок в одном месте гарантирует,
    что мы напечатаем сообщения, которые будут иметь смысл для наших конечных пользователей.
    Давайте решим эти четыре проблемы путём рефакторинга нашего проекта.
    Разделение ответственности для бинарных проектов

    Организационная проблема распределения ответственности за выполнение нескольких задач функции 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


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