iOS面试题 - Swift语言
1、类(class)和结构体(struct)有什么区别?
在Swift中,class是引用类型,struct是值类型。值类型在传递和赋值的过程中将进行复制,而引用类型则只会使用引用对象的一个指向,所以两者之间的主要区别还是类型的区别。
看个简单例子,代码如下:
class Person {
var name: String
init(name: String) {
self.name = name
}
}
struct SomeStruct {
var name: String
}
简单使用
let person = Person(name: "Jack")
let anotherPerson = person
anotherPerson.name = "Justin"
print(person.name, anotherPerson.name) //Justin Justin
let structA = SomeStruct(name: "Lisa")
var structB = structA
structB.name = "Judy"
print(structA.name, structB.name) //Lisa Judy
由上面代码可知,Person是class类型即引用类型,所以当修改anotherPerson对象的name属性时候,原有person对象的name值也发生了改变,而SomeStruct是struct,是值类型,所以structB对象的name属性改变并不会影响structA的name值。
在内存中,引用类型,诸如类,是在堆上进行存储和操作的,而值类型,诸如结构体,则是在栈上存储和操作的。相比栈上的操作,堆上的操作更加复杂耗时,所以苹果公司官方推荐使用结构体,这样可以提高APP的运作效率。
两者的区别
- class的如下功能是struct没有的:
1、可以继承,这样子类可以使用父类的特性和方法
2、类型转换可以在运行时检查和解释一个实例的类型
3、可以用deinit来释放资源
4、一个类可以被多次引用
- struct也有如下优势:
1、结构较小,适用于复制操作,相比一个class的实例被多次引用,struct更安全
2、无须担心内存泄露或者多线程冲突问题
什么时候使用类(class)什么时候使用结构体(struct)?
- 如果是封装一些简单数据请使用
struct - 如果希望封装的值是被复制而不是引用请使用
struct - 无须使用继承来使用相关属性和行为请使用
struct
除以上情况请使用class
对于class和struct的详细内容可以看这里
2、Swift是面向对象还是函数式的编程语言?
Swift既是面向对象的编程语言,也是函数式的编程语言。
说Swift是面向对象的编程语言,是因为Swift支持类的封装、继承和多态,从这一点来看,Swift与Java这类纯面向对象的编程语言几乎没什么差别。
说Swift是函数式的编程语言,那是因为Swift支持map,reduce,filter,flatmap这类去除中间状态、数学函数式的方法,更加强调运算结果而不是中间过程。
3、在Swift中,什么是可选类型(optionals)?
在Swift中,可选型是为处理值可能缺失的情况,当一个变量值为空时,那么该值就是nil。在Swift中,无论变量是引用类型还是值类型,都可以是可选类型变量
例如:
var value: Float? = 37.0 //值类型为Float,value的默认值为37.0
var key: String? = nil //值类型为string,key的默认值为nil
let image: UIImage? //引用类型为UIImage,image的默认值为nil
在OC中没有明确提出可选型的概念,然而,其引用类型却可以为nil,以此来标志其变量值为空的情况,而在Swift中这一理念扩大到值类型,并且明确提出来可选型的概念
4、在Swift中,什么是泛型?
在Swift中,泛型主要是为增加代码的灵活性而生的,它可以使对应的代码满足任意类型的变量或者方法。
举个简单例子:假如要写一个方法可以交换两个Int类型变量的值,通常我们会这样实现:
func swap(_ a: inout Int, _ b: inout Int) {
(a, b) = (b, a)
}
上面的函数写法正确但是并不高效和通用,因为,假如需要实现一个方法交换两个Float值,那么又得重新写一般。两个方法的差别仅仅是输入参数的类型不同。范型就是为来解决这类问题而来的,通过实现一个一般性的方法,可以交换任意类型的变量
func swap<T>(_ a: inout T, _ b: inout T) {
(a, b) = (b, a)
}
注意:Swift是类型安全的语言,所以这里交换两个变量的类型必须一致
5、关键字:open,public,internal,fileprivate,private
Swift中有五个级别的访问控制权限,从高到低依次为open,public,internal,fileprivate,private
它们遵循的基本原则是:高级别的变量不允许被定义为低级别变量的成员变量。例如:一个private的class中不能包含public的String值。反之,低级别的变量却可以定义在高级别的变量中。比如,public的class中可以含有private的Int值
open:具备最高的访问权限。其修饰的类和方法可以在任意的module中被访问和重写;它是Swift3中新添加的访问权限
public:public的权限仅次于open。与open唯一的区别在于,它修饰的对象可以在任意module中被访问,但不能被重写
internal:默认的权限。它表示只能在当前定义的module中访问和重写,它可以被一个module中的多个文件访问,但不可以被其他module访问
fileprivate:也是Swift3新添加的权限,其修饰的对象只能在当前文件中使用。例如:它可以被一个文件中的class、extension、struct共同使用
private:最低级别的访问权限。它的对象只能在定义的作用域内使用,离开来这个作用域,即使是同一个文件中的其他作用域,也无法访问。
6、比较关键词:strong、weak、unonwed
Swift的内存管理机制与OC一样,都是ARC(Automatic Reference Counting)自动引用计数。其基本原理是,一个对象在没有强引用指向它时,所占用的内存就会被回收。反之,只要有任何一个强引用指向该对象,它就会一直存在于内存中
strong:强引用,是默认属性。当一个对象被声明为strong时,表示对该对象进行强引用,此时,该对象的引用计数会增加1
weak:弱引用,会被定义为可选类型变量。当一个对象被声明为weak时,表示对该对象不会对其引用的实例保持强引用,该对象的引用计数不会增加1。在该对象被释放后,弱引用也随机消失。继续访问该对象,程序会得到nil,并不会奔溃。另外注意:当 ARC设置弱引用为 nil 时,属性观察不会被触发。
unowned:无主引用,与弱引用的本质一样。唯一不同的是,对象被释放后,依然有一个无效的引用指向对象,它不是optional,也不指向nil。如果继续访问该对象,则程序就会奔溃
引入weak和unowned是为了解决由strong带来的循环引用问题。简单来说,就是当两个对象互相有一个强引用指向对方时,就会导致两个对象在内存中无法释放。
weak和unwoned的使用场景有如下区别:
当访问对象可能已经被释放时,则使用weak,比如:delegate的修饰
当访问对象不可能被释放时,则用unwoned,比如:self的引用
实际上,为了安全,很多情况下基本使用weak
7、在Swift中,如何理解copy-on-write
当值类型(比如:struct)在复制时,复制的对象和原对象实际上在内存中指向同一个对象。当前仅当修改复制后的对象时,才会在内存中重新创建一个新的对象。
let arrayA = [1,2,3]
var arrayB = arrayA // 这时arrayB和arrayA在内存中时同一个数组,内存中并没有生成新的数组
print("before: \(arrayA), b: \(arrayB)") //before: [1, 2, 3], b: [1, 2, 3]
arrayB.append(4) // arrayB被修改了,此时arrayB在内存中变成了一个新的数组,而不是原来的arrayA
print("after: \(arrayA), b: \(arrayB)") //after: [1, 2, 3], b: [1, 2, 3, 4]
从上面的代码可以看出,复制的数组和原数组共享同一个地址,直到其中之一发生改变。这样设计使得值类型可以被多次复制而无须耗费多余的内存,只有变化的时候才增加开销。因此内存的使用更加高效
8、属性观察(Property Observer)
属性观察是指在当前类型内对特定属性进行监听,并作出响应的行为。属性观察是Swift的特性,具有两种:willSet和didSet.
var title: String {
willSet {
print("将标题从\(title)设置到\(newValue)")
}
didSet {
print("已将标题从\(oldValue)设置到\(title)")
}
}
上面代码都对title进行了监听,在title发生改变之前,willSet对应的作用域将被执行,新的值是newValue,在title发生改变之后,didSet对应的作用域将被执行,原来的值为oldValue,这就是属性观察
注意:在初始化方法对属性的设定,以及在
willSet和didSet中对属性的再次设定,都不会触发调用属性观察
9、在结构体中如何修改成员变量的方法
下面代码存在什么问题
protocol Pet {
var name: String { get set }
}
struct MyDog: Pet {
var name: String
func changeName(name: String) {
self.name = name
}
}
一旦我们编写这样的代码,编译器就会告诉我们当前的self是不可变的,我们不能为name进行复制,如果想要修改name属性,那么必须在方法前加上mutating关键词,表示该方法会修改结构体中自己的成员变量
注意:
1、类不存在这样的问题,因为类可以随意修改自己的成员变量
2、在设计协议的时候,由于protocol可以被class和struct以及enum实现,所以需要考虑是否使用mutating关键词来修饰方法
10、如何使用Swift实现或(||)操作
直接一点的解法:
func ||(left: Bool, right: Bool) -> Bool {
return left ? left: right
}
上面解法虽然正确,但是效率不高。或(||)操作的本质是,当表达式左边的值为真的时候,无须计算表达式右边的值。而上面的解法是将表达式右边的默认值准备好了,再传入进行操作。当表达式右边的值计算释放复杂时,会造成性能上的浪费,所以,上面这种做法违反了或(||)操作的本质。正确的实现方法如下:
func ||(left: Bool, right: @autoclosure () -> Bool) -> Bool {
return left ? left: right()
}
自动闭包(autoclosure)可以将表达式右边的值的计算推迟到判定left为false时,这样就可以避免第一种方法带来的不必要的计算。
11、实现一个函数:输入是任意一个整数,输出为输入的整数+2
题目看起来很简单,直接写代码如下所示:
func addTwo(_ num: Int) -> Int {
return num + 2
}
但是接下来要实现输入的整数加4的功能,难道我们又写如下代码:
func addFour(_ num: Int) -> Int {
return num + 4
}
如果要是加8加10呢?能不能定义一个方法解决所有的问题呢?必须的,使用柯里化(currying)特性
func add(_ num: Int) -> (Int) -> Int {
return { num + $0 }
}
// 简单使用
let addTwo = add(2)
print(addTwo(10)) // 12
let addFour = add(4)
print(addFour(10)) // 14
let addSix = add(6)
print(addSix(10)) // 16
Swift的柯里化特性是函数式编程思想的体现,它将接受多个参数的方法进行变形,并用高阶函数的方式进行处理,使整个代码更加灵活
12、求0-100(包括0和100)中为偶数并且恰好是其他数字平方的数字
题目也很简单,满足两个条件即可,实现如下:
func num(fromValue: Int, toValue: Int) -> [Int] {
var result = [Int]()
for num in fromValue...toValue where num % 2 == 0 {
if (fromValue...toValue).contains(num * num) {
result.append(num)
}
}
return result
}
上面的实现虽然正确,但是不够优雅,其实直接使用函数式编程思路实现简洁,如下:
(0...10).map { $0 * $0 }.filter { $0 % 2 == 0 }
13、Swift为什么将String,Array和Dictionary设计成为值类型
要了解Swift为什么将String,Array和Dictionary设计成为值类型,就要和OC中相同的数据结构设计进行比较。在OC中,String,Array和Dictionary是皆为引用类型。
值类型相比引用类型,最大的优势在于可以高效地使用内存。值类型在栈上进行操作,引用类型在堆上操作。栈上的操作仅仅是单个指针的上下移动,而堆上的操作则牵涉合并、移位、重链接等。也就说,Swift这样设计大幅度减少了堆上的内存分配和回收的次数。同时,copy-on-write又将值传递和复制的开销降到最低。
Swift将String,Array和Dictionary设计成为值类型也是为了线程安全。通过Swift的let设置,使得这些数据达到真正意义上的“不变”,也从根本上解决了多线程中内存访问和操作顺序的问题。
Swift将String,Array和Dictionary设计成为值类型还可以提升API 的灵活度。例如,通过实现Collection这样的协议,可以遍历String,使得整个开发更加灵活、高效。
14、如何使用Swift将协议(protocol)中的部分方法设计成为可选
@optional和@required是OC中特有的关键字。
在Swift中,协议中所有的方法默认都必须实现,而且在协议中,方法是不能直接被定义为optional。有两种解决方案:
1)在协议和方法前面都加@objc关键字,然后再在方法前加上optional关键字。该方法实际上把协议转换为OC的方式,然后就可以进行可选定义了。如下:
@objc protocol SomeProtocol {
func run() // 必须实现方法
@objc optional func jump() //可选方法
}
2)使用扩展(extension)来规定可选方法。在Swift中,协议扩展可以定义部分方法的默认实现,这样,这些方法在实际调用中就是可选实现来。如:
protocol SomeProtocol {
func run() // 必须实现方法
func jump() //可选方法
}
extension SomeProtocol {
func jump() {
print("jump")
}
}
class Person: SomeProtocol {
func run() { //只需要实现run方法
print("run")
}
}
15、协议的代码实战
下面代码有什么问题?
protocol SomeProtocol {
func doSomething()
}
class Person {
weak var delegate: SomeProtocol?
}
声明delegate属性的时候错误,编译器会报错。
Swift中协议不仅可以被class这样的引用类型实现,也可以被struct和enum这样的值类型实现(这是和OC的一大区别)。weak关键词是ARC环境下,为引用类型提供引用计数这样的内存管理,它是不能被用来修饰值类型的。
有两种方法解决这个问题:
1)在protocol前面加上@objc。在OC中,协议只能由class来实现,这样一来,weak修饰的对象与OC一样,只不过是class类型。如下:
@objc protocol SomeProtocol {
func doSomething()
}
2)在SomeProtocol之后添加class关键词。如此一来就声明该协议只能由类(class)来实现。如下:
protocol SomeProtocol: class {
func doSomething()
}
16、在Swift和OC的混合项目中,如何在Swift文件中调用OC文件中定义的方法?又如何在OC文件中调用Swift文件中定义的方法
在Swift中,若要使用OC代码,则可以在ProjectName-Bridging-Header.h文件中添加OC的头文件名称,这样在Swift文件中即可调用相应的OC代码。一般情况下,Xcode会在Swift项目中第一次创建OC文件时,自动创建ProjectName-Bridging-Header.h文件
在OC中如果想要调用Swift代码,则可以导入Swift生成的头文件ProjectName-Swift.h文件
在Swift文件中,若要将固定的方法或属性暴露给OC使用,则可以在方法或属性前加上@objc。如果该类是NSObject子类,那么Swift会在非private的方法或属性前自动加上@objc.
17、比较Swift和OC的初始化方法的异同
简单来说:Swift的初始化方法更加严格和准确
在OC中,初始化方法无法保证所有成员变量都完成初始化;编译器对属性甚至并无警告,但是,在实际操作中会出现初始化不完全的问题;初始化方法与普通方法并无实际差别,可以多次调用。
在Swift中,初始化方法必须保证所有非optional的成员变量都完成初始化;同时,新增convenience和required两个修饰初始化方法的关键词。
-
convenience:只是提供一个方便的初始化方法,必须通过调用同一个类中的designated初始化方法来完成 -
required:强制子类重写父类中所修饰的初始化方法
18、比较Swift和OC中协议的不同
相同点:两者都可以被用作代理,OC中的protocol类似Java中的Interface,在实际开发中主要用于适配器模式
不同点:Swift中的protocol还可以对接口进行抽象,例如:sequence,配合扩展(extension),泛型、关联类型等可以实现面向协议编程,从而提高整个代码的灵活性。同时,Swift中的protocol还可以用于值类型,如:结构体和枚举
19、谈谈对OC和Swift动态性理解
Runtime其实就是OC的动态机制。Runtime执行的是编译后的代码,这时它可以动态加载对象、添加方法、修改属性、传递信息等。具体过程是,在OC中,对象调用方法时,如[self.tableView reloadData],经历了两个阶段:
-
编译阶段:编译器会把这句话翻译为
objc_msgSend(self.tableView, @selector(reloadData)),把消息发送给tableView -
运行阶段:接受者
self.tableView会响应这个消息,期间可能会直接执行、转发消息,也可能会找不到方法导致程序奔溃。所以,整个流程是:编译器编译->给接收者发送消息->接收者响应消息
例如:[self.tableView reloadData]中,self.tableView就是接收者,reloadData就是消息,所以,方法调用的格式在编译器看来是[receiver message]。其中,接收者响应代码,就发生在运行时(Runtime)。Runtime的运行机制就是OC的动态特性
Swift目前被公认为是一门静态的语言,它的动态特性都是通过桥接OC实现的。如果要把其动态特性写的更“Swift”一点,则可以用protocol来处理,比如:可以将OC中的reflection这样写:
if ([someImage respondsToSelector: @selector(shake)]) {
[someImage performSelector: shake];
}
在Swift中实现如下
if let shakeableImage = someImage as? Shakeable {
shakeableImage.shake()
}
20、语言特性代码实战
下面代码输出什么?
protocol Chef {
func makeFood()
}
extension Chef {
func makeFood() {
print("make food")
}
}
struct SeafoodChef: Chef {
func makeFood() {
print("make seafood")
}
}
let chefOne: Chef = SeafoodChef()
let chefTwo: SeafoodChef = SeafoodChef()
chefOne.makeFood()
chefTwo.makeFood()
代码运行输出:
make seafood
make seafood
在Swift中,协议中是动态派发,扩展中是静态派发,也就是说,协议中如果有方法声明,那么会根据对象的实际类型进行调用。
上面makeFood()方法在Chef协议中已经声明了,而chefOne虽然声明为Chef,但实际实现为SeafoodChef。所以,根据实际情况,makeFood()会调用SeafoodChef中的实现。chefTwo也是同样的道理。
如果protocol中没有声明makeFood()方法,代码又会输出什么?
运行如下:
make food
make seafood
因为协议中没有声明makeFood()方法,所以,此时只会按照扩展中的声明类型进行静态派发。也就是说,会根据对象的声明类型进行调用。chefone被声明为Chef,所以会调用扩展的实现,chefTwo被声明为SeafoodChef,则会调用SeafoodChef中的实现。
21、Swift和OC的自省有什么不同
自省在OC中就是判断一个对象是否属于某个类。它有两种形式:
[object isKindOfClass: [SomeClass class]];
[object isMemberOfClass: [SomeClass class]];
isKindOfClass是用来判断object是否为SomeClass或者其子类的实例对象。
isMemberOfClass则是判断object是SomeClass(非子类)的实例对象时,才返回真。
注意:这两个方法都有一个前提,即object必须是NSObject或其子类
在Swift中,由于很多class并非继承自NSObject,故而Swift用is函数来进行判断,它相当于isKindOfClass。这样做的优点是is函数不仅可以用于任何class类型上,也可以用来判断enum和struct类型
自省经常是与动态一起使用,动态类型就是id类型。任何类型的对象都可以用id来代替,这个时候常常需要自省来判断对象的实际所属类。如下:
id vehicle = someCarInstance;
if ([vehicle isKindOfClass: [Car Class]]) {
NSLog(@"vehicle is a car");
} else if ([vehicle isKindOfClass: [Trunk class]]) {
NSLog(@"vehicle is a truck");
}
22、能否通过Category给已有的类添加属性
可以通过Category给已有的类添加属性,无论是OC还是Swift。
在OC中,正常情况下,在Category添加属性会报错,提示找不到getter和setter方法,这是因为Category不会自动生成这两个方法。解决的办法是引入运行时头文件,并配合关联对象的方法来实现。其中主要涉及的两个函数是objc_getAssociatedObject和objc_setAssociatedObject。在Swift中,解决办法OC是一样的,只是写法上不一样。
假如有一个class叫做User,我们在其Category中添加name属性,如下:
// .h
#import "User.h"
@interface User (Add)
@property (nonatomic, copy) NSString *name;
@end
// .m
#import "User+Add.h"
#import <objc/runtime.h>
static const void *nameKey = @"nameKey";
- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name {
return objc_getAssociatedObject(self, nameKey);
}
@end
代码分析:
1、在.h文件中添加name属性,此属性为私有
2、在.m文件中引入运行时头文件 <objc/runtime.h>,接着设置关联属性的key,最后实现setter和getter。
其中,objc_setAssociatedObject这个方法的四个参数分别为原对象、关联属性key、关联值、关联策略。
Swift中的实现如下:
private var nameKey: Void?
class User: NSObject {}
extension User {
var name: String? {
get {
return objc_getAssociatedObject(self, &nameKey) as? String
}
set {
objc_setAssociatedObject(self, &nameKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
}
}
}
23、OC和Swift在单例模式的创建上有什么区别
单例模式在创建过程中,要保证实例变量只被创建一次,在整个开发中需要特别注意线程安全,即使在多线程情况下,依然只初始化一次变量
+(instancetype)sharedManager {
static Manager *sharedManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedManager = [[Manager alloc]init];
});
return sharedManager;
}
在Swift中,let关键词已经保证了实例变量不会被修改,所以单例的创建就简单很多:
class Manager {
static let sharedManager = Manager()
private init() {}
}
24、解释Swift中的懒加载?
懒加载主要是为了延迟加载,保证数据在用到的时候才会被加载,这样可以减少内存消耗
如下:使用懒加载创建UITableView,懒加载说白了就是使用闭包创建对象,当使用实例对象的时候会执行闭包
lazy var tableView: UITableView = {
let tableView = UITableView (frame: self.view.bounds,
style: UITableView.Style.plain)
tableView.delegate = self
tableView.dataSource = self
return tableView
}()
与OC中懒加载的区别:
-
Swift中懒加载只会在第一次调动的时候执行闭包, 然后将闭包的结果存在属性中,如果以后将属性置空,属性不在会有值,懒加载也不再执行 -
OC中第一次使用点语法调用属性的时候,执行懒加载,置空以后再使用点语法调用,会再次执行懒加载
25、什么是OOP,它在iOS开发中有哪些优缺点?
OOP(Object Oritented Programming),即面向对象编程,是目前最主流的编程方式。在iOS中绝大多数运用的都是OOP
在iOS开发中,OOP有如下优点:
封装和权限控制
在Swift中,相关属性和方法被放入一个类中,OC中.h文件负责声明公共变量和方法,.m文件负责声明私有变量,并实现所有方法。在Swift中也有public/internal/fileprivate/private/open等权限控制
命名空间
在Swift中,不同的class即使命名相同,在不同的bundle中,由于命名空间不同,它们依然可以和谐共存,毫无冲突。当App体积很大、bundle很多的时候,这一点非常有用。而OC没有命名空间,所以很多类在命名时都加入类“驼峰式”的前缀
扩展性
在Swift中,class可以通过extension来增加新方法,通过动态特性可以增加新变量。这样可以保证在不破坏原来代码的封装的情况下实现新的功能。而在OC中,可以用category来实现类似功能。另外,在Swift和OC中,还可以通过protocol和代理模式来实现更加灵活的扩展
继承和多态
同其他语言一样,在iOS开发中,可以将共同的方法和变量定义在父类中,在子类继承时再各自实现对应的功能,高效率实现代码复用。同时,针对不同情况,可以调用不同子类,从而大大增加代码的灵活性。
缺点
隐式共享
class是引用类型,当在代码中的某处改变某个实例变量时,另外一处调用此变量时就会受此修改的影响。例如:
class Person {
var name: String = ""
}
let person1 = Person()
person1.name = "person1"
let peron2 = person1
peron2.name = "person2"
print(person1.name, peron2.name) // person2 person2
这很容易造成异常。尤其是在多线程时,我们经常遇到的资源竞争就属于这种情况。解决的方案是在多线程时加锁,当前,这个方案会引入死锁和代码复杂度剧增的问题。解决这个问题的最好方案是尽可能地用struct这样的值类型替代class
冗杂的父类
想象一个场景:一个UIViewController的子类和一个UITableViewController中都需要加入handleSomething()方法。OOP的解决方案是直接在extension中加入handleSomething()方法。但是随着新方法越来越多,倒置UIViewController会越来越冗杂。当然也可以引入一个专门的父类或工具类,但是依然有职责不明确、依赖、冗杂等多种问题
另外一个方面,父类中的handleSomething()方法必须有具体的实现,因为它不能根据子类作出灵活调整。子类如果要特定操作,则必须重写方法来实现,那么父类中的实现又显得多此一举了。解决的方案是使用protocol,这样它的方法就不需要具体的实现了,交给遵守它的类或结构体即可
多继承
Swift和OC是不支持多继承的,因为这会造成“菱形”问题,即多个父类实现了同一个方法,子类无法判断继承那个父类的情况。Swift中又类似protocol的解决方案
26、Swift为什么要推出POP?
POP(Protocol Oriented Programming)面向协议编程
1、OOP有自身的缺点。在继承、代码复用等方面,其灵活度不高。而POP恰好解决来这些问题
2、POP可以保证Swift作为静态语言的安全性,而OC时代的OOP,其动态性经常会倒置异常
3、OOP无法应用于值类型,而POP却可以将其优势扩展到结构体、枚举类型中
27、POP相比OOP有哪些优势
优势:
更加灵活
比如:可以使用协议来定义公共的方法,让所有的服从类都可以有默认的实现,也可以自己实现
protocol SomethingProtocol {
func handleSomething()
}
extension SomethingProtocol {
func handleSomething() {
print("handleSomething")
}
}
class TableViewController: UITableViewController, SomethingProtocol {}
class ViewController: UIViewController, SomethingProtocol {}
减少依赖
相对于传入具体的实例变量,我们可以传入protocol来实现多态。同时,在测试时也可以利用protocol来模拟真实的实例,减少对对象极其实现的依赖。
protocol Request {
func send(request: Info)
}
protocol Info {}
class UserInfo: Info {}
class UserRequest: Request {
// 这里传入Info是protocol,不需要是具体的UserInfo,这方便了我们之后的测试
func send(request: Info) {
// 实际实现,一般是吧info发送给server
}
}
如果需要测试,实现protocol即可
class MockUserRequest: Request {
func send(request: Info) {
// 这里进行测试,方便实现
}
}
func testUserRequest() {
let userRequest = MockUserRequest()
userRequest.send(request: UserInfo())
}
消除动态分发的风险
对遵守protocol的类或结构体而言,它必须实现protocol声明的所有方法。否则编译器报错,这杜绝了运行时程序锁具有的风险。
协议可用于值类型
相比OOP只能用于class,POP可以用于struct和enum这样的值类型。
28、defer关键字的使用
工作原理:延迟执行,等当前范围内的陈述语句执行完成最后执行
defer
1、单个defer语句的执行
func updateImage() {
defer { print("Did update image") }
print("Will update image")
imageView.image = updatedImage
}
// Will update Image
// Did update image
2、多个defer语句的执行顺序
在相同的范围内有多个defer语句,执行的顺序跟显示的顺序相反,即从最后的defer语句开始执行
func printStringNumbers() {
defer { print("1") }
defer { print("2") }
defer { print("3") }
print("4")
}
/// Prints 4, 3, 2, 1
3、defer 和闭包
另一个比较有意思的事实是,虽然defer后面跟了一个闭包,但是它更多地像是一个语法糖,和我们所熟知的闭包特性不一样,并不会持有里面的值。比如:
func foo() {
var number = 1
defer { print("Statement 2: \(number)") }
number = 100
print("Statement 1: \(number)")
}
将会输出:
Statement 1: 100
Statement 2: 100
在defer中如果要依赖某个变量值时,需要自行进行复制:
func foo() {
var number = 1
var closureNumber = number
defer { print("Statement 2: \(closureNumber)") }
number = 100
print("Statement 1: \(number)")
}
// Statement 1: 100
// Statement 2: 1
使用场景
- 关闭文件句柄(FileHandle)
func writeFile() {
let file: FileHandle? = FileHandle(forReadingAtPath: filepath)
defer { file?.closeFile() }
// Write changes to the file
}
- 确保结果
确保回调闭包能够被执行
func getData(completion: (_ result: Result<String>) -> Void) {
var result: Result<String>?
defer {
guard let result = result else {
fatalError("We should always end with a result")
}
completion(result)
}
// Generate the result..
}
Defer usage in Swift
Swift defer 的正确使用
29、Static和Class的区别
在Swift中static和class都表示“类型范围作用域”的关键字。在所有类型中(class、static、enum)中,我们可以使用static来描述类型作用域。class是专门用于修饰class类型的。
-
static可以修饰属性和方法
class Person {
// 存储属性
static let age: Int = 20
// 计算属性
static var workTime: Int {
return 8
}
// 类方法
static func sleep() {
print("sleep")
}
}
但是所修饰的属性和方法不能够被重写,也就是说
static修饰的类方法和属性包含了final关键字的特性。
重写会报错:
20180301190454894.png
-
class修饰方法和计算属性
我们同样可以使用
class修饰方法和计算属性,但是不能够修饰存储属性。
20180301184734222.png
对于 class修饰的类方法和计算属性是可以被重写的,可以使用class关键字也可以是static关键字
class Person {
class var workTime: Int {
return 8
}
class func sleep() {
print("sleep")
}
}
class Student: Person {
// 使用class - ok
// override class func sleep() {
// }
// override class var workTime: Int {
// return 10
// }
// 使用static
override static func sleep() {
}
override static var workTime: Int {
return 10
}
}
-
static和protocol
Swift中的class,struct,enum都可以实现某个指定的协议。如果我们想在protocol中定义一个类型作用域上的方法或者计算属性,应该使用哪个关键字?答案显而易见,肯定是static,因为static是通用的。
注意:在使用
protocol的时候,在enum和struct中仍然使用static进行修饰,在class中,class和static都可以使用。
protocol MyProtocol {
static func foo() -> String
static func bar() -> String
}
struct MyStruct: MyProtocol {
static func foo() -> String {
return "MyStruct.foo()"
}
static func bar() -> String {
return "MyStruct.bar()"
}
}
enum MyEnum: MyProtocol {
static func foo() -> String {
return "MyEnum.foo()"
}
static func bar() -> String {
return "MyEnum.bar()"
}
}
class MyClass: MyProtocol {
// 在 class 中可以使用 class
class func foo() -> String {
return "MyClass.foo()"
}
// 也可以使用 static
static func bar() -> String {
return "MyClass.bar()"
}
}
总结
1、用class指定的类方法可以被子类重写,而static指定的类方法是不能被子类重写的,因为static修饰的类方法和属性包含了final关键字的特性。
2、class不能修饰存储属性,而static可以
3、对protocol而言,推荐使用static关键字修饰类方法和属性
30、讲讲deinit函数
当一个类的实例被释放之前,析构器会被立即调用。析构器用关键字deinit来标示,类似于构造器要用init来标示。析构器只适用于类类型。在类的定义中,每个类最多只能有一个析构器,而且析构器不带任何参数,如下所示:
deinit {
// perform the deinitialization
}
析构器是在实例释放发生前被自动调用。你不能主动调用析构器。子类继承了父类的析构器,并且在子类析构器实现的最后,父类的析构器会被自动调用。即使子类没有提供自己的析构器,父类的析构器也同样会被调用。
因为直到实例的析构器被调用后,实例才会被释放,所以析构器可以访问实例的所有属性,并且可以根据那些属性可以修改它的行为。开发中,经常在deinit函数中进行一些资源释放,如:去除通知、观察者等。
31、Swift编程风格指南
objc出版的《Swift进阶》中,看到编程风格习惯的内容,觉得很不错。所以这里记录下来,希望我们在自己的项目中使用Swift代码时,也应该尽量遵循如下的原则:
-
1、对于命名,在使用时能清晰表意是最重要。因为
API被使用的次数要远远多于被声明的次数,所以我们应当从使用者的⻆度来考虑它们的名字。尽快熟悉Swift API设计准则,并且在你自己的代码中坚持使用这些准则。 -
2、简洁经常有助于代码清晰,但是简洁本身不应该独自成为我们编码的目标。
-
3、务必为函数添加文档注释—特别是泛型函数。
-
4、类型使用大写字母开头,函数、变量和枚举成员使用小写字母开头,两者都使用驼峰式命名法。
-
5、使用类型推断,省略掉显而易⻅的类型会有助于提高可读性。
-
6、如果存在歧义或者在进行定义的时候不要使用类型推断。(比如func就需要显式地指定
返回类型) -
7、优先选择结构体,只在确实需要使用到类特有的特性或者是引用语义时才使用类。
-
8、除非你的设计就是希望某个类被继承使用,否则都应该将它们标记为
final。 -
9、除非一个闭包后面立即跟随有左括号,否则都应该使用尾随闭包(trailingclosure)的语 法。
-
10、使用
guard来提早退出方法。 -
11、避免对可选值进行强制解包和隐式强制解包。它们偶尔有用,但是经常需要使用它们的话往往意味着有其他不妥的地方。
-
12、不要写重复的代码。如果你发现你写了好几次类似的代码片段的话,试着将它们提取到 一个函数里,并且考虑将这个函数转化为协议扩展的可能性。
-
13、试着去使用
map和reduce,但这不是强制的。当合适的时候,使用for循环也无可厚 非。高阶函数的意义是让代码可读性更高。但是如果使用reduce的场景难以理解的话, 强行使用往往事与愿违,这种时候简单的for循环可能会更清晰。 -
14、试着去使用不可变值:除非你需要改变某个值,否则都应该使用
let来声明变量。不过 如果能让代码更加清晰高效的话,也可以选择使用可变的版本。用函数将可变的部分封 装起来,可以把它带来的副作用进行隔离。 -
15、
Swift的泛型可能会导致非常⻓的函数签名。坏消息是我们现在除了将函数声明强制写成几行以外,对此并没有什么好办法。我们会在示例代码中在这点上保持一贯性,这样 你能看到我们是如何处理这个问题的。 -
16、除非你确实需要,否则不要使用
self.。在闭包表达式中,使用self是一个清晰的信号, 表明闭包将会捕获self。 -
17、尽可能地对现有的类型和协议进行扩展,而不是写一些全局函数。这有助于提高可读性, 让别人更容易发现你的代码。
参考
《iOS面试之道》