Existe alguma maneira de forçar a digitação em NSArray, NSMutableArray, etc.?

Respostas:

35

Você poderia criar uma categoria com um -addSomeClass:método para permitir a verificação de tipo estático em tempo de compilação (então o compilador poderia avisá-lo se você tentar adicionar um objeto que ele sabe que é uma classe diferente através desse método), mas não há nenhuma maneira real de impor isso uma matriz contém apenas objetos de uma determinada classe.

Em geral, não parece haver necessidade de tal restrição em Objective-C. Acho que nunca ouvi um programador experiente do Cocoa desejar esse recurso. As únicas pessoas que parecem ser programadores de outras linguagens que ainda pensam nessas linguagens. Se você quiser apenas objetos de uma determinada classe em um array, coloque apenas objetos dessa classe ali. Se você quiser testar se seu código está se comportando corretamente, teste-o.

Mandril
fonte
136
Eu acho que os 'programadores Cocoa experientes' simplesmente não sabem o que estão perdendo - a experiência com Java mostra que as variáveis ​​de tipo melhoram a compreensão do código e possibilitam mais refatorações.
tgdavies
11
Bem, o suporte a genéricos do Java está muito quebrado por si só, porque eles não o colocaram desde o início ...
dertoni
28
Tenho que concordar com @tgdavies. Sinto falta dos recursos de intellisense e refatoração que tinha com o C #. Quando eu quiser a digitação dinâmica, posso obtê-la em C # 4.0. Quando eu quero coisas de tipos fortes, posso ter isso também. Descobri que existe um tempo e um lugar para essas duas coisas.
Steve
18
@charkrit O que há no Objective-C que o torna 'desnecessário'? Você sentiu que era necessário quando estava usando C #? Eu ouço muitas pessoas dizendo que você não precisa disso em Objective-C, mas acho que essas mesmas pessoas acham que você não precisa em nenhuma linguagem, o que torna isso uma questão de preferência / estilo, não necessariamente.
bacar
17
Não se trata de permitir que seu compilador realmente o ajude a encontrar problemas. Claro, você pode dizer "Se você quiser apenas objetos de uma determinada classe em um array, coloque apenas objetos dessa classe lá." Mas se os testes são a única maneira de fazer com que isso aconteça, você está em desvantagem. Quanto mais longe de escrever o código você encontrar um problema, mais caro será esse problema.
GreenKiwi
145

Ninguém colocou isso aqui ainda, então eu coloco!

Tthis agora é oficialmente suportado em Objective-C. A partir do Xcode 7, você pode usar a seguinte sintaxe:

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

Nota

É importante observar que esses são apenas avisos do compilador e você ainda pode tecnicamente inserir qualquer objeto em seu array. Existem scripts disponíveis que transformam todos os avisos em erros que impediriam a construção.

Logan
fonte
Estou sendo preguiçoso aqui, mas por que isso está disponível apenas no XCode 7? Podemos usar nonnullno XCode 6 e, pelo que me lembro, eles foram introduzidos ao mesmo tempo. Além disso, o uso de tais conceitos depende da versão do XCode ou da versão do iOS?
Guven
@Guven - nulidade veio em 6, você está correto, mas os genéricos ObjC não foram introduzidos até o Xcode 7.
Logan
Tenho certeza de que depende apenas da versão do Xcode. Os genéricos são apenas avisos do compilador e não são indicados no tempo de execução. Tenho certeza de que você pode compilar para qualquer OS que quiser.
Logan
2
@DeanKelly - Você poderia fazer isso assim: @property (nonatomic, strong) NSArray<id<SomeProtocol>>* protocolObjects; Parece um pouco desajeitado, mas funciona!
Logan
1
@Logan, não existe apenas o conjunto de scripts, que impede a construção caso seja detectado algum aviso. O Xcode possui um mecanismo perfeito chamado "Configuração". Confira boredzo.org/blog/archives/2009-11-07/warnings
adnako
53

Esta é uma pergunta relativamente comum para pessoas em transição de linguagens de tipo forte (como C ++ ou Java) para linguagens de tipo mais fraco ou dinâmico como Python, Ruby ou Objective-C. Em Objective-C, a maioria dos objetos herda de NSObject(tipo id) (o resto herda de outra classe raiz, como NSProxye também pode ser tipo id) e qualquer mensagem pode ser enviada para qualquer objeto. Claro, enviar uma mensagem para uma instância que ela não reconhece pode causar um erro de tempo de execução (e também irá causar um aviso do compiladorcom sinalizadores -W apropriados). Contanto que uma instância responda à mensagem que você envia, você pode não se importar a qual classe ela pertence. Isso é frequentemente referido como "digitação de pato" porque "se ele grasna como um pato [ou seja, responde a um seletor], é um pato [ou seja, pode lidar com a mensagem; quem se importa com a classe]".

Você pode testar se uma instância responde a um seletor em tempo de execução com o -(BOOL)respondsToSelector:(SEL)selectormétodo. Supondo que você queira chamar um método em cada instância em uma matriz, mas não tenha certeza de que todas as instâncias podem manipular a mensagem (então você não pode apenas usar NSArray's -[NSArray makeObjectsPerformSelector:], algo assim funcionaria:

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

Se você controlar o código-fonte das instâncias que implementam o (s) método (s) que deseja chamar, a abordagem mais comum seria definir um @protocolque contenha esses métodos e declarar que as classes em questão implementam esse protocolo em sua declaração. Nesse uso, a @protocolé análogo a uma interface Java ou uma classe base abstrata C ++. Você pode então testar a conformidade com todo o protocolo, em vez de responder a cada método. No exemplo anterior, não faria muita diferença, mas se você estivesse chamando vários métodos, isso poderia simplificar as coisas. O exemplo seria:

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

assumindo MyProtocoldeclara myMethod. Essa segunda abordagem é favorecida porque esclarece a intenção do código mais do que a primeira.

Freqüentemente, uma dessas abordagens o livra de se preocupar se todos os objetos em uma matriz são de um determinado tipo. Se você ainda se importa, a abordagem de linguagem dinâmica padrão é teste de unidade, teste de unidade, teste de unidade. Como uma regressão neste requisito produzirá um erro de tempo de execução (provavelmente irrecuperável) (não de tempo de compilação), você precisa ter cobertura de teste para verificar o comportamento, de modo que não libere um penetra na selva. Nesse caso, execute uma operação que modifique o array e, em seguida, verifique se todas as instâncias do array pertencem a uma determinada classe. Com a cobertura de teste adequada, você nem mesmo precisa da sobrecarga de tempo de execução adicional para verificar a identidade da instância. Você tem uma boa cobertura de teste de unidade, não é?

Barry Wark
fonte
35
O teste de unidade não é um substituto para um sistema de tipo decente.
TBA
8
Sim, quem precisa das ferramentas que os arrays digitados permitiriam. Tenho certeza de que @BarryWark (e qualquer outra pessoa que mexeu em qualquer base de código que ele precisa para usar, ler, entender e oferecer suporte) tem 100% de cobertura de código. No entanto, aposto que você não usa ids brutos, exceto quando necessário, mais do que os codificadores Java passam Objects. Por que não? Não precisa disso se você tiver testes de unidade? Porque está lá e torna seu código mais fácil de manter, assim como os arrays digitados. Parece que as pessoas que investiram na plataforma não querem ceder um ponto e, portanto, inventam razões pelas quais essa omissão é de fato um benefício.
funkybro
"Digitação de pato" ?? isso é hilário! Nunca ouvi isso antes.
John Henckel
11

Você pode NSMutableArraycriar uma subclasse para reforçar a segurança de tipo.

NSMutableArrayé um cluster de classes , portanto, a criação de subclasses não é trivial. Acabei herdando NSArraye encaminhando invocações para um array dentro dessa classe. O resultado é uma classe chamada ConcreteMutableArrayque é fácil de subclassificar. Aqui está o que eu inventei:

Atualização: verifique esta postagem do blog de Mike Ash sobre como criar subclasses de um cluster de classe.

Inclua esses arquivos em seu projeto e, em seguida, gere quaisquer tipos que desejar usando macros:

MyArrayTypes.h

CUSTOM_ARRAY_INTERFACE(NSString)
CUSTOM_ARRAY_INTERFACE(User)

MyArrayTypes.m

CUSTOM_ARRAY_IMPLEMENTATION(NSString)
CUSTOM_ARRAY_IMPLEMENTATION(User)

Uso:

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

outros pensamentos

  • Ele herda de NSArraypara oferecer suporte à serialização / desserialização
  • Dependendo do seu gosto, você pode querer substituir / ocultar métodos genéricos como

    - (void) addObject:(id)anObject

bendytree
fonte
Bom, mas por agora falta uma tipificação forte, substituindo alguns métodos. Atualmente é apenas uma digitação fraca.
Coeur
7

Dê uma olhada em https://github.com/tomersh/Objective-C-Generics , uma implementação genérica em tempo de compilação (pré-processador implementada) para Objective-C. Esta postagem do blog tem uma boa visão geral. Basicamente, você obtém verificação em tempo de compilação (avisos ou erros), mas nenhuma penalidade de tempo de execução para genéricos.

Barry Wark
fonte
1
Experimentei, ideia muito boa, mas infelizmente com erros e não verifica os elementos adicionados.
Binarian
4

Este projeto Github implementa exatamente essa funcionalidade.

Você pode usar os <>colchetes, assim como faria em C #.

Dos exemplos deles:

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

Uma maneira possível seria criar uma subclasse do NSArray, mas a Apple recomenda não fazer isso. É mais simples pensar duas vezes sobre a necessidade real de um NSArray digitado.

mouviciel
fonte
1
Economiza tempo para ter verificação de tipo estático no momento da compilação, a edição é ainda melhor. Especialmente útil quando você está escrevendo lib para uso de longo prazo.
pinxue
0

Criei uma subclasse NSArray que está usando um objeto NSArray como ivar de apoio para evitar problemas com a natureza de cluster de classe do NSArray. Leva blocos para aceitar ou recusar a adição de um objeto.

para permitir apenas objetos NSString, você pode definir um AddBlockcomo

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

Você pode definir um FailBlockpara decidir o que fazer, se um elemento falhar no teste - falhar normalmente para filtragem, adicioná-lo a outro array ou - este é o padrão - gerar uma exceção.

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

Use-o como:

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

Este é apenas um código de exemplo e nunca foi usado em aplicativos do mundo real. para fazer isso, provavelmente será necessário implementar o método mais NSArray.

vikingosegundo
fonte
0

Se você misturar c ++ e objetivo-c (ou seja, usando o tipo de arquivo mm), você pode forçar a digitação usando par ou tupla. Por exemplo, no método a seguir, você pode criar um objeto C ++ do tipo std :: pair, convertê-lo em um objeto do tipo OC wrapper (wrapper de std :: pair que você precisa definir) e, em seguida, passá-lo para algum outro método OC, dentro do qual você precisa converter o objeto OC de volta em objeto C ++ para usá-lo. O método OC aceita apenas o tipo de wrapper OC, garantindo assim a segurança do tipo. Você pode até usar tupla, modelo variável, lista de tipos para aproveitar recursos C ++ mais avançados para facilitar a segurança de tipos.

- (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
fonte
0

meus dois centavos para ser um pouco "mais limpo":

use typedefs:

typedef NSArray<NSString *> StringArray;

no código, podemos fazer:

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