Objective-C
Table of Contents
1. Objective-C 简介
Objective-C 是 C 语言的简单扩展,它在 C 语言基础上增加了与 Smalltalk 语言相似的类以及消息发送机制。
参考:
Programming with Objective-C: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Introduction/Introduction.html
本文主要摘自:Objective-C 程序设计(第 4 版)
1.1. Hello World 程序
下面是使用 Objective-C(源文件往往以.m 为后缀)编写的 Hello World 程序:
// file: hello.m #import <Foundation/Foundation.h> // import和include类似,import效率更高 int main(int argc, const char * argv[]) { @autoreleasepool { // 表示在“自动释放池”的语境中执行 // insert code here... NSLog(@"Hello, World!"); // 字符串前面的@表示“NSString字符串对象” } return 0; }
编译和执行上面程序:
$ clang -fobjc-arc -framework Foundation hello.m -o hello # 编译 $ ./hello 2018-09-08 14:56:57.228 hello[1070:14758] Hello, World!
1.2. 数据类型
1.2.1. id 类型(任意类型)
id 数据类型可存储任何类型的对象。id 类型是 Objective-C 中十分重要的特性,它是多态和动态绑定的基础。
如:
id myObject; // 将myObject声明为id类型的变量
又如:
- (id) newObject: (int) type; // 方法newObject:具有id类型的返回值,后文将说明类方法定义格式
2. 类、对象和方法
Objective-C 采用下面的语法调用类或实例的方法:
[ClassOrInstance method];
请求一个类或实现来执行某个操作时,就是在向它发送一个消息,消息的接收者称为接收者。因此,有另一种方式可以表示上面所描述的一般格式,具体如下:
[receiver message];
2.1. 实例化对象
要创建类的一个新实例,可以调用类的 alloc
方法(它返回对象的指针),如:
ClassName *myObject; myObject = [ClassName alloc];
在使用对象之前,需要将其初始化。 init
方法可以完成这个任务(它返回新初始化的对象),如:
ClassName *myObject; myObject = [ClassName alloc]; [myObject init];
我们往往使用下面代码来创建对象:
ClassName *myObject = [[ClassName alloc] init];
2.2. 创建自己的类
假设要编写一个用于处理分数的程序,可能需要处理加、减、乘、除等运算。我们可以从一个简单的程序开始,代码如下:
#import <Foundation/Foundation.h> int main(int argc, const char * argv[]) { @autoreleasepool { int numerator = 1; int denominator = 3; NSLog(@"The fraction is %i/%i", numerator, denominator); // 输出 The fraction is 1/3 } return 0; }
我们把上面的程序提炼出一个 Fraction 类,改写为面向对象的方式。代码如下:
// 前面程序的类版本 #import <Foundation/Foundation.h> @interface Fraction: NSObject // 自定义类继承自基类NSObject - (void) print; - (void) setNumerator: (int) n; - (void) setDenominator: (int) d; @end @implementation Fraction { int numerator; // 实例变量放在@implementation部分时,它们都是private的 int denominator; // 实例变量也可以放在@interface部分,但它们会变为public的 } - (void) print { NSLog(@"%i/%i", numerator, denominator); } - (void) setNumerator: (int) n { numerator = n; } - (void) setDenominator: (int) d { denominator = d; } @end int main(int argc, const char * argv[]) { @autoreleasepool { Fraction *myFraction = [[Fraction alloc] init]; // 设置分类为 1/3 [myFraction setNumerator:1]; // 方法名和参数之间用冒号分开 [myFraction setDenominator:3]; // 使用打印方法显示分数 NSLog(@"The fraction is:"); [myFraction print]; } return 0; }
上面程序可分为以下 3 个部分:
1、@interface 部分
2、@implementation 部分
3、program 部分
其中,@interface 部分用于描述类和类的方法;@implementation 部分用于描述数据(类对象的实例变量存储的数据),并实现在接口中声明方法的实际代码;program 部分的代码用于实现程序的业务逻辑。
2.2.1. @interface 部分
@interface 部分用于描述类和类的方法,这部分的一般格式类似于下列语句:
@interface NewClassName: ParentClassName propertyAndMethodDeclarations; @end
关于属性(property)后面会介绍。声明一个方法的语法如 1 所示。
Figure 1: 声明方法
最开头的负号(-)表示这是一个“实例方法”;如果写为正号(+),则表示这是一个“类方法”。
2.2.2. @implementation 部分
@implementation 部分的一般格式如下:
@implementation NewClassName // 这行也可以写为 @implementation NewClassName: ParentClassName { memberDeclarations; // 定义实例变量(它们是私有的) } methodDefinitions; @end
2.2.3. 实例变量的访问及数据的封装
在前面类 Fraction 所示例子中,可以通过 setNumerator
和 setDenominator
方法可以给两个实例变量 numerator
和 denominator
设定值。如何在 main
方法中直接访问这两个实例变量的值呢?我们可以创建两个名为 numerator
和 denominator
的新方法用于访问相应的 Fraction 实例变量。以下是这两个新方法的声明:
- (int) numerator; - (int) denominator;
下面是定义:
- (int) numerator { return numerator; } - (int) denominator { return denominator; }
注意, 方法名和实例变量名是相同,这样做不存在任何问题(虽然似乎在些奇怪)。事实上,这是很常见的情况。
2.3. 分离接口和实现文件
把类的声明和实现放在单独的文件中是比较好的工程实践。
我们改造前面的例子。首先把类声明放入 Fraction.h 中:
// 接口文件 Fraction.h #import <Foundation/Foundation.h> @interface Fraction: NSObject - (void) print; - (void) setNumerator: (int) n; - (void) setDenominator: (int) d; - (int) numerator; - (int) denominator; @end
类的实现放入 Fraction.m 中:
// 实现文件 Fraction.m #import "Fraction.h" @implementation Fraction { int numerator; int denominator; } - (void) print { NSLog(@"%i/%i", numerator, denominator); } - (void) setNumerator: (int) n { numerator = n; } - (void) setDenominator: (int) d { denominator = d; } - (int) numerator { return numerator; } - (int) denominator { return denominator; } @end
主程序 main.m:
// 主程序 main.m #import "Fraction.h" int main(int argc, const char * argv[]) { @autoreleasepool { Fraction *myFraction = [[Fraction alloc] init]; // 设置分类为 1/3 [myFraction setNumerator:1]; [myFraction setDenominator:3]; // 使用打印方法显示分数 NSLog(@"The fraction is:"); [myFraction print]; } return 0; }
编译和运行:
$ clang -fobjc-arc -framework Foundation Fraction.m main.m -o main $ ./main 2018-09-09 16:50:57.073 main[1521:19406] The fraction is: 2018-09-09 16:50:57.073 main[1521:19406] 1/3
2.4. 合成存取方法(@property, @synthesize)
从 Objective-C 2.0 开始,可自动生成设值方法和取值方法(统称为存取方法)。
第一步,在接口部分中使用 @property
指令标识属性。
第二步,在实现部分中使用 @synthesize
指令让其自动生成存取方法(如果属性名为 x
,则生成的设值方法为 setX
,生成的取值方法为 x
。
前面例子中接口文件 Fraction.h 可简化为:
// 接口文件 Fraction.h #import <Foundation/Foundation.h> @interface Fraction: NSObject @property int numerator, denominator; - (void) print; @end
实现文件 Fraction.m 可简化为:
// 实现文件 Fraction.m #import "Fraction.h" @implementation Fraction // 自动生成相应的存取方法 setNumerator/numerator/setDenominator/denominator @synthesize numerator, denominator; - (void) print { NSLog(@"%i/%i", numerator, denominator); } @end
2.5. 使用点运算符访问属性
Objective-C 语言允许你使用非常简便的语法访问属性。要获得 myFraction 中存储的 numerator 的值,可使用以下语句:
[myFraction numerator]
这会向 myFractlon 对象发送 numerator 消息,从而返回所需的值。在 Objective-C 中也可以使用点运算符编写以下等价的表达式:
myFraction.numerator // 等价于 [myFraction numerator]
对于赋值也有类似的语法:
instance.property = value // 等价于 [instance setProperty: value]
这等价于编写以下表达式:
[instance setProperty: value]
前面例子中:
[myFraction setNumerator:1]; [myFraction setDenominator:3];
等价于:
myFraction.numerator = 1; myFraction.denominator = 3;
2.6. 具有多个参数的方法
让我们继续使用 Fraction 类。如果有一个方法只用一条消息同时设置 numerator 和 denominator,就太好了。
通过列出每个连续的参数并用冒号将其连起来,就可以定义一个接收多个参数的方法。
下面例子中演示了如何声明和实现具有多个参数的方法:
接口文件 Fraction.h:
// 接口文件 Fraction.h #import <Foundation/Foundation.h> @interface Fraction: NSObject @property int numerator, denominator; - (void) print; - (void) setNumerator: (int) n andDenominator: (int) d; // 声明具有多个参数的方法 @end
实现文件 Fraction.m:
// 实现文件 Fraction.m #import "Fraction.h" @implementation Fraction @synthesize numerator, denominator; - (void) print { NSLog(@"%i/%i", numerator, denominator); } - (void) setNumerator: (int) n andDenominator: (int) d { // 实现具有多个参数的方法 numerator = n; denominator = d; } @end
主程序 main.m:
// 主程序 main.m #import "Fraction.h" int main(int argc, const char * argv[]) { @autoreleasepool { Fraction *myFraction = [[Fraction alloc] init]; // 设置分类为 1/3 [myFraction setNumerator:1 andDenominator: 3]; // 使用具有多个参数的方法 // 使用打印方法显示分数 NSLog(@"The fraction is:"); [myFraction print]; } return 0; }
2.7. self 关键字
关键字 self
用来指明对象是当前方法的接收者,类似于 C++中的 this
。
3. Blocks
块是对 C 语言的一种扩展,由 Apple 公司添加到语言中的。块看起来更像是函数,这样的语法需要一些时间来适应。可以给块传递参数,正如给函数传递一样。块也具有返回值。与函数不同的是,块定义在函数或者方法内部,并能够访问在函数或者方法范围内块之外的任何变量。一般来说,这些变量能够访问但是并不能够改变这些变量的值。有一个特殊的块修改器( __block
)能够修改块内变量的值,后面会简短介绍如何使用。
下面看一个简单的例子,假设有个 printMessage 函数:
void printMessage(void) { NSLog(@"Programming is fun."); }
下面块能够完成同样的任务:
^(void) { NSLog(@"Programming is fun."); }
块是以插入字符 ^
开头为标识的。后面跟的一个括号表示块所需要的参数列表。 在这个例子中,块并没有参数,所以在函数定义中仅需填入 void。
同样,也可以将这个块赋给一个名为 printMessage 的变量,只要变量声明正确:
void (^printMessage)(void) = ^(void) { NSLog(@"Programming is fun."); };
等号左边表示 printMessage 指向一个没有参数和返回值的块指针。需要注意的是,赋值语句是以分号终止的。
执行一个变量引用的块,与函数的调用方式一致:
printMessage();
下面是一个完整的例子:
#import <Foundation/Foundation.h> int main (int argc, char *argv[]) { @autoreleasepool { void (^printMessage)(void) = ^(void) { NSLog(@"Programming is fun."); }; printMessage(); // 输出 Programming is fun. } return 0; }
块可以访问当前块外、函数或者方法范围内的变量。如:
#import <Foundation/Foundation.h> int main (int argc, char *argv[]) { @autoreleasepool { int foo = 10; void (^printFoo)(void) = ^(void) { NSLog(@"foo = %i", foo); // 访问了块外的变量 }; foo = 15; printFoo(); // 会输出10,还是15呢? } return 0; }
运行上面程序,发现 printFoo()
输出了数字 10。这是因为变量在定义块时已具有值了,而不是在块执行的时候。
不可以在块内修改已经定义过变量的值。
1: #import <Foundation/Foundation.h> 2: 3: int main (int argc, char *argv[]) { 4: @autoreleasepool { 5: int foo = 10; // 修改为 __block int foo = 10; 可通过编译 6: 7: void (^printFoo)(void) = 8: ^(void) { 9: NSLog(@"foo = %i", foo); 10: 11: foo = 20; // 会报错,不能修改块外变量foo 12: }; 13: 14: foo = 15; 15: 16: printFoo(); 17: 18: NSLog(@"foo = %i", foo); 19: } 20: 21: return 0; 22: }
如何定义变量时指定 __block
,则该变量可以在块内被修改。比如上面代码的第 5 行如何换为:
__block int foo = 10;
则编译不会报错,程序运行会输出:
foo = 15 foo = 20
4. 分类和协议
4.1. 分类(Category)
如何扩展一个类的功能呢?显然,我们可以基于它定义一个子类,在子类中增加相应的方法。除此外,我们还有另一个选择:分类。 分类(Category)提供了扩展现有类定义的简便方式。
分类的语法为:
@interface ClassName (CategoryName) // 声明增加的方法 @end @implementation ClassName (CategoryName) // 实现增加的方法 @end
下面是分类的一个例子(代码摘自:https://www.tutorialspoint.com/objective_c/objective_c_categories.htm ):
#import <Foundation/Foundation.h> @interface NSString(MyAdditions) + (NSString *) getCopyRightString; @end @implementation NSString(MyAdditions) + (NSString *) getCopyRightString { return @"Copyright TutorialsPoint.com 2013"; } @end int main(int argc, const char * argv[]) { @autoreleasepool { NSString *copyrightString = [NSString getCopyRightString]; NSLog(@"Accessing Category: %@",copyrightString); } return 0; }
上面程序比较简单,我们把所有代码放到了一个文件中。我们最好接口和实现放入到不同的文件中,比如:
NSStringMyAdditions.h // 分类的@interface部分 NSStringMyAdditions.m // 分类的@implementation部分
也有一些程序员使用符号 +
来分隔类和分类的名字,如:
NSString+MyAdditions.h // 分类的@interface部分 NSString+MyAdditions.m // 分类的@implementation部分
4.2. 扩展(Extension)
创建一个未命名的分类(Anonymous Category),即在括号 ()
之间不指定名字,这种特殊的语法被称为类的扩展(Extension)。如:
@interface XYZPerson () // 这是扩展(Extension) @property NSObject *extraProperty; @end
在分类中定义的方法会被其子类继承;在扩展中定义的属性或方法不会被子类继承。
4.3. 协议(@protocol)
协议是多个类共享的一个方法列表。
定义一个协议很简单:只要使用 @protocol
指令,后面跟上你给出的协议名称;然后声明一些方法; @end
指令之前的所有方法声明都是协议的一部分。
Foundation 框架中已有一些协议,其中一个名为 NSCopying
,下面是 Foundation 头文件 NSObject.h 中定义 NSCopying
协议的方式:
@protocol NSCopying - (id) copyWithZone: (NSZone *) zone; @end
如果你的类采用 NSCopying
协议,则必须实现名为 copyWithZone:
的方法。 *通过在 @interface
行的一对尖括号 <>
内列出协议的名称,可以告诉编译器你正在采用一个协议。这项协议的名称放在类名和它的父类名称之后,语句如下:
@interface AddressBook: NSObject <NSCopying> // 声明AddressBook采用协议NSCopying
上一行表明 AddressBook 的父类为 NSObject,且 AddressBook 遵守 NSCopying
协议,所以必须实现部分定义协议规定的 copyWithZone:
方法。
如果你的类采用多项协议,只需把它们都列出在尖括号中,并用逗号分开,语法如下:
@interface AddressBook: NSObject <NSCopying, NSCoding>
4.3.1. 协议中的可选方法(@optional)
默认地,协议中的方法是必须实现的。使用 @optional
指令可使协议中的方法变为“选择实现”,如:
@protocol XYZPieChartViewDataSource - (NSUInteger)numberOfSegments; // 必须实现 - (CGFloat)sizeOfSegmentAtIndex:(NSUInteger)segmentIndex; // 必须实现 @optional - (NSString *)titleForSegmentAtIndex:(NSUInteger)segmentIndex; // 选择实现 @end
@required
指令后的方法是必须实现的,如:
@protocol XYZPieChartViewDataSource - (NSUInteger)numberOfSegments; - (CGFloat)sizeOfSegmentAtIndex:(NSUInteger)segmentIndex; // 必须实现 @optional - (NSString *)titleForSegmentAtIndex:(NSUInteger)segmentIndex; // 选择实现 - (BOOL)shouldExplodeSegmentAtIndex:(NSUInteger)segmentIndex; // 选择实现 @required - (UIColor *)colorForSegmentAtIndex:(NSUInteger)segmentIndex; // 必须实现 @end
4.3.2. 测试对象是否遵守协议
可以使用 conformsToProtocol: 方法检查一个对象是否遵守某项协议。 例如,如果有一个名为 currentObject
的对象,想要查看它是否遵守名为 Drawing
协议:
@protocol Drawing - (void) paint; - (void) erase; @optional - (void) outline; // 协议中的可选方法 @end
测试代码为:
id currentObject; ...... if ([currentObject conformsToProtocol: @protocol (Drawing)] == YES) { // currentObject遵守协议Drawing }
这里使用的专用 @protocol
指令用于根据协议名称产生一个 Protocol 对象, conformsToProtocol:
方法期望这个对象作为它的参数。
使用 respondsToSelector: 可以检查一个对象是否实现某个方法。 比如下面代码可测试 currentObject
对象是否实现了 outline
方法:
if ([currentObject respondsToSelector: @selector (outline)] == YES) { // currentObject实现了outline方法 }
通过在类型名称之后的尖括号内添加协议名称,可以借助编译器来检查变量的一致性,如:
id <Drawing> currentObject; // 编译器会检查currentObject是否遵守协议Drawing
4.3.3. 协议继承其它协议
定义一项协议时,可以扩展现有协议的定义,语法实例:
@protocol Drawing3D <Drawing> ... @end
上面表示Drawing3D协议也采用Drawing协议,任何采用Drawing3D协议的类还需要实现Drawing协议中定义的方法。