iOS面试资料将来跳槽用人猿星球

iOS面试题 - Swift语言

2019-05-08  本文已影响256人  Longshihua

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的运作效率。

两者的区别

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

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

什么时候使用类(class)什么时候使用结构体(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两个修饰初始化方法的关键词。

18、比较Swift和OC中协议的不同

相同点:两者都可以被用作代理,OC中的protocol类似Java中的Interface,在实际开发中主要用于适配器模式

不同点:Swift中的protocol还可以对接口进行抽象,例如:sequence,配合扩展(extension),泛型、关联类型等可以实现面向协议编程,从而提高整个代码的灵活性。同时,Swift中的protocol还可以用于值类型,如:结构体和枚举

19、谈谈对OC和Swift动态性理解

Runtime其实就是OC的动态机制。Runtime执行的是编译后的代码,这时它可以动态加载对象、添加方法、修改属性、传递信息等。具体过程是,在OC中,对象调用方法时,如[self.tableView reloadData],经历了两个阶段:

例如:[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中懒加载的区别:

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来实现类似功能。另外,在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

使用场景

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类型的。

class Person {
    // 存储属性
    static let age: Int = 20
 
    // 计算属性
    static var workTime: Int {
         return 8
    }
    // 类方法
    static func sleep() {
        print("sleep")
    }
}

但是所修饰的属性和方法不能够被重写,也就是说static修饰的类方法和属性包含了final关键字的特性。

重写会报错:


20180301190454894.png

我们同样可以使用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
    }
}

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、Swift编程风格指南

objc出版的《Swift进阶》中,看到编程风格习惯的内容,觉得很不错。所以这里记录下来,希望我们在自己的项目中使用Swift代码时,也应该尽量遵循如下的原则:

参考

《iOS面试之道》

上一篇下一篇

猜你喜欢

热点阅读