objective c - Creando una clase abstracta en Objective-C
abstract-class (21)
Originalmente soy un programador de Java que ahora trabaja con Objective-C. Me gustaría crear una clase abstracta, pero eso no parece ser posible en Objective-C. es posible?
Si no es así, ¿qué tan cerca de una clase abstracta puedo obtener en Objective-C?
¿No puedes crear un delegado?
Un delegado es como una clase base abstracta en el sentido de que usted dice qué funciones deben definirse, pero en realidad no las define.
Luego, cada vez que implementa su delegado (es decir, clase abstracta), el compilador le advierte de para qué funciones opcionales y obligatorias necesita definir el comportamiento.
Esto me suena como una clase base abstracta.
(más de una sugerencia relacionada)
Quería tener una forma de hacerle saber al programador "no llamar desde el niño" y anularlo por completo (en mi caso aún ofrezco algunas funciones predeterminadas en nombre de los padres cuando no está extendido):
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
La ventaja es que el programador VERÁ la "anulación" en la declaración y sabrá que no deben llamar [super ..]
.
Por supuesto, es desagradable tener que definir tipos de retorno individuales para esto, pero sirve como una sugerencia visual lo suficientemente buena y no se puede usar la parte "anular_" en una definición de subclase.
Por supuesto, una clase aún puede tener una implementación predeterminada cuando una extensión es opcional. Pero como dicen las otras respuestas, implemente una excepción de tiempo de ejecución cuando sea apropiado, como para las clases abstractas (virtuales).
Sería bueno tener incorporados sugerencias de compilación como esta, incluso sugerencias sobre cuándo es mejor realizar una pre / post llamada al implemento del super, en lugar de revisar los comentarios / documentación o ... asumir.
Cambiando un poco lo que @redfood sugirió aplicando el comentario de @ dotToString, realmente tiene la solución adoptada por IGListKit de Instagram.
- Cree un protocolo para todos los métodos que no tienen sentido que se definan en la clase base (resumen), es decir, necesitan implementaciones específicas en los elementos secundarios.
- Cree una clase base (abstracta) que no implemente este protocolo. Puede agregar a esta clase cualquier otro método que tenga sentido para tener una implementación común.
- En cualquier parte de su proyecto, si un niño de
AbstractClass
debe ser ingresado o generado por algún método, escríbalo comoAbstractClass<Protocol>
.
Debido a que AbstractClass
no implementa el Protocol
, la única forma de tener una instancia de AbstractClass<Protocol>
es a través de subclases. Como AbstractClass
solo no se puede utilizar en ninguna parte del proyecto, se vuelve abstracto.
Por supuesto, esto no impide que los desarrolladores no asesorados agreguen nuevos métodos que se refieran simplemente a AbstractClass
, lo que terminaría permitiendo una instancia de la clase abstracta (ya no).
Ejemplo del mundo real: IGListKit tiene una clase base IGListSectionController
que no implementa el protocolo IGListSectionType
; sin embargo, todos los métodos que requieren una instancia de esa clase, realmente solicitan el tipo IGListSectionController<IGListSectionType>
. Por lo tanto, no hay forma de usar un objeto de tipo IGListSectionController
para nada útil en su marco.
De hecho, Objective-C no tiene clases abstractas, pero puedes usar Protocolos para lograr el mismo efecto. Aquí está la muestra:
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
De la lista de correo de Omni Group :
Objective-C no tiene la compilación abstracta como Java en este momento.
Entonces, todo lo que hace es definir la clase abstracta como cualquier otra clase normal e implementar apéndices de métodos para los métodos abstractos que están vacíos o que no son compatibles con el selector. Por ejemplo...
- (id)someMethod:(SomeObject*)blah
{
[self doesNotRecognizeSelector:_cmd];
return nil;
}
También hago lo siguiente para evitar la inicialización de la clase abstracta a través del inicializador predeterminado.
- (id)init
{
[self doesNotRecognizeSelector:_cmd];
[self release];
return nil;
}
En Xcode (usando clang, etc.) me gusta usar __attribute__((unavailable(...)))
para etiquetar las clases abstractas para que aparezca un error / advertencia si lo intentas y usas.
Proporciona cierta protección contra el uso accidental del método.
Ejemplo
En la etiqueta de la clase base @interface
los métodos "abstractos":
- (void)myAbstractMethod:(id)param1 __attribute__((unavailable("You should always override this")));
Yendo un paso más allá, creo una macro:
#define UnavailableMacro(msg) __attribute__((unavailable(msg)))
Esto te permite hacer esto:
- (void)myAbstractMethod:(id)param1 UnavailableMacro(@"You should always override this");
Como dije, esto no es una protección real del compilador, pero es tan bueno como lo que obtendrás en un lenguaje que no admita métodos abstractos.
En lugar de intentar crear una clase base abstracta, considere usar un protocolo (similar a una interfaz Java). Esto le permite definir un conjunto de métodos, y luego aceptar todos los objetos que cumplen con el protocolo e implementar los métodos. Por ejemplo, puedo definir un protocolo de operación y luego tener una función como esta:
- (void)performOperation:(id<Operation>)op
{
// do something with operation
}
Donde op puede ser cualquier objeto implementando el protocolo de operación.
Si necesita que su clase base abstracta haga más que simplemente definir métodos, puede crear una clase regular de Objective-C y evitar que se cree una instancia. Simplemente anule la función - (id) init y haga que devuelva nil o afirme (false). No es una solución muy limpia, pero como Objective-C es completamente dinámico, realmente no existe un equivalente directo a una clase base abstracta.
Este hilo es un poco antiguo, y la mayoría de lo que quiero compartir ya está aquí.
Sin embargo, no se menciona mi método favorito, y AFAIK no hay soporte nativo en el Clang actual, así que aquí voy ...
En primer lugar, y lo más importante (como ya han señalado otros) las clases abstractas son algo poco común en Objective-C: usualmente usamos la composición (a veces a través de la delegación). Esta es probablemente la razón por la que tal característica no existe ya en el lenguaje / compilador, aparte de @dynamic
propiedades @dynamic
, que IIRC se han agregado en ObjC 2.0 que acompaña a la introducción de CoreData.
Pero dado que (después de una evaluación cuidadosa de su situación), ha llegado a la conclusión de que la delegación (o la composición en general) no es adecuada para resolver su problema, así es como lo hago:
- Implementar todos los métodos abstractos en la clase base.
- Haga que la implementación
[self doesNotRecognizeSelector:_cmd];
... - ... seguido de
__builtin_unreachable();
para silenciar la advertencia que obtendrá para los métodos no nulos, indicándole que "el control alcanzó el final de la función no nula sin retorno". - Combine los pasos 2. y 3. en una macro, o anote
-[NSObject doesNotRecognizeSelector:]
usando__attribute__((__noreturn__))
en una categoría sin implementación para no reemplazar la implementación original de ese método, e incluya el encabezado de la misma. Categoría en PCH de tu proyecto.
Personalmente prefiero la versión macro, ya que me permite reducir la placa de repetición tanto como sea posible.
Aquí está:
// 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
Como puede ver, la macro proporciona la implementación completa de los métodos abstractos, reduciendo la cantidad necesaria de repetitivo al mínimo absoluto.
Una opción aún mejor sería presionar al equipo de Clang para que proporcione un atributo de compilador para este caso, a través de solicitudes de características. (Mejor, porque esto también habilitaría los diagnósticos en tiempo de compilación para aquellos escenarios en los que se subclase, por ejemplo, NSIncrementalStore).
Por qué elijo este método
- Se hace el trabajo de manera eficiente, y algo conveniente.
- Es bastante fácil de entender. (De acuerdo,
__builtin_unreachable()
puede sorprender a la gente, pero también es fácil de entender). - No se puede eliminar en las compilaciones de versión sin generar otras advertencias o errores del compilador, a diferencia de un enfoque que se basa en una de las macros de aserción.
Ese último punto necesita una explicación, supongo:
Algunas personas (¿la mayoría?) Eliminan las aserciones en las versiones de lanzamiento. (No estoy de acuerdo con ese hábito, pero esa es otra historia ...) No implementar un método requerido, sin embargo, es malo , terrible , equivocado y, básicamente, es el fin del universo para su programa. Su programa no puede funcionar correctamente a este respecto porque está indefinido, y el comportamiento indefinido es lo peor. Por lo tanto, ser capaz de eliminar esos diagnósticos sin generar nuevos diagnósticos sería completamente inaceptable.
Ya es bastante malo que no pueda obtener los diagnósticos adecuados en tiempo de compilación para tales errores de programador, y tenga que recurrir al descubrimiento en tiempo de ejecución para estos, pero si puede usarlo en las versiones de lanzamiento, ¿por qué intentar tener una clase abstracta en el ¿primer lugar?
La respuesta a la pregunta está dispersa en los comentarios bajo las respuestas ya dadas. Entonces, estoy resumiendo y simplificando aquí.
Opción 1: Protocolos
Si desea crear una clase abstracta sin implementación, use ''Protocolos''. Las clases que heredan un protocolo están obligadas a implementar los métodos en el protocolo.
@protocol ProtocolName
// list of methods and properties
@end
Opción 2: Patrón de método de plantilla
Si desea crear una clase abstracta con implementación parcial como "Patrón de método de plantilla", esta es la solución. Objective-C - Patrón de métodos de plantilla?
La solución que se me ocurrió es:
- Cree un protocolo para todo lo que quiera en su clase "abstracta"
- Cree una clase base (o tal vez llame resumen) que implemente el protocolo. Para todos los métodos que quiera "abstractos", impleméntelos en el archivo .m, pero no en el archivo .h.
- Haga que su clase secundaria herede de la clase base Y que implemente el protocolo.
De esta manera, el compilador le dará una advertencia para cualquier método en el protocolo que no esté implementado por su clase hija.
No es tan breve como en Java, pero sí obtiene la advertencia del compilador deseado.
No, no hay manera de crear una clase abstracta en Objective-C.
Puede simular una clase abstracta haciendo que los métodos / selectores llamen a doesNotRecognizeSelector: y, por lo tanto, provocar una excepción que haga que la clase sea inutilizable.
Por ejemplo:
- (id)someMethod:(SomeObject*)blah
{
[self doesNotRecognizeSelector:_cmd];
return nil;
}
También puedes hacer esto para init.
Normalmente deshabilito el método init en una clase que quiero abstraer:
- (instancetype)__unavailable init; // This is an abstract class.
Esto generará un error en el momento de la compilación siempre que llame a init en esa clase. Luego uso métodos de clase para todo lo demás.
Objective-C no tiene una forma integrada para declarar clases abstractas.
Otra alternativa
Simplemente marque la clase en la clase de Resumen y Afirmar o Excepción, lo que le apetezca.
@implementation Orange
- (instancetype)init
{
self = [super init];
NSAssert([self class] != [Orange class], @"This is an abstract class");
if (self) {
}
return self;
}
@end
Esto elimina la necesidad de anular init
Por lo general, las clases de Objective-C son abstractas solo por convención; si el autor documenta una clase como abstracta, simplemente no la use sin subclasificarla. Sin embargo, no existe una aplicación en tiempo de compilación que impida la creación de instancias de una clase abstracta. De hecho, no hay nada que impida que un usuario proporcione implementaciones de métodos abstractos a través de una categoría (es decir, en tiempo de ejecución). Puede forzar a un usuario a que, como mínimo, anule ciertos métodos al generar una excepción en la implementación de esos métodos en su clase abstracta:
[NSException raise:NSInternalInconsistencyException
format:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)];
Si su método devuelve un valor, es un poco más fácil de usar
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)]
userInfo:nil];
como entonces no es necesario agregar una declaración de retorno del método.
Si la clase abstracta es realmente una interfaz (es decir, no tiene implementaciones de métodos concretos), la opción más apropiada es usar un protocolo Objective-C.
Probablemente este tipo de situaciones solo deberían ocurrir en el momento del desarrollo, por lo que esto podría funcionar:
- (id)myMethodWithVar:(id)var {
NSAssert(NO, @"You most override myMethodWithVar:");
return nil;
}
Puedes usar un método propuesto por @Yar (con alguna modificación):
#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()
Aquí recibirás un mensaje como:
<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''
O afirmación:
NSAssert(![self respondsToSelector:@selector(<MethodName>)], @"Not implemented");
En este caso obtendrás:
<Date> ProjectName[7926:1967491] *** Assertion failure in -[<Class Name> <Method name>], /Users/kirill/Documents/Projects/root/<ProjectName> Services/Classes/ViewControllers/YourClass:53
También puedes usar protocolos y otras soluciones, pero esta es una de las más simples.
Si está acostumbrado a que el compilador capture infracciones de ejemplificación abstracta en otros idiomas, entonces el comportamiento de Objective-C es decepcionante.
Como lenguaje de enlace tardío, está claro que Objective-C no puede tomar decisiones estáticas sobre si una clase es realmente abstracta o no (podría estar agregando funciones en tiempo de ejecución ...), pero para casos de uso típicos, esto parece ser una deficiencia. Preferiría que el compilador de plano evitase las instancias de clases abstractas en lugar de lanzar un error en tiempo de ejecución.
Aquí hay un patrón que estamos usando para obtener este tipo de comprobación estática usando un par de técnicas para ocultar los inicializadores:
//
// 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
Simplemente hojeé la respuesta de @Barry Wark arriba (y actualizando para iOS 4.3) y dejé esto para mi propia referencia:
#define mustOverride() @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"%s must be overridden in a subclass/category", __PRETTY_FUNCTION__] userInfo:nil]
#define methodNotImplemented() mustOverride()
entonces en tus métodos puedes usar esto
- (void) someMethod {
mustOverride(); // or methodNotImplemented(), same thing
}
Notas: No estoy seguro si hacer que una macro se vea como una función de C es una buena idea o no, pero la mantendré hasta que se me indique lo contrario. Creo que es más correcto usar NSInvalidArgumentException
(en lugar de NSInternalInconsistencyException
) ya que eso es lo que el sistema de ejecución lanza en respuesta a la doesNotRecognizeSelector
a doesNotRecognizeSelector
(ver documentos de NSObject
).
Un ejemplo simple de crear una clase abstracta.
// 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
Usar @property
y @dynamic
también podría funcionar. Si declara una propiedad dinámica y no proporciona una implementación de método coincidente, todo se seguirá compilando sin advertencias y obtendrá un error de unrecognized selector
en el tiempo de ejecución si intenta acceder a ella. Esencialmente, es lo mismo que llamar a [self doesNotRecognizeSelector:_cmd]
, pero con mucho menos escritura.
Cocoa no proporciona nada llamado abstracto. Podemos crear un resumen de clase que se verifique solo en tiempo de ejecución, y en el momento de la compilación esto no se verifica.