Thursday, July 24, 2014

Benefits to using `instancetype`

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 with `instancetype` where appropriate. This is typically the case for `init` methods and class factory methods. Even though the compiler automatically converts methods that begin with “alloc,” “init,” or “new” and have a return type of `id` to return `instancetype`, it doesn’t convert other methods. **Objective-C convention is to write `instancetype` explicitly for all methods.**

* Emphasis mine. Source: [Adopting Modern Objective-C](https://developer.apple.com/library/ios/releasenotes/ObjectiveC/ModernizationObjC/AdoptingModernObjective-C/AdoptingModernObjective-C.html#//apple_ref/doc/uid/TP40014150-CH1-SW11)

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`.)

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][1]. 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:

1. **Explicit.** Your code is doing what it says, rather than something else.
2. **Pattern.** You're building good habits for times it does matter, which do exist.
3. **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.


(http://stackoverflow.com/)

No comments:

Post a Comment