Objective-C

Objective-C — ("Обджектів Сі") рефлективна, високорівнева об'єктно-орієнтована мова програмування загального призначення, розроблена у вигляді набору розширень стандартної С.

Objective-C
Парадигма об'єктно-орієнтоване програмування, мультипарадигмальне програмуванняd, class-based programmingd і Рефлексія
Дата появи 1986
Творці Tom Love & Brad Cox
Розробник Apple Inc.
Останній реліз
Система типізації статичнаd, динамічна типізація і слабка типізаціяd
Основні реалізації Clang, GCC
Під впливом від Smalltalk, C
Вплинула на TOM, Java, Objective-J
Операційна система Cross-platform
Звичайні розширення файлів .h, .m, .mm або .C
Вебсайт The Objective-C Programming Language
 Objective-C у Вікісховищі

Мова програмування Objective-C, розроблена на початку 1980-х років, була основною мовою, що використовувалася компанією NeXT для операційної системи NeXTSTEP, від якої пішли macOS і iOS.

На даний час використовується в основному у macOS та GNUStep — середовищах, розроблених на основі стандарту OpenStep, та Cocoa — бібліотеки компонентів для розробки програм. Програму на Objective-C, що не використовує цих бібліотек, можна скомпілювати для будь-якої платформи, яку підтримує gcc компілятор з підтримкою Objective-C.

Objective-C є розширенням С, і тому будь-яку програму на С можна скомпілювати компілятором Objective-C.

ООП в Objective-C включає інтерфейси, класи, категорії. Реалізовано одиничне, невіртуальне спадкування. Немає єдиного базового класу для всіх об'єктів. Всі методи в класі — віртуальні. Категорія — парадигма, яка дозволяє описувати інтерфейс з методами, які «необов'язково» імплементувати.

Синтакс Objective-C породжений одночасно від С та Smalltalk. Від останньої взято основний семантичний конструкт мови — замість виклику методу об'єктові надсилається повідомлення. Наприклад, якщо клас об'єкта obj імплементує метод doJob, то говориться, що об'єкт відкликається на повідомлення doJob. Щоб надіслати повідомлення doJob цьому об'єктові, потрібно написати:

[obj doJob];

Такий механізм дозволяє надсилати повідомлення навіть до тих об'єктів, які не підтримують їхньої обробки. Такий підхід відрізняється від тих, що використовуються в статично типізованих мовах С++ чи Java.

Історія

На початку 1980-х років було популярним структурне програмування. Воно дозволяло «розбивати» алгоритм на малі частини, в основному щоб виділити етапи алгоритму в окремі блоки і працювати з ними. Однак, із зростанням складності завдань, структурне програмування призводило до зниження якості коду. Доводилося писати все більше функцій, які дуже рідко могли використовуватися в інших програмах.

Багато хто побачив в об'єктно-орієнтованому програмуванні потенційне вирішення проблеми, яка виникла. З одного боку, Smalltalk використовували майже всі більш-менш складні системи. З іншого - використання віртуальних машин сильно гальмувало роботу системи і вимагало величезних ресурсів. ObjC створив Бред Кокс на початку 1980-х у своїй компанії Stepstone. Він намагався вирішити проблему повторного використання коду.

Метою Кокса було створення мови, яка підтримуватиме концепцію software IC. Під цією концепцією розуміється можливість збирати програми з готових компонентів (об'єктів) подібно до того, як складні електронні пристрої можуть бути легко зібрані з набору готових інтегральних мікросхем. При цьому така мова має бути досить простою і заснованою на мові С, щоб полегшити перехід розробників на неї. Однією з цілей було також створення моделі, в якій самі класи також є повноцінними об'єктами і підтримувалася б інтроспекція і динамічна обробка повідомлень.

Отримана в результаті мова Objective-C виявилася вкрай простою - на її "освоєння" у С-програміста піде всього кілька днів. Вона є саме розширенням мови С - в мову С просто додано нові можливості для об'єктно-орієнтованого програмування. При цьому будь-яка програма на С є програмою і на Objective-C.

Однією з відмінних рис Objective-C є її динамічність - цілий ряд рішень, які зазвичай приймаються на етапі компіляції, тут відкладаються безпосередньо до етапу виконання. Ще однією особливістю мови є те, що вона message-oriented, у той час як C++ - function-oriented. Це означає, що в ній виклики методу інтерпретуються не як виклик функції (хоча до цього, зазвичай, все зводиться), а саме як відправлення повідомлення (з ім'ям і аргументами) об'єкту, подібно до того, як це відбувається в Smalltalk. Такий підхід дає цілий ряд плюсів - будь-якому об'єктові можна послати будь-яке повідомлення. Об'єкт може замість обробки повідомлення просто переслати його іншому об'єктові для обробки (так зване делегування), зокрема саме так можна легко реалізувати розподілені об'єкти (тобто об'єкти, що знаходяться в різних адресних просторах і навіть на різних комп'ютерах). Прив'язка повідомлення до відповідної функції відбувається безпосередньо на етапі виконання.

Мова Objective-C підтримує нормальну роботу з метаінформацією - так у об'єкта безпосередньо на етапі виконання можна запитати його клас, список методів (з типами переданих аргументів) і instance-змінних, перевірити, чи є клас нащадком заданого і чи підтримує він заданий протокол і т. ін. У мові є нормальна підтримка протоколів (тобто поняття інтерфейсу об'єкта та протоколу чітко розділено). Для об'єктів підтримується успадкування (не множинне), для протоколів підтримується множинне успадкування. Об'єкт може бути успадкований від іншого об'єкта і відразу кількох протоколів (хоча це скоріше не успадкування протоколу, а його підтримка). На даний момент мова Objective-C підтримується компіляторами Clang і GCC (під управлінням Windows Windows використовується у складі MinGW або cygwin). Досить багато в мові перенесено на runtime-бібліотеку і сильно залежить від неї. Разом з компілятором gcc поставляється мінімальний варіант такої бібліотеки. Також можна вільно завантажити runtime-бібліотеку компанії Apple: Apple's Objective-C runtime. Ці дві runtime-бібліотеки досить схожі (в основному відмінність полягає в іменах методів), далі приклади будуть орієнтуватися на runtime-бібліотеку від компанії Apple.

Синтаксис мови

У мові Objective-C для позначення об'єктів використовується спеціальний тип id. Змінна типу id фактично є вказівником на довільний об'єкт. Для позначення нульового вказівника на об'єкт використовується константа nil (= NULL). При цьому замість id можна використовувати і більш звичне позначення з явною вказівкою класу. Зокрема останнє дозволяє компілятору здійснювати деяку перевірку підтримки повідомлення об'єктами - якщо компілятор з типу змінної не може зробити висновок про підтримку об'єктом даного повідомлення, то він видасть попередження. Тим самим мова підтримує перевірку типів, але в нестрогій формі (тобто знайдені невідповідності повертаються як попередження, а не помилки). Для відправлення повідомлень використовується наступний синтаксис:

   [receiver message];

У цій конструкції receiver є вказівником на об'єкт, а message - іменем методу. На відміну від мови C + +, надсилання повідомлення nil'у є дозволеною операцією, яка завжди повертає нульове значення (nil). Повідомлення може також містити параметри:

   [myRect setOrigin:30.0 :50.0];

У цьому прикладі іменем методу (повідомлення) є setOrigin. Зверніть увагу, що кожному переданому аргументу відповідає рівно одна двокрапка. При цьому в наведеному прикладі перший аргумент має мітку (текст перед двокрапкою), а другий - ні. Мова Objective-C дозволяє забезпечувати мітками кожен аргумент, що помітно підвищує читність коду і знижує ймовірність передачі неправильного параметра. Саме такий стиль прийнятий більшістю розробників.

 [myRect setWidth:10.0 height:20.0];

У цьому прикладі іменем повідомлення виступає setWidth: height:. Також підтримується можливість передачі довільної кількості аргументів в повідомленні:

 [myObject makeGroup: obj1, obj2, obj3, obj4, nil];

Як і функції, повідомлення можуть повертати значення. При цьому, на відміну від мови С, типом значення, яке повертається, за замовчуванням, є id.

 float area = [myRect area];

Результат одного повідомлення можна відразу ж використовувати в іншому повідомленні:

 [myRect setColor:[otherRect color]];

Як уже зазначалося, в Objective-C класи самі є об'єктами. Основним завданням таких об'єктів (так званих class objects) є створення екземплярів даного класу (це дуже схоже на патерн Abstract Factory). При цьому саме ім'я класу відіграє подвійну роль - з одного боку, воно виступає типом даних (тобто воно може бути використане для опису вказівників на об'єкти даного класу), а з іншого боку - ім'я класу може виступати об'єктом, якому надсилається повідомлення (в повідомленнях ім'я класу може брати участь тільки як receiver).

 Rect * myRect = [[Rect alloc] init];

У мові Objective-C немає вбудованого типу для булевих величин, тому, зазвичай, такий тип вводиться штучно. Далі для логічних величин буде використовуватися тип BOOL з можливими значеннями YES і NO (як це робиться в операційних системах NextStep, Mac OS X). Першим досить серйозним застосуванням мови Objective-C було його використання в операційній системі NextStep. Для цієї системи було написано велику кількість різних класів на Objective-C, багато з яких дотепер використовуються в Mac OS X. Імена всіх цих класів починаються з префікса NS, який позначає свою приналежність до операційної системи NextStep. Зараз вони входять в бібліотеку Foundation, на якій будуються програми для OS X і iOS. З одним із них - NSString - ми ознайомимось в даній статті. Цей клас служить для роботи з рядками (при цьому для внутрішнього представлення символів використовується Юнікод). Компілятор підтримує даний тип, автоматично переводячи конструкції виду @ "my string" у вказівник на об'єкт класу NSString, який містить даний рядок (точніше його підкласу, відповідного константним рядками).

Властивості

Припустимо, в класі Company існує instance-змінна name.

@interface Company : NSObject
{
     NSString *name;
}

Для доступу до неї ззовні найкраще скористатися властивостями, які з'явилися у Objective C 2.0. Для декларації властивостей використовується ключове слово @ property.

@property (retain) NSString *name;

У дужках перераховуються атрибути доступу до instance-змінної. Атрибути поділяються на 3 основні групи.

Імена акцессора і мутатора
  • getter = getterName, використовується для задання імені функції, яка використовується для отримання значення instance-змінної.
  • setter = setterName, використовується для задання імені функції, яка використовується для задання значення instance-змінної.
Обмеження читання / запису
  • readwrite - властивість може бути "прочитана" та перезаписана
  • readonly - властивість може бути тільки "прочитана"

Ці атрибути взаємовиключають один одного.

Атрибути мутатора.
  • assign - для задання нового значення використовується оператор присвоєння. Використовується тільки для POD-типів або для об'єктів, якими ми не володіємо.
  • retain - вказує на те, що для об'єкта, який використовується як нове значення instance-змінної, управління пам'яттю відбувається вручну (не забуваємо потім звільнити пам'ять).
  • copy - вказує на те, що для присвоєння буде використана копія переданого об'єкта.
  • weak - аналог assign при застосуванні режиму автоматичного підрахунку посилань. (ARC повинен підтримуватися компілятором)
  • strong - аналог retain при застосуванні режиму автоматичного підрахунку посилань. (ARC повинен підтримуватися компілятором)

При роботі під GC ніякої різниці між використанням assign, retain, copy немає. Для створення коду властивостей, згідно з тим як вони описані у декларації, можна скористатися автогенерацією коду:

@synthesize name;

Автоматично створений код - не завжди оптимальне рішення і може знадобитися створення методів доступу до instance-змінних вручну. Мову часто критикують за перевантажений, в порівнянні з іншими мовами, синтаксис. Однак при цьому нерідко відзначається його вища читність.

Створення нових класів

Всі нові директиви компілятору у мові Objective-C починаються з символу @. Як і у C + +, опис класу і його реалізація розділені (зазвичай опис поміщається у заголовному файли з розширенням h, а реалізація - у файли з розширенням m). Нижче наводиться загальна структура опису нового класу:

@interface ClassName : SuperClass
{
    instance variable declarations
}

method declarations
@end

У версії runtime від Apple всі класи мають спільного предка - клас NSObject, який містить цілий ряд важливих методів. Опис змінних нічим не відрізняється від опису змінних в структурах в мові С:

@interface Rect : NSObject
{
        float     width;
        float     height;
        BOOL      isFilled;
        NSColor * color;
}
@end

Опису методів помітно відрізняються від прийнятих в C + + і дуже сильно схожі на описи методів в мові Smalltalk. Кожний опис починається зі знака плюс або мінус. Знак плюс позначає, що даний метод є методом класу (тобто його можна надсилати тільки class object'у, а не екземплярам даного класу). Фактично методи класу є аналогами статичних методів в класах у мові C + +. Знак мінус служить для позначки методів об'єктів - екземплярів даного класу. Зверніть увагу, що в Objective-C всі методи є віртуальними, тобто можуть бути перевизначені. Нижче наводяться описи можливих методів для класу Rect.

@interface Rect : NSObject
{
        float     x, y;
        float     width;
        float     height;
        BOOL      isFilled;
        NSColor * color;
}
+ newRect;
- (void) display;
- (float) width;
- (float) height;
- (float) area;
- (void) setWidth: (float) theWidth;
- (void) setHeight: (float) theHeight;
- (void) setX: (float) theX y: (float) theY;
@end

Зверніть увагу, що ім'я методу може збігатися з ім'ям instance-змінної даного класу (наприклад, width і height). Тип значення, яке повертається методом, вказується у круглих дужках відразу ж після знака плюс або мінус (але перед назвою методу). Якщо тип не вказаний, то вважається, що повертається значення типу id. Далі йде ім'я методу, де після кожної двокрапки задається тип аргументу (у круглих дужках) і сам аргумент. Мова Objective-C дозволяє для аргументів методу задавати також один з наступних описувачів - oneway, in, out, inout, bycopy і byref. Дані описувачі служать для задання напряму передачі даних і способу передачі. Їх наявність помітно спрощує реалізацію та роботу з розподіленими об'єктами (які були реалізовані у операційній системі NextStep до початку 90-х років минулого століття). Метод, що приймає довільну кількість параметрів, може бути описаний таким чином:

- makeGroup: (id) object, ...;

Для підключення заголовного файлу в Objective-C використовується директива #import, аналогічна # include, але яка гарантує, що даний файл буде підключений усього один раз. У ряді випадків виникає необхідність у тому, що дане ім'я є ім'ям класу, але без явного його опису (така необхідність виникає при описі двох класів, кожен з яких посилається на інший клас). У цьому випадку можна скористатися директивою @ class, яка оголошує, що наступні за нею імена є іменами класів.

@class Shape, Rect, Oval;

Реалізація методів класу виглядає наступним чином:

#import "ClassName.h"
@implementation ClassName
 
    method implementations
 
@end

Нижче наведено приклад реалізація методів класу Rect, описаного вище.

#import "Rect.h"

@implementation Rect

+ newRect
{
        Rect * rect = [[Rect alloc] init];
 
        [rect setWidth:  1.0f];
        [rect setHeight: 1.0f];
        [rect setX: 0.0f y: 0.0f];
 
        return rect;
}
 
- (float) width
{
   return width;
}
 
- (float) height
{
   return height;
}
 
- (float) area
{
   return [self width] * [self height];
}
 
- (void) setWidth: (float) theWidth
{
   width = theWidth;
}
 
- (void) setHeight: (float) theHeight
{
   height = theHeight;
}
 
- (void) setX: (float) theX y: (float) theY
{
   x = theX;
   y = theY;
}
@end

Як видно з прикладу, в методах доступні всі instance-змінні. Однак, як і в C + +, є можливість управляти видимістю змінних (видимістю методів керувати не можна) за допомогою директив @ private, @ protected і @ public (які діють повністю аналогічно мові C + +).

@interface Worker : NSObject
{
        char * name;
@private
        int    age;
        char * evaluation;
@protected
        int    job;
        float  wage;
@public
        id     boss
}

При цьому до public змінних класу можна звертатися безпосередньо, використовуючи оператор -> (наприклад objPtr -> fieldName).

Як працює механізм повідомлень

Компілятор переводить кожне надсилання повідомлення, тобто конструкцію виду [object msg] у виклик функції objc_msgSend. Ця функція своїм першим параметром приймає вказівник на об'єкт-отримувач повідомлення, другим параметром виступає т. зв. селектор, який слугує для ідентифікації повідомлення, яке надсилається. Якщо в повідомленні присутні аргументи, то вони також передаються objc_msgSend як третій, четвертий і т. д. параметри. Кожен об'єкт Objective-C містить в собі атрибут isa - вказівник на class object для даного об'єкта. Class object автоматично створюється компілятором і існує як один екземпляр, на який через isa посилаються всі екземпляри даного класу. Кожен class object обов'язково містить у собі вказівник на class object для батьківського класу (superclass) і dispatch table. Остання являє собою словник, який співставляє селекторам повідомлень фактичні адреси методів, які їх реалізують.

Таким чином, функція objc_msgSend шукає метод із даним селектором у dispatch table для даного об'єкта. Якщо його там немає, то пошук продовжується у dispatch table для його батьківського класу і т. д. Якщо метод (тобто відповідна йому функція) знаходиться, то здійснюється його виклик з передачею всіх необхідних аргументів. В іншому випадку об'єкту дається останній шанс обробити повідомлення перед викликом винятку - селектор повідомлення разом з параметрами «загортається» у спеціальний об'єкт типу NSInvocation і об'єкту надсилається повідомлення forwardInvocation, де параметром виступає об'єкт класу NSInvocation. Якщо об'єкт підтримує forwardInvocation, то він може або сам обробити повідомлення, яке посилається, або переслати іншому об'єкту для обробки:

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ( [someOtherObject respondsToSelector: [anInvocation selector]] )
        [anInvocation invokeWithTarget: someOtherObject];
    else
       ..........
}

Для прискорення пошуку повідомлень по dispatch table використовується кешування, яке дозволяє помітно знизити витрати на пересилання повідомлення. Також полегшує пошук методу за таблицями використання так званих селекторів замість звичайних імен. Зазвичай селектор являє собою 32-бітну величину, що дозволяє однозначно ідентифікувати метод. Тип селектора позначається як SEL і існує ряд функцій і конструкцій, що дозволяють здійснювати перетворення імені у селектор і назад. Так для отримання селектора повідомлення безпосередньо по імені служить конструкція @ selector ():

SEL setWidth = @selector(setWidth:);
   SEL setPos   = @selector(setPosition:y:);

Для отримання селектора по рядку символів (на етапі виконання) та перекладу селектора в рядок працюють функції NSSelectorFromString і NSStringFromSelector:

SEL        setWidth   = NSSelectorFromString ( @"setWidth:" );
   NSString * methodName = NSStringFromSelector  ( setPos );

Потужна підтримка метаінформації у Objective-C дозволяє прямо на етапі виконання перевірити чи підтримує об'єкт метод із даними селектором за допомогою надсилання йому повідомлення respondsToSelector ::

if ( [anObject respondsToSelector: @selector(setWidth:)] )
         [anObject setWidth: 200.0];

Досить легко можна послати повідомлення, яке відповідає даному селектору (без аргументів, із одним, двома або трьома аргументами), за допомогою методу performSelector:, performSelector: withObject:, performSelector: withObject: withObject:, performSelector: withObject: withObject: withObject: і так далі.

[myObject performSelector:sel withObject: nil];

Зверніть увагу, що методи performSelector завжди повертають значення типу id. Можна отримати клас для даного об'єкта, надіславши йому повідомлення class. Це повідомлення повертає клас у вигляді вказівника на об'єкт типу Class.

Class    * cls     = [anObject class];
NSString * clsName = NSStringFromClass ( cls );

З іншого боку, також можна легко отримати відповідний class object по імені класу:

 Class * cls = NSClassFromString ( clsName );

Кожен метод фактично являє собою функцію з двома невидимими аргументами - self і _cmd. Перший є аналогом this, тобто вказує на сам об'єкт - отримувач повідомлення. Другий містить селектор даного методу. Аргумент self може використовуватися для надсилання повідомлень самому собі, як. наприклад, у наступному методі:

- (float) area
{
   return [self width] * [self height];
}

Однак крім self є ще одна величина, якій можуть надсилатися повідомлення super. Насправді super не є нормальною змінною - це всього лише ще одне позначення для вказівника на поточний об'єкт. Але при надсиланні повідомлення super пошук методу починається не з dispatch table поточного об'єкта, а із dispatch table батьківського об'єкта. Таким чином, надсилаючи повідомлення super, ми тим самим викликаємо старі версії методів, перевизначені даним класом. У мові Objective-C можна по селектору методу отримати адресу функції, яка його реалізує. Така функція відрізняється від опису методу тільки вставкою у початок списку аргументів двох додаткових параметрів - вказівника на сам об'єкт (self) і селектора даного методу (_cmd). Надіславши об'єкту повідомлення methodForSelector ми отримуємо у відповідь адресу функції, яка реалізує цей метод.

typedef float (*WidthFunc)( id, SEL );
typedef void  (*SetWidthFunc)( id, SEL, float );
 
WidthFunc    widthFunc    = (WidthFunc)    [myRect methodForSelector: @selector (width)];
SetWidthFunc setWidthFunc = (SetWidthFunc) [myRect methodForSelector: @selector (setWidth:)];
 
(*setWidthFunc)( myRect, @selector (setWidth:), 27.5f );

Це дозволяє при необхідності багаторазового виклику одного і того ж методу у заданого об'єкта повністю уникнути всіх витрат, пов'язаних з пересиланням повідомлень.

Протоколи

Мова Objective-C містить повноцінну підтримку протоколів (в C + + це абстрактний клас, який також інколи прийнято називати інтерфейсом). Протокол являє собою просто список описів методів. Об'єкт реалізує протокол, якщо він містить реалізації всіх методів, описаних в протоколі. Протоколи зручні тим, що дозволяють виділяти загальні риси у різнорідних об'єктів і передавати інформацію про об'єкти заздалегідь невідомих класів. Найпростіше опис протоколу виглядає наступним чином:

@protocol ProtocolName
method declarations
@end

Так протокол Serializable може бути описаний таким чином:

@protocol Serializable
- (id)   initWithCoder: (NSCoder *) coder;
- (void) encodeWithCoder: (NSCoder *) coder;
@end

Протокол може бути успадкований від довільної кількості інших протоколів:

@protocol MyProto <Protocol1, Protocol2, Serializable, Drawable>

Точно так само можна при описі класу задати не тільки батьківський клас, але і набір протоколів:

@interface MyClass : SuperClass <Protocol1, Protocol2, Serializable, Drawable>

Для перевірки під час виконання програми підтримки об'єктом заданого протоколу можна використовувати повідомлення conformsToProtocol

if ( [myObject conformsToProtocol: @protocol (Serializable)] )
     [myObject encodeWithCoder: myCoder];

Крім того, ім'я протоколу (протоколів) можна використовувати при описі змінних для явної вказівки компілятору про підтримку відповідними об'єктами протоколу (протоколів). Так, якщо змінна myObject містить покажчик на об'єкт заздалегідь невідомого класу, але при цьому задовольняє протоколи Serializable і Drawable, то її можна описати таким чином:

id <Serializable,Drawable>  myObject;

Точно так само, якщо заздалегідь відомо, що myObject буде містити вказівник на об'єкт, успадкований від класу Shape і підтримуючого протокол Serializable, то цю змінну можна описати таким чином:

Shape <Serializable>  *myObject;

Зверніть увагу, що подібний опис служить тільки для повідомлення компілятору, які повідомлення підтримує даний об'єкт. Як і класи, всі протоколи в Objective-C представлені за допомогою об'єктів (класу Protocol):

Protocol * myProto = @protocol ( Serializable );

Для попереднього оголошення протоколів можна використовувати наступну конструкцію:

@protocol MyProto, Serializable, Drawable;

Ця конструкція повідомляє компілятор про те, що MyProto, Serializable і Drawable є іменами протоколів, які будуть визначені пізніше.

Ресурси

  1. Mac Developer Library

Джерела

  1. Advanced Flow Control for Objective-C
  2. Debugging C-Family Languages
  3. Fun with the Objective-C Runtime
  4. Steve Kochan on the Evolution of Objective-C
  5. The Dynamic Languages Renaissance
This article is issued from Wikipedia. The text is licensed under Creative Commons - Attribution - Sharealike. Additional terms may apply for the media files.