@class与#import的区别

718
据我所了解,如果ClassA需要包含ClassB头文件,并且ClassB需要包含ClassA头文件以避免循环包含,那么应使用前向类声明。我也知道,#import是一个简单的ifndef,使得包含只会发生一次。
我的问题是:什么情况下使用#import,什么情况下使用@class?有时,如果我使用@class声明,我会看到一个常见的编译器警告,如下所示:

warning: receiver 'FooController' is a forward class and corresponding @interface may not exist.

我真的很想理解这个问题,而不是只删除@class前向声明并添加#import来消除编译器给我的警告。

10
前向声明只是告诉编译器:“嘿,我知道我在声明一些你不认识的东西,但当我说@MyClass时,我保证我会在实现中导入它”。注意不要改变原意,简化句子并使其易于理解。 - JoeCortopassi
16个回答

758

如果您看到以下警告:

警告:接收器“MyCoolClass”是前向类,相应的@interface可能不存在

那么您需要在实现文件(.m)中 #import 文件,并在头文件中使用 @class 声明。

@class(通常)不会消除需要 #import 文件的必要性,它只是将要求移到更接近信息有用的地方。

例如

如果您说 @class MyCoolClass,编译器就知道可能会看到像下面这样的东西:

MyCoolClass *myObject;

它无需担心除了MyCoolClass是一个有效的类并且应为其保留一个指针(实际上,只是一个指针)以外的任何事情。 因此,在您的头文件中,@class在90%的情况下就足够了。

但是,如果您需要创建或访问myObject的成员,则需要让编译器知道这些方法是什么。 此时(可能在您的实现文件中),您需要#import "MyCoolClass.h",以告诉编译器超出“这是一个类”的基本信息。


5
非常好的答案,谢谢。供日后参考:这也适用于以下情况:在您的.h文件中使用了@class,但忘记在.m文件中进行#import,尝试访问@class对象上的方法时会收到类似于“警告:未找到-X方法”的警告信息。 - Tim
24
如果.h文件包含了你的类接口所必需的数据类型或其他定义,那么你需要使用#import而不是@class。 - Ken Aspeslagh
2
这里没有提到的另一个巨大优势是快速编译。请参考Venkateshwar的答案。 - MartinMoizard
@BenGottlieb 那个“myCoolClass”中的“m”不应该是大写的吗?就像“MyCoolClass”一样? - Basil Bourque

183

三个简单的规则:

  • 只在头文件 (.h 文件) 中使用 #import 导入超类和采用的协议。
  • 在实现文件 (.m 文件) 中导入你发送消息给的所有类和协议。
  • 对于其他一切,都使用前向声明。

如果你在实现文件中使用前向声明,那么你可能做错了什么。


22
在头文件中,您可能还需要 #import 定义您的类采用的协议的任何内容。 - Tyler
在h接口文件或m实现文件中声明#import有什么区别吗? - Samuel Goldenbaum
如果您从类中使用实例变量,则需要使用 #import。 - mmmmmm
1
@Mark - 受规则#1的限制,只有在必要时才能从你的超类访问实例变量。 - PeyloW
@Tyler 为什么不使用协议的前向声明呢? - JoeCortopassi
@JoeCortopassi - 就像一个类需要完全了解它的超类一样,它也需要完全了解它所采用的协议。如果该协议仅用于属性和/或方法,则可以进行前向声明。 - PeyloW

110

请查看Objective-C编程语言文档,文档在ADC上。

在定义类|类接口的章节中,它描述了为什么要这样做:

@class指令最小化编译器和链接器所看到的代码量,因此是提供类名的前向声明的最简单方法。由于其简单性,它避免了导入导入其他文件可能带来的潜在问题。例如,如果一个类声明了另一个类的静态类型实例变量,并且它们两个接口文件互相导入,那么两个类都可能无法正确编译。


48
如果需要,在头文件中使用前向声明,并在实现中引入任何类的头文件。换句话说,您始终要导入实现中使用的文件,如果需要在头文件中引用类,则也要使用前向声明。
例外情况是,如果您从一个类或正式协议继承,则应在头文件中导入它(在这种情况下,您不需要在实现中导入它)。

24

通常的做法是在头文件中使用@class(但您仍需要导入超类),在实现文件中使用#import。这将避免任何循环包含,并且它只是工作。


2
我认为 #import 比 #include 更好,因为它只导入一个实例? - Matthew Schinckel
2
不确定是否涉及循环包含或顺序不正确,但我违反了这个规则(在头文件中导入一个模块后,在子类的实现中就不再需要导入),结果很混乱。总之,请遵循这个规则,编译器会很高兴。 - Steph Thirion
1
当前文档中提到,#import“类似于 C 的 #include 指令,但确保同一文件不会被重复包含。” 因此,根据这个说法,#import 负责处理循环包含,@class指令并不能特别帮助解决这个问题。 - Eric

24

另一个优点:快速编译

如果您包含一个头文件,其中任何更改都会导致当前文件重新编译,但是如果将类名包含为@class name,则情况并非如此。当然,您需要在源文件中包含头文件。


18
我的问题是:什么时候使用 #import,什么时候使用 @class?
简单的答案是:当有物理依赖关系时,您会使用 #import 或 #include。否则,使用前向声明(@class MONClass、struct MONStruct、@protocol MONProtocol)。
以下是一些常见的物理依赖关系示例:
- 任何 C 或 C++ 值(指针或引用不是物理依赖项)。如果您将 CGPoint 作为 ivar 或 property,则编译器需要查看 CGPoint 的声明。 - 您的超类。 - 您使用的方法。
有时,如果我使用 @class 声明,我会看到一个常见的编译器警告,如下所示:"warning: receiver 'FooController' is a forward class and corresponding @interface may not exist."
实际上,编译器在这方面非常宽容。它会给出提示(如上面的提示),但是如果您忽略它们并且没有正确 #import,则很容易崩溃。虽然它应该(在我看来),但编译器并不强制执行。在 ARC 中,编译器更加严格,因为它负责引用计数。发生的情况是,当您调用一个未知方法时,编译器会回退到默认值。每个返回值和参数都被认为是 id。因此,您应该从代码库中清除每个警告,因为这应该被视为物理依赖关系。这类似于调用未声明的 C 函数。对于 C,参数被认为是 int。您会倾向于使用前向声明的原因是可以减少构建时间,因为它们之间具有最小的依赖性。对于没有物理依赖关系的类名,通过前向声明,编译器可以正确解析和编译程序而无需看到类声明或其所有依赖项。这样可以节省清除构建的时间,也可以节省增量构建的时间。当然,您需要花费一些更多的时间来确保每个翻译都可以看到您所需的所有标头,但这将很快得到回报,因为构建时间减少了(假设您的项目不是微不足道的)。
如果您使用#import#include,则会向编译器抛出比必要更多的工作。还引入了复杂的头文件依赖关系。这就像暴力算法一样。当您使用#import时,您会带入大量不必要的信息,这需要大量的内存,磁盘I/O和CPU来解析和编译源代码。
ObjC对于基于C的语言来说非常接近理想状态,因为NSObject类型永远不是值--NSObject类型始终是引用计数指针。因此,如果适当地结构化程序的依赖关系并尽可能地前向声明,那么您可以获得非常快的编译时间,因为几乎不需要物理依赖关系。您还可以在类扩展中声明属性,以进一步最小化依赖性。对于大型系统,这是一个巨大的优势--如果您曾经开发过大型C++代码库,您会知道它所产生的差异。
因此,我的建议是尽可能使用前向声明,然后在存在物理依赖关系的情况下使用#import。如果看到警告或其他暗示有物理依赖关系的信息--请全部修复。解决方法是在实现文件中使用#import
当构建库时,您可能会将一些接口分类为一组,在这种情况下,您应将该库#import引入引入物理依赖性的地方(例如#import <AppKit/AppKit.h>)。这可能会引入依赖关系,但库维护者通常可以根据需要处理物理依赖关系--如果他们引入一个功能,则可以将其在您的构建中最小化影响。

顺便说一句,解释得很好,但它们似乎相当复杂。 - Ajay Sharma
“NSObject类型永远不是值-- NSObject类型始终是引用计数指针”并非完全正确。只是说,块可能会使您的回答出现漏洞。 - Richard J. Ross III
@RichardJ.RossIII...GCC允许声明和使用值,而clang则禁止。当然,指针后面必须有一个值。 - justin

11

我看到了很多“这样做”的建议,但是没有看到任何关于“为什么”的答案。

所以: 为什么你应该在头文件中使用@class,在实现文件中只使用#import呢?这样做会使你的工作量增加,因为你需要一直使用@class和#import。除非你使用继承,否则这样做就会导致你为一个@class引入多个#import。然后如果你突然决定不再需要访问声明,你就必须记得从多个不同的文件中删除它。

由于#import的性质,多次导入同一文件并不是问题。编译性能也不是问题。如果是的话,我们就不会在几乎每个头文件中都导入Cocoa/Cocoa.h之类的文件了。


1
请参考上面Abizem的文档示例,了解为什么应该这样做。这是一种防御性编程方式,用于处理两个类头文件相互导入并具有对方类的实例变量的情况。 - jackslash

7

如果我们这样做

@interface Class_B : Class_A

意味着我们将Class_A继承到Class_B中,在Class_B中,我们可以访问Class_A的所有变量。

如果我们这样做:

#import ....
@class Class_A
@interface Class_B

在这里,我们说我们在程序中使用了Class_A,但是如果我们想在Class_B中使用Class_A的变量,我们必须在.m文件中导入Class_A(创建一个对象并使用它的函数和变量)。

5

关于文件依赖性、#import和@class的更多信息,请参阅以下内容:

http://qualitycoding.org/file-dependencies/ 这是一篇不错的文章。

文章摘要:

头文件中的导入:

  • #import超类和实现的协议。
  • 除非来自具有主头文件的框架,否则其他所有内容都应该进行前向声明。
  • 尽量消除所有其他#imports。
  • 在它们自己的头文件中声明协议以减少依赖性。
  • 过多的前向声明?你拥有一个大型类。

实现文件中的导入:

  • 消除未使用的废弃#imports。
  • 如果一个方法委托给另一个对象并返回它得到的结果,则尝试前向声明该对象而不是#import它。
  • 如果包含一个模块强制您包含层层递进的依赖项,则可能有一组想成为库的类。用主头文件构建它作为单独的库,这样所有内容就可以作为单个预构建块引入。
  • 太多的#imports?你拥有一个大型类。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接