编写 Objective-C 代码
如果您未曾开发过 iOS 或 Mac OS X 平台的程序,那就需要开始了解它们的首要程序设计语言 Objective-C。Objective-C 并不是一种很难的语言,如果能花一点时间学习,相信您会渐渐领会到它的优雅之处。Objective-C 程序设计语言使您能进行复杂的、面向对象的编程。通过提供用于定义类和方法的语法,它扩展了标准的 ANSI C 程序设计语言。它还促进类和接口(任何类可采用)的动态扩展。
如果您熟悉 ANSI C,那么下述信息应该能帮助您学习 Objective-C 的基本语法。如果您使用其他面向对象程序设计语言进行过编程,您会发现许多传统的面向对象概念,例如封装、继承、多态,都出现在 Objective-C 中。如果您不熟悉 ANSI C,在尝试阅读此文章时,最好先阅读一下 C 语言的概述。
Objective-C 语言在《The Objective-C Programming Language》(Objective-C 程序设计语言)中有完整说明。
Objective-C 是 C 语言的超集
Objective-C 程序设计语言采用特定的语法,来定义类和方法、调用对象的方法、动态地扩展类,以及创建编程接口,来解决具体问题。Objective-C 作为 C 程序设计语言的超集,支持与 C 相同的基本语法。您会看到所有熟悉的元素,例如基本类型(int
、float
等)、结构、函数、指针,以及流程控制结构,如 if...else
语句和 for
语句。您还可以访问标准 C 库例程,例如在 stdlib.h
和 stdio.h
中声明的那些例程。
Objective-C 为 ANSI C 添加了下述语法和功能:
-
定义新的类
-
类和实例方法
-
方法调用(称为发消息)
-
属性声明(以及通过它们自动合成存取方法)
-
静态和动态类型化
-
块 (block),已封装的、可在任何时候执行的多段代码
-
基本语言的扩展,例如协议和类别
如果您现在还不太熟悉 Objective-C 的这些方面,也不必担心。随着您读完这篇文章的剩余部分,将会逐渐了解它们。如果您是过程化程序开发人员,不懂面向对象的概念,那么先将对象从本质上视为具有关联函数的结构,可能会有助于理解。这个概念与事实差不多,特别是在运行时实现方面。
除了提供在其他面向对象语言中已有的多数抽象和机制之外,Objective-C 还是一种非常动态的程序设计语言,而且这种动态是其最大优势。这种动态体现在它允许在运行应用程序时(即运行时)才去确定其行为,而不是在生成期间就已固定下来。因此,Objective-C 的动态机制让程序免受约束(编译和链接程序时施加的约束);进而在用户控制下,将大多数符号解析责任转移到运行时。
类和对象
如同其他大多数面向对象语言那样,Objective-C 中的类支持数据的封装,并定义对这些数据执行的操作。对象是类的运行时实例。它包含自己的实例变量(由其类声明)的内存副本,以及类方法的指针。您可以采用两步法(称为分配和初始化)创建对象。
Objective-C 中某个类的规格需要两个不同的部分:接口和实现。接口部分包含类声明,并定义该类的公共接口。如同 C 代码那样,您定义头文件和源代码文件,将公共声明与代码的实现细节分开。(如果其他声明是编程接口的一部分,并且打算专有,您可以将它们放在实现文件中。)这些文件的文件扩展名,列在下表中。
扩展名 | 源类型 |
---|---|
| 头文件。头文件包含类、类型、函数和常量声明。 |
| 实现文件。具有此扩展名的文件可以同时包含 Objective-C 代码和 C 代码。有时也称为源文件。 |
| 实现文件。具有此扩展名的实现文件,除了包含 Objective-C 代码和 C 代码以外,还可以包含 C++ 代码。仅当您实际引用您的 Objective-C 代码中的 C++ 类或功能时,才使用此扩展名。 |
当您想要在源代码中包括头文件时,请在头文件或源文件的前几行之中,指定一个导入 (#import
) 指令,#import
指令类似于 C 的 #include
指令,不过前者确保同一文件只被包括一次。如果您要导入框架的大部分或所有头文件,请导入框架的包罗头文件 (umbrella header file),它具有与框架相同的名称。导入(假设的)Gizmo 框架的头文件的语法是:
#import <Gizmo/Gizmo.h> |
下列框图中的语法声明名为 MyClass
的类,它是从基础类(或根类)NSObject
继承而来的。(根类是供其他类直接或间接继承的类。)类声明以编译器指令 @interface
开始,以 @end
指令结束。类名称后面(以冒号分隔),是父类的名称。在 Objective-C 中,一个类只能有一个父类。
在 @interface
指令和 @end
指令之间,编写属性和方法的声明。这些声明组成了类的公共接口。(已声明的属性在“已声明的属性和存取方法”中有介绍。)分号标记每个属性和方法声明的结尾。如果类具有与其公共接口相关的自定函数、常量或数据类型,请将它们的声明放在 @interface
...@end
块之外。
类实现的语法与类接口文件类似。它以 @implementation
编译器指令开始(接着是该类的名称),以 @end
指令结束。中间是方法实现。(函数实现应在 @implementation
...@end
块之外。)一个实现应该总是将导入它的接口文件作为代码的第一行。
#import "MyClass.h" |
@implementation MyClass |
- (id)initWithString:(NSString *)aName |
{ |
// code goes here |
} |
+ (MyClass *)myClassWithString:(NSString *)aName |
{ |
// code goes here |
} |
@end |
对于包含对象的变量,Objective-C 既支持动态类型化,也支持静态类型化。静态类型化的变量,要在变量类型声明中包括类名称。动态类型化的变量,则要给对象使用类型 id
。您会发现在某些情况下,会需要使用动态类型化的变量。例如,集 (collection) 对象,如数组,在它包含对象的类型未知的情况下,可能会使用动态类型化的变量。此类变量提供了极大的灵活性,也让 Objective-C 程序拥有了更强大的活力。
下面的例子,展示了静态类型化和动态类型化的变量声明:
MyClass *myObject1; // Static typing |
id myObject2; // Dynamic typing |
NSString *userName; // From Your First iOS App (static typing) |
请注意第一个声明中的星号 (*
)。在 Objective-C 中,执行对象引用的只能是指针。如果您还不能完全理解这个要求,不用担心。并非一定要成为指针专家才能开始 Objective-C 编程。只需要记住,在静态类型化的对象的声明中,变量的名称前面应放置一个星号。id
类型意味着一个指针。
方法和发消息
如果您不熟悉面向对象编程,则将方法想像成一个规范特定对象的函数,可能会有所帮助。通过将一则消息发送到——或发消息给——一个对象,您可调用该对象的一个方法。Objective-C 中有两种类型的方法:实例方法和类方法。
-
实例方法,由类的实例来执行。换言之,在调用实例方法之前,必须先创建该类的实例。实例方法是最常见的方法类型。
-
类方法,可由它所在的类直接执行。它不需要对象的实例作为消息的接收者。
方法声明包含方法类型标识符、返回类型、一个或多个签名关键词,以及参数类型和名称信息。以下是 insertObject:atIndex:
实例方法的声明。
对于实例方法,声明前面是减号 (-
);对于类方法,对应指示器是加号 (+
)。下面的“类方法”将详细介绍类方法。
一个方法的实际名称 (insertObject:atIndex:
) 是包括冒号字符在内的所有签名关键词的串联。冒号字符表明有参数存在。在上述示例中,该方法采用两个参数。如果方法没有参数,则省略第一个(也是仅有的一个)签名关键词后面的冒号。
当您想要调用一个方法时,通过给实施该方法的对象发送一则消息来实现。(虽然“发送消息”常可与“调用方法”互换,但实际上,Objective-C 在运行时才会执行实际地发送。)消息包含方法名称,以及方法所需的参数信息(类型要匹配)。您发送到一个对象的所有消息,都被动态地分派,这样使 Objective-C 类的多态行为更加容易。(多态性是指不同类型的对象响应同一消息的能力。)有时被调用的方法,是由接收消息对象的类之超类来实现。
要分派消息,运行时需要一个消息表达式。消息表达式使用方括号([
和 ]
)将消息本身(以及任何所需参数)括起来,同时将接收消息的对象放在最左边方括号右侧。例如,要将 insertObject:atIndex:
消息发送给 myArray
变量保存的对象,您会使用以下语法:
[myArray insertObject:anObject atIndex:0]; |
为避免声明大量局部变量来储存临时结果,Objective-C 让您嵌套消息表达式。每个嵌套表达式的返回值,都用作另一个消息的一个参数或接收对象。例如,可以将上个示例中使用的任何变量替换为取回值的消息。因此,如果您具有另一个名为 myAppObject
的对象,并且此对象具有访问数组对象的方法以及要插入数组的对象,则可以将上个示例编写为如下形式:
[[myAppObject theArray] insertObject:[myAppObject objectToInsert] atIndex:0]; |
Objective-C 还提供用于调用存取方法的点记法语法。存取方法获取并设定对象的状态,因此对于封装很重要,是所有对象的重要功能。对象隐藏或封装其状态,并显示接口,该接口是访问该状态的所有实例都通用的。使用点记法语法,您可以将上个示例重新编写为如下形式:
[myAppObject.theArray insertObject:myAppObject.objectToInsert atIndex:0]; |
您还可以使用点记法语法进行赋值:
myAppObject.theArray = aNewArray; |
此语法只是编写 [myAppObject setTheArray:aNewArray];
的另一种方式。在点记法表达式中,您不能使用对动态类型化的对象(类型为 id
的对象)的引用。
在“您的首个 iOS 应用程序”中,您已经使用点语法来进行变量赋值:
self.userName = self.textField.text; |
类方法
尽管前几个示例将消息发送到了类的实例,但您也可以将消息发送到类本身。(类是运行时创建的、类型为 Class
的对象。)向类发送消息时,您指定的方法必须定义为类方法,而非实例方法。类方法是一种功能,类似于 C++ 中的静态类方法。
您通常这样使用类方法:要么将类方法用作工厂方法创建类的新实例,要么访问与该类关联的一些共享信息。类方法声明的语法,与实例方法声明的语法相同,只是方法类型标识符使用加号 (+
),而非减号。
以下示例说明如何将类方法用作类的工厂方法。在这种情况下,array
方法是 NSArray
类上的类方法——被 NSMutableArray
继承——它分配并初始化类的新实例,并将其返回给您的代码。
NSMutableArray *myArray = nil; // nil is essentially the same as NULL |
// Create a new array and assign it to the myArray variable. |
myArray = [NSMutableArray array]; |
已声明的属性和存取方法
属性通常是指某些由对象封装或储存的数据。它可以是标志(如名称或颜色),也可以是与一个或多个其他对象的关系。一个对象的类定义一个接口,该接口使其对象的用户能获取并设定所封装属性的值。执行这些操作的方法,称为存取方法。
存取方法有两种类型,每个方法都必须符合命名约定。“getter”存取方法返回属性的值,且名称与属性相同。“setter”存取方法设定属性的新值,且形式为 set
PropertyName:
,其中属性名称的第一个字母大写。正确命名的存取方法是 Cocoa 和 Cocoa Touch 框架的多种技术的关键元素,如键-值编码 (KVC),它的机制是,通过对象的名称间接访问对象的属性。
Objective-C 提供已声明的属性作为一种方便的写法,用于存取方法的声明和实现。在“您的首个 iOS 应用程序”中,您声明了 userName
属性:
@property (nonatomic, copy) NSString *userName; |
使用已声明的属性后,就不必为该类中用到的每个属性实现 getter 和 setter 方法。而是使用属性声明,指定您想要的行为。编译器接着可以根据该声明,创建或合成实际的 getter 和 setter 方法。已声明的属性减少了您必须编写的样板文件代码量,因此使代码更简洁、更少机会出错。使用已声明的属性或存取方法,来获取和设定各项对象状态。
您在类接口中包括方法声明和属性声明。您在类的头文件中声明公共属性;而在源文件的类扩展中声明专有属性。(有关类扩展的简短说明及其示例,请参阅“协议和类别”。)控制器对象(如委托和视图控制器)的属性通常应该为专有的。
属性的基本声明使用 @property
编译器指令,后面紧跟属性的类型信息和名称。您还可以使用自定选项来配置属性,以定义存取方法如何表现、属性是否为弱引用,以及是否为只读。选项位于圆括号中,前面是 @property
指令。
以下代码行说明了更多的属性声明:
@property (copy) MyModelObject *theObject; // Copy the object during assignment. |
@property (readonly) NSView *rootView; // Declare only a getter method. |
@property (weak) id delegate; // Declare delegate as a weak reference |
编译器自动合成所声明的属性。在合成属性时,它创建自己的存取方法,以及“支持”该属性的专有实例变量。实例变量的名称与属性的名称相同,但具有下划线前缀 (_
)。只有在对象初始化和取消分配的方法中,您的应用程序应该直接访问实例变量(而不是其属性)。
如果您想要让实例变量采用不同名称,可以绕过自动合成,并明确地合成属性。在类实现中使用 @synthesize
编译器指令,让编译器产生存取方法,以及进行特殊命名的实例变量。例如:
@synthesize enabled = _isEnabled; |
同时,在声明属性时,您可以指定存取方法的自定名称,通常是使 Boolean 属性的 getter 方法遵循约定形式,如下所示:
@property (assign, getter=isEnabled) BOOL enabled; // Assign new value, change name of getter method |
块
块是封装工作单元的对象,即可在任何时间执行的代码段。它们在本质上是可移植的匿名函数,可作为方法和函数的参数传入,或可从方法和函数中返回。块本身具有一个已类型化的参数列表,且可能具有推断或声明的返回类型。您还可以将块赋值给变量,然后像调用函数一样调用它。
插入符号 (^) 是用作块的语法标记。块的参数、返回值和正文(即执行的代码)存在其他类似的语法约定。下图解释了该语法,尤其是在将块赋值给变量时。
您接着可以调用块变量,就像它是一个函数一样:
int result = myBlock(4); // result is 28 |
块共享局部词法作用范围内的数据。块的这项特征非常有用,因为如果您实现一个方法,并且该方法定义一个块,则该块可以访问该方法的局部变量和参数(包括堆栈变量),以及函数和全局变量(包括实例变量)。这种访问是只读的,但如果使用 __block
修饰符声明变量,则可在块内更改其值。即使包含有块的方法或函数已返回,并且其局部作用范围已销毁,但是只要存在对该块的引用,局部变量仍作为块对象的一部分继续存在。
作为方法或函数参数时,块可用作回调。被调用时,方法或函数执行部分工作,并在适当时刻,通过块回调正在调用的代码,以从中请求附加信息,或获取程序特定行为。块使调用方在调用时能够提供回调代码。块从相同的词法作用范围内采集数据(就像宿主方法或函数所做的那样),而非将所需数据打包在“关联”结构中。由于块代码无需在单独的方法或函数中实现,您的实施代码会更简单且更容易理解。
Objective-C 框架具有许多含块参数的方法。例如,Foundation 框架的 NSNotificationCenter
类声明以下方法,该方法具有一个块参数:
- (id)addObserverForName:(NSString *)name object:(id)obj queue:(NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block |
此方法将一个观察者添加到通知中心(通知在“采用设计模式使您的应用程序合理化”中有介绍)。指定名称的一则通知发布时,块被调用以处理该通知。
opQ = [[NSOperationQueue alloc] init]; |
[[NSNotificationCenter defaultCenter] addObserverForName:@"CustomOperationCompleted" |
object:nil queue:opQ |
usingBlock:^(NSNotification *notif) { |
// handle the notification |
}]; |
协议和类别
协议可声明由任何类实施的方法,即使实施该协议的那些类没有共同的超类。协议方法定义了独立于任何特定类的行为。协议简单定义了一个由其他类负责实现的接口。当您的类实施了一个协议的方法时,就是说您的类符合该协议。
从实践角度而言,一个协议定义了一列方法,这些方法在对象之间建立合约,而无需那些对象成为任何特定类的实例。此合约使那些对象能够相互通信。一个对象想要告知另一个对象它遇到的事件,或者它可能想要寻求有关那些事件的建议。
UIApplication
类实现一个应用程序必需的行为。UIApplication
类不是强制您对 UIApplication
进行子类化,来接收有关应用程序当前状态的简单通知,而是通过调用指定给它的委托对象的特定方法,为您传送那些通知。实施 UIApplicationDelegate
协议的对象,可以接收那些通知,并提供适当的响应。
在接口块中,您指定您的类符合或采用一个协议,方法是将该协议的名称,放在尖括号 (<...>
) 中,并且放在它继承的类的名称后面。在“您的首个 iOS 应用程序”中,您指明了采用 UITextFieldDelegate
协议,代码行如下:
@interface HelloWorldViewController :UIViewController <UITextFieldDelegate> { |
您无需声明所实现的协议方法。
协议的声明,看起来类似于类接口的声明,只是协议没有父类,并且不定义实例变量(尽管它们可以声明属性)。以下示例展示使用一个方法进行一个简单的协议声明:
@protocol MyProtocol |
- (void)myProtocolMethod; |
@end |
对于许多委托协议来说,采用一个协议仅仅是实现该协议定义的方法。有些协议要求您明确地声明支持该协议,而协议可以指定必需方法和可选方法。
当您开始探索 Objective-C 框架的头文件时,将很快遇到如下的代码行:
@interface NSDate (NSDateCreation) |
这行代码通过使用圆括号将类别名称括起来的语法约定,声明了该类别。类别是 Objective-C 语言的一项功能,可让您扩展类的接口,而无需对类进行子类化。类别中的方法成为类类型的一部分(在程序的作用范围内),而这些方法由类的所有子类继承。您可以将消息发送给类(或其子类)的任何实例,以调用在类别中定义的方法。
您可以将类别用作一种手段,来对头文件内的相关方法声明进行分组。您甚至还可以将不同的类别声明放在不同的头文件中。框架在其所有头文件中使用这些技巧,来达到清晰明确。
您还可以使用称为类扩展的匿名类别,在实现 (.m
) 文件中声明专有属性和专有方法。类扩展看起来类似于类别,只是圆括号之间没有文本。例如,以下是一个典型的类扩展:
@interface MyAppDelegate () |
@property (strong) MyDataObject *data; |
@end |
已定义的类型和编码策略
Objective-C 有几个不能用作变量名称的术语,保留用于特殊用途。其中部分术语是以 @
符号为前缀的编译器指令,如 @interface
和 @end
。其他保留的术语,包括已定义的类型以及与这些类型相配的字面常量。Objective-C 使用很多已定义的类型和字面常量,这些却不会出现在 ANSI C 中。在某些情况下,这些类型和字面常量替换 ANSI C 相应的类型和字面常量。下表介绍几种重要类型,包括每种类型的允许字面常量。
类型 | 描述和字面常量 |
---|---|
| 动态对象类型。动态类型化的对象和静态类型化的对象的否定字面常量,都是 |
| 动态类类型。其否定字面常量是 |
| 选择器的数据类型 ( |
| Boolean 类型。字面常量值是 |
在错误检查和控制流代码中,通常使用已定义类型和字面常量。在程序的控制流语句中,您可以测试合适的字面常量,来确定如何继续。例如:
NSDate *dateOfHire = [employee dateOfHire]; |
if (dateOfHire != nil) { |
// handle this case |
} |
解释一下此代码,如果表示雇用日期的对象不是 nil
(换言之,如果它是一个有效对象),则逻辑在某个方向继续。以下是执行相同分支的简写形式:
NSDate *dateOfHire = [employee dateOfHire]; |
if (dateOfHire) { |
// handle this case |
} |
您甚至可以进一步减少代码行数(假设无需引用 dateOfHire
对象):
if ([employee dateOfHire]) { |
// handle this case |
} |
您可采用大体相同方式,来处理 Boolean 值。在下面的示例中,isEqual:
方法返回一个 Boolean 值。
BOOL equal = [objectA isEqual:objectB]; |
if (equal == YES) { |
// handle this case |
} |
您可以采用与测试是否存在 nil
的代码相同的方式,来缩短此代码。
在 Objective-C 中,您可以将消息发送到 nil
,而没有副作用。事实上,完全没有影响,只是如果方法应该返回一个对象,运行时就会返回 nil
。只要返回的内容类型化为一个对象,即可保证发送给 nil
的消息的返回值正常运行。
Objective-C 中其他两个重要的保留术语,是 self
和 super
。第一个术语 self
是可在消息实现内使用的局部变量,用于引用当前对象;它等同于 C++ 中的 this
。您可以用保留字 super
替换 self
,但在消息表达式中,只能作为接收者。如果您将消息发送到 self
,运行时先在当前对象的类中查找方法实现;如果在那里找不到方法,则在其超类中查找(依此类推)。如果您将消息发送到 super
,运行时先在超类中查找方法实现。
self
和 super
的主要用途,都是发送消息。当要调用的方法是由 self
的类实现时,您将消息发送到 self
。例如:
[self doSomeWork]; |
self
还用于点记法,用于调用由已声明属性合成的存取方法。例如:
NSString *theName = self.name; |
在继承自超类的方法的覆盖(即重新实现)中,通常将消息发送到 super
。在这种情况下,被调用方法与被覆盖方法的签名,都是相同的。