NSMutableArray - force the array to hold specific object type only

54,086

Solution 1

Update in 2015

This answer was first written in early 2011 and began:

What we really want is parametric polymorphism so you could declare, say, NSMutableArray<NSString>; but alas such is not available.

In 2015 Apple apparently changed this with the introduction of "lightweight generics" into Objective-C and now you can declare:

NSMutableArray<NSString *> *onlyStrings = [NSMutableArray new];

But all is not quite what it seems, notice the "lightweight"... Then notice that the initialisation part of the above declaration does not contain any generic notation. While Apple have introduced parametric collections, and adding a non-string directly to the above array, onlyStrings, as in say:

[onlyStrings addObject:@666]; // <- Warning: Incompatible pointer types...

will illicit the warning as indicated, the type security is barely skin deep. Consider the method:

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

and the code fragment in another method of the same class:

NSMutableArray<NSString *> *oops = [NSMutableArray new];
[self push:@"asda" onto:oops]; // add a string, fine
[self push:@42 onto:oops];     // add a number, no warnings...

What Apple have implemented is essentially a hinting system to assist with automatic inter-operation with Swift, which does have a flavour of type-safe generics. However on the Objective-C side, while the compiler provides some extra hints the system is "lightweight" and type-integrity is still ultimately down to the programmer - as is the Objective-C way.

So which should you use? The new lightweight/pseudo generics, or devise your own patterns for your code? There really is no right answer, figure out what makes sense in your scenario and use it.

For example: If you are targeting interoperation with Swift you should use the lightweight generics! However if the type integrity of a collection is important in your scenario then you could combine the lightweight generics with your own code on the Objective-C side which enforces the type integrity that Swift will on its side.

The Remainder of the 2011 Answer

As another option here is a quick general subclass of NSMutableArray which you init with the kind of object you want in your monomorphic array. This option does not give you static type-checking (in as much as you ever get it in Obj-C), you get runtime exceptions on inserting the wrong type, just as you get runtime exceptions for index out of bounds etc.

This is not thoroughly tested and assumes the documentation on overriding NSMutableArray is correct...

@interface MonomorphicArray : NSMutableArray
{
    Class elementClass;
    NSMutableArray *realArray;
}

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

@end

And the implementation:

@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

Use as:

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

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

Solution 2

With XCode 7 generics are now available in Objective-C!

So you can declare your NSMutableArray as:

NSMutableArray <Wheel*> *wheels = [[NSMutableArray alloc] initWithArray:@[[Wheel new],[Wheel new]];

The compiler will give you a warning if you try to put non-Wheel object in your array.

Solution 3

I could be wrong (I'm a noob), but I think, if you create a custom protocol and make sure the objects you are adding to the array follow the same protocol, then when you declare the array you use

NSArray<Protocol Name>

That should prevent objects being added that do not follow the said protocol.

Solution 4

as per i know.. before you added any object in wheels mutableArray, u have to add some check mark. Is the object which i am adding is class "wheel". if it is then add, other wise not.

Example:

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

Something like this. i dont remember the exact syntax.

Solution 5

I hope this will help (and work... :P )

Wheel.h file:

@protocol Wheel
@end

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

Car.h file:

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

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

Car.m file:

#import "Car.h"
@implementation Car

-(id)init{
   if (self=[super init]){
   self.wheels = (NSMutableArray<Wheel,Optional>*)[NSMutableArray alloc]init];
   }
return self;
}
@end
Share:
54,086
Tuyen Nguyen
Author by

Tuyen Nguyen

work for a better world....

Updated on July 05, 2022

Comments

  • Tuyen Nguyen
    Tuyen Nguyen almost 2 years

    Is there a way to force NSMutableArray to hold one specific object type only?

    I have classes definitions as follow:

    @interface Wheel:NSObject  
    {    
      int size;  
      float diameter;  
    }  
    @end  
    
    
    @interface Car:NSObject  
    {  
       NSString *model;  
       NSString *make;  
       NSMutableArray *wheels;  
    }  
    @end
    

    How can I force wheels array to hold Wheel objects only with code? (and absolutely not other objects)

  • Tom Andersen
    Tom Andersen about 13 years
    I agree - don't subclass nsmutablearray - just hide it in your car, and make the only two methods addWheel:(Wheel*)aWheel and deleteWheel:. (for example).
  • Tom Andersen
    Tom Andersen about 13 years
    When I say hide the wheel array, I mean make it private, don't make it a property, etc. You might also need a -(NSArray*)wheels; call, which does this return [NSArray arrayWithArray:wheels];
  • Tuyen Nguyen
    Tuyen Nguyen about 13 years
    Thanks, that's what I'm looking for.
  • vikingosegundo
    vikingosegundo over 12 years
    +1 — based on this code I got the idea to use blocks to test, if a certain object should be added to an array. This can be a membership for a class, a value for a property of the object or virtual anything else. Please have a look: stackoverflow.com/questions/8190530/…
  • CRD
    CRD over 11 years
    @bendytree - using macros to generate one class per type for static typing checking is an interesting idea. However you don't need the forward invocation stuff at all, just subclass NSMutableArray properly as in the above example and it will be much more efficient. So combine your macros with the above and you'll get the best of both.
  • CRD
    CRD about 11 years
    While this works to give you the types it doesn't actually restrict the contents of the array to be the type as you can just call addObject: directly - which means a subsequent wheelAtIndex: could be casting something other than a wheel as a wheel and chaos will ensue. You can combine your approach with MonomorphicArray from the other answer; just extend MonomorphicArray with a class with typed getters and setters.
  • Roel
    Roel almost 11 years
    Why answer this if you don't know it?
  • Gravedigga
    Gravedigga almost 11 years
    It's known as attempting to help fellow programmers.
  • NetDeveloper
    NetDeveloper over 10 years
    @CRD I am facing problem where I need to check if object to be added to array is implementing a delegate or not...
  • CRD
    CRD over 10 years
    @Jignesh - You can use the same outline but instead of testing for class membership you'd be testing for protocol conformance or selector implementation. If that doesn't make sense post a question (not another comment).
  • Paul Praet
    Paul Praet over 10 years
    Why do you still need a NSMutableArray realArray in your subclass. MonomorphicArray IS an NSMutableArray.. Can't you call the super method of the parent class ?
  • CRD
    CRD over 10 years
    @PaulPraet - NSArray and NSMutableArray are a class cluster and the way they are designed any subclass must provide its own storage for the elements - which can be a separate instance of NSArray/NSMutableArray as used here. See "Subclassing Notes" in the NSArray documentation.
  • johnMa
    johnMa over 10 years
    Have you test your code?? why NSArray conform Person protocol can do the check?
  • lbsweek
    lbsweek over 10 years
    the compiler would only give a warning if your object did not conform to the protocol.
  • johnMa
    johnMa over 10 years
    No, it won't, In fact you will get Incompatible pointer types initializing 'NSArray<Person> *' with an expression of type 'NSArray *',even when you insert Person in it.
  • Jagveer Singh Rajput
    Jagveer Singh Rajput about 9 years
    how to initialize wheels?
  • Aviram Netanel
    Aviram Netanel about 9 years
    @JagveerSinghRajput here you go - I edited my answer for you :)
  • NKorotkov
    NKorotkov over 8 years
    I think you've forgotten an * before wheels.
  • Ky -
    Ky - over 8 years
    Reluctantly voting down since this answer is now outdated and incorrect. I hope OP changes the accepted answer.
  • Ky -
    Ky - over 8 years
    Don't I need to say [[NSMutableArray<Wheel*> alloc] init...]?
  • CRD
    CRD over 8 years
    We might all want parametric types, unfortunately we need to read Apple's small print; these are what Apple term "lightweight" generics and essentially only exist as comments to assist the Swift compiler. They unfortunately do not always "give you a warning" if you attempt an ill-typed operation, I've updated my answer to provide some more detail.
  • CRD
    CRD over 8 years
    @BenC.R.Leggiero - The answer isn't "incorrect", indeed if you want a monomorphic array the answer gives you that at runtime; while the new lightweight generics get you closer, but not all the way there, at compile time. Yes language changes means it is a bit "outdated" as it doesn't cover stuff that has happened since it was written, I've edited to add some details (as doing so has some merit in this case) but you'll often find stuff on SO whose veracity time has changed. If providing answers incurred editing in perpetuity there would be far fewer answers! There can be value in history. :-)
  • Ky -
    Ky - over 8 years
    @CRD Thank you so much for providing the modern approach; I voted it back up! I agree there's value in history, but indeed answering on SO implies a dedication to ensuring your answer is up-to-date, since no one is allowed to ask a question that's already been asked, and your Google rank will likely only climb.
  • DanSkeel
    DanSkeel over 5 years
    This is wrong, this type allows you only to assign subclasses of NSArray that conform to Protocol Name. So it's related to the array object itself and not its contents. You should write NSArray<id<Protocol Name>> to achieve your initial goal.