Статьи

Objective-C лаконично: категории и расширения

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

Это мощные функции, которые имеют много потенциальных применений. Во-первых, категории позволяют разделить интерфейс и реализацию класса на несколько файлов, что обеспечивает столь необходимую модульность для больших проектов. Во-вторых, категории позволяют исправлять ошибки в существующем классе (например, NSString ) без необходимости его подкласса. В-третьих, они предоставляют эффективную альтернативу защищенным и закрытым методам, которые есть в C # и других языках, подобных Simula.


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

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

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
// Person.h
@interface Person : NSObject
 
@interface Person : NSObject
@property (readonly) NSMutableArray* friends;
@property (copy) NSString* name;
 
— (void)sayHello;
— (void)sayGoodbye;
 
@end
 
 
// Person.m
#import «Person.h»
 
@implementation Person
 
@synthesize name = _name;
@synthesize friends = _friends;
 
-(id)init{
    self = [super init];
    if(self){
        _friends = [[NSMutableArray alloc] init];
    }
 
    return self;
}
 
— (void)sayHello {
    NSLog(@»Hello, says %@.», _name);
}
 
— (void)sayGoodbye {
    NSLog(@»Goodbye, says %@.», _name);
}
@end

Здесь нет ничего нового — просто класс Person с двумя свойствами (свойство friends будет использоваться нашей категорией) и двумя методами. Далее мы будем использовать категорию для хранения некоторых методов взаимодействия с другими экземплярами Person . Создайте новый файл, но вместо класса используйте шаблон категории Objective-C . Используйте Отношения для имени категории и Персона для Категории в поле:

Рисунок 28 Создание класса PersonRelations

Создание класса Person + Relations

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

01
02
03
04
05
06
07
08
09
10
11
// Person+Relations.h
#import <Foundation/Foundation.h>
#import «Person.h»
 
@interface Person (Relations)
 
— (void)addFriend:(Person *)aFriend;
— (void)removeFriend:(Person *)aFriend;
— (void)sayHelloToFriends;
 
@end

Вместо обычного объявления @interface мы включаем имя категории в круглые скобки после расширяемого имени класса. Имя категории может быть любым, если оно не конфликтует с другими категориями того же класса. Имя файла категории должно быть именем класса, за которым следует знак плюс, за которым следует название категории (например, Person+Relations.h ).

Итак, это определяет интерфейс нашей категории. Любые методы, которые мы добавим сюда, будут добавлены в исходный класс Person во время выполнения. Это будет выглядеть так, как если бы addFriend: removeFriend: и sayHelloToFriends были определены в Person.h , но мы можем сохранить нашу функциональность инкапсулированной и поддерживаемой. Также обратите внимание, что вы должны импортировать заголовок для исходного класса Person.h . Реализация категории происходит по аналогичной схеме:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
// Person+Relations.m
#import «Person+Relations.h»
 
@implementation Person (Relations)
 
— (void)addFriend:(Person *)aFriend {
    [[self friends] addObject:aFriend];
}
 
— (void)removeFriend:(Person *)aFriend {
    [[self friends] removeObject:aFriend];
}
 
— (void)sayHelloToFriends {
    for (Person *friend in [self friends]) {
        NSLog(@»Hello there, %@!», [friend name]);
    }
}
 
@end

Это реализует все методы в Person+Relations.h . Как и интерфейс категории, имя категории отображается в скобках после имени класса. Имя категории в реализации должно совпадать с именем в интерфейсе.

Также обратите внимание, что нет способа определить дополнительные свойства или переменные экземпляра в категории. Категории должны ссылаться на данные, хранящиеся в основном классе ( friends в данном случае).

Также возможно переопределить реализацию, содержащуюся в Person.m , просто переопределив метод в Person+Relations.m . Это может быть использовано для исправления существующего класса; однако, это не рекомендуется, если у вас есть альтернативное решение проблемы, так как не было бы способа переопределить реализацию, определенную категорией. То есть, в отличие от иерархии классов, категории представляют собой плоскую организационную структуру — если вы реализуете один и тот же метод в двух отдельных категориях, среда выполнения не сможет определить, какой из них использовать.

Единственное изменение, которое вы должны сделать, чтобы использовать категорию, это импортировать файл заголовка категории. Как видно из следующего примера, класс Person имеет доступ к методам, определенным в Person.h а также к методам, определенным в категории Person+Relations.h :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
// main.m
#import <Foundation/Foundation.h>
#import «Person.h»
#import «Person+Relations.h»
 
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *joe = [[Person alloc] init];
        joe.name = @»Joe»;
        Person *bill = [[Person alloc] init];
        bill.name = @»Bill»;
        Person *mary = [[Person alloc] init];
        mary.name = @»Mary»;
 
        [joe sayHello];
        [joe addFriend:bill];
        [joe addFriend:mary];
        [joe sayHelloToFriends];
    }
    return 0;
}

И это все, что нужно для создания категорий в Objective-C.

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

Идея проста: объявить «защищенные» методы как категорию в отдельном заголовочном файле. Это дает подклассам возможность «подписаться» на защищенные методы, в то время как несвязанные классы, как обычно, используют «открытый» заголовочный файл. Например, возьмем стандартный интерфейс Ship :

1
2
3
4
5
6
7
8
// Ship.h
#import <Foundation/Foundation.h>
 
@interface Ship : NSObject
 
— (void)shoot;
 
@end

Как мы видели много раз, это определяет публичный метод, который называется shoot . Чтобы объявить защищенный метод, нам нужно создать категорию « Ship » в отдельном заголовочном файле:

1
2
3
4
5
6
7
8
// Ship_Protected.h
#import <Foundation/Foundation.h>
 
@interface Ship(Protected)
 
— (void)prepareToShoot;
 
@end

Любые классы, которым требуется доступ к защищенным методам (а именно, суперкласс и любые подклассы), могут просто импортировать Ship_Protected.h . Например, реализация Ship должна определять поведение по умолчанию для защищенного метода:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
// Ship.m
#import «Ship.h»
#import «Ship_Protected.h»
 
@implementation Ship {
    BOOL _gunIsReady;
}
 
— (void)shoot {
    if (!_gunIsReady) {
        [self prepareToShoot];
        _gunIsReady = YES;
    }
    NSLog(@»Firing!»);
}
 
— (void)prepareToShoot {
    // Execute some private functionality.
    NSLog(@»Preparing the main weapon…»);
}
@end

Обратите внимание, что если бы мы не импортировали Ship_Protected.h , эта реализация prepareToShoot была бы закрытым методом, как обсуждалось в главе « Методы» . Без защищенной категории у подклассов не будет доступа к этому методу. Давайте создадим подкласс Ship чтобы увидеть, как это работает. Мы назовем это ResearchShip :

1
2
3
4
5
6
7
8
// ResearchShip.h
#import «Ship.h»
 
@interface ResearchShip : Ship
 
— (void)extendTelescope;
 
@end

Это обычный интерфейс подкласса — он не должен импортировать защищенный заголовок, так как это сделает защищенные методы доступными для любого, кто импортирует ResearchShip.h , и это именно то, чего мы пытаемся избежать. Наконец, реализация для подкласса импортирует защищенные методы и (необязательно) переопределяет их:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
// ResearchShip.m
#import «ResearchShip.h»
#import «Ship_Protected.h»
 
@implementation ResearchShip
 
— (void)extendTelescope {
    NSLog(@»Extending the telescope»);
}
 
// Override protected method
— (void)prepareToShoot {
    NSLog(@»Oh shoot! We need to find some weapons!»);
}
 
@end

Для обеспечения защищенного статуса методов в Ship_Protected.h другим классам не разрешается его импортировать. Они просто импортируют обычные «публичные» интерфейсы суперкласса и подкласса:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
// main.m
#import <Foundation/Foundation.h>
#import «Ship.h»
#import «ResearchShip.h»
 
int main(int argc, const char * argv[]) {
    @autoreleasepool {
 
        Ship *genericShip = [[Ship alloc] init];
        [genericShip shoot];
 
        Ship *discoveryOne = [[ResearchShip alloc] init];
        [discoveryOne shoot];
 
    }
    return 0;
}

Поскольку ни main.m , ни main.m , ни ResearchShip.h импортируют защищенные методы, этот код не будет иметь к ним доступа. Попробуйте добавить метод [discoveryOne prepareToShoot] — он выдаст ошибку компилятора, так как объявление prepareToShoot нигде не найдено.

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

Хотя рабочий процесс, представленный здесь, является полностью допустимым организационным инструментом, имейте в виду, что Objective-C никогда не предназначался для поддержки защищенных методов. Думайте об этом как об альтернативном способе структурирования метода Objective-C, а не о прямой замене защищенных методов в стиле C # / Simula. Часто лучше искать другой способ структурировать свои классы, чем заставлять код Objective-C действовать как программа на C #.

Одна из самых больших проблем с категориями заключается в том, что вы не можете надежно переопределить методы, определенные в категориях для одного и того же класса. Например, если вы определили класс addFriend: в Person(Relations) а затем решили изменить реализацию addFriend: через категорию Person(Security) , у среды выполнения не будет способа узнать, какой метод следует использовать, поскольку категории по определению, плоская организационная структура. В таких случаях вам необходимо вернуться к традиционной парадигме подклассов.

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


Расширения (также называемые расширениями классов ) — это особый тип категории, который требует, чтобы их методы были определены в основном блоке реализации для связанного класса, в отличие от реализации, определенной в категории. Это можно использовать для переопределения публично объявленных атрибутов свойств. Например, иногда удобно изменить свойство только для чтения на свойство чтения-записи в реализации класса. Рассмотрим обычный интерфейс для класса Ship :

Включенный пример кода: Расширения

01
02
03
04
05
06
07
08
09
10
11
// Ship.h
#import <Foundation/Foundation.h>
#import «Person.h»
 
@interface Ship : NSObject
 
@property (strong, readonly) Person *captain;
 
— (id)initWithCaptain:(Person *)captain;
 
@end

Можно переопределить определение @property внутри расширения класса. Это дает вам возможность повторно объявить свойство как readwrite в файле реализации. Синтаксически расширение выглядит как объявление пустой категории:

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
// Ship.m
#import «Ship.h»
 
 
// The class extension.
@interface Ship()
 
@property (strong, readwrite) Person *captain;
 
@end
 
 
// The standard implementation.
@implementation Ship
 
@synthesize captain = _captain;
 
— (id)initWithCaptain:(Person *)captain {
    self = [super init];
    if (self) {
        // This WILL work because of the extension.
        [self setCaptain:captain];
    }
    return self;
}
 
@end

Обратите внимание на () добавляемое к имени класса после директивы @interface . Это то, что помечает его как расширение, а не как обычный интерфейс или категорию. Любые свойства или методы, которые появляются в расширении, должны быть объявлены в основном блоке реализации для класса. В этом случае мы не добавляем никаких новых полей — мы переопределяем существующее. Но в отличие от категорий, расширения могут добавлять дополнительные переменные экземпляра в класс, поэтому мы можем объявлять свойства в расширении класса, но не в категории.

Поскольку мы повторно объявили свойство readwrite атрибутом readwrite , initWithCaptain: метод может использовать setCaptain: accessor сам по себе. Если вы удалите расширение, свойство вернется в состояние только для чтения, и компилятор будет жаловаться. Клиенты, использующие класс Ship , не должны импортировать файл реализации, поэтому свойство captain останется только для чтения.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
#import <Foundation/Foundation.h>
#import «Person.h»
#import «Ship.h»
 
int main(int argc, const char * argv[]) {
    @autoreleasepool {
 
        Person *heywood = [[Person alloc] init];
        heywood.name = @»Heywood»;
        Ship *discoveryOne = [[Ship alloc] initWithCaptain:heywood];
        NSLog(@»%@», [discoveryOne captain].name);
 
        Person *dave = [[Person alloc] init];
        dave.name = @»Dave»;
        // This will NOT work because the property is still read-only.
        [discoveryOne setCaptain:dave];
 
    }
    return 0;
}

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

1
2
3
4
5
6
7
8
// Ship.h
#import <Foundation/Foundation.h>
 
@interface Ship : NSObject
 
— (void)shoot;
 
@end

Далее мы собираемся воссоздать пример, который мы использовали, когда обсуждали частные методы в главе « Методы» . Вместо того, чтобы просто добавить частный метод prepareToShoot в реализацию, нам нужно предварительно объявить его в расширении класса.

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
// Ship.m
#import «Ship.h»
 
// The class extension.
@interface Ship()
 
— (void)prepareToShoot;
 
@end
 
// The rest of the implementation.
@implementation Ship {
    BOOL _gunIsReady;
}
 
— (void)shoot {
    if (!_gunIsReady) {
        [self prepareToShoot];
        _gunIsReady = YES;
    }
    NSLog(@»Firing!»);
}
 
— (void)prepareToShoot {
    // Execute some private functionality.
    NSLog(@»Preparing the main weapon…»);
}
 
@end

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


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

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

Этот урок представляет собой главу из Objective-C, лаконично , бесплатную электронную книгу от команды Syncfusion .