《Cocoa programming for mac os X 4th - charpter 3 Objectiv-C》
曾几何时,一位名叫BradCox的人认为是时候为世界提出一种更为模块化的编程风格了。在当时,C已经是一种很流行的语言,同时Smalltalk在当时也是一种优雅的无类型的面向对象语言。Brad Cox便以C语言为基础,在其中增加了类似Smalltalk语言的类和消息发送机制,并称之为Objective-C。因此,Objective-C对C语言进行了简单的扩展。实际上,它仅仅起源于C的一个预处理器和库。
Objective-C并不是一种专有的语言,相反,它是一种开放标准,被收入了自由软件基金会GNU C编译器(gcc)很多年了。最近,苹果公司深入涉足了clang/LLVM(底层虚拟机)开源编译器项目,这是一种比gcc更快和更通用的编译器。在Xcode的项目开发中,LLVM是默认的编译器。
Cocoa采用Objective-C开发应用程序,大多数Cocoa的编程工作都使用Objective-C完成。详细介绍C语言以及基本的面向对象的概念可能要花费整整一本书的内容。这一章将假设读者已经有了一些C和一些有关对象的基础知识,以及一些Objective-C的基础。如果满足这个要求,你会发现学习Objective-C很容易。否则,请你参考《Objective-C Programming: The Big Nerd RanchGuide》或者《The Objective-C Language》这两本书。
创建和使用实例
在第1章,我们介绍了一些用于创建对象的类,这些对象拥有一些方法,你可以通过向这些对象发送消息来触发这些方法。在本节,你将会学习如何创建一个对象并发送消息给它。
作为一个例子,我们将用到NSMutableArray类。通过向NSMutableArray类发送消息alloc(就是一个方法),就可以创建一个NSMutableArray的新实例,像这样:
[NSMutableArray alloc];
这个方法返回一个指针,指向分配给该对象的空间。你可以在变量中获取该指针,就像这样:
NSMutableArray *foo;
foo = [NSMutableArray alloc];
在使用Objective-C时,一定要记住,foo仅仅是一个指针。比如上述代码中,它指向一个对象。
在使用foo指针所指向的对象之前,你需要确定它已经被完全初始化。Init方法可以用来完成这个任务,就像这样:
NSMutableArray *foo;
foo = [NSMutableArray alloc];
[foo init];
仔细看上面最后以行,它发送init消息给foo指针所指的对象。我们可以这样说:“foo是消息init的接收者。”注意,一条发送出去的消息由一个接收者(例如foo所指的对象)和一条消息(例如init)组成,用方括号括起来。你也可以发送消息给类,例如上面示例中将消息alloc发送给类NSMutableArray。
方法init返回一个新的初始化了的对象。你也可以将消息发送语句写成这样:
NSMutableArray *foo;
foo = [[NSMutableArray alloc] init];
在不需要使用对象时,怎么样销毁它?我们将在下一章进行介绍。
有些方法带有参数。如果一个方法带有一个参数,方法名(称为选择器(selector))将以分号结束。例如,在数组末尾添加一个对象,可以使用addObjecti:方法(假设bar是指向另外一个对象的指针):
[foo addObject:bar];
如果有多个参数,选择器将有多个部分构成。例如,要在某个索引出添加一个对象,你应该这样:
[foo insertObject:bar atIndex:5];
注意insertObject:atIndex:是一个选择器,不是两个,它将触发一个带有两个参数的方法。这样的表示方法对大多数C和Java程序员来说有些奇怪,但对熟悉Smalltalk的程序员来说并不新鲜。这种语法使得代码读起来更容易,例如,C++中常见方法调用一般都是这样:
if (x.intersectsArc(35.0, 19.0,23.0,90.0,120.0))
如果用Objective-C来表示,则应该更容易读懂其含义:
if ([x intersectsArcWithRadius:35.0
centeredAtX:19.0
Y:23.0
fromAngle:90.0
toAngle:120.0])
如果感觉奇怪,那么就多用它一回。大多数程序员都会逐渐喜欢Objective-C消息语法。
你现在应该已经能够读一些简单的Objective-C代码了,因此下面将编写一段程序来创建一个NSMutableArray的实例,并用十个NSNumber来填充它。
使用已有的类
运行Xcode。关闭所有正在工作的项目。在File菜单下,选择New->New Project….,当面板弹出后,选择创建一个Command Line Tool(图3.1)。
图3.1 选择项目类型
Command-line tool没有图形化用户接口,通常运行在命令行或作为守护进程运行在后台。与应用程序工程不同,你总是要修改command-line tool的main函数。
将工程命名为lottery(图3.2)。与应用程序名字不同的是,大多数tool名字都是小写的。选择Type为Foundation。
图3.2 命名工程
当新的工程出现后,选择lottery组中的main.m。编辑main.m文件:
#import <Foundation/Foundation.h>
int main (int argc, const char * argv[])
{
@autorelasepool{
NSMutableArray *array;
Array = [[NSMutableArray alloc] init];
int i;
for (i= 0; i<10; i++)
{
NSNumber *newNumber = [[NSNumber alloc] initWithInt:(i * 3)]];
[array addObject:newNumber];
}
for(i= 0; i<10; i++)
{
NSNumber *numberToPrint = [array objectAtIndex:i];
NSLog(@”The number at index %d is %@”, i, numberToPrint);
}
}
return 0;
}
下面我们来一步步分析上述代码:
#import <Foundation/Foundation.h>
这一语句包含了Foundation框架中的所有类的头文件。这些头文件都被预编译了,因此这种方式并不是所听说那般计算复杂性。
int main (int argc, const char * argv[])
main函数的声明和任何Unix C程序一样。
@autoreleasepool{
这段代码为其括号所括语句定义了一个池。我们将在下一章介绍autorelease池的重要性。
NSMutableArray *array;
这里声明了一个变量:array是一个指向NSMutableArray实例的指针。注意现在还没有array存在。你可以简单地声明一个指针,用于一旦创建了array后对其进行引用。
array = [[NSMutableArray alloc] init];
这里,你创建了一个NSMutableArray的实例,并使array变量指向它。
for (i= 0; i<10; i++)
{
NSNumber *newNumber = [[NSNumber alloc] initWithInt:(i * 3)]];
[array addObject:newNumber];
}
在这个for循环中,你创建了一个名为newNumber的局部变量,并且将它指向了一个NSNumber的新实例。这时,你就可以添加对象到array。
array并不复制NSNumber对象,相反,它仅简单地保留一个指向NSNumber对象的指针列表。Objective-C程序员很少复制对象,因为很少有这样的必要。
for(i= 0; i<10; i++)
{
NSNumber *numberToPrint = [array objectAtIndex:i];
NSLog(@”The number at index %d is %@”, i, numberToPrint);
}
这里,你在console中打印出array的内容。NSLog是一个和C函数printf()很象的函数;它采用格式化字符串,并用一个由分号分隔的变量列表来替换格式化字符串。当显示该字符串时,NSLog用应用程序名和时间戳作为所生成字符串的前缀。
例如,在printf中,你可能会用%x来表示一个十六进制整数。在NSLog中,我们使用了所有printf的表示方法,并让%@来显示一个对象。对象会被发送消息description,并且返回一个字符串,其中%@被字符串所代替。我们稍后将详细讨论description方法。
NSLog()所能识别的标志如表3.1所示。
注意:语句@”The number at index %d is %@”中引号前面的@符号看上去有点器官,但请记住,Objective-C是C语言加上一些扩展构成的。其中一个扩展是泪NSString实例的字符串。在C中,字符串仅仅是指向字符缓冲区的指针,并以null字符结尾。C字符串和NSString实例可以被用在相同文件中。为了区分常数C字符串和常数NSSring,你必须在常数NSString的引号前面放置@。
//C string
char *foo;
//NSString
NSString *bar;
foo = “this is a C string”;
bar = @”this is an NSString”;
在进行Cocoa编程时,大多数时候都会使用NSString。不论在何处需要string,框架中的类都期望NSString。但是,如果你已经打包了使用C字符串的C函数,你将发现你要经常使用char *。
你可以在C字符串和NSString之间进行转换:
const char *foo = “Blah blah”;
NSString *bar;
//Create an NSSting from a C string
Bar = [NSString stringWithUTF8String:foo];
//Create a C string from an NSString
foo = [bar UTF8String];
因为NSString可以存放Unicode字符串,你需要在C字符串中正确处理多种类型的字符,这可能相当困难并且耗费时间。(除了多类型问题外,你还不得不处理一些从右往左读取的语言。)只要可能,你就应该用NSString代替C字符串。
我们的main()函数以返回0结束,以指明没有发生错误:
return 0;
}
运行该程序(图3.3)。(如果console没有出现,使用View->Show Debug Area菜单并确认console被启用。)
图3.3 完成运行
发送消息给nil
在大多数面向对象的语言中,如果你发送消息给null,程序将会崩溃。在这些程序中,在发送消息之前,你会发现有很对检查是否为null的语句。例如,在Java中,你会经常看到以下语句:
if (foo !=null){
foo.doThatThingYouDo();
}
在Objective-C中,给nil发送消息没有问题。简单地声明消息,而无须进行上述检查。例如,下面的代码将会正确生成和运行:
id foo;
foo = nil;
int bar = [foo count];
这一方法与大多数语言不同,但你会习惯使用的。
你可能会一遍一遍的问自己“为什么这个方法无法调用?”改变在于你所使用的指针,确定它不为nil,实际上是nil。
在前面的例子中,bar设置为什么?零。如果bar是一个指针,它将被设置为nil(指针的零)。对于其他类型,其值是不可预测的。
NSObject,NSArray,NSMutableArray和NSString
现在,你已经使用过过了这些标准的Cocoa对象:NSObject,NSMutableArray和NSString。(所有来自Cocoa的类都以NS为前缀,你自己创建的类不要用NS作前缀。)这些类都是Foundation框架的一部分。图3.4显示了这些类的继承关系。
图3.4 继承图
让我们来看一下这些类常用的一些方法。要想获得全部列表,可以访问Xcode帮助菜单中的在线文档。
NSObject
NSObject是所有Objective-C类的根。NSObject的一些常用的方法如下:
-(id)init
在分配完内存后,初始化接受者。Init消息通常伴随着alloc消息,并放在同一行代码中:
TheClass *newObject = [[TheClass alloc] init];
-(NSString *)description
返回一个描述接收者的NSString。调试器打印对象命令(“po”)调用这一方法。一个好的decription方法通常会让调试更为容易。同样,如果你在格式化字符串中使用%@,将要替代的对象会被发送消息description。description方法返回的值被放进log字符串中。例如,main函数中
NSLog(@”The number at index %d is %@”, i, numberToPrint);
等价于
NSLog(@”The number at index %d is %@”, i, [numberToPrint description]);
-(BOOL)isEqual:(id)anObject
如果接收者和anObject相等则返回YES,否则为NO。你可以这样使用它:
if ([myObject isEqual:anotherObject]){
NSLog(@”They are equal.”);
}
但“相等”的真正含义是什么?在NSObject中,这个方法被定义为当且仅当接收者和anObject是同一个对象,也就是如果两者指针都指向同一个内存位置时,返回YES。
很明显,这并不是你希望的“相等”,因此这个方法被很多类重写以实现更为实际的“相等”。例如,NSString重写了该方法,用于比较接收者和anObject中的字符。如果两个字符串有相同的字符,并且顺序也相同,那么它们就被视为相等。
因此,如果x和y都是NSString,那么下述这两个表达式之间将有一个巨大的不同之处:
x == y
和
[x isEqual:y]
前一个表达式比较两个指针。后一个表达式比较字符串中的字符。但是,要注意的是,如果x和y都是一个类的实例,并且没有重写NSObject的isEqual:方法,那么这两个表达式是等价的。
NSArray
NSArray是一组指向其他对象的指针列表,它由整数索引。因此,如果一个数组中有n个对象,对象索引值从整数0到n-1。你不能将nil放到NSArray中。(这意味着在NSArray中没有“洞”,这会使那些习惯使用Java的Object[]的程序员感到迷惑。)NSArray继承自NSObject。
NSArray随其存储的所有对象一起创建。你既不能通过NSArray实例添加,也不能通过它删除对象。我们称NSArray是“不变”的。(稍后将介绍它的可变子类,NSMutableArray)。“不变”是一种很好的特性。因为是“不变”,一组对象可以分享一个NSArray,而不用担心它会改变。NSString和NSNumber都是不变的。要改变一个字符串或数字,你仅需要再创建另外一个。(在NSString,同样也有一个NSMutableString类可以改变其实例。)
一个单独的数组可以存放多个不同类的对象,但是数组不能存放C的基本类型,如int或float。
下面是NSArray的一些常用方法:
-(unsigned)count
返回数组中当前对象的数量。
-(id)objectAtIndex:(unsigned)i
返回索引值为i的对象。如果i超出了数组的末尾,将在运行时返回错误。
-(id)lastObject
返回数组中的所以值最大的对象。如果数组是空的,将返回nil。
-(BOOL)containsObject:(id)anObject
如果anObject在数组中,则返回YES。该方法通过发送isEqual消息给数据中的每个对象并传入anObject参数来确定一个对象是否在数组中。
-(unsigned)indexOfObject:(id)anObjec
在接收者中搜索anObject,返回和anObject相等的数组值所对应的最小索引。如果isEqual:返回YES则认为对象相等。如果在数组中没有和anObject相等的对象,indexOfObject:返回NSNotFound。
NSMutableArray
NSMutableArray继承自NSArray,但扩展了添加和删除对象的功能。要从一个不变数组创建可变数组,使用NSArray的mutalbeCopy方法。
下面是NSMutableArray常用的一些方法:
-(void)addObject:(id)anObject
将anObject插入到接收者的尾端。你不能将nil添加到数组中。
-(void)addObjectsFromArray:(NSArray *)otherArray
将包含在otherArray中的对象添加到对象的接收者数组中。
-(void)insertObject:(id)anObject atIndex:(unsigned)index
将anObject插入到接收者的index位置,不能大于数组中的元素数量。如果index已经被占用了,则index处及以上的对象将会移动一格,以腾出空间。如果anObject是nil或者index大于数组元素总数,则会报错。
-(void)removAllObjects
清空接收者的所有元素。
-(void)removeObject:(id)anObject
删除数组中所有的anObject。匹配由anObject对isEqual:消息的响应来确定。
-(void)removeObjectAtIndex:(unsigned)index
删除位于index的对象,并将所有在index之上的元素下移一格填满空隙。如果index超过数组元素总数就会报错。
正如前面所述,不能将nil加入数组。有时候,你希望在数组中添加一个对象来表示空,则可以用NSNull类。这里确实有一个NSNull实例,因此如果你希望在数组中放置一个表示空的占位符,就这样使用NSNull:
[myArray addObject:[NSNull null]];
NSSting
NSString是Unicode字符缓冲区。在Cocoa中,所有关于字符串的操作都由NSString处理。为方便起见,Objective-C语言同样支持@”...”结构来从一个7bit的ASCII编码创建一个字符串对象常量:
NSString *temp = @”this is a constant string”;
NSString继承自NSObject,下面是一些常用的方法:
-(id)initWithFormat:(NSString *)format, …
类似于sprintf。在这里,format是一个包含占位符的字串,例如%d。其他的参数将取代这些符号占位符:
Int x = 5;
Char *y = “abc”;
Id z = @”123”;
NSString *aString = [[NSString alloc] initWithFormat:
@”The int %d, the C String %s, and the NSString %@”, x, y, z];
-(NSUInteger)length
返回接收者字符数。
-(NSString *)stringByAppendingString:(NSSting *)aString
返回一个在接收者后面追加了aString的字串对象。下述代码片段将产生字串:“Error:unable to read file.”
NSString *errorTag = @”Error:”;
NSString *errorString = @”unable to read file.”;
NSString *errorMessage;
erroMessage = [errorTag stringByAppendingString:errorString];
-(NSComparisonResult)compare:(NSString *)otherString
比较接收者和otherString,如果接收者字典序先于otherString则返回NSOrderedAscending,如果otherString先于接收者,则返回NSOrderedDescending,如果相等则返回NSOrderedSame。
-(NSComarisonResult)caseInsersitiveCompare:(NSString *)otherString
除了忽略大小写外,类似于compare。
“继承自”与“使用”或“了解”的比较
Cocoa程序员新手都急于创建NSString和NSMutalbeArray的子类,有经验的Objective-C程序员大多数都不会这么做。他们会将NSString和NSMutableArray作为更大对象的一部分,其中一个技巧就是组合。例如,一个BankAccount类可以是NSMutableArray的一个子类。但是,银行帐号仅仅是一些合约的集合吗?新手可能会走这条路。相反,老手会创建一个继承自NSObject的BankAccount类,并添加一个实例变量transactions指向一个NSMutableArray。区分“使用”(uses)和“是…的子类”(is a subclass of)很重要。新手会说“BankAccount继承自NSMutableArray”,老手则会说“BankAccount使用了NSMutableArray”。在Objective-C术语中,“使用”比“是…的子类”更为常用。
你将会发现使用类比创建子类更容易。创建子类包含更多的代码,并且需要对子类有更深入的理解。使用组合代替继承,Cocoa开发者可以利用强大的类,而无需真正理解它们是如何工作的。
在强类型语言中,例如C++,继承是很关键的。在无类型语言,如Objective-C中,继承仅仅节约了开发者打字的时间。本书仅有两个继承图,其他所有图都是用来说明哪一个对象是如何知道另外一个对象的。这对于Cocoa程序员来说是一个重要信息。
创建自己的类
假设抽奖数字有两个 ,都在1到100(含)之间。编写代码构造将来10周的抽奖结果。每个LottoryEntry对象都将有一个日期和两个随机整数(图3.5)。除了学习如何创建类,你还要开发一个能让你变成富翁的工具。:)
图3.5 完成的程序
创建LotteryEntry类
在Xcode中,创建一个新的文件。选择Objective-C Class类型。命名该类为LotteryEntry,并设为NSObject的子类(图3.6)。
图3.6 新的LotteryEntry类
注意LotteryEntry.h文件同时会被创建。将两个文件拖到lottery组中,如果它们不在那里的话。
LotteryEntry.h
编辑LotteryEntry.h文件如下:
#import <Foundation/Foundation.h>
@interface LotteryEntry:NSObject{
NSDate *entryDate;
Int firstNumber;
Int secondNumber;
}
-(void)prepareRadomNumber;
-(void)setEntryDate:(NSDate *)date;
-(NSDate *)entryDate;
-(int)firstNumber;
-(int)secondNumber;
@end
你已经为继承自NSObject的新类LotteryEntry声明了一个头文件,它有三个实例变量:
- EntryDate是一个NSDate。
- FirstNumber和secondNumber都是int。
同时有5个方法:
- prepareRandomNumbers将设置firstNumber和secondNumber为在1到100间的随机数。它没有参数并没有返回值。
- entryDate和setEntryDate:允许其他对象读取和设置entryDate变量。方法entryDate将返回存储在entryDate变量中的值。方法setEntryDate:将允许设置entryDate变量值。允许变量被读取和设置的方法被称为访问器方法(accessor methods)。
- 同样声明了读取firstNumber和secondNumber的访问器方法。(没有声明设置这些变量的访问器,这个工作已经直接在prepareRadomNumbers中完成了。)
LotteryEntry.m
编辑LotteryEntry.m文件如下:
@implementation LotteryEntry
-(void)prepareRandomNumbers
{
firstNumber = ((int)random() % 100) + 1;
secondNumber = ((int)random() % 100) + 1;
}
-(void)setEntryDate:(NSDate *)date
{
entryDate = date;
}
-(NSDate *)entryDate
{
Return entryDate;
}
-(int)firstNumber
{
Return firstNumber;
}
-(int)secondNumber
{
Return secondNumber;
}
@end
下面逐句解释这些方法:
PrepareRandomNumbers使用了标准的random函数生成一个伪随机数。你可以使用模操作符(%)并加1来得到1-100范围内的数字。
setEntryDate: 设置entryDate指针为一个新值。
EntryDate,firstNumber和secondNumber返回变量值。
修改main.m
现在查看main.m。很多行都一样,但有一些要修改。最重要的改动是我们使用了LotteryEntry对象代替了NSNumber对象。
下面加粗的代码。(无需敲入注释)
#impor <Foundation/Foundation.h>
#import “LotteryEntry.h”
int main(int argc, const char *argv[]){
@autoreleasepool {
//Create the date object
NSDate *now = [[NSDate alloc] init];
NSCalendar *cal = [NSCalendar currentCalender];
NSDateComponents *weekComponents = [[NSDateComponents alloc] init];
//seed the random number generator
Srandom((unsigned)time(NULL));
NSMutableArray *array;
Array = [[NSMutableArray alloc] init];
int i;
for (i=0; i<10; i++) {
[weekComponents setWeek:i];
//Create a date/time object that is ‘i’ weeks from now
NSDate *iWeeksFromNow;
iWeeksFromNow = [cal dateByAddingComponents:weekComponents toDate:now options:0];
//Create a new instance of LotteryEntry
LotteryEntry *newEntry = [[LotteryEntry alloc] init];
[newEntry prepareRandomNumbers];
[newEntry setEntryDate:iWeeksFromNow];
//Add the LotteryEntry object to the array
[array addObject:newEntry];
}
for(LotteryEntry *entryToPrint in array) {
//Display its contents
NSLog(@”%@”, entryToPrint);
}
}
Return 0;
}
注意第二个循环。在这里你使用了Objective-C枚举集合成员的机制。
该程序将创建一个LotteryEntry对象数组,如图3.7所示。
图3.7 对象图
实现description方法
生成并运行你的应用程序。你会看到类似图3.8所示结果。
这不是我们希望的。毕竟,我们假设程序能够显示日期,以及相应的数字,但在这里并没有看见这些。(你所见的是NSObject中默认定义的description方法。)下面,我们将会让LotteryEntry对象以更为有意义的方式显示。
图3.8 执行后
在LotteryEntry.m中添加一个description方法:
-(NSString *)description { NSDateFormatter *df = [[NSDateFormatter alloc] init]; [df setTimeStyle: NSDateFormatterNoStyle]; [df setDateStyle:NSDateFormatterMediumStyle]; NSString *result; result = [[NSString alloc] initWithFormat:@”%@ = %d and %d”,
[df stringFromDate:entryDate], firstNumber, secondNumber]; return result; }
生成并运行该程序,现在将看到日期和数字:
图3.9 带有description的运行结果
NSDate
在继续新的内容之前,让我们来深入看一下NSDate。NSDate实例表示了一个时间点,并且基本上是定长的:一旦创建,你就不能修改day或time。因为NSDate是定长的,许多对象经常共享一个单独的date对象。很少需要创建NSDate对象的拷贝。
下面是NSDate常用的方法:
+(id)date
创建并返回一个日期,用当前的日期和时间初始化。
这是一个类方法。在接口文件、实现文件和文档中,类方法都通过前面的+号来识别。一个类方法通过向类而不是实例发送消息来触发。例如,可以这么使用:
NSDate *now; now = [NSDate date];
-(id)dateByAddingTimeInterval:(NSTimeInterval)interval
创建并返回一个日期,用接收者的当前日期加上一个给定时间间隔来初始化。
-(NSTimeInterval)timeIntervalSinceDate:(NSDate *)anotherDate
返回接收者和anotherDate之间的间隔,以秒为单位。如果接收者早于anotherDate,返回值是负的。NSTimeInterval和double相同。
+(NSTimeInterval)timeIntervalSinceReferenceDate
返回2001年1月1日与接收者时间之间的间隔,以秒为单位。
-(NSComparisonResult)compare:(NSDate *)otherDate
如果接收者早于otherDate,则返回NSOrderedAscending,如果otherDate更早,则返回NSOrderedDescending,如果相等则返回NSOrderedSame。
编写初始化器
请注意main函数中的下述行:
newEntry = [[LotteryEntry alloc] init]; [newEntry prepareRandomNumbers];
创建了一个新实例而后立刻调用prepareRandomNumbers方法来初始化firstNumber和secondNumber。这个工作应该由初始化器来完成,因此你需要在LotteryEntry类中重写init方法。在LotteryEntry.m文件中,修改方法prepareRandomNumbers,将其放入一个init方法中:
-(id)init { self = [super init]; If (self) { firstNumber = ((init)random() % 100) +1; secondNumber = ((nit)random() % 100) +1; } return self; }
init方法一开始就调用超类的初始化器,初始化自己的变量,然后返回self,指向对象自身(已经运行该方法的对象)的指针。(如果你是Java或C++程序员,self语this指针相同。)现在删除main.m中的下述行:
[newEntry prepareRandomNumbers];
在LotteryEntry.h中,删除下述声明:-(void)prepareRandomNumbers;
生成并运行程序,确保其能够工作。再来看一下init方法。为何我们如此麻烦地将超类初始化器的值返回给self,而后又检验self的值呢?答案在于一些Cocoa类的初始化器无法初始化时,将会返回nil。为了优雅地处理这些情况,我们必须既要检验[super init]的返回值,又要通过我们自己的初始化器为self返回相关的值。
这种模式在一些Objective-C程序员中产生了争论。有些认为没必要这么做,因为大多数类的初始化器都不会失败,并且大多数类的初始化器不会返回不同的值给self。我们认为最好养成为self赋值并检验的习惯。这种努力与头疼的调试比起来是微不足道的。
带参数的初始化器
查看main.m的相同位置,如下:
LotteryEntry *newEntry = [[LotteryEntry alloc] init]; [newEntry setEntryDate:iWeeksFromNow];
如果能够将日期作为参数提交给初始化器,那么看上去会更完美。修改代码如下:LotteryEntry *newEntry = [[LotteryEntry alloc] initWithEntryDate:iWeeksFromNow];
你会看到一个编译器错误;先忽略它,我们后面会解决这个问题。接下来,在LotteryEntry.h中声明一个方法:
-(id)initWithEntryDate:(NSDate *)theDate;
现在,修改(重命名)LotteryEntry.m中的init方法:-(id)initWithEntryDate:(NSDate *)theDate { self = [super init]; if (self) { entryDate = theDate; firstNumber = ((init)random() % 100) + 1; secondNumber = ((init)random() % 100) + 1; } Return self; }
生成并运行该程序,它将正确执行。
但是,类LotteryEntry有一个问题。如果你将该类email给你的朋友Rex,Rex计划在他的程序中使用类LotteryEntry,但他可能并不知道你已经写了initWithEntryDate:。如果他犯了错,可能会写下如下语句:
NSDate *today = [NSDate date]; LotteryEntry *bigWin = [[LotteryEntry alloc] init]; [bigWin setEntryDate:today];
这段代码不会产生错误信息。相反,它会沿着继承树找到NSObject的init方法。这个问题将导致firstNumber和secondNumber无法正确初始化,都会被设置为0。
要防止Rex出现这种错误,你应该重写init使用默认日期调用你的初始化器:
-(id)init { return [self initWithEntryDate:[NSDate date]]; }
将该方法添加到LotteryEntry.m文件中。
注意initWithEntryDate:仍然做了所有工作。因为一个类可以有多个初始化器,我们将实际完成初始化工作的那个称为指定初始化器(designated initializer)。如果类有多个初始化器,其指定初始化器将携带最多的参数。你应该清楚地在文档中指明哪个是指定初始化器。例如,NSObject的指定初始化器是init。
创建初始化器的约定(Cocoa程序员对待初始化器的规则):
- 如果超类的初始化器已经足够多,就不要在自己的类中创建任何初始化器。
- 如果你决定创建一个初始化器,你必须重写超类的指定初始化器。
- 如果你创建了多个初始化器,仅有一个工作,也就是指定初始化器。所有其他初始化器都要调用指定初始化器。
- 你的类中的指定初始化器将调用它自己超类的指定初始化器。
总有一天,当你需要创建一个类时,它必须带有参数。重写超类的指定初始化器,抛出异常:
-(id)init
{
@throw [NSException exceptionWithName:@”BNRBadInitCall” reason:@”Initialize Lawsuit with initWithDefendant:” userInfo:nil];
return nil;
}
调试器
自由软件联盟开发了编译器(gcc)和调试器(gdb),它们都是苹果开发人员的工具。苹果近年来对它们做了重大改进。这一节我们来讨论一下设置断点、调用调试器和浏览变量值得过程。
在浏览代码时,你应该注意一个灰色空白在代码的左边。如果你点击该空白,断点将被添加到相应行。在main.m中下述行添加断点(图3.10):
图3.10 创建断点
当你运行程序时,如果有断点,Xcode将在调试器中运行程序。调试器启动要花费一些时间,然后它将运行程序直到遇到断点。
程序在运行时,调试器工具栏将出现在编辑器区域下方。调试器工具栏包含一个按钮,用于切换全部调试器区域可视与否,包括变量视图和console,正如控制程序执行和当前线程及函数信息的按钮。
Xcode的默认行为是当遇到断点时显示全部调试器区域。如果你不希望调试器出现在窗体的下部,使用调试器工具栏(toolbar)里面的调试器区域视图切换,或者View->show->Debugger Area菜单项目。
你应该也能发现左边的Debug导航器,显示了程序中的线程和每个线程栈上的帧。因为断点在main()中,栈并不会很深入。在调试区域左边上的变量视图里,你能够看见变量和它们的值。(图3.11)
图3.11 在断点处停止
注意到现在变量i的值为0。
重新回到调试器工具栏上,变量视图上方的四个按钮用于停止(或继续)和stepping over,以及step out函数。点击Continue按钮来执行循环的另一个迭代。点击Step-Over按钮继续单步执行代码。
gdb调试器是一个Unix事物,被设计运行于terminal。当执行中止时,gdb终端将在Console面板上出现。
在调试终端里,你对所有gdb的能力都有访问权限。一个很好的功能是“print-object” (po)。如果一个变量是一个指向一个对象的指针,当你po它时,该对象被发送一个description消息,其结果打印在console中。尝试打印newEntry变量。
po newEntry
你应该能看见你的description方法的结果(图3.12)。
图3.12 使用gdb终端
当出现问题时会产生异常。要想在抛出异常时让调试器停止,你需要添加异常断点。点击断点导航器底部的Add按钮,并选择Add Exception Breakpoint….设置异常类型为Objective-C,并点击Done(图3.13)。通过点击断点导航器上的蓝色断点图标来禁用main()中的已有断点。当被禁用时,断点将变暗。
图3.13 添加异常断点
你可以通过要求数组中不存在的索引来测试异常断点。当数组创建后立刻查询器第一个对象是:
array = [[NSMutableArray alloc] init];
NSLog(@”first item = %@”, [array objectAtIndex:0]);
重新生成并启动程序,它将在异常发生时停止。
有关调试Cocoa程序的一个挑战是它们经常会在中断状态徘徊很久。使用宏NSAssert(),你可以让程序一旦出现问题时就抛出异常。例如,在setEntryDate:中,你可能希望如果参数为nil就抛出异常。添加一个调用到NSAssert():
-(id)initWithEntryDate:(NSDate *)theDate
{
Self = [super init];
If (self) {
NSAssert(theDate != nil, @”Argument must be non-nil”);
entryDate = theDate;
firstNumber = ((int)random() % 100) +1;
secondNumber = ((int)random() % 100) +1;
}
return self;
}
生成并运行它,程序因为没有问题,不会抛出异常。因此修改断言使之不正确:
NSAssert(theDate == nil, @”Argument must be non-nil”);
现在生成并运行你的程序。注意一条包含类和方法名字的消息被记录下来,并且抛出了一个异常。灵活地运用NSAssert()可以帮助你更快地发现bug。
你可能不需要在你完成的产品中进行断言调用检查。大多数工程中,有两种生成配置:Debug和Release。在Debug版本下,你会希望所有断言检查。在Release配置下下,你不希望如此。你可以在Release配置(图3.14)下阻止断言检查。
图3.14 禁用断言检查
要达成这个目的,在工程导航器(最上面的条目)通过选择lottery工程进行build设置。然后选择lottery目标,切换到Build Settings选项卡,找到Preprocessor Macros项。一种快速方式是使用Build Settings面板上部的搜索区域。Preprocessor Macros项将有一个项在其下方,用于每个build配置:Debug和Release。设置Release项的值为NS_BLOCK_ASSERTIONS。
现在,如果你生成并运行Release配置,你会发现你的断言不再被检查了。(在继续之前,修复你的断言:它将确保dates不为nil。)
你可以通过打开方案编辑器(在Product菜单中,点击Edit Scheme…)来修改你的当前生成配置为Release。我们将在37章更详细地讨论生成配置。
NSAssert()仅在Objective-C方法中工作。如果你需要在C函数中检查断言,使用NSCAssert()。
这些知识已经足够你使用调试器了。更为深入的信息,请参考自由软件联盟(www.gnu.org)的文档。
你已经做了些什么?
你已经写了一个简单的Objective-C程序,包括一个main()函数创建了数个对象。其中一些对象是LotteryEntry的实例,LotteryEntry是你创建的一个类。该程序会在console上显示一些信息。
到此为止,你应该已经对Objective-C有了一个相对完整的理解。Objective-C不是一种复杂的语言。本书的剩余部分将结合Cocoa框架。从现在开始,你将创建事件驱动的应用程序,而不再是命令行工具。
遭遇静态分析器
Xcode中最为方便的一个工具是静态分析器。静态分析器使用了苹果的LLVM编译器技术来分析代码查找bug。
开发人员传统上依赖于编译器警告来发现代码中潜在的问题。静态分析其更为深入,查看过去的语法并跟踪代码中值是如何使用的。
因为默认编译器设置和我们的小心打字,如果你现在运行分析器,你会发现我们的应用程序没有任何问题。让我们修改一下工程设置,以便我们能够更好地观察静态分析的工作。
正如我们先前所为,通过在工程导航器中选择工程,打开工程的build settings。然后选择lottery目标。在Build Settings选项卡中,找到设置:Objective-C Automatic Reference Counting。修改其值为No(图3.15)。
图3.15 禁用Automatic Reference Counting
现在来分析lottery程序。在Product菜单中,点击Analyze。在问题导航栏中,你会看到一些有静态分析器发现的问题;选贼一个并下拉打开,查看分析其思维过程(图3.16)。
图3.16 静态分析器在工作
这种情况下,静态分析其发现了大量内存相关的问题,这是因为我们禁用了称为automatic reference counting的功能,这一功能将在下一章讨论。这是静态分析器最有用的特性之一:它知道Objective-C中retain-count内存管理规则,并且能够识别代码中其它的危险模式。
现在保持automatic reference counting为禁用。
更为重要的:Messaging如何工作?
正如前面所述,对象类似于C的结构。NSObject声明一个实例变量称为isa。因为NSObject是所有类的根,所以每个对象都有一个isa指针指向创建该对象的类结构(图3.17)。类结构包括类的实例变量的名字和类型。它还有类方法的实现。类结构有一个指针指向其超类的类结构。
图3.17 每个对象都有一个指向其类的指针
方法由选择器进行索引。选择器的类型为SEL。尽管SEL被定义为char *,但将他看作是int最为有用。每个方法的名字都被映射到一个唯一的整数。例如,方法名addObject:可能映射为数字12。当你查找方法时,你将使用选择器,而不是字符串@”addObject:”。
作为Objective-C数据结构的一部分,使用表江方法名字映射到它们的选择器。图3.18显示了这样的一个例子。
图3.18 选择器表
在编译时,无论何时遇到发送的消息,编译器就会查找选择器。因此,
[myObject addObject:yourObject];
将变为(假设addObject:的选择器为12)
bjc_msgSend(myObject, 12, yourObject);
这里,Objc_msgSend()查看myObject的isa指针来获得它的类结构,并查找有关12的方法。如果它找不到方法,就会查看超类的指针。如果超类也没有12方法,它会继续沿着继承树向上搜索。如果在树顶端都没有找到方法,函数将抛出一个异常。
很明显,这是最为动态的处理消息的方式。这些类结构可以在运行时被改编。特别地,在运行时使用NSBundle类可以使得向程序中添加类和方法变得相对容易。这是一种非常强大的技术,已经被用于创建那些可以被其它开发人员扩展的应用程序。
挑战
使用NSDateFormatter的setDateFormat:来自定义LotteryEntry类中date对象上的格式化字符串。