Objective-C 是苹果Mac OS X、iOS 平台的开发语言,Objective-C 基于C 语言的,增加面向对
象的相关特性。你可以认为Objective-C 就是另一个版本的C++,也就是它采用了与C++不同
的语法,但也实现了面向对象。
NextStep 是一个使用Objective-C 语言编写的功能强大的工具包,里面有大量的类库、结构
体等,被苹果收购之后,更名为Cocoa,但是苹果并未更改NextStep 中的类库名称,因此你
会看到大量的以NS 为前缀的类名、结构体、枚举等。在Objective-C 中使用前缀可以有效的
防止名称冲突。
Cocoa 框架由Foundation Kit、App Kit 两部分组成,前者是基础工具库,是你必须首先要学
会的,后者主要是UI 库、高级对象等,我们这里只介绍Foundation Kit。
本文档使用Windows 上的GNUStep 作为Objective-C 的编译器,不支持Objective-C 2.0 的相
关新特性,但基本完全支持Cocoa 的Foundation Kit、App Kit 工具库。
1. GNUStep 的安装:
首先前往网址http://www.gnustep.org/experience/Windows.html,下载文件:
然后按照下面的顺序安装这四个文件到同一个目录(例如:C:\GNUstep):
(1.)gnustep-msys-system-xxx.exe
(2.)gnustep-core-xxx.exe
(3.)gnustep-devel-xxx.exe
(4.)gnustep-cairo-xxx.exe
安装完成后,进入开始---程序---GNUStep---Shell,你会看到一个在Windows 上打开的命令行
窗口,你可以在其中使用Linux 的Shell 命令cd、ls、rm 等进行操作。启动Shell 之后,它会
在GNUStep 的目录中建一个/home/xxx/的文件夹,xxx 为你当前登陆Windows 系统的用户名
称,Shell 默认进入的就是这个目录,也就是Linux 上的cd ~。
你可以在Shell 中使用vi 命令创建Objective-C 的源文件,但是推荐的方式是使用UltraEdit
等编辑器编辑Objective-C 的源文件,然后在Shell 中编译、运行。
GNUStep 使用GCC 编译器,编译Objective-C 的命令:
gcc -o hello.exe hello.m
-I/GNUstep/System/Library/Headers
-fconstant-string-class=NSConstantString
-L/GNUstep/System/Library/Libraries
-lobjc -lgnustep-base
(1.)红色部分为编译生成的可运行文件,蓝色部分为要编译的源文件,可以有多个,使用空
格分隔。
(2.) 参数-I 表示头文件查找的路径,-L 表示库文件查找路径,-l 表示需要链接的库文件,
-fconstant-string-class=NSConstantString 主要是指定常量字符串所使用的class。
2. 类定义:
我们定义一个类,这个类完成的功能是使用两个int 类型的数字组成一个分数。在Objective-C
中必须首先定义一个接口,该接口用于描述这个类的组成,包含成员变量、类变量、类方法、
成员方法。接口文件的扩展名为h,也就是定义为C 语言中的头文件。
Fraction.h
#import <Foundation/Foundation.h>
static int t=0;
@interface Fraction: NSObject{
int numerator;//分子
@public int denominator;//分母
}
-(void) setNumerator: (int) numerator;//numerator 的setter 方法
-(void) setDenominator: (int) denominator;//denominator 的setter 方法
-(void) setNumerator: (int) numerator andDenominator: (int) denominator;
//一个同时设置两个成员变量的快捷方法
-(int) numerator;//numerator 的getter 方法
-(int) denominator;//denominator 的getter 方法
-(void) print;
+(void) t;
@end
一个Objective-C 中的接口就是C 语言中的一个header 文件,这个文件结构如下所示:
#import Header
static 类型 变量名; // 只能在本类中访问 (static 变量只能在本文件中访问)
@interface 接口名: 父类名称{
访问修饰符 类型 变量名; // 类成员变量
… …
}
-(返回值类型) 方法名: (参数类型) 参数名 标签1: (参数类型) 参数名 … … // 成员方法
+(返回值类型) 方法名: (参数类型) 参数名 标签1: (参数类型) 参数名 … … // 类方法
@end
我们来逐行看一下上面的内容:
(1.) 这里与C 语言不同的是导入头文件使用的是import,而不是include。另外与C 语言一样
的地方是如果你想从当前目录查找Header 文件,找不到就到系统的头文件库中查找,
请使用#import “Header 文件”,如果你只想从系统的头文件库中查找,请使用#import
<Header 文件>。Foundation/Foundation.h 包含了Foundation Kit 中的所有的头文件定义,
GNUStep 的Objective-C 的Foundation 头文件在
\GNUStep 安装目录\GNUstep\System\Library\Headers\Foundation 文件夹。
GNUStep 的Objective-C 的AppKit 头文件在
\GNUStep 安装目录\GNUstep\System\Library\HeadersAppKit 文件夹。
(2.) static 标识的类变量定义在接口的外面,类变量只能本类访问,除非提供类方法给外部
访问这个类变量。
(3.) Objective-C 中的@+指令表示C 语言之外的Objective-C 的衍生语法,因此@interface 表示
定义了一个接口,接口名称之后紧跟了一个冒号,冒号后是父类的名字,Objective-C 中
的顶级父类是NSObject。
(4.) 接口定义之后紧接着一对{ },其中定义了成员变量,所谓的成员变量就相当于JAVA 中的
实例变量,从属于类的对象。Objective-C 中的成员变量使用@public、@protected、@private
作为访问修饰符,默认是@protected。这里你要知道的是Objective-C 中只有成员变量有
访问修饰符,类变量、类方法、成员方法是没有访问修饰符的,所有的方法都是public
的,所有的类变量都是私有的。
(5.) 以-开头的方法为成员方法,以+开头的方法为类方法,方法中的类型描述(返回值类型、
参数类型)都必须使用( )包围。如果方法有多个参数,每个参数都有一个标签名(可以
省略,但不建议这样做),每个标签名之后使用冒号与参数描述分隔。在有多个参数的
方法中,实际的方法名称为 方法名:标签名1:标签名2:… …,上面的拥有两个参数的方
法的方法名为setNumerator:andDenominator:。
这里与JAVA不同的是Objective-C中的类方法只能类调用,如果你使用对象调用会报错,
而JAVA 仅仅是在编译期给出警告。另外,Objective-C 中的大多数类方法都被用来提
供初始化对象的便捷方法Convenience method。
(6.) 以@end 表示接口定义结束。这是因为与JAVA 不同的是JAVA 的类型定义使用{ }包围,
而Objective-C 中的{ }只包围成员变量,因此必须有个结束标志,一般JAVA 程序员经常
会忘记写这个结束标记。
这里你要知道Objective-C 的@interface 与JAVA 的interface 并不是一回事儿,后面你会看到
Objective-C 中的@protocol 与JAVA 中的interface 才是等同的。这里你只需要记住的是
Objective-C 中的@interface 只是类的一个描述,因为@interface 通常在独立的h 文件中,你
可以把它类比成C 语言中的函数原型,也就是在Objective-C 里应该叫做类的原型。通过这
个原型,编译器可以知道具体实现类有哪些功能。
上面的接口中的方法很简单,主要就是成员变量的setter、getter 方法,这与JAVA 没有什么
不同的。但是你会发现getter 方法没有以get 作为方法名称前缀,这是因为get 开头的方法
在Objective-C 中有着特殊的含义,这在后面将会看到。
下面我们编写实现类,Objective-C 的类文件使用扩展名m。
Fraction.m
#import "Fraction.h"
@implementation Fraction
-(void) setNumerator: (int) n{
numerator=n;
}
-(void) setDenominator: (int) d{
denominator=d;
}
-(void) setNumerator: (int) n andDenominator: (int) d{
numerator=n;
denominator=d;
}
-(int) numerator{
return numerator;
}
-(int) denominator{
return denominator;
}
-(void) print{
printf("%d/%d\n",numerator,denominator);
}
-(void) m{
printf("-m:The class variable t is %d\n",++t);
}
+(void) t{
printf("+t:The class variable t is %d\n",++t);
}
@end
因为我们将Fraction.m 与Fraction.h 放在一个文件夹下面,所以#import 使用了” ”,这个类的
任务就是实现接口中的方法,因此与接口的结构不同的地方就是,你不能在这里定义变量,
@interface 换成了@implementation,其余就没有什么特别的了。你不必在这里实现@interface
中的全部方法,这不会导致错误。这里有个-(void) m 方法比较特别,我们并没有在@interface
中声明,那么这个方法可以调用吗?因为Objective-C 是动态语言,即便是@interface 中没有
定义的方法,依然可以被调用。
另外,你需要注意的是setter 方法与接口中不同的是参数名缩写成了n、d,这是因为在方
法中,本地变量(参数、方法中定义的变量)在名称冲突的情况下,会隐藏成员变量,因此
导致numerator=numerator 变成了无意义的操作。当然你可以使用后面提到的self 关键
字,写成 self->numerator=numerator,也就是JAVA 中的常用的this.x=x 的写法。
下面我们编写调用代码,因为Objective-C 基于C 语言,所以程序的入口依然是main 函数。
这里注意#import 的是h,不是m。
main.m
#import "Fraction.h"
int main(int argc,const char *argv[])
{
Fraction *frac=[[Fraction alloc] init];
[frac setNumerator: 3 andDenominator: 5];
[frac print];
printf("The denominator of Fraction is %d\n",frac->denominator);
[Fraction t]; //调用类方法
[frac m];
[frac release];
return 0;
}
(1.) 第一行我们创建了Fraction 的实例(对象),Objective-C 中实例只能使用指针作为变量,
而不能使用值,所以你看到了*frac,而不是frac,这与JAVA 是一致的,JAVA 中的指向
实例的变量(JAVA 中叫做引用)也是指针,只不过JAVA 中没有指针的概念,所以你没
有看到*。至于等号右侧的创建实例的代码,你可以在下面看到,这里先不用理会。
(2.) 第二行代码调用同时设置两个变量的方法,我们看到Objective-C 的调用方法的语法格式
为[类或者实例的指针 方法名: 参数1 标签1: 参数2… …]。这种调用格式被称为中
缀语法,初次看起来有点儿怪,但实际这样更加有效。举个例子,你接手了一个离职的
人程序,其中的JAVA 程序调用了一个有五个甚至更多的参数的方法,但是你手里没有
这个方法的API,那么你很难猜得出来这五个参数到底都干什么用的,但是Objective-C
调用的时候,每个参数前面都必须有方法的标签名,这样你便能很容易的从标签名看出
这个参数是什么意思。
(3.) 第四行在C 的printf()函数中使用了对象->成员变量的语法访问实例的变量,但一般我
们不推荐这么做,而是使用getter 方法。这里你不能访问numerator 变量,因为它是
@protected 的,只能本类、子类直接访问。
(4.) 第五行我们调用了类方法t,你也可以换成这样的写法:
[[Fraction class] t];
或者
Class clazz=[Fraction class];
[clazz t];
class 来自于NSObject,相当于JAVA 中的getClass()方法,也就是获取这个类的Class 对象,
clazz 前面没有*,这是因为Class 已经是一个指针。另外这种嵌套调用的方式,你也要习
惯,这就和JAVA 中的A.b().c()没有什么区别。
获取Class 有如下几种方法:
[类或者对象 class]
[类或者对象 superclasss]
NSClassFromString(类名的字符串形式)
你也可以通过如下的函数把Class 转换为字符串形式:
NSStringFromClass(Class)
(5.) 第六行我们调用了m 方法,这个方法你会发现并没有在@interface 中声明,这里依然调
用了,只是在编译的时候收到一个警告。这就是前面所有的Objective-C 是动态语言的原
因。但是一般情况下,你都会给别人提供h 文件,所以你在m 文件中写的h 文件中没
有的方法,别人也是不会知道的,这个方法相当于变相的私有化了。
(6.) 第七行我们释放了frac 实例在第一行alloc 所申请的内存空间,Objective-C 的内存管理
后面会看到。另外,你会发现Fraction.h 中没有定义alloc、init、release 方法,但是我们
上面调用了,很显然,这些方法来自于父类NSObject。
编译、运行上面的程序,你会看到Shell 窗口输出如下内容:
3. Objective-C 中的布尔类型:
早期的C 语言中是没有布尔类型的(C99 增加了布尔类型),Objective-C 中增加BOOL 类型
来表示YES、NO,注意不是TRUE、FALSE。
BOOL 使用了一个8 位(一个字节)的整数进行表示,8 位全0 就是NO。
我们知道C 语言中非0 值即为逻辑真,因此常常会有int i=5;while(i){… …}的写法。在
Objective-C 中一定要注意慎用C 语言中的这种数字与逻辑真假混合对待的做法去操作BOOL
类型变量。例如:
BOOL bi=8960;
if(bi==YES)
{
printf("YES");
}
这里会输出YES 吗?不会的。为什么呢?8960 是非0 值,它不是逻辑真吗?还记得上面说
过BOOL 是一个8 位的整数吗?
因为 8960 用二进制表示是大于8 位的,也就是说高位无效,只保留8960 的低八位,8960 的低八位恰好全都是0,因此bi 就是NO 了。
因此在Objective-C中一定要注意这个问题,非零值未必是BOOL 的YES,但是0 一定是NO。
所以有C 语言编程经验的,最好不要把BOOL 与整数掺合在一起作为布尔类型的判断,可能
C 语言的开发者认为直接用数字作为布尔值进行判断在写法上更为简洁。
4. Objective-C 中的null:
Objective-C 中的对象使用nil 表示null,但是它与null 是有区别的,如果你向null 发送消息
(使用null 调用方法)会得到运行时错误,这在JAVA 中司空见惯的空指针异常就已经知道
了。但是nil 是可以回应消息(respond to message),因此你不必再为空指针而烦恼。
5. 与C 混合编写:
// 混合编写,文件名的后缀必须写成 .mm 的格式,要不编译报错
我们看一段代码:
BOOL differentInt(int m , int n)
{
if(m!=n)
return YES;
else
return NO;
}
NSString *boolString(BOOL yn){
if(yn==YES){
return @"YES";
}
else{
return @"No";
}
}
int main(int argc,const char *argv[]){
NSLog(boolString(differentInt(5,3)));
return 0;
}
这里我们定义了函数differentInt()用于比较两个整数是否相等,boolString()函数用于将BOOL
类型转换为字符串。这两个函数的返回值都是Objective-C 中的类型,其中的BOOL 不是对象
类型,所以不使用指针,因此方法名称前面没有*,NSString 是Objective-C 中的字符串对象,
相当于JAVA 中的String 类型,@”… …”是一种NSString 的字面值的表示方法,与JAVA 中的”… …”
可以直接表示字符串,而不必非得显示的用String 来引用字符串是一样的。这里注意与
Objective-C 的类型中的方法定义不同的是,函数的返回值如果是指针,*写在C 语言的函数
名称前面,而不是返回值的前面。
在main 函数中我们使用了Objective-C 的函数NSLog(@”格式化字符串”,变量1,变量2,… …),
这与C 语言的printf(”格式化字符串”,变量1,变量2,… …)很相似,不过它会像JAVA 中的LOG4J
一样,在输出语句之前增加日期戳、运行的类名、自动追加换行符\n 等信息。
你可能又会问Objective-C 不都是对象吗?怎么还出来个NSLog()的函数呢?函数不是C 语言
面向过程编程的东西吗?其实Cocoa 中有很多的东西都不是对象,而是C 语言的函数、结构
体等。例如:
struct NSRange{
NSUInteger location;
NSUInteger length;
}
结构体NSRang 表示一个范围,location 是范围的起始点,length 是范围的长度。
struct NSRect{
NSPoint point;
NSSize size;
}
结构体NSRect 表示一个矩形,point 是左顶点的坐标,size 是长宽。
struct NSPoint{
float x;
float y;
}
struct NSSzie{
float width;
float height;
}
NSRange 常用来做字符串处理。NSRect 常用来做图形处理,譬如:动画中不断的重新绘制矩
形等。你很容易知道这是个频繁操作的处理过程,也就是矩形要不断绘制、擦除。假如NSRect
是一个Objective-C 类型,由于类实例化对象要在堆内存中动态分配存储空间,这是个很消
耗资源的操作,而动画又是频率较高的操作,反复的创建、销毁对象,效率将会极其低下。
所以Cocoa 这里将NSRect 定义为C 语言的结构体就大大提高了运行效率。相比较Android
平台,有人说做游戏纯靠JAVA 是不行的,JAVA 最多就是画画UI 和做些简单的应用,因为
JAVA 不具备和C 语言混合编程的能力,事事都要借助对象,所以必然要引入C 去编写Android
的*.SO 的图形引擎等,以提高处理能力。
6. 对象的初始化:
JAVA 的对象创建只有一个过程,就是调用构造方法,但是Objective-C 分为两个步骤:分配
内存、初始化,如上面的例子中的如下语句:
Fraction *frac=[[Fraction alloc] init];
(1.) alloc 是从NSObject 继承而来的类方法,用于给对象分配存储空间,所有的成员变量在
此时对确定了自己的内存位置,并被赋初值,整数类型为0,浮点数为0.0,BOOL 为NO,
对象类型为nil,alloc 方法返回对象的指针。
(2.) init 是从NSObject 继承而来的成员方法,这个方法是你在对象创建过程可以参与的方法,
因此你可以定制自己的init 方法。Objective-C 对init 方法没有特殊的要求,就是一个普
通方法而已,只不过习惯以init 作为方法前缀。一般init 方法都返回当前类型的指针或
者id 类型。id 类型在Objective-C 中是一个泛型对象,与前面所说的Class 对象一样,id
类型的变量的前面不使用*,因为id 已经是一个结构体指针,并且结构体内部通过Class
类型的isa 指向了继承了NSObject 的对象。id 可以表示任意类型的Objective-C 对象。
下面我们定制Fraction 的初始化方法。
在Fraction.h 中增加如下两个方法原型:
-(id) init;
-(Fraction*) initWithNumerator: (int) numerator andDenominator: (int) denominator;
Fraction.m 的实现:
-(id) init{
self=[super init];
}
-(Fraction*) initWithNumerator: (int) n andDenominator: (int) d
{
self=[self init];
if(self)
{
[self setNumerator: n andDenominator: d];
}
return self;
}
(1.) 第一个方法我们覆盖了NSObject 中的init 初始化方法,返回值id 你也可以写成Fraction*,
这里只是为了让你知道id 可以代表任意类型。方法的实现中我们首先调用了父类的init
方法,使用了关键字super,这与JAVA 没什么不同的。另外,你还看到了关键字self,
它相当于JAVA 中的this,用于指向当前实例的指针。但是奇怪的是我把[super init]的返
回值赋给了self,这在JAVA 中是没有的操作。因为Objective-C 是动态语言,所以init 方
法很可能会返回一个不是当前类型的返回值,也就是Fraction 的init 方法是可能返回另
一个类型的(Objective-C 中的NSString 的实现代码init 方法返回的就是NSCFString)。因
此为了保证程序可以正常工作,你需要把父类的init 方法产生的返回值赋给self,让self
指向父类init 方法返回的对象,然后才能开始其它操作。
(2.) 第二个方法首先调用了第一个方法,这就相当于JAVA 中的策略模式,重载方法互相调
用,这没有什么特别的。需要知道的是首先判断了一下self 是否为nil(if(slef)与
if(self!=nil)是相同的),因为父类可能因为某些原因导致初始化异常。
你可以在main 函数中使用新的init 方__________法:
Fraction *frac=[[Fraction alloc] initWithNumerator :2 denominator :3];
7. Objective-C 的description 方法:
JAVA中的对象都有从Object中继承而来的String toString()方法,用于获取对象的字符串表示,
Objective-C 中的这个方法的方法签名为:
-(NSString*) description;
由于这是NSObject 中的成员方法,因此我们就不必在Fraction.h 文件中声明它了,直接在
Fraction.m 中实现如下所示:
-(NSString*) description{
return @"I am a fraction!";
}
编写main 函数访问:
int main(int argc,const char *argv[]){
Fraction *frac=[[Fraction alloc] initWithNumerator: 2 andDenominator: 3];
NSLog(@"%@",frac);
return 0;
}
这里我们看到NSLog 输出对象用的格式化字符串是%@,这样description 方法就会被调用。
如果我们不实现自己的description,那么会调用NSObject 的实现,输出对象的首地址。
8. Objective-C 的异常处理:
我们的Fraction 忽略了一个问题,那就是没有对denominator 为0 的情况作处理,也就是分
母不能为0。我们可以使用Objective-C 的异常机制对这个问题进行处理。
Objective-C 的异常都继承自 N***ception ,当然 N***ception 本身也继承自NSObject。
DenominatorNotZeroException.h
#import <Foundation/Foundation.h>
@interface DenominatorNotZeroException: N***ception
@end
DenominatorNotZeroException.m
#import "DenominatorNotZeroException.h"
@implementation DenominatorNotZeroException
@end
我们定义了分母不能为0 的异常,Objective-C 中的异常定义起来很简单,其实除了写了类名
之外什么都没有写。
下面我们改写Fraction.m 的如下两个setter 方法:
-(void) setDenominator: (int) d{
if(d==0){
N***ception *e=[DenominatorNotZeroException
exceptionWithName: @"DenominatorNotZeroException"
reason: @"The denominator is not 0!"
userInfo:nil];
@throw e;
}
denominator=d;
}
-(void) setNumerator: (int) n andDenominator: (int) d{
if(d==0){
N***ception *e=[DenominatorNotZeroException
exceptionWithName: @"DenominatorNotZeroException"
reason: @"The denominator is not 0!"
userInfo:nil];
@throw e;
}
numerator=n;
denominator=d;
}
这两个方法中我们判断如果denominator 为0,则抛出DenominatorNotZeroException,
DenominatorNotZeroException 的创建使用了父类 N***ception 的类方法
exceptionWithName:reason:userInfo:,前两个参数表示异常的名字、原因,参数类型为
NSString,第三个参数为用户信息,暂不清楚做什么的,我们传递nil 就可以了。异常定义
完成后使用@throw 抛出。
下面我们编写捕获异常的代码:
int main(int argc,const char *argv[]){
NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init];
@try{
Fraction *frac=[[Fraction alloc] initWithNumerator: 3 andDenominator: 0];
}@catch (DenominatorNotZeroException *dne){
printf("%s\n",[[dne reason] cString]);
}@catch (N***ception *e){
printf("%s\n",[[e name] cString]);
}@finally{
printf("finally run!");
}
[pool release];
return 0;
}
这里的捕获代码基本与JAVA 的意义相同,只不过是try、catch、finally 前加@。另外,由
于 N***ception 的成员方法name、reason 返回的是NSString,因此又接着调用了NSString
的成员方法cString,得到C 的字符串类型char[],然后才能传递给C 的函数printf()。
这里你还看到了一个NSAutoreleasePool,先不用理会它,因为不写它会报错,后面会做讲
解。
但你会发现上面的程序无法在GNUStep 中运行,提示不认识@throw,所以你只需要知道上
面的写法就可以了,运行可以在Mac 电脑上操作。
9. id 类型:
下面我们演示一下id 类型是如何可以指向任意类型的实例的。我们定义一个复数类型
Complex。
Complex.h
#import <Foundation/Foundation.h>
@interface Complex: NSObject {
double real;//复数的实部
double imaginary;//复数的虚部
}
-(Complex*) initWithReal: (double) r andImaginary: (double) i;
-(void) setReal: (double) r;
-(void) setImaginary: (double) i;
-(void) setReal: (double) r andImaginary: (double) i;
-(double) real;
-(double) imaginary;
-(void) print;
@end
Complex.m
#import "Complex.h"
@implementation Complex
-(Complex*) initWithReal: (double) r andImaginary: (double) i{
self=[super init];
if(self){
[self setReal: r andImaginary: i];
}
return self;
}
-(void) setReal: (double) r{
real=r;
}
-(void) setImaginary: (double) i{
imaginary=i;
}
-(void) setReal: (double) r andImaginary: (double) i;{
real=r;
imaginary=i;
}
-(double) real{
return real;
}
-(double) imaginary{
return imaginary;
}
-(void) print{
printf( "%f + %fi", real, imaginary );//输出复数z=a+bi
}
@end
main.m
#import "Fraction.h"
#import "Complex.h"
int main(int argc,const char *argv[]){
Fraction *frac=[[Fraction alloc] initWithNumerator: 3 andDenominator: 5];
Complex *comp=[[Complex alloc] initWithReal: 1.5 andImaginary: 3.5];
id number=frac;
[number print];
number=comp;
[comp print];
[frac release];
[comp release];
return 0;
}
我们看到id 类型number 首先指向了frac 实例,调用了它的print()方法,然后指向了comp
实例,调用了它的print()方法。所以id 你可以把它理解为随便,也就是它表示任意的东西。
10. 类的继承:
下面我们以矩形、正方形的对象展示一下Objective-C 中的继承结构。
矩形MyRectangle.h
#import <Foundation/Foundation.h>
@interface MyRectangle: NSObject{
int width;
int height;
}
-(MyRectangle*) initWithWidth: (int) weight andHeight: (int) height;
-(void) setWidth: (int) width;
-(void) setHeight: (int) height;
-(int) width;
-(int) height;
-(void) area;//计算面积
@end
矩形MyRectangle.m
#import "MyRectangle.h"
@implementation MyRectangle
-(MyRectangle*) initWithWidth: (int) w andHeight: (int) h{
self=[super init];
if(self){
[self setWidth: w];
[self setHeight: h];
}
}
-(void) setWidth: (int) w{
width=w;
}
-(void) setHeight: (int) h{
height=h;
}
-(int) width{
return width;
}
-(int) height{
return height;
}
-(void) area{
printf("%d\n",width*height);
}
@end
正方形MySquare.h
#import "MyRectangle.h"
@interface MySquare: MyRectangle{
int size;
}
-(MySquare*) initWithSize: (int) size;
-(void) setSize: (int) size;
-(int) size;
@end
这里正方形的父类是MyRectangle。
正方形MySquare.m
#import "MySquare.h"
@implementation MySquare
-(MySquare*) initWithSize: (int) s{
self=[super init];
if(self){
[self setWidth: s];
[self setHeight: s];
//因为正方形的长宽相等,所以我们把正方形的边长赋给父类的长宽,以
便复用计算面积的方法area()。
}
}
-(void) setSize: (int) s{
size=s;
}
-(int) size{
return size;
}
@end
main.m
#import "MySquare.h"
int main(int argc,const char *argv[]){
MyRectangle *rec=[[MyRectangle alloc] initWithWidth: 2 andHeight: 5];
[rec area];
MySquare *squa=[[MySquare alloc] initWithSize: 6];
[squa area];//使用父类的计算面积的方法
[rec release];
[squa release];
}
11. 动态判定与选择器:
第9 节中我们使用了id 这个泛型对象,假如别人传递给你一个id,你如何动态判断这个在
运行时传递过来的对象到底有没有XXX()方法呢?因为id 可以代表任意对象,你也不知道运
行时传递的这个id 里面到底有什么东西。其实NSObject 中有一系列这样的方法用于动态判
定,类似于JAVA 的反射机制,但是Objective-C 的用起来更为简单。
-(BOOL) isMemberOfClass: (Class) clazz 用于判断对象是否是clazz 类型的实例,但不包含子类的实例。
-(BOOL) isKindOfClass: (Class) clazz 用于判断对象是否是clazz 类型的实例或者clazz 的子类的实例。
-(BOOL) respondsToSelector: (SEL) selector 用于判断类型或者对象是否能够回应某个方法,这个方法使用选择器表示。
+(BOOL) instancesRespondToSelector:(SEL) selector 用于判断类型所产生的实例是否能够回应某个方法,这个方法使用选择器表示。
- (id) performSelector: (SEL) selector 用于动态调用类型或者对象上的一个方法。
上面的后三个方法中都涉及到了一个新类型选择器SEL,它用于表示Objective-C 的一个方法,
我们知道在C 语言中有函数指针指向一个函数,也就是Objective-C 的选择器就相当于C 语
言中的函数指针,只不过选择器只能表示Objective-C 类型中定义的方法。选择器使用
@selector(方法名)的形式获取,例如:@selector(initWithWidth:andHeight:) 、@selector(alloc)。
同时,SEL 是在继Class、id 之后第三个不需要在变量前使用*的类型,因为它已经是一个指针。
另外,注意上面的红色文字,你可以看到这两行的方法都是-开头的,也就是成员方法,但
是为什么类也可以去掉用呢?其实是类的Class 对象去掉用的,因为Class 对象有着两个方法
的回应能力。
我们以第10 节的矩形、正方形的程序为基础,进行代码演示。
int main(int argc,const char *argv[]){
MyRectangle *rec=[[MyRectangle alloc] initWithWidth: 2 andHeight: 5];
[rec area];
MySquare *squa=[[MySquare alloc] initWithSize: 6];
[squa area];
//-(BOOL) isMemberOfClass: (Class) clazz 用于判断对象是否是clazz 的实例,但
不包含子类的实例。
if([squa isMemberOfClass: [MyRectangle class]]){
printf("squa isMemberOfClass MyRectangle\n");
}else{
printf("squa not isMemberOfClass MyRectangle\n");
}
if([squa isMemberOfClass: [MySquare class]]){
printf("squa isMemberOfClass MySquare\n");
}else{
printf("squa not isMemberOfClass MySquare\n");
}
printf("----------------------------------------------\n");
//-(BOOL) isKindOfClass: (Class) clazz 用于判断对象是否是clazz 的实例或者参数
的子类的实例。
if([squa isKindOfClass: [MyRectangle class]]){
printf("squa isKindOfClass MyRectangle\n");
}else{
printf("squa not isKindOfClass MyRectangle\n");
}
if([squa isKindOfClass: [MySquare class]]){
printf("squa isKindOfClass MySquare\n");
}else{
printf("squa not isKindOfClass MySquare\n");
}
printf("----------------------------------------------\n");
//-(BOOL) respondsToSelector: (SEL) selector 用于判断对象或者类型是否有能力
回应指定的方法。
if([squa respondsToSelector: @selector(initWithSize:)]){
printf("squa respondsToSelector initWithSize:\n");
}else{
printf("squa not respondsToSelector initWithSize:\n");
}
if([MySquare respondsToSelector: @selector(alloc)]){
printf("MySquare respondsToSelector alloc\n");
}else{
printf("MySquare not respondsToSelector alloc\n");
}
if([rec respondsToSelector: @selector(initWithWidth:andHeight:)]){
printf("rec respondsToSelector initWithWidth:andHeight:\n");
}else{
printf("rec not respondsToSelector initWithWidth:andHeight:\n");
}
printf("----------------------------------------------\n");
//+(BOOL) instancesRespondToSelector: (SEL) selector 用于判断类产生的实例
是否是由有能力回应指定的方法。
if([MySquare instancesRespondToSelector: @selector(initWithSize:)]){
printf("MySquare instancesRespondToSelector initWithSize:\n");
}else{
printf("MySquare not instancesRespondToSelector initWithSize:\n");
}
if([MySquare instancesRespondToSelector: @selector(alloc)]){
printf("MySquare instancesRespondToSelector alloc\n");
}else{
printf("MySquare not instancesRespondToSelector alloc\n");
}
printf("----------------------------------------------\n");
//-(id) performSelector: (SEL) selector 用于动态调用类或者对象上的一个方法。
id x_id=[rec performSelector: @selector(area)];
[MyRectangle performSelector: @selector(alloc)];
[rec release];
[squa release];
}
12. 类别Category:
如果你想扩充一个类的功能,但又不想使用继承,你可以选择类别。
下面我们写一个Fraction 的类别,为Fraction 类增加计算两个分数的加减乘除的方法。
FractionMath.h
#import "Fraction.h"
@interface Fraction (Math1)
-(Fraction*) mul: (Fraction*) f;//乘法,就是传入一个Fraction 作为参数,与当前的Fraction
进行计算
-(Fraction*) div: (Fraction*) f;//除法
@end
@interface Fraction (Math2)
-(Fraction*) add: (Fraction*) f;//加法
@end
其实类别就是在你想要扩展的@interface 的名字后面加个( ),里面写上类别的名字,这个名
字必须是唯一的,也就是说一个@interface 只能有一个类别为XXX 的Category。
FractionMath.m
#import "FractionMath.h"
@implementation Fraction (Math1)
-(Fraction*) mul: (Fraction*) f{
return [[Fraction alloc] initWithNumerator: numerator*[f numerator]
denominator: denominator*[f denominator]];
}
-(Fraction*) div: (Fraction*) f{
return [[Fraction alloc] initWithNumerator: numerator*[f denominator]
denominator: denominator*[f numerator]];
}
@end
@implementation Fraction (Math2)
-(Fraction*) add: (Fraction*) f{
return [[Fraction alloc] initWithNumerator:
numerator*[f denominator] + denominator*[f numerator]
denominator: denominator*[f denominator]];
}
@end
类别的实现类也就是类型名后后面加个( ),里面写上类别的名字,其他的没有什么不同的。
上面唯一可能会让你头晕的就是加减乘除的实现代码,实际上就是嵌套调用,写成了一个语
句而已。拿add 的实现来说,就是创建要返回的计算结果Fraction 的实例,然后依据分数相
加要先通分的规则,结果的分子initWithNumber 就是第一个分数的分子*第二个分数的分母,
再加上第二个分数的分子*第一个分数的分母,分母denominator 参数就是两个分数的分母
相乘。
main.m
#import "FractionMath.h"
int main( int argc, const char *argv[] ) {
Fraction *frac1 = [[Fraction alloc] initWithNumerator: 1 denominator: 3];
Fraction *frac2 = [[Fraction alloc] initWithNumerator: 2 denominator: 5];
Fraction *frac3 = [frac1 mul: frac2];
[frac1 print];
printf( " * " );
[frac2 print];
printf( " = " );
[frac3 print];
printf( "\n" );
Fraction *frac5 = [frac1 add: frac2];
[frac1 print];
printf( " + " );
[frac2 print];
printf( " = " );
[frac5 print];
printf( "\n" );
[frac1 release];
[frac2 release];
[frac3 release];
[frac5 release];
return 0;
}
运行GCC 之后,Shell 窗口输出如下内容:
我们看到没有使用继承语法,就为Fraction 添加了数学运算的方法。另外,利用一个类可以有多个类别的特性(上面的Math1、Math2)。
如果一个@interface 有上百个方法,那么你可以让编写的@implementation 什么都不实现,然后按照@interface 中的各个方法的功能的不同,在不同的类别中实现不同的方法,这样可以防止一个@implementation 实现全部的接口而显得臃肿。
那么类别与继承相比,有什么缺点吗?
类别不可以声明新的成员变量,而且一旦你定义的方
法与原始类中的方法名称相同,那么原始方法将被隐藏起来,因为不是继承结构,你不能在
类别中的方法使用super 激活原始类的同名方法。
类别还有一个功能,就是隐藏方法,我们在Fraction.m 的最后增加如下的内容:
@interface Fraction (Math3)
-(Fraction*) sub: (Fraction*) f;//减法
@end
@implementation Fraction (Math3)
-(Fraction*) sub: (Fraction*) f{
return [[Fraction alloc] initWithNumerator:
numerator*[f denominator] - denominator*[f numerator]
denominator: denominator*[f denominator]];
}
@end
在.m 文件中定义@interface?你没有看错,因为@interface 一旦定义在.m 文件中,它就不能以Header 文件的形式被导入到其他的类了,也就是这样的@interface 中定义的方法相当于被你给隐藏了,只能这个.m 编译单元内看见。(只能在当前.m 文件中使用)
此时如果你在main 函数中调用sub 做减法运算,会在编译器得到一个警告,但是实际上你
还是能够访问到sub 方法,因为Objective-C 是动态语言,只要运行期有这个方法,就能够
调用。我们把@interface 定义在.m 文件中只能做到让别人不知道有这个方法,但是如果他有你的.m 文件的源码或者恰好猜到(这种撞大运的几率太低了)你有sub 方法,还是可以调用的。
类别的应用比较广泛,譬如:第三方的Objective-C 库RegexKitLite 就是对NSString、
NSMutableString 做的类别,为Objective-C 的字符类型增加了正则表达式的功能。
13. 协议@protocol:
前面说过@interface 相当于是Objective-C 的类的原型,与JAVA 中的接口意义是不同的,
Objective-C 中的 @protocol 才是和JAVA 中的接口等价的东西。例如:Objective-C 的继承也是单继承,只允许有一个父类,但是@protocol 是允许多继承的(按照Objective-C 的说法叫做某类遵从了协议A、协议B,而不是继承),这些都与JAVA 的接口一致。
Printing.h
@protocol Printing1
-(void) print1;
@end
@protocol Printing2
-(void) print2;
@end
@protocol Printing3 <Printing2>
-(void) print3;
@end
Printing3 <Printing2>的意思是Printing3 遵从Printing2,<>是遵从@protocol 协议的语法。
Fraction.h
#import <Foundation/Foundation.h>
#import "Printing.h"
@interface Fraction: NSObject <Printing1,Printing3>{
int numerator;
int denominator;
}
-(Fraction*) initWithNumerator: (int) n denominator: (int) d;
-(void) setNumerator: (int) n;
-(void) setDenominator: (int) d;
-(int) numerator;
-(int) denominator;
@end
Fraction.m
#import "Fraction.h"
@implementation Fraction
-(Fraction*) initWithNumerator: (int) n denominator: (int) d{
self=[super init];
if(self){
[self setNumerator: n];
[self setDenominator: d];
}
}
-(void) setNumerator: (int) n{
numerator=n;
}
-(void) setDenominator: (int) d{
denominator=d;
}
-(int) numerator{
return numerator;
}
-(int) denominator{
return denominator;
}
-(void) print1{
printf("1:%d/%d\n",numerator,denominator);
}
-(void) print2{
printf("2:%d/%d\n",numerator,denominator);
}
-(void) print3{
printf("3:%d/%d\n",numerator,denominator);
}
@end
main.m
#import "Fraction.h"
int main (int argc , const char *argv[])
{
Fraction *frac=[[Fraction alloc] initWithNumerator: 3 denominator: 5];
<Printing1> p1=frac;//使用protocol 类型,相当于JAVA 中使用接口类型作为对象
的引用List list=ArrayList 的实例。
//也可以写作 id <Printing1> p1=frac;
[p1 print1];
id<Printing1,Printing2,Printing3> p2=frac;
//从这里可以看出id 是一个泛型对象,在id 后面使用<>作为泛型参数可以明确的
告诉别人你想把id 当作哪些种协议去使用,当然你可以不写泛型参数。
[p2 print2];
[p2 print3];
//-(BOOL) conformsToProtocol: (Protocol*) prot 用于判断对象是否遵从某
个protocol。
if([frac conformsToProtocol: @protocol(Printing1)]
&&[frac conformsToProtocol: @protocol(Printing2)]
&&[frac conformsToProtocol: @protocol(Printing3)])
{
printf("YES");
}else{
printf("NO");
}
[frac release];
return 0;
}