一、閉包
-
1.1、閉包表達式(Closure Expression)
在 Swift 里面可以通過函數func
定義一個函數,也可以通過閉包表達式定義一個函數func sum(_ v1:Int,_ v2:Int) -> Int{ return v1+v2 } sum(1,2) // 3
閉包的格式
{ 參數列表 -> 返回值類型 in 具體的代碼 }
閉包的具體舉例
var fn = { (_ v1:Int,_ v2:Int) -> Int in return v1+v2 } fn(2,3) // 5
提示:上面僅僅是給閉包定義了一個變量,如果不寫變量和下面的意思一樣
{ (_ v1:Int,_ v2:Int) -> Int in return v1+v2 }(2,3)
-
1.2、閉包表達式的簡寫
基礎的閉包var fn = { (v1:Int,v2:Int) -> Int in return v1+v2 }
簡寫的代碼
func exec(v1:Int,v2:Int,fn:(Int,Int)->Int){ print(fn(v1,v2)) }
提示:傳進去一個閉包
調用 exec 函數方式如下:等效,結果都是 20
方式一exec(1, 2, fn: fn)
方式二
exec(v1: 10, v2: 10) { (v3, v4) -> Int in return v3 + v4 }
方式三
exec(v1: 10, v2: 10, fn: { v3,v4 in return v3+v4 })
方式四
exec(v1: 10, v2: 10, fn: { v3,v4 in v3+v4 })
方式五
exec(v1: 10, v2: 10, fn: {$0 + $1})
方式六
exec(v1: 10, v2: 10, fn: +)
-
1.3、尾隨閉包
如果將一個很長的閉包表達式作為函數的最后一個實參,使用尾隨閉包可以增強函數的可讀性
-
尾隨閉包是一個被書寫在函數調用括號外面(后面)的閉包表達式
func exec(v1:Int,v2:Int,fn:(Int,Int)->Int){ print(fn(v1,v2)) } exec(v1: 6, v2: 7, fn: {$0 + $1})
結果:13,讓兩個參數相加,我們還可以:
-
、*
等等 -
如果閉包表達式是函數的唯一實參,而且使用了尾隨閉包的寫法,那就不需要再函數名后面寫圓括號
func exec(fn:(Int,Int)->Int){ print(fn(2,5)) }
調用方式如下,結果為 7
exec(fn: {$0 + $1}) exec(){$0 + $1} exec{$0 + $1}
-
1.4、數組的排序
func cmp(i1:Int,i2:Int) -> Bool { // 大的排到前面 return i1 > i2 } // 返回 false: i1 排在 i2 后面,也就是 i1 的值小于 i2 // 返回 true: i1 排在 i2 前面,也就是 i1 的值大于 i2
-
1.5、忽略參數
func exec(fn:(Int,Int)->Int){ print(fn(1,5)) } exec{_,_ in 11} // 11
-
1.6、閉包(Closure):的實質是一段有具體功能的代碼塊。
- 閉包:一個函數和他捕獲的變量/常量環境組合起來,稱為閉包。閉包的核心是在其使用的局部變量/常量 會被額外的復制或者引用,使這些變量脫離其作用域后依然有效。
一般指定義在函數內部的函數
-
一般它捕獲的是外層函數的局部變量/常量
// 返回的 plus 與 num 形成了閉包 func getFn() -> Fn{ var num = 0 func plus(_ i:Int) -> Int{ num += I return num } return plus } var fn1 = getFn() var fn2 = getFn() fn1(1) fn2(2)
- 可以把閉包想象成一個類的實例對象
存儲在堆空間
捕獲的局部變量/常量就是對象的成員(存儲屬性)
-
組成閉包的函數就是類內部定義的方法
class Closure{ var num = 2 func plus(_ i:Int) -> Int { num += I return num } } var cs1 = Closure() var cs2 = Closure() cs1.plus(2) // 4 cs2.plus(2) // 4 cs1.plus(3) // 7 cs2.plus(3) // 7
- 閉包:一個函數和他捕獲的變量/常量環境組合起來,稱為閉包。閉包的核心是在其使用的局部變量/常量 會被額外的復制或者引用,使這些變量脫離其作用域后依然有效。
-
1.7、自動閉包
自動閉包的條件:自動閉包參數使用有嚴格的條件是首先此閉包不能有參數,其次在調用函數傳參的時,此閉包的實現只能由一句表達式組成,閉包的返回值即為此表達式的值,自動閉包由@autoclosure
來聲明,如下 例三-
例一:如果第1個數大于0,返回第一個數。否則返回第2個數
func getFirstPositive(_ v1: Int, _ v2: Int) -> Int { return v1 > 0 ? v1 : v2 } getFirstPositive(10, 20)
-
例二:改成函數類型的參數,可以讓v2延遲加載
func getFirstPositive(_ v1: Int, _ v2: () -> Int) -> Int? { return v1 > 0 ? v1 : v2() } getFirstPositive(-4) { 20 }
-
例三:為了避免與期望沖突,使用了@autoclosure的地方最好明確注釋清楚:這個值會被推遲執行
func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int? { return v1 > 0 ? v1 : v2() } getFirstPositive(-4, 20)
-
結論
-
@autoclosure
會自動將 20 封裝成閉包 { 20 } -
@autoclosure
只支持() -> T
格式的參數@autoclosure
并非只支持最后1個參數 - 空合并運算符
??
使用了@autoclosure
技術 - 有
@autoclosure
、無@autoclosure
,構成了 函數重載
-
-
-
1.8、逃逸閉包和非逃逸閉包
- 逃逸閉包:是指函數內的閉包在函數執行結束后在函數外依然可以使用
- 非逃逸閉包:是指在函數的聲明周期結束后,閉包也將會被銷毀。換句話說,非逃逸閉包只能在函數內部使用,在函數外部不能使用。默認情況下函數中的閉包都為非逃逸閉包,這樣做的優點是可以提高代碼的性能,節省內存消耗,開發者可以根據實際需求將閉包參數聲明為逃逸閉包。
提示:
非逃逸閉包也不可以作為返回值返回,如果這么做,編譯器會拋出一個錯誤。
-
將閉包聲明為非逃逸型,需要使用
@noescape
修飾。需要注意的是,在最新的Xcode版本里面已經不需要再使用,參數默認都是非逃逸的,如下代碼只有一個閉包的函數,將此閉包聲明為非逃逸的,此閉包既不可最為返回值也不可賦值給外部變量 在xcode10.1中會有警告,這個關鍵字可以忽略
@noescape 默認的,不需要單獨再寫 逃逸類型的閉包通常用于異步操作中,例如一個請求完成后要執行閉包回調,需要使用逃逸類型。
二、屬性
-
2.1、Swift 里面跟實例相關的屬性可以分為2大類
-
存儲屬性(Sored Property)
- 類似于成員變量這個概念
- 存儲在實例的內存中
- 結構體和類可以定義存儲屬性
- 枚舉不可以定義存儲屬性
提示:
-
關于存儲屬性,在Swift里面有個明確的規定,在創建類和結構體的時候,必須為所有的存儲屬性設置一個合適的初始值。
struct Circle { /// 存儲屬性 var radius:Double } var circle = Circle(radius: 2)
-
可以在初始化器里面為存儲屬性設置一個初始值
struct Circle { /// 存儲屬性 var radius:Double init () { radius = 2 } } var circle = Circle()
-
可以分配一個默認的屬性值作為屬性定義的一部分。如在定義屬性的時候直接給一個 值,
var radius:Int = 2
struct Circle { /// 存儲屬性 var radius:Double = 2.0 } var circle = Circle()
-
計算屬性(Computed Property)
- 本質就是方法(函數)
- 不占用實例的內存
- 枚舉、結構體、類 都可以定義計算屬性
提示:
- set傳入的新值默認叫做 newValue,也可以自定義
- 定義計算屬性只能用 var ,不能用 let(代表常量,值是一成不變的)
- 計算屬性的值是可能發生變化的(即使是只讀計算屬性)
- 有
set
方法的話必須有get
方法,有get
方法可以沒有set
方法
-
存儲屬性(Sored Property)
-
2.2、以結構體舉例
struct Circle { /// 存儲屬性 var radius:Double /// 計算屬性 var diameter:Double { set { radius = newValue / 2.0 } get { radius * 2.0 } } } var circle = Circle(radius: 2) circle.diameter = 20; //結果是 8 個字節 print("占用的內存大小=\(MemoryLayout.stride(ofValue: circle))")
提示:計算屬性是不占內存的,原因是它的set 和 get 類似于兩個函數,如下代碼,由此可以看出是不占用內存的
func setDiameter (newValue:Double) { radius = newValue / 2.0 } func getDiameter (newValue:Double) -> Double { radius * 2.0 }
-
2.3、枚舉原始值 rawvalue 原理
enum TestEnum : Int { case test1 = 1, test2 = 2, test3 = 3 var rawValue: Int { switch self { case .test1: return 8 case .test2: return 8 case .test3: return 10 } } } print(TestEnum.test3.rawValue) // 結果 10
枚舉原始值
rawValue
的本質是:只讀計算屬性,查看匯編里面只有get
方法 -
2.4、延遲存儲屬性(Lazy Stored Property)
-
使用
lazy
可以定義一個延遲存儲屬性,在第一次用到屬性的時候才會進行初始化class Car { init() { print("Car init") } func run() { print("Car is running!") } } class Person { lazy var car = Car() init() { print("Person init") } func goOut() { car.run() } } // 使用 let person = Person() print("------") person.goOut()
打印結果如下:我們可以看到在
let person = Person()
執行的時候lazy var car = Car()
沒有立馬執行,而是在用到 car 的時候才被調用的,由此可以看出在存儲屬性前面加上 lazy 是不會立馬執行的,在用到的時候才會被執行Person init ------ Car init Car is running!
提示
-
lazy
屬性必須是var
,不能是let -
let
必須在實例的初始化方法完成之前就擁有值 - 如果 多條線程 同時第一次訪問
lazy
屬性,是無法保證屬性只被初始化1次,這個是線程不安全的
lazy var image:UIImage = { //圖片url let imgUrl = "http://www.uw3c.com/images/index/logo_monkey.png"/ let url : NSURL = NSURL(string:imgUrl)!// 轉換網絡URL let data : NSData = NSData(contentsOf:url as URL)! return UIImage(data: data as Data)! }()
-
-
-
2.5、延遲屬性的注意點
-
當一個結構體包含一個延遲屬性的時候,只有 var 才能訪問延遲屬性,因為延遲屬性初始化時需要改變結構體的內存
解釋:如果實例定義為 let,那么節結構體的內存就固定了,不能被修改,那么延遲屬性的本質是修改結構體的內存,編譯器就會直接報錯
struct Point1 { var x = 1 var y = 2 lazy var z = 3 } // 使用 let point = Point1() point.z
-
-
2.6、屬性觀察器
struct Circle { /// 存儲屬性 var radius:Double { willSet { print("willSet",newValue) } didSet { print("didSet",oldValue,radius) } } init() { self.radius = 1.0 print("Circle init") } } var circle = Circle() circle.radius = 10 print(circle.radius) 打印結果如下 Circle init willSet 10.0 didSet 1.0 10.0 10.0
提示:
-
willSet
會傳遞新值,默認值叫newValue
-
didSet
會傳遞舊值,默認值叫oldValue
- 在初始化器中設置屬性值不會觸發
willSet
和didSet
- 在屬性定義時設置初始值也不會觸發
willSet
和didSet
-
-
2.7、全局變量 和 局部變量
屬性觀察器、計算屬性的功能,同樣可以應用在全局變量,局部變量身上
-
全局變量
var num:Int { get { return 10 } set { print("setNum",newValue) } } num = 12 // setNum 12 print(num) // 10
-
局部變量
fun test() { var age:Int = 10 { willSet { print("willSet",newValue) } didSet { print("didSet",oldValue,radius) } } age = 11 // 打印:willSet 11 // didSet 10 11 } // 調用方法 test()
-
2.8、inout 本質的探究
struct Shape { var width:Int var side:Int { willSet { print("willSet - side",newValue) } didSet { print("willSet - side",oldValue,side) } } var girth:Int { set { width = newValue / side print("setGirth",newValue) } get { print("getGirth") return width * side } } func show() -> Void { print("width = \(width), side = \(side), girth = \(girth)") } } func test(_ num: inout Int) -> Void { num = 20 } var s = Shape(width: 10, side: 4) test(&s.width) s.show() print("----------") test(&s.side) s.show() print("----------") test(&s.girth) s.show() 打印結果是 getGirth width = 20,side = 4,width = 80 ---------- willSet - side 20 willSet - side 4 20 getGirth width = 20,side = 20,width = 400 ---------- getGirth setGirth 20 getGirth width = 1,side = 20,width = 20
- 如果實參有物理內存地址,且沒有設置屬性觀察器:直接將實參的內存地址傳入函數(實參進行引用傳遞)
- 如果實參是計算屬性 或者 設置了屬性觀察器:采取了 Copy In Copy Out 的做法
- 調用函數時,先復制實參的值,產生副本 【get】
- 將副本的內存地址傳入函數 (副本進行引用傳遞),在函數的內部可以修改副本的值
- 函數返回后,再將副本的值覆蓋實參的值 【set】
- 總結:inout 的本質是引用傳遞 (地址傳遞)
-
2.9、類型屬性(Type Property)
嚴格來說屬性可以分為:實例屬性 和 類型屬性-
實例屬性(Instance Property):只能通過實例去訪問
- 存儲實例屬性(Stored Instance Property):存儲在實例內存中,每個實例都有一份內存
- 計算實例屬性(Computed Instance Property)
-
類型屬性(Type Property):只能通過類去訪問
存儲類屬性(Stored Instance Property):整個程序運行中,就只有一份內存(類似于全局變量)
計算類屬性(Computed Instance Property)
-
可以通過
static
定義類型屬性,如果是類可以使用關鍵字class
struct Car { static var count = 0 init() { Car.count += 1 } } let c1 = car() let c2 = car() let c3 = car() print(Car.count) // 結果是 3
-
總結:類屬性細節
- 不同于 存儲實例屬性,你必須給 存儲類型屬性 設定初始值,因為類型沒有像實例那樣的init初始化器來初始化存儲屬性
- 存儲類型屬性默認就是lazy,會在第一次使用的時候才初始化,就算被多個線程同時訪問,保證只會初始化一次
- 存儲類型屬性可以是 let
提示:枚舉類型也可以定義類型屬性(存儲類型屬性、計算類型屬性)
-
類存儲屬性的使用-- 單利
class Person { public static let share = Person() // 類存儲屬性,在這個程序中只有一份內存 private init() {} }
-
三、方法
-
3.1、類、枚舉、結構體 都可以定義 實例方法、類型方法
實例方法(Instance Method):通過實例對象調用
-
類型方法(Type Method):通過 類型調用,用 class/static關鍵字定義
struct Car { static var count = 0 init() { Car.count += 1 } // 在函數(方法) 前面加上 class/static關鍵字 就可以變成類方法,用類名來調用 static func getCount() -> Int { count } } let c1 = car() let c2 = car() let c3 = car() print(Car.getCount()) // 結果是 3
提示:self
- 在實例方法里面代表實例對象
- 在類方法里面代表類型
上代碼代碼static func getCount()
里面的count
等價于self.count
、Car.self.count
、Car.count
-
3.2、mutating 的使用
結構體和枚舉都是值類型,默認情況下,值類型的屬性不能被自身的實例方法修改
-
解決辦法:在 func 前面加上 mutaing 可以允許這種修改行為
struct Point { var x = 0.0,y = 0.0 mutating func moveBy(deltaX:Double,deltaY:Double) { x += deltaX y += deltaY // 等效于上面的代碼 //self = Point2(x: x + deltaX, y: y + deltaY) } } enum StateSwitch { case low,middle,high mutating func next() { switch self { case .low: self = .middle case .middle: self = .high case .high: self = .low } } }
-
3.3、@discardableResult 消除方法返回值沒使用的警告,如下
struct Point { var x = 0.0,y = 0.0 @discardableResult mutating func moveBy(deltaX:Double,deltaY:Double) -> Double { x += deltaX y += deltaY return x + y } } var point = Point() point.moveBy(deltaX: 2.0, deltaY: 2.0)
提示
:如果 函數moveBy
不加@discardableResult
,point.moveBy(deltaX: 2.0, deltaY: 2.0) 會提提示Result of call to 'moveBy(deltaX:deltaY:)' is unused
四、下標(subscrip)
4.1、使用 subscrip 可以給任意類型(枚舉、結構體、類)增加下標功能,有些地方翻譯為:下標腳本
-
4.2、subscrip的語法類似于實例方法、計算屬性,本質就是方法 (函數)
struct Point { var x = 0.0,y = 0.0 subscript(index: Int) -> Double { set { if index == 0 { x = newValue } else if index == 1 { y = newValue } } get { if index == 0 { return x } else if index == 1 { return y } return 0 } } } var p = Point5() p[0] = 1.1 p[1] = 2.2 print(p.x) // 1.1 print(p.y) // 2.2 print(p[0]) // 1.1 print(p[1]) // 2.2
提示:
- subscript 中定義的返回值類型決定了:get 方法的返回值 和 set 方法中 newValue 的類型
- subscript 可以接受多個參數,并且類型任意
-
4.3、subscript 下標細節
-
細節一:subscript 下標可以沒有 set 方法,但必須有 get 方法,如果只有get方法可以把
get
關鍵字去掉,如下struct Point { var x = 0.0,y = 0.0 subscript(index: Int) -> Double { if index == 0 { return x } else if index == 1 { return y } return 0 } }
-
細節二:可以設置參數標簽
struct Point { var x = 0.0,y = 0.0 subscript(index i: Int) -> Double { if i == 0 { return x } else if i == 1 { return y } return 0 } } var p = Point6() p.y = 22.0 // 訪問的時候必須加上參數標簽 print(p[index: 1])
-
細節三:下標可以是 類型方法
class Sum { static subscript(v1:Int,v2:Int) -> Int { return v1 + v2 } } print(Sum[10,20]) // 打印結果是 30
-
-
4.4、接收多個參數的下標
class Grid { var data = [ [0, 1, 2], [3, 4, 5], [6, 7, 8] ] subscript(row: Int, column: Int) -> Int { set { guard row >= 0 && row < 3 && column >= 0 && column < 3 else { return } data[row][column] = newValue } get { guard row >= 0 && row < 3 && column >= 0 && column < 3 else { return 0 } return data[row][column] } } } var grid = Grid() grid[0, 1] = 77 // 第1行第2列 grid[1, 2] = 88 // 第2行第3列 grid[2, 0] = 99 // 第3行第1列 print(grid.data)
打印結果如下:
[[0, 77, 2], [3, 4, 88], [99, 7, 8]]