Шокирующий Objective-C для Java программистов, часть вторая

Продолжаю краткий обзор особенностей Objective-C, удивляющих Java программистов (первая часть). Во второй части немного будет рассказано о философии языка, о поддержке пространств имен и о типе id. Судя по комментариям к первой части, необходимо немного пофилософствовать, поэтому обещанные свойства (property) переносятся в третью часть. Также узнаем какие вопросы задают в американских супермаркетах.



Совершенство это не когда нечего добавить, а когда нечего отнять



Как правильно было отмечено bobermaniac в комментариях к первой части — синтаксис не очень важен, важнее понимать идиомы языка. Не менее важно понимать и общие принципы построения языка, его философию. Самого дрожь берет от высокопарности.

Я начал с заезженной цитаты, но она отлично характеризует дизайн языка С. Простота, выразительность, лаконичность и все это без ущерба возможностям. Создатели С верили в людей — в результате в споре компилятора и программиста всегда побеждает последний (хотя и не всегда конечный пользователь этому рад). Но потом программисты захотели работать с объектами. И на компактный фундамент С водрузили колонны объектной ориентации. Наибольших успехов в этом добился С++. Но формально используя С как основу, С++ не следует его духу минимализма. Разница в толщине «Кернигана-Ричи» и «Страуструпа» наводит на размышления.

Брэд Кокс (Brad Cox) хотел добавить возможности SmallTalk в С, не создавая полностью новый язык. У него это получилось и созданный им Objective-C является надмножеством языка С, сохраняя дух его дизайна и в объектных расширениях. Т.е. язык С оставлен как есть, объектно-ориентированные нововведения сделаны в четко оговоренных границах и их не так много. Отчасти это объясняет непривычный синтаксис Objective-C? все эти [] и @ — это те желтые ленты, которые огораживают зону строительных работ (или желтые ленты вешают только на месте преступления?). Один характерный пример минимализма — в Objective-C даже нет специального синтаксиса для создания объектов, как нет его и для определения конструктора и деструктора. Методы alloc, init, new, dealloc являются не частью языка, а частью фреймворка.

Создатели Java пошли немного другим путем и основное внимание уделили безопасности языка, введя более строгую проверку соотвествия типов, проверку выхода за границы массива, обязательную обработку и спецификацию исключений и много другое. Вера в человека была заменена верой в бездушный компилятор. Поэтому программистам на Java очень важно помнить, что Objective-C, как и С, гораздо меньше ограничивает автора, часто в ущерб безопасности кода. Я даже говорю не об управлении памятью, а таких особенностях языка, как категории, непроверяемые компилятором исключения, неформальные протоколы, отсутствие гарантии существования метода (не всегда, но бывает). При желании по своим ногам можно палить с двух рук очередями.

Теперь давайте проследуем за настроженно озирающимся Java программистом и рассмотрим пару практических вопросов.

Do you have id?



Подобный вопрос можно услышать при покупке алкоголя в Северной Америке. В ответ следует показать удостоверение личности с данными о возрасте (обычно водительские права). Я первый раз услышал «idea» в вопросе и удивленно ответил, что собираюсь безыдейно выпить все 6 бутылок пива. Продавец осторожно потянул упаковку к себе.

В Objective-C тоже есть тип id — универсальная ссылка на объект любого класса. Если Java программист подумал, что это напоминает ссылку на Object, то он ошибается. Если С программист подумал, что это напоминает void*, то он ошибается. Компилятор рассматривает id именно как ссылку на объект любого класса. Можно вызывать любой ранее определенный метод любого класса (но если метод не определен — нельзя), присваивать объект любого класса без приведения типов и т.п. Единственное ограничение — нельзя обращаться к полям объектов.

Еще раз напомню структуру классов из предыдущей статьи

@interface Profile : NSObject
@property (readonly) int version;
@end

@interface Feature : NSObject
- (Profile*) getProfile:(NSString*)name;
@end

@interface Phone : NSObject
+ (Phone*) designAndProduce:(NSString*)name;
-(Feature*) getFeature:(NSString*)name;
@end



Теперь несколько примеров использования id:

// Можно так
id idPtr =  [Phone designAndProduce:@"iphone5"];
// Можно и так
Phone* anotherPhone = idPtr;
// Можно даже так (с ошибкой во время выполнения программы)
Feature* badFeature = idPtr;
// Так нельзя - компилятор скажет, что сообщение getFeture не определно и будет прав
Feature*  anotherBadFeature = [idPtr getFeture:@"bt"];
// А так можно
Feature*  btFeature = [idPtr getFeature:@"bt"];
// Даже так можно, сообщение существует, компилятор нам верит, а зря
Profile* profile = [idPtr getProfile:@"a2dp"];



В последнем случае при запуске программы мы получим сообщение об ошибке: Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Phone getProfile:]: unrecognized selector sent to instance 0x100114510'
и начнем немного лучше понимать как работает диспетчеризация сообщений.

Plastic or paper?



Еще один хороший вопрос от кассиров американских супермаркетов. Спрашивают какой именно пакет дать, но с первого раза мало кто догадывается.

Так вот, о пакетах. Неприятная новость для Java программистов — пакетов (packages) здесь нет. Нет вообще никаких механизмов управления пространствами имен. Можно только выбирать какие заголовочные файлы включать, а какие нет. Скажем спасибо минимализму С и консервативности авторов Objective-С. Чтобы избежать конфликтов имен рекомендуется использовать префиксы. Например, NS в имени класса NSObject — это префикс классов, разработанных для ОС NextStep компании NeXT. Последнюю основал Стив Джобс в перерыве между работой в компании Apple и работой в компании Apple.

Нужно помнить одно важное отличие от Java. JVM доступны все классы платформы Java, все классы добавленные пользователем в classpath и/или lib/ext и кое-что еще. Поэтому конфликта имен избежать очень сложно. Программа на Objective-C имеет дело только с классами, использующимися в коде, так что вероятность конфликта на порядок меньше. Это объясняется разной семантикой import в Java и #import в Objective-C. Первый импортирует пространство имен, позволяя использовать короткие имена классов в коде. Но сам класс доступен и без этого, придется лишь писать полное имя класса (т.е. не List, а java.util.List). В Objective-C происходит настоящий импорт определения класса, без него компилятор не имеет никаких знаний о данном классе.

Иногда полезна полумера — директива @class позволяет объявить класс без его определения.

@class Profile

@interface Feature : NSObject
- (Profile*) getProfile:(NSString*)name;
@end


В этом случае мы уверяем компилятор, что такой класс существует и он соглашается откомпилировать наш код. Т.к. компилятор все равно ничего не знает о структуре класса, посылка сообщений объектам этого класса вызовет ошибку компиляции. Но это обычно и не надо, т.к. директива обычно используется в заголовочных файлах. Возникает вопрос — зачем вообще она нужна, если можно обойтись #import? Обычно для оптимизации компиляции в больших проектах, т.к. импорт может сильно увеличить объем кода и, соответственно, время компиляции. Вторая причина — цикличные ссылки между классами.

Заключение


Надеюсь, единственный оставшийся после прочтения вопрос — при чем тут американские супермаркеты? Не знаю, просто такие подзаголовки получились.
+19
21 февраля 2012, 17:57
144
hashmap 5,0

комментарии (19)

+1
bobermaniac #
Вообще говоря, рекомендуется через #import подключать только те заголовочные файлы, описание классов которых будет использоваться в данном файле.

То есть, если у вас заголовочный файл — вы подключаете через #import только тот класс, от которого наследуетесь (или, соответственно, протоколы, которые реализуете), а все классы, которые принимает или возвращает ваш объект, определяете через @class.

В файле реализации, соответственно, подключаются те заголовки, которые реально используются в коде.
+2
egormerkushev #
Пришлось два раза читать, чтоб всё уловить.
+1
AnatolyB #
> JVM доступны все классы платформы Java, все классы добавленные пользователем
> в classpath и/или lib/ext и кое-что еще. Поэтому конфликта имен избежать очень сложно.

Извините, о каких конфликтах вы говорите?
+1
egormerkushev #
Наверное, когда автор программы пытается мучительно придумать имя своему новому классу?
+2
AnatolyB #
А зачем мучительно придумывать? Нельзя, что ли, просто придумать, без мазохизма? :-)
+1
egormerkushev #
Не знаю, на Java не писал. Но вот могу сказать, что в HTML-верстке придумать осмысленное имя классу иногда сложно )))
+1
AnatolyB #
HTML — это да. Но в Java, слава Богу, с этим все ок :-)
0
hashmap #
О гипотетических. Пытался объяснить необходимость пакетов, наверное, не очень ясно написал.
–1
Limosha #
При совпадении имен классов иногда получается интересное поведение. Проиложение собирается и даже запускается. Но при старте в консоль выдает, что есть два класса с одинаковым именем и какой будет использован незнает.
+6
Stam #
Приходящим из мира Java и автору очень рекомендую почитать данный документ:
developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CodingGuidelines/CodingGuidelines.pdf
Данные принципы именования сложились уже давно и желательно их придерживаться.

В отличие от Java (<irony>в которой принято все подряд начинать с get</irony>), в Objective-C префикс-глагол «get…» используется для косвенного получения данных (для одного или нескольких значений).

Например, у NSData есть метод:

-(void)getBytes:(void *)buffer length:(NSUInteger)length

Он не возвращает байты напрямую (метод является void), а записывает их по ссылке в buffer.

В вашем случае код Feature должен выглядеть как-то так:

@interface Feature: NSObject
-(Profile *)profileWithName:(NSString*)name;
@end

PS. Прошу прощения за отсутствие форматирования.
0
hashmap #
Полезный комментарий, спасибо
0
Throwable #
(Лирическое отступление) Кстати, в Java именно с этим проблемы. Спецификацией четко не определено, что должен возвращать метод
List getSomeList() {
return someList;
}

— ссылку на someList (чаще всего так и делают)
— unmodifiable wrapper для someList
— копию someList
При отсутствии в Java явных ссылок большинство кодеров не видят и не понимают различий (какая ссылка? мне же вернули сам объект!). А потом вылезают трудно отловимые ошибки с concurrent modification и immutability.
0
notxcain #
«Можно вызывать любой ранее определенный метод любого класса (но если метод не определен — нельзя)...»
Как бы можно:

id someObject;
[someObject performSelector:@selector(anySelectorYouCanImagine)];
0
hashmap #
Вы правы, но я хотел показать как компилятор нам помогает, а не как его обмануть.
0
EntropiouS #
id можно слать любой message напрямую, и по дефолту warning'ов не будет, но есть нюансы — cocoawithlove.com/2011/06/big-weakness-of-objective-c-weak-typing.html
0
notxcain #
Спасибо, интересный момент!
0
Agent_Smith #
Мне понравилась эта статья больше чем предыдущая, хоть и пишу на Objective-C уже давно, читать было все равно интересно!
0
nepx #
Как структурировать классы, если нет пакетов? Все классы проекта должны быть в одном месте? О_о.
0
hashmap #
Не обязательно, файлы можно разбросать как угодно, просто надо правильно сделать сборку. Все как в С. Пространство имен не обязательно определяет структуру файлов проекта и наоборот.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.