Objective-C 类的定义

1,134 阅读17分钟

当我们编写OS X或者iOS的软件时, 大多是时间是在和对象打交道. Objective-C中的对象和其它面向对象语言中的对象相似: 他们对数据和数据相关的行为进行了打包封装.

构建一个App, 就是在构建一个互相关联的众多对象的生态系统. 在这个系统中, 对象之间进行交互以解决特定的问题 --- 比如展示一个可视化界面, 响应用户输入,或是存储信息. 对于OS X和iOS 开发, 我们不必费时间解决每一个问题, Cocoa(OS X) 和 Cocoa Touch(iOS) 已经为我们准备好了众多对象来解决问题.

有一些系统对象可以直接使用, 比如像字符串和数字这些基本数据类型以及按钮(Buttons)和TableView. 还有一些是需要结合自定义的代码去实现指定需求的. 如何自定义对象, 如何把自定义的对象和系统提供的对象以最好的方式结合在一起以赋予我们的App独特的功能和特性, 是我们在App开发的过程中需要多多思考的问题.

在面向对象编程中,一个对象就是一个类的实例. 这一章主要强调如何通过声明接口, 定义一个Objective-C类, 用以描述我们期望类和它的实例如何被使用. 接口中包含了这个类可以接受的消息列表, 所以我们也需要为类提供类实现, 在类实现中实现响应消息的代码段.

类是对象的模版/蓝图

一个类描述了对象的行为和属性.比如字符串对象(在Objective-C中, 是NSString的一个实例对象), NSString类提供了众多方法来检查和转换它内部的字符. 类似地, 描述数字的对象(NSNumber)提供了对数值进行操作的各种方法, 比如把内部数值转换成其它数值类型.

从同一个模版/蓝图中建造建筑时,每个建筑的结构是完全一样的. 每个类的实例对象都拥有着相同的属性和行为. 每一个NSString 实例,尽管它内部的字符可能各不相同,但是他们都有着相同的方法。

特定的对象有特定的使用方式. 一个字符串对象表示由字符组成的一串字符,但是我们不必了解它内部的存储字符的机制就可以使用它. 我们也不必了解字符串内部对字符进行处理的机制,但是我们需要了解如何与字符串对象进行交互,比如如何获取它内部的某一个字符,或者如何获取将内部字符转换成大写的新字符串对象. 在Objective-C中,***类接口(class interface)***指定了特定的对象如何与其它对象进行交互. 换句话说,类接口定义了类实例对象和其它部分的公共接口(public interface).

可变性(Mutability)决定了对象值是否可以改变

通过一些类定义出的对象是不可变的(immutable). 这意味着当这种对象内部的内容必须在创建时就被设置好了,并且随后不能被其它对象改变. 在Objective-C种,所有的NSString对象和所有的NSNumber对象都是不可变的. 如果我们需要表示一个不同的数值,只能创建一个新的NSNumber对象.

一些不可变类也会提供一个可变的版本. 如果在运行时需要动态改变字符串的内容,比如在网络请求之后根据响应的内容向原字符串尾部添加字符,这时可以使用NSMutableString类. 这个类的实例除了可以动态改变字符串的内容以外,和NSString类大多相同.

虽然NSStringNSMutableStriing是不同的类,但是他们有很多相似之处. 因此相比于写两个完全不同的类来说,使用继承是更加合理的.

继承(Inherit)

自然界中, 分类学把动物们按照科/属/种等划分. 这种划分是有层级的, 比如多个物种可能属于同一属种,而多个属种有归属于同一科种.

如下图: 人类(Human), 黑猩猩(Gorilla), 其它灵长类物种(Orangutan)属于不同的物种. 虽然他们的物种不同,甚至属种也不同. 但是他们归属于同一科种, 人科(Hominidae).

Image

在面向对象编程的世界里, 对象的归类也是有层次的. 不像自然界分类学那样分为科/属/种, 面向对象简单地按照类(Class)划分. 正如人继承了人科的特征, 一个类可以从父类处继承特性.

当一个类继承自另一个类时, 子类会继承父类定义的所有行为和属性. 当然子类也可以定义自己独有的行为和属性, 或者***重写(Override)***父类的行为.

在Objective-C 字符串类的例子中, 如下图, NSMutableString就是继承自NSString.所有NSString提供了的功能, NSMutableString都是可以使用的. 比如获取指定位置字符串/获取全大写的字符串. 但是NSMutableString 添加了一些方法, 比如添加,插入,替换,删除子串/字符.

Image

根类(Root Class - NSObject)提供基本的能力

正如所有生物都具备生命这个特质, Objective-C的所有类也有一些公共的特性.

当Objective-C对象需要和其它类对象交互的时候, 它期望其它类提供一些确定的基本特性和行为. 因此, Objective-C 定义了一个叫做NSObject的根类(Root Class), Objective-C 绝大多数类都继承自它. 当一个对象遇到另一个对象时, 它期望它们之间可以通过NSObject中定义的基本行为进行交互.

当我们自定义类时, 我们至少应当让它继承自NSObject. 通常来说, 我们应当在Cocoa/Cocoa Touch 框架中找到一个可以提供给我们最需要的功能的类上继承.

举例来说, 如果我们想要在iOS App 自定义一个按钮(Button). 而UIButton类不能提供足够的自定义属性来满足我们的需求. 我们应该创建一个继承自UIButton的类, 而不是直接继承NSObject. 如果我们继承了NSObject, 那么为了实现我们的需求, 我们需要重新实现UIButton中已经实现的复杂的视觉效果以及交互方法, 但是如果我们直接继承UIButton, 我们的子类自动就会获得所有UIButton的功能, 不仅如此, 如果后续UIButton未来有功能的强化或者Bug的修复, 我们的子类都可以从中获益.

UIButton类继承自UIControl, UIControl描述了iOS上所有与用户交互相关的基本行为. UIControl类继承自UIView, UIView给了它展示到屏幕上的能力. UIView类继承自UIResponder, UIResponder提供了响应用户输入的功能, 比如点击,手势或者摇晃. UIResponder最终继承自NSObject. 如下图所示.

Image

从这个继承链可以看出, 任何继承自UIButton的类, 不仅仅可以继承UIButton内部定义的能力, 也可以继承所有父类的能力. 最终我们获得了一个描述这样一种对象的类: 行为上像一个按钮(UIButton/UIControl), 可以在屏幕上展示(UIView), 可以响应用户输入(UIResponder), 可以和其它Cocoa Touch对象交互(NSObject).

为了让我们定义的类恰当地完成它的功能, 牢记继承链是至关重要的.

类的接口(Interface)定义期望的交互

面向对象语言的好处之一就是当我们想要使用一个类的时候, 只要了解如何和它的对象交互就可以了. 更详尽地说, 也就是对象应该把实现细节隐藏在内部实现.

假设我们使用了UIButton, 我们不需要关心每个像素点都是如何展示到屏幕上的. 我们仅需了解我们如何去改变当前的属性, 比如按钮的titlecolor. 当我们把这个按钮添加到界面上的时候, 它自然就会正确地展示到屏幕之上.

当我们自定义类的时候, 我们首先需要考虑对外的属性和行为. 哪些属性我们对外开放? 是否应该允许外界修改这些属性? 其它类的对象如何和我们的类对象交互呢? 这些信息决定了我们如何定义类的接口 -- 接口定义了我们期望其它对象如何和我们的类对象交互. 类的接口和类的内部实现是分开描述的. 在Objective-C语言中, 接口和实现常常分开在两个文件中放置, 而我们只需要对外开放接口文件.

定义接口

Objective-C定义一个类接口(Interface)的语法如下:

@interface SimpleClass : NSObject
 
@end

这个例子定义了一个叫做SimpleClass的类, 它继承自NSObject.

公共属性和行为在@interface声明中定义. 在这个例子中, 没有定义任何公共属性/行为, 所以SimpleClass只是简单继承了NSObject的行为.

属性(Properties)控制对象值的获取

对象常常会对外提供属性. 如果要定义一个类表示人的信息, 就可能需要定义字符串属性来表示人的姓和名.

这些属性的声明应当添加在interface内部.

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

在这个例子中, Person类声明了两个公共属性, 这两个属性都是NSString类的实例.

这些属性都是Objective-C对象, 所以使用了*来表面它是C 指针. 它们的声明就像是C语言变量的声明一样, 因此, 不要忘记了声明结束处的;.

如果要添加一个出生年的属性, 可以这样添加:

@property NSNumber *yearOfBirth;

由于只存储一个数值, 我们可以替换为C的基本数值类型int

@property int yearOfBirth;

使用属性(Property)的特性(Attribute)表示数据读写和存储方式.

目前为止的示例中, 声明的所有属性都是对外完全可访问的. 也就是说, 其它对象可以读写这些属性.

在某些情况下, 我们不希望一些属性被改变. 对于Person类来说, 如果我们不希望外界修改firstName 以及lastName属性, 我可以使用readonly 来限制这个属性为可读但不可写.

Objective-C 属性声明可以通过添加readonly attribute 来表明属性是否为只读.

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

属性的attributes可以在@property 后的括号内指定. 之后的文章会详细说明. // Todo: Add a link.

使用方法(Method)声明表示对象可以接受的消息(Message)

目前为止的例子我们描述了一个典型的Model-View-Controller 模型中的Model 对象 -- 主要用于数据封装的对象. 在Person类的例子中, 可能除了获取两个属性以外, 不需要额外添加其它功能. 但是在大多数类中, 除了添加属性外, 还要添加方法声明.

上文提及过, Objective-C 是由对象的网络构成的, 需要重点指出的是, 这些对象之间的交互是通过发送消息(Messages)实现的. 在Objective-C 中, 一个对象通过调用方法(Calling a method)向另一个对象发送消息.

Objective-C 方法(Methods) 在概念上和C和其它语言的函数(Functions)相似, 即使他们的语法大不相同. 一个C函数声明如下所示:

void SomeFunction();

等价的Objective-C 方法声明如下:

- (void)someMethod;

在这个例子中, 方法没有参数. C的 void 关键字在声明前方使用括号包围, 表明方法没有任何返回值.

在方法最前面的-表示方法是实例方法 -- 可以被该类的实例对象调用. 与其相对应的是类方法, 类方法是被类本身调动的一种方法. 之后的文章会详细说明. // TODO: Add a link.

和C函数原型(Prototypes)一样, Objective-C 类接口内的方法声明需要以分号;结尾.

方法(Method)可以接受参数

定义一个包含1个或以上的参数的方法时, Objective-C的语法和经典C函数有很大的不同.

对于C函数, 参数是在括号内被指定的, 如下所示:

void SomeFunction(SomeType value);

Objective-C 方法声明使用:把参数作为方法名的一部分, 如下所示:

- (void)someMethodWithValue:(SomeType)value;

同返回值类型一样, 参数类型被括号()包住, 就像标准C类型转换(C type-cast)一样.

如果需要指定多个参数, Objective-C的语法和C语法仍然大有不同. C函数把多个函数使用()包住, 使用,分隔, 而在Objective-C中, 两个参数的函数声明如下所示:

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

在这个例子中, value1value2 是为了在实现中获取到参数中传入的值的形式参数.

一些编程语言允许函数定义所谓的具名参数(named arguments). 需要重点指明的是, Objective-C语言中不允许这种特性. 方法调用中的参数顺序必须和方法声明中的参数顺序一致, 并且方法声明中的secondValue:部分, 是方法名的一部分.

someMethodWithFirstValue:secondValue:

由于参数值在方法中是紧挨着关联方法名内联(inline)传入的, 这个特性使得Objective-C的可读性很强.

Note: 这里的value1value2 并不是方法声明严格的一部分. 也就是说, 这部分在方法声明和方法实现中不必使用完全相同的名称. 唯一的要求是签名(signature)一致 -- 方法名称一致并且参数和返回值的类型也一致. 举例说明, 如下方法和上面的方法拥有相同的签名:

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

如下两个方法签名与上述方法不同:

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

类名必须唯一

每个类的类名必须唯一, 即使跨library/framework 也不可以. 如果创建了重复的类名, 将会产生一个编译时错误.

因此, 建议使用3个或更多字母的类名前缀(Prefix). 此文档之后的例子都将使用如下所示的类名前缀:

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

Note: 或许你会好奇为什么我们使用的这么多类都使用NS 前缀. 这是因为Cocoa和Cocoa Touch过去的历史. Cocoa起初是作为NeXTStep 操作系统的框架而诞生的. 当Apple 在1996年收购了NeXT, 大部分NeXTStep都被合并到了OS X中, 这就包含了已有的类名.Cocoa Touch是Cocoa 移植到iOS平台上的产物. 很多类在Cocoa和Cocoa Touch上都是可以通用的, 虽然也有一大部分平台独有的类. 一些两个字母的前缀比如NSUI被Apple保留作为关键字.

对比类名来说, 方法和属性名称只需要在它们被定义的类中是唯一的即可. 虽然每一个C函数必须有一个唯一名称, 但是在多个Objective-C类中定义相同的名称是完全可以接受的. 我们不能在同一个类声明中多次定义一个方法, 但是当我们希望重写父类的方法时, 我们必须使用和父类声明一致的方法名.

和方法名一样, 对象的属性和实例变量也必须在所定义的类中唯一. 如果定义了全局变量, 那么这些全局变量的名称必须在App或者项目内唯一. // TODO: Add link.

使用类的实现(Implementation)提供类的内部行为

当我们定义了类的接口之后(包含对外的属性和方法), 我们需要通过类的实现(Implementation)来实现类的具体行为.

正如之前所说的那样, 类的接口经常放在一个独立的文件中 -- 头文件. 一个头文件的扩展名为.h . 而我们将Objective-C类的实现保存在.m文件中.

每当我们在头文件中定义接口时, 我们都需要告知编译器在编译实现文件之前需要先读取头文件. Objective-C提供了预编译指令, #import , 来达成这个目的. 这个指令和 C 的 #include 指令很相似, 但是#import可以保证每个文件在编译时只会被包含一次.

基本语法

类实现的基本语法如下:

#import "XYZPerson.h"
 
@implementation XYZPerson
 
@end

如果我们在类接口中声明了方法, 就需要在这里实现所声明的方法.

实现方法

对于如下的简单的类接口声明:

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

类的实现可能是这样的:

#import "XYZPerson.h"
 
@implementation XYZPerson
- (void)sayHello {
    NSLog(@"Hello, World!");
}
@end

这个例子中使用了NSLog方法在控制台中输出字符串. 它和C中的printf()函数很相似, 可接受多个参数, 但是首个参数必须为Objective-C String类型.

方法的实现和C方法的定义很相似, 都用{}括住相关代码.另外, 方法名必须和它的原型一致, 参数和返回值类型也必须完全一致.

Objective-C 和C 一样, 是大小写敏感的语言. 因此如下的方法:

- (void)sayhello {
}

和前面的sayHello方法是完全不同的两个方法.

通常来说, 方法名应当以小写字母开头. Objective-C的惯例是使用更加具有描述性的方法名. 如果方法名包含了多个单词, 使用驼峰式命名(每一个新单词的首字母大写)使得方法名更加便于阅读.

另外一个需要注意的地方就是空白字符. 空白字符在Objective-C中是灵活的. 我们既可以使用制表符tab也可以使用空格spaces对代码进行缩进. 并且左花括号经常是独占一行的, 如下所示:

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

苹果的开发工具XCode会自动根据用户偏好缩进代码. // TODO: Add link

Objective-C 类(Classes)也是对象

在Objective-C中, 每个类都是一个类型为Class的对象. Class类没有像之前例子中的实例对象那样声明属性, 但是它们可以接受消息(Messages).

类方法(Class method)的经典使用场景就是作为一个工厂方法(Factory method), 提供了一种创建并初始化对象的方法.比如 NSString 类就有很多工厂方法供我们使用, 它们或者可以创建一个空的字符串对象,或者可以通过特定字符创建字符串对象:

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

从如上代码中可以看出, 类方法都是以+开头, 这和实例方法以-开头是不同的.

类方法原型(Class method prototypes)可能被包含在类的接口中, 就像实例方法原型那样. 类方法和实例方法的实现也是一致的, 在@implementation中实现.

练习:

  1. 使用XCode创建一个继承自NSObjectXYZPerson类, 包含类的接口声明和类的实现文件.
  2. XYZPerson类的接口中声明firstName,lastName ,以及NSDate类型的dateOfBirth.
  3. 声明sayHello并且实现它.
  4. 添加类的工厂方法声明person.

参考资料: Defining Classes