Skip to content

Latest commit

 

History

History
2625 lines (1865 loc) · 78.5 KB

知识点.md

File metadata and controls

2625 lines (1865 loc) · 78.5 KB

swift知识点

1、柯里化(Currying)

Swift 里可以将方法进行柯里化 (Currying),也就是把接受多个参数的方法变换成接受第一个参数的方法,并且返回接受余下的参数并且返回结果的新方法

func add(_ v1:Int,_ v2:Int) -> Int {
    return v1 + v2
}
print(add(1, 2))


//柯里化(Currying)
func add(_ v:Int) -> (Int) -> Int {
    return {$0 + v}
}
print(add(1)(2))

2、mutating

Swift 的 protocol 不仅可以被 class 类型实现,也适用于 struct 和 enum

Swift 的 mutating 关键字修饰方法是为了能在该方法中修改 struct 或是 enum 的变量,所以如果你没在协议方法里写 ``mutating 的话,别人如果用 struct 或者 enum 来实现这个协议的话,就不能在方法里改变自己的变量了

在使用 class 来实现带有 mutating的方法的协议时,具体实现的前面是不需要加 mutating修饰的,因为 class 可以随意更改自己的成员变量。所以说在协议里用 mutating修饰方法,对于 class 的实现是完全透明,可以当作不存在的

protocol Vehicle {
   var numberOfWheels:Int{get}
   mutating func changeNumberOfWheels()
}

struct MyCar:Vehicle {
   var numberOfWheels: Int = 4
   
   mutating func changeNumberOfWheels() {
       numberOfWheels = 4
   }
}

class Cars: Vehicle {
   var numberOfWheels: Int = 0
   func changeNumberOfWheels() {
       numberOfWheels = 2
   }
}

3、Sequence

Sequence 是一系列相同类型的值的集合,并且提供对这些值的迭代能力。

迭代一个Sequence最常见的方式就是 for-in 循环

let animals = ["Antelope", "Butterfly", "Camel", "Dolphin"]
for animal in animals {
   print(animal)
}

Sequence 协议的定义

protocol Sequence {
   associatedtype Iterator: IteratorProtocol
   func makeIterator() -> Iterator
}

Sequence 协议只有一个必须实现的方法 makeIterator()

makeIterator() 需要返回一个 Iterator,它是一个 IteratorProtocol 类型。

也就是说只要提供一个Iterator 就能实现一个 Sequence,那么 Iterator 又是什么呢?

Iterator Iterator 在 Swift 3.1 标准库中即为 IteratorProtocol,它用来为 Sequence 提供迭代能力。对于 Sequence,我们可以用 for-in 来迭代其中的元素,其实 for-in 的背后是 IteratorProtocol 在起作用

IteratorProtocol 的定义如下:

public protocol IteratorProtocol {
   associatedtype Element
   public mutating func next() -> Self.Element?
}

对于这个for...in循环

 let animals = ["Antelope", "Butterfly", "Camel", "Dolphin"]
for animal in animals {
   print(animal)
}

实际上编译器会把以上代码转换成下面的代码

var animalIterator = animals.makeIterator()
while let animal = animalIterator.next() {
    print(animal)
}
  • 1、获取到 animals 数组的 Iterator
  • 2、在一个 while 循环中,通过 Iterator 不断获取下一个元素,并对元素进行操作
  • 3、当 next() 返回 nil 时,退出循环

实现一个逆序

//我们先实现一个IteratorProtocol协议类型
class ReverseIterator<T>: IteratorProtocol {
   typealias Element = T
   var array: [Element]
   var currentIndex = 0

   init(array: [Element]) {
       self.array = array
       currentIndex = array.count - 1
   }
   func next() -> Element? {
       if currentIndex < 0{
           return nil
       }
       else {
           let element = array[currentIndex]
           currentIndex -= 1
           return element
       }
   }
}

// 然后我们来定义 Sequence
struct ReverseSequence<T>:Sequence {
   var array:[T]
   init (array: [T]) {
       self.array = array
   }
   typealias Iterator = ReverseIterator<T>
   func makeIterator() -> ReverseIterator<T> {
       return ReverseIterator(array: self.array)
   }
}

for item in ReverseSequence(array: animals){
   print(item)
}

参考:Swift 中的 Sequence(一)

4、元组(Tuple)

元组是swift编程语言中唯一的一种复合类型,他可以将指定有限个数的任何类型一次整理为一个对象,元组中的每一种类型都可以是任何的结构体、枚举或类类型,甚至也可以是一个元组以及空元组。

比如交换输入,普通程序员亘古以来可能都是这么写的

func swapMel1<T>(a:inout T, b:inout T) {
    let temp = a
    a = b
    b = temp
}

但是要是使用多元组的话,我们可以不使用额外空间就完成交换,一下子就达到了文艺程序员的写法

func swapMel2<T>(a:inout T, b:inout T) {
   (a,b) = (b,a)
}

5、自动闭包(@autoclosure)

自动闭包是一种自动创建的用来把作为实际参数传递给函数的表达式打包的闭包。它不接受任何实际参数,并且当它被调用时,它会返回内部打包的表达式的值

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

func getFirstPositive1(_ v1:Int, _ v2:Int) -> Int {
    return v1 > 0 ? v1 : v2
}
getFirstPositive1(1, 2)


func getFirstPositive2(_ v1:Int, _ v2:() -> Int) -> Int {
    return v1 > 0 ? v1 : v2()
}
getFirstPositive2(1, 2) //这个报错
getFirstPositive2(1, {2})

func getFirstPositive3(_ v1:Int, _ v2:@autoclosure () -> Int) -> Int {
    return v1 > 0 ? v1 : v2()
}
getFirstPositive3(1, 2)
  • @autoclosure会自动的将2封装为{2}

  • @autoclosure只支持() -> T的格式参数

??

在 Swift 中,有一个非常有用的操作符,可以用来快速地对 nil 进行条件判断,那就是 ??。这个操作符可以判断输入并在当左侧的值是非 nil 的 Optional 值时返回其 value,当左侧是 nil 时返回右侧的值

??就是一个@autoclosure

public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T) rethrows -> T

我们来猜测一下??的实现

func ??<T>(optional: T?, defaultValue: @autoclosure () -> T) -> T {
   switch optional {
       case .Some(let value):
           return value
       case .None:
           return defaultValue()
       }
}

可能你会有疑问,为什么这里要使用autoclosure,直接接受T 作为参数并返回不行么,为何要用 () -> T 这样的形式包装一遍,岂不是画蛇添足?其实这正是 autoclosure 的一个最值得称赞的地方。如果我们直接使用 T,那么就意味着在 ?? 操作符真正取值之前,我们就必须准备好一个默认值传入到这个方法中,一般来说这不会有很大问题,但是如果这个默认值是通过一系列复杂计算得到的话,可能会成为浪费 -- 因为其实如果optional 不是 nil 的话,我们实际上是完全没有用到这个默认值,而会直接返回 optional 解包后的值的。这样的开销是完全可以避免的,方法就是将默认值的计算推迟到 optional 判定为 nil 之后

在 Swift 中,其实 &&|| 这两个操作符里也用到了 @autoclosure

public static func && (lhs: Bool, rhs: @autoclosure () throws -> Bool) rethrows -> Bool

public static func || (lhs: Bool, rhs: @autoclosure () throws -> Bool) rethrows -> Bool

6、逃逸闭包(@escaping )与非逃逸闭包(@noescaping)

逃逸闭包(@escaping )

当闭包作为一个实际参数传递给一个函数的时候,我们就说这个闭包逃逸了,因为它是在函数返回之后调用的。当你声明一个接受闭包作为形式参数的函数时,你可以在形式参数前写 @escaping 来明确闭包是允许逃逸的。

闭包可以逃逸的一种方法是被储存在定义于函数外的变量里。比如说,很多函数接收闭包实际参数来作为启动异步任务的回调。函数在启动任务后返回,但是闭包要直到任务完成——闭包需要逃逸,以便于稍后调用

例如:当网络请求结束后调用的闭包。发起请求后过了一段时间后这个闭包才执行,并不一定是在函数作用域内执行的

override func viewDidLoad() {
        super.viewDidLoad()
         
        getData { (data) in
            print("闭包返回结果:\(data)")
        }
    }

    func getData(closure:@escaping (Any) -> Void) {
        print("函数开始执行--\(Thread.current)")
        DispatchQueue.global().async {
            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()+2, execute: {
                print("执行了闭包---\(Thread.current)")
                closure("345")
            })
        }
        print("函数执行结束---\(Thread.current)")
    }

从结果可以看出,逃逸闭包的生命周期是长于函数的。

逃逸闭包的生命周期:

  • 1、闭包作为参数传递给函数;
  • 2、退出函数;
  • 3、闭包被调用,闭包生命周期结束

即逃逸闭包的生命周期长于函数,函数退出的时候,逃逸闭包的引用仍被其他对象持有,不会在函数结束时释放。

非逃逸闭包(@noescaping)

一个接受闭包作为参数的函数, 闭包是在这个函数结束前内被调用。

    override func viewDidLoad() {
        super.viewDidLoad()
         
        handleData { (data) in
            print("闭包返回结果:\(data)")
        }
    }

    func handleData(closure:(Any) -> Void) {
        print("函数开始执行--\(Thread.current)")
        print("执行了闭包---\(Thread.current)")
        closure("123")
        print("函数执行结束---\(Thread.current)")
    }

为什么要分逃逸闭包和非逃逸闭包

为了管理内存,闭包会强引用它捕获的所有对象,比如你在闭包中访问了当前控制器的属性、函数,编译器会要求你在闭包中显示 self 的引用,这样闭包会持有当前对象,容易导致循环引用。

非逃逸闭包不会产生循环引用,它会在函数作用域内释放,编译器可以保证在函数结束时闭包会释放它捕获的所有对象;使用非逃逸闭包的另一个好处是编译器可以应用更多强有力的性能优化,例如,当明确了一个闭包的生命周期的话,就可以省去一些保留(retain)和释放(release)的调用;此外非逃逸闭包它的上下文的内存可以保存在栈上而不是堆上。

7、操作符

与 Objective-C 不同,Swift 支持重载操作符这样的特性,最常见的使用方式可能就是定义一些简便的计算了

系统操作符

比如我们需要一个表示二维向量的数据结构

struct Vector2D {
   var x:CGFloat = 0
   var y:CGFloat = 0
}

一个很简单的需求是两个 Vector2D 相加

let v1 = Vector2D(x: 2.0, y: 3.0)
let v2 = Vector2D(x: 1.0, y: 4.0)
let v3 = Vector2D(x: v1.x + v2.x, y: v1.y + v2.y)  

如果只做一次的话似乎还好,但是一般情况我们会进行很多这种操作。这样的话,我们可能更愿意定义一个 Vector2D 相加的操作,来让代码简化清晰

func +(left:Vector2D,right:Vector2D) -> Vector2D {
   Vector2D(x: left.x + right.x, y: left.y + right.y)
}
let v3 = v1 + v2

8、自定义操作符

在Swift语言中,常见的操作符有+、-、*、/、>、<、==、&&、||等等,如果不喜欢,你也可以定义自己喜欢的操作符。

  • precedencegroup:定义操作符的优先级
  • associativity:操作符的结合律
  • higherThanlowerThan:运算符的优先级
  • prefix、infix、postfix:前缀、中缀、后缀运算符

中缀

/// 定义优先级组
precedencegroup MyPrecedence {
    // higherThan: AdditionPrecedence   // 优先级,比加法运算高
    lowerThan: AdditionPrecedence       // 优先级, 比加法运算低
    associativity: none                 // 结合方向:left, right or none
    assignment: false                   // true=赋值运算符,false=非赋值运算符
}

infix operator +++: MyPrecedence        // 继承 MyPrecedence 优先级组
// infix operator +++: AdditionPrecedence // 也可以直接继承加法优先级组(AdditionPrecedence)或其他优先级组
func +++(left: Int, right: Int) -> Int {
    return left+right*2
}
 
print(2+++3) // 8

前缀

prefix operator ==+
prefix func ==+(left: Int) -> Int {
   
   return left*2
}
print(==+2) // 4

后缀

postfix operator +==
postfix func +==(right: Int) -> Int {
   
   return right*3
}
print(2+==) // 6

9、inout:输入输出参数

可变形式参数只能在函数的内部做改变。如果你想函数能够修改一个形式参数的值,而且你想这些改变在函数结束之后依然生效,那么就需要将形式参数定义为输入输出形式参数。

在形式参数定义开始的时候在前边添加一个 inout关键字可以定义一个输入输出形式参数。输入输出形式参数有一个能输入给函数的值,函数能对其进行修改,还能输出到函数外边替换原来的值。

你只能把变量作为输入输出形式参数的实际参数。你不能用常量或者字面量作为实际参数,因为常量和字面量不能修改。在将变量作为实际参数传递给输入输出形式参数的时候,直接在它前边添加一个和符号 ( &) 来明确可以被函数修改。

var b = 10
func test(a:inout Int) {
   a = 20
}
test(a: &b)
print(b) //20

可以用inout定义一个输入输出参数:可以在函数内部修改外部实参的值

  • 1、不可变参数不能标记为inout
  • 2、inout参数不能有默认值
  • 3、inout参数只能传入可以被多次赋值的
  • 4、inout参数的本质就是地址传递

10、下标

下标相信大家都很熟悉了,在绝大多数语言中使用下标来读写类似数组或者是字典这样的数据结构的做法,似乎已经是业界标准。在 Swift 中,Array 和 Dictionary 当然也实现了下标读写

var arr = [1,2,3]
arr[2]            // 3
arr[2] = 4        // arr = [1,2,4]

var dic = ["cat":"meow", "goat":"mie"]
dic["cat"]          // {Some "meow"}
dic["cat"] = "miao" // dic = ["cat":"miao", "goat":"mie"]  

作为一门代表了先进生产力的语言,Swift 是允许我们自定义下标的,我们找到Array已经支持的下标类型

subscript (index: Int) -> T
subscript (subRange: Range<Int>) -> Slice<T>

我们发现如果我们想要取出0、2、4下标值,我们需要循环枚举。

其实这里有一个更好的做法,比如可以实现一个接受数组作为下标输入的读取方法

extension Array {
   subscript(input: [Int]) -> ArraySlice<Element> {
       get {
           var result = ArraySlice<Element>()
           for i in input {
               assert(i < self.count, "Index out of range")
               result.append(self[i])
           }
           return result
       }
       set {
           for (index,i) in input.enumerated() {
               assert(i < self.count, "Index out of range")
               self[i] = newValue[index]
           }
       }
   }
}

var arr = ["a","b","c","d","z"]
print(arr[[0,3]]) //["a", "d"]

11、嵌套函数

我们可以把函数当成参数或者变量来使用,函数内部嵌套函数

func forward(_ forward:Bool) -> (Int) -> Int {
    
    func next(_ input:Int) -> Int {
        input + 1
    }

    func previous(_ input:Int) -> Int {
        input - 1
    }
    
    return forward ? next : previous
}

12、命名空间

Objective-C 一个一直以来令人诟病的地方就是没有命名空间,在应用开发时,所有的代码和引用的静态库最终都会被编译到同一个域和二进制中。这样的后果是一旦我们有重复的类名的话,就会导致编译时的冲突和失败。为了避免这种事情的发生,Objective-C 的类型一般都会加上两到三个字母的前缀,比如 Apple 保留的 NS 和 UI 前缀,各个系统框架的前缀 SK (StoreKit),CG (CoreGraphic) 等。Objective-C 社区的大部分开发者也遵守了这个约定,一般都会将自己名字缩写作为前缀,把类库命名为 AFNetworking 或者 MBProgressHUD 这样。这种做法可以解决部分问题,至少我们在直接引用不同人的库时冲突的概率大大降低了,但是前缀并不意味着不会冲突,有时候我们确实还是会遇到即使使用前缀也仍然相同的情况。另外一种情况是可能你想使用的两个不同的库,分别在它们里面引用了另一个相同的很流行的第三方库,而又没有更改名字。在你分别使用“这两个库中的一个时是没有问题的,但是一旦你将这两个库同时加到你的项目中的话,这个大家共用的第三方库就会和自己发生冲突了。

在 Swift 中,由于可以使用命名空间了,即使是名字相同的类型,只要是来自不同的命名空间的话,都是可以和平共处的。和 C# 这样的显式在文件中指定命名空间的做法不同,Swift 的命名空间是基于 module 而不是在代码中显式地指明,每个 module 代表了 Swift 中的一个命名空间。也就是说,同一个 target 里的类型名称还是不能相同的。在我们进行 app 开发时,默认添加到 app 的主 target 的内容都是处于同一个命名空间中的,我们可以通过创建 Cocoa (Touch) Framework 的 target 的方法来新建一个 module,这样我们就可以在两个不同的 target 中添加同样名字的类型了

13、typealias别名

我们可以给一个复杂的难以理解的类型起一个别名,方便我们使用和理解

按照swift标准库的定义Void就是一个空元组

public typealias Void = ()

我们知道swift中没有byte、short、Long类型,如果我们想要这样的类型,就可以用typealias实现

typealias Byte = Int8
typealias Short = Int16
typealias Long = Int64

我们还可以给函数起一个别名

typealias IntFn = (Int,Int) -> Int
func difference(v1:Int,v2:Int) -> Int {
   v1 - v2
}
let fn:IntFn = difference
print(fn(2,1))  //1

我们还可以给元组起别名

typealias Date = (year:Int,month:Int,day:Int)
func test(_ date:Date) {
   print(date.year)
}
test((2019,10,30))

14、associatedtype

我们在 Swift 协议中可以定义属性和方法,并要求满足这个协议的类型实现它们:

protocol Food { }

protocol Animal {
   func eat(_ food: Food)
}

struct Meat: Food { }
struct Grass: Food { }
struct Tiger: Animal {
   func eat(_ food: Food) {

   }
}

因为老虎并不吃素,所以在 Tiger 的 eat 中,我们很可能需要进行一些转换工作才能使用 meat

associatedtype 声明中可以使用冒号来指定类型满足某个协议

protocol Animal {
   associatedtype F: Food
   func eat(_ food: F)
}

struct Tiger: Animal {
   func eat(_ food: Meat) {
       print("eat \(meat)")
   }
}

struct Sheep: Animal {
   func eat(_ food: Grass) {
       print("eat \(food)")
   }
} 

不过在添加associatedtype 后,Animal 协议就不能被当作独立的类型使用了。试想我们有一个函数来判断某个动物是否危险:

func isDangerous(animal: Animal) -> Bool {
   if animal is Tiger {
       return true
   } else {
       return false
   }
}

会报错

Protocol 'Animal' can only be used as a generic constraint because it has Self or associated type requirements

这是因为 Swift 需要在编译时确定所有类型,这里因为 Animal 包含了一个不确定的类型,所以随着 Animal 本身类型的变化,其中的 F 将无法确定 (试想一下如果在这个函数内部调用 eat 的情形,你将无法指定 eat 参数的类型)。在一个协议加入了像是 associatedtype 或者 Self 的约束后,它将只能被用为泛型约束,而不能作为独立类型的占位使用,也失去了动态派发的特性。也就是说,这种情况下,我们需要将函数改写为泛型

func isDangerous<T: Animal>(animal: T) -> Bool {
   if animal is Tiger {
       return true
   } else {
       return false
   }
}

isDangerous(animal: Tiger()) // true

15、可变参数

一个可变形式参数可以接受零或者多个特定类型的值,可变参数必须是同一类型的。当调用函数的时候你可以利用可变形式参数来声明形式参数可以被传入值的数量是可变的。可以通过在形式参数的类型名称后边插入三个点符号(...)来书写可变形式参数。

func sum(_ numbers:Int...) -> Int{
   var total = 0
   for item in numbers {
       total += item
   }
   return total
}

Swift自带的print函数

/// - Parameters:
///   - items: Zero or more items to print.
///   - separator: A string to print between each item. The default is a single
///     space (`" "`).
///   - terminator: The string to print after all items have been printed. The
///     default is a newline (`"\n"`).
public func print(_ items: Any..., separator: String = " ", terminator: String = "\n")
  • 1、第一个参数:就是需要打印的值,是一个可变参数
  • 2、第二个参数:两个打印值连接的地方,默认是空格
  • 3、第三个参数:结尾默认是\n换行

16、初始化

初始化是为类、结构体或者枚举准备实例的过程。这个过需要给实例里的每一个存储属性设置一个初始值并且在新实例可以使用之前执行任何其他所必须的配置或初始化

你通过定义初始化器来实现这个初始化过程,它更像是一个用来创建特定类型新实例的特殊的方法。不同于 Objective-C 的初始化器,Swift 初始化器不返回值。这些初始化器主要的角色就是确保在第一次使用之前某类型的新实例能够正确初始化。

类有两种初始化器

  • 1、指定初始化器(designated initializer)
  • 2、便捷初始化器(convenience initializer)
class Person {
   var age: Int
   var name: String
   
   //指定初始化器
   init(age:Int, name:String) {
       self.age = age
       self.name = name
   }
   //便捷初始化器
   convenience init(age:Int){
       self.init(age:age,name:"")
   }
}
  • 1、每一个类至少有一个指定初始化器,指定初始化器是类的最主要初始化器
  • 2、默认初始化器总是类的指定初始化器
  • 3、类偏向于少量指定初始化器,一个类通常就只有一个指定初始化器

初始化器的相互调用规则

  • 1、指定初始化器必须从他的直系父类调用指定初始化器
  • 2、便捷初始化器必须从相同的类里调用另一个初始化器
  • 3、便捷初始化器最终必须调用一个指定初始化器

17、Static & Class

Swift 中表示 “类型范围作用域” 这一概念有两个不同的关键字,它们分别是 staticclass。这两个关键字确实都表达了这个意思

非class的类型上下文中,我们统一使用 static 来描述类型作用域

18、default 参数

Swift 的方法是支持默认参数的,也就是说在声明方法时,可以给某个参数指定一个默认使用的值。在调用该方法时要是传入了这个参数,则使用传入的值,如果缺少这个输入参数,那么直接使用设定的默认值进行调用

func test(a:String = "1",b:String,c:String = "3"){}

19、匹配模式

什么是模式 模式是用于匹配的规则,比如switchcase、捕获错误的catchif\guard\while\for语句

Swift中模式有

  • 1、通配符模式(Wildcard Pattern)
  • 2、标识符模式(Identifier Pattern)
  • 3、值绑定模式(Value-Binding Pattern)
  • 4、元祖模式(Tuple Pattern)
  • 5、枚举Case模式(Enumeration Case Pattern)
  • 6、可选模式(Optional Pattern)
  • 7、类型转换模式(Type-Casting Pattern)
  • 8、表达式模式(Expression Pattern)

通配符模式(Wildcard Pattern)

  • _ 匹配任何值
  • _?匹配非nil值
enum Life {
    case human(name:String,age:Int?)
    case animal(name:String,age:Int?)
}

func check(_ life:Life){
    switch life {
    case .human(let name,_):
        print("human:",name)
    case .animal(let name,_?):
        print("animal",name)
    default:
        print("other")
        break
    }
}

check(.human(name: "小明", age: 20)) //human: 小明
check(.human(name: "小红", age: nil))//human: 小红
check(.animal(name: "dog", age: 5))//animal dog
check(.animal(name: "cat", age: nil))//other

标识符模式(Identifier Pattern)

就是给对应的变量常亮赋值

let a = 10
let b = "text"

值绑定模式(Value-Binding Pattern)

let point = (2,3)
switch point {
case (let x,let y):
    print("x:\(x)  y:\(y)")
}
//x:2  y:3

元祖模式(Tuple Pattern)

匹配任何元祖

let point = [(0,0),(1,0),(2,0)]
for (x,_) in point{
    print(x)
}
//0
//1
//2

枚举Case模式(Enumeration Case Pattern)

if case语句等价于一个caseswitch语句,简化了一些判断语句

let age = 2
//if
if age >= 0 && age <= 9 {
    print("[0,9]")
}
//枚举模式
if case 0...9 = age{
    print("[0,9]")
}
let ages:[Int?] = [2,3,nil,5]
for case nil in ages{
    print("有nil值")
}

可选模式(Optional Pattern)

let ages:[Int?] = [nil,2,3,nil]
for case let age? in ages{
    print(age)
}
// 2
//3

等价于

let ages:[Int?] = [nil,2,3,nil]
for item in ages{
    if let age = item {
        print(age)
    }
}

类型转换模式(Type-Casting Pattern)

class Animal {
    func eat() {
        print(type(of: self),"eat")
    }
}

class Dog: Animal {
    func run() {
        print(type(of: self),"run")
    }
}

class Cat: Animal {
    func jump() {
        print(type(of: self),"jump")
    }
}

func check(_ animal:Animal) {
    switch animal {
    case let dog as Dog:
        dog.run()
        dog.eat()
    case is Cat:
        animal.eat()
    default:
        break
    }
}
check(Dog())
//Dog run
//Dog eat
check(Cat())
//Cat eat

表达式模式(Expression Pattern)

可以通过重载运算符,自定义表达式模式的匹配规则

struct Student {
    var score = 0, name = ""
    static func ~=(pattern:Int,value:Student) -> Bool{
        value.score >= pattern
    }
    
    static func ~=(pattern:ClosedRange<Int>,value:Student) -> Bool{
        pattern.contains(value.score)
    }
    static func ~=(pattern:Range<Int>,value:Student) -> Bool{
        pattern.contains(value.score)
    }
    
}
var stu = Student(score: 81, name: "tom")
switch stu{
case 100:print(">=100")
case 90:print(">=90")
case 80..<90:print("[80,90]")
case 60...79:print("[60,79]")
default:break
}
extension String{
    static func ~=(pattern:(String) -> Bool,value:String) ->Bool{
        pattern(value)
    }
}

func hasPrefix(_ prefix:String) ->((String) -> Bool){{$0.hasPrefix(prefix)}}
func hasSuffix(_ prefix:String) ->((String) -> Bool){{$0.hasSuffix(prefix)}}



var str = "jack"
switch str {
case hasPrefix("j"),hasSuffix("k"):
    print("以j开头,或者以k结尾")
default:
    break
}

20、... 和 ..<

Range 操作符,用来简单地指定一个从 X 开始连续计数到 Y 的范围

我们可以仔细看看 Swift 中对着两个操作符的定义

/// Forms a closed range that contains both `minimum` and `maximum`.
func ...<Pos : ForwardIndexType>(minimum: Pos, maximum: Pos)
        -> Range<Pos>

/// Forms a closed range that contains both `start` and `end`.
/// Requres: `start <= end`
func ...<Pos : ForwardIndexType where Pos : Comparable>(start: Pos, end: Pos)
        -> Range<Pos>


/// Forms a half-open range that contains `minimum`, but not
/// `maximum`.
func ..<<Pos : ForwardIndexType>(minimum: Pos, maximum: Pos)
        -> Range<Pos>

/// Forms a half-open range that contains `start`, but not
/// `end`.  Requires: `start <= end`
func ..<<Pos : ForwardIndexType where Pos : Comparable>(start: Pos, end: Pos)
        -> Range<Pos>


/// Returns a closed interval from `start` through `end`
func ...<T : Comparable>(start: T, end: T) -> ClosedInterval<T>
  
  
 /// Returns a half-open interval from `start` to `end`
func ..<<T : Comparable>(start: T, end: T) -> HalfOpenInterval<T>  

不难发现,其实这几个方法都是支持泛型的。除了我们常用的输入 Int 或者 Double,返回一个 Range 以外,这个操作符还有一个接受 Comparable 的输入,并返回 ClosedInterval 或 HalfOpenInterval 的重载

比如想确认一个单词里的全部字符都是小写英文字母的话,可以这么做

let test = "helLo"
let interval = "a"..."z"
for c in test {
    if !interval.contains(String(c)) {
        print("\(c) 不是小写字母")
    }
}

// 输出
// L 不是小写字母

21、AnyClass,元类型和 .self

在 Swift 中能够表示 “任意” 这个概念的除了 Any 和 AnyObject 以外,还有一个 AnyClassAnyClass 在 Swift 中被一个 typealias 所定义

public typealias AnyClass = AnyObject.Type

通过 AnyObject.Type 这种方式所得到是一个元类型 (Meta)。在声明时我们总是在类型的名称后面加上 .Type,比如 A.Type 代表的是 A 这个类型的类型。也就是说,我们可以声明一个元类型来存储 A 这个类型本身,而在从A中取出其类型时,我们需要使用到 .self

其实在 Swift 中,.self 可以用在类型后面取得类型本身,也可以用在某个实例后面取得这个实例本身。前一种方法可以用来获得一个表示该类型的值,这在某些时候会很有用;而后者因为拿到的实例本身

class A {
    class func method() {
        print("Hello")
    }
}

let typeA: A.Type = A.self
typeA.method()

// 或者
let anyClass: AnyClass = A.self
(anyClass as! A.Type).method() 

也许你会问,这样做有什么意义呢,我们难道不是可以直接使用 A.method() 来调用么?没错,对于单个独立的类型来说我们完全没有必要关心它的元类型,但是元类型或者元编程的概念可以变得非常灵活和强大,这在我们编写某些框架性的代码时会非常方便。比如我们想要传递一些类型的时候,就不需要不断地去改动代码了。在下面的这个例子中虽然我们是用代码声明的方式获取了 MusicViewController 和 AlbumViewController 的元类型,但是其实这一步骤完全可以通过读入配置文件之类的方式来完成的。而在将这些元类型存入数组并且传递给别的方法来进行配置这一点上,元类型编程就很难被替代了

class MusicViewController: UIViewController {

}

class AlbumViewController: UIViewController {

}

let usingVCTypes: [AnyClass] = [MusicViewController.self,
    AlbumViewController.self]

func setupViewControllers(_ vcTypes: [AnyClass]) {
    for vcType in vcTypes { 
            if vcType is UIViewController.Type {
            let vc = (vcType as! UIViewController.Type).init()
            print(vc)
        }

    }
}

setupViewControllers(usingVCTypes) 

这么一来,我们完全可以搭好框架,然后用 DSL 的方式进行配置,就可以在不触及 Swift 编码的情况下,很简单地完成一系列复杂操作了

22、动态类型

Swift 中我们虽然可以通过 dynamicType 来获取一个对象的动态类型 (也就是运行时的实际类型,而非代码指定或编译器看到的类型)。但是在使用中,Swift 现在却是不支持多方法的,也就是说,不能根据对象在动态时的类型进行合适的重载方法调用

class Pet {}
class Dog:Pet {}
class Cat:Pet {}
 
func eat(_ pet:Pet) {
    print("pet eat")
}

func eat(_ dog:Dog) {
    print("dog eat")
}

func eat(_ cat:Cat) {
    print("cat eat")
}
 
func eats(_ pet:Pet,_ cat:Cat) {
    eat(pet)
    eat(cat)
}
eats(Dog(), Cat())
//pet eat
//cat eat

我们在打印Dog类型信息的时候,并没有被用来在运行时选择合适的func eat(_ dog:Dog) {}方法,而是被忽略了,并采用了编译期间决定的Pet方法。

因为 Swift 默认情况下是不采用动态派发的,因此方法的调用只能在编译时决定

要想绕过这个限制,我们可能需要进行通过对输入类型做判断和转换

func eats(_ pet:Pet,_ cat:Cat) {
    if let aCat = pet as? Cat {
        eat(aCat)
    }else if let aDog = pet as? Cat{
        eat(aDog)
    }
    eat(cat)
}

23、属性

在Swift中所声明的属性包括

  • 1、存储属性:存储属性将会在内存中实际分配地址进行属性的存储
  • 2、计算属性:计算属性则不包括存储,只是提供setget方法

存储属性

我们可以在存储属性中提供了willSetdidSet两种属性观察方法

class Person {
    var age:Int = 0{
        willSet{
            print("即将将年龄从\(age)设置为\(newValue)")
        }
        didSet{
            print("已经将年龄从\(oldValue)设置为\(age)")
        }
    }
}

let p = Person()
p.age = 10
//即将将年龄从0设置为10
//已经将年龄从0设置为10

willSetdidSet中我们分别可以使用newValueoldValue 来获取将要设定的和已经设定的值。

初始化方法对属性的设定,以及在 willSet 和 didSet 中对属性的再次设定都不会再次触发属性观察的调用

计算属性

在 Swift 中所声明的属性包括存储属性计算属性两种。其中存储属性将会在内存中实际分配地址对属性进行存储,而计算属性则不包括背后的存储,只是提供setget 两种方法。在同一个类型中,属性观察和计算属性是不能同时共存的。也就是说,想在一个属性定义中同时出现 setwillSet didSet 是一件办不到的事情。

计算属性中我们可以通过改写 set 中的内容来达到和 willSetdidSet 同样的属性观察的目的。如果我们无法改动这个类,又想要通过属性观察做一些事情的话,可能就需要子类化这个类,并且重写它的属性

重写的属性并不知道父类属性的具体实现情况,而只从父类属性中继承名字和类型,因此在子类的重载属性中我们是可以对父类的属性任意地添加属性观察的,而不用在意父类中到底是存储属性还是计算属性

class A {
    var number:Int {
        get{
            print("get")
            return 1
        }
        set{
            print("set")
        }
    }
}

class B:A {
    override var number: Int{
        willSet{
            print("willSet")
        }
        didSet{
            print("didSet")
        }
    }
}

let b = B()
b.number = 10

get
willSet
set
didSet

set 和对应的属性观察的调用都在我们的预想之中。这里要注意的是 get 首先被调用了一次。这是因为我们实现了 didSetdidSet 中会用到 oldValue,而这个值需要在整个 set 动作之前进行获取并存储待用,否则将无法确保正确性。如果我们不实现 didSet 的话,这次 get操作也将不存在。

24、lazy修饰符

延时加载或者说延时初始化是很常用的优化方法,在构建和生成新的对象的时候,内存分配会在运行时耗费不少时间,如果有一些对象的属性和内容非常复杂的话,这个时间更是不可忽略。另外,有些情况下我们并不会立即用到一个对象的所有属性,而默认情况下初始化时,那些在特定环境下不被使用的存储属性,也一样要被初始化和赋值,也是一种浪费

swift提供了一个关键字lazy

class A {
    lazy var str:String = {
        let str = "Hello"
        print("首次访问的时候输出")
        return str
    }()
}

let a = A()
print(a.str)

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

lazy var str1 = "word"

25、Reflection 和 Mirror

Objective-C 中我们不太会经常提及到 “反射” 这样的词语,因为 Objective-C 的运行时比一般的反射还要灵活和强大。可能很多读者已经习以为常的像是通过字符串生成类或者 selector,并且进而生成对象或者调用方法等,其实都是反射的具体的表现。而在 Swift 中其实就算抛开 Objective-C 的运行时的部分,在纯 Swift 范畴内也存在有反射相关的一些内容,只不过相对来说功能要弱得多。

Swift提供了Mirror类型来做映射的事情

struct Person {
    let name:String
    let age:Int
}

let xiaoming = Person(name: "小明", age: 10)
let r = Mirror(reflecting: xiaoming)

print("xiaoming是\(r.displayStyle)")
print("属性个数:\(r.children.count)")

for child in r.children{
    print("属性名:\(child.label) 值:\(child.value)")
}


xiaoming是Optional(Swift.Mirror.DisplayStyle.struct)
属性个数:2
属性名:Optional("name") 值:小明
属性名:Optional("age") 值:10

通过 Mirror 初始化得到的结果中包含的元素的描述都被集合在 children 属性下

public typealias Child = (label: String?, value: Any)
public typealias Children = AnyCollection<Mirror.Type.Child> 

如果觉得一个个打印太过于麻烦,我们也可以简单地使用 dump 方法来通过获取一个对象的镜像并进行标准输出的方式将其输出出来

print(dump(xiaoming))

▿ 反射.Person
  - name: "小明"
  - age: 10
Person(name: "小明", age: 10)

常见的应用场景是类似对 Swift 类型的对象做像 Objective-C 中 KVC 那样的 valueForKey: 的取值。通过比较取到的属性的名字和我们想要取得的 key 值就行了,非常简单

func valueFrom(_ object: Any, key: String) -> Any? {
    let mirror = Mirror(reflecting: object)
    for child in mirror.children {
        let (targetKey, targetMirror) = (child.label, child.value)
        if key == targetKey {
            return targetMirror
        }
    }
    return nil
}


if let name = valueFrom(xiaoming, key: "name") as? String {
    print("通过 key 得到值: \(name)")
}

通过 key 得到值: 小明

在现在的版本中,Swift 的反射特性并不是非常强大,我们只能对属性进行读取,还不能对其设定。 另外需要特别注意的是,虽然理论上将反射特性应用在实际的 app 制作中是可行的,但是这一套机制设计的最初目的是用于 REPL 环境和 Playground 中进行输出的。所以我们最好遵守 Apple 的这一设定,只在 REPL 和 Playground 中用它来对一个对象进行深层次的探索,而避免将它用在 app 制作中 -- 因为你永远不知道什么时候它们就会失效或者被大幅改动

26、可选值(optional)

我们点击进入官方文档,可以看到optional是一个枚举

enum Optional<Wrapped> : ExpressibleByNilLiteral {
    case none
    case some(Wrapped)
    public init(_ some: Wrapped)
}

我们来看看下面这两个值是否相同anotherString literalString

var string:String? = "string"
var anotherString:String?? = string
var literalString:String?? = "string"

print(anotherString)
print(literalString)
print(anotherString == literalString)

//Optional(Optional("string"))
//Optional(Optional("string"))
//true
  • 1、var anotherString:String?? = string的写法其实就是Optional.some(string)
  • 2、var literalString:String?? = "string"的写法是Optional.some(Optional.some("string"))

两者的返回值是一样的,所以两者相等

下面两个对象是否相等呢?

var aNil:String? = nil
var anoterNil:String?? = aNil
var literalNil:String?? = nil
print(anoterNil)
print(literalNil)
print(anoterNil == literalNil)
//Optional(nil)
//nil
//false
  • 1、anoterNil是可选项里面包含一个可选项值为nil的可选项
  • 2、literalNil是可选择值为nil的可选项

我们可以使用fr v -R来打印具体信息

1

27、匹配模式

什么是模式 模式是用于匹配的规则,比如switchcase、捕获错误的catchif\guard\while\for语句

Swift中模式有

  • 1、通配符模式(Wildcard Pattern)
  • 2、标识符模式(Identifier Pattern)
  • 3、值绑定模式(Value-Binding Pattern)
  • 4、元祖模式(Tuple Pattern)
  • 5、枚举Case模式(Enumeration Case Pattern)
  • 6、可选模式(Optional Pattern)
  • 7、类型转换模式(Type-Casting Pattern)
  • 8、表达式模式(Expression Pattern)

通配符模式(Wildcard Pattern)

  • _ 匹配任何值
  • _?匹配非nil值
enum Life {
    case human(name:String,age:Int?)
    case animal(name:String,age:Int?)
}

func check(_ life:Life){
    switch life {
    case .human(let name,_):
        print("human:",name)
    case .animal(let name,_?):
        print("animal",name)
    default:
        print("other")
        break
    }
}

check(.human(name: "小明", age: 20)) //human: 小明
check(.human(name: "小红", age: nil))//human: 小红
check(.animal(name: "dog", age: 5))//animal dog
check(.animal(name: "cat", age: nil))//other

标识符模式(Identifier Pattern)

就是给对应的变量常亮赋值

let a = 10
let b = "text"

值绑定模式(Value-Binding Pattern)

let point = (2,3)
switch point {
case (let x,let y):
    print("x:\(x)  y:\(y)")
}
//x:2  y:3

元祖模式(Tuple Pattern)

匹配任何元祖

let point = [(0,0),(1,0),(2,0)]
for (x,_) in point{
    print(x)
}
//0
//1
//2

枚举Case模式(Enumeration Case Pattern)

if case语句等价于一个caseswitch语句,简化了一些判断语句

let age = 2
//if
if age >= 0 && age <= 9 {
    print("[0,9]")
}
//枚举模式
if case 0...9 = age{
    print("[0,9]")
}
let ages:[Int?] = [2,3,nil,5]
for case nil in ages{
    print("有nil值")
}

可选模式(Optional Pattern)

let ages:[Int?] = [nil,2,3,nil]
for case let age? in ages{
    print(age)
}
// 2
//3

等价于

let ages:[Int?] = [nil,2,3,nil]
for item in ages{
    if let age = item {
        print(age)
    }
}

类型转换模式(Type-Casting Pattern)

class Animal {
    func eat() {
        print(type(of: self),"eat")
    }
}

class Dog: Animal {
    func run() {
        print(type(of: self),"run")
    }
}

class Cat: Animal {
    func jump() {
        print(type(of: self),"jump")
    }
}

func check(_ animal:Animal) {
    switch animal {
    case let dog as Dog:
        dog.run()
        dog.eat()
    case is Cat:
        animal.eat()
    default:
        break
    }
}
check(Dog())
//Dog run
//Dog eat
check(Cat())
//Cat eat

表达式模式(Expression Pattern)

可以通过重载运算符,自定义表达式模式的匹配规则

struct Student {
    var score = 0, name = ""
    static func ~=(pattern:Int,value:Student) -> Bool{
        value.score >= pattern
    }
    
    static func ~=(pattern:ClosedRange<Int>,value:Student) -> Bool{
        pattern.contains(value.score)
    }
    static func ~=(pattern:Range<Int>,value:Student) -> Bool{
        pattern.contains(value.score)
    }
    
}
var stu = Student(score: 81, name: "tom")
switch stu{
case 100:print(">=100")
case 90:print(">=90")
case 80..<90:print("[80,90]")
case 60...79:print("[60,79]")
default:break
}
extension String{
    static func ~=(pattern:(String) -> Bool,value:String) ->Bool{
        pattern(value)
    }
}

func hasPrefix(_ prefix:String) ->((String) -> Bool){{$0.hasPrefix(prefix)}}
func hasSuffix(_ prefix:String) ->((String) -> Bool){{$0.hasSuffix(prefix)}}



var str = "jack"
switch str {
case hasPrefix("j"),hasSuffix("k"):
    print("以j开头,或者以k结尾")
default:
    break
}

28、属性访问控制

Swift代码中五种访问级别与定义实体的源文件和源文件所属的模块相关。open的访问级别最高,private的访问级别最低,internal是默认的访问级别。

  • open和public:使实体在其所定义模块的源文件中,或通过导入其所定义模块在另一个模块的源文件中使用。而public修饰的类或类成员只能在当前模块被继承或被子类重写;而open则可以在另一个模块中被继承或被子类重写。在定义框架的公共接口时,通常使用openpublic;
  • internal:使实体可以在其所定义模块的任何源文件中使用,但不能在其他模块的的源文件中使用。在定义应用程序或框架的内部结构时,通常使用internal
  • fileprivate:使实体只能在其定义的源文件中使用。当这些详细信息在整个文件中使用时,使用 fileprivate隐藏特定功能块的实现细节。
  • private:使实体在其所声明的类,以及同一文件中该声明的扩展中使用。当这些详细信息仅在单个声明中使用时,使用private来隐藏特定功能的实现细节。

open可以在不同模块中访问,public只能在当前模块中访问,internal系统默认的访问控制权限,fileprivate可以被继承但不能被子类修改或重写,private不能被继承

29、尾递归

递归在程序设计中是一种很有用的方法,它可以将复杂的过程用易于理解的方式转化和描述。举个例子,比如我们想要写一个从 0 累加到 n 的函数,如果我们不知道等差数列求和公式的话,就可以用递归的方式来做:

func sum(_ n:Int) -> Int {
    if n == 0 {
        return 0
    }
    return n + sum(n - 1)
}

sum(1000000)

会报错Thread 1: EXC_BAD_ACCESS (code=2, address=0x7ffeef3ffff8)这个错误

这是因为每次对于 sum 的递归调用都需要在调用栈上保存当前状态,否则我们就无法计算最后的 n + sum(n - 1)。当 n 足够大,调用栈足够深的时候,栈空间将被耗尽而导致错误,也就是我们常说的栈溢出了。

尾递归 一般对于递归,解决栈溢出的一个好方法是采用尾递归的写法。顾名思义,尾递归就是让函数里的最后一个动作是一个函数调用的形式,这个调用的返回值将直接被当前函数返回,从而避免在栈上保存状态

func tailSum(_ n: Int) -> Int {
    func sumInternal(_ n: Int, current: Int) -> Int {
        if n == 0 {
            return current
        } else {
            return sumInternal(n - 1, current: current + n)
        }
    }

    return sumInternal(n, current: 0)
}

print(tailSum(1000000))

但是如果你在项目中直接尝试运行这段代码的话还是会报错,因为在 Debug 模式下 Swift 编译器并不会对尾递归进行优化。我们可以在 scheme 设置中将 Run 的配置从 Debug 改为 Release,这段代码就能正确运行了。

OC到Swift

1、Selector

@selectorObjective-C 时代的一个关键字,它可以将一个方法转换并赋值给一个 SEL 类型,它的表现很类似一个动态的函数指针。在 Objective-C 时 selector 非常常用,从设定 target-action,到自举询问是否响应某个方法,再到指定接受通知时需要调用的方法等等,都是由 selector 来负责的。在 Objective-C 里生成一个 selector 的方法一般是这个样子的

-(void) callMe {
    //...
}

-(void) callMeWithParam:(id)obj {
    //...
}

SEL someMethod = @selector(callMe);
SEL anotherMethod = @selector(callMeWithParam:);

// 或者也可以使用 NSSelectorFromString
// SEL someMethod = NSSelectorFromString(@"callMe");
// SEL anotherMethod = NSSelectorFromString(@"callMeWithParam:"); 

一般为了方便,很多人会选择使用 @selector,但是如果要追求灵活的话,可能会更愿意使用 NSSelectorFromString 的版本 -- 因为我们可以在运行时动态生成字符串,通过方法名来调用对应的方法

在 Swift 中没有 @selector 了,取而代之,从 Swift 2.2 开始我们使用 #selector 来从暴露给 Objective-C 的代码中获取一个 selector。类似地,在 Swift 里对应原来 SEL 的类型是一个叫做 Selector 的结构体

@objc func callMe() {
    //...
}
@objc func callMeWithParam(obj: AnyObject!) {
    //...
}
let someMethod = #selector(callMe)
let anotherMethod = #selector(callMeWithParam(obj:))  

【注】selector 其实是 Objective-C runtime 的概念。在 Swift 4 中,默认情况下所有的 Swift 方法在 Objective-C 中都是不可见的,所以你需要在这类方法前面加上 @objc 关键字,将这个方法暴露给 Objective-C,才能进行使用

如果方法名字在方法所在域内是唯一的话,我们可以简单地只是用方法的名字来作为 #selector 的内容。相比于前面带有冒号的完整的形式来说,这么写起来会方便一些

let someMethod = #selector(callMe)
let anotherMethod = #selector(callMeWithParam)

如果同一个作用域里面存在同样名字的两个方法,但是参数不同,我们可以通过将方法强制转换来使用

@objc func commonFunc() {}

@objc func commonFunc(input: Int) -> Int {
    return input
} 

let method1 = #selector(commonFunc as ()->())
let method2 = #selector(commonFunc as (Int)->Int)

2、实例方法的动态调用

class MyClass {
    func method(number: Int) -> Int {
        return number + 1
    }
}

想要调用method方法的话,最普通的使用方式是生成MyClass的实例,然后用 .method 来调用它

let cls = MyClass()
cls.method(number: 1)

我们还可以把刚才的方法该成下面这样

let f = MyClass.method
let object = MyClass()
let result = f(object)(1)

我们观察f类:alt+单击

let f: (MyClass) -> (Int) -> Int

其实对于 Type.instanceMethod 这样的取值语句,实际上刚才

let f = MyClass.method

做的事情类似于下面字面量的转换

let f = { (obj: MyClass) in obj.method }

3、单例

在OC中单例的公认写法

@implementation MyManager
+ (id)sharedManager {
    static MyManager * staticInstance = nil;
    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{
        staticInstance = [[self alloc] init];
    });
    return staticInstance;
}
@end 

使用 GCD 中的 dispatch_once_t 可以保证里面的代码只被调用一次,以此保证单例在线程上的安全

在Swift中移出了dispatch_once,但是我们有更简单的写法

class MyManager  {
    static let shared = MyManager()
    private init() {}
}

4、条件编译

在 C 系语言中,可以使用#if或者 #ifdef 之类的编译条件分支来控制哪些代码需要编译,而哪些代码不需要。Swift 中没有宏定义的概念,因此我们不能使用 #ifdef 的方法来检查某个符号是否经过宏定义。但是为了控制编译流程和内容,Swift 还是为我们提供了几种简单的机制来根据需求定制编译内容的。

首先是 #if 这一套编译标记还是存在的,#elseif#else 是可选的。

#if <condition>

#elseif <condition>

#else

#endif

但是这几个表达式里的 condition 并不是任意的。Swift 内建了几种平台和架构的组合,来帮助我们为不同的平台编译不同的代码,具体地

方法 可选参数
os() macOS, iOS, tvOS, watchOS, Linux
arch() x86_64, arm, arm64, i386
swift() >= 某个版本

如果我们统一我们在 iOS 平台和 Mac 平台的关于颜色的 API 的话,一种可能的方法就是配合 typealias 进行条件编译:

#if os(macOS)
    typealias Color = NSColor
#else
    typealias Color = UIColor
#endif 

#if arch(x86_64)
     
#else
     
#endif


#if swift(>=14.0)
     
#else
     
#endif

对自定义符号进行编译

我们需要使用同一个 target 完成同一个 app 的收费版和免费版两个版本,并且希望在点击某个按钮时收费版本执行功能,而免费版本弹出提示的话,可以使用类似下面的方法

func someButtonPressed(sender: AnyObject!) {
    #if FREE_VERSION
        // 弹出购买提示,导航至商店等
    #else
        // 实际功能
    #endif
}

在这里我们用 FREE_VERSION 这个编译符号来代表免费版本。为了使之有效,我们需要在项目的编译选项中进行设置,在项目的 Build Settings 中,找到 Swift Compiler - Custom Flags,并在其中的Other Swift Flags加上 -D FREE_VERSION 就可以了。

5、@UIApplicationMain

在 C 系语言中,程序的入口都是 main 函数。对于一个 Objective-C 的 iOS app 项目,在新建项目时, Xcode 将帮我们准备好一个 main.m 文件,其中就有这个 main 函数

int main(int argc, char * argv[])
{
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil,
                   NSStringFromClass([AppDelegate class]));
    }
}

这个方法将根据第三个参数初始化一个 UIApplication 或其子类的对象并开始接收事件 (在这个例子中传入 nil,意味使用默认的 UIApplication)。最后一个参数指定了 AppDelegate 类作为应用的委托,它被用来接收类似 didFinishLaunching 或者 didEnterBackground 这样的与应用生命周期相关的委托方法。另外,虽然这个方法标明为返回一个 int,但是其实它并不会真正返回。它会一直存在于内存中,直到用户或者系统将其强制终止

新建一个 Swift 的 iOS app 项目后,我们会发现所有文件中都没有一个像 Objective-C 时那样的 main 文件,也不存在 main 函数。唯一和main有关系的是在默认的 AppDelegate 类的声明上方有一个 @UIApplicationMain 的标签。

其实 Swift 的 app 也是需要 main 函数的,只不过默认情况下是 @UIApplicationMain 帮助我们自动生成了而已。

如我们在删除 @UIApplicationMain 后,在项目中添加一个 main.swift 文件,然后加上这样的代码

UIApplicationMain(Process.argc, Process.unsafeArgv, nil,
    NSStringFromClass(AppDelegate)) 

现在编译运行,就不会再出现错误了。当然,我们还可以通过将第三个参数替换成自己的 UIApplication 子类,这样我们就可以轻易地做一些控制整个应用行为的事情了。比如将 main.swift 的内容换成

UIApplicationMain(
    CommandLine.argc,
    UnsafeMutableRawPointer(CommandLine.unsafeArgv)
        .bindMemory(
            to: UnsafeMutablePointer<Int8>.self,
            capacity: Int(CommandLine.argc)),
    NSStringFromClass(MyApplication.self),
    NSStringFromClass(AppDelegate.self)
)

import UIKit
class MyApplication: UIApplication {
    override func sendEvent(_ event: UIEvent) {
        super.sendEvent(event)
        print("Event sent:\(event)")
    }
}

let cls = MyClass()
cls.mustProtocolMethod()
cls.mustProtocolMethod1()

这样每次发送事件 (比如点击按钮) 时,我们都可以监听到这个事件了

6、可选协议和协议扩展

Objective-C 中的 protocol 里存在 @optional 关键字,被这个关键字修饰的方法并非必须要被实现。我们可以通过协议定义一系列方法,然后由实现协议的类选择性地实现其中几个方法。最好的例子我想应该是 UITableViewDataSource 和 UITableViewDelegate。前者中有两个必要方法

-tableView:numberOfRowsInSection:
-tableView:cellForRowAtIndexPath:  

原生的 Swift protocol 里没有可选项,所有定义的方法都是必须实现的

protocol MyProtocol {
    func mustProtocolMethod() //必须实现方法
    func mustProtocolMethod1() //必须实现方法
}


class MyClass: MyProtocol {
    func mustProtocolMethod() {
        print("MyClass-->必须实现方法:mustProtocolMethod")
    }
    
    func mustProtocolMethod1() {
        print("MyClass-->必须实现方法:mustProtocolMethod1")
    }
    
}

如果我们想要像 Objective-C 里那样定义可选的协议方法,就需要将协议本身和可选方法都定义为Objective-C 的,也即在 protocol 定义之前以及协议方法之前加上 @objc。另外和 Objective-C 中的 @optional 不同,我们使用没有 @ 符号的关键字 optional 来定义可选方法

@objc protocol MyProtocol1 {
    @objc optional func optionalProtocolMethod() //可选方法
    func mustProtocolMethod1() //必须实现方法
}


class MyClass1: MyProtocol1 {
    func mustProtocolMethod1() {
         print("MyClass1-->必须实现方法:mustProtocolMethod1")
    }
}

let cls1 = MyClass1()
cls1.mustProtocolMethod1()

一个不可避免的限制是,使用 @objc 修饰的 protocol 就只能被 class 实现了,也就是说,对于 struct 和 enum 类型,我们是无法令它们所实现的协议中含有可选方法或者属性的

在 Swift 2.0 中,我们有了另一种选择,那就是使用 protocol extension。我们可以在声明一个 protocol 之后再用 extension 的方式给出部分方法默认的实现。这样这些方法在实际的类中就是可选实现的了

protocol MyProtocol2 {
    func optionalProtocolMethod1() //可选方法
    func optionalProtocolMethod2() //可选方法
    func mustProtocolMethod1() //必须实现方法
}

extension MyProtocol2{
    func optionalProtocolMethod1(){}
    func optionalProtocolMethod2(){}
}

7、内存管理,weak 和 unowned

跟OC一样,Swift也是采用基于引用计算的ARC内存管理方案(针对堆空间)

Swift中ARC有3种引用

  • 1、强引用:默认情况下,引用都是强引用
  • 2、弱引用(weak):通过weak定义弱引用
    • 必须是可选类型的var,因为实例销毁后,ARC会自动将弱引用设置为nil
    • ARC自动给弱引用设置nil时,不会触发属性观察器
  • 3、无主引用(unowned):通过unowned定义无主引用
    • 不会产生强引用,实例销毁后仍然存储着实例的内存地址(类似于OC中的unsafe_unretained
    • 试图销毁后访问无主引用,会产生运行时错误(野指针)
    • Fatal error: Attempted to read an unowned reference but object 0x10070a460 was already deallocated
class Person {
    func eat() {
    }
    deinit {
        print("Person销毁")
    }
}

unowned var p = Person()
p.eat()

这段代码就会产生运行时错误

循环引用

weak、unowned 都能解决循环引用的问题,unowned 要比weak 少一些性能消耗

  • 生命周期中可能被置为nil使用weak
  • 初始化赋值以后不会被置为nil使用unowned

闭包的循环引用

  • 闭包表达式默认会对用到的外层对象产生额外的强引用(对外层进行了retain操作)
class Person {
    var fn:(() -> ())?
    func run() {
        print("run")
    }
    deinit {
        print("Person销毁")
    }
}
func test() {
    let p = Person()
    p.fn = {
        p.run()
    }
}
test()

下面这段代码就会造成循环引用,想要解决这个问题,可以使用weak或者unowned

func test() {
    let p = Person()
    p.fn = {[weak p] in
        p?.run()
    }
}


func test() {
    let p = Person()
    p.fn = {[unowned p] in
        p.run()
    }
}

如果想在定义闭包属性的同时引用self,这个闭包必须是lazy的,因为在实例初始化完毕后才能引用self

class Person {
    lazy var fun:(() -> ()) = {
        [weak self] in
        self?.run()
    }
    func run() {
        print("run")
    }
    deinit {
        print("Person销毁")
    }
}

闭包fn内部如果用到了实例成员,属性,方法,编译器会强制要求明确的写出self

【注】:编译器强制要求明确的写出self的时候有可能会导致循环引用,需要注意的

如果lazy属性是闭包调用的结果,那么不用考虑循环引用问题,(因为闭包调用后,闭包的声明周期就结束了)

class Person {
    var age: Int = 0
    lazy var getAge: Int = {
        self.age
    }()
    deinit {
        print("Person销毁")
    }
}

8、值类型与引用类型

内存(RAM)中有两个区域,栈区(stack)和堆区(heap)。在 Swift 中,值类型,存放在栈区;引用类型,存放在堆区。

值类型(Value Type)

值类型,即每个实例保持一份数据拷贝

在 Swift 中,典型的有 struct,enum,以及 tuple 都是值类型。而平时使用的 Int, Double,Float,String,Array,Dictionary,Set 其实都是用结构体实现的,也是值类型。

Swift 中,值类型的赋值为深拷贝(Deep Copy),值语义(Value Semantics)即新对象和源对象是独立的,当改变新对象的属性,源对象不会受到影响,反之同理。

struct CoordinateStruct {
   var x: Double
   var y: Double
}

var coordA = CoordinateStruct(x: 0, y: 0)
var coordB = coordA

coordA.x = 100.0
print("coordA.x -> \(coordA.x)")
print("coordB.x -> \(coordB.x)")

如果声明一个值类型的常量,那么就意味着该常量是不可变的(无论内部数据为 var/let)

let coordC = CoordinateStruct(x: 0, y: 0)

在 Swift 3.0 中,可以使用 withUnsafePointer(to:_:) 函数来打印值类型变量的内存地址,这样就能看出两个变量的内存地址并不相同。

withUnsafePointer(to: &coordA) { print("\($0)") }
withUnsafePointer(to: &coordB) { print("\($0)") }

0x0000000100007670
0x0000000100007680

在 Swift 中,双等号(== & !=)可以用来比较变量存储的内容是否一致,如果要让我们的 struct 类型支持该符号,则必须遵守Equatable协议。

extension CoordinateStruct: Equatable {
    static func ==(left: CoordinateStruct, right: CoordinateStruct) -> Bool {
        return (left.x == right.x && left.y == right.y)
    }
}

if coordA != coordB {
    print("coordA != coordB")
}

引用类型(Reference Type)

引用类型,即所有实例共享一份数据拷贝

在 Swift 中,class 和闭包是引用类型。引用类型的赋值是浅拷贝(Shallow Copy),引用语义(Reference Semantics)即新对象和源对象的变量名不同,但其引用(指向的内存空间)是一样的,因此当使用新对象操作其内部数据时,源对象的内部数据也会受到影响。

class Dog {
    var height = 0.0
    var weight = 0.0
}

var dogA = Dog()
var dogB = dogA

dogA.height = 50.0
print("dogA.height -> \(dogA.height)")
print("dogB.height -> \(dogB.height)")

// dogA.height -> 50.0
// dogB.height -> 50.0

在 Swift 3.0 中,可以使用以下方法来打印引用类型变量指向的内存地址。从中即可发现,两个变量指向的是同一块内存空间。

print(Unmanaged.passUnretained(dogA).toOpaque())
print(Unmanaged.passUnretained(dogB).toOpaque())

//0x0000000100772ff0
//0x0000000100772ff0

在 Swift 中,三等号(=== & !==)可以用来比较引用类型的引用(即指向的内存地址)是否一致。也可以在遵守 Equatable 协议后,使用双等号(== & !=)用来比较变量的内容是否一致。

9、String 还是 NSString

简单来说:没有特别需要,尽可能的还是使用String,有以下三个原因

  • 1、虽然 StringNSString 有着良好的互相转换的特性,但是现在 Cocoa 所有的 API 都接受和返回 String类型。我们没有必要也不必给自己凭空添加麻烦去把框架中返回的字符串做一遍转换
  • 2、因为在 Swift 中 String是struct,相比起 NSObject 的 NSString 类来说,更切合字符串的 "不变" 这一特性。通过配合常量赋值 (let) ,这种不变性在多线程编程时就非常重要了,它从原理上将程序员从内存访问和操作顺序的担忧中解放出来。另外,在不触及 NSString 特有操作和动态特性的时候,使用 String 的方法,在性能上也会有所提升
  • 3、因为 String 实现了 Collection 这样的协议,因此有些 Swift 的语法特性只有 String 才能使用,而 NSString 是没有的。一个典型就是 for...in 的枚举

10、GCD

GCD中Swift和OC都差不多,为了方便使用,我们可以简单封装以下GCD

typealias Task = (_ cancel : Bool) -> Void

@discardableResult
func delay(_ time: TimeInterval, task: @escaping ()->()) ->  Task? {

    func dispatch_later(block: @escaping ()->()) {
        let t = DispatchTime.now() + time
        DispatchQueue.main.asyncAfter(deadline: t, execute: block)
    }

    var closure: (()->Void)? = task
    var result: Task?

    let delayedClosure: Task = {
        cancel in
        if let internalClosure = closure {
            if (cancel == false) {
                DispatchQueue.main.async(execute: internalClosure)
            }
        }
        closure = nil
        result = nil
    }

    result = delayedClosure
    dispatch_later {
        if let delayedClosure = result {
            delayedClosure(false)
        }
    }
    return result;
}
func cancel(_ task: Task?) {
    task?(true)
}

11、自省

向一个对象发出询问,以确定他是不是属于某个类,这种操作就称为自省。

在OC中一个对象询问它是不是属于某个类。常用的方法有下面两类

OC方法

[obj1 isKindOfClass:[ClassA class]];
[obj2 isMemberOfClass:[ClassB class]];
  • 1、-isKindOfClass: 判断 obj1 是否是 ClassA 或者其子类的实例对象;
  • 2、isMemberOfClass: 则对 obj2 做出判断,当且仅当 obj2 的类型为 ClassB 时返回为真

Swift方法

class ClassA: NSObject {}
class ClassB: ClassA {}

let obj1 = ClassA()
let obj2 = ClassB()

print(obj1.isKind(of: ClassA.self))
print(obj2.isMember(of: ClassA.self))

//true
//false

对于一个不确定的类型,我们现在可以使用 is 来进行判断。is 在功能上相当于原来的 isKindOfClass,可以检查一个对象是否属于某类型或其子类型。is 和原来的区别主要在于亮点,首先它不仅可以用于 class 类型上,也可以对 Swift 的其他像是 structenum类型进行判断

class ClassA { }
class ClassB: ClassA { }

let obj: AnyObject = ClassB()

if (obj is ClassA) {
    print("属于 ClassA")
}

if (obj is ClassB) {
    print("属于 ClassB")
}

12、KVO

在Swift中KVO仅限于NSObject的子类,我们还需要做额外的工作,那就是将想要观测的对象标记为 dynamic 和 @objc

在 Swift 4 之前的版本中,为一个 NSObject 的子类实现 KVO 的最简单的例子看起来是这样的


class MyClass: NSObject {
    @objc dynamic var date = Date()
}

private var myContext = 0
class Class: NSObject {

    var myObject: MyClass!

    override init() {
        super.init()
        myObject = MyClass()
        print("初始化 MyClass,当前日期: \(myObject.date)")
        myObject.addObserver(self,
            forKeyPath: "date",
            options: .new,
            context: &myContext)

        delay(3) {
            self.myObject.date = Date()
        }
    }

    override func observeValue(forKeyPath keyPath: String?,
                            of object: Any?,
                               change: [NSKeyValueChangeKey : Any]?,
                              context: UnsafeMutableRawPointer?)
    {
        if let change = change, context == &myContext {
            if let newDate = change[.newKey] as? Date {
                print("MyClass 日期发生变化 \(newDate)")
            }
        }
    }
}
 
let obj = Class()


初始化 MyClass,当前日期: 2020-04-08 07:26:22 +0000
MyClass 日期发生变化 2020-04-08 07:26:25 +0000

Swift 4 中 Apple 引入了新的 KeyPath 的表达方式,现在,对于类型 Foo 中的变量 bar: Bar,对应的 KeyPath 可以写为 \Foo.bar

class AnotherClass: NSObject {
    var myObject: MyClass!
    var observation: NSKeyValueObservation?
    override init() {
        super.init()
        myObject = MyClass()
        print("初始化 AnotherClass,当前日期: \(myObject.date)")

        observation = myObject.observe(\MyClass.date, options: [.new]) { (_, change) in
            if let newDate = change.newValue {
                print("AnotherClass 日期发生变化 \(newDate)")
            }
        }

        delay(1) { self.myObject.date = Date() }
    }
} 

使用Swift 4.0 KeyPath的好处有很多

  • 1、设定观察和处理观察的代码被放在了一起,让代码维护难度降低很多;
  • 2、其次在处理时我们得到的是类型安全的结果,而不是从字典中取值;
  • 3、我们不再需要使用 context 来区分是哪一个观察量发生了变化,而且使用 observation 来持有观察者可以让我们从麻烦的内存管理中解放出来,观察者的生命周期将随着 AnotherClass 的释放而结束

Swift 中使用 KVO 还是有有两个显而易见的问题

  • 1、在 Objective-C 中我们几乎可以没有限制地对所有满足 KVC 的属性进行监听,而现在我们需要属性有 dynamic 和 @objc 进行修饰,有时候我们很可能也无法修改想要观察的类的源码,遇到这种情况,一个可行的方案是继承这个类,并且将需要观察的属性使用dynamic 和 @objc重写
class MyClass: NSObject {
    var date = Date()
}

class MyChildClass: MyClass {
    @objc dynamic override var date: Date {
        get { return super.date }
        set { super.date = newValue }
    }
}
  • 2、另一个大问题是对于那些非 NSObject 的 Swift 类型怎么办。我们可以通过属性观察器来处理

13、局部scope

C 系语言中在方法内部我们是可以任意添加成对的大括号 {} 来限定代码的作用范围的。这么做一般来说有两个好处,首先是超过作用域后里面的临时变量就将失效,这不仅可以使方法内的命名更加容易,也使得那些不被需要的引用的回收提前进行了,可以稍微提高一些代码的效率;另外,在合适的位置插入括号也利于方法的梳理,对于那些不太方便提取为一个单独方法,但是又应该和当前方法内的其他部分进行一些区分的代码,使用大括号可以将这样的结构进行一个相对自然的划分

OC代码

- (void)loadView {
    UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 480)];
    {
        UILabel *titleLabel = [[UILabel alloc]
                initWithFrame:CGRectMake(150, 30, 200, 40)];
        titleLabel.textColor = [UIColor redColor];
        titleLabel.text = @"Title";
        [view addSubview:titleLabel];
    }

    {
        UILabel *textLabel = [[UILabel alloc]
                initWithFrame:CGRectMake(150, 80, 200, 40)];
        textLabel.textColor = [UIColor redColor];
        textLabel.text = @"Text";
        [view addSubview:textLabel];
    }

    self.view = view;
}

Swift方法

在 Swift 中,直接使用大括号的写法是不支持的,因为这和闭包的定义产生了冲突。如果我们想类似地使用局部 scope 来分隔代码的话,一个不错的选择是定义一个接受 ()->() 作为函数的全局方法,然后执行它

override func loadView() {
        let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 480))
        view.backgroundColor = .white

        local {
            let titleLabel = UILabel(frame: CGRect(x: 150, y: 30, width: 200, height: 40))
            titleLabel.textColor = .red
            titleLabel.text = "Title"
            view.addSubview(titleLabel)
        }

        local {
            let textLabel = UILabel(frame: CGRect(x: 150, y: 80, width: 200, height: 40))
            textLabel.textColor = .red
            textLabel.text = "Text"
            view.addSubview(textLabel)
        }

        self.view = view
    }

我们还可以使用匿名闭包来实现

override func loadView() {
        
        let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 480))
        view.backgroundColor = .white
        
        let titleLabel: UILabel = {
            let label = UILabel(frame: CGRect(x: 150, y: 30, width: 200, height: 40))
            label.textColor = .red
            label.text = "Title"
            return label
        }()
        view.addSubview(titleLabel)
        
        let textLabel: UILabel = {
            let label = UILabel(frame: CGRect(x: 150, y: 80, width: 200, height: 40))
            label.textColor = .red
            label.text = "Text"
            return label
        }()
        view.addSubview(textLabel)
        
        self.view = view
    }

14、关联对象

我们经常会遇到给分类添加成员变量的问题,对于这类问题,OC的写法大家都是耳熟能详了。譬如给UIView添加一个viewId的成员变量

#import <objc/runtime.h>
static const void *RunTimeViewID = @"RunTimeViewID";

@implementation UIView (JHExtension)

- (NSString *)viewID{
    NSString *ID = objc_getAssociatedObject(self, &RunTimeViewID);
    return ID;
}
- (void)setViewID:(NSString *)viewID{
    objc_setAssociatedObject(self, &RunTimeViewID, viewID, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

Swift

在 Swift 中这样的方法依旧有效,只不过在写法上可能有些不同。两个对应的运行时的 get 和 set Associated Object 的 API 是这样的

func objc_getAssociatedObject(object: AnyObject!,
                                 key: UnsafePointer<Void>
                             )  -> AnyObject!

func objc_setAssociatedObject(object: AnyObject!,
                                 key: UnsafePointer<Void>,
                               value: AnyObject!,
                              policy: objc_AssociationPolicy)  
struct RunTimeViewKey {
    static let RunTimeViewID = UnsafeRawPointer.init(bitPattern: "RunTimeViewID".hashValue)
}

extension UIView {
    var ViewID: String? {
        set {
            objc_setAssociatedObject(self, RunTimeViewKey.RunTimeViewID!, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
       
        }
        get {
            return  objc_getAssociatedObject(self, RunTimeViewKey.RunTimeViewID!) as? String
        }
    }
    
}

15、Lock

无并发,不编码。而只要一说到多线程或者并发的代码,我们可能就很难绕开对于锁的讨论。简单来说,为了在不同线程中安全地访问同一个资源,我们需要这些访问顺序进行

OC方法

- (void)myMethod:(id)anObj {
    @synchronized(anObj) {
        // 在括号内持有 anObj 锁
    }
}

Swift方法

在Swift中去掉了synchronized方法,其实 @synchronized 在幕后做的事情是调用了 objc_sync 中的 objc_sync_enterobjc_sync_exit 方法,并且加入了一些异常判断。因此,在 Swift 中,如果我们忽略掉那些异常的话,我们想要 lock 一个变量的话

//定义一个闭包
func synchronized(_ lock: AnyObject, closure: () -> ()) {
    objc_sync_enter(lock)
    closure()
    objc_sync_exit(lock)
}


func myMethodLocked(anObj: AnyObject!) {
    synchronized(anObj) {
        // 在括号内持有 anObj 锁
    }
} 

举一个具体的使用例子,比如我们想要为某个类实现一个线程安全的 setter,可以这样进行重写

class Obj {
    var _str = "123"
    var str: String {
        get {
            return _str
        }
        set {
            synchronized(self) {
                _str = newValue
            }
        }
    // 下略
    }
} 

16、性能方面

相比于 Objective-C,Swift 最大的改变就在于方法调用上的优化

OC方法调用

在 Objective-C 中,所有的对于 NSObject 的方法调用在编译时会被转为 objc_msgSend 方法。这个方法运用 Objective-C 的运行时特性,使用派发的方式在运行时对方法进行查找。因为 Objective-C 的类型并不是编译时确定的,我们在代码中所写的类型不过只是向编译器的一种“建议”,不论对于怎样的方法,这种查找的代价基本都是同样的

这个过程的等效的表述可能类似这样 (注意这只是一种表述,与实际的代码和工作方式无关)

methodToCall = findMethodInClass(class, selector);
// 这个查找一般需要遍历类的方法表,需要花费一定时间

methodToCall();  // 调用  

Swift方法调用

Swift 因为使用了更安全和严格的类型,如果我们在编写代码中指明了某个实际的类型的话 (注意,需要的是实际具体的类型,而不是像 Any 这样的抽象的协议),我们就可以向编译器保证在运行时该对象一定属于被声明的类型 因为有了更多更明确的类型信息,编译器就可以在类型中处理多态时建立虚函数表 (vtable),这是一个带有索引的保存了方法所在位置的数组。在方法调用时,与原来动态派发和查找方法不同,现在只需要通过索引就可以直接拿到方法并进行调用了,这是实实在在的性能提升。这个过程大概相当于:

let methodToCall = class.vtable[methodIndex]
// 直接使用 methodIndex 获取实现

methodToCall();  // 调用

更进一步,在确定的情况下,编译器对 Swift 的优化甚至可以做到将某些方法调用优化为 inline 的形式。比如在某个方法被 final 标记时,由于不存在被重写的可能,vtable 中该方法的实现就完全固定了。对于这样的方法,编译器在合适的情况下可以在生成代码的阶段就将方法内容提取到调用的地方,从而完全避免调用

总结

  • 1、文章是读王巍 (onevcat). “Swifter - Swift 必备 Tips (第四版)总结所得
  • 2、文章中代码的demo地址