Wie schreibe ich 2018 auf Objective-C? Teil 1

Die meisten iOS-Projekte wechseln teilweise oder vollständig zu Swift. Swift ist eine großartige Sprache und damit liegt die Zukunft der iOS-Entwicklung. Die Sprache ist jedoch untrennbar mit dem Toolkit verbunden, und das Swift-Toolkit weist Nachteile auf.


Der Swift-Compiler weist immer noch Fehler auf, die zum Absturz führen oder falschen Code generieren. Swift hat keinen stabilen ABI. Und vor allem laufen Swift-Projekte zu lange.


In dieser Hinsicht können bestehende Projekte rentabler sein, um die Entwicklung von Ziel-C fortzusetzen. Und Objective-C ist nicht mehr das, was es früher war!


In dieser Artikelserie zeigen wir Ihnen die nützlichen Funktionen und Verbesserungen von Objective-C, mit denen das Schreiben von Code viel angenehmer wird. Jeder, der in Objective-C schreibt, findet etwas Interessantes für sich.



let und var


Objective-C muss keine Variablentypen mehr explizit angeben: In Xcode 8 wurde die __auto_type , und bevor die Typinferenz von Xcode 8 in Objective-C ++ verfügbar war (unter Verwendung des Schlüsselworts auto mit dem Aufkommen von C ++ 0X).


Fügen let Makros let und var :


 #define let __auto_type const #define var __auto_type 

 //  NSArray<NSString *> *const items = [string componentsSeparatedByString:@","]; void(^const completion)(NSData * _Nullable, NSURLResponse * _Nullable, NSError * _Nullable) = ^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { // ... }; //  let items = [string componentsSeparatedByString:@","]; let completion = ^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { // ... }; 

Wenn Sie früher const nach einem Zeiger auf eine Objective-C-Klasse geschrieben haben, war dies ein unzulässiger Luxus, aber jetzt wurde die implizite Deklaration von const (via let ) als selbstverständlich angesehen. Der Unterschied macht sich insbesondere beim Speichern eines Blocks in einer Variablen bemerkbar.


Für uns selbst haben wir eine Regel entwickelt, mit der let und var alle Variablen deklarieren können. Auch wenn eine Variable auf nil initialisiert wird:


 - (nullable JMSomeResult *)doSomething { var result = (JMSomeResult *)nil; if (...) { result = ...; } return result; } 

Die einzige Ausnahme ist, wenn Sie sicherstellen müssen, dass einer Variablen in jedem Codezweig ein Wert zugewiesen wird:


 NSString *value; if (...) { if (...) { value = ...; } else { value = ...; } } else { value = ...; } 

Nur so erhalten wir eine Compiler-Warnung, wenn wir vergessen, einem der Zweige einen Wert zuzuweisen.


Und schließlich: -Wno-auto-var-id let und var für Variablen vom Typ id , müssen Sie die auto-var-id Warnung deaktivieren (fügen Sie -Wno-auto-var-id zu den "Anderen -Wno-auto-var-id " in den Projekteinstellungen hinzu).


Rückgabewert vom Typ Auto Block


Nur wenige Leute wissen, dass der Compiler auf den Typ des Rückgabewerts eines Blocks schließen kann:


 let block = ^{ return @"abc"; }; // `block`   `NSString *(^const)(void)` 

Es ist sehr bequem. Besonders wenn Sie reaktiven Code mit ReactiveObjC schreiben. Es gibt jedoch eine Reihe von Einschränkungen, unter denen Sie den Typ des Rückgabewerts explizit angeben müssen.


  1. Wenn ein Block mehrere return , die Werte unterschiedlichen Typs zurückgeben.


     let block1 = ^NSUInteger(NSUInteger value){ if (value > 0) { return value; } else { // `NSNotFound`   `NSInteger` return NSNotFound; } }; let block2 = ^JMSomeBaseClass *(BOOL flag) { if (flag) { return [[JMSomeBaseClass alloc] init]; } else { // `JMSomeDerivedClass`   `JMSomeBaseClass` return [[JMSomeDerivedClass alloc] init]; } }; 

  2. Wenn der Block eine return , die nil zurückgibt.


     let block1 = ^NSString * _Nullable(){ return nil; }; let block2 = ^NSString * _Nullable(BOOL flag) { if (flag) { return @"abc"; } else { return nil; } }; 

  3. Wenn der Block BOOL .


     let predicate = ^BOOL(NSInteger lhs, NSInteger rhs){ return lhs > rhs; }; 


Ausdrücke mit einem Vergleichsoperator in C (und damit in Objective-C) sind vom Typ int . Daher ist es besser, die Regel so festzulegen, dass der Rückgabetyp BOOL immer explizit angegeben BOOL .


Generika und for...in


In Xcode 7 wurden in Objective-C Generika (oder besser gesagt Lightweight-Generika) angezeigt. Wir hoffen, dass Sie sie bereits verwenden. Wenn nicht, können Sie die WWDC-Sitzung ansehen oder hier oder hier lesen.


Für uns selbst haben wir eine Regel entwickelt, mit der generische Parameter immer angegeben werden, auch wenn es sich um id ( NSArray<id> * ). Somit ist es einfach, zwischen Legacy-Code zu unterscheiden, in dem noch keine generischen Parameter angegeben sind.


Mit den Makros let und var erwarten wir, dass wir sie in einer for...in Schleife verwenden können:


 let items = (NSArray<NSString *> *)@[@"a", @"b", @"c"]; for (let item in items) { NSLog(@"%@", item); } 

Ein solcher Code wird jedoch nicht kompiliert. Höchstwahrscheinlich wurde __auto_type in for...in nicht unterstützt, da for...in nur mit Sammlungen funktioniert, die das NSFastEnumeration Protokoll implementieren. Und für Protokolle in Objective-C gibt es keine Unterstützung für Generika.


Versuchen Sie, Ihr foreach Makro zu erstellen, um diesen Nachteil zu beheben. Das erste, was mir in den Sinn kommt: Alle Sammlungen in Foundation haben eine objectEnumerator Eigenschaft, und das Makro könnte folgendermaßen aussehen:


 #define foreach(object_, collection_) \ for (typeof([(collection_).objectEnumerator nextObject]) object_ in (collection_)) 

Bei NSDictionary und NSMapTable Protokollmethode NSMapTable die Schlüssel und nicht die Werte (Sie müssten keyEnumerator und nicht objectEnumerator ).


Wir müssen eine neue Eigenschaft deklarieren, die nur verwendet wird, um den Typ im Typ des Ausdrucks typeof :


 @interface NSArray<__covariant ObjectType> (ForeachSupport) @property (nonatomic, strong, readonly) ObjectType jm_enumeratedType; @end @interface NSDictionary<__covariant KeyType, __covariant ObjectType> (ForeachSupport) @property (nonatomic, strong, readonly) KeyType jm_enumeratedType; @end #define foreach(object_, collection_) \ for (typeof((collection_).jm_enumeratedType) object_ in (collection_)) 

Jetzt sieht unser Code viel besser aus:


 //  for (MyItemClass *item in items) { NSLog(@"%@", item); } //  foreach (item, items) { NSLog(@"%@", item); } 

Snippet für Xcode
 foreach (<#object#>, <#collection#>) { <#statements#> } 

Generika und copy / mutableCopy


Ein weiterer Ort, an dem die Eingabe in Objective-C nicht verfügbar ist, sind die -mutableCopy und -mutableCopy (sowie die Methoden -copyWithZone: und -mutableCopyWithZone: wir jedoch nicht direkt aufrufen).


Um die Notwendigkeit expliziter Casts zu vermeiden, können Sie Methoden mit dem Rückgabetyp neu deklarieren. Für NSArray wären NSArray Erklärungen beispielsweise:


 @interface NSArray<__covariant ObjectType> (TypedCopying) - (NSArray<ObjectType> *)copy; - (NSMutableArray<ObjectType> *)mutableCopy; @end 

 let items = [NSMutableArray<NSString *> array]; // ... //  let itemsCopy = (NSArray<NSString *> *)[items copy]; //  let itemsCopy = [items copy]; 

warn_unused_result


Da wir die -mutableCopy -copy und -mutableCopy erneut deklariert haben, wäre es schön zu garantieren, dass das Ergebnis des Aufrufs dieser Methoden verwendet wird. Zu diesem warn_unused_result verfügt Clang über das Attribut warn_unused_result .


 #define JM_WARN_UNUSED_RESULT __attribute__((warn_unused_result)) 

 @interface NSArray<__covariant ObjectType> (TypedCopying) - (NSArray<ObjectType> *)copy JM_WARN_UNUSED_RESULT; - (NSMutableArray<ObjectType> *)mutableCopy JM_WARN_UNUSED_RESULT; @end 

Für den folgenden Code generiert der Compiler eine Warnung:


 let items = @[@"a", @"b", @"c"]; [items mutableCopy]; // Warning: Ignoring return value of function declared with 'warn_unused_result' attribute. 

overloadable


Nur wenige wissen, dass Sie mit Clang Funktionen in der C-Sprache (und damit in Objective-C) neu definieren können. Mit dem overloadable Attribut können Sie Funktionen mit demselben Namen, jedoch mit unterschiedlichen Argumenttypen oder mit unterschiedlichen Nummern erstellen.


Überschreibbare Funktionen können sich nicht nur in der Art des Rückgabewerts unterscheiden.


 #define JM_OVERLOADABLE __attribute__((overloadable)) 

 JM_OVERLOADABLE float JMCompare(float lhs, float rhs); JM_OVERLOADABLE float JMCompare(float lhs, float rhs, float accuracy); JM_OVERLOADABLE double JMCompare(double lhs, double rhs); JM_OVERLOADABLE double JMCompare(double lhs, double rhs, double accuracy); 

Boxed Ausdrücke


Bereits 2012 stellte Apple auf einer WWDC 413-Sitzung Literale für NSNumber , NSArray und NSDictionary sowie Boxed-Ausdrücke vor. Weitere Informationen zu Literalen und Boxed-Ausdrücken finden Sie in der Clang-Dokumentation .


 //  @YES // [NSNumber numberWithBool:YES] @NO // [NSNumber numberWithBool:NO] @123 // [NSNumber numberWithInt:123] @3.14 // [NSNumber numberWithDouble:3.14] @[obj1, obj2] // [NSArray arrayWithObjects:obj1, obj2, nil] @{key1: obj1, key2: obj2} // [NSDictionary dictionaryWithObjectsAndKeys:obj1, key1, obj2, key2, nil] // Boxed expressions @(boolVariable) // [NSNumber numberWithBool:boolVariable] @(intVariable) // [NSNumber numberWithInt:intVariable)] 

Mit Literalen und Boxed-Ausdrücken können Sie leicht ein Objekt erhalten, das eine Zahl oder einen Booleschen Wert darstellt. Um jedoch ein Objekt zu erhalten, das die Struktur umschließt, müssen Sie folgenden Code schreiben:


 //  `NSDirectionalEdgeInsets`  `NSValue` let insets = (NSDirectionalEdgeInsets){ ... }; let value = [[NSValue alloc] initWithBytes:&insets objCType:@encode(typeof(insets))]; // ... //  `NSDirectionalEdgeInsets`  `NSValue` var insets = (NSDirectionalEdgeInsets){}; [value getValue:&insets]; 

Hilfsmethoden und -eigenschaften sind für einige Klassen definiert (wie die +[NSValue valueWithCGPoint:] -Methode und die CGPointValue Eigenschaften), dies ist jedoch immer noch nicht so praktisch wie ein Box-Ausdruck!


Und 2015 hat Alex Denisov einen Patch für Clang erstellt, mit dem Sie Boxed-Ausdrücke verwenden können, um alle Strukturen in NSValue .


Damit unsere Struktur Boxed-Ausdrücke unterstützt, müssen Sie nur das Attribut objc_boxable für die Struktur hinzufügen.


 #define JM_BOXABLE __attribute__((objc_boxable)) 

 typedef struct JM_BOXABLE JMDimension { JMDimensionUnit unit; CGFloat value; } JMDimension; 

Und wir können die @(...) Syntax für unsere Struktur verwenden:


 let dimension = (JMDimension){ ... }; let boxedValue = @(dimension); //   `NSValue *` 

Sie müssen -[NSValue getValue:] Struktur noch über die Methode -[NSValue getValue:] oder die Kategoriemethode -[NSValue getValue:] .


CoreGraphics definiert sein eigenes Makro, CG_BOXABLE , und Boxed-Ausdrücke werden bereits für die CGPoint , CGSize , CGVector und CGRect .


Für andere häufig verwendete Strukturen können wir selbst Unterstützung für Boxed-Ausdrücke hinzufügen:


 typedef struct JM_BOXABLE _NSRange NSRange; typedef struct JM_BOXABLE CGAffineTransform CGAffineTransform; typedef struct JM_BOXABLE UIEdgeInsets UIEdgeInsets; typedef struct JM_BOXABLE NSDirectionalEdgeInsets NSDirectionalEdgeInsets; typedef struct JM_BOXABLE UIOffset UIOffset; typedef struct JM_BOXABLE CATransform3D CATransform3D; 

Zusammengesetzte Literale


Ein weiteres nützliches Sprachkonstrukt ist das zusammengesetzte Literal . Zusammengesetzte Literale wurden in GCC als Spracherweiterung angezeigt und später zum C11-Standard hinzugefügt.


Wenn wir zuvor, nachdem wir den Aufruf von UIEdgeInsetsMake , nur erraten konnten, welche Einrückung wir erhalten würden (wir mussten die Deklaration der UIEdgeInsetsMake Funktion beobachten), spricht der Code mit zusammengesetzten Literalen für sich selbst:


 //  UIEdgeInsetsMake(1, 2, 3, 4) //  (UIEdgeInsets){ .top = 1, .left = 2, .bottom = 3, .right = 4 } 

Es ist noch bequemer, eine solche Konstruktion zu verwenden, wenn einige der Felder Null sind:


 (CGPoint){ .y = 10 } //  (CGPoint){ .x = 0, .y = 10 } (CGRect){ .size = { .width = 10, .height = 20 } } //  (CGRect){ .origin = { .x = 0, .y = 0 }, .size = { .width = 10, .height = 20 } } (UIEdgeInsets){ .top = 10, .bottom = 20 } //  (UIEdgeInsets){ .top = 20, .left = 0, .bottom = 10, .right = 0 } 

Natürlich können Sie in zusammengesetzten Literalen nicht nur Konstanten, sondern auch beliebige Ausdrücke verwenden:


 textFrame = (CGRect){ .origin = { .y = CGRectGetMaxY(buttonFrame) + textMarginTop }, .size = textSize }; 

Schnipsel für Xcode
 (NSRange){ .location = <#location#>, .length = <#length#> } (CGPoint){ .x = <#x#>, .y = <#y#> } (CGSize){ .width = <#width#>, .height = <#height#> } (CGRect){ .origin = { .x = <#x#>, .y = <#y#> }, .size = { .width = <#width#>, .height = <#height#> } } (UIEdgeInsets){ .top = <#top#>, .left = <#left#>, .bottom = <#bottom#>, .right = <#right#> } (NSDirectionalEdgeInsets){ .top = <#top#>, .leading = <#leading#>, .bottom = <#bottom#>, .trailing = <#trailing#> } (UIOffset){ .horizontal = <#horizontal#>, .vertical = <#vertical#> } 

Nullbarkeit


In Xcode 6.3.2 wurden in Objective-C Anmerkungen zur Nullbarkeit angezeigt . Apple-Entwickler haben sie hinzugefügt, um die Objective-C-API in Swift zu importieren. Wenn der Sprache jedoch etwas hinzugefügt wird, sollten Sie versuchen, es in Ihren Dienst zu stellen. Und wir werden erklären, wie wir die Nullbarkeit in einem Objective-C-Projekt verwenden und welche Einschränkungen bestehen.


Um Ihr Wissen aufzufrischen, können Sie sich die WWDC-Sitzung ansehen.


Als erstes haben wir begonnen, die NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_BEGIN / NS_ASSUME_NONNULL_END in alle .m Dateien zu .m . Um dies nicht von Hand zu tun, patchen wir Dateivorlagen direkt in Xcode.


Wir haben auch begonnen, die Nullfähigkeit für alle privaten Eigenschaften und Methoden korrekt festzulegen.


Wenn wir die Makros NS_ASSUME_NONNULL_BEGIN / NS_ASSUME_NONNULL_END zu einer vorhandenen .m Datei hinzufügen, fügen wir sofort die fehlenden nullable , null_resettable und _Nullable in der gesamten Datei hinzu.


Alle nützlichen Warnungen des Nullfähigkeits-Compilers sind standardmäßig aktiviert. Es gibt jedoch eine extreme Warnung, die ich einschließen möchte: - -Wnullable-to-nonnull-conversion (festgelegt in den "Anderen -Wnullable-to-nonnull-conversion " in den Projekteinstellungen). Der Compiler generiert diese Warnung, wenn eine Variable oder ein Ausdruck mit einem nullbaren Typ implizit in einen nicht null Typ umgewandelt wird.


 + (NSString *)foo:(nullable NSString *)string { return string; // Implicit conversion from nullable pointer 'NSString * _Nullable' to non-nullable pointer type 'NSString * _Nonnull' } 

Leider __auto_type diese Warnung für __auto_type (und daher let und var ) nicht. Der über __auto_type abgeleitete __auto_type verwirft die Annotation zur Nullbarkeit. Und nach dem Kommentar des Apple-Entwicklers in rdar: // 27062504 wird sich dieses Verhalten nicht ändern. Es wurde experimentell beobachtet, dass das Hinzufügen von _Nullable oder _Nonnull zu __auto_type nichts beeinflusst.


 - (NSString *)test:(nullable NSString *)string { let tmp = string; return tmp; //   } 

Um die nullable-to-nonnull-conversion einer nullable-to-nonnull-conversion zu unterdrücken nullable-to-nonnull-conversion wir ein Makro geschrieben, das "Entpacken erzwingen". Idee aus dem RBBNotNil Makro. Aufgrund des Verhaltens von __auto_type es jedoch möglich, die Hilfsklasse loszuwerden.


 #define JMNonnull(obj_) \ ({ \ NSCAssert(obj_, @"Expected `%@` not to be nil.", @#obj_); \ (typeof({ __auto_type result_ = (obj_); result_; }))(obj_); \ }) 

Ein Beispiel für die Verwendung des JMNonnull Makros:


 @interface JMRobot : NSObject @property (nonatomic, strong, nullable) JMLeg *leftLeg; @property (nonatomic, strong, nullable) JMLeg *rightLeg; @end @implementation JMRobot - (void)stepLeft { [self step:JMNonnull(self.leftLeg)] } - (void)stepRight { [self step:JMNonnull(self.rightLeg)] } - (void)step:(JMLeg *)leg { // ... } @end 

Beachten Sie, dass zum Zeitpunkt des Schreibens die Warnung zur nullable-to-nonnull-conversion nicht nullable-to-nonnull-conversion nicht nullable-to-nonnull-conversion funktioniert: Der Compiler versteht noch nicht, dass eine nullable Variable nach Überprüfung auf Ungleichung nil als nonnull nil interpretiert werden nonnull .


 - (NSString *)foo:(nullable NSString *)string { if (string != nil) { return string; // Implicit conversion from nullable pointer 'NSString * _Nullable' to non-nullable pointer type 'NSString * _Nonnull' } else { return @""; } } 

In Objective-C ++ - Code können Sie diese Einschränkung umgehen, indem Sie das Konstrukt if let verwenden, da Objective-C ++ Variablendeklarationen im Ausdruck einer if zulässt.


 - (NSString *)foo:(nullable NSString *)stringOrNil { if (let string = stringOrNil) { return string; } else { return @""; } } 

Nützliche Links


Es gibt auch eine Reihe bekannter Makros und Schlüsselwörter, die ich erwähnen möchte: das Schlüsselwort @available , die Makros NS_DESIGNATED_INITIALIZER , NS_UNAVAILABLE , NS_REQUIRES_SUPER , NS_NOESCAPE , NS_ENUM , NS_OPTIONS (oder Ihre Makros für dieselben @ Attribute). Wir empfehlen Ihnen auch, sich den Rest der libextobjc- Bibliothek anzusehen .



→ Der Code für den Artikel ist in gist veröffentlicht .


Fazit


Im ersten Teil des Artikels haben wir versucht, über die Hauptfunktionen und einfachen Sprachverbesserungen zu sprechen, die das Schreiben und die Unterstützung von Objective-C-Code erheblich erleichtern. Im nächsten Teil werden wir zeigen, wie Sie Ihre Produktivität mithilfe von Enums wie in Swift (es handelt sich auch um Fallklassen; es handelt sich auch um algebraische Datentypen , ADTs) und die Möglichkeit der Implementierung von Methoden auf Protokollebene weiter steigern können.

Source: https://habr.com/ru/post/de431236/


All Articles