13

Having a look at the block of NSArray creation methods in NSArray.h.

Is there a legitimate reason for the methods that are returning id to not return instancetype?

Apple even went through the effort of adding inline comments to let us know that id in this case returns an NSArray.

@interface NSArray (NSArrayCreation)

+ (instancetype)array;
+ (instancetype)arrayWithObject:(id)anObject;
+ (instancetype)arrayWithObjects:(const id [])objects count:(NSUInteger)cnt;
+ (instancetype)arrayWithObjects:(id)firstObj, ... NS_REQUIRES_NIL_TERMINATION;
+ (instancetype)arrayWithArray:(NSArray *)array;

- (instancetype)init;   /* designated initializer */
- (instancetype)initWithObjects:(const id [])objects count:(NSUInteger)cnt; /* designated   initializer */

- (instancetype)initWithObjects:(id)firstObj, ... NS_REQUIRES_NIL_TERMINATION;
- (instancetype)initWithArray:(NSArray *)array;
- (instancetype)initWithArray:(NSArray *)array copyItems:(BOOL)flag;

+ (id /* NSArray * */)arrayWithContentsOfFile:(NSString *)path;
+ (id /* NSArray * */)arrayWithContentsOfURL:(NSURL *)url;
- (id /* NSArray * */)initWithContentsOfFile:(NSString *)path;
- (id /* NSArray * */)initWithContentsOfURL:(NSURL *)url;

@end

The only thing I could come up with these particular methods was this guidance from Apple

"The array representation at the location identified by aURL must contain only property list >objects (NSString, NSData, NSArray, or NSDictionary objects). The objects contained by this >array are immutable, even if the array is mutable."

However, this still to me doesn't explain the use of id over instancetype as they are still allowing NSArray sublclasses to return their own instancetype

NSDictionary follows the exact same pattern, where creating a dictionary with the contents of a file or URL uses id and all other creation methods use instancetype

- (instancetype)initWithObjectsAndKeys:(id)firstObject, ... NS_REQUIRES_NIL_TERMINATION;
- (instancetype)initWithDictionary:(NSDictionary *)otherDictionary;
- (instancetype)initWithDictionary:(NSDictionary *)otherDictionary copyItems:(BOOL)flag;
- (instancetype)initWithObjects:(NSArray *)objects forKeys:(NSArray *)keys;

+ (id /* NSDictionary * */)dictionaryWithContentsOfFile:(NSString *)path;
+ (id /* NSDictionary * */)dictionaryWithContentsOfURL:(NSURL *)url;
- (id /* NSDictionary * */)initWithContentsOfFile:(NSString *)path;
- (id /* NSDictionary * */)initWithContentsOfURL:(NSURL *)url;

I am aware that Apple is just getting around to replacing id in foundation classes to instancetype but do the patterned inconsistencies in its usage within single classes act as guidance towards our own usage, or did they just not get around to finishing classes that they began working on?

to expand just a bit I wanted to explore the return type of dictionaryWithContentsOfFile when called on NSMutableDictionary

NSString * plistPath = [[NSBundle mainBundle] pathForResource:@"myFile" ofType:@"plist"];
NSMutableDictionary *myDictionary = [NSMutableDictionary dictionaryWithContentsOfFile:plistPath]; 
    if ([ myDictionary isKindOfClass:[NSMutableDictionary class]])
    {
        NSLog(@"This is a mutable dictionary why id and not instancetype?");
        [myDictionary setObject:@"I can mutate the dictionary" forKey:@"newKey"];
    }
NSLog (@"%@", myDictionary[@"newKey"]); 
    return YES;
} 

The following was output to my console:

This is a mutable dictionary why id and not instancetype?

I can mutate the dictionary

Therefore, I am able to add new keys and objects to the dictionary.

Community
  • 1
  • 1
altyus
  • 606
  • 4
  • 13
  • 3
    a) Why does it matter to you? b) Objective-C has thousands of rough edges and irregularities, largely for historical reasons. – Hot Licks Feb 28 '14 at 12:53
  • 2
    _"but do the patterned inconsistencies in its usage within single classes act as guidance towards our own usage, or did they just not get around to finishing classes that they began working on?"_ Inquiring minds want to know. – Jeff Loughlin Feb 28 '14 at 12:55
  • You may wanna take a look http://stackoverflow.com/questions/8972221/would-it-be-beneficial-to-begin-using-instancetype-instead-of-id – Basheer_CAD Feb 28 '14 at 13:08
  • Yes, I've been all through that post, and that kind of just backs up the question I'm asking. I would assert that it would make sense for them to be using instancetype in methods such as this `+ (id /* NSDictionary * */)dictionaryWithContentsOfFile:(NSString *)path;` but they've clearly gone through the class and left a few that make sense to return instancetype as id. – altyus Feb 28 '14 at 13:12
  • I'm curious as to why you were surprised that NSMutableDictionary dictionaryWithContentsOfFile: gave you an NSMutableDictionary. Regardless of whether the formal return type is `id` or `instancetype` this should be the case. – Hot Licks Feb 28 '14 at 22:06
  • 1
    (And I'm guessing that the decision to use one or the other is based on what the Xcode syntax checker will do. `id` is treated as "trust me" and no type mismatches or unknown methods are diagnosed, while `instancetype` tells the syntax checker to assume the return type matches the "target" class, performing type checking accordingly. So `id` would be used when the syntax checker might be confused by `instancetype` for some reason.) – Hot Licks Feb 28 '14 at 22:12
  • @Hot Licks I'm not surprised that the return type is NSMutableDictionary, that's the expected behavior. I'm wondering why Apple is using instancetype instead of id when they've used instancetype in all of the other constructors. Can you think of an example where these methods would ever return anything other than instancetype? Isn't the entire point of adding instancetype to LLVM to be as explicit as possible while still allowing dynamism for subclassing? – altyus Mar 01 '14 at 02:44
  • 1
    You didn't read the above comment? – Hot Licks Mar 01 '14 at 04:08
  • @altyus I checked your code. Seems like you can change objects that is already exist in your dictionary but you can not add new objects to it. – Basheer_CAD Mar 01 '14 at 19:52

2 Answers2

5

Ok, so to answer this question, first we need to know what is the class cluster design pattern ?

From Apple's documentations:

Class clusters are a design pattern that the Foundation framework makes extensive use of. Class clusters group a number of private concrete subclasses under a public abstract superclass. The grouping of classes in this way simplifies the publicly visible architecture of an object-oriented framework without reducing its functional richness. Class clusters are based on the Abstract Factory design pattern.

So the super class will decide what type we will have for our newly created object

Now because these methods are shared between NSArray and NSMutableArray, the results could be different, then they return id, because we don't know what object will be returned.(mutableArray or immutableArray).

+ (id /* NSArray * */)arrayWithContentsOfFile:(NSString *)path;
+ (id /* NSArray * */)arrayWithContentsOfURL:(NSURL *)url;
- (id /* NSArray * */)initWithContentsOfFile:(NSString *)path;
- (id /* NSArray * */)initWithContentsOfURL:(NSURL *)url;

These methods return only NSArray if the message was sent to NSArray and NSMutableArray if the method was sent to NSMutableArray. Thats why they return instancetype

+ (instancetype)arrayWithObjects:(id)firstObj, ... NS_REQUIRES_NIL_TERMINATION;
+ (instancetype)arrayWithArray:(NSArray *)array;

- (instancetype)init;

Ok, so we said that the methods above return only instance type of the receiver. But what if we want arrayWithArray method to always return immutableArray no matter who is the receiver ?

That means NSMutableArray will get different type than instanceType, because NSArray is not of NSMutableArray type, in this case we would change the method to be like this:

// from
+ (instancetype)arrayWithArray:(NSArray *)array;
// to
+ (id)arrayWithArray:(NSArray *)array;

We say now return id, despite the object's type.

UPDATE:
Example similar to your code

NSString *filePath = [[NSBundle mainBundle]pathForResource:@"myFile" ofType:@"plist"];
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithContentsOfFile:filePath];
NSMutableDictionary *dict2 = [[NSMutableDictionary alloc] init];
NSLog(@"%@", NSStringFromClass([dict class]));  // prints __NSCFDictionary  // converted to immutable
NSLog(@"%@", NSStringFromClass([dict2 class])); // prints __NSDictionaryM, its mutable

[dict setObject:@"obj" forKey:@"key"];  // this will do nothing, because its immutable, we can't add new object

Here is what Apple say about using isKindOfClass: to check the mutability of class cluster

Be careful when using this method on objects represented by a class cluster. Because of the nature of class clusters, the object you get back may not always be the type you expected. If you call a method that returns a class cluster, the exact type returned by the method is the best indicator of what you can do with that object. For example, if a method returns a pointer to an NSArray object, you should not use this method to see if the array is mutable, as shown in the following code:

// DO NOT DO THIS! 
if ([myArray isKindOfClass:[NSMutableArray class]])
{
   // Modify the object 
}

Link: https://developer.apple.com/library/ios/documentation/cocoa/reference/foundation/Protocols/NSObject_Protocol/Reference/NSObject.html#//apple_ref/occ/intfm/NSObject/isKindOfClass:

Basheer_CAD
  • 4,908
  • 24
  • 36
  • 1
    I was thinking along these lines, perhaps Apple is returning an NSArray even if I call arrayWithContentsOfFile on an NSMutable Array, but that is simply not the case. See my test `NSString * plistPath = [[NSBundle mainBundle] pathForResource:@"myFile" ofType:@"plist"]; if ([[NSMutableDictionary dictionaryWithContentsOfFile:plistPath] isKindOfClass: [NSMutableDictionary class]]) { NSLog(@"This is a mutable dictionary why id and not instancetype?"); } ` An NSMutableDictionary is returned NOT an NSDictionary. The same holds true for the other methods that return `id` – altyus Feb 28 '14 at 14:07
  • @altyus, try to do the same with NSDictionary, will you get a NSMutableDictionary ? – Basheer_CAD Feb 28 '14 at 14:11
  • 1
    No, calling `+dictionaryWithContentsOfFile:` on an NSDictionary returns an NSDictionary as you'd expect, while on NSMutableDictionary, returns an NSMutableDictionary. Which to my eyes is the ideal use case for instancetype. – altyus Feb 28 '14 at 14:43
  • I am able to add new keys for new objects but not mutate objects for existing keys as per Apple's documentation. `The objects contained by this dictionary are immutable, even if the dictionary is mutable.` However, I'm not seeing that the dictionary itself is immutable. Check and make sure that the line that you said does nothing actually does nothing, because that's not the result I'm seeing. I'll add my test code to the question. – altyus Feb 28 '14 at 19:40
  • @altyus, I have tested my code before posting it, but anyway I will test your code and comeback to you. Thanks :) – Basheer_CAD Feb 28 '14 at 20:36
  • 1
    You make no sense. You state, for `arrayWithContentsOfFile`, that "we don't know what object will be returned", but for `arrayWithObjects` it will be the "instance type of the receiver". What is the difference between those two scenarios? Both are class methods where the target class is explicitly specified (since it's the called class). What makes them different??? (Have you actually tried to run your example code?) – Hot Licks Mar 01 '14 at 20:13
  • Of course I have tested my example code (have you?). if (instancetype)arrayWithArray called on NSMutableArray and the receiver is NSArray then the compiler will show warning, but if the return type is id then no compiler warnings. What you think that means. @HotLicks – Basheer_CAD Mar 01 '14 at 20:22
  • That isn't what I asked. Did you RUN the example?? If, as you assert, it actually returns an immutable, the `setObject` will throw an exception (but it won't). All you've shown is that Xcode will diagnose the case where you attempt to do `setObject` on something *declared* as immutable. – Hot Licks Mar 01 '14 at 20:28
  • @HotLicks, I was expecting getting an exception on setObject:forKey:, but actually nothing happened. The object wasn't added and the app did not crash :). Any ideas ? – Basheer_CAD Mar 01 '14 at 20:41
  • Well, pretty obviously, `dict` was nil because your file was no good. – Hot Licks Mar 01 '14 at 21:44
  • @HotLicks, dict was not nil, see the log in the example – Basheer_CAD Mar 02 '14 at 14:25
  • 1
    @Basheer_CAD Just add this line to your test : `NSLog(@"%@", [dict objectForKey@"key"]);` You will see that the key/value pair has been added to the dictionary, so `dict` is mutable. `__NSCFDictionary`class doesn't mean that the dictionary is immutable, but a CoreFoundation object. Read [About Mutability](http://blog.bignerdranch.com/803-about-mutability/) – Emmanuel Mar 03 '14 at 14:51
  • Yes, I wrote in my answer the line does nothing, means the dictionary is mutable. Thanks for the reading buddy :) – Basheer_CAD Mar 03 '14 at 14:53
  • @Basheer_CAD On which OS version ? Because on OS 10.8 your code add the key/value to `dict`. ("does nothing, means the dictionary is mutable" immutable you mean ??) – Emmanuel Mar 03 '14 at 15:15
  • oops, sorry yes, immutable – Basheer_CAD Mar 03 '14 at 15:20
  • @altyus I found more information that can answer your question. see my updated answer – Basheer_CAD Mar 16 '14 at 12:58
1

Class clusters may return a class other than the class you create them from. This is generally true with Foundation classes as they will create some kind of optimized class in many cases. That class will still return YES from isKindOfClass:

Also some toll free bridged classes return a class that is shared between Foundation and Core Foundation. One example is NSCFString.

uchuugaka
  • 12,679
  • 6
  • 37
  • 55