Swift-高级面试题总结
基础篇
1. class 、struct、enmu 的区别?
相同点:
都可以定义属性和方法;
下标语法访问值;
初始化器;
支持扩展、协议
类特有的功能:
继承
允许类型转换
析构方法类型转换
引用计数
区别:
class 是类, 引用类型,可以继承;
struct 是结构体,值类型, 结构体不可以继承;
struct在小数据模型传递和拷贝时比 class 要更安全,在多线程和网络请求时保证数据不被修改;
2.非继承下的代码复用方式?
1.在swift 文件里直接写方法,相当于一个全局函数
2.extension 给类直接扩展方法
3.Set 独有的方法有那些?
let setA: <Int> = [1, 2, 3]
ler setB:<Int> = [1, 3, 5, 7, 8]
// 区并集
let setUnion = setA.union(setB)
// 取交集 A & B
let setIntersect = setA.intersection(setB)
// 取差集 A - B
let setRevers = setA.subtracting(setB)
// 取对称差集, A XOR B = A - B | B - A
let setXor = setA.symmetricDifference(setB)
总结: 并级、交集、差集、对称差集
4.交换数组中的两个元素?
var x = 1
var y = 2
func swap<T>(nums: inout [T], p: Int , q : Int) {
(nums[p], nums[q]) = (nums[q], nums[p]);
}
// 交换两个变量的值
func swap(a: inout Int, b: inout Int) {
(a, b) = (b , a)
}
swap(a: &x, b: &y)
func swap<T, U>(x: T, y: U) -> (T, U) {
return (y, x)
}
5.实现一个 min 函数,返回两个元素较小的元素
func min<T : Comparable>(_ a : T , b : T) -> T {
return a < b ? a : b
}
6.下面的代码有什么问题?
public class Node {
public var value: Int
public var prev: Node?
public var post: Node?
public init(value: Int) {
self.value = value
}
}
在 var prev 或者var post 前面加weak, 避免相互指向, 形成循环引用;
7.实现一个函数,输入是任一整数,输出要返回输入的整数 + 2
func add(num: Int ) -> (Int) -> Int {
return { val in
return num + val
}
}
利用Swift 的 Currying(柯里化) 特性;
函数柯里化:
柯里化(Currying)指的是将原来接受两个参数的函数变成新的接受一个参数的函数的过程。新的函数返回一个以原有第二个参数为参数的函数。
7. map、filter、reduce 的作用
map: 映射,将一个元素根据某个函数 映射 成另一个元素(可以是同类型,也可以是不同类型)
filter: 过滤,将一个元素传入闭包中,如果返回的是false , 就过滤掉
reduce : 先映射后融合,将数组中的所有元素映射融合在一起。
举个例子:
求所有人的钱的总和,这里钱是字符串表示的
struct Person {
let name : String
let money : String
}
let a = Person(name: "王", money: "5.0")
let b = Person(name: "李", money: "6.0")
let c = Person(name: "张", money: "7.0")
let sumMoney = [a , b , c].reduce(0) {
return $0 + ($1.money as NSString).doubleValue
}
print(sumMoney)
7.map与flatmap的区别
map不能将元素映射成可选类型,flatmap可以
8.什么是 copy on write?
写时复制, 指的是 swift 中的值类型, 并不会在一开始赋值的时候就去复制, 只有在需要修改的时候, 才去复制.
9. 如何声明一个只能被类 conform 的 protocol?
protocol OnlyClassProtocol : class {
}
10. guard 使用场景
相当于:
if !(...) {
return
}
可以理解为拦截,凡是不满足 guard 后面条件的,都不会再执行下面的代码。
一般用来解包,不能解包的,就不能执行下面的代码;
11. defer 使用场景?
defer 语句块中的代码, 会在当前作用域结束前调用, 常用场景如异常退出后, 关闭数据库连接
func someQuery() -> ([Result], [Result]){
let db = DBOpen("xxx")
defer {
db.close()
}
guard results1 = db.query("query1") else {
return nil
}
guard results2 = db.query("query2") else {
return nil
}
return (results1, results2)
}
需要注意的是, 如果有多个 defer, 那么后加入的先执行
func someDeferFunction() {
defer {
print("\(#function)-end-1-1")
print("\(#function)-end-1-2")
}
defer {
print("\(#function)-end-2-1")
print("\(#function)-end-2-2")
}
if true {
defer {
print("if defer")
}
print("if end")
}
print("function end")
}
someDeferFunction()
// 输出
// if end
// if defer
// function end
// someDeferFunction()-end-2-1
// someDeferFunction()-end-2-2
// someDeferFunction()-end-1-1
// someDeferFunction()-end-1-2
12.String与NSString的关系与区别.
let someString = "123"
let someNSString = NSString(string: "n123")
let strintToNSString = someString as NSString
let nsstringToString = someNSString as String
1.能够相互转换
2.String是值类型, NSString是引用类型.
13.为什么数组索引越界会Crash,而字典用下标取值时 key 没有对应值的话返回的是 nil 不会Crash?
- 数组索引本来就是访问一段连续地址,越界访问也能访问到内存,但这段内存不一定可用,所以会引起Crash.
2.字典的key并没有对应确定的内存地址,所以是安全的.
14. 下面的代码是否有问题?请说明.
var mutableArray = [1,2,3]
for _ in mutableArray {
mutableArray.removeLast()
}
一直remove到空数组时,空数组不会被遍历,所以永远也不会崩溃.
15.什么是 copy on write时候?
写时复制, 指的是 swift 中的值类型, 并不会在一开始赋值的时候就去复制, 只有在需要修改的时候, 才去复制.
16.try? 和 try!是什么意思?
这两个都用于处理可抛出异常的函数, 使用这两个关键字可以不用写 do catch.
区别:
try? 在用于处理可抛出异常函数时, 如果函数抛出异常, 则返回 nil, 否则返回函数返回值的可选值, 如:
// Optional(2.0)
print(try? divide(2, 1))
// nil
print(try? divide(2, 0))
而 try! 则在函数抛出异常的时候崩溃, 否则则返会函数返回值, 相当于(try? xxx)!, 如:
// 2.0
print(try! divide(2, 1))
// Carsh
print(try! divide(2, 0))
17.associatedtype(关联类型) 的作用?
简单来说就是 protocol 使用的泛型
例如定义一个列表协议
protocol ListProtcol {
associatedtype Element
func push(_ element: Element)
func pop(_ element: Element) -> Element?
}
实现协议的时候, 可以使用 typealias 指定为特定的类型, 也可以自动推断, 如
class IntList: ListProtcol {
// 使用 typealias 指定为 Int
typealias Element = Int
var list = [Element]()
func push(_ element: Element) {
self.list.append(element)
}
func pop(_ element: Element) -> Element? {
return self.list.popLast()
}
}
class DoubleList: ListProtcol {
var list = [Double]()
func push(_ element: Double) {// 自动推断
self.list.append(element)
}
func pop(_ element: Double) -> Double? {
return self.list.popLast()
}
}
使用泛型也可以
class AnyList<T>: ListProtcol {
var list = [T]()
func push(_ element: T) {
self.list.append(element)
}
func pop(_ element: T) -> T? {
return self.list.popLast()
}
}
可以使用 where 字句限定 Element 类型, 如:
extension ListProtcol where Element == Int {
func isInt() ->Bool {
return true
}
}
18.什么时候使用 final
final 用于限制继承和重写.
如果只是需要限制某一个属性,则在某一个属性前加一个 final.
如果需要限制整个类无法被继承, 那么可以在类名之前加一个final
19.public 和 open 的区别
这两个都用于在模块中声明需要对外界暴露的函数.
区别在于, public 修饰的类, 在模块外无法继承,.
open 则可以任意继承
公开度来说, public < open
20.声明一个只有一个参数没有返回值闭包的别名
没有返回值也就是返回值为 Void
typealias SomeClosuerType = (String) -> (Void)
let someClosuer: SomeClosuerType = { (name: String) in
print("hello,", name)
}
someClosuer("world")
// hello, world
21.Self 的使用场景
Self 通常在协议中使用, 用来表示实现者或者实现者的子类类型.
例如, 定义一个复制的协议
protocol CopyProtocol {
func copy() -> Self
}
如果是结构体去实现, 要将Self 换为具体的类型
struct SomeStruct: CopyProtocol {
let value: Int
func copySelf() -> SomeStruct {
return SomeStruct(value: self.value)
}
}
如果是类去实现, 则有点复杂, 需要有一个 required 初始化方法, 具体可以看这里 http://swifter.tips/use-self/
class SomeCopyableClass: CopyProtocol {
func copySelf() -> Self {
return type(of: self).init()
}
required init(){}
}
22.throws 和 rethrows 的用法与作用
throws 用在函数上, 表示这个函数会抛出错误.
有两种情况会抛出错误, 一种是直接使用 throw 抛出, 另一种是调用其他抛出异常的函数时, 直接使用 try xx 没有处理异常.
如:
enum DivideError: Error {
case EqualZeroError;
}
func divide(_ a: Double, _ b: Double) throws -> Double {
guard b != Double(0) else {
throw DivideError.EqualZeroError
}
return a / b
}
func split(pieces: Int) throws -> Double {
return try divide(1, Double(pieces))
}
rethrows 与 throws 类似, 不过只适用于参数中有函数, 且函数会抛出异常的情况, rethrows 可以用 throws 替换, 反过来不行
如:
func processNumber(a: Double, b: Double, function: (Double, Double) throws -> Double) rethrows -> Double {
return try function(a, b)
}
23.观察者模式(设计模式为了解决什么问题,其次通过什么方案来解决,最后才是当前体系下的具体实现方案)
1.首先,观察者模式(Observer Pattern):定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。
- 其次在IOS中典型的观察者模型实现方式为NSNotificationCenter和KVO。
Notification原理:
1.注册通知,观察者Observer,通过NSNotificationCenter的addObserver:selector:name:object接口来注册通知.
2.广播通知,通过postNotificationName:object:userInfo:发送某一类型通知,广播改变
通知对象NSNotification,当有通知来的时候,Center会调用观察者注册的接口来广播通知,同时传递存储着更改内容的NSNotification对象。
KOV原理:
KVO的全称是Key-Value Observer,即键值观察。是一种没有中心枢纽的观察者模式的实现方式。一个主题对象管理所有依赖于它的观察者对象,并且在自身状态发生改变的时候主动通知观察者对象。
1.注册观察者 [object addObserver:self forKeyPath:property options:NSKeyValueObservingOptionNew context:]。
2.更改主题对象属性的值,即触发发送更改的通知。
3.在制定的回调函数中,处理收到的更改通知。
4.注销观察者 [object removeObserver:self forKeyPath:property]。
24.https的连接过程?
-
客户端打包请求,包括url,端口啊,你的账号密码等等。账号密码登陆应该用的是Post方式,所以相关的用户信息会被加载到body里面。这个请求应该包含三个方面:网络地址,协议,资源路径。注意,这里是HTTPS,就是HTTP + SSL / TLS,在HTTP上又加了一层处理加密信息的模块(相当于是个锁)。这个过程相当于是客户端请求钥匙。
-
服务器接受请求。一般客户端的请求会先发送到DNS服务器。 DNS服务器负责将你的网络地址解析成IP地址,这个IP地址对应网上一台机器。这其中可能发生Hosts Hijack和ISP failure的问题。过了DNS这一关,信息就到了服务器端,此时客户端会和服务器的端口之间建立一个socket连接,socket一般都是以file descriptor的方式解析请求。这个过程相当于是服务器端分析是否要向客户端发送钥匙模板。
-
服务器端返回数字证书。服务器端会有一套数字证书(相当于是个钥匙模板),这个证书会先发送给客户端。这个过程相当于是服务器端向客户端发送钥匙模板。
-
客户端生成加密信息。根据收到的数字证书(钥匙模板),客户端会生成钥匙,并把内容锁上,此时信息已经加密。这个过程相当于客户端生成钥匙并锁上请求。
-
客户端发送加密信息。服务器端会收到由自己发送出去的数字证书加锁的信息。 这个时候生成的钥匙也一并被发送到服务器端。这个过程是相当于客户端发送请求。
-
服务器端解锁加密信息。服务器端收到加密信息后,会根据得到的钥匙进行解密,并把要返回的数据进行对称加密。这个过程相当于服务器端解锁请求、生成、加锁回应信息。
-
服务器端向客户端返回信息。客户端会收到相应的加密信息。这个过程相当于服务器端向客户端发送回应。
-
客户端解锁返回信息。客户端会用刚刚生成的钥匙进行解密,将内容显示在浏览器上。
HTTPS加密过程详解请去https原理:证书传递、验证和数据加密、解密过程解析
25.在一个app中间有一个button,在你手触摸屏幕点击后,到这个button收到点击事件,中间发生了什么?(runloop和响应链需要说的清楚,有时还会顺便问问UIResponder、UIControl、UIView的关系)
响应链大概有以下几个步骤:
- 设备将touch到的UITouch和UIEvent对象打包, 放到当前活动的Application的事件队列中.
- 单例的UIApplication会从事件队列中取出触摸事件并传递给单例UIWindow.
- UIWindow使用hitTest:withEvent:方法查找touch操作的所在的视图view.
RunLoop这边我大概讲一下
- 主线程的RunLoop被唤醒
- 通知Observer,处理Timer和Source 0
- Springboard接受touch event之后转给App进程中
- RunLoop处理Source 1,Source1 就会触发回调,并调用_UIApplicationHandleEventQueue() 进行应用内部的分发。
- RunLoop处理完毕进入睡眠,此前会释放旧的autorelease pool并新建一个autorelease pool
深挖请去深入理解RunLoop
UIResponder是UIView的父类,UIView是UIControl的父类。
26. swift 什么是值类型, 什么是引用类型? 数组、enum是什么类型?
swift 里面的类型分为两种:
值类型(Value Types):每个实例都保留了一分独立的数据拷贝,一般以结构体 (struct)、枚举(enum) 或者 元组(tuple)的形式出现。
引用类型(Reference Type):每个实例共享同一份数据来源,一般以 (class)的形式出现。
值类型和引用类型最基本的分别在复制之后的结果,当一个值类型被复制的时候,相当于创造了一个完全独立的实例,这个实例保有属于自己的独有数据,数据不会受到其他实例的数据变化影响.
什么时候该用值类型:
要用==运算符来比较实例的数据时.
你希望那个实例的拷贝能保持独立的状态时.
数据会被多个线程使用时.
什么时候该用引用类型(class):
要用==运算符来比较实例身份的时候.
你希望有创建一个共享的、可变对象的时候.
在 Swift 里面,数组(Array)、字符串(String)、字典(Dictionary)都属于值类型。它们就像 C 语言里面简单的 int 值,是一个个独立的数据个体。你不需要花任何功夫来防范其他代码在暗地里修改它们。更重要的是,你可以在线程之间安全的传递变量,而不需要特地去同步。
25.dynamic framework 和 static framework 的区别是什么?swift静态库的使用?
静态库和动态库
static framework 静态库是每一个程序单独一份
dynamic framework 动态库多个程序之间共享
个人只能打包静态库
26.精简代码:
func divide(dividend: Double?, by divisor: Double?) -> Double? {
if dividend == nil {
return nil
}
if divisor == nil {
return nil
}
if divisor == 0 {
return nil
}
return dividend! / divisor!
}
func divide(dividend: Double?, by divisor: Double?) -> Double? {
guard let dividend = dividend, let divisor = divisor, divisor != 0 else {
return nil
}
return dividend! / divisor!
}
27.下面函数会打印出什么?
var car = "Benz"
let closure = { [car] in
print("I drive \(car)")
}
car = "Tesla"
closure()
- 此时的car是局部变量, 因为 clousre(闭包) 已经申明将 car 复制进去了([car]),不再与外面的 car有关,所以会打印出”I drive Benz”.
如果修改一下呢?
var car = "Benz"
let closure = {
print("I drive \(car)")
}
car = "Tesla"
closure()
- 此时 closure 没有申明复制拷贝 car,所以clousre 用的还是全局的 car 变量,此时将会打印出 “I drive Tesla”
总结: 当声明闭包的时候,捕获列表会创建一份car的copy,所以被捕获到的值是不会改变的,即使你改变car的值。
如果你去掉闭包中的捕获列表,编译器会使用引用代替copy,在这种情况下,当闭包被调用时,变量的值是可以改变的.
28.以下代码会打印出什么?
protocol Pizzeria {
func makePizza(_ ingredients: [String])
func makeMargherita()
}
extension Pizzeria {
func makeMargherita() {
return makePizza(["tomato", "mozzarella"])
}
}
struct Lombardis: Pizzeria {
func makePizza(_ ingredients: [String]) {
print(ingredients)
}
func makeMargherita() {
return makePizza(["tomato", "basil", "mozzarella"])
}
}
let lombardis1: Pizzeria = Lombardis()
let lombardis2: Lombardis = Lombardis()
lombardis1.makeMargherita()
lombardis2.makeMargherita()
["tomato", "basil", "mozzarella"]
["tomato", "basil", "mozzarella"]
分析: 在Lombardis的代码中,重写了makeMargherita的代码,所以永远调用的是Lombardis 中的 makeMargherita.
再进一步,我们把 protocol Pizzeria 中的 func makeMargherita() 删掉,代码变为:
protocol Pizzeria {
func makePizza(_ ingredients: [String])
}
extension Pizzeria {
func makeMargherita() {
return makePizza(["tomato", "mozzarella"])
}
}
struct Lombardis: Pizzeria {
func makePizza(_ ingredients: [String]) {
print(ingredients)
}
func makeMargherita() {
return makePizza(["tomato", "basil", "mozzarella"])
}
}
let lombardis1: Pizzeria = Lombardis()
let lombardis2: Lombardis = Lombardis()
lombardis1.makeMargherita()
lombardis2.makeMargherita()
* 打印结果:
["tomato", "mozzarella"]
["tomato", "basil", "mozzarella"]
因为lombardis1 是 Pizzeria,而 makeMargherita() 有默认实现,这时候我们调用默认实现。
29.Swift 中定义的常量和Objective-C 中定义的常量有什么区别?
OC中定义的常量:
const int number = 0;
Swift 是这样定义常量的:
let number: Int = 0
首先第一个区别:
OC中用 const 来表示常量,而 Swift 中用 let 来判断是不是常量.
上面的区别更进一步说:
OC中 const 表明的常量类型和数值是在 compilation time 时确定的;
Swift 中 let 只是表明常量(只能赋值一次),其类型和值既可以是静态的,也可以是一个动态的计算方法,它们在 runtime 时确定的。
30.写一个函数, 取两个数的最小数?
func getMin<T: Comparable>(a: T, b: t) -> T {
return a < b ? a : b
}
getMin(3, 6)
31. map、 filter、 reduce 的作用?
map 用于映射, 可将一个列表转换成另一个列表;
["1", "2" ,"3"].map{\($0)} 字符串数组转换为数字数组;
[1, 2, 3]
filter用于过滤, 筛选出符合条件的元素;
[4, 5, 6].filter{($0 / 2 == 0)}
[4, 6]
reduce 用于合并;
[1, 2, 3].reduce(""){$0 + "\($1)"}
["123"]
组合使用:
(0 ..< 10).filter{$0 % 2 == 0}.map{"\($0)"}.reduce(""){$0 + $1}
// 02468
Swift 深度篇
1. 一个Sequence(序列) 的索引是不是从0开始的?
不一定, 两个for in 并不能保证都是从0开始,且输出结果一致;
class Countdown: Sequence, IteratorProtocol {
var count: Int
init(count: Int) {
self.count = count
}
func next() -> Int? {
if count == 0 {
return nil
} else {
defer { count -= 1 }
return count
}
}
}
var countDown = Countdown(count: 5)
print("begin for in 1")
for c in countDown {
print(c)
}
print("end for in 1")
print("begin for in 2")
for c in countDown {
print(c)
}
print("end for in 2")
2. 数组都实现了那些协议?
- MutableCollection: 实现了可修改数组, 如a[1] = 2;
- ExpressibleByArrayLiteral: 实现了数组可以从[1, 2, 3]这种字面量初始化的能力;
3.如何自定义模式匹配?
http://swifter.tips/pattern-match/
4.autoclosure 的作用?
- 自动闭包, 会自动将某一个表达式封装为闭包;
func autoClosureFunction(_ closure: @autoclosure () -> Int) {
closure()
}
autoClosureFunction(1)
5.编译选择 whole module optmization 优化了什么?
- 编译器可以跨文件优化编译代码, 不局限于一个文件;
http://www.jianshu.com/p/8dbf2bb05a1c
6.下面代码中mutaing 的作用是什么?
struct Person {
var name: String {
mutating get {
return store
}
}
}
- 让不可变对象无法访问name 属性;
7. Swift 如何让自定义对象支持字面量初始化?
-
有几个协议, 分别是:
ExpressibleByArrayLiteral 可以由数组形式初始化
ExpressibleByDictionaryLiteral 可以由字典形式初始化
ExpressibleByNilLiteral 可以由nil 值初始化
ExpressibleByIntegerLiteral 可以由整数值初始化
ExpressibleByFloatLiteral 可以由浮点数初始化
ExpressibleByBooleanLiteral 可以由布尔值初始化
ExpressibleByUnicodeScalarLiteral
ExpressibleByExtendedGraphemeClusterLiteral
ExpressibleByStringLiteral -
这三种都是由字符串初始化, 上面两种包含有 Unicode 字符和特殊字符
8.dynamic framework 和 static framework 的区别是什么?
静态库和动态库
static framework 静态库是每一个程序单独一份
dynamic framework 动态库多个程序之间共享
个人只能打包静态库
9.为什么数组越界会崩溃,而字典用下标取值时key没有对应值的话返回的是nil而不会崩溃?
数组的对象的储蓄地址是连续的,如果越界了,那取到的地址不一定可用,所以报错。毕竟还是需要有可以信任的部分的
10.一个函数的参数类型只要是数字(Int、Float)都可以,要怎么表示?
泛型
func isNumber<T : SignedNumber>(number : T){
print(" it is a number")
}
10.Swift 中定义常量和 Objective-C 中定义常量有什么区别?
OC是这样定义常量的:
const int number = 0;
Swift 是这样定义常量的:
let number = 0
1.第一个区别,OC中用 const 来表示常量,而 Swift 中用 let 来判断是不是常量.
2.上面的区别更进一步说,OC中 const 表明的常量类型和数值是在 compilation time 时确定的;而 Swift 中 let 只是表明常量(只能赋值一次),其类型和值既可以是静态的,也可以是一个动态的计算方法,它们在 runtime 时确定的。
11.?? 的作用
当可选值为nil时,输出后面给定的值
var test : String? = nil
print(test ?? "optional = nil")
//输出 optional = nil
12.swift inout 的作用?
在siwft中,除了class是默认引用传递外,其他数据类型如float,struct等等都属于值传递。如果我们在对其进行处理的时候希望能够在函数中直接对其原值进行修改直接修改,那么最好的方法就是直接使用inout来修饰传入参数,值得注意的是inout 无法修饰带有默认值的参数且经过inout修饰之后,无法再被let和var修饰
https://www.jianshu.com/p/28eec9d7da84
13.swift dynamic的作用.
由于swift是一门静态语言,所以没有Objective-C中的消息发送这些动态机制,dynamic的作用就是让swift代码也能有oc中的动态机制,常用的就是KVO。
使用dynamic关键字标记属性,使属性启用Objc的动态转发功能;
dynamic只用于类,不能用于结构体和枚举,因为它们没有继承机制,而Objc的动态转发就是根据继承关系来实现转发。
14.深入理解swift 的派发机制(重要)
15.?? 的作用?
当 ?? 前面的值为nil 的时候就取 ??后面的值
16.lazy 的作用?
懒加载,只有属性被调用的时候才会执行lazy
17. Swift中的mutating作用?
Swift中protocol的功能比OC中强大很多,不仅能再class中实现,同时也适用于struct、enum。
使用 mutating 关键字修饰方法是为了能在该方法中修改 struct 或是 enum 的变量,在设计接口的时候,也要考虑到使用者程序的扩展性。所以要多考虑使用mutating来修饰方法。