Objective-C面向对象

面向对象的实现

封装

在Objective-C中通过访问修饰符实现数据的封装,分别为@public、@protected、@private和@package,不同的访问修饰符对外界有不同的可见性。

  • @public:在类内和类外都可以被使用,且可以被继承
  • @protected:在类内可以使用,在类外不能使用,可以被继承;默认为@protect类型,在类外通过点运算符和指向运算符都不能访问
  • @private:在类内可以使用,在类外不能使用,并且不能被继承
  • @package:框架权限,在框架内相当于受保护,在框架外相当于私有

Objective-C中的方法是没有访问修饰符的,方法默认可以被外界调用。如果不想被外界调用,只要删除方法的声明,就对外界不可见了。

继承

Objective-C中和Java一样,不支持多继承。Java中使用接口实现多继承,而Objective-C中使用协议实现多继承。

1
2
3
4
5
6
// 简单单继承
@interface MyClass : NSObject
@end
@interface MySubClass : MyClass
@end

Objective-C中没有多继承,多继承通过协议的方式实现。

多态

多态一般通过方法的重写(Override)和方法重载(Overload)实现。重载发生在子类对父类函数继承的时候。而在Objective-C中不支持方法的重载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@interface Printer : NSObject
- (void)print;
@end
@implementation Printer
- (void)print {
NSLog(@"This is printer");
}
@end
@interface ColorPrinter : Printer
- (void)print;
@end
@implementation ColorPrinter
- (void)print {
NSLog(@"This is color printer");
}
@end
@interface BlackPrinter : Printer
- (void)print;
@end
@implementation BlackPrinter
- (void)print {
[super print]; // 调用父类方法
NSLog(@"This is black printer");
}
@end

如果一个类引用了子类的头文件,那么其父类的头文件也被引用:

1
2
Printer *printer = [[ColorPrinter alloc] init];
[printer print];

类似于Java的接口用法。

协议(Protocol)

协议的定义

协议是一组没有实现的方法列表,任何的类均可采纳协议并具体实现这组方法。Objective-C在NeXT时期曾经试图引入多重继承的概念,但由于协议的出现而没有实现。

协议类似于Java与C#语言中的接口(Interface)。在Objective-C中,有两种定义协议的方式:由编译器保证的“正式协议”,以及为特定目的设定的“非正式协议”。

非正式协议为一个可以选择性实现的一系列方法列表。非正式协议虽名为协议,但实际上是挂于NSObject上的未实现分类(Unimplemented Category)的一种称谓。Objetive-C语言机制上并没有非正式协议这种东西,OSX 10.6版本之后由于引入@optional关键字,使得正式协议已具备同样的能力,所以非正式协议已经被废弃不再使用。

正式协议类似于接口,它是一系列方法的列表,任何类都可以声明自身实现了某个协议。在Objective-C 2.0之前,一个类必须实现它声明匹配的协议中的所有方法,否则编译器会报告错误,表明这个类没有实现它声明匹配的协议中的全部方法。Objective-C 2.0版本允许标记协议中某些方法为可选的(Optional),这样编译器就不会强制实现这些可选的方法。(类似implements和extends区别)

协议经常应用于Cocoa中的委托及事件触发。例如文本框类通常会包括一个委托(delegate)对象,该对象可以实现一个协议,该协议中可能包含一个实现文字输入的自动完成方法。若这个委托对象实现了这个方法,那么文本框类就会在适当的时候触发自动完成事件,并调用这个方法用于自动完成功能。

Objective-C中协议的概念与Java中接口的概念并不完全相同,即一个类可以在不声明它匹配某个协议的情况下,实现这个协议所包含的方法,也即实质上匹配这个协议,而这种差别对外部代码而言是不可见的。正式协议的声明不提供实现,它只是简单地表明匹配该协议的类实现了该协议的方法,保证调用端可以安全调用方法。

协议的语法实现

协议以关键字@protocol作为区块起始,@end结束,中间为方法列表。

1
2
3
4
@protocol Locking
- (void)lock;
- (void)unlock;
@end

这是一个协议的例子,多线程编程中经常要确保一份共享资源同时只有一个线程可以使用,会在使用前给该资源挂上锁 ,以上即为一个表明有”锁”的概念的协议,协议中有两个方法,只有名称但尚未实现。
下面的SomeClass宣称他采纳了Locking协议:

1
2
@interface SomeClass : SomeSuperClass <Locking>
@end

一旦SomeClass表明他采纳了Locking协议,SomeClass就有义务实现Locking协议中的两个方法。

1
2
3
4
5
6
7
8
@implementation SomeClass
- (void)lock {
// 实现lock方法...
}
- (void)unlock {
// 实现unlock方法...
}
@end

由于SomeClass已经确实遵从了Locking协议,因此调用端可以安全的发送lock或unlock消息给SomeClass实体变量,不需担心他没有办法回应消息。

插件是另一个使用抽象定义的例子,可以在不关心插件的实现的情况下定义其希望的行为。

快速枚举

比起利用NSEnumerator对象或在集合中依次枚举,Objective-C 2.0提供了快速枚举的语法。在Objective-C 2.0中,以下循环的功能是相等的,但性能特性不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用NSEnumerator
NSEnumerator *enumerator = [thePeople objectEnumerator];
Person *p;
while ( (p = [enumerator nextObject]) != nil ) {
NSLog(@"%@ is %i years old.", [p name], [p age]);
}
// 使用依次枚举
for ( int i = 0; i < [thePeople count]; i++ ) {
Person *p = [thePeople objectAtIndex:i];
NSLog(@"%@ is %i years old.", [p name], [p age]);
}
// 使用快速枚举
for (Person *p in thePeople) {
NSLog(@"%@ is %i years old.", [p name], [p age]);
}

快速枚举可以比标准枚举产生更有效的代码,由于枚举所调用的方法被使用NSFastEnumeration协议中提供的指针算术运算所代替了。

动态类型

类似于Smalltalk,Objective-C具备动态类型:即消息可以发送给任何对象实体,无论该对象实体的公开接口中有没有对应的方法。对比于C++这种静态类型的语言,编译器会挡下对(void*)指针调用方法的行为。但在Objective-C中,你可以对id发送任何消息(id很像void*,但是被严格限制只能使用在对象上),编译器仅会发出”该对象可能无法回应消息”的警告,程序可以通过编译。而实际发生的事则取决于运行期该对象的真正形态,若该对象的确可以回应消息,则依旧运行对应的方法。

一个对象收到消息之后,它有三种处理消息的可能手段,第一是回应该消息并运行方法;若无法回应,则可以转发消息给其他对象。若以上两者均无法实现,就要处理无法回应而抛出的例外。只要进行三者之其一,该消息就算完成任务而被丢弃。若对nil(空对象指针)发送消息,该消息通常会被忽略。这取决于编译器选项,也可能会抛出例外。

虽然Objective-C具备动态类型的能力,但编译期的静态类型检查依旧可以应用到变量上。以下三种声明在运行时效力是完全相同的,但是三种声明提供了一个比一个更明显的类型信息,附加的类型信息让编译器在编译时可以检查变量类型,并对类型不符的变量提出警告。

1
2
3
4
5
6
7
// id形态表示参数"foo"可以是任何类的实例
- setMyValue:(id) foo;
// id<aProtocol>表示"foo"可以是任何类的实例,但必须采纳"aProtocol"协议
- setMyValue:(id <aProtocol>) foo;
// 该声明表示"foo"必须是"NSNumber"的实例
- setMyValue:(NSNumber*) foo;

动态类型是一种强大的特性。在缺少泛型的静态类型语言(如Java 5以前的版本)中实现容器类时,程序员需要写一种针对通用类型对象的容器类,然后在通用类型和实际类型中不停的强制类型转换。无论如何,类型转换会破坏静态类型,例如写入一个整数而将其读取为字符串,就会产生运行时错误。这样的问题被泛型解决,但容器类需要其内容对象的类型一致,而对于动态类型语言则完全没有这方面的问题。

消息转发

Objective-C允许对一个对象发送消息,不管它是否能够响应之。除了响应或丢弃消息以外,对象也可以将消息转发到可以响应该消息的对象。转发可以用于简化特定的设计模式,例如观测器模式或代理模式。

Objective-C运行时在Object中定义了一对消息转发方法:

1
2
3
4
5
6
7
/* 发送方法 */
- (retval_t) forward:(SEL) sel :(arglist_t) args; // with GCC
- (id) forward:(SEL) sel :(marg_list) args; // with NeXT/Apple systems
/* 响应方法 */
- (retval_t) performv:(SEL) sel :(arglist_t) args; // with GCC
- (id) performv:(SEL) sel :(marg_list) args; // with NeXT/Apple systems

希望实现转发的对象只需用新的方法覆盖以上方法来定义其转发行为,而无需重写响应方法performv::。该方法只是单纯的对响应对象发送消息并传递参数。其中SEL类型是Objective-C中消息的类型。

以下代码演示了转发的基本概念:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// Forwarder.h
#import <objc/Object.h>
@interface Forwarder : Object
{
id recipient; //该对象是我们希望转发到的对象。
}
@property (assign, nonatomic) id recipient;
@end
// Forwarder.m
#import "Forwarder.h"
@implementation Forwarder
@synthesize recipient;
- (retval_t) forward: (SEL) sel : (arglist_t) args
{
/*
*检查转发对象是否响应该消息。
*若转发对象不响应该消息,则不会转发,而产生一个错误。
*/
if([recipient respondsTo:sel])
return [recipient performv: sel : args];
else
return [self error:"Recipient does not respond"];
}
@end
// Recipient.h
#import <objc/Object.h>
@interface Recipient : Object // A simple Recipient object.
- (id) hello;
@end
// Recipient.m
#import "Recipient.h"
@implementation Recipient
- (id) hello
{
printf("Recipient says hello!\n");
return self;
}
@end
// main.m
#import "Forwarder.h"
#import "Recipient.h"
int main(void)
{
Forwarder *forwarder = [Forwarder new];
Recipient *recipient = [Recipient new];
forwarder.recipient = recipient; //Set the recipient.
/*
*转发者不响应hello消息!该消息将被转发到转发对象。
*(若转发对象响应该消息)
*/
[forwarder hello];
return 0;
}

利用GCC编译时,编译器报告:

1
2
3
$ gcc -x objective-c -Wno-import Forwarder.m Recipient.m main.m -lobjc
main.m: In function `main':
main.m:12: warning: `Forwarder' does not respond to `hello'

如前文所提到的,编译器报告Forwarder类不响应hello消息。在这种情况下,由于实现了转发,可以忽略这个警告。 运行该程序产生如下输出:

1
2
$ ./a.out
Recipient says hello!

类别 (Category)

在Objective-C的设计中,一个主要的考虑即为大型代码框架的维护。结构化编程的经验显示,改进代码的一种主要方法即为将其分解为更小的片段。Objective-C借用并扩展了Smalltalk实现中的”分类”概念,用以帮助达到分解代码的目的。

一个分类可以将方法的实现分解进一系列分离的文件。程序员可以将一组相关的方法放进一个分类,使程序更具可读性。举例来讲,可以在字符串类中增加一个名为”拼写检查”的分类,并将拼写检查的相关代码放进这个分类中。

进一步的,分类中的方法是在运行时被加入类中的,这一特性允许程序员向现存的类中增加方法,而无需持有原有的代码,或是重新编译原有的类。例如若系统提供的字符串类的实现中不包含拼写检查的功能,可以增加这样的功能而无需更改原有的字符串类的代码。

在运行时,分类中的方法与类原有的方法并无区别,其代码可以访问包括私有类成员变量在内的所有成员变量。若分类声明了与类中原有方法同名的函数,则分类中的方法会被调用。因此分类不仅可以增加类的方法,也可以代替原有的方法。这个特性可以用于修正原有代码中的错误,更可以从根本上改变程序中原有类的行为。若两个分类中的方法同名,则被调用的方法是不可预测的。

以下展示了一个使用分类的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// Integer.h
#import <objc/Object.h>
@interface Integer : Object
{
@private
int integer;
}
@property (assign, nonatomic) integer;
@end
// Integer.m
#import "Integer.h"
@implementation Integer
@synthesize integer;
@end
// Arithmetic.h
#import "Integer.h"
@interface Integer(Arithmetic)
- (id) add: (Integer *) addend;
- (id) sub: (Integer *) subtrahend;
@end
// Arithmetic.m
#import "Arithmetic.h"
@implementation Integer(Arithmetic)
- (id) add: (Integer *) addend
{
self.integer = self.integer + addend.integer;
return self;
}
- (id) sub: (Integer *) subtrahend
{
self.integer = self.integer - subtrahend.integer;
return self;
}
@end
// Display.h
#import "Integer.h"
@interface Integer(Display)
- (id) showstars;
- (id) showint;
@end
// Display.m
#import "Display.h"
@implementation Integer(Display)
- (id) showstars
{
int i, x = self.integer;
for(i=0; i < x; i++)
printf("*");
printf("\n");
return self;
}
- (id) showint
{
printf("%d\n", self.integer);
return self;
}
@end
// main.m
#import "Integer.h"
#import "Arithmetic.h"
#import "Display.h"
int main(void)
{
Integer *num1 = [Integer new], *num2 = [Integer new];
int x;
printf("Enter an integer: ");
scanf("%d", &x);
num1.integer = x;
[num1 showstars];
printf("Enter an integer: ");
scanf("%d", &x);
num2.integer = x;
[num2 showstars];
[num1 add:num2];
[num1 showint];
return 0;
}

这里我们创建了一个Integer类,其本身只定义了integer属性,然后增加了两个分类Arithmetic与Display以扩展类的功能。虽然分类可以访问类的私有成员,但通常利用属性的访问方法来访问是一种更好的做法,可以使得分类与原有类更加独立。这是分类的一种典型应用。另外的应用是利用分类来替换原有类中的方法,虽然用分类而不是继承来替换方法不是一种好的做法。

利用以下命令来编译:

1
gcc -x objective-c main.m Integer.m Arithmetic.m Display.m -lobjc

在编译时间,可以利用省略#import "Arithmetic.h"[num1 add:num2]命令,以及Arithmetic.m文件来实验。程序仍然可以运行,这表明了允许动态的、按需的加载分类;若不需要某一分类提供的功能,只需要不参与编译即可。

垃圾收集

Objective-C 2.0提供了一个可选的垃圾收集器。在向后兼容模式中,Objective-C运行时会将引用计数操作,例如retainrelease变为无操作。当垃圾收集启用时,所有的对象都是收集器的工作对象。普通的C指针可以以_strong修饰,标记指针指向的对象仍在使用中。被标记为_weak的指针不被计入收集器的计数中,并在对象被回收时改写为nil

iOS上的Objective-C 2.0实现中不包含垃圾收集器。垃圾收集器运行在一个低优先级的后台线程中,并可以在用户动作时暂停,从而保持良好的用户体验。