iOS面试题 - Swift语言

作者: Longshihua | 来源:发表于2019-05-08 15:32 被阅读256次

    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中,不同的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
    

    使用场景

    • 关闭文件句柄(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、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面试之道》

    相关文章

      网友评论

        本文标题:iOS面试题 - Swift语言

        本文链接:https://www.haomeiwen.com/subject/qudqiftx.html