NSMutableArray - 强制数组仅容纳特定类型的对象

69

有没有一种方法可以强制NSMutableArray仅包含特定类型的对象?

我有以下类定义:

@interface Wheel:NSObject  
{    
  int size;  
  float diameter;  
}  
@end  


@interface Car:NSObject  
{  
   NSString *model;  
   NSString *make;  
   NSMutableArray *wheels;  
}  
@end

我该如何使用代码强制“wheels”数组只保存Wheel对象?(绝不允许其他类型的对象)


12个回答

101

2015年的更新

这篇答案最初是在2011年早期写的,开头是这样的:

我们真正想要的是参数多态性,这样你就可以声明诸如 NSMutableArray<NSString>;但遗憾的是目前并不支持。

2015年苹果引入了“轻量级泛型”到Objective-C中,似乎改变了这种情况,现在你可以声明:

NSMutableArray<NSString *> *onlyStrings = [NSMutableArray new];
但这并不完全是看起来的样子,注意 "轻量级" ... 然后注意上述声明的初始化部分没有包含任何通用符号。尽管苹果公司引入了参数化集合,并且可以将非字符串 直接 添加到上述数组中,例如:onlyStrings
[onlyStrings addObject:@666]; // <- Warning: Incompatible pointer types...

运行该方法会触发如上所示的警告,类型安全性不够严密。考虑下面的方法:

- (void) push:(id)obj onto:(NSMutableArray *)array
{
   [array addObject:obj];
}

在同一类的另一个方法中的代码片段:

NSMutableArray<NSString *> *oops = [NSMutableArray new];
[self push:@"asda" onto:oops]; // add a string, fine
[self push:@42 onto:oops];     // add a number, no warnings...
苹果实现的是一种提示系统,用于辅助与Swift自动交互,它确实具有类型安全泛型的风味。然而,在Objective-C方面,虽然编译器提供了一些额外的提示,但这个系统是“轻量级”的,类型完整性最终仍取决于程序员 - 这是Objective-C的方式。
那么你应该使用哪个呢?新的轻量级/伪泛型,还是为您的代码设计自己的模式?真的没有正确答案,找出在您的情况下有意义的内容并使用它。
例如:如果您的目标是与Swift进行交互,您应该使用轻量级泛型!但是,如果集合的类型完整性在您的情况下很重要,则可以将轻量级泛型与在Objective-C端强制执行Swift将在其端执行的类型完整性的自己的代码相结合。
作为另一个选项,这里有一个快速的NSMutableArray通用子类,您可以使用要在单态数组中使用的对象来初始化它。此选项不会给您静态类型检查(就像在Obj-C中一样),您会在插入错误类型时得到运行时异常,就像索引超出范围等运行时异常一样。
这不是经过彻底测试的,并假设覆盖NSMutableArray的文档是正确的...
@interface MonomorphicArray : NSMutableArray
{
    Class elementClass;
    NSMutableArray *realArray;
}

- (id) initWithClass:(Class)element andCapacity:(NSUInteger)numItems;
- (id) initWithClass:(Class)element;

@end

而且实现:

@implementation MonomorphicArray

- (id) initWithClass:(Class)element andCapacity:(NSUInteger)numItems
{
    elementClass = element;
    realArray = [NSMutableArray arrayWithCapacity:numItems];
    return self;
}

- (id) initWithClass:(Class)element
{
    elementClass = element;
    realArray = [NSMutableArray new];
    return self;
}

// override primitive NSMutableArray methods and enforce monomorphism

- (void) insertObject:(id)anObject atIndex:(NSUInteger)index
{
    if ([anObject isKindOfClass:elementClass]) // allows subclasses, use isMemeberOfClass for exact match
    {
        [realArray insertObject:anObject atIndex:index];
    }
    else
    {
        NSException* myException = [NSException
            exceptionWithName:@"InvalidAddObject"
            reason:@"Added object has wrong type"
            userInfo:nil];
        @throw myException;
    }
}

- (void) removeObjectAtIndex:(NSUInteger)index
{
    [realArray removeObjectAtIndex:index];
}

// override primitive NSArray methods

- (NSUInteger) count
{
    return [realArray count];
}

- (id) objectAtIndex:(NSUInteger)index
{
    return [realArray objectAtIndex:index];
}


// block all the other init's (some could be supported)

static id NotSupported()
{
    NSException* myException = [NSException
        exceptionWithName:@"InvalidInitializer"
        reason:@"Only initWithClass: and initWithClass:andCapacity: supported"
        userInfo:nil];
    @throw myException;
}

- (id)initWithArray:(NSArray *)anArray { return NotSupported(); }
- (id)initWithArray:(NSArray *)array copyItems:(BOOL)flag { return NotSupported(); }
- (id)initWithContentsOfFile:(NSString *)aPath { return NotSupported(); }
- (id)initWithContentsOfURL:(NSURL *)aURL { return NotSupported(); }
- (id)initWithObjects:(id)firstObj, ... { return NotSupported(); }
- (id)initWithObjects:(const id *)objects count:(NSUInteger)count { return NotSupported(); }

@end

用法:

MonomorphicArray *monoString = [[MonomorphicArray alloc] initWithClass:[NSString class] andCapacity:3];

[monoString addObject:@"A string"];
[monoString addObject:[NSNumber numberWithInt:42]]; // will throw
[monoString addObject:@"Another string"];

@CRD 我面临的问题是,我需要检查要添加到数组中的对象是否实现了委托。 - NetDeveloper
@Jignesh - 你可以使用相同的大纲,但是不是测试类成员资格,而是测试协议符合性或选择器实现。如果这不明白,请发一个问题(而不是另一个评论)。 - CRD
为什么你的子类中还需要一个NSMutableArray realArray?MonomorphicArray本身就是一个NSMutableArray,难道你不能调用父类的super方法吗? - Paul Praet
3
NSArrayNSMutableArray是一个类簇,它们被设计成任何子类都必须为元素提供自己的存储方式,这可以是单独的NSArray/NSMutableArray实例(如此处所用)。请参阅NSArray文档中的“子类说明”。 - CRD
1
@BenC.R.Leggiero - 答案并不是“错误的”,实际上,如果你想要一个单态数组,答案在运行时会给你这个;而新的轻量级泛型让你更接近目标,但在编译时还没有完全到达。是的,语言的变化意味着它有点“过时”,因为它没有涵盖自写作以来发生的事情,我已经编辑添加了一些细节(在这种情况下这样做有一定的价值),但你经常会在SO上找到一些时间验证已经改变的东西。如果提供答案需要永久编辑,那么就会有更少的答案!历史也有价值。 :-) - CRD
显示剩余4条评论

29

自从Xcode 7以来,在Objective-C中就可以使用泛型了。

您可以将NSMutableArray声明为:

NSMutableArray <Wheel*> *wheels = [[NSMutableArray alloc] initWithArray:@[[Wheel new],[Wheel new]];
编译器会在您尝试将非Wheel对象放入数组中时发出警告。

我觉得你忘记在"wheels"之前加上一个* - NKorotkov
1
我不需要说 [[NSMutableArray<Wheel*> alloc] init...] 吗? - Ky -
1
我们可能都想要参数化类型,但不幸的是,我们需要阅读苹果的小字条款;这些被苹果称为“轻量级”泛型,本质上只存在于注释中以帮助Swift编译器。如果您尝试进行类型不匹配的操作,它们很遗憾并不总是“提醒您”。我已更新我的答案,提供了更多细节。 - CRD

10

可能我说错了(我是一个新手),但是我认为,如果你创建一个自定义协议并确保将要添加到数组中的对象遵循相同的协议,那么当你声明数组时,你可以使用

NSArray<Protocol Name>

那应该可以防止添加不符合所述协议的对象。


3
如果你不知道答案,为什么还要回答这个问题? - Roel
20
这被称为试图帮助其他程序员。 - Gravedigga
1
这是错误的,这种类型只允许你分配符合“协议名称”的NSArray子类。因此,它与数组对象本身有关,而不是其内容。你应该写成NSArray<id<Protocol Name>>来实现你最初的目标。 - DanSkeel

5

据我所知,在将任何对象添加到车轮可变数组之前,您必须添加一些检查标记。我要添加的对象是否是“wheel”类。如果是,则添加,否则不添加。

例如:

if([id isClassOf:"Wheel"] == YES)
{
[array addObject:id) }

类似这样的内容。我不记得确切的语法。


2
我同意 - 不要子类化NSMutableArray - 只需将其隐藏在您的车中,并使唯一的两个方法为addWheel:(Wheel*)aWheel和deleteWheel:。(例如)。 - Tom Andersen
当我说隐藏轮子数组时,我的意思是将其设置为私有,不要将其设置为属性等。您可能还需要一个-(NSArray*)wheels;调用,它会返回[NSArray arrayWithArray:wheels]; - Tom Andersen

3

我希望这会有所帮助(并且能够起作用... :P)

Wheel.h 文件:

@protocol Wheel
@end

@interface Wheel : NSObject
@property ...
@end

Car.h文件:

#import "Wheel.h"
@interface Car:NSObject  

{  
   NSString *model;  
   NSString *make;  
   NSMutableArray<Wheel, Optional> *wheels;  
}  
@end

Car.m文件:

#import "Car.h"
@implementation Car

-(id)init{
   if (self=[super init]){
   self.wheels = (NSMutableArray<Wheel,Optional>*)[NSMutableArray alloc]init];
   }
return self;
}
@end

1
如何初始化轮子? - Jagveer Singh
@JagveerSinghRajput,这是给你的 - 我为你编辑了我的答案 :) - Aviram Netanel

2

Xcode 7允许你定义数组、字典,甚至是你自己的类作为泛型。数组语法如下:

NSArray<NSString*>* array = @[@"hello world"];

1

我不相信使用NSMutableArray有现成的解决方法。你可以通过子类化和覆盖所有构造函数和插入方法来强制执行此操作,但这可能不值得。你希望通过这个做到什么?


0

这是不可能的。NSArray(无论可变还是不可变)将保存任何对象类型。您可以像Jim建议的那样创建自己的自定义子类。或者,如果您想要过滤一个数组以删除不符合所需类型的对象,则可以执行以下操作:

- (void)removeObjectsFromArray:(NSMutableArray *)array otherThanOfType:(Class)type
{
    int c = 0;
    while(c < [array length])
    {
        NSObject *object = [array objectAtIndex:c];
        if([object isKindOfClass:type])
          c++;
        else
          [array removeObjectAtIndex:c];
    }
}

...
[self removeObjectsFromArray:array otherThanOfType:[Car class]];

或者根据isKindOfClass:的结果做出其他判断,例如将包含汽车和轮子混合的数组分成两个数组,每个数组只包含一种对象。

0
这是我为了避免子类化NSMutableArray而做的事情:使用分类。通过这种方式,您可以拥有所需的参数和返回类型。请注意命名约定:用元素类的名称替换您将使用的每个方法中的“object”一词。 "objectAtIndex" 变成 "wheelAtIndex"等等。这样就没有名称冲突了。非常整洁。
typedef NSMutableArray WheelList;
@interface NSMutableArray (WheelList) 
- (wheel *) wheelAtIndex: (NSUInteger) index;
- (void) addWheel: (wheel *) w;
@end

@implementation NSMutableArray (WheelList)

- (wheel *) wheelAtIndex: (NSUInteger) index 
{  
    return (wheel *) [self objectAtIndex: index];  
}

- (void) addWheel: (wheel *) w 
{  
    [self addObject: w];  
} 
@end


@interface Car : NSObject
@property WheelList *wheels;
@end;


@implementation Car
@synthesize wheels;

- (id) init 
{
    if (self = [super init]) {
        wheels = [[WheelList alloc] initWithCapacity: 4];
    }
    return self;
}

@end

虽然这种方法可以为您提供类型,但它实际上并不限制数组的内容为该类型,因为您可以直接调用addObject: - 这意味着后续的wheelAtIndex:可能会将其他类型的对象强制转换为轮子类型,从而导致混乱。您可以将自己的方法与另一个答案中的MonomorphicArray相结合;只需使用具有类型化getter和setter的类扩展MonomorphicArray即可。 - CRD

0

如果您没有特定的对象,可以使用nsexception。

for (int i = 0; i<items.count;i++) {
 if([[items objectAtIndex:i] isKindOfClass:[Wheel class]])
 {
  // do something..!
 }else{
  [NSException raise:@"Invalid value" format:@"Format of %@ is invalid", items];
  // do whatever to handle or raise your exception.
 }
}

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