上一章Objective-C编程快速入门教程请查看:OC入门编程介绍
定义类当你为OS X或iOS编写软件时,你的大部分时间都花在处理对象上。Objective-C中的对象就像其他面向对象编程语言中的对象:它们用相关的行为和数据封装。
应用程序被构建为一个由相互连接的对象组成的大型生态系统,这些对象相互通信以解决特定的问题,如显示可视界面、响应用户输入或存储信息。对于OS X或iOS开发,你不需要从头创建对象来解决所有可能的问题;
相反,你可以使用Cocoa(针对OS X)和Cocoa Touch(针对iOS)提供的大量现有对象库。
其中一些对象可以立即使用,如字符串和数字等基本数据类型,或按钮和表视图等用户界面元素。有些是为你设计的,可以使用你自己的代码进行自定义,以按照你需要的方式运行。应用程序开发过程包括决定如何最好地将底层框架提供的对象与你自己的对象进行自定义和组合,从而为应用程序提供独特的特性和功能。
在面向对象编程术语中,对象是类的实例。本章通过声明一个接口来演示如何在Objective-C中定义类,这个接口描述了你想要使用类及其实例的方式。这个接口包含类可以接收的消息列表,因此你还需要提供类实现,它包含响应每个消息时要执行的代码。
类是对象的蓝图类描述任何特定类型对象的公共行为和属性。对于一个string对象(在Objective-C中,这是类NSString的一个实例),该类提供了各种方法来检查和转换它所表示的内部字符。类似地,用于描述数字对象(NSNumber)的类提供了围绕内部数字值的功能,例如将该值转换为不同的数字类型。
以相同的方式从同一个蓝图构建的多个建筑物在结构上是相同的,一个类的每个实例与该类的所有其他实例共享相同的属性和行为。每个NSString实例的行为都是相同的,不管它持有的内部字符串是什么。
任何特定的对象都被设计成以特定的方式使用。你可能知道string对象表示一些字符串,但不需要知道用于存储这些字符的确切内部机制。你不知道任何关于使用的内部行为对象本身工作直接与它的字符,但是你需要知道你预计将与对象交互,也许问它特定的字符或请求一个新对象的所有原始字符转换为大写。
在Objective-C中,类接口精确地指定了给定类型的对象如何被其他对象使用。换句话说,它定义了类实例和外部世界之间的公共接口。
可变性决定所表示的值是否可以更改
有些类定义不可变的对象。这意味着在创建对象时必须设置内部内容,其他对象随后不能更改这些内容。在Objective-C中,所有基本的NSString和NSNumber对象都是不可变的。如果你需要表示一个不同的数字,你必须使用一个新的NSNumber实例。
一些不可变类还提供了一个可变的版本。如果你特别需要在运行时更改字符串的内容,例如在网络连接接收字符时附加字符,那么你可以使用NSMutableString类的一个实例。该类的实例的行为类似于NSString对象,只是它们还提供了更改对象所代表的字符的功能。
虽然NSString和NSMutableString是不同的类,但它们有很多相似之处。与其从头开始编写两个完全独立的类,而碰巧具有一些类似的行为,不如使用继承。
类从其他类继承
在自然界中,分类法用物种、属和科等术语将动物分类。这些组是分级的,这样多个物种可能属于一个属,多个属属于一个科。
例如,大猩猩、人类和猩猩有许多明显的相似之处。虽然它们各自属于不同的种类,甚至不同的属、部落和亚科,但它们在分类上是相关的,因为它们都属于同一科(称为人科),如图1-1所示。
图1-1种间分类关系
文章图片
在面向对象编程的世界中,对象也被分为层次结构的组。对象被简单地组织到类中,而不是使用不同的术语来表示不同的层次层次,如属或种。就像人类作为人科成员继承某些特性一样,可以将类设置为继承父类的功能。
当一个类从另一个类继承时,子类继承父类定义的所有行为和属性。它还可以定义自己的附加行为和属性,或者覆盖父类的行为。
对于Objective-C字符串类,NSMutableString的类描述指定该类继承自NSString,如图1-2所示。NSString提供的所有功能在NSMutableString中都是可用的,例如查询特定字符或请求新的大写字符串,但是NSMutableString添加了允许你追加、插入、替换或删除子字符串和单个字符的方法。
图1-2 NSMutableString类继承
文章图片
根类提供基本功能
就像所有生命体都具有一些基本的“生命”特征一样,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,如图1-3所示。
图1-3 UIButton类继承
文章图片
这个继承链意味着UIButton的任何自定义子类不仅会继承UIButton本身声明的功能,还会依次继承每个超类继承的功能。你最终会得到一个对象的类,它的行为类似于一个按钮,可以在屏幕上显示自己,响应用户输入,并与任何其他基本的Cocoa Touch对象通信。
对于需要使用的任何类,务必记住继承链,以便准确地确定它可以做什么。例如,为Cocoa和Cocoa Touch提供的类引用文档允许从任何类轻松导航到它的每个超类。如果你在一个类接口或引用中找不到要查找的内容,则很可能在链的更上层的超类中定义或记录它。
类的接口定义了预期的交互面向对象编程的诸多好处之一就是前面提到的思想—为了使用类,你需要知道的只是如何与它的实例交互。更具体地说,应该设计一个对象来隐藏其内部实现的细节。
例如,如果你在iOS应用程序中使用一个标准的UIButton,你不需要担心像素是如何操作的,这样按钮就会出现在屏幕上。你需要知道的是,你可以更改某些属性,例如按钮的标题和颜色,并相信当你将其添加到可视界面时,它将正确显示,并以你期望的方式运行。
在定义自己的类时,你需要首先确定这些公共属性和行为。你希望公开访问哪些属性?你应该允许更改这些属性吗?其他对象如何与类的实例通信?
此信息进入类的接口—它定义了你希望其他对象与类实例交互的方式。公共接口与构成类实现的类的内部行为是分开描述的。在Objective-C中,接口和实现通常被放在单独的文件中,这样你只需要让接口公开。
基本语法
用来声明类接口的Objective-C语法如下:
@interface SimpleClass : NSObject
@end
这个例子声明了一个名为SimpleClass的类,它继承自NSObject。
公共属性和行为在@interface声明中定义。在本例中,除了超类之外没有指定任何内容,因此SimpleClass实例中惟一可用的功能是继承自NSObject的功能。
属性控制对对象值的访问
对象通常具有用于公共访问的属性。例如,如果你在记录应用程序中定义了一个类来表示一个人,那么你可能需要为表示一个人的姓和名的字符串定义属性。
应该在接口中添加这些属性的声明,如下所示:
@interface Person : 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中完整地进行了描述。
方法声明指示对象可以接收的消息
到目前为止的例子涉及到一个描述典型模型对象的类,或者一个主要用来封装数据的对象。对于Person类,除了能够访问两个声明的属性外,可能不需要任何功能。但是,大多数类除了声明的属性之外,还包含行为。
鉴于Objective-C软件是由一个庞大的对象网络构建而成的,需要注意的是这些对象可以通过发送消息来相互交互。在Objective-C语言中,一个对象通过调用另一个对象上的方法向另一个对象发送消息。
Objective-C方法在概念上与C语言和其他编程语言中的标准函数相似,尽管语法有很大的不同。C函数声明是这样的:
void SomeFunction();
等价的Objective-C方法声明如下:
- (void)someMethod;
在这种情况下,方法没有参数。C void关键字在声明开头的圆括号中使用,表示方法完成后不返回任何值。
方法名前面的负号(-)表示它是一个实例方法,可以在类的任何实例上调用。这将它与类方法区别开来,类方法可以在类本身上调用,正如Objective-C类中所描述的,类也是对象。
与C函数原型一样,Objective-C类接口中的方法声明与任何其他C语句一样,都需要一个终止分号。
方法可以接受参数
如果需要声明一个方法来获取一个或多个参数,其语法与典型的C函数非常不同。
对于C函数,参数在括号内指定,如下所示:
void SomeFunction(SomeType value);
Objective-C方法声明包含参数作为其名称的一部分,使用冒号,如下所示:
- (void)someMethodWithValue:(SomeType)value;
与返回类型一样,参数类型也是在括号中指定的,就像标准的C类型转换一样。
如果需要提供多个参数,语法也与C非常不同。C函数的多个参数在圆括号中指定,用逗号分隔; 在Objective-C中,带有两个参数的方法的声明如下:
- (void)someMethodWithFirstValue:(SomeType)value1 secondValue:(AnotherType)value2;
在本例中,value1和value2是实现中用于访问方法调用时提供的值的名称,就好像它们是变量一样。
一些编程语言允许使用所谓的命名参数来定义函数; 需要注意的是,在Objective-C中不是这样的。方法调用中参数的顺序必须与方法声明匹配,实际上方法声明的secondValue:部分是方法名称的一部分:
someMethodWithFirstValue:secondValue:
这是使Objective-C成为可读语言的特性之一,因为通过方法调用传递的值是内联指定的,就在方法名的相关部分旁边,如你可以为方法参数传递对象中所述。
注意:上面使用的value1和value2值名严格来说不是方法声明的一部分,这意味着没有必要在声明中使用与实现中完全相同的值名。惟一的要求是签名匹配,这意味着必须保持方法的名称、参数和返回类型完全相同。
举个例子,这个方法和上面的方法有相同的签名:
- (void)someMethodWithFirstValue:(SomeType)info1 secondValue:(AnotherType)info2;
这些方法具有与上述方法不同的签名
- (void)someMethodWithFirstValue:(SomeType)info1 anotherValue:(AnotherType)info2;
- (void)someMethodWithFirstValue:(SomeType)info1 secondValue:(YetAnotherType)info2;
类名必须是唯一的
需要注意的是,每个类的名称在应用程序中必须是唯一的,即使是在包含的库或框架中也是如此。如果试图创建与项目中现有类同名的新类,则会收到编译器错误。
出于这个原因,建议在你定义的任何类的名称前面加上前缀,使用三个或更多的字母。这些字母可能与你当前正在编写的应用程序有关,或者与可重用代码框架的名称有关,或者仅仅与你的首字母有关。
本文档其余部分给出的所有示例都使用类名前缀,如下所示:
@interface XYZPerson : NSObject
@property (readonly) NSString *firstName;
@property (readonly) NSString *lastName;
@end
历史注释:如果你想知道为什么这么多你遇到的类都有NS前缀,那是因为Cocoa和Cocoa Touch的历史。Cocoa最初是用来为NeXTStep操作系统构建应用程序的框架。当苹果在1996年收购NeXT时,NeXTStep的大部分内容都被整合到OS X中,包括现有的类名。Cocoa Touch作为Cocoa的iOS版本引入; 有些类在Cocoa和Cocoa Touch中都可用,尽管每个平台也有大量独特的类。
两个字母的前缀,如NS和UI(用于iOS上的用户界面元素),被苹果保留使用。
相反,方法和属性名只需要在定义它们的类中是唯一的。尽管应用程序中的每个C函数都必须有唯一的名称,但是对于多个Objective-C类来说,用相同的名称定义方法是完全可以接受的(通常也是可取的)。你不能在同一个类声明中多次定义方法,但是,如果你希望覆盖从父类继承的方法,则必须使用原始声明中使用的名称。
与方法一样,对象的属性和实例变量(在大多数属性中由实例变量支持)只需要在定义它们的类中是惟一的。但是,如果使用全局变量,这些变量必须在应用程序或项目中惟一命名。
在约定中给出了进一步的命名约定和建议。
类的实现提供了它的内部行为一旦定义了类的接口,包括用于公共访问的属性和方法,就需要编写代码来实现类行为。
如前所述,类的接口通常放在一个专用文件中,通常称为头文件,它通常具有文件名扩展名.h。在扩展名为.m的源代码文件中为Objective-C类编写实现。
无论何时在头文件中定义接口,你都需要告诉编译器在尝试编译源代码文件中的实现之前读取它。Objective-C为此提供了一个预处理指令#import。它类似于c# include指令,但确保在编译期间只包含一个文件。
注意,预处理程序指令与传统的C语句不同,并且不使用终止分号。
基本语法
为类提供实现的基本语法如下:
#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字符串。
方法实现类似于C函数定义,因为它们使用大括号来包含相关代码。此外,方法的名称必须与其原型相同,参数和返回类型必须完全匹配。
Objective-C继承了C的大小写敏感性,所以这个方法:
- (void)sayhello {
}
将被编译器视为与前面显示的sayHello方法完全不同的方法。
通常,方法名应该以小写字母开头。Objective-C约定对方法使用比典型C函数更多的描述性名称。如果方法名包含多个单词,则使用驼峰大小写(每个新单词的第一个字母大写)使其易于阅读。
还要注意,空格在Objective-C中是灵活的。习惯上使用制表符或空格来缩进代码块中的每一行,你会经常在单独的一行中看到左大括号,就像这样:
- (void)sayHello
{
NSLog(@"Hello, World!");
}
用于创建OS X和iOS软件的集成开发环境(IDE) Xcode将根据一组可定制的用户首选项自动缩进你的代码。有关更多信息,请参阅更改Xcode工作区指南中的缩进和选项卡宽度。
在下一章中,你将看到更多使用对象的方法实现示例。
Objective-C类也是对象在Objective-C中,类本身就是一个对象,它有一个不透明的类类型。类不能使用前面为实例显示的声明语法定义属性,但是它们可以接收消息。
类方法的典型用法是作为工厂方法,它是动态创建对象中描述的对象分配和初始化过程的替代方法。例如,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;
如这些示例所示,使用+号表示类方法,这将它们与使用-号的实例方法区分开来。
类方法原型可以包括在类接口中,就像实例方法原型一样。类方法是在类的@implementation块内以与实例方法相同的方式实现的。
练习注意:为了遵循每一章末尾给出的练习,你可能希望创建一个Xcode项目。这将允许你确保你的代码编译没有错误。
使用Xcode的新项目模板窗口从可用的OS X应用程序项目模板创建命令行工具。当出现提示时,将项目的类型指定为Foundation。
- 使用Xcode的新文件模板窗口为一个名为XYZPerson的Objective-C类创建接口和实现文件,它继承自NSObject。
- 向XYZPerson类接口添加人名、姓氏和出生日期(日期由NSDate类表示)的属性。
- 声明sayHello方法并实现它,如本章前面所示。
- 添加一个类工厂方法的声明,称为“person”。在阅读下一章之前,不要担心如何实现这个方法。
推荐阅读
- 使用对象 – Objective-C编程快速入门教程
- 入门编程介绍 – Objective-C编程快速入门教程
- Core Foundation编程概念全解
- Objective-C线程技术(线程同步和线程安全)
- Objective-C运行时Runtime完全解读
- iOS内存管理(引用计数、Runloop、AutoreleasePool和引用循环)
- 快速了解iOS内存管理
- 内存管理之(__bridge、__bridge_transfer、__bridge_retained)
- GUI编程基本原理之(event loop和run loop(运行循环))