ObjC是面向对象语言。因此面向对象的中的很多概念,ObjC都支持,比如:
- 类
- 对象(实例)
- 实例化
- 继承
- 接口(协议)
- 实现
- 覆盖(重写)方法
- 复合(组合)
- super关键字
下面就分别介绍一下。
类的定义和实现
面向对象语言一般都有类的概念,虽然也有特殊的情况,比如javascript只有原型的概念而没有类的概念。ObjC有类的概念。
如下图所示,创建一个类文件:
然后是下一步,给出这个类的名称,ObjC的命名规则是类名首字母大写,驼峰方式,这点和Java是一样的。
这里强调一点,我把Also create "Book.h"的默认勾选项取消了。勾选该项,将生成两个文件,Book.h和Book.m。前者是头文件,后者是实现文件。一般会将二者分开,这样比较清楚,便于维护。这里的做法是为了说明可直接写m文件而无需写头文件。因为可以在一个头文件中声明多个类,几个m文件共享同一个头文件。
我把Book的头文件类声明部分,放到其他类的头文件中,比如:
#import <Foundation/Foundation.h>
@interface CarInfo : NSObject {
}
-(void)sayCarBrand;
@end
@interface Book : NSObject {
}@end
然后,m文件部分要引用该头文件:
#import "CarInfo.h"
@implementation Book
@end
一般情况下,类文件是生成一对文件的,即勾选Also create "Book.h"的情况。那么会生成一个名为Book.h的头文件,和一个名为Book.m的实现文件。
头文件内容:
#import <Foundation/Foundation.h>
@interface Book : NSObject {
}@end
实现文件:
#import "Book.h"
@implementation Book
@end
这里要提几点:
- ObjC和Java类似,类是单根继承的,也就是说,有一个最上级的根类,即NSObject,所有其他类都是这个类的直接或者间接的子类。
- 写法上比Java要啰嗦。类要分声明和实现两部分。其中:
- 声明部分放在头文件中。使用@interface关键字做类的声明。Book表示是要声明的类名称,:NSObject,表示Book继承自NSObject。花括号内可存放实例变量的声明,后面再讲。@end表示类的声明结束。
- 实现部分要放在m文件中。使用@implementation关键字,表示实现某类。。@end表示类的实现结束。
- 实现的这个类是个空的类。既没有属性,也没有方法。没什么用。后面再讲属性和方法的使用。
类的实例化
声明并且实现了一个类,就是为了使用它:
Book *book=[[Book alloc]init];
这里可以拆解来写上面的这行语句:
Book *book;
book=[[Book alloc]init];
其中第一句,表示声明一个Book类的变量book。book前面加*号,表示该变量是个Book指针类型变量。
ObjC中,所有对象类型的变量都是指针类型的,因此都需要加*号,这里有一个例外:
Book *book;
book=[[Book alloc]init];id myBook=book;
斜体加粗这句话编译不会报错。如果写成:
id *myBook=book;
反而会有编译警告。因为id这个数据类型,本身就是指针类型,不需要再增加*号了。在ObjC中,id类型表示任意类型的指针变量。
[[Book alloc] init]这句,类似Java中的:
new Book();
这里还能进一步拆解语句,写成:
book=[Book alloc];
book=[book init];
这里要先提到ObjC的一个语法,后面会详细介绍,方法调用。和Java不一样,ObjC使用[]来表示方法调用,[Book alloc]类似Java中的Book.alloc()。alloc是静态方法,如果按照Java的提法来看。在ObjC术语中,叫做类方法。alloc方法可以在NSObject的reference中找到。可以看到该方法是+ alloc,+号表示类方法,-号表示实例方法。这个后面会讲。
alloc将分配内存,创建名为book的Book类实例。alloc还将为该实例创建内存计数。这涉及到ObjC的内存管理机制,在后面再讲解。
init方法,在NSObject的reference中也能找到。是带-号的方法,也就是说是实例方法。该方法的作用,是初始化。即可通过init方法对实例的成员变量做初始化。具体怎么做,要到覆盖方法部分再详细说。
类的继承
在ObjC的语法中,可以对任何类继承,如上面已经提到的,要在类的声明中:
@interface Book : NSObject {
ObjC语言中,没有限制继承的机制,比如在Java中,可以用关键字final标识一个类,强制规定该类不可继承。比如:
public final class String
这样就无法继承java.lang.String了。
方法的声明和使用
在ObjC的类中,可以声明方法。
ObjC语言的方法声明比较特别。不像Java、.net的那么容易识别。
先写个最简单的,帮助读者适应一下。Book类,想声明一个打印书名的方法。这个方法为什么简单呢?因为,既不需要传入参数,也没有返回值。
方法的声明,在头文件中:
#import <Cocoa/Cocoa.h>
@interface Book : NSObject {
}
-(void) printName;
@end
方法的实现,在m文件中:
#import "Book.h"
@implementation Book
-(void) printName{
}@end
怎样使用这个方法,完整代码如下:
Book *book=[[Book alloc] init];
[book printName];
我现在没有在printName方法中加入任何可执行语句。读者可考虑调用NSLog函数打印日志。
下面说说,有返回值的情况,这个比较简单。声明方法:
#import <Cocoa/Cocoa.h>
@interface Book : NSObject {
}
-(void) printName;
-(NSString *) getPrice;
@end
实现方法:
#import "Book.h"
@implementation Book
-(void) printName{
}-(NSString *) getPrice{
return @"$17";
}
这里返回值是NSString指针类型,因此不能只写(NSString),而是要(NSString *)。
在代码中调用getPrice方法:
NSLog(@"Book price is %@", [book getPrice]);
这里补充说一下NSLog函数的使用,第一个参数是字符串。%@表示,打印后面参数,其中%表示打印的占位符,后面的@表示打印的数据类型是ObjC对象类型。我后面的参数正式ObjC对象类型,是一个NSString类型。
最后,说一下ObjC声明方法比较令人费解的地方,就是传入方法参数的语法了。下面改写printName方法说明这点。假设要传入一个书名,并打印该书名称。
那么方法声明:
#import <Cocoa/Cocoa.h>
@interface Book : NSObject {
}
-(void) printName:(NSString *)bookName;
-(NSString *) getPrice;
@end
实现:
#import "Book.h"
@implementation Book
-(void) printName:(NSString *)bookName{
NSLog(@"书名:%@",bookName);
}-(NSString *) getPrice{
return @"$17";
}@end
调用:
Book *book=[[Book alloc] init];
[book printName:@"Objective-C入门手册"];
这里需要说明ObjC的一个独特的机制——方法调度程序。ObjC认为对象的方法调用,就是向对象发送消息。消息的内容呢?就是传递的参数。
ObjC在程序运行时,对发送给对象的消息(也就是调用对象方法),做动态的判断,查找该对象是否实现了该方法(包括它的超类和类别中,次序是先类别,然后该类对象本身,然后超类),如果没有找到实现的方法,就会报运行时异常。比如:
2011-05-16 21:30:33.673 SimpleObjcDemo[18967:a0f] -[Book aaa]: unrecognized selector sent to instance 0×100110400
值得注意的是,ObjC在编译时,并不会指出对不存在方法调用的错误。在Xcode中仅仅是警告一下:
如果是多个传入参数呢?比如打印书名需要书的名称和作者名称,方法的声明:
#import <Cocoa/Cocoa.h>
@interface Book : NSObject {
}
-(void) printName:(NSString *)bookName authorName:(NSString *)author;
-(NSString *) getPrice;
@end
方法的实现:
#import "Book.h"
@implementation Book
-(void) printName:(NSString *)bookName authorName:(NSString *)author{
NSLog(@"书名:%@, 作者:%@",bookName,author);
}-(NSString *) getPrice{
return @"$17";
}@end
调用:
Book *book=[[Book alloc] init];
[book printName:@"Objective-C入门手册" authorName:@"Marshal"];
方法的覆盖(重写)
覆盖,override,也有翻译为重写的。覆盖是面向对象语言必备的特性,通过该技术来实现多态。覆盖就是子类实现了父类相同的方法。这样,无论通过父类还是通过子类的类型调用,都将调用子类实现的方法。
首先,我编写了一个名为MusicBook的类,继承自上文实现的Book类。头文件:
#import <Cocoa/Cocoa.h>
#import "Book.h"@interface MusicBook : Book {
}
@end
实现文件:
#import "MusicBook.h"
@implementation MusicBook
@end
什么也没做,只是简单的继承。如果这时调用:
MusicBook *musicBook=[[MusicBook alloc] init];
NSLog(@"Book price is %@",[musicBook getPrice]);
这样执行的是超类的getPrice方法。下面开始覆盖。覆盖不需要在子类的头文件中再声明方法了。直接在m文件中实现方法就是覆盖:
#import "MusicBook.h"
@implementation MusicBook
-(NSString *) getPrice{
return @"$0.99";
}@end
这时再调用,发现执行的是子类的方法了。为了验证多态,在调用时使用超累类型,效果是一样的:
Book *musicBook=[[MusicBook alloc] init];
NSLog(@"Book price is %@",[musicBook getPrice]);
协议(接口)
面向对象编程语言的语法中,一般都支持接口的概念。比如Java中的interface。但是在ObjC中,interface关键字被类的声明使用了。对应的关键字是@protocol。叫协议也有道理。基于接口的编程又叫基于契约编程嘛。
创建一个协议,和创建类略有不同:
比如我想创建一个播放音乐的接口,PlayMusic,用于MusicBook播放声音:
#import <Cocoa/Cocoa.h>
@protocol PlayMusic
@end
空的协议没什么用。在里面声明方法:
#import <Cocoa/Cocoa.h>
@protocol PlayMusic
-(void)play;
@end
使用协议,比如要让MusicBook类实现协议,需要在MusicBook头文件中:
#import <Cocoa/Cocoa.h>
#import "Book.h"
#import "PlayMusic.h"@interface MusicBook : Book <PlayMusic>{
}
@end
然后在MusicBook类中实现该方法:
#import "MusicBook.h"
@implementation MusicBook
-(NSString *) getPrice{
return @"$0.99";
}-(void)play{
NSLog(@"play music.");
}@end
使用该方法,可以通过MusicBook类型调用:
MusicBook *musicBook=[[MusicBook alloc] init];
NSLog(@"Book price is %@",[musicBook getPrice]);
[musicBook play];
使用super关键字
在面向对象语言里,如果使用了继承,那么当想调用超类的方法时,需要用到super关键字。ObjC在这点和Java很相似。
比如在MusicBook类中调用超类方法:
#import "MusicBook.h"
@implementation MusicBook
-(NSString *) getPrice{
NSLog(@"super getPrice: %@",[super getPrice]);
return @"$0.99";
}
使用self关键字
ObjC的self关键字,相当于Java中的this指针。可这样使用:
#import "MusicBook.h"
@implementation MusicBook
-(NSString *) getPrice{
NSLog(@"super getPrice: %@",[super getPrice]);
[self play];
return @"$0.99";
}-(void)play{
NSLog(@"play music.");
}
面向对象中的复合
上面提到了继承,在自然语义中是is a的意思,比如Music Book is a Book。在语义中除了is a,还有一种是has a,比如Book has an Author。图书有一个作者。这在面向对象中是复合的概念。Java中的复合,比如:
public class Book{
private Author author;
}
这里Author类是隶属于Book的。
这种表达,在Objc中,要用以下的写法。首先,咱们要创建一个Author类。头文件:
#import <Cocoa/Cocoa.h>
@interface Author : NSObject {
NSString *name;
}@end
这里,在{}括号内,为Author类创建了个实例属性,作者姓名。实际上这个属性相对于Author类,就已经是复合关系了。即Author has a name。
简化起见,实现Author的m文件,没做任何事情:
#import "Author.h"
@implementation Author
@end
那么下面,我们让Book类和Author建立复合关系,也是在头文件中:
#import <Cocoa/Cocoa.h>
#import "Author.h"@interface Book : NSObject {
Author *author;
}-(void) printName:(NSString *)bookName;
-(NSString *) getPrice;
@end
在对象内部使用实例变量
现在,author变量,从对象以外是无法访问的,只能在对象内部被访问。author变量默认是nil,也就是空。需要为该变量赋值。可以通过覆盖init方法(从NSObjcet继承来的初始方法)来为author变量赋值:
#import "Book.h"
#import "Author.h"@implementation Book
-(id)init{
self=[super init];
if (self) {
author=[[Author alloc] init];
}
return self;
}
这里面先不用管init方法为什么这么写,只考虑赋值一行语句即可。
那么可以在其他地方访问该属性了:
-(void) printName:(NSString *)bookName {
NSLog(@"书名:%@, 作者:%@",bookName,author);
}
当然,打印出来的是一个内存地址字符串,还不是友好提示:
2011-05-17 12:13:53.108 SimpleObjcDemo[20018:a0f] print Book instance <Book: 0×100110420>
但是,我们已经能通过对象内部访问到实例变量了。
让实例变量能被外界访问
如果用过Java,就应该了解它的setter和getter,比如这样:
public class Book{
private Author author;
public void setAuthor(Author author){
this.author=author;
}
public Author getAuthor(){
return this.author;
}
}
那么,ObjC中对应的语法是什么呢?首先,要声明使用@property,在头文件中:
#import <Cocoa/Cocoa.h>
#import "Author.h"@interface Book : NSObject {
Author *author;
}@property (nonatomic,retain) Author *author;
这里nonatomic,表示不在多线程环境下使用,因此可提高一点儿性能,这对台式机可能没有多大影响,但是对iOS设备上还是有意义的。retain是保持该变量,在有关内存管理中再详细讲解。
在实现文件中:
#import "Book.h"
#import "Author.h"@implementation Book
@synthesize author;
这样就可以这么调用了:
Book *book=[[Book alloc] init];
[book printName:@"Objective-C入门手册"];
NSLog(@"book author: %@",book.author);
或者,赋值:
book.author=[[Author alloc]init];