开始使用instancetype而不是id会有益吗?


226

instancetype据我所知,Clang添加了一个关键字,据我所知,它替换id-allocand中的返回类型init

使用instancetype代替有好处id吗?


10
不。不适合alloc和init,因为它们已经像这样工作了。instancetype的重点是让您可以像behavoir一样为自定义方法分配alloc / init。
hooleyhoop,2012年

@hooleyhoop他们不这样工作。他们返回ID。id是“一个Obj-C对象”。就这些。除此之外,编译器对返回值一无所知。
griotspeak

5
他们确实是这样工作的,契约和类型检查已经在进行init了,只有对于构造者,您才需要这样做。nshipster.com/instancetype
kocodude 2013年

是的,事情已经发展了很多。相关的结果类型是相对较新的,其他变化意味着这将很快成为一个更加清晰的问题。
griotspeak

1
我只是想评论一下,现在在iOS 8上,很多用于返回的方法id已被替换 instancetype,甚至是initNSObject。如果您想使代码与Swift兼容,则必须使用instancetype
Hola Soy Edu Feliz Navidad 2014年

Answers:


192

绝对有好处。当使用'id'时,实际上根本没有类型检查。使用instancetype,编译器和IDE知道将返回什么类型的东西,并且可以更好地检查代码并更好地自动完成。

仅在合理的情况下使用它(即返回该类实例的方法);id仍然有用。


8
allocinitinstancetype由编译器自动升级。但这并不是说没有好处。有,但是不是。
史蒂芬·费舍尔

10
它对便利构造函数主要很有用
Catfish_Man 2013年

5
它是在ARC(于2011年Lion发行)中引入的(并帮助其提供支持)。使可可中广泛采用的一件事复杂化的是,必须对所有候选方法进行审核,以查看它们是否是[self alloc]而不是[NameOfClass] alloc],因为用[convenienceConstructor]声明要返回instancetype而不让它返回SomeSubClass的实例来进行[SomeSubClass comfortConstructor]会造成极大的混乱。
Catfish_Man 2013年

2
仅当编译器可以推断方法族时,“ id”才转换为实例类型。对于便利构造函数或其他id返回方法,它肯定不会这样做。
Catfish_Man 2013年

4
请注意,在iOS 7中,Foundation中的许多方法已转换为instancetype。我个人已经看到,这种方法至少可以捕获3种不正确的预先存在的代码。
Catfish_Man 2013年

336

是的,instancetype在所有适用的情况下使用都有好处。我将更详细地解释,但让我从以下粗体语句开始:instancetype在适当时使用,即当类返回相同类的实例时使用。

实际上,这就是苹果现在在这个问题上所说的:

在您的代码中,将出现的id作为返回值替换为instancetype适当的位置。init方法和类工厂方法通常是这种情况。即使编译器自动转换以“ alloc”,“ init”或“ new”开头且返回类型id为return的方法instancetype,它也不会转换其他方法。Objective-C约定是instancetype为所有方法显式编写。

顺便说一句,让我们继续并解释为什么这是一个好主意。

首先,一些定义:

 @interface Foo:NSObject
 - (id)initWithBar:(NSInteger)bar; // initializer
 + (id)fooWithBar:(NSInteger)bar;  // class factory
 @end

对于班级工厂,应始终使用instancetype。编译器不会自动转换idinstancetype。那id是一个通用对象。但是,如果您将其设为instancetype,编译器就会知道该方法返回的对象类型。

不是学术问题。例如,[[NSFileHandle fileHandleWithStandardOutput] writeData:formattedData]将在Mac OS X上生成错误(。发现多个名为'writeData:'的方法,它们的结果,参数类型或属性不匹配。原因是NSFileHandle和NSURLHandle都提供了一个writeData:。由于[NSFileHandle fileHandleWithStandardOutput]返回id,编译器不确定writeData:正在调用什么类。

您需要使用以下任一方法来解决此问题:

[(NSFileHandle *)[NSFileHandle fileHandleWithStandardOutput] writeData:formattedData];

要么:

NSFileHandle *fileHandle = [NSFileHandle fileHandleWithStandardOutput];
[fileHandle writeData:formattedData];

当然,更好的解决方案是声明fileHandleWithStandardOutput为返回instancetype。这样就不需要强制转换或分配。

(请注意,在iOS上,此示例不会产生错误,因为它仅NSFileHandle提供了a writeData:。存在其他示例,例如length,它返回CGFloatfrom,UILayoutSupport但是NSUIntegerfrom NSString。)

注意:自从我撰写本文以来,macOS标头已被修改为返回a NSFileHandle而不是an id

对于初始化程序而言,它更加复杂。当您键入以下内容时:

- (id)initWithBar:(NSInteger)bar

…编译器会假装您键入以下内容:

- (instancetype)initWithBar:(NSInteger)bar

这对于ARC是必需的。Clang语言扩展相关的结果类型对此进行了描述。这就是为什么人们会告诉您没有必要使用的原因instancetype,尽管我认为您应该这样做。此答案的其余部分处理此问题。

有三个优点:

  1. 明确的。您的代码正在执行其所声明的内容,而不是其他内容。
  2. 模式。您会在确实存在的时间建立起良好的习惯。
  3. 一致性。您已经建立了代码的一致性,使其更具可读性。

明确的

的确,从退货没有技术上的好处。但这是因为编译器会自动将转换为。您是在依赖这个怪癖;当您编写返回值an时,编译器会将其解释为好像返回了。instancetypeinitidinstancetypeinitidinstancetype

这些等效于编译器:

- (id)initWithBar:(NSInteger)bar;
- (instancetype)initWithBar:(NSInteger)bar;

这些不等于你的眼睛。充其量,您将学会忽略差异并略过差异。这不是您应该学会忽略的东西。

模式

虽然有没有区别init等方法,也就是只要你定义一个类工厂的差异。

这两个不相等:

+ (id)fooWithBar:(NSInteger)bar;
+ (instancetype)fooWithBar:(NSInteger)bar;

您需要第二种形式。如果习惯于将类型instancetype用作构造函数的返回类型,则每次都会正确使用它。

一致性

最后,想象一下是否将它们放在一起:既要有一个init函数又要有一个类工厂。

如果将idfor用于init,则最终会得到如下代码:

- (id)initWithBar:(NSInteger)bar;
+ (instancetype)fooWithBar:(NSInteger)bar;

但是,如果使用instancetype,则会得到以下信息:

- (instancetype)initWithBar:(NSInteger)bar;
+ (instancetype)fooWithBar:(NSInteger)bar;

它更一致,更易读。他们返回相同的东西,现在很明显。

结论

除非有意为旧的编译器编写代码,否则应instancetype在适当时使用。

在编写返回的消息之前,请先犹豫id。问问自己:这是否返回该类的实例?如果是这样,则为instancetype

在某些情况下,您需要返回id,但是您可能instancetype会更频繁地使用它。


4
自从我问这个问题以来已经有一段时间了,很长一段时间以来我就采取了您在这里提出的立场。我将其放到这里,是因为我认为不幸的是,这将被视为初始化样式的问题,并且早期答复中提供的信息确实清楚地回答了这个问题。
griotspeak

6
需要明确的是:我相信Catfish_Man的答案仅在第一句话中是正确的。hooleyhoop的答案除了第一句话是正确的。这是一个两难境地。我想提供一些东西,随着时间的推移,它们会比任何一种都更有用和更正确。(对这两个人都给予应有的尊重;毕竟,这比写他们的答案时回想起来要明显得多。)
史蒂芬·费舍尔

1
随着时间的流逝,我希望如此。我想不出任何理由,除非苹果公司认为保持源代码兼容性很重要,例如(不带强制转换)将新的NSArray分配给NSString。
史蒂芬·费舍尔

4
instancetypevs id真的不是风格决定。最近的变化instancetype确实表明,我们应该instancetype在诸如-init“我的班级实例”之类的地方使用
griotspeak

2
您说有使用id的原因,可以详细说明吗?我能想到的仅有两个是简洁和向后兼容。考虑到两者都以表达您的意图为代价,我认为这些说法确实很糟糕。
史蒂芬·费舍尔

10

以上答案足以解释这个问题。我只想添加一个示例,供读者理解编码方面的知识。

A类

@interface ClassA : NSObject

- (id)methodA;
- (instancetype)methodB;

@end

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
}

如果不使用#import“ ClassB.h”,这只会产生编译器错误。否则,编译器将假定您知道自己在做什么。例如,以下代码可编译,但在运行时崩溃:id myNumber = @(1); id iAssumedThatWasAnArray = myNumber [0]; id iAssumedThatWasADictionary = [myNumber objectForKey:@“ key”];
OlDor

1

您还可以在“指定的初始化程序”中获取详细信息

**

实例类型

**此关键字只能用于返回类型,与接收者的返回类型匹配。init方法始终声明为返回instancetype。例如,为什么不将返回类型设为Party呢?如果党的阶级曾经被阶级化,那将引起一个问题。子类将继承Party的所有方法,包括初始化程序及其返回类型。如果子类的实例发送了此初始化消息,那会返回吗?不是指向Party实例的指针,而是指向子类实例的指针。您可能会认为这没问题,我将重写子类中的初始化程序以更改返回类型。但是在Objective-C中,不能有两个方法具有相同的选择器和不同的返回类型(或参数)。通过指定初始化方法返回“

ID

**在Objective-C中引入实例类型之前,初始化程序将返回id(eye-dee)。此类型定义为“指向任何对象的指针”。(id很像C中的void *。)撰写本文时,XCode类模板仍然使用id作为添加在样板代码中的初始化程序的返回类型。与instancetype不同,id不仅可以用作返回类型,还可以用作ID。当您不确定变量最终指向哪种对象时,可以声明类型为id的变量或方法参数。在使用快速枚举迭代多个或未知类型的对象的数组时,可以使用id。请注意,由于id未定义为“指向任何对象的指针”,因此在声明此类型的变量或对象参数时,请勿包含*。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.