定义类
当你为OS X或iOS系统编写程序时,你会把大多数时间花在 对象(object) 上。在Objective-C的对象和面向对象语言中(object-oriented programming languages) 的对象一样:它们封装数据及其相关的方法。
一个APP是一个巨大的生态系统,它将内部对象互相联系起来,解决特定的问题,例如展示一个界面,响应用户的操作,或者存储信息等等。在OS X和iOS应用的开发中,你不必为了解决各种问题花很多心思创造各种对象,而可以使用一个由OS X的Cocoa
和iOS的Cocoa Touch
提供的巨大的、已经存在的对象库。
在这个对象库中,有些对象是可以直接用的,例如一些基本数据类型:字符串和数字,或者用户界面的基本元素:按钮(button) 和 表视图(table view) 。另一些对象则是专门给你用自己的代码定制的,然后这些对象会按照你的规定执行工作。在APP的开发过程实际上就是在已提供的基本框架下自定义你的对象并且把各种对象组合起来,以赋予你的APP与众不同的特征和功能。
在面向对象编程中,一个对象是 类(class) 的一个 实例(instance) 。在这一章节中,我们会展示如何在Objective-C中通过声明 接口interface 的方式定义一个类。类的接口(interface)描述了你需要的类,及该类中你需要使用的成员变量和方法。接口包含这个类可以接受的信息列表,因此你必须提供这个类的 实现(implementation) 。实现包括一些可执行的代码,这些代码会在收到相应信息时执行。
类是对象的蓝图(Blueprints)
类描述了这个类所有实例对象的共同行为和属性。比如一个字符串对象(在Objective-C中,这是一个 NSString
类的实例),NSString
类提供了各种方法来处理和转换字符。相似地,用来描述数字对象的类——NSNumber
,则提供各种关于数字类型的方法,例如把数值转换成各种数据类型。
就像很多用相同的蓝图建设的建筑一样,这些建筑有着相同的结构,类似的,类的每一个对象都拥有相同的属性和方法。无论字符串对象所装载的字符是什么,这些NSSrting
类的对象都拥有相同的方法。
每一个特定的对象用于解决特定的问题。你只需要知道一个字符串对象可以表现一些字符的集合,而不必知道这些字符串在内部是怎样被储存起来的。你不需要知道任何关于这个对象内部是怎么操作字符的,但你要知道怎样操作这些字符串对象,例如获取字符串中某个特定的字符,或者把字符串中所有的字符转换成大写。
在Objective-C中,类的声明部分(Class interface)具体声明了它的对象有哪些方法供外部使用。换句话来说,声明部分定义了外部环境和类的实例对象之间的接口。
对象的可变性
一些类的对象是不可变的。这意味着对象的内部属性在创建时必须被初始化,而且以后再也不能被改变。在Objective-C中,所有基本的NSString
和NSNumber
类的对象都是不可变的。如果你要表示一个不同的数字,你必须创建一个新的NSNumber
类的实例对象。
一些不可变的类有与之类似的可变的类。如果你在程序运行的时候想要改变字符串的内容,例如在它后面再加上一些从网络上获取的字符,你可以使用 NSMutableString
类的对象。除了提供一些可以改变字符的方法外,这个类的对象和NSString
类的对象没有什么不同。
尽管NSString
类和NSMutableString
类是不同的类,但是它们有很多相似的地方。相对于构造两个完全独立的类,使用 继承(inheritance)特性是更好的选择。
类的继承
在自然世界中,分类学把各种动物分类到不同的种、属、科中。这些分类都是分等级的,就好像很多种属于同一个属,很多属又属于同一个科。
举个例子,大猩猩、人和猩猩,有很多明显相同的地方。尽管它们属于不同的种,甚至不同的属、群、亚科,但它们在生物学上都是有关联的,他们都属于同一个科“人科”。图1-1表示了它们的关系。
在面向对象编程中,对象被分类成有等级的集合。对象以 类 为单位编成小组,而不是像生物学一样用直接的专业术语分类。就像人类从父母身上继承了一些属性一样,类也可以从它的 父类(superclass) 中继承属性。
当一个类继承另一个类时, 子类(subclass) 能继承父类定义的所有的属性和方法。子类也可以定义自己的属性和方法,也可以 重载(override) 父类的方法。
在Objective-C的字符串类中,NSMutableString
的声明部分定义了NSMutableString
这个类继承自NSString
类。图1-2说明表示了这个关系。NSString
所有的属性和方法都能在NSMutableString
中使用,但NSMutableString
增加了一些方法,使你可以加长、插入、替换或删除字符串的单个字符或者 子串(substrings) 。
图1-2
提供最基本的功能的基类
就像所有活的有机体有着相同的"活"的属性一样,所有Objective-C的对象有着一些相同的最基本的功能。
当一个Objective-C对象需要和另一个类的对象共同工作时,它们就需要提供一些基本的属性和方法。为此,Objective-C定义了一个所有类都继承自它的类————基类NSObject
。当一个对象遇到另一个对象时,它们可以用来自NSObject
声明的方法进行交互。
当你定义一个Objective-C类时,这个类最起码要继承自NSObject
。而且,这个类要继承和它需要的功能最接近的Cocoa 或 Cocoa Touch的类。
例如,你想给你的iOS app自定义一个按钮,但是Objective-C中提供的UIButton
类并不能满足你的需求,你就可以创造一个继承自UIButton
的新的类,而不是继承自NSObject
。如果这个新的类是继承自NSObject
的,你就要重复写那些UIButton
已经提供了的功能。如果这个新的类是继承自UIButton
的,那么这个类就拥有了UIButton
的所有属性,只需要加上自己需要的功能就可以了。
UIButton
这个类本身是继承自UIControl
的,UIControl
描述了iOS系统中所有相同的用户界面操作。UIControl
又是继承自UIView
的,它定义了所有在屏幕上展示的对象的共有的属性和方法。UIView
又继承自UIResponder
,UIResponder
能够响应用户的操作,例如点(tap),手势(gesture)和摇晃(shake)。最后,在继承树的最底端,UIResponder
继承自NSObject
类。图1-3表示这些继承关系。
图1-3
这一连串的继承意味着一个新建的自定义的UIButton
的子类不但你能继承UIButton
声明的属性和方法,还能继承它每一个父类的属性和方法。这个新类的对象有着按钮的功能,可以在屏幕上展示,响应用户的操作,还能与所有Cocoa Touch的对象交互。
在使用一个类时,把它继承的关系链记在心中是非常重要的,这样做可以使我们明白这些类都能做些什么。在Cocoa 和 Cocoa Touch的类的参考文件中,我们可以很快地找到一个类和它的父类们。如果你在一个类的声明部分或者参考文件中找不到它的一个属性或者方法,或许你可以在它的父类中找到这个属性和方法。
类的声明部分定义了它和外界的交互
面向对象编程有很多好处,其中一个就是当你使用一个类时你只需要知道怎样和这个类的实例交互就可以了。准确来说,一个对象的内部实现方法是应该被隐藏起来的。
例如,如果你在你的iOS app中使用UIButton
,你不用担心按钮的像素是怎样显示的,你只需要知道怎么去改变这个按钮的一些属性,例如按钮的标题和颜色,当你把这个按钮加到屏幕上时,它就会准确地按照你的要求显示出来。
当你定义你自己的类时,你需要从定义这个类的公有属性和方法(Public)。哪些属性是可以被公开访问的?这些属性能被修改吗?其他对象怎么和你的对象交互?
这些信息需要在你的类的声明部分中说明,它一般用来定义这个类的公有方法。公有方法和私有方法是分开定义的。在Objective-C中,类的声明部分和实现部分一般是放在不同的文件中的(.h文件和.m文件),因此你只需要把 类的声明部分(interface) 公有就行了。
基本语法
在Objective-C语法中,类是这样定义的:
@interface SimpleClass : NSObject
@end
这个例子定义了一个继承自NSObject
的名叫SimpleClass
的类。
公有的属性和方法需要被定义在@interface
和@end
中间这个声明部分里面。在这个例子中,没有定义和父类不一样的属性和方法,所以SimpleClass
的对象所拥有的功能和NSObject
的一模一样。
属性(property)
对象通常都会有属性来让外界访问。举个例子,如果你要需要在你的app内表示一个人,你需要字符串来表示这个人的名字。
这些属性的声明需要写在@interface里面。
@interface Person : NSObject
@property NSString *firstName;
@property NSString *lastName;
@end
在这个例子里面,继承自NSObject
的类Person
拥有两个公有属性,而且两个都是NSString
类的对象。
这些属性都是Objective-C的对象,所以它们都用一个*
号(和C语言的指针一样)来指向它的地址。就像C语言的声明语句一样,这些声明语句都需要分号;
结尾。
你可能需要增加一个属性来表示这个人的生日年份,所以你可以使用一个数字类型的对象。
@property NSNumber *yearOfBirth;
但对于一个简单的年份数字来说,用NSNumber
类的对象来表示可能有点过了。我们还有一种方法,就是用C语言中的基本数据类型来存储这些简单的数据,例如整型:
@property int yearOfBirth;
属性的可读性和存储类型
目前为止,我们的例子展示了如何声明一个完全公有的属性。这意味着其他对象可以读和写这些属性的值。
在一些情况中,你需要声明一些不能被改变的属性。在现实世界中,一个人要改变他的名字是非常困难的。那么在我们的app中,我们可以把名字这个属性的可访问性设置为read-only,这样外部就无法直接改变这个属性的值,这个操作需要一个 中间对象(intermediary object) 间接进行。
Objective-C中可以声明 属性的特性property attributes,用来指示这个属性可读性和其他特性。在我们的app中,我们可以把Person
类的interface部分修改成这样:
@interface Person : NSObject
@property (readonly) NSString *firstName;
@property (readonly) NSString *lastName;
@end
属性的属性需要写在@property关键字的后面,用括号包起来。关于这部分的更多内容请参考Declare Public Properties for Exposed Data这一节。
方法的声明
目前为止,我们的例子描述了一个典型的对象,或者换句话说,一个简单的用来封装数据的对象。在Person
类中,不需要方法来操作它的属性。但是大多数的类,除了声明它的属性,还需要声明它的方法。
用Objective-C写的程序都是建立在巨大的各种对象的联系网之上的,因此对象通过发送信息来和其他对象交互是非常重要的。在Objective-C中,一个对象可以通过调用另一个对象的方法来传递信息。
Objective-C的方法(函数)和C语言以及其他语言的方法(函数)在概念上是相似的,但是定义的语法有点不同。在C语言中,方法的定义是这样的:
void SomeFunction();
同样的方法在Objective-C中是这样定义的:
-(void)someMethod;
这个方法没有参数。C语言的关键字void
在Objective-C中被放在了括号里面,而且放在方法声明的开始部分,用来表示这个方法在结束时不返回值。
减号(-
)用在方法声明的最开头位置,用来表示这是一个 实例方法(instance method),只能被类的实例对象所调用。你也可以声明一个 类方法(factory method),可以被类本身调用,详情请参考Objective-C Classes Are also Objects这一节。
和C语言一样,Objective-C的方法声明也需要一个分号;
结尾。
带参数的方法
如果你需要声明一个带一个或者多个参数的方法,Objective-C的语法和C语言的语法有着很大的区别。
在C语言中,参数在括号内具体说明,例如:
void SomeFunction(SomeType value);
在Objective-C中,只有参数类型用括号包起来,整个参数部分放在一个冒号:
后面,例如:
-(void)someMethodWithValue:(SomeType)value;
在这个例子中,SomeType
是参数的类型,value
是参数的名字。
如果你需要传入多个参数,语法和C语言就更加不一样了。C语言的多个参数都放在一个括号里面,用括号隔开;而在Objective-C中,带两个参数的方法的声明是这样的:
- (void)someMethodWithFirstValue:(SomeType)value1 secondValue:(AnotherType)value2;
在这个例子中,value1
和value2
是方法在实现时的内部变量名,它们和你调用这个方法时传入的参数一一对应。
在一些编程语言中,允许在方法的定义中加入 命名参数(named arguements) ;但是在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;
类的名字必须是唯一的
在一个app中,就算在不同的 类库(library) 和 框架(framework) 中,类的名字都必须是唯一的。如果你打算用工程中已存在的类的名字创造一个新的类,你会收到一个编译错误。因此,你最好在你这创造的类前用三到四个字母加上你的前缀,是你的类看起来是唯一的。这些字母可以和你的app有关,也可以和你的框架有关,还可以是你名字的首字母。接下来我们的例子都会在类名中使用前缀,就像这样:
@interface XYZPerson : NSObject
@property (readonly) NSString *firstName;
@property (readonly) NSString *lastName;
@end
历史小知识:如果你想知道为什么你遇到的类中为什么很多都有NS
的前缀,那就要从Cocoa 和 Cocoa Touch的历史说起了。Cocoa是NeXT公司(乔布斯在第一次离开苹果公司后创立的公司)为了在NeXTStep操作系统上写程序而开发的框架。乔布斯回归苹果后,苹果公司在1996年收购了NeXT公司,并且把大部分NeXTStep操作系统合并到了OS X操作系统,其中就包括了很多已完成的类。而Cocoa Touch则是作为Cocoa的替代物被引入到iOS操作系统的;尽管两个框架间有很多类都是不一样的,但仍有一些类在Cocoa 和 Cocoa Touch都能使用。像NS
和UI
(代表iOS的用户界面User Interface)这种两个字母的前缀就被苹果保留了起来。
相比较之下,方法名和属性名只需要在类里面保持唯一就可以了。尽管在C语言中函数名必须在一个工程中唯一,但是在Objective-C中我们鼓励在不同的类中使用相同的方法名。在同一个类的声明中,一个方法我们只能声明一次,但是如果你想重载一个继承而来的方法的话,你必须使用和父类中与该方法完全一致的方法名。
像方法一样,一个对象的属性和 实参(instace variable)只需要在同一个类中保持唯一就可以了(详情请见Most Properties Are Backed by Instance Variables)这一节。但是,如果你想使用全局变量,这些变量名就必须在app或者工程中是唯一的。
更多关于命名传统和建议请参考Conventions这一节。
方法的实现
当你写好了类的公有属性和方法之后,你就要开始写这个类的实现代码了。
一个类的声明部分通常放在一个叫 头文件(header file) 的文件中,这个文件一般以.h
作为扩展名。而你需要在以.m
为拓展名的文件里为你的类写实现的代码。
当类的声明代码写在头文件时,你需要在编译实现的代码前告诉编译器去读取这个类的声明代码。这就要使用Objective-C提供的预处理指令#import
了。这和C语言的#include
指令很像,但你要确保一个文件在编译时只能被include一次。
要注意预处理指令和传统的C语句不一样,是不需要在结尾加分号的。
基本语法
实现一个类的基本语法是这样写的:
#import "XYZPerson.h"
@implementation XYZPerson
@end
如果在.h声明部分声明了一些方法,你需要在这个文件里面实现它们。
实现一个方法
这是一个类有一个方法的类的声明部分:
@interface XYZPerson : NSObject
- (void)sayHello;
@end
那么这个类的实现就应该长这样:
#import "XYZPerson.h"
@implementation XYZPerson
- (void)sayHello {
NSLog(@"Hello, World!");
}
@end
这个例子使用了NSLog()
这个方法来在控制台打印一些信息。这个方法和C语言中的printf()
函数是一样的。NSLog()
方法可以传入一些参数,但第一个参数必须是Objective-C的字符串。
就像C语言的函数一样,Objective-C的方法用花括号{}
把方法的实现代码包起来。而且方法的名字、参数类型和返回类型都必须和声明时一模一样。
Objective-C和C语言一样,对方法名是敏感的,所以像这个方法:
- (void)sayhello {
}
和方法
-(void)sayHello{
}
会被编译器当作不一样的方法来处理。
总的来说,方法名的第一个字母应该是小写的。Objective-C和C语言的命名传统不一样,Objective-C会使用更多描述性的语言来为这个方法命名。如果一个方法名中有很多个单词,就应该使用 驼峰命名法(camel case) 来增强这个方法名的可读性。
要注意空格在Objective-C里是非常灵活的。我们要经常在代码块中用tab键或者space键来进行缩进,而且你会经常看到一个左花括号独占一行的写法:
- (void)sayHello
{
NSLog(@"Hello, World!");
}
苹果公司的官方开发工具(IDE)Xcode会根据开发者的偏好设置来自动缩进你的代码。如果你想知道怎么改变自动缩进和设置tab键的宽度,请参考Xcode Workspace Guide。
在下一章Working with Objects中你会看到更多方法实现的例子。
Objective-C的类也是一种对象
在Objective-C中,一个类本身就是这个类的一个对象,这种对象类型叫做class
。类本身不能像之前那样声明属性,但是它们却可以接收信息。
当一个类要调用方法时,它只能调用这个类的类方法(factory method)。这里典型的用法就是为对象 分配内存(allocation) 和 初始化(initialization),详情请参考Objects Are Created Dynamically。举个例子,在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;
从上面的例子中我们可以发现,类方法是用一个加号+
声明的,这和实例对象的方法用减号-
来声明不一样。
练习
注意:为了完成下面的练习,你将要在Xcode中创建一个新的工程,这样可以确保你写的代码不会出现编译错误。 创建Xcode新工程时在OS X Application模版中选择创建Command Line Tool。
1.用Xcode新建一个Objective-C语言的类的interface和implementation文件,类名为XYZPerson
,继承自NSObject
。
2.在这个类的声明部分加入属性,来表示这个人的名(first name)和姓(last name),还有出生的日期(用NSDate类型)。
3.声明一个叫sayHello
的方法并且实现它。
4.声明一个叫person
的类方法。不用实现这个方法。
注意:如果你编译你的代码,你会得到一个Incomplete implementation的警告,那是因为你没有实现这个类方法。不用理会它。