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 所示。

objc_method.png

Figure 1: 声明方法

最开头的负号(-)表示这是一个“实例方法”;如果写为正号(+),则表示这是一个“类方法”。

2.2.2. @implementation 部分

@implementation 部分的一般格式如下:

@implementation NewClassName   // 这行也可以写为 @implementation NewClassName: ParentClassName

{
    memberDeclarations;        // 定义实例变量(它们是私有的)
}

methodDefinitions;
@end

2.2.3. 实例变量的访问及数据的封装

在前面类 Fraction 所示例子中,可以通过 setNumeratorsetDenominator 方法可以给两个实例变量 numeratordenominator 设定值。如何在 main 方法中直接访问这两个实例变量的值呢?我们可以创建两个名为 numeratordenominator 的新方法用于访问相应的 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

在分类中定义的方法会被其子类继承;在扩展中定义的属性或方法不会被子类继承。

参考:https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html#//apple_ref/doc/uid/TP40011210-CH6-SW3

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协议中定义的方法。

Author: cig01

Created: <2018-06-30 Sat>

Last updated: <2018-09-23 Sun>

Creator: Emacs 27.1 (Org mode 9.4)