[翻译]学习 Objective-C
接到 ceo 通知,招聘、培训都不用我再负责了,所有人从“闲人”要变成“战斗人员”。于是撸撸袖子,潜心学习 cocoa 和 Object-c。静心学习两日,小成。不经意看到这篇,好文,兴起,翻译之⋯⋯
原文地址:http://cocoadevcentral.com/d/learn_objectivec/
Objective-C
Objective-C 是编写 Mac 软件的主要语言。如果你适应基本的面向对象和C语言,Objective-C 会给向你展示许多这些内容。如果你不知到C,你应当先阅读 C 指南[英文]。
这个指南由Scott Stevenson撰写并排版。
1. Calling Methods
为了尽快开始,让我们先来看一些简单的例子。调用某个对象的方法的一些基本语法如下:
[object method]; [object methodWithInput:input];
方法可以返回一个值:
output = [object methodWithOutput]; output = [object methodWithInputAndOutput:input];
同样可以调用类的方法,用于创建对象。在下面的例子中,调用了类NSString的string方法,返回了一个新的NSString对象:
id myObject = [NSString string];
类型id表示myObject变量可以是任何类型的对象的引用,所以在编译的时候,它并不知道实际实现的类和方法。
在这个例子中,显然对象类型是NSString,所以可以改变类型为:
NSString* myString = [NSString string];
现在这是一个NSString变量,这样编译器会在常识调用NSString不支持的方法的时候进行警告。
留意在对象类型的右边有一个星号。所有的Objective-C对象变量都是指针类型。id类型被预定义为指针类型,所以不需要添加星号。
嵌套消息
在许多语言中,嵌套的方法或函数调用像下面的形式:
function1 ( function2() );
函数2的结果作为函数1的输入。在Objective-C中,嵌套消息是如下的形式:
[NSString stringWithFormat:[prefs format]];
避免在移行中嵌套超过两次的调用,这样代码可以更加易懂。
多输入方法
一些方法接受多输入值。在Objective-C中,一个方法名可以被分为许多段。首先,多输入方法是如下形式:
-(BOOL)writeToFile:(NSString *)path atomically:(BOOL)useAuxiliaryFile;
你可以这样调用这个方法:
BOOL result = [myData writeToFile:@"/tmp/log.txt" atomically:NO];
这不仅仅是命名参数。在运行时的系统中,方法名实际上是writeToFile:atomically:。
2. 存取器
在Objective-C中,默认所有的实例变量都是私有的,所以在多数情况下,应当使用存取器来获取或者设置值。有两种语法。这是传统的1.x语法:
[photo setCaption:@"Day at the Beach"]; output = [photo caption];
代码的第二段不是直接访问实例变量。它实际上调用了叫做caption的方法。在多数情况下,在Objective-C中,你不用向getter添加“get”前缀。
当你看到在方括号中的代码,就表示向对象或类发送消息。
点语法
在Mac OS X 10.5带的Objective-C 2.0中新增了点语法支持getter和setter:
photo.caption = @"Day at the Beach"; output = photo.caption;
可以在两种语法中任选一种使用,但是在每个工程中只能使用一种。点语法只能用在setter和getter,不能用在其他目的的方法上。
3. 创建对象
主要有两种方式创建对象。下面是第一种:
NSString* myString = [NSString string];
这是更加方便的自动创建的形式。在这个例子中,创建了一个自动释放的对象,一会我们会了解更多的细节。在许多情况下,你需要用手工模式创建对象:
NSString* myString = [[NSString alloc] init];
这是嵌套方法调用。首先NSString本身调用alloc方法。这是相对低等级的调用,用于分配内存并实例化对象。
第二个部分是调用新对象的init。init通常实现一些基本的设置,例如创建实例变量。对于使用类的用户来说实现细节是不知道的。
在一些情况下,可以使用带有输入的不同的init版本:
NSNumber* value = [[NSNumber alloc] initWithFloat:1.0];
4. 基本内存管理
当编写Mac OS X程序时,可以选择开启垃圾收集。通常,这意味着在遇到相当复杂的情况之前,无需关注内存管理。
然而,不可能总是工作在支持垃圾收集的环境。在这种情况下,需要了解一些关键点。
如果使用手工的alloc形式创建对象,需要在使用后释放对象。不应当手动释放任何自动释放对象,如果这么做的话应用会异常退出。
这有两个例子:
// string1 将会被自动释放 NSString* string1 = [NSString string];
// 使用完后必须释放must release this when done NSString* string2 = [[NSString alloc] init]; [string2 release];
在这个指南中,可以认为自动对象在当前函数结束后总是会被释放。
关于内存管理还有许多需要学习,不过需要等到我们了解更多的一些其他要点之后。
5. 设计类接口
Objective-C创建类的语法非常简单。通常有两部分。
类接口通常保存在文件ClassName.h,用于定义实例变量和公共方法。
实现在文件ClassName.m中,包含其方法的实际代码。通常也定义类的用户不可用的私有方法。
下面展示了接口文件的样子。类被称作Photo,因此文件被命名为Photo.h:
#import <Cocoa/Cocoa.h> @interface Photo : NSObject { NSString* caption; NSString* photographer; } @end
首先,引入了Cocoa.h,用于引入Cocoa应用所需的所有基本类。#import指令自动处理在一个文件中多次引用的问题。
@interface表示这是类Photo的定义。冒号后定义了父类,这里是NSObject。
在花括号中,有两个实例变量:caption和photographer。这里都是NSString,但其实变量可以是任何类型,包括id。
最后的@end标记结束了类的定义。
添加方法
为实例变量添加getter和setter:
#import <Cocoa/Cocoa.h> @interface Photo : NSObject { NSString* caption; NSString* photographer; } - caption; - photographer; @end
记住,Objective-C方法通常不要“get”前缀。在方法名前的横线表示这是一个实例方法。在方法名前的加号表示这是一个类方法。
默认,编译器假设方法返回id对象,并且所有输入值都为id。上面的代码技术上是正确的,但是通常不用。给返回值指定类型:
#import <Cocoa/Cocoa.h> @interface Photo : NSObject { NSString* caption; NSString* photographer; } - (NSString*) caption; - (NSString*) photographer; @end
现在添加setter:
#import <Cocoa/Cocoa.h> @interface Photo : NSObject { NSString* caption; NSString* photographer; } - (NSString*) caption; - (NSString*) photographer; - (void) setCaption: (NSString*)input; - (void) setPhotographer: (NSString*)input; @end
Setter不需要返回值,所以设置为void。
6. 类实现
从getter开始创建实现:
#import "Photo.h" @implementation Photo - (NSString*) caption { return caption; } - (NSString*) photographer { return photographer; } @end
这部分的代码由@implementation和类名开始,并且像接口一样有@end。所有的方法必须放在这两个声明之间。
如果之前做过编码,getter看起来应该非常熟悉,所以将精力放在setter上,这需要一些解释:
- (void) setCaption: (NSString*)input { ; caption = [input retain]; } - (void) setPhotographer: (NSString*)input { [photographer autorelease]; photographer = [input retain]; }
每个setter有两个变量。第一个是已有对象的引用,第二个是新输入的对象。在垃圾回收环境中,可以直接设置新值:
- (void) setCaption: (NSString*)input { caption = input; }
但是如果不能使用垃圾回收,就需要释放(release)旧对象,并保持(retain)新对象。
实际上有两种方式释放对象的引用:释放(release)和自动释放(autorelease)。标准的释放将会立即移除引用。自动释放方法将会在一小会后释放,但可以明确的是它会保留到当前函数结束(除非添加自定义的代码明示改变这个规则)。
自动释放方法在setter中更加安全一些,因为变量的新旧值会指向相同的对象。肯定不想立刻释放需要保持的对象。
现在似乎有一些混乱,但是将会按照进度有一个整体的介绍。现在无需弄清楚所有内容。
Init
可以创建一个init方法用于初始化实例变量:
- (id) init { if ( self = [super init] ) { [self setCaption:@"Default Caption"]; [self setPhotographer:@"Default Photographer"]; } return self; }
这段代码自己已经解释了很多问题,除了第二行看起来不同寻常以外。这里有一个等号,将[super init]的结果赋值给self。
这实质上是告诉父类进行其自己的初始化。那个if语句用于保证在尝试设置变量默认值之前验证初始化已经成功。
Dealloc
dealloc方法在对象被从内存中移除时调用。这通常是最好的释放所有子实例变量引用的时机:
- (void) dealloc { ; [photographer release]; [super dealloc]; }
在前两行,发送了release到每个实例变量。不需要在这里使用autorelease,标准的release会更快一些。
最后一行非常重要。必须发送[super dealloc]消息告诉父类进行它自己的清理。如果不这样做的话,对象不会被移除,从而导致内存泄露。
在执行垃圾收集激活的情况下,dealloc方法不会被调用。 需要实现finalize方法。
7. 内存管理进阶
Objective-C的内存管理系统叫做引用计数。需要做的全部事情就是跟踪引用,在运行时进行真正的内存释放。
在通常的情况下,alloc一个对象,可能在某个位置retain它,需要对每个alloc/retain消息发送对应的release。 所以,如果使用alloc一次,然后retain一次,需要release两次。
这个算法叫做引用计数。但是在实践中,只有两种情况需要创建一个对象:
1. 将其保存为实例变量
2. 在函数中临时使用
在多数情况下,实例变量的setter应当自动释放(autorelease)旧的对象,并且保持(retain)新的。同时只要确保dealloc中正确释放即可。
所以真正需要做的,只是管理函数中的本地引用。并且只有一个规则:如果通过alloc或copy创建了一个对象,在函数的最后向其发送release或autorelease消息。如果通过其他方式创建的对象,什么也不要做。
这里有第一种情况的例子,管理实例变量:
- (void) setTotalAmount: (NSNumber*)input { [totalAmount autorelease]; totalAmount = [input retain]; } - (void) dealloc { [totalAmount release]; [super dealloc]; }
下面是另一种情况,本地引用。只需要释放通过alloc创建的对象:
NSNumber* value1 = [[NSNumber alloc] initWithFloat:8.75]; NSNumber* value2 = [NSNumber numberWithFloat:14.78]; // 只释放value1,不操作value2 [value1 release];
这里有一个集成:使用本地引用设置对象的实例变量:
NSNumber* value1 = [[NSNumber alloc] initWithFloat:8.75]; [self setTotal:value1]; NSNumber* value2 = [NSNumber numberWithFloat:14.78]; [self setTotal:value2]; [value1 release];
注意这和管理本地引用的规则完全一致,不论是否将其设置为实例变量。也不用考虑setter是如何实现的。
如果明白了这个,就明白了90%需要知道的关于Objective-C内存管理的内容。
日志
在Objective-C中向控制台记录消息非常简单。实际上,NSLog()函数相当接近C的printf()函数,除了额外增加的用于对象的%@标记。
NSLog ( @"The current date and time is: %@", [NSDate date] );
可以将一个对象作为日志记录到控制台。NSLog函数调用对象的description方法,然后打印其返回的NNString。可以在类中重写description方法返回自定义字符串。
9. 属性
之前编写caption和author访问器方法的时候,可能已经留意到那些代码很直接,并且很普通。
属性是Objective-C的特性之一,用于自动创建通用的访问器,同时还有一些其他的功能。现在来修改Photo类使用属性。
这是修改之前的样子:
#import <Cocoa/Cocoa.h> @interface Photo : NSObject { NSString* caption; NSString* photographer; } - (NSString*) caption; - (NSString*) photographer; - (void) setCaption: (NSString*)input; - (void) setPhotographer: (NSString*)input; @end
这里是转换为属性写法的样子:
#import <Cocoa/Cocoa.h> @interface Photo : NSObject { NSString* caption; NSString* photographer; } @property (retain) NSString* caption; @property (retain) NSString* photographer; @end
@property是Objective-C用于定义属性的指令。圆括号中的“retain”表示setter应当保持(retain)输入的值,剩下的部分只是指定属性的类型和名称。
现在看看类的实现:
#import "Photo.h" @implementation Photo @synthesize caption; @synthesize photographer; - (void) dealloc { ; [photographer release]; [super dealloc]; } @end
@synthesize指令自动生成了setter和getter,所以这个类仅剩需要实现的是dealloc方法。
访问器仅在其不存在时自动创建,所以对你的属性随意使用@synthesize,如果需要可以另外实现自定义的getter和setter。编译器将自动填充不完整的方法。
属性定义还有许多其他选项,不过那些已经超出了本指南的范围。
10. 在Nil上调用方法
在Objective-C中,nil对象等同于其他许多语言的NULL指针。不同之处在于你可以在nil上调用方法而不会引起崩溃或异常。
这个技术通过许多不同的方式使用在框架中,但是现在这主要意味着不需要在调用对象方法前检查对象是否为nil。如果调用了一个nil对象的方法返回一个对象,返回值将会是nil。
可以用这个来改进dealloc方法:
- (void) dealloc { self.caption = nil; self.photographer = nil; [super dealloc]; }
由于将实例变量设置为nil,setter仅仅保存了nil(这个什么也不做)并且释放了原有的值,所以这个能正常工作。这个写法的dealloc通常更好,因为这避免了变量指向一个随机的数据,而这个数据是变量之前存储的位置。
注意这里使用了self.语法,这意味着使用了setter和内存管理。如果像这样直接设置值,就会有内存泄露产生:
// 错误,引起内存泄露。 // 通过self.caption使用setter caption = nil;
分类
分类是Objective-C的又一常用功能。本质上说,分类允许在不继承或不了解类的任何实现细节的情况下对已有的类添加方法。
因为可以向内建对象添加方法,所以通常这是很有用的。如果希望在应用中对所有的NSString实例添加一个方法,仅仅需要添加一个分类。不需要将所有工作都放到子类来完成。
例如,为NSString添加一个方法来判断是否是合法的URL,可能类似这样:
#import <Cocoa/Cocoa.h> @interface NSString (Utilities) - (BOOL) isURL; @end
这非常接近类定义。不同之处在于没有列出父类,同时在括号中有分类的名称。名称可以是期望的任何内容,虽然它应当同内部的方法进行通信。
这里是实现。需要留意这不是一个很好的URL检测的实现。这仅仅是为了简介一下分类:
#import "NSString-Utilities.h" @implementation NSString (Utilities) - (BOOL) isURL { if ( [self hasPrefix:@"http://"] ) return YES; else return NO; } @end
现在可以在任何NSString上使用这个方法。接下来的代码将会在控制台打印”string1 is a URL”:
NSString* string1 = @"http://pixar.com/"; NSString* string2 = @"Pixar"; if ( [string1 isURL] ) NSLog (@"string1 is a URL"); if ( [string2 isURL] ) NSLog (@"string2 is a URL");
不象子类那样,分类不能添加实例变量。然而,可以使用分类重写类中已有的方法,但是这么做需要相当小心。
记住,使用分类修改一个类时,应用中的所有这个类的实例都会受到影响。
总结
这是Objective-C一个基本的概览。如同已经看到的,这个语言相当简单。没有许多语法需要学习,相似的约定一遍又一遍的在Cocoa中被使用。
如果你喜欢这里的例子,下载工程并浏览全部源代码:
LearnObjectiveC Xcode 3.0 Project (56k)
谢谢!