Como escrever no Objective-C em 2018. Parte 1

A maioria dos projetos iOS alterna parcial ou totalmente para o Swift. Swift é uma ótima linguagem, e com ela reside o futuro do desenvolvimento do iOS. Mas a linguagem está indissoluvelmente ligada ao kit de ferramentas, e o Swift tem falhas.


Ainda existem erros no compilador Swift que causam falha ou geram código incorreto. Swift não possui uma ABI estável. E, muito importante, os projetos Swift estão em andamento há muito tempo.


Nesse sentido, os projetos existentes podem ser mais lucrativos para continuar o desenvolvimento no Objective-C. E Objective-C não é mais o que costumava ser!


Nesta série de artigos, mostraremos os recursos e aprimoramentos úteis do Objective-C, com os quais se torna muito mais agradável escrever código. Todo mundo que escreve no Objective-C encontrará algo interessante para si.



let e var


O Objective-C não precisa mais especificar explicitamente os tipos de variáveis: no Xcode 8, a extensão de idioma __auto_type apareceu e antes da inferência do tipo Xcode 8 estava disponível no Objective-C ++ (usando a palavra auto chave auto no advento do C ++ 0X).


Primeiro, let adicionar as macros let e 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) { // ... }; 

Se você costumava escrever const após um ponteiro para uma classe Objective-C, era um luxo inadmissível, mas agora a declaração implícita de const (via let ) é um dado adquirido. A diferença é especialmente notável ao salvar um bloco em uma variável.


Para nós mesmos, desenvolvemos uma regra para usar let e var para declarar todas as variáveis. Mesmo quando uma variável é inicializada como nil :


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

A única exceção é quando você precisa garantir que uma variável seja atribuída a um valor em cada ramificação de código:


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

Somente dessa maneira obteremos um aviso do compilador se esquecermos de atribuir um valor a uma das ramificações.


E finalmente: para usar let e var para variáveis ​​do tipo id , você precisa desativar o aviso de auto-var-id (adicione -Wno-auto-var-id aos "Outros sinalizadores de aviso" nas configurações do projeto).


Valor de retorno do tipo de bloqueio automático


Poucas pessoas sabem que o compilador pode inferir o tipo do valor de retorno de um bloco:


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

É muito conveniente Especialmente se você escrever código reativo usando ReactiveObjC . Mas há várias restrições sob as quais você deve especificar explicitamente o tipo do valor de retorno.


  1. Se houver várias return em um bloco que retornem valores de tipos diferentes.


     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. Se houver uma return no bloco que retorne nil .


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

  3. Se o bloco retornar BOOL .


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


Expressões com um operador de comparação em C (e, portanto, em Objective-C) são do tipo int . Portanto, é melhor tornar uma regra sempre especificar explicitamente o tipo de retorno BOOL .


Genéricos e for...in


No Xcode 7, os genéricos (ou melhor, os leves) apareceram no Objective-C. Esperamos que você já os use. Mas se não, você pode assistir à sessão da WWDC ou lê-la aqui ou aqui .


Para nós mesmos, desenvolvemos uma regra para sempre especificar parâmetros genéricos, mesmo que seja id ( NSArray<id> * ). Portanto, é fácil distinguir entre código legado no qual parâmetros genéricos ainda não foram especificados.


Com as macros let e var , esperamos poder usá-las em um loop for...in :


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

Mas esse código não compila. Provavelmente, __auto_type não __auto_type suportado for...in , porque for...in funciona apenas com coleções que implementam o protocolo NSFastEnumeration . E para protocolos no Objective-C, não há suporte para genéricos.


Para corrigir essa desvantagem, tente fazer sua macro foreach . A primeira coisa que vem à mente: todas as coleções no Foundation têm uma propriedade objectEnumerator e a macro pode ficar assim:


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

Mas para NSDictionary e NSMapTable método de protocolo NSMapTable NSFastEnumeration sobre as chaves, não sobre os valores (você precisaria usar keyEnumerator , não objectEnumerator ).


Precisamos declarar uma nova propriedade que será usada apenas para obter o tipo na expressão 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_)) 

Agora nosso código parece muito melhor:


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

Snippet para Xcode
 foreach (<#object#>, <#collection#>) { <#statements#> } 

Genéricos e copy / mutableCopy


Outro local em que a digitação não está disponível no Objective-C são os -mutableCopy e -mutableCopy (assim como os -copyWithZone: e -mutableCopyWithZone: mas não os chamamos diretamente).


Para evitar a necessidade de conversões explícitas, é possível redefinir os métodos com o tipo de retorno. Por exemplo, para o NSArray declarações seriam:


 @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


Como re-declaramos os -mutableCopy e -mutableCopy , seria bom garantir que o resultado da chamada desses métodos seja usado. Para fazer isso, Clang possui o atributo 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 

Para o código a seguir, o compilador gerará um aviso:


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

overloadable


Poucos sabem que o Clang permite redefinir funções na linguagem C (e, portanto, no Objective-C). Usando o atributo overloadable , você pode criar funções com o mesmo nome, mas com diferentes tipos de argumentos ou com números diferentes.


As funções substituíveis não podem diferir apenas no tipo do valor de retorno.


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

Expressões em caixa


Em 2012, em uma sessão da WWDC 413, a Apple introduziu literais para NSNumber , NSArray e NSDictionary , além de expressões em caixa. Você pode ler mais sobre literais e expressões em caixa na documentação do 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)] 

Usando literais e expressões em caixa, você pode facilmente obter um objeto representando um número ou um valor booleano. Mas, para obter um objeto que envolve a estrutura, você precisa escrever um código:


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

Os métodos e propriedades auxiliares são definidos para algumas classes (como o método +[NSValue valueWithCGPoint:] e CGPointValue propriedades CGPointValue ), mas isso ainda não é tão conveniente quanto a expressão em caixa!


E em 2015, Alex Denisov fez um patch para o Clang, permitindo que você use expressões em caixa para NSValue qualquer estrutura no NSValue .


Para que nossa estrutura suporte expressões em caixa, basta adicionar o atributo objc_boxable para a estrutura.


 #define JM_BOXABLE __attribute__((objc_boxable)) 

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

E podemos usar a sintaxe @(...) para nossa estrutura:


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

Você ainda precisa -[NSValue getValue:] estrutura pelo método -[NSValue getValue:] ou pelo método de categoria.


O CoreGraphics define sua própria macro, CG_BOXABLE , e as expressões em caixa já são suportadas para as estruturas CGPoint , CGSize , CGVector e CGRect .


Para outras estruturas comumente usadas, podemos adicionar suporte para expressões em caixa por conta própria:


 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; 

Literais compostos


Outra construção de linguagem útil é literal composta . Literais compostos apareceram no GCC como uma extensão de idioma e foram posteriormente adicionados ao padrão C11.


Se anteriormente, tendo atendido a chamada para UIEdgeInsetsMake , poderíamos apenas adivinhar qual recuo UIEdgeInsetsMake (tivemos que observar a declaração da função UIEdgeInsetsMake ), então, com literais compostos, o código fala por si:


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

É ainda mais conveniente usar essa construção quando alguns dos campos são zero:


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

Obviamente, em literais compostos, você pode usar não apenas constantes, mas também quaisquer expressões:


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

Snippets para 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#> } 

Anulabilidade


No Xcode 6.3.2, as anotações de nulidade apareceram no Objective-C. Os desenvolvedores da Apple os adicionaram para importar a API do Objective-C para o Swift. Mas se algo for adicionado ao idioma, tente colocá-lo em seu serviço. E explicaremos como usamos a nulidade em um projeto Objective-C e quais são as limitações.


Para atualizar seu conhecimento, você pode assistir à sessão da WWDC .


A primeira coisa que fizemos foi começar a gravar as NS_ASSUME_NONNULL_END / NS_ASSUME_NONNULL_END em todos os arquivos .m . Para não fazer isso manualmente, corrigimos modelos de arquivo diretamente no Xcode.


Também começamos a definir corretamente a nulidade para todas as propriedades e métodos particulares.


Se adicionarmos as macros NS_ASSUME_NONNULL_BEGIN / NS_ASSUME_NONNULL_END a um arquivo .m existente, adicionaremos imediatamente os nullable , null_resettable , null_resettable e _Nullable no arquivo inteiro.


Todos os avisos úteis do compilador de nulidade são ativados por padrão. Mas há um aviso extremo que eu gostaria de incluir: -Wnullable-to-nonnull-conversion (definido em "Outros sinalizadores de aviso" nas configurações do projeto). O compilador gera esse aviso quando uma variável ou expressão com um tipo nulo é implicitamente convertida em um tipo não nulo.


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

Infelizmente, para __auto_type (e, portanto, let e var ), esse aviso não funciona. O tipo deduzido por __auto_type descarta a anotação de nulidade. E, a julgar pelo comentário do desenvolvedor da Apple em rdar: // 27062504 , esse comportamento não será alterado. Foi experimentalmente observado que adicionar _Nullable ou _Nonnull a __auto_type não afeta nada.


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

Para suprimir o nullable-to-nonnull-conversion escrevemos uma macro que "força o desembrulho". Ideia retirada da macro RBBNotNil . Mas devido ao comportamento de __auto_type foi possível se livrar da classe auxiliar.


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

Um exemplo de uso da 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 

Observe que, no momento da escrita, o aviso de nullable-to-nonnull-conversion não funciona nullable-to-nonnull-conversion : o compilador ainda não entende que uma variável nullable após verificar a desigualdade nil pode ser interpretada como nonnull nil .


 - (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 @""; } } 

No código Objective-C ++, você pode contornar essa limitação usando a construção if let , pois o Objective-C ++ permite declarações variáveis ​​na expressão de uma if .


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

Links úteis


Há também várias macros e palavras-chave mais conhecidas que eu gostaria de mencionar: a palavra-chave @available , as macros NS_DESIGNATED_INITIALIZER , NS_UNAVAILABLE , NS_REQUIRES_SUPER , NS_NOESCAPE , NS_ENUM , NS_OPTIONS (ou suas macros para os mesmos atributos) e a biblioteca @keypath. Também recomendamos que você olhe o restante da biblioteca libextobjc .



→ O código do artigo é publicado em essência .


Conclusão


Na primeira parte do artigo, tentamos falar sobre os principais recursos e melhorias simples de linguagem, o que facilita muito a escrita e o suporte ao código Objective-C. Na próxima parte, mostraremos como você pode aumentar ainda mais sua produtividade com a ajuda de enumerações, como no Swift (são classes Case; são tipos de dados algébricos , ADTs) e a possibilidade de implementar métodos no nível do protocolo.

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


All Articles