В Objective-C есть два типа ошибок, которые могут возникнуть во время работы программы. Неожиданные ошибки — это «серьезные» ошибки программирования, которые обычно приводят к преждевременному завершению работы программы. Это так называемые исключения , так как они представляют исключительное условие в вашей программе. С другой стороны, ожидаемые ошибки возникают естественным образом в ходе выполнения программы и могут использоваться для определения успеха операции. Они упоминаются как ошибки .
Вы также можете приблизить различие между исключениями и ошибками как разницу в целевой аудитории. Как правило, исключения используются для информирования программиста о том, что произошло не так, в то время как ошибки используются для информирования пользователя о том, что запрошенное действие не может быть выполнено.
Например, попытка получить доступ к индексу массива, который не существует, является исключением (ошибка программиста), тогда как невозможность открыть файл является ошибкой (ошибка пользователя). В первом случае, вероятно, что-то пошло не так в потоке вашей программы, и оно, вероятно, должно закрыться вскоре после исключения. В последнем случае вы хотели бы сообщить пользователю, что файл не может быть открыт, и, возможно, попросить повторить действие, но нет причины, по которой ваша программа не сможет продолжить работу после ошибки.
Обработка исключений
Основным преимуществом возможностей обработки исключений в Objective-C является возможность отделить обработку ошибок от обнаружения ошибок. Когда часть кода встречает исключение, он может «выбросить» его в ближайший блок обработки ошибок, который может «перехватить» определенные исключения и обработать их соответствующим образом. Тот факт, что исключения могут быть выброшены из произвольных местоположений, устраняет необходимость постоянно проверять сообщения об успехе или сбое каждой функции, участвующей в конкретной задаче.
@try
@catch()
@try
, @catch()
и @finally
используются для перехвата и обработки исключений, а директива @throw
— для их обнаружения. Если вы работали с исключениями в C #, эти конструкции обработки исключений должны быть вам знакомы.
Важно отметить, что в Objective-C исключения относительно медленные. В результате их использование должно быть ограничено выявлением серьезных ошибок программирования, а не для базового потока управления. Если вы пытаетесь определить, что делать на основе ожидаемой ошибки (например, не удается загрузить файл), обратитесь к разделу « Обработка ошибок ».
Класс NSException
Исключения представлены как экземпляры класса NSException
или его подкласса. Это удобный способ инкапсулировать всю необходимую информацию, связанную с исключением. Три свойства, которые составляют исключение, описываются следующим образом:
-
name
— экземплярNSString
который однозначно идентифицирует исключение. -
reason
— экземплярNSString
содержащий удобочитаемое описание исключения. -
userInfo
— экземплярNSDictionary
который содержит специфичную для приложения информацию, связанную с исключением.
Платформа Foundation определяет несколько констант, которые определяют «стандартные» имена исключений . Эти строки можно использовать для проверки того, какой тип исключения был пойман.
Вы также можете использовать метод initWithName:reason:userInfo:
initialization для создания новых объектов исключений со своими собственными значениями. Объекты пользовательских исключений могут быть перехвачены и выброшены с использованием тех же методов, которые описаны в следующих разделах.
Генерация исключений
Давайте начнем с рассмотрения поведения программы по умолчанию при обработке исключений. objectAtIndex:
метод NSArray
определен для выброса NSRangeException
(подкласс NSException
) при попытке доступа к индексу, который не существует. Итак, если вы запросите 10- й элемент массива, который имеет только три элемента, у вас будет исключение для экспериментов:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSArray *crew = [NSArray arrayWithObjects:
@»Dave»,
@»Heywood»,
@»Frank», nil];
// This will throw an exception.
NSLog(@»%@», [crew objectAtIndex:10]);
}
return 0;
}
|
Когда он встречает необработанное исключение, XCode останавливает программу и указывает вам на строку, которая вызвала проблему.
Далее мы узнаем, как перехватывать исключения и предотвращать завершение программы.
Ловить исключения
Для обработки исключения любой код, который может привести к исключению, должен быть помещен в блок @try
. Затем вы можете перехватывать определенные исключения, используя директиву @catch()
. Если вам нужно выполнить какой-либо служебный код, вы можете разместить его в блоке @finally
. В следующем примере показаны все три из этих директив обработки исключений:
01
02
03
04
05
06
07
08
09
10
|
@try {
NSLog(@»%@», [crew objectAtIndex:10]);
}
@catch (NSException *exception) {
NSLog(@»Caught an exception»);
// We’ll just silently ignore the exception.
}
@finally {
NSLog(@»Cleaning up»);
}
|
Это должно вывести следующее в вашей консоли XCode:
1
2
3
4
|
Caught an exception!
Name: NSRangeException
Reason: *** -[__NSArrayI objectAtIndex:]: index 10 beyond bounds [0 .. 2]
Cleaning up
|
Когда программа встречает сообщение [crew objectAtIndex:10]
, она генерирует NSRangeException
, которое @catch()
директиве @catch()
. Внутри блока @catch()
исключение фактически обрабатывается. В этом случае мы просто отображаем описательное сообщение об ошибке, но в большинстве случаев вы, вероятно, захотите написать некоторый код для решения проблемы.
Когда в блоке @try
встречается @try
, программа переходит к соответствующему @catch()
, что означает, что любой код после @catch()
исключения не будет выполнен. Это создает проблему, если блок @try
нуждается в некоторой очистке (например, если он открыл файл, этот файл должен быть закрыт). Блок @finally
решает эту проблему, поскольку он гарантированно выполняется независимо от того, произошло ли исключение. Это делает его идеальным местом для завязывания любых свободных концов из блока @try
.
@catch()
после директивы @catch()
позволяют вам определить, какой тип исключения вы пытаетесь перехватить. В данном случае это NSException
, который является стандартным классом исключений. Но исключением может быть любой класс, а не NSException
. Например, следующая директива @catch()
будет обрабатывать универсальный объект:
1
|
@catch (id genericException)
|
В следующем разделе мы узнаем, как NSException
экземпляры NSException
а также общие объекты.
Бросать исключения
Когда вы обнаруживаете исключительное условие в своем коде, вы создаете экземпляр NSException
и заполняете его соответствующей информацией. Затем вы бросаете его, используя метко названную директиву @throw
, предлагая ближайший @try
/ @catch
для обработки.
Например, в следующем примере определяется функция для генерации случайных чисел между заданным интервалом. Если вызывающая сторона передает недопустимый интервал, функция выдает пользовательскую ошибку.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
#import <Foundation/Foundation.h>
int generateRandomInteger(int minimum, int maximum) {
if (minimum >= maximum) {
// Create the exception.
NSException *exception = [NSException
exceptionWithName:@»RandomNumberIntervalException»
reason:@»*** generateRandomInteger(): «
«maximum parameter not greater than minimum parameter»
userInfo:nil];
// Throw the exception.
@throw exception;
}
// Return a random integer.
return arc4random_uniform((maximum — minimum) + 1) + minimum;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
int result = 0;
@try {
result = generateRandomInteger(0, 10);
}
@catch (NSException *exception) {
NSLog(@»Problem!!! Caught exception: %@», [exception name]);
}
NSLog(@»Random Number: %i», result);
}
return 0;
}
|
Поскольку этот код передает допустимый интервал ( 0, 10
) для generateRandomInteger()
, он не будет иметь исключение для перехвата. Однако, если вы измените интервал на что-то вроде ( 0, -10
), вы увидите блок @catch()
в действии. По сути, это то, что происходит NSRangeException
когда классы фреймворка сталкиваются с исключениями (например, NSRangeException
созданный NSArray
).
Также возможно перебрасывать исключения, которые вы уже поймали. Это полезно, если вы хотите получить информацию о том, что произошло конкретное исключение, но не обязательно хотите обрабатывать его самостоятельно. Для удобства вы можете даже опустить аргумент в директиве @throw
:
1
2
3
4
5
6
7
8
9
|
@try {
result = generateRandomInteger(0, -10);
}
@catch (NSException *exception) {
NSLog(@»Problem!!! Caught exception: %@», [exception name]);
// Re-throw the current exception.
@throw
}
|
Это передает перехваченное исключение до следующего наивысшего обработчика, который в данном случае является обработчиком исключений верхнего уровня. Это должно отобразить вывод из нашего @catch()
, а также из Terminating app due to uncaught exception...
по умолчанию Terminating app due to uncaught exception...
, за которым следует внезапный выход.
Директива @throw
не ограничивается объектами NSException
— она может генерировать буквально любой объект. В следующем примере объект NSNumber
выбрасывается вместо обычного исключения. Также обратите внимание, как вы можете ориентироваться на разные объекты, добавив несколько @catch()
после блока @try
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
#import <Foundation/Foundation.h>
int generateRandomInteger(int minimum, int maximum) {
if (minimum >= maximum) {
// Generate a number using «default» interval.
NSNumber *guess = [NSNumber
numberWithInt:generateRandomInteger(0, 10)];
// Throw the number.
@throw guess;
}
// Return a random integer.
return arc4random_uniform((maximum — minimum) + 1) + minimum;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
int result = 0;
@try {
result = generateRandomInteger(30, 10);
}
@catch (NSNumber *guess) {
NSLog(@»Warning: Used default interval»);
result = [guess intValue];
}
@catch (NSException *exception) {
NSLog(@»Problem!!! Caught exception: %@», [exception name]);
}
NSLog(@»Random Number: %i», result);
}
return 0;
}
|
Вместо того, чтобы бросать объект NSException
, generateRandomInteger()
пытается генерировать новое число между некоторыми границами «по умолчанию». В этом примере показано, как @throw
может работать с различными типами объектов, но, строго говоря, это не лучший дизайн приложения и не самое эффективное использование инструментов обработки исключений в Objective-C. Если бы вы действительно планировали использовать выброшенное значение, как в предыдущем коде, вам лучше было бы использовать простую старую условную проверку с использованием NSError
, как обсуждалось в следующем разделе.
Кроме того, некоторые из базовых структур Apple ожидают, что будет NSException
объект NSException
, поэтому будьте осторожны с пользовательскими объектами при интеграции со стандартными библиотеками.
Обработка ошибок
Принимая во внимание, что исключения предназначены для того, чтобы программисты знали, когда что-то пошло не так, ошибки — это эффективный и простой способ проверить, успешно выполнено действие или нет. В отличие от исключений, ошибки предназначены для использования в ваших ежедневных инструкциях потока управления.
Класс NSError
Общим для ошибок и исключений является то, что они оба реализованы как объекты. Класс NSError
инкапсулирует всю необходимую информацию для представления ошибок:
-
code
—NSInteger
, представляющий уникальный идентификатор ошибки. -
domain
— экземплярNSString
определяющий домен для ошибки (более подробно описан в следующем разделе). -
userInfo
— экземплярNSDictionary
который содержит информацию о приложении, связанную с ошибкой. Это обычно используется гораздо больше, чем словарьNSException
.
В дополнение к этим основным атрибутам, NSError
также хранит несколько значений, предназначенных для помощи при рендеринге и обработке ошибок. Все это на самом деле ярлыки в словарь userInfo
описанный в предыдущем списке.
-
localizedDescription
—NSString
содержащая полное описание ошибки, которая обычно включает причину сбоя. Это значение обычно отображается пользователю на панели предупреждений. -
localizedFailureReason
—NSString
содержащая автономное описание причины ошибки. Это используется только клиентами, которые хотят изолировать причину ошибки от ее полного описания. -
recoverySuggestion
—NSString
пользователю, как восстанавливаться после ошибки. -
localizedRecoveryOptions
—NSArray
заголовков, используемых для кнопок диалога ошибки. Если этот массив пуст, отображается одна кнопка OK , чтобы отключить предупреждение. -
helpAnchor
—helpAnchor
отображаемая, когда пользователь нажимает кнопку привязки справки на панели предупреждений.
Как и в случае с NSException
, метод initWithDomain:code:userInfo
можно использовать для инициализации пользовательских экземпляров NSError
.
Домены ошибок
Домен ошибок подобен пространству имен для кодов ошибок. Коды должны быть уникальными в пределах одного домена, но они могут перекрываться с кодами из других доменов. Помимо предотвращения кодовых коллизий, домены также предоставляют информацию о том, откуда возникла ошибка. Четыре основных встроенных домена ошибок: NSMachErrorDomain
, NSPOSIXErrorDomain
, NSOSStatusErrorDomain
и NSCocoaErrorDomain
. NSCocoaErrorDomain
содержит коды ошибок для многих стандартных платформ Apple Objective-C; однако, есть некоторые платформы, которые определяют свои собственные домены (например, NSXMLParserErrorDomain
).
Если вам нужно создать собственные коды ошибок для своих библиотек и приложений, вы всегда должны добавлять их в свой собственный домен ошибок — никогда не расширять ни один из встроенных доменов. Создание собственного домена — довольно тривиальная работа. Поскольку домены являются просто строками, все, что вам нужно сделать, это определить строковую константу, которая не конфликтует ни с одним из других доменов ошибок в вашем приложении. Apple предлагает, чтобы домены имели вид com.<company>.<project>.ErrorDomain
.
Захват ошибок
Не существует специальных языковых конструкций для обработки экземпляров NSError
(хотя несколько встроенных классов предназначены для их обработки). Они предназначены для использования в сочетании со специально разработанными функциями, которые возвращают объект в случае успеха и nil
случае неудачи. Общая процедура обнаружения ошибок следующая:
-
NSError
переменнуюNSError
. Вам не нужно выделять или инициализировать его. - Передайте эту переменную как двойной указатель на функцию, которая может привести к ошибке. Если что-то пойдет не так, функция будет использовать эту ссылку для записи информации об ошибке.
- Проверьте возвращаемое значение этой функции для успеха или неудачи. Если операция завершилась неудачно, вы можете использовать
NSError
для самостоятельной обработки ошибки или ее отображения пользователю.
Как вы можете видеть, функция обычно не возвращает объект NSError
— она возвращает любое значение, которое должно быть в случае успеха, в противном случае она возвращает nil
. Вы всегда должны использовать возвращаемое значение функции для обнаружения ошибок — никогда не используйте наличие или отсутствие объекта NSError
чтобы проверить, успешно ли выполнено действие. Предполагается, что объекты ошибок описывают только потенциальную ошибку, а не сообщают о ее возникновении.
В следующем примере демонстрируется реалистичный вариант использования NSError
. Он использует метод загрузки файлов NSString
, который на самом деле выходит за рамки книги. В книге iOS Succinctly подробно рассказывается об управлении файлами, но сейчас давайте сосредоточимся на возможностях обработки ошибок в Objective-C.
Сначала мы генерируем путь к файлу, указывающий на ~/Desktop/SomeContent.txt.
Затем мы создаем ссылку NSError
и передаем ее в stringWithContentsOfFile:encoding:error:
метод для сбора информации обо всех ошибках, возникающих при загрузке файла. Обратите внимание, что мы передаем ссылку на указатель *error
, что означает, что метод запрашивает указатель на указатель (т.е. двойной указатель). Это позволяет методу заполнять переменную собственным содержимым. Наконец, мы проверяем возвращаемое значение (а не существование переменной error
), чтобы увидеть, если stringWithContentsOfFile:encoding:error:
успешно или нет. Если это так, можно безопасно работать со значением, хранящимся в переменной content
; в противном случае мы используем переменную error
для отображения информации о том, что пошло не так.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// Generate the desired file path.
NSString *filename = @»SomeContent.txt»;
NSArray *paths = NSSearchPathForDirectoriesInDomains(
NSDesktopDirectory, NSUserDomainMask, YES
);
NSString *desktopDir = [paths objectAtIndex:0];
NSString *path = [desktopDir
stringByAppendingPathComponent:filename];
// Try to load the file.
NSError *error;
NSString *content = [NSString stringWithContentsOfFile:path
encoding:NSUTF8StringEncoding
error:&error];
// Check if it worked.
if (content == nil) {
// Some kind of error occurred.
NSLog(@»Error loading file %@!», path);
NSLog(@»Description: %@», [error localizedDescription]);
NSLog(@»Reason: %@», [error localizedFailureReason]);
} else {
// Content loaded successfully.
NSLog(@»Content loaded!»);
NSLog(@»%@», content);
}
}
return 0;
}
|
Поскольку файл ~/Desktop/SomeContent.txt
вероятно, не существует на вашем компьютере, этот код, скорее всего, приведет к ошибке. Все, что вам нужно сделать, чтобы загрузка прошла успешно, это создать SomeContent.txt
на вашем рабочем столе.
Пользовательские ошибки
Настраиваемые ошибки можно настроить, приняв двойной указатель на объект NSError
и NSError
его самостоятельно. Помните, что ваша функция или метод должны возвращать либо объект, либо nil
, в зависимости от того, успешно он или нет (не возвращайте ссылку NSError
).
В следующем примере вместо исключения используется ошибка, чтобы смягчить недопустимые параметры в функции generateRandomInteger()
. Обратите внимание, что **error
— это двойной указатель, который позволяет нам заполнить базовую переменную из функции. Очень важно проверить, что пользователь действительно передал допустимый **error
параметр **error
с помощью if (error != NULL)
. Вы всегда должны делать это в своих собственных функциях, генерирующих ошибки. Поскольку параметр **error
является двойным указателем, мы можем присвоить значение базовой переменной с помощью *error
. И снова, мы проверяем ошибки, используя возвращаемое значение ( if (result == nil)
), а не переменную error
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
#import <Foundation/Foundation.h>
NSNumber *generateRandomInteger(int minimum, int maximum, NSError **error) {
if (minimum >= maximum) {
if (error != NULL) {
// Create the error.
NSString *domain = @»com.MyCompany.RandomProject.ErrorDomain»;
int errorCode = 4;
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
[userInfo setObject:@»Maximum parameter is not greater than minimum parameter»
forKey:NSLocalizedDescriptionKey];
// Populate the error reference.
*error = [[NSError alloc] initWithDomain:domain
code:errorCode
userInfo:userInfo];
}
return nil;
}
// Return a random integer.
return [NSNumber
numberWithInt:arc4random_uniform((maximum — minimum) + 1) + minimum];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSError *error;
NSNumber *result = generateRandomInteger(0, -10, &error);
if (result == nil) {
// Check to see what went wrong.
NSLog(@»An error occurred!»);
NSLog(@»Domain: %@ Code: %li», [error domain], [error code]);
NSLog(@»Description: %@», [error localizedDescription]);
} else {
// Safe to use the returned value.
NSLog(@»Random Number: %i», [result intValue]);
}
}
return 0;
}
|
Все localizedDescription
, localizedFailureReason
и связанные свойства NSError
на самом деле хранятся в его словаре userInfo
с использованием специальных ключей, определенных NSLocalizedDescriptionKey
, NSLocalizedFailureReasonErrorKey
и т. Д. Итак, все, что нам нужно сделать, чтобы описать ошибку, это добавить несколько строк в соответствующие ключи, как показано в последнем примере.
Как правило, вы хотите определить константы для пользовательских доменов ошибок и кодов, чтобы они были согласованы между классами.
Резюме
В этой главе подробно обсуждаются различия между исключениями и ошибками. Исключения предназначены для информирования программистов о фатальных проблемах в их программе, тогда как ошибки представляют собой неудачное действие пользователя. Как правило, готовое к работе приложение не должно генерировать исключения, за исключением случаев, когда возникают действительно исключительные обстоятельства (например, нехватка памяти на устройстве).
Мы рассмотрели базовое использование NSError
, но имейте в виду, что есть несколько встроенных классов, предназначенных для обработки и отображения ошибок. К сожалению, все они являются графическими компонентами, и поэтому выходят за рамки этой книги. Продолжение iOS Succinctly имеет специальный раздел, посвященный отображению и устранению ошибок.
В заключительной главе Objective-C Вкратце мы обсудим одну из наиболее запутанных тем в Objective-C. Мы узнаем, как блоки позволяют нам относиться к функциональности так же, как к данным . Это будет иметь далеко идущие последствия для того, что возможно в приложении Objective-C.
Этот урок представляет собой главу из Objective-C, лаконично , бесплатную электронную книгу от команды Syncfusion .