Данный пост был задуман после того, как на некоторые важные вопросы не было найдено внятных ответов. Я отнюдь не претендую на то, что стал крутым программистом. Нет, всё ещё впереди, но период высиживания уже пройден. Это статья из цикла «Не умеешь сам — научи другого». В смысле, чтобы что-то лучше понять нужно это что-то, кому-то объяснить. Мопед не мой, эта фраза встречалась мной раньше в публикациях на Хабре. Некоторые вещи очень сложно понять. И люди, которые понимают, обычно не могут объяснить начинающему. Может быть меня тоже это ждёт. Это как разговор взрослого и ребёнка. Практически конфликт поколений. Пока мой уровень не перерос в профессионала, нужно изложить моё текущее видение.
В статье, термины которые используются при объяснении различных понятий взяты в кавычки. Статья написана для людей, которые уже научились отличать циклы от массивов и понимают что такое функция и метод.
Если Вы, никогда не программировали, то книга Кернигана и Ритчи (в любом издании), Вам явно будет не по силам. В природе не существует такой силы воли, которая смогла бы заставить Вас дочитать книгу до конца, при этом решить все приведённые задачи.
Очень рекоммендую BecomeAnXcoder. Там всё написано достаточно понятно для людей, которые о программировании знают крайне мало. Информация немного устарела, но для старта сойдёт. Может она Вам уже попадалась на глаза или вы даже скачали её, но отложили в каталог с литературой (ну такой каталог есть у каждого, он занимает десятки гигабайт и всё это мы собираемся прочитать как только появится свободное время, а каталог всё растёт...).
Понятно, что оптимальный вариант — MAC. На худой конец — Хакинтош или виртуальная машина. И даже если процессор не позволяет запустить ничего из вышеизложенного — начать можно прямо в блокноте с последующей компиляцией в коммандной строке.
На многих форумах можно найти популярный вопрос, задаваемый людьми, которые, по-видимому находятся в тяжело-бронированном танке: «Нужно ли программисту Objective-C учить собственно С?». Везде пишут одно и то же. Те кто побородатее и начинали со структурного программирования — говорят, что просто необходимо стать, как минимум MiddleDeveloper'ом в C, и лишь потом искать путь в лютых дебрях ООП, коим является Objective-C. Те кто не знал С, но уже каким-то образом достиг статуса MiddleDeveloper'а, возражают: мол, выучи как кнопки на iPhone рисуются и как им назначить функцию, а матчасть — потом подтянется, по мере необходимости. Категорически заявляю, что выучить как кнопкам назначаются функции, не есть программирование! Я не призываю потратить полгода на понимание чистого С. Но пару недель — месяц уделить стоит. Это даст некоторый бэкграунд Вашим амбициям. По мере обучения уже Objective-C обязательно будут возникать такие структуры кода, что не будет ничего понятно. Поэтому, предлагаю безо всякого фанатизма, но детально ознакомиться с книгой Крупника А.Б. «Изучаем Си». Она раза в 3 больше предыдущей по объему, но читается легко и непринуждённо. В книге опять же, на достаточно доступном уровне, объясняются основные азы программирования. В ней можно узнать как отличить массив от цикла и что такое указатели.
Недавно на Хабре промелькнула статья, где автор приводил кучу литературы, которую советовал прочитать и пойти работать джуниором за хлеб. Если Вы просто прочитаете всю приведенную в той статье литературу, то ничему не научитесь, а если вы сможете перерешать и запомнить опять же всю литературу, то Вам можно будет идти не джуниором, а солидным разработчиком.
Начало: метод класса и метод экземпляра
Что такое метод? Это набор команд/инструкций или одна команда/инструкция, которая может быть применена к объекту и вызывает в нём необходимые процессы. Если так не совсем понятно, то читайте дальше и поймёте. На самом деле тяжело понять что такое метод класса и метод экземпляра. Все говорят: «со знаком „+“ — это метод класса, а вот со знаком „-“ — это метод экземпляра. Понятно?» И в ответ слышат: «Да-а, метод класса и метод экземпляра». Я лично сначала понимал только то что они разные, а вот что и когда применять — осталось не понятным. Википедия упорно повторяет то же самое, что мы слышали и читали.
В подавляющем большинстве случаев, Вы будете работать с методами экземпляра.
Итак, метод экземпляра. Представим, что у нас есть программа, в которой мы будем использовать 2 класса: Primus и Kitchen.
Класс для Objective-C, часто можно интерпретировать как объект и наоборот. Но есть классы, которые нельзя назвать объектами. Они называются «протоколы». В некоторых других языках программирования их называют «абстрактные классы». Различие состоит в том, что «протокол» не может иметь подкласс или дочерний объект. Применение протоколов в реальной жизни я затрону в этой статье. Любой класс имеет методы и/или функции.
Класс может иметь несколько методов со знаком "+" перед названием метода (метод класса) и несколько методов со знаком "-" перед названием метода (метод экземпляра). Вот те методы, которые со знаком "-" мы активно применяем как в отношении объектов — экземпляров класса, так и отношении самого класса. А методы со знаком "+" можно применять только в отношении самого класса, где этот метод был объявлен.
Что такое экземпляр класса?
Звучит гордо, но непонятно. Кто помнит из школьного курса: в геометрии точка — родитель для круга, прямой и прочих фигур, потому что каждая фигура состоит из множества точек.
Представим что есть некий класс «Горелка газовая». Он будет родительским для «Примуса» и «Газовой плиты». Для того, чтобы приблизиться к программированию, нужно немного абстрагироваться.
В Objective-C есть родительский класс NSObject. В языке программирования он как точка в геометрии. Это идеальный класс: в нём нет ничего лишнего, он ничего конкретного делает, но из него можно создать всё что угодно. Такой вот кирпичик мироздания. Если в Xcode зажать кнопку «Command» и кликнуть на NSObject, то перед Вами откроется содержимое файла NSObject.h. В нём около 200 строк описания различных методов. Есть как методы со знаком "+" перед названием, а также методы со знаком "-". Суть методов со знаком "+" такова что их можно применить только непосредственно к самому NSObject. Таким методом является, например,
+(id) alloc;
и применять его можно только так:
[NSObject alloc];
Любой другой объект в Objective-C по определению — потомок NSObject. В нём реализовываются дополнительные методы, которые придают ему черты индивидуальности. Допустим это NSResponder. У этого потомка — NSResponder’а тоже могут быть дочерние объекты. К примеру UIView. Далее по древу наследования можно в качестве примера взять UIScrollView. Потом — UITableView. В каждом случае потомки обрастают дополнительным функционалом. Кроме того, что они умеют делать что-то своё, индивидуальное, они могут делать всё то что может делать их предок.
В Ваших программах вы будете брать нужный Вам класс и добавляя в него нужные методы — расширять его функционал. Также можно брать созданный Вами класс и делать его потомка, опять же расширяя функционал и одновременно создавая уникальный инструмент, для решения нужной задачи. Описанный механизм называется — создание подклассов «subclass».
Не буду вдаваться в широкие описания методов со знаком "+". Нагляднее будет показать, что применение метода
+(id) alloc;
к любому классу или подклассу выделяет под него нужный объём памяти в ОЗУ компьютера.
NSObject *object = [NSObject alloc];
Применение метода
-(id) init;
инициализирует объект, выделяет для него место в ОЗУ и собственно после этого объект начинает существовать и с ним можно работать.
NSObject *object = [[NSObject alloc] init];
Эти 2 метода («alloc» и «init») только что создали объект «object». И данный объект является экземпляром объекта или экземпляром класса NSObject. Если применить эти методы по раздельности
NSObject *object = [NSObject alloc];
[object init];
то объект тоже будет создан. Но есть ненулевая вероятность того Вы создадите не тот объект, которому выделили память. Допустим, вызов метода
NSObject *object = [NSObject alloc];
выделил для него такой адрес в памяти
0x000000000
Так происходит потому что при инициализации, исполняемая среда, всем объектам задаёт значение «nil», чтобы избежать ссылки на место в памяти, заполненное «мусором». Или ссылки на другой конкретный объект, который по факту тоже уже является «мусором», потому что он закончил свой жизненный цикл и уже не «хранится» в памяти, а просто «находится». Для того чтобы понять что такое «мусор», создайте простейшую программку, в которой объявите
int i;
не присваивая ей значение, а потом выведите её значение в консоль
NSLog(@"%i", i);
Вы будете удивлены тем, чему равняется Ваше «i».
То есть теоретически объект уже есть после применения метода «alloc». Но он не сможет пройти проверку на
if (!self){
...
}
потому что он вроде как есть, но в принципе он равен «nil». Не инициализированный объект — это как машина, которую Вы только собираетесь куплить в кредит. Вроде как есть, но она не Ваша. И если не отходя от кассы применить метод «init»
NSObject *object = [[NSObject alloc] init];
то «Бинго!», и объект принадлежит Вам, с ним можно делать всё что хотите. А вот если к нему применить метод
[object init];
то инициализироваться может другой объект. Вроде как вы выплатили кредит за машину, но оказалось что не за ту машину и Вам вместо одного автомобиля дают другой. Они на первый взгляд не отличимы друг от друга. Но у них разные «VIN» номера, а значит при проверке на посту «ГАИ» Вам сообщат что авто не Ваш. Ниже будет приведено дополненное объяснение этого явления.
Как Вы уже поняли метод "+alloc" — метод класса, а метод "-init" — метод экземпляра.
Приведу пример с реальными вещами.
Полная реализация метода включает в себя обычно 2 файла с расширениями *.h и *.m. Бывает и другое количество, в зависимости от того, что это за класс и какие цели он преследует.
В Вашей программе есть класс «Primus». В файле «Primus.h» объявлены методы:
Primus. h
//метод класса, можно применить только к классу "Primus"
(Primus *) hotAsHell;
//метод экземпляра, инициализирует экземпляр класса "Primus"
(id) initWithGas:(Gas *)gas temperature:(NSInteger)t;
//метод экземпляра, задаёт параметры пламени
(void) doFireWithGas:(Gas *)gas;
//метод экземпляра, но его применение к текущему классу, вызывает экземпляр совсем другого объекта
(MasterPoRemontu *) serviceItems:(NSArray *)i
serviceSkills:(NSArray *)s;
Также в программе есть класс «Kitchen», где в файле «Kitchen.h», объявлены методы:
Kitchen.h
//метод класса, создаёт экземпляр объекта "Kitchen" и применяется непосредственно к объекту "Kitchen"
+(Kitchen *) kitchenFurniture:(NSArray *)furniture
otherDishes:(NSArray *)dish;
//метод экземпляра, применение его к кухне вызывает экземпляр другого объекта
(UkrainianBorsch *) borschWithIngredients:(NSarray)ingredients
casserolAndPan:(NSArray)pan;
В файлах «Primus.m» и «Kitchen.m» все объявленные методы должны быть реализованы, то есть описаны: как и что в каждом методе происходит.
Если вы собираетесь в каком-либо классе создавать экземпляр объекта другого класса, то в шапке нужно будет импортировать файл *.h создаваемого объекта, например в шапке файла Kitchen.h мы впишем
#import "Primus.h"
То есть можно будет создать класс «Primus» внутри класса «Kitchen».
Так как мы импортировали заголовочный файл Primus.h в файл «Kitchen.h», то в классе «Kitchen» нам стали доступны те методы экземпляра класса «Primus», которые объявлены в файле Primus.h. Их можно будет применять либо по отношению к классу, либо по отношению к экземпляру класса «Primus». Для начала создадим экземпляр класса «Primus» внутри класса «Kitchen»
Primus *myPrimus = [Primus hotAsHell];
Как видите, мы посылаем сообщение методу c "+" не экземпляру класса «myPrimus», а непосредственно классу «Primus». Экземпляр класса «Primus» под названием «myPrimus» создан. В дальнейшем, методы из «Primus.h» со знаком "-" будут применяться к экземпляру класса — «myPrimus». И если захотим создать новый экземпляр класса «Primus», то можно будет опять воспользоваться методом "+hotAsHell". Также в классе «Primus» есть метод
-(id) initWithGas:(Gas *)gas temperature:(NSInteger)t;
Обычно все методы, название которых начинается с «init», делают то же самое что и метод «init» класса NSObject, только с расширенными возможностями. И в данном случае применение метода «init» в таком ракурсе
Primus *myPrimus = [[Primus alloc] initWithGas:Propan temperature:750];
создаст новый объект класса «Primus» с уникальными характеристиками.
В описанном только что примере был применён метод с аргументами. В данном, конкретном случае, он применяется для создания экземпляра класса «Primus» с чётко заданными характеристиками. По названию характеристик видно, что данный экземпляр класса «Primus» будет работать на газе типа «Propan» и при температуре 750 градусов.
Обратите внимание, что названия методов в Objective-C весьма осмысленны. Временами Вы будете поражены их названиями состоящими из 10 слов, но это способствует лучшему понимаю написанного. Грубо говоря код получается самодокумментируемым. Что конечно же не исключает необходимости написания комментариев. Но если взять за правило создавать свои методы, название которых будет нести смысловую нагрузку и таким же образом относиться к созданию переменных, то количество комментариев в коде можно заметно снизить.
В отличие от некоторых других языков программирования в Objective-C методы не вызываются, а посылаются объектам. Таким образом, этот ЯП относится к «message oriented language», на русском языке звучит не так строго, поэтому перевод не привожу.
Также обратите внимание на то, что все методы которые после посылки объекту создают (возвращают) новый объект имеют в скобочках после "+" или "-" либо конкретное название объекта, экземпляр которого они будут возвращать, либо «id». То есть, по том, какой объект указан в скобочках сразу после знака "+" или "-", можно судить что мы получим после посылки этого метода. Что такое «id»? Это такой тип объекта, который может принимать форму (тип) любого другого объекта. Скажем это «слабый» тип объекта. Не в смысле того, что ему что-то «слабо». Если говорить общепринятыми понятиями, то это слабо типизированный объект. Наличие таких объектов в ЯП делает его слабо типизированным.
Также можно в этих самых скобочках обнаружить тип «void».
Что такое «void»?
Все говорят: «это когда метод ничего не возвращает». А зачем же он нужен? Применение метода этого типа всё же производит манипуляции с объектами, структурами и цифрами. Также объекты и скалярные величины, попавшие в него в качестве аргументов могут измениться либо вообще исчезнуть. Всё зависит от того, что реализовано внутри каждого конкретного метода. Дело в том, что «void» таки ничего не возвращает… нового. Как и было сказано, то что попавшие в него аргументы могут изменяться сами, либо влиять на другие объекты, манипуляция с которыми производится в методе с «void». Он может даже создавать новые объекты внутри себя, но наружу ничего не возвращается в явном виде. Обычно метод, который возвращает что-то, имеет ключевое слово «return» в самом конце. А метод типа «void», «return» в конце не имеет. Он может его иметь внутри своих собственных циклов или условий, чтобы иметь возможность прервать их в нужном месте. Но не в конце. Метод не создаёт ничего принципиально нового, он просто, работая с указателем на какой-то объект (число, массив, ячейка и т.д.), меняет данные по этому адресу или применяет их для расчёта других данных, которые присваивает другим объектам.
Что такое «self»?
Это слово очень часто применяется где попало. Сам факт его применения во всевозможных местах, вносит путаницу в стройные кладовые Ваших знаний.
Вернёмся к классам «Primus» и «Kitchen». Возьмём конкретный метод класса «Primus» и рассмотрим как будет выглядеть его реализация в файле «Primus.m». Например это метод.
(void) doFireWithGas:(Gas *)gas;
может быть реализован таким образом:
-(void) doFireWithGas:(Gas *)gas {
if (!myPrimus){
[self initWithGas:gas temperature:750];
}
}
В фигурных скобках происходит реализация или «имплиментация» метода. В данном случае, реализация метода читается так: если не myPrimus, то выполнить команду
[self initWithGas:propan temperature:750];
которая в свою очередь читается как: себе инициализировать с газом:gas температурой:750. То есть, метод посылается себе, а не какому-нибудь другому объекту. В данном случае «себе» — это классу «Primus», так как метод находится внутри класса «Primus». Его объявление — в файле Primus.h и реализация в файле Primus.m.
Почему был использован знак восклицания "!" в скобках после «if»? В программировании это — знак отрицания. То есть "!=" звучит как «не равно». Этот способ условия удобен своей лаконичностью. В данном примере можно было использовать другое по структуре условие, которое несло бы ту же самую функцию.
if (myPrimus == nil){}
Это условие имеет в 2 раза большую длину и нужно полностью читать всю строку, чтобы понять его. Это условие небольшое, но если Вам нужно записать что-то вроде этого
if ((myPrimus == nil || yourPrimus == nil) && (mamaPrimus == nil || papaPrimus == nil)){}
то эта структура
if ((!myPrimus || !yourPrimus) && (!mamaPrimus || !papaPrimus)){}
будет в более выигрышной позиции, как по лаконичности так и по физическому размеру. Предыдущая конструкция — банально может просто не влезть в одну строку.
Рассмотрим как выглядела бы реализация этого метода в рамках класса «Kitchen». В файле «Kitchen.m» тот же метод будет реализован так:
(void) doFireWithGas:(Gas *)gas {
if (!myPrimus){
Primus *myPrimus = [Primus initWithGas:gas temperature:750];
}
}
Видно, что здесь нужно указать, какой объект будет создаваться, как будет называться его экземпляр и потом непосредственно родительскому классу посылать метод.
То есть, хитрое слово «self», применяется внутри класса, как будто вы уже создали его экземпляр.
Primus *myPrimus;
и обращаетесь к экземпляру класса. Только фишка в том, что вы ничего не создавали, и обращаетесь не к экземпляру, а напрямую к классу.
Для того, чтобы понять можно ли написать «self» или нельзя — просто представьте себе какой метод какому объекту Вы хотите послать и начните вводить слово «self», но не до конца, например «sel». Xcode предложит несколько вариантов, среди которых будет собственно «self». Слева от него будет описание — какой это класс. Если это именно тот класс, которому вы хотели послать метод (класс внутри которого вы сейчас производите действие), значит используйте «self». Если Xcode указывает, что «self» это не тот объект, который Вам нужен — то используйте имя класса или экземпляра нужного Вам.
Также видно что здесь были задействованы аргументы. У каждого аргумента есть тип, который тоже описан в скобках, но после двоеточия. После скобок с типом аргумента идёт имя аргумента. Если в скобках присутствует знак "*" после типа аргумента, то это объект. Если знака "*" нет, то это скалярный тип, например «NSInteger», который по сути является простым «int». Имя аргумента, в данном методе
-(void) doFireWithGas:(Gas *)gas;
будет звучать как «gas». Вы обратили внимание, что в реализации не было использовано нормального названия газа? Например «Propan». Дело в том, что в аргументе «gas» как раз и передаётся нужное название газа. Откуда? Мы рассмотрели как реализовуется метод
-(void) doFireWithGas:(Gas *)gas;
В нём был задействован метод
-(id) initWithGas:(Gas *)gas temperature:(NSInteger)t;
В этом методе было 2 аргумента «gas» и «t». В посылке метода объекту, температуру мы указали, а тип газа просто продублировали от метода
-(void) doFireWithGas:(Gas *)gas;
То есть этот метод тоже где-то посылается. Рассмотрим поближе метод
(Primus *) hotAsHell;
Его реализация может выглядеть так
(Primus *) hotAsHell{
self = [super init];
if (self){
[self doFireWithGas:propanButan];
}
return self;
}
Здесь видно что метод посылается объекту при определённых условиях.
Нужно знать что ни один Ваш метод сам по себе не может сработать. Ему нужен «пинок» снаружи. Нажатие кнопки, возникновения какого-либо события или его должен запустить метод протокола, который среда исполнения запускает в нужный ей момент. Данный метод «hotAsHell» посылается лишь после явного вызова, это не системный метод, он тоже где-то должен быть прописан.
Теперь возьмём метод
-(id) initWithGas:(Gas *)gas temperature:(NSInteger)t;
Он состоит из типа возвращаемого объекта, типов аргументов, аргументов и селектора. Селектор это то что остаётся от метода когда от него отнять тип возвращаемого объекта, тип аргументов, и аргументы.
initWithGas:temperature:
именно с двоеточиями. Если аргументов нет, то двоеточий тоже нет, как в случае с «hotAsHell». Часто прийдётся сталкиваться с необходимостью употребить селектор. Теперь вы знаете что употреблять.
Немного выше было использовано понятие «super».
Что такое «super»?
Рассмотрим реализованный выше метод «hotAsHell». В нём есть такая конструкция
self = [super init];
Так принято инициализировать дочерний класс — через инициализацию родительского.
Предположим, что класс «Primus» — непосредственный потомок класса «NSObject». Именно тот класс, который стоит по иерархии сразу над тем классом, с которым мы работает имеет право называться «super». Команда
[super init];
вызывает метод init экземпляра NSObject. То есть, автоматически создаётся экземпляр прародителя всех объектов — NSObject, с дефолтными (сугубо NSObject'овскими) параметрами. И этот экземпляр присваивается нашему объекту «Primus». Ведь «self» в данном случае — является именно «Primus’ом». И мы на данном этапе получаем экземпляр объекта «Primus», который ничем не отличается от NSObject’а. Индивидуальные черты ему придаёт наша посылка метода
[self doFireWithGas:propanButan];
а также остальные методы, реализованные в рамках данного класса.
Конструкция
if (self) {...}
это просто перестраховка на случай, если что-то пойдёт не так и дефолтный «Primus» не создастся. т.е. метод init класса родителя не выполнится. В таком случае обычно делают
...
else {...}
где пытаются исправить положение дел, но, к сожалению, это не тот случай и «else» нам уже ничем не поможет.
И здесь я хочу Вам напомнить о неинициализированном объекте из начала статьи. Именно при инициализации экземпляра объекта его суперклассом, происходит назначение ему места в памяти. И когда проводим проверку
if (self = [super init])
то именно в этот самый момент «self» может получить неожиданный адрес в памяти. Суперкласс не возвратит нам что-то из ряда вон выходящее. Экземпляр объекта будет идентичен тому с которым мы собираемся работать. Но экземпляр именно этого объекта не будет тем экземпляром, который нам нужен.
Последнее, что мы должны сделать для завершения инициализации
return self;
Поскольку данный метод не «void», то он ожидает от нас, что в конце мы скажем ему что нужно возвратить. В данном случае будет возвращён объект типа «Primus», потому что «self» в рамках данного класса — именно «Primus». Также тип возвращаемого объекта нам говорит о том что ожидается именно «Primus».
В этих методах
+(Kitchen *) kitchenFurniture:(NSArray *)furniture
otherDishes:(NSArray *)dish;
(UkrainianBorsch *) borschWithIngredients:(NSarray)ingredients
casserolAndPan:(NSArray)pan;
(MasterPoRemontu *) serviceItems:(NSArray *)i
serviceSkills:(NSArray *)s;
тип возвращаемых объектов — «Kitchen», «UkrainianBorsch» и «MasterPoRemontu».
Таким образом, в методе «hotAsHell» мы говорим, что в конце выполнения данного метода желаем получить объект типа «Primus» с заданными параметрами.
Какими именно заданными параметрами?
Опишем такую реализацию метода
- (id) initWithGas:(Gas *)gas temperature:(NSInteger)t {
...
[self setGas:gas];
[self setTemperature:t];
...
}
Это значит, что при вызове этого метода
Primus *myPrimus = [[Primus alloc] initWithGas:metan temperature:500];
задаются буквально такие параметры
[self setGas:metan];
[self setTemperature:500];
Параметры задаются переменным, которые объявлены в файле *.h
Обратите внимание на конструкцию слова «setGas» или «setTemperature». Если есть переменная, например «variable», то задать ей нужное значение можно через префикс «set»:
setVariable
при этом первая буква переменной становится заглавной. Таким образом, мы выяснили что в файле Primus.h были объявлены переменные «gas» и «temperature». Но само наличие переменных не даёт нам возможности назначать их с помощью префикса «set»
Для получения такой возможности нужно объявить свойства для этих переменных. Допустим такие:
@property (nonatomic, strong) Gas *gas;
@property (nonatomic, strong) NSInteger temperature;
и только после этого станет возможным присвоение им значения через «setGas» и «setTemperature». Эти префиксы называются сэттерами.
В любой удобный момент можно попросить объект «Primus» показать Вам эти значения, обращаясь к его экземпляру
[myPrimus gas];
[myPrimus temperature];
или
myPrimus.gas;
myPrimus.temperature;
например для команды
NSLog(@"Примус газовый работает на газе марки %@, рабочая температура %i", [myPrimus gas], myPrimus.temperature);
Эти методы называются гэттерами. В случае обращения за значениями к названию переменной, ничего дописывать не нужно.
Обратите внимание, что к свойству объекта можно обращаться как через конструкцию
[myPrimus gas];
так и через
myPrimus.gas;
Они выполняют аналогичные действия.
Есть одна хитрость при доступе к переменным объекта. Это можно производить не только через свойства, но и через доступ к ним по принципу ключ-значение «key-value coding». То есть можно обратиться к переменной
myPrimus.gas;
так
[myPrimus valueForKey:@"gas"];
Мы обратились за значением, которое находится в объекте «Primus» в ключевом слове «gas».
А такая конструкция
[myPrimus setGas:metan];
в контексте «key-value coding» будет выглядеть так
[myPrimus setValue:@metan forKey:@"gas"];
Вместо свойств использовать «key-value coding» — вполне нудное занятие. Но иметь понятие о нём необходимо, чтобы в случае, когда без этого не обойтись — воспользоваться.
runtime
Даже если этого ещё не произошло, есть вероятность того что вскоре Вы встретите в документации слово «runtime» и в пределах той же статьи слово «isa».
Понятие «runtime» можно охарактеризовать как «среда в которой происходит перевод Вашего кода на более низкоуровневый код». «runtime» написан непосредственно с использованием C и Ассемблера. Это ещё не перевод в машинный код, а приведение Вашего кода к языкам C и Ассемблеру. Ваш метод
[myPrimus initWithGas:gelium temperature:nil];
в «runtime» на С выглядит примерно как
objc_msgSend(myPrimus,@selector(initWithGas:temperature:),gelium,nil);
Этого для начинающего Cocoa программиста должно быть достаточно, чтобы понять: дальше лучше в дебри не лезть. Как только Вы перейдёте порог вхождения в клуб, то сами сможете отыскать нужную Вам информацию.
Пока же нас интересует что такое «isa».
Это переменная, которая объявлена непосредственно в NSObject’е. Единственная его переменная. Когда мы вызывали метод
[super init];
то создавался не только экземпляр класса NSobject, но и экземпляр этой переменной «isa», которая ссылается на NSObject. То есть, она конкретно говорит «runtime’у», что она принадлежит объекту NSObject. А значит, и работать с экземпляром Вашего, только что созданного объекта нужно как и с NSObject’ом. Допустим, Ваш объект — потомок NSArray или UITableView или CFDictionaryRef или любого другого объекта. В таком случае «isa» указывает на NSArray или UITableView или CFDictionaryRef или любой другой объект соответственно. Так что создание экземпляра любого объекта, создаёт и переменную класса — «isa», которая ссылается непосредственно на родительский класс, вследствие чего «runtime» знает как будет поступать с каждым экземпляром.
Эта информация на этапе обучения нужна не для чего-то конкретного, а в принципе, для более объёмного понимания философии программирования Objective-C.
В процессе чтения книг и различной документации, на глаза Вам не раз попадётся понятие «singleton». Как говорит один популярный интернет-мем: «Нельзя вот так сразу взять и понять, что такое „singleton“.
Что такое синглтон?
Вообразим себе, что Вам нужно создать объект, который при каждом его вызове в любой точке приложения, возвращает один и тот же экземпляр. На самом деле, в процессе создания приложений, Вам действительно нужно будет такое создавать. Так почему же нельзя создать объект, несколько раз и присвоить ему те же данные через метод „initWithSomething:“ или с помощью сеттеров? Всё дело в работе с памятью и быстродействием, да и с собственно, меньшими затратами времени на написание кода. Памяти всегда мало, и даже когда на iPhone6 поставят 2Гб ОЗУ — её опять-таки будет мало. Создание одного экземпляра объекта, и последующее обращение к нему — экономит ресурсы устройства и ускоряет работу приложения. А ведь каждый хочет, чтобы его приложение было быстрым как „Bugatti Veyron“ и юзабельным как слово „хрен“.
Допустим что наш „Primus“ вполне может быть синглтоном. Тогда его метод
+(Primus *) hotAsHell;
при реализации будет выглядеть так
+(Primus*) hotAsHell{
static Primus *myPrimus = nil;
static dispatch_once_t predicate;
dispatch_once (&predicate, ^{myPrimus = [[super allocWithZone:nil] init];});
return myPrimus;
}
Рассмотрим что это значит позже. Пока что выясним, зачем „Primus“ делать синглтоном.
Допустим, это не вполне обычный „Primus“, а раритетный. На нём есть гравировка неизвестного мастера, он имеет крайне низкое потребление газа, а также у него есть специальная коробочка, в которой он идеально укладывается. А теперь задайте себе вопрос: „нужен ли Вам другой примус?“. Конечно же нет! Но при посылке метода
Primus *myPrimus = [[Primus alloc] initWitGas:metan temperature:500];
будет создан один „Primus“, а при посылке метода
Primus *myPrimus = [[Primus alloc] initWitGas:propan temperature:600];
будет создан абсолютно другой „Primus“. Без гравировки и коробочки.
Теперь вернёмся к тому, что написано в реализации метода „hotAsHell“.
Для начала нужно создать экземпляр объекта со свойством „static“, для того, чтобы перекрыть доступ к объекту извне. Затем присвоить ему „nil“, для того чтобы он не взял случайный адрес в памяти. Конструкция
static dispatch_once_t predicate;
создаёт предикат (условие) — »predicate" который тоже не будет виден извне. Условие заключается в том, что запрещается автоматический или динамический вызов блока, стоящего за предикатом. А вот строка
dispatch_once (&predicate, ^{myPrimus = [[super allocWithZone:nil] init];});
уже производит все необходимые действия для создания уникального экземпляра объекта. Конкретно «dispatch_once» означает что выражение в скобках после него гарантированно будет запущенно только один раз на протяжении всего жизненного цикла приложения. Также «dispatch_once» гарантирует потокобезопасность. То есть, при запуске приложения в многопоточном режиме эта функция не будет вызвана одновременно в нескольких потоках, и у ж точно не создаст нам ещё один «уникальный „Primus“». Также есть блок
^{myPrimus = [[super allocWithZone:nil] init];}
Это вроде маленькой функции или метода. Бывают также большие блоки. В блоке происходит инициализация объекта-родителя, опять же присваивая ему «nil» в качестве адреса размещения.
Вместе вся строка
dispatch_once (&predicate, ^{myPrimus = [[super allocWithZone:nil] init];});
означает: один раз за весь жизненный цикл приложения будет инициализирован экземпляр объекта «Primus» со свойствами объекта-родителя и не будет возможности обратиться к этому блоку другим путём. Всё это происходит благодаря GCD (Grand Central Dispatch). Рассказ о котором — отдельная тема.
И конечно же в конце мы возвращаем созданный синглтон
return myPrimus;
Синглтон — «Primus» у нас есть, теперь можно добавить ему переменные: коробочка — «Box», гравировка — «Etching», КПД — «Performance». И если объявить для них свойства, то можно будет извне менять эти переменные. Обшить коробочку, почистить гравировку, прочиповать наш «Primus» для повышения КПД. Но это останется наш старый добрый «Primus». Доступ извне к объекту «Primus» будет у тех классов, в шапке которых он объявлен. Но теперь, если делать так
Primus *myPrimus = [Primus hotAsHell];
а потом работать с «myPrimus» как с синглтоном, то ничего работать не будет. Все обращения к переменным синглтона должны происходить в следующей манере
Box *someBox = [Primus hotAsHell].Box;
так можно создать экземпляр «someBox» совсем другого класса коробок и ему присвоить значение, которое имеет коробка нашего синглтона или наоборот
[Primus hotAsHell].Box = someBox;
поменять коробку нашего «Primus».
Синглтон можно применять когда нужно вызвать NSLog с описанием свойств синглтона, а самого синглтона в данном классе нет в принципе. В таком случае его нужно просто объявить в шапке файла и вызвать один раз там где нужно. Синглтон можно и даже рекомендуют использовать в качестве глобальной переменной. Точнее его переменные будут глобальными.
После усвоения основных принципов, можно будет начинать решать предлагаемые в учебниках задачи. И если с написанием своих методов в своих классах и посылкой метода объекту разобраться можно, то в дальнейшем, нужно будет использовать делегаты, протоколы и прочие аспекты MVC. Необходимо будет использовать документацию Apple и применять тысячи различных методов, заботливо созданных и описанных купертиновскими программистами.
И тут становится непонятно в принципе как и что работает. Если созданный Вами метод создаёт внутри себя массив, потом вносит в него объекты, потом запускает цикл и что-то в нём делает, а потом этот метод запускает другой Ваш метод, то вроде всё понятно. Но вот Вы открываете документацию по интересующему Вас объекту, а в нём 20 методов, которые могут делать весьма занятные вещи. Кроме того, есть объекты предки, методы которых этот объект тоже может принимать. Кроме того, в начале статьи я писал о протоколах, которым может соответствовать объект. Итого, методов может быть сотни. Какой из них нужно применять? Встречный вопрос: «для чего именно Вам нужен метод?». Правильно заданный вопрос — уже половина ответа.
И чем отличаются методы классов и протоколов?
Если Вы хотите чтобы этот экземпляр объекта сделал что-то, присущее только классу от которого он произошёл, то в документации по этому классу нужно внимательно поискать метод, который производит необходимые Вам операции. Послать это метод объекту нужно так
[myPrimus doSomethingWith:Potato];
То есть, в документации Вы узнали, что этот метод берёт указанный аргумент «Potato» и делает с ним что-то, что в конечном итоге приводит Вас к цели если применить метод к объекту «myPrimus». Не нужно реализовывать этот метод, он реализован за Вас для прямого применения.
Но есть также методы протоколов или абстрактных классов. Как я уже указывал в начале статьи, у них не может быть потомков. Они являются просто набором методов. Для того, чтобы применить метод протокола внутри себя, объект должен соответствовать этому протоколу. Это указывается в файле *.h
@interface Primus : UIViewController <UITableViewDataSource, UITableViewDelegate>
В данном случае, объект соответствует сразу двум протоколам «UITableViewDataSource» и «UITableViewDelegate». Если зайти в описание этих протоколов, то там можно найти методы, которые объект может реализовать. Обратите внимание. Методы протоколов Вы не посылаете своему объекту, а должны внутри них указать как должен отреагировать ваш объект, при обращении программы к этим методам. Некоторые методы в протоколах могут быть обязательными. И если вы решили что Ваш объект должен соответствовать протоколу, то обязательные методы нужно реализовать в первую очередь. Внутри любого метода протокола может быть любой метод класса. То есть, метод протокола — это контейнер, в котором находятся любые другие методы, собственно как и любой реализуемый Вами метод. Реализовать необходимый функционал внутри каждого метода протокола нужно исходя из потребностей. К примеру, нужно сделать так, чтобы Ваша формочка делала что-то, при определённых условиях. К примеру, меняла цвет, после того, как стала активной/неактивной. Идём в документацию Apple, смотрим каким протоколам соответствует родительский класс формочки. Если этих протоколов нет в стандартном наборе функций, то добавляем их в < > скобках. В описании этих протоколов ищем методы, которые реализуются после какого-либо события. Допустим
(UIColor *) areaDidChangeColor:(CGRect)rect isActive:(BOOL)active;
который автоматически выполняется когда аргумент «active» принимает значение «YES». И меняет цвет в части экрана описанной в «rect»:
{
if (isActive && myForm == rect){
myColor = [UIColor redColor];
}
return myColor;
}
Методы протоколов задают параметры работы экземпляра класса, меняют функционал, передают значения. Например:
(NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
возвращает количество строк в секции «section» для заданной таблицы «tableView».
На этом позволю себе закончить. Если статья поможет целевой аудитории, в которой несколько месяцев назад числился и я, то значит, что я думаю правильно и такие статьи нужны. Статьи не профессионалов, а людей, которые понимают что-то на таком уровне, который тяжело перешагнуть, не имея опоры или трамплина. Надеюсь эта статья кому-то будет трамплином или хотя бы табуреткой. Отсутствие хороших учителей, которые могут нормально что-то объяснить — фундаментальная проблема современности. У меня нет педагогического образования, но в статье изложено понятие в программировании в таком ключе, в каком лично мне было бы понятно.
Автор: tricton