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

由上面代码可知,Personclass类型即引用类型,所以当修改anotherPerson对象的name属性时候,原有person对象的name值也发生了改变,而SomeStructstruct,是值类型,所以structB对象的name属性改变并不会影响structAname值。

在内存中,引用类型,诸如类,是在堆上进行存储和操作的,而值类型,诸如结构体,则是在栈上存储和操作的。相比栈上的操作,堆上的操作更加复杂耗时,所以苹果公司官方推荐使用结构体,这样可以提高APP的运作效率。

两者的区别

  • class的如下功能是struct没有的:

1、可以继承,这样子类可以使用父类的特性和方法
2、类型转换可以在运行时检查和解释一个实例的类型
3、可以用deinit来释放资源
4、一个类可以被多次引用

  • struct也有如下优势:

1、结构较小,适用于复制操作,相比一个class的实例被多次引用,struct更安全
2、无须担心内存泄露或者多线程冲突问题

什么时候使用类(class)什么时候使用结构体(struct)?

  • 如果是封装一些简单数据请使用struct
  • 如果希望封装的值是被复制而不是引用请使用struct
  • 无须使用继承来使用相关属性和行为请使用struct

除以上情况请使用class

对于classstruct的详细内容可以看这里

2、Swift是面向对象还是函数式的编程语言?

Swift既是面向对象的编程语言,也是函数式的编程语言。

Swift是面向对象的编程语言,是因为Swift支持类的封装、继承和多态,从这一点来看,SwiftJava这类纯面向对象的编程语言几乎没什么差别。

Swift是函数式的编程语言,那是因为Swift支持mapreducefilterflatmap这类去除中间状态、数学函数式的方法,更加强调运算结果而不是中间过程。

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、关键字:openpublicinternalfileprivateprivate

Swift中有五个级别的访问控制权限,从高到低依次为openpublicinternalfileprivateprivate

它们遵循的基本原则是:高级别的变量不允许被定义为低级别变量的成员变量。例如:一个private的class中不能包含public的String值。反之,低级别的变量却可以定义在高级别的变量中。比如,public的class中可以含有private的Int值

open:具备最高的访问权限。其修饰的类和方法可以在任意的module中被访问和重写;它是Swift3中新添加的访问权限

publicpublic的权限仅次于open。与open唯一的区别在于,它修饰的对象可以在任意module中被访问,但不能被重写

internal:默认的权限。它表示只能在当前定义的module中访问和重写,它可以被一个module中的多个文件访问,但不可以被其他module访问

fileprivate:也是Swift3新添加的权限,其修饰的对象只能在当前文件中使用。例如:它可以被一个文件中的classextensionstruct共同使用

private:最低级别的访问权限。它的对象只能在定义的作用域内使用,离开来这个作用域,即使是同一个文件中的其他作用域,也无法访问。

6、比较关键词:strongweakunonwed

Swift的内存管理机制与OC一样,都是ARC(Automatic Reference Counting)自动引用计数。其基本原理是,一个对象在没有强引用指向它时,所占用的内存就会被回收。反之,只要有任何一个强引用指向该对象,它就会一直存在于内存中

strong:强引用,是默认属性。当一个对象被声明为strong时,表示对该对象进行强引用,此时,该对象的引用计数会增加1

weak:弱引用,会被定义为可选类型变量。当一个对象被声明为weak时,表示对该对象不会保持强引用,该对象的引用计数不会增加1。在该对象被释放后,弱引用也随机消失。继续访问该对象,程序会得到nil,并不会奔溃。另外注意:当 ARC设置弱引用为 nil 时,属性观察不会被触发。

unowned:无主引用,与弱引用的本质一样。唯一不同的是,对象被释放后,依然有一个无效的引用指向对象,它不是optional,也不指向nil。如果继续访问该对象,则程序就会奔溃

引入weakunowned是为了解决由strong带来的循环引用问题。简单来说,就是当两个对象互相有一个强引用指向对方时,就会导致两个对象在内存中无法释放。

weakunwoned的使用场景有如下区别:

当访问对象可能已经被释放时,则使用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的特性,具有两种:willSetdidSet.

var title: String {
    willSet {
        print("将标题从\(title)设置到\(newValue)")
    }
    didSet {
        print("已将标题从\(oldValue)设置到\(title)")
    }
}

上面代码都对title进行了监听,在title发生改变之前,willSet对应的作用域将被执行,新的值是newValue,在title发生改变之后,didSet对应的作用域将被执行,原来的值为oldValue,这就是属性观察

注意:在初始化方法对属性的设定,以及在willSetdidSet中对属性的再次设定,都不会触发调用属性观察

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可以被classstruct以及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)可以将表达式右边的值的计算推迟到判定leftfalse时,这样就可以避免第一种方法带来的不必要的计算。

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为什么将StringArrayDictionary设计成为值类型,就要和OC中相同的数据结构设计进行比较。在OC中,StringArrayDictionary是皆为引用类型。

值类型相比引用类型,最大的优势在于可以高效地使用内存。值类型在栈上进行操作,引用类型在堆上操作。栈上的操作仅仅是单个指针的上下移动,而堆上的操作则牵涉合并、移位、重链接等。也就说,Swift这样设计大幅度减少了堆上的内存分配和回收的次数。同时,copy-on-write又将值传递和复制的开销降到最低。

SwiftStringArrayDictionary设计成为值类型也是为了线程安全。通过Swiftlet设置,使得这些数据达到真正意义上的“不变”,也从根本上解决了多线程中内存访问和操作顺序的问题。

SwiftStringArrayDictionary设计成为值类型还可以提升API 的灵活度。例如,通过实现Collection这样的协议,可以遍历String,使得整个开发更加灵活、高效。

14、如何使用Swift将协议(protocol)中的部分方法设计成为可选

@optional@requiredOC中特有的关键字。

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这样的引用类型实现,也可以被structenum这样的值类型实现(这是和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的成员变量都完成初始化;同时,新增conveniencerequired两个修饰初始化方法的关键词。

  • 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则是判断objectSomeClass(非子类)的实例对象时,才返回真。

注意:这两个方法都有一个前提,即object必须是NSObject或其子类

Swift中,由于很多class并非继承自NSObject,故而Swiftis函数来进行判断,它相当于isKindOfClass。这样做的优点是is函数不仅可以用于任何class类型上,也可以用来判断enumstruct类型

自省经常是与动态一起使用,动态类型就是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添加属性会报错,提示找不到gettersetter方法,这是因为Category不会自动生成这两个方法。解决的办法是引入运行时头文件,并配合关联对象的方法来实现。其中主要涉及的两个函数是objc_getAssociatedObjectobjc_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,最后实现settergetter

其中,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中,由于可以使用命名空间了,即使是名字相同的类型,只要是来自不同的命名空间的话,都是可以和平共处的。Swift的命名空间是基于module而不是在代码中显式地指明,每个module代表了Swift中的一个命名空间。也就是说,同一个target里的类型名称还是不能相同的。在我们进行app 开发时,默认添加到 app 的主 target 的内容都是处于同一个命名空间中的。而OC没有命名空间,所以很多类在命名时都加入类“驼峰式”的前缀

扩展性

Swift中,class可以通过extension来增加新方法,通过动态特性可以增加新变量。这样可以保证在不破坏原来代码的封装的情况下实现新的功能。而在OC中,可以用category来实现类似功能。另外,在SwiftOC中,还可以通过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,这样它的方法就不需要具体的实现了,交给遵守它的类或结构体即可

多继承

SwiftOC是不支持多继承的,因为这会造成“菱形”问题,即多个父类实现了同一个方法,子类无法判断继承那个父类的情况。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只能用于classPOP可以用于structenum这样的值类型。

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的区别

Swiftstaticclass都表示“类型范围作用域”的关键字。在所有类型中(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
    }
}
  • staticprotocol

Swift中的class,struct,enum都可以实现某个指定的协议。如果我们想在protocol中定义一个类型作用域上的方法或者计算属性,应该使用哪个关键字?答案显而易见,肯定是static,因为static是通用的。

注意:在使用protocol的时候,在enumstruct中仍然使用static进行修饰,在class中,classstatic都可以使用。

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、元组

把多个值组合成一个复合值。元组内的值可以是任意类型,并不要求是相同类型。

例如:下面这个例子中,(404, "Not Found") 是一个描述 HTTP 状态码(HTTP status code)的元组。HTTP 状态码是当请求网页的时候 web 服务器返回的一个特殊值。如果我们请求的网页不存在就会返回一个 404 Not Found 状态码。

let http404Error = (404, "Not Found")
// http404Error 的类型是 (Int, String),值是 (404, "Not Found")

(404, "Not Found") 元组把一个 Int 值和一个 String 值组合起来表示 HTTP 状态码的两个部分,这个元组可以被描述为“一个类型为 (Int, String) 的元组”。

元组的使用 (Tuple)

比如交换输入,我们经常会这么写

func swap<T>(a: inout T, b: inout T) {
    let temp = a
    a = b
    b = temp
}

但是要是使用多元组的话,我们可以不使用额外空间就完成交换,编写简单:

func swap2<T>(a: inout T, b: inout T) {
    (a,b) = (b,a)
}

32、协议中的 Self

我们在看一些协议的定义时,可能会注意到出现了首字母大写的Self出现在类型的位置上:

protocol IntervalType {
    func clamp(intervalClamp: Self) -> Self
}

比如上面这个IntervalType的协议定义了一个方法,接受实现该协议的自身的类型,并返回一个同样的类型

这么定义是因为协议其实本身是没有自己的上下文类型信息的,在声明协议的时候,我们并不知道最后究竟会是什么样的类型来实现这个协议,Swift中也不能在协议中定义泛型进行限制。

而在声明协议时,我们希望在协议中使用的类型就是实现这个协议本身的类型的话,就需要使用Self进行指代。

但是在这种情况下,Self不仅指代的是实现该协议的类型本身,也包括了这个类型的子类。从概念上来说,Self十分简单,但是实际实现一个这样的方法却稍微要转个弯。

协议中的 Self

33、final关键字

final关键字可以用在 class ,func 或者 var前面进行修饰,表示不允许对该内容进行继承或者重写操作。这样的禁止继承和重写的做法是非常有益的,它可以更好地对代码进行版本控制,得到更佳的性能,以及使代码更安全。在写 Swift 的时候可能会在什么情况下使用final

  • 权限控制

给一段代码加上final就意味着编译器向你作出保证,这段代码不会再被修改;同时,这也意味着你认为这段代码已经完备并且没有再被进行继承或重写的必要

  • 类或者方法的功能确实已经完备了

对于很多的辅助性质的工具类或者方法,可能我们会考虑加上final。这样的类有一个比较大的 特点,是很可能只包含类方法而没有实例方法。比如我们很难想到一种情况需要继承或重写一个负责计算一段字符串的MD5或者AES加密解密的工具类。这种工具类和方法的算法是经过完备 验证和固定的,使用者只需要调用,而相对来说不可能有继承和重写的需求。

  • 子类继承和修改是一件危险的事情

  • 为了父类中某些代码一定会被执行

class Parent {

    final func method() {
        print("开始配置")
        // ..必要的代码

        methodImpl()

        // ..必要的代码
        print("结束配置")
    }

    func methodImpl() {
        fatalError("子类必须实现这个方法")
        // 或者也可以给出默认实现
    }

}

class Child: Parent {
    override func methodImpl() {
        //..子类的业务逻辑
    }
}

这样,无论如何我们如何使用method ,都可以保证需要的代码一定被运行过,而同时又给了子类 继承和重写自定义具体实现的机会。

  • 性能考虑

使用final的另一个重要理由是可能带来的性能改善。因为编译器能够从final中获取额外的信 息,因此可以对类或者方法调用进行额外的优化处理。

34、Swift中delegate的使用?

Cocoa开发中接口-委托 (protocol-delegate) 模式是一种常用的设计模式,它贯穿于整个Cocoa 框架中,为代码之间的关系清理和解耦合做出了不可磨灭的贡献。

ARC中,对于一般的delegate,我们会在声明中将其指定为weak,在这个delegate实际的对象被释放的时候,会被重置回nil。这可以保证即使delegate已经不存在时,我们也不会由于访问到已被回收的内存而导致崩溃。

在 Swift 中我们当然也会希望这么做。但是当我们尝试书写这样的代码的时候,编译器不会让我们通过:

protocol MyClassDelegate {
    func method()
}

class MyClass {
    weak var delegate: MyClassDelegate?
}

class ViewController: UIViewController, MyClassDelegate {
    // ...
    var someInstance: MyClass!

    override func viewDidLoad() {
        super.viewDidLoad()

        someInstance = MyClass()
        someInstance.delegate = self
    }

    func method() {
        print("Do something")
    }

    //...
}

// weak var delegate: MyClassDelegate? 编译错误
// 'weak' cannot be applied to non-class type 'MyClassDelegate'

这是因为Swiftprotocol是可以被除了class以外的其他类型遵守的,而对于像struct或是enum这样的类型,本身就不通过引用计数来管理内存,所以也不可能用weak这样的ARC的概念来进行修饰。

想要在Swift中使用 weak delegate,我们就需要将protocol限制在class内。一种做法是将protocol声明为 Objective-C的,这可以通过在protocol前面加上 @objc 关键字来达到,Objective-Cprotocol都只有类能实现,因此使用 weak 来修饰就合理了:

@objc protocol MyClassDelegate {
    func method()
}

另一种可能更好的办法是在protocol声明的名字后面加上class,这可以为编译器显式地指明这个protocol只能由 class来实现。

protocol MyClassDelegate: class {
    func method()
}

相比起添加 @objc,后一种方法更能表现出问题的实质,同时也避免了过多的不必要的Objective-C兼容,可以说是一种更好的解决方式。

delegate

35、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面试之道》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容