有没有办法在NSArray、NSMutableArray等对象上强制类型检查?

96

我可以创建一个NSMutableArray实例,其中所有元素都是SomeClass类型的吗?


如何强制NSMutableArray仅包含特定类型的对象? - Hugo
11个回答

151

还没有人将这个放在这里,所以我来发布!

现在在Objective-C中官方支持此功能。从Xcode 7开始,您可以使用以下语法:

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

注意

需要注意的是,这些仅仅是编译器警告,你仍然可以在数组中插入任何对象。有一些脚本可以将所有警告变成错误,从而防止构建。


@Guven - nullability是在6中引入的,你是正确的,但ObjC泛型直到Xcode 7才被引入。 - Logan
我非常确定这只取决于Xcode版本。泛型只是编译器警告,不会在运行时指示。我相信你可以编译到任何你想要的操作系统。 - Logan
2
@DeanKelly - 你可以这样做: @property (nonatomic, strong) NSArray<id<SomeProtocol>>* protocolObjects; 看起来有点笨重,但很实用! - Logan
1
@Logan,不仅有一组脚本可以在检测到任何警告时防止构建。Xcode还有一个完美的机制叫做“配置”。请查看此链接http://boredzo.org/blog/archives/2009-11-07/warnings - adnako
我猜测__kindof可能允许子类?我注意到在iOS 9.0 UINavigationController.h:64中:@property(nonatomic,copy) NSArray<__kindof UIViewController *> *viewControllers; - tboyce12
显示剩余7条评论

53
这是一个相对常见的问题,对于那些从强类型语言(如C++或Java)转向更弱或动态类型语言(例如Python、Ruby或Objective-C)的人来说。在Objective-C中,大多数对象都继承自NSObject(类型为id)(其余继承自其他根类,例如NSProxy,也可以是类型id),并且可以向任何对象发送任何消息。当然,向一个实例发送它不识别的消息可能会导致运行时错误(并且还会在适当的-W标志下导致编译器警告)。只要一个实例响应您发送的消息,您可能不关心它属于什么类。这通常被称为“鸭子类型”,因为“如果它像鸭子一样叫[即响应一个选择器],它就是一只鸭子[即它可以处理该消息;谁关心它属于什么类]”。
您可以使用-(BOOL)respondsToSelector:(SEL)selector方法在运行时测试一个实例是否响应一个选择器。假设您想调用数组中每个实例的一个方法,但不确定所有实例是否都能处理该消息(因此不能只使用NSArray-[NSArray makeObjectsPerformSelector:]方法),则可以使用以下代码:
for(id o in myArray) {
  if([o respondsToSelector:@selector(myMethod)]) {
    [o myMethod];
  }
}
如果您控制实现所需调用方法的实例的源代码,则更常见的方法是定义一个包含这些方法的协议@protocol,并在声明中指定相关类实现该协议。在这种情况下,@protocol类似于Java接口或C++抽象基类。然后,您可以测试是否符合整个协议而不是每个方法的响应。在前面的示例中,这不会有太大的区别,但如果要调用多个方法,则可能会简化操作。示例如下:
for(id o in myArray) {
  if([o conformsToProtocol:@protocol(MyProtocol)]) {
    [o myMethod];
  }
}

假设MyProtocol声明了myMethod。更倾向于第二种方法,因为它比第一种方法更清晰地阐明了代码的意图。

通常情况下,其中一种方法可以使你不必关心数组中的所有对象是否都属于给定类型。如果您仍然关心,则标准动态语言方法是进行单元测试,单元测试和再单元测试。因为在此要求中的回归将产生(很可能无法恢复的)运行时(而非编译时)错误,所以需要具备测试覆盖率以验证行为,以便您不会发布一个会崩溃的版本。在这种情况下,执行修改数组的操作,然后验证数组中的所有实例都属于给定类。有了适当的测试覆盖率,您甚至无需额外的运行时开销来验证实例标识。您确保已经有良好的单元测试覆盖率了,对吧?


36
单元测试不是一个良好类型系统的替代品。 - tba
8
是的,谁需要类型化数组提供的工具。我相信@BarryWark(以及任何接触过他需要使用、阅读、理解和支持的代码库的人)已经有了100%的代码覆盖率。但是我打赌你不会像Java程序员传递Object一样,除非必要,否则不使用原始的id。为什么?如果你有单元测试,那就不需要它了吗?因为它在那里,并且使您的代码更易维护,就像类型化数组一样。听起来像是投资于该平台的人们不愿意让步,因此发明了各种原因来证明这种省略实际上是一种好处。 - funkybro
“鸭子类型”??太搞笑了!以前从未听过这个词。 - John Henckel

36

你可以使用一个 -addSomeClass: 方法创建一个类别,以允许编译时静态类型检查(这样编译器就可以让你知道如果通过该方法添加已知是不同类的对象),但是没有真正的方法来强制要求数组只包含给定类的对象。

总的来说,在 Objective-C 中似乎并不需要这样的限制。我认为我从来没有听过有经验的 Cocoa 程序员希望拥有这个功能。唯一似乎需要的人是来自其他语言的程序员,他们仍然在使用这些语言进行思考。如果你只想在数组中使用给定类的对象,只需将该类的对象放入其中。如果你想测试你的代码是否正常运行,请对其进行测试。


138
我认为“有经验的Cocoa程序员”只是不知道他们错过了什么 - 通过Java的经验表明,类型变量可以提高代码理解能力,并使更多的重构成为可能。 - tgdavies
12
嗯,Java的泛型支持本身存在严重问题,因为它们没有从一开始就将其加入... - dertoni
28
完全同意@tgdavies的观点。我想念在使用C#时拥有的智能感知和重构功能。当我想要动态类型时,我可以在C#4.0中得到它。当我想要强类型的东西时,我也可以得到它。我发现这两种东西都有适用的时间和场合。 - Steve
18
@charkrit,Objective-C 有什么使其“不必要”的特点吗?当你使用 C# 的时候觉得它是必需的吗?我听到很多人说在 Objective-C 中不需要它,但我认为这些人认为在任何语言中都不需要它,这使得它成为偏好/风格问题,而不是必要性问题。 - bacar
17
这不就是让编译器帮你找问题吗?你也可以说:“如果你只想要一个给定类的对象数组,只需要把这个类的对象放进去。” 但是,如果只有测试才能强制执行,那么你就处于劣势。离编写代码越远,发现问题就越耗费成本。 - GreenKiwi
显示剩余11条评论

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


目前虽然不错,但是它通过覆盖一些方法来实现强类型检查。目前只支持弱类型检查。 - Cœur

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

1

2020年,简单明了的回答。恰好我需要一个类型为NSString的可变数组。

语法:

Type<ArrayElementType *> *objectName;

例子:

@property(nonatomic, strong) NSMutableArray<NSString *> *buttonInputCellValues;

0
如果您混合使用C++和Objective-C(即使用mm文件类型),您可以使用pair或tuple强制进行类型匹配。例如,在以下方法中,您可以创建一个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
我创建了一个NSArray子类,使用NSArray对象作为后备ivar,以避免NSArray的类簇特性问题。它接受块来接受或拒绝添加对象。
要仅允许NSString对象,请定义一个AddBlock如下:
^BOOL(id element) {
    return [element isKindOfClass:[NSString class]];
}

您可以定义一个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);

这只是一个示例代码,从未在真实的应用程序中使用过。要这样做,它可能需要实现更多的NSArray方法。

0
一种可能的方法是子类化NSArray,但Apple建议不要这样做。更简单的方法是仔细考虑是否真正需要一个类型化的NSArray。

1
在编译时进行静态类型检查可以节省时间,即使是编辑也更好。当您编写长期使用的库时,这尤其有帮助。 - pinxue

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接