泛型、函數類型、閉包
泛型
泛型是什么?簡單說來泛型就是泛指的類型,里面可以實例化任何你想要的類型。
比如我想寫個函數交換兩個數大小:
泛型函數
func exchange (x: inout Int, y: inout Int)
{
let a = x
x = y
y = a
}
var a = 55
var b = 22
print("a is \(a), b is \(b).")
//輸出結果:a is 55, b is 22.
exchange(x: &a, y: &b)
print("After exchange: a is \(a), b is \(b).")
//輸出結果:After exchange: a is 22, b is 55.
好,看起來很完美,可是你有沒有發現這個函數只能交換兩個整數,如果要交換小數呢?也許你會說,這不簡單,把參數類型換成 Double 就好了嘛??墒侨绻阌窒虢粨Q兩個字符串呢?這樣換來換去不是個辦法,所以泛型自然應運而生。
func exchange<T>(x: inout T, y: inout T)
{
let a = x
x = y
y = a
}
var a = "xiaoMing"
var b = "xiaoHong"
print("a is \(a), b is \(b).")
//輸出結果:a is xiaoMing, b is xiaoHong.
exchange(x: &a, y: &b)
print("After exchange: a is \(a), b is \(b).")
//輸出結果:After exchange: a is xiaoHong, b is xiaoMing.
我們把參數類型換成 T ,然后在函數名后面加一個尖括號,也寫上T ,實際上你可以寫上任何你想寫的字符,只要后面參數類型與它保持一致就行了。
一點點小改動,現在我們即可用來交換整數,又可以交換字符串啦。
現在看起來貌似很不錯了,可是如果你想比較 a 和 b 大小可能會碰到一點小問題。你如何能保證傳進去的參數支持 “<”、“>” 呢?幸好 Swift 早就想好這個問題了,所以引入了泛型約束。
泛型約束
這個怎么用,看代碼:
func max<T:Comparable>(array: [T]) -> T {
var value=array[0]
for index in 1..<array.count
{
if array[index]>value {
value=array[index]
}
}
return value
}
var data1=[1,6,3,8,5,2]
var data2=[12.3, 87.6, 77.8, 20.1, 50.2]
let m1=max(array: data1) // m1 = 8
let m2=max(array: data2) // m2 = 87.6
和泛型函數非常類似,我們只是添加了一個泛型的返回值,再讓函數名背后的泛型 T 遵循 Comparable 協議,就這么簡單?就這么簡單。
只要你傳進去的類型遵循 Comparable 協議,那你就可以隨意調用這個函數,大家可以點進去看看 Comparable 協議都實現了定義了哪些東西。這樣,只要我們想拿該類型做什么事,我們就讓他遵循什么協議就好了,如果你隨意傳個自己寫的類,那編譯器就會提示你錯了,非常方便。
- 除了協議約束,還有基類約束,二者語法一樣,不再贅述。
- Where 字句約束
func find<T:Container>(sequence :T, item:T.ItemType)->Bool where T.ItemType: Equatable{
for index in 0..<sequence.count{
if(sequence[index]==item){
return true
}
}
return false
} ```
這個奇奇怪怪的東西是什么?在了解它之前,我們得先知道協議的關聯類型。
#### 關聯類型
protocol Container {
associatedtype ItemType //關聯類型
func append(item: ItemType)
var count: Int { get }
subscript(i: Int) -> ItemType { get }
}
class Stack<T>: Container {
typealias ItemType=T
var items = [T]()
func push(item: T) {
items.append(item)
}
func pop() -> T {
return items.removeLast()
}
func append(item: T) {
self.push(item: item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> T {
return items[i]
}
} ```
哇,不要說你看不懂,我也看暈了,怎么又是 T 又是 ItemType ,這都是些什么???
不要慌,先看第一個 Container,它是一個協議。其實它的意思如下偽碼:
protocol Container<ItemType> {
func append(item: ItemType)
var count: Int { get }
subscript(i: Int) -> ItemType { get }
}
是不是有點兒眉目了,這不就是泛型協議嗎,Swift 為啥還要畫蛇添足搞出一個關聯類型出來?稍安勿躁,蘋果這么做自然有他的好處了。
參考鏈接
我簡單總結下好處:
- 讓代碼更簡潔清晰,特別是當你的協議需要定義多個泛型類型時
- 可以使用“.”表達式,方便 where 約束
好了,有了關聯類型,我們就可以像上方那樣使用 T.ItemType 的語法來用 where 約束了,是不是非常清晰明了,當你定義的 associatedtype 多了之后,好處就更加明顯了。
函數類型與閉包
基本概念
閉包是函數類型的實例,而函數類型就是一個指針,類似于類,棧上儲存指針,指向堆上的對象。
乍一看有點兒犯迷糊?不要緊,一個個來分析。
函數類型是一個指針,它在堆上有兩個對象,這兩個對象也是指針。一個叫對象指針,一個叫函數指針。
- 對象指針:這個指針并不是每個函數類型的實例都會有,對象指針,看名字就知道它指向的是一個對象,至于什么樣的函數類型擁有對象指針,咱們放到變量捕獲那兒講。
- 函數指針:這個指向的就是這個函數類型里面的函數真正內存所在地
函數類型怎么用呢?你可以將它用在任何使用類型的地方。例如: - 聲明一個變量
var addInt : (Int, Int) -> Int
現在你可以將任何參數是兩個 Int 返回值是一個 Int 的函數賦值給 addInt,就像使用一個變量去使用它,只不過現在該變量是一個函數。 - 傳遞給某個函數作為一個參數
- 將它作為某個函數的返回值
這兩種不具體寫出來了,總之當做它是一個類型就可以了。
函數類型實例化
那既然是一個類型,可以被賦值,那它支持哪些函數給它賦值呢?
- 全局函數
- 嵌套函數
//嵌套函數
func algorithmFunction(symbol:String)-> (Double, Double)->Double{
func add(x:Double, y:Double)->Double{
return x+y
}
func minus(x:Double, y:Double)->Double{
return x-y
}
func multiply(x:Double, y:Double)->Double{
return x*y
}
func divide(x:Double, y:Double)->Double{
return x/y
}
switch(symbol){
case "+":
return add
case "-":
return minus
case "*":
return multiply
case "/":
return divide
default:
return add
}
}
var algorithm=algorithmFunction(symbol: "/")
let result4 = algorithm(600,80) // result4 = 7.5
- 成員函數(實例方法與靜態方法)
閉包
閉包就是函數類型實例,一樣可用于變量、參數、返回值,他倆內存模型一致,那為啥要叫這個為閉包呢?說到這里不得不談談捕獲了,在談捕獲前,想先介紹下內存泄漏。
內存在結構上可以分為堆和棧,棧上的內存由系統分配系統回收又迅捷又方便,不用我們操心。但棧的空間是極其有限的,所以必須要有堆來存放比較龐大的數據,在 Swift 中類和閉包的實例就存放在堆上并且受到 ARC 管理。
基礎數值類型、結構、枚舉、元組等不受 ARC 管理。
ARC 又叫自動引用計數管理機制,在堆上的對象只要有一個指針指向它,它的引用計數就加一,如圖:
FileStream 此時的引用計數就為三,因為有三個指針指向它。當某個對象沒有指針引用的時候,表明此對象沒有任何利用價值了,ARC 會將引用計數降為零,并且銷毀這個對象,釋放所占內存。
好,目前看起來非常完美,perfect!可是,大家應該很快就會發現有個小問題:
這是兩個類,每個類都有一個屬性,而這個屬性剛好又指向這兩個類。真是你中有我我中有你不可分離啊,代碼大概會是這樣:
import Foundation
class Computer{
var name: String
var display: Monitor?
init(name:String){
self.name=name
print("Computer init")
}
deinit{
print("Computer deinit")
}
}
class Monitor{
var no: Int
var device: Computer?
init(no:Int){
self.no=no
print("Monitor init")
}
deinit{
print("Monitor deinit")
}
}
var imac:Computer?
var screen:Monitor?
imac=Computer(name: "Jason's iMac")
screen=Monitor(no: 29)
imac!.display=screen
screen!.device=imac
//imac!.display=nil
imac=nil
screen=nil
/* print :
Computer init
Monitor init */
?。∈遣皇巧倭它c什么,析構器怎么沒被調用?這就是經典的內存泄漏了??戳松厦娴膬却婺P?,相信大家對這段代碼為何會輸出如此結構就了解了,這兩個類對象還互相指著呢,引用計數都為一,怎么可能會被釋放呢?
還好,世界上的聰明人早就猜到會發生這種事所以,我們有了處理這種辦法的措施,那就是。。。手工賦值其中一個堆對象指針為 nil !哈,開個玩笑,其實這也是一種非常好的處理辦法,非常靈活,下面我們來看看更普適的方法。
解決辦法也著實簡單,只要我們將其中一個強引用指針換成弱引用就 OK 了,也就是說這個弱引用指針將不被算進 ARC 引用計數里去,這樣當棧上的兩個指針斷掉后,兩個對象循環引用自然被打破,如圖:
有時人們并不想該對象可被賦值為 nil,所以我們又創造了一個無主引用,和弱引用一樣,只是無主引用聲明的對象不許為 nil:
好了下面再來講捕獲。
捕獲
這里重點要講的是捕獲生存周期小于閉包對象的參數和局部變量,因為其他的值談不上捕獲,頂多算使用而已,但是這二者卻截然不同,我們是真真正正的復制了他們封裝在臨時對象上,并且閉包的對象指針指向該臨時對象。還記得前面講函數類型時我說過不是所有函數類型的實例都會有這個對象指針嗎?
有對象指針有兩種情況,一是捕獲了類的實例成員,包括實例方法和實例屬性;二就是捕獲了參數或局部變量。第一種就是將該閉包的對象指針指向該類的實例對象,第二種則是將對象指針指向閉包自己創建的臨時對象。
并且,很重要一點,這個對象指針是強引用該對象,這樣,大家可能明白又要出事情了。還是先看內存模型吧:
似乎并沒有什么不對,那我們現在思考一種情形。我是一個閉包,我捕獲了類的實例成員,這樣我的對象指針就將指向該實例對象;我是這個類,我定義了一個屬性,該屬性是這個閉包,這樣我的實例對象就將有一個屬性指針指向該閉包??创a:
class Employee{
var name: String
var printer:(()->())?
/* lazy var printer: (()->())? = {
print("name: \(self.name)")
} */
init(name:String){
self.name=name
self.printer = {
print("name: \(self.name)")
}
print("Employee init")
}
deinit {
print("Employee deinit")
}
}
var employee:Employee?
employee=Employee(name: "Jason")
employee?.printer?()
//employee?.printer=nil
employee = nil
/* print :
Employee init
name: Jason */
同樣析構器不會被調用,因為閉包和這個對象還兩兩相指呢,只有當我手動賦值該對象的閉包屬性為 nil 時,析構器才會調用。看圖:
那解決辦法大家應該也猜到了,同樣是聲明弱引用。
只放出不同部分:
self.printer = { [weak self] in print("name: \(self!.name)") }
Weak-Strong Dance
這個有點兒詩意的名字是用來延長弱引用對象的生存周期的,別還沒等到你要用,對象就被系統給咔嚓了。
下面運用了一點多線程知識,不詳講了,總之你要明白在代碼中弱引用是非常不安全的。在進行一系列操作時,可能等不到你要使用它,就被系統回收了它的內存。在本例中,注釋部分就是臨時轉換成一個強引用局部變量,沒有被注釋的代碼就是
withExtendedLifetime
函數了。
import UIKit
class Employee {
var name: String
init(name:String){
self.name=name
print("Employee init")
}
func doClosure() {
DispatchQueue.global().async { [weak self] in
/*
self?.process("first process")
usleep(500)
self?.process("second process")
*/
/*
if let strongRef=self {
strongRef.process("first process")
usleep(500)
strongRef.process("second process")
}*/
withExtendedLifetime(self){
self?.process(message: "first process")
usleep(500)
self?.process(message: "second process")
}
}
}
deinit {
print("Employee deinit")
}
func process(message: String) {
print(message)
}
}
var employee: Employee? = Employee(name:"Jason")
employee?.doClosure()
DispatchQueue.global().async {
usleep(100)
employee = nil
}
dispatchMain()
/* print :
Employee init
first process
second process
Employee deinit
*/
第三周作業
題目
請說出下面代碼存在的問題,以及改進方式。
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit { print("\(name) is being deinitialized") }
}
class CreditCard {
let number: UInt64
let customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card #\(number) is being deinitialized") }
}
答案
一個簡單的循環引用,不解釋了。
class Customer {
let name: String
weak var card: CreditCard?
init(name: String) {
self.name = name
}
deinit { print("\(name) is being deinitialized") }
}
class CreditCard {
let number: UInt64
let customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card #\(number) is being deinitialized") }
}
var xiaoMing: Customer?
var xiaoMingID: CreditCard?
xiaoMing = Customer(name: "xiaoMing")
xiaoMingID = CreditCard(number: 522644, customer: xiaoMing!)
xiaoMing?.card = xiaoMingID
xiaoMing = nil
xiaoMingID = nil