В этой главе мы рассмотрим методы Objective-C гораздо более подробно, чем в предыдущих главах. Это включает в себя подробное обсуждение методов экземпляра, методов класса, важных встроенных методов, наследования, соглашений об именах и общих шаблонов проектирования.
Методы экземпляра против класса
На протяжении всей этой книги мы работали с методами экземпляра и класса, но давайте уделим немного времени формализации двух основных категорий методов в Objective-C:
- Методы экземпляра — функции, связанные с объектом. Методы экземпляра — это «глаголы», связанные с объектом.
- Методы класса — функции, связанные с самим классом. Они не могут использоваться экземплярами класса. Это похоже на статические методы в C #.
Как мы видели много раз, методы экземпляра обозначаются дефисом перед именем метода, в то время как методы класса имеют префикс со знаком плюс. Например, давайте возьмем упрощенную версию нашего файла Person.h
:
1
2
3
4
5
6
7
8
|
@interface Person : NSObject
@property (copy) NSString *name;
— (void)sayHello;
+ (Person *)personWithName:(NSString *)name;
@end
|
Аналогичным образом, соответствующим методам реализации также должен предшествовать дефис или знак плюс. Итак, минимальный Person.m
может выглядеть примерно так:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
#import «Person.h»
@implementation Person
@synthesize name = _name;
— (void)sayHello {
NSLog(@»HELLO»);
}
+ (Person *)personWithName:(NSString *)name {
Person *person = [[Person alloc] init];
person.name = name;
return person;
}
@end
|
Метод sayHello
может вызываться экземплярами класса Person
, тогда как метод personWithName
может вызываться только самим классом:
1
2
|
Person *p1 = [Person personWithName:@»Frank»];
[p1 sayHello];
|
Большая часть этого уже должна быть вам знакома, но теперь у нас есть возможность поговорить о некоторых уникальных соглашениях в Objective-C.
Супер ключевое слово
В любой объектно-ориентированной среде важно иметь доступ к методам из родительского класса. Objective-C использует схему, очень похожую на C #, за исключением того, что вместо base
он использует ключевое слово super
. Например, следующая реализация sayHello
отобразит HELLO
на панели вывода, а затем вызовет версию sayHello
родительского класса:
1
2
3
4
|
— (void)sayHello {
NSLog(@»HELLO»);
[super sayHello];
}
|
В отличие от C #, методы переопределения не должны быть явно помечены как таковые. Вы увидите это с методами init
и dealloc
которые обсуждаются в следующем разделе. Даже если они определены в классе NSObject
, компилятор не будет жаловаться, когда вы создаете свои собственные методы init
и dealloc
в подклассах.
Методы инициализации
Методы инициализации требуются для всех объектов — вновь выделенный объект не считается «готовым к использованию», пока не был вызван один из его методов инициализации. Они являются местом установки значений по умолчанию для переменных экземпляра и в противном случае устанавливают состояние объекта. Класс NSObject
определяет метод init
умолчанию, который ничего не делает, но часто полезно создать свой собственный. Например, пользовательская реализация init
для нашего класса Ship
может назначать значения по умолчанию переменной экземпляра _ammo
:
1
2
3
4
5
6
7
|
— (id)init {
self = [super init];
if (self) {
_ammo = 1000;
}
return self;
}
|
Это канонический способ определения пользовательского метода init
. Ключевое слово self
является эквивалентом языка C # this
-оно используется для ссылки на экземпляр, вызывающий метод, что позволяет объекту отправлять сообщения самому себе. Как видите, все методы init
необходимы для возврата экземпляра. Это то, что позволяет использовать синтаксис [[Ship alloc] init]
для назначения экземпляра переменной. Также обратите внимание, что поскольку интерфейс NSObject
объявляет метод init
, нет необходимости добавлять объявление init
в Ship.h
Несмотря на то, что простые методы init
подобные показанному в предыдущем примере, полезны для установки значений переменных экземпляра по умолчанию, часто более удобно передавать параметры в метод инициализации:
1
2
3
4
5
6
7
|
— (id)initWithAmmo:(unsigned int)theAmmo {
self = [super init];
if (self) {
_ammo = theAmmo;
}
return self;
}
|
Если вы пришли из C # фона, вам может быть неудобно с именем метода initWithAmmo
. Вы, вероятно, ожидаете увидеть параметр Ammo
отделенным от фактического имени метода, например void init(uint ammo)
; однако метод именования в Objective-C основан на совершенно другой философии.
Напомним, что цель Objective-C состоит в том, чтобы заставить API быть настолько описательным, насколько это возможно, гарантируя, что нет абсолютно никакой путаницы относительно того, что будет делать вызов метода. Вы не можете думать о методе как об отдельной сущности от его параметров — они единая единица. Это проектное решение фактически отражено в реализации Objective-C, которая не делает различий между методом и его параметрами. Внутренне имя метода на самом деле является составным списком параметров .
Например, рассмотрим следующие три объявления метода. Обратите внимание, что второй и третий не являются встроенными методами NSObject
, поэтому вам необходимо добавить их в интерфейс класса перед их реализацией.
1
2
3
|
— (id)init;
— (id)initWithAmmo:(unsigned int)theAmmo;
— (id)initWithAmmo:(unsigned int)theAmmo captain:(Person *)theCaptain;
|
Хотя это выглядит как перегрузка метода, технически это не так. Это не вариации метода init
— все они полностью независимые методы с разными именами методов. Имена этих методов следующие:
1
2
3
|
init
initWithAmmo:
initWithAmmo:captain:
|
Это причина, по которой вы видите нотацию, такую как indexOfObjectWithOptions:passingTest:
и indexOfObjectAtIndexes:options:passingTest:
для ссылки на методы в официальной документации Objective-C (взятой из NSArray ).
С практической точки зрения это означает, что первый параметр ваших методов должен всегда описываться «основным» именем метода. Неоднозначные методы, подобные следующим, обычно не одобряются программистами Objective-C:
1
|
— (id)shoot:(Ship *)aShip;
|
Вместо этого вы должны использовать предлог для включения первого параметра в имя метода, например так:
1
|
— (id)shootOtherShip:(Ship *)aShip;
|
Включение OtherShip
и aShip
в определение метода может показаться излишним, но помните, что аргумент aShip
используется только для внутреннего использования. Кто-то, вызывающий метод, напишет что-то вроде shootOtherShip:discoveryOne
, где discoveryOne
— это переменная, содержащая корабль, который вы хотите выстрелить. Это именно тот тип многословия, к которому стремятся разработчики Objective-C.
Инициализация класса
В дополнение к методу init
для инициализации экземпляров Objective-C также предоставляет способ настройки классов . Прежде чем вызывать какие-либо методы класса или создавать экземпляры каких-либо объектов, среда выполнения Objective C вызывает метод класса initialize
рассматриваемого класса. Это дает вам возможность определить любые статические переменные, прежде чем кто-либо использует класс. Одним из наиболее распространенных вариантов использования для этого является настройка синглетонов:
01
02
03
04
05
06
07
08
09
10
11
|
static Ship *_sharedShip;
+ (void)initialize {
if (self == [Ship class]) {
_sharedShip = [[self alloc] init];
}
}
+ (Ship *)sharedShip {
return _sharedShip;
}
|
Перед первым [Ship sharedShip]
среда выполнения вызовет [Ship initialize]
, что обеспечит определение синглтона. Модификатор статической переменной служит той же цели, что и в C # — он создает переменную уровня класса вместо переменной экземпляра. Метод initialize
вызывается только один раз, но он вызывается для всех суперклассов, поэтому вам следует позаботиться о том, чтобы не инициализировать переменные уровня класса несколько раз. Вот почему мы включили условное _shareShip
self == [Ship class]
чтобы убедиться, что _shareShip
выделен только в классе Ship
.
Также обратите внимание, что внутри метода класса ключевое слово self
ссылается на сам класс, а не на экземпляр. Таким образом, [self alloc]
в последнем примере является эквивалентом [Ship alloc]
.
Методы распределения
Логическим аналогом метода инициализации экземпляра является метод dealloc
. Этот метод вызывается для объекта, когда его счетчик ссылок достигает нуля и его основная память собирается быть освобождена.
Распределение в MMR
Если вы используете ручное управление памятью (не рекомендуется), вам нужно освободить все переменные экземпляра, которые ваш объект выделил в методе dealloc
. Если вы не освободите переменные экземпляра до того, как ваш объект выйдет из области видимости, у вас будут свисающие указатели на переменные экземпляра, что означает утечку памяти при каждом освобождении экземпляра класса. Например, если наш класс Ship
выделил переменную с именем _gun
в своем методе init
, вам придется освободить ее в dealloc
. Это продемонстрировано в следующем примере ( Gun.h
содержит пустой интерфейс, который просто определяет класс Gun
):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
#import «Ship.h»
#import «Gun.h»
@implementation Ship {
BOOL _gunIsReady;
Gun *_gun;
}
— (id)init {
self = [super init];
if (self) {
_gun = [[Gun alloc] init];
}
return self;
}
— (void)dealloc {
NSLog(@»Deallocating a Ship»);
[_gun release];
[super dealloc];
}
@end
|
Вы можете увидеть метод dealloc
в действии, создав Ship
и выпустив его, вот так:
1
2
3
4
5
6
7
8
9
|
int main(int argc, const char * argv[]) {
@autoreleasepool {
Ship *ship = [[Ship alloc] init];
[ship autorelease];
NSLog(@»Ship should still exist in autoreleasepool»);
}
NSLog(@»Ship should be deallocated by now»);
return 0;
}
|
Это также демонстрирует, как работают автоматически выпущенные объекты. Метод dealloc
не будет вызываться до конца блока @autoreleasepool
, поэтому предыдущий код должен вывести следующее:
1
2
3
|
Ship should still exist in autoreleasepool
Deallocating a Ship
Ship should be deallocated by now
|
Обратите внимание, что первое сообщение NSLog()
в main()
отображается раньше, чем в методе dealloc
, даже если оно было autorelease
после вызова autorelease
.
Распределение в ARC
Однако, если вы используете автоматический подсчет ссылок, все переменные вашего экземпляра будут автоматически освобождены, и вам также будет вызван [super dealloc]
(вы никогда не должны вызывать его явно). Таким образом, единственное, о чем вам нужно беспокоиться, это не-объектные переменные, такие как буферы, созданные с помощью malloc()
.
Как и init
, вам не нужно реализовывать метод dealloc
если ваш объект не требует какой-либо специальной обработки перед его освобождением. Это часто бывает в случае автоматического подсчета ссылок.
Частные Методы
Большим препятствием для разработчиков C # при переходе на Objective-C является очевидное отсутствие частных методов. В отличие от C #, все методы в классе Objective C доступны для третьих лиц; однако, возможно подражать поведению частных методов.
Помните, что клиенты импортируют только интерфейс класса (т.е. файлы заголовков) — они никогда не должны видеть основную реализацию. Таким образом, добавляя новые методы внутри файла реализации, не включая их в интерфейс , мы можем эффективно скрывать методы от других объектов. Хотя это и более основанный на соглашениях, чем «настоящие» приватный метод, но по сути это та же функциональность: попытка вызвать метод, который не объявлен в интерфейсе, приведет к ошибке компилятора.
Например, допустим, вам нужно добавить частный метод prepareToShoot
в класс Ship
. Все, что вам нужно сделать, это опустить его из Ship.h
при добавлении его в Ship.m
:
1
2
3
4
5
6
7
8
|
// Ship.h
@interface Ship : NSObject
@property (weak) Person *captain;
— (void)shoot;
@end
|
Это объявляет открытый метод shoot
, который будет использовать приватный метод prepareToShoot
. Соответствующая реализация может выглядеть примерно так:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// Ship.m
#import «Ship.h»
@implementation Ship {
BOOL _gunIsReady;
}
@synthesize captain = _captain;
— (void)shoot {
if (!_gunIsReady) {
[self prepareToShoot];
_gunIsReady = YES;
}
NSLog(@»Firing!»);
}
— (void)prepareToShoot {
// Execute some private functionality.
NSLog(@»Preparing the main weapon…»);
}
@end
|
Начиная с Xcode 4.3, вы можете определять приватные методы в любом месте реализации. Если вы используете приватный метод до того, как его увидит компилятор (как в предыдущем примере), компилятор проверяет оставшуюся часть блока реализации на определение метода. До Xcode 4.3 вы должны были либо определить закрытый метод, прежде чем он был использован в другом месте файла, либо объявить его с расширением класса .
Расширения классов являются частным случаем категорий , которые представлены в следующей главе. Так же, как нет способа пометить метод как частный, нет способа пометить метод как защищенный; однако, как мы увидим в следующей главе, категории предоставляют мощную альтернативу защищенным методам.
Селекторы
Селекторы — это способ представления методов в Objective-C. Они позволяют динамически «выбирать» один из методов объекта, который можно использовать для ссылки на метод во время выполнения, передачи метода другой функции и выяснения, есть ли у объекта определенный метод. В практических целях вы можете рассматривать селектор как альтернативное имя для метода.
Внутренне Objective-C использует уникальный номер для идентификации каждого имени метода, которое использует ваша программа. Например, метод с именем sayHello
может переводиться в 4984331082
. Этот идентификатор называется селектором , и для компилятора это гораздо более эффективный способ обращения к методам, чем их полное строковое представление. Важно понимать, что селектор представляет только имя метода, а не конкретную реализацию метода. Другими словами, метод sayHello
определенный классом Person
имеет тот же селектор, что и метод sayHello
определенный классом Ship
.
Три основных инструмента для работы с селекторами:
-
@selector()
— возвращает селектор, связанный с именем метода исходного кода. -
NSSelectorFromString()
— Возвращает селектор, связанный со строковым представлением имени метода. Эта функция позволяет определить имя метода во время выполнения, но оно менее эффективно, чем@selector()
. -
NSStringFromSelector()
— Возвращает строковое представление имени метода из селектора.
Как видите, существует три способа представления имени метода в Objective-C: в виде исходного кода, в виде строки или в качестве селектора. Эти функции преобразования показаны графически на следующем рисунке:
Селекторы хранятся в специальном типе данных, который называется SEL
. Следующий фрагмент демонстрирует основное использование трех функций преобразования, показанных на предыдущем рисунке:
01
02
03
04
05
06
07
08
09
10
11
12
|
int main(int argc, const char * argv[]) {
@autoreleasepool {
SEL selector = @selector(sayHello);
NSLog(@»%@», NSStringFromSelector(selector));
if (selector == NSSelectorFromString(@»sayHello»)) {
NSLog(@»The selectors are equal!»);
}
}
return 0;
}
|
Сначала мы используем директиву @selector()
чтобы выяснить селектор для метода с именем sayHello
, который представляет собой представление исходного кода имени метода. Обратите внимание, что вы можете передать любое имя метода в @selector()
— он не должен существовать в другом месте вашей программы. Затем мы используем функцию NSStringFromSelector()
чтобы преобразовать селектор обратно в строку, чтобы мы могли отобразить его на панели вывода. Наконец, условие показывает, что селекторы имеют непосредственное соответствие с именами методов, независимо от того, находите ли вы их через жестко закодированные имена методов или строки.
Имена методов и селекторы
В предыдущем примере используется простой метод, который не принимает параметров, но важно иметь возможность передавать методы, которые принимают параметры. Напомним, что имя метода состоит из основного имени метода, объединенного со всеми именами параметров. Например, метод с подписью
1
2
|
— (void)sayHelloToPerson:(Person *)aPerson
withGreeting:(NSString *)aGreeting;
|
будет иметь имя метода:
1
|
sayHelloToPerson:withGreeting:
|
Это то, что вы передадите в @selector()
или NSSelectorFromString()
чтобы вернуть идентификатор для этого метода. Селекторы работают только с именами методов (но не с подписями), поэтому между селекторами и подписями нет однозначного соответствия. В результате имя метода в последнем примере также будет соответствовать сигнатуре с различными типами данных, включая следующие:
1
2
|
— (void)sayHelloToPerson:(NSString *)aName
withGreeting:(BOOL)useGreeting;
|
Многословие соглашений об именах Objective-C позволяет избежать самых запутанных ситуаций; однако, селекторы для методов с одним параметром все еще могут быть хитрыми, потому что добавление двоеточия к имени метода фактически меняет его на совершенно другой метод. Например, в следующем примере первое имя метода не принимает параметр, а второе -:
1
2
|
sayHello
sayHello:
|
Опять же, соглашения об именах имеют большое значение для устранения путаницы, но вам все равно нужно знать, когда необходимо добавить двоеточие в конец имени метода. Это обычная проблема, если вы новичок в селекторах, и ее может быть сложно отладить, так как завершающий двоеточие по-прежнему создает совершенно правильное имя метода.
Выполнение селекторов
Конечно, запись селектора в переменную SEL
является относительно бесполезной без возможности выполнить его позже. Поскольку селектор — это просто имя метода (а не реализация), его всегда нужно связать с объектом, прежде чем вы сможете его вызвать. Для этой цели класс performSelector:
определяет метод performSelector:
.
1
|
[joe performSelector:@selector(sayHello)];
|
Это эквивалентно вызову sayHello
непосредственно на joe
:
1
|
[joe sayHello];
|
Для методов с одним или двумя параметрами вы можете использовать связанные performSelector:withObject:
и performSelector:withObject:withObject:
Следующая реализация метода:
1
2
3
|
— (void)sayHelloToPerson:(Person *)aPerson {
NSLog(@»Hello, %@», [aPerson name]);
}
|
может вызываться динамически, передавая аргумент aPerson
в performSelector:withObject:
метод, как показано здесь:
1
|
[joe performSelector:@selector(sayHelloToPerson:) withObject:bill];
|
Это эквивалентно передаче параметра непосредственно методу:
1
|
[joe sayHelloToPerson:bill];
|
Аналогично, performSelector:withObject:withObject:
метод позволяет передать два параметра целевому методу. Единственное предостережение с этим заключается в том, что все параметры и возвращаемое значение метода должны быть объектами — они не работают с примитивными типами данных C, такими как int
, float
и т. Д. Если вам действительно нужна эта функциональность, вы можете либо пометить примитив введите один из множества классов- NSNumber
Objective-C (например, NSNumber
) или используйте объект NSInvocation для инкапсуляции полного вызова метода.
Проверка на наличие селекторов
Невозможно выполнить селектор для объекта, который не определил связанный метод. Но в отличие от статических вызовов методов, во время компиляции невозможно определить, будет ли performSelector:
вызывать ошибку. Вместо этого вы должны проверить, может ли объект ответить на селектор во время выполнения, используя метко названный respondsToSelector:
метод. Он просто возвращает YES
или NO
зависимости от того, может ли объект выполнить селектор:
1
2
3
4
5
6
7
|
SEL methodToCall = @selector(sayHello);
if ([joe respondsToSelector:methodToCall]) {
[joe performSelector:methodToCall];
} else {
NSLog(@»Joe doesn’t know how to perform %@.»,
NSStringFromSelector(methodToCall));
}
|
Если ваши селекторы генерируются динамически (например, если methodToCall
выбран из списка опций) или у вас нет контроля над целевым объектом (например, joe
может быть одним из нескольких различных типов объектов), важно запустить эта проверка перед попыткой вызова performSelector:
Использование селекторов
Вся идея селекторов заключается в том, чтобы иметь возможность передавать методы так же, как вы передаете объекты. Это может быть использовано, например, для динамического определения «действия» для объекта Person
будет выполняться позже в программе. Например, рассмотрим следующий интерфейс:
Включенный пример кода: селекторы
01
02
03
04
05
06
07
08
09
10
11
|
@interface Person : NSObject
@property (copy) NSString *name;
@property (weak) Person *friend;
@property SEL action;
— (void)sayHello;
— (void)sayGoodbye;
— (void)coerceFriend;
@end
|
Наряду с соответствующей реализацией:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
#import «Person.h»
@implementation Person
@synthesize name = _name;
@synthesize friend = _friend;
@synthesize action = _action;
— (void)sayHello {
NSLog(@»Hello, says %@.», _name);
}
— (void)sayGoodbye {
NSLog(@»Goodbye, says %@.», _name);
}
— (void)coerceFriend {
NSLog(@»%@ is about to make %@ do something.», _name, [_friend name]);
[_friend performSelector:_action];
}
@end
|
Как видите, вызов метода coerceFriend
заставит другой объект выполнить какое-либо произвольное действие. Это позволяет вам настроить дружбу и поведение на ранних этапах вашей программы и ждать, пока не произойдет определенное событие, прежде чем запускать действие:
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
|
#import <Foundation/Foundation.h>
#import «Person.h»
NSString *askUserForAction() {
// In the real world, this would be capture some
// user input to determine which method to call.
NSString *theMethod = @»sayGoodbye»;
return theMethod;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// Create a person and determine an action to perform.
Person *joe = [[Person alloc] init];
joe.name = @»Joe»;
Person *bill = [[Person alloc] init];
bill.name = @»Bill»;
joe.friend = bill;
joe.action = NSSelectorFromString(askUserForAction());
// Wait for an event…
// Perform the action.
[joe coerceFriend];
}
return 0;
}
|
Это почти точно, как компоненты пользовательского интерфейса в iOS реализованы. Например, если у вас есть кнопка, вы можете настроить ее с помощью целевого объекта (например, friend
) и действия (например, action
). Затем, когда пользователь в конце концов нажимает кнопку, он может использовать performSelector:
для выполнения желаемого метода на соответствующем объекте. Возможность варьировать объект и метод независимо друг от друга обеспечивает значительную гибкость — кнопка может буквально выполнять любое действие с любым объектом без какого-либо изменения класса кнопки. Это также формирует основу шаблона проектирования Target-Action, на который в значительной степени полагаются в кратком справочнике по iOS .
Резюме
В этой главе мы рассмотрели методы экземпляра и класса, а также некоторые из наиболее важных встроенных методов. Мы тесно работали с селекторами, которые позволяют ссылаться на имена методов как на исходный код или строки. Мы также кратко рассмотрели шаблон проектирования Target-Action, который является неотъемлемым аспектом программирования на iOS и OS X.
В следующей главе обсуждается альтернативный способ создания частных и защищенных методов в Objective-C.
Этот урок представляет собой главу из Objective-C, лаконично , бесплатную электронную книгу от команды Syncfusion .