所有设计良好的子类都有一些共同的特征,不管它继承自什么类、作用是什么。设计不好的子类容易出现错误、不好用、难于扩展、以及性能不好,而设计良好的子类则恰恰相反。本文将为设计高效、强壮、既易于使用又可以重用的子类提供一些建议。
进一步阅读:虽然这篇文档也描述一些制作子类的技巧,但主要还是关注基本设计。文中没有描述如何使用Xcode和Interface Builder开发工具来自动化类定义的部分工作。如果要学习使用这些工具。这个部分描述的设计信息特别适用于模型对象。
本部分包含如下主要内容:
子类定义的形式
重载超类的方法
实例变量
入口和出口点
初始化还是解码?
存取方法
键-值机制
对象的基础设施
错误处理
资源管理和其它与效率有关的部分
函数、常量、和其它C类型
开发公共类的时候
子类定义的形式
Objective-C类是由一个接口部分和一个实现部分组成的。根据约定,这两个类定义部分分别放在两个文件中。接口文件(和ANSI C头文件一样)的扩展名是.h
;实现文件的扩展名为.m
。文件名中除了扩展名之外的部分通常是类的名称。这样,对于名为Controller的类,相应的文件为:
Controller.h |
Controller.m |
接口文件中包含一个构成类公共接口的方法声明的列表,还有实例变量、常量、全局字符串、以及其它数据类型的声明。@interface
导向符用于引出基本的接口声明,@end
导向符则用于终止接口的声明。@interface
导向符特别重要,因为它标识了类的名称以及直接的继承类,它的形式如下:
@interface
ClassName :
Superclass
列表3-2给出了先前假定的Controller类在加入任何声明之前的接口文件。
列表3-2 The 接口文件的基本结构
#import <Foundation/Foundation.h> |
@interface Controller : NSObject { |
} |
@end |
为了完成类接口,您必须在接口结构的恰当地方加入必要的声明,如图3-4所示。
有关这些声明类型的更多信息,请参见"实例变量"和"函数、常量、和其它C类型"部分。
开始时,您需要为接口中出现的类型导入对应的Cocoa框架和头文件。#import
预处理命令在合并指定的头文件方面和#include
类似。但是,#import
的效率更高,在编译时,它只包含之前没有直接或间接包含的头文件。命令之后的尖括号(<...>
)中的内容标识头文件所在的框架,斜线之后是头文件自身。这样#import
语句的语法形式如下:
#import <
Framework/
File.h>
框架必须位于框架的标准系统位置上。在列表3-2的实例中,类接口文件导入了Foundation框架中的Foundation.h
文件。和这个例子一样,Cocoa约定框架的同名头文件包含一个#import
命令的列表,列表中又包含框架的所有公共接口(以及其它公共头文件)。顺便提一下,如果您定义的类是应用程序的一部分,则只需要导入Cocoa雨伞框架中的Cocoa.h
文件就可以了。
如果您导入的头文件是工程的一部分,需要使用引号而不是尖括号来作为分隔符,例如:
#import "MyDataTypes.h" |
类实现文件的结构更加简单,如列表3-3所示。重要的一点是,在类实现文件开始部分要导入类接口文件。
列表3-3 实现文件的基本结构
#import "Controller.h" |
@implementation Controller |
@end |
您必须在@implementation
和@end
导向符之间书写所有的方法实现。根据约定,与类相关的函数实现也应该放在这两个导向符之间,但实际上可以放在文件的任何地方。私有类型(函数、结构等)的声明通常放在#import
命令和@implementation
导向符之间。
重载超类方法
顾名思义,定制类就是以一些面向具体程序的方式对超类行为进行修改的类。对超类的方法进行重载是做这种修改的常用方式。在设计子类时,一个基本的步骤是识别希望重载的方法,以及考虑如何重新实现这些方法。
虽然"什么时候对方法进行重载"部分提供了一些一般的指导原则,但是您还是需要对类进行调查,详细阅读类的头文件和文档,才能识别需要重载的超类方法,还需要找出超类的指定初始化方法,因为您必须在子类的指定初始化方法中调用相应的超类版本,才能成功进行初始化(详细信息请参见"对象的创建" 部分)。
一旦您识别了要重载的超类方法,就可以开始—纯粹从实践的角度看—将这些方法声明拷贝到您的接口文件中,再将同样的声明拷贝到.m
文件中作为方法实现的框架,并用花括号代替结尾的分号。
请记住,如"什么时候对方法进行重载"部分讨论的那样,Cocoa框架(不包含您自己的代码)通常会调用您重载的框架方法。在某些情况下,您需要让框架知道应该调用的是您重载的版本,而不是原始的方法。Cocoa为此提供了不同的解决方法,比如在Interface Builder的Info (查看器)窗口中,您可以用自己的类来代替(兼容的)框架类。如果您创建了一个定制的NSCell
类,则可以通过NSControl的setCell:
方法将它关联到特定的控件上。
实例变量
创建定制类的原因除了修改超类的行为之外,还有增加新的属性。这里提到的“属性”既指子类实例的属性,也指该实例对其它对象的引用(也就是它的关系)。假定有一个Circle类,如果您要生成一个子类来为形状加入颜色,则子类在某些程度上必须携带颜色属性。为此,您可能要在类接口中加入一个color
实例变量(可能是类型为NSColor
的对象)。您的定制类的实例需要封装这个新的属性,将它作为一个持久的特征数据加以保有。
Objective-C的实例变量是指对象、结构、以及作为类定义一部分的其它数据类型声明。如果是对象,则类型的声明可以是动态(使用id
)的,也可以是静态的。下面的例子显示了两种声明风格:
id delegate; |
NSColor *color; |
一般说来,当对象所属的类不确定或不重要时,就使用动态类型来声明实例变量。以实例变量保有的对象需要进行创建、拷贝、或显式的保持—如果父对象没有对其进行保持的话(使用委托对象也是一样的)。如果对象实例是通过解档生成的,则应该在initWithCoder:
方法进行解码和赋值的时候保持对象实例变量(更多对象归档和解档的信息请参见"入口点和出口点"部分)。
实例变量的命名规则是使用小写字符串,不包含标点符号和特殊字符。如果变量名称包含多个词,就直接把这些词连起来,且第二个及之后的词的首字大写。比如:
NSString *title; |
NSColor *backgroundColor; |
NSRange currentSelectedRange; |
实例变量声明之前的IBOutlet
标识表示一个带有连接的插座变量,该连接信息存储在nib文件中。IBOutlet
标识也使Interface Builder和Xcode可以协调自己的活动。从 Interface Builder的nib文件解档的插座变量是自动保持的。
实例变量不仅仅可以保有提供给对象客户的对象属性。有些时候,实例变量也可以保有一些私有数据,用于支持对象执行某些任务,后备存储器或缓存就是这样的例子(如果数据不是基于实例,而是在类的多个实例之间共享,则需要全局变量,而不是实例变量)。
当您为子类增加实例变量时,下面这些原则值得注意:
-
只加入一些绝对必要的实例变量。您加入的实例变量越多,实例的尺寸越大。而且,您的类实例创建得越多,对开销的关注就越大。在可能的情况下,尽量从现有的实例变量中计算出一个关键值,而不是增加新的实例变量。
-
出于同样的经济上的原因,尽量有效使用类的实例数据。举例来说,如果您希望将一些标志指定为实例变量,则可以用位域来代替一系列布尔声明(然而需要知道,位域的归档复杂一些)。您可以通过
NSDictionary
对象来把一些互相关联的属性整合成键-值对。如果采取这种方法,需要确保键有良好的文档说明。 -
赋给实例变量正确的作用域。永远不要将变量设置为
@public
,因为这违反了封装的原则。如果您定义的类(比如您的应用程序类)的子类可能需要使用且需要进行高效访问,可以使用@protected
。否则,@private
就是合理的选择,这可以很大程度上隐藏实现的细节。对于框架输出的、被应用程序或其它框架使用的类,隐藏实现细节特别重要;这样可以在修改类实现时,不必重新编译所有客户代码。 -
确保类基本属性对应的实例变量有存取方法(存取方法用于获取和设置实例变量的值)。这个主题的更多信息请参见"存取方法"部分。
-
如果您希望将子类公开化—也就是说,您希望其它人基于您的子类派生新的类—可以在实例变量列表最后补丁一些保留子段,通常使用
id
作为类型。如果在将来的某个时候,您需要在类中加入另一个实例变量,保留字段有助于保证二进制的兼容性。更多为公共类做准备的信息,请参见"类什么时候是公共的"部分。
入口点和出口点
Cocoa框架会在对象生命周期的不同时候向对象发送信息。几乎所有对象(包括类,它们本身实际上也是个对象)在运行时生命周期开始时和对象被析构之前,都会收到特定的消息。这些消息调用的方法(如果实现的话)是一些“勾子”,使对象可以在特定时机上执行一些任务。这些方法(按照调用顺序)如下:
-
initialize
这个类方法使类可以在自身或其实例接收任何其它消息之前,自行进行初始化。超类在子类之前接收到这个消息。使用老的归档机制的类可以通过实现初始化方法来设置类的版本。其它可能的初始化工作包括注册某些服务,以及初始化那些所有实例都使用的全局状态。然而,有些时候更好的做法是将这些工作推迟到某个实例方法来实现(也就是在首次使用的时候),而不是在初始化时实现。 -
init
(或其它初始化方法)如果超类的指定初始化方法所做的工作不足以初始化您的类,就必须实现init
或其它基本初始化方法,以便对类实例状态进行初始化。有关初始化方法,包括指定初始化方法的信息,请参见"对象的创建"部分。 -
initWithCoder:
如果您希望类对象可以被归档—比如当您的类是模型类的时候—则应该采纳(必要的话)NSCoding
协议,并实现其中的两个方法:initWithCoder:
和encodeWithCoder:
,并在这两个方法中分别将对象的实例变量编码和解码为适合归档的形式。为此,您可以使用NSCoder
类的解码和编码方法,以及NSKeyedArchiver
和NSKeyedUnarchiver
类提供的键-归档的支持。当对象通过解档的方式生成、而不是显式创建时,initWithCoder:
方法(而不是其它的初始化方法)就会被调用。在这个方法中,您可以在解码实例变量值之后将它们赋给相应的变量,并在必要时对其进行保持或拷贝。 -
awakeFromNib
应用程序在装载nib文件时,会向从档案中装载的对象发送一个awakeFromNib
消息,但只是发送给可以响应该消息的对象,且在档案中的所有对象都装载和初始化完成后发送。当对象接收到awakeFromNib
消息时,Cocoa会保证对象中所有的插座实例变量都准备好了。典型情况下,拥有nib文件的对象(File’s Owner)会实现awakeFromNib
方法,以执行插座变量和目标-动作连接准备好之后才能执行的初始化工作。 -
encodeWithCoder:
如果您的类实例需要支持归档,则需要实现这个方法。这个方法在对象析构之前被调用。请参见上面的initWithCoder:
方法的描述。 -
dealloc
您可以实现这个方法,以释放实例变量,解除类实例占用的其它所有内存。在这个方法返回后不久,实例就会被销毁。有关dealloc
方法的进一步信息。
一个应用程序的全局应用程序对象(由NSApp表示
)在应用程序生命周期的开始和结束时也会向相应的对象发送消息—如果该对象是NSApp
的委托并实现了恰当的方法的话。在应用程序启动后,NSApp
会向它的委托对象发送applicationWillFinishLaunching:
和applicationDidFinishLaunching:
消息(前者在任何被双击的文档打开之前发送,后者则在文档被打开之后发送)。委托对象可以在这两个方法中任何一个实现应用程序在运行时存在之前需要的一次性全局逻辑。NSApp
在终止运行之前会向其委托对象发送applicationWillTerminate:
消息,您可以在这个方法中保存文档和应用程序状态,以便优雅地终止应用程序。
Cocoa框架为您提供了很多其它的事件挂钩,从窗口的关闭到应用程序的激活,以及从活动状态变为不活动状态。这些挂钩通常实现为委托消息,要求您的对象成为框架对象的委托,并实现必要的方法。有些时候,挂钩也可以是通告。
初始化还是解码?
如果您希望类的对象可以支持归档和解档,则该类必须遵循NSCoding
协议;必须实现对对象进行编码(encodeWithCoder:
)和解码(initWithCoder:
)的方法。和初始化方法或其它方法不同的是,调用initWithCoder:
方法是为了初始化解档得到的对象。
由于类的初始化方法和initWithCoder:
可能会做很多同样的工作,一个合理的做法是将一些共同的工作实现为一个辅助方法,然后在初始化方法和initWithCoder:
中调用。举例来说,如果一个对象在配置例程中需要指定拖拽类型和拖拽源,则可能需要按列表3-4所示的方式来实现:
列表 3-4 辅助的初始化方法
(id)initWithFrame:(NSRect)frame { |
self = [super initWithFrame:frame]; |
if (self) { |
[self setTitleColor:[NSColor lightGrayColor]]; |
[self registerForDragging]; |
} |
return self; |
} |
- (id)initWithCoder:(NSCoder *)aCoder { |
self = [super initWithCoder:aCoder]; |
titleColor = [[aCoder decodeObject] copy]; |
[self registerForDragging]; |
return self; |
} |
- (void)registerForDragging { |
[theView registerForDraggedTypes: |
[NSArray arrayWithObjects:DragDropSimplePboardType, NSStringPboardType, |
NSFilenamesPboardType, nil]]; |
[theView setDraggingSourceOperationMask:NSDragOperationEvery forLocal:YES]; |
} |
类的初始化方法和initWithCoder:
在角色上的并行性也有例外。在为框架类创建定制子类、而框架类的实例出现在Interface Builder的选盘上时,在工程中定义子类的一般过程是将对象从Interface Builder选盘中拖到界面上,然后通过Interface Builder的Info窗口中的Custom Class(定制类)面板把对象和定制子类关联起来。但是在这种情况下,子类的initWithCoder:
方法在解档时并不被调用,取而代之的是发送一个init
消息。定制对象的所有特殊的配置任务都应该在awakeFromNib
方法中执行,该方法在nib文件中所有对象解档完成后被调用。
存取方法
存取方法(或者简单地称为“存取器”)负责获取或设置实例变量的值。它们是设计良好的类接口的必要组成部分,相当于通向对象属性的一道门槛,负责提供对象属性的访问通道,并强制对对象实例数据的封装。
由于命名规范的原因—也由于这种命名规范使类得以遵循键-值编码的约定—存取方法必须以指定的形式命名。对于返回实例变量值的方法(有时也称为getter),就简单地使用实例变量名;对于为实例变量设置值的方法(也称为setter),其名称以“set”开头,后面紧接着实例变量的名称(首字母大写)。例如,如果您有一个名为“color”的实例变量,则其getter和setter存取方法的声明应为:
- (NSColor *)color; |
- (void)setColor:(NSColor *)aColor; |
如果实例变量的类型为C的数值类型,比如int
或者float
,则存取方法的实现就趋于非常简单。假定有一个实例变量名为currentRate
,类型为float
,列表3-5显示如何为其实现存取方法。
列表3-5 为非对象实例变量实现存取方法
- (float)currentRate { |
return currentRate; |
} |
- (void)setCurrentRate:(float)newRate { |
currentRate = newRate; |
} |
如果实例变量保存的是对象,情况就有些细微的区别。由于是实例变量,所以这些对象必须可以持久,因此在赋值时必须进行创建、拷贝、或保持。setter方法在改变实例变量的值时,不仅要保证它的可持久性,还要正确处理原来的值;getter方法则是将实例变量的值传给发出请求的对象。两类存取方法的操作在内存管理方面都有源自Cocoa对象所有权策略的两个隐含假定:
-
方法(比如getter存取方法)返回的对象在调用该方法的对象的作用域内是正当的。换句话说,需要保证对象在该作用域内(如果没有其它文档说明的话)不被释放或修改。
-
调用对象从某个方法(比如存取方法)接收到对象时,不应该对其进行释放,除非它先前进行显式的保持(或拷贝)。
记住这两个假定之后,让我们看一个名为title
、类型为NSString的实例变量的getter和setter存取方法的两种可能的实现。列表3-6显示了第一种实现方式。
列表3-6 为对象实例变量实现存取方法—好技术
- (NSString *)title { |
return title; |
} |
- (void)setTitle:(NSString *)newTitle { |
if (title != newTitle) { |
[title autorelease]; |
title = [newTitle copy]; |
} |
} |
请注意,getter方法只是简单地返回实例变量的引用。相比之下,setter方法更忙一些,在确认传入值和当前值不一样之后,它就自动释放当前值,然后将新值拷贝到实例变量上(向对象发送autorelease
消息比发送release
消息更加“线程安全”一些)。然而,这种方法仍然有潜在的危险。如果一个客户正在使用getter方法返回的对象,与此同时setter方法自动释放了老的NSString对象,且该对象在之后很快被释放和销毁,那会有什么结果呢?客户对象对实例变量的引用将不再是正当的了。
列表3-7显示了存取方法的另一种实现,通过在getter方法中保持和自动释放实例变量的值,使这个问题得到解决。
列表3-7 为对象实例变量实现存取方法—更好技术
- (NSString *)title { |
return [[title retain] autorelease]; |
} |
- (void)setTitle:(NSString *)newTitle { |
if (title != newTitle) { |
[title release]; |
title = [newTitle copy]; |
} |
} |
在上面两个setter方法的实例(列表3-6和列表3-7)中,新的NSString
实例变量是通过拷贝,而不是通过保持得到。为什么不使用保持的方式呢?一般的规则是:当赋给实例变量的对象是一个值对象时—也就是说,该对象代表的是一个诸如字符串、日期、数字、或组合记录这样的属性—您就应该进行拷贝。您感兴趣的是保留该属性的值,不希望它的值被潜在的程序修改。换句话说,您希望自己拥有该对象的拷贝。
但是,如果要存储和访问的是一个实体对象,比如NSView
或者NSWindow
对象,则应该加以保持。实体对象聚合度更高,关系更为复杂,拷贝起来开销更大一些。确定一个对象是值对象还是实体对象的方法之一是弄清楚您感兴趣的是对象的值,还是对象的本身。如果您感兴趣的是对象的值,则该对象可能是一个值对象,应该进行拷贝(当然,我们假定对象遵循NSCopying
协议)。
确定在setter方法中应该保持实例对象还是进行拷贝的另一种方法是确定实例变量是一个属性还是一种关系。这对表示应用程序数据的模型对象来说尤其正确。属性和值对象基本是相同的:它表示的是自己封装的对象的某个定义特征,比如对象的颜色(NSColor
对象)或标题(NSString
对象);另一方面,关系仅仅是指对象本身和一个或多个其它对象之间的一种关系(或者引用)。一般地说,在setter方法中,您对属性进行拷贝,而对关系进行保持。然而关系有一个基数,可能对一对一的,也可能是一对多的。一对多的关系通常由集合对象来表示,比如NSArray
或NSSet
的实例,可能需要在setter方法中做更多的工作,而不是简单地对实例变量进行保持。更多有关对象特性的信息,无论是属性还是关系。
如果一个类的setter方法以列表3-6或列表3-7所示的方式来实现,则在类的dealloc
方法中对实例变量进行解除分配时,只需要调用合适的setter方法,并传入nil
就可以了。
键-值机制
名称中带有“键-值”的几个机制都是Cocoa的基本部分:键-值绑定、键-值编码、和键-值观察。它们都是Cocoa技术的必要成分,比如对象间自动进行值的通讯和同步的绑定技术就是基于这些技术的;它们也至少为实现应用程序的脚本控制(就是使应用程序可以响应AppleScript命令)提供部分的基础设施。键-值编码和键-值观察在定制子类的设计时特别重要。
“键-值” 这个术语指的是将属性名称作为键,并通过这个键取得相应的值。这个术语是对象建模模式中使用的词汇,而对象建模模式反过来又是从描述关系数据库用到的实体-关系模型衍生出来的。在对象建模的模式中,对象—特别是模型-视图-控制器模式下与数据有关的模型对象—拥有一些特性(properties),它们在形式上通常(但并不总是)表现为实例变量。特性可以是一个属性(attribute),比如名称或者颜色,也可以是指向一个或多个其它对象的引用。这些引用称为关系,关系可以是一对一的,也可以是一对多的。一个程序中的对象网络通过它们之间的关系组成一个对象图。在对象模型中,您可以使用键路径—就是由若干个键组成的字符串,键与键之间用圆点分隔的字符串—来遍历对象图中的关系,以及访问对象的特性。
键-值绑定、键-值编码、和键-值观察是支持这种遍历的机制。
-
键-值绑定(Key-value binding,简称KVB)机制负责建立对象间的绑定关系,以及移除和公布这种绑定关系。它用了几个非正式的协议。属性的绑定必须指定一个对象和一个指向该属性的键路径。
-
键-值编码(Key-value coding,简称KVC)机制通过实现名为
NSKeyValueCoding
的非正式协议,使开发者可以通过键直接设置和获取对象属性,而不需要调用对象的存取方法(Cocoa为该协议提供了缺省的实现)。键通常和被访问对象中的实例变量或存取方法的名称相对应。 -
键-值观察(Key-value observing,简称KVO)机制通过实现名为
NSKeyValueObserving
的非正式协议,其作用是使对象可以将自己注册为其它对象的观察者。当被观察对象的属性之一发生改变时,会直接通知相应的观察者。Cocoa为遵循KVO的对象的每个属性都实现了自动观察者通知机制。
为使子类的每个属性都遵循键-值编码的要求,需要进行如下的工作:
-
对于名为key的属性或者目标基数为一(to-one)的关系,实现名为key (getter)和
set
Key:
(setter)的存取方法。举例来说,如果您有一个名为salary
的属性,则需要实现名为salary
和setSalary:
的存取方法。 -
对于一个目标基数大于一(to-many)的关系,如果其属性对应的实例变量是一个集合(比如一个
NSArray
对象)或者返回集合的存取方法,则将getter方法命名为属性名(比如employees
)。如果该属性是可以改变的,而getter方法并不返回一个可变的集合(比如NSMutableArray
),您必须实现insertObject:in
KeyAtIndex:
和removeObjectFrom
KeyAtIndex:
方法。如果实例变量不是集合类型,且getter方法不返回集合,则必须实现其它的NSKeyValueCoding
方法。
在满足自动观察者通知的基础上,简单地保证您的对象遵循KVC就可以使之遵循KVO。然而,如果您选择实现手工的键-值观察,就需要额外的工作。
对象的基础设施
如果一个子类是设计良好的,它的实例就应该以Cocoa对象期望的方式工作。使用这种对象的代码可以将它和类的其它实例相比较,探索它的内容(比如在调试器中),以及用该对象执行类似的基本操作。
定制子类应该实现绝大多数(如果不是全部的话)根类和基本协议的方法:
-
isEqual:
和hash
实现这些NSObject
方法可以在对象比较时执行某些对象的具体逻辑。举例来说,如果您的类实例根据序列号的不同进行区分,则可以将序列号作为相等比较的基础。更多信息请参见"内省"部分。 -
description
通过实现这个NSObject
方法来返回简要描述对象属性和内容的字符串。这个信息在gdb
调试器中可以通过print object
命令返回,在格式化字符串中则可以由对象的%@
指示符来使用。举例来说,假定您有一个Employee类,带有姓名(name)、雇佣日期(date of hire)、部门(department)、和位置ID(位置ID)等属性,则类的描述方法可能如下:- (NSString *)description {
return [NSString stringWithFormat:@"Employee:Name = %@,
Hire Date = %@, Department = %@, Position = %i\n", [self name],
[[self dateOfHire] description], [self department],
[self position]];
}
-
copyWithZone:
如果您希望类的客户代码对类实例进行拷贝,则应该实现这个NSCopying
协议中的方法。值对象,包括模型对象,是拷贝操作的典型候选者;而诸如NSWindow
和NSColorPanel
这样的对象则不是。如果您的类实例是可变的,则应该相应地遵循NSMutableCopying
协议。 -
initWithCoder:
和encodeWithCoder:
如果您希望自己设计的类的实例可以支持归档(比如一个模型类),则应该采纳NSCoding
协议(必要的话),并实现上面这两个方法。NSCoding
方法的更多信息请参见"入口点和出口点"部分。
如果您设计的类的所有祖先类都采纳了某个正式协议,则必须保证您的类也正确遵循该协议。也就是说,如果在您的类中协议方法的超类实现是不充分的,则应该对其进行重新实现。
错误处理
正确地进行错误处理是不言自明的编程纪律。然而,怎样处理才是“正确”的呢?这取决于不同的编程语言、应用程序环境、以及其它因素。Cocoa对于子类代码的错误处理有自己的一套约定。
-
如果在方法实现中碰到的错误是系统级错误或Objective-C的运行时错误,可以在必要时创建和抛出一个例外。如果可能的话,则最好就在当时进行处理。
在Cocoa中,例外通常是为编程时或意料之外的运行时错误保留的,比如集合的越界访问、试图改变不可修改的对象、发送不正当的消息、以及丢失窗口服务器连接发生的错误。您通常是在编写应用程序时(而不是运行时)通过例外处理这些错误。Cocoa预定义了几个例外,您可以通过例外处理器来捕捉。更多有关这些预定义例外,以及抛出和处理例外的例程和API的信息。
-
对于其它类型的错误,包括意料之中的运行时错误,请向调用者返回
nil
、NO
、NULL
、或一些恰当形式的零值。这种错误的例子包括不能进行文件的读写、对象的初始化错误、不能建立网络连接、或者不能定位集合中的对象。如果您觉得有必要向调用者返回错误的补充信息,请使用NSError
对象。NSError
对象封装了错误的有关信息,包括一个错误代码(这个代码可能专门用于Mach、POSIX、或者OSStatus域)和一个包含具体程序信息的字典。直接返回的负值(nil
、NO
等等)应该是错误的基本指示器,如果您确实需要表现更多的具体错误信息,则可以通过方法的参数间接返回一个NSError
对象。 -
如果错误需要用户输入一个选择或动作,则显示一个警告对话框。
请使用
NSAlert
类的实例(以及相关的设施)来显示警告对话框和处理用户响应。
有关NSError
对象、处理错误、和显示错误警告的更多信息。
资源管理和其它效率问题
您可以通过很多方面来增强对象以及负责组合和管理对象的应用程序的性能,包括采纳多线程技术、优化图形的描画、以及使用减少代码印迹的技术。您可以通过阅读Cocoa性能指南和其它性能文档来了解更多这方面的技术。
然而,在采纳更为先进的性能技巧之前,您可以通过遵循下面三个简单的、常识性的指导原则,显著提供对象的性能:
-
在真正需要之前,不要装载资源或分配内存。
如果您装载了程序的资源,比如一个nib文件或一幅图像,而等到之后很久才需要使用,或者根本不需要使用,这就是严重的效率低下。您的程序的内存印迹膨胀了,却没有很好的理由。您应该在马上需要使用的时候,才进行资源的装载和内存的分配。
举例来说,如果您的应用程序的偏好设置窗口是一个独立的nib文件,则在用户首次从应用程序菜单中选择Preferences(预置)命令之前,不要对其进行装载。为某些任务分配内存也需要同样的谨慎,在需要使用之前,且慢分配内存。这种迟缓装载(lazy-loading)或迟缓分配(lazy-allocation)机制是很容易实现的。举例来说,您的应用程序有一幅图像,且希望在用户首次请求时对其进行装载,以便显示在用户界面上。列表3-8显示了一种方法,即在图像的getter方法中装载。
列表3-8 资源的迟缓装载
- (NSImage *)fooImage {
if (!fooImage) { // fooImage is an instance variable
NSString *imagePath = [[NSBundle mainBundle] pathForResource:@"Foo" ofType:@"jpg"];
if (!imagePath) return nil;
fooImage = [[NSImage alloc] initWithContentsOfFile:imagePath];
}
return fooImage;
}
-
使用Cocoa的API,不要挖掘更底层的编程接口。
为了使Cocoa框架的实现尽可能强壮、安全、和高效,苹果公司已经付出了很多努力。而且,这些实现处理了框架中的可能不需要您知道的相互依赖性。如果您决定在实现某个任务时不使用Cocoa API,而是“回退”到底层的接口,则最后您很可能要编写更多代码,并因此增加了发生错误和降低效率的风险。另外,使用Cocoa可以更好地为利用未来的增强做好准备,而在底层的实现发生变化时又可以更好地隔离。因此,如果可能的话,请使用Cocoa的方法来完成编程任务。举例来说,您可以不使用BSD例程来对打开的文件进行查找,而是使用
NSFileHandle
类;或者,您可以使用NSBezierPath
和NSColor
方法来进行描画,而不是调用相应的Quartz(Core Graphics)函数。 -
为了提高定制对象的效率和强壮性,您能做的最重要的一件事可能就是使用好的内存管理技巧。确保每个对象分配(alloc)、拷贝(copy)、和保持(retain)操作都有一个匹配的
释放(release)
操作。熟悉正确的内存管理策略和技术,并对其进行实践、实践、再实践。
函数、常量、和其它C类型
由于Objective-C是ANSI C的超集,因此可以在代码中使用任何C类型,包括函数、typedef
结构、enum
常量、和宏。一个重要的设计问题是如何在定制类的接口和实现中使用这些类型。
下面的列表为您提供一些在定制类的定义中使用C类型的指导原则:
-
将经常使用而又不需要在子类中进行重载的功能定义为函数,而不是方法。这样做是因为性能上的考虑。在这种情况下,最好将代码定义为私有函数,而不是类API的一部分。您也可以把和其它类不相关(因为它是全局的)的行为或者对简单类型(C的原始类型或结构)的操作实现为函数。然而,对于全局的功能,创建一个产生单件实例的类可能更好(因为扩展性的考虑)。
-
在满足下面的条件时,定义一个结构类型比定义一个简单的类更好:
-
您不希望在字段列表中增加新的成员。
-
所有的字段都是公共的(由于性能上的原因)。
-
所有字段都不是动态的(动态字段可能要求特殊的处理,比如保持或释放)。
-
您不倾向于使用面向对象的技术,比如子类化。
即使所有的条件都满足,对于Objective-C代码中的结构的最基本判断还是性能。换句话说,如果没有显著的性能上的好处,定义一个简单类还是更好一些。
-
-
声明
enum
常量,而不是使用#define
常量。前者更适合类型判断,您可以在调试器中看到常量的值。
开发公共类的时候
在为自己定义一个定制类,比如应用程序的控制类时,您有很大的灵活性,因为您充分了解这个类,在必要时可以进行重新设计。但是,如果您的定制类可能被其它开发者用作超类—换句话说,它是个公共类—其它人对该类的期望要求您必须更加认真地进行类设计。
下面是给公共的Cocoa类开发者的一些原则: