类别扩展现有的类
定制现有的类
对象应该具有明确定义的任务,例如为特定信息建模、显示可视内容或控制信息流。正如您已经看到的,类接口定义了其他类与对象交互以帮助其完成这些任务的方式。
有时候,您可能希望通过添加仅在某些情况下有用的行为来扩展现有的类。例如,您可能会发现您的应用程序经常需要在可视化界面中显示一串字符。与其每次需要显示字符串时创建一些string-drawing对象来使用,不如让NSString类本身能够在屏幕上绘制自己的字符。
在这种情况下,将实用程序行为添加到原始的主类接口并不总是有意义的。例如,在应用程序中使用任何字符串对象时,大多数情况下都不太可能需要绘图能力,对于NSString,您不能修改原始接口或实现,因为它是一个框架类。
此外,子类化现有的类可能没有意义,因为您可能希望绘图行为不仅对原始NSString类可用,而且对该类的任何子类(如NSMutableString)也可用。而且,尽管NSString在OS X和iOS上都可用,但每个平台的绘图代码需要不同,因此需要在每个平台上使用不同的子类。
相反,Objective-C允许您通过类别和类扩展将自己的方法添加到现有的类中。
与其创建一个全新的类来为现有类提供较小的额外功能,还不如定义一个类别来为现有类添加自定义行为。可以使用类别向任何类添加方法,包括没有原始实现源代码的类,如NSString之类的框架类。
如果有类的原始源代码,则可以使用类扩展来添加新属性,或修改现有属性的属性。类扩展通常用于隐藏私有行为,以便在单个源代码文件中使用,或者在自定义框架的私有实现中使用。
category向现有类中添加方法
如果您需要向现有的类中添加方法(可能是为了添加功能以便在自己的应用程序中更容易地执行某些操作),最简单的方法是使用类别。
声明类别的语法使用@interface关键字,就像标准的Objective-C类描述一样,但不表示从子类继承。相反,它在括号中指定类别的名称,如下所示:
@interface ClassName (CategoryName)
@end
类别可以为任何类声明,即使您没有原始的实现源代码(如标准Cocoa或Cocoa Touch类)。在类别中声明的任何方法都将对原始类的所有实例以及原始类的任何子类可用。在运行时,类别添加的方法与原始类实现的方法没有区别。
XYZPerson类,它具有人名和姓的属性。如果您正在编写一个记录应用程序,您可能会发现经常需要按姓氏显示人员列表,如下所示:
Appleseed, John
Doe, Jane
Smith, Bob
Warwick, Kate
你可以在XYZPerson类中添加一个类别,就像这样:
#import "XYZPerson.h"
@interface XYZPerson (XYZPersonNameDisplayAdditions)
- (NSString *)lastNameFirstNameString;
@end
在本例中,xyzpersonnamedisplayextensions类别声明了一个额外的方法来返回必要的字符串。
类别通常在单独的头文件中声明,并在单独的源代码文件中实现。对于XYZPerson,可以在名为XYZPerson+XYZPersonNameDisplayAdditions.h的头文件中声明类别。
即使类别添加的任何方法对该类及其子类的所有实例都可用,您仍需要在希望使用其他方法的任何源代码文件中导入类别头文件,否则将遇到编译器警告和错误。
类别实现可能是这样的:
#import "XYZPerson+XYZPersonNameDisplayAdditions.h"
@implementation XYZPerson (XYZPersonNameDisplayAdditions)
- (NSString *)lastNameFirstNameString {
return [NSString stringWithFormat:@"%@, %@", self.lastName, self.firstName];
}
@end
一旦你声明了一个类别并实现了这些方法,你就可以从类的任何实例中使用这些方法,就好像它们是原始类接口的一部分:
#import "XYZPerson+XYZPersonNameDisplayAdditions.h"
@implementation SomeObject
- (void)someMethod {
XYZPerson *person = [[XYZPerson alloc] initWithFirstName:@"John"
lastName:@"Doe"];
XYZShoutingPerson *shoutingPerson =
[[XYZShoutingPerson alloc] initWithFirstName:@"Monica"
lastName:@"Robinson"];
NSLog(@"The two people are %@ and %@",
[person lastNameFirstNameString], [shoutingPerson lastNameFirstNameString]);
}
@end
除了向现有类中添加方法之外,还可以使用类别将复杂类的实现拆分为多个源代码文件。例如,如果几何计算、颜色和渐变等特别复杂,您可以将定制用户界面元素的绘图代码放在单独的文件中,以供实现的其他部分使用。或者,您可以为category方法提供不同的实现,这取决于您是为OS X还是iOS编写应用程序。
类别可用于声明实例方法或类方法,但通常不适用于声明其他属性。在类别接口中包含属性声明是有效的语法,但不可能在类别中声明额外的实例变量。这意味着编译器不会合成任何实例变量,也不会合成任何属性访问器方法。您可以在category实现中编写自己的访问器方法,但是您无法跟踪该属性的值,除非它已经被原始类存储。
向现有类添加由新实例变量支持的传统属性的惟一方法是使用类扩展,如类扩展扩展内部实现中所述。
避免类别方法名称冲突
因为类别中声明的方法被添加到现有类中,所以需要非常小心地使用方法名。
如果在类别中声明的方法的名称与原始类别中的方法相同,或与同一类别中另一类别中的方法(甚至是超类别)相同,则该行为未定义为在运行时使用哪个方法实现。如果您在自己的类中使用类别,则这不太可能成为问题,但在使用类别向标准Cocoa或Cocoa Touch类中添加方法时,可能会造成问题。
例如,使用远程web服务的应用程序可能需要一种使用Base64编码对字符串进行编码的简单方法。在NSString上定义一个category来添加一个实例方法来返回一个base64编码的字符串版本是有意义的,所以您可以添加一个名为base64EncodedString的方便方法。
如果您链接到另一个框架,而该框架恰好在NSString上定义了它自己的类别,包括它自己的方法base64EncodedString,那么就会出现问题。在运行时,只有一个方法实现会“获胜”并被添加到NSString中,但是哪个是未定义的。
如果向Cocoa或Cocoa Touch类添加方便的方法,然后在以后的版本中添加到原始类中,那么可能会出现另一个问题。例如NSSortDescriptor类,它描述了对象集合应该如何排序,它总是有一个initWithKey:: initialize方法,但是在早期的OS X和iOS版本中没有提供相应的类工厂方法。
按照惯例,类工厂方法应该被称为sortDescriptorWithKey:升序:,因此您可能已经选择在NSSortDescriptor上添加一个类别,以便提供这个方法。这是你期望在旧版本的OS X和iOS,但与Mac OS X版本10.6的发布和iOS 4.0, sortDescriptorWithKey:提升:方法添加到原始NSSortDescriptor类,这意味着你现在得到一个命名冲突当应用程序运行在这些或更高的平台。
为了避免未定义的行为,最佳实践是在框架类的categories中为方法名添加前缀,就像您应该为自己的类的名称添加前缀一样。您可以选择使用与类前缀相同的三个字母,但是在方法名称的其余部分之前,使用小写字母遵循方法名称的常规约定,然后使用下划线。对于NSSortDescriptor的例子,你自己的类别可能是这样的:
@interface NSSortDescriptor (XYZAdditions)
+ (id)xyz_sortDescriptorWithKey:(NSString *)key ascending:(BOOL)ascending;
@end
这意味着您可以确保您的方法将在运行时使用。消除了歧义,因为您的代码现在看起来是这样的:
NSSortDescriptor *descriptor =
[NSSortDescriptor xyz_sortDescriptorWithKey:@"name" ascending:YES];
类扩展扩展了内部实现
类扩展与类别有一些相似之处,但它只能添加到您在编译时拥有源代码的类中(类与类扩展在同一时间编译)。类扩展声明的方法是在原始类的@implementation块中实现的,因此不能在框架类上声明类扩展,例如Cocoa或NSString之类的Cocoa Touch类。
声明类扩展的语法类似于类别的语法,如下所示:
@interface ClassName ()
@end
由于括号中没有给出名称,所以类扩展通常称为匿名类别。
与常规类别不同,类扩展可以向类中添加自己的属性和实例变量。如果在类扩展中声明属性,如下所示:
@interface XYZPerson ()
@property NSObject *extraProperty;
@end
编译器将在主类实现中自动合成相关的访问器方法和实例变量。
如果在类扩展中添加任何方法,则必须在类的主实现中实现这些方法。
还可以使用类扩展来添加自定义实例变量。这些是在类扩展接口的括号中声明的:
@interface XYZPerson () {
id _someCustomInstanceVariable;
}
...
@end
使用类扩展来隐藏私有信息
类的主接口用于定义其他类与它交互的方式。换句话说,它是类的公共接口。
类扩展通常用于使用附加的私有方法或属性扩展公共接口,以便在类本身的实现中使用。例如,通常在接口中将属性定义为readonly,但在实现上面声明的类扩展中将属性定义为readwrite,以便类的内部方法可以直接更改属性值。
例如,XYZPerson类可能会添加一个名为uniqueIdentifier的属性,用于跟踪美国的社会安全号码等信息。
在现实世界中,通常需要大量的文书工作才能为个体分配唯一的标识符,因此XYZPerson类接口可能会将该属性声明为只读,并提供一些方法来请求分配标识符,如下所示:
@interface XYZPerson : NSObject
...
@property (readonly) NSString *uniqueIdentifier;
- (void)assignUniqueIdentifier;
@end
这意味着uniqueIdentifier不可能由另一个对象直接设置。如果一个人还没有标识符,则必须请求通过调用assignUniqueIdentifier方法来分配标识符。
为了使XYZPerson类能够在内部更改属性,有必要在类的实现文件顶部定义的类扩展名中重新声明该属性:
@interface XYZPerson ()
@property (readwrite) NSString *uniqueIdentifier;
@end
@implementation XYZPerson
...
@end
注意:readwrite属性是可选的,因为它是默认的。为了清晰起见,在重新声明属性时可能会使用它。
这意味着编译器现在还将合成setter方法,因此XYZPerson实现中的任何方法都将能够使用setter或点语法直接设置属性。
通过在XYZPerson实现的源代码文件中声明类扩展名,信息对XYZPerson类保持私有。如果另一种类型的对象试图设置该属性,编译器将生成一个错误。
注意:通过添加上面所示的类扩展名,将uniqueIdentifier属性重新声明为读写属性,setUniqueIdentifier:方法将在运行时存在于每个XYZPerson对象上,而不管其他源代码文件是否知道类扩展名。
编译器会抱怨如果代码的其他源代码文件试图调用私有方法或设置一个只读的属性,但可以避免编译器错误和利用动态运行时功能调用这些方法在其他方面,例如使用performSelector之一:……NSObject提供的方法。您应该避免必要的类层次结构或设计;相反,主类接口应该始终定义正确的“公共”交互。
如果您打算使“私有”方法或属性可用来选择其他类,例如框架中的相关类,那么您可以在单独的头文件中声明类扩展名,并将其导入需要它的源文件中。一个类通常有两个头文件,例如XYZPerson.h和XYZPersonPrivate.h。当您发布框架时,您只发布公共的XYZPerson.h头文件。
考虑类定制的其他替代方案
类别和类扩展使直接向现有类添加行为变得容易,但有时这并不是最佳选择.
面向对象编程的主要目标之一是编写可重用代码,这意味着类应该在各种可能的情况下可重用。例如,如果您正在创建一个视图类来描述一个在屏幕上显示信息的对象,那么最好考虑这个类在多种情况下是否可用。
与其硬编码有关布局或内容的决策,还不如利用继承,将这些决策保留在专门设计的方法中,以便由子类覆盖。尽管这使得重用类相对容易,但是每次您想要使用原始类时,仍然需要创建一个新的子类。
另一种方法是类使用委托对象。任何可能限制可重用性的决策都可以委托给另一个对象,让它在运行时做出这些决策。一个常见的例子是标准的表视图类(对于OS X是NSTableView,对于iOS是UITableView)。为了使泛型表视图(使用一个或多个列和行显示信息的对象)有用,它在运行时将关于其内容的决策留给另一个对象来决定。
直接与Objective-C运行时交互
Objective-C通过Objective-C运行时系统提供动态行为。
许多决策(例如在发送消息时调用哪些方法)不是在编译时做出的,而是在运行应用程序时决定的。Objective-C不仅仅是一种编译成机器码的语言。相反,它需要一个运行时系统来执行这些代码。
可以直接与此运行时系统进行交互,例如向对象添加关联引用。与类扩展不同,关联引用不会影响原始类的声明和实现,这意味着您可以将它们与无法访问原始源代码的框架类一起使用。
关联引用以类似于属性或实例变量的方式将一个对象链接到另一个对象。
使用协议
在现实生活中,公务人员在处理某些情况时往往需要遵守严格的程序。例如,执法人员在询问或收集证据时必须“遵守礼仪”。
在面向对象编程的世界中,能够定义给定情况下对象的一组行为是很重要的。例如,表视图希望能够与数据源对象通信,以了解需要显示什么。这意味着数据源必须响应表视图可能发送的一组特定消息。
数据源可以是任何类的实例,例如视图控制器(OS X上的NSViewController的子类或iOS上的UIViewController)或专用的数据源类,它可能只是继承自NSObject。为了让表视图知道对象是否适合作为数据源,重要的是能够声明对象实现了必要的方法。
Objective-C允许你定义协议,它声明了特定情况下需要使用的方法。本章描述了定义正式协议的语法,并解释了如何将类接口标记为符合协议,这意味着类必须实现所需的方法。
协议定义消息传递契约
类接口声明与该类关联的方法和属性。相反,协议用于声明独立于任何特定类的方法和属性。
定义协议的基本语法如下:
@protocol ProtocolName
//方法和属性列表
@end
协议可以包括实例方法和类方法以及属性的声明。
为了使视图尽可能地可重用,关于信息的所有决策都应该留给另一个对象,数据源。这意味着同一个视图类的多个实例仅通过与不同的源通信就可以显示不同的信息。
饼图视图所需的最小信息包括段数、每个段的相对大小和每个段的标题。因此,饼图的数据源协议可能是这样的:
@protocol XYZPieChartViewDataSource
- (NSUInteger)numberOfSegments;
- (CGFloat)sizeOfSegmentAtIndex:(NSUInteger)segmentIndex;
- (NSString *)titleForSegmentAtIndex:(NSUInteger)segmentIndex;
@end
饼图视图类接口需要一个属性来跟踪数据源对象。这个对象可以是任何类,所以基本属性类型将是id。关于这个对象,我们只知道它符合相关协议。
为视图声明数据源属性的语法如下所示:
@interface XYZPieChartView : UIView
@property (weak) id dataSource;
...
@end
Objective-C使用尖括号表示协议的一致性。这个例子为一个符合XYZPieChartViewDataSource协议的通用对象指针声明了一个弱属性。
注意:由于前面描述的对象图管理原因,委托和数据源属性通常被标记为弱,以避免强引用周期。
通过在属性上指定所需的协议一致性,如果试图将属性设置为不符合协议的对象,即使基本属性类类型是泛型的,也会得到编译器警告。对象是UIViewController的实例还是NSObject的实例并不重要。重要的是它符合协议,这意味着饼图视图知道它可以请求所需的信息。
协议可以有可选的方法
默认情况下,协议中声明的所有方法都是必需的方法。这意味着任何符合协议的类都必须实现这些方法。
还可以在协议中指定可选方法。只有在需要时,类才能实现这些方法。
例如,您可以决定饼图上的标题应该是可选的。如果数据源对象没有实现titleForSegmentAtIndex:,视图中不应该显示标题。
您可以使用@optional指令将协议方法标记为可选,如下所示:
@protocol XYZPieChartViewDataSource
- (NSUInteger)numberOfSegments;
- (CGFloat)sizeOfSegmentAtIndex:(NSUInteger)segmentIndex;
@optional
- (NSString *)titleForSegmentAtIndex:(NSUInteger)segmentIndex;
@end
在这种情况下,只有titleForSegmentAtIndex:方法被标记为optional。前面的方法没有指令,因此假设是必需的。
@optional指令适用于任何遵循它的方法,要么直到协议定义结束,要么直到遇到另一个指令,比如@required。您可以向协议中添加更多的方法,如下所示:
@protocol XYZPieChartViewDataSource
- (NSUInteger)numberOfSegments;
- (CGFloat)sizeOfSegmentAtIndex:(NSUInteger)segmentIndex;
@optional
- (NSString *)titleForSegmentAtIndex:(NSUInteger)segmentIndex;
- (BOOL)shouldExplodeSegmentAtIndex:(NSUInteger)segmentIndex;
@required
- (UIColor *)colorForSegmentAtIndex:(NSUInteger)segmentIndex;
@end
这个例子定义了一个协议,其中包含三个必需的方法和两个可选的方法。
检查可选方法是否在运行时实现
如果协议中的方法被标记为可选,那么在尝试调用该方法之前,必须检查对象是否实现了该方法。
例如,饼图视图可以测试段标题方法,如下所示:
NSString *thisSegmentTitle;
if ([self.dataSource respondsToSelector:@selector(titleForSegmentAtIndex:)]) {
thisSegmentTitle = [self.dataSource titleForSegmentAtIndex:index];
}
方法使用选择器,该选择器引用编译后方法的标识符。您可以通过使用@selector()指令并指定方法的名称来提供正确的标识符。
如果本例中的数据源实现了该方法,则使用标题;否则,标题保持为nil。
记住:局部对象变量会自动初始化为nil。
如果您试图在符合上面定义的协议的id上调用respondsToSelector:方法,您将得到一个编译器错误,该错误没有已知的实例方法。一旦用协议限定了id,所有静态类型检查都会返回;如果尝试调用任何未在指定协议中定义的方法,则会得到错误。避免编译器错误的一种方法是将自定义协议设置为采用NSObject协议。
协议继承自其他协议
就像Objective-C类可以从超类继承一样,您也可以指定一个协议与另一个协议一致。
例如,最好的做法是定义符合NSObject协议的协议(NSObject的一些行为从它的类接口分离为一个单独的协议;NSObject类采用NSObject协议)。
通过指示您自己的协议符合NSObject协议,您就指示了采用自定义协议的任何对象也将为每个NSObject协议方法提供实现。因为您可能正在使用NSObject的某个子类,所以不必担心为这些NSObject方法提供自己的实现。但是,协议的采用对于上述情况是有用的。
要指定一个协议与另一个协议一致,可以在尖括号中提供另一个协议的名称,如下所示:
@protocol MyProtocol
...
@end
在本例中,任何采用MyProtocol的对象也有效地采用了NSObject协议中声明的所有方法。
符合协议
表示类采用协议的语法同样使用尖括号,如下所示
@interface MyClass : NSObject
...
@end
这意味着MyClass的任何实例不仅将响应接口中特定声明的方法,而且还将为MyProtocol中所需的方法提供实现。不需要在接口类中重新声明协议方法——协议的采用就足够了。
注意:编译器不会自动合成所采用协议中声明的属性。
如果需要一个类来采用多个协议,可以将它们指定为逗号分隔的列表,如下所示:
@interface MyClass : NSObject
...
@end
提示:如果您发现自己在一个类中采用了大量协议,这可能意味着您需要重构一个过于复杂的类,方法是将必要的行为划分到多个更小的类中,每个类都有明确定义的职责。
对于新的OS X和iOS开发人员来说,一个相对常见的缺陷是使用单个应用程序委托类来包含应用程序的大部分功能(管理底层数据结构,将数据提供给多个用户界面元素,以及响应手势和其他用户交互)。随着复杂性的增加,类变得更加难以维护
一旦您表明了与协议的一致性,该类至少必须为每个必需的协议方法以及您选择的任何可选方法提供方法实现。如果您未能实现任何必需的方法,编译器将警告您。
注意:协议中的方法声明与任何其他声明一样。实现中的方法名和参数类型必须与协议中的声明匹配。
Cocoa和Cocoa Touch定义了大量的协议
Cocoa和Cocoa Touch对象在各种不同的情况下使用协议。例如,表视图类(OS X的NSTableView和iOS的UITableView)都使用一个数据源对象为它们提供必要的信息。两者都定义了自己的数据源协议,其使用方式与上面的XYZPieChartViewDataSource协议示例非常相似。这两个表视图类还允许您设置一个委托对象,该对象同样必须符合相关的NSTableViewDelegate或UITableViewDelegate协议。委托负责处理用户交互,或定制某些条目的显示。
一些协议用于表示类之间的非层次相似性。有些协议没有链接到特定的类需求,而是与更通用的Cocoa或Cocoa Touch通信机制相关,这些机制可能被多个不相关的类采用。
例如,许多框架模型对象(例如NSArray和NSDictionary这样的集合类)支持NSCoding协议,这意味着它们可以对属性进行编码和解码,以便作为原始数据存档或分发。NSCoding使得将整个对象图写入磁盘变得相对容易,前提是图中的每个对象都采用该协议。
一些Objective-C语言级的特性也依赖于协议。例如,为了使用快速枚举,集合必须采用NSFastEnumeration协议,就像快速枚举中描述的那样,这使得枚举集合变得很容易。此外,可以复制一些对象,例如在使用具有复制属性的属性时(如复制属性中所述),可以维护它们自己的副本。您试图复制的任何对象都必须采用NSCopying协议,否则您将得到一个运行时异常。
协议用于匿名
协议在对象的类未知或需要隐藏的情况下也很有用。
例如,框架的开发人员可以选择不发布框架中某个类的接口。因为类名是未知的,所以框架的用户不可能直接创建该类的实例。相反,框架中的其他一些对象通常被指定为返回一个现成的实例,如下所示:
id utility = [frameworkObject anonymousUtility];
为了使这个匿名对象变得有用,框架的开发人员可以发布一个协议,该协议揭示了它的一些方法。即使没有提供原始的类接口,这意味着类仍然是匿名的,对象仍然可以以一种有限的方式使用:
id utility = [frameworkObject anonymousUtility];
例如,如果你正在编写一个使用Core Data框架的iOS应用程序,你可能会遇到NSFetchedResultsController类。该类的设计目的是帮助数据源对象向iOS UITableView提供存储的数据,使其更容易提供行数等信息。
如果您使用的是内容被分割为多个部分的表视图,您还可以向提取的结果控制器询问相关的部分信息。NSFetchedResultsController类不返回包含此节信息的特定类,而是返回一个匿名对象,该对象符合NSFetchedResultsSectionInfo协议。这意味着仍然可以查询该对象以获得所需的信息,比如一个section中的行数:
NSInteger sectionNumber = ...
id sectionInfo =
[self.fetchedResultsController.sections objectAtIndex:sectionNumber];
NSInteger numberOfRowsInSection = [sectionInfo numberOfObjects];
即使您不知道sectionInfo对象的类,NSFetchedResultsSectionInfo协议规定它可以响应numberOfObjects消息。
值和集合
虽然Objective-C是一种面向对象的编程语言,但它是C的超集,这意味着在Objective-C代码中可以使用任何标准的C标量(非对象)类型,如int、float和char。在Cocoa和Cocoa Touch应用程序中还可以使用其他标量类型,例如NSInteger、NSUInteger和CGFloat,它们根据目标体系结构有不同的定义。
标量类型用于不需要使用对象表示值的好处(或相关的开销)的情况。虽然字符串通常表示为NSString类的实例,但数值通常存储在标量局部变量或属性中。
在Objective-C中声明一个c风格的数组是可能的,但是您会发现Cocoa和Cocoa Touch应用程序中的集合通常使用类的实例来表示,比如NSArray或NSDictionary。这些类只能用于收集Objective-C对象,这意味着在将它们添加到集合之前,您需要创建类的实例,如NSValue、NSNumber或NSString,以表示值。
Objective-C中有基本的C基本类型
每种标准的C标量变量类型在Objective-C中都是可用的:
int someInteger = 42;
float someFloatingPointNumber = 3.1415;
double someDoublePrecisionFloatingPointNumber = 6.02214199e23;
以及标准C运算符:
int someInteger = 42;
someInteger++; // someInteger == 43
int anotherInteger = 64;
anotherInteger--; // anotherInteger == 63
anotherInteger *= 2; // anotherInteger == 126
如果你对Objective-C属性使用标量类型,像这样:
@interface XYZCalculator : NSObject
@property double currentValue;
@end
当通过点语法访问值时,也可以在属性上使用C运算符,如下所示:
@implementation XYZCalculator
- (void)increment {
self.currentValue++;
}
- (void)decrement {
self.currentValue--;
}
- (void)multiplyBy:(double)factor {
self.currentValue *= factor;
}
@end
点语法纯粹是访问器方法调用的语法包装器,因此本例中的每个操作都相当于首先使用get访问器方法获取值,然后执行操作,然后使用set访问器方法将值设置为结果。
Objective-C定义了额外的基本类型
BOOL标量类型在Objective-C中定义为保存布尔值,布尔值为YES或NO。如您所料,YES在逻辑上等价于true和1,而NO等价于false和0。
Cocoa和Cocoa Touch对象上的方法的许多参数也使用特殊的标量数字类型,例如NSInteger或CGFloat。
@protocol NSTableViewDataSource
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView;
...
@end
这些类型,如NSInteger和NSUInteger,根据目标体系结构的不同而有不同的定义。在构建32位环境(如iOS)时,它们分别是32位有符号整数和无符号整数;在为64位环境(如现代OS X运行时)构建时,它们分别是64位有符号整数和无符号整数。
如果您可能跨API边界(包括内部API和导出API)传递值,例如应用程序代码和框架之间的方法或函数调用中的参数或返回值,那么最好使用这些特定于平台的类型。
对于局部变量,比如循环中的计数器,如果您知道值在标准范围内,那么使用基本的C类型是没有问题的。
C结构可以保存原始值
一些Cocoa和Cocoa Touch API使用C结构来保存它们的值。例如,可以向string对象请求子字符串的范围,如下所示:
NSString *mainString = @"This is a long string";
NSRange substringRange = [mainString rangeOfString:@"long"];
如果需要编写自定义绘图代码,则需要与Quartz交互,Quartz需要基于CGFloat数据类型的结构,比如OS X上的NSPoint和NSSize, iOS上的CGPoint和CGSize。同样,根据目标体系结构的不同,CGFloat的定义也不同。
数字由NSNumber类的实例表示
NSNumber类用于表示任何基本的C标量类型,包括char、double、float、int、long、short和每种类型的无符号变体,以及Objective-C布尔类型BOOL。
和NSString一样,你有很多创建NSNumber实例的选项,包括分配和初始化或者类工厂方法:
NSNumber *magicNumber = [[NSNumber alloc] initWithInt:42];
NSNumber *unsignedNumber = [[NSNumber alloc] initWithUnsignedInt:42u];
NSNumber *longNumber = [[NSNumber alloc] initWithLong:42l];
NSNumber *boolNumber = [[NSNumber alloc] initWithBOOL:YES];
NSNumber *simpleFloat = [NSNumber numberWithFloat:3.14f];
NSNumber *betterDouble = [NSNumber numberWithDouble:3.1415926535];
NSNumber *someChar = [NSNumber numberWithChar:'T'];
也可以使用Objective-C文字语法创建NSNumber实例:
NSNumber *magicNumber = @42;
NSNumber *unsignedNumber = @42u;
NSNumber *longNumber = @42l;
NSNumber *boolNumber = @YES;
NSNumber *simpleFloat = @3.14f;
NSNumber *betterDouble = @3.1415926535;
NSNumber *someChar = @'T';
这些示例相当于使用NSNumber类工厂方法。
一旦创建了NSNumber实例,就可以使用访问器方法之一来请求标量值:
int scalarMagic = [magicNumber intValue];
unsigned int scalarUnsigned = [unsignedNumber unsignedIntValue];
long scalarLong = [longNumber longValue];
BOOL scalarBool = [boolNumber boolValue];
float scalarSimpleFloat = [simpleFloat floatValue];
double scalarBetterDouble = [betterDouble doubleValue];
char scalarChar = [someChar charValue]
使用NSValue类的实例表示其他值
NSNumber类本身是基本NSValue类的子类,该类提供了一个对象包装器,用于包装单个值或数据项。除了基本的C标量类型,NSValue还可以用来表示指针和结构。
NSValue类提供了各种工厂方法来创建一个具有给定标准结构的值,这使得创建一个实例来表示NSRange变得很容易,就像本章前面的例子:
NSString *mainString = @"This is a long string";
NSRange substringRange = [mainString rangeOfString:@"long"];
NSValue *rangeValue = [NSValue valueWithRange:substringRange];
也可以创建NSValue对象来表示自定义结构。如果你特别需要使用C结构(而不是Objective-C对象)来存储信息,就像这样:
typedef struct {
int i;
float f;
} MyIntegerFloatStruct;
您可以通过提供指向该结构的指针以及经过编码的Objective-C类型来创建NSValue实例。@encode()编译器指令用于创建正确的Objective-C类型,如下所示:
struct MyIntegerFloatStruct aStruct;
aStruct.i = 42;
aStruct.f = 3.14;
NSValue *structValue = [NSValue value:&aStruct
withObjCType:@encode(MyIntegerFloatStruct)];
标准的C引用操作符(&)用于为值参数提供aStruct的地址。
基本的NSArray、NSSet和NSDictionary类是不可变的,这意味着它们的内容是在创建时设置的。每个类都有一个可变的子类,允许您随意添加或删除对象。
数组是有序集合
NSArray用于表示对象的有序集合。唯一的要求是每个项都是Objective-C对象——不要求每个对象都是同一个类的实例。
文字语法
也可以用Objective-C文字创建一个数组,像这样:
NSArray *someArray = @[firstObject, secondObject, thirdObject];
在使用这种文字语法时,不应该使用nil来终止对象列表,事实上nil是一个无效值。如果您尝试执行以下代码,您将在运行时获得异常,例如:
id firstObject = @"someString";
id secondObject = nil;
NSArray *someArray = @[firstObject, secondObject];
//异常:“尝试插入nil对象”
如果您确实需要在某个集合类中表示nil值,那么应该使用NSNull单例类,就像在用NSNull表示nil中所描述的那样。
排序数组对象
NSArray类还提供了多种方法来对其收集的对象进行排序。因为NSArray是不可变的,所以每个方法都返回一个新的数组,其中包含按排序顺序排列的项。
例如,您可以通过对每个字符串调用compare:的结果对字符串数组进行排序,如下所示:
NSArray *unsortedStrings = @[@"gammaString", @"alphaString", @"betaString"];
NSArray *sortedStrings =
[unsortedStrings sortedArrayUsingSelector:@selector(compare:)];
集合是无序的集合
NSSet类似于数组,但维护一组无序的不同对象。
因为集合不维护顺序,所以在测试成员关系时,它们提供了优于数组的性能改进。
基本的NSSet类也是不可变的,所以它的内容必须在创建时指定,使用分配和初始化或者类工厂方法,就像这样:
NSSet *simpleSet =
[NSSet setWithObjects:@"Hello, World!", @42, aValue, anObject, nil];
与NSArray一样,initWithObjects:和setWithObjects:方法都采用以零结尾的可变数量的参数。
集合仅存储对单个对象的一个引用,即使您尝试多次添加对象:
NSNumber *number = @42;
NSSet *numberSet =
[NSSet setWithObjects:number, number, number, number, nil];
// numberSet 只包含一个对象
字典收集键-值对
NSDictionary不是简单地维护有序或无序的对象集合,而是根据给定的键存储对象,然后可以使用这些键进行检索。
注意:可以使用其他对象作为键,但是重要的是要注意每个键都被字典复制以供使用,因此必须支持NSCopying。
但是,如果您希望能够使用键值编码(如键值编码编程指南中所述),则必须为dictionary对象使用字符串键。
用NSNull表示nil
不可能向本节描述的集合类中添加nil,因为Objective-C中的nil意味着“没有对象”。如果需要在集合中表示“无对象”,可以使用NSNull类:
NSArray *array = @[ @"string", @42, [NSNull null] ];
NSNull是一个单例类,这意味着null方法总是返回相同的实例。这意味着您可以检查数组中的对象是否等于共享的NSNull实例:
for (id object in array) {
if (object == [NSNull null]) {
NSLog(@"Found a null object");
}
}
快速枚举使枚举集合变得容易
许多集合类遵循NSFastEnumeration协议,包括NSArray、NSSet和NSDictionary。这意味着您可以使用快速枚举,这是Objective-C语言级别的特性。
枚举数组或集合内容的快速枚举语法如下所示:
for ( in ) {
...
}
例如,您可以使用快速枚举来记录数组中每个对象的描述,如下所示:
for (id eachObject in array) {
NSLog(@"Object: %@", eachObject);
}
对于每次循环,eachObject变量被自动设置为当前对象,因此每个对象出现一条日志语句。
如果你使用快速枚举的字典,你迭代的字典键,像这样:
for (NSString *eachKey in dictionary) {
id object = dictionary[eachKey];
NSLog(@"Object: %@ for key: %@", object, eachKey);
}
快速枚举的行为类似于标准的C for循环,因此可以使用break关键字中断迭代,或者continue前进到下一个元素。
int index = 0;
for (id eachObject in array) {
NSLog(@"Object at index %i is: %@", index, eachObject);
index++;
}
即使集合是可变的,也不能在快速枚举期间更改集合。如果尝试从循环中添加或删除收集的对象,将生成运行时异常。
注意:因为使用C赋值运算符(=)是程序员常见的错误,当你指的是等号运算符(==)时,编译器会警告你,如果你在一个条件分支或循环中设置一个变量,像这样:
if (someVariable = YES) {
...
}
如果你真的想重新分配一个变量(整个赋值的逻辑值是左边的最终值),你可以把赋值放在括号里来表示,就像这样:
if ( (someVariable = YES) ) {
...
}
许多集合支持基于块的枚举
也可以使用block枚举NSArray, NSSet和NSDictionary。
处理错误
几乎每个应用程序都会遇到错误。其中一些错误将超出您的控制范围,例如耗尽磁盘空间或丢失网络连接。其中一些错误是可恢复的,比如无效的用户输入。而且,当所有开发人员都在追求完美时,偶尔也会出现程序员错误。
如果您来自其他平台和语言,那么您可能已经习惯了为大多数错误处理处理异常。当您使用Objective-C编写代码时,异常仅用于程序员错误,如越界数组访问或无效的方法参数。在发布应用程序之前,您应该在测试过程中发现并修复这些问题。
所有其他错误都由NSError类的实例表示。
对大多数错误使用NSError
错误是任何应用程序生命周期中不可避免的一部分。例如,如果您需要从远程web服务请求数据,可能会出现各种潜在的问题,包括:
1.没有网络连接
2.远程web服务可能无法访问
3.远程web服务可能无法提供您请求的信息
4.您收到的数据可能不符合您的预期
遗憾的是,不可能为每一个可以想到的问题都制定应急计划和解决方案。相反,您必须计划错误并知道如何处理它们,以提供尽可能好的用户体验。
一些委托方法会警告您错误
如果您正在实现一个委托对象,以便与执行特定任务(如从远程web服务下载信息)的框架类一起使用,您通常会发现您需要实现至少一个与错误相关的方法。例如,NSURLConnectionDelegate协议包含一个连接:didFailWithError: method:
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
如果发生错误,将调用此委托方法来为您提供一个NSError对象来描述问题。
NSError对象包含数字错误代码、域和描述,以及打包在用户信息字典中的其他相关信息。
与要求每个可能的错误都有唯一的数字代码不同,Cocoa和Cocoa Touch错误被划分为不同的域。例如,如果NSURLConnection中发生错误,则上面的connection:didFailWithError:方法将提供来自NSURLErrorDomain的错误。
错误对象还包括本地化描述,例如“找不到具有指定主机名的服务器”。
有些方法通过引用传递错误
一些Cocoa和Cocoa Touch API通过引用返回错误。例如,您可能决定使用NSData方法writeToURL:options:error:将从web服务接收到的数据写入磁盘来存储数据。该方法的最后一个参数是对NSError指针的引用:
- (BOOL)writeToURL:(NSURL *)aURL
options:(NSDataWritingOptions)mask
error:(NSError **)errorPtr;
在你调用这个方法之前,你需要创建一个合适的指针,这样你就可以传递它的地址:
NSError *anyError;
BOOL success = [receivedData writeToURL:someLocalFileURL
options:0
error:&anyError];
if (!success) {
NSLog(@"Write failed with error: %@", anyError);
// 向用户显示错误
}
如果发生错误,writeToURL:…方法将返回NO,并更新anyError指针以指向描述问题的错误对象。
在处理通过引用传递的错误时,重要的是测试方法的返回值,以查看是否发生了错误,如上所示。不要只是测试错误指针是否设置为指向错误。
提示:如果您对error对象不感兴趣,只需为error: parameter传递NULL即可。
如果可能,恢复或向用户显示错误
最好的用户体验是从错误中透明地恢复。例如,如果您正在发出一个远程web请求,您可以尝试使用不同的服务器再次发出请求。或者,在再次尝试之前,您可能需要向用户请求其他信息,比如有效的用户名或密码凭证。
如果无法从错误中恢复,应该警告用户。如果你用Cocoa Touch开发iOS,你需要创建和配置一个UIAlertView来显示错误。如果您正在使用Cocoa为OS X开发,您可以在任何NSResponder对象上调用presentError:(比如视图、窗口甚至应用程序对象本身),错误将传播到responder链上以进行进一步的配置或恢复。当它到达应用程序对象时,应用程序通过警报面板向用户显示错误。
生成您自己的错误
为了创建自己的NSError对象,您需要定义自己的错误域,错误域的形式应该是:
com.companyName.appOrFrameworkName.ErrorDomain
您还需要为您的域中可能发生的每个错误选择唯一的错误代码,以及适当的描述,这些描述存储在错误的用户信息字典中,如下所示:
NSString *domain = @"com.MyCompany.MyApplication.ErrorDomain";
NSString *desc = NSLocalizedString(@"Unable to…", @"");
NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : desc };
NSError *error = [NSError errorWithDomain:domain
code:-101
userInfo:userInfo];
这个例子使用NSLocalizedString函数从Localizable中查找错误描述的本地化版本。字符串文件,如本地化字符串资源中所述。
如果您需要像前面描述的那样通过引用返回错误,那么您的方法签名应该包括一个指向NSError对象的指针的指针的参数。您还应该使用返回值来指示成功或失败,如下所示:
- (BOOL)doSomethingThatMayGenerateAnError:(NSError **)errorPtr;
如果发生错误,您应该首先检查是否为错误参数提供了非空指针,然后尝试解除引用以设置错误,然后返回NO以指示失败,如下所示:
- (BOOL)doSomethingThatMayGenerateAnError:(NSError **)errorPtr {
...
// error occurred
if (errorPtr) {
*errorPtr = [NSError errorWithDomain:...
code:...
userInfo:...];
}
return NO;
}
协议定义消息传递契约
Objective-C应用程序中的大部分工作都是对象相互发送消息的结果。通常,这些消息是由类接口中显式声明的方法定义的。然而,有时能够定义一组不直接绑定到特定类的相关方法是很有用的。
Objective-C使用协议来定义一组相关的方法,比如对象可能在其委托上调用的方法,这些方法要么是可选的,要么是必需的。任何类都可以表明它采用了协议,这意味着它还必须为协议中所有必需的方法提供实现。
值和集合通常表示为Objective-C对象
在Objective-C中,使用Cocoa或Cocoa Touch类来表示值是很常见的。NSString类用于字符串,NSNumber类用于不同类型的数字,如整数或浮点数,NSValue类用于其他值,如C结构。您还可以使用C语言定义的任何基本类型,例如int、float或char。
集合通常表示为集合类之一的实例,例如NSArray、NSSet或NSDictionary,它们各自用于收集其他Objective-C对象
块简化了常见任务
block是C语言引入的一种语言特性,Objective-C和c++表示一个工作单元;它们封装了代码块和捕获的状态,这使得它们类似于其他编程语言中的闭包。块通常用于简化常见任务,如集合枚举、排序和测试。它们还可以使用大中央调度(Grand Central Dispatch, GCD)等技术轻松地为并发或异步执行调度任务。
错误对象用于运行时问题
虽然Objective-C包含异常处理语法,但Cocoa和Cocoa Touch仅在编程错误(比如越界数组访问)时使用异常,这些错误应该在应用程序发布之前修复。
所有其他错误(包括运行时问题,如耗尽磁盘空间或无法访问web服务)都由NSError类的实例表示。你的应用程序应该为错误做计划,并决定如何最好地处理它们,以便在出现问题时提供尽可能好的用户体验。
Objective-C代码遵循既定的约定
在编写Objective-C代码时,您应该记住一些已经建立的编码约定。例如,方法名以小写字母开头,对于多个单词使用驼峰大小写;例如,做某事或做某事。但重要的不只是资本;您还应该确保代码尽可能易读,这意味着方法名应该具有表达性,但不要太冗长。
此外,如果希望利用语言或框架特性,还需要一些约定。例如,属性访问器方法必须遵循严格的命名约定,以便使用键值编码(KVC)或键值观察(KVO)等技术。