Иногда компилятор, пытаясь спасти вас от ситуации нераспознанного селектора, выдает предупреждение или сообщение об ошибке, в то время как вы очень хорошо знаете, что то, что вы пытаетесь сделать, — безопасно и правильно.

Проблема в данном случае заключается в том, что вы знаете больше компилятора о том, что в действительности происходит. Чтобы продолжить работу, вам нужно поделиться своими знаниями с компилятором. Как правило, это делается с помощью приведения типа ссылки на объект (см. главу 1). Такое приведение служит в качестве объявления класса, которому объект будет принадлежать при выполнении программы. Компилятор верит в то, что вы говорите ему в выражении приведения; таким образом, вы можете развеять сомнения компилятора в корректности выполняемых вами действий.

Эта ситуация часто возникает в связи с наследованием классов. Пока что мы не будем обсуждать наследование (см. главу 4), но пример я все равно приведу.

Возьмем встроенный класс Cocoa UINavigationController. Его метод topView Controller объявлен как возвращающий экземпляр UlViewController. В реальной же жизни он скорее вернет экземпляр некоторого созданного вами подкласса UlViewController. Для того чтобы вызов метода созданного вами класса для экземпляра, возвращенного методом topViewController, не свел с ума компилятор, последнему следует пояснить, что этот экземпляр на самом деле будет экземпляром созданного вами класса.

Вот пример из одного из моих собственных приложений:

[[navigationController topViewController] setAlbums: arr];

Эта строка кода не компилируется; компилятор выдает ту же ошибку “no visible @interface”, о которой я говорил в предыдущем разделе. Встроенный метод topView Controller возвращает UlViewController, a UlViewController не имеет метода setAlbums.

Однако я знаю, что в данном случае контроллер верхней панели контроллера навигации является экземпляром моего собственного класса RootViewController. А мой класс RootViewController имеет метод setAlbums; и именно его я и пытаюсь вызвать. То, что я делаю — разумно, законно и желательно. Мне надо действовать именно таким образом! Чтобы компилятор не мешал мне работать, я должен убедить его, что знаю, что делаю, сообщив, что объект, возвращаемый вызовом метода topViewController, на самом деле представляет собой RootViewController. Я делаю это с помощью приведения типов:

[(RootViewController*)[navigationController topViewController]
setAlbums: arr];

Этого достаточно. He будет никаких предупреждений и сообщений об ошибках. Приведение типов заставляет компилятор замолчать, когда я собираюсь отправить этому экземпляру сообщение setAlbums:, поскольку мой класс RootViewController имеет метод экземпляра setAlbums:, и компилятор знает об этом. А программа при запуске не завершается аварийно, поскольку я не лгу компилятору: вызов метода topViewController действительно возвращает экземпляр RootViewController.

Но чем больше власть — тем больше и ответственность. Не лгите компилятору! Вспомните пример из предыдущего раздела:

MyClass* m - @"Hello, world!";
[m rockTheCasbah];

Первая строка заставляет компилятор предупредить нас о “incompatible pointer types” (несовместимых типах указателей); и в данном случае компилятор совершенно прав: ведь в дальнейшем мы получим аварийное завершение (“unrecognized selector”) при попытке отправить сообщение rockTheCasbah объекту NSString. Мы можем заставить компилятор промолчать с помощью приведения типов:

MyClass* m - (MyClass*)@"Hello, world!";
[m rockTheCasbah];

Да, предупреждения не будет. Но проблема останется: ведь мы солгали компилятору и будем отправлять сообщение rockTheCasbah, как и ранее, экземпляру NSString. Мораль проста: поскольку компилятор верит всему, что вы говорите ему с помощью приведения типов, врать ему категорически нельзя.

 

Приведение типов не меняет чудесным образом реальные типы объектов. Это 0 % просто метод подсказки компилятору об информации о типе. На сам приводимый объект это никакие влияет. Выражение приведения (MyClass*) @ "Hello, world! " не превращает экземпляр NSString, который представляет собой @"Hello, world! ", в экземпляр MyClass! На удивление распространенная ошибка среди новичков — считать, что при этом действительно выполняются какие-то преобразования.

 

Язык Objective-C предоставляет также специальный тип, предназначенный для того, чтобы все заботы компилятора о типах данных выполнялись в тишине. Это тип id. Он представляет собой указатель, так что вы не должны использовать выражение id*. Этот тип определен как “указатель на просто объект”, без каких бы то ни было дальнейших уточнений. Таким образом, каждая ссылка на экземпляр также представляет собой id.

Использование типа id заставляет компилятор перестать беспокоиться о взаимосвязи между типами объектов и сообщениями. Компилятор не может ничего знать о том, какого типа в действительности будет тот или иной объект, поэтому он просто поднимает (или опускает — как кому нравится) руки и не предупреждает больше ни о чем. Кроме того, объекту типа id может быть присвоено любое объектное значение, и к типу id может быть приведен любой другой тип. Значение типа id может быть использовано в любом присваивании там, где ожидается значение некоторого конкретного объектного типа. Понятие назначения включает в себя передачу параметров; таким образом, значение типа id можно передать в качестве аргумента везде, где ожидается параметр некоторого конкретного объектного типа. (Мне нравится думать об id как об аналоге групп крови АВ и О: это универсальный реципиент и универсальный донор.) Вот пример применения типа id:

NSString* s - @"Hello, world!";
id unknown - si
[unknown rockTheCasbah];

Вторая строка корректна, поскольку любое объектное значение может быть присвоено переменной типа id. Третья строка не генерирует предупреждения компилятора, поскольку типу id может быть послано любое сообщение. (Очевидно, что программа при запуске все равно завершится аварийно, когда выяснится, что unknown имеет тип NSString — который, конечно же, не может получать сообщения rockTheCasbah!)

На самом деле это чрезмерное упрощение. При использовании механизма ARC этот код не может быть скомпилирован. Вместо скомпилированного кода вы получите сообщение об ошибке: “No known instance method for selector ‘rockTheCasbah’”. Оно означает, что компилятор ничего не знает о методе rockTheCasbah ни в каком классе. Однако если rockTheCasbah объявляется в заголовочном файле любого класса, импортированном в данный, или если rockTheCasbah реализован в текущем классе, то код будет компилироваться без предупреждения.

Если способность типа id получать любые сообщения напоминает вам nil, то так и должно быть. Я уже говорил, что nil представляет собой разновидность нуля; теперь я могу сказать, какой разновидностью нуля является nil. Это нуль, приведенный к типу id. Конечно, во время выполнения имеется разница, равен ли id значению nil или некоторому иному; отправка сообщения nil не вызовет аварийного завершения программы, но отправка неизвестного сообщения реальному объекту, вероятно, приведет к таковому.

Таким образом, результат применения id выражается в полном отключении проверки типов компилятором. Выяснения, каким является тип объекта, откладываются до тех пор, пока программа не будет запущена на выполнение. Но я не рекомендую широко применять id таким образом. Компилятор — ваш друг; вы должны позволить ему разведывать и перехватывать возможные ошибки в коде. Поэтому я почти никогда не объявляю переменную или параметр как имеющие тип id. Я хочу, чтобы типы моих объектов были максимально конкретными, чтобы компилятор мог Максимально проверить мой код. С другой стороны, тип id часто используется в интерфейсе API каркаса Cocoa — и это именно то, что, как я предупреждал вас в предыдущем разделе, может приводить к аварийному завершению из-за нераспознанного селектора.

Рассмотрим, например, класс NSArray, который представляет собой объектно-ориенти-рованную версию массива. В чистом С вы должны объявить, какого типа объекты находятся в массиве; например, у вас может быть “массив целых чисел int”. В Objective-C, используя NSArray, вы не можете этого сделать. Каждый NSArray представляет собой массив элементов типа id, что означает, что каждый элемент массива может быть любого объектного типа. Вы можете поместить в массив NSArray объект некоторого типа, поскольку любой тип объекта может быть присвоен ссылке на id (id является универсальным реципиентом). Вы можете получить обратно из NSArray любой конкретный тип объекта, так как id может быть присвоен объекту любого типа (id является универсальным донором).

Метод lastObject класса NSArray, таким образом, определен как возвращающий тип id. Для данного массива arr типа NSArray последний элемент можно получить следующим образом:

id unknown - [arr lastObject];

Теперь мы находимся в потенциально опасной ситуации. После этого кода объекту unknown можно послать любое сообщение; компилятор не будет этому препятствовать. Следовательно, если мне известен тип элементов массива, я всегда могу выполнить присваивание или приведение типа к типу, который я получаю из массива. Пусть, например, я знаю, что массив arr не содержит ничего, кроме экземпляров NSString (поскольку я перед этим поместил их туда). Тогда я могу написать

NSString* s - [arr lastObject];

Компилятор не будет жаловаться на этот код, поскольку тип id может быть присвоен любому конкретному типу объекта (id является универсальным донором). Кроме того, далее компилятор рассматривает s как NSString и использует свои возможности проверки типов для того, чтобы убедиться, что я не посылаю s никаких сообщений, кроме тех, что можно

посылать NSString. И я не лгу компилятору; во время выполнения s действительно представляет собой объект NSString, так что все в порядке.

Однако есть еще одна опасность: эта ситуация — открытое приглашение случайно солгать компилятору. Предположим, что последний элемент в этом массиве NSArray не является NSString. Компилятор этого не знает; он вообще ничего не знает о том, что NSArray на самом деле будет содержать во время выполнения, a lastObject возвращает id, который компилятор с радостью позволит присвоить ссылке, объявленной как NSString. И тут мы просто напрашиваемся на неприятности. Предположим, например, что в следующей строке я отправлю сообщение uppercaseString объекту s. Компилятор молчит: в конце концов, я объявил эту ссылку как NSString, a uppercaseString является методом NSString. Но если s не является NSString, то, вероятно, мы попадем в ситуацию нераспознанного селектора и получим аварийное завершение программы.

Еще одной связанной с id ловушкой являются конфликты имен методов. Ранее я говорил, что один и тот же класс не может определять методы одного и того же типа (методы класса или методы экземпляра) с одинаковыми именами, но различными сигнатурами. Но я не говорил, что происходит, когда два разных класса объявляют методы с одинаковыми именами, но различными сигнатурами. Если в коде указан тип объекта-получателя сообщения, никаких проблем нет, потому что нет никаких сомнений в том, какой метод вызывается: он один в классе этого объекта. Но если объектом-получателем сообщения является id, то с использованием механизма ARC вы получите сообщение об ошибке: “Multiple methods named ‘rockTheCasbah’ found with mismatched result, parameter type or attributes” (найдено несколько методов с именем rockTheCasbah с несоответствующими типами результата, параметров или атрибутами). Это еще одна причина, по которой имена методов так многословны: для того, чтобы сделать каждое имя метода уникальным, предотвращая объявление в разных классах конфликтующих сигнатур для одного и того же имени метода.


 

 

 

Добавить комментарий