Would it be beneficial to begin using instancetype instead of id?
Solution 1:
Yes, there are benefits to using instancetype
in all cases where it applies. I'll explain in more detail, but let me start with this bold statement: Use instancetype
whenever it's appropriate, which is whenever a class returns an instance of that same class.
In fact, here's what Apple now says on the subject:
In your code, replace occurrences of
id
as a return value withinstancetype
where appropriate. This is typically the case forinit
methods and class factory methods. Even though the compiler automatically converts methods that begin with “alloc,” “init,” or “new” and have a return type ofid
to returninstancetype
, it doesn’t convert other methods. Objective-C convention is to writeinstancetype
explicitly for all methods.
- Emphasis mine. Source: Adopting Modern Objective-C
With that out of the way, let's move on and explain why it's a good idea.
First, some definitions:
@interface Foo:NSObject
- (id)initWithBar:(NSInteger)bar; // initializer
+ (id)fooWithBar:(NSInteger)bar; // class factory
@end
For a class factory, you should always use instancetype
. The compiler does not automatically convert id
to instancetype
. That id
is a generic object. But if you make it an instancetype
the compiler knows what type of object the method returns.
This is not an academic problem. For instance, [[NSFileHandle fileHandleWithStandardOutput] writeData:formattedData]
will generate an error on Mac OS X (only) Multiple methods named 'writeData:' found with mismatched result, parameter type or attributes. The reason is that both NSFileHandle and NSURLHandle provide a writeData:
. Since [NSFileHandle fileHandleWithStandardOutput]
returns an id
, the compiler is not certain what class writeData:
is being called on.
You need to work around this, using either:
[(NSFileHandle *)[NSFileHandle fileHandleWithStandardOutput] writeData:formattedData];
or:
NSFileHandle *fileHandle = [NSFileHandle fileHandleWithStandardOutput];
[fileHandle writeData:formattedData];
Of course, the better solution is to declare fileHandleWithStandardOutput
as returning an instancetype
. Then the cast or assignment isn't necessary.
(Note that on iOS, this example won't produce an error as only NSFileHandle
provides a writeData:
there. Other examples exist, such as length
, which returns a CGFloat
from UILayoutSupport
but a NSUInteger
from NSString
.)
Note: Since I wrote this, the macOS headers have been modified to return a NSFileHandle
instead of an id
.
For initializers, it's more complicated. When you type this:
- (id)initWithBar:(NSInteger)bar
…the compiler will pretend you typed this instead:
- (instancetype)initWithBar:(NSInteger)bar
This was necessary for ARC. This is described in Clang Language Extensions Related result types. This is why people will tell you it isn't necessary to use instancetype
, though I contend you should. The rest of this answer deals with this.
There's three advantages:
- Explicit. Your code is doing what it says, rather than something else.
- Pattern. You're building good habits for times it does matter, which do exist.
- Consistency. You've established some consistency to your code, which makes it more readable.
Explicit
It's true that there's no technical benefit to returning instancetype
from an init
. But this is because the compiler automatically converts the id
to instancetype
. You are relying on this quirk; while you're writing that the init
returns an id
, the compiler is interpreting it as if it returns an instancetype
.
These are equivalent to the compiler:
- (id)initWithBar:(NSInteger)bar;
- (instancetype)initWithBar:(NSInteger)bar;
These are not equivalent to your eyes. At best, you will learn to ignore the difference and skim over it. This is not something you should learn to ignore.
Pattern
While there's no difference with init
and other methods, there is a difference as soon as you define a class factory.
These two are not equivalent:
+ (id)fooWithBar:(NSInteger)bar;
+ (instancetype)fooWithBar:(NSInteger)bar;
You want the second form. If you are used to typing instancetype
as the return type of a constructor, you'll get it right every time.
Consistency
Finally, imagine if you put it all together: you want an init
function and also a class factory.
If you use id
for init
, you end up with code like this:
- (id)initWithBar:(NSInteger)bar;
+ (instancetype)fooWithBar:(NSInteger)bar;
But if you use instancetype
, you get this:
- (instancetype)initWithBar:(NSInteger)bar;
+ (instancetype)fooWithBar:(NSInteger)bar;
It's more consistent and more readable. They return the same thing, and now that's obvious.
Conclusion
Unless you're intentionally writing code for old compilers, you should use instancetype
when appropriate.
You should hesitate before writing a message that returns id
. Ask yourself: Is this returning an instance of this class? If so, it's an instancetype
.
There are certainly cases where you need to return id
, but you'll probably use instancetype
much more frequently.
Solution 2:
There definitely is a benefit. When you use 'id', you get essentially no type checking at all. With instancetype, the compiler and IDE know what type of thing is being returned, and can check your code better and autocomplete better.
Only use it where it makes sense of course (i.e. a method that is returning an instance of that class); id is still useful.
Solution 3:
Above answers are more than enough to explain this question. I would just like to add an example for the readers to understand it in terms of coding.
ClassA
@interface ClassA : NSObject
- (id)methodA;
- (instancetype)methodB;
@end
Class B
@interface ClassB : NSObject
- (id)methodX;
@end
TestViewController.m
#import "ClassA.h"
#import "ClassB.h"
- (void)viewDidLoad {
[[[[ClassA alloc] init] methodA] methodX]; //This will NOT generate a compiler warning or error because the return type for methodA is id. Eventually this will generate exception at runtime
[[[[ClassA alloc] init] methodB] methodX]; //This will generate a compiler error saying "No visible @interface ClassA declares selector methodX" because the methodB returns instanceType i.e. the type of the receiver
}