@class vs. #import

<p>насколько я понимаю, следует использовать объявление прямого класса в случае, если classa необходимо включить заголовок classb, а classb должен включить заголовок classa, чтобы избежать каких-либо циклических включений. я также понимаю, что <code>#import</code> является простым <code>ifndef</code>, так что включение происходит только один раз.</p><p>мой вопрос таков: когда кто-то использует <code>#import</code> и когда он использует <code>@class</code>? иногда, если я использую объявление <code>@class</code>, я вижу общее предупреждение компилятора, например следующее:</p><blockquote> <p><code>warning: receiver 'foocontroller' is a forward class and corresponding @interface may not exist.</code></p> </blockquote><p>очень хотелось бы это понять, вместо того, чтобы просто удалить предварительное объявление <code>@class</code> и добавить код <code>#import</code>, чтобы заглушить предупреждения, которые компилятор дает мне.</p>

<p>если вы видите это предупреждение:</p><blockquote> <p>предупреждение: получатель mycoolclass является классом пересылки, и соответствующий @interface может не существовать</p> </blockquote><p>вам нужно <code>#import</code> файл, но вы можете сделать это в вашем файле реализации (.m) и использовать объявление <code>@class</code> в вашем заголовочном файле.</p><p><code>@class</code> (обычно) не устраняет необходимость в <code>#import</code> файлах, он просто перемещает требование ближе к тому месту, где информация полезна.</p><p><strong>например</strong></p><p>если вы скажете <code>@class mycoolclass</code>, компилятор знает, что он может увидеть что-то вроде:</p><pre><code>mycoolclass *myobject; </code></pre><p>он не должен беспокоиться ни о чем, кроме <code>mycoolclass</code>, является допустимым классом, и он должен зарезервировать место для указателя на него (на самом деле, просто указатель). таким образом, в вашем заголовке <code>@class</code> достаточно 90% времени.</p><p>однако, если вам когда-либо понадобится создать или получить доступ к членам <code>myobject</code>, вам нужно сообщить компилятору, что это за методы. на этом этапе (предположительно в вашем файле реализации) вам понадобится <code>#import "mycoolclass.h"</code>, чтобы сообщить компилятору дополнительную информацию, помимо просто "это класс".</p>

<p>три простых правила:</p><ul> <li>только <code>#import</code> суперкласс и принятые протоколы в заголовочных файлах (<code>.h</code> файлы).</li> <li><code>#import</code> все классы и протоколы, в которые вы отправляете сообщения при реализации (<code>.m</code> файлы).</li> <li>форвард декларации для всего остального.</li> </ul><p>если вы делаете предварительное объявление в файлах реализации, то вы, вероятно, делаете что-то не так.</p>

<p>посмотрите документацию по языку программирования objective-c на <a href="https://developer.apple.com/legacy/library/documentation/cocoa/conceptual/oopandobjc1/oopandobjc1.pdf">adc</a> </p><p>под разделом по определению класса | класс interface описывает, почему это делается:</p><blockquote> <p>директива @class минимизирует объем кода, видимого компилятором и компоновщиком, и, следовательно, является самым простым способом дать прямое объявление имени класса. проще говоря, это позволяет избежать потенциальных проблем, которые могут возникнуть при импорте файлов, импортирующих еще другие файлы. например, если один класс объявляет статически типизированную переменную экземпляра другого класса, и их два интерфейсных файла импортируют друг друга, ни один класс не может скомпилироваться правильно.</p> </blockquote><p>надеюсь, это поможет.</p>

<p>при необходимости используйте предварительное объявление в заголовочном файле и <code>#import</code> заголовочные файлы для всех классов, которые вы используете в реализации. другими словами, вы всегда <code>#import</code> файлы, которые вы используете в своей реализации, и если вам нужно сослаться на класс в вашем заголовочном файле, используйте также предварительное объявление.</p><p><strong>исключение</strong> для этого, вы должны <code>#import</code> класс или формальный протокол, который вы наследуете в вашем заголовочном файле (в этом случае вам не нужно будет импортировать его в реализации).</p>

<p>обычной практикой является использование @class в заголовочных файлах (но вам все еще нужно #import суперкласса) и #import в файлах реализации. это позволит избежать любых круглых включений, и это просто работает.</p>

<p>еще одно преимущество: быстрая компиляция</p><p>если вы включаете заголовочный файл, любое изменение в нем также приводит к компиляции текущего файла, но это не тот случай, если имя класса включено как <code>@class name</code>. конечно, вам нужно будет включить заголовок в исходный файл</p>

<blockquote> <p>мой запрос таков. когда кто-то использует #import и когда он использует @class?</p> </blockquote><p>простой ответ: вы <code>#import</code> или <code>#include</code>, когда есть физическая зависимость. в противном случае вы используете предварительные объявления (<code>@class monclass</code>, <code>struct monstruct</code>, <code>@protocol monprotocol</code>).</p><p>вот несколько распространенных примеров физической зависимости:</p><ul> <li>любое значение c или c++ (указатель или ссылка не является физической зависимостью). если у вас есть <code>cgpoint</code> в качестве ивара или свойства, компилятор должен будет увидеть объявление <code>cgpoint</code>.</li> <li>ваш суперкласс.</li> <li>метод, который вы используете.</li> </ul><blockquote> <p>иногда, если я использую объявление @class, я вижу общее предупреждение компилятора, например: "предупреждение: получатель" foocontroller "является классом пересылки, и соответствующий @interface может не существовать".</p> </blockquote><p>компилятор на самом деле очень снисходителен в этом отношении. он будет сбрасывать подсказки (например, приведенные выше), но вы можете легко очистить свой стек, если проигнорируете их и не правильно <code>#import</code>. хотя это должно (imo), компилятор не обеспечивает это. в arc компилятор более строг, потому что он отвечает за подсчет ссылок. что происходит, так это то, что компилятор возвращается к значению по умолчанию, когда он встречает неизвестный метод, который вы вызываете. предполагается, что каждое возвращаемое значение и параметр являются <code>id</code>. таким образом, вы должны исключить каждое предупреждение из ваших кодовых баз, потому что это следует рассматривать как физическую зависимость. это аналогично вызову функции c, которая не объявлена. с c параметры предполагаются равными <code>int</code>.</p><p>причина, по которой вы бы предпочли предварительные декларации, заключается в том, что вы можете сократить время сборки за счет факторов, поскольку зависимость минимальна. с помощью предварительных объявлений компилятор видит, что есть имя, и может правильно анализировать и компилировать программу, не видя объявления класса или всех его зависимостей, когда нет физической зависимости. чистые сборки занимают меньше времени. инкрементные сборки занимают меньше времени. конечно, в конечном итоге вы потратите немного больше времени на то, чтобы все заголовки, которые вам нужны, были видны для каждого перевода, как следствие, но это быстро окупается за меньшее время сборки (если ваш проект не крошечный).</p><p>если вместо этого вы используете <code>#import</code> или <code>#include</code>, вы потратите на компилятор гораздо больше работы, чем необходимо. вы также вводите сложные зависимости заголовка. вы можете сравнить это с алгоритмом грубой силы. когда вы <code>#import</code>, вы перетаскиваете кучу ненужной информации, которая требует много памяти, дискового ввода-вывода и цп для анализа и компиляции источников.</p><p>objc довольно близок к идеалу для языка на основе си в отношении зависимости, потому что типы <code>nsobject</code> никогда не являются значениями - типы <code>nsobject</code> всегда являются указателями с подсчетом ссылок. таким образом, вы можете получить невероятно быстрое время компиляции, если вы правильно структурируете зависимости вашей программы и, по возможности, продвигаетесь вперед, потому что требуется очень мало физической зависимости. вы также можете объявить свойства в расширениях класса для дальнейшей минимизации зависимости. это огромный бонус для больших систем - вы бы знали разницу, если бы вы когда-либо разрабатывали большую кодовую базу c++.</p><p>поэтому я рекомендую использовать форварды, где это возможно, а затем <code>#import</code>, где есть физическая зависимость. если вы видите предупреждение или другое, которое подразумевает физическую зависимость - исправьте их все. исправление к <code>#import</code> в вашем файле реализации.</p><p>когда вы создаете библиотеки, вы, вероятно, классифицируете некоторые интерфейсы как группу, и в этом случае вы будете <code>#import</code> ту библиотеку, в которой вводится физическая зависимость (например, <code>#import <appkit/appkit.h></code>). это может привести к зависимости, но сопровождающие библиотеки часто могут обрабатывать физические зависимости по мере необходимости - если они вводят функцию, они могут минимизировать влияние, которое она оказывает на ваши сборки.</p>

<p>я вижу много "делай так", но я не вижу ответов на "почему?"</p><p>итак: <em>почему</em> вы должны @class в своем заголовке и #import только в своей реализации? вы удваиваете свою работу, постоянно @class <em>и</em> #import. если вы не используете наследство. в этом случае вы будете #importing несколько раз для одного @class. затем вы должны забыть удалить из нескольких разных файлов, если вдруг решите, что вам больше не нужен доступ к объявлению.</p><p>многократный импорт одного и того же файла не является проблемой из-за природы #import. компиляция производительности тоже не проблема. если бы это было так, мы бы не #importing cocoa/cocoa.h или что-то подобное почти во всех наших заголовочных файлах.</p>

<p>если мы сделаем это</p><pre><code>@interface class\_b : class\_a </code></pre><p>значит, мы наследуем class\_a в class\_b, в class\_b мы можем получить доступ ко всем переменным class\_a.</p><p>если мы делаем это</p><pre><code>#import .... @class class\_a @interface class\_b </code></pre><p>здесь мы говорим, что мы используем class\_a в нашей программе, но если мы хотим использовать переменные class\_a в class\_b, мы должны #import class\_a в файле .m (создать объект и использовать его функцию и переменные).</p>

<p>для дополнительной информации о зависимостях файлов & #import & @class проверьте это:</p><p> <a href="http://qualitycoding.org/file-dependencies/">http://qualitycoding.org/file-dependencies/</a> это хорошая статья</p><p>краткое содержание статьи</p><blockquote> <h1>импорт в заголовочных файлах:</h1> <ul> <li># импортируйте суперкласс, который вы наследуете, и протоколы, которые вы реализуете.</li> <li>форвард-объявить все остальное (если это не от фреймворка с главным заголовком).</li> <li>попробуйте устранить все другие #imports.</li> <li>объявите протоколы в своих заголовках, чтобы уменьшить зависимости.</li> <li>слишком много предварительных деклараций? у вас большой класс.</li> </ul> <h1>импорт в файлах реализации:</h1> <ul> <li>устраните ненужные импортные операции.</li> <li>если метод делегирует другому объекту и возвращает то, что он возвращает, попробуйте объявить этот объект вперед вместо #importing.</li> <li>если включение модуля заставляет вас включать уровень за уровнем последовательных зависимостей, у вас может быть набор классов, который хочет стать библиотекой. создайте его как отдельную библиотеку с главным заголовком, чтобы все можно было представить как один готовый блок.</li> <li>слишком много импорта? у вас большой класс.</li> </ul> </blockquote>

<p>если вы попытаетесь объявить переменную или свойство в вашем заголовочном файле, который вы еще не импортировали, вы получите ошибку, сообщающую, что компилятор не знает этот класс.</p><p>ваша первая мысль, вероятно, <code>#import</code> это.<br> в некоторых случаях это может вызвать проблемы.</p><p>например, если вы реализуете группу c-методов в заголовочном файле, или структурах, или что-то подобное, потому что они не должны импортироваться несколько раз.</p><p>поэтому вы можете сообщить компилятору с помощью <code>@class</code>:</p><blockquote> <p>я знаю, что вы не знаете этот класс, но он существует. он будет импортирован или реализован в другом месте.</p> </blockquote><p>он в основном говорит компилятору закрыть и скомпилировать, даже если он не уверен, будет ли когда-либо реализован этот класс.</p><p>обычно вы будете использовать <code>#import</code> в <strong>. m</strong> и <code>@class</code> в <strong>. h</strong> файлах.</p>

<p>когда я развиваюсь, я имею в виду только три вещи, которые никогда не вызывают у меня никаких проблем.</p><ol> <li>импорт супер классов</li> <li>импорт родительских классов (если у вас есть дети и родители)</li> <li>импортировать классы вне вашего проекта (как в фреймворках и библиотеках)</li> </ol><p>для всех других классов (подклассов и дочерних классов в моем проекте self) я объявляю их через forward-class.</p>

<p>это пример сценария, где нам нужен @class.</p><p>подумайте, хотите ли вы создать протокол в заголовочном файле, в котором есть параметр с типом данных того же класса, тогда вы можете использовать @class. пожалуйста, помните, что вы можете также объявлять протоколы отдельно, это только пример.</p><pre><code>// dronesearchfield.h #import <uikit/uikit.h> @class dronesearchfield; @protocol dronesearchfielddelegate<uitextfielddelegate> @optional - (void)dronetextfieldbuttonclicked:(dronesearchfield *)textfield; @end @interface dronesearchfield : uitextfield @end </code></pre>

<p>компилятор будет жаловаться только в том случае, если вы собираетесь использовать этот класс таким образом, что компилятору необходимо знать его реализацию.</p><p>пример:</p><ol> <li>это может быть как если вы собираетесь извлечь свой класс из него или</li> <li>если вы собираетесь использовать объект этого класса в качестве переменной-члена (хотя и редко).</li> </ol><p>он не будет жаловаться, если вы просто собираетесь использовать его в качестве указателя. конечно, вам нужно будет #import его в файле реализации (если вы создаете экземпляр объекта этого класса), так как он должен знать содержимое класса для создания экземпляра объекта.</p><p>примечание: #import не совпадает с #include. это означает, что нет ничего, что называется круговым импортом. import - это своего рода запрос компилятора на поиск определенного файла для получения определенной информации. если эта информация уже доступна, компилятор игнорирует ее.</p><p>просто попробуйте это, импортируйте a.h в b.h и b.h в a.h. там не будет никаких проблем или жалоб, и это будет работать нормально.</p><p><strong>когда использовать @class</strong></p><p>вы используете @class, только если вы даже не хотите импортировать заголовок в свой заголовок. это может быть случай, когда вам даже не важно знать, что это будет за класс. случаи, когда у вас может даже не быть заголовка для этого класса.</p><p>примером этого может быть то, что вы пишете две библиотеки. один класс, назовем его a, существует в одной библиотеке. эта библиотека содержит заголовок из второй библиотеки. этот заголовок может иметь указатель a, но, опять же, может не потребоваться его использование. если библиотека 1 еще не доступна, библиотека b не будет заблокирована, если вы используете @class. но если вы хотите импортировать a.h, то прогресс библиотеки 2 будет заблокирован.</p>

<p>представьте, что @class говорит компилятору: "поверьте мне, это существует".</p><p>думайте о #import как о копировании-вставке.</p><p>вы хотите свести к минимуму количество импортов по ряду причин. без каких-либо исследований первое, что приходит на ум, - это сокращение времени компиляции.</p><p>обратите внимание, что когда вы наследуете от класса, вы не можете просто использовать предварительное объявление. вам нужно импортировать файл, чтобы объявленный вами класс знал, как он определен.</p>

<p>переслать объявление только для того, чтобы компилятор не показывал ошибку.</p><p>компилятор будет знать, что есть класс с именем, которое вы использовали в вашем заголовочном файле для объявления.</p>