有什么方法可以在NSArray,NSMutableArray等上强制键入吗?


Answers:


35

您可以使用-addSomeClass:允许进行编译时静态类型检查的方法来创建类别(这样,编译器可以告知您是否尝试通过该方法添加一个已知不同的类的对象),但是没有真正的方法来强制执行该操作。数组仅包含给定类的对象。

通常,在Objective-C中似乎不需要这种约束。我认为我从未听说过有经验的Cocoa程序员希望使用该功能。似乎只有其他语言的程序员仍在思考这些语言的人。如果只希望将给定类的对象放在数组中,则仅将该类的对象粘贴在其中。如果要测试代码是否正常运行,请对其进行测试。


136
我认为“经验丰富的Cocoa程序员”只是不知道他们缺少什么-Java的经验表明,类型变量可以提高代码理解能力,并使更多的重构成为可能。
tgdavies

11
好吧,Java的Generics支持本身就被严重破坏了,因为他们没有从一开始就将其投入……
dertoni 2010年

28
要同意@tgdavies。我想念我在C#中具有的智能感知和重构功能。当我想要动态键入时,可以在C#4.0中获得它。当我想要强类型的东西时,我也可以拥有。我发现这两件事都有时间和地点。
史蒂夫

18
@charkrit关于Objective-C的“不必要”作用是什么?使用C#时,您是否觉得有必要?我听到很多人说您不需要在Objective-C中使用它,但我认为这些人认为您不需要任何语言的使用,这使它成为偏好/样式而不是必要的问题。
bacar 2012年

17
这不是关于允许您的编译器实际帮助您发现问题的。当然,您可以说:“如果只希望将给定类的对象放入数组中,则仅将该类的对象粘贴在其中。” 但是,如果测试是强制执行测试的唯一方法,那么您将处于不利地位。您发现编写问题的代码越远离代码,问题的成本就越高。
GreenKiwi

145

还没有人把它放在这里,所以我会做!

现在,这已在Objective-C中得到正式支持。从Xcode 7开始,您可以使用以下语法:

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

注意

重要的是要注意,这些只是编译器警告,从技术上讲,您仍然可以将任何对象插入数组。有可用的脚本将所有警告变成错误,从而阻止构建。


我在这里很懒,但是为什么这仅在XCode 7中可用?我们可以nonnull在XCode 6中使用,据我所记得,它们是同时引入的。此外,此类概念的使用是否取决于XCode版本或iOS版本?
2015年

@Guven -空性排在6,你是正确的,但ObjC仿制药并没有介绍到的Xcode 7
洛根

我很确定这仅取决于Xcode版本。泛型仅是编译器警告,不会在运行时指示。我很确定您可以将其编译为所需的任何操作系统。
Logan

2
@DeanKelly-您可以这样:@property (nonatomic, strong) NSArray<id<SomeProtocol>>* protocolObjects; 看起来有些笨拙,但是可以解决问题!
洛根

1
@Logan,不仅有脚本集,还可以防止在检测到任何警告时进行构建。Xcode具有一种名为“配置”的完善机制。查看此内容boredzo.org/blog/archives/2009-11-07/warnings
adnako

53

对于从强类型语言(例如C ++或Java)过渡到更弱或动态类型的语言(例如Python,Ruby或Objective-C)的人们来说,这是一个相对普遍的问题。在Objective-C中,大多数对象都继承自NSObject(type id)(其余对象继承自其他根类,例如NSProxyand也可以是type id),并且任何消息都可以发送到任何对象。当然,向无法识别的实例发送消息可能会导致运行时错误(并且还会导致编译器警告带有适当的-W标志)。只要实例响应您发送的消息,您就可能不在乎它属于哪个类。这通常被称为“鸭子类型”,因为“如果它像鸭子一样发出嘎嘎声(即响应选择器),那它就是鸭子(即它可以处理消息;谁在乎它是什么类)”。

您可以使用方法测试实例在运行时是否响应选择器-(BOOL)respondsToSelector:(SEL)selector。假设您要在数组中的每个实例上调用一个方法,但不确定所有实例都可以处理该消息(因此,您不能只使用NSArray-[NSArray makeObjectsPerformSelector:],这样的方法将起作用:

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

如果控制实现您要调用的方法的实例的源代码,则更常见的方法是定义一个@protocol包含这些方法的,并声明所涉及的类在其声明中实现了该协议。在这种用法中,a @protocol类似于Java接口或C ++抽象基类。然后,您可以测试与整个协议的一致性,而不是对每种方法的响应。在前面的示例中,它并没有多大区别,但是如果您调用多个方法,则可能会简化事情。该示例将是:

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

假设MyProtocol宣布myMethod。之所以喜欢第二种方法,是因为它比第一种方法更能说明代码的意图。

通常,这些方法之一使您不必担心数组中的所有对象是否都具有给定类型。如果您仍然很在意,则标准动态语言方法是单元测试,单元测试,单元测试。由于对此要求的回归将产生(可能无法恢复的)运行时(而不是编译时)错误,因此您需要进行测试覆盖以验证行为,以免使崩溃的人丧生。在这种情况下,执行修改数组的操作,然后验证数组中的所有实例都属于给定的类。有了适当的测试覆盖范围,您甚至不需要增加验证实例身份的运行时开销。您的单元测试覆盖面很好,不是吗?


35
单元测试不能替代体面的类型系统。
tba 2012年

8
是的,他需要类型数组可以提供的工具。我确信@BarryWark(以及其他接触过他需要使用,阅读,理解和支持的任何代码库的人)的代码覆盖率均为100%。但是我敢打赌,id除非有必要,否则不要使用raw ,除了Java编码器会传递Objects以外。为什么不?如果您有单元测试,则不需要它吗?因为它在那里,使您的代码更容易维护,就像键入数组一样。听起来好像人们在平台上投资了很多,但又不愿承认一个观点,因此发明了这种省略实际上是有益的原因。
funkybro 2013年

“鸭子打字” ??那真好笑!从来没有听说过那个。
约翰·亨克尔

11

您可以子类化NSMutableArray以强制类型安全。

NSMutableArray是一个类集群,因此子类化并非易事。我最终继承NSArray了该类中的数组并将其转发给该类中的数组。其结果是一类叫做ConcreteMutableArray很容易的子类。这是我想出的:

更新:请检查Mike Ash的这篇博客文章关于类类的继承。

在项目中包含这些文件,然后使用宏生成所需的任何类型:

MyArrayTypes.h

CUSTOM_ARRAY_INTERFACE(NSString)
CUSTOM_ARRAY_INTERFACE(User)

MyArrayTypes.m

CUSTOM_ARRAY_IMPLEMENTATION(NSString)
CUSTOM_ARRAY_IMPLEMENTATION(User)

用法:

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

其他想法

  • 它继承自NSArray以支持序列化/反序列化
  • 根据您的喜好,您可能想覆盖/隐藏通用方法,例如

    - (void) addObject:(id)anObject


很好,但目前它缺少通过覆盖某些方法的强类型输入。目前,这只是弱输入。
心教堂

7

看看https://github.com/tomersh/Objective-C-Generics,这是Objective-C的编译时(预处理器实现)泛型实现。这篇博客文章有一个很好的概述。基本上,您会获得编译时检查(警告或错误),但对泛型没有运行时的损失。


1
我尝试了一下,这是个很好的主意,但不幸的是,它有很多错误,并且不检查添加的元素。
Binarian

4

这个Github项目完全实现了该功能。

然后<>,您可以像使用C#一样使用方括号。

从他们的例子:

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

0

一种可能的方法是子类化NSArray,但Apple建议不要这样做。考虑类型化NSArray的实际需求更容易三思。


1
节省了在编译时进行静态类型检查的时间,甚至更好。当您为长期使用而编写lib时特别有用。
pinxue

0

我创建了一个NSArray子类,该子类使用NSArray对象作为ivar的后盾,以避免NSArray的类集群性质出现问题。它需要接受或拒绝添加对象的块。

只允许NSString对象,你可以定义AddBlock

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

您可以定义一个a FailBlock来决定要执行的操作,如果某个元素未通过测试-过滤失败,将其添加到另一个数组,或者-这是默认设置-引发异常。

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

像这样使用它:

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

这只是示例代码,在实际应用中从未使用过。这样做可能需要执行mor NSArray方法。


0

如果混合使用c ++和Objective-c(即使用mm文件类型),则可以使用对或元组强制输入。例如,在以下方法中,您可以创建类型为std :: pair的C ++对象,将其转换为OC包装器类型的对象(需要定义的std :: pair的包装器),然后将其传递给某些对象。其他OC方法,您需要在其中将OC对象转换回C ++对象才能使用它。OC方法仅接受OC包装器类型,从而确保类型安全。您甚至可以使用元组,可变参数模板,类型列表来利用更高级的C ++功能来促进类型安全。

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

我的两美分有点“清洁”:

使用typedefs:

typedef NSArray<NSString *> StringArray;

在代码中我们可以做到:

StringArray * titles = @[@"ID",@"Name", @"TYPE", @"DATE"];
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.