Статьи

Objective-C сжато: исключения и ошибки

В Objective-C есть два типа ошибок, которые могут возникнуть во время работы программы. Неожиданные ошибки — это «серьезные» ошибки программирования, которые обычно приводят к преждевременному завершению работы программы. Это так называемые исключения , так как они представляют исключительное условие в вашей программе. С другой стороны, ожидаемые ошибки возникают естественным образом в ходе выполнения программы и могут использоваться для определения успеха операции. Они упоминаются как ошибки .

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

Рисунок 34 Поток управления для исключений и ошибок

Поток управления для исключений и ошибок

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


Основным преимуществом возможностей обработки исключений в Objective-C является возможность отделить обработку ошибок от обнаружения ошибок. Когда часть кода встречает исключение, он может «выбросить» его в ближайший блок обработки ошибок, который может «перехватить» определенные исключения и обработать их соответствующим образом. Тот факт, что исключения могут быть выброшены из произвольных местоположений, устраняет необходимость постоянно проверять сообщения об успехе или сбое каждой функции, участвующей в конкретной задаче.

@try @catch() @try , @catch() и @finally используются для перехвата и обработки исключений, а директива @throw — для их обнаружения. Если вы работали с исключениями в C #, эти конструкции обработки исключений должны быть вам знакомы.

Важно отметить, что в Objective-C исключения относительно медленные. В результате их использование должно быть ограничено выявлением серьезных ошибок программирования, а не для базового потока управления. Если вы пытаетесь определить, что делать на основе ожидаемой ошибки (например, не удается загрузить файл), обратитесь к разделу « Обработка ошибок ».

Исключения представлены как экземпляры класса 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 останавливает программу и указывает вам на строку, которая вызвала проблему.

Рисунок 35 Отмена программы из-за необработанного исключения

Отмена программы из-за необработанного исключения

Далее мы узнаем, как перехватывать исключения и предотвращать завершение программы.

Для обработки исключения любой код, который может привести к исключению, должен быть помещен в блок @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 инкапсулирует всю необходимую информацию для представления ошибок:

  • codeNSInteger , представляющий уникальный идентификатор ошибки.
  • domain — экземпляр NSString определяющий домен для ошибки (более подробно описан в следующем разделе).
  • userInfo — экземпляр NSDictionary который содержит информацию о приложении, связанную с ошибкой. Это обычно используется гораздо больше, чем словарь NSException .

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

  • localizedDescriptionNSString содержащая полное описание ошибки, которая обычно включает причину сбоя. Это значение обычно отображается пользователю на панели предупреждений.
  • localizedFailureReasonNSString содержащая автономное описание причины ошибки. Это используется только клиентами, которые хотят изолировать причину ошибки от ее полного описания.
  • recoverySuggestionNSString пользователю, как восстанавливаться после ошибки.
  • localizedRecoveryOptionsNSArray заголовков, используемых для кнопок диалога ошибки. Если этот массив пуст, отображается одна кнопка OK , чтобы отключить предупреждение.
  • helpAnchorhelpAnchor отображаемая, когда пользователь нажимает кнопку привязки справки на панели предупреждений.

Как и в случае с NSException , метод initWithDomain:code:userInfo можно использовать для инициализации пользовательских экземпляров NSError .

Домен ошибок подобен пространству имен для кодов ошибок. Коды должны быть уникальными в пределах одного домена, но они могут перекрываться с кодами из других доменов. Помимо предотвращения кодовых коллизий, домены также предоставляют информацию о том, откуда возникла ошибка. Четыре основных встроенных домена ошибок: NSMachErrorDomain , NSPOSIXErrorDomain , NSOSStatusErrorDomain и NSCocoaErrorDomain . NSCocoaErrorDomain содержит коды ошибок для многих стандартных платформ Apple Objective-C; однако, есть некоторые платформы, которые определяют свои собственные домены (например, NSXMLParserErrorDomain ).

Если вам нужно создать собственные коды ошибок для своих библиотек и приложений, вы всегда должны добавлять их в свой собственный домен ошибок — никогда не расширять ни один из встроенных доменов. Создание собственного домена — довольно тривиальная работа. Поскольку домены являются просто строками, все, что вам нужно сделать, это определить строковую константу, которая не конфликтует ни с одним из других доменов ошибок в вашем приложении. Apple предлагает, чтобы домены имели вид com.<company>.<project>.ErrorDomain .

Не существует специальных языковых конструкций для обработки экземпляров NSError (хотя несколько встроенных классов предназначены для их обработки). Они предназначены для использования в сочетании со специально разработанными функциями, которые возвращают объект в случае успеха и nil случае неудачи. Общая процедура обнаружения ошибок следующая:

  1. NSError переменную NSError . Вам не нужно выделять или инициализировать его.
  2. Передайте эту переменную как двойной указатель на функцию, которая может привести к ошибке. Если что-то пойдет не так, функция будет использовать эту ссылку для записи информации об ошибке.
  3. Проверьте возвращаемое значение этой функции для успеха или неудачи. Если операция завершилась неудачно, вы можете использовать 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 .