《Advanced Swift》第四章 函數:讀書筆記 一、綜述 二、函數的靈活性 三、函數作爲代理 四、inout參數和可變方法 五、屬性 六、下標 七、鍵路徑 八、自動閉包

一、綜述

        在Swift中函數是一等對象——所有能用變量的地方都可以用函數!,其具有以下特徵:

  • 函數可以像 Int 或者 String 那樣被賦值給變量,也可以作爲另一個函數的輸入參數,或者另一個函數的返回值來使用
  • 函數能夠捕獲存在於其局部作用域之外的變量
  • 有兩種方法可以創建函數,一種是使用 func 關鍵字,另一種是 { }。在 Swift 中,後一種被稱爲閉包表達式
            在通常意義上來說,函數(類似於C語言中的函數)、方法(從屬於某類型的函數)、閉包表達式(使用閉包語法的函數)可以認爲是一種的東西。

1.1 函數可以被賦值給變量,也能夠作爲函數的輸入和輸出

        理解這一點就像理解C語言中的指針一樣重要。

// 定義一個簡單的函數
func printInt(i: Int) {
  print("You passed \(i).")
}

// 把函數複製給其他變量
let funVar = printInt

// 使用變量調用函數,此時不需要傳遞參數標籤
funVar(2) // You passed 2.

// 此時需要傳遞參數標籤
printInt(i:3)

// 函數作爲參數
func useFunction(function: (Int) -> () ) {
  function(3)
}
// 調用參數是函數的函數
useFunction(function: printInt) // You passed 3.
useFunction(function: funVar) // You passed 3.

// 函數作爲返回值
func returnFunc() -> (Int) -> String {
  func innerFunc(i: Int) -> String {
    return "you passed \(i)"
  }
  return innerFunc
}

// 使用函數作爲變量的返回值
let myFunc = returnFunc()
myFunc(3) // you passed 3

1.2 函數可以捕獲存在於它們作用域之外的變量

        當函數引用了在其作用域之外的變量時,這個變量就被捕獲了,它們將會繼續存在,而不是在超過作用域後被摧毀。

func counterFunc() -> (Int) -> String { 
  var counter = 0
  func innerFunc(i: Int) -> String {
    counter += i // counter 被捕獲
    return "Running total: \(counter)"
  }
  return innerFunc
}

let f = counterFunc()
f(3) // Running total: 3
f(4) // Running total: 7

// 再次調用counterFunc時捕獲一個新的變量
// 也就是說每次調用counterFunc函數,就會捕獲一個新的counter
let g = counterFunc()
g(2) // Running total: 2
g(2) // Running total: 4

// 一個函數和它所捕獲的變量環境組合起來被稱爲閉包

1.3 函數可以使用 { } 來聲明爲閉包表達式

func doubler(i: Int) -> Int {
  returni*2
}
[1, 2, 3, 4].map(doubler) // [2, 4, 6, 8]

// 使用閉包表達式的語法來寫相同的函數,像之前那樣將它傳給 map:
let doublerAlt = { (i: Int) -> Int in return i*2 }
[1, 2, 3, 4].map(doublerAlt) // [2, 4, 6, 8]


// 閉包表達式的省略語法
[1, 2, 3].map( { (i: Int) -> Int in return i * 2 } )
[1, 2, 3].map( { i in return i * 2 } )
[1, 2, 3].map( { i in i * 2 } )
[1, 2, 3].map( { $0 * 2 } )
[1, 2, 3].map() { $0 * 2 }
[1,2,3].map{$0*2}


func isEven<T: BinaryInteger>(_ i: T) -> Bool {
  return i%2 == 0
}
// 要把這個全局函數賦值給變量的話,你需要先決定它的參數類型
// 變量不能持有泛型函數,它只能持有一個類型具體化之後的版本:
let int8isEven: (Int8) -> Bool = isEven

二、函數的靈活性

  • 這裏是使用OC的排序功能對Swift語言中定義的類進行排序的代碼
@objcMembers
final class Person: NSObject {
    let first: String
    let last: String
    let yearOfBirth: Int
    
    init(first: String, last: String, yearOfBirth: Int) {
        self.first = first
        self.last = last
        self.yearOfBirth = yearOfBirth
        // super.init() 在這裏被隱式調用
    }
}

let people = [
    Person(first: "Emily", last: "Young", yearOfBirth: 2002),
    Person(first: "David", last: "Gray", yearOfBirth: 1991),
    Person(first: "Robert", last: "Barnes", yearOfBirth: 1985),
    Person(first: "Ava", last: "Barnes", yearOfBirth: 2000),
    Person(first: "Joanne", last: "Miller", yearOfBirth: 1994),
    Person(first: "Ava", last: "Barnes", yearOfBirth: 1998),
]

let lastDescriptor = NSSortDescriptor(key: #keyPath(Person.last), ascending: true, selector: #selector(NSString.localizedStandardCompare(_:)))
let firstDescriptor = NSSortDescriptor(key: #keyPath(Person.first), ascending: true, selector: #selector(NSString.localizedStandardCompare(_:)))
let yearDescriptor = NSSortDescriptor(key: #keyPath(Person.yearOfBirth), ascending: true)


let descriptors = [lastDescriptor, firstDescriptor, yearDescriptor]
(people as NSArray).sortedArray(using: descriptors)

  • 以下代碼示例,說明了,如何把上述功能進行深度的Swift翻譯
// 用 localizedStandardCompare 來排序
var strings = ["Hello", "hallo", "Hallo", "hello"]
strings.sort { $0.localizedStandardCompare($1) == .orderedAscending} strings // ["hallo", "Hallo", "hello", "Hello"]

// 如果只是想用對象的某一個屬性進行排序的話,也非常簡單:
people.sorted { $0.yearOfBirth < $1.yearOfBirth }

// 基於多個屬性進行排序
people.sorted { p0, p1 in
let left = [p0.last, p0.first]
let right = [p1.last, p1.first]
return left.lexicographicallyPrecedes(right) {
$0.localizedStandardCompare($1) == .orderedAscending }
}
/*
[Ava Barnes (2000), Ava Barnes (1998), Robert Barnes (1985),
David Gray (1991), Joanne Miller (1994), Emily Young (2002)] */

  • 在Swift中另外一種實現
// 讓我們先定義一個泛型別名來表達這 種函數形式的排序描述符:
typealias SortDescriptor<Root> = (Root, Root) -> Bool

// 實際的排序方法
let sortByYear: SortDescriptor<Person> = { $0.yearOfBirth < $1.yearOfBirth }
let sortByLastName: SortDescriptor<Person> = {$0.last.localizedStandardCompare($1.last) == .orderedAscending }

// 對上面方法的進一步改造
/// `key` 函數,根據輸入的參數返回要進行比較的元素
/// `by` 進行比較的斷言
/// 通過用 `by` 比較 `key` 返回值的方式構建 `SortDescriptor` 函數
func sortDescriptor<Root, Value>(
key: @escaping (Root) -> Value,
by areInIncreasingOrder: @escaping (Value, Value) -> Bool) -> SortDescriptor<Root>
{
  return { areInIncreasingOrder(key($0), key($1)) }
}

// 實際使用
let sortByYearAlt: SortDescriptor<Person> = sortDescriptor(key: { $0.yearOfBirth }, by: <)
people.sorted(by: sortByYearAlt)

三、函數作爲代理

3.1 Cocoa 風格的代理

        此風格的代理與OC中常見的代理處理方式一致,示例如下:

protocol AlertViewDelegate: AnyObject {
  func buttonTapped(atIndex: Int)
}

class AlertView {
  var buttons: [String]
  weak var delegate: AlertViewDelegate?
  init(buttons: [String] = ["OK", "Cancel"]) {
    self.buttons = buttons
  }
  func fire() {
    delegate?.buttonTapped(atIndex: 1)
  }
}

class ViewController: AlertViewDelegate {
  let alert: AlertView
  init() {
    alert = AlertView(buttons: ["OK", "Cancel"]) alert.delegate = self
  }
  func buttonTapped(atIndex index: Int) {
    print("Button tapped: \(index)")
  }
}

3.2 結構體上實現代理

        在上面的實例中講述了,通過類來實現委託。但是在Swift中委託一般不能用來實現代理,具體示例如下:

// 進行如下修改,使協議能夠被結構體實現
protocol AlertViewDelegate {
  mutating func buttonTapped(atIndex: Int)
}

// AlertView沒有變化
class AlertView {
  var buttons: [String]
  var delegate: AlertViewDelegate?
  init(buttons: [String] = ["OK", "Cancel"]) {
    self.buttons = buttons
  }
  func fire() {
    delegate?.buttonTapped(atIndex: 1)
  }
}

// 結構體實現了此委託協議
struct TapLogger: AlertViewDelegate {
  var taps: [Int] = []
  mutating func buttonTapped(atIndex index: Int) {
    taps.append(index) }
 }

// 不過,如果我們在事件被觸發後再檢查 logger.taps,會發現數組依然爲空:
let alert = AlertView()
var logger = TapLogger()
alert.delegate = logger
alert.fire()
logger.taps // []

// 原因是執行alert.delegate = logger此語句時發生了值的複製
// 所以alert.fire()之後調用的委託對象不是logger

3.3 使用函數,而非代理

        如果代理協議中只定義了一個函數的話,我們完全可以用一個存儲回調函數的屬性來替換原來的代理屬性。

class AlertView {
  var buttons: [String]
  var buttonTapped: ((_ buttonIndex: Int) -> ())?
  init(buttons: [String] = ["OK", "Cancel"]) {
    self.buttons = buttons
  }
  func fire() {
    buttonTapped?(1)
  }
}


struct TapLogger {
  var taps: [Int] = []
  mutating func logTap(index: Int) {
    taps.append(index)
  }
}


let alert = AlertView()
var logger = TapLogger()

// 這句話導致編譯錯誤:這個賦值的結果不明確。是 logger 需要複製一份呢,還是 buttonTapped 需要改變它原來的狀態 (即 logger 被捕獲) 呢?
alert.buttonTapped = logger.logTap

// 通過如下的方法解決
alert.buttonTapped = { logger.logTap(index: $0) }

class ViewController {
  let alert: AlertView
  init() {
  alert = AlertView(buttons: ["OK", "Cancel"])
  // 此條語句導致循環引用
  // 所有指向某個對象的實例方法的引用 (比如這個例子中 的 self.buttonTapped) 都會在背後捕獲這個對象。
  alert.buttonTapped = self.buttonTapped(atIndex:)
}
func buttonTapped(atIndex index: Int) {
  print("Button tapped: \(index)")
  }
}


// 上面循環引用可以進行如下修改
alert.buttonTapped = { [weak self] index in
  self?.buttonTapped(atIndex: index)
}

四、inout參數和可變方法

        如果你有些 C 或 C++ 的背景,Swift 中用在 inout 參數前面的&可能會給你一種這是在傳遞引用的錯覺。但事實並非如此,inout 做的事情是傳值,然後複製回來,並不是傳遞引用。
        一個 inout 參數持有一個傳遞給函數的值,函數可以改變這個值,然後從函數中傳出 並替換掉原來的值。
        爲了瞭解什麼樣的表達式可以作爲 inout 參數,我們需要區分 lvalue 和 rvalue。lvalue 描述的 是一個內存地址,它是 “左值 (left value)” 的縮寫,因爲 lvalues 是可以存在於賦值語句左側的 表達式。舉例來說,array[0] 是一個 lvalue,因爲它描述了數組中第一個元素所在的內存位置。而 rvalue 描述的是一個值。2 + 2 是一個 rvalue,它描述的是 4 這個值。你不能把 2 + 2 或者 4 放到賦值語句的左側。
        對於 inout 參數,你只能傳遞左值,因爲右值是不能被修改的。當你在普通的函數或者方法中 使用 inout 時,需要顯式地將它們傳入:即在每個左值前面加上 & 符號。例如,當調用 increment 時 (它有一個 inout int 參數),我們就要在傳入的變量前添加 &。
        編譯器可能會把 inout 變量優化成引用傳遞,而非傳入和傳出時的複製。不過,文檔已經明確指出我們不應該依賴這個行爲。
        某些運算符,如自增運算符,需要接受一個inout參數,但是在調用的時候不需要顯式使用&。

func incrementTenTimes(value: inout Int) {

func inc() {
  value += 1
}
  for _ in 0..<10 {
    inc()
  }
}

var x=0 
ncrementTenTimes(value: &x) x // 10

// 不過,你不能夠讓這個 inout 參數逃逸
func escapeIncrement(value: inout Int) -> () -> () {
  func inc() {
    value += 1
  }
  // error: 嵌套函數不能捕獲 inout 參數然後讓其逃逸
  return inc
}

        說到不安全 (unsafe) 的函數,你應該小心 & 的另一種含義:把一個函數參數轉換爲一個不安全指針。
        如果一個函數接受 UnsafeMutablePointer 作爲參數,你可以用和 inout 參數類似的方法,在 一個 var 變量前面加上 & 傳遞給它。在這種情況下,你確實在傳遞引用,更確切地說,是在傳 遞指針。

五、屬性

        有兩種方法和其他普通的方法有所不同,那就是計算屬性和下標操作符。計算屬性看起來和常規的屬性很像,但是它並不使用任何內存來存儲自己的值。相反,這個屬性每次被訪問時,返回值都將被實時計算出來。計算屬性實際上只是一個方法,只是它的定義和調用約定不太尋常。
        屬性觀察者必須在聲明一個屬性的時候就被定義,你無法在擴展裏進行追加。所以,這不是一 個提供給類型用戶的工具,它是專門提供給類型的設計者的。willSet 和 didSet 本質上是一對 屬性的簡寫:一個是存儲值的私有存儲屬性;另一個是讀取值的公開計算屬性,這個計算屬性 的 setter 會在將值存儲到私有存儲屬性之前和/或之後,進行額外的工作。這和 Foundation 中 的鍵值觀察 (KVO,key-value observing)有本質的不同,鍵值觀察通常是對象的消費者來觀察 對象內部變化的手段,而與類的設計者是否希望如此無關。
        不過,你可以在子類中重寫一個屬性,來添加觀察者。下面就是一個例子:
        存儲屬性和需要存儲的延遲屬性不能被定義在擴展中。
        訪問一個延遲屬性是 mutating 操作,因爲這個屬性的初始值會在第一次訪問時被設置。當結 構體包含一個延遲屬性時,這個結構體的所有者如果想要訪問該延遲屬性的話,也需要將結構 體聲明爲可變量,因爲訪問這個屬性的同時,也會潛在地對這個屬性的容器進行改變。所以, 下面的代碼是不被允許的。
        讓想訪問這個延遲屬性的所有 Point 用戶都使用 var 是非常不方便的事情,所以在結構體中使 用延遲屬性通常不是一個好主意。
        另外需要注意,lazy 關鍵字不會進行任何線程同步。如果在一個延遲屬性完成計算之前,多個 線程同時嘗試訪問它的話,計算有可能進行多次,計算過程中的各種副作用也會發生多次。

六、下標

        在標準庫中,我們已經看到過一些下標用法了,例如:用 dictionary[key] 這樣的方式在字典查 找元素。這些下標很像函數和計算屬性的混合體,只不過它們使用了特殊的語法。之所以像函 數,是因爲它們也可以接受參數;之所以像計算屬性,是因爲它們要麼是隻讀的 (只提供 get), 要麼是可讀寫的 (同時提供 get 和 set)。和普通的函數類似,我們可以通過重載提供不同類型的 下標操作符。比如,數組默認有兩個下標操作,一個用來訪問單個元素,另一個用來返回一個 切片 (更精確地說,它們是被定義在 Collection 協議中的):

extension Collection {
  subscript(indices indexList: Index...) -> [Element] {
    var result: [Element] = []
    for index in indexList {
      result.append(self[index])
  }
  return result
}
var japan: [String: Any] = [
    "name": "Japan",
    "capital": "Tokyo",
    "population": 126_740_000,
    "coordinates": [
        "latitude": 35.0,
        "longitude": 139.0
    ]
]

extension Dictionary {
    subscript<Result>(key: Key, as type: Result.Type) -> Result? {
        get {
            return self[key] as? Result
        }
        set {
            // 如果傳入 nil, 就刪除現存的值。
            guard let value = newValue else {
                self[key] = nil
                return
            }
            // 如果類型不匹配,就忽略掉。
            guard let value2 = value as? Value else {
                return
            }
            self[key] = value2
        }
    }
      
}

// Any沒有下標運算符,所以報錯
// japan["coordinate"]?["latitude"] = 36.0

// 不能把值賦值給一個不可變的表達式
// (japan["coordinates"] as? [String: Double])?["coordinate"] = 36.0

japan["coordinates", as: [String: Double].self]?["latitude"] = 36.0

七、鍵路徑

        Swift 4 中添加了鍵路徑 (key paths) 的概念。鍵路徑是一個指向屬性的未調用的引用,它和對 某個方法的未使用的引用很類似。在之前,你無法像引用方法 (比如 String.uppercased) 那樣引用一個類型的屬性 (比如 String.count)。和 Objective-C 及 Foundation 中的鍵路徑相比,除了擁有共同的名字以外, Swift 中的鍵路徑有很大不同。
        鍵路徑表達式以一個反斜槓開頭,比如 \String.count。反斜槓是爲了將鍵路徑和同名的類型屬 性區分開來 (假如 String 也有一個 static count 屬性的話,String.count 返回的就會是這個屬 性值了)。類型推斷對鍵路徑也是有效的,在上下文中如果編譯器可以推斷出類型的話,你可以 將類型名省略,只留下 .count。
        "Hello"[keyPath: .count] 等效 於 "Hello".count。
        而對於可寫的鍵路徑來說,則對應着一對獲取和設置值的函數。相對於這樣 的函數,鍵路徑除了在語法上更簡潔外,最大的優勢在於它們是值。你可以測試鍵路徑是否相 等,也可以將它們用作字典的鍵 (因爲它們遵守 Hashable)。另外,不像函數,鍵路徑是不包含 狀態的,所以它也不會捕獲可變的狀態。
        鍵路徑還可以通過將一個鍵路徑附加到另一個鍵路徑的方式來生成。這麼做時,類型必須要匹 配;如果你有一個從 A 到 B 的鍵路徑,那麼你要附加的鍵路徑的根類型必須爲 B,得到的將會 是一個從 A 到 C 的鍵路徑,其中 C 是所附加的鍵路徑的值的類型:

// KeyPath<Person, String> + KeyPath<String, Int> = KeyPath<Person, Int>
let nameCountKeyPath = nameKeyPath.appending(path: \.count) // Swift.KeyPath<Person, Swift.Int>

八、自動閉包

8.1 自動閉包

        某些情況下,編譯器可以通過關鍵字@autoclosure把,代碼設置爲自動閉包,從而降低使用難度。
        在大多數語言中,&&、||都有短路求值的功能,我們寫一個and函數來展示一下自動閉包。

// 此函數只有在l爲true的情況下,纔會調用第二個參數代表的閉包
func and(_ l: Bool, _ r: () -> Bool) -> Bool {
  guard l else { return false }
  return r()
}

// 使用的時候要傳遞一個閉包
if and(!evens.isEmpty, { evens[0] > 10 }) {
  //執行操作
}

// 把第二個參數設置爲自動閉包後,可以降低使用難度
func and(_ l: Bool, _ r: @autoclosure () -> Bool) -> Bool {
  guard l else { return false }
  return r()
}

// 此時使用and函數的時候,第二個參數不需要使用閉包表達式
if and(!evens.isEmpty, evens[0] > 10) {
  //執行操作
}

注意
過度使用自動閉包可能會讓你的代碼難以理解。使用時的上下文和函數名應該清晰地指出實際求值會被推遲。

8.2 逃逸閉包

  • 逃逸閉包
    一個被保存在某個地方 (比如一個屬性中) 等待稍後再調用的閉包就叫做。
  • 非逃逸閉包
    相對的,永遠不會離開一個函數的局部作用域的閉包
  • 閉包參數默認是非逃逸的(也有許多例外情況)
  • 閉包參數標記爲 @escaping,標識爲其是逃逸閉包

8.3 withoutActuallyEscaping

        你確實知道一個閉包不會逃逸,但是編譯器無法證明這點,所以它會 強制你添加 @escaping 標註。

extension Array {
    func allSatisfy2(_ predicate: (Element) -> Bool) -> Bool {
        return withoutActuallyEscaping(predicate) { escapablePredicate in
            self.lazy.filter { !escapablePredicate($0) }.isEmpty
        }
    }
}

let areAllEven = [1,2,3,4].allSatisfy2 { $0 % 2 == 0 } // false
let areAllOneDigit = [1,2,3,4].allSatisfy2 { $0 < 10 } // true
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章