Статьи

Objective-C лаконично: методы

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

Если вы используете ручное управление памятью (не рекомендуется), вам нужно освободить все переменные экземпляра, которые ваш объект выделил в методе 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 .

Однако, если вы используете автоматический подсчет ссылок, все переменные вашего экземпляра будут автоматически освобождены, и вам также будет вызван [super dealloc] (вы никогда не должны вызывать его явно). Таким образом, единственное, о чем вам нужно беспокоиться, это не-объектные переменные, такие как буферы, созданные с помощью malloc() .

Как и init , вам не нужно реализовывать метод dealloc если ваш объект не требует какой-либо специальной обработки перед его освобождением. Это часто бывает в случае автоматического подсчета ссылок.


Большим препятствием для разработчиков C # при переходе на Objective-C является очевидное отсутствие частных методов. В отличие от C #, все методы в классе Objective C доступны для третьих лиц; однако, возможно подражать поведению частных методов.

Помните, что клиенты импортируют только интерфейс класса (т.е. файлы заголовков) — они никогда не должны видеть основную реализацию. Таким образом, добавляя новые методы внутри файла реализации, не включая их в интерфейс , мы можем эффективно скрывать методы от других объектов. Хотя это и более основанный на соглашениях, чем «настоящие» приватный метод, но по сути это та же функциональность: попытка вызвать метод, который не объявлен в интерфейсе, приведет к ошибке компилятора.

Рисунок 25 Попытка вызова частного метода

Попытка вызвать «частный» метод

Например, допустим, вам нужно добавить частный метод 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. Они позволяют динамически «выбирать» один из методов объекта, который можно использовать для ссылки на метод во время выполнения, передачи метода другой функции и выяснения, есть ли у объекта определенный метод. В практических целях вы можете рассматривать селектор как альтернативное имя для метода.

Рис. 26 Представление разработчиками метода по сравнению с представлением Objective-Cs

Представление метода разработчиками против представления Objective-C

Внутренне Objective-C использует уникальный номер для идентификации каждого имени метода, которое использует ваша программа. Например, метод с именем sayHello может переводиться в 4984331082 . Этот идентификатор называется селектором , и для компилятора это гораздо более эффективный способ обращения к методам, чем их полное строковое представление. Важно понимать, что селектор представляет только имя метода, а не конкретную реализацию метода. Другими словами, метод sayHello определенный классом Person имеет тот же селектор, что и метод sayHello определенный классом Ship .

Три основных инструмента для работы с селекторами:

  • @selector() — возвращает селектор, связанный с именем метода исходного кода.
  • NSSelectorFromString() — Возвращает селектор, связанный со строковым представлением имени метода. Эта функция позволяет определить имя метода во время выполнения, но оно менее эффективно, чем @selector() .
  • NSStringFromSelector() — Возвращает строковое представление имени метода из селектора.

Как видите, существует три способа представления имени метода в Objective-C: в виде исходного кода, в виде строки или в качестве селектора. Эти функции преобразования показаны графически на следующем рисунке:

Рисунок 27 Преобразование между строками исходного кода и селекторами

Преобразование между исходным кодом, строками и селекторами

Селекторы хранятся в специальном типе данных, который называется 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 .