Метаклассы в Objective-C

в 8:36, , рубрики: objective-c, ооп, метки: ,

Данная статья представляет собой перевод статьи What is a meta-class in Objective-C?

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

Небольшое вступление

Давайте попробуем рассмотреть такое понятие языка Objective-C как метакласс.
Как и любому новичку, решившему учить Objective-C, мне стала любопытна одна деталь.
Если класс одновременно является объектом и мы можем отправлять ему сообщения, вызывающие метод класса (перед типом возвращаемого значения стоит "+"), то что же из себя представляет такой класс-объект?

Для начала вспомним, чем отличается метод класса от метода экземпляра.

@interface MyClass : NSObject

+ (void)aClassMethod;                               //метод класса
- (void)anInstanceMethod;                          //метод экземпляра
@end
.................................................
[MyClass aClassMethod];                            //вызываем метод класса + (void)aClassMethod;
                                               
MyClass *object = [[MyClass alloc] init];    //создаем экземпляр класса MyClass
[object anInstanceMethod];                         //вызываем метод экземпляра - (void)anInstanceMethod; 

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

Создание класса вручную в run-time

Итак, каждый класс в языке Objective-C имеет свой соответствующий метакласс. Давайте рассмотрим метаклассы в ран-тайме.
Создадим вручную класс в рантайме.
Следующий код создает подкласс RuntimeErrorSubclass надкласса NSError и добавляет к нему метод report.

Class newClass =
    objc_allocateClassPair([NSError class], "RuntimeErrorSubclass", 0);
class_addMethod(newClass, @selector(report), (IMP)ReportFunction, "v@:");
objc_registerClassPair(newClass);

Добавленный с помощью функции class_addMethod метод report использует функцию ReportFunction (imp — это указатель на функцию, которая является имплементацией нового метода. Функция должна принимать как минимум два параметра — self и _cmd), имеющая следующее определение:

void ReportFunction(id self, SEL _cmd)
{
    NSLog(@"This object is %p.", self);
    NSLog(@"Class is %@, and super is %@.", [self class], [self superclass]);
     
    Class currentClass = [self class];
    for (int i = 1; i < 5; i++)
    {
        NSLog(@"Following the isa pointer %d times gives %p", i, currentClass);
        currentClass = object_getClass(currentClass);
    }
 
    NSLog(@"NSObject's class is %p", [NSObject class]);
    NSLog(@"NSObject's meta class is %p", object_getClass([NSObject class]));
}

Внешне все выглядит достаточно просто. Всего за три шага мы создали класс.
1. Создали класс с помощью objc_allocateClassPair
2. Добавили к нему метод с помощью class_addMethod (При желании можно добавит и переменные с помощью class_addIvar)
3. Зарегистрировали класс для возможности работы с ним с помощью objc_registerClassPair

В тоже время, возникает вопрос: А что же такое «class pair»? Конечно, если заглянуть в документацию Apple и посмотреть на описание функции objc_allocateClassPair, то можно увидеть, что она создает не только класс, но и наш таинственный метакласс. Вот и пара классов.
Но для того, чтобы вникнуть в понятие метакласса, понадобится знакомство с некоторыми основами.

Когда структура данных может быть рассмотрена как объект

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

В Objective-C класс объекта определяется с помощью указателя isa.

В целом, базовое определение объекта в Objective-C может выглядеть следующим образом:

typedef struct objc_object {
    Class isa;
} *id;

Это означает, что любая структура данных, которая начинается с указателя на класс, может быть рассмотрена как объект.

Важнейшей особенностью объектов в Objective-C является возможность отправлять сообщения объектам.

[@"stringValue"
    writeToFile:@"/file.txt" atomically:YES encoding:NSUTF8StringEncoding error:NULL];

Это работает потому что когда Вы отправляете сообщение объекту Objective-C (как NSCFString выше), то среда выполнения следует указателю на класс isa, который ведет к классу объекта (к классу NSCFString как в нашем случае). Класс содержит список методов, которые можно применять ко всем объектам данного класса и указатель на суперкласс (или надкласс) для просмотра наследуемых методов. Среда выполнения просматривает список методов класса и супекласса чтобы найти метод, совпадающий с селектором сообщения (в вышеуказанном случае, writeToFile:atomically:encoding:error on NSString). Далее среда выполнения вызывает функцию (IMP) для данного метода.

Важным моментом является то, что Класс определяет, какие сообщения можно отправлять объекту.

Что же такое метакласс?

Как Вы уже теперь знаете, классы в Objective-C также являются и объектами. Это означает что Вы можете отправлять сообщения классу.

NSStringEncoding defaultStringEncoding = [NSString defaultStringEncoding];

В этом случае классу NSString отправляется сообщение defaultStringEncoding.

Это работает потому что каждый класс в Objective-C одновременно является и объектом. Это означает, что структура класса также начинается с указателя на класс isa, таким образом, его структура схожа со структурой объекта, рассмотренной выше, а следующем полем структуры должен быть указатель на суперкласс (или nil для базовых классов).

Хотя в зависимости от версии среды исполнения структура классов может отличаться, но все они начинаются с поля указателя на класс isa и указателя на суперкласс.

typedef struct objc_class *Class;
struct objc_class {
    Class isa;
    Class super_class;
    /* далее зависит от версии среды исполнения... */
};

Однако, для того чтобы вызвать метод Класса, указатель на Класс isa должен указывать на структуру Класса и эта структура Класса должна содержать список методов, которые может вызывать Класс.

Это приводит нас к определению метакласса: метакласс — это класс для объекта Класса.

Проще говоря:
— Когда Вы отправляете сообщение объекту, сообщение просматривается в списке методов объекта Класса.
— Когда Вы отправляете сообщение классу, такое сообщение просматривается в списке методов метакласса.

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

Что является классов для метакласса?

Метакласс, как и рассмотренные ранее классы, также является объектом. Это означает, что Вы также можете вызывать методы метакласса. В целом, это означает, что у метакласса также должен быть класс.

Все метаклассы используют базовый метакласс (метакласс наивысшего класса в своей иерархии классов) как свой класс. Это означает, что для всех классов, которые наследуются от NSObject (а это большинство классов), метаклассы имеют метакласс NSObject как свой класс.

Следуя правилу, что для всех метаклассов, использующих базовый метакласс Класса как свой класс, любой базовый метакласс будет их собственным классом (их указатель isa будет указывать непосредственно на них). Это означает, что указатель isa метакласса NSObject указывает сам на себя.

Иерархия классов и метаклассов.

Также как и Класс, который имеет указатель на надкласс с помощью указателя super_class pointer, метакласс имеет свой собственный указатель super_class pointer на метакласс суперкласса Класса

Указатель на суперкласс базового метокласса указывает сам на себя.

Результатом этой иерархии является то, что все экземпляры, классы и метаклассы в иерархии наследуются от иерархии базового класса.

Для всех экземпляров, классов и метаклассов в иерерхии NSObject, это означает что для них также применимы все методы экземпляра NSObject.

Хотя все это неплохо смотрится как тест, Грэг Паркер предложил отличную диаграмму, иллюстрирующую иерархию экземпляров, классов и метаклассов.

image

Экспериментальное подтверждение.

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

Для того, чтобы воспользоваться ReportFunction, нам нужно создать экземпляр динамически созданного класса и вызвать метод report

id instanceOfNewClass =
    [[newClass alloc] initWithDomain:@"someDomain" code:0 userInfo:nil];
[instanceOfNewClass performSelector:@selector(report)];
[instanceOfNewClass release];

Так как у нас нет объявления метода report, мы вызываем его с помощью функции performSelector, таким образом компилятор не выдает нам предупреждение.

Далее функция ReportFunction проходит по указателям isa и говорит нам какой объект используется как класс, какой как метакласс и класс метакласса.

Получение класса объекта: функция ReportFunction использует функцию object_getClass для того, чтобы пройтись по указателям на класс isa, так как указатель isa является защищенным членом класса (вы не можете напрямую получить доступ к указателю isa других объектов в нашей цепочке). ReportFunction также не использует вызов метода class объектом Класса, так как вызов метода class не возвращает метакласс, а вместо этого снова возвращает Класс (например, [NSString class] снова вернет класс NSString вместо метакласса). object_getClass возвращает объект Класса, экзамепляром которого является переданный в качестве аргумента объект.

Class object_getClass(id object) 

Вывод нашей программы:

This object is 0x10010c810.
Class is RuntimeErrorSubclass, and super is NSError.
Following the isa pointer 1 times gives 0x10010c600
Following the isa pointer 2 times gives 0x10010c630
Following the isa pointer 3 times gives 0x7fff71038480
Following the isa pointer 4 times gives 0x7fff71038480
NSObject's class is 0x7fff710384a8
NSObject's meta class is 0x7fff71038480

Просмотрев полученные адреса:

Объект: 0x10010c810.
Класс: 0x10010c600.
Метакласс: 0x10010c630.
Класс метакласса (метакласс NSObject) 0x7fff71038480.
Класс метакласса NSObject указывает сам на себя.

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

Выводы

Метакласс — это класс объекта Класса. Каждый Класс имеет свой собственный уникальный метакласс (так как каждый Класс имеет свой собственный уникальный список методов).

Метакласс всегда гарантирует, что объект Класса имеет все необходимые переменные и методы базового Класса, а также все методы Класса. Для классов, наследуемых от NSObject это означает что все экземпляры NSObject и методы протокола определены для всех объектов классов и метаклассов.

Все метаклассы используют базовый метакласс класса NSObject как свой класс.

Автор: Lamanzana2012

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js