Минусы на Objective-C Категории и расширения - PullRequest
34 голосов
/ 14 января 2011

Я узнал что-то новое, пытаясь выяснить, почему мое свойство readwrite, объявленное в частной категории, не генерировало установщик. Это потому, что моя категория была названа:

// .m
@interface MyClass (private)
@property (readwrite, copy) NSArray* myProperty;
@end

Изменение на:

// .m
@interface MyClass ()
@property (readwrite, copy) NSArray* myProperty;
@end

и мой сеттер синтезирован. Теперь я знаю, что расширение класса - это не просто другое имя для анонимной категории. Если оставить категорию без имени, она превратится в другого зверя: тот, который теперь обеспечивает реализацию реализации метода во время компиляции и позволяет добавлять ivars. Теперь я понимаю общие принципы, лежащие в основе каждого из них: категории обычно используются для добавления методов к любому классу во время выполнения, а расширения классов обычно используются для обеспечения реализации частного API и добавления ivars. Я принимаю это.

Но есть мелочи, которые меня смущают. Во-первых, на высоком уровне: зачем так дифференцироваться? Эти понятия кажутся похожими идеями, которые не могут решить, являются ли они одинаковыми или разными понятиями. Если бы они были одинаковыми, я ожидал бы, что с использованием категории без имени можно будет сделать то же самое, что и с именованной категорией (которой они не являются). Если они разные, (которые они есть), я бы ожидал большего синтаксического различия между ними. Кажется странным сказать: «О, кстати, чтобы реализовать расширение класса, просто напишите категорию, но пропустите имя. Оно волшебным образом меняется».

Во-вторых, по вопросу применения времени компиляции: если вы не можете добавить свойства в именованную категорию, почему это убеждает компилятор в том, что вы сделали именно это? Чтобы уточнить, я проиллюстрирую своим примером. Я могу объявить свойство только для чтения в заголовочном файле:

// .h
@interface MyClass : NSObject
@property (readonly, copy) NSString* myString;
@end

Теперь я хочу перейти к файлу реализации и предоставить себе частный доступ для чтения и записи к свойству. Если я делаю это правильно:

// .m
@interface MyClass ()
@property (readwrite, copy) NSString* myString;
@end

Я получаю предупреждение, когда я не синтезирую, а когда я это делаю, я могу установить свойство, и все персиковое. Но, к сожалению, если я немного ошибаюсь в разнице между категорией и расширением класса, я стараюсь:

// .m
@interface MyClass (private)
@property (readwrite, copy) NSString* myString;
@end

Компилятор полностью усвоил мысль о том, что свойство перезаписано. Я не получаю предупреждений и даже не вижу приятной ошибки компиляции «Объект не может быть установлен - либо свойство только для чтения, либо не найден установщик» после установки myString, если бы я не объявил свойство readwrite в категории. Я просто получаю исключение «Не отвечает селектору» во время выполнения. Если добавление ivars и свойств не поддерживается (именованными) категориями, не слишком ли много вопросов, чтобы компилятор проигрывал по тем же правилам? Я скучаю по какой-то великой философии дизайна?

Ответы [ 4 ]

53 голосов
/ 14 января 2011

Расширения класса были добавлены в Objective-C 2.0 для решения двух конкретных проблем:

  1. Разрешить объекту иметь "закрытый" интерфейс, который проверяется компилятором.
  2. Разрешить общедоступные, доступные для записи свойства.

Частный интерфейс

До Objective-C 2.0, если разработчик хотел иметь набор методов в Objective-C, они частообъявил категорию "Private" в файле реализации класса:

@interface MyClass (Private)
- (id)awesomePrivateMethod;
@end

Однако эти закрытые методы часто смешивались в блок @implementation класса ( не отдельный блок @implementationдля категории Private).И почему бы нет?Это на самом деле не расширение класса;они просто компенсируют отсутствие публичных / частных ограничений в категориях Objective-C.

Проблема в том, что компиляторы Objective-C предполагают, что методы, объявленные в категории, будут реализованы в других местах, поэтому они не проверяютчтобы убедиться, что методы реализованы.Таким образом, разработчик может объявить awesomePrivateMethod, но не сможет его реализовать, и компилятор не предупредит их о проблеме.Это проблема, которую вы заметили: в категории вы можете объявить свойство (или метод), но не получите предупреждение, если вы никогда не реализуете его - это потому, что компилятор ожидает, что оно будет реализовано «где-то» (скорее всего,, в другом модуле компиляции, независимом от этого).

Введите расширения класса.Предполагается, что методы, объявленные в расширении класса, реализованы в основном блоке @implementation;если это не так, компилятор выдаст предупреждение.

Общедоступные, Частно-записываемые свойства

Часто полезно реализовать неизменную структуру данных, то есть одну вкакой внешний код не может использовать установщик для изменения состояния объекта.Тем не менее, может быть неплохо иметь свойство записи для внутреннего использования.Расширения классов позволяют: в общедоступном интерфейсе разработчик может объявить свойство только для чтения, но затем объявить его доступным для записи в расширении класса.Для внешнего кода свойство будет доступно только для чтения, но сеттер может использоваться внутренне.

Так почему я не могу объявить доступное для записи свойство в категории?

Категории не могут добавить экземплярпеременные.Сеттер часто требует некоторого резервного хранилища.Было решено, что разрешить категории объявлять собственность, которая, вероятно, потребует бэк-магазина, было A Bad Thing ™.Следовательно, категория не может объявить свойство, доступное для записи.

Они выглядят одинаково, но различаются

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

8 голосов
/ 14 января 2011

Вас смущает синтаксическое сходство. Расширение класса не просто безымянная категория. Расширение класса - это способ сделать часть вашего интерфейса приватной, а часть общедоступной - оба рассматриваются как часть объявления интерфейса класса. Будучи частью интерфейса класса, расширение должно быть определено как часть класса.

Категория, с другой стороны, это способ добавления методов в существующий класс во время выполнения. Это может быть, например, отдельный пакет, который загружается только по четвергам.

Для большей части истории Objective-C было невозможно добавить переменные экземпляра в класс во время выполнения, когда категории загружены. Это было исправлено совсем недавно в новой среде выполнения, но язык все еще показывает шрамы своих хрупких базовых классов. Одним из них является то, что язык не поддерживает категории, добавляющие переменные экземпляра. Вам придется самостоятельно выписывать геттеры и сеттеры в стиле старой школы.

Переменные экземпляра в категориях тоже несколько хитры. Поскольку они не обязательно присутствуют при создании экземпляра, а инициализатор может ничего о них не знать, их инициализация - это проблема, которой нет в обычных переменных экземпляра.

3 голосов
/ 14 января 2011

Вы можете добавить свойство в категорию, вы просто не можете его синтезировать. Если вы используете категорию, вы не получите предупреждения компиляции, потому что она ожидает, что установщик будет реализован в категории.

0 голосов
/ 10 сентября 2014

Просто небольшое разъяснение ПРИЧИНЫ для различного поведения неназванных категорий (теперь известных как Расширения классов) и обычных (именованных) категорий.

Все очень просто.Вы можете иметь МНОГИЕ категории, расширяющие один и тот же класс, загруженные во время выполнения, без ведома компилятора и компоновщика.(обратите внимание на множество прекрасных расширений, которые люди написали для NSObject, которые добавляют его функциональность пост-hoc).

Теперь Objective-C не имеет понятия NAME SPACE.Следовательно, определение iVars в именованной категории может создать конфликт символов во время выполнения.Если две разные категории смогут определять одно и то же

@interface myObject (extensionA) {
 NSString *myPrivateName;
}
@end
@interface myObject (extensionB) {
 NSString *myPrivateName;
}
@end

, то, по крайней мере, будет переполнение памяти во время выполнения.

В противоположность этому расширения класса не имеют ИМЯ ИМЯ, итаким образом, может быть только ОДИН.Вот почему вы можете определить iVars там.Они гарантированно уникальны.

Что касается ошибок и предупреждений компилятора, связанных с категориями и расширениями классов + ivars и определениями свойств, я должен согласиться, что они не очень полезны, и я потратил слишком много времени, пытаясьпонять, почему что-то компилируется или нет, и как они работают (если они работают) после компиляции.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...