web-dev-qa-db-fra.com

Création d'une classe abstraite en Objective-C

Je suis à l'origine un programmeur Java qui travaille maintenant avec Objective-C. J'aimerais créer une classe abstraite, mais cela ne semble pas possible dans Objective-C. Est-ce possible?

Sinon, à quel point puis-je me rapprocher d'une classe abstraite en Objective-C?

501
Jonathan Arbogast

En règle générale, les classes Objective-C sont abstraites par convention uniquement. Si l'auteur documente une classe de manière abstraite, ne l'utilisez pas sans la sous-classer. Aucune application au moment de la compilation n'empêche toutefois l'instanciation d'une classe abstraite. En fait, rien n'empêche un utilisateur de fournir des implémentations de méthodes abstraites via une catégorie (c'est-à-dire au moment de l'exécution). Vous pouvez forcer un utilisateur à au moins remplacer certaines méthodes en générant une exception dans leur implémentation dans votre classe abstraite:

[NSException raise:NSInternalInconsistencyException 
            format:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)];

Si votre méthode retourne une valeur, c'est un peu plus facile à utiliser

@throw [NSException exceptionWithName:NSInternalInconsistencyException
                               reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)]
                             userInfo:nil];

car vous n'avez pas besoin d'ajouter une instruction return de la méthode.

Si la classe abstraite est vraiment une interface (c’est-à-dire qu’elle n’a pas d’implémentation de méthode concrète), l’utilisation d’un protocole Objective-C est l’option la plus appropriée.

627
Barry Wark

Non, il n'y a aucun moyen de créer une classe abstraite dans Objective-C.

Vous pouvez simuler une classe abstraite - en appelant méthodes/sélecteurs avec doNotRecognizeSelector: et, par conséquent, déclencher une exception rendant la classe inutilisable.

Par exemple:

- (id)someMethod:(SomeObject*)blah
{
     [self doesNotRecognizeSelector:_cmd];
     return nil;
}

Vous pouvez également le faire pour init.

267
Grouchal

Il suffit de suivre la réponse de @Barry Wark ci-dessus (et de la mettre à jour pour iOS 4.3) et de laisser ceci pour ma propre référence:

#define mustOverride() @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"%s must be overridden in a subclass/category", __PRETTY_FUNCTION__] userInfo:nil]
#define methodNotImplemented() mustOverride()

alors dans vos méthodes, vous pouvez utiliser cette

- (void) someMethod {
     mustOverride(); // or methodNotImplemented(), same thing
}



Notes: Je ne sais pas si faire ressembler une macro à une fonction C est une bonne idée ou non, mais je la garderai jusqu'à ce que scolarisé au contraire. Je pense qu'il est plus correct d'utiliser NSInvalidArgumentException (plutôt que NSInternalInconsistencyException) car c'est ce que le système d'exécution renvoie en réponse à l'appel de doesNotRecognizeSelector (voir NSObject docs).

60
Dan Rosenstark

La solution que j'ai trouvée est:

  1. Créez un protocole pour tout ce que vous voulez dans votre classe "abstraite"
  2. Créez une classe de base (ou appelez-la abstraite) qui implémente le protocole. Pour toutes les méthodes que vous voulez "abstraites", implémentez-les dans le fichier .m, mais pas dans le fichier .h.
  3. Faites en sorte que votre classe enfant hérite de la classe de base ET implémente le protocole.

De cette façon, le compilateur vous avertira de toute méthode du protocole non implémentée par votre classe enfant.

Ce n'est pas aussi succinct que dans Java, mais vous obtenez l'avertissement du compilateur souhaité.

41
redfood

À partir de la liste de diffusion Omni Group :

Objective-C ne dispose pas de la structure de compilation abstraite telle que Java pour le moment.

Vous ne devez donc définir que la classe abstraite comme toute autre classe normale et implémenter des stubs de méthodes pour les méthodes abstraites vides ou ne pas prendre en charge le sélecteur. Par exemple...

- (id)someMethod:(SomeObject*)blah
{
     [self doesNotRecognizeSelector:_cmd];
     return nil;
}

Je fais aussi ce qui suit pour empêcher l’initialisation de la classe abstraite via l’initialiseur par défaut.

- (id)init
{
     [self doesNotRecognizeSelector:_cmd];
     [self release];
     return nil;
}
34
user79854

Au lieu d'essayer de créer une classe de base abstraite, envisagez d'utiliser un protocole (similaire à une interface Java). Cela vous permet de définir un ensemble de méthodes, puis d'accepter tous les objets conformes au protocole et de les implémenter. Par exemple, je peux définir un protocole d'opération, puis une fonction comme celle-ci:

- (void)performOperation:(id<Operation>)op
{
   // do something with operation
}

Où op peut être n'importe quel objet implémentant le protocole Operation.

Si vous souhaitez que votre classe de base abstraite fasse plus que simplement définir des méthodes, vous pouvez créer une classe Objective-C standard et empêcher son instanciation. Remplacez simplement la fonction - (id) init et faites-la retourner nil ou assert (false). Ce n'est pas une solution très propre, mais comme Objective-C est entièrement dynamique, il n'y a pas d'équivalent direct à une classe de base abstraite.

21
Ben Gotow

Ce fil est un peu vieux, et la plupart de ce que je veux partager est déjà là.

Cependant, ma méthode préférée n’est pas mentionnée et, autant que je sache, le Clang actuel n’a pas de support natif, alors je vais y aller…

D'abord et avant tout (comme d'autres l'ont déjà souligné), les classes abstraites sont quelque chose de très rare en Objective-C - nous utilisons généralement la composition (parfois par délégation). C’est probablement la raison pour laquelle une telle fonctionnalité n’existe pas déjà dans le langage/compilateur - mis à part les propriétés @dynamic que IIRC ont été ajoutées à ObjC 2.0 lors de l’introduction de CoreData.

Mais étant donné que (après une évaluation minutieuse de votre situation!), Vous êtes parvenu à la conclusion que la délégation (ou la composition en général) n'est pas bien adaptée pour résoudre votre problème, voici comment I fais le:

  1. Implémentez toutes les méthodes abstraites de la classe de base.
  2. Faites cette implémentation [self doesNotRecognizeSelector:_cmd];
  3. … Suivi de __builtin_unreachable(); pour désactiver l'avertissement que vous obtiendrez pour les méthodes non-vides, en vous indiquant que "le contrôle de la fonction non-vide est terminé sans retour".
  4. Vous pouvez combiner les étapes 2. et 3. dans une macro ou annoter -[NSObject doesNotRecognizeSelector:] en utilisant __attribute__((__noreturn__)) dans une catégorie sans implémentation afin de ne pas remplacer le implémentation originale de cette méthode et incluez l’en-tête de cette catégorie dans le fichier PCH de votre projet.

Personnellement, je préfère la version macro car cela me permet de réduire le plus possible le passe-partout.

C'est ici:

// 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

Comme vous pouvez le constater, la macro fournit l’implémentation complète des méthodes abstraites, réduisant ainsi au minimum la quantité nécessaire de passe-partout.

Une option encore meilleure serait de faire pression sur leéquipe Clang de fournir un attribut du compilateur pour ce cas, via des demandes de fonctionnalité. (Mieux, car cela activerait également les diagnostics au moment de la compilation pour les scénarios dans lesquels vous sous-classez, par exemple, NSIncrementalStore.)

Pourquoi je choisis cette méthode

  1. Le travail est fait de manière efficace et un peu commode.
  2. C’est assez facile à comprendre. (Très bien, cette __builtin_unreachable() peut surprendre les gens, mais il est assez facile à comprendre aussi.)
  3. Il ne peut pas être supprimé dans les versions de publication sans générer d’autres avertissements ou erreurs du compilateur, contrairement à une approche basée sur l’une des macros d’affirmation.

Ce dernier point mérite quelques explications, je suppose:

Certaines personnes (la plupart?) Suppriment les assertions dans les versions de version. (Je ne suis pas d’accord avec cette habitude, mais c’est une autre histoire…) Ne pas mettre en œuvre une méthode requise - est cependant mauvais , terrible , faux et fondamentalement la fin de l'univers pour votre programme. Votre programme ne peut pas fonctionner correctement à cet égard car il est indéfini, et un comportement indéfini est la pire des choses. Par conséquent, pouvoir supprimer ces diagnostics sans générer de nouveaux diagnostics serait totalement inacceptable.

Il est déjà suffisamment grave pour que vous ne puissiez pas obtenir de diagnostics corrects au moment de la compilation pour de telles erreurs de programmeur, et que vous deviez recourir à une découverte au moment de l'exécution, mais si vous pouvez le plâtrer dans des versions release, pourquoi avoir une classe abstraite première place?

19
danyowdee

Utiliser @property et @dynamic pourrait également fonctionner. Si vous déclarez une propriété dynamique sans donner d'implémentation de méthode correspondante, tout sera toujours compilé sans avertissements et vous obtiendrez une erreur unrecognized selector au moment de l'exécution si vous essayez d'y accéder. Ceci revient essentiellement à appeler [self doesNotRecognizeSelector:_cmd], mais avec beaucoup moins de frappe.

12
Cameron Spickert

La réponse à la question est dispersée dans les commentaires sous les réponses déjà données. Donc, je ne fais que résumer et simplifier ici.

Option1: Protocoles

Si vous voulez créer une classe abstraite sans implémentation, utilisez "Protocoles". Les classes qui héritent d'un protocole sont obligées d'implémenter les méthodes du protocole.

@protocol ProtocolName
// list of methods and properties
@end

Option2: Modèle de méthode de modèle

Si vous voulez créer une classe abstraite avec une implémentation partielle telle que "Template Method Pattern", alors c'est la solution. Objective-C - Pattern de méthodes de template?

7
hadaytullah

Dans Xcode (utilisation de clang, etc.), j'aime bien utiliser __attribute__((unavailable(...))) pour baliser les classes abstraites afin que vous obteniez une erreur/un avertissement si vous essayez de l'utiliser.

Il fournit une certaine protection contre l'utilisation accidentelle de la méthode.

Exemple

Dans la classe de base @interface, identifiez les méthodes "abstraites":

- (void)myAbstractMethod:(id)param1 __attribute__((unavailable("You should always override this")));

Pour aller plus loin, je crée une macro:

#define UnavailableMacro(msg) __attribute__((unavailable(msg)))

Cela vous permet de faire ceci:

- (void)myAbstractMethod:(id)param1 UnavailableMacro(@"You should always override this");

Comme je l'ai dit, il ne s'agit pas d'une véritable protection du compilateur, mais elle vaut à peu près autant que vous irez dans un langage qui ne prend pas en charge les méthodes abstraites.

7
rjstelling

Une autre alternative

Il suffit de vérifier la classe dans la classe Abstract et Assert ou Exception, selon vos envies.

@implementation Orange
- (instancetype)init
{
    self = [super init];
    NSAssert([self class] != [Orange class], @"This is an abstract class");
    if (self) {
    }
    return self;
}
@end

Cela supprime la nécessité de remplacer init

6
bigkm

(plus d'une suggestion connexe)

Je voulais avoir un moyen de laisser le programmeur savoir "ne pas appeler d'enfant" et de le remplacer complètement (dans mon cas, offre toujours certaines fonctionnalités par défaut pour le compte du parent lorsqu'il n'est pas étendu):

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

L'avantage est que le programmeur verra le "remplacement" dans la déclaration et saura qu'il ne devrait pas appeler [super ..].

Certes, il est déplorable de devoir définir des types de retour individuels pour cela, mais cela constitue un indice visuel suffisant et vous ne pouvez facilement pas utiliser la partie "override_" dans une définition de sous-classe.

Bien entendu, une classe peut toujours avoir une implémentation par défaut lorsqu'une extension est facultative. Mais comme le disent les autres réponses, implémentez une exception d'exécution le cas échéant, comme pour les classes abstraites (virtuelles).

Il serait bien d’avoir des astuces de compilation intégrées comme celle-ci, même pour le meilleur moment pour pré/post-appeler l’implémentation du super, au lieu d’avoir à creuser avec des commentaires/documentation ou ... supposer.

example of the hint

5
Ivan Dossev

Si vous êtes habitué à ce que le compilateur intercepte des violations d'instanciation abstraites dans d'autres langages, le comportement d'Objective-C est décevant.

En tant que langage de liaison tardif, il est clair qu'Objective-C ne peut pas prendre de décision statique sur le statut abstrait ou non d'une classe (vous pouvez ajouter des fonctions au moment de l'exécution ...), mais dans les cas d'utilisation typiques, cela semble être une lacune. Je préférerais que le compilateur prévienne les instanciations de classes abstraites à plat plutôt que de générer une erreur lors de l'exécution.

Voici un modèle que nous utilisons pour obtenir ce type de vérification statique en utilisant deux techniques pour masquer les initialiseurs:

//
//  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
4
Cross_

Vous pouvez utiliser une méthode proposée par @ Yar (avec quelques modifications):

#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()

Ici, vous recevrez un message comme:

<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'

Ou affirmation:

NSAssert(![self respondsToSelector:@selector(<MethodName>)], @"Not implemented");

Dans ce cas, vous obtiendrez:

<Date> ProjectName[7926:1967491] *** Assertion failure in -[<Class Name> <Method name>], /Users/kirill/Documents/Projects/root/<ProjectName> Services/Classes/ViewControllers/YourClass:53

Vous pouvez aussi utiliser des protocoles et d’autres solutions, mais c’est l’une des plus simples.

3
gbk

Ce genre de situation ne devrait probablement survenir qu'au moment du développement. Cela pourrait donc fonctionner:

- (id)myMethodWithVar:(id)var {
   NSAssert(NO, @"You most override myMethodWithVar:");
   return nil;
}
3
juanjo

Cocoa ne fournit rien d’abstrait. Nous pouvons créer un résumé de classe qui ne sera vérifié qu'au moment de l'exécution, et lors de la compilation, ce n'est pas vérifié.

2
Hussain Shabbir

En général, je ne fais que désactiver la méthode init dans une classe que je veux résumer:

- (instancetype)__unavailable init; // This is an abstract class.

Cela générera une erreur lors de la compilation chaque fois que vous appelez init sur cette classe. J'utilise ensuite des méthodes de classe pour tout le reste.

Objective-C n'a pas de moyen intégré pour déclarer des classes abstraites.

1
The_Androctonus

Un exemple simple de création d'une classe abstraite

// 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
0
Say2Manuj

En fait, Objective-C n'a pas de classes abstraites, mais vous pouvez utiliser Protocoles pour obtenir le même effet. Voici l'échantillon:

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
0
york

En modifiant un peu ce que @redfood a suggéré en appliquant le commentaire de @ dotToString, vous avez en fait adopté la solution proposée par Instagram IGListKit .

  1. Créez un protocole pour toutes les méthodes qui n’ont aucun sens à définir dans la classe de base (abstraite), c’est-à-dire qu’elles nécessitent une implémentation spécifique dans les enfants.
  2. Créez une classe de base (abstraite) que ne met pas en œuvre ce protocole. Vous pouvez ajouter à cette classe toute autre méthode utile pour une implémentation commune.
  3. Partout dans votre projet, si un enfant de AbstractClass doit être entré ou sorti par une méthode, tapez-le comme AbstractClass<Protocol> à la place.

Comme AbstractClass n'implémente pas Protocol, le seul moyen d'avoir une instance AbstractClass<Protocol> consiste à sous-classer. Comme AbstractClass seul ne peut être utilisé nulle part dans le projet, il devient abstrait.

Bien entendu, cela n’empêche pas les développeurs peu avisés d’ajouter de nouvelles méthodes faisant simplement référence à AbstractClass, qui autoriseraient une instance de la classe abstraite (et non plus).

Exemple concret: IGListKit a une classe de base IGListSectionController qui n'implémente pas le protocole IGListSectionType, mais chaque méthode nécessitant une instance de cette classe demande en fait le type IGListSectionController<IGListSectionType>. Par conséquent, il n’ya aucun moyen d’utiliser un objet de type IGListSectionController pour quoi que ce soit d’utile dans leur framework.

0
Gobe