@class vs.#导入

我的理解是,如果ClassA需要包含ClassB标头,而ClassB需要包含ClassA标头以避免任何循环包含,则应该使用前向类声明。我还理解#import是一个简单的ifndef,因此包含只发生一次。

我的问题是:什么时候使用#import,什么时候使用@class?有时如果我使用@class声明,我会看到一个常见的编译器警告,如下所示:

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

真的很想理解这一点,而不仅仅是删除@class前向声明并抛出#import来沉默编译器给我的警告。

171479 次浏览

如果需要,请在头文件中使用前置声明,并#import您在实现中使用的任何类的头文件。换句话说,您总是#import您在实现中使用的文件,如果您需要引用头文件中的类,请使用前置声明。

例外是您应该#import在头文件中继承的类或形式协议(在这种情况下,您不需要在实现中导入它)。

如果您看到此警告:

警告:接收器'MyCoolClass'是一个转发类,相应的@接口可能不存在

您需要将文件#import,但您可以在实现文件(. m)中执行此操作,并在头文件中使用@class声明。

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

举个例子

如果你说@class MyCoolClass,编译器知道它可能会看到如下内容:

MyCoolClass *myObject;

它不必担心MyCoolClass是一个有效的类之外的任何事情,它应该为指向它的指针保留空间(实际上,只是一个指针)。因此,在你的标头中,@class足以满足90%的时间。

但是,如果你需要创建或访问myObject的成员,你需要让编译器知道这些方法是什么。此时(大概在你的实现文件中),你需要#import "MyCoolClass.h"来告诉编译器除了“这是一个类”之外的其他信息。

常见的做法是在头文件中使用@class(但您仍然需要#导入超类),并在实现文件中使用#导入。这将避免任何循环包含,而且它很有效。

查看adc上的Objective-C编程语言留档

在定义类|类接口一节中,它描述了这样做的原因:

@class指令最大限度地减少了编译器和链接器看到的代码量,因此是给出类名前置声明的最简单方法。由于简单,它避免了导入仍导入其他文件的文件可能带来的潜在问题。例如,如果一个类声明了另一个类的静态类型实例变量,并且它们的两个接口文件相互导入,则两个类都无法正确编译。

我希望这能有所帮助。

三个简单的规则:

  • 只有#import超类,并采用协议,在头文件(.h文件)中。
  • #import所有类和协议,您在实现中发送消息(.m文件)。
  • 转发其他所有内容的声明。

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

另一个优点:快速编译

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

当我发展时,我心中只有三件事,永远不会给我带来任何问题。

  1. 导入超类
  2. 导入父类(当您有孩子和父母时)
  3. 导入项目之外的类(如框架和库中)

对于所有其他类(我自己的项目中的子类和子类),我通过前向类声明它们。

我看到很多“这样做”,但我看不到“为什么”的任何答案

所以:为什么你应该在标题中@class,而在实现中只导入吗?你必须一直导入@class#,这会使你的工作加倍。除非你使用继承。在这种情况下,你将为一个@class#导入多次。然后,如果你突然决定不再需要访问声明,你必须记住从多个不同的文件中删除。

多次导入同一个文件不是问题,因为#导入的性质。 编译性能也不是真正的问题。如果是,我们不会在几乎所有的头文件中#导入Cocoa/Cocoa. h或类似的东西。

如果我们这么做

@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(创建一个对象并使用它的函数和变量)。

我的问题是这个。什么时候使用#导入,什么时候使用@class?

简单的回答:当存在物理依赖项时,您使用#import#include。否则,您使用前向声明(@class MONClassstruct MONStruct@protocol MONProtocol)。

以下是一些常见的身体依赖的例子:

  • 任何C或C++值(指针或引用不是物理依赖项)。如果您有CGPoint作为ivar或属性,编译器需要查看CGPoint的声明。
  • 你的超级班。
  • 你使用的方法。

有时如果我使用@class声明,我会看到一个常见的编译器警告,如下所示: msgstr"警告:接收者'FoController'是一个转发类,对应的@接口可能不存在。"

编译器在这方面实际上很宽松。它会丢弃提示(比如上面的那个),但是如果你忽略它们并且没有正确#import,你很容易就会破坏你的堆栈。尽管它应该(IMO),编译器并没有强制执行这一点。在ARC中,编译器更严格,因为它负责引用计数。当编译器遇到你调用的未知方法时,会回到默认值。每个返回值和参数都被假定为id。因此,你应该从代码库中消除每一个警告,因为这应该被认为是物理依赖。这类似于调用一个未声明的C函数。对于C,假设参数为int

你喜欢前向声明的原因是你可以按因素减少构建时间,因为依赖最小。使用前向声明,编译器会看到有一个名称,并且可以正确解析和编译程序,而无需看到类声明或其所有依赖项(当没有物理依赖项时)。干净构建需要更少的时间。增量构建需要更少的时间。当然,你最终会花更多的时间确保你需要的所有标头因此对每个翻译都是可见的,但这很快减少了构建时间(假设你的项目不是很小)。

如果你使用#import#include,你会在编译器上投入比必要的多得多的工作。你还引入了复杂的标头依赖项。你可以将其比作蛮力算法。当你#import时,你会拖入大量不必要的信息,这需要大量内存、磁盘I/O和CPU来解析和编译源。

ObjC在依赖关系方面非常接近理想的基于C的语言,因为NSObject类型从来都不是值--NSObject类型总是引用计数指针。所以如果你适当地结构程序的依赖关系,并尽可能向前推进,你就可以避免令人难以置信的快速编译时间,因为所需的物理依赖很少。你还可以在类扩展中声明属性以进一步最小化依赖。这对大型系统来说是一个巨大的好处--如果你开发过大型C++代码库,你就会知道它的区别。

因此,我的建议是在可能的情况下使用转发,然后在存在物理依赖性的地方使用#import。如果您看到警告或其他暗示物理依赖性的警告-将它们全部修复。修复方法是在您的实现文件中使用#import

在构建库时,您可能会将一些接口分类为一组,在这种情况下,您将#import引入物理依赖的库(例如#import <AppKit/AppKit.h>)。这可能会引入依赖,但库维护者通常可以根据需要为您处理物理依赖-如果他们引入了一项功能,他们可以最大限度地减少它对您的构建的影响。

转发声明只是为了防止编译器显示错误。

编译器将知道有一个类的名称与您在头文件中使用的名称一起声明。

如果您尝试在您的头文件中声明一个变量或一个属性,您还没有导入,您会收到一个错误,说编译器不知道这个类。

你的第一个想法可能是#import它。
这在某些情况下可能会导致问题。

例如,如果您在头文件、结构或类似的东西中实现了一堆C方法,因为它们不应该被多次导入。

因此,您可以使用@class告诉编译器:

我知道你不知道这个类,但它确实存在,它将被导入或在其他地方实现

它基本上告诉编译器关闭并编译,即使它不确定这个类是否会被实现。

您通常会在. m中使用#import,在. h文件中使用@class

只有当您要以编译器需要知道其实现的方式使用该类时,编译器才会抱怨。

例如:

  1. 这可能就像如果你要从它派生你的类或者
  2. 如果您将该类的对象作为成员变量(尽管很少见)。

如果你只是将其用作指针,它不会抱怨。当然,你必须在实现文件中导入它(如果你正在实例化该类的对象),因为它需要知道类内容来实例化一个对象。

注意:#导入与#包含不同。这意味着没有任何称为循环导入的内容。导入是一种要求编译器查看特定文件以获取某些信息的请求。如果该信息已经可用,编译器会忽略它。

试试这个,在B. h中导入A. h,在A. h中导入B. h。不会有任何问题或投诉,它也会正常工作。

何时使用@class

仅当您甚至不想在标头中导入标头时才使用@class。这可能是您甚至不关心该类是什么的情况。您甚至可能还没有该类的标头。

举个例子,你正在编写两个库。一个库中存在一个类,我们称之为A。这个库包含来自第二个库的标头。该标头可能有一个指针A,但也可能不需要使用它。如果库1还不可用,如果你使用@class,库B不会被阻止。但是如果你想导入A. h,那么库2的进度会被阻止。

有关文件依赖项和#导入和@类的更多信息,请查看:

http://qualitycoding.org/file-dependencies/ 好文章

文章摘要

在头文件中导入:

  • #导入你要继承的超类,以及你要实现的协议。
  • 前向声明其他所有内容(除非它来自框架) 有一个主标题)。
  • 尝试删除所有其他#导入。
  • 在它们自己的标头中声明协议以减少依赖关系。
  • 太多的前向声明?你有一个大类。

在实现文件中导入:

  • 消除未使用的cruft#导入。
  • 如果一个方法委托给另一个对象并返回它得到的东西 返回,尝试前向声明该对象而不是#导入它。
  • 如果包含一个模块强制您在一个又一个的级别中包含 连续的依赖项,您可能有一组想要 成为一个图书馆。把它建成一个独立的图书馆,有一个主人 头,因此所有内容都可以作为单个预构建块引入。
  • #导入太多?你有一个大类。

这是一个示例场景,我们需要@class。

考虑如果您希望在头文件中创建一个协议,该文件的参数具有相同的类的数据类型,那么您可以使用@class。请记住,您也可以单独声明协议,这只是一个示例。

// DroneSearchField.h


#import <UIKit/UIKit.h>
@class DroneSearchField;
@protocol DroneSearchFieldDelegate<UITextFieldDelegate>
@optional
- (void)DroneTextFieldButtonClicked:(DroneSearchField *)textField;
@end
@interface DroneSearchField : UITextField
@end

将@class视为告诉编译器“相信我,这是存在的”。

将#导入视为复制粘贴。

出于多种原因,您希望最小化导入的数量。在没有任何研究的情况下,首先想到的是它减少了编译时间。

请注意,当您从类继承时,您不能简单地使用前置声明。您需要导入文件,以便您声明的类知道它是如何定义的。