object-c 第二章

第二章 Objective-C 与面向对象

Objective-C 是一种面向对象的语言。本章带你了解面向对象的含义、Objective-C 如何实现面向对象,以及如何用Objective-C程序实现对象。

毫无疑问,面向对象编程是新鲜事物,19世纪80年代开始流行。
它保留了大多数共通的编程规范,而且特别适用开发公开某类接口给用户的应用,因为人们习惯就他们所见所接触的对象来思考。

Objective-C 使用一种非常独特的方式去实现所谓的面向对象。这带来了一系列的好处,让开发者以较少的代价去理解关于面向对象如何工作的细节以及CoCoa 对你代码的期待。

###面向对象编程

在任何平台开发任何应用,你都会将代码组织成不同的部分,每一个部分负责程序不同的功能块。即程序每一个任务的逻辑应该避免相互耦合。

比如,通过网络和数据库交互的代码就不应该和展示结果给用户的代码混在一起。相反,你应该把代码分成数据库处理层和数据展示层。如果这两层需要交互,那么你就应该清晰地定义接口。

这样做使你的代码更易写,易维护和易调试的,代码也容易模块化,因为在不影响程序其他部分的情况下,添加代码(或替换)就变得很直接。

除了根据目的分离逻辑外,程序的数据也应该分离。比如,用于展示信息的相关数据(例如字体,颜色,屏幕的位置)不能被负责和数据库交互的代码所修改。另外,你的逻辑应该能够在假设所要处理的数据不会被程序其他部分修改的前提下正常工作。

面向对象语言通过引进对象的概念推崇和强制这种分离。

###对象

对象是由数据块和定义在数据块的操作组成的。只有对象内部的代码才能修改数据,但对象之间可以通过共享数据进行通信。

这种隐藏数据的方式称为封装,并且可确保改变一个对象的数据仅能通过对象的提供的函数进行。

同一个对象的多个副本能够同一时间存在。每个副本称为对象的一个实例。创建实例的模版则称为类。当你编写面向对象的代码时,你要编写类,然后通过类创建一个或多个实例。

一个对象里面的数据称为实例变量,而其函数则称为方法。

在大多数面向对象的语言里,一个方法既可能定义成实例方法,也可能是类方法。Objective-C 里所谓的类方法,通常用于创建类的实例。比如,类NSData中一个叫dataWithContentsOfFile:的类方法,可以加载文件并返回NSData对象。

###继承

面向对象语言允许通过另一个类定义子类。子类和父类相同,但可以添加额外的方法和实例变量。

子类创建了一个版本更加具体的类。例如,你定义了一个Server 类,用于处理诸如接收网络连接的任务,然后分别创建了FTPServer 和 HttpServer 子类,处理更具体的连接任务。

在Objective-C 中,一个类只有一个父类(不同于C++, 支持继承多个父类)。

子类可以覆盖父类的方法。即可以编写一个替换父类一个或多个方法的子类–确切地说,使用Cocoa编程的主要工作都涉及到替换某些方法。

接口和实现

每个对象都有访问域:私有域和公开域。
一个类的公开域就是所谓的接口。它列出了其他类所有能够访问的方法(在Objective-C中,没有公开实例变量一说,不过可以用属性代替,虽然两者作用一样,但属性可以更好地控制其他对象访问本对象的数据)

一个类私有域就是所谓的类实现。它包含为类编写的代码,也包含了任何属于该类的私有变量。

当你使用一个对象,仅仅是使用对象的接口。即每个对象在其他对象能够对本对象数据做什么和本对象函数能够做什么之间是严格区分的。

在Objective-C,一个类的接口的声明和实现是分开的,并且放在不同的文件。包含了类接口的文件叫头文件(历史原因才这么叫),而包含了类实现的文件则是实现文件。头文件以.h作为扩展名,实现文件则以.m作为扩展名。

Objective-C 的类接口声明类似如下:

@interface MyObject : NSObject {
    [instance variables]
}
    [method declarations]

@end

@interface一行定义了类的名字,同时指明了其父类。在花括号里面,可以声明实例变量-这是可选的,同样你可以在实现文件里面声明这些变量;参考“类扩展”这节。

对应的实现文件类似如下:

@implementation MyObject
    [method implementations]
@end

方法

方法即类的函数。和其他函数一样,可以带参数和有返回值。

之前说过,方法分为实例方法和类方法。实例方法属于一个类的实例,只能访问实例变量。类方法则不能访问实例变量,因为其不属于实例对象。

声明方法:

- (void) launchPlane;

以上声明了一个不带参数,没有返回值名为launchPlane的方法。方法名前面的 表示它是个实例方法,+ 表示类方法。

Objective-C比较有趣和独特的一个地方就是,它的方法名是分段写的。举例会比较容易解释,

比如,下面是带一个参数的方法:

- (void) launchPlane: (NSString*) planeName;

方法里面的参数planeName 是一个指向 NSString 对象的指针。

对比下面带两个参数的方法:

-(void) launchPlane: (NSString*) planeName fuelCapacity: (int) litresOfFuel;

这个方法带有一个NSString 的指针planeName和一个int 的 litresOfFuel 参数。此时参数名是并入方法名的。

调用一个对象的方法的语法同样和其他语言很不同。假设一个planeLauncher 对象调用方法:

[planeLauncher launchPlane];

可以看到,调用方法的对象在方括号的作左边,而方法名则在右边。对多个参数的方法的调用,语法差不多,几乎和声明是一样:

[planeLauncher launchPlane: @"Boeing 747-300" fuelCapacity: 183380];

这种设计让代码从左到右读起来更能体验语句的意义。例如,读一下上面例子(忽略英语语法),你几乎可以当它是一条命令。

@interface 处声明一个类的方法。举例如下:

@interface SomeObject : NSObject
- (void) launchPlane: (NSSTring *) planeName;
- (int) numberOfPlanesInTheAir;
@end

消息

Objective-C是消息传递语言,很像Ruby。这个特性使它区别于像C++这样使用早期绑定方法再调用的语言。

消息传递意味着一个对象接收一个方法调用时,会在运行时查询再决定运行何种代码。相反,C++ 和其他语言在编译期处理。

当对象接收到方法调用时,Objective-C会在运行时查询对象所实现的方法列表。如果找到该方法匹配的消息,就运行该方法的代码。否则,就开始递归查找父类的方法列表,直到返回找到或不能查找为止。如果找不到,就抛出异常,程序终止运行。也可以不这样做--即捕获异常并处理。XCode就是这么做的:如果有异常,Xcode 就提示错误信息,并允许用户忽略或终止程序。

这意味着对象可以用它所接收的消息去做一些有意思的事。例如,(to do)。还可以写自己的逻辑处理没有方法匹配的消息的情况(通常用在数据库的处理代码)

因为方法声明和实现代码是分离的,Objective-C 比其它编译语言更具动态性。Cocoa 的威力就是来自这种动态性。在日常工作中,方法和消息的区别并不是很重要。不过,越是明白Objective-C其中的工作原理就越好。

属性

面向对象编程中,一个对象直接访问另一个对象的数据是糟糕的实践。它破坏了封装,使得一个对象的代码依赖一个对象的存储数据。

为了访问和改变另一个对象的变量,可以使用settergetter 实例方法。后者返回变量当前值,前者则改变变量的值。

这两个方法命名需要遵守一些约定。例如,有个实例变量 planeName, setter 方法命名为 setPlaneName: 而getter 方法则命名为 planeName.

由于实例变量不允许被其他对象直接访问,如果想被其他对象访问,每个实例变量都要包括getter 和 setter 方法。这样就需要手动写大量getter 和 setter 方法。自从2007 年发布了Objective-C 2.0 后,它包含了一些新特性,移除了这种手写方法,同时使得实例变量易于暴露给其他类。

当声明一个可以由其他类的实例访问的变量时,可以这么做:

@interface SomeClass : NSObject
@property (strong, nonatomic) NSObject* myProperty;
@end

属性声明在 @interface 之间,关键字为 @property,其后的括号里面则是该属性的特性列表,接着就是属性类型和名字。

属性的特性描述属性对其他对象(或编译器)行为,如下:

strong
表示属性强引用。参考 “对象表”。一个对象使用 `strong` 和 `weak` 的属性,可以控制被引用的属性是否保留在内存中。

weak
表示属性弱引用。当引用该属性的对象释放时,属性会自动置为nil。

assign
这个属性的setter 方法仅进行简单的赋值,没内存管理。

copy
这个属性的setter 方法会复制所赋对象,创建原对象的副本。

readwrite
这个属性会产生setter 和 getter 方法(默认设置,当覆盖父类的属性时需要显式设置)

readonly
属性不会产生setter 方法,使属性对其他类来说只读(所属类依然可以修改)。

nonatomic
改变变量值时,属性的setter 和 getter 方法不会加锁,使之线程安全。

当你声明属性时,编译器自动产生实例变量存储属性值,还有自动产生getter 和 setter 方法。实例名默认和属性命一致。如果想要实例变量有不同的调用,可以在属性前手动添加@synthesize指令:

@implementation MyClass
@synthesize myProperty = _myCustomVariableName;
// the rest of class code goes here
@end

可以用 @dynamic 指令告诉编译器不要合成属性和方法。
但你需要自己实现 getter 和 setter 方法:

@implementation MyClass
@dymatic myProperty;

- (int) myProperty{
    // this is the getter method for this property

    return 123;
}

- (void) setMyProperty: (int) newValue {
    // this is the setter method for this property
}

@end

协议

协议即要求类去实现的方法列表。它用于表示类的某一项具体能力,比如复制能力,序列化和反序列化,或者作为其他类的数据源。

声明协议的语法是:

@protocol SomeProtocol
    [method declarations]
@end

一个类要实现协议可以这样:

@interface SomeObject : NSObject <SomeProtocol>

@end

SomeObject 类遵从 SomeProtocol 协议。这是和子类继承方法或覆盖某些方法是不一样的。而且,协议里面指定的必选方法是必须由类实现的。

Cocoa 使用协议来和从未见过的类协作比较频繁;如果一个类实现了协议,其可保证Cocoa任何任务需要协议实现的方法都能实现。

使用对象时,你一般会显式指定对象类型:

NSSTring* aString;

不过,有些情况你可能不知道类型。为了确切知道对象是否实现协议,可以使用如下语法:

id <SomeProtocol> someObjectConformingToAProtocol;

运行时,someObjectConformingToAProtocol 对象可能时任何类,但一定是实现了协议。

类扩展

Objectiv-C 的类可以添加额外的实例变量和方法,这适用于自定义类和系统类。

所以,像给NSString 这样的系统类添加方法和实例变量也是可能的,一旦进行类扩展,类的所有实例,都会有所添加的方法。

你代码的任何地方都可以声明和实现类扩展。

需要类扩展的理由的有:

  1. 你想要为已存在类加入新的行为或逻辑。
    这种情况很少出现,不过某些情况你需要Cocoa 加入一些功能,比如,你想给NSString 类加入一个置换字母的方法,就可以这么做。

  2. 你想把自己的类单独分离成组件。
    这种情况对开发者来说日益普遍,因为它允许你只把公共的实属性和方法放到你的头文件,而在其他任何地方声明私有的属性和方法。

类扩展的语法如下:

@interface SomeClass(){
    [additional instance variables]
}

[additional instance or class method declarations]
@end

看上去很熟悉-几乎和类接口一样。唯一的改变是用()代替了父类(之所以没有父类是因为类扩展不允许改变父类)

添加多少个类扩展是没有限制的。只要在你项目某处的@implementation 块实现了你所添加的方法即可。

为了帮你组织多个扩展,可以对它们进行命名处理。即目录,代码首行可以这么写:

@interface SomeClass (SomeCategory)

剩下是一样的了–唯一不同的就是那个目录名。

类扩展允许你最小程度公开一些东西给其他类。看看下面的例子,头文件是这样的:

@interface SomeClass
-(void) doSomethingInteresting; // public, other classes can call this method

@end

对应的实现文件:

@import "SomeClass.h"

@interface SomeClass(){
    NSString* privateInstanceVariable; // Only visible to this class
}

// no other class can see this method because it's not in the header file, and therefore private
- (void) doSomethingPrivate;

@end

@implementation SomeClass
    [method implemenation for both doSomethingInteresting and doSomethingPrivate]

@end

这么一来,你可以保持你头文件的干净同时仍然可以添加你想要的东西。

模块

模块是一种链接文件和库到工程的新方法。想了解模块如何工作及其带来的好处,就要回看Objective-C 的历史和#import语句了。

当你要引入一个文件时,一般需要这么写代码:

#import "someFile.h"

或假如涉及要引入某框架某文件时:

#import <SomeLibrary/SomeFile.h>

因为Objective-C 是C语言的超集,#import 语句是C#include语句的细化。include 语句非常简单,编译期间会将include 文件里所搜索到所有代码复制过来。因此有时会产生重大问题。例如,试想如果有两个头文件:SomeFileA.h 和 SomeFileB.h;前者包含后者,后者也要包含前者,导致循环引用,得编译器就会混淆。C 程序员不得不写保护性代码避免这种情况的发生。

而使用import,则不必担心这种问题或者去写头文件保护性语句了。但是,import 本质还只是复制-黏贴动作,很容易使编译在一大堆小而危险的问题种变慢。(比如所引入的一个文件使你某处声明的代码无效)

模块 就是为了解决此类问题。现在不再是 复制-黏贴源代码了,而是按需导入到你代码。使用模块,代码一般比使用 #include#import编译更快,更安全。

回到前面导入一个框架某文件的例子:

#import <SomeLibrary/SomeFile.h>

而使用模块,则可以改成这样:

@import SomeLibrary;

(To Do)

内存管理

自从电脑面世后,程序员就要面对存储的问题。简单地说,不可能保留每块单独的数据,即你用完了内存,就要记得归还给系统。否则,机器会耗尽内存而无法工作。

所以下面就要讲讲Objective-C 的内存管理,它是通过所谓的引用计数器进行的。

引用计数器

每个对象都会保存其引用其他对象的数目的变量。当创建一个对象时,引用计数的值开始就是1,并且可增可减。当其值为0时,对象就会被释放,归还内存给系统。

当一个对象需要保留另一个对象引用时,就会给对象发送 retain消息,此时引用计数增1。当一个对象不在需要保留另一个对象时,会发送 release 消息,引用计数减1.

这么做的好处是,内存管理变的相当容易明白,不用受垃圾回收的偶尔减速,也相对安全–而不需小心翼翼跟踪所释放的内存块,当对象不再被引用时,它会自动归还内存。

不好的地方是,程序员需要手动完成这些事情。如果你忘记了你要释放的对象(导致内存泄漏,永远不能释放),或者你释放对象的次数多于其引用的次数(释放一个本来就已被释放的对象会导致崩溃,有时不会–或导致不可预料的情况)。

自动引用计数器

随着OS X Lion(10.7) 和 iOS 5 的发布,苹果公司引入了全新的基于引用计数的机制,目标是让它既有之前的优点兼具的不会有之前的缺点。这种机制称为 自动引用计数器,简称ARC。

除了不需要手动调用 retain和release外–编译器自动调用,ARC 和手动引用计数是一样的。编译器内置源码分析器,可以确定一个对象何时何处开始和停止使用另一个对象,基于这些信息,编译期会适时插入 retain和release方法。

这样程序员可放心使用内存而不用担心内存泄漏--实际可以当作一个无缝的垃圾回收。

对象图

任何内存管理机制都有一个对象保留周期的问题。为了搞明白这个问题,考虑ARC在什么情况下释放内存是值得的。

引用计数机制会在对象的引用数为0时才释放对象的内存。ARC通过跟踪一个对象何时引用另一个对象和引用何时结束来管理引用计数的。

问题是,当两个或两个以上的对象之间存在相互引用,除此外没被程序其他地方引用,这些对象就无法被app 触及,因为循环引用的对象,根本不会释放内存。

解决这个问题,要用到两种引用类型:强引用和弱引用。

  • 强引用:可使被引用对象保留在内存。
  • 弱引用:虽然仍然指向被引用对象,但不会像强引用那样进行引用计数,即ARC 不会增加引用值。
  • 弱引用一个额外的好处是,当被引用对象释放时,会自动置成nil。即弱引用是安全的,因为它不是有效对象就是nil,从不引用未分配的内存。由于Objective-C 允许向对象发送nil 消息,因此不会导致程序无法运行。

声明对对象的强引用,可在声明属性时,用strong特性,弱引用则用weak特性。

NSObject 生命周期

Cocoa 里无论何种类型的对象都遵循一样模式。下面以对象生命周期作为本章的总结。

以下方法定义在Cocoa 的根类NSObject。

分配内存和初始化

对象只有在程序为其分配内存后才能存在。要做到这一点需通过发送alloc 消息给类。

alloc 方法只是简单为对象预留内存,并未使对象可用。为了使对象真正工作起来,你必须调用对象指派的初始化方法。所谓的初始化方法是类的设计者明确必须调用的。对NSObject来说,就是init方法。所以,大部分对象一般都这么作:

SomeClass* anObject = [[SomeClass alloc]init];

其他一些对象可能指派不同的初始化方法,或者有多个初始化方法。比如NSString类:

NSString* myString = [[NSString alloc] initWithFormat: @"here's a number: %i", 123];

NSString* anotherString = [[NSString alloc] initWithData: anNSDataObject encoding: NSUTF8Encoding];

NSString* oneMoreString = [[NSString alloc] initWithContentsOfFile: @"path to a file" encoding: NSUTF8ENCODING error: someErrorPointer];

以上方法都可以初始化NSString对象,不过是殊途同归。

另外,某些类通过类方法包装alloc 和init 方法直接返回对象。这个又称为工厂方法,因为这就像一家为你生产对象的工厂。下面例子就是使用了工厂方法:

NSString* myString = [NSString stringWithFormaty:@"here's a number: %i", 123];

retain 和 release

当运行在引用计数环境下,就会接受到 retain 和 release 消息。
这两个方法都是有根类实现并管理。

程序员永远不用调用这些方法,尤其在ARC,如果调用了,反而会导致错误。

终止和释放内存

当然,我们希望所有对象内存都在不需要时得到释放。不过,在对象内存释放前,对象还有最后的机会去执行代码。在终止时,运行对象移除对其他对象的引用,关闭所有打开的文件,并挽手收尾工作。

等到对象引用的数目为0时,对象就会发送dealloc方法。这是对象最后要发送的消息,以用于处理最后的工作。

调用了dealloc 之后,对象内存随即归还给系统。