2008年5月28日星期三

学习Objective-C

Objective-C


Objective-C是编写Mac软件使用的主要语言。如果你对面向对象的基本概念和C语言有所了解,学习Objective-C也不会很困难。如果你不了解C语言,你应该首先阅读C语言教程(英文/中文)。

本教程的文字和图片均由Scott Stevenson创作。

Copyright © 2008 Scott Stevenson





第1部分 方法调用


为了最快速的起步,让我们先看几个简单的例子。调用一个对象的方法的基本语法如下:

[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];

现在,myString就成了一个NSString变量了,因此,如果我们调用了这个对象不支持的方法,编译器就会发出警告。

注意,对象类型的右侧有一个星号。所有的Objective-C对象变量都是指针类型。id类型被预定义为指针类型,因此不需要加星号了。

嵌套消息


在很多语言中,嵌套方法或函数调用看起来类似这样:

function1 ( function2() );

function2的结果作为参数传递给function1。在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的取值器上加上"get"前缀。

只要你看到方括号,就要知道这是在向对象或类发送消息。

点操作符


用点来操作取值器和赋值器是与Mac OS X 10.5一起发布的Objective-C 2.0的新特征:

photo.caption = @"Day at the Beach";
output = photo.caption;

你可以随便使用哪种风格,但是在同一个项目中只能使用其中的一种。点操作符只能用来处理取值器和赋值器,不能用来调用常规的方法。

第3部分 创建对象


主要有两种方法可以用来创建对象。其中的一种你已经在之前看到过了:

NSString* myString = [NSString string];

这是一种方便的自动创建对象的方法。在这个例子中,你创建了一个自动释放(autorelease)的对象,我们后文会做出进一步解释。在很多情况下,你需要用手动方法创建一个对象:

NSString* myString = [[NSString alloc] init];

这是一个嵌套方法调用。第一个是对NSString调用alloc方法。这是一个相对底层的调用,作用是分配内存和实例化对象。

第二次是对新对象调用了init方法。init的实现通常是进行基本设置,例如创建实例变量等。作为类的客户,你是无法知道这个过程的细节的。(The details of that are unknown to you as a client of the class.  这句话的翻译还存在疑问

在一些情况下,你可能使用一个不同版本的init方法,以接受参数输入。

NSNumber* value = [[NSNumber alloc] initWithFloat:1.0];

第4部分 内存管理基础


如果你在为Mac OS X写程序,你可以选择是否启用垃圾收集。通常,启用它意味着你无需考虑内存管理,除非遇到了非常复杂的情况。

然而,你可能不总是能在支持垃圾收集的环境下工作。在这种情况下,你就需要知道一些关于内存管理的基本概念了。

如果你用手工alloc风格创建了一个对象,你需要在后面释放(release)这个对象。你不能手动释放一个自动释放的对象,如果你这么做会导致应用程序崩溃。

下面有两个例子:


// string1 会自动释放
NSString* string1 = [NSString string];

// 不再使用时需要手工释放
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#include衍生而来,不同的是它能保证一个头文件不被多次包含。

@interface只是了这是Photo类的声明段。冒号指示了Photo类的超类是NSObject

在大括号内,有两个实例变量:captionphotographer,都属于NSString类。实例变量可以是任何对象类型,包括id

最后,@end标志了类定义的结束。

添加方法


让我们为实例变量添加取值器:

#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

现在,加入赋值器:

#import <Cocoa/Cocoa.h>

@interface Photo : NSObject {
NSString* caption;
NSString* photographer;
}

- (NSString*) caption;
- (NSString*) photographer;

- (void) setCaption: (NSString*)input;
- (void) setPhotographer: (NSString*)input;

@end

赋值器没有返回值,因此把类型指定为void

第6部分 类的实现


让我们来创建一个实现,首先从赋值器开始:

#import "Photo.h"

@implementation Photo

- (NSString*) caption {
return caption;
}

- (NSString*) photographer {
return photographer;
}

@end

这段代码由@implementation和类名起始,以@end结束,这与接口的定义方法类似。所有的方法定义必须在这两个语句之间。

如果你曾经写过程序,取值器的实现看起来可能比较眼熟,所以我们重点看赋值器,理解它需要更多的解释。

- (void) setCaption: (NSString*)input
{
【caption autorelease】; //用中文标点是为了避免本行无法显示。请将中括号改成英文中括号。
caption = [input retain];
}

- (void) setPhotographer: (NSString*)input
{
[photographer autorelease];
photographer = [input retain];
}

每个赋值器处理两个变量。第一个是对已有对象的引用,第二个是一个新的input对象。在启用垃圾收集的环境中,我们可以直接设置新值:

- (void) setCaption: (NSString*)input {
caption = input;
}

但是如果你不能使用垃圾收集,你需要释放(release)旧对象,保留(retain)新对象。

事实上有两种方法可以解除一个对象的引用:释放(release)自动释放(autorelease)。标准的release方法会立刻删除引用。autorelease方法则是尽快的释放引用,但是这个对象在当前函数结束前绝对不会消失(除非你使用了自定义代码特意改变这种行为)。

在赋值器中使用autorelease方法会更加安全,因为变量的新值和旧值可能指向同一个对象。你肯定不希望立刻释放那个你即将保留的对象。

这可能看起来很混淆,不过随着学习的进行,你就能渐渐理解了。你现在不需要完全理解这些内容。

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
{
【caption release】;
[photographer release];
[super dealloc];
}

前面两行,我们只是向两个实例变量发送了release消息。这里我们不需要使用autorelease了,标准的release方法会更快。

最后一行非常重要。我们发送[super dealloc]消息,让超类进行清理工作。如果我们不加这行代码,对象就无法被移除,这就导致了内存泄漏。

dealloc方法在启用垃圾收集时是不会调用的。你可以实现finalize方法来实现类似的功能。

第7部分 内存管理进阶


Objective-C的内存管理系统被称为引用计数。你所要做的事跟踪引用,实际的内存释放工作是由运行时系统来完成的。

简单的说,首先alloc一个对象,可能需要retain一段时间,然后针对每个alloc/retain发送release消息。因此,如果你使用了allocretain各一次,你需要release两次。


这就是引用计数的理论。但是实际应用中,只有两个理由需要创建一个对象:



  1. 把它保留为实例变量

  2. 在函数中临时使用



在大多数情况下,实例变量的赋值器应该把旧对象自动释放(autorelease)保留(retain)新对象。你只需要确保在后面dealloc它。

实际的内存管理工作其实就是在函数中管理本地引用。规则只有一条:如果你用alloccopy创建了一个对象,在函数的最后向它发送一条releaseautorelease消息。如果你用其他方法创建了一个对象,无需额外处理。

下面是第一种情况,管理一个实例变量:

- (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];

注意到,不管本地引用被设置成实例变量与否,管理规则是完全一样的。你不需要考虑赋值器是如何实现的。

如果你理解这些内容,你对Objective-C内存管理的理解已经达到90%了。

第8部分 回显(Logging)


Objective-C中,要向控制台回显消息是很简单的。事实上,NSLog()函数几乎与C的printf()函数完全一样,除了它能使用一个%@标记,来回显对象。

NSLog ( @"The current date and time is: %@", [NSDate date] );

你可以在控制台上回显一个对象。NSLog函数调用对象的description方法,并打印返回的NSString对象。你可以在自己的类中覆盖description方法,返回一个自定义字符串。

第9部分 属性


在我们之前为captionauthor编写访问器方法的时候,你可能已经注意到了代码非常直观易懂,甚至可以被泛化处理(Generalized)。

属性是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指示了赋值器需要保留input值;这一行的其他部分指示了属性的类型和名称。

现在让我们来看看类的实现:

#import "Photo.h"
@implementation Photo

@synthesize caption;
@synthesize photographer;

-
(void) dealloc
{
【caption release】;
[photographer release];
[super dealloc];
}

@end

@synthesize让编译器为我们自动生成赋值器和取值器,这样,我们只需为这个类实现dealloc方法。

访问器只会在他们不存在的情况下才被生成,所以请放心的对一个属性使用@synthesize,然后在你需要的时候实现自定义的取值器和赋值器。编译器会填补任何方法的空白。

第10部分 对Nil调用方法


在Objective-C中,你来对象在功能上等价于其他很多语言中的NULL指针。区别是可以对nil调用方法,而不致导致程序崩溃或抛出异常。

这种技术以多种不同的形式被用在框架(frameworks)中,但是目前你需要知道的是,在对一个对象调用方法的时候,无需检验它是不是nil。如果你对一个能返回对象的nil调用方法,你将得到的返回值是nil。

我们还可以使用这种技术稍稍改进一下我们的dealloc方法:

- (void) dealloc
{
self.caption = nil;
self.photographer = nil;
[super dealloc];
}

这是可行的,因为我们把nil设置为一个实例变量了。赋值器只是保留了nil(它什么都不会做)并释放了旧值。这种方法对dealloc来说通常更好些,因为这样,变量就不会指向一个对象之前所在的随机数据位置了。

注意,我们使用了self.<var>的语法形式,这意味着我们使用了赋值器,并释放了内存。如果我们仅仅想下面这样直接赋值,将会导致内存泄漏。

// 错误。这将导致内存泄漏。
// 使用self.caption来处理访问器
caption = nil;

第11部分 分类(Categories)


分类是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中会不断的用到。

如果你想要实践一下这些例子,可以下载下面的项目文件并通读源代码。

学习Objective-C Xcode 3.0 项目文件(56k)

谢谢!



(本翻译用 Prism + Google Docs 制作)

原文地址:http://cocoadevcentral.com/d/learn_objectivec/

版权说明:本文来自Cocoa Dev Central, 原文版权在上文已经给出。关于那个捐助信息,我只是照原文翻译,如果你真的有意捐献原文作者,请访问原文,地址在上面也已经给出。因为不知道原文的授权方 式,我也懒得联系原作者询问版权事宜,因此本翻译的文字内容暂时按照Public Domain(公有域)方式授权使用,即,本人放弃本翻译的版权。文中的所有图片均来自原文,版权归原作者所有。

免责声明:本人不保证翻译的正确性。本人也不对因使用本翻译造成的直接或间接的损失负责。

(Venj@iLoveMac.cn 译)

4 条评论:

  1. Hi,文章很不错,翻译的也不错,就是短了些,还有其他的深入一些章节吗?

    回复删除
  2. 我把文章翻译完了,发布在http://www.weiphone.com/thread-149737-1-1.html
    原本以为没有人翻译的,后来google了一下才发现这里已经翻译了一些-_-!
    对比了一下,觉得我翻译的也还不错,呵呵

    回复删除
  3. 我翻译完整了。上次在Google Docs里也丢内容了,我没注意这里居然也丢了。我修改下。
    原文是在这里:
    http://docs.google.com/Doc?docid=ajktsxj4msx2_207cfjjszcp

    回复删除
  4. 翻译的不错,正在学习ObjC, 多谢您的文章

    回复删除