Swift語法01

Swift 簡介

查看Swift當前版本

$ xcrun swift --version

簡介

  • Swift 語言由蘋果公司在 2014 年推出,用來撰寫 OS X 和 iOS 應用程序
  • 2014 年,在 Apple WWDC 發布
  • 2016.3月 Apple WWDC 支持 Linux系統,國外已經有人開始用Swift 寫服務端

歷史

  • 2010 年 7 月,蘋果開發者工具部門總監克里斯·拉特納開始著手 Swift 編程語言的設計
  • 用一年時間,完成基本架構
  • Swift 大約歷經 4 年的開發期,2014 年 6 月發布
  • 在 2015.12.3 開源, 現在GitHub上超過30000個星星

從發布至今,蘋果的每一個舉措都彰顯其大力推廣 Swift 的決心

版本

  • 正式版 Swift 3.0, Xcode 8.0

Swift 特色

  • Swfit3.0 是變化最大的一個版本,全面的去OC化了,Foundation框架的NS前綴去掉了
  • 可以使用現有的 CocoaCocoa Touch 框架
  • 蘋果宣稱 Swift的特點是:快速、現代、安全、互動,而且明顯優于 Objective-C 語言
  • 取消了預編譯指令包括宏
  • Swift 取消了 Objective-C 的指針及其他不安全訪問的使用
  • 舍棄 Objective-C 早期應用 Smalltalk 的語法,全面改為句點表示法
  • 提供了類似 Java 的名字空間(namespace)、泛型(generic)、運算對象重載(operator overloading
  • Swift 被簡單的形容為 “沒有 C 的 Objective-C”(Objective-C without the C)

Swift 現狀

  • 目前國內有些公司的新項目已經直接采用 Swift 開發
  • 目前很多公司都在做 Swift 的人才儲備
  • 應聘時,會 Swift 開發無疑會增加自身籌碼
  • 很多公司現在已經深陷Swfit無法自拔了,包括唐巧的猿題庫

為什么要學習 Swift?

  1. 從4月份開始,蘋果提供的資料已經沒有 OC 的了,這說明蘋果推動 Swift 的決心
  2. OC 源自于 smalltack-c,迄今已經有 40 多年的歷史,雖然 OC 的項目還會在未來持續一段時間,但是更換成 Swift 是未來必然的趨勢
  3. 現在很多公司都注重人才儲備,如果會Swift,就業會有很大的優勢,簡歷中如果寫上會 Swift,雖然面試中雖然不會怎么被問到,但對于薪資提升有很大幫助,同時可以從另外一個側面證明我們是有自學能力的人,這是所有企業都需要的
  4. Swift 里面融合了很多其他面向對象語言的思想,不像OC那么封閉,學會 Swift,再轉其他語言會輕松很多
  5. Swift 畢竟也是出身自蘋果,整體程序開發思路和 OC 是一樣的,等 Swift 項目講完后,大家完全可以用同樣的思路寫出 OC 的來,而且在翻寫的過程中,能夠對很多原本忽略的 OC 基本功有很大的加強和改善

Swift 開發快速體驗

目標

  • playground 快速體驗 & 學習資源分享
  • 項目開發快速體驗,了解 Swift 基本程序結構

學習資源

基本語法

目標

  • 熟悉 Swift 基本語法
    • 常量 & 變量
    • 可選項
    • 控制流
      • if
      • 三目
      • if let
      • guard
      • switch
    • 字符串
    • 循環
    • 集合
      • 數組
      • 集合

變量和常量

需要掌握


定義

  • let 定義常量,一經賦值不允許再修改
  • var 定義變量,賦值之后仍然可以修改
//: # 常量
//: 定義常量并且直接設置數值
let x = 20
//: 常量數值一經設置,不能修改,以下代碼會報錯
// x = 30

//: 使用 `: 類型`,僅僅只定義類型,而沒有設置數值
let x1: Int
//: 常量有一次設置數值的機會,以下代碼沒有問題,因為 x1 還沒有被設置數值
x1 = 30
//: 一旦設置了數值之后,則不能再次修改,以下代碼會報錯,因為 x1 已經被設置了數值
// x1 = 50

//: # 變量
//: 變量設置數值之后,可以繼續修改數值
var y = 200
y = 300

自動推導

  • Swift能夠根據右邊的代碼,推導出變量的準確類型
  • 通常在開發時,不需要指定變量的類型
  • 如果要指定變量,可以在變量名后使用:,然后跟上變量的類型

重要技巧:Option + Click 可以查看變量的類型

[圖片上傳失敗...(image-1c3941-1536302339170)]

沒有隱式轉換!!!

  • Swift 對數據類型要求異常嚴格
  • 任何時候,都不會做隱式轉換

如果要對不同類型的數據進行計算,必須要顯式的轉換

let x2 = 100
let y2 = 10.5

let num1 = Double(x2) + y2
let num2 = x2 + Int(y2)

let & var 的選擇

  • 應該盡量先選擇常量,只有在必須修改時,才需要修改為 var
  • 在 Xcode 7.0 中,如果沒有修改變量,Xcode 會提示修改為 let

Optional 可選值

需要掌握


  • 理解可選項的概念
  • 理解可選項的概念 --> 要么有值,要么為nil
  • 知道可選項的規則 --> 參與計算需要強制解包
  • 知道兩個符號
    1. '?' 定義可選項
    2. '!' 對可選類型進行強制解包, 程序員一定要對解包負責
  • 常量的可選項使用前需要設置初始值
  • 變量的可選項默認是nil

介紹

  • Optional 是 Swift 的一大特色,也是 Swift 初學者最容易困惑的問題
  • 定義變量時,如果指定是可選的,表示該變量可以有一個指定類型的值,也可以是 nil
  • 定義變量時,在類型后面添加一個 ?,表示該變量是可選的
  • 變量可選項的默認值是 nil
  • 常量可選項沒有默認值,主要用于在構造函數中給常量設置初始數值
//: num 可以是一個整數,也可以是 nil,注意如果為 nil,不能參與計算
let num: Int? = 10
  • 如果 Optional 值是 nil,不允許參與計算
  • 只有解包(unwrap)后才能參與計算
  • 在變量后添加一個 !,可以強行解包

注意:必須要確保解包后的值不是 nil,否則會報錯

//: num 可以是一個整數,也可以是 nil,注意如果為 nil,不能參與計算
let num: Int? = 10

//: 如果 num 為 nil,使用 `!` 強行解包會報錯
let r1 = num! + 100

//: 使用以下判斷,當 num 為 nil 時,if 分支中的代碼不會執行
if let n = num {
    let r = n + 10
}

常見錯誤

unexpectedly found nil while unwrapping an Optional value

翻譯

在[解包]一個可選值時發現 nil

?? 運算符

  • ?? 運算符可以用于判斷 變量/常量 的數值是否是 nil,如果是則使用后面的值替代
  • 在使用 Swift 開發時,?? 能夠簡化代碼的編寫
let num: Int? = nil

let r1 = (num ?? 0) + 10
print(r1)

控制流

if

  • Swift 中沒有 C 語言中的非零即真概念
  • 在邏輯判斷時必須顯示地指明具體的判斷條件 true / false
  • if 語句條件的 () 可以省略
  • 但是 {} 不能省略
let num = 200
if num < 10 {
    print("比 10 小")
} else if num > 100 {
    print("比 100 大")
} else {
    print("10 ~ 100 之間的數字")
}

三目運算

  • Swift 中的 三目 運算保持了和 OC 一致的風格
var a = 10
var b = 20
let c = a > b ? a : b
print(c)

適當地運用三目,能夠讓代碼寫得更加簡潔

可選項判斷

  • 由于可選項的內容可能為 nil,而一旦為 nil 則不允許參與計算
  • 因此在實際開發中,經常需要判斷可選項的內容是否為 nil

單個可選項判斷

let url = NSURL(string: "http://www.baidu.com")

//: 方法1: 強行解包 - 缺陷,如果 url 為空,運行時會崩潰
let request = NSURLRequest(URL: url!)

//: 方法2: 首先判斷 - 代碼中仍然需要使用 `!` 強行解包
if url != nil {
    let request = NSURLRequest(URL: url!)
}


可選項條件判斷

//: 1> 初學 swift 一不小心就會讓 if 的嵌套層次很深,讓代碼變得很丑陋
if let u = url {
    if u.host == "www.baidu.com" {
        let request = NSURLRequest(URL: u)
    }
}

//: 2> 使用 ',' 可以獲取前面賦值對象
if let u = url , u.host == "www.baidu.com" {
    let request = NSURLRequest(URL: u)
}
  • 小結
    • if let 不能與使用 &&|| 等條件判斷

多個可選項判斷

//: 3> 可以使用 `,` 同時判斷多個可選項是否為空
let oName: String? = "張三"
let oNo: Int? = 100

if let name = oName {
    if let no = oNo {
        print("姓名:" + name + " 學號: " + String(no))
    }
}

if let name = oName, let no = oNo {
    print("姓名:" + name + " 學號: " + String(no))
}

判斷之后對變量需要修改

let oName: String? = "張三"
let oNum: Int? = 18

if var name = oName, num = oNum {

    name = "李四"
    num = 1

    print(name, num)
}

guard

  • guard 是與 if let 相反的語法,Swift 2.0 推出的
let oName: String? = "張三"
let oNum: Int? = 18

guard let name = oName else {
    print("name 為空")
    return
}

guard let num = oNum else {
    print("num 為空")
    return
}

// 代碼執行至此,name & num 都是有值的
print(name)
print(num)
  • 在程序編寫時,條件檢測之后的代碼相對是比較復雜的
  • 使用 guard 的好處
    • 能夠判斷每一個值
    • 在真正的代碼邏輯部分,省略了一層嵌套

switch

  • switch 不再局限于整數
  • switch 可以針對任意數據類型進行判斷
  • 不再需要 break
  • 每一個 case后面必須有可以執行的語句
  • 要保證處理所有可能的情況,不然編譯器直接報錯,不處理的條件可以放在 default 分支中
  • 每一個 case 中定義的變量僅在當前 case 中有效,而 OC 中需要使用 {}
let score = "優"

switch score {
    case "優":
        let name = "學生"
        print(name + "80~100分")
    case "良": print("70~80分")
    case "中": print("60~70分")
    case "差": print("不及格")
    default: break
}

for 循環

  • OC 風格的循環 '++'語法已經廢棄
var sum = 0
for var i = 0; i < 10; i++ {
    sum += I
}
print(sum)
  • for-in,0..<10 表示從0到9
sum = 0
for i in 0..<10 {
    sum += I
}
print(sum)
  • 范圍 0...10 表示從0到10
sum = 0
for i in 0...10 {
    sum += I
}
print(sum)
  • 省略下標
    • _ 能夠匹配任意類型
    • _ 表示忽略對應位置的值
for _ in 0...10 {
    print("hello")
}

字符串

在 Swift 中絕大多數的情況下,推薦使用 String 類型

  • String 是一個結構體,性能更高
    • String 目前具有了絕大多數 NSString 的功能
    • String 支持直接遍歷
  • NSString 是一個 OC 對象,性能略差
  • Swift 提供了 StringNSString 之間的無縫轉換

字符串演練

  • 遍歷字符串中的字符
for s in str.characters {
    print(s)
}
  • 字符串長度
// 返回以字節為單位的字符串長度,一個中文占 3 個字節
let len1 = str.lengthOfBytesUsingEncoding(NSUTF8StringEncoding)
// 返回實際字符的個數
let len2 = str.characters.count

  • 字符串拼接
    • 直接在 "" 中使用 \(變量名) 的方式可以快速拼接字符串
let str1 = "Hello"
let str2 = "World"
let i = 32
str = "\(i) 個 " + str1 + " " + str2

我和我的小伙伴再也不要考慮 stringWithFormat 了 :D

  • 可選項的拼接
    • 如果變量是可選項,拼接的結果中會有 Optional
    • 為了應對強行解包存在的風險,蘋果提供了 ?? 操作符
    • ?? 操作符用于檢測可選項是否為 nil
      • 如果不是 nil,使用當前值
      • 如果是 nil,使用后面的值替代
let str1 = "Hello"
let str2 = "World"
let i: Int? = 32
str = "\(i ?? 0) 個 " + str1 + " " + str2
  • 格式化字符串
    • 在實際開發中,如果需要指定字符串格式,可以使用 String(format:...) 的方式
let h = 8
let m = 23
let s = 9
let timeString = String(format: "%02d:%02d:%02d", arguments: [h, m, s])
let timeStr = String(format: "%02d:%02d:%02d", h, m, s)

String & Range 的結合

  • 在 Swift 中,StringRange連用時,語法結構比較復雜
  • 如果不習慣 Swift 的語法,可以將字符串轉換成 NSString 再處理
let helloString = "我們一起飛"
(helloString as NSString).substringWithRange(NSMakeRange(2, 3))
  • 使用 Range<Index> 的寫法 前面不要試圖去掌握這段代碼,每一個版本都在變化,沒用
let str = "愛笑的人運氣不會太差喲"
        let startIndex = str.index(str.startIndex, offsetBy: 2)
        let endIndex = str.index(str.endIndex, offsetBy: -1)
        let subStr1 = str.substring(to: startIndex)
        print(subStr1)

        let subStr2 = str.substring(from: endIndex)
        print(subStr2)

        let subStr3 = str.substring(with: startIndex..<endIndex)
        print(subStr3)

        //String 和  NSString 之間可以相互轉換 可以轉換為 NSString 來截取子串
        let NStr = (str as NSString).substring(with: NSRange(location: 1, length: 2))
        print(NStr)

集合

數組

  • 數組使用 [] 定義,這一點與 OC 相同
//: [Int]
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  • 遍歷
for num in numbers {
    print(num)
}
  • 通過下標獲取指定項內容
let num1 = numbers[0]
let num2 = numbers[1]
  • 可變&不可變
    • let 定義不可變數組
    • var 定義可變數組
let array = ["zhangsan", "lisi"]
//: 不能向不可變數組中追加內容
//array.append("wangwu")
var array1 = ["zhangsan", "lisi"]

//: 向可變數組中追加內容
array1.append("wangwu")
  • 數組的類型
    • Swift中數組建議存放相同類型的元素
    • 如果初始化時,所有內容類型不一致,需要手動將數組的類型轉換為[Any]
//: array1 僅允許追加 String 類型的值
//array1.append(18)

var array2: [Any] = ["zhangsan", 18]
//: 在 Swift 中,數字可以直接添加到集合,不需要再轉換成 `NSNumber`
array2.append(100)

  • 數組的定義和實例化
    • 使用 : 可以只定義數組的類型
    • 實例化之前不允許添加值
    • 使用 [類型]() 可以實例化一個空的數組
var array3: [String]
//: 實例化之前不允許添加值
//array3.append("laowang")
//: 實例化一個空的數組
array3 = [String]()
array3.append("laowang")
  • 數組的合并
    • 必須是相同類型的數組才能夠合并
    • 開發中,通常數組中保存的對象類型都是一樣的!
array3 += array1

//: 必須是相同類型的數組才能夠合并,以下兩句代碼都是不允許的
//array3 += array2
//array2 += array3
  • 數組的刪除
//: 刪除指定位置的元素
array3.removeAtIndex(3)
//: 清空數組
array3.removeAll()

字典

  • 定義
    • 同樣使用 [] 定義字典
    • let 不可變字典
    • var 可變字典
    • [String : Any] 是最常用的字典類型
//: [String : Any] 是最常用的字典類型
var dict = ["name": "zhangsan", "age": 18]
  • 賦值
    • 賦值直接使用 dict[key] = value 格式
    • 如果 key 不存在,會設置新值
    • 如果 key 存在,會覆蓋現有值
//: * 如果 key 不存在,會設置新值
dict["title"] = "boss"
//: * 如果 key 存在,會覆蓋現有值
dict["name"] = "lisi"
dict
  • 遍歷
    • kv 可以隨便寫
    • 前面的是 key
    • 后面的是 value
//: 遍歷
for (k, v) in dict {
    print("\(k) ~~~ \(v)")
}
  • 合并字典
    • 如果 key 不存在,會建立新值,否則會覆蓋現有值
//: 合并字典
var dict1 = [String: NSObject]()
dict1["nickname"] = "大老虎"
dict1["age"] = 100

//: 如果 key 不存在,會建立新值,否則會覆蓋現有值
for (k, v) in dict1 {
    dict[k] = v
}
print(dict)

錯誤處理

需要了解

  1. 知道錯誤處理機制的三種方式
  2. 開發中通常使用第二種 try?
  3. 在 Swfit 3.0 中 Foundation 的部分類去除了 NS 前綴

代碼演練,網絡訪問反序列化數據

  • 網絡訪問代碼
let url = URL(string: "http://www.weather.com.cn/data/sk/101010100.html")!
URLSession.shared.dataTask(with: url, completionHandler: { (data, _, error) in
    if error != nil {
        print(error)
        return
    }
}).resume()
  • JSON反序列化
// 方式1:強try.加載失敗直接崩潰
let dict = try! JSONSerialization.jsonObject(with: data!, options: [])
print(dict)

// 方式2:可選try.反序列化失敗返回nil
let dict = try? JSONSerialization.jsonObject(with: data!, options: [])
print(dict)

// 方式3:默認try.返回序列化失敗可以捕捉詳細信息
do {
    let dict = try JSONSerialization.jsonObject(with: data!, options: [])
    print(dict)
}catch {
    print(error)
}

函數

目標

  • 函數
    • 定義格式
    • 外部參數
    • 無返回值的三種情況
  • 閉包
    • 閉包的定義
    • 尾隨閉包
    • 循環引用
    • OC Block復習

函數

目標

  • 掌握函數的定義
  • 掌握外部參數的用處
  • 掌握無返回類型的三種函數定義方式

代碼實現

  • 函數的定義
    • 格式 func 函數名(行參列表) -> 返回值 {代碼實現}
    • 調用 let result = 函數名(值1, 參數2: 值2...)
//Swfit 3.0
func sum(a: Int, b: Int) -> Int {
    return a + b
}

let result = sum(a:10, b: 20)


//Swfit 2.0
func sum(a: Int, b: Int) -> Int {
    return a + b
}

let result = sum(10, b: 20)
  • 沒有返回值的函數,一共有三種寫法
    • 省略
    • ()
    • Void
func demo(str: String) -> Void {
    print(str)
}
func demo1(str: String) -> () {
    print(str)
}
func demo2(str: String) {
    print(str)
}

demo("hello")
demo1("hello world")
demo2("olleh")
  • 外部參數
    • 在形參名前再增加一個外部參數名,能夠方便調用人員更好地理解函數的語義
    • 格式:func 函數名(外部參數名 形式參數名: 形式參數類型) -> 返回值類型 { // 代碼實現 }
    • Swift 2.0 中,默認第一個參數名省略
func sum1(num1 a: Int, num2 b: Int) -> Int {
    return a + b
}

sum1(num1: 10, num2: 20)
  • 函數的內部函數
func demo() {

        //該內部函數的范圍只能在 demo函數中可以被訪問
        func sum() {
            print("哈哈哈")
        }

        //必須先聲明內部函數才能調用調用
        sum()

    }

函數格式小結

// 格式:func 函數名(形參1: 類型 = 默認值, _ 形參2: 類型 = 默認值...) -> 返回值 { // 代碼實現 }
// 說明:包含默認值的函數可以不用傳遞,并且可以任意組合
//
// 格式:func 函數名(形參1: 類型, _ 形參2: 類型...) -> 返回值 { // 代碼實現 }
// 說明:_ 可以忽略外部參數,與其他語言的函數風格更加類似
//
// 格式:func 函數名(外部參數1 形參1: 類型, 外部參數2 形參2: 類型...) -> 返回值 { // 代碼實現 }
// 說明:外部參數名供外部調用使用,形參 在函數內部使用
//
// 格式:func 函數名(形參列表) -> 返回值 { // 代碼實現 }

閉包

與 OC 中的 Block 類似,閉包主要用于異步操作執行完成后的代碼回調,網絡訪問結果以參數的形式傳遞給調用方

目標

  • 掌握閉包的定義
  • 掌握閉包的概念和用法
  • 了解尾隨閉包的寫法
  • 掌握解除循環引用的方法

OC 中 Block 概念回顧

  • 閉包類似于 OC 中的 Block
    • 預先定義好的代碼
    • 在需要時執行
    • 可以當作參數傳遞
    • 可以有返回值
    • 包含 self 時需要注意循環引用

閉包的定義

  • 函數實際上是一個特殊的閉包, 函數知識閉包的一種新式
  • 可以結合函數的使用 來學習閉包
  • 定義一個函數在函數內部定義一個沒有參數也沒有返回值的內部函數
    func demo1A() {
        func sum() {
            print("哈哈哈")
        }
        sum()

    }

  • 定義一個閉包 實現函數的內部函數一樣的效果
    • 閉包 = { (行參) -> 返回值 in // 代碼實現 }
    • in 用于區分函數定義和代碼實現
//使用閉包實現
    func demo1B() {
        //定義代碼塊   () -> ()
        //沒有參數沒有返回值的閉包
        let closure = { () -> Void in
            print("OK")
        }

        //執行代碼塊
        closure()
    }
  • 定義一個函數,內部函數有參數也有返回值
    func demo3A() {
        func sum(num1 a: Int, num2 b: Int) -> Int{
            return a + b
        }

        let result = sum(num1: 10, num2: 20)
        print(result)
    }
  • 定義一個函數,實現上述函數的內部函數
    func demo3B() {
        let closure = { (num1 a: Int, num2 b: Int ) -> Int in

            return a + b
        }


        let result = closure(num1: 100, num2: 200)
        print(result)
    }
  • 定義一個閉包 實現函數的內部函數一樣的效果
    • 閉包 = { (行參) -> 返回值 in // 代碼實現 }
    • in 用于區分函數定義和代碼實現
  • 最簡單的閉包,如果沒有參數/返回值,則 參數/返回值/in 統統都可以省略
    • { 代碼實現 }
let closure = {
    print("hello")
}

尾隨閉包

  • 當閉包是函數的最后一個參數時,函數的參數的小括號可以提前關閉,閉包可以寫在小括號后面當做尾隨閉包

閉包的三種簡寫形式只需要了解即可,在實際開發中做到看到不陌生即可,直接使用系統提示的類型

基本使用

GCD 異步

  • 模擬在后臺線程加載數據
func loadData() {
     DispatchQueue.global().async {
            //假裝耗時
            Thread.sleep(forTimeInterval: 2)
    }
}
  • 尾隨閉包,如果閉包是最后一個參數,可以用以下寫法

自定義閉包參數,實現主線程回調

  • 添加沒有參數,沒有返回值的閉包
override func viewDidLoad() {
    super.viewDidLoad()

    loadData {
        print("完成回調")
    }
}

//異步請求網絡數據沒在主隊列中回調 更新UI

    //TODO: 待會解釋該關鍵詞
    //@escaping
    func loadData(a: Int,finished: @escaping (String) -> () ) {
        DispatchQueue.global().async {
            //假裝耗時
            Thread.sleep(forTimeInterval: 2)
            //獲取結果
            let res = "老王"
            //在主隊列中回調數據
            DispatchQueue.main.async {
                //回調數據  只負責請求數據 不負責更新UI
                //從外界傳遞閉包
                finished(res)
            }
        }
    }
  • 添加回調參數
override func viewDidLoad() {
    super.viewDidLoad()

    //定義一個有參數沒有返回值的閉包當做參數傳遞
        let closure = { (res: String) -> () in
            print("我是請求結果: \(res)")
        }
        loadData(a:20,finished: closure)

    //使用尾隨閉包的方式滴啊用
        loadData(a: 20) { (res) in
            print(res)
        }
}

循環引用

  • 建立 NetworkTools 對象
class NetworkTools: NSObject {

    /// 加載數據
    ///
    /// - parameter finished: 完成回調
    func loadData(finished: () -> ()) {
        print("開始加載數據...")

        // ...
        finished()
    }

    deinit {
        print("網絡工具 88")
    }
}
  • 實例化 NetworkTools 并且加載數據
class ViewController: UIViewController {

    var tools: NetworkTools?

    override func viewDidLoad() {
        super.viewDidLoad()

        tools = NetworkTools()
        tools?.loadData() {
            print("come here \(self.view)")
        }
    }

    /// 與 OC 中的 dealloc 類似,注意此函數沒有()
    deinit {
        print("控制器 88")
    }
}

運行不會形成循環引用,因為 loadData 執行完畢后,就會釋放對 self 的引用

  • 修改 NetworkTools,定義回調閉包屬性
/// 完成回調屬性
var finishedCallBack: (()->())?

/// 加載數據
///
/// - parameter finished: 完成回調
func loadData(finished: () -> ()) {

    self.finishedCallBack = finished

    print("開始加載數據...")

    // ...
    working()
}

func working() {
    finishedCallBack?()
}

deinit {
    print("網絡工具 88")
}

運行測試,會出現循環引用

解除循環引用

  • 與 OC 類似的方法
/// 類似于 OC 的解除引用
func demo() {
    weak var weakSelf = self
    tools?.loadData() {
        print("\(weakSelf?.view)")
    }
}
  • Swift 推薦的方法
loadData { [weak self] in
    print("\(self?.view)")
}
  • 還可以
loadData { [unowned self] in
    print("\(self.view)")
}

閉包(Block) 的循環引用小結

  • Swift

    • [weak self]
      • self是可選項,如果self已經被釋放,則為nil
    • [unowned self]
      • self不是可選項,如果self已經被釋放,則出現野指針訪問
  • Objc

    • __weak typeof(self) weakSelf;
      • 如果self已經被釋放,則為nil
    • __unsafe_unretained typeof(self) weakSelf;
      • 如果self已經被釋放,則出現野指針訪問

面相對象

目標

  • 構造函數
    • 構造函數的基本概念
    • 構造函數的執行順序
    • KVC 在構造函數中的使用及原理
    • 便利構造函數
    • 析構函數
    • 區分 重載重寫
  • 懶加載
  • 只讀屬性(計算型屬性)
  • 設置模型數據(didSet

構造函數基礎

構造函數是一種特殊的函數,主要用來在創建對象時初始化對象,為對象成員變量設置初始值,在 OC 中的構造函數是 initWithXXX,在 Swift 中由于支持函數重載,所有的構造函數都是 init

構造函數的作用

  • 分配空間 alloc
  • 設置初始值 init

必選屬性

  • 自定義 Person 對象
class Person: NSObject {

    /// 姓名
    var name: String
    /// 年齡
    var age: Int
}

提示錯誤 Class 'Person' has no initializers -> 'Person' 類沒有實例化器s

原因:如果一個類中定義了必選屬性,必須通過構造函數為這些必選屬性分配空間并且設置初始值

  • 重寫 父類的構造函數
/// `重寫`父類的構造函數
override init() {

}

提示錯誤 Property 'self.name' not initialized at implicitly generated super.init call -> 屬性 'self.name' 沒有在隱式生成的 super.init 調用前被初始化

  • 手動添加 super.init() 調用
/// `重寫`父類的構造函數
override init() {
    super.init()
}

提示錯誤 Property 'self.name' not initialized at super.init call -> 屬性 'self.name' 沒有在 super.init 調用前被初始化

  • 為比選屬性設置初始值
/// `重寫`父類的構造函數
override init() {
    name = "張三"
    age = 18

    super.init()
}

小結

  • 非 Optional 屬性,都必須在構造函數中設置初始值,從而保證對象在被實例化的時候,屬性都被正確初始化
  • 在調用父類構造函數之前,必須保證本類的屬性都已經完成初始化
  • Swift 中的構造函數不用寫 func

子類的構造函數

  • 自定義子類時,需要在構造函數中,首先為本類定義的屬性設置初始值
  • 然后再調用父類的構造函數,初始化父類中定義的屬性
/// 學生類
class Student: Person {

    /// 學號
    var no: String

    override init() {
        no = "001"

        super.init()
    }
}

小結

  • 先調用本類的構造函數初始化本類的屬性
  • 然后調用父類的構造函數初始化父類的屬性
  • Xcode 7 beta 5之后,父類的構造函數會被自動調用,強烈建議寫 super.init(),保持代碼執行線索的可讀性
  • super.init() 必須放在本類屬性初始化的后面,保證本類屬性全部初始化完成

Optional 屬性

  • 將對象屬性類型設置為 Optional
class Person: NSObject {
    /// 姓名
    var name: String?
    /// 年齡
    var age: Int?
}
  • 可選屬性不需要設置初始值,默認初始值都是 nil
  • 可選屬性是在設置數值的時候才分配空間的,是延遲分配空間的,更加符合移動開發中延遲創建的原則

重載構造函數

  • Swift 中支持函數重載,同樣的函數名,不一樣的參數類型
/// `重載`構造函數
///
/// - parameter name: 姓名
/// - parameter age:  年齡
///
/// - returns: Person 對象
init(name: String, age: Int) {
    self.name = name
    self.age = age

    super.init()
}

注意事項

  • 如果重載了構造函數,但是沒有實現默認的構造函數 init(),則系統不再提供默認的構造函數
  • 原因,在實例化對象時,必須通過構造函數為對象屬性分配空間和設置初始值,對于存在必選參數的類而言,默認的 init() 無法完成分配空間和設置初始值的工作

調整子類的構造函數

  • 重寫父類的構造函數
/// `重寫`父類構造函數
///
/// - parameter name: 姓名
/// - parameter age:  年齡
///
/// - returns: Student 對象
override init(name: String, age: Int) {
    no = "002"

    super.init(name: name, age: age)
}
  • 重載構造函數
/// `重載`構造函數
///
/// - parameter name: 姓名
/// - parameter age:  年齡
/// - parameter no:   學號
///
/// - returns: Student 對象
init(name: String, age: Int, no: String) {
    self.no = no

    super.init(name: name, age: age)
}

注意:如果是重載的構造函數,必須 super 以完成父類屬性的初始化工作

重載重寫

  • 重載,函數名相同,參數名/參數類型/參數個數不同
    • 重載函數并不僅僅局限于構造函數
    • 函數重載是面相對象程序設計語言的重要標志
    • 函數重載能夠簡化程序員的記憶
    • OC 不支持函數重載,OC 的替代方式是 withXXX...
  • 重寫,子類需要在父類擁有方法的基礎上進行擴展,需要 override 關鍵字

KVC 字典轉模型構造函數

/// `重寫`構造函數
///
/// - parameter dict: 字典
///
/// - returns: Person 對象
init(dict: [String: Any]) {
    setValuesForKeysWithDictionary(dict)
}
  • 以上代碼編譯就會報錯!

  • 原因:

    • KVC 是 OC 特有的,KVC 本質上是在運行時,動態向對象發送 setValue:ForKey: 方法,為對象的屬性設置數值
    • 因此,在使用 KVC 方法之前,需要確保對象已經被正確實例化
  • 添加 super.init() 同樣會報錯

  • 原因:

    • 必選屬性必須在調用父類構造函數之前完成初始化分配工作
  • 講必選參數修改為可選參數,調整后的代碼如下:

/// 個人模型
class Person: NSObject {

    /// 姓名
    var name: String?
    /// 年齡
    var age: Int?

    /// `重寫`構造函數
    ///
    /// - parameter dict: 字典
    ///
    /// - returns: Person 對象
    init(dict: [String: AnyObject]) {
        super.init()

        setValuesForKeysWithDictionary(dict)
    }
}

運行測試,仍然會報錯

錯誤信息:this class is not key value coding-compliant for the key age. -> 這個類的鍵值 age 與 鍵值編碼不兼容

  • 原因:
    • 在 Swift 中,如果屬性是可選的,在初始化時,不會為該屬性分配空間
    • 而 OC 中基本數據類型就是保存一個數值,不存在可選的概念
  • 解決辦法:給基本數據類型設置初始值
  • 修改后的代碼如下:
/// 姓名
var name: String?
/// 年齡
var age: Int? = 0

/// `重寫`構造函數
///
/// - parameter dict: 字典
///
/// - returns: Person 對象
init(dict: [String: Any]) {
    super.init()

    setValuesForKeysWithDictionary(dict)
}

提示:在定義類時,基本數據類型屬性一定要設置初始值,否則無法正常使用 KVC 設置數值

KVC 函數調用順序

init(dict: [String: Any]) {
    super.init()

    setValuesForKeysWithDictionary(dict)
}

override func setValue(value: Any?, forKey key: String) {
    print("Key \(key) \(value)")

    super.setValue(value, forKey: key)
}

// `NSObject` 默認在發現沒有定義的鍵值時,會拋出 `NSUndefinedKeyException` 異常
override func setValue(value: Any?, forUndefinedKey key: String) {
    print("UndefinedKey \(key) \(value)")
}
  • setValuesForKeysWithDictionary 會按照字典中的 key 重復調用 setValue:forKey 函數
  • 如果沒有實現 forUndefinedKey 函數,程序會直接崩潰
    • NSObject 默認在發現沒有定義的鍵值時,會拋出 NSUndefinedKeyException 異常
  • 如果實現了 forUndefinedKey,會保證 setValuesForKeysWithDictionary 繼續遍歷后續的 key
  • 如果父類實現了 forUndefinedKey,子類可以不必再實現此函數

子類的 KVC 函數

/// 學生類
class Student: Person {

    /// 學號
    var no: String?
}
  • 如果父類中已經實現了父類的相關方法,子類中不用再實現相關方法

convenience 便利構造函數

  • 條件判斷,只有滿足條件,才實例化對象,可以防治造成不必要的內存開銷
  • 簡化對象的創建
  • 本身不負責屬性的創建和初始化工作

特點

  • 默認情況下,所有的構造方法都是指定構造函數 Designated
  • convenience 關鍵字修飾的構造方法就是便利構造函數
  • 便利構造函數具有以下特點:
    • 可以構造失敗 返回 nil,
    • 只有便利構造函數中可以調用 self.init()
    • 便利構造函數不能被重寫或者 super
/// `便利構造函數`
///
/// - parameter name: 姓名
/// - parameter age:  年齡
///
/// - returns: Person 對象,如果年齡過小或者過大,返回 nil
convenience init?(name: String, age: Int) {
    if age < 20 || age > 100 {
        return nil
    }

    self.init(dict: ["name": name, "age": age])
}

注意:在 Xcode 中,輸入 self.init 時沒有智能提示

/// 學生類
class Student: Person {

    /// 學號
    var no: String?

    convenience init?(name: String, age: Int, no: String) {
        self.init(name: name, age: age)

        self.no = no
    }
}

便利構造函數應用場景

  • 根據給定參數判斷是否創建對象,而不像指定構造函數那樣必須要實例化一個對象出來
  • 在實際開發中,可以對已有類的構造函數進行擴展,利用便利構造函數,簡化對象的創建

構造函數小結

  • 指定構造函數必須調用其直接父類的的指定構造函數(除非沒有父類)
  • 便利構造函數必須調用同一類中定義的其他指定構造函數或者用 self. 的方式調用父類的便利構造函數
  • 便利構造函數可以返回 nil
  • 便利構造函數可以被繼承

懶加載

在 iOS 開發中,懶加載是無處不在的

  • 懶加載的格式如下:
lazy var person: Person = {
    print("懶加載")
    return Person()
}()
  • 懶加載本質上是一個閉包
  • 懶加載的簡單寫法
lazy var demoPerson: Person = Person()
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容