Overview 总览
Concepts 概念
我们直接来看 Objective-C 与 Swift 最显著的区别。我真的很想用代码来说明,但在此之前有一些你必须了解的事情,所以请多担待,我保证马上就有代码可以读啦!
了解 Objective-C 最重要的一点是:它是 C 语言的一个严格超集,C 是一个40多岁的语言。这意味着,有效的 C 语言代码,也是有效的 Objective-C 代码,你可以自由地混编。你甚至可以使用 C++ 与 Objective-C 混编,也就是我们所说的 Objective-C++,但这并不常见。
C 和 C++ 有很多传统的东西,C 和 Objective-C 汇集在一起的地方有点粗糙,但确实,在基于 Objective-C 的项目中,使用 C 和 C++ 的代码是非常容易的。我在 iOS 平台编写的第一个游戏是用 Objective-C++ 编写的,其中用了少量的 Objective-C 代码用来调用底层 C++ 代码。
第一个显而易见的传统是「头文件」:当你在 Objective-C 中创建一个类时,它由 YourClass.h(头文件)和 YourClass.m(实现文件)组成。 “m” 最初代表 「消息」,但现在我们认为它是 “iMplementation” 文件。你的「头文件」描述了暴露给外部世界的「类」:可以访问的「属性」和可以调用的「方法」。你的「实现文件」是你为这些方法编写实际代码的地方。
在 Swift 中,不是把「类」或「结构体」分割成 “.h” 和 “.m” 文件,而是在单个文件中创建。但在 Objective-C 中,分割两者的概念非常重要。当你想使用某一个类时,编译器只需要读取 “.h” 文件,就可以知道怎么使用这个类�。这个机制保证了你使用的是封闭的源码组件,这一点类似Google的分析库:同样给了你一个 “.h” 的头文件,它描述了组件之间如何工作;以及 “.a” 文件,其中包含真正为实现某一功能所写的源码。
第二个明显的「历史遗留」是 C 语言的「预处理」。如果你对它的工作原理感兴趣,我将在本书后面一个独立的章节来阐明。但其实它的概念并不复杂:「预处理」是一个编译的阶段,这个阶段发生在 Objective-C 代码构建(build)之前,并且允许你在代码编译之前重写它。但在 Swift 里你不能这么做,因为预处理需要头文件和宏,宏命令可以实现在代码构建的时候完成替换,但 Swift 里并没有头文件。举个例子,你可以创建一个叫 PI 的宏,并赋予它一个值,而不是重复写3.14159265359。这个有点类似在 Swift 定义常量。宏命令可以做很多事情,使用它也有利有弊,更多关于「预处理」的内容,你可以查阅「预处理」那一章。
Easy differences 一些简单的区别
和 Objective-C 相比, Swift 是一门非常现代的语言。对比两者可以发现,Objective-C 不包含下面这些特性:
- 自动类型推断
- 运算符重载
- 协议扩展
- 字符串插值
- 命名空间
- 元组
- 可选值
- Playgrounds
- guard 和 defer 语法
- 闭区间和半开区间
- 带值枚举
Objective-C 中也有结构体(struct),但使用的频率比 Swift 中低很多,正如 Objective-C 的 “Objective”命名一样,它把更多的精力放在「对象」上。
Objective-C 与 Swift 在大多数运算符上是一致的。但是, Swift 2.2 中弃用了 “++” 和 “–” 运算符, nil 合并运算符由 “??” 变为 “?:”。
Namespaces 命名空间
乍一看 Objective-C 中没有「命名空间」这一点,没什么特别的,但这里要再解释一下这一点。所谓的「命名空间」是一种组合形式,它把离散的、可以重复使用的区块,组合成一个具有某种功能的整体。当你给了你的代码一个「命名空间」的时候,它就保证了你在创建对象时所使用的命名,和别人用的不会重叠。举个例子,你可以创建一个名为 Person 的类,不用担心和 Apple 创建另一个名为 Person 的类重叠。因为 Swift 会自动给你的代码一个「命名空间」,把你的类被自动包装在里面,类似 YourApp.YourClass 。
Objective-C 里没有「命名空间」的概念,这意味着,它要求所有的类名都是全局唯一的。这说起来容易做起来就难了。假设你使用了五个库,这些库可能每个都使用三个其他的库,那每个库可能定义许多类名。这可能是库 A,库 B,库 C,甚至包括了它们的各个不同版本。(译者注:这也是为什么 OC 的命名总是那么长……)
这就很麻烦了。苹果的解决方案是简单粗暴的:使用2-4个字母的前缀,使每个类名称独特。想想看:UITableView,SKSpriteNode,MKMapView,XCTestCase…… 你看,可能会因为一直在用这种前缀方法,你甚至都没有意识到它是在解决 Objective-C 没有「命名空间」这个缺点。
What, no optionals? 什么鬼,竟然没有可选值?
Objective-C 中没有「可选值」的概念,这也是有人欢喜有人愁。「可选值」导致了 Swift 首次发布时出现的�各种问题:所有的Objective-C API 都是要导入到 Swift 中去的,这意味着,程序员必须要确定这里是 “UIView” 还是 “UIView?”,换句话说,也就是必须想好这里「必须有一个视图」,还是「可以为空」。
在 Swift 中,这种区分真的很重要。假设你想使用的是一个值,但是实际上这里是为空(nil)的,那么你的程序就崩溃了。但是在Objective-C 中,使用 nil 值是完全可以的.你可以向 nil 对象发送一个消息,它只是不回应,什么都不会发生。
重要的事情要加粗:你可以发送消息到一个 nil 对象,什么都不会发生。 这在 Swift 中是绝对不行的,这样做就是一个程序员考虑不周产生的错误。但在 Objective-C 中不一样,如果你跑了代码,但什么都没有发生,你应该去检查一下,是不是给一个 nil 对象发送了消息。
为了更好地桥接 Objective-C 和 Swift,苹果引入了一些新的关键字,来填补 Objective-C 没有可选值的空白。然后它重审了自己的 API 来使用这些新的关键字,这就是为什么随着时间的推移,Swift 中的一些 API 改变了它们的可选性。这些都会在本书后面的 Nullability 章节中进行讨论。这些改动对 Objective-C 来说是一种完善,但很少有人用就是了。
Safety 安全性
由于没有可选值,并且你能够给 nil 对象发送消息,Objective-C 的安全性看起来比 Swift 要低。
然而,不仅是看起来,事实上 Objective-C 的安全性就是比 Swift 要低得多。Objective-C 允许你强制把一种数据类型转换成另一种数据类型,直到最近它才引入「泛型」的概念(例如,只能容纳字符串的数组)。在 Swift 中,我们能轻松地混合 ASCII码 和表情符号,但这样的高级字符串功能在 Objective-C 里是没有的。同时,它允许你读取不存在的数组值(译者注:也就是没有「数组越界」这一说)。在 Objective-C 中使用 switch 语法的时候,你也不需要穷举。并且几乎所有 Objective-C 中的属性都是变量,极少有人声明常量。
当你从 Swift 世界来到 Objective-C 世界的时候,这些特性看起来更像是错误,你会觉得 Objective-C 世界就像蛮荒之地一样。在某种程度上,确实是这个样子。如果你一直以来是 Swift 开发者,那 Objective-C 对你来说简直松散得不像话,你会疑惑苹果是如何用它建立起自己的整个生态系统的。但是换位想象一下,对于Objective-C 开发者来说,Swift 是什么样的呢?编译器就变得显得更加迂腐了,你需要把最后一个字母都拼出来,才能构建运行代码。 「对的,我确确实实要把一个 UInt 转换成 Int」这类完全没有必要在 Objective-C 存在的东西,在 Swift 世界里,这只是恼人的开始。
这里有一个小小的帮助,可以让 Objective-C 的危险性降低一点。当你创建 Objective-C 项目的时候,在 Xcode 中的 ” Build Settings” 选项卡中,将 “Treat Warnings as Errors” 设置为 “Yes”。这个设置会阻止你犯一些可怕的错误,例如试图将一个数字强行转化成一个字符串。
Basic syntax 基础语法
现在让我们来看看基础的 Objective-C 语法:变量、条件语句、Switch/case 结构和循环。
最好的练习方式是打开 Xcode,新建一个项目。依次点击 “File > New > Project” ,然后选择 “OS X > Application > Command Line Tool”(译者注:在 Xcode 8.2.1 的版本中,要选择 “macOS > Command Line Tool”)。
对的,你之前可能没有创建过命令行项目,但是相信我,这是在 Objective-C 项目里最接近 playground 的存在。
再多啰嗦一句,确定你在 language 这一栏选择了 Objective-C。毕竟你从 Swift 世界来,这一栏的默认语言是 Swift,所以不要忘记更改它哦。
What’s in the template? 模板里有什么?
当你建好这个项目的时候,会发现这个项目里面只有一个文件, “main.m”,而在这个文件里面,你只会看到几行代码。然而就是这几行代码,就已经向我们介绍了几个非常重要的概念,你看到的代码是这样的:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
}
return 0; }
如果你有一些 C 语言的编程经验,你会发现其中大多数代码,是常规的命令行应用程序的写法。 但其中有两个部分是 Objective-C 所独有的:@autoreleasepool 和 @”Hello,World!” 。
“@”是 Objective-C 中非常非常常见的符号,所以你最好赶紧适应它。”@”的意思是「接下来执行的代码是 Objective-C ,而不是 C 语言。 」NSLog()是一个类似于 swift 中print()的函数,在这个模板代码中,用来打印消息。如果在”Hello, World!”之前没有@符号,那么她就会被认为是 C 字符串(ASCII中的字符数组,以0结尾)。Objective-C 和 Swift 一样,有着属于自己的字符串数据结构。在这里, NSLog()期望接收的是 Objective-C 字符串,而不是 C 字符串,这就是为什么必须写上”@”符号。
@autoreleasepool 的意思是「我要开始分配大量的内存了;当我完成的时候,请自动释放。」其后的花括号区间(”{}”)内的所有内容都是这个自动释放池的一部分,在上面的这段示例代码里,就是整个程序。
请让我简单地提一下用 C 语言写的代码,特别是函数的写法。就是这一句:
int main(int argc, const char * argv[]) {
下面是每个符号的含义:
- int:此函数返回一个整数。
- main:函数名为 “main()” 。
- int argc:函数的第一个参数是一个称为 argc 的整数。
- const char * argv []:函数的第二个参数是一个名为 argv 的字符串数组。
这个带有参数的 main() 函数是创建命令行程序的标准方法,它会在程序运行时自动调用。
在继续下一步之前,还有一些小知识点:
- 首先,注意 “return” 用于从函数返回一个值,这点和 Swift 一样。
- 第二,每个语句必须以分号结束。也就是说,在着 NSLog()和 return 分别以分号结束。
- 第三,”//” 是添加注释,这点也和 Swift 一样。
Importing headers 接入头文件
在上一小节里,有一行代码我没有解释,就是这一行:
#import <Foundation/Foundation.h>。
这是一个预处理器指令,在之前的章节我已经简要提到过「预处理」这个概念。这意味着即使在构建代码之前,这个代码也被预处理器所替代。所有以”#”开头的代码行都是预处理器指令,对付它们要小心。
这行代码的具体意思是「找到”Foundation”(苹果的基本 Objective-C 框架)的头文件,并粘贴在这里」。预处理器语义上接受了 “Foundation.h” 这个头文件的内容,而这个头文件本身接入许多其他的头文件,并复制在”#import”行代替原来的代码。
如果你之前写过 C 或 C++ ,你会对 #include 更熟悉,它和 #import 几乎是在做同样的事情。然而,#import 有一个微妙的特性,这使它更易用:如果你在文件头部试用 #import,在整个文件里,它只被接入一次,而 #include 则可能被接入数次。 C 语言的程序员经常要通过编写头文件守卫(header guards)的方法,解决 #include 的问题,但 Objective-C 的 #import 自动解决了这个问题。
当你 “#import” 了一个系统库,你要把库的名字放在一对尖括号里。例如,#import <UIKit/UIKit.h> 。但是如果你想导入自己的头文件时,就要使用双引号,比如:#import”MyClass.h”。区分这两者非常重要:使用尖括号表示「在系统库中搜索该头文件」,而使用双引号表示「同时在系统库和项目中搜索该头文件」。
Creating variables 创建变量
Objective-C 和 Swift 不同,它不支持自动类型推理,同时在 Objective-C 中,几乎所有的属性都会被创建为变量。这就意味着,你必须要告诉 Xcode 你想要使用的每一个数据的类型。
现在,请用下面这行代码替换原有的 “// insert code here…”:
int i = 10;
这一行代码创建了一个新的整数,并且给它赋值为10。注意啦,这里没有 var 或者 let ,并且默认 “i” 是一个变量。如果你想创建一个常量,就要这样写:
const int i = 10;
在 Objective-C 里很少有人会这么做就对了。
如果你想创建字符串,就需要用到 NSString 这个类,请注意,在 Objective-C 里,它是一个类,而不是结构体。而 “NS” 正是一个由于没有命名空间而存在的前缀,同样你需要用上表示 Objective-C 语句的符号 “@”。
试试把上面的内容串联起来写一写:
NSString str = @"Reject common sense to make the impossible possible!";
然而如果你真的这么写了,Xcode 会跟你说 “Interface type cannot be statically allocated.”, 也就是「连接类型的数据,是不能静态初始化的」。
Xcode 的意思是,就像 NSString 一样,无论什么样的对象,必须要用一种特殊的方法来初始化,这种方法就叫做「指针」。你可以来回忆一下在 Swift 里,我们对可选值是如何的纠结,那么在 Objective-C 里面,指针就有那么的纠结。我会在指针的那一章里更详细地阐述它的概念,但是目前,你至少要知道,指针的指向是本地的内存某一位置,在那里存了一些你想要的数据。举个例子,比如说如果你想存一张照片,它占了 30M 的内存。当你想用这张照片的时候,总不能把所有的数据都拷贝过来,然后才能找到这张照片吧?用指针的话,直接找到照片对应的这 30M 空间就好啦。
在 Objective-C 里面,所有的对象都必须用指针来存储,而 NSString 又是一个对象,所以刚刚那行代码是要这样写的:
NSString *str = @"Reject common sense to make the impossible possible!";
看这个星号(*),这就是指针的标记。所以说, str 并不是 NSString 对象,而是一个指针,指向这个 NSString 对象在内存里的位置。
我们再看一个数据类型:数组,这里叫做 NSArray, 在这里你也需要在数组的开头写一个 @ 符号,并且因为我写的是一个字符串数组,在每个字符串前面,也要用上 @ 符号。
NSArray *array = @[@"Hello", @"World"];
在后面的章节里,我会更详细地解释「字符串」这些数据类型。到此为止,你应该已经知道怎么创建变量了,那我们就可以进行下一步了。
Conditions 条件语句
在 Objective-C 里使用条件语句和在 Swift 中几乎一般无二,要注意的是,在这里必须在条件语句上加圆括号。圆括号对于从 Swift 过来的开发者来说,就好像每句话后面的分号一样,非常容易遗漏。好在 Xcode 是会提示你的。
以下是一个基础的条件语句:
int i = 10;
if (i == 10) {
NSLog(@"Hello, World!");
}
在 Objective-C 里,如果在你的条件语句的位置上,有且只有一个条件,那么后面的实现语句,就可以不带花括号。这个特性,让程序员少敲了很多代码,但同时制造了很多新的 bug。
举个例子,下面两种写法在 Objective-C 里是等价的:
if (i == 10) {
NSLog(@"Hello, World!");
} else {
NSLog(@"Goodbye!");
}
if (i == 10)
NSLog(@"Hello, World!");
else
NSLog(@"Goodbye!");
目前你只是初学 Objective-C ,我强烈建议你不要写第二种形式。如果你非要用这种形式,至少把 if 语句写在同一行里面,就像这样:
if (i == 10) NSLog(@"Hello, World!");
这种写法到底好在哪里,反正我是不明白。(-_-)ゞ゛
Switch/case Switch/case 结构
在 Objective-C 里,如果只有一个语法必须保证滴水不漏,那是 switch / case 了。 原因有二:其一,Objective-C 没有 Swift 那么强大,因此你需要手动地做配置很多东西;其二,case 语句有程序出错的可能。 这与 Swift 不同,在 Objective-C 里每一个 case 语句后面总是跟着 break ,以避免程序出错。
还是先来看基础写法:
int i = 20;
switch (i) {
case 20:
NSLog(@"It's 20!");
break;
case 40:
NSLog(@"It's 40!");
break;
case 60:
NSLog(@"It's 60!");
break;
default:
NSLog(@"It's something else.");
}
敲黑板!请注意 switch(i)这里的圆括�号。
现在运行这段代码,你应该在控制台看到 “It’s 20!”。
但如果删除 “break;” 语句,你会看到刚才所谓的「程序出错」。在控制台里会有:
“It’s 20!”
“It’s 40!”
“It’s 60!”
“It’s something else.”
在 Objective-C 中,没有 break 相当于在 Swift 中添加 fallthrough 命令。
Objective-C 也支持模式匹配,但它只能写范围区间,比如这样:先写一个数字,然后写 “…” 符号,注意符号两边都有空格,接着写另一个数字,像这样:
switch (i) {
case 1 ... 30:
NSLog(@"It's between 1 and 30!");
break;
default:
NSLog(@"It's something else.");
}
有一点需要注意。可能这只是一种特殊情况,但是万一哪天要用到呢?这一点是:你不能使用 case 结构的第一行,声明一个新的变量,如果非要这么做,就要用花括号({})把 case 结构括起来。
请用下面的代码来演示一下:
switch (i) {
case 10:
int foo = 1;
NSLog(@"It's something else.");
}
Objective-C 不要求 switch / case 结构是穷举的,这意味着 default 语句并不是必须的。因此,如果不是因为 case 之后直接声明了一个新的变量,这段代码依然是有效的。
有两种方法来解决这个问题:可以把 case 语句的内容扩上花括号,也可以先执行 NSLog()语句:
switch (i) {
case 10:
{int foo = 1;
NSLog(@"It's something else.");
}
}
switch (i) {
case 10:
NSLog(@"It's something else.");
int foo = 1;
}
Loops 循环
Objective-C 有一整套可供选择的循环方案,包括在 Swift 2.2 中被弃用的 C 语言风格的 for 循环。
让我们从最常见的循环类型开始,这一类被称为「快速枚举」:
NSArray *names = @[@"Laura", @"Janet", @"Kim"];
for (NSString *name in names) {
NSLog(@"Hello, %@", name);
}
还记得我之前提过的 @ 符号在 Objective-C 使用频率之高吗?仅仅在上面这几行代码里,它就被用了6次。苹果真的简直了,好像把大部分盈利用来给开发者换 @ 键一样。(/= _ =)/~┴┴
这段代码先创建了一个存有姓名的数组,然后在循环里面,对数组里每个人打印了一句问候语。NSLog()的语法可能看起来特别奇怪,这是因为 Objective-C 没有字符串插值的功能。 NSLog()是一个可变函数,它把第一个参数中的字符串和别的值组合起来。 “%@” 被称为「格式说明符」,意思是「在这里插入对象的内容」。在我们的例子里,就是 name 变量。
你也可以使用 C 语言风格的 for 循环:
for (int i = 1; i <= 5; ++i) {
NSLog(@"%d * %d is %d", i, i, i * i);
}
“%d” 是另一种格式说明符,表示 “int”。在这里我调用了三次,你可以更清楚地看到它们在字符串中被替换的过程:第一个 “%d” 匹配第一个 i ,第二个 “%d” 匹配第二个 i ,第三个 “%d” 匹配 i * i 的运算结果。
你也可以像在 Swift 里一样使用 do/while 语法,用法和 Swift 里的 repeat/while 相同。
与条件语句一样,如果循环体只包含一个语句,则可以从循环中省略大括号。但风险极大,请谨慎地使用它。
Calling methods 调用方法
此后有一个专门关于「方法」(method)的章节,我将逐个在案例的基础上,介绍常见的数据类型。但在此之前,我至少想告诉你「方法」在 Objective-C 看起来是什么样子的。
我们首先来回忆一下 Swift 里的写法:
let myObject = new MyObject()
但在 Objective-C 里,不是这样的。首先, “new” 变成了你向 MyObject 发送的一个消息,所以变成了:
MyObject *myObject = [MyObject new];
在这段代码里,首先要写一对中括号([]),在里面先写你的对象名称(MyObject),然后写你的消息名称(new)。
小提示:尽管苹果公司的人在技术上叫它「发送消息」(sending a message),但是一般我们都会叫它「调用方法」(calling a method)。
然而当你想同时调用两个方法的时候,事情就变得棘手了。在 Swift 中你可能会这样写:
myObject.method1().method2()
但是在 Objective-C 里,你要写很多的中括号,像这样:
[[myobject method1] method2]
当你在写 Objective-C 的时候,你可能会在一行里写两个,三个甚至四个中括号,尽管 Xcode 会帮你检查这些括号是否补全,但出错了还是非常麻烦。
一种特殊情况下你会愿意在一行里使用两对中括号,就是创建一个新对象。之前我们用的是 new 方法,就是为一个对象分配了内存,并初始化。当然你也可以把这两步分开来执行,先分配内存,再初始化。就像这行代码:
MyObject *myObject = [[MyObject alloc] init];
内部括号里的 alloc 先运行,留出足够的内存来存储对象,接着运行 init,把默认值放入对象中,将它初始化。分离这两步是为了辅助函数,也就是在 Swift 中的初始化器,让你可以从文件、链接及其他东西里创建字符串。
Objective-C 方法调用看起来与 Swift 类似,区别只是:使用方括号,且不要在参数之间使用逗号。没有逗号也就意味着,在参数名的冒号后面,也不要加空格。
下面是在 Swift 和 Objective-C 调用方法的例子:
myObject.executeMethod(hello, param2: world)
[myObject executeMethod:hello param2:world];
我们将在后面的章节里,进一步阐述相关内容。
Nil coalescing 合并空值
和在 Swift 里一样,Objective-C 确保值存在的一种有效方法是使用 nil 合并。 Objective-C 没有专用的 “??” 运算符,而是允许您劫持三元运算符 “?:”。
举个例子,在这段代码里,如果姓名(name)的值不为空,就会打印欢迎语句,否则就打印「匿名」(Anonymous)。
NSString *name = nil;
NSLog(@"Hello, %@!", name ?: @"Anonymous");