Swift常见知识解答

一、基础


1、class 和 struct 的区别?

a、struct会自动生成需要的构造方法(constructor),哪个属性没有赋初始值就会生成以哪个属性为参数的构造方法。而class没有,要自己写

struct StructTest {
    var name:String
    var age:Int
}

class ClassTest {
    var name:String?
    var age:Int?
}
var structTest = StructTest(data: 66)
var classTest = ClassTest()

b、struct的属性可以不赋初始值,而class的属性必须赋初始值或者设为可选类型,下面也是可以的,区别只是struct自动生成了带参数的init方法

class ScanInfo: NSObject {
    var advertisementData: [String : Any]
    var rssi: NSNumber
    init(advertisementData: [String : Any], rssi: NSNumber) {
        self.advertisementData = advertisementData
        self.rssi = rssi
    }
}

class遵守了协议没有实现方法会报错,而结构体(struct)不会,在正常的项目中struct也会报错,是xcode playground的问题

c、struct是值类型(Value Type),深拷贝。class是引用类型(Reference Type),浅拷贝。

struct aStruct {
    var data: Int = 77
}
class aClass {
    var data: Int = 77
}

var orig = aStruct()
var copied = orig
orig.data = 88
print("Original data:\(orig.data), copied data:\(copied.data)")
//Original data:88, copied data:77

var ori = aClass()
let ref = ori
ori.data = 88
print("Original data:\(ori.data), copied data:\(ref.data)")
//Original data:88, copied data:88

上面代码当ori.data的值变了之后,ref.data的值也跟着变了。而orig.data的值没有跟着copied.data的值变化
ref前面用let修饰,依然可以改变ref.data的值。而要改变copied.data的值,copied前面必须用var

class中可以有单例对象属性,struct中不能有(区别是在let前加static,struct中是可以创建单例的)

class ClassTest {
    let sharaedInstance = ClassTest()
    private init() {
        print("调用单利类")
    }
}
struct StructTest {
    static let sharaedInstance = StructTest()
    private init() {
        print("调用单利类")
    }
}

d、struct不能继承,class可以继承
e、NSUserDefaults:Struct 不能被序列化成 NSData 对象,无法归解档

class ClassTest: NSCoding {
    required init?(coder: NSCoder) {
    }
    func encode(with coder: NSCoder) {
    }
    var name: String = "class"
    var age: Int = 0
}

struct StructTest: NSCoding {//报错
    //Non-class type 'StructTest' cannot conform to class protocol 'NSCoding'
    var name: String = "struct"
    var age: Int = 10
}

因为归解档的类必须遵守NSCoding协议,struct不能遵守NSCoding协议
iOS-NSUserDefaults
Swift学习记录 – Swift中throws处理方式
iOS 13归档解档
f、当你的项目的代码是 Swift 和 Objective-C 混合开发时,你会发现在 Objective-C 的代码里无法调用 Swift 的 Struct。因为要在 Objective-C 里调用 Swift 代码的话,对象需要继承于 NSObject。
g、内存分配:struct分配在栈中,class分配在堆中。struct比class更“轻量级”(struct是跑车跑得快,class是SUV可以载更多的人和货物)

知识延伸:“堆”和“栈”的区别,为什么访问struct比class块?
“堆”和“栈”并不是数据结构上的Heap跟Stack,而是程序运行中的不同内存空间。栈是程序启动的时候,系统事先分配的,使用过程中,系统不干预;堆是用的时候才向系统申请的,用完了需要交还,这个申请和交还的过程开销相对就比较大了。

栈是编译时分配空间,而堆是动态分配(运行时分配空间),所以栈的速度快。

从两方面来考虑:
**分配和释放:堆在分配和释放时都要调用函数(MALLOC,FREE),比如分配时会到堆空间去寻找足够大小的空间(因为多次分配释放后会造成空洞),这些都会花费一定的时间,而栈却不需要这些。
**访问时间:访问堆的一个具体单元,需要两次访问内存,第一次得取得指针,第二次才是真正得数据,而栈只需访问一次。


2、不通过继承,代码复用(共享)的方式有哪些?

在swift 文件里直接写方法,相当于一个全局函数。
扩展 --- Extensions
扩展 就是为一个已有的类、结构体、枚举类型或者协议类型添加新功能。这包括在没有权限获取原始源代码的情况下扩展类型的能力(即 逆向建模 )。扩展和 Objective-C 中的分类类似。
Swift 中的扩展可以:
添加计算型属性和计算型类型属性
定义实例方法和类型方法
提供新的构造器
定义下标
定义和使用新的嵌套类型
使一个已有类型符合某个协议
协议 --- Protocols
协议 规定了用来实现某一特定任务或者功能的方法、属性,以及其他需要的东西。类、结构体或枚举都可以遵循协议,并为协议定义的这些要求提供具体实现
另外,规定了用来实现某一特定任务或者功能的方法、属性,以及其他需要的东西。类、结构体或枚举都可以遵循协议,并为协议定义的这些要求提供具体实现


3、Set 独有的方法有哪些?

let setA: Set<Int> = [1, 2, 3, 4, 4] //[1, 2, 3, 4]
let setB: Set<Int> = [1, 3, 5, 7, 9]
//取并集 A | B
let setUnion = setA.union(setB)
print(setUnion) //[7, 4, 3, 9, 2, 1, 5]
//取交集 A & B
let setIntersect = setA.intersection(setB) 
print(setIntersect) //[1, 3]
//取差集 A - B
let setRevers = setA.subtracting(setB)
print(setRevers) //[4, 2]
//取对称差集, A XOR B = A - B | B - A
let setXor = setA.symmetricDifference(setB)
print(setXor) //[9, 7, 4, 5, 2]

使用“是否相等”运算符(==)来判断两个集合包含的值是否全部相同。
使用 isSubset(of:) 方法来判断一个集合中的所有值是否也被包含在另外一个集合中。
使用 isSuperset(of:) 方法来判断一个集合是否包含另一个集合中所有的值。
使用 isStrictSubset(of:) 或者 isStrictSuperset(of:) 方法来判断一个集合是否是另外一个集合的子集合或者父集合并且两个集合并不相等。
使用 isDisjoint(with:) 方法来判断两个集合是否不含有相同的值(是否没有交集)。

let houseAnimals: Set = ["🐶", "🐱"]
let farmAnimals: Set = ["🐮", "🐔", "🐑", "🐶", "🐱"]
let cityAnimals: Set = ["🐦", "🐭"]
houseAnimals.isSubset(of: farmAnimals)
// true
farmAnimals.isSuperset(of: houseAnimals)
// true
farmAnimals.isDisjoint(with: cityAnimals)
// true

4、实现一个 min 函数,返回两个元素较小的元素

func myMain<T: Comparable>(_ a: T, _ b: T) -> T {
    return a < b ? a : b
}
print(myMain(8, 2)) // 2

5、map、filter、reduce 的作用

Swift的标准数组支持三个高阶函数:map,filter和reduce.是OC中没有实现的方法。
a、map 作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后放入到新的数组中。

[1, 2, 3].map($0 + 1) // -> [2, 3, 4]
[1, 2, 3].map{"\($0)"}// 数字数组转换为字符串数组  ["1", "2", "3"]

map:map方法作用是把数组[T]通过闭包函数把每一个数组中的元素变成U类型的值,最后组成数组[U]。定义如下:

func map<T, U>(xs: [T], f: (T) ->U) -> [U] {
    var result: [U] = []
    for x in xs {
        result.append(f(x))
    }
    return result
}

另外 map 的回调函数接受三个参数,分别是当前索引元素,索引,原数组

['1','2','3'].map(parseInt) //parseInt('1', 0) -> 1

b、filter 的作用也是生成一个新数组,在遍历数组的时候将返回值为 true 的元素放入新数组,我们可以利用这个函数删除一些不需要的元素

[1, 2, 3].filter{$0 % 2 == 0} // 筛选偶数 // [2]
let array = [1, 2, 4, 6]
let newArray = array.filter($0 !== 6)
console.log(newArray) // [1, 2, 4]

filter就是筛选的功能,参数是一个用来判断是否筛除的筛选闭包,根据闭包函数返回的Bool值来过滤值。为True则加入到结果数组中。定义如下:

func filter(includeElement: (T) -> Bool) -> [T]

和 map 一样,filter 的回调函数也接受三个参数,用处也相同。
c、reduce的作用给定一个类型为U的初始值,把数组[T]中每一个元素传入到combine的闭包函数里面,通过计算得到最终类型为U的结果值。
reduce的类似实现是:

func reduce<A, R>(arr: [A], _ initialValue: R, combine: (R, A) -> R) -> R {
    var result = initialValue
    for i in arr {
            result = combine(result, i)
    }
    return result 
}

reduce 合并

[1, 2, 3].reduce(""){$0 + "\($1)"}// 转换为字符串并拼接
// "123" 

6、map 与 flatmap 的区别
和 map 不同, flatMap 有两个重载。

func flatMap(transform: (Self.Generator.Element) throws -> T?) -> [T]
func flatMap(transform: (Self.Generator.Element) -> S) -> [S.Generator.Element]

flatMap 的闭包接受的是数组的元素,但返回的是一个 SequenceType 类型,也就是另外一个数组。

let numbersCompound = [[1,2,3],[4,5,6]];
var res = numbersCompound.map { $0.map{ $0 + 2 } }
// [[3, 4, 5], [6, 7, 8]]
var flatRes = numbersCompound.flatMap { $0.map{ $0 + 2 } }
// [3, 4, 5, 6, 7, 8]

那么为什么 flatMap 调用后会对数组降维呢? 我们可以从它的源码中窥探一二(Swift 不是开源了吗~)。
文件位置: swift/stdlib/public/core/SequenceAlgorithms.swift.gyb

extension Sequence {
//...
public func flatMap(
@noescape transform: (${GElement}) throws -> S
) rethrows -> [S.${GElement}] {
var result: [S.${GElement}] = []
for element in self {
result.append(contentsOf: try transform(element))
}
return result
}
//...
}

这就是 flatMap 的完整源码了, 它的源码也很简单, 对遍历的每一个元素调用 try transform(element)。 transform 函数就是我们传递进来的闭包。
然后将闭包的返回值通过 result.append(contentsOf:) 函数添加到 result 数组中。
那我们再来看一下 result.append(contentsOf:) 都做了什么, 它的文档定义是这样:
Append the elements of newElements to self.
简单说就是将一个集合中的所有元素,添加到另一个集合。 还以我们刚才这个二维数组为例:

let numbersCompound = [[1,2,3],[4,5,6]];
var flatRes = numbersCompound.flatMap { $0.map{ $0 + 2 } }
// [3, 4, 5, 6, 7, 8]

flatMap 首先会遍历这个数组的两个元素 [1,2,3] 和 [4,5,6], 因为这两个元素依然是数组, 所以我们可以对他们再进行 map 操作: 0.map{0 + 2 }。
这样, 内部的 0.map{0 + 2 } 调用返回值类型还是数组, 它会返回 [3,4,5] 和 [6,7,8]。
然后, flatMap 接收到内部闭包的这两个返回结果, 进而调用 result.append(contentsOf:) 将它们的数组中的内容添加到结果集中,而不是数组本身。
那么我们最终的调用结果理所当然就应该是 [3, 4, 5, 6, 7, 8] 了。

flatMap 的另一个重载

func flatMap(transform: (Self.Generator.Element) -> T?) -> [T]

从定义中我们看出, 它的闭包接收的是 Self.Generator.Element 类型, 返回的是一个 T? 。 我们都知道,在 Swift 中类型后面跟随一个 ?, 代表的是 Optional 值。 也就是说这个重载中接收的闭包返回的是一个 Optional 值。 更进一步来说,就是闭包可以返回 nil。
我们来看一个例子:

let optionalArray: [String?] = [``"AA"``, nil, ``"BB"``, ``"CC"``]
var optionalResult = optionalArray.flatMap{ $0 }
// ["AA", "BB", "CC"]`

这样竟然没有报错, 并且 flatMap 的返回结果中, 成功的将原数组中的 nil 值过滤掉了。 再仔细观察,你会发现更多。 使用 flatMap 调用之后, 数组中的所有元素都被解包了, 如果同样使用 print 函数输出原始数组的话, 大概会得到这样的结果:

[Optional(``"AA"``), nil, Optional(``"BB"``), Optional(``"CC"``)]

而使用 print 函数输出 flatMap 的结果集时,会得到这样的输出:

["AA", "BB", "CC"]

也就是说原始数组的类型是 [String?] 而 flatMap 调用后变成了 [String]。 这也是 flatMap 和 map 的一个重大区别。 如果同样的数组,我们使用 map 来调用, 得到的是这样的输出:

[Optional(``"AA"``), nil, Optional(``"BB"``), Optional(``"CC"``)]

这就和原始数组一样了。 这两者的区别就是这样。 map 函数值对元素进行变换操作。 但不会对数组的结构造成影响。 而 flatMap 会影响数组的结构。再进一步分析之前,我们暂且这样理解。

flatMap 的这种机制,而已帮助我们方便的对数据进行验证,比如我们有一组图片文件名, 我们可以使用 flatMap 将无效的图片过滤掉:

var imageNames = [``"test.png"``, ``"aa.png"``, ``"icon.png"``]
imageNames.flatMap{ UIImage(named: $0) }

那么 flatMap 是如何实现过滤掉 nil 值的呢? 我们还是来看一下源码:

extension Sequence {
// ...
public func flatMap(
    @noescape transform: (${GElement}) throws -> T?) rethrows -> [T] {
    var result: [T] = []
    for element in self {
        if let newElement = try transform(element) {
            result.append(newElement)
        }
    }
    return result
}
// ...
}

依然是遍历所有元素,并应用 try transform(element) 闭包的调用, 但关键一点是,这里面用到了 if let 语句, 对那些只有解包成功的元素,才会添加到结果集中:

if let newElement = try transform(element) {
    result.append(newElement)
}

这样, 就实现了我们刚才看到的自动去掉 nil 值的效果了。


7、什么是 copy on write
当值类型(如struct)在复制时,复制的对象和元、原对象实际上在内存中指向同一个对象。当且仅当修改复制后的对象时,才会在内存中重新创建一个新的对象。举例:

var array1: [Int] = [0, 1, 2, 3]
var array2 = array1
unsafeAddressOf(address: array1)
unsafeAddressOf(address: array2)
array2.append(4)
unsafeAddressOf(address: array2)

我们看到当array2的值没有发生变化的时候,array1和array2指向同一个地址,但是当array2的发生变化时,array2指向地址也变了,很奇怪是吧。在 Swift 标准库中,像是 Array,Dictionary 和 Set 这样的集合类型是通过一种叫做写时复制 (copy-on-write) 的技术实现的。


8、如何获取当前代码的函数名和行号

print(#line)      //用于获取当前行号
print(#file)      //用于获取当前文件文件名
print(#column)    //用于获取当前列编号
print(#function)  //用于获取当前函数名

9、如何声明一个只能被类 conform 的 protocol

声明协议的时候, 加一个 class 即可,协议的继承列表中,通过添加 class 关键字来限制协议只能被类类型遵循,而结构体或枚举不能遵循该协议。class 关键字必须第一个出现在协议的继承列表中,在其他继承的协议之前:

protocol SomeClassOnlyProtocol: class, SomeInheritedProtocol {
    // 这里是类类型专属协议的定义部分
}

10、guard 使用场景
guard 和 if 类似, 不同的是, guard 总是有一个 else 语句, 如果表达式是假或者值绑定失败的时候, 会执行 else 语句, 且在 else 语句中一定要停止函数调用
使用 guard 来表达 “提前退出”的意图,有以下 使用场景 :
在验证入口条件时
在成功路径上提前退出
在可选值解包时(拍扁 if let..else 金字塔)
return 和 throw 中
日志、崩溃和断言中
而下面则是尽量 避免使用 的场景:
不要用 guard :替代琐碎的 if..else 语句
不要用 guard :作为 if 的相反情况
不要:在 guard 的 else 语句中放入复杂代码
例如

guard 1 + 1 == 2 else {
    fatalError("something wrong")
}

常用使用场景为, 用户登录的时候, 验证用户是否有输入用户名密码等

guard let userName = self.userNameTextField.text,
  let password = self.passwordTextField.text else {
    return
}

11、defer 使用场景

很简单,用一句话概括,就是 defer block 里的代码会在函数 return 之前执行,无论函数是从哪个分支 return 的,还是有 throw,还是自然而然走到最后一行。
这个关键字就跟 Java 里的 try-catch-finally 的 finally 一样,不管 try catch 走哪个分支,它都会在函数 return 之前执行。而且它比 Java 的 finally 还更强大的一点是,它可以独立于 try catch 存在,所以它也可以成为整理函数流程的一个小帮手。在函数 return 之前无论如何都要做的处理,可以放进这个 block 里,让代码看起来更干净一些~

//伪代码
var fridgeIsOpen = false
let fridgeContent = ["milk", "eggs", "leftovers"]
func fridgeContains(_ food: String) -> Bool {
    fridgeIsOpen = true
    defer {
        fridgeIsOpen = false
    }
    let result = fridgeContent.contains(food)
    return result
}
fridgeContains("banana")
print(fridgeIsOpen)

这个例子里执行的顺序是,先 fridgeIsOpen = true ,然后是函数体正常的流程,最后在 return 之前执行 fridgeIsOpen = false 。
几个简单的使用场景

try catch 结构
最典型的场景,我想也是 defer 这个关键字诞生的主要原因吧:

func foo() {
    defer {
        print("finally")
    }
    do {
        throw NSError()
        print("impossible")
    } catch {
        print("handle error")
    }
}

不管 do block 是否 throw error,有没有 catch 到,还是 throw 出去了,都会保证在整个函数 return 前执行 defer 。在这个例子里,就是先 print 出 "handle error" 再 print 出 "finally"。
do block 里也可以写 defer :

do {
    defer {
       print("finally")
    }
    throw NSError()
    print("impossible")
} catch {
    print("handle error")
}

那么它执行的顺序就会是在 catch block 之前,也就是先 print 出 "finally" 再 print 出 "handle error"。

清理工作、回收资源
跟 swift 文档举的例子类似, defer 一个很适合的使用场景就是用来做清理工作。文件操作就是一个很好的例子:
关闭文件

func foo() {
    let fileDescriptor = open(url.path, O_EVTONLY)
    defer {
        close(fileDescriptor)
    }
// use fileDescriptor...
}

这样就不怕哪个分支忘了写,或者中间 throw 个 error,导致 fileDescriptor 没法正常关闭。还有一些类似的场景:
dealloc 手动分配的空间

func foo() {
   let valuePointer = UnsafeMutablePointer<T>.allocate(capacity: 1)
   defer {
       valuePointer.deallocate(capacity: 1)
   }
 // use pointer...
}

加/解锁:下面是 swift 里类似 Objective-C 的 synchronized block 的一种写法,可以使用任何一个 NSObject 作 lock

func foo() {
   objc_sync_enter(lock)
   defer { 
       objc_sync_exit(lock)
   }
 // do something...
}

像这种成对调用的方法,可以用 defer 把它们放在一起,一目了然。

调 completion block
这是一个让我感觉“如果当时知道 defer ”就好了的场景,就是有时候一个函数分支比较多,可能某个小分支 return 之前就忘了调 completion block,结果藏下一个不易发现的 bug。用 defer 就可以不用担心这个问题了:

func foo(completion: () -> Void) {
    defer {
        self.isLoading = false
        completion()
    }
    guard error == nil else { return } 
// handle success
}

有时候 completion 要根据情况传不同的参数,这时 defer 就不好使了。不过如果 completion block 被存下来了,我们还是可以用它来确保执行后能释放:

func foo() {
    defer {
        self.completion = nil
    }
    if (succeed) {
        self.completion(.success(result))
    } else {
    self.completion(.error(error))
 }
}

调 super 方法
有时候 override 一个方法,主要目的是在 super 方法之前做一些准备工作,比如 UICollectionViewLayout 的 prepare(forCollectionViewUpdates:) ,那么我们就可以把调用 super 的部分放在 defer 里:

func override foo() {
    defer {
        super.foo()
    }
// some preparation before super.foo()...
}

多个 defer
一个 scope 可以有多个 defer,顺序是像栈一样倒着执行的:每遇到一个 defer 就像压进一个栈里,到 scope 结束的时候,后进栈的先执行。如下面的代码,会按 1、2、3、4、5、6 的顺序 print 出来。

func foo() {
    print("1")
    defer {
        print("6")
    }
    print("2")
    defer {
        print("5")
    }
    print("3")
    defer {
        print("4")
    }
}

一些细节
任意 scope 都可以有 defer
虽然大部分的使用场景是在函数里,不过理论上任何一个 { } 之间都是可以写 defer 的。比如一个普通的循环:

var sumOfOdd = 0
for i in 0...10 {
 defer {
 print("Look! It's \(i)")
 }
 if i % 2 == 0 {
 continue
 }
 sumOfOdd += i
}

continue 或者 break 都不会妨碍 defer 的执行。甚至一个平白无故的 closure 里也可以写 defer :

{
 defer { print("bye!") }
 print("hello!")
}

就是这样没什么意义就是了……
必须执行到 defer 才会触发
假设有这样一个问题:一个 scope 里的 defer 能保证一定会执行吗? 答案是否……比如下面这个例子:

func foo() throws {
 do {
 throw NSError()
 print("impossible")
 }
 defer {
 print("finally")
 }
}
try?foo()

不会执行 defer,不会 print 任何东西。这个故事告诉我们,至少要执行到 defer 这一行,它才保证后面会触发。同样道理,提前 return 也是一样不行的:

func foo() {
 guard false else { return }
 defer {
 print("finally")
 }
}

12、String 与 NSString 的关系与区别
a、String类型是值类型(不再是对象类型),字符串在进行常量、变量赋值操作或在函数/方法中传递时,会进行值拷贝。 任何情况下,都会对已有字符串值创建新副本,并对该新副本进行传递或赋值操作。
b、String 可以支持字符遍历NSString 不支持。
c、String 是一个结构体,性能更高;NSString 是一个 NSObject 对象,性能相对会差。
d、现在还有一些功能,用 String 不方便实现: 取字符串的字串/判断包含/正则表达式。
NSString 与 String 之间可以随意转换

let someString = "123"
let someNSString = NSString(string: "n123")
let strintToNSString = someString as NSString
let nsstringToString = someNSString as String 

13、怎么获取一个 String 的长度
不考虑编码, 只是想知道字符的数量, 用characters.count

"hello".characters.count // 5
"你好".characters.count // 2
"こんにちは".characters.count // 5

如果想知道在某个编码下占多少字节, 可以用

"hello".lengthOfBytes(using: .ascii) // 5
"hello".lengthOfBytes(using: .unicode) // 10
"你好".lengthOfBytes(using: .unicode) // 4
"你好".lengthOfBytes(using: .utf8) // 6
"こんにちは".lengthOfBytes(using: .unicode) // 10
"こんにちは".lengthOfBytes(using: .utf8) // 15

14、如何截取 String 的某段字符串
转成NSString,然后进行截取,这样比较方便,返回的对象类型就是String,不需要再次转换,很方便


15、throws 和 rethrows 的用法与作用

Swift中 throwrethrows 关键字用于异常处理(Error handling),都是用在函数中,可以简单理解为throw让函数或者方法可能出现的异常得到处理(比如用do-catch处理),而rethrows只是传递throw,本身没有针对函数或方法,下面详细解释一下:
throw
throws关键字首先用在函数申明中,放在返回类型的前面,比如标准库中map的函数签名:

func map<T>(_ transform: (Int) throws -> T) rethrows -> [T]

然后在函数内部,如果出现可能的异常,就可以throw这个异常,通常可以用一个枚举来代表一类异常,这个枚举可以实现Error协议,比如:

enum TestError: Error {
    case errorOne(Int)
    case errorTwo(String)
    case errorThree
    case errorUnknown
}

func testThrow(num: Int) throws -> String {
    switch num {
    case 1:
        throw TestError.errorOne(1)
    case 2:
        throw TestError.errorTwo("2")
    case 3:
        throw TestError.errorThree
    case 4:
        throw TestError.errorUnknown
    default:
        return "No Error"
    }
}

这样,通过throw函数中可能出现的异常,让函数反馈错误,强制调用这个函数的程序员处理所有可能的错误,减少维护成本。
下面使用do-catch来处理可能的异常:

do {
    let testResult: String = try testThrow(num: 2)
    print(testResult)  // Will no print
} catch TestError.errorOne(let num) {
    print(num)  // 1
} catch TestError.errorTwo(let str) {
    print(str)  // 2
} catch TestError.errorThree {
    print(TestError.errorThree)  // errorThree
} catch let err {
    print(err)  // errorUnknown
}
//  2

rethrows
rethrows关键字只起到传递异常的作用,在一个函数或者方法中,调用一个会throw的函数,就可以通过rethrows传递可能的异常,接上面的例子

func testRethrow(testThrowCall: (Int) throws -> String, num: Int) rethrows -> String {
    try testThrowCall(num)
}

观察函数申明,其实就是有一个会throw函数作为参数,然后在返回类型前面添加关键字rethrows,函数内部直接调用那个会throw的函数,传递可能的异常,处理rethrows的函数或方法就和处理throw的函数或方法一样:

do {
    let testResult: String = try testRethrow(testThrowCall: testThrow, num: 4)
    print(testResult)  // Will no print
} catch TestError.errorOne(let num) {
    print(num)  // 1
} catch TestError.errorTwo(let str) {
    print(str)  // 2
} catch TestError.errorThree {
    print(TestError.errorThree)  // errorThree
} catch let err {
    print(err)  // errorUnknown
}
//  errorUnknown

总结
throw在函数或者方法中抛出异常,让调用者必须明确地处理可能的异常,rethrows本身并不抛出异常或者处理异常,其只起到传递异常的作用,最后回到标准库中的map函数,综合上述举个例子:

enum NegativeError: Error {
    case negative
}

let nums = [-1, 1, 2, 3, 4]

do {
    let strNums = try nums.map { (num) throws -> String in
        if num >= 0 {
            return String(num)
        } else {
            throw NegativeError.negative
        }
    }
    print(strNums)  // Will no print
} catch let err {
    print(err)
}
// negative

这里在map的函数参数中使用了throw,当元素小于0时抛出异常


16、 try?和 try!是什么意思
这两个都用于处理可抛出异常的函数, 使用这两个关键字可以不用写do catch.
try? 是用来修饰一个可能会抛出错误的函数。会将错误转换为可选值,当调用try?+函数或方法语句时候,如果函数或方法抛出错误,程序不会发崩溃,而返回一个nil,如果没有抛出错误则返回可选值
try! 会忽略错误传递链,并声明“do or die”。如果被调用函数或方法没有抛出异常,那么一切安好;但是如果抛出异常,二话不说,给你崩


17、 associatedtype 的作用


18、什么时候使用 final
final 用于限制继承和重写. 如果只是需要在某一个属性前加一个 final。
如果需要限制整个类无法被继承, 那么可以在类名之前加一个final


19、open、public等修饰符
open
用open修饰的类可以在本某块(sdk),或者其他引入本模块的(sdk,module)继承,如果是修饰属性的话可以被此模块或引入了此某块(sdk)的模块(sdk)所重写
public
类用public(或级别更加等级更低的约束(如private等))修饰后只能在本模块(sdk)中被继承,如果public是修饰属性的话也是只能够被这个module(sdk)中的子类重写
internal
是在模块内部可以访问,在模块外部不可以访问,a belong A , B import A, A 可以访问 a, B 不可以访问a.比如你写了一个sdk。那么这个sdk中有些东西你是不希望外界去访问他,这时候你就需要internal这个关键字(我在导入第三方框架时发现其实没有定义的话sdk里面是默认internal的)
fileprivate
这个修饰跟名字的含义很像,file private 就是文件之间是private的关系,也就是在同一个source文件中还是可以访问的,但是在其他文件中就不可以访问了 a belong to file A, a not belong to file B , 在 file A 中 可以访问 a,在 file B不可以访问a
private
这个修饰约束性比fileprivate的约束性更大,private 作用于某个类,也就是说,对于 class A ,如果属性a是private的,那么除了A外其他地方都不能访问了(fileprivate 和private都是一种对某个类的限制性约束。fileprivate的适用场景可以是某个文件下的extension,如果你的类中的变量定义成了private那么这个变量在你这个类在这个类的文件的拓展中就无法访问了,这时就需要定义为fileprivate)
最后是 Guiding Principle of Access Levels (访问级别的推导原则):不能在低级别的修饰中定义比自身更高的级别修饰,如public不能修饰在private类中的属性


20、声明一个只有一个参数没有返回值闭包的别名

typealias SomeClosuerType = (String) -> (Void)
let someClosuer: SomeClosuerType = { (name: String) in
    print("hello,", name)
}
someClosuer("world")
// hello, world

21、定义静态方法时关键字 static 和 class 有什么区别

static 定义的方法不可以被子类继承, class 则可以

class AnotherClass {
    static func staticMethod(){}
    class func classMethod(){}
}
class ChildOfAnotherClass: AnotherClass {
    override class func classMethod(){}
    //override static func staticMethod(){}// error
}

22、Swift中self和Self

当你用错Self的时候编译器会这样提示

'Self' is only available in a protocol or as the result of a method in a class

分割开来的话就是两个意思
a、Self可以用于协议(protocol)中限制相关的类型
b、Self可以用于类(Class)中来充当方法的返回值类型
对于第一种情况,可以参考书中的例子

protocol Copyable {
    func copy() -> Self
    func clamp(intervalToClamp: Self) -> Self
}

在这个协议中的两个方法都用Self来对类型进行限制
第二种情况可以参考下面这个例子

class A: Copyable {
    var num = 1
    required init() {
    }
    
    func copy() -> Self {
        let types = type(of: self)
        print(types)
        let result = types.init()
        result.num = num
        return result
    }
    
    func clamp(intervalToClamp: A) -> Self {
        let result = type(of: self).init()
        result.num = num
        return result
    }
    
    class func calssFunc() -> Self {
        let type = self
        print(type)
        let result = type.init()
        return result
    }
}

class B: A {
    func clamp(intervalToClamp: B) -> Self {
        let result = type(of: self).init()
        result.num = num
        return result
    }
}

let type = A.self
type.calssFunc()

let typeB = B.self
typeB.calssFunc()

let objectA = A()
objectA.num = 100

let newObjectA = objectA.copy()
objectA.num = 1

let objectB = B()
objectB.num = 100
let newB = objectB.copy()

这个例子中有两个类A和B,A实现了协议中的两个方法并包含一个类方法,B是A的子类,它也实现了协议的方法。
可以看到对于A来说
A的实例方法中self表示当前实例,利用type(of: self)获取当前对象的类型,
A的类方法中self就表示当前类的类型,而Self则只能用来表示返回值的类型。
对比A和B所实现的协议的方法可以看出在协议中的方法接收的参数类型必须换成各自类的类型,否则会报文章开头的那个错误。

综上可看出对于Self来说它只是表示特定类型,并且只能用在协议中或者作为某个类的方法的返回值类型,而self在实例方法中代指当前实例,在类方法中则代指当前类。


22、dynamic 的作用

由于 swift 是一个静态语言, 所以没有 Objective-C 中的消息发送这些动态机制, dynamic 的作用就是让 swift 代码也能有 Objective-C 中的动态机制, 常用的地方就是 KVO 了, 如果要监控一个属性, 则必须要标记为 dynamic

使用dynamic关键字标记属性,使属性启用Objc的动态转发功能;
dynamic只用于类,不能用于结构体和枚举,因为它们没有继承机制,而Objc的动态转发就是根据继承关系来实现转发。


23、什么时候使用 @objc

@objc 用途是为了在 Objective-C 和 Swift 混编的时候, 能够正常调用 Swift 代码. 可以用于修饰类, 协议, 方法, 属性。
常用的地方是在定义 delegate 协议中, 会将协议中的部分方法声明为可选方法, 需要用到@objc

@objc protocol OptionalProtocol {
    @objc optional func optionalFunc()
    func normalFunc()
}
class OptionProtocolClass: OptionalProtocol {
    func normalFunc() {
    }
}
let someOptionalDelegate: OptionalProtocol = OptionProtocolClass()
someOptionalDelegate.optionalFunc?()

25、Optional(可选型) 是用什么实现的

Optional 是一个泛型枚举
大致定义如下:

enum Optional<Wrapped> {
  case none
  case some(Wrapped)
}

除了使用let someValue: Int? = nil之外, 还可以使用let optional1: Optional<Int> = nil来定义


26、如何自定义下标获取
实现 subscript 即可, 如

extension AnyList {
    subscript(index: Int) -> T{
        return self.list[index]
    }
    subscript(indexString: String) -> T?{
        guard let index = Int(indexString) else {
            return nil
        }
        return self.list[index]
    }
}

索引除了数字之外, 其他类型也是可以的


27、?? 的作用

?? 是空合运算符。可选值的默认值, 当可选值为nil 的时候, 会返回后面的值. 如
let someValue = optional1 ?? 0
比如a ?? b ,将对可选类型a进行为空判断,如果a包含一个值,就进行解封,否则就返回一个默认值b。
表达式 a 必须是 Optional 类型。默认值 b 的类型必须要和 a 存储值的类型保持一致


28、lazy 的作用

懒加载, 当属性要使用的时候, 才去完成初始化

lazy var names: NSArray = {
    let names = NSArray()
    print("只在首次访问输出")
    return names
}()

分析:相比起Objective-C中的实现,现在的lazy是在是简单的多了,而且更加的直观。除了上述直接在属性后面定义闭包调用的方法以外,还可以使用实例方法(func)和类方法(class func)来为lazy属性初始化添加必要的逻辑。

为了简化,我们如果不需要做什么额外工作的话,也可以对这个 lazy 的属性直接写赋值语句:

lazy var str: String = "Hello"

使用场景
延迟加载主要有以下两个使用的场景:

a、属性的初始值依赖于其他的属性值,只有其他的属性值有值之后才能得出该属性的值。
b、属性的初始值需要大量的计算。


29、一个类型表示选项,可以同时表示有几个选项选中(类似 UIViewAnimationOptions ),用什么类型表示

需要实现自 OptionSet, 一般使用 struct 实现. 由于 OptionSet 要求有一个不可失败的init(rawValue:) 构造器, 而 枚举无法做到这一点(枚举的原始值构造器是可失败的, 而且有些组合值, 是没办法用一个枚举值表示的)

struct SomeOption: OptionSet {
    let rawValue: Int
    static let option1 = SomeOption(rawValue: 1 << 0)
    static let option2 =  SomeOption(rawValue:1 << 1)
    static let option3 =  SomeOption(rawValue:1 << 2)
}
let options: SomeOption = [.option1, .option2]

30、inout 的作用

可以让值类型以引用方式传递,比如有时需要通过一个函数改变函数外面变量的值,例如:

var value = 50
print(value)  // 此时value值为50

func increment(inout value: Int, length: Int = 10) {
    value += length
}
increment(&value)
print(value)  // 此时value值为60,成功改变了函数外部变量value的值

31、Error 如果要兼容 NSError 需要做什么操作

其实直接转换就可以, 例如SomeError.someError as NSError但是这样没有错误码, 描述等等, 如果想和 NSError 一样有这些东西, 只需要实现 LocalizedError和CustomNSError协议, 有些方法有默认实现, 可以略过, 如:

enum SomeError: Error, LocalizedError, CustomNSError {
    case error1, error2
    public var errorDescription: String? {
        switch self {
        case .error1:
            return "error description error1"
        case .error2:
            return "error description error2"
        }
    }
    var errorCode: Int {
        switch self {
        case .error1:
            return 1
        case .error2:
            return 2
        }
    }
    public static var errorDomain: String {
        return "error domain SomeError"
    }
    public var errorUserInfo: [String : Any] {
        switch self {
        case .error1:
            return ["info": "error1"]
        case .error2:
            return ["info": "error2"]
        }
    }
}
print(SomeError.error1 as NSError)
// Error Domain=error domain SomeError Code=1 "error description error1" UserInfo={info=error1}

32、下面的代码都用了哪些语法糖

[1, 2, 3].map{ $0 * 2 }

[1, 2, 3] 使用了, Array 实现的ExpressibleByArrayLiteral 协议, 用于接收数组的字面值
map{xxx} 使用了闭包作为作为最后一个参数时, 可以直接写在调用后面, 而且, 如果是唯一参数的话, 圆括号也可以省略
闭包没有声明函数参数, 返回值类型, 数量, 依靠的是闭包类型的自动推断
闭包中语句只有一句时, 自动将这一句的结果作为返回值


33、什么是高阶函数

一个函数如果可以以某一个函数作为参数, 或者是返回值, 那么这个函数就称之为高阶函数, 如 map, reduce, filter


34、如何解决引用循环
a、转换为值类型, 只有类会存在引用循环, 所以如果能不用类, 是可以解引用循环的
b、delegate 使用 weak 属性
c、闭包中, 对有可能发生循环引用的对象, 使用 weak 或者 unowned, 修饰


35、下面的代码会不会崩溃,说出原因

var mutableArray = [1,2,3]
for _ in mutableArray {
    mutableArray.removeLast()
}

var mutableArray = [1,2,3]
for _ in mutableArray {
mutableArray.removeLast()
}


36、给集合中元素是字符串的类型增加一个扩展方法,应该怎么声明

使用 where 子句, 限制 Element 为 String

extension Array where Element == String {
    var isStringElement:Bool {
        return true
    }
}
["1", "2"].isStringElement
//[1, 2].isStringElement// error

二、高级












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

推荐阅读更多精彩内容

  • 链接:https://www.jianshu.com/p/a70841d17fb2基础1、class 和 stru...
    osnail阅读 792评论 0 1
  • 1.Array和ContiguousArray的区别?答:Array是值类型,其中装载的元素也是值类型,再进行赋值...
    轰炸机上调鸡尾酒阅读 588评论 0 0
  • swift: 1.struct和class区别 1.property初始化的不同 2.赋值不同struct 赋值“...
    JasonL阅读 130评论 0 2
  • 目录 1.class 和 struct 的区别 相同点: 我们可以使用完全使用相同的语法规则来为 class 和 ...
    勇敢的_心_阅读 953评论 0 1
  • 1. 结构体struct 和 类class (1) 结构体和类区别? (2) 为什么使用结构体? 在多线程环境中,...
    流萤的分享阅读 702评论 0 1