ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 11.01.2024
Просмотров: 1173
Скачиваний: 5
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
Листинг 12-17: Итерация по каждой строке из
contents
Метод lines возвращает итератор. Мы подробно поговорим об итераторах в
Главе 13
,
но вспомните, что вы видели этот способ использования итератора в
Листинге 3-5
, где мы использовали цикл for с итератором, чтобы выполнить некоторый код для каждого элемента в коллекции.
Поиск в каждой строке текста запроса
Далее мы проверяем, содержит ли текущая строка нашу искомую строку. К счастью, у строк есть полезный метод contains
, который именно это и делает! Добавьте вызов метода contains в функции search
, как показано в листинге 12-18. Обратите внимание,
что это все ещё не компилируется.
Файл: src/lib.rs
Листинг 12-18. Добавление проверки, содержится ли
query
в строке
Сохранение совпавшей строки
Нам также нужен способ хранить строки, содержащие искомую строку. Для этого мы можем создать изменяемый вектор перед циклом for и вызывать метод push для сохранения line в векторе. После цикла for мы возвращаем вектор, как показано в листинге 12-19.
Файл: src/lib.rs pub fn search
<
'a
>(query: &
str
, contents: &
'a str
) ->
Vec
<&
'a str
> { for line in contents.lines() {
// do something with line
}
} pub fn search
<
'a
>(query: &
str
, contents: &
'a str
) ->
Vec
<&
'a str
> { for line in contents.lines() { if line.contains(query) {
// do something with line
}
}
}
Листинг 12-19. Сохранение совпадающих строк, чтобы вернуть их
Теперь функция search должна возвратить только строки, содержащие query
, и тест должен пройти. Запустим его:
Наш тест пройден, значит он работает!
На этом этапе мы могли бы рассмотреть возможности изменения реализации функции поиска, сохраняя прохождение тестов и поддерживая имеющуюся функциональность.
Код в функции поиска не так уж плох, но он не использует некоторые полезные функции итераторов. Вернёмся к этому примеру в главе 13
, где будем исследовать итераторы подробно, и посмотрим как его улучшить.
Использование функции search в функции run pub fn search
<
'a
>(query: &
str
, contents: &
'a str
) ->
Vec
<&
'a str
> { let mut results =
Vec
::new(); for line in contents.lines() { if line.contains(query) { results.push(line);
}
} results
}
$
cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished test [unoptimized + debuginfo] target(s) in 1.22s
Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 1 test test tests::one_result ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests minigrep running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Теперь, когда функция search работает и протестирована, нужно вызвать search из нашей функции run
. Нам нужно передать значение config.query и contents
, которые run читает из файла, в функцию search
. Тогда run напечатает каждую строку,
возвращаемую из search
:
Файл: src/lib.rs
Мы по-прежнему используем цикл for для возврата каждой строки из функции search и
её печати.
Теперь вся программа должна работать! Давайте попробуем сначала запустить её со словом «frog», которое должно вернуть только одну строчку из стихотворения Эмили
Дикинсон:
Здорово! Теперь давайте попробуем слово, которое будет соответствовать нескольким строкам, например «body»:
И наконец, давайте удостоверимся, что мы не получаем никаких строк, когда ищем слово, отсутствующее в стихотворении, например «monomorphization»:
Отлично! Мы создали собственную мини-версию классического инструмента и научились тому, как структурировать приложения. Мы также немного узнали о файловом вводе и pub fn run
(config: Config) ->
Result
<(),
Box
<
dyn
Error>> { let contents = fs::read_to_string(config.file_path)?; for line in search(&config.query, &contents) { println!
(
"{line}"
);
}
Ok
(())
}
$
cargo run -- frog poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.38s
Running `target/debug/minigrep frog poem.txt`
How public, like a frog
$
cargo run -- body poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep body poem.txt`
I'm nobody! Who are you?
Are you nobody, too?
How dreary to be somebody!
$
cargo run -- monomorphization poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep monomorphization poem.txt`
выводе, временах жизни, тестировании и разборе аргументов командной строки.
Чтобы завершить этот проект, мы кратко продемонстрируем пару вещей: как работать с переменными окружения и как печатать в стандартный поток ошибок, обе из которых полезны при написании консольных программ.
Чтобы завершить этот проект, мы кратко продемонстрируем пару вещей: как работать с переменными окружения и как печатать в стандартный поток ошибок, обе из которых полезны при написании консольных программ.
Работа с переменными окружения
Мы улучшим minigrep
, добавив дополнительную функцию: опцию для поиск без учёта регистра, которую пользователь может включить с помощью переменной среды окружения. Мы могли бы сделать эту функцию параметром командной строки и потребовать, чтобы пользователи вводили бы её каждый раз при её применении, но вместо этого мы будем использовать переменную среды окружения. Это позволит нашим пользователям устанавливать переменную среды один раз и все поиски будут не чувствительны к регистру в этом терминальном сеансе.
Написание ошибочного теста для функции search с учётом регистра
Мы хотим добавить новую функцию search_case_insensitive
, которую мы будем вызывать, когда переменная окружения включена. Мы продолжим следовать процессу
TDD, поэтому первый шаг - это снова написать не проходящий тест. Мы добавим новый тест для новой функции search_case_insensitive и переименуем наш старый тест из one_result в case_sensitive
, чтобы прояснить различия между двумя тестами, как показано в листинге 12-20.
Файл: src/lib.rs
Листинг 12-20. Добавление нового не проходящего теста для функции поиска нечувствительной к регистру,
которую мы собираемся добавить
Обратите внимание, что мы также отредактировали содержимое переменной contents из старого теста. Мы добавили новую строку с текстом "Duct tape."
, используя заглавную D, которая не должна соответствовать запросу "duct"
при поиске с учётом регистра. Такое изменение старого теста помогает избежать случайного нарушения функциональности поиска чувствительного к регистру, который мы уже реализовали.
Этот тест должен пройти сейчас и должен продолжать выполняться успешно, пока мы работаем над поиском без учёта регистра.
Новый тест для поиска нечувствительного к регистру использует "rUsT"
качестве строки запроса. В функции search_case_insensitive
, которую мы собираемся реализовать,
запрос "rUsT"
должен соответствовать строке содержащей "Rust:"
с большой буквы R
и соответствовать строке "Trust me."
, хотя обе имеют разные регистры из запроса. Это наш не проходящий тест, он не компилируется, потому что мы ещё не определили функцию search_case_insensitive
. Не стесняйтесь добавлять скелет реализация,
которая всегда возвращает пустой вектор, аналогично тому, как мы это делали для функции search в листинге 12-16, чтобы увидеть компиляцию теста и его сбой.
#[cfg(test)]
mod tests { use super::*;
#[test]
fn case_sensitive
() { let query =
"duct"
; let contents =
"\
Rust: safe, fast, productive.
Pick three.
Duct tape."
; assert_eq!
(
vec!
[
"safe, fast, productive."
], search(query, contents));
}
#[test]
fn case_insensitive
() { let query =
"rUsT"
; let contents =
"\
Rust: safe, fast, productive.
Pick three.
Trust me."
; assert_eq!
( vec!
[
"Rust:"
,
"Trust me."
], search_case_insensitive(query, contents)
);
}
}
Реализация функции search_case_insensitive
Функция search_case_insensitive
, показанная в листинге 12-21, будет почти такая же,
как функция search
. Разница лишь в том, что текст будет в нижнем регистре для query и
для каждой line
, так что для любого регистра входных аргументов это будет тот же случай, когда мы проверяем, содержит ли строка запрос.
Файл: src/lib.rs
Листинг 12-21. Определение функции
search_case_insensitive
с уменьшением регистра строки запроса и
строки содержимого перед их сравнением
Сначала преобразуем в нижний регистр строку query и сохраняем её в затенённой переменной с тем же именем. Вызов to_lowercase для строки запроса необходим, так что независимо от того, будет ли пользовательский запрос "rust"
,
"RUST"
,
"Rust"
или "rUsT"
, мы будем преобразовывать запрос к "rust"
и делать значение нечувствительным к регистру. Хотя to_lowercase будет обрабатывать Unicode, он не будет точным на 100%. Если бы мы писали реальное приложение, мы бы хотели проделать здесь немного больше работы, но этот раздел посвящён переменным среды, а не Unicode, поэтому мы оставим это здесь.
Обратите внимание, что query теперь имеет тип
String
, а не срез строки, потому что вызов to_lowercase создаёт новые данные, а не ссылается на существующие. К примеру,
запрос:
"rUsT"
это срез строки не содержащий строчных букв u
или t
, которые мы можем использовать, поэтому мы должны выделить новую
String
, содержащую
«rust»
Когда мы передаём запрос query в качестве аргумента метода contains
, нам нужно добавить амперсанд, поскольку сигнатура contains
, определена для приёмы среза строки.
Затем мы добавляем вызов to_lowercase у каждой строки line
, прежде чем проверять,
содержит ли она query из всех строчных символов. Теперь, когда мы преобразовали line и query в нижний регистр, мы найдём совпадения независимо от того, в каком регистре находится переменная с запросом.
pub fn search_case_insensitive
<
'a
>( query: &
str
, contents: &
'a str
,
) ->
Vec
<&
'a str
> { let query = query.to_lowercase(); let mut results =
Vec
::new(); for line in contents.lines() { if line.to_lowercase().contains(&query) { results.push(line);
}
} results
}
Давайте посмотрим, проходит ли эта реализация тесты:
Отлично! Тесты прошли. Теперь давайте вызовем новую функцию search_case_insensitive из функции run
. Во-первых, мы добавим параметр конфигурации в структуру
Config для переключения между поиском с учётом регистра и без учёта регистра. Добавление этого поля приведёт к ошибкам компилятора, потому что мы ещё нигде не инициализируем это поле:
Файл: src/lib.rs
Обратите внимание, что мы добавили поле case_sensitive
, которое содержит логическое значение. Далее нам нужна функция run
, чтобы проверить значение поля case_sensitive и использовать его, чтобы решить, вызывать ли функцию search или функцию search_case_insensitive
, как показано в листинге 12-22. Обратите внимание,
что код все ещё не компилируется.
Файл: src/lib.rs
$
cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished test [unoptimized + debuginfo] target(s) in 1.33s
Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 2 tests test tests::case_insensitive ... ok test tests::case_sensitive ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests minigrep running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s pub struct
Config
{ pub query:
String
, pub file_path:
String
, pub ignore_case: bool
,
}
1 ... 28 29 30 31 32 33 34 35 ... 62
Листинг 12-22. Вызов либо
search
, либо
search_case_insensitive
на основе значения в
config.case_sensitive
Наконец, нам нужно проверить переменную среды. Функции для работы с переменными среды находятся в модуле env стандартной библиотеки, поэтому мы хотим подключить этот модуль в область видимости с помощью строки use std::env;
в верхней части
src/lib.rs. Затем мы будем использовать функцию var из модуля env для проверки переменной среды с именем
CASE_INSENSITIVE
, как показано в листинге 12-23.
Файл: src/lib.rs
Листинг 12-23. Проверка переменной среды с именем
CASE_INSENSITIVE
Здесь мы создаём новую переменную case_sensitive
. Чтобы установить её значение,
мы вызываем функцию env::var и передаём ей имя переменной окружения pub fn run
(config: Config) ->
Result
<(),
Box
<
dyn
Error>> { let contents = fs::read_to_string(config.file_path)?; let results = if config.ignore_case { search_case_insensitive(&config.query, &contents)
} else
{ search(&config.query, &contents)
}; for line in results { println!
(
"{line}"
);
}
Ok
(())
} use std::env;
// --snip-- impl
Config { pub 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(); let ignore_case = env::var(
"IGNORE_CASE"
).is_ok();
Ok
(Config { query, file_path, ignore_case,
})
}
}
CASE_INSENSITIVE
. Функция env::var возвращает
Result
, который будет успешным вариантом
Ok содержащий значение переменной среды, если переменная среды установлена. Он вернёт вариант
Err
, если переменная окружения не установлена.
Мы используем метод is_err у
Result
, чтобы проверить возвращается ли ошибка и следовательно, переменная среды не установлена, что означает, что должен
выполняться чувствительный к регистру поиск. Если для переменной среды
CASE_INSENSITIVE
что-либо задано, то is_err вернёт значение false и программа выполнит поиск без учёта регистра. Мы не заботимся о значении переменной среды, нас интересует только установлена она или нет, поэтому мы проверяем is_err
, а не используем unwrap
, expect или любой другой метод, который мы видели у
Result
Мы передаём значение переменной case_sensitive экземпляру
Config
, чтобы функция run могла прочитать это значение и решить, следует ли вызывать search или search_case_insensitive
, как мы реализовали в листинге 12-22.
Давайте попробуем! Во-первых, мы запустим нашу программу без установленной переменной среды и с помощью значения запроса to
, который должен соответствовать любой строке, содержащей слово «to» в нижнем регистре:
Похоже, все ещё работает! Теперь давайте запустим программу с
CASE_INSENSITIVE
,
установленным в
1
, но с тем же значением запроса to
Если вы используете PowerShell, вам нужно установить переменную среды и запустить программу двумя командами, а не одной:
Это заставит переменную окружения
CASE_INSENSITIVE
сохраниться до конца сеанса работы консоли. Переменную можно отключить с помощью команды
Remove-Item
:
Мы должны получить строки, содержащие «to», которые могут иметь заглавные буквы:
$
cargo run -- to poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep to poem.txt`
Are you nobody, too?
How dreary to be somebody!
PS>
$Env
:CASE_INSENSITIVE=1; cargo run to poem.txt
PS>
Remove-Item Env:CASE_INSENSITIVE
Отлично, мы также получили строки, содержащие «To»! Наша программа minigrep теперь может выполнять поиск без учёта регистра, управляемая переменной среды.
Теперь вы знаете, как управлять параметрами, заданными с помощью аргументов командной строки или переменных среды.
Некоторые программы допускают использование аргументов и переменных среды для одной и той же конфигурации. В таких случаях программы решают, что из них имеет больший приоритет. Для другого самостоятельного упражнения попробуйте управлять нечувствительностью к регистру с помощью аргумента командной строки или переменной окружения. Решите, должен ли приоритет иметь аргумент командной строки или переменная среды, если программа запускается с установленным параметром для нечувствительного к регистру поиска и установленного для поиска с учётом регистра.
Модуль std::env содержит много других полезных функций для работы с переменными среды: ознакомьтесь с его документацией, чтобы узнать доступные.
$
CASE_INSENSITIVE=1 cargo run to poem.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep to poem.txt`
Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!
Запись сообщений ошибок в поток ошибок вместо
стандартного потока вывода
В данный момент мы записываем весь наш вывод в терминал, используя функцию println!
. В большинстве терминалов предоставлено два вида вывода: стандартный
поток вывода ( stdout
) для общей информации и стандартный поток ошибок ( stderr
)
для сообщений об ошибках. Это различие позволяет пользователям выбирать,
направлять ли успешный вывод программы в файл, но при этом выводить сообщения об ошибках на экран.
Функция println!
может печатать только в стандартный вывод, поэтому мы должны использовать что-то ещё для печати в стандартный поток ошибок.
Проверка, куда записываются ошибки
Во-первых, давайте посмотрим, как содержимое, напечатанное из minigrep в настоящее время записывается в стандартный вывод, включая любые сообщения об ошибках,
которые мы хотим вместо этого записать в стандартный поток ошибок. Мы сделаем это,
перенаправив стандартный поток вывода в файл, а также намеренно вызовем ошибку.
Мы не будем перенаправлять стандартный поток ошибок, поэтому любой контент,
отправленный в поток стандартных ошибок будет продолжать отображаться на экране.
Ожидается, что программы командной строки будут отправлять сообщения об ошибках в стандартный поток ошибок, поэтому мы все равно можем видеть сообщения об ошибках на экране, даже если мы перенаправляем стандартный поток вывода в файл. Наша программа в настоящее время не ведёт себя правильно: мы увидим, что она сохраняет вывод сообщения об ошибке в файл!
Способ продемонстрировать это поведение - запустите программу с
>
и именем файла
output.txt в который мы хотим перенаправить стандартный поток вывода. Мы не передадим никаких аргументов, что должно вызвать внутри ошибку:
Синтаксис
>
указывает оболочке записывать содержимое стандартного вывода в
output.txt вместо экрана. Мы не увидели сообщение об ошибке, которое мы ожидали увидеть на экране, так что это означает, что оно должно быть в файле. Вот что содержит
output.txt:
Да, наше сообщение об ошибке выводится в стандартный вывод. Гораздо более полезнее, чтобы подобные сообщения об ошибках печатались в стандартной поток
$
cargo run > output.txt
Problem parsing arguments: not enough arguments
ошибок, поэтому в файл попадают только данные из успешного запуска. Мы поменяем это.
Печать ошибок в поток ошибок
Мы будем использовать код в листинге 12-24, чтобы изменить способ вывода сообщений об ошибках. Из-за рефакторинга, который мы делали ранее в этой главе, весь код,
который печатает сообщения об ошибках, находится в одной функции: main
Стандартная библиотека предоставляет макрос eprintln!
который печатает в стандартный поток ошибок, поэтому давайте изменим два места, где мы вызывали println!
для печати ошибок, чтобы использовать eprintln!
вместо этого.
Файл: src/main.rs
Листинг 12-24. Запись сообщений об ошибках в стандартный поток ошибок вместо потока стандартного
вывода используя макрос
eprintln!
После изменения println!
на eprintln!
, давайте снова запустим программу без каких- либо аргументов и перенаправим стандартный вывод с помощью
>
:
Теперь мы видим ошибку на экране и output.txt не содержит ничего, что мы ожидаем от программы командной строки.
Давайте снова запустим программу с аргументами, которые не вызывают ошибку, но все же перенаправляют стандартный вывод в файл, например так:
Мы не увидим никакого вывода в терминал, а output.txt будет содержать наши результаты:
fn main
() { let args:
Vec
<
String
> = env::args().collect(); let config = Config::build(&args).unwrap_or_else(|err| { eprintln!(
"Problem parsing arguments: {err}"
); process::exit(
1
);
}); if let
Err
(e) = minigrep::run(config) { eprintln!(
"Application error: {e}"
); process::exit(
1
);
}
}
$
cargo run > output.txt
Problem parsing arguments: not enough arguments
$
cargo run to poem.txt > output.txt
Печать ошибок в поток ошибок
Мы будем использовать код в листинге 12-24, чтобы изменить способ вывода сообщений об ошибках. Из-за рефакторинга, который мы делали ранее в этой главе, весь код,
который печатает сообщения об ошибках, находится в одной функции: main
Стандартная библиотека предоставляет макрос eprintln!
который печатает в стандартный поток ошибок, поэтому давайте изменим два места, где мы вызывали println!
для печати ошибок, чтобы использовать eprintln!
вместо этого.
Файл: src/main.rs
Листинг 12-24. Запись сообщений об ошибках в стандартный поток ошибок вместо потока стандартного
вывода используя макрос
eprintln!
После изменения println!
на eprintln!
, давайте снова запустим программу без каких- либо аргументов и перенаправим стандартный вывод с помощью
>
:
Теперь мы видим ошибку на экране и output.txt не содержит ничего, что мы ожидаем от программы командной строки.
Давайте снова запустим программу с аргументами, которые не вызывают ошибку, но все же перенаправляют стандартный вывод в файл, например так:
Мы не увидим никакого вывода в терминал, а output.txt будет содержать наши результаты:
fn main
() { let args:
Vec
<
String
> = env::args().collect(); let config = Config::build(&args).unwrap_or_else(|err| { eprintln!(
"Problem parsing arguments: {err}"
); process::exit(
1
);
}); if let
Err
(e) = minigrep::run(config) { eprintln!(
"Application error: {e}"
); process::exit(
1
);
}
}
$
cargo run > output.txt
Problem parsing arguments: not enough arguments
$
cargo run to poem.txt > output.txt
Файл: output.txt
Это демонстрирует, что в зависимости от ситуации мы теперь используем стандартный поток вывода для успешного текста и стандартный поток ошибок для вывода ошибок.
Итоги
В этой главе были повторены некоторые основные концепции, которые вы изучили до сих пор и было рассказано, как выполнять обычные операции ввода-вывода в Rust.
Используя аргументы командной строки, файлы, переменные среды и макрос eprintln!
для печати ошибок и вы теперь готовы писать приложения командной строки. Благодаря использованию концепций из предыдущих главах ваш код будет хорошо организован,
будет эффективно хранить данные в соответствующих структурах, хорошо обрабатывать ошибки и хорошо тестироваться.
Далее мы рассмотрим некоторые возможности Rust, на которые повлияли функциональные языки: замыкания и итераторы.
Are you nobody, too?
How dreary to be somebody!
Функциональные возможности языка:
итераторы и замыкания
Дизайн языка Rust был вдохновлён многими существующими языками и техниками, и существенное влияние на него оказало функциональное программирование.
Программирование в функциональном стиле часто включает использование функций в качестве значений, передаваемых в аргументах, возвращаемых из других функций,
назначаемых переменным для последующего выполнения и так далее.
В этой главе мы не будем обсуждать вопрос о том, чем является или не является функциональное программирование. Вместо этого мы обсудим некоторые функции Rust,
которые похожи на функции во многих других языках, которые часто называются функциональными.
Более подробно мы поговорим про:
Замыкания, функциональную конструкцию, которую вы можете хранить в переменной,
Итераторы — способ обработки последовательности элементов,
То, как, используя замыкания и итераторы, улучшить работу с операциями ввода- вывода в проекте из главы 12
Производительность замыканий и итераторов (спойлер: они быстрее, чем вы думаете!)
Мы уже рассмотрели другие возможности Rust, такие как сопоставление с образцом и перечисления, на которые также влияет функциональный стиль. Поскольку освоение замыканий и итераторов — важная часть написания идиоматичного, быстрого кода на
Rust, мы посвятим им всю эту главу.
Закрытия: Анонимные функции, подхватывающие
своё окружение
Замыкания в Rust - это анонимные функции, которые можно сохранять в переменных или передавать в качестве аргументов другим функциям. Вы можете создать замыкание в одном месте, а затем вызвать его в каком-нибудь другом, чтобы выполнить обработку в другом контексте. В отличие от функций, замыкания могут использовать значения из области видимости, в которой они были определены. Мы продемонстрируем, как эти возможности замыканий позволяют повторно использовать код и изменять поведение.
Захват переменных окружения с помощью замыкания
Сначала мы рассмотрим, как можно использовать замыкания для фиксирования значений среды, в которой они определены, для последующего использования. Вот сценарий: Время от времени наша компания по производству футболок выдаёт эксклюзивную футболку, выпущенную ограниченным тиражом, кому-то из нашего списка рассылки в качестве рекламной акции. Люди в списке рассылки могут по желанию добавить свой любимый цвет в свой профиль. Если человек, выбранный для получения бесплатной футболки, указал свой любимый цвет, он получает футболку этого цвета. Если человек не указал свой любимый цвет, он получит рубашку того цвета, которых в данный момент у компании больше всего.
Существует множество способов реализовать это. В данном примере мы будем использовать перечисление
ShirtColor
, которое имеет варианты
Red и
Blue
(для простоты ограничим количество доступных цветов). Запасы компании мы представим структурой
Inventory
, которая имеет поле shirts
, содержащее
Vec
,
который содержит цвета рубашек, имеющихся на складе. Метод giveaway
,
определённый у
Inventory
, получает опциональный цвет рубашки, который предпочитает обладатель бесплатной рубашки, и возвращает цвет рубашки, которую он получит. Эта схема показана в листинге 13-1:
Файл : src/main.rs
Листинг 13-1: Ситуация с раздачей рубашек компанией
#[derive(Debug, PartialEq, Copy, Clone)]
enum
ShirtColor
{
Red,
Blue,
} struct
Inventory
{ shirts:
Vec
} impl
Inventory { fn giveaway
(&
self
, user_preference:
Option
.most_stocked())
} fn most_stocked
(&
self
) -> ShirtColor { let mut num_red =
0
; let mut num_blue =
0
; for color in
&
self
.shirts { match color {
ShirtColor::Red => num_red +=
1
,
ShirtColor::Blue => num_blue +=
1
,
}
} if num_red > num_blue {
ShirtColor::Red
} else
{
ShirtColor::Blue
}
}
} fn main
() { let store = Inventory { shirts: vec!
[ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
}; let user_pref1 =
Some
(ShirtColor::Red); let giveaway1 = store.giveaway(user_pref1); println!
(
"The user with preference {:?} gets {:?}"
, user_pref1, giveaway1
); let user_pref2 =
None
; let giveaway2 = store.giveaway(user_pref2); println!
(
"The user with preference {:?} gets {:?}"
, user_pref2, giveaway2
);
}
В магазине store
, определённом в main
, осталось две синие и одна красная рубашки для этой ограниченной акции. Мы вызываем метод giveaway для пользователя предпочитающего красную рубашку и для пользователя без каких-либо предпочтений.
Опять же, этот код мог быть реализован множеством способов, но в данном случае,
чтобы сосредоточиться на замыканиях, мы придерживались изученных ранее концепций, за исключением тела метода giveaway
, в котором используется замыкание. В
методе giveaway мы получаем пользовательское предпочтение цвета как параметр типа
Option
и вызываем метод unwrap_or_else на user_preference
. Метод unwrap_or_else на
Option
определён стандартной библиотекой. Он принимает один аргумент: замыкание без аргументов, которое возвращает значение
T
(тот же тип,
который хранится в
Some варианта
Option
, в данном случае
ShirtColor
). Если
Option
является вариантом
Some
, unwrap_or_else возвращает значение из
Some
Если
Option
является вариантом
None
, unwrap_or_else вызывает замыкание и вернёт значение, возвращённое замыканием.
В качестве аргумента unwrap_or_else мы передаём замыкание
|| self.most_stocked()
Это замыкание, которое само не принимает никаких параметров (если бы у замыкания были параметры, они появились бы между двумя вертикальными полосами). В теле замыкания вызывается self.most_stocked()
. Здесь мы определили замыкание, а реализация unwrap_or_else такова, что выполнится оно позднее, когда потребуется получить результат.
Выполнение этого кода выводит:
Интересным аспектом здесь является то, что мы передали замыкание, которое вызывает self.most_stocked()
текущего экземпляра
Inventory
. Стандартной библиотеке не нужно знать ничего о типах
Inventory или
ShirtColor
, которые мы определили, или о логике, которую мы хотим использовать в этом сценарии. Замыкание фиксирует неизменяемую ссылку на self
Inventory и передаёт её с указанным нами кодом в метод unwrap_or_else
. А вот функции не могут фиксировать своё окружение таким образом.
Выведение и аннотация типов замыкания
Есть и другие различия между функциями и замыканиями. Замыкания обычно не требуют аннотирования типов параметров или возвращаемого значения, как это делается в функциях fn
. Аннотации типов требуются для функций, потому что типы
$
cargo run
Compiling shirt-company v0.1.0 (file:///projects/shirt-company)
Finished dev [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/shirt-company`
The user with preference Some(Red) gets Red
The user with preference None gets Blue
являются частью явного интерфейса, предоставляемого пользователям. Жёсткое определение этого интерфейса важно для того, чтобы все согласились с тем, какие типы значений использует и возвращает функция. С другой стороны, замыкания не используются подобным образом: они хранятся в переменных и используются без указания их имён и не предоставляются для использования пользователям нашей библиотеки.
Замыкания, как правило, короткие и уместны только в узком контексте, а не в произвольном сценарии. В этих ограниченных контекстах компилятор может вывести типы параметров и возвращаемого типа, подобно тому, как он может вывести типы большинства переменных (есть редкие случаи, когда компилятору также нужны аннотации типов замыканий).
Как и в случае с переменными, мы можем добавить аннотации типов, если хотим повысить ясность и чёткость описания ценой большей многословности, чем это необходимо. Аннотирование типов для замыкания будет выглядеть как определение,
показанное в листинге 13-2. В этом примере мы определяем замыкание и храним его в переменной, а не определяем замыкание в том месте, куда мы передаём его в качестве аргумента, как это было в листинге 13-1.
Файл : src/main.rs
Замыкания, как правило, короткие и уместны только в узком контексте, а не в произвольном сценарии. В этих ограниченных контекстах компилятор может вывести типы параметров и возвращаемого типа, подобно тому, как он может вывести типы большинства переменных (есть редкие случаи, когда компилятору также нужны аннотации типов замыканий).
Как и в случае с переменными, мы можем добавить аннотации типов, если хотим повысить ясность и чёткость описания ценой большей многословности, чем это необходимо. Аннотирование типов для замыкания будет выглядеть как определение,
показанное в листинге 13-2. В этом примере мы определяем замыкание и храним его в переменной, а не определяем замыкание в том месте, куда мы передаём его в качестве аргумента, как это было в листинге 13-1.
Файл : src/main.rs
1 ... 29 30 31 32 33 34 35 36 ... 62