一、綜述
在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