Gibt es eine Möglichkeit, die Eingabe in NSArray, NSMutableArray usw. zu erzwingen?


Antworten:


35

Sie könnten eine Kategorie mit einer -addSomeClass:Methode erstellen, um die statische Typprüfung zur Kompilierungszeit zu ermöglichen (der Compiler könnte Sie also informieren, wenn Sie versuchen, ein Objekt hinzuzufügen, von dem er weiß, dass es sich bei dieser Methode um eine andere Klasse handelt), aber es gibt keine echte Möglichkeit, dies zu erzwingen Ein Array enthält nur Objekte einer bestimmten Klasse.

Im Allgemeinen scheint es in Objective-C keine Notwendigkeit für eine solche Einschränkung zu geben. Ich glaube nicht, dass ich jemals einen erfahrenen Cocoa-Programmierer gehört habe, der sich diese Funktion wünscht. Die einzigen Leute, die scheinen, sind Programmierer aus anderen Sprachen, die immer noch in diesen Sprachen denken. Wenn Sie nur Objekte einer bestimmten Klasse in einem Array haben möchten, kleben Sie nur Objekte dieser Klasse dort ein. Wenn Sie testen möchten, ob sich Ihr Code ordnungsgemäß verhält, testen Sie ihn.


136
Ich denke, dass 'erfahrene Cocoa-Programmierer' einfach nicht wissen, was sie vermissen - die Erfahrung mit Java zeigt, dass Typvariablen das Codeverständnis verbessern und mehr Refactorings ermöglichen.
Tgdavies

11
Nun, Javas Generics-Unterstützung ist an sich schon stark kaputt, weil sie nicht von Anfang an eingesetzt wurde ...
dertoni

28
Ich muss @tgdavies zustimmen. Ich vermisse die Intellisense- und Refactoring-Funktionen, die ich mit C # hatte. Wenn ich dynamisch tippen möchte, kann ich es in C # 4.0 erhalten. Wenn ich stark typisieren will, kann ich das auch haben. Ich habe festgestellt, dass es für beide Dinge eine Zeit und einen Ort gibt.
Steve

18
@charkrit Was macht Objective-C "nicht notwendig"? Hielten Sie es für notwendig, als Sie C # verwendeten? Ich höre viele Leute sagen, dass Sie es in Objective-C nicht brauchen, aber ich denke, dass dieselben Leute denken, dass Sie es in keiner Sprache brauchen, was es zu einer Frage der Präferenz / des Stils macht, nicht der Notwendigkeit.
Bacar

17
Geht es nicht darum, dass Ihr Compiler Ihnen tatsächlich hilft, Probleme zu finden? Sicher können Sie sagen: "Wenn Sie nur Objekte einer bestimmten Klasse in einem Array haben möchten, stecken Sie nur Objekte dieser Klasse dort hinein." Aber wenn Tests der einzige Weg sind, dies durchzusetzen, sind Sie im Nachteil. Je weiter Sie vom Schreiben des Codes entfernt sind, desto teurer wird dieses Problem.
GreenKiwi

145

Niemand hat das hier bisher aufgestellt, also werde ich es tun!

Dies wird jetzt offiziell in Objective-C unterstützt. Ab Xcode 7 können Sie die folgende Syntax verwenden:

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

Hinweis

Es ist wichtig zu beachten, dass dies nur Compiler-Warnungen sind und Sie technisch immer noch jedes Objekt in Ihr Array einfügen können. Es sind Skripte verfügbar, die alle Warnungen in Fehler umwandeln, die das Erstellen verhindern würden.


Ich bin hier faul, aber warum ist dies nur in XCode 7 verfügbar? Wir können den nonnullin XCode 6 verwenden und soweit ich mich erinnere, wurden sie gleichzeitig eingeführt. Hängt die Verwendung solcher Konzepte auch von der XCode-Version oder von der iOS-Version ab?
Guven

@ Guven - Nullability kam in 6, Sie sind richtig, aber ObjC Generika wurden erst mit Xcode 7 eingeführt.
Logan

Ich bin mir ziemlich sicher, dass es nur von der Xcode-Version abhängt. Die Generika sind nur Compiler-Warnungen und werden zur Laufzeit nicht angezeigt. Ich bin mir ziemlich sicher, dass Sie zu jedem gewünschten Os kompilieren können.
Logan

2
@ DeanKelly - Das könnte man so machen: @property (nonatomic, strong) NSArray<id<SomeProtocol>>* protocolObjects; Sieht etwas klobig aus, macht aber den Trick!
Logan

1
@Logan, es gibt nicht nur eine Reihe von Skripten, die das Erstellen verhindern, wenn eine Warnung erkannt wird. Xcode hat einen perfekten Mechanismus namens "Konfiguration". Überprüfen Sie dies aus gelangzozo.org/blog/archives/2009-11-07/warnings
adnako

53

Dies ist eine relativ häufige Frage für Personen, die von stark typisierten Sprachen (wie C ++ oder Java) zu schwach oder dynamisch typisierten Sprachen wie Python, Ruby oder Objective-C wechseln. In Objective-C erben die meisten Objekte von NSObject(Typ id) (der Rest erbt von einer anderen Stammklasse wie NSProxyund kann auch Typ sein id), und jede Nachricht kann an jedes Objekt gesendet werden. Das Senden einer Nachricht an eine Instanz, die nicht erkannt wird, kann natürlich einen Laufzeitfehler verursachen (und auch eine Compiler- Warnung auslösenmit entsprechenden -W-Flags). Solange eine Instanz auf die von Ihnen gesendete Nachricht antwortet, ist es Ihnen möglicherweise egal, zu welcher Klasse sie gehört. Dies wird oft als "Ententypisierung" bezeichnet, weil "wenn es wie eine Ente quakt [dh auf einen Selektor reagiert], es eine Ente ist [dh es kann mit der Nachricht umgehen; wen interessiert es, welche Klasse es ist]".

Sie können testen, ob eine Instanz zur Laufzeit mit dem auf einen Selektor reagiert -(BOOL)respondsToSelector:(SEL)selector Methode . Angenommen , Sie sind auf jeden Fall eine Methode aufrufen wollen in einem Array sind aber nicht sicher , dass alle Instanzen die Nachricht verarbeiten kann (so dass Sie nicht nur verwenden können , NSArrayist -[NSArray makeObjectsPerformSelector:], so etwas wie dies funktionieren würde:

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

Wenn Sie den Quellcode für die Instanzen steuern, die die Methode (n) implementieren, die Sie aufrufen möchten, besteht der häufigere Ansatz darin, eine zu definieren @protocol, die diese Methoden enthält, und zu deklarieren, dass die betreffenden Klassen dieses Protokoll in ihrer Deklaration implementieren. In dieser Verwendung ist a @protocolanalog zu einer Java-Schnittstelle oder einer abstrakten C ++ - Basisklasse. Sie können dann die Konformität mit dem gesamten Protokoll testen, anstatt auf jede Methode zu antworten. Im vorherigen Beispiel würde dies keinen großen Unterschied machen, aber wenn Sie mehrere Methoden aufrufen, könnte dies die Dinge vereinfachen. Das Beispiel wäre dann:

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

unter der Annahme MyProtocolerklärt myMethod. Dieser zweite Ansatz wird bevorzugt, weil er die Absicht des Codes mehr verdeutlicht als der erste.

Oft befreit Sie einer dieser Ansätze davon, sich darum zu kümmern, ob alle Objekte in einem Array von einem bestimmten Typ sind. Wenn Sie sich immer noch darum kümmern, besteht der Standardansatz für dynamische Sprache in Unit-Test, Unit-Test, Unit-Test. Da eine Regression dieser Anforderung zu einem (wahrscheinlich nicht behebbaren) Laufzeitfehler (nicht zur Kompilierungszeit) führt, müssen Sie über eine Testabdeckung verfügen, um das Verhalten zu überprüfen, damit Sie keinen Crasher in die Wildnis entlassen. Führen Sie in diesem Fall eine Operation aus, die das Array ändert, und überprüfen Sie dann, ob alle Instanzen im Array zu einer bestimmten Klasse gehören. Bei ordnungsgemäßer Testabdeckung benötigen Sie nicht einmal den zusätzlichen Laufzeitaufwand für die Überprüfung der Instanzidentität. Sie haben eine gute Unit-Test-Abdeckung, nicht wahr?


35
Unit-Tests sind kein Ersatz für ein anständiges System.
bis

8
Ja, wer braucht das Werkzeug, das sich typisierte Arrays leisten würden? Ich bin sicher, dass @BarryWark (und jeder andere, der eine Codebasis berührt hat, die er verwenden, lesen, verstehen und unterstützen muss) eine 100% ige Codeabdeckung aufweist. Ich wette jedoch, dass Sie keine rohen ids verwenden, außer wenn dies erforderlich ist, und nicht mehr als Java-Codierer um Objects herumgeben . Warum nicht? Benötigen Sie es nicht, wenn Sie Unit-Tests haben? Weil es da ist und Ihren Code wartbarer macht, wie es typisierte Arrays tun würden. Klingt so, als ob Leute, die in die Plattform investiert haben, keinen Punkt einräumen wollen und deshalb Gründe erfinden, warum diese Auslassung tatsächlich ein Vorteil ist.
Funkybro

"Ente tippen" ?? das ist komisch! habe das noch nie gehört.
John Henckel

11

Sie können eine Unterklasse erstellen NSMutableArray, um die Typensicherheit zu erzwingen.

NSMutableArrayist ein Klassencluster , daher ist die Unterklasse nicht trivial. Am Ende habe ich Aufrufe geerbt NSArrayund an ein Array innerhalb dieser Klasse weitergeleitet. Das Ergebnis ist eine Klasse aufgerufen , ConcreteMutableArraydie ist einfach zu Unterklasse. Folgendes habe ich mir ausgedacht:

Update: Lesen Sie diesen Blog-Beitrag von Mike Ash über die Unterklasse eines Klassenclusters.

Fügen Sie diese Dateien in Ihr Projekt ein und generieren Sie dann mithilfe von Makros alle gewünschten Typen:

MyArrayTypes.h

CUSTOM_ARRAY_INTERFACE(NSString)
CUSTOM_ARRAY_INTERFACE(User)

MyArrayTypes.m

CUSTOM_ARRAY_IMPLEMENTATION(NSString)
CUSTOM_ARRAY_IMPLEMENTATION(User)

Verwendung:

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

andere Gedanken

  • Es erbt von NSArray , um die Serialisierung / Deserialisierung zu unterstützen
  • Abhängig von Ihrem Geschmack möchten Sie möglicherweise generische Methoden wie überschreiben / ausblenden

    - (void) addObject:(id)anObject


Schön, aber im Moment fehlt es an starker Schreibweise, indem einige Methoden überschrieben werden. Derzeit ist es nur schwach tippen.
Cœur

7

Schauen Sie sich https://github.com/tomersh/Objective-C-Generics an , eine Generikimplementierung zur Kompilierungszeit (vom Präprozessor implementiert) für Objective-C. Dieser Blog-Beitrag hat einen schönen Überblick. Grundsätzlich erhalten Sie eine Überprüfung der Kompilierungszeit (Warnungen oder Fehler), aber keine Laufzeitstrafe für Generika.


1
Ich habe es ausprobiert, sehr gute Idee, aber leider fehlerhaft und es überprüft nicht die hinzugefügten Elemente.
Binarian

4

Dieses Github-Projekt implementiert genau diese Funktionalität.

Sie können dann die verwenden <> Klammern wie in C # verwenden.

Aus ihren Beispielen:

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

0

Ein möglicher Weg könnte die Unterklasse von NSArray sein, aber Apple empfiehlt, dies nicht zu tun. Es ist einfacher, zweimal über den tatsächlichen Bedarf an einem typisierten NSArray nachzudenken.


1
Es spart Zeit, die statische Typprüfung beim Kompilieren durchzuführen. Die Bearbeitung ist sogar noch besser. Besonders hilfreich, wenn Sie lib für den Langzeitgebrauch schreiben.
Pinxue

0

Ich habe eine NSArray-Unterklasse erstellt, die ein NSArray-Objekt als Hintergrund-IVAR verwendet, um Probleme mit der Klassencluster-Natur von NSArray zu vermeiden. Es dauert Blöcke, um das Hinzufügen eines Objekts zu akzeptieren oder abzulehnen.

Um nur NSString-Objekte zuzulassen, können Sie ein AddBlockals definieren

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

Sie können ein definieren FailBlock, um zu entscheiden, was zu tun ist, wenn ein Element den Test nicht bestanden hat - das Filtern ordnungsgemäß fehlschlägt, es einem anderen Array hinzufügt oder - dies ist die Standardeinstellung - eine Ausnahme auslöst.

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

Verwenden Sie es wie:

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);

Dies ist nur ein Beispielcode und wurde in realen Anwendungen nie verwendet. Dazu muss wahrscheinlich mehr NSArray-Methode implementiert werden.


0

Wenn Sie c ++ und objectiv-c mischen (dh den Dateityp mm verwenden), können Sie die Eingabe mithilfe von Paaren oder Tupeln erzwingen. In der folgenden Methode können Sie beispielsweise ein C ++ - Objekt vom Typ std :: pair erstellen, es in ein Objekt vom Typ OC-Wrapper (Wrapper von std :: pair, den Sie definieren müssen) konvertieren und dann an einige übergeben andere OC-Methode, bei der Sie das OC-Objekt zurück in ein C ++ - Objekt konvertieren müssen, um es verwenden zu können. Die OC-Methode akzeptiert nur den OC-Wrapper-Typ, wodurch die Typensicherheit gewährleistet wird. Sie können sogar Tupel, variable Vorlagen und Typelisten verwenden, um erweiterte C ++ - Funktionen zu nutzen und die Typensicherheit zu verbessern.

- (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];
}

0

meine zwei Cent, um ein bisschen "sauberer" zu sein:

benutze typedefs:

typedef NSArray<NSString *> StringArray;

im Code können wir tun:

StringArray * titles = @[@"ID",@"Name", @"TYPE", @"DATE"];
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.