Objective-C 2.0 with Cocoa Foundation --- 3,类的声明和定义

3,类的声明和定义

本系列讲座有着很强的前后相关性,如果你是第一次阅读本篇文章,为了更好的理解本章内容,笔者建议你最好从本系列讲座的第1章开始阅读,请点击这里

上一章我们写了一个非常简单的Obejctive-C下面的Hello, World!的小程序,并且对里面出现的一些新的概念进行了解释。这一章,我们将要深入到Objective-C的一个基本的要素,也就是类的声明和定义。通过本章的学习,同学们应该可以定义类,给类加上变量,还有通过方法访问类的变量。不过准确的说,变量和方法的名词在Objective-C里面并不是最准确的称呼,我们暂时引用Java的定义,稍后我们将统一我们的用语定义。

3.1,本章的程序的执行结果。

我们将构筑一个类,类的名字叫做Cattle,也就是牛的意思,今年是牛年而且我还想给在股市奋战的同学们一个好的名字,所以我们暂时把这个类叫做牛类。

我们在main里面初始化这个牛类,然后调用这个类的方法设定类的变量,最后调用这个类的一个方法,在屏幕上输出,最终输出的结果如下图3-1所示

 

图3-1,牛类的输出结果

完整的代码在这里。不过为了熟悉编辑环境以及代码,笔者强烈建议同学们按照下面的步骤自己输入。

3.2,实现步骤

第一步,按照我们在第二章所述的方法,新建一个项目,项目的名字叫做03-Hello Class。当然,你也可以起一个别的更好听的名字,比如说Hello Cattle等等,这个并不妨碍我们的讲解。如果你是第一次看本系列文章,请到这里参看第二章的内容。

第二步,把鼠标移动到左侧的窗口的“Source”目录,然后单击鼠标右键,选择“Add”,然后界面上会出来一个子菜单,在子菜单里面选择“New File...” 。如图3-2所示:

 

图3-2,新建文件

第三步,在新建文件对话框的左侧选择“Cocoa Touch Classes”,然后在右侧窗口选择“NSObject subclass”,然后单击“Next”。如图3-3所示:

 

 第四步,在“New File”对话框里面的“File Name”栏内输入“Cattle.m”。注意,在确省状态下,Xcode为你加上了“.m”的后缀,这个也是编译器识别Objective-C源文件的方法,没有特殊理由请不要修改这个后缀,否则会让编译器感到不舒服。另外请确认文件名字输入栏的下方有一个“Also create "Cattel.h"”选择框,请保持这个选择框为选择的状态。如图3-4所示。


 第5步,在项目浏览器里面选择“Cattle.h”文件,把文件改为如下代码并且保存(Command键+S):

#import <Foundation/Foundation.h>


@interface Cattle : NSObject {
    
int legsCount;
}
- (void)saySomething;
- (void)setLegsCount:(int) count;
@end
为什么legsCattle者,牛也;legs者,股也。不过牛股里面的牛正确的英文说法应该是Bull,请大家不要着急,我们会在类的继承里面命名一个Bull类的。

 

第六步,在项目浏览器里面选择“Cattle.m”文件,把文件改为如下代码并且保存(Command键+S):

#import "Cattle.h"


@implementation Cattle
-(void) saySomething
{
    NSLog(
@"Hello, I am a cattle, I have %d legs.", legsCount);
}
-(void) setLegsCount:(int) count
{
    legsCount 
= count;
}
@end

 

第七步,在项目浏览器里面选择“03-Hello Class.m” 文件,把文件改为如下代码并且保存(Command键+S):

#import <Foundation/Foundation.h>
#import 
"Cattle.h"

int main (int argc, const char * argv[]) {
    NSAutoreleasePool 
* pool = [[NSAutoreleasePool alloc] init];

    id cattle 
= [Cattle new];    
    [cattle setLegsCount:
4];
    [cattle saySomething];

    [pool drain];
    
return 0;
}

 

第八步,选择屏幕上方菜单里面的“Run”,然后选择“Console”,打开了Console对话框之后,选择对话框上部中央的“Build and Go”,如果不出什么意外的话,那么应该出现入图3-1所示的结果。如果出现了什么意外导致错误的话,那么请仔细检查一下你的代码。如果经过仔细检查发现还是不能执行的话,可以到这里下载笔者为同学们准备的代码。 如果笔者的代码还是不能执行的话,请告知笔者。

3.3,类的声明

从Objective-C名字我们就可以得知,这是一个面向对象的语言。面向对象的一个最基础的要素就是类的概念,Objective-C也不例外。所谓的类的概念,其实是从C语言的结构体发展而来的。我们知道,C语言里面的结构体仅仅有数据的概念,面向对象的语言不仅仅支持数据,还可以在结构体里面封装用于存取结构体数据的方法。结构体的数据和方法结合,我们把整个结构体称为类(Class)。仅仅有了类,是不能执行任何操作的,我们必须把类进行实体化,实体化后的类我们称之为对象(Object)。从这个角度上来说,我们可以认为类是对象的模版。

如果要使用类,那么和构造体相类似,我们必须声明这个类。

请参照“Cattle.h” 文件:

1 #import <Foundation/Foundation.h>
2 
3 
4 @interface Cattle : NSObject {
5     int legsCount;
6 }
7 - (void)saySomething;
8 - (void)setLegsCount:(int) count;
9 @end

如果看过本系列第二章的同学们,第一行应该是一个老面孔了,我们知道我们需要这个东西免费获得苹果公司为我们精心准备的Foundation Framework里面的很多的功能。如果不使用这个东西的话,我们的工作将会很复杂。

同学们请看第4行和第9行的第一个字母,又出现了“@”符号。为什么说又呢,因为我们在第二章的字符串前面也看到过这个东西。字符串前面出现这个符号是因为我们需要和C语言的字符串定义区别开来,我们需要编译器导向。在这里,我要告诉同学们的是,这里的“@”符号的作用还是同样是编译器导向。我们知道Java和C++定义了一个关键字class用于声明一个类,在Objective-C里面,不存在这样的关键字。在Objective-C里面,类的定义从@interface开始到@end结束,也就是说,编译器看到了@interface就知道了这是类的定义的开始,看到了@end就知道,类的定义结束了。

我们这里类的名字是“Cattle”,我们使用了空格和@interface分开,通知编译器,我们要声明一个类,名字叫做Cattle。在Cattle的后面,我们有“: NSObject”,这是在通知编译器我们的Cattle是从NSObject继承而来的,关于继承和NSObject,我们将在后面的章节里面详细介绍,关于“: NSObject”我们现在可以理解为,通过这样写,我们免费获得了苹果公司为我们精心准备的一系列的类和对象的必备的方法。NSObject被称为root class,也就是根类。在Java或者.NET里面,根类是必备的,C++不需要。在Obejctive-C里面原则上,你可以不使用NSObject,构筑一个你自己的根类,但是事实上这样做将会有很大工作量,而且这样做没有什么意义,因为苹果为你提供的NSObject经过了很长时间的检验。也许有好奇心的同学们想自己构筑根类,不过至少笔者不会有自己去构筑一个根类的欲望。

好的,大家现在来看第5行。我们以前把这个东西叫做变量,我们从现在开始,需要精确的使用Objective-C的用语了,这是实体变量(instance variables,在有的英文资料里面会简写为iVars)。虽然作为一个Cattle,它有不止一个实体变量,比如说体重等等,但是为了代码简洁,我们在这里声明一个就是牛腿也就是牛股的数目,这个实体变量是int型,表示一个整数,我们当然不希望有4.5个牛腿。

我们来看第6行,第6行的括弧和在第4行最后的括弧用来表示实体变量的定义区间,编译器认为在这两个括弧之间的定义是实体变量的定义。当然,如果你的类没有实体变量,那么这两个括弧之间允许什么都没有。和Java以及C++不一样,Objective-C要求在括弧里面不能有方法也就是函数的定义,那么Objective-C里面的方法的定义放在什么地方呢,请看第7行。

第7行的第一个字母是一个减号“-”。这个减号就是告诉编译器,减号后面的方法,是实体方法(instance method)。实体方法的意思就是说,这个方法在类没有被实体化之前,是不能运行的。我们在这里看到的是减号,在有减号的同时也有加号,我们把带加号的方法称为类方法(class method),和实体方法相对应,类方法可以脱离实体而运行。关于类方法,我们将在后面的章节里面讲解。大家也许可以想起来在C++和Java里面同样也有类似的区分,不是么。

在Objective-C里面方法的返回类型需要用圆括号包住,当编译器看到减号或者加号后面的括号了之后,就会认为这是在声明方法的返回值。你也可以不声明返回值,Objective-C的编译器会给没有写显式的返回值函数加上一个默认的返回值,它的类型是id,关于id类型我们将在后面讲解,不过笔者不推荐不写返回值的类型。

在第7行我们定义了这个方法的名字是saySomething,当然Cattle说的话我们人类是听不懂的,笔者只是想让它在我们的控制台里面输出一些我们可以看得懂得字符串。方法的声明最后,需要分号来标识,这一点保持了和C没有任何区别。

我们再来看看第8行,第8行和第7行多了“:(int) count”。其中冒号放在方法的后面是用来表示后面是用来定义变量的,同样变量的类型使用括号给包住,如果不写变量的类型的化,编译器同样认为这是一个id类型的。最后的count,就是变量的名字。如果有不只一个变量怎么办?答案就是在第一个变量后面加冒号,然后加园括号包住变量的类型,接着是变量的名字。

好了,我们在这里总结一下,类的定义方法如下:

@interface 类的名字 : 父类的名字 {
    实体变量类型 实体变量名字;
    
}
- (返回值类型)方法名字;
+ (返回值类型)方法名字;
- (返回值类型)方法名字:(变量类型) 变量名字 标签1:(变量类型) 变量1名字;

@end

 

...的意思在本系列入门讲座里面,...表示省略了一些代码的意思。

 

3.4,类的定义

我们在前一节讲述了类的声明,我们下一步将要看一下类的定义。请同学们打开“Cattle.m”文件:

 

 1 #import "Cattle.h"
 2 
 3 
 4 @implementation Cattle
 5 -(void) saySomething
 6 {
 7     NSLog(@"Hello, I am a cattle, I have %d legs.", legsCount);
 8 }
 9 -(void) setLegsCount:(int) count
10 {
11     legsCount = count;
12 }
13 @end
14 

Cattle.m文件的第一行就import了Cattle.h文件,这一点和C的机制是一样的,关于#import的说明请参照第二章。

我们来看第4行和第13行,和头文件里面的@一样,我们这里类的定义也是使用的编译导向。编译器会把从@implementation到@end之间的部分看作是类的定义。@implementation的后面有一个空格,空格的后面是我们的类的名字Cattle,这是在告诉编译器,我们要定义Cattle类了。第4行和第13行之间是我们在头文件里面定义的实体方法或者类方法的定义部分,当然我们的类如果没有任何的实体方法和类方法的话,我们也许要写上@implementation和@end,把中间留为空就可以了。

第5行是我们定义的saySomething的实现,我们可以发现第5行的内容和头文件Cattle.h的第7行是一致的。笔者个人认为在编写实体方法和类方法的定义的时候,为了避免手工输入产生的误差,可以从头文件当中把声明的部分拷贝过来,然后删除掉分号,加上两个花括弧。我们知道地6行到第8行是方法的定义的部分,我们再来看看第7行。第7行和第二章的Hello, World输出有些相似,只不过多了一个%d,还有实体变量legsCount,这个写法和C语言里面的printf是类似的,输出的时候会使用legsCount来替代字符串里面的%d。

第9行的内容和Cattle.h的第8行一致的,这个不需要再解释了。我们来看看第11行,第11行是在说,把参数count的数值赋值给实体变量legsCount。我们可以通过使用setLegsCount方法来控制Cattle对象里面legsCount的数值。

这部分内容的关键点为@implementation和@end,理解了这个东西,其余的就不难理解了。我们来总结一下,类的定义部分的语法:

@implementation 类的名字
-(方法返回值) 方法名字
{
    方法定义
    
}
-(方法返回值) 方法名字:(变量类型) 变量名字
{
    方法定义
    
}

@end

3.5,类的实例化

我们在3.3和3.4节里面分别声明和定义了一个Cattle的类。虽然定义好的类,但是我们是不能直接使用这个类的。因为类的内容需要被调入到内存当中我们称之为内存分配(Allocation),然后需要把实体变量进行初始化(Initialization),当这些步骤都结束了之后,我们的类就被实例化了,我们把实例化完成的类叫做对象(Object)。好的,我们知道了我们在类的实例化过程当中需要做哪些工作,我们接着来看看我们已经搞定的Cattle类的定义和声明是怎样被实例化的。

 1 #import <Foundation/Foundation.h>
 2 #import "Cattle.h"
 3 
 4 int main (int argc, const char * argv[]) {
 5     NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
 6 
 7     id cattle = [Cattle new];    
 8     [cattle setLegsCount:4];
 9     [cattle saySomething];
10 
11     [pool drain];
12     return 0;
13 }

同学们请看第7行的第一个单词id。 id是英文identifier的缩写,我们在很多地方都遇到过id,比如说在博客园里面,我们都使用id来登陆系统的,我们的id就代表着系统的一个用户。由于id在一个系统当中是唯一的,所以系统获得我们的id之后就知道我们是谁了。Objective-C也是一样的道理,使用id来代表一个对象,在Objective-C当中,所有的对象都可以使用id来进行区分。我们知道一个类仅仅是一些数据外加上操作这些数据的代码,所以id实际上是指向数据结构的一个指针而已,相当于void*。

第7行的第二个单词是cattle,就是我们给这个id起的一个名字。当然,你可以起系统保留的名字以外的任何名字,不过为了维持代码的可读性,我们需要一个有意义的名字,我们这里使用头文字为小写的cattle。

第7行的[Cattle new]是创建对象,new实际上是alloc和init的组合,在Objective-C里面创建对象是一个为对象分配内存和初始化的过程。new,alloc还有init定义在Cattle的超类NSObject里面,笔者将要在第7章里面详细的解释一下如何创建对象。在第7章之前我们都是用new来创建对象。

Objective-C里面的方法的使用和其他语言有些不同,Objective-C使用消息(Message)来调用方法。所以笔者认为在讲解第7行等号右边的部分之前,需要首先向大家介绍一个我们的新朋友,消息(Message)。所谓的消息就是一个类或者对象可以执行的动作。消息的格式如下:

[对象或者类名字 方法名字:参数序列];

首先我们观察到有两个中括弧, 最右边的括弧之后是一个分号,当编译器遇到了这个格式之后会把中间的部分当作一个消息来发送。在上文的表达式当中,包括中括弧的所有部分的内容被称作消息表达式(Message expression),“对象或者类名字”被称作接收器(Receiver),也就是消息的接受者,“方法名字:参数序列”被称为一个消息(Message),“方法名字”被称作选择器(Selector)或者关键字(Keyword)。Objective-C和C语言是完全兼容的,C语言里面的中括弧用于表示数组,但是数组的格式明显和消息的发送的格式是不一样的,所以我们可以放心,编译器不会把我们的消息发送当作一个数组。

我们来回忆一下C语言里面函数的调用过程,实际上编译器在编译的时候就已经把函数相对于整个执行包的入口地址给确定好了,函数的执行实际上就是直接从这个地址开始执行的。Objective-C使用的是一种间接的方式, Objective-C向对象或者类(具体上是对象还是类的名字取决于方法是实体方法还是类方法)发送消息,消息的格式应该和方法相同。具体来说,第7行等号右边的部分[Cattle new]就是说,向Cattle类发送一个new的消息。这样当Cattle类接收到new的时候,就会查找它可以相应的消息的列表,找到了new之后就会调用new的这个类方法,分配内存和初始化完成之后返回一个id,这样我们就得到一个对象。

Objective-C在编译的过程当中,编译器是会去检查方法是否有效的,如果无效会给你一个警告。但是编译器并不会阻止你执行,因为只有在执行的时候才会触发消息,编译器是无法预测到执行的时候会发生什么奇妙的事情的。使用这样的机制给程序毫无疑问将给带来极大的灵活性,因为我们和任意的对对象或者类发送消息,只要我们可以保证执行的时候类可以准确地找到消息并且执行就可以了,当然如果找不到的话,运行会出错。

任何事物都是一分为二的 ---

任何事物都是一分为二的,在我们得到了灵活性的时候我们损失的是执行的时间。Objective-C的这种方式要比直接从函数的入口地址执行的方式要消耗更多的执行时间,虽然编译器对寻找的过程作过一定的优化。

有的同学会觉得奇怪,我们在Cattle里面并没有定义new,我们可以向Cattle发送这个类方法么?答案是可以,因为new在NSObject里面,实际上响应new消息的是NSObject。实际上new类似于一个宏,并不是一个“原子”的不可再分的方法,关于详细的情况,我们将在后续的章节里面讲解。

有了第7行的讲解,那么第8行的内容就不难理解了,第8行实际上是想cattle对象发送一个setLegsCount的消息,参数是4,参照Catttle.m,我们可以发现这个时候我们希望实体变量legsCount是4。第8行就更简单了,就是说向cattle对象发送一个saySomething的消息,从而实现了控制台的输出。

3.6,本章总结

通过本章的学习,同学们应该掌握如下概念

  1. 如何声明一个类
  2. 如何定义一个类
  3. 实体变量的定义
  4. 类方法和实体方法的定义
  5. id是什么
  6. NSObject的奇妙作用
  7. 如何从类开始初始化对象
  8. 消息的调用
感谢大家看到这里!我们下一章将要讲述继承的概念。


posted @ 2009-03-31 12:04  Yaski  阅读(17585)  评论(35编辑  收藏  举报