Статьи

Обработка ошибок в Swift 2

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

В Objective-C было слишком легко игнорировать обработку ошибок. Взгляните на следующий пример, в котором я игнорирую любые ошибки, которые могут возникнуть в результате выполнения запроса на выборку.

1
2
3
4
5
6
7
// Execute Fetch Request
NSArray *results = [managedObjectContext executeFetchRequest:fetchRequest error:nil];
 
if (results) {
    // Process Results
    …
}

Приведенный выше пример показывает, что обработка ошибок в Objective-C — это то, что разработчик должен выбрать. Если вы хотите узнать, что пошло не так, если что-то пошло не так, то вы сообщите об этом API, передав ему указатель NSError . Пример ниже иллюстрирует, как это работает в Objective-C.

01
02
03
04
05
06
07
08
09
10
11
12
13
NSError *error = nil;
 
// Execute Fetch Request
NSArray *results = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
 
if (error) {
    // Handle Error
    …
 
} else {
    // Process Results
    …
}

В то время как более ранние версии Swift не предлагали хорошего решения для обработки ошибок, Swift 2 дал нам то, о чем мы просили, и это стоило ожидания. В Swift 2 обработка ошибок включена по умолчанию. В отличие от Objective-C, разработчики должны явно указать компилятору, если они решат игнорировать обработку ошибок. Хотя это не заставит разработчиков принимать обработку ошибок, оно делает решение явным.

Если бы мы перевели приведенный выше пример на Swift, мы бы получили такое же количество строк. Несмотря на то, что объем кода, который вам нужно написать, остается неизменным, синтаксис делает его очень четким, что вы пытаетесь сделать.

01
02
03
04
05
06
07
08
09
10
11
12
13
do {
    // Execute Fetch Request
    let results = try managedObjectContext.executeFetchRequest(fetchRequest)
 
    // Process Results
    …
 
} catch {
    let fetchError = error as NSError
 
    // Handle Error
    …
}

В конце этого урока вы поймете приведенный выше фрагмент кода и узнаете все, что вам нужно знать для обработки ошибок в Swift 2.

Основой обработки ошибок в Swift является способность функций и методов генерировать ошибки. На языке Swift функция, которая может генерировать ошибки, называется функцией выброса. Определение функции бросающей функции очень ясно показывает эту способность, как показано в следующем примере.

1
init(contentsOfURL url: NSURL, options readOptionsMask: NSDataReadingOptions) throws

Ключевое слово throws указывает, что init(contentsOfURL:options:) может выдать ошибку, если что-то пойдет не так. Если вы вызываете функцию throwing, компилятор выдаст ошибку, говоря об иронии. Это почему?

1
let data = NSData(contentsOfURL: URL, options: [])

Создатели Swift уделили много внимания тому, чтобы сделать язык выразительным, и обработка ошибок именно такова, выразительна. Если вы попытаетесь вызвать функцию, которая может выдать ошибку, вызову функции должно предшествовать ключевое слово try . Ключевое слово try не является волшебным. Все, что он делает, это заставляет разработчика осознавать способность функции кидать.

Обработка ошибок без попытки

Подожди секунду. Компилятор продолжает жаловаться, хотя мы предшествовали вызову функции с ключевым словом try . Чего нам не хватает?

Обработка ошибок с помощью try

Компилятор видит, что мы используем ключевое слово try , но правильно указывает на то, что у нас нет возможности отследить любые ошибки, которые могут быть сгенерированы. Чтобы отлавливать ошибки, мы используем новое заявление Свифта do-catch .

Если функция выброса выдает ошибку, ошибка будет автоматически распространяться за пределы текущей области, пока не будет обнаружена. Это похоже на исключения в Objective-C и других языках. Идея состоит в том, что ошибка должна быть поймана и обработана в некоторый момент. Более конкретно, ошибка распространяется до тех пор, пока она не будет catch предложением do-catch оператора do-catch .

В обновленном примере ниже мы вызываем методы init(contentsOfURL:options:) в операторе do-catch . В предложении do мы вызываем функцию, используя ключевое слово try . В предложении catch мы обрабатываем все ошибки, возникшие при выполнении функции. Это образец, который очень распространен в Swift 2.

1
2
3
4
5
do {
    let data = try NSData(contentsOfURL: URL, options: [])
} catch {
    print(«\(error)»)
}

В предложении catch вас есть доступ к ошибке, которая возникла из-за локальной постоянной error . Предложение catch гораздо более мощное, чем показано в приведенном выше примере. Мы рассмотрим более интересный пример чуть позже.

В Objective-C вы обычно используете NSError , определенный в платформе Foundation, для обработки ошибок. Поскольку язык не определяет, как следует реализовывать обработку ошибок, вы можете сами определять свой класс или структуру для создания ошибок.

Это не правда в Swift. Хотя любой класс или структура могут выступать в качестве ошибки, они должны соответствовать протоколу ErrorType . Протокол, однако, довольно легко реализовать, так как он не объявляет какие-либо методы или свойства.

Перечисления являются мощными в Swift, и они хорошо подходят для обработки ошибок. Перечисления отлично подходят для функциональности сопоставления с образцом в предложении do-catch оператора do-catch . Проще проиллюстрировать это на примере. Начнем с определения перечисления, соответствующего протоколу ErrorType .

1
2
3
4
5
6
enum PrinterError: ErrorType {
    case NoToner
    case NoPaper
    case NotResponding
    case MaintenanceRequired
}

Мы определяем enum, PrinterError , который соответствует протоколу ErrorType . Перечисление имеет четыре переменных-члена. Теперь мы можем определить функцию для печати документа. Мы передаем функции экземпляр NSData и сообщаем компилятору, что она может генерировать ошибки, используя ключевое слово throws .

1
2
3
func printDocumentWithData(data: NSData) throws {
    …
}

Чтобы напечатать документ, мы вызываем printDocumentWithData(_:) . Как мы видели ранее, нам нужно использовать ключевое слово try и заключить вызов функции в оператор do-catch . В приведенном ниже примере мы обрабатываем любые ошибки в предложении catch .

1
2
3
4
5
do {
    try printDocumentWithData(data)
} catch {
    // Handle Errors
}

Мы можем улучшить пример, проверив выдавшуюся ошибку. Предложение catch похоже на оператор switch в том смысле, что оно допускает сопоставление с образцом. Посмотрите на обновленный пример ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
do {
    try printDocumentWithData(data)
     
} catch PrinterError.NoToner {
    // Notify User
     
} catch PrinterError.NoPaper {
    // Notify User
     
} catch PrinterError.NotResponding {
    // Schedule New Attempt
     
}

Это выглядит намного лучше. Но есть одна проблема. Компилятор уведомляет нас, что мы не обрабатываем каждую возможную ошибку, которую может printDocumentWithData(_:) метод printDocumentWithData(_:) .

Обработка ошибок должна быть исчерпывающей

Компилятор прав конечно. Предложение catch похоже на оператор switch в том смысле, что оно должно быть исчерпывающим и обрабатывать все возможные случаи. Мы можем добавить еще одно предложение catch для PrinterError.MaintenanceRequired или добавить в конце предложение catch-all. При добавлении предложения catch умолчанию ошибка компилятора должна исчезнуть.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
do {
    try printDocumentWithData(data)
     
} catch PrinterError.NoToner {
    // Notify User
     
} catch PrinterError.NoPaper {
    // Notify User
     
} catch PrinterError.NotResponding {
    // Schedule New Attempt
     
} catch {
    // Handle Any Other Errors
     
}

Чем больше я узнаю о языке Swift, тем больше ценю его. defer заявление — еще одно замечательное дополнение к языку. Название довольно хорошо подводит итог, но позвольте мне показать вам пример, чтобы объяснить концепцию.

01
02
03
04
05
06
07
08
09
10
11
func printDocumentWithData(data: NSData) throws {
    if canPrintData(data) {
        powerOnPrinter()
         
        try printData(data)
         
        defer {
            powerOffPrinter()
        }
    }
}

Пример немного надуманный, но он иллюстрирует использование defer . Блок оператора defer выполняется до того, как выполнение выходит из области, в которой появляется оператор defer. Вы можете прочитать это предложение еще раз.

Это означает, что powerOffPrinter() вызывается, даже если printData(_:) выдает ошибку. Я уверен, что вы можете видеть, что это работает очень хорошо с обработкой ошибок Swift.

Положение оператора defer операторе if не имеет значения. Следующий обновленный пример идентичен в том, что касается компилятора.

01
02
03
04
05
06
07
08
09
10
11
func printDocumentWithData(data: NSData) throws {
    if canPrintData(data) {
        defer {
            powerOffPrinter()
        }
         
        powerOnPrinter()
         
        try printData(data)
    }
}

Вы можете иметь несколько операторов defer если помните, что они выполняются в обратном порядке, в котором они появляются.

Вполне возможно, что вы не хотите обрабатывать ошибку, а вместо этого позволите ей всплыть на объект, который способен или отвечает за обработку ошибки. Это хорошо. Не каждое выражение try должно быть заключено в оператор do-catch . Однако есть одно условие: функция, которая вызывает функцию выброса, должна быть самой функцией выброса. Посмотрите на следующие два примера.

1
2
3
4
5
6
7
8
func printTestDocument() {
    // Load Document Data
    let dataForDocument = NSData(contentsOfFile: «pathtodocument»)
     
    if let data = dataForDocument {
        try printDocumentWithData(data)
    }
}
1
2
3
4
5
6
7
8
func printTestDocument() throws {
    // Load Document Data
    let dataForDocument = NSData(contentsOfFile: «pathtodocument»)
     
    if let data = dataForDocument {
        try printDocumentWithData(data)
    }
}

Первый пример приводит к ошибке компилятора, потому что мы не обрабатываем ошибки, которые может printDocumentWithData(_:) . Мы решили эту проблему во втором примере, отметив printTestDocument() как throwing. Если printDocumentWithData(_:) выдает ошибку, то ошибка передается вызывающей стороне функции printTestDocument() .

В начале этой статьи я писал, что Swift хочет, чтобы вы приняли обработку ошибок, сделав ее простой и интуитивно понятной. Могут быть ситуации, когда вы не хотите или должны обрабатывать возникающие ошибки. Вы решаете остановить распространение ошибок. Это возможно при использовании варианта ключевого слова try! , try! ,

В Swift восклицательный знак всегда служит предупреждением. Восклицательный знак в основном говорит разработчику, что Swift больше не несет ответственности, если что-то пойдет не так. И это то, что try! Ключевое слово говорит вам. Если вы предшествуете вызову функции throw с помощью try! Ключевое слово, также известное как выражение принудительной попытки, распространение ошибок отключено.

Хотя это может звучать фантастически для некоторых из вас, я должен предупредить вас, что это не то, что вы думаете. Если функция throwing выдает ошибку и вы отключили распространение ошибки, вы столкнетесь с ошибкой во время выполнения. Это в основном означает, что ваше приложение будет зависать. Вы были предупреждены.

Команда Swift в Apple приложила немало усилий, чтобы сделать обработку ошибок максимально прозрачной для API Objective-C. Например, вы заметили, что первым примером Swift этого руководства является API Objective-C. Несмотря на то, что API написан на Objective-C, метод не принимает указатель NSError качестве последнего аргумента. Для компилятора это обычный метод броска. Вот как выглядит определение метода в Objective-C.

1
— (NSArray *)executeFetchRequest:(NSFetchRequest *)request error:(NSError **)error;

И это то, как выглядит определение метода в Swift.

1
public func executeFetchRequest(request: NSFetchRequest) throws -> [AnyObject]

Ошибки, которые executeFetchRequest(request: NSFetchRequest) являются экземплярами NSError . Это возможно только потому, что NSError соответствует протоколу ErrorType , как мы обсуждали ранее. Посмотрите на колонку « Соответствует» ниже.

Ссылка на класс NSError

Swift 2 имеет много новых функций и возможностей. Пройдите наш курс по разработке Swift 2, чтобы научиться быстро. Обработка ошибок — лишь малая часть возможностей Swift 2.

Вывод этой статьи — ошибка обработки ошибок в Swift. Если вы обратили внимание, то вы также поняли, что вам нужно будет принять обработку ошибок, если вы решите разрабатывать в Swift. Используя try! Ключевое слово не выведет вас из обработки ошибок. Напротив, слишком частое использование может привести к неприятностям. Попробуйте, и я уверен, что вам это понравится, если вы уделите ему немного времени.