Язык программирования Rust
Скачать 7.02 Mb.
|
Листинг 9-8: Цепочка вызовов методов после оператора ? Мы перенесли создание новой String в username в начало функции; эта часть не изменилась. Вместо создания переменной username_file мы соединили вызов read_to_string непосредственно с результатом File::open("hello.txt")? . У нас по- прежнему есть ? в конце вызова read_to_string , и мы по-прежнему возвращаем значение Ok , содержащее username , когда и File::open и read_to_string завершаются успешно, а не возвращают ошибки. Функциональность снова такая же, как в Листинге 9-6 и Листинге 9-7; это просто другой, более эргономичный способ её написания. Продолжая рассматривать разные способы записи данной функции, листинг 9-9 демонстрирует способ сделать её ещё короче. Файл: src/main.rs Листинг 9-9: Использование fs::read_to_string вместо открытия и последующего чтения файла use std::fs::File; use std::io; use std::io::Read; fn read_username_from_file () -> Result < String , io::Error> { let mut username = String ::new(); File::open( "hello.txt" )?.read_to_string(& mut username)?; Ok (username) } use std::fs; use std::io; fn read_username_from_file () -> Result < String , io::Error> { fs::read_to_string( "hello.txt" ) } Чтение файла в строку довольно распространённая операция, так что стандартная библиотека предоставляет удобную функцию fs::read_to_string , которая открывает файл, создаёт новую String , читает содержимое файла, размещает его в String и возвращает её. Конечно, использование функции fs::read_to_string не даёт возможности объяснить обработку всех ошибок, поэтому мы сначала изучили длинный способ. Где можно использовать оператор ? ? может использоваться только в функциях, тип возвращаемого значения которых совместим со значением ? используется на. Это потому, что ? оператор определён для выполнения раннего возврата значения из функции таким же образом, как и выражение match , которое мы определили в листинге 9-6. В листинге 9-6 match использовало значение Result , а ответвление с ранним возвратом вернуло значение Err(e) . Тип возвращаемого значения функции должен быть Result , чтобы он был совместим с этим return В листинге 9-10 давайте посмотрим на ошибку, которую мы получим, если воспользуемся ? оператор в main функции с типом возвращаемого значения, несовместимым с типом используемого нами значения ? на: Файл : src/main.rs Листинг 9-10: Попытка использовать ? в main функции, которая возвращает () , не будет компилироваться Этот код открывает файл, что может привести к сбою. ? оператор следует за значением Result , возвращаемым File::open , но эта main функция имеет возвращаемый тип () , а не Result . Когда мы компилируем этот код, мы получаем следующее сообщение об ошибке: use std::fs::File; fn main () { let greeting_file = File::open( "hello.txt" )?; } Эта ошибка указывает на то, что оператор ? разрешено использовать только в функции, которая возвращает Result , Option или другой тип, реализующий FromResidual Для исправления ошибки есть два варианта. Первый - изменить возвращаемый тип вашей функции так, чтобы он был совместим со значением, для которого вы используете оператор ? , если у вас нет ограничений, препятствующих этому. Другой способ - использовать match или один из методов Result для обработки Result любым подходящим способом. Эта ошибка указывает на то, что оператор ? разрешено использовать только в функции, которая возвращает Result , Option или другой тип, который реализует FromResidual Чтобы исправить ошибку, есть два варианта. Первый - изменить тип возвращаемый из функции, чтобы он был совместим со значением, для которого вы используете оператор ? , если у вас нет ограничений, препятствующих этому. Второй заключается в использовании match или одного из методов Result для обработки Result любым подходящим способом. Listing 9-11: Using the ? operator on an Option value Эта функция возвращает Option , потому что возможно, что там есть символ, но также возможно, что его нет. Этот код принимает аргумент среза text строки и вызывает для него метод lines , который возвращает итератор для строк в строке. Поскольку эта функция хочет проверить первую строку, она вызывает next у итератора, чтобы получить первое значение от итератора. Если text является пустой строкой, этот вызов next вернёт None , и в этом случае мы используем ? чтобы остановить и вернуть $ cargo run Compiling error-handling v0.1.0 (file:///projects/error-handling) error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`) --> src/main.rs:4:48 | 3 | / fn main() { 4 | | let greeting_file = File::open("hello.txt")?; | | ^ cannot use the `?` operator in a function that returns `()` 5 | | } | |_- this function should return `Result` or `Option` to accept `?` | = help: the trait `FromResidual For more information about this error, try `rustc --explain E0277`. error: could not compile `error-handling` due to previous error fn last_char_of_first_line (text: & str ) -> Option < char > { text.lines().next()?.chars().last() } None из last_char_of_first_line . Если text не является пустой строкой, next вернёт значение Some , содержащее фрагмент строки первой строки в text Символ ? извлекает фрагмент строки, и мы можем вызвать chars для этого фрагмента строки. чтобы получить итератор символов. Нас интересует последний символ в первой строке, поэтому мы вызываем last , чтобы вернуть последний элемент в итераторе. Вернётся Option , потому что возможно, что первая строка пустая - например, если text начинается с пустой строки, но имеет символы в других строках, как в "\nhi" . Однако, если в первой строке есть последний символ, он будет возвращён в варианте Some Оператор ? в середине даёт нам лаконичный способ выразить эту логику, позволяя реализовать функцию в одной строке. Если бы мы не могли использовать оператор ? в Option , нам пришлось бы пришлось бы реализовать эту логику, используя больше вызовов методов или выражение match Обратите внимание, что вы можете использовать ? оператор Result в функции, которая возвращает Result , и вы можете использовать оператор ? оператор на Option в функции, которая возвращает Option , но вы не можете смешивать и сопоставлять. ? оператор не будет автоматически преобразовывать Result в Option или наоборот; в этих случаях вы можете использовать такие методы, как метод ok для Result или метод ok_or для Option , чтобы выполнить преобразование явно. Обратите внимание, что вы можете использовать оператор ? на Result в функции, которая возвращает Result , и вы можете использовать оператор ? на Option в функции, которая возвращает Option , но вы не можете смешивать и сочетать их. Оператор ? не будет автоматически преобразовывать Result в Option или наоборот; в этих случаях, вы можете использовать такие методы, как ok из Result или ok_or из Option для явного преобразования. К счастью, main также может возвращать Result<(), E> . В листинге 9-12 используется код из листинга 9-10, но мы изменили возвращаемый тип main на Result<(), Box и добавили возвращаемое значение Ok(()) в конец. Теперь этот код будет скомпилирован: Листинг 9-12: Замена main на return Result<(), E> позволяет использовать оператор ? оператор над значениями Result Тип Box является трейт-объектом, о котором мы поговорим в разделе "Использование трейт-объектов, допускающих значения разных типов" в главе 17. Пока use std::error::Error; use std::fs::File; fn main () -> Result <(), Box < dyn Error>> { let greeting_file = File::open( "hello.txt" )?; Ok (()) } что вы можете считать, что Box означает "любой вид ошибки". Использование ? для значения Result в функции main с типом ошибки Box разрешено, так как позволяет вернуть любое значение Err раньше времени. Даже если тело этой функции main будет возвращать только ошибки типа std::io::Error , указав Box , эта сигнатура останется корректной, даже если в тело main будет добавлен код, возвращающий другие ошибки. Когда main функция возвращает Result<(), E> , исполняемый файл завершится со значением 0 , если main вернёт Ok(()) , и выйдет с ненулевым значением, если main вернёт значение Err . Исполняемые файлы, написанные на C, при выходе возвращают целые числа: успешно завершённые программы возвращают целое число 0 , а программы с ошибкой возвращают целое число, отличное от 0 . Rust также возвращает целые числа из исполняемых файлов, чтобы быть совместимым с этим соглашением. Функция main может возвращать любые типы, реализующие трейт std::process::Termination , в которых имеется функция report , возвращающая ExitCode . Обратитесь к документации стандартной библиотеки за дополнительной информацией о порядке реализации трейта Termination для ваших собственных типов. Теперь, когда мы обсудили детали вызова panic! или возврата Result , давайте вернёмся к тому, как решить, какой из случаев подходит для какой ситуации. panic! или не panic! Итак, как принимается решение о том, когда следует вызывать panic! , а когда вернуть Result ? При панике код не имеет возможности восстановить своё выполнение. Можно было бы вызывать panic! для любой ошибочной ситуации, независимо от того, имеется ли способ восстановления или нет, но с другой стороны, вы принимаете решение от имени вызывающего вас кода, что ситуация необратима. Когда вы возвращаете значение Result , вы делегируете принятие решения вызывающему коду. Вызывающий код может попытаться выполнить восстановление способом, который подходит в данной ситуации, или же он может решить, что из ошибки в Err нельзя восстановиться и вызовет panic! , превратив вашу исправимую ошибку в неисправимую. Поэтому возвращение Result является хорошим выбором по умолчанию для функции, которая может дать сбой. В таких ситуация как примеры, прототипы и тесты, более уместно писать код, который паникует вместо возвращения Result . Давайте рассмотрим почему, а затем мы обсудим ситуации, в которых компилятор не может доказать, что ошибка невозможна, но вы, как человек, можете это сделать. Глава будет заканчиваться некоторыми общими руководящими принципами о том, как решить, стоит ли паниковать в коде библиотеки. Примеры, прототипирование и тесты Когда вы пишете пример, иллюстрирующий некоторую концепцию, наличие хорошего кода обработки ошибок может сделать пример менее понятным. Понятно, что в примерах вызов метода unwrap , который может привести к панике, является лишь обозначением способа обработки ошибок в приложении, который может отличаться в зависимости от того, что делает остальная часть кода. Точно так же методы unwrap и expect являются очень удобными при создании прототипа, прежде чем вы будете готовы решить, как обрабатывать ошибки. Они оставляют чёткие маркеры в коде до момента, когда вы будете готовы сделать программу более надёжной. Если в тесте происходит сбой при вызове метода, то вы бы хотели, чтобы весь тест не прошёл, даже если этот метод не является тестируемой функциональностью. Поскольку вызов panic! это способ, которым тест помечается как провалившийся, использование unwrap или expect - именно то, что нужно. Случаи, в которых у вас больше информации, чем у компилятора Также было бы целесообразно вызывать unwrap или expect когда у вас есть какая-то другая логика, которая гарантирует, что Result будет иметь значение Ok , но вашу логику не понимает компилятор. У вас по-прежнему будет значение Result которое нужно обработать: любая операция, которую вы вызываете, все ещё имеет возможность неудачи в целом, хотя это логически невозможно в вашей конкретной ситуации. Если, проверяя код вручную, вы можете убедиться, что никогда не будет вариант с Err , то вполне допустимо вызывать unwrap , а ещё лучше задокументировать причину, по которой, по вашему мнению, у вас никогда не будет варианта Err в тексте expect . Вот пример: Мы создаём экземпляр IpAddr , анализируя жёстко закодированную строку. Можно увидеть, что 127.0.0.1 является действительным IP-адресом, поэтому здесь допустимо использование expect . Однако наличие жёстко закодированной допустимой строки не меняет тип возвращаемого значения метода parse : мы все ещё получаем значение Result и компилятор все также заставляет нас обращаться с Result так, будто возможен вариант Err , потому что компилятор недостаточно умён, чтобы увидеть, что эта строка всегда действительный IP-адрес. Если строка IP-адреса пришла от пользователя, то она не является жёстко запрограммированной в программе и, следовательно, может привести к ошибке, мы определённо хотели бы обработать Result более надёжным способом. Упоминание предположения о том, что этот IP-адрес жёстко закодирован, побудит нас изменить expect для лучшей обработки ошибок, если в будущем нам потребуется вместо этого получить IP-адрес из какого-либо другого источника. Руководство по обработке ошибок Желательно, чтобы код паниковал, если он может оказаться в некорректном состоянии. В этом контексте некорректное состояние это когда некоторое допущение, гарантия, контракт или инвариант были нарушены. Например, когда недопустимые, противоречивые или пропущенные значения передаются в ваш код - плюс один или несколько пунктов из следующего перечисленного в списке: Не корректное состояние — это что-то неожиданное, отличается от того, что может происходить время от времени, например, когда пользователь вводит данные в неправильном формате. Ваш код после этой точки должен полагаться на то, что он не находится в не корректном состоянии, вместо проверок наличия проблемы на каждом этапе. Нет хорошего способа закодировать данную информацию в типах, которые вы используете. Мы рассмотрим пример того, что мы имеем в виду в разделе “Кодирование состояний и поведения на основе типов” главы 17. Если кто-то вызывает ваш код и передаёт значения, которые не имеют смысла, лучше всего вернуть ошибку, если вы это можете, чтобы пользователь библиотеки мог решить, use std::net::IpAddr; let home: IpAddr = "127.0.0.1" .parse() .expect( "Hardcoded IP address should be valid" ); что он хочет делать в этом случае. Однако в тех случаях, когда продолжение выполнения программы может быть небезопасным или вредным, лучшим выбором будет вызов panic! и оповещение пользователя, использующего вашу библиотеку, об ошибке в его коде, чтобы он мог исправить её во время разработки. Аналогично panic! подходит, если вы вызываете внешний, неподконтрольный вам код, и он возвращает недопустимое состояние, которое вы не можете исправить. Однако, когда ожидается сбой, лучше вернуть Result , чем выполнить вызов panic! . В качестве примера можно привести синтаксический анализатор, которому передали неправильно сформированные данные, или HTTP-запрос, возвращающий статус указывающий на то, что вы достигли ограничения на частоту запросов. В этих случаях возврат Result означает, что ошибка является ожидаемой и вызывающий код должен решить, как её обрабатывать. Когда ваш код выполняет операцию, которая может подвергнуть пользователя риску, если она вызывается с использованием недопустимых значений, ваш код должен сначала проверить допустимость значений и паниковать, если значения недопустимы. Так рекомендуется делать в основном из соображений безопасности: попытка оперировать некорректными данными может привести к уязвимостям. Это основная причина, по которой стандартная библиотека будет вызывать panic!, если попытаться получить доступ к памяти вне границ массива: доступ к памяти, не относящейся к текущей структуре данных, является известной проблемой безопасности. Функции часто имеют контракты: их поведение гарантируется, только если входные данные отвечают определённым требованиям. Паника при нарушении контракта имеет смысл, потому что это всегда указывает на дефект со стороны вызывающего кода, и это не ошибка, которую вы хотели бы, чтобы вызывающий код явно обрабатывал. На самом деле, нет разумного способа для восстановления вызывающего кода; программисты, вызывающие ваш код, должны исправить свой. Контракты для функции, особенно когда нарушение вызывает панику, следует описать в документации по API функции. Тем не менее, наличие множества проверок ошибок во всех ваших функциях было бы многословным и раздражительным. К счастью, можно использовать систему типов Rust (следовательно и проверку типов компилятором), чтобы она сделала множество проверок вместо вас. Если ваша функция имеет определённый тип в качестве параметра, вы можете продолжить работу с логикой кода зная, что компилятор уже обеспечил правильное значение. Например, если используется обычный тип, а не тип Option , то ваша программа ожидает наличие чего-то вместо ничего. Ваш код не должен будет обрабатывать оба варианта Some и None : он будет иметь только один вариант для определённого значения. Код, пытающийся ничего не передавать в функцию, не будет даже компилироваться, поэтому ваша функция не должна проверять такой случай во время выполнения. Другой пример - это использование целого типа без знака, такого как u32 , который гарантирует, что параметр никогда не будет отрицательным. |