Is there any way to enforce typing on NSArray, NSMutableArray, etc.?
Asked Answered
Q

11

96

Can I make a NSMutableArray instance where all the elements are of type SomeClass?

Quail answered 16/3, 2009 at 6:51 Comment(1)
the same question: #5197946Barbicel
P
36

You could make a category with an -addSomeClass: method to allow compile-time static type checking (so the compiler could let you know if you try to add an object it knows is a different class through that method), but there's no real way to enforce that an array only contains objects of a given class.

In general, there doesn't seem to be a need for such a constraint in Objective-C. I don't think I've ever heard an experienced Cocoa programmer wish for that feature. The only people who seem to are programmers from other languages who are still thinking in those languages. If you only want objects of a given class in an array, only stick objects of that class in there. If you want to test that your code is behaving properly, test it.

Pickax answered 16/3, 2009 at 7:0 Comment(16)
I think that 'experienced Cocoa programmers' just don't know what they're missing -- experience with Java shows that type variables improve code comprehension and make more refactorings possible.Wilhelm
Well, Java's Generics support is heavily broken in it's own right, because they didn't put it in from the start...Edema
Gotta agree with @tgdavies. I miss the intellisense and refactoring capabilities I had with C#. When I want dynamic typing I can get it in C# 4.0. When I want strongly types stuff I can have that too. I've found there is a time and place for both of those things.Alys
There is a good degree of strong type checks in Objective-C. After awhile you will understand that you DON'T need such thing as generic NSArray. It's just not needed. period. Most of the time If you need it you havn't been programming in Objective-C long enough and just trying to fit your old programming model on a new language. People need to let go of things. (and yeah I have had many years of C# under my belt if that matters)Aureomycin
@charkrit What is it about Objective-C that makes it 'not necessary'? Did you feel it was necessary when you were using C#? I hear a lot of people saying you don't need it in Objective-C but I think these same people think you don't need it in any language, which makes it an issue of preference/style, not of necessity.Benevento
@bacar: Some languages have more restrictive static type systems, where things need to be typed correctly or you can't call their methods.Pickax
@Pickax hm. That's nothing you can't do with a cast though, right? I.e. that's not an argument for a stronger need for generics in statically typed systems. I believe people who like generics with static typing like it because it gives them extra safety, not because it reduces typing.Benevento
@bacar: You can't cast unless you know the type of the receiver. My point is, Objective-C accesses everything through pointers that can be converted to any other type without complaint from the compiler. It is not a typesafe langauge. Generics would give you a little bit more type safety, but in a language as pervasively dynamic as Objective-C, it's a drop in the pond. The only other languages with dynamic typing as pervasive as Objective-C's don't have static type systems at all — JavaScript, Python, Ruby, etc. Neither Java nor C# was born with an id type — generics are their equivalent.Pickax
Isn't this about allowing your compiler to actually help you find problems. Sure you can say "If you only want objects of a given class in an array, only stick objects of that class in there." But if tests are the only way to enforce that, you are at a disadvantage. The further away from writing the code you find a problem, the more costly that problem is.Pinon
@GreenKiwi: Yes, in a language with a type system that permits this, it can help you find problems. Objective-C does not have such a type system, so the degree to which the compiler can help you is limited. And in practice, this does not seem to be a very costly problem for most people. Since getting comfortable in Objective-C, how many hours have you spent debugging problems that arise from having heterogeneous arrays? Is it even one hour? I'm a fan of helpful static type systems, but Objective-C isn't such a language. At any rate, this is the correct answer for Objective-C, like it or not.Pickax
The only things to be learned from Java are from its mistakes.During
@Pickax -- not true re: other languages with a basis in dynamic typing not having static type systems. See ActionScript3's Vector class, and also Haxe. help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/… haxe.orgCarnahan
It won't enforce contents, but typedef NSMutableArray *SomeClassList at least helps with code readability and consistency (especially for return values and method parameters) and gives some compile-time warnings with very little overhead.Accrete
Definitely it is should be a feature of language. If it is not there, justifying its absence is not worthwhile.Yahweh
So not necessary, in fact... that Apple added the feature, finally. This answer no longer reflects official opinion on the matter (thank goodness).Covered
This answer did not age well. Hello, Swift!Embitter
H
151

Nobody's put this up here yet, so I'll do it!

Tthis is now officially supported in Objective-C. As of Xcode 7, you can use the following syntax:

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

Note

It's important to note that these are compiler warnings only and you can technically still insert any object into your array. There are scripts available that turn all warnings into errors which would prevent building.

Hydroid answered 25/6, 2015 at 2:29 Comment(12)
I am being lazy here, but why is this only available in XCode 7? We can use the nonnull in XCode 6 and as far as I remember, they were introduced at the same time. Also, does the usage of such concepts depend on the XCode version or on the iOS version?Lasonyalasorella
@Lasonyalasorella - nullability came in 6, you are correct, but ObjC generics weren't introduced until Xcode 7.Hydroid
I am pretty sure it depends on Xcode version only. The generics are compiler warnings only and are not indicated at runtime. I'm pretty sure you could compile to whatever Os you want.Hydroid
Got it! Thanks for the detailed explanation. This actually should be the right answer as of XCode 7!Lasonyalasorella
@Hydroid how would this look if instead of MyClass * you had an array of some unknown id<MyProtocol> objects? Is that possible?Vantassel
@DeanKelly - You could do that like this: @property (nonatomic, strong) NSArray<id<SomeProtocol>>* protocolObjects; Looks a little clunky, but does the trick!Hydroid
NSArray <NSString *> *array = @[@"", @1]; NSLog(@"Array: %@", array); Array: ( "", 1 )Guillemot
@Guillemot - I'm not sure I understand your comment, but I believe you're demonstrating that although you type constrained to NSString, you inserted an NSNumber. I'll add this to the answer, but its important to remember that these constraints are compiler warnings only at the ObjC level. If you ignore them, the system will allow you to.Hydroid
@Hydroid exactly - this is only clever compiler warnings. Runtime still as same as it was.Guillemot
@Logan, there is not only the set of scripts, that prevent building in case of any warning detected. Xcode has a perfect mechanism named "Configuration". Check this out boredzo.org/blog/archives/2009-11-07/warningsGuillemot
I speculate __kindof might allow for subclasses? I noticed in the iOS 9.0 UINavigationController.h:64: @property(nonatomic,copy) NSArray<__kindof UIViewController *> *viewControllers;Vesta
Is it possible to get somehow the information about the type in the array during runtime? e.g. using property_getAttributes does not work, it just returns NSArray.Upstream
G
53

This is a relatively common question for people transitioning from strongly type languages (like C++ or Java) to more weakly or dynamically typed languages like Python, Ruby, or Objective-C. In Objective-C, most objects inherit from NSObject (type id) (the rest inherit from an other root class such as NSProxy and can also be type id), and any message can be sent to any object. Of course, sending a message to an instance that it does not recognize may cause a runtime error (and will also cause a compiler warning with appropriate -W flags). As long as an instance responds to the message you send, you may not care what class it belongs to. This is often referred to as "duck typing" because "if it quacks like a duck [i.e. responds to a selector], it is a duck [i.e. it can handle the message; who cares what class it is]".

You can test whether an instance responds to a selector at run time with the -(BOOL)respondsToSelector:(SEL)selector method. Assuming you want to call a method on every instance in an array but aren't sure that all instances can handle the message (so you can't just use NSArray's -[NSArray makeObjectsPerformSelector:], something like this would work:

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

If you control the source code for the instances which implement the method(s) you wish to call, the more common approach would be to define a @protocol that contains those methods and declare that the classes in question implement that protocol in their declaration. In this usage, a @protocol is analogous to a Java Interface or a C++ abstract base class. You can then test for conformance to the entire protocol rather than response to each method. In the previous example, it wouldn't make much of a difference, but if you were calling multiple methods, it might simplify things. The example would then be:

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

assuming MyProtocol declares myMethod. This second approach is favored because it clarifies the intent of the code more than the first.

Often, one of these approaches frees you from caring whether all objects in an array are of a given type. If you still do care, the standard dynamic language approach is to unit test, unit test, unit test. Because a regression in this requirement will produce a (likely unrecoverable) runtime (not compile time) error, you need to have test coverage to verify the behavior so that you don't release a crasher into the wild. In this case, peform an operation that modifies the array, then verify that all instances in the array belong to a given class. With proper test coverage, you don't even need the added runtime overhead of verifying instance identity. You do have good unit test coverage, don't you?

Greenheart answered 16/3, 2009 at 18:27 Comment(3)
Unit testing is not a substitute for a decent type system.Audiogenic
Yeah, who needs the tooling that typed arrays would afford. I'm sure @BarryWark (and anyone else who has touched any codebase that he needs to use, read, understand and support) has 100% code coverage. However I bet you don't use raw ids except where necessary, any more than Java coders pass around Objects. Why not? Don't need it if you've got unit tests? Because it's there and makes your code more maintainable, as would typed arrays. Sounds like people invested in the platform not wishing to concede a point, and therefore inventing reasons why this omission is in fact a benefit.Illlooking
"Duck typing"?? that's hilarious! never heard that one before.Luxe
P
36

You could make a category with an -addSomeClass: method to allow compile-time static type checking (so the compiler could let you know if you try to add an object it knows is a different class through that method), but there's no real way to enforce that an array only contains objects of a given class.

In general, there doesn't seem to be a need for such a constraint in Objective-C. I don't think I've ever heard an experienced Cocoa programmer wish for that feature. The only people who seem to are programmers from other languages who are still thinking in those languages. If you only want objects of a given class in an array, only stick objects of that class in there. If you want to test that your code is behaving properly, test it.

Pickax answered 16/3, 2009 at 7:0 Comment(16)
I think that 'experienced Cocoa programmers' just don't know what they're missing -- experience with Java shows that type variables improve code comprehension and make more refactorings possible.Wilhelm
Well, Java's Generics support is heavily broken in it's own right, because they didn't put it in from the start...Edema
Gotta agree with @tgdavies. I miss the intellisense and refactoring capabilities I had with C#. When I want dynamic typing I can get it in C# 4.0. When I want strongly types stuff I can have that too. I've found there is a time and place for both of those things.Alys
There is a good degree of strong type checks in Objective-C. After awhile you will understand that you DON'T need such thing as generic NSArray. It's just not needed. period. Most of the time If you need it you havn't been programming in Objective-C long enough and just trying to fit your old programming model on a new language. People need to let go of things. (and yeah I have had many years of C# under my belt if that matters)Aureomycin
@charkrit What is it about Objective-C that makes it 'not necessary'? Did you feel it was necessary when you were using C#? I hear a lot of people saying you don't need it in Objective-C but I think these same people think you don't need it in any language, which makes it an issue of preference/style, not of necessity.Benevento
@bacar: Some languages have more restrictive static type systems, where things need to be typed correctly or you can't call their methods.Pickax
@Pickax hm. That's nothing you can't do with a cast though, right? I.e. that's not an argument for a stronger need for generics in statically typed systems. I believe people who like generics with static typing like it because it gives them extra safety, not because it reduces typing.Benevento
@bacar: You can't cast unless you know the type of the receiver. My point is, Objective-C accesses everything through pointers that can be converted to any other type without complaint from the compiler. It is not a typesafe langauge. Generics would give you a little bit more type safety, but in a language as pervasively dynamic as Objective-C, it's a drop in the pond. The only other languages with dynamic typing as pervasive as Objective-C's don't have static type systems at all — JavaScript, Python, Ruby, etc. Neither Java nor C# was born with an id type — generics are their equivalent.Pickax
Isn't this about allowing your compiler to actually help you find problems. Sure you can say "If you only want objects of a given class in an array, only stick objects of that class in there." But if tests are the only way to enforce that, you are at a disadvantage. The further away from writing the code you find a problem, the more costly that problem is.Pinon
@GreenKiwi: Yes, in a language with a type system that permits this, it can help you find problems. Objective-C does not have such a type system, so the degree to which the compiler can help you is limited. And in practice, this does not seem to be a very costly problem for most people. Since getting comfortable in Objective-C, how many hours have you spent debugging problems that arise from having heterogeneous arrays? Is it even one hour? I'm a fan of helpful static type systems, but Objective-C isn't such a language. At any rate, this is the correct answer for Objective-C, like it or not.Pickax
The only things to be learned from Java are from its mistakes.During
@Pickax -- not true re: other languages with a basis in dynamic typing not having static type systems. See ActionScript3's Vector class, and also Haxe. help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/… haxe.orgCarnahan
It won't enforce contents, but typedef NSMutableArray *SomeClassList at least helps with code readability and consistency (especially for return values and method parameters) and gives some compile-time warnings with very little overhead.Accrete
Definitely it is should be a feature of language. If it is not there, justifying its absence is not worthwhile.Yahweh
So not necessary, in fact... that Apple added the feature, finally. This answer no longer reflects official opinion on the matter (thank goodness).Covered
This answer did not age well. Hello, Swift!Embitter
C
11

You could subclass NSMutableArray to enforce type safety.

NSMutableArray is a class cluster, so subclassing isn't trivial. I ended up inheriting from NSArray and forwarded invocations to an array inside that class. The result is a class called ConcreteMutableArray which is easy to subclass. Here's what I came up with:

Update: checkout this blog post from Mike Ash on subclassing a class cluster.

Include those files in your project, then generate any types you wish by using macros:

MyArrayTypes.h

CUSTOM_ARRAY_INTERFACE(NSString)
CUSTOM_ARRAY_INTERFACE(User)

MyArrayTypes.m

CUSTOM_ARRAY_IMPLEMENTATION(NSString)
CUSTOM_ARRAY_IMPLEMENTATION(User)

Usage:

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

Other Thoughts

  • It inherits from NSArray to support serialization/deserialization
  • Depending on your taste, you may want to override/hide generic methods like

    - (void) addObject:(id)anObject

Chibcha answered 8/1, 2012 at 21:35 Comment(1)
Nice but for now it lacks strong typing by overriding some methods. Currently it's only weak typing.Massachusetts
G
7

Have a look at https://github.com/tomersh/Objective-C-Generics, a compile-time (preprocessor-implemented) generics implementation for Objective-C. This blog post has a nice overview. Basically you get compile-time checking (warnings or errors), but no runtime penalty for generics.

Greenheart answered 8/8, 2013 at 2:56 Comment(1)
I tried it out, very good idea, but sadly buggy and it doesn't check the added elements.Levenson
I
4

This Github Project implements exactly that functionality.

You can then use the <> brackets, just like you would in C#.

From their examples:

NSArray<MyClass>* classArray = [NSArray array];
NSString *name = [classArray lastObject].name; // No cast needed
Igbo answered 5/11, 2013 at 11:3 Comment(0)
V
1

2020, straightforward answer. It just so happened that I need a mutable array with type of NSString.

Syntax:

Type<ArrayElementType *> *objectName;

Example:

@property(nonatomic, strong) NSMutableArray<NSString *> *buttonInputCellValues;
Vassell answered 16/9, 2020 at 6:44 Comment(0)
T
0

A possible way could be subclassing NSArray but Apple recommends not to do it. It is simpler to think twice of the actual need for a typed NSArray.

Till answered 16/3, 2009 at 9:42 Comment(1)
It save time to have static type checking at compiling time, editing is even better. Especially helpful when you are writing lib for long term usage.Yoko
T
0

I created a NSArray subclass that is using an NSArray object as backing ivar to avoid issues with the class-cluster nature of NSArray. It takes blocks to accept or decline adding of an object.

to only allow NSString objects, you can define an AddBlock as

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

You can define a FailBlock to decide what to do, if an element failed the test — fail gracefully for filtering, add it to another array, or — this is default — raise an exception.

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 it like:

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

This is just an example code and was never used in real world application. to do so it probably needs mor NSArray method implemented.

Thrasher answered 20/3, 2013 at 14:37 Comment(0)
M
0

If you mix c++ and objective-c (i.e. using mm file type), you can enforce typing using pair or tuple. For example, in the following method, you can create a C++ object of type std::pair, convert it to an object of OC wrapper type (wrapper of std::pair that you need to define), and then pass it to some other OC method, within which you need to convert the OC object back to C++ object in order to use it. The OC method only accepts the OC wrapper type, thus ensuring type safety. You can even use tuple, variadic template, typelist to leverage more advanced C++ features to facilitate type safety.

- (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];
}
Moore answered 11/12, 2017 at 19:22 Comment(0)
S
0

my two cents to be a bit "cleaner":

use typedefs:

typedef NSArray<NSString *> StringArray;

in code we can do:

StringArray * titles = @[@"ID",@"Name", @"TYPE", @"DATE"];
Seducer answered 23/12, 2018 at 11:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.