Existe-t-il un moyen d'imposer la saisie sur NSArray, NSMutableArray, etc.?

Réponses:

35

Vous pouvez créer une catégorie avec une -addSomeClass:méthode pour permettre la vérification du type statique au moment de la compilation (afin que le compilateur puisse vous faire savoir si vous essayez d'ajouter un objet qu'il sait être une classe différente via cette méthode), mais il n'y a pas de moyen réel de faire appliquer cela un tableau ne contient que des objets d'une classe donnée.

En général, une telle contrainte ne semble pas nécessaire en Objective-C. Je ne pense pas avoir jamais entendu un programmeur expérimenté de Cocoa souhaiter cette fonctionnalité. Les seules personnes qui semblent être des programmeurs d'autres langues qui pensent encore dans ces langues. Si vous ne voulez que des objets d'une classe donnée dans un tableau, n'y insérez que les objets de cette classe. Si vous souhaitez tester que votre code se comporte correctement, testez-le.

Mandrin
la source
136
Je pense que les «programmeurs Cocoa expérimentés» ne savent tout simplement pas ce qui leur manque - l'expérience avec Java montre que les variables de type améliorent la compréhension du code et rendent plus de refactorisations possibles.
tgdavies
11
Eh bien, le support des génériques de Java est fortement cassé en soi, car ils ne l'ont pas mis en place depuis le début ...
dertoni
28
Je suis d'accord avec @tgdavies. Les capacités d'intellisense et de refactoring que j'avais avec C # me manquent. Lorsque je veux un typage dynamique, je peux l'obtenir en C # 4.0. Quand je veux taper fortement des trucs, je peux avoir ça aussi. J'ai trouvé qu'il y a un temps et un lieu pour ces deux choses.
Steve
18
@charkrit Qu'est-ce qui rend l'Objective-C «inutile»? Avez-vous pensé que c'était nécessaire lorsque vous utilisiez C #? J'entends beaucoup de gens dire que vous n'en avez pas besoin en Objective-C, mais je pense que ces mêmes personnes pensent que vous n'en avez besoin dans aucune langue, ce qui en fait une question de préférence / style, pas de nécessité.
bacar
17
N'est-ce pas pour permettre à votre compilateur de vous aider à trouver des problèmes? Bien sûr, vous pouvez dire «Si vous ne voulez que des objets d'une classe donnée dans un tableau, ne collez que les objets de cette classe». Mais si les tests sont le seul moyen de faire respecter cela, vous êtes désavantagé. Plus vous trouvez un problème loin de l'écriture du code, plus ce problème est coûteux.
GreenKiwi
145

Personne n'a encore mis ça ici, alors je vais le faire!

Ceci est désormais officiellement pris en charge dans Objective-C. À partir de Xcode 7, vous pouvez utiliser la syntaxe suivante:

NSArray<MyClass *> *myArray = @[[MyClass new], [MyClass new]];

Remarque

Il est important de noter qu'il ne s'agit que d'avertissements du compilateur et que vous pouvez techniquement toujours insérer n'importe quel objet dans votre tableau. Il existe des scripts disponibles qui transforment tous les avertissements en erreurs qui empêcheraient la construction.

Logan
la source
Je suis paresseux ici, mais pourquoi est-ce uniquement disponible dans XCode 7? Nous pouvons utiliser le nonnulldans XCode 6 et pour autant que je me souvienne, ils ont été introduits en même temps. De plus, l'utilisation de tels concepts dépend-elle de la version XCode ou de la version iOS?
Guven
@Guven - nullability est arrivé en 6, vous avez raison, mais les génériques ObjC n'ont pas été introduits avant Xcode 7.
Logan
Je suis sûr que cela dépend uniquement de la version de Xcode. Les génériques sont uniquement des avertissements du compilateur et ne sont pas indiqués lors de l'exécution. Je suis à peu près sûr que vous pouvez compiler sur tout ce que vous voulez.
Logan
2
@DeanKelly - Vous pourriez faire ça comme ça: ça a l'air @property (nonatomic, strong) NSArray<id<SomeProtocol>>* protocolObjects; un peu maladroit, mais ça fait l'affaire!
Logan le
1
@Logan, il n'y a pas que l'ensemble des scripts, qui empêchent la construction en cas d'avertissement détecté. Xcode a un mécanisme parfait nommé "Configuration". Découvrez boredzo.org/blog/archives/2009-11-07/warnings
adnako
53

C'est une question relativement courante pour les personnes qui passent de langages fortement typés (comme C ++ ou Java) à des langages plus faiblement ou dynamiquement typés comme Python, Ruby ou Objective-C. En Objective-C, la plupart des objets héritent de NSObject(type id) (le reste hérite d'une autre classe racine telle que NSProxyet peut également être de type id), et tout message peut être envoyé à n'importe quel objet. Bien sûr, l'envoi d'un message à une instance qu'elle ne reconnaît pas peut provoquer une erreur d'exécution (et entraînera également un avertissement du compilateuravec les indicateurs -W appropriés). Tant qu'une instance répond au message que vous envoyez, vous ne vous souciez peut-être pas de la classe à laquelle elle appartient. Ceci est souvent appelé "frappe de canard" parce que "s'il charlatine comme un canard [c'est-à-dire qu'il répond à un sélecteur], c'est un canard [c'est-à-dire qu'il peut gérer le message; peu importe de quelle classe il s'agit]".

Vous pouvez tester si une instance répond à un sélecteur au moment de l'exécution avec la -(BOOL)respondsToSelector:(SEL)selectorméthode. En supposant que vous souhaitiez appeler une méthode sur chaque instance d'un tableau mais que vous ne soyez pas sûr que toutes les instances puissent gérer le message (vous ne pouvez donc pas simplement utiliser NSArray's -[NSArray makeObjectsPerformSelector:], quelque chose comme ça fonctionnerait:

for(id o in myArray) {
  if([o respondsToSelector:@selector(myMethod)]) {
    [o myMethod];
  }
}

Si vous contrôlez le code source des instances qui implémentent la ou les méthodes que vous souhaitez appeler, l'approche la plus courante consiste à définir un @protocolcontenant ces méthodes et à déclarer que les classes en question implémentent ce protocole dans leur déclaration. Dans cet usage, a @protocolest analogue à une interface Java ou à une classe de base abstraite C ++. Vous pouvez ensuite tester la conformité à l'ensemble du protocole plutôt que la réponse à chaque méthode. Dans l'exemple précédent, cela ne ferait pas beaucoup de différence, mais si vous appeliez plusieurs méthodes, cela pourrait simplifier les choses. L'exemple serait alors:

for(id o in myArray) {
  if([o conformsToProtocol:@protocol(MyProtocol)]) {
    [o myMethod];
  }
}

en supposant MyProtocoldéclare myMethod. Cette seconde approche est privilégiée car elle clarifie davantage l'intention du code que la première.

Souvent, l'une de ces approches vous évite de vous soucier de savoir si tous les objets d'un tableau sont d'un type donné. Si vous vous en souciez toujours, l'approche standard du langage dynamique est le test unitaire, le test unitaire, le test unitaire. Étant donné qu'une régression dans cette exigence produira une erreur d'exécution (probablement irrécupérable) (pas au moment de la compilation), vous devez disposer d'une couverture de test pour vérifier le comportement afin de ne pas libérer un crasher dans la nature. Dans ce cas, effectuez une opération qui modifie le tableau, puis vérifiez que toutes les instances du tableau appartiennent à une classe donnée. Avec une couverture de test appropriée, vous n'avez même pas besoin de la surcharge d'exécution supplémentaire de la vérification de l'identité de l'instance. Vous avez une bonne couverture des tests unitaires, n'est-ce pas?

Barry Wark
la source
35
Les tests unitaires ne remplacent pas un système de type décent.
tba
8
Ouais, qui a besoin de l'outillage que les tableaux typés pourraient se permettre. Je suis sûr que @BarryWark (et toute autre personne qui a touché à toute base de code dont il a besoin pour utiliser, lire, comprendre et prendre en charge) a une couverture de code à 100%. Cependant, je parie que vous n'utilisez pas de raw ids sauf lorsque cela est nécessaire, pas plus que les codeurs Java ne passent de Objects. Pourquoi pas? Vous n'en avez pas besoin si vous avez des tests unitaires? Parce qu'il est là et rend votre code plus maintenable, comme le feraient les tableaux typés. On dirait que des personnes investies dans la plateforme ne souhaitent pas concéder de point, et donc inventent des raisons pour lesquelles cette omission est en fait un avantage.
funkybro
"Duck typing" ?? c'est hilarant! Je n'ai jamais entendu ça.
John Henckel
11

Vous pouvez créer NSMutableArrayune sous-classe pour appliquer la sécurité de type.

NSMutableArrayest un cluster de classes , donc le sous- classement n'est pas trivial. J'ai fini par hériter NSArrayet transféré des appels à un tableau à l'intérieur de cette classe. Le résultat est une classe appelée ConcreteMutableArrayqui est facile à sous- classer . Voici ce que j'ai trouvé:

Mise à jour: consultez ce billet de blog de Mike Ash sur le sous-classement d'un cluster de classes.

Incluez ces fichiers dans votre projet, puis générez tous les types que vous souhaitez en utilisant des macros:

MyArrayTypes.h

CUSTOM_ARRAY_INTERFACE(NSString)
CUSTOM_ARRAY_INTERFACE(User)

MyArrayTypes.m

CUSTOM_ARRAY_IMPLEMENTATION(NSString)
CUSTOM_ARRAY_IMPLEMENTATION(User)

Usage:

NSStringArray* strings = [NSStringArray array];
[strings add:@"Hello"];
NSString* str = [strings get:0];

[strings add:[User new]];  //compiler error
User* user = [strings get:0];  //compiler error

d'autres pensées

  • Il hérite de NSArraypour prendre en charge la sérialisation / désérialisation
  • Selon votre goût, vous voudrez peut-être remplacer / masquer des méthodes génériques telles que

    - (void) addObject:(id)anObject

bendytree
la source
Bien mais pour l'instant il manque un typage fort en remplaçant certaines méthodes. Actuellement, ce n'est qu'un typage faible.
Cœur
7

Jetez un œil à https://github.com/tomersh/Objective-C-Generics , une implémentation générique au moment de la compilation (implémentée par un préprocesseur) pour Objective-C. Ce billet de blog a une belle vue d'ensemble. En gros, vous obtenez une vérification à la compilation (avertissements ou erreurs), mais aucune pénalité d'exécution pour les génériques.

Barry Wark
la source
1
Je l'ai essayé, très bonne idée, mais malheureusement bogué et cela ne vérifie pas les éléments ajoutés.
Binarian
4

Ce projet Github implémente exactement cette fonctionnalité.

Vous pouvez ensuite utiliser les <>crochets, comme vous le feriez en C #.

D'après leurs exemples:

NSArray<MyClass>* classArray = [NSArray array];
NSString *name = [classArray lastObject].name; // No cast needed
IluTov
la source
0

Un moyen possible pourrait être de sous-classer NSArray mais Apple recommande de ne pas le faire. Il est plus simple de réfléchir à deux fois au besoin réel d'un NSArray typé.

mouviciel
la source
1
La vérification du type statique au moment de la compilation permet de gagner du temps, l'édition est encore meilleure. Particulièrement utile lorsque vous écrivez lib pour une utilisation à long terme.
pinxue
0

J'ai créé une sous-classe NSArray qui utilise un objet NSArray comme support ivar pour éviter les problèmes avec la nature de cluster de classe de NSArray. Il faut des blocs pour accepter ou refuser l'ajout d'un objet.

seulement permettre NSString, vous pouvez définir un AddBlockcomme

^BOOL(id element) {
    return [element isKindOfClass:[NSString class]];
}

Vous pouvez définir a FailBlockpour décider quoi faire, si un élément a échoué au test - échouer correctement pour le filtrage, l'ajouter à un autre tableau, ou - c'est par défaut - lever une exception.

VSBlockTestedObjectArray.h

#import <Foundation/Foundation.h>
typedef BOOL(^AddBlock)(id element); 
typedef void(^FailBlock)(id element); 

@interface VSBlockTestedObjectArray : NSMutableArray

@property (nonatomic, copy, readonly) AddBlock testBlock;
@property (nonatomic, copy, readonly) FailBlock failBlock;

-(id)initWithTestBlock:(AddBlock)testBlock FailBlock:(FailBlock)failBlock Capacity:(NSUInteger)capacity;
-(id)initWithTestBlock:(AddBlock)testBlock FailBlock:(FailBlock)failBlock;
-(id)initWithTestBlock:(AddBlock)testBlock;    
@end

VSBlockTestedObjectArray.m

#import "VSBlockTestedObjectArray.h"

@interface VSBlockTestedObjectArray ()
@property (nonatomic, retain) NSMutableArray *realArray;
-(void)errorWhileInitializing:(SEL)selector;
@end

@implementation VSBlockTestedObjectArray
@synthesize testBlock = _testBlock;
@synthesize failBlock = _failBlock;
@synthesize realArray = _realArray;


-(id)initWithCapacity:(NSUInteger)capacity
{
    if (self = [super init]) {
        _realArray = [[NSMutableArray alloc] initWithCapacity:capacity];
    }

    return self;
}

-(id)initWithTestBlock:(AddBlock)testBlock 
             FailBlock:(FailBlock)failBlock 
              Capacity:(NSUInteger)capacity
{
    self = [self initWithCapacity:capacity];
    if (self) {
        _testBlock = [testBlock copy];
        _failBlock = [failBlock copy];
    }

    return self;
}

-(id)initWithTestBlock:(AddBlock)testBlock FailBlock:(FailBlock)failBlock
{
    return [self initWithTestBlock:testBlock FailBlock:failBlock Capacity:0];
}

-(id)initWithTestBlock:(AddBlock)testBlock
{
    return [self initWithTestBlock:testBlock FailBlock:^(id element) {
        [NSException raise:@"NotSupportedElement" format:@"%@ faild the test and can't be add to this VSBlockTestedObjectArray", element];
    } Capacity:0];
}


- (void)dealloc {
    [_failBlock release];
    [_testBlock release];
    self.realArray = nil;
    [super dealloc];
}


- (void) insertObject:(id)anObject atIndex:(NSUInteger)index
{
    if(self.testBlock(anObject))
        [self.realArray insertObject:anObject atIndex:index];
    else
        self.failBlock(anObject);
}

- (void) removeObjectAtIndex:(NSUInteger)index
{
    [self.realArray removeObjectAtIndex:index];
}

-(NSUInteger)count
{
    return [self.realArray count];
}

- (id) objectAtIndex:(NSUInteger)index
{
    return [self.realArray objectAtIndex:index];
}



-(void)errorWhileInitializing:(SEL)selector
{
    [NSException raise:@"NotSupportedInstantiation" format:@"not supported %@", NSStringFromSelector(selector)];
}
- (id)initWithArray:(NSArray *)anArray { [self errorWhileInitializing:_cmd]; return nil;}
- (id)initWithArray:(NSArray *)array copyItems:(BOOL)flag { [self errorWhileInitializing:_cmd]; return nil;}
- (id)initWithContentsOfFile:(NSString *)aPath{ [self errorWhileInitializing:_cmd]; return nil;}
- (id)initWithContentsOfURL:(NSURL *)aURL{ [self errorWhileInitializing:_cmd]; return nil;}
- (id)initWithObjects:(id)firstObj, ... { [self errorWhileInitializing:_cmd]; return nil;}
- (id)initWithObjects:(const id *)objects count:(NSUInteger)count { [self errorWhileInitializing:_cmd]; return nil;}

@end

Utilisez-le comme:

VSBlockTestedObjectArray *stringArray = [[VSBlockTestedObjectArray alloc] initWithTestBlock:^BOOL(id element) {
    return [element isKindOfClass:[NSString class]];
} FailBlock:^(id element) {
    NSLog(@"%@ can't be added, didn't pass the test. It is not an object of class NSString", element);
}];


VSBlockTestedObjectArray *numberArray = [[VSBlockTestedObjectArray alloc] initWithTestBlock:^BOOL(id element) {
    return [element isKindOfClass:[NSNumber class]];
} FailBlock:^(id element) {
    NSLog(@"%@ can't be added, didn't pass the test. It is not an object of class NSNumber", element);
}];


[stringArray addObject:@"test"];
[stringArray addObject:@"test1"];
[stringArray addObject:[NSNumber numberWithInt:9]];
[stringArray addObject:@"test2"];
[stringArray addObject:@"test3"];


[numberArray addObject:@"test"];
[numberArray addObject:@"test1"];
[numberArray addObject:[NSNumber numberWithInt:9]];
[numberArray addObject:@"test2"];
[numberArray addObject:@"test3"];


NSLog(@"%@", stringArray);
NSLog(@"%@", numberArray);

Ceci n'est qu'un exemple de code et n'a jamais été utilisé dans une application du monde réel. pour ce faire, il a probablement besoin de la méthode NSArray plus implémentée.

vikingosegundo
la source
0

Si vous mélangez c ++ et objective-c (c'est-à-dire en utilisant le type de fichier mm), vous pouvez imposer le typage en utilisant paire ou tuple. Par exemple, dans la méthode suivante, vous pouvez créer un objet C ++ de type std :: pair, le convertir en un objet de type wrapper OC (wrapper de std :: pair que vous devez définir), puis le transmettre à certains autre méthode OC, dans laquelle vous devez reconvertir l'objet OC en objet C ++ pour pouvoir l'utiliser. La méthode OC n'accepte que le type wrapper OC, garantissant ainsi la sécurité du type. Vous pouvez même utiliser un tuple, un modèle variadique, une liste de types pour tirer parti des fonctionnalités C ++ plus avancées afin de faciliter la sécurité des types.

- (void) tableView:(UITableView*) tableView didSelectRowAtIndexPath:(NSIndexPath*) indexPath
{
 std::pair<UITableView*, NSIndexPath*> tableRow(tableView, indexPath);  
 ObjCTableRowWrapper* oCTableRow = [[[ObjCTableRowWrapper alloc] initWithTableRow:tableRow] autorelease];
 [self performSelector:@selector(selectRow:) withObject:oCTableRow];
}
Colin
la source
0

mes deux cents pour être un peu "plus propre":

utilisez des typedefs:

typedef NSArray<NSString *> StringArray;

dans le code, nous pouvons faire:

StringArray * titles = @[@"ID",@"Name", @"TYPE", @"DATE"];
ingconti
la source