iOS 面试策略之语言工具 -Swift vs. Objective-C
我曾经一度在想苹果为什么要大费周章的出一门新语言,而不是去把同样的精力和时间放在优化 Objective-C 上?后来 Chris Lattner 在他的访谈中说,因为 Objective-C 是一门以 C 语言为基础的语言,所以天生具备 C 的缺点;况且这门语言历经多年,各种弊病也是积重难返。所以,苹果决定,重新开发一门语言,名为 Swift。
所以,Swift 从一开始就要和 Objective-C 语言分道扬镳。我们会发现 Swift 注重安全性,Objective-C 注重灵活性;Swift 有函数式编程、面向对象编程、面向协议编程,Objective-C 几乎只有面向对象编程;Swift 更注重值类型的数据结构,而 Objective-C 遵循 C 的老一套,注重指针和索引; Swift 是静态类型语言,Objective-C 却是动态类型语言。
本章将从数据结构、编程思路、语言特性三个角度来回答两种语言的异同。从比较当中我们也更能体会,尽管两者都是为 iOS 开发而定制的语言,Objective-C 和 Swift 依然有着天壤之别。
数据结构
1.说说 Swift 为什么将 String,Array,Dictionary 设计成值类型?
关键词:#引用类型 #值类型 #多线程 #协议
要解答这个问题,就要和 Objective-C 中相同的数据结构设计进行比较。Objective-C 中,字符串,数组,字典,皆被设计为引用类型。
值类型相比引用类型,最大的优势在于内存使用的高效。值类型在栈上操作,引用类型在堆上操作。栈上的操作仅仅是单个指针的上下移动,而堆上的操作则牵涉到合并、移位、重新链接等。也就是说 Swift 这样设计,大幅减少了堆上的内存分配和回收的次数。同时 copy-on-write 又将值传递和复制的开销降到了最低。
String,Array,Dictionary 设计成值类型,也是为了线程安全考虑。通过 Swift 的 let 设置,使得这些数据达到了真正意义上的“不变”,它也从根本上解决了多线程中内存访问和操作顺序的问题。
2.用 Swift 将协议(protocol)中的部分方法设计成可选(optional),该怎样实现?
关键词:#协议 #协议扩展
@optional
和 @required
是 Objective-C 中特有的关键字。
在 Swift 中,默认所有方法在协议中都是必须实现的。而且,协议里方法不可以直接定义 optional。先给出两种解决方案:
在协议和方法前都加上
@objc
关键字,然后再在方法前加上 optional 关键字。该方法实际上是把协议转化为 Objective-C 的方式然后进行可选定义。示例如下:
用扩展(extension)来规定可选方法。在 Swift 中,协议扩展(protocol extension)可以定义部分方法的默认实现,这样这些方法在实际调用中就是可选实现的了。示例如下:
3.下面代码有什么问题?
关键词:#协议
SomeClass 中的 delegate 那行会报错。
Swift 中的协议不仅可以被 class 这样的引用类型来实现,也可能被 struct 或者 enum 这样的值类型实现(这是和 Objective-C 最大的不同)。weak 关键词是用于 ARC 环境下,为引用类型提供引用计数这般的内存管理。它是不能被用来修饰值类型的。
有两种修正方法。
在 protocol 前加上
@objc
。Objective-C 中协议只能由 class 来实现,这样一来 weak 修饰的对象就与 Objective-C 一样,只是 class 类型。修正如下:
在 SomeProtocolDelegate 后添加关键词 class 。如此一来声明了该协议只能由 class 来实现。修正如下:
编程思路
4.在 Swift 和 Objective-C 的混编项目中,如何在 Swift 文件中调用 Objective-C 文件中已经定义的方法?如何在 Objective-C 文件中调用 Swift 文件中定义的方法?
关键词:#头文件 #@objc
在 Swift 中,若要使用 Objective-C 代码,可以在 ProjectName-Bridging-Header.h 里添加 Objective-C 的头文件名称,这样在 Swift 文件中即可调用相应的 Objective-C 代码。一般情况 Xcode 会在 Swift 项目中第一次创建 Objective-C 文件时自动创建 ProjectName-Bridging-Header.h 文件。
Objective-C 中若要调用 Swift 代码,可以导入 Swift 生成的头函数 ProjectName-Swift.h 来实现。
加分回答:
在 Swift 文件中,若要规定固定的方法或属性暴露给 Objective-C 使用,可以在方法或属性前加上
@objc
来声明。如果该类是 NSObject 子类,那么 Swift 会在非 private 的方法或属性前自动加上@objc
。
5.试比较 Swift 和 Objective-C 中的初始化方法(init)有什么异同?
关键词:#初始化
一言以蔽之,在 Swift 中的初始化方法更加严格和准确。
Objective-C 中,初始化方法无法保证所有成员变量都完成初始化;编译器对属性设置并无警告,但是实际操作中会出现初始化不完全的问题;初始化方法与普通方法并无实际差别,可以多次调用。
在 Swift 中,初始化方法必须保证所有非 optional 的成员变量都完成初始化。同时新增 convenience 和 required 两个修饰初始化方法的关键词。convenience 只是提供一种方便的初始化方法,必须通过调用同一个类中 designated 初始化方法来完成。required 是强制子类重写父类中所修饰的初始化方法。
6.试比较 Swift 和 Objective-C 中的协议(Protocol)有什么异同?
关键词:#协议
相同点在于,Swift 和 Objective-C 中的 Protocol 都可以被用作代理。Objective-C 中的 Protocol 类似于 Java 中的 Interface,实际开发中主要用于适配器模式(Adapter Pattern,详见第 3 章第 4 节设计模式)。
不同点在于,Swift 的 Protocol 还可以对接口进行抽象,例如 Sequence,配合拓展(extension)、泛型、关联类型等可以实现面向协议的编程,从而大大提高整个代码的灵活性。同时 Swift 的 Protocol 还可以用于值类型,如结构体和枚举。
语言特性
7.谈谈对 Objective-C 和 Swift 动态特性的理解
关键词:#动态特性 #@runtime #面向协议编程
runtime 其实就是 Objective-C 的动态机制。runtime 执行的是编译后的代码,这时它可以动态加载对象、添加方法、修改属性、传递信息等等。具体过程是在 Objective-C 中对象调用方法时,如 [self.tableview reload],发生了两件事。
编译阶段,编译器(compiler)会把这句话翻译成
objc_msgSend(self.tableview,[@selector](https://xiaozhuanlan.com/u/undefined)(reload))
,把消息发送给 self.tableview。运行阶段,接收者 self.tableview 会响应这个消息,期间可能会直接执行、转发消息,也可能会找不到方法崩溃。
所以整个流程是编译器翻译–> 给接收者发送消息 –> 接收者响应消息三个流程。
如 [self.tableview reload] 中,self.tableview 就是接收者,reload 就是消息,所以方法调用的格式在编译器看来是 [receiver message]。
其中接收者如何响应代码,就发生在运行时(runtime)。runtime 执行的是编译后的代码,这时它可以动态加载对象、添加方法、修改属性、传递信息等等,runtime 的运行机制就是 Objective-C 的动态特性。
Swift 目前被公认为是一门静态语言。它的动态特性都是通过桥接 OC 来实现。如果要把动态特性写得更 Swift 一点,可以用 protocol 来处理,比如 OC 中的 reflection 这样写:
Swift 中可以这样写:
8.以下代码输出什么?
关键词:#动态特性 #协议 #扩展
会打印出两行 Cook Seafood。
原因在于 Swift 中,协议中是动态派发,扩展中是静态派发。也就是说,协议中如果有方法声明,那么方法在调用中,会根据对象的实际类型进行调用。
此题中 makeFood() 方法在 Chef 协议中已经声明,而 chefOne 虽然声明为 Chef,但实际实现为 SeaFoodChef 。所以根据实际情况,makeFood() 会调用 SeaFoodChef 中的实现。chefTwo 也是同样的道理。
追问:如果 protocol 中没有声明 makeFood() 方法,代码又会输出什么?
会打印出两行,第一行为 Make Food,第二行为 Cook Seafood。
因为协议中没有声明 makeFood() 方法,所以此时只会按照扩展中进行静态派发。也就是说,会根据对象的声明类型进行调用。chefOne 声明为 Chef,所以调用扩展中的实现,chefTwo 声明为 SeafoodChef,所以调用 SeafoodChef 中的实现。
9. message send 如果找不到对象,会如何进行后续处理?
关键词:#动态特性
找不到对象分 2 种情况:对象为空(nil);对象不为空,却找不到对应的方法。
对象为空时,Objective-C 在向 nil 发送消息是有效的,在 runtime 中不会产生任何效果。如果信息中的方法返回值是对象,那么给 nil 发送消息返回 nil;如果方法返回值是结构体,那么给 nil 发送消息返回 0 。
对象不为空却找不到对应的方法时,程序异常,引发 unrecognized selector。
10. 什么是 method swizzling?
关键词:#动态特性
每个类都维护一个方法列表,其中方法名与其实现是一一对应的关系,即 SEL(方法名)和 IMP(指向实现的指针)的对应关系。method swizzling 可以在 runtime 时将 SEL 和 IMP 进行更换。比如 SELa 原来对应 IMPa,SELb 原来对应 IMPb,method swizzling 之后,SELa 就可以对应 IMPb,Selb 就对应 IMPa。下面是一个封装好的实现示范:
加分回答:
方法交换应该保证唯一性和原子性。唯一性是指应该尽可能在 +load 方法中实现,这样可以保证方法一定会调用且不会出现异常。原子性是指使用 dispatch_once 来执行方法交换,这样可以保证只运行一次。
不要轻易使用 method swizzling。因为动态交换方法实现并没有编译器的安全保证,可能会在运行时造成奇怪的 bug。
11. Swift 和 Objective-C 的自省(Introspection)有什么不同?
关键词:#动态特性
自省在 Objective-C 中就是:判断一个对象是不是属于某个类的操作。它有两种形式:
第一句话,isKindOfClass 用来判断 obj 是否为 SomeClass 或其子类的实例对象;第二句话,isMemberOfClass 则对 obj 做出判断,当且仅当 obj 是 SomeClass(非子类)的实例对象时才返回真。这两个方法的使用有个前提,即 obj 必须是 NSObject 或其子类。
Swift 中由于很多 class 并非继承自 NSObject,故而 Swift 用 is 来进行判断。它相当于 isKindOfClass。相比之下优点是 is 不仅可以用于任何 class 类型上,也可以用来判断 enum 和 struct 类型。
加分回答:
自省经常与动态类型一起运用。动态类型就是 id 类型,任何类型的对象都可以用 id 来代指。这个时候我们常常用自省来判断对象的实际所属类。示例代码如下:
12. 能否通过 Category 给已有的类添加属性(property)?
关键词:#动态特性
答案是可以。无论是对于 Objective-C 还是 Swift 而言。
Objective-C 中,正常情况下在 Category 中添加属性会报错,说是找不到 getter 和 setter 方法,这是因为 Category 不会自动生成这两个方法。解决方法是引入运行时头文件,配合关联对象的方法来实现。主要的两个函数是 objc_getAssociatedObject 和 objc_setAssociatedObject 。Swift 中,解决方法与 Objective-C 中相同。只是在写法上更加 Swift 化。
假如我们有个 class 叫 User。由于我们 App 要打开国际化市场了,所以 PM 要求我们 User 能满足有中间名字的老外。于是我们会想在它的 Category 里给它添加 middleName 这个属性。示例的 Objective-C 代码如下:
这段代码解释一下是这样的:
在 ".h" 文件中添加属性。此属性是私有属性。
在".m" 文件中引入运行时头文件
<objc/runtime.h>
,接着设置关联属性的 Key,最后实现 setter 和 getter。
其中 objc_setAssociatedObject 这个方法的四个参数分别为原对象,关联属性 key,关联属性,关联策略。具体细节可参考苹果官方 API,这里不做展开。用 Swift 实现类似功能是这样的:
小编推荐阅读
推荐👇:
如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群931 542 608来获取一份详细的大厂面试资料为你的跳槽多添一份保障。
评论