Swift 面试知识小节

207 阅读15分钟

Swift 面试知识点总结

文章参考网络,与此同时,对部分参考内容做了部分调整

Swift 优点 (相对 OC)

  • Swift 更加安全,是类型安全的语言
  • 代码少,语法简洁,可以省去大量冗余代码
  • Swift 速度更快,运算性能更高,(Apple 专门对编译器进行了优化)

Swift 中 类(class) 和 结构体(struct) 的区别,以及各自优缺点?

  • 类:

    • 引用类型
      • 在进行变量赋值时,是通过指针copy,属于浅拷贝(shallow copy)
      • 数据的存储是在堆空间
      • 可以被继承(前提是类没有被 final 关键字修饰),子类可以使用父类的属性和方法
      • (当class继承自 Object,拥有runtime机制)类型转换可以在运行时检查和解释一个实例对象
      • 用 deinit(析构函数)来释放资源 类似OC(dealloc)
      • 类的方法地址 是不确定的,只有在具体运行时,才能确定调用的具体值
  • 结构体

    • 值类型
      • 在进行变量赋值是,是深拷贝(deep copy),产生了新的副本
      • 数据的存储时在栈空间(大部分情况下,不需要考虑内存泄露问题,栈空间的特点是用完即释放)
      • 结构体调用方法,在编译完成就可以确定方法具体的地址值,以便直接调用

综上,在满足程序要求的情况下 优先使用 结构体


Swift中strong 、weak和unowned是什么意思?二者有什么不同?何时使用unowned?

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

  • strong 代表着强引用,是默认属性。当一个对象被声明为 strong 时,就表示父层级对该对象有一个强引用的指向。此时该对象的引用计数会增加1。
  • weak 代表着弱引用。当对象被声明为 weak 时,父层级对此对象没有指向,该对象的引用计数不会增加1。它在对象释放后弱引用也随即消失。继续访问该对象,程序会得到 nil,不亏崩溃
  • unowned 与弱引用本质上一样。不同的是,unowned 无主引用 实例销毁后仍然存储着实例的内存地址(类似于OC中的unsafe_unretained), 试图在实例销毁后访问无主引用,会产生运行时错误(野指针)
  • weak unowned 只能用在 类实例上面
  • weakunowned 都能解决 循环引用,unowned 要比 weak 性能 稍高
    • 在生命周期中可能会 变成 nil 的使用 weak
    • 初始化赋值以后再也不会变成 nil 使用 unowned

Swift 中什么是可选类型?

  • Swift中可选类型为了表示 一个变量 允许为 空(nil)的情况

  • 类型名称后 加 ? 定义 可选项

  • 选项的本质是 枚举类型

更多参考:链接


Swift 中什么 是 泛型?

  • 跟JS和Dart 类似,泛型 可以将类型 参数化,提高代码复用率,减少代码量
  • Swift泛型函数 并不会 在底层 生成 若干个 (匹配类型)函数 ,产生函数重载,而是: 在函数调用时,会将 参数的类型 传递给 目标函数
  • Swift泛型应用在协议上时,需要使用关联类型(associatedtype)

更多参考:泛型

怎么理解 Swift中的泛型约束

泛型约束 可以 更精确的知道 参数 需要 遵循什么标准

//someT遵循的是某个class,someU遵循的是某个协议,这样在传参的时候明确参数类型
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
    // 这里是泛型函数的函数体部分
}

Swift 中 static 和 class 关键字的区别

在 Swift 中 staticclass 都是表示「类型范围作用域」的关键字。

在所有类型(class[类]、struct、enum )中使用

  • static 修饰都可以表示类方法类与属性(包括存储属性和计算属性)。
  • class 是专门用在 calss 类型中修饰类方法和类的计算属性(注意:无法使用 class 修饰存储属性)。

结构体只能用 static 修饰 类方法或属性

class类型 static class 的区别

class 类型staticclass 都可以表示类型范围作用域,那区别是

  1. class 无法修饰存储属性,而 static 可以。
  2. 使用 static 修饰的类方法和类属性无法在子类中重载。也就是说 static 修饰的类方法和类属性包含了 final 关键字的特性。相当于 final class 。

一般在 protocol定义一个类方法或者类计算属性推荐使用 static 关键字来修饰。使用 protocol 时,在 struct 和 enum 中仍然使用 static,在 class 类型中 class 和 static 关键字都可以使用。

参考链接 : www.jianshu.com/p/d75b403e0…


Swift 中的模式匹配?

参考 : 模式匹配

模式 是用于 匹配的规则,比如 switch 的 case 、捕捉错误的 catch 、 if guard while for 语句条件等


Swift 中的访问控制?

Swift 提供了 5个 不同的访问级别,从高到低排列如下:

  • open

    • 允许在任意模块中访问、继承、重写
    • open 只能用在 类 、 类成员上
  • public

    • 允许在任意模块中访问
    • 修饰类时不允许其他模块进行继承、重写
  • internal - 默认

    • 只允许在定义实体的模块中访问,不允许在其他模块中访问
  • fileprivate

    • 只允许在定义实体的源文件中访问
  • private:

    • 只允许在定义实体的封闭声明中(作用域)访问

更多查看 : 链接


怎么理解 copy - on - write? 或者 理解Swift中的写时复制

值类型在复制时,复制对象 与 原对象 实际上在内存中指向同一个对象,当且仅当 修改复制的对象时,才会在内存中创建一个新的对象,

  • 为了提升性能,Struct, String、Array、Dictionary、Set采取了Copy On Write的技术
  • 比如仅当有“写”操作时,才会真正执行拷贝操作
  • 对于标准库值类型的赋值操作,Swift 能确保最佳性能,所有没必要为了保证最佳性能来避免赋值

原理

在结构体内部用一个引用类型来存储实际的数据,

  • 在不进行写入操作的普通传递过程中,都是将内部的reference的应用计数+1,
  • 当进行写入操作时,对内部的 reference 做一次 copy 操作用来存储新的数据;防止和之前的reference产生意外的数据共享。

swift中提供[isKnownUniquelyReferenced]函数,他能检查一个类的实例是不是唯一的引用,如果是,我们就不需要对结构体实例进行复制,如果不是,说明对象被不同的结构体共享,这时对它进行更改就需要进行复制。

Swift 为什么将 Array,String,Dictionary,Set,设计为值类型?

值类型 相比 引用类型的优点

  • 值类型和引用类型相比,最大优势可以高效的使用内存;
  • 值类型在栈上操作,引用类型在堆上操作;
  • 栈上操作仅仅是单个指针的移动,
  • 堆上操作牵涉到合并,位移,重链接

Swift 这样设计减少了堆上内存分配和回收次数,使用 copy-on-write将值传递与复制开销降到最低

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


什么是属性观察器?

参考:属性观察器

属性观察是指在当前类型内对特性属性进行监测,并作出响应,属性观察是 swift 中的特性,具有2种, willsetdidset

  • willSet 传递新值 newValue
  • didSet 传递旧值 oldValue
  • 在初始化器中对属性初始化时,不会触发观察器
  • 属性观察器 只能用在 存储属性 ,不可用在 计算属性

Swift 异常捕获

参考链接:www.yuque.com/jk-coder/rh…

do - try - catch 机制

  • Swift中可以通过 Error 协议自定义运行时的错误信息
  • 函数内部通过throw 抛出 自定义 Error , 抛出 Error 的函数必须加上 throws 声明(逻辑通过不会抛出,反之可能抛出)
  • 需要使用 try 调用 可能会 抛出 的Error 函数
  • 通过 try 尝试调用 函数 抛出的异常 必须要 处理异常;否在会编译报错; 反之 运行时 在 top level (main) 报错,闪退

defer 的用法

  • 使用defer代码块来表示在函数返回前,函数中最后执行的代码。无论函数是否会抛出错误,这段代码都将执行。

如何将Swift中协议 部分方法 设计成可选?

  • 方案一(不推荐,除非需要暴露给OC用)
    • 在协议和方法前面添加 @objc,然后在方法前面添加 optional关键字,改方式实际上是将协议转为了OC的方式
@objc protocol someProtocol {
  @objc  optional func test()
}
  • 方案二

参考 Swift中协议


Swift和OC中的 protocol 有什么不同?

  • 相同点,两者都是用作代理

  • 不同点

    • Swift
      • Swift中的 protocol 还可以对接口进行抽象,可以实现面向协议,从而大大提高编程效率
      • Swift中的 protocol 可以用于值类型,结构体,枚举;

比较Swift 和OC中的初始化方法 (init) 有什么不同?

swift 的初始化方法,因为引入了两段式初始化和安全检查因此更加严格和准确,

swift初始化方法需要保证所有的非optional的成员变量都完成初始化,

同时 swfit 新增了convenience和 required两个修饰初始化器的关键字

  • convenience只提供一种方便的初始化器,必须通过一个指定初始化器来完成初始化
  • required是强制子类重写父类中所修饰的初始化方法

Swift 和OC 中的自省 有什么区别?

  • OC

    • 自省在OC中就是判断某一对象是否属于某一个类的操作,有以下2中方式
      • [obj iskinOfClass:[SomeClass class]] : obj 必须是 SomeClass 的 对象或 其子类对象;return YES;
      • [obj isMemberOfClass:[SomeClass class]] : obj 必须是 SomeClass 的 对象;return YES;
  • Swift

    • Swift 中由于很多 class 并非继承自 NSObject, 故而 Swift 使用 is 来判断是否属于某一类型, is 不仅可以作用于class, 还可以作用于enum和struct

Swift 与 OC 如何相互调用

  • Swift -> OC
    • 需要创建一个 Target-BriBridging-Header.h (默认在OC项目中,会提示自动创建)的桥文件,在该文件中,导入需要调用的OC代码的头文件即可
  • OC -> Swift
    • 直接导入Target-Swift.h(该文件是Xcode自动创建) Swift如何需要被OC调用,需要使用 @objc 对方法或属性进行修饰

更多请参考 链接


Swift定义常量 和 OC定义常量的区别?

//OC:
const int price = 0;
//Swift:
let price = 0
  • OC中 const 常量类型和数值是在编译时确定的
  • Swift 中 let 常量(只能赋值一次),其类型和值既可以是静态的,也可以是一个动态的计算方法,它们在运行时确定的。

Swift 中的函数重载

参考链接:函数重载

构成函数重载的规则

  • 函数名相同
  • 参数个数不同 || 参数类型不同 || 参数标签不同

注意: 返回值类型 与函数重载无关;返回值类型不同时,函数重载会报错:


Swift 中的枚举,关联值 和 原始值的区分?

参考链接:

  • 关联值
    • 将 枚举的成员值 跟 其他类型的值 关联 存储在一起
    • 存储在枚举变量中,占用枚举变量内存
enum Score {
    case points(Int)
    case grade(Character)
}
  • 原始值
    • 枚举成员可以使用相同类型的默认值预先关联,这个默认值叫做:原始值
    • 不会存储在 枚举变量中,不占用枚举变量内存
enum PokerSuit : Character {
    case spade = "♠"
    case heart = "♥"
    case diamond = "♦"
    case club = "♣"
}

Swift 闭包

闭包是引用类型吗?

闭包和函数都是是引用类型。如果一个闭包被分配给一个变量,这个变量复制给另一个变量,那么他们引用的是同一个闭包,他们的捕捉列表也会被复制。

swift 中的闭包结构是什么样子的?

{
    (参数列表) -> 返回值类型 in 函数体代码
}

什么是尾随闭包

  • 尾随闭包 是一个被 书写在 函数调用 括号 后面的 闭包表达式

参考 :链接 里尾随闭包

什么是逃逸闭包

  • 闭包有可能在函数结束后调用,闭包调用 逃离了函数的作用域,需要通过@escaping 声明

注意:逃逸闭包 不可以 捕获 inout 参数

原因是: 逃逸闭包不确定 何时开始执行,有可能 在执行逃逸闭包时,可变参数已经被程序回收,造成野指针访问

参考:逃逸与非逃逸闭包

什么是自动闭包

自动闭包是一种自动创建的用来把作为实际参数传递给函数的表达式打包的闭包。

它不接受任何实际参数,并且当它被调用时,它会返回内部打包的表达式的值。

这个语法的好处在于通过写普通表达式代替显式闭包而使你省略包围函数形式参数的括号。

func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int? {
    return v1 > 0 ? v1 : v2()
}
getFirstPositive(10, 20)
  • 为了避免与期望冲突,使用了@autoclosure的地方最好明确注释清楚:这个值会被推迟执行
  • @autoclosure 会自动将 20 封装成闭包 { 20 }
  • @autoclosure 只支持 () -> T 格式的参数
  • @autoclosure 并非只支持最后1个参数
  • 有@autoclosure、无@autoclosure,构成了函数重载

如果你想要自动闭包允许逃逸,就同时使用 @autoclosure 和 @escaping 标志。


Swift 中的存储属性与计算属性

存储属性(Stored Property)

  • 类似于成员变量这个概念
  • 存储在实例对象的内存中
  • 结构体、类可以定义存储属性
  • 枚举不可以定义存储属性

计算属性(Computed Property)

  • 本质就是方法(函数)
  • 不占用实例对象的内存
  • 枚举、结构体、类都可以定义计算属性

什么是延迟存储属性(Lazy Stored Property)

  • 使用 lazy 可以定义一个 延迟存储属性,在第一次用到属性的时候才会进行初始化(类似 OC 中的懒加载)

注意点:

  • lazy 属性 必须是 var
  • 因为,let 属性 必须在实例的初始化方法 完成之前 就拥有值
  • 如果多条线程同时第一次访问 lazy 属性,无法保证 属性 只被 初始化 一次 (即线程不是安全的)

什么是 类型 属性?

答案: 参考链接

  • 类型属性(Type Property) :通过类型去访问
    • 存储类型属性(Stored Type Property):整个程序运行过程中,就只有1份内存(类似于全局变量,底层采用了 gcd_once 操作)
    • 计算类型数据(Computed Type Property):

Swift 中如何定义单例模式

可以通过类型属性+let+private 来写单例; 代码如下如下:

// 方式一
public class SingleManager{
    public static let shared = SingleManager()
    private init(){}
}

// 方式二
public class SingleManager{
    public static let shared = {
        //...
        //...
        return SingleManager()
    }()
    private init(){}
}

// 上述两个方法等价,一般推荐 方式二

Swift 中 下标是什么?

  • 使用subscript 可以给任意类型(枚举,结构体,类) 增加下标的功能

subscript 的语法 类似于 实例方法、计算属性,本质就是方法(函数)

func xiabiaoTest() {
    class Point {
        var x = 0.0
        var y = 0.0
        
        subscript (index:Int) -> Double {
            set{
                if index == 0 {
                    x = newValue
                } else if index == 1 {
                    y = newValue
                }
            }
            get{
                if index == 0 {
                    return x
                } else if index == 1 {
                    return y
                }
                return 0
            }
        }
    }
    
    let p = Point()
    p[0] = 11.1 // 调用subscript
    p[1] = 22.2 // 调用subscript
    print(p.x)//11.1 不会调用 subscript
    print(p.y)//22/2 不会调用 subscript
    print(p[0])//11.1 // 调用subscript
    print(p[1])//22.2 // 调用subscript
}

参考 : www.yuque.com/jk-coder/rh…


简要说明Swift中的初始化器?

一图胜千言 针对类

  • 结构体会默认生成 含有参数的初始化器,一旦自定义初始化器,默认的初始化器则不可用
  • 类默认只会生成无参的指定初始化器

  • 类、结构体、枚举都可以定义初始化器
  • 类有2种初始化器: 指定初始化器(designated initializer)、便捷初始化器(convenience initializer)

详情参考:链接


什么是可选链?

可选链是一个调用和查询可选属性、方法和下标的过程,它可能为 nil 。

  • 如果可选项包含值,属性、方法或者下标的调用成功;
  • 如果可选项是 nil ,属性、方法或者下标的调用会返回 nil 。
  • 多个查询可以链接在一起,如果链中任何一个节点是 nil ,那么整个链就会得体地失败。

详情参考:链接


什么是运算符重载?

类、结构体、枚举可以为现有的运算符提供自定义的实现,这个操作叫做:运算符重载

详情参考:链接


Swift中函数的柯里化

参考:柯里化


Swift 实现的一些排序算法


拓展:

关于架构思考,和第三方框架源码请参考

github.com/draveness/a… 强烈推荐

文章参考网络具体链接如下:

同时对部分知识做了添加修改,

部分参考网址如下: