《Swift 開發(fā)者必備 Tips》 閱讀筆記(一)

本編是Swifter - Swift 開發(fā)者必備 Tips 閱讀筆記

一、柯里化(Currying), 百度百科

  • 柯里化(Currying)是把接受多個參數(shù)的函數(shù)變換成接受一個單一參數(shù)(最初函數(shù)的第一個參數(shù))的函數(shù),并且返回接受余下的參數(shù)且返回結(jié)果的新函數(shù)的技術(shù)

  • 例如, 現(xiàn)在有如下函數(shù), 作用是每次傳入一個數(shù), 返回這個數(shù)+1的數(shù)字

func addOne(num: Int) -> Int {
    return num + 1
}
  • 如果我們需要一個得到數(shù)字 +2, +3.... 等等的函數(shù), 就需要分別定義
func addTwo(num: Int) -> Int {
    return num + 2
}

func addThree(num: Int) -> Int {
    return num + 3
}
  • 我們可以將這類方法簡化為, 傳入兩個參數(shù), 第一個參數(shù)是需要增加的數(shù)量, 第二個是基數(shù)
func addTo(adder: Int, num: Int) -> Int {
    return num + adder
}
  • addTo函數(shù)可以進行柯里化, 接收一個單一參數(shù)adder, 返回一個接受余下參數(shù)的新函數(shù), 并且這個新函數(shù)需要返回原有函數(shù)的結(jié)果
func addTo(adder: Int) -> (Int) -> Int {
    func addNum(num: Int) -> Int {
        return num + adder
    }
    return addNum
}
  • 最終簡化為:
func addTo(adder: Int) -> (Int) -> Int {
    return {
        num in
        return num + adder
    }
}
  • 通過將需要傳入兩個參數(shù)的addTo函數(shù), 簡化為只需要傳入一個參數(shù)的addTo函數(shù)的過程, 就是柯里化

  • addTo返回的函數(shù), 能夠接收余下參數(shù), 并且能夠返回最終結(jié)果

  • 這時我們想要獲取 +2, +3的方法, 只需要如下處理

let addTwo = addTo(2)
print(addTwo(1))          // 打印: 3
print(addTwo(2))          // 打印: 4
print(addTwo(3))          // 打印: 5

let addThree = addTo(3)
print(addThree(1))        // 打印: 4
print(addThree(2))        // 打印: 5
print(addThree(3))        // 打印: 6
  • 柯里化是一種量產(chǎn)相似方法的好辦法,可以通過柯里化一個方法模板來避免寫出很多重復(fù)代碼,也方便了今后維護。
柯里化應(yīng)用實例
  • Swift中, 可以使用如下的方式定義一個方法的管理器
struct Control {
    let target: AnyObject
    let action: Selector
    
    func performAction() {
        target.perform(action)
    }
}
  • Control的實例持有target, 以及target的某一個方法action, 就可以使用調(diào)用performAction來調(diào)用這個方法
class SomeClass: UIView {
    
    var control: Control?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.control = aControl(target: self, action: NSSelectorFromString("say"))
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.control?.performAction()
    }
    
    @objc func say() {
        print("啦啦啦")
    }
    
}
  • 上面的代碼中, SomeClass類有一個control屬性, 在初始化時, 使control持有了say方法, 當(dāng)點擊SomeClass的實例時, 使用control觸發(fā)say方法

  • 這種寫法針對只有一個調(diào)用事件時還算可以, 如果有多個調(diào)用事件, 就需要寫很多個control屬性, 這時就會使代碼顯得很亂

  • 我們可以通過柯里化, 對這種寫法進行優(yōu)化

  • 首先, 我們定義一個包裝target-action的結(jié)構(gòu)體, 這個結(jié)構(gòu)體遵從TargetAction協(xié)議

protocol TargetAction {
    func performAction()
}

struct TargetActionWrapper<T: AnyObject>: TargetAction {
    weak var target: T?
    let action: (T) -> () -> ()
    
    func performAction() -> () {
        if let t = target {
            action(t)()
        }
    }
}
  • 接著定義事件觸發(fā)時機的枚舉類型
enum ControlEvent {
    case touchesBegan
    case touchesEnded
    // ...
}
  • 最后定義一個控制容器, 用來存放多個TargetActionWrapper類型的對象, 并管理這些對象
class Control: UIView {
    var actions = [ControlEvent: TargetAction]()
    
    func setTarget<T: AnyObject>(target: T, action: @escaping (T) -> () -> (), controlEvent: ControlEvent) {
        
        actions[controlEvent] = TargetActionWrapper(target: target, action: action)
    }
    
    func removeTargetForControlEvent(controlEvent: ControlEvent) {
        actions[controlEvent] = nil
    }
    
    func performActionForControlEvent(controlEvent: ControlEvent) {
        actions[controlEvent]?.performAction()
    }
}
  • 我們重寫SomClass
class SomeClass: UIView {
    
    var control: Control = Control()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.control.setTarget(target: self, action: SomeClass.touchesBegan, controlEvent: .touchesBegan)
        self.control.setTarget(target: self, action: SomeClass.touchesEnded, controlEvent: .touchesEnded)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.control.performActionForControlEvent(controlEvent: .touchesBegan)
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.control.performActionForControlEvent(controlEvent: .touchesEnded)
    }
    
    func touchesBegan() {
        print("開始點擊")
    }
    
    func touchesEnded() {
        print("結(jié)束點擊")
    }
}

二、將protocol的方法聲明為mutating

  • 對于值類型(結(jié)構(gòu)體和枚舉), 想要在實例方法中修改成員變量, 就需要在方法前加上mutating關(guān)鍵字
protocol Vehicle
{
    var numberOfWheels: Int {get}
    var color: UIColor {get set}

    mutating func changeColor()
}

struct MyCar: Vehicle {
    let numberOfWheels = 4
    var color = UIColor.blue

    mutating func changeColor() {
        // 因為 `color` 的類型是 `UIColor`,這里直接寫 .red 就足以推斷類型了
        color = .red
    }
}
  • 另外,在使用 class 來實現(xiàn)帶有 mutating 的方法的協(xié)議時,具體實現(xiàn)的前面是不需要加 mutating 修飾的,因為 class 可以隨意更改自己的成員變量。所以說在協(xié)議里用 mutating 修飾方法,對于 class 的實現(xiàn)是完全透明,可以當(dāng)作不存在的。

三、Sequence

1、迭代器, 百度百科
  • 迭代器是一個對象,它的工作是遍歷并選擇序列中的對象,它提供了一種訪問一個容器對象中的各個元素,而又不必暴露該對象內(nèi)部細節(jié)的方法。通過迭代器,開發(fā)人員不需要了解容器底層的結(jié)構(gòu),就可以實現(xiàn)對容器的遍歷。由于創(chuàng)建迭代器的代價小,因此迭代器通常被稱為輕量級的容器。

  • Swift中的迭代器需要遵從IteratorProtocol協(xié)議

public protocol IteratorProtocol {

    associatedtype Element

    // 返回下一個元素, 如果沒有返回 nil
    // 通過反復(fù)調(diào)用, 可以獲取所有的元素, 一旦元素的序列已經(jīng)耗盡, 所有后續(xù)的調(diào)用返回 nil
    public mutating func next() -> Self.Element?
}
  • 以數(shù)組的迭代器為例, 可以使用數(shù)組的makeIterator()方法, 獲取數(shù)組的迭代器
let animals = ["Antelope", "Butterfly", "Camel", "Dolphin"]
var animalIterator = animals.makeIterator()

while let animal = animalIterator.next() {
    print(animal)
}
// 打印結(jié)果:
Antelope
Butterfly
Camel
Dolphin
  • 此時已經(jīng)通過迭代器遍歷了數(shù)組中的所有元素

  • 如果再次調(diào)用animalIterator.next()方法, 只會返回nil

print(animalIterator.next())
// 打印結(jié)果: nil
  • 迭代器只能通過next()方法獲取下一個元素, 一直遍歷到最后為止, 而無法返回到一開始的位置重新遍歷

  • 我們也可以自定義迭代器, 只需準守IteratorProtocol協(xié)議

struct CustomIterator<T>: IteratorProtocol {
    
    typealias Element = T
    
    var array: [Element]
    
    var currentIndex = 0
    
    init(_ array: [Element]) {
        self.array = array
    }
    
    mutating func next() -> Element? {
        if currentIndex < array.count {
            let int = array[currentIndex]
            currentIndex += 1
            return int
        }else {
            return nil;
        }
    }
}
  • 通過數(shù)組創(chuàng)建一個CustomIterator迭代器對象
let animals = ["Antelope", "Butterfly", "Camel", "Dolphin"]
var custom = CustomIterator(animals)
  • 遍歷剛創(chuàng)建的結(jié)構(gòu)體
while let animal = custom.next() {
    print(animal)
}

// 打印:
Antelope
Butterfly
Camel
Dolphin
  • 再次調(diào)用custom.next(), 將會返回nil
print(custom.next())
// 返回: nil
  • 上面自定義的迭代器是正向的遍歷集合中的元素, 我們還可以定義一個反向遍歷的迭代器
struct CustomReverseIterator<T>: IteratorProtocol {
    
    typealias Element = T
    
    var array: [Element]
    
    var currentIndex: Int
    
    init(_ array: [Element]) {
        self.array = array
        currentIndex = array.count - 1
    }
    
    mutating func next() -> Element? {
        guard currentIndex >= 0 else {
            return nil
        }
        let int = array[currentIndex]
        currentIndex -= 1
        return int
    }
}
2、Sequence
  • 在Swift中, 所有想要實現(xiàn)for-in循環(huán)的對象, 都必須繼承Sequence協(xié)議

  • 遵守Sequence協(xié)議的最簡單的類型, 需要實現(xiàn)以下方法

public protocol Sequence {
    // 關(guān)聯(lián)類型, 必須遵守了IteratorProtocol協(xié)議
    associatedtype Iterator : IteratorProtocol
    // 獲取一個迭代器
    public func makeIterator() -> Self.Iterator
}
  • for-in循環(huán)每一次都會調(diào)用序列的makeIterator()方法, 來獲取一個新的迭代器, 然后開始遍歷

  • 現(xiàn)在我們自定義一個可倒序遍歷的序列

struct CustomReverseSequence<T>: Sequence {
    
    var array: [T]
    
    init(_ array: [T]) {
        self.array = array
    }
    
    typealias intrator = CustomReverseIterator<T>
    
    func makeIterator() -> intrator {
        return CustomReverseIterator(self.array)
    }
}
  • 我們通過數(shù)組創(chuàng)建一個CustomReverseSequence的實例
let animals = ["Antelope", "Butterfly", "Camel", "Dolphin"]
let sequence = CustomReverseSequence(animals)
  • 此時, 我們可以使用for-in循環(huán), 來遍歷sequence對象
for animal in sequence {
    print(animal)
}
// 打印:
Dolphin
Camel
Butterfly
Antelope
  • 這段代碼等價于
var iterator = sequence.makeIterator()
while let animal = iterator.next() {
    print(animal)
}
  • 有一點比較方便的是, Sequence擴展中, 實現(xiàn)了mapfilterreduce這些方法, 我們可以直接調(diào)用, 下面以map為例
let animals = ["Antelope", "Butterfly", "Camel", "Dolphin"]
let sequence = CustomReverseSequence(animals)
let newSequence = sequence.map {
    return $0 + "123"
}
print(newSequence)
// 打印: ["Dolphin123", "Camel123", "Butterfly123", "Antelope123"]

四、多元組(Tuple)

  • 元組可以將多種類型的值, 包裝起來, 并做為返回值使用

  • 例如OC版本的CGRect, 有一個輔助方法CGRectDivide, 可以講一個CGRect實例切割成兩部分

/*
CGRectDivide(CGRect rect, CGRect *slice, CGRect *remainder, 
                             CGFloat amount, CGRectEdge edge)
*/
CGRect rect = CGRectMake(0, 0, 100, 100);
CGRect small;
CGRect large;
CGRectDivide(rect, &small, &large, 20, CGRectMinXEdge);
  • 上面的代碼將 {0,0,100,100} 的 rect 分割為兩部分,分別是 {0,0,20,100}small{20,0,80,100}large。由于 C 系語言的單一返回,我們不得不通過傳入指針的方式讓方法來填充需要的部分,可以說使用起來既不直觀,又很麻煩。

  • 而在Swift中, 可以使用元組來返回多個值

extension CGRect {
    //...
    func divided(atDistance: CGFloat, from fromEdge: CGRectEdge) 
                    -> (slice: CGRect, remainder: CGRect)
    //...
}
  • 在使用的時候, 對比OC會方便很多, 也更容易理解
let rect = CGRect(x: 0, y: 0, width: 100, height: 100)
let (small, large) = rect.divided(atDistance: 20, from: .minXEdge)
  • 上面的代碼, 就是使用元組做為返回值的一個例子, 除此之外, 還可以使用元組簡化交換兩個變量的值的寫法
  • 在OC中, 想要交換兩個變量的值, 需要使用如下類似的寫法, 用一個常量做為中轉(zhuǎn)
func swapMe1<T>( a: inout T, b: inout T) {
    let temp = a
    a = b
    b = temp
}
  • 而使用Swift的元組, 就可以如下處理
func swapMe2<T>(a: inout T, b: inout T) {
    (a, b) = (b, a)
}

五、@autoclosure 和 ??

1、自動閉包 @autoclosure
  • 自動閉包是一種自動創(chuàng)建的閉包,用于包裝傳遞給函數(shù)作為參數(shù)的表達式。這種閉包不接受任何參數(shù),當(dāng)它被調(diào)用的時候,會返回被包裝在其中的表達式的值。
  • 簡單地說, 就是把一句表達式封裝成一個閉包, 并將表達式的結(jié)果作為返回值
  • 比如有如下函數(shù), logIfTrue接收一個()->Bool類型的閉包
func logIfTrue(_ predicate: () -> Bool) {
    if predicate() {
        print("true")
    }
}
  • 當(dāng)我們調(diào)用logIfTrue方法時
logIfTrue { () -> Bool in
    return 2 > 1
}
  • 可以簡化為
logIfTrue {2 > 1}
  • 但是不管哪一種方式, 要么是書法起來十分麻煩, 要么是表達上不太清晰, 所以蘋果提供了@autoclosure關(guān)鍵字, 可以將logIfTrue函數(shù)的定義改寫為下面的樣子
func logIfTrue(_ predicate: @autoclosure () -> Bool) {
    if predicate() {
        print("true")
    }
}
  • 這時候, 再次調(diào)用logIfTrue函數(shù)
logIfTrue (2 > 1)
  • Swift將會把2 > 1這個表達式自動轉(zhuǎn)換為() -> Bool類型的閉包, 這樣我們就得到了一個寫法簡單, 表意清除的式子
2、??
  • 在Swift中, 有一個非常有用的操作符, 可以用來快速地對nil進行條件判斷, 那就是??。這個操作符可以判斷輸入并在當(dāng)左側(cè)的值是非nil的Optional值時返回其value, 當(dāng)左側(cè)是nil時返回右側(cè)的值, 比如:
var a: Int?
var b = 10
var c = a ?? b
  • 這個例子, 我們沒有給a賦值, 所以最終會將b的值賦值給c, 即c = 10

  • 我們可以點進??的定義中查看

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

func ??<T>(optional: T?, defaultValue: @autoclosure () -> T) -> T
  • 在這里, 我們的輸入滿足的是后者, 雖然表面上看b只是一個Int, 但是在使用的時候, 被自動封裝成了() -> Int類型的閉包

  • 此時, 推測一下??的具體實現(xiàn)

func ??<T>(optional: T?, defaultValue: @autoclosure () -> T) -> T {
    switch optional {
    case .some(let value):
        return value
    case .none:
        return defaultValue()
    }
}
  • 這里將defaultValue參數(shù)定義為了() -> T類型的自動閉包, 而不是T類型
  • 如果我們直接使用T做為defaultValue參數(shù)的類型, 那么在使用??操作符之前就需要準備一個默認值。
  • 正常情況下, 這樣不會有大問題, 但是如果這個默認值是通過一系列復(fù)雜計算得到的話, 可能會造成資源浪費, 因為如果optional不是nil的話, 這個默認是根本不會被用到, 而會直接返回optional解包后的值
  • 使用autoclosure就避免了這樣的開銷, 方法會將默認值的計算推遲到optional判定為nil之后

@autoclosure并不支持帶有輸入?yún)?shù)的寫法, 也就是說只有形如() -> T的參數(shù)才能使用這個特性簡化

六、@escaping

  • Swift中我們可以定義一個接受函數(shù)做為參數(shù)的函數(shù), 而在調(diào)用時, 使用閉包的方式來傳遞這個參數(shù)是常見手段
  • 定義函數(shù)
func doWork(block: () -> ()) {
    block()
}
  • 調(diào)用函數(shù)
doWork {
    print("work")
}
  • 這種最簡單的形式的閉包其實還默認隱藏了一個假設(shè), 那就是參數(shù)block的內(nèi)容會在doWork返回前就完成, 也就是說, 對于block的調(diào)用是同步行為。
  • 如果我們改變一下代碼, 將block放到一個Dispatch中去, 讓他在doWork返回后被調(diào)用的話, 我們就需要在block的類型前加上@escaping標記來表名這個閉包是會逃逸出該方法的
func doWorkAsync(block: @escaping: () -> ()) {
    DispatchQueue.main.async {
        block()
    }
}
  • 在使用閉包調(diào)用這個兩個方法時,也會有一些行為的不同。我們知道,閉包是可以捕獲其中的變量的。對于 doWork 參數(shù)里這樣的沒有逃逸行為的閉包,因為閉包的作用域不會超過函數(shù)本身,所以我們不需要擔(dān)心在閉包內(nèi)持有 self 等。
  • 而接受 @escapingdoWorkAsync 則有所不同。由于需要確保閉包內(nèi)的成員依然有效,如果在閉包內(nèi)引用了 self 及其成員的話,Swift 將強制我們明確地寫出 self。對比下面的兩個用例的不同之處:
class S {
    var foo = "foo"

    func method1() {
        doWork {
            print(foo)
        }
        foo = "bar"
    }

    func method2() {
        doWorkAsync {
            print(self.foo)
        }
        foo = "bar"
    }
}

S().method1() // foo
S().method2() // bar
  • 如果我們不希望在閉包中持有self, 可以使用[weak self]的方式來表達:
func method3() {
    doWorkAsync {
        [weak self] in
        print(self?.foo ?? "nil")
    }
    foo = "bar"
}

S().method3() // nil
  • 因為S()沒有被強引用, 所以作用域結(jié)束后就會銷毀, 此時才會去調(diào)用doWorkAsync中的block, 這時self已經(jīng)銷毀, 所以才會打印nil

如果你在協(xié)議或者父類中定義了一個接受@escaping為參數(shù)的方法, 那么在實際協(xié)議和類型或者是這個父類的子類時, 對應(yīng)的方法也必須被聲明為@escaping, 否則兩個方法會被認為擁有不同的函數(shù)簽名

protocol P {
    func work(b: @escaping ()->())
}

// 可以編譯
class C: P {
    func work(b: @escaping () -> ()) {
        DispatchQueue.main.async {
           print("in C")
           b()
        }
    }
}

// 而這樣是無法編譯通過的:
class C1: P {
    func work(b: () -> ()) {
        // ...
    }
}

七、Optional Chaining

  • 在使用可選鏈的時候, 可以讓我們擺脫很多不必要的判斷和取值
  • 比如有下面的一段代碼
class Toy {
    let name: String
    init(name: String) {
        self.name = name
    }
}

class Pet {
    var toy: Toy?
}

class Child {
    var pet: Pet?
}
  • 我們可以使用可選鏈, 調(diào)用一個孩子的寵物的玩具的名字
let xiaoming = Child()
let toyName = xiaoming.pet?.toy?.name
  • 在實際的使用中, 我們大多數(shù)情況下可能更希望使用可選綁定來直接取值:
if let toyName = xiaoming.pet?.toy?.name {
    // 小明有寵物, 并且寵物擁有玩具
}
  • 上面的代碼是對屬性進行可選判斷, 除此之外, 還可以對方法進行可選綁定, 判斷方法是否調(diào)用成功

  • Toy定義一個擴展

extension Toy {
    func play() {
        // 寵物玩玩具
    }
}
  • 還是使用上面的例子, 如果孩子有玩具, 就玩
xiaoming.pet?.toy?.play()
  • 我們知道沒有返回值的方法具有隱式的返回類型Void, 等價于(), 或者說空的元組

  • 所以我們可以使用可選鏈來判斷, 孩子的寵物是否真的調(diào)用play()方法

if let result: () = xiaoming.pet?.toy?.play() {
    // 玩玩具
}else{
    // 沒有玩具可以玩
}
  • 除了例子中的小明, 可能還會有小紅, 小李等其他孩子, 這個時候如果每個孩子都判斷他們各自的寵物是否玩玩具, 就需要分別寫
if let result: () = xiaohong.pet?.toy?.play() {
    // 玩玩具
}else{
    // 沒有玩具可以玩
}

if let result: () = xiaoli.pet?.toy?.play() {
    // 玩玩具
}else{
    // 沒有玩具可以玩
}
  • 這個時候我們就會想著將xiaoming.pet?.toy?.play()這一段代碼抽象出來, 做成一個閉包方便使用。傳入一個Child對象, 如果小朋友有寵物并且寵物有玩具的話, 就去玩, 于是很可能你會寫出這樣的代碼:

這是錯誤的代碼

let playClosure = { (child: Child) -> () in
    child.pet?.toy?.play()
}
  • 這樣的代碼是沒有意義的, 因為這段代碼等價于
let playClosure = { (child: Child) -> () in
    child.pet?.toy?.play()
   return ()
}
  • 我們需要將child.pet?.toy?.play()的返回值做為閉包playClosure的返回值, 所以我們需要如下代碼
let playClosure = { (child: Child) -> ()? in
    child.pet?.toy?.play()
}
  • 上面的代碼, 有一個隱式的return, 實際代碼如下
let playClosure = { (child: Child) -> ()? in
    return child.pet?.toy?.play()
}
  • 這時我們可以使用可選綁定來判斷方法是否調(diào)用成功
if let toyName = playClosure(xiaoming) {
    print("玩玩具~")
}else {
    print("沒有玩具玩~")
}

if let toyName = playClosure(xiaohong) {
    print("玩玩具~")
}else {
    print("沒有玩具玩~")
}

if let toyName = playClosure(xiaoli) {
    print("玩玩具~")
}else {
    print("沒有玩具玩~")
}

八、操作符

  • 與Objective-C不同, Swift支持重載操作符這樣的特性, 最常見的使用方式可能就是定義一些簡便的計算

  • 比如我們需要定義一個表示二位向量的數(shù)據(jù)結(jié)構(gòu)

struct Vector2D {
    var x = 0.0
    var y = 0.0
}
  • 一個很簡單的需求是像個Vector2D相加
let v1 = Vector2D(x: 1, y: 2)
let v2 = Vector2D(x: 3, y: 4)
let v3 = Vector2D(x: v1.x + v2.x, y: v1.y + v2.y)
// v3為 Vector2D(x: 4.0, y: 6.0)
  • 這樣的計算很簡單, 如果只做一次兩次很不錯, 但是一般情況下我們會進行很多這樣的操作, 這樣的話, 我們可能更愿意定義一個Vector2D相加的操作, 讓代碼簡化清晰
  • 對于兩個向量的相加, 我們可以重載加號(+)操作符:
func + (left: Vector2D, right: Vector2D) -> Vector2D {
    return Vector2D(x: left.x + right.x, y: left.y + right.y)
}
  • 這樣, 上面的v3以及之后的所有表示兩個向量相加的操作就全部可以用加號來表示了
let v4 = v1 + v2
// v4為 Vector2D(x: 4.0, y: 6.0)
  • 類似的, 我們還可以為Vector2D定義像-(減號, 兩個向量相減), -(負號, 單個向量x和y同時取負)等等這樣的運算符
// - 減號
func - (left: Vector2D, right: Vector2D) -> Vector2D {
    return Vector2D(x: left.x - right.x, y: left.y - right.y)
}

// - 負號
prefix func - (vector: Vector2D) -> Vector2D {
    return Vector2D(x: -vector.x, y: -vector.y)
}
  • 調(diào)用時, 直接使用操作符即可
let v5 = v1 - v2
print(v5)
// v5 為 Vector2D(x: -2.0, y: -2.0)

let v6 = -v1
print(v6)
// v6 為 Vector2D(x: -1.0, y: -2.0)
  • 上面的加號, 減號和負號都是已經(jīng)存在于Swift中的運算符了, 我們所做的只是變換他的參數(shù)進行重載
  • 如果我們想要定義一個全新的運算符的話, 要做的事會多一件
  • 比如點積運算就是一個矢量運算中很常用的運算符, 它表示兩個向量對應(yīng)坐標的乘積的和。
  • 根據(jù)定義以及參考重載運算符的方法, 我們選取+*來表示這個運算的話, 不難寫出:
func +* (letf: Vector2D, right: Vector2D) -> Double {
    return letf.x * right.x + letf.y * right.y
}
  • 但是編譯器會給我們一個錯誤:

Operator implementation without matching operator declaration

  • 這是因為+*與之前的+, -等操作符不同, +, -這樣的操作符是Swift中已經(jīng)有定義聲明的, 如果我們想要添加新的操作符的話, 需要先對其進行生命, 告訴編譯器這個符號其實是一個操作符。添加如下代碼:
precedencegroup DotProductPrecedence {
    associativity: none
    higherThan: MultiplicationPrecedence
}

infix operator +*: DotProductPrecedence

precedencegroup

定義一個操作符優(yōu)先級別。操作符優(yōu)先級的定義和類型聲明有些類似, 一個操作符需要屬于某個特定的優(yōu)先級。Swift標準庫中已經(jīng)定義了一些常用的運算優(yōu)先級組, 比如加法優(yōu)先級組(AdditionPrecedence)和乘法優(yōu)先級組(MultiplicationPrecedence)等。如果沒有合適你的運算符的優(yōu)先級組, 你就需要向我們在例子中做的這樣, 自己指定結(jié)合律方式和優(yōu)先級順序了

associativity

定義了結(jié)合律, 即多個同類的操作符順序出現(xiàn)時的計算順序。比較常見的加法和減法都是left, 就是說多個加法同時出現(xiàn)時按照從左往右的順序計算。
點乘的結(jié)果是一個Double, 不會再和其他點乘結(jié)合使用, 所以這里是none

higherThan

運算的優(yōu)先級, 點積運算是優(yōu)先于乘法運算的。除了higherThan, 也支持使用loverThan來指定優(yōu)先級低于某個其他組。

infix

表示要定義的是一個中位操作符, 即前后都是輸入; 其他的修飾子還包括prefixpostfix

  • 有了這些之后, 點積運算符的定義如下
precedencegroup DotProductPrecedence {
    associativity: none
    higherThan: MultiplicationPrecedence
}

infix operator +*: DotProductPrecedence

func +* (letf: Vector2D, right: Vector2D) -> Double {
    return letf.x * right.x + letf.y * right.y
}
  • 我們可以很簡單地進行向量的點擊運算了:
let result = 3 * 5 + v1 +* v2
print(result)
// 打印: 26.0

最后需要多提一點的是,Swift 的操作符是不能定義在局部域中的,因為至少會希望在能在全局范圍使用你的操作符,否則操作符也就失去意義了。另外,來自不同 module 的操作符是有可能沖突的,這對于庫開發(fā)者來說是需要特別注意的地方。如果庫中的操作符沖突的話,使用者是無法像解決類型名沖突那樣通過指定庫名字來進行調(diào)用的。因此在重載或者自定義操作符時,應(yīng)當(dāng)盡量將其作為其他某個方法的 "簡便寫法",而避免在其中實現(xiàn)大量邏輯或者提供獨一無二的功能。這樣即使出現(xiàn)了沖突,使用者也還可以通過方法名調(diào)用的方式使用你的庫。運算符的命名也應(yīng)當(dāng)盡量明了,避免歧義和可能的誤解。因為一個不被公認的操作符是存在沖突風(fēng)險和理解難度的,所以我們不應(yīng)該濫用這個特性。在使用重載或者自定義操作符時,請先再三權(quán)衡斟酌,你或者你的用戶是否真的需要這個操作符。

九、func的參數(shù)修飾

  • 在聲明一個Swift 的方法的時候, 我們一般不去指定參數(shù)前面的修飾符, 而是直接聲明參數(shù)
func incrementor(variable: Int) -> Int {
    return variable + 1
}
  • 如果我們想要對增加后的變量做點什么,又不想引入一個新的變量的話,很可能會寫出這樣的代碼:

這是錯誤的代碼

func incrementor(variable: Int) -> Int {
   variable += 1
   print(variable)
   return variable
}
  • 之所以會有錯誤, 是因為Swift是一門討厭變化的語言, 所有有可能的地方, 都被默認認為是不可變的, 也就是用let進行聲明的
  • 現(xiàn)在我們?nèi)绻胫辉诤瘮?shù)內(nèi)部對這樣的輸入值進行修改的話, 只能顯示地在函數(shù)內(nèi)部進行使用var進行賦值以后再操作了:
func incrementor2(variable: Int) -> Int {
    var num = variable
    num += 1
    return num
}
  • 有些時候我們會希望在方法內(nèi)部直接修改輸入的值, 這時候我們可以使用inout來對參數(shù)進行修飾
func incrementor(variable: inout Int) {
    variable += 1
}
  • 因為在函數(shù)內(nèi)部就更改了值,所以也不需要返回了。調(diào)用也要改變?yōu)橄鄳?yīng)的形式,在前面加上 & 符號:
var luckyNumber = 7
incrementor(variable: &luckyNumber)

print(luckyNumber)
// luckyNumber = 8
  • 最后,要注意的是參數(shù)的修飾是具有傳遞限制的,就是說對于跨越層級的調(diào)用,我們需要保證同一參數(shù)的修飾是統(tǒng)一的。舉個例子,比如我們想擴展一下上面的方法,實現(xiàn)一個可以累加任意數(shù)字的 +N器 的話,可以寫成這樣:
func makeIncrementor(addNumber: Int) -> ((inout Int) -> ()) {
    func incrementor(variable: inout Int) -> () {
        variable += addNumber;
    }
    return incrementor;
}
  • 外層的 makeIncrementor 的返回里也需要在參數(shù)的類型前面明確指出修飾詞,以符合內(nèi)部的定義,否則將無法編譯通過。

十、字面量表達

1、字面量
  • 所謂字面量, 就是指像特定的數(shù)字, 字符串或者是布爾值這樣, 能夠直截了當(dāng)?shù)闹赋鲎约旱念愋筒樽兞窟M行賦值的值
  • 比如在下面中的3hello world以及true就稱為字面量
let aNumber = 3
let aString = "hello world"
let aBool = true
  • 在Swift中, ArrayDictionary在使用簡單的描述賦值的時候, 使用的也是字面量, 比如:
let anArray = [1, 2, 3]
let aDictionary = ["key1" : "value1", "key2" : "vakue2"]

2、使用字面量創(chuàng)建指定類型

  • Swift為我們提供了一組協(xié)議, 使用字面量表達特定的類型
  • 這些協(xié)議包括了各個原生的字面量, 在開發(fā)中可能經(jīng)常用的有以下幾種:
ExpressibleByArrayLiteral
ExpressibleByBooleanLiteral
ExpressibleByDictionaryLiteral
ExpressibleByFloatLiteral
ExpressibleByNilLiteral
ExpressibleByIntegerLiteral
ExpressibleByStringLiteral
  • 對于實現(xiàn)了上面協(xié)議的類型, 在提供字面量賦值的時候, 就可以簡單地按照協(xié)議方法中定義的規(guī)則"無縫對應(yīng)"的通過賦值的方式將值表達為對應(yīng)類型
  • 所有的字面量表達協(xié)議都定義了一個 typealias 和對應(yīng)的 init 方法。拿 ExpressibleByBooleanLiteral 舉個例子:
public protocol ExpressibleByBooleanLiteral {

    associatedtype BooleanLiteralType : _ExpressibleByBuiltinBooleanLiteral

    public init(booleanLiteral value: Self.BooleanLiteralType)
}
  • 在這個協(xié)議中, Swift的標準庫已經(jīng)定義了BooleanLiteralType的類型是Bool, 如果在類型中將BooleanLiteralType設(shè)置為其他類型, 就會報錯
/// The default type for an otherwise-unconstrained boolean literal
typealias BooleanLiteralType = Bool
  • 舉一個例子: 我們可以自定義Int的分類, 并遵守ExpressibleByBooleanLiteral協(xié)議
extension Int: ExpressibleByBooleanLiteral {
    public typealias BooleanLiteralType = Bool
    
    public init(booleanLiteral value: Int.BooleanLiteralType) {
        self = value ? 1 : 0
    }
}
  • 這時, 我們可以給一個整形變量賦值true:
let num1: Int = true
let num2: Int = false
print(num1, num2)        // 打印: 1 0
3、ExpressibleByStringLiteral
  • ExpressibleByStringLiteral協(xié)議稍微有些不同, 情況要復(fù)雜一些。
  • 如果類型遵守ExpressibleByStringLiteral協(xié)議, 那么同時必須遵守下面的兩個協(xié)議
ExpressibleByExtendedGraphemeClusterLiteral
ExpressibleByUnicodeScalarLiteral
  • 這兩個協(xié)議我們在日常項目中基本上不會使用,它們對應(yīng)字符簇和字符的字面量表達。雖然復(fù)雜一些,但是形式上還是一致的,只不過在實現(xiàn) ExpressibleByStringLiteral 時我們需要將這三個 init 方法都進行實現(xiàn)。

  • 還是以例子來說明,比如我們有個 Person 類,里面有這個人的名字:

class Person {
    let name: String
    init(name value: String) {
        self.name = value
    }
}
  • 如果想要通過 String 賦值來生成 Person 對象的話,可以改寫這個類:
class Person: ExpressibleByStringLiteral {
    let name: String
    init(name value: String) {
        self.name = value
    }

    required init(stringLiteral value: String) {
        self.name = value
    }

    required init(extendedGraphemeClusterLiteral value: String) {
        self.name = value
    }

    required init(unicodeScalarLiteral value: String) {
        self.name = value
    }
}
  • 在所有的協(xié)議定義的 init 前面我們都加上了 required 關(guān)鍵字,這是由初始化方法的完備性需求所決定的,這個類的子類都需要保證能夠做類似的字面量表達,以確保類型安全。

注意: 如果是結(jié)構(gòu)體和枚舉, 就不需要加required, 這是因為值類型無法被繼承, 就不會有子類

  • 在上面的例子里有很多重復(fù)的對 self.name 賦值的代碼,這是我們所不樂見的。一個改善的方式是在這些初始化方法中去調(diào)用原來的 init(name value: String),這種情況下我們需要在這些初始化方法前加上 convenience
class Person: ExpressibleByStringLiteral {
    let name: String
    init(name value: String) {
        self.name = value
    }

    required convenience init(stringLiteral value: String) {
        self.init(name: value)
    }

    required convenience init(extendedGraphemeClusterLiteral value: String) {
        self.init(name: value)
    }

    required convenience init(unicodeScalarLiteral value: String) {
        self.init(name: value)
    }
}

let p: Person = "xiaoMing"
print(p.name)

// 輸出:
// xiaoMing
  • 上面的 Person 的例子中,我們沒有像 Int 中做的那樣,使用一個 extension 的方式來擴展類使其可以用字面量賦值,這是因為在 extension 中,我們是不能定義 required 的初始化方法的。也就是說,我們無法為現(xiàn)有的非 finalclass 添加字面量表達
總結(jié)一下,字面量表達是一個很強大的特性,使用得當(dāng)?shù)脑拰s短代碼和清晰表意都很有幫助;但是這同時又是一個比較隱蔽的特性:因為你的代碼并沒有顯式的賦值或者初始化,所以可能會給人造成迷惑:比如上面例子中為什么一個字符串能被賦值為 Person?你的同事在閱讀代碼的時候可能不得不去尋找這些負責(zé)字面量表達的代碼進行查看 (而如果代碼庫很大的話,這不是一件容易的事情,因為你沒有辦法對字面量賦值進行 Cmd + 單擊跳轉(zhuǎn))。

來自: 王巍 (onevcat). “Swifter - Swift 必備 Tips (第四版)。”

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,182評論 6 543
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,489評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,290評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,776評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,510評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,866評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,860評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,036評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,585評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,331評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,536評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,058評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,754評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,154評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,469評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,273評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,505評論 2 379

推薦閱讀更多精彩內(nèi)容