Создание абстрактного класса в Objective-C
Я изначально программист на Java, который сейчас работает с Objective-C. Я хотел бы создать абстрактный класс, но это не представляется возможным в Objective-C. Это возможно?
Если нет, то как близко к абстрактному классу я могу получить в Objective-C?
22 ответа
Как правило, класс Objective-C является абстрактным только по соглашению - если автор документирует класс как абстрактный, просто не используйте его без его подкласса. Однако не существует принудительного исполнения во время компиляции, которое предотвращает создание экземпляров абстрактного класса. Фактически, ничто не мешает пользователю предоставлять реализации абстрактных методов через категорию (т.е. во время выполнения). Вы можете заставить пользователя по крайней мере переопределить определенные методы, вызвав исключение в реализации этих методов в вашем абстрактном классе:
[NSException raise:NSInternalInconsistencyException
format:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)];
Если ваш метод возвращает значение, это немного проще в использовании
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)]
userInfo:nil];
как тогда вам не нужно добавлять оператор возврата из метода.
Если абстрактный класс действительно является интерфейсом (то есть не имеет конкретных реализаций метода), использование протокола Objective-C является более подходящим вариантом.
Нет, в Objective-C нет способа создать абстрактный класс.
Вы можете смоделировать абстрактный класс - сделав вызов метода / селекторов didNotRecognizeSelector: и, следовательно, вызвать исключение, делающее класс непригодным для использования.
Например:
- (id)someMethod:(SomeObject*)blah
{
[self doesNotRecognizeSelector:_cmd];
return nil;
}
Вы также можете сделать это для init.
Просто перефразирую ответ @Barry Wark выше (и обновление для iOS 4.3) и оставлю это для моей собственной справки:
#define mustOverride() @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"%s must be overridden in a subclass/category", __PRETTY_FUNCTION__] userInfo:nil]
#define methodNotImplemented() mustOverride()
тогда в ваших методах вы можете использовать это
- (void) someMethod {
mustOverride(); // or methodNotImplemented(), same thing
}
Примечания: Не уверен, что делать макрос похожим на функцию C - это хорошая идея или нет, но я буду держать его до тех пор, пока не изучу обратное. Я думаю, что более правильно использовать NSInvalidArgumentException
(скорее, чем NSInternalInconsistencyException
) так как это то, что система времени выполнения выдает в ответ на doesNotRecognizeSelector
вызывается (см. NSObject
документы).
Решение, которое я придумал:
- Создайте протокол для всего, что вы хотите в своем "абстрактном" классе
- Создайте базовый класс (или, возможно, назовите его абстрактным), который реализует протокол. Для всех методов, которые вы хотите, чтобы "абстрактные" реализовали их в файле.m, но не в файле.h.
- Пусть ваш дочерний класс наследует от базового класса И реализует протокол.
Таким образом, компилятор выдаст вам предупреждение для любого метода в протоколе, который не реализован вашим дочерним классом.
Это не так лаконично, как в Java, но вы получите предупреждение от нужного компилятора.
Из списка рассылки Omni Group:
В настоящее время Objective-C не имеет абстрактной конструкции компилятора, как Java.
Поэтому все, что вам нужно сделать, это определить абстрактный класс как любой другой нормальный класс и реализовать заглушки методов для абстрактных методов, которые либо пусты, либо сообщают о неподдержке селектора. Например...
- (id)someMethod:(SomeObject*)blah
{
[self doesNotRecognizeSelector:_cmd];
return nil;
}
Я также делаю следующее, чтобы предотвратить инициализацию абстрактного класса через инициализатор по умолчанию.
- (id)init
{
[self doesNotRecognizeSelector:_cmd];
[self release];
return nil;
}
Вместо того, чтобы пытаться создать абстрактный базовый класс, рассмотрите возможность использования протокола (похожего на интерфейс Java). Это позволяет вам определить набор методов, а затем принять все объекты, которые соответствуют протоколу, и реализовать методы. Например, я могу определить протокол операции, а затем иметь такую функцию:
- (void)performOperation:(id<Operation>)op
{
// do something with operation
}
Где op может быть любым объектом, реализующим протокол операции.
Если вам нужен ваш абстрактный базовый класс, чтобы делать больше, чем просто определять методы, вы можете создать обычный класс Objective-C и предотвратить его создание. Просто переопределите функцию инициализации - (id) и сделайте так, чтобы она возвращала nil или assert(false). Это не очень чистое решение, но, поскольку Objective-C является полностью динамическим, в действительности нет прямого эквивалента абстрактному базовому классу.
Эта ветка довольно старая, и большая часть того, чем я хочу поделиться, уже здесь.
Тем не менее, мой любимый метод не упоминается, и AFAIK нет родной поддержки в текущем Clang, так что здесь я иду...
Во-первых, и прежде всего (как уже отмечали другие) абстрактные классы являются чем-то необычным в Objective-C - вместо этого мы обычно используем композицию (иногда посредством делегирования). Это, вероятно, причина того, что такая функция еще не существует в языке / компиляторе - кроме @dynamic
свойства, которые IIRC были добавлены в ObjC 2.0, сопровождающих введение CoreData.
Но, учитывая, что (после тщательной оценки вашей ситуации!) Вы пришли к выводу, что делегирование (или состав в целом) не очень подходит для решения вашей проблемы, вот как я это делаю:
- Реализуйте каждый абстрактный метод в базовом классе.
- Сделать эту реализацию
[self doesNotRecognizeSelector:_cmd];
... - …с последующим
__builtin_unreachable();
чтобы заставить замолчать предупреждение, которое вы получите для не пустых методов, сообщая вам "контролируйте достигнутый конец не пустой функции без возврата". - Либо объедините шаги 2. и 3. в макросе, либо аннотируйте
-[NSObject doesNotRecognizeSelector:]
с помощью__attribute__((__noreturn__))
в категории без реализации, чтобы не заменить первоначальную реализацию этого метода, и включить заголовок для этой категории в PCH вашего проекта.
Я лично предпочитаю макро-версию, так как она позволяет максимально уменьшить шаблон.
Вот:
// Definition:
#define D12_ABSTRACT_METHOD {\
[self doesNotRecognizeSelector:_cmd]; \
__builtin_unreachable(); \
}
// Usage (assuming we were Apple, implementing the abstract base class NSString):
@implementation NSString
#pragma mark - Abstract Primitives
- (unichar)characterAtIndex:(NSUInteger)index D12_ABSTRACT_METHOD
- (NSUInteger)length D12_ABSTRACT_METHOD
- (void)getCharacters:(unichar *)buffer range:(NSRange)aRange D12_ABSTRACT_METHOD
#pragma mark - Concrete Methods
- (NSString *)substringWithRange:(NSRange)aRange
{
if (aRange.location + aRange.length >= [self length])
[NSException raise:NSInvalidArgumentException format:@"Range %@ exceeds the length of %@ (%lu)", NSStringFromRange(aRange), [super description], (unsigned long)[self length]];
unichar *buffer = (unichar *)malloc(aRange.length * sizeof(unichar));
[self getCharacters:buffer range:aRange];
return [[[NSString alloc] initWithCharactersNoCopy:buffer length:aRange.length freeWhenDone:YES] autorelease];
}
// and so forth…
@end
Как видите, макрос обеспечивает полную реализацию абстрактных методов, сводя необходимое количество шаблонов к абсолютному минимуму.
Еще лучшим вариантом было бы побудить команду Clang предоставить атрибут компилятора для этого случая через запросы функций. (Лучше, потому что это также позволило бы диагностику во время компиляции для тех сценариев, где вы подкласс, например, NSIncrementalStore.)
Почему я выбираю этот метод
- Это сделано эффективно и несколько удобно.
- Это довольно легко понять. (Хорошо, это
__builtin_unreachable()
может удивить людей, но и этого достаточно легко понять.) - Его нельзя удалить в сборках релиза без генерации других предупреждений компилятора или ошибок - в отличие от подхода, основанного на одном из макросов утверждений.
Этот последний пункт требует пояснения, я думаю:
Некоторые (большинство?) Люди отбрасывают утверждения в сборках релизов. (Я не согласен с этой привычкой, но это другая история...) Неспособность реализовать требуемый метод - однако, это плохо, ужасно, неправильно и, по сути, является концом вселенной для вашей программы. Ваша программа не может работать правильно в этом отношении, потому что она не определена, а неопределенное поведение - худшая вещь в мире. Следовательно, возможность лишить эту диагностику без создания новой диагностики будет совершенно неприемлемо.
Достаточно плохо, что вы не можете получить надлежащую диагностику во время компиляции для таких ошибок программиста, и вынуждены прибегать к их обнаружению во время выполнения, но если вы можете обмазать это в сборках релиза, зачем пытаться иметь абстрактный класс в первое место?
С помощью @property
а также @dynamic
также может работать. Если вы объявите динамическое свойство и не дадите подходящую реализацию метода, все по-прежнему будет компилироваться без предупреждений, и вы получите unrecognized selector
ошибка во время выполнения, если вы пытаетесь получить к нему доступ. Это по сути то же самое, что звонить [self doesNotRecognizeSelector:_cmd]
, но с гораздо меньшим набором текста.
Ответ на вопрос разбросан в комментариях под уже предоставленными ответами. Итак, я просто суммирую и упрощаю здесь.
Вариант 1: протоколы
Если вы хотите создать абстрактный класс без реализации, используйте "Протоколы". Классы, наследующие протокол, обязаны реализовывать методы в протоколе.
@protocol ProtocolName
// list of methods and properties
@end
Вариант 2: Шаблон Метод Шаблон
Если вы хотите создать абстрактный класс с частичной реализацией, такой как "Шаблон метода шаблона", то это решение. Objective-C - Шаблон методов шаблонов?
В Xcode (используя clang и т. Д.) Я люблю использовать __attribute__((unavailable(...)))
помечать абстрактные классы, чтобы вы получили сообщение об ошибке / предупреждение, если попытаетесь его использовать.
Это обеспечивает некоторую защиту от случайного использования метода.
пример
В базовом классе @interface
пометьте "абстрактные" методы:
- (void)myAbstractMethod:(id)param1 __attribute__((unavailable("You should always override this")));
Продолжая этот шаг, я создаю макрос:
#define UnavailableMacro(msg) __attribute__((unavailable(msg)))
Это позволяет вам сделать это:
- (void)myAbstractMethod:(id)param1 UnavailableMacro(@"You should always override this");
Как я уже сказал, это не настоящая защита компилятора, но она примерно так же хороша, как и в языке, который не поддерживает абстрактные методы.
Еще одна альтернатива
Просто проверьте класс в классе Abstract и Assert или Exception, как вам нравится.
@implementation Orange
- (instancetype)init
{
self = [super init];
NSAssert([self class] != [Orange class], @"This is an abstract class");
if (self) {
}
return self;
}
@end
Это устраняет необходимость переопределения init
(больше связанного предложения)
Я хотел иметь способ дать программисту знать "не звонить от ребенка" и полностью переопределить (в моем случае все еще предлагать некоторую функциональность по умолчанию от имени родителя, когда он не расширен):
typedef void override_void;
typedef id override_id;
@implementation myBaseClass
// some limited default behavior (undesired by subclasses)
- (override_void) doSomething;
- (override_id) makeSomeObject;
// some internally required default behavior
- (void) doesSomethingImportant;
@end
Преимущество состоит в том, что программист ВИДЕТ "переопределение" в объявлении и будет знать, что он не должен вызывать [super ..]
,
Конечно, для этого некрасиво определять отдельные типы возвращаемых данных, но это служит достаточно хорошим визуальным советом, и вы не можете легко использовать часть "override_" в определении подкласса.
Конечно, класс все еще может иметь реализацию по умолчанию, когда расширение является необязательным. Но, как говорят другие ответы, при необходимости реализуйте исключение времени выполнения, как для абстрактных (виртуальных) классов.
Было бы неплохо иметь встроенные подсказки компилятора, подобные этой, даже подсказки, когда лучше предварительно / опубликовать вызов агрегата супер, вместо того, чтобы копаться в комментариях / документации или... предполагать.
Если вы привыкли к тому, что компилятор отлавливает нарушения абстрактных реализаций в других языках, то поведение Objective-C разочаровывает.
Как язык позднего связывания, ясно, что Objective-C не может принимать статические решения о том, действительно ли класс является абстрактным или нет (вы можете добавлять функции во время выполнения...), но для типичных случаев использования это кажется недостатком. Я бы предпочел, чтобы компилятор предотвращал создание экземпляров абстрактных классов, а не выдавал ошибку во время выполнения.
Вот образец, который мы используем, чтобы получить этот тип статической проверки, используя несколько методов, чтобы скрыть инициализаторы:
//
// Base.h
#define UNAVAILABLE __attribute__((unavailable("Default initializer not available.")));
@protocol MyProtocol <NSObject>
-(void) dependentFunction;
@end
@interface Base : NSObject {
@protected
__weak id<MyProtocol> _protocolHelper; // Weak to prevent retain cycles!
}
- (instancetype) init UNAVAILABLE; // Prevent the user from calling this
- (void) doStuffUsingDependentFunction;
@end
//
// Base.m
#import "Base.h"
// We know that Base has a hidden initializer method.
// Declare it here for readability.
@interface Base (Private)
- (instancetype)initFromDerived;
@end
@implementation Base
- (instancetype)initFromDerived {
// It is unlikely that this becomes incorrect, but assert
// just in case.
NSAssert(![self isMemberOfClass:[Base class]],
@"To be called only from derived classes!");
self = [super init];
return self;
}
- (void) doStuffUsingDependentFunction {
[_protocolHelper dependentFunction]; // Use it
}
@end
//
// Derived.h
#import "Base.h"
@interface Derived : Base
-(instancetype) initDerived; // We cannot use init here :(
@end
//
// Derived.m
#import "Derived.h"
// We know that Base has a hidden initializer method.
// Declare it here.
@interface Base (Private)
- (instancetype) initFromDerived;
@end
// Privately inherit protocol
@interface Derived () <MyProtocol>
@end
@implementation Derived
-(instancetype) initDerived {
self= [super initFromDerived];
if (self) {
self->_protocolHelper= self;
}
return self;
}
// Implement the missing function
-(void)dependentFunction {
}
@end
Вы можете использовать метод, предложенный @Yar (с некоторыми изменениями):
#define mustOverride() @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"%s must be overridden in a subclass/category", __PRETTY_FUNCTION__] userInfo:nil]
#define setMustOverride() NSLog(@"%@ - method not implemented", NSStringFromClass([self class])); mustOverride()
Здесь вы получите сообщение типа:
<Date> ProjectName[7921:1967092] <Class where method not implemented> - method not implemented
<Date> ProjectName[7921:1967092] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[<Base class (if inherited or same if not> <Method name>] must be overridden in a subclass/category'
Или утверждение:
NSAssert(![self respondsToSelector:@selector(<MethodName>)], @"Not implemented");
В этом случае вы получите:
<Date> ProjectName[7926:1967491] *** Assertion failure in -[<Class Name> <Method name>], /Users/kirill/Documents/Projects/root/<ProjectName> Services/Classes/ViewControllers/YourClass:53
Также вы можете использовать протоколы и другие решения - но это одно из самых простых.
Вероятно, такого рода ситуации должны происходить только во время разработки, поэтому это может сработать:
- (id)myMethodWithVar:(id)var {
NSAssert(NO, @"You most override myMethodWithVar:");
return nil;
}
Какао не дает ничего, что называется абстрактным. Мы можем создать реферат класса, который проверяется только во время выполнения, а во время компиляции это не проверяется.
Обычно я просто отключаю метод init в классе, который хочу абстрагировать:
- (instancetype)__unavailable init; // This is an abstract class.
Это сгенерирует ошибку во время компиляции всякий раз, когда вы вызываете init для этого класса. Затем я использую методы класса для всего остального.
Objective-C не имеет встроенного способа объявления абстрактных классов.
Немного изменив то, что предложил @redfood, применив комментарий @ dotToString, вы фактически получаете решение, принятое IGListKit из Instagram.
- Создайте протокол для всех методов, которые не имеют смысла определяться в базовом (абстрактном) классе, т. Е. Им нужны конкретные реализации в дочерних элементах.
- Создайте базовый (абстрактный) класс, который не реализует этот протокол. Вы можете добавить в этот класс любые другие методы, которые имеют смысл иметь общую реализацию.
- Везде в вашем проекте, если ребенок из
AbstractClass
должен быть введен или выведен каким-либо методом, введите его какAbstractClass<Protocol>
вместо.
Так как AbstractClass
не реализует Protocol
Единственный способ иметь AbstractClass<Protocol>
Экземпляр является подклассом. Как AbstractClass
одно не может быть использовано где-либо в проекте, оно становится абстрактным.
Конечно, это не мешает сторонним разработчикам добавлять новые методы, ссылаясь просто на AbstractClass
, что в конечном итоге позволило бы экземпляр (больше) абстрактного класса.
Пример из реальной жизни: IGListKit имеет базовый класс IGListSectionController
который не реализует протокол IGListSectionType
Однако каждый метод, который требует экземпляр этого класса, на самом деле запрашивает тип IGListSectionController<IGListSectionType>
, Поэтому нет возможности использовать объект типа IGListSectionController
за что-нибудь полезное в их рамках.
На самом деле Objective-C не имеет абстрактных классов, но вы можете использовать протоколы для достижения того же эффекта. Вот образец:
CustomProtocol.h
#import <Foundation/Foundation.h>
@protocol CustomProtocol <NSObject>
@required
- (void)methodA;
@optional
- (void)methodB;
@end
TestProtocol.h
#import <Foundation/Foundation.h>
#import "CustomProtocol.h"
@interface TestProtocol : NSObject <CustomProtocol>
@end
TestProtocol.m
#import "TestProtocol.h"
@implementation TestProtocol
- (void)methodA
{
NSLog(@"methodA...");
}
- (void)methodB
{
NSLog(@"methodB...");
}
@end
Простой пример создания абстрактного класса
// Declare a protocol
@protocol AbcProtocol <NSObject>
-(void)fnOne;
-(void)fnTwo;
@optional
-(void)fnThree;
@end
// Abstract class
@interface AbstractAbc : NSObject<AbcProtocol>
@end
@implementation AbstractAbc
-(id)init{
self = [super init];
if (self) {
}
return self;
}
-(void)fnOne{
// Code
}
-(void)fnTwo{
// Code
}
@end
// Implementation class
@interface ImpAbc : AbstractAbc
@end
@implementation ImpAbc
-(id)init{
self = [super init];
if (self) {
}
return self;
}
// You may override it
-(void)fnOne{
// Code
}
// You may override it
-(void)fnTwo{
// Code
}
-(void)fnThree{
// Code
}
@end
Objective-C не поддерживает напрямую абстракцию, как Swift, однако AnyObject основан на NSObject, поэтому мы можем моделировать 90% поведения для внедрения зависимостей.
Это мой пример кода.
#import <XCTest/XCTest.h>
@protocol CacheInterface <NSObject>
- (void)saveItem: (NSString*)item withKey:(NSString*)key;
- (NSString*)getItemForKey:(NSString*)key;
@end
//------------------------------------
@interface Cache : NSObject<CacheInterface>
- (instancetype)initWithMemoryCache: (NSObject<CacheInterface>*)memCache diskCache:(NSObject<CacheInterface>*) diskCache;
@end
@interface Cache()
@property(nonatomic) NSObject<CacheInterface>* memCache;
@property(nonatomic) NSObject<CacheInterface>* diskCache;
@end
@implementation Cache
- (instancetype)initWithMemoryCache: (NSObject<CacheInterface>*)memCache diskCache:(NSObject<CacheInterface>*) diskCache {
self = [super init];
self.memCache = memCache;
self.diskCache = diskCache;
return self;
}
- (void)saveItem: (NSString*)item withKey:(NSString*)key {
[self.memCache saveItem:item withKey:key];
//save to disk cache
}
- (NSString *)getItemForKey:(NSString *)key {
return [self.memCache getItemForKey:key];
//fallback to disk cache if can't get from memcache
}
@end
//---------------------------------
@interface MockCache : NSObject<CacheInterface>
@end
@interface MockCache()
@property(nonatomic)NSMutableDictionary*dict;
@end
@implementation MockCache
- (instancetype)init {
self = [super init];
self.dict = [[NSMutableDictionary alloc] init];
return self;
}
- (void)saveItem:(NSString *)item withKey:(NSString *)key {
self.dict[key] = item;
}
- (NSString *)getItemForKey:(NSString *)key {
return self.dict[key];
}
@end
//--------------------------------
@interface SampleTest : XCTestCase
@end
@implementation SampleTest
- (void)testCache {
Cache *sut = [self makeSUT];
NSString *item = @"ABC";
NSString *key = @"key";
[sut saveItem: item withKey:key];
XCTAssertEqual(item, [sut getItemForKey:key]);
}
- (Cache*)makeSUT {
MockCache *mockMemCache = [[MockCache alloc] init];
Cache *sut = [[Cache alloc] initWithMemoryCache:mockMemCache diskCache:nil];
return sut;
}
@end
Вы не можете просто создать делегата?
Делегат подобен абстрактному базовому классу в том смысле, что вы говорите, какие функции должны быть определены, но на самом деле вы их не определяете.
Затем всякий раз, когда вы реализуете свой делегат (т.е. абстрактный класс), компилятор предупреждает вас о том, для каких необязательных и обязательных функций вам нужно определить поведение.
Это звучит как абстрактный базовый класс для меня.