Comment écrire sur Objective-C en 2018. Partie 1

La plupart des projets iOS passent partiellement ou entièrement à Swift. Swift est un excellent langage, et c'est avec lui que réside l'avenir du développement iOS. Mais le langage est inextricablement lié à la boîte à outils, et il y a des inconvénients à la boîte à outils Swift.


Il y a encore des bogues dans le compilateur Swift qui le font planter ou génèrent du code incorrect. Swift n'a pas d'ABI stable. Et, ce qui est très important, les projets Swift durent depuis trop longtemps.


À cet égard, les projets existants pourraient être plus rentables pour poursuivre le développement de l'objectif-C. Et Objective-C n'est plus ce qu'il était!


Dans cette série d'articles, nous allons vous montrer les fonctionnalités utiles et les améliorations d'Objective-C, avec lesquelles il devient beaucoup plus agréable d'écrire du code. Tous ceux qui écrivent dans Objective-C trouveront quelque chose d'intéressant pour eux-mêmes.



let et var


Objective-C n'a plus besoin de spécifier explicitement les types de variables: dans Xcode 8, l'extension de langage __auto_type est apparue, et avant que l'inférence de type Xcode 8 ne soit disponible dans Objective-C ++ (en utilisant le mot auto clé auto avec l'avènement de C ++ 0X).


Tout d'abord, ajoutons les macros let et 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) { // ... }; 

Si vous aviez l'habitude d'écrire const après un pointeur sur une classe Objective-C, c'était un luxe inadmissible, mais maintenant la déclaration implicite de const (via let ) a été prise pour acquise. La différence est particulièrement visible lors de l'enregistrement d'un bloc dans une variable.


Pour nous, nous avons développé une règle pour utiliser let et var pour déclarer toutes les variables. Même lorsqu'une variable est initialisée à nil :


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

La seule exception est lorsque vous devez vous assurer qu'une variable se voit attribuer une valeur dans chaque branche de code:


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

Ce n'est que de cette manière que nous obtiendrons un avertissement du compilateur si nous oublions d'affecter une valeur à l'une des branches.


Et enfin: pour utiliser let et var pour les variables de type id , vous devez désactiver l'avertissement auto-var-id (ajoutez -Wno-auto-var-id aux "Autres drapeaux d'avertissement" dans les paramètres du projet).


Valeur de retour du type de bloc automatique


Peu de gens savent que le compilateur peut déduire le type de la valeur de retour d'un bloc:


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

C'est très pratique. Surtout si vous écrivez du code réactif à l'aide de ReactiveObjC . Mais il existe un certain nombre de restrictions en vertu desquelles vous devez spécifier explicitement le type de la valeur de retour.


  1. S'il y a plusieurs return dans un bloc qui renvoient des valeurs de différents types.


     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. S'il y a une return dans le bloc qui retourne nil .


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

  3. Si le bloc doit retourner BOOL .


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


Les expressions avec un opérateur de comparaison en C (et donc en Objective-C) sont de type int . Par conséquent, il vaut mieux en faire une règle pour toujours spécifier explicitement le type de retour BOOL .


Génériques et for...in


Dans Xcode 7, des génériques (ou plutôt des génériques légers) sont apparus dans Objective-C. Nous espérons que vous les utilisez déjà. Sinon, vous pouvez regarder la session de la WWDC ou la lire ici ou ici .


Pour nous, nous avons développé une règle pour toujours spécifier des paramètres génériques, même si c'est id ( NSArray<id> * ). Ainsi, il est facile de distinguer le code hérité dans lequel les paramètres génériques ne sont pas encore spécifiés.


Avec les macros let et var , nous nous attendons à pouvoir les utiliser dans une boucle for...in :


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

Mais un tel code ne se compile pas. Très probablement, __auto_type pas pris en charge dans for...in , car for...in ne fonctionne qu'avec les collections qui implémentent le protocole NSFastEnumeration . Et pour les protocoles dans Objective-C, il n'y a pas de support pour les génériques.


Pour corriger cet inconvénient, essayez de créer votre macro foreach . La première chose qui vient à l'esprit: toutes les collections de Foundation ont une propriété objectEnumerator , et la macro pourrait ressembler à ceci:


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

Mais pour NSDictionary et NSMapTable méthode du protocole NSMapTable NSFastEnumeration sur les clés, pas sur les valeurs (vous devrez utiliser keyEnumerator , pas objectEnumerator ).


Nous devrons déclarer une nouvelle propriété qui ne sera utilisée que pour obtenir le type dans l'expression 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_)) 

Maintenant, notre code est bien meilleur:


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

Extrait pour Xcode
 foreach (<#object#>, <#collection#>) { <#statements#> } 

Génériques et copy / mutableCopy


Un autre endroit où la saisie n'est pas disponible dans Objective-C est les -mutableCopy et -mutableCopy (ainsi que les -copyWithZone: et -mutableCopyWithZone: mais nous ne les appelons pas directement).


Pour éviter d'avoir besoin de transtypages explicites, vous pouvez redéclarer les méthodes avec le type de retour. Par exemple, pour NSArray déclarations seraient:


 @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


Puisque nous avons re-déclaré les -mutableCopy -copy et -mutableCopy , il serait bien de garantir que le résultat de l'appel de ces méthodes sera utilisé. Pour ce faire, Clang a l'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 

Pour le code suivant, le compilateur générera un avertissement:


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

overloadable


Peu de gens savent que Clang vous permet de redéfinir des fonctions en langage C (et donc en Objective-C). En utilisant l'attribut overloadable , vous pouvez créer des fonctions avec le même nom, mais avec différents types d'arguments ou avec des nombres différents.


Les fonctions remplaçables ne peuvent pas différer uniquement par le type de la valeur de retour.


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

Expressions encadrées


En 2012, lors d'une session WWDC 413, Apple a présenté des littéraux pour NSNumber , NSArray et NSDictionary , ainsi que des expressions encadrées. Vous pouvez en savoir plus sur les littéraux et les expressions encadrées dans la documentation de Clang .


 //  @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)] 

En utilisant des littéraux et des expressions encadrées, vous pouvez facilement obtenir un objet représentant un nombre ou une valeur booléenne. Mais pour obtenir un objet qui enveloppe la structure, vous devez écrire du code:


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

Des méthodes et propriétés d'assistance sont définies pour certaines classes (comme la méthode +[NSValue valueWithCGPoint:] et les propriétés CGPointValue ), mais ce n'est toujours pas aussi pratique qu'une expression encadrée!


Et en 2015, Alex Denisov a créé un patch pour Clang, vous permettant d'utiliser des expressions encadrées pour envelopper toutes les structures dans NSValue .


Pour que notre structure objc_boxable en charge les expressions encadrées, il vous suffit d'ajouter l'attribut objc_boxable pour la structure.


 #define JM_BOXABLE __attribute__((objc_boxable)) 

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

Et nous pouvons utiliser la syntaxe @(...) pour notre structure:


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

Vous devez toujours -[NSValue getValue:] structure via la méthode -[NSValue getValue:] ou la méthode category.


CoreGraphics définit sa propre macro, CG_BOXABLE et les expressions encadrées sont déjà prises en charge pour les CGPoint , CGSize , CGVector et CGRect .


Pour d'autres structures couramment utilisées, nous pouvons ajouter nous-mêmes la prise en charge des expressions encadrées:


 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; 

Littéraux composés


Une autre construction de langage utile est le littéral composé . Les littéraux composés sont apparus dans GCC en tant qu'extension de langage et ont ensuite été ajoutés à la norme C11.


Si plus tôt, après avoir rencontré l'appel à UIEdgeInsetsMake , nous ne pouvions que deviner quelle indentation nous obtiendrions (nous devions regarder la déclaration de la fonction UIEdgeInsetsMake ), puis avec les littéraux composés, le code parle de lui-même:


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

Il est encore plus pratique d'utiliser une telle construction lorsque certains des champs sont nuls:


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

Bien sûr, dans les littéraux composés, vous pouvez utiliser non seulement des constantes, mais aussi toutes les expressions:


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

Extraits pour 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#> } 

Annulation


Dans Xcode 6.3.2, les annotations de nullité sont apparues dans Objective-C. Les développeurs Apple les ont ajoutés pour importer l'API Objective-C dans Swift. Mais si quelque chose est ajouté à la langue, vous devriez essayer de le mettre à votre service. Et nous expliquerons comment nous utilisons la nullité dans un projet Objective-C et quelles sont les limitations.


Pour rafraîchir vos connaissances, vous pouvez regarder la session WWDC .


La première chose que nous avons faite a été de commencer à écrire les NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_BEGIN / NS_ASSUME_NONNULL_END dans tous les fichiers .m . Afin de ne pas le faire à la main, nous corrigeons les modèles de fichiers directement dans Xcode.


Nous avons également commencé à définir correctement la nullité pour toutes les propriétés et méthodes privées.


Si nous ajoutons les macros NS_ASSUME_NONNULL_BEGIN / NS_ASSUME_NONNULL_END à un fichier .m existant, nous ajoutons immédiatement les null_resettable , null_resettable et _Nullable dans l'ensemble du fichier.


Tous les avertissements utiles du compilateur de nullité sont activés par défaut. Mais il y a un avertissement extrême que je voudrais inclure: -Wnullable-to-nonnull-conversion (défini dans les "Autres drapeaux d'avertissement" dans les paramètres du projet). Le compilateur génère cet avertissement lorsqu'une variable ou une expression avec un type nullable est implicitement convertie en un type non nul.


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

Malheureusement, pour __auto_type (et donc let et var ), cet avertissement ne fonctionne pas. Le type déduit via __auto_type supprime l'annotation de nullité. Et, à en juger par le commentaire du développeur Apple dans rdar: // 27062504 , ce comportement ne changera pas. Il a été observé expérimentalement que l'ajout de _Nullable ou _Nonnull à __auto_type n'affecte rien.


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

Pour supprimer l' nullable-to-nonnull-conversion nous avons écrit une macro qui "force le débouclage". Idée tirée de la macro RBBNotNil . Mais en raison du comportement de __auto_type il a été possible de se débarrasser de la classe auxiliaire.


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

Un exemple d'utilisation de la macro JMNonnull :


 @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 

Notez qu'au moment de l'écriture, l'avertissement de nullable-to-nonnull-conversion ne fonctionne pas nullable-to-nonnull-conversion : le compilateur ne comprend pas encore qu'une variable nullable , après avoir vérifié l'inégalité, nil peut être interprétée comme non 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 @""; } } 

Dans le code Objective-C ++, vous pouvez contourner cette limitation en utilisant la construction if let , car Objective-C ++ autorise les déclarations de variables dans l'expression d'une if .


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

Liens utiles


Il y a aussi un certain nombre de macros et de mots clés plus connus que je voudrais mentionner: le mot clé @available , les macros NS_DESIGNATED_INITIALIZER , NS_UNAVAILABLE , NS_REQUIRES_SUPER , NS_NOESCAPE , NS_ENUM , NS_OPTIONS (ou vos macros pour les mêmes attributs) et la macro de bibliothèque @keypath. Nous vous recommandons également de consulter le reste de la bibliothèque libextobjc .



→ Le code de l'article est affiché dans gist .


Conclusion


Dans la première partie de l'article, nous avons essayé de parler des principales fonctionnalités et des améliorations simples du langage, qui facilitent grandement l'écriture et la prise en charge du code Objective-C. Dans la partie suivante, nous montrerons comment vous pouvez augmenter votre productivité à l'aide d'énumérations comme dans Swift (ce sont des classes Case; ce sont des types de données algébriques , ADT) et la possibilité de mettre en œuvre des méthodes au niveau du protocole.

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


All Articles