При выполнении кода листинга 8-24 будет напечатано {"Yellow": 50, "Blue": 10} Первый вызов метода entry вставит ключ для команды "Yellow" со значением 50, потому что для жёлтой команды ещё не имеется значения в HashMap. Второй вызов entry не изменит хеш-карту, потому что для ключа команды "Blue" уже имеется значение 10. Создание нового значения на основе старого значения Другим распространённым вариантом использования хеш-карт является поиск значения по ключу, а затем обновление этого значения на основе старого значения. Например, в листинге 8-25 показан код, который подсчитывает, сколько раз определённое слово встречается в некотором тексте. Мы используем HashMap со словами в качестве ключей и увеличиваем соответствующее слову значение, чтобы отслеживать, сколько раз мы встретили это слово. Если мы впервые встретили слово, то сначала вставляем значение 0. Листинг 8-25: Подсчёт количества вхождений слов с использованием хеш-карты, которая хранит слова и счётчики Этот код напечатает {"world": 2, "hello": 1, "wonderful": 1} . Если вы увидите, что пары ключ/значение печатаются в другом порядке, то вспомните, что мы писали в секции "Доступ к данным в HashMap" , что итерация по хеш-карте происходит в произвольном порядке. Метод split_whitespace возвращает итератор по срезам строки, разделённых пробелам, для строки text . Метод or_insert возвращает изменяемую ссылку ( &mut V ) на значение ключа. Мы сохраняем изменяемую ссылку в переменной count , для этого, чтобы присвоить переменной значение, необходимо произвести разыменование с помощью звёздочки (*). Изменяемая ссылка удаляется сразу же после выхода из области видимости цикла for , поэтому все эти изменения безопасны и согласуются с правилами заимствования. Функция хеширования use std::collections::HashMap; let text = "hello world wonderful world" ; let mut map = HashMap::new(); for word in text.split_whitespace() { let count = map.entry(word).or_insert( 0 ); *count += 1 ; } println! ( "{:?}" , map);
По умолчанию HashMap использует функцию хеширования SipHash, которая может противостоять атакам класса отказ в обслуживании, Denial of Service (DoS) с использованием хэш-таблиц ^siphash . Это не самый быстрый из возможных алгоритмов хеширования, в данном случае производительность идёт на компромисс с обеспечением лучшей безопасности. Если после профилирования вашего кода окажется, что хеш- функция, используемая по умолчанию, очень медленная, вы можете заменить её используя другой hasher. Hasher - это тип, реализующий трейт BuildHasher . Подробнее о типажах мы поговорим в Главе 10. Вам совсем не обязательно реализовывать свою собственную функцию хеширования; crates.io имеет достаточное количество библиотек, предоставляющих разные реализации hasher с множеством общих алгоритмов хеширования. Итоги Векторы, строки и хеш-карты предоставят большое количество функционала для программ, когда необходимо сохранять, получать доступ и модифицировать данные. Теперь вы готовы решить следующие учебные задания: Есть список целых чисел. Создайте функцию, используйте вектор и верните из списка: среднее значение; медиану (значение элемента из середины списка после его сортировки); моду списка (mode of list, то значение которое встречается в списке наибольшее количество раз; HashMap будет полезна в данном случае). Преобразуйте строку в кодировку "поросячьей латыни" (Pig Latin), где первая согласная каждого слова перемещается в конец и к ней добавляется окончание "ay". Например "first" в поросячьей латыни станет "irst-fay". Если слово начинается на гласную, то в конец слова добавляется суффикс "hay" ("apple" становится "apple- hay"). Помните о деталях работы с кодировкой UTF-8! Используя хеш-карту и векторы, создайте текстовый интерфейс позволяющий пользователю добавлять имена сотрудников к названию отдела компании. Например, "Add Sally to Engineering" или "Add Amir to Sales". Затем позвольте пользователю получить список всех людей из отдела или всех людей в компании отсортированным в алфавитном порядке по отделам. Документация API стандартной библиотеки описывает методы у векторов, строк и HashMap. Рекомендуем воспользоваться ей при решении упражнений. Потихоньку мы переходим к более сложным программам, в которых операции могут потерпеть неудачу. Наступило идеальное время для обсуждения обработки ошибок.
Обработка ошибок Возникновение ошибок в ходе выполнения программ - это суровая реальность в жизни программного обеспечения, поэтому Rust имеет ряд функций для обработки ситуаций в которых что-то идёт не так. Во многих случаях Rust требует, чтобы вы признали возможность ошибки и предприняли некоторые действия, прежде чем ваш код будет скомпилирован. Это требование делает вашу программу более надёжной, гарантируя, что вы обнаружите ошибки и обработаете их надлежащим образом, прежде чем развернёте свой код в производственной среде! В Rust ошибки группируются на две основные категории исправимые (recoverable) и неисправимые (unrecoverable). В случае исправимой ошибки, такой как файл не найден, мы, скорее всего, просто хотим сообщить о проблеме пользователю и повторить операцию. Неисправимые ошибки всегда являются симптомами дефектов в коде, например, попытка доступа к ячейке за пределами границ массива, и поэтому мы хотим немедленно остановить программу. Большинство языков не различают эти два вида ошибок и обрабатывают оба вида одинаково, используя такие механизмы, как исключения. В Rust нет исключений. Вместо этого он имеет тип Result для обрабатываемых (исправимых) ошибок и макрос panic! , который останавливает выполнение, когда программа встречает необрабатываемую (неисправимую) ошибку. Сначала эта глава расскажет про вызов panic! , а потом расскажет о возврате значений Result . Кроме того, мы рассмотрим, что нужно учитывать при принятии решения о том, следует ли попытаться исправить ошибку или остановить выполнение.
Неустранимые ошибки с макросом panic! Иногда в коде происходят плохие вещи, и вы ничего не можете с этим поделать. В этих случаях у Rust есть макрос panic! На практике существует два способа вызвать панику: путём выполнения действия, которое вызывает панику в нашем коде (например, обращение к массиву за пределами его размера) или путём явного вызова макроса panic! . В обоих случаях мы вызываем панику в нашей программе. По умолчанию паника выводит сообщение об ошибке, раскручивает и очищает стек вызовов, и завершают работу. С помощью переменной окружения вы также можете заставить Rust отображать стек вызовов при возникновении паники, чтобы было легче отследить источник паники. Раскручивать стек или прерывать выполнение программы в ответ на панику? По умолчанию, когда происходит паника, программа начинает процесс раскрутки стека, означающий в Rust проход обратно по стеку вызовов и очистку данных для каждой обнаруженной функции. Тем не менее, этот обратный проход по стеку и очистка генерируют много работы. Rust как альтернативу предоставляет вам возможность немедленного прерывания (aborting), которое завершает работу программы без очистки. Память, которую использовала программа, должна быть очищена операционной системой. Если в вашем проекте нужно насколько это возможно сделать маленьким исполняемый файл, вы можете переключиться с варианта раскрутки стека на вариант прерывания при панике, добавьте panic = 'abort' в раздел [profile] вашего Cargo.toml файла. Например, если вы хотите прервать панику в режиме релиза, добавьте это: Давайте попробуем вызвать panic! в простой программе: Файл: src/main.rs При запуске программы, вы увидите что-то вроде этого: [profile.release] panic = 'abort' fn main () { panic! ( "crash and burn" ); }
Выполнение макроса panic! вызывает сообщение об ошибке, содержащееся в двух последних строках. Первая строка показывает сообщение паники и место в исходном коде, где возникла паника: src/main.rs: 2:5 указывает, что это вторая строка, пятый символ внутри нашего файла src/main.rs В этом случае указанная строка является частью нашего кода, и если мы перейдём к этой строке, мы увидим вызов макроса panic! . В других случаях вызов panic! мог бы произойти в стороннем коде, который вызывает наш код, тогда имя файла и номер строки для сообщения об ошибке будет из чужого кода, где макрос panic! выполнен, а не из строк нашего кода, которые в конечном итоге привели к выполнению panic! . Мы можем использовать обратную трассировку вызовов функций которые вызвали panic! чтобы выяснить, какая часть нашего кода вызывает проблему. Мы обсудим обратную трассировку более подробно далее. Использование обратной трассировки panic! Давайте посмотрим на другой пример, где, вызов panic! происходит в сторонней библиотеке из-за ошибки в нашем коде (а не как в примере ранее, из-за вызова макроса нашим кодом напрямую). В листинге 9-1 приведён код, который пытается получить доступ по индексу в векторе за пределами допустимого диапазона значений индекса. Файл: src/main.rs Листинг 9-1: Попытка доступа к элементу за пределами вектора, которая вызовет panic! Здесь мы пытаемся получить доступ к 100-му элементу вектора (который находится по индексу 99, потому что индексирование начинается с нуля), но вектор имеет только 3 элемента. В этой ситуации, Rust будет вызывать панику. Использование [] должно возвращать элемент, но вы передаёте неверный индекс: не существует элемента, который Rust мог бы вернуть. В языке C, например, попытка прочесть за пределами конца структуры данных (в нашем случае векторе) приведёт к неопределённому поведению, undefined behavior, UB. Вы всё $ cargo run Compiling panic v0.1.0 (file:///projects/panic) Finished dev [unoptimized + debuginfo] target(s) in 0.25s Running `target/debug/panic` thread 'main' panicked at 'crash and burn', src/main.rs:2:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace fn main () { let v = vec! [ 1 , 2 , 3 ]; v[ 99 ]; }
равно получите значение, которое находится в том месте памяти компьютера, которое соответствовало бы этому элементу в векторе, несмотря на то, что память по тому адресу совсем не принадлежит вектору (всё просто: C рассчитал бы место хранения элемента с индексом 99 и считал бы то, что там хранится, упс). Это называется чтением за пределом буфера, buffer overread, и может привести к уязвимостям безопасности. Если злоумышленник может манипулировать индексом таким образом, то у него появляется возможность читать данные, которые он не должен иметь возможности читать. Чтобы защитить вашу программу от такого рода уязвимостей при попытке прочитать элемент с индексом, которого не существует, Rust остановит выполнение и откажется продолжить работу программы. Давайте попробуем так сделать и посмотрим на поведение Rust: Следующая строка говорит, что мы можем установить переменную среды RUST_BACKTRACE , чтобы получить обратную трассировку того, что именно стало причиной ошибки. Обратная трассировка создаёт список всех функций, которые были вызваны до какой-то определённой точки выполнения программы. Обратная трассировка в Rust работает так же, как и в других языках. По этому предлагаем вам читать данные обратной трассировки как и везде - читать сверху вниз, пока не увидите информацию о файлах написанных вами. Это место, где возникла проблема. Другие строки, которые выше над строками с упоминанием наших файлов, - это код, который вызывается нашим кодом; строки ниже являются кодом, который вызывает наш код. Эти строки могут включать основной код Rust, код стандартной библиотеки или используемые крейты. Давайте попробуем получить обратную трассировку с помощью установки переменной среды RUST_BACKTRACE в любое значение, кроме 0. Листинг 9-2 показывает вывод, подобный тому, что вы увидите. $ cargo run Compiling panic v0.1.0 (file:///projects/panic) Finished dev [unoptimized + debuginfo] target(s) in 0.27s Running `target/debug/panic` thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Листинг 9-2: Обратная трассировка, сгенерированная вызовом panic! , когда установлена переменная окружения RUST_BACKTRACE Тут много вывода! Вывод, который вы увидите, может отличаться от представленного, в зависимости от вашей операционной системы и версии Rust. Для того, чтобы получить обратную трассировку с этой информацией, должны быть включены символы отладки, debug symbols. Символы отладки включены по умолчанию при использовании cargo build или cargo run без флага --release , как у нас в примере. В выводе обратной трассировки листинга 9-2, строка #6 указывает на строку в нашем проекте, которая вызывала проблему: строка 4 из файла src/main.rs. Если мы не хотим, чтобы наша программа запаниковала, мы должны начать исследование с места, на которое указывает первая строка с упоминанием нашего файла. В листинге 9-1, где мы для демонстрации обратной трассировки сознательно написали код, который паникует, способ исправления паники состоит в том, чтобы не запрашивать элемент за пределами диапазона значений индексов вектора. Когда ваш код запаникует в будущем, вам нужно будет выяснить, какое выполняющееся кодом действие, с какими значениями вызывает панику и что этот код должен делать вместо этого. $ RUST_BACKTRACE=1 cargo run thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5 stack backtrace: 0: rust_begin_unwind at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/std/src/panicking.rs:483 1: core::panicking::panic_fmt at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/panicking.rs:85 2: core::panicking::panic_bounds_check at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/panicking.rs:62 3: >::index at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/slice/index.rs:255 4: core::slice::index:: for [T]>::index at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/slice/index.rs:15 5: <:vec::vec> as core::ops::index::Index>::index at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/alloc/src/vec.rs:1982 6: panic::main at ./src/main.rs:4 7: core::ops::function::FnOnce::call_once at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/ops/function.rs:227 note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
Мы вернёмся к обсуждению макроса panic! , и того когда нам следует и не следует использовать panic! для обработки ошибок в разделе " panic! или НЕ panic! " этой главы. Далее мы рассмотрим, как восстановить выполнение программы после исправляемых ошибок, использующих тип Result
1 ... 15 16 17 18 19 20 21 22 ... 62
Исправимые ошибки с ResultМногие ошибки являются не настолько критичными, чтобы останавливать выполнение программы. Иногда, когда в функции происходит сбой, необходима просто правильная интерпретация и обработка ошибки. К примеру, при попытке открыть файл может произойти ошибка из-за отсутствия файла. Вы, возможно, захотите исправить ситуацию и создать новый файл вместо остановки программы.Вспомните раздел ["Обработка потенциального сбоя с помощью типа Result"] главы 2:мы использовали там перечисление Result, имеющее два варианта, Ok и Err для обработки сбоев. Само перечисление определено следующим образом:Типы T и E являются параметрами обобщённого типа: мы обсудим обобщённые типы более подробно в Главе 10. Все что вам нужно знать прямо сейчас - это то, что Tпредставляет тип значения, которое будет возвращено в случае успеха внутри варианта Ok, а E представляет тип ошибки, которая будет возвращена при сбое внутри варианта Err. Так как тип Result имеет эти обобщённые параметры (generic type parameters), мы можем использовать тип Result и функции, которые определены для него, в разных ситуациях, когда тип успешного значение и значения ошибки, которые мы хотим вернуть, отличаются.Давайте вызовем функцию, которая возвращает значение Result, потому что может потерпеть неудачу. В листинге 9-3 мы пытаемся открыть файл.Файл: src/main.rsЛистинг 9-3: Открытие файлаFile::open возвращает значения типа Result . Универсальный тип T в реализации File::open соответствует типу успешно полученного значения, std::fs::File , а именно дескриптору файла. Тип E , используемый для значения в случае возникновения ошибки, - std::io::Error . Такой возвращаемый тип означает, что вызов File::open может быть успешным и вернуть дескриптор файла, из которого мы можем читать или в который можем писать. Также вызов функции может завершиться неудачей: например, файл может не существовать, или у нас может не быть разрешения enum Result { Ok (T), Err (E), } use std::fs::File; fn main () { let greeting_file_result = File::open( "hello.txt" ); }
на доступ к файлу. Функция File::open должна иметь способ сообщить нам об успехе или неудаче и в то же время дать нам либо дескриптор файла, либо информацию об ошибке. Эту возможность как раз и предоставляет перечисление Result В случае успеха File::open значением переменной greeting_file_result будет экземпляр Ok , содержащий дескриптор файла. В случае неудачи значение в переменной greeting_file_result будет экземпляром Err , содержащим дополнительную информацию о том, какая именно ошибка произошла. Необходимо дописать в код листинга 9-3 выполнение разных действий в зависимости от значения, которое вернёт вызов File::open . Листинг 9-4 показывает один из способов обработки Result - пользуясь базовым инструментом языка, таким как выражение match , рассмотренным в Главе 6. Файл: src/main.rs Листинг 9-4: Использование выражения match для обработки возвращаемых вариантов типа Result Обратите внимание, что также как перечисление Option , перечисление Result и его варианты, входят в область видимости благодаря авто-импорту (prelude), поэтому не нужно указывать Result:: перед использованием вариантов Ok и Err в ветках выражения match Если результатом будет Ok , этот код вернёт значение file из варианта Ok , а мы затем присвоим это значение файлового дескриптора переменной greeting_file . После match мы можем использовать дескриптор файла для чтения или записи. Другая ветвь match обрабатывает случай, где мы получаем значение Err после вызова File::open . В этом примере мы решили вызвать макрос panic! . Если в нашей текущей директории нет файла с именем hello.txt и мы выполним этот код, то мы увидим следующее сообщение от макроса panic! : use std::fs::File; fn main () { let greeting_file_result = File::open( "hello.txt" ); let greeting_file = match greeting_file_result { Ok (file) => file, Err (error) => panic! ( "Problem opening the file: {:?}" , error), }; }
Как обычно, данное сообщение точно говорит, что пошло не так. Обработка различных ошибок с помощью match Код в листинге 9-4 будет вызывать panic! независимо от того, почему вызов File::open не удался. Однако мы хотим предпринять различные действия для разных причин сбоя. Если открытие File::open не удалось из-за отсутствия файла, мы хотим создать файл и вернуть его дескриптор. Если вызов File::open не удался по любой другой причине - например, потому что у нас не было прав на открытие файла, то все равно мы хотим вызвать panic! как у нас сделано в листинге 9-4. Для этого мы добавляем выражение внутреннего match , показанное в листинге 9-5. Файл: src/main.rs Листинг 9-5: Обработка различных ошибок разными способами Типом значения возвращаемого функцией File::open внутри Err варианта является io::Error , структура из стандартной библиотеки. Данная структура имеет метод kind , который можно вызвать для получения значения io::ErrorKind . Перечисление io::ErrorKind из стандартной библиотеки имеет варианты, представляющие различные типы ошибок, которые могут появиться при выполнении операций в io . Вариант, $ cargo run Compiling error-handling v0.1.0 (file:///projects/error-handling) Finished dev [unoptimized + debuginfo] target(s) in 0.73s Running `target/debug/error-handling` thread 'main' panicked at 'Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:8:23 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace use std::fs::File; use std::io::ErrorKind; fn main () { let greeting_file_result = File::open( "hello.txt" ); let greeting_file = match greeting_file_result { Ok (file) => file, Err (error) => match error.kind() { ErrorKind::NotFound => match File::create( "hello.txt" ) { Ok (fc) => fc, Err (e) => panic! ( "Problem creating the file: {:?}" , e), }, other_error => { panic! ( "Problem opening the file: {:?}" , other_error); } }, }; }
который мы хотим использовать, это ErrorKind::NotFound , который даёт информацию, о том, что файл который мы пытаемся открыть ещё не существует. Итак, во второй строке мы вызываем сопоставление шаблона с переменной greeting_file_result и попадаем в ветку с обработкой ошибки, но также у нас есть внутренняя проверка для сопоставления error.kind() ошибки. Условие, которое мы хотим проверить во внутреннем match , заключается в том, является ли значение, возвращаемое error.kind() , вариантом NotFound перечисления ErrorKind . Если это так, мы пытаемся создать файл с помощью функции File::create Однако, поскольку вызов File::create тоже может завершиться ошибкой, нам нужна обработка ещё одной ошибки, теперь уже во внутреннем выражении match . Заметьте: если файл не может быть создан, выводится другое, специализированное сообщение об ошибке. Вторая же ветка внешнего match (который обрабатывает вызов error.kind() ), остаётся той же самой - в итоге программа паникует при любой ошибке, кроме ошибки отсутствия файла. Альтернативы использованию match с Result Как много match ! Выражение match является очень полезным, но в то же время довольно примитивным. В главе 13 вы узнаете о замыканиях (closures), которые используются во многих методах типа Result . Эти методы помогают быть более лаконичным, чем использование match при работе со значениями ResultE> в вашем коде. Например, вот другой способ написать ту же логику, что показана в Листинге 9-5, но с использованием замыканий и метода unwrap_or_else : Хотя этот код ведёт себя так же, как и код из листинга 9-5, он не содержит никаких выражений match и его легче читать. После прочтения главы 13 и поищите метод unwrap_or_else в документации по стандартной библиотеке. Множество других use std::fs::File; use std::io::ErrorKind; fn main () { let greeting_file = File::open( "hello.txt" ).unwrap_or_else(|error| { if error.kind() == ErrorKind::NotFound { File::create( "hello.txt" ).unwrap_or_else(|error| { panic! ( "Problem creating the file: {:?}" , error); }) } else { panic! ( "Problem opening the file: {:?}" , error); } }); }
подобных методов могут очистить огромные вложенные выражения match, когда вы имеете дело с ошибками. Лаконичные способы обработки ошибок - unwrap и expect Использование match работает достаточно хорошо, но может быть довольно многословным и не всегда хорошо передаёт смысл. Тип Result имеет множество вспомогательных методов для выполнения различных, более специфических задач. Метод unwrap - это метод быстрого доступа к значениям, реализованный так же, как и выражение match , которое мы написали в Листинге 9-4. Если значение Result является вариантом Ok , unwrap возвращает значение внутри Ok . Если Result - вариант Err , то unwrap вызовет для нас макрос panic! . Вот пример unwrap в действии: Файл: src/main.rs Если мы запустим этот код при отсутствии файла hello.txt, то увидим сообщение об ошибке из вызова panic! метода unwrap : Другой метод, похожий на unwrap , это expect , позволяющий указать сообщение об ошибке для макроса panic! . Использование expect вместо unwrap с предоставлением хорошего сообщения об ошибке выражает ваше намерение и делает более простым отслеживание источника паники. Синтаксис метода expect выглядит так: Файл: src/main.rs expect используется так же как и unwrap : либо возвращается дескриптор файла либо вызывается макрос panic! Наше сообщение об ошибке в expect будет передано в panic! и заменит стандартное use std::fs::File; fn main () { let greeting_file = File::open( "hello.txt" ).unwrap(); } thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:49 use std::fs::File; fn main () { let greeting_file = File::open( "hello.txt" ) .expect( "hello.txt should be included in this project" ); }
используемое сообщение. Вот как это выглядит: В рабочем коде, большинство выбирает expect в угоду unwrap и добавляет описание, почему операция должна закончиться успешно. Но даже если предположение оказалось неверным, информации для отладки будет больше. Проброс ошибок Когда вы пишете функцию, реализация которой вызывает что-то, что может завершиться ошибкой, вместо обработки ошибки в этой функции, вы можете вернуть ошибку в вызывающий код, чтобы он мог решить, что с ней делать. Такой приём известен как распространение ошибки (propagating the error). Благодаря нему мы даём больше контроля вызывающему коду, где может быть больше информации или логики, которая диктует, как ошибка должна обрабатываться, чем было бы в месте появления этой ошибки. Например, код программы 9-6 читает имя пользователя из файла. Если файл не существует или не может быть прочтён, то функция возвращает ошибку в код, который вызвал данную функцию. Файл: src/main.rs Листинг 9-6: Функция, которая возвращает ошибки в вызывающий код, используя оператор match thread 'main' panicked at 'hello.txt should be included in this project: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:5:10 use std::fs::File; use std::io::{ self , Read}; fn read_username_from_file () -> Result < String , io::Error> { let username_file_result = File::open( "hello.txt" ); let mut username_file = match username_file_result { Ok (file) => file, Err (e) => return Err (e), }; let mut username = String ::new(); match username_file.read_to_string(& mut username) { Ok (_) => Ok (username), Err (e) => Err (e), } }
Эта функция может быть написана гораздо более коротким способом, но мы начнём с того, что многое сделаем вручную, чтобы изучить обработку ошибок; а в конце покажем более короткий способ. Давайте сначала рассмотрим тип возвращаемого значения: Result . Здесь есть возвращаемое значение функции типа ResultE> где шаблонный параметр T был заполнен конкретным типом String и шаблонный параметр E был заполнен конкретным типом io::Error Если эта функция выполнится без проблем, то код, вызывающий эту функцию, получит значение Ok , содержащее String - имя пользователя, которое эта функция прочитала из файла. Если функция столкнётся с какими-либо проблемами, вызывающий код получит значение Err , содержащее экземпляр io::Error , который включает дополнительную информацию о том, какие проблемы возникли. Мы выбрали io::Error в качестве возвращаемого типа этой функции, потому что это тип значения ошибки, возвращаемого из обеих операций, которые мы вызываем в теле этой функции и которые могут завершиться неудачей: функция File::open и метод read_to_string Тело функции начинается с вызова File::open . Затем мы обрабатываем значение Result с помощью match , аналогично match из листинга 9-4. Если File::open завершается успешно, то дескриптор файла в переменной образца file становится значением в изменяемой переменной username_file и функция продолжит свою работу. В случае Err , вместо вызова panic! , мы используем ключевое слово return для досрочного возврата из функции и передаём значение ошибки из File::open , которое теперь находится в переменной образца e , обратно в вызывающий код как значение ошибки этой функции. Таким образом, если у нас есть файловый дескриптор в username_file , функция создаёт новую String в переменной username и вызывает метод read_to_string для файлового дескриптора в username_file , чтобы прочитать содержимое файла в username . Метод read_to_string также возвращает Result , потому что он может потерпеть неудачу, даже если File::open завершился успешно. Поэтому нам нужен ещё один match для обработки этого Result : если read_to_string завершится успешно, то наша функция сработала, и мы возвращаем имя пользователя из файла, которое теперь находится в username , обёрнутое в Ok . Если read_to_string потерпит неудачу, мы возвращаем значение ошибки таким же образом, как мы возвращали значение ошибки в match , который обрабатывал возвращаемое значение File::open . Однако нам не нужно явно указывать return , потому что это последнее выражение в функции. Затем код, вызывающий этот, будет обрабатывать получение либо значения Ok , содержащего имя пользователя, либо значения Err , содержащего io::Error Вызывающий код должен решить, что делать с этими значениями. Если вызывающий код получает значение Err , он может вызвать panic! и завершить работу программы, использовать имя пользователя по умолчанию или найти имя пользователя, например, не в файле. У нас недостаточно информации о том, что на самом деле пытается сделать вызывающий код, поэтому мы распространяем всю информацию об успехах или ошибках вверх, чтобы она могла обрабатываться соответствующим образом.
Эта схема передачи ошибок настолько распространена в Rust, что Rust предоставляет оператор вопросительного знака ? , чтобы облегчить эту задачу. Сокращение для проброса ошибок: оператор ? В листинге 9-7 показана реализация read_username_from_file , которая имеет ту же функциональность, что и в листинге 9-6, но в этой реализации используется оператор ? Файл: src/main.rs Листинг 9-7: Функция, возвращающая ошибки в вызывающий код с помощью оператора ? Выражение ? , расположенное после Result , работает почти так же, как и те выражения match , которые мы использовали для обработки значений Result в листинге 9-6. Если в качестве значения Result будет Ok , то значение внутри Ok будет возвращено из этого выражения, и программа продолжит работу. Если же значение представляет собой Err , то Err будет возвращено из всей функции, как если бы мы использовали ключевое слово return , так что значение ошибки будет передано в вызывающий код. Существует разница между тем, что делает выражение match из листинга 9-6 и тем, что делает оператор ? : значения ошибок, для которых вызван оператор ? , проходят через функцию from , определённую в трейте From стандартной библиотеки, которая используется для преобразования значений из одного типа в другой. Когда оператор ? вызывает функцию from , полученный тип ошибки преобразуется в тип ошибки, определённый в возвращаемом типе текущей функции. Это полезно, когда функция возвращает только один тип ошибки, для описания всех возможных вариантов сбоев, даже если её отдельные компоненты могут выходить из строя по разным причинам. Например, мы могли бы изменить функцию read_username_from_file в листинге 9-7, чтобы возвращать пользовательский тип ошибки с именем OurError , который мы определим. Если мы также определим impl From<:error> for OurError для создания экземпляра OurError из io::Error , то оператор ? , вызываемый в теле read_username_from_file , вызовет from и преобразует типы ошибок без необходимости добавления дополнительного кода в функцию. use std::fs::File; use std::io; use std::io::Read; fn read_username_from_file () -> Result < String , io::Error> { let mut username_file = File::open( "hello.txt" )?; let mut username = String ::new(); username_file.read_to_string(& mut username)?; Ok (username) }
В случае листинга 9-7 оператор ? в конце вызова File::open вернёт значение внутри Ok в переменную username_file . Если произойдёт ошибка, оператор ? выполнит ранний возврат значения Err вызывающему коду. То же самое относится к оператору ? в конце вызова read_to_string Оператор ? позволяет избавиться от большого количества шаблонного кода и упростить реализацию этой функции. Мы могли бы даже ещё больше сократить этот код, если бы использовали цепочку вызовов методов сразу после ? , как показано в листинге 9-8. Файл: src/main.rs