《Swift開發者必備Tips》讀書筆記 一、Swift新元素

        這是一本比基礎的Swift教程稍微深入一點的書籍,特別適合看過官方的教程之後的提高。總體來說,本書並不是很深入的書籍,適合剛接觸Swift的讀者,以及想要充實自己基礎知識的讀者。

一、Swift新元素

1.1 柯里化

        簡單來說,柯里化就是把方法、函數的參數個數減少。柯里化主要用在函數式編程中,筆者認爲其主要作用有以下幾個:

  1. 把具有不同參數個數的函數、方法統一爲只有一個(或相同參數個數)參數,簡單來說就是:把原本不統一的類型給統一
  2. 創建一個函數模板(下面的示例代碼進行了展示)
// 如果想要寫一個給輸入參數+2、+3、+4的函數,則需要重新寫類似的函數
func addOne(num : Int)->Int {
  return num + 1
}

// 可以通過柯里化來解決此問題
func addNumber(_ adder: Int)->(Int)->Int {
  return {
    num in
    return num + adder
  }
}

let add1 = addNumber(1)
print(add1(2)) // 3
var add2 = addNumber(2)
print(add2(2)) // 4

        作者在此小節中講的實際應用例子,筆者沒有看懂,希望有識之士能夠留言,助我解惑。總體來看,筆者的感覺是,作者在很早寫了此書,然後沒有隨着語言的更新,及時更新書中的例子。

1.2 講protocol的方法聲明爲mutating

        在協議中,某些方法可能會修改遵循此寫一點對象本身。此時,如果協議的方法沒有標明mutating,當遵循此協議的是類時,沒有問題;但,當遵循此協議的對象是值類型時,則會編譯不過。爲了使協議具有通用性(在Swift中struct、enum都可以實現協議),所以推薦給協議的方法添加mutating關鍵字。

1.3 Sequence

        在Swift中使用for in語句、map等迭代性質的語法時,都需要對應的容器、對象實現Sequence協議。Sequence協議只支持——遵循協議的對象實現單次遍歷。關於Sequence的詳細內容,參見《Swift進階》中的集合類型、集合類型協議兩個章節,那裏講述的較深入、全面、詳細。

1.4 元組

        在談到Swift時,經常提到的值類型是struct、enum,其實元組也是值類型。元組可以簡單理解爲更加簡單的結構體,其可提高代碼的靈活性。

1.5 @autoclosure和??

        簡單來說,某些情況下,我們想要寫一個閉包,但是,寫個閉包又比較費事,那麼通過@autoclosure關鍵字,編譯器就可以幫我們實現一個閉包,降低我們的工作量。

func logIfTrue(_ predicate: @autoclosure ()->Bool) {
  if predicate() {
    print("log")
  }
}

logIfTrue(2 > 1)

        自動閉包除了可以實現代碼簡單外,其還有一個重要作用:延遲調用。

var level: Int?
var startLevel = 1
var currentLevel = level ?? startLevel
// 此時,??運算符直接把startLevel賦值給currentLevel

func ??<T>(optional: T?, defaultValue: @autoclosure () -> T) ->T ) {
  switch optional {
  case .Some(let value):
    return value
  case .None:
    return defaultValue()
  }
}

// 爲什麼??運算符的第二個參數要寫一個自動閉包?????
// 如果不這麼寫的話,會導致調用??運算符時,默認值必須被計算,某些情況下,這會是一個耗時的操作
// 通過自動閉包實現了延遲調用,從而在用戶不知不覺中提升了性能

1.5 @escaping

        某些情況下,作爲參數傳遞到函數中的閉包,不是在此函數出棧之前就被執行了,而是在出棧之後被執行,此類閉包逃逸出了此函數的棧空間,爲了標示此情況,需要明確標識出此種情況,所以使用@escaping函數來做標識。Swift編譯器也會對此種情況進行檢查,如果發現則會報語法錯誤。

1.6 Optional Chaining

        由於Swift引入了可選值,爲了獲取可選值包含的實際值,就需要解包等操作。某些情況下,此類解包代碼太過於繁瑣,通過可選鏈來有效避免此類問題。

 class Toy {
    let name: String
    init(name: String) {
      self.name = name
  }
}

class Pet {
    var toy: Toy?
}

class Child {
    var pet: Pet?
}

 if let toyName = xiaoming.pet?.toy?.name {
  // 太好了,小明既有寵物,而且寵物還正好有個玩具
}

1.7 操作符

        與 Objective-C 不同,Swift 支持重載操作符、新建運算符這樣的特性。

struct Vector2D {
  var x = 0.0
  var y = 0.0
}

// 重載運算符
func +(left: Vector2D, right: Vector2D) -> Vector2D {
  return Vector2D(x: left.x + right.x, y: left.y + right.y)
}

// 新建運算符
 precedencegroup DotProductPrecedence {
    associativity: none
    higherThan: MultiplicationPrecedence
}
infix operator +*: DotProductPrecedence

func +* (left: Vector2D, right: Vector2D) -> Double {
  return left.x * right.x + left.y * right.y
}
  • precedencegroup
    定義了一個操作符優先級別。操作符優先級的定義和類型聲明有些相似,一個操作符必需要屬於某個特定的優先級。Swift 標準庫中已經定義了一些常用的運算優先級組,比如加法 優先級 ( AdditionPrecedence ) 和乘法優先級 ( MultiplicationPrecedence ) 等,你可以在這裏找 到完整的列表。如果沒有適合你的運算符的優先級組,你就需要像我們在例子中做得這 樣,自己指定結合律方式和優先級順序了。
  • associativity
    定義了結合律,即如果多個同類的操作符順序出現的計算順序。比如常⻅的加法和減法都 是 left ,就是說多個加法同時出現時按照從左往右的順序計算 (因爲加法滿足交換律,所 以這個順序無所謂,但是減法的話計算順序就很重要了)。點乘的結果是一個 Double ,不 再會和其他點乘結合使用,所以這裏是 none 。
  • higherThan
    運算的優先級,點積運算是優先於乘法運算的。除了 higherThan ,也支持使用 lowerThan 來指定優先級低於某個其他組。
  • infix
    表示要定義的是一箇中位操作符,即前後都是輸入;其他的修飾子還包括 prefix 和 postfix ,不再贅述;
            Swift 的操作符是不能定義在局部域中的,因爲至少會希望在能在全局範圍使用你的操作符,否則操作符也就失去意義了。另外,來自不同 module 的操作符是有可能衝突的,這對於庫開發者來說是需要特別注意的地方。像解決類型名衝突那樣通過指定庫名字來進行調用的。因此在重載或者自定義操作符時,應當盡 量將其作爲其他某個方法的 "簡便寫法",而避免在其中實現大量邏輯或者提供獨一無二的功能。

1.8 func 的參數修飾

        在Swift中參數默認是let的,如果想要修改參數則需要使用inout參數。

1.9 字面量表達

        所謂字面量,就是指像特定的數字,字符串或者是布爾值這樣,能夠直截了當地指出自己的類型併爲變量進行賦值的值。這些是經常可能會用到的一些字面量協議(ExpressibleByArrayLiteral、ExpressibleByBooleanLiteral、ExpressibleByDictionaryLiteral、ExpressibleByFloatLiteral、ExpressibleByNilLiteral、ExpressibleByIntegerLiteral、ExpressibleByStringLiteral),類型實現此協議後,就可以通過這些字面量創建對應的類型。

 protocol ExpressibleByBooleanLiteral {
    typealias BooleanLiteralType
    /// Create an instance initialized to `value`.
    init(booleanLiteral value: BooleanLiteralType)
}

 /// The default type for an otherwise-unconstrained boolean literal
typealias BooleanLiteralType = Bool

enum MyBool: Int {
    case myTrue, myFalse
}
extension MyBool: ExpressibleByBooleanLiteral {
    init(booleanLiteral value: Bool) {
      self = value ? .myTrue : .myFalse
  }
}

let myTrue: MyBool = true
let myFalse: MyBool = false
myTrue.rawValue // 0
myFalse.rawValue // 1

1.9 下標

extension Array {
  subscript(input: [Int]) -> ArraySlice<Element> {
    get {
      var result = ArraySlice<Element>()
      for i in input {
        assert(i < self.count, "Index out of range")
        result.append(self[i]) }
        return result
    }
    set {
        for (index,i) in input.enumerated() {
          assert(i < self.count, "Index out of range")
          self[i] = newValue[index]
        }
      }
  }
}

var arr = [1,2,3,4,5]
arr[[0,2,3]] //[1,3,4]
arr[[0,2,3]] = [-1,-3,-4]
arr   //[-1,2,-3,-4,5]

1.10 方法嵌套

        方法嵌套,爲一些較長的方法的重構提供了新的可能:這些拆分出來的方法又不能在其他地方使用,又沒必要暴露出去,通過在當前方法內嵌套方法,則有效解決此問題。

1.11 命名空間

        OC不支持命名空間,所以只能通過前綴來近似解決,由此導致OC中標識符一般都比較長。在Swift中,通過module來支持命名空間,能夠有效避免問題。其類似於C++中的namespace。

 // MyFramework.swift
// 這個文件存在於 MyFramework.framework 中 public class MyClass {
  public class func hello() {
    print("hello from framework")
  }
}
// MyApp.swift
// 這個文件存在於 app 的主 target 中
class MyClass {
  class func hello() {
    print("hello from app")
  }
}

 MyClass.hello()
// hello from app
MyFramework.MyClass.hello()
// hello from framework

1.12 typealias

        typealias是用來爲已經存在的類型重新定義名字的,通過命名,可以使代碼變得更加清晰。使用的語法也很簡單,使用 typealias 關鍵字像使用普通的賦值語句一樣,可以將某個已經存在的類型 賦值爲新的名字。

typealias Location = CGPoint
typealias Distance = Double

class Person<T> {
}
typealias Worker = Person

// 以下代碼編譯錯誤
typealias Worker = Person<T> // Cannot find type 'T' in scope

// 通過以下方法修正
typealias Worker<T> = Person<T>
  
// 另外一種用法
protocol Cat { ... }
protocol Dog { ... }
typealias Pat = Cat & Dog

1.13 associatedtype

        associatedtype用於協議,使協議支持泛型。具有此關鍵字的協議不能用於類型,只能用於類型約束。

1.14 可變參數函數

        在Swift中,實現可變參數大大簡單起來。

func myFunc(numbers: Int..., string: String) {
    numbers.forEach {
      for i in 0..<$0 {
      print("\(i + 1): \(string)")
    }
  }
}
myFunc(numbers: 1, 2, 3, string: "hello") // 輸出:
// 1: hello
// 1: hello
// 2: hello
// 1: hello
// 2: hello
// 3: hello
  • 可變參數可以在函數列表的任意位置
  • 在同一個方法中只能有一個參數是可變的
  • 可變參數都必須是同一種類型的等

1.15 初始化方法順序

        在某個類的子類中,初始化方法里語句的順序並不是隨意的,我們需要保證在當前子類實例的成員初始化完成後才能調用父類的初始化方法。
        一般來說,子類的初始化順序是:

  • 設置子類自己需要初始化的參數
  • 調用父類的相應的初始化方法(可選的,如果不寫,系統自動幫我們調用;如果有第三步的語句,則此語句不可省略)
  • 對父類中的需要改變的成員進行設定(可選的,如果不需要,則不用調用)

1.16 Designated,Convenience 和 Required

        一方面,Swift 強化了 designated 初始化方法的地位。 Swift 中不加修飾的 init 方法都需要在方法中保證所有非 Optional 的實例變量被賦值初始化,而在 子類中也強制 (顯式或者隱式地) 調用 super 版本的 designated 初始化。
        與 designated 初始化方法對應的是在 init 前加上 convenience 關鍵字的初始化方法。這類方法 是 Swift 初始化方法中的 “二等公⺠”,只作爲補充和提供使用上的方便。所有的 convenience 初始 化方法都必須調用同一個類中的 designated 初始化完成設置,另外 convenience 的初始化方法是 不能被子類重寫或者是從子類中以 super 的方式被調用的。

  • 初始化路徑必須保證對象完全初始化,這可以通過調用本類型的 designated 初始化方法來得 到保證;
  • 子類的 designated 初始化方法必須調用父類的 designated 方法,以保證父類也完成初始 化。
            對於某些我們希望子類中一定實現的 designated 初始化方法,我們可以通過添加 required 關鍵 字進行限制,強制子類對這個方法重寫實現。這樣做的最大的好處是可以保證依賴於某個 designated 初始化方法的 convenience 一直可以被使用。

備註
此處說明並不詳細,其實實際情況比此處描述的要複雜,不過日常也不經常用到那麼複雜的初始化放阿飛。如果需要連接,可以參考官方的《Swift編程語言》。

1.17 初始化返回 nil

  • 初始化方法沒有必要使用return語句
  • 對於可能失敗的初始化方法,可以顯示return nil

1.18 static 和 class

  • static、class的作用相同,都是標明類型相關的屬性、方法
  • 建議一直使用static,來避免class導致的,值類型不適用問題

1.19 多類型和容器

        通常情況下,Array、Dictionary、Set都是存儲相同類型。但是,在某些特定情況下,集合類型也可以存儲不同類型。

  • 可以通過存儲Any、AnyObject來實現存儲不同類型
  • 通過存儲遵循某種協議的對象
// 以下是另外實現存儲多種不同類型的方法
// 此種方法可以保留編譯器的檢查
enum IntOrString {
    case IntValue(Int)
    case StringValue(String)
}
let mixed = [IntOrString.IntValue(1), IntOrString.StringValue("two"),
IntOrString.IntValue(3)]
for value in mixed {
    switch value {
  case let .IntValue(i):
    print(i * 2)
  case let .StringValue(s):
    print(s.capitalized)
} }
// 輸出: // 2
// Two // 6

1.20 default 參數

        Swift 的方法是支持默認參數的,也就是說在聲明方法時,可以給某個參數指定一個默認使用的值。在調用該方法時要是傳入了這個參數,則使用傳入的值,如果缺少這個輸入參數,那麼直接使用設定的默認值進行調用。
        和其他很多語言的默認參數相比較,Swift 中的默認參數限制更少,並沒有所謂 "默認參數之後不 能再出現無默認值的參數"這樣的規則,舉個例子,下面兩種方法的聲明在 Swift 裏都是合法可用的:

func sayHello1(str1: String = "Hello", str2: String, str3: String) {
  print(str1 + str2 + str3)
}

func sayHello2(str1: String, str2: String, str3: String = "World") {
  print(str1 + str2 + str3)
}

sayHello1(str2: " ", str3: "World")
sayHello2(str1: "Hello", str2: " ")
//輸出都是 Hello World
 

        與上面不同的另外一種指定默認參數的方法如下面代碼所示。default指定的默認值,在方法內部來給出。

func NSLocalizedString(key: String,
                                      tableName: String? = default,
                                      bundle: NSBundle = default,
                                      value: String = default,
                                      comment: String) -> String

1.21 正則表達式

        Swift目前尚未提供語言層面的正則表達式支持功能。但是,可以通過自定義運算符來實現近似的支持。

 struct RegexHelper {
  let regex: NSRegularExpression

  init(_ pattern: String) throws {
    try regex = NSRegularExpression(pattern: pattern, options: .caseInsensitive)
  }

  func match(_ input: String) -> Bool {
    let matches = regex.matches(in: input, options: [],range: NSMakeRange(0, input.utf16.count))
    return matches.count > 0
    }
}

let mailPattern = "^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$"
let matcher: RegexHelper
do {
  matcher = try RegexHelper(mailPattern)
}

let maybeMailAddress = "[email protected]"
if matcher.match(maybeMailAddress) {
  print("有效的郵箱地址")
}
// 輸出:
// 有效的郵箱地址


precedencegroup MatchPrecedence {
    associativity: none
    higherThan: DefaultPrecedence
}
infix operator =~: MatchPrecedence
func =~(lhs: String, rhs: String) -> Bool {
  do {
    return try RegexHelper(rhs).match(lhs)
  } catch _ {
        return false
  }
}

 if "[email protected]" =~ "^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$" {
  print("有效的郵箱地址")
}
// 輸出:
// 有效的郵箱地址

1.22 模式匹配

        此處講述的模式匹配比較簡單,如果想要深入瞭解,可以參考如下文檔。

  1. 模式匹配第一彈: switch, enums & where 子句
  2. 模式匹配第二彈:元組,range 和類型
  3. 模式匹配第三彈: 自定義的模式匹配和語法糖
  4. 模式匹配第四彈:if case,guard case,for case

1.23 ... 和 ..<

        範圍操作符,一個支持[],左閉右閉,一個支持[),左閉右開。

1.24 AnyClass,元類型和 .self

在 Swift 中能夠表示 “任意” 這個概念的除了 Any 和 AnyObject 以外,還有一個AnyClass 。 AnyClass 在 Swift 中被一個 typealias 所定義:

  typealias AnyClass = AnyObject.Type

        通過 AnyObject.Type 這種方式所得到是一個元類型 (Meta)。在聲明時我們總是在類型的名稱後面 加上 .Type ,比如 A.Type 代表的是 A 這個類型的類型。也就是說,我們可以聲明一個元類型來 存儲 A 這個類型本身,而在從 A 中取出其類型時,我們需要使用到 .self :

 class A {
}
let typeA: A.Type = A.self

        AnyClass 所表達的東⻄其實並沒有什 麼奇怪,就是任意類型本身

 class A {
}
let typeA: AnyClass = A.self
class A {
  class func method() {
    print("Hello")
  }
}

let typeA: A.Type = A.self typeA.method()
// 或者
let anyClass: AnyClass = A.self
(anyClass as! A.Type).method()

1.25 協議和類方法中的 Self

        Self指實現協議的自身。

1.26 動態類型和多方法

        Swift默認使用靜態消息派發。

class Pet {}
class Cat: Pet {}
class Dog: Pet {}
func printPet(_ pet: Pet) { print("Pet")
}
func printPet(_ cat: Cat) { print("Meow")
}
func printPet(_ dog: Dog) { print("Bark")
}

printPet(Cat()) // Meow
printPet(Dog()) // Bark
printPet(Pet()) // Pet

func printThem(_ pet: Pet, _ cat: Cat) {
    printPet(pet)
    printPet(cat)
}
printThem(Dog(), Cat())
// 輸出: // Pet // Meow

1.27 屬性觀察

        屬性觀察 (Property Observers) 是 Swift 中一個很特殊的特性,利用屬性觀察我們可以在當前類型 內監視對於屬性的設定,並作出一些響應。Swift 中爲我們提供了兩個屬性觀察的方法,它們分別 是 willSet 和 didSet 。

class MyClass {
  var date: NSDate {
  willSet {
      let d = date
      print("即將將日期從 \(d) 設定至 \(newValue)")
    }
    didSet {
      print("已經將日期從 \(oldValue) 設定至 \(date)")
    }
  }

  init() {
    date = NSDate()
    }
 }

let foo = MyClass()
foo.date = foo.date.addingTimeInterval(10086)

// 輸出
// 即將將日期從 2014-08-23 12:47:36 +0000 設定至 2014-08-23 15:35:42 +0000 // 已經將日期從 2014-08-23 12:47:36 +0000 設定至 2014-08-23 15:35:42 +0000

1.28 final

        final 關鍵字可以用在 class , func 或者 var 前面進行修飾,表示不允許對該內容進行繼承或 者重寫操作。
        給一段代碼加上 final 就意味着編譯器向你作出保證,這段代碼不會再被修改;同時,這也意味 着你認爲這段代碼已經完備並且沒有再被進行繼承或重寫的必要,因此這往往會是一個需要深思 熟慮的決定。

  • 類或者方法的功能確實已經完備了
  • 子類繼承和修改是一件危險的事情
  • 爲了父類中某些代碼一定會被執行
  • 性能考慮

1.29 lazy 修飾符和 lazy 方法

        在 Swift 中我們使用在變量屬性前加 lazy 關鍵字的方式來簡單地指定延時加載。我們在使用 lazy 作爲屬性修飾符時,只能聲明屬性是變量。另外我們需要顯式地指定屬性類型,並使用一個可以對這個屬性進行賦值的語句來在首次訪問屬性時運行。如果我們多次訪問這 個實例的 str 屬性的話,可以看到只有一次輸出。

 class ClassA {
  lazy var str: String = {
    let str = "Hello" print("只在首次訪問輸出") return str
  }()
}

// 爲了簡化,我們如果不需要做什麼額外工作的話,也可以對這個 lazy 的屬性直接寫賦值語句:
 lazy var str: String = "Hello"

        關於容器的lazy用法參見:Swift高階函數解析

1.30 Reflection 和 Mirror

struct Person {
   let name: String
   let age: Int

    func test() -> () {
        print("sdfsd")
    }
}
let xiaoMing = Person(name: "XiaoMing", age: 16)
let r = Mirror(reflecting: xiaoMing) // r 是 MirrorType
print("xiaoMing 是 \(r.displayStyle!)")
print("屬性個數:\(r.children.count)")
for child in r.children {
    print("屬性名:\(String(describing: child.label)),值:\(child.value)")
}
// 輸出:
// xiaoMing 是 Struct
// 屬性個數:2
// 屬性名:name,值:XiaoMing // 屬性名:age,值:16

1.31 隱式解包 Optional

  var maybeObject: MyClass!

1.32 多重 Optional

var aNil: String? = nil
var anotherNil: String?? = aNil
var literalNil: String?? = nil

// 輸出anotherNil
if anotherNil != nil {
  print("anotherNil") 
}
// 沒有輸出
if literalNil != nil {
  print("literalNil")
}

1.32 Optional Map

let num: Int? = 3
let result = num.map {
  $0 * 2
}
// result 爲 {Some 6}

1.33 Protocol Extension

  • 如果類型推斷得到的是實際的類型
    • 那麼類型中的實現將被調用;如果類型中沒有實現的話,那麼協議擴展中的默認實現將被使用
  • 如果類型推斷得到的是協議,而不是實際類型
    • 並且方法在協議中進行了定義,那麼類型中的實現將被調用;如果類型中沒有實現,那 麼協議擴展中的默認實現被使用
    • 否則 (也就是方法沒有在協議中定義),擴展中的默認實現將被調用

1.34 where 和模式匹配

//  switch 語句中,可以使用 where 來限定某些條件 case:
let name = ["王小二","張三","李四","王二小"]
name.forEach { switch $0 {
  case let x where x.hasPrefix("王"):
    print("\(x)是筆者本家")
  default:
    print("你好,\($0)")
  }
}
// 輸出:
// 王小二是筆者本家 // 你好,張三
// 你好,李四
// 王二小是筆者本家

// 在 for 中我們也可以使用 where 來做類似的條件限定:

extension Sequence where Self.Iterator.Element : Comparable {     public func sorted() -> [Self.Iterator.Element]
}

1.35 indirect 和嵌套 enum

        參見《Swift進階》中的枚舉的介紹。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章