第六章——函式(計算屬性和下標指令碼)

bestswifter發表於2017-12-14

本文系閱讀閱讀原章節後總結概括得出。由於需要我進行一定的概括提煉,如有不當之處歡迎讀者斧正。如果你對內容有任何疑問,歡迎共同交流討論。

除了普通的方法,還有兩種特殊的方法:計算屬性和下標指令碼。計算屬性和普通的屬性看上去類似,但它不會佔用記憶體空間來儲存某些內容,而是每次被訪問時進行動態的計算。下標指令碼本質上也是方法,只是它的定義和呼叫與普通的方法不同。舉個例子,下面是一個表示檔案的結構體,它有一個方法用於計算檔案大小:

struct File {
let path: String  // 檔案路徑

func computeSize() -> Int {
var size = 0
// 這裡有一些複雜的I/O和計算
return size
}
}
複製程式碼

computeSize可能是一個非常耗時的方法,因為它有可能會遞迴呼叫子目錄的方法。這時候我們可以考慮對計算結果做一個快取,也就是用一個私有屬性來儲存computeSize的返回值。因為要在結構體的方法中修改結構體的屬性,所以需要把方法標記為mutating

private var cachedSize: Int?

mutating func computeSize() -> Int {
guard cachedSize == nil else { return cachedSize! }
// 如果快取不存在,那麼再執行下面的程式碼
var size = 0
// 這裡有一些複雜的I/O和計算
cachedSize = size  // 新增到快取中
return size
}
複製程式碼

這種延遲載入+資料快取的模式在Swift中非常常見,遇到這種問題時,更好的解決辦法是使用lazy關鍵字:

lazy var size: Int? = {
var size: Int? = 0
// 這裡有一些複雜的I/O和計算
return size
}()
複製程式碼

標記為lazy的屬性是可變的,所以它必須被宣告成var。它被定義成一個閉包,當屬性第一次被訪問時,會立即執行這個閉包(這就是為什麼閉包的結尾有一對括號),閉包的返回值會賦值給這個屬性。

不過,上面兩種方法都有一個缺點:結構體變數必須定義成var。因為它的內部有mutating方法或lazy成員。

如果我們不希望做快取,又想通過屬性而不是函式來獲取檔案大小,我們可以把這個屬性定義為計算屬性,這樣每次它被訪問時,值都會被重新計算:

var size: Int? {
var size: Int? = 0
// 這裡有一些複雜的I/O和計算
return size
}
複製程式碼

這裡我們實現的是計算屬性的get方法,如果還想實現它的set方法,則需要把它與get方法分開實現:

var data: NSData? {
get {
return nil
}
set {
data = newValue
}
}
複製程式碼

不管是普通屬性,還是計算屬性,我們都可以實現它的didSetwillSet回撥函式,這兩個回撥函式分別在set方法呼叫前、後被呼叫,一個常用的場景是在IBOutlet被連線後進行一些初始化設定:

class ViewController: UIViewController {
@IBOutlet weak var label: UILabel? {
didSet {
label?.textColor = .blackColor()
}
}
}
複製程式碼

過載下標指令碼

你一定已經見過下標指令碼的使用:字典通過下標指令碼查詢元素。下標指令碼也是函式,只不過是語法比較怪異。下標指令碼可以是隻讀的(使用get),也可以是可讀可寫的(使用getset)。和普通函式一樣,下標指令碼也可以過載,它可以有不同型別的引數,這一點我們在使用陣列切片時也曾見過:

let fibs = [0,1,1,2,3,5]
let first = fibs[0] // 下標指令碼的引數是Int型別
let nums = fibs[1..<3]  // 下標指令碼的引數是Range<Int>型別
print(nums)  // 結果是: [1,1]
複製程式碼

在Swift中,Range型別表示一段有界的區間:每個Range變數都有開始位置和結束位置。我們還可以擴充一下集合型別,使它的下標指令碼支援開始位置和結束位置只有一個確定的半有界區間,首先定義兩個新的結構體:

struct RangeStart<I: ForwardIndexType> {
let start: I
}

struct RangeEnd<I: ForwardIndexType> {
let end: I
}
複製程式碼

我們可以定義兩個便捷運算子來表示半確定區間,它們是單目運算子,一個是字首運算子,一個是字尾運算子。這樣一來,只知道開始位置的半確定區間可以表示為x..<,只知道結束位置的半確定區間可以表示為..<x

postfix operator ..< {}
postfix func ..<<I: ForwardIndexType>(lhs: I) -> RangeStart<I> {
return RangeStart(start: lhs)
}

prefix operator ..< {}
prefix func ..<<I: ForwardIndexType>(rhs: I) -> RangeEnd<I> {
return RangeEnd(end: rhs)
}
複製程式碼

做好這些準備工作之後,就可以過載集合型別的下標指令碼了:

extension CollectionType {
subscript(r: RangeStart<Self.Index>) -> SubSequence {
return self[r.start..<endIndex]
}

subscript(r: RangeEnd<Self.Index>) -> SubSequence {
return self[startIndex..<r.end]
}
}
複製程式碼

我們來測試一下:

let fibs = [0,1,1,2,3,5]
print(fibs[2..<])  // 輸出結果:[1, 2, 3, 5]
print(fibs[..<4])  // 輸出結果:[0, 1, 1, 2]
複製程式碼

除此以外,半確定區間還可以用於實現一個搜尋函式,用於進行模式匹配,它可以查詢某個子集在集合中第一次出現的位置:

extension CollectionType
where Generator.Element: Equatable, SubSequence.Generator.Element == Generator.Element {
func search<S: SequenceType where S.Generator.Element == Generator.Element>
(pattern: S) -> Self.Index? {
return self.indices.indexOf {
//這裡的每一個$0,都是集合的一個下標,所以self[$0..<]是一個逐漸縮短的字串
self[$0..<].startsWith(pattern)  //如果返回true說明pattern第一次出現的位置就是$0
}
}
}
複製程式碼

我們通過search方法查詢字串中某個字串第一次出現的位置,基於這個位置獲得從字串的開端到這個位置之間所有的字元:

let greeting = "Hello, world"
if let index = greeting.characters.search(", ".characters) {
print(String(greeting.characters[..<index]))
}
複製程式碼

這個方法可以理解為低效、簡單版的KMP演算法。

下標指令碼進階

下標指令碼不僅可以過載(接收不同型別的引數),還可以接收多個引數(準確的說這也是過載),這一點與函式完全相同。我們可以擴充字典型別,使它的下標指令碼具有預設返回值,也就是如果沒有找到key,則返回這個預設值。在下標指令碼的set方法中則無需這個預設值,因為newValue不可能是可選型別的:

extension Dictionary {
subscript(key: Key, or or: Value) -> Value {
get {
return self[key] ?? or  // 使用空合運算子,如果self[key]為nil則返回or
}

set {
self[key] = newValue
}
}
}
複製程式碼

這樣的下標指令碼可以簡化不少程式碼。比如我們實現一個函式,統計陣列中所有元素出現的頻率,統計結果用字典表示,鍵就是元素,值是元素出現的次數:

extension SequenceType where Generator.Element: Hashable {
func frequencies() -> [Generator.Element:Int] {
var result: [Generator.Element:Int] = [:]
for x in self {
result[x, or: 0]++
}
return result
}
}
複製程式碼

這段程式碼最核心、也是最巧妙的地方在於result[x, or: 0]++這句,如果你認為當result[x, or: 0] = 0時,這句話等價於0++,那麼你會發現這完全無法在字典中新增一條鍵值對。事實上,它的實現如下:

result[x, or: 0] = result[x, or: 0] + 1
複製程式碼

也就是說我們會先後呼叫到下標指令碼的setget。你可以通過斷點自己體會一番。

測試一下:

var array = [100,200,100,300,200,200]
print(array.frequencies())   // 輸出結果:[100: 2, 200: 3, 300: 1]
複製程式碼

相關文章