【问题标题】:Would it be beneficial to begin using instancetype instead of id?开始使用 instancetype 而不是 id 是否有益?
【发布时间】:2012-02-16 20:05:35
【问题描述】:

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

使用instancetype 代替id 有什么好处吗?

【问题讨论】:

  • 不.. 不适用于 alloc 和 init,因为它们已经像这样工作了。 instancetype 的重点是让您可以像行为一样提供自定义方法 alloc/init。
  • @hooleyhoop 他们不是这样工作的。他们返回 id。 id 是“一个 Obj-C 对象”。就这些。除此之外,编译器对返回值一无所知。
  • 他们确实像这样工作,契约和类型检查已经在 init 进行,只有 custructors 才需要这个。 nshipster.com/instancetype
  • 事情已经进步了很多,是的。相关结果类型相对较新,其他变化意味着这将很快成为一个更清晰的问题。
  • 我只想评论一下,现在在 iOS 8 上,很多用于返回 id 的方法已被 instancetype 替换,甚至是 NSObject 中的 init。如果你想让你的代码与 swift 兼容,你必须使用instancetype

标签: objective-c instancetype


【解决方案1】:

是的,在所有适用的情况下使用instancetype 都有好处。我将更详细地解释,但让我从这个大胆的声明开始:在适当的时候使用instancetype,即每当一个类返回同一个类的实例时。

事实上,苹果现在在这个问题上是这样说的:

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

说完这些,让我们继续解释为什么这是个好主意。

首先,一些定义:

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

对于类工厂,您应该始终使用instancetype。编译器不会自动将id 转换为instancetypeid 是一个通用对象。但是如果你把它设为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 在那里提供了 writeData:。还有其他示例,例如 length,它从 UILayoutSupport 返回一个 CGFloat,但是来自NSStringNSUInteger。)

注意:由于我写了这篇文章,macOS 标头已被修改为返回 NSFileHandle 而不是 id

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

- (id)initWithBar:(NSInteger)bar

…编译器会假装你输入了这个:

- (instancetype)initWithBar:(NSInteger)bar

这对于 ARC 来说是必要的。这在 Clang 语言扩展 Related result types 中有描述。这就是为什么人们会告诉你没有必要使用instancetype,尽管我认为你应该这样做。这个答案的其余部分涉及这个。

有三个优点:

  1. 显式。您的代码正在做它所说的,而不是其他的。
  2. 模式。您正在为确实存在的重要时期养成良好的习惯。
  3. 一致性。您已经为代码建立了某种一致性,这使其更具可读性。

显式

确实,从init 返回instancetype 没有技术 好处。但这是因为编译器会自动将id 转换为instancetype。你依赖这个怪癖;当您编写 init 返回 id 时,编译器会将其解释为返回 instancetype

这些等价于编译器:

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

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

图案

虽然init 和其他方法没有区别,但只要定义了类工厂,就会有区别。

这两个不等价:

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

你想要第二种形式。如果您习惯于键入 instancetype 作为构造函数的返回类型,那么每次都会正确。

一致性

最后,想象一下,如果你把它们放在一起:你想要一个 init 函数和一个类工厂。

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

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

但是如果你使用instancetype,你会得到这个:

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

它更一致且更具可读性。他们返回相同的东西,现在这很明显。

结论

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

在编写返回 id 的消息之前,您应该犹豫一下。问问自己:这是返回这个类的一个实例吗?如果是这样,那就是instancetype

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

【讨论】:

  • 我已经有一段时间没有问这个问题了,我早就采取了像你在这里提出的立场。我把它放在这里是因为我认为,不幸的是,这将被视为 init 的样式问题,并且之前的回复中提供的信息确实非常清楚地回答了这个问题。
  • 明确一点:我相信 Catfish_Man 的回答是正确的仅在第一句话中。 hooleyhoop 的答案是正确的除了第一句话。这是一个两难的困境。我想提供一些东西,随着时间的推移,它们会被认为比任何一个都更有用、更正确。 (尽管对这两个人表示敬意;毕竟,现在这比他们写答案时要明显得多。)
  • 随着时间的推移,我希望如此。我想不出任何理由不这样做,除非 Apple 认为保持源兼容性很重要(例如)将新的 NSArray 分配给 NSString 而不进行强制转换。
  • instancetype vs id 真的不是风格决定。最近围绕instancetype 的变化明确表明我们应该在-init 这样的地方使用instancetype,我们的意思是“我的班级的一个实例”
  • 你说用id是有原因的,能详细点吗?我能想到的唯一两个是简洁和向后兼容性。考虑到两者都以表达您的意图为代价,我认为这些论点确实很糟糕。
【解决方案2】:

肯定有好处。当你使用 'id' 时,你基本上没有得到任何类型检查。使用 instancetype,编译器和 IDE 可以知道返回的是什么类型的东西,并且可以更好地检查您的代码并更好地自动完成。

只在当然有意义的地方使用它(即返回该类实例的方法); id 还是有用的。

【讨论】:

  • allocinit 等会被编译器自动提升为instancetype。这并不是说没有好处。有,但不是这个。
  • 它主要用于方便的构造函数
  • 它是在 2011 年发布的 Lion 中与 ARC 一起引入的(并帮助支持)。使 Cocoa 广泛采用复杂的一件事是,必须审核所有候选方法以查看它们是否有效[self alloc] 而不是 [NameOfClass alloc],因为使用 +convenienceConstructor 声明返回 instancetype 并且不返回 SomeSubClass 实例的 [SomeSubClass 便利构造器] 会非常令人困惑。
  • 'id' 仅在编译器可以推断方法族时转换为 instancetype。对于方便的构造函数或其他返回 id 的方法,它肯定不会这样做。
  • 请注意,在 iOS 7 中,Foundation 中的许多方法都已转换为实例类型。我亲眼目睹了至少 3 例不正确的预先存在的代码。
【解决方案3】:

以上答案足以解释这个问题。我只是想添加一个例子,让读者从编码的角度理解它。

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"];
【解决方案4】:

您也可以通过The Designated Initializer获取详细信息

**

实例类型

** 该关键字只能用于返回类型,它与接收者的返回类型相匹配。 init 方法总是声明返回实例类型。 例如,为什么不为派对实例创建返回类型 Party? 如果党类曾经被子类化,那将导致问题。子类将继承 Party 的所有方法,包括初始化程序及其返回类型。如果向子类的一个实例发送了这个初始化消息,那会返回吗?不是指向 Party 实例的指针,而是指向子类实例的指针。你可能认为没问题,我会重写子类中的初始化器来改变返回类型。但是在 Objective-C 中,不能有两个方法具有相同的选择器和不同的返回类型(或参数)。通过指定初始化方法返回“接收对象的实例”,您永远不必担心在这种情况下会发生什么。 **

身份证

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

【讨论】:

    【解决方案5】:

    特殊类型instancetype 表示init 方法的返回类型将与它正在初始化的对象类型(即init 消息的接收者)是同一类。这是对编译器的帮助,以便它可以检查您的程序并标记潜力 类型不匹配——它根据上下文确定返回对象的类;也就是说,如果您将 init 消息发送到新的 alloc'ed Fraction 对象,编译器将推断从该 init 方法返回的值(其返回类型已声明为 type instancetype ) 将是一个Fraction 对象。过去,初始化方法的返回类型被声明为类型id。当您考虑子类化时,这种新类型更有意义,因为继承的初始化方法无法显式定义它们将返回的对象类型。

    初始化对象,Stephen G. Kochan,Objective-C 编程,第 6 版

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-09-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-03-09
      相关资源
      最近更新 更多