Programming with Objective-C 翻译计划(2)--- 定义类

定义类

当你为 OS X 或 iOS 编写应用时,大部分时间都将与对象打交道。Objective-C 中的对象与其他面向对象语言中的对象一样:他们将数据与相关的行为打包。

一个应用其实是一个很大的由很多内在相连的对象组成的生态系统,这些对象通过相互之间的交流来解决特定的问题,比如展示一个虚拟的交互界面,响应用户输入,或存储信息。在 OS X 或 iOS 开发过程中,你并不需要从头创建对象来解决可能出现的问题。Cocoa (for OS X) 和 Cocoa Touch (for iOS) 已经提供了一个巨大的对象库供你使用。

在这个库中,有一些对象立即就可以使用,比如基本数据类型如 strings 和 numbers,或是用户界面元素比如 buttons 或table views。有一些则还需要你完成一些自定义的代码来满足特定的需求。应用开发的过程包括了决定如何才能最好的自定义以及如何将底层框架提供的对象与你自己的对象结合来给你的应用赋予独特的特性和功能。

在面向对象编程中,一个对象是类的实例。这一章将说明如何通过声明一个接口类来定义 Objective-C类,这个接口将描述你将要使用的类及其实例。这个接口包括这个类能接收的消息的列表,所以你还需要提供类的实习,以包含每条消息对应的处理代码。

类是对象的蓝图

一个类描述了特定对象的普遍属性和行为。对于一个字符串对象(在 Objective-C 中,字符串对象是NSString类的实例),它的类提供了各种各样的方法来检查和转换组成它的字符。同样的,用来描述数字对象的类(NSNumber)提供围绕其内部数值的功能,比如将数值转换为另一种数值类型。

与建造建筑同理,同一个蓝图建造出来的建筑结构完全一致,一个类的所有实例都拥有同样的属性和行为。除了内部存的字符串不同,每一个NSString实例的行为都是一致的。

任何特定的对象都是为了特定的使用而设计的。你知道一个字符串对象表示了一串字符,但你不需要知道存储这些字符所用的内在机制。你不知道字符串对象为了与字符交互使用了什么内在行为,但你需要知道你应该怎样与对象进行交互,比如提取特定的字符,或获取字符串对应的大写字符串。

在 Objective-C中,类接口明确说明了一个给定的对象应该如何被其他对象使用。换句话说,它定义了类实例与外界的公共接口。

可变性决定了一个代表值能否被改变

有些类将对象定义为不可变的。这意味着这个对象的内容必须在对象被创建时设置好,并且之后不能被其他对象所改变。在 Objective-C中,所有基本的NSStringNSNumber对象都是不可改变的。如果你想表示一个不同的数值,你需要新建一个NSNumber对象。

有些不可变类提供一个可变版本。如果你明确需要在运行时改变字符串的内容,比如将从网络连接上获得的字符添加到字符串末尾,你可以使用NSMutableString类。这个类的实例和NSString类行为一致,只不过它提供了改变对象所表示字符的功能。

尽管NSStringNSMutableString是不同的类,他们还是有很多的相似之处。与其从头开始编写两个具有相似行为的类,还不如利用继承。

继承自其他类的类

在自然世界中,分类学将动物按照如物种,属,科进行分组。这些组是分层的,许多的物种可能属于同一属,许多的属又属于同一族。

比如,大猩猩,人类,星星,有很多相似之处。尽管他们属于不同的物种,甚至不同的属,不同科,不同的亚科,他们在分类学上还是相关的,因为它们属于同一个族(人科),如Figure 1-1 所示。

Figure 1-1 物种间的分类学关系

humansgorillas.png

在面向对象编程的世界里,对象也会被归类为分等级的组。与使用明确的属于标示不同的分级比如科或种,对象仅仅是被组织进了类。与人类作为人科的一员可以继承特定的属性一样,一个类也可以从他的父类那里继承功能。

当一个类继承了另一个类,子类会继承其父类定义的所有属性和行为。它也可以添加自己的属性和行为,或是重写父类的属性和行为。

对于 Objective-C 类来说,NSMutableString的类描述指明了它继承自NSString,如Figure 1-2 所示。NSString提供的所有功能NSMutableString都有,比如取出特定的字符或获取大写字符串,但是NSMutableString添加了用于添加,插入,替换,或删除子字符串或独立字符的方法。

Figure 1-1 NSMutableString继承关系

nsstringmutablestring.png

根类提供基础的功能

与所有的生物体共享某些基本的生命特征一样,Objective-C 中的一些功能也是在所有对象中通用的。

当一个 Objective-C 对象需要和其他类的对象合作时,它需要其他类提供基本的特征和行为。处于这个原因,Objective-C 定义了一个根类NSObject,用于被大部分其他类集成。当一个对象与另一个对象遭遇,它起码能通过被 NSObject定义的方法与其他对象交互。

定义自己的类时,至少应该继承NSObject。一般来说,你应该找一个提供最接近你想要的功能的 Cocoa 或 Cocoa Touch 类,然后继承它。

如果你想自定义一个iOS应用的按钮,并且已有的UIButton类不能提供足够的可定制的特性来满足你的需求,通过继承UIButton来创建新的类比继承NSObject要合理的多。如果你只是继承自NSObject你需要自己完全复制UIButton定义的复杂的可视化操作和通信功能来使其按用户预期的状态工作。不仅如此,通过继承UIButton,你的子类将自动地获得未来可能应用到UIButton内部行为的增强和对bug的修复。

UIButton类继承自UIControl,后者定义了对iOS中所有界面控制器通用的行为。UIControl类又继承自UIView,给了它被展示在屏幕上的控件的通用行为。UIView继承自UIResponder,使它可以相映用户的输入比如点击,手势,或晃动。最后,在继承数的根部,UIResponder继承自NSObject,如图Figure 1-3 所示。

Figure 1-3 UIButton 类继承链

buttoninheritance.png

这条继承链条表明,任何 UIButton的子类除了继承了UIButton的功能,还继承了之上的所有父类的功能。你会得到一个类,对应一个表现为按钮的对象,能将自己展示在屏幕上,响应用户的输入,并且与其他基本的 Cocoa Touch 对象交流。

对于任何要使用的类,要想知道它能做什么,弄清楚继承链很重要。通过Cocoa 和 Cocoa Touch提供的文档,可以很容易的定位到每个类的父类。如果你不能在一个类的接口或参考中找到你想找的,很有可能它其实是在继承链的父类中被很好的定义了。

类接口定义预期的交互

之前提到的面向对象的编程方式的好处之一就是,你只需要知道如何与一个类的实例交互就可以知道如何使用一个类。更具体的说,一个对象应该被设计为隐藏其内部的实现细节。

比如,当你在iOS应用中使用标准的UIButton,你不需要知道每一个像素是如何被操作的以使对象被显示在屏幕上。你只需要知道你能改变某些属性,比如按钮的标题和颜色,并相信它能在被加入到可视化界面时能按你预期的方式正确表现。

定义自己的类时,你需要从弄清需要的共有属性和行为开始。什么属性需要被公开访问?这些属性应该允许被改变吗?其他的对象如何与你的类的实例交流。

这些信息将体现在你的类的接口上,它定义了你的类与其他类交互的方式。类公共的接口与内部的行为是被分开表述的,后者组成了类的实现。在 Objective-C 中,接口和实现一般被放在不同的文件中,这样你只需要使接口公开。

基本语法

Objective-C 中声明一个类接口的语法是这样的:

@interface SimpleClass : NSObject
 
@end

这个例子定义了一个名为SimpleClass的类,继承自NSObject

公开的属性和行为被定义在@interface声明内部。在这个例子中,除了继承夫类没有定义任何属性和行为,所以该类可用的功能只有从NSObject类继承来的功能。

属性控制对对象值的访问

对象一般会有被设计来供公共访问的属性。如果你为一个记录保持app定义一个类表示人,你可能会需要字符串属性来表示一个人的姓和名,像这样:

@interface Persion: NSObject

@property NSString *firstName;
@property NSString *lastName;

@end

在这个例子中,Person 类定义了两个公有属性,它们都是NSString的实例。

这两个属性都是 Objective-C对象,使用星号来表示它们是C指针。它们的声明语句和C对象的定义一样,所以尾部需要有一个分号。

也许你决定加入一个表示人出生年份的属性,这样就可以通过年份来为人们分组,而不是只通过年龄。你可以使用一个数字对象属性:

@property NSNumber *yearOfBirth;

但是仅仅存一个简单的数值,这样就有点大材小用了。一个替代的方式就是使用C提供的原始类型,这种类型能够保存数值,比如一个整数:

@property int yearOfBirth;

属性特性表明了数据访问和存储的注意事项

到目前为止的例子中,声明的属性都是保证了能被完整的公开访问。这意味着其他的对象能够读写这些属性。

有的时候,你可能需要一个对象不被改变。在现实世界中,一个人要想改变他的名字,必须填一大堆文件。编写正式记录保持应用时,你应该让公开的人名属性被设定为只读,并让一个中间对象负责所有变换请求的确认,以及请求的通过和拒绝。

Objective-C 属性声明可以包括属性特性,用于指示一个属性是否只是刻度的。在一个正式记录保持应用钟,一个 Person 类的接口可能看起来是这样的:

@interface Person: NSObject
@property (readonly) NSString *firstName;
@property (readonly) NSString *lastName;
@end

属性特性在@property关键字之后的括号中被指定,详情见Declare Public Properties for Exposed Data

方法声明表明了对象可以接收的消息

目前为止展示的类描述了一个典型的model对象(model对象是一种典型的包含应用数据,提供数据访问,并实现操纵数据逻辑的对象),或者说是一个主要用于封装数据的对象。在Person类中,除了用于访问两个被声明的属性,并不需要其他别的功能。但是,大部分的类,除了属性还将包括行为。

鉴于 Objective-C 软件是构建在对象的网络之上的,保证对象之间能通过发送消息来交互是很重要的。在 Objective-C 中,一个对象通过调用另一个对象的方法来给那个对象发送消息。

Objective-C 方法在概念上和标准的C方法以及其他语言的方法很相似,尽管语法有点不一样。一个C语言的方法声明:

void SomeFunction();

等价的 Objective-C 方法声明:

- (void) someMethod;

在这个例子中,方法没有参数。来自C的void关键字被放在声明开头的括号中表明方法结束之后不会返回任何值。

方法名墙面的减号(-)表明这是一个实例方法,只能通过类的实例来调用。这将它与类方法区分开来,类方法可以通过类本身进行调用,详见:Objective-C Classes Are also Objects

方法可以接收参数

接收一个或多个参数的方法的语法与标准的C方法非常不一样。

C方法中,参数被指定在括号中,如:

void SomeFunction(SomeType value);

一个 Objective-C 方法的声明使用冒号将参数作为其名称的一部分,比如:

- (void)someMethodWithValue:(SomeType)value;

如返回值类型,参数类型也被指定在括号中,就像标准的C类型转换。

如果参数有多个,语法和C更不一样。C方法中的多个参数全部被定义在括号内,通过逗号隔开;在 Objective-C 中,一个接收两个参数的方法:

- (void) someMethodWithFirstValue: (SomeType) value1 secondValue:(AnotherType) value2;

在这个例子中,value1value2是当方法被调用是在方法实现中被用到的,用于获取调用者提供的参数值,他们就像变量。

一些编程语言允许功能定义参数名;需要注意的是,Objective-C 中并不允许这样,调用方法时,参数的顺序必须符合方法的声明,事实上,secondValue: 是方法名的一部分。

someMethodWithFirstValue:secondValue:

这是使得 Objective-C 可读性高的其中一个特性,因为通过方法调用传递的信息被指定在了一行之中,就在与方法名相关的部分旁边,详见:You Can Pass Objects for Method Parameters

注意:value1和value2这两个参数名并不直接是方法声明的一部分,这意味着实现方法时并不一定需要用和声明完全一样的方法名。你只需要保持声明和实现的签名一致,也就是说你需要保持方法名以及参数和返回值的类型完全一致。

比如,这个方法和之前的方法有着一样的签名

- (void)someMethodWithFirstValue:(SomeType)info1 secondValue:(AnotherType) info2;

这两个方法有着不同的签名

- (void)someMethodWithFirstValue:(SomeType)info1 anotherValue:(AnotherType) info2;
- (void)someMethodWithFirstValue:(SomeType)info1 secondValue:(YetAnotherType)info2;

类名必须是独一无二的

在一个应用程序中,每一个类的名字都必须是独一无二的,即使是在包含的类库和框架之间。如果你创建了一个与项目中已有类重名的类,你会得到一个编译错误。

因此,建议为你的类添加三个或三个以上字幕组成的前缀。这些字母可以与你正在写的应用有有关,或者与一个可重用的库的名字有关,或着直接是你名字的大写首字母。

这份文档接下来给出的例子都将使用命名前缀,比如:

@interface XYZPersion: NSObject
@property (readonly) NSString *firstName;
@property (readonly) NSString *lastName;
@end

历史遗留问题:你可能好奇为什么使用到的很多类都有NS前缀,这与 Cocoa 和 Cocoa Touch的起源 有关。Cocoa 最初是作为为 NeXTStep 操作系统搭建应用的库被创造出来的。1996年,苹果收购了NeXT,包括已经存在的类名。Cocoa Touch 相当于 Cocoa 在 iOS 上的等价库;有一些类在 Cocoa 和 Cocoa Touch中都可用,不过也有很大一部分的类只适用于各自的平台。
两个字母的前缀比如NS和UI(iOS中的界面元素)已经被苹果保留。

与类名不同的是,方法和属性名只需要在类内保持独一无二。尽管在C应用中,每一个方法的名字都应该保持独特,但在多个 Objective-C类中使用相同的方法名是完全可以的(通常是更合适的)。但你不能在一个类的声明中超过一次定义同一个方法,并且如果要重写从父类继承来的方法,必须使用与父类声明完全一致的方法名。

和方法一样,一个对象的属性和实例变量(详见Most Properties Are Backed by Instance Variables)需要在被定义的类中保证独一无二。但是如果你使用全局变量,他们必须在应用或项目中保持独一无二。

更多的命名约定和建议见Conventions

一个类的实现提供了它的内部行为

一旦你定义了一个类的借口,包括为公开访问而定义的属性和方法,你就需要编写类行为实现的代码了。

如之前所说,一个类的借口经常被放置在一个专用的文件中,通常被称为头文件,通常带有文件拓展名.h。Objective-C 类的实现在编写在一个带有.m拓展名的源代码文件中。

无论何时,只要接口被定义在头文件中,你就需要告诉编译器在编译实现中的源代码之前阅读头文件中的接口。Objective-C 为此提供一个预处理指令,#import。它和C的#include执行相似,不过可以确保一个文件在编译的过程中只被包含一次。

注意编译预处理指令和C的传统语句不同,并不适用结尾分号。

基本语法

类实现的基本语法是这样:

#import "XXZPersion.h"

@implementation XYZPerson

@end

在接口中声明的方法需要在这里提供实现

方法实现

对于一个只有一个方法的类,比如:

@interface XYZPersion : NSObject
- (void)sayHello;
@end

实现可以是这样:

#import "XYZPerson.h"

@implementation XYZPersion
- (void)sayHello {
        NSLog(@"Hello, World!");
}
@end

这个例子使用NSLogz()方法输出一条消息到控制台。它和C标准库的方法printf()类似,并且接受一系列不同类型的参数,但第一个必须是 Objective-C 字符串。

方法实现和C方法定义相似,使用花括号包含相关的代码。此外,方法名必须与声明完全一致,参数和返回值的类型也要一致。

Objective-C 从 C 继承了大小写敏感,所以这个方法:

- (void) sayhello {
}

编译器会将其看待为与之前sayHello方法安全不同的方法。

一般来说,方法名应该以小写字母开头。Objective-C协定建议使用比典型的C方法名更具有描述性的方法名。如果一个方法名包括多个单词,使用驼峰命名法(每一个单词的首字母大写)提高可读性。

注意空白在 Objective-C 中的灵活性。习惯上,任何块内的代码都应该使用制表符或空格进行缩进,并且将左花括号放在独立的一行,如:

- (void)sayHello
{
    NSLog(@"Hello, World!");
}

Xcode,苹果为编写 OS X 和 iOS软件提供的集成开发环境,会根据你的偏好自动的帮你缩紧代码。改变缩紧和制表符宽度见Xcode Workspace Guide

下一章将展示更多实现的例子, Working with Objects

Objective-C 类也是对象

在 Objective-C 中,类自身也是一个Class类型的对象,这种类型是不透明的。类对象不能拥有用之前介绍的语法定义出来的实例属性,但是可以接收消息。

类方法的典型应用即为工厂方法,工厂方法是 Objects Are Created Dynamically 中描绘的对象内存分配与初始化过程的一种替代方式。比如,NSString类有一系列工厂方法可以用来创建一个空字符串,或用特定字符初始化的字符串,包括:

+ (id)string;
+ (id)stringWithString:(NSString *)aString;
+ (id)stringWithString:(NSString *)format, ...;
+ (id)stringWithContentsOfFile:(NSString *)path encoding:(NSStringEncoding)enc error:(NSError **)error;
+ (id)stringWithCString:(const char *)cString encoding:(NSStringEncoding)enc;

如上所示,类方法被+号标示,使他们区分于用-号标示的实例方法。

类方法可能像实力方法原型一样被包含在类接口中。类方法实现的方式与实例方法一样,在类的@implentation块中提供实现。

练习

注意:为了能完成每章末尾给出的练习,你需要创建一个Xcode项目。这样可以保证你的代码可以正确编译。

使用 Xcode 的新项目模版窗口选择一个可选的 OS X 应用模版来创建一个命令行工具。当出现提示时,指明项目的类型是Foundation

  1. 使用 Xcode 的 New File 模版窗口创建继承自 NSObject类 的 Objective-C 类XYZPersion的接口和实现文件。

  2. XYZPersion类接口添加姓,名的属性,以及生日(用NSDate表示)。

  3. 声明sayhello方法,并按本章所示实现它。

  4. 加入一个类工厂方法“person”的声明。在阅读下一章之前,不用实现它。

注意:如果你编译代码,会因为没有提供这个实现得到一个关于 “未完成的实现” 的警告。

推荐阅读更多精彩内容