Objective-C 入门

 

《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对象上的格式化字符串。






  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值