Исправимые ошибки с Result

Многие ошибки являются не настолько критичными, чтобы останавливать выполнение программы. Иногда, когда в функции происходит сбой, необходима просто правильная интерпретация и обработка ошибки. К примеру, при попытке открыть файл может произойти ошибка из-за отсутствия файла. Вы, возможно, захотите исправить ситуацию и создать новый файл вместо остановки программы.

Вспомните раздел "Обработка потенциального сбоя с помощью типа Result" главы 2: мы использовали там перечисление Result, имеющее два варианта, Ok и Err для обработки сбоев. Само перечисление определено следующим образом:


#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}
}

Типы T и E являются параметрами обобщённого типа: мы обсудим обобщённые типы более подробно в Главе 10. Все что вам нужно знать прямо сейчас - это то, что T представляет тип значения, которое будет возвращено в случае успеха внутри варианта Ok, а E представляет тип ошибки, которая будет возвращена при сбое внутри варианта Err. Так как тип Result имеет эти типовые параметры (generic type parameters), мы можем использовать тип Result и его методы, которые определены в стандартной библиотеке, в ситуациях, когда тип успешного значения и значения ошибки, которые мы хотим вернуть, отличаются.

Давайте вызовем функцию, которая возвращает значение Result, потому что может потерпеть неудачу. В листинге 9-3 мы пытаемся открыть файл.

Файл: src/main.rs

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");
}

Листинг 9-3: Открытие файла

Откуда мы знаем, что File::open возвращает Result? Мы могли бы посмотреть документацию по API стандартной библиотеки или мы могли бы спросить компилятор! Если мы припишем переменной f тип, отличный от возвращаемого типа функции, а затем попытаемся скомпилировать код, компилятор скажет нам, что типы не совпадают. Сообщение об ошибке подскажет нам, каким должен быть тип f. Давайте попробуем! Мы знаем, что возвращаемый тип File::open не является типом u32, поэтому давайте изменим выражение let f на следующее:

use std::fs::File;

fn main() {
    let f: u32 = File::open("hello.txt");
}

Попытка компиляции выводит сообщение:

$ cargo run
   Compiling error-handling v0.1.0 (file:///projects/error-handling)
error[E0308]: mismatched types
 --> src/main.rs:4:18
  |
4 |     let f: u32 = File::open("hello.txt");
  |            ---   ^^^^^^^^^^^^^^^^^^^^^^^ expected `u32`, found enum `Result`
  |            |
  |            expected due to this
  |
  = note: expected type `u32`
             found enum `Result<File, std::io::Error>`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
error: could not compile `error-handling`

To learn more, run the command again with --verbose.

Ошибка говорит нам о том, что возвращаемым типом функции File::open является Result<T, E>. Типовой параметр T здесь равен типу успешного выполнения, std::fs::File, то есть дескриптору файла. Тип E, используемый в значении ошибки, равен std::io::Error.

Этот возвращаемый тип означает, что вызов File::open может завершиться успешно и вернуть дескриптор файла, с помощью которого можно читать из файла или писать в него. Вызов функции также может завершиться ошибкой: например, файла может не существовать или у нас может не быть прав на доступ к нему. Функция File::open должна иметь способ сообщить нам, был ли её вызов успешен или потерпел неудачу и одновременно возвратить либо дескриптор файла либо информацию об ошибке. Эта информация - именно то, что возвращает перечисление Result.

Когда вызов File::open успешен, значение в переменной f будет экземпляром Ok, внутри которого содержится дескриптор файла. Если вызов не успешный, значением переменной f будет экземпляр Err, который содержит больше информации о том, какая ошибка произошла.

Необходимо дописать в код листинга 9-3 выполнение разных действий в зависимости от значения, которое вернёт вызов File::open. Листинг 9-4 показывает один из способов обработки Result - пользуясь базовым инструментом языка, таким как выражение match, рассмотренным в Главе 6.

Файл: src/main.rs

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => panic!("Problem opening the file: {:?}", error),
    };
}

Листинг 9-4: Использование выражения match для обработки возвращаемых вариантов типа Result

Обратите внимание, что также как перечисление Option, перечисление Result и его варианты, входят в область видимости благодаря авто-импорту (prelude), поэтому не нужно указывать Result:: перед использованием вариантов Ok и Err в ветках выражения match.

Здесь мы говорим Rust, что когда результат - это Ok, то надо вернуть внутреннее значение file из варианта Ok, и затем мы присваиваем это значение дескриптора файла переменной f. После match мы можем использовать дескриптор файла для чтения или записи.

Другая ветвь match обрабатывает случай, где мы получаем значение Err после вызова File::open. В этом примере мы решили вызвать макрос panic!. Если в нашей текущей директории нет файла с именем hello.txt и мы выполним этот код, то мы увидим следующее сообщение от макроса panic!:

$ 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

Как обычно, данное сообщение точно говорит, что пошло не так.

Обработка различных ошибок с помощью match

Код в листинге 9-4 будет вызывать panic! независимо от того, почему вызов File::open не удался. Мы бы хотели предпринять различные действия для разных причин сбоя. Если открытие File::open не удалось из-за отсутствия файла, мы хотим создать файл и вернуть его дескриптор. Если вызов File::open не удался по любой другой причине (например, потому что у нас не было прав на открытие файла), то мы хотим вызвать panic! как у нас сделано в листинге 9-4. Посмотрите листинг 9-5, в котором мы добавили дополнительное внутреннее выражение match.

Файл: src/main.rs

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        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)
            }
        },
    };
}

Листинг 9-5: Обработка различных ошибок разными способами

Типом значения возвращаемого функцией File::open внутри Err варианта является io::Error, структура из стандартной библиотеки. Данная структура имеет метод kind, который можно вызвать для получения значения io::ErrorKind. Перечисление io::ErrorKind из стандартной библиотеки имеет варианты, представляющие различные типы ошибок, которые могут появиться при выполнении операций в io (крейте который занимается проблемами ввода/вывода данных). Вариант, который мы хотим использовать, это ErrorKind::NotFound. Он даёт информацию, о том, что файл который мы пытаемся открыть ещё не существует. Итак, во второй строке мы вызываем сопоставление шаблона с переменной f и попадаем в ветку с обработкой ошибки, но также у нас есть внутренняя проверка для сопоставления error.kind() ошибки.

Условие, которое мы хотим проверить во внутреннем match - это то, что значение, которое вернул вызов error.kind(), является вариантом NotFound перечисления ErrorKind. Если это так, мы пытаемся создать файл с помощью функции File::create. Однако, поскольку вызов File::create тоже может завершиться ошибкой, нам нужна обработка ещё одной ошибки теперь уже во внутреннем выражении match - третий вложенный match. Заметьте: если файл не может быть создан, выводится другое сообщение об ошибке, более специализированное. Вторая же ветка внешнего match (который обрабатывает вызов error.kind()), остаётся той же самой. В итоге программа паникует при любой ошибке, кроме ошибки отсутствия файла.

Достаточно про match! Код с match является очень удобным, но также достаточно примитивным. В Главе 13 вы узнаете про замыкания (closures); тип Result<T, E> имеет много методов, реализованных с помощью выражения match и принимающих замыкание в качестве входного значения. Использование данных методов сделает ваш код более лаконичным. Более опытные разработчики могли бы написать код как в листинге 9-5, вместо нашего:

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = 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);
        }
    });
}

Несмотря на то, что данный код имеет такое же поведение как в листинге 9-5, он не содержит ни одного выражения match и проще для чтения. Рекомендуем вам вернутся к примеру этого раздела после того как вы прочитаете Главу 13 и изучите метод unwrap_or_else по документации стандартной библиотеки. Многие из методов о которых вы узнаете в документации и Главе 13 могут очистить код от больших, вложенных выражений match при обработке ошибок.

Сокращённые способы обработки ошибок unwrap и expect

Использование match работает неплохо, однако может выглядеть несколько многословно и не всегда хорошо передаёт намерения. У типа Result<T, E> есть много методов для различных задач. Один из них, unwrap, является сокращённым методом, который реализован прямо как выражение match из листинга 9-4. Если значение Result это Ok, unwrap вернёт значение внутри Ok. Если же Result это Err, unwrap вызовет макрос panic!. Вот пример unwrap в действии:

Файл: src/main.rs

use std::fs::File;

fn main() {
    let f = File::open("hello.txt").unwrap();
}

Если мы запустим этот код при отсутствии файла hello.txt , то увидим сообщение об ошибке из вызова panic! метода unwrap :

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error {
repr: Os { code: 2, message: "No such file or directory" } }',
src/libcore/result.rs:906:4

Другой метод, похожий на unwrap, это expect, позволяющий выбрать сообщение об ошибке для макроса panic!. Использование expect вместо unwrap с предоставлением хорошего сообщения об ошибке выражает ваше намерение и делает более простым отслеживание источника паники. Синтаксис метода expect выглядит так:

Файл: src/main.rs

use std::fs::File;

fn main() {
    let f = File::open("hello.txt").expect("Failed to open hello.txt");
}

Мы используем expect таким же образом, как и unwrap: чтобы вернуть дескриптор файла или вызвать макрос panic!. Сообщением об ошибке, которое expect передаст в panic!, будет параметр функции expect, а не значение по умолчанию, используемое unwrap. Вот как оно выглядит:

thread 'main' panicked at 'Failed to open hello.txt: Error { repr: Os { code:
2, message: "No such file or directory" } }', src/libcore/result.rs:906:4

Так как сообщение об ошибке начинается с нашего пользовательского текста: Failed to open hello.txt, то потом будет проще найти из какого места в коде данное сообщение приходит. Если использовать unwrap во множестве мест, то придётся потратить время для выяснения какой именно вызов unwrap вызывает "панику", так как все вызовы unwrap генерируют одинаковое сообщение.

Проброс ошибок

Когда вы пишете функцию, реализация которой вызывает что-то, что может завершиться ошибкой, вместо обработки ошибки в этой функции, вы можете вернуть ошибку в вызывающий код, чтобы он мог решить, что с ней делать. Такой приём известен как распространение ошибки, propagating the error. Благодаря нему мы даём больше контроля вызывающему коду, где может быть больше информации или логики, которая диктует, как ошибка должна обрабатываться, чем было бы в месте появления этой ошибки.

Например, код программы 9-6 читает имя пользователя из файла. Если файл не существует или не может быть прочтён, то функция возвращает ошибку в код, который вызвал данную функцию:

Файл: src/main.rs


#![allow(unused)]
fn main() {
use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
    let f = File::open("hello.txt");

    let mut f = match f {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut s = String::new();

    match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        Err(e) => Err(e),
    }
}
}

Листинг 9-6: Функция, которая возвращает ошибки в вызывающий код, используя оператор match

Данную функцию можно записать гораздо короче. Чтобы больше проникнуться обработкой ошибок, мы сначала сделаем многое самостоятельно, а в конце покажем более короткий способ. Давайте сначала рассмотрим тип возвращаемого значения: Result<String, io::Error>. Здесь есть возвращаемое значение функции типа Result<T, E> где шаблонный параметр T был заполнен конкретным типом String и шаблонный параметр E был заполнен конкретным типом io::Error. Если эта функция выполнится успешно, будет возвращено Ok, содержащее значение типа String - имя пользователя прочитанное функцией из файла. Если же при чтении файла будут какие-либо проблемы, то вызываемый код получит значение Err с экземпляром io::Error, в котором содержится больше информации об ошибке. Мы выбрали io::Error в качестве возвращаемого значения функции, потому что обе операции, которые мы вызываем внутри этой функции, возвращают этот тип ошибки: функция File::open и метод read_to_string.

Тело функции начинается с вызова File::open. Затем мы обрабатываем значение Result возвращённое с помощью match аналогично коду match листинга 9-4, но вместо вызова panic! для случая Err делаем ранний возврат из данной функции и передаём ошибку из File::open обратно в вызывающий код, как ошибку уже текущей функции. Если File::open выполнится успешно, мы сохраняем дескриптор файла в переменной f и выполнение продолжается далее.

Затем мы создаём новую String в переменной s и вызываем метод read_to_string у дескриптора файла в переменной f, чтобы считать содержимое файла в переменную s. Метод read_to_string также возвращает Result, потому что он может потерпеть неудачу, даже если File::open пройдёт успешно. Таким образом, нам нужно ещё одно выражение match, чтобы справиться с этим Result: если read_to_string выполнится успешно, то наша функция завершится успешно и мы вернём имя пользователя из файла, которое сейчас находится в s, завёрнутым в Ok. Если вызов read_to_string не успешен, мы возвращаем значение ошибки так же, как мы вернули значение ошибки в match, обработавшем возвращаемое значение File::open. Тем не менее, нам не нужно явно писать return, потому что это последнее выражение в функции.

Код, вызывающий данный код, будет обрабатывать либо значение Ok, содержащее имя пользователя, либо значение Err, содержащее io::Error. Мы не знаем, что будет делать вызывающий код с этими значениями. Если вызывающий код получает значение Err, он может вызвать panic! и завершить программу, использовать имя пользователя по умолчанию, или например, попытается получить имя пользователя из какого-то другого места. У нас недостаточно информации о том, чего пытается достичь вызывающий код, поэтому мы пробрасываем всю информацию об успехе или ошибке наверх для её правильной обработки.

Такая схема распространения ошибок настолько распространена в Rust, что Rust предоставляет оператор вопросительный знак ? для простоты.

Сокращение для проброса ошибок: оператор ?

Код программы 9-6 показывает реализацию функции read_username_from_file, функционал которой аналогичен коду программы 9-5, но реализация использует оператор ? :

Файл: src/main.rs


#![allow(unused)]
fn main() {
use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
    let mut f = File::open("hello.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}
}

Листинг 9-7: Функция, которая возвращает ошибки в вызывающий код, используя оператор ?

Оператор ?, помещаемый после значения типа Result, работает почти таким же образом, как выражение match, которое мы определили для обработки значений типа Result в листинге 9-6. Если значение Result равно Ok, значение внутри Ok будет возвращено из этого выражения и программа продолжит выполнение. Если значение является Err, то Err будет возвращено из всей функции, как если бы мы использовали ключевое слово return, таким образом ошибка передаётся в вызывающий код.

Имеется разница между тем, что делает выражение match листинга 9-6 и оператор ?. Ошибочные значения при выполнении методов с оператором ? возвращаются через функцию from, определённую в типаже From стандартной библиотеки. Данный типаж используется для конвертирования ошибок одного типа в ошибки другого типа. Когда оператор ? вызывает функцию from, то полученный тип ошибки конвертируется в тип ошибки, который определён для возврата в текущей функции. Это удобно, когда функция возвращает один тип ошибки для представления всех возможных вариантов, из-за которых она может не завершиться успешно, даже если части кода функции могут не выполниться по разным причинам. Если каждый тип ошибки реализует функцию from определяя, как конвертировать себя в возвращаемый тип ошибки, то оператор ? позаботится об этой конвертации автоматически.

В коде примера 9-7 оператор ? в конце вызова функции File::open возвращает значения содержимого Ok в переменную f. Если же в при работе этой функции произошла ошибка, оператор ? произведёт ранний возврат из функции со значением Err. То же касается ? на конце вызова read_to_string.

Использование оператора ? позволят уменьшить количество строк кода и сделать реализацию проще. Написанный в предыдущем примере код можно
сделать ещё короче с помощью сокращения промежуточных переменных и конвейерного вызова нескольких методов подряд, как показано в листинге 9-8:

Файл: src/main.rs


#![allow(unused)]
fn main() {
use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
    let mut s = String::new();

    File::open("hello.txt")?.read_to_string(&mut s)?;

    Ok(s)
}
}

Листинг 9-8. Цепочка вызовов методов после оператора ?

Мы перенесли в начало функции создание новой переменной s типа String; эта часть не изменилась. Вместо создания переменной f мы добавили вызов read_to_string непосредственно к результату File::open("hello.txt")?, У нас ещё есть ? в конце вызова read_to_string, и мы по-прежнему возвращаем значение Ok, содержащее имя пользователя в s когда оба метода: File::open и read_to_string успешны, а не возвращают ошибки. Функциональность снова такая же, как в листинге 9-6 и листинге 9-7; это просто другой, более эргономичный способ решения той же задачи.

Продолжая рассматривать разные способы записи данной функции, листинг 9-9 показывает способ сделать её ещё короче.

Файл: src/main.rs


#![allow(unused)]
fn main() {
use std::fs;
use std::io;

fn read_username_from_file() -> Result<String, io::Error> {
    fs::read_to_string("hello.txt")
}
}

Листинг 9-9: Использование fs::read_to_string вместо открытия и чтения файла

Чтение файла в строку довольно распространённая операция, так что Rust предоставляет удобную функцию fs::read_to_string, которая открывает файл, создаёт новую String, читает содержимое файла, размещает его в String и возвращает её. Конечно, использование функции fs::read_to_string не даёт возможности объяснить обработку всех ошибок, поэтому мы сначала изучили длинный способ.

Оператор ? можно использовать для функций возвращающих Result

Оператор ? может использоваться в функциях, которые имеют возвращаемый тип Result, потому что он работает так же, как выражение match, определённое в листинге 9-6. Той частью match, которая требует возвращаемый тип Result, является код return Err(e), таким образом возвращаемый тип функции может быть Result, чтобы быть совместимым с этим return.

Посмотрим что происходит, если использовать оператор ? в теле функции main, которая, как вы помните, имеет возвращаемый тип ():

use std::fs::File;

fn main() {
    let f = File::open("hello.txt")?;
}

При компиляции этого кода, мы получим следующее сообщение об ошибке:

$ 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 `Try`)
 --> src/main.rs:4:13
  |
3 | / fn main() {
4 | |     let f = 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 `Try` is not implemented for `()`
  = note: required by `from_error`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling`

To learn more, run the command again with --verbose.

Эта ошибка указывает на то, что разрешено использовать оператор ? только в функциях, которые возвращают Result или Option или другой тип, который реализует типаж std::ops::Try. Если вы пишете функцию, которая не возвращает один из этих типов, и хотите использовать ? при вызове других функций, возвращающих Result<T, E>, у вас есть два варианта решения этой проблемы. Один из методов - изменить тип возвращаемого значения вашей функции на Result<T, E>, при условии что у вас нет ограничений, препятствующих этому. Другая техника заключается в использовании match или одного из методов Result<T, E> для обработки Result<T, E> любым подходящим способом.

Функция main является специальной и имеются ограничение на то, какой должен быть её возвращаемый тип. Один из допустимых типов для main это (), другой - Result<T, E>, как в примере:

use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
    let f = File::open("hello.txt")?;

    Ok(())
}

Тип Box<dyn Error> называется типаж объектом, о котором мы поговорим в разделе "Использование типаж объектов, которые допускают значения различных типов" Главы 17. А пока вы можете читать обозначение Box<dyn Error> как "любая ошибка". Использование ? в main функции с этим возвращаемым типом также разрешено.

Теперь, когда мы обсудили детали вызова panic! или возврата Result, давайте вернёмся к тому, как решить, какой из случаев подходит для какой ситуации.