這是一本比基礎的Swift教程稍微深入一點的書籍,特別適合看過官方的教程之後的提高。總體來說,本書並不是很深入的書籍,適合剛接觸Swift的讀者,以及想要充實自己基礎知識的讀者。
一、Swift新元素
1.1 柯里化
簡單來說,柯里化就是把方法、函數的參數個數減少。柯里化主要用在函數式編程中,筆者認爲其主要作用有以下幾個:
- 把具有不同參數個數的函數、方法統一爲只有一個(或相同參數個數)參數,簡單來說就是:把原本不統一的類型給統一
- 創建一個函數模板(下面的示例代碼進行了展示)
// 如果想要寫一個給輸入參數+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 模式匹配
此處講述的模式匹配比較簡單,如果想要深入瞭解,可以參考如下文檔。
- 模式匹配第一彈: switch, enums & where 子句
- 模式匹配第二彈:元組,range 和類型
- 模式匹配第三彈: 自定義的模式匹配和語法糖
- 模式匹配第四彈: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進階》中的枚舉的介紹。
二、從 Objective-C/C 到 Swift
2.1 Selector
在Swift當中,#selector的作用與@selector相同。但是要注意,#selector是爲OC而存在,所以,參數需要有@objc。
2.2 實例方法的動態調用
這是一個奇怪的語法,有什麼用?誰知道可以告訴我,謝謝!
class MyClass {
func method(number: Int) -> Int {
return number + 1
}
}
// 日常調用實例方法的方法
let object = MyClass()
let result = object.method(number: 1)
// 奇怪的調用方法
let f = MyClass.method
// f: MyClass -> (Int) -> Int 柯里化方法
// 類似這樣 let f = { (obj: MyClass) in obj.method }
let object = MyClass()
let result = f(object)(1)
// 對於屬性的 getter 或者 setter 是不能用類似的寫法的
////////////////////////////////////////////////////
class MyClass {
func method(number: Int) -> Int {
return number + 1
}
class func method(number: Int) -> Int {
return number
}
}
// MyClass.method 將取到的是類型方法
let f1 = MyClass.method
// class func method 的版本
let f2: (Int) -> Int = MyClass.method // 和 f1 相同
let f3: (MyClass) -> (Int) -> Int = MyClass.method // func method 的柯里化版本
2.3 單例
在Swift當中,單例的寫法得到大大的簡化。
// OC 中的單例的寫法
@implementation MyManager
+ (id)sharedManager {
static MyManager * staticInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
staticInstance = [[self alloc] init];
});
return staticInstance;
}
@end
class MyManager {
static let shared = MyManager()
private init() {}
}
// 在初始化類變量的時候,Apple 將會把這個初 始化包裝在一次 swift_once_block_invoke 中,以保證它的唯一性。
// 不僅如此,對於所有的全局變 量,Apple 都會在底層使用這個類似 dispatch_once 的方式來確保只以 lazy 的方式初始化一次。
2.4 條件編譯
- Swift當中沒有宏,所以提供了其他的一些方法來實現條件編譯
// 這種形式的指令還存在,但是condition則不同
#if <condition>
#elseif <condition>
#else
#endif
// 如下所示
#if os(macOS)
typealias Color = NSColor
#else
typealias Color = UIColor
#endif
方法 | 可選參數 |
---|---|
os() | macOS, iOS, tvOS, watchOS, Linux |
arch() | x86_64, arm, arm64, i386 |
swift() | >= 某個版本 |
2.5 編譯標記
- 在Swift中// MARK: 或 // MARK -: 與#param的作用相同
- swift還支持 // TODO: // FIXME:
- 在OC中支持 // WARNING: Add your API key here
- #warning("This is bad method of encryption")
- #error("This is bad method of encryption")
2.6 @UIApplicationMain
// 這作爲入口函數,在OC程序存在
int main(int argc, char * argv[])
{
@autoreleasepool {
return UIApplicationMain(argc, argv, nil,
}
}
// 在Swift中以下代碼起到了相同的作用
@UIApplicationMain
// 也可以刪除以上代碼,自己手動提供main函數
// 手動創建main.swift,添加如下代碼即可
UIApplicationMain(Process.argc, Process.unsafeArgv, nil, NSStringFromClass(AppDelegate))
// 也可以按照如下方式來做
import UIKit
class MyApplication: UIApplication {
override func sendEvent(event: UIEvent!) {
super.sendEvent(event)
print("Event sent: \(event)");
}
}
UIApplicationMain(Process.argc, Process.unsafeArgv, NSStringFromClass(MyApplication), NSStringFromClass(AppDelegate))
2.7 @objc 和 dynamic
通過 @objc 和 dynamic來實現Swift與OC的互操作性。
2.8 可選協議和協議擴展
- OC中協議支持可選與必須‘
- 在Swift中協議的方法都是必須的
// 如果我們想要像 Objective-C 裏那樣定義可選的協議方法,就需要將協議本身和可選方法都定義爲 Objective-C 的,也即在 protocol 定義之前以及協議方法之前加上 @objc 。
// 另外和 Objective-C 中的@optional 不同,我們使用沒有 @ 符號的關鍵字 optional 來定義可選方法:
@objc protocol OptionalProtocol {
@objc optional func optionalMethod()
}
// 另外,對於所有的聲明,它們的前綴修飾是完全分開的
// 也就是說你不能像是在 Objective-C 裏那 樣用一個 @optional 指定接下來的若干個方法都是可選的了,必須對每一個可選方法添加前綴, 對於沒有前綴的方法來說,它們是默認必須實現的:
@objc protocol OptionalProtocol {
@objc optional func optionalMethod() // 可選
func necessaryMethod() // 必須
@objc optional func anotherOptionalMethod() // 可選
}
// 一個不可避免的限制是,使用 @objc 修飾的 protocol 就只能被 class 實現了,也就是說,對於 struct 和 enum 類型,我們是無法令它們所實現的協議中含有可選方法或者屬性的。
// 另外,實現它的 class 中的方法還必須也被標註爲 @objc ,或者整個類就是繼承自 NSObject 。這對我們寫代 碼來說是一種很讓人鬱悶的限制。
// 在 Swift 2.0 中,我們有了另一種選擇,那就是使用 protocol extension。我們可以在聲明一個 protocol 之後再用 extension 的方式給出部分方法默認的實現。這樣這些方法在實際的類中就是可 選實現的了。
// 還是舉上面的例子,使用協議擴展的話,會是這個樣子:
protocol OptionalProtocol {
func optionalMethod() // 可選
func necessaryMethod() // 必須
func anotherOptionalMethod() // 可選
}
extension OptionalProtocol {
func optionalMethod() {
print("Implemented in extension")
}
func anotherOptionalMethod() {
print("Implemented in extension")
}
}
class MyClass: OptionalProtocol {
func necessaryMethod() {
print("Implemented in Class3")
}
func optionalMethod() {
print("Implemented in Class3")
}
}
let obj = MyClass()
obj.necessaryMethod() // Implemented in Class3
obj.optionalMethod() // Implemented in Class3
obj.anotherOptionalMethod() // Implemented in extension
2.9 內存管理,weak 和 unowned
Swift 是自動管理內存的,這也就是說,我們不再需要操心內存的申請和分配。當我們通過初始化 創建一個對象時,Swift 會替我們管理和分配內存。而釋放的原則遵循了自動引用計數 (ARC) 的規 則:當一個對象沒有引用的時候,其內存將會被自動回收。這套機制從很大程度上簡化了我們的 編碼,我們只需要保證在合適的時候將引用置空 (比如超過作用域,或者手動設爲 nil 等),就可 以確保內存使用不出現問題。但是,所有的自動引用計數機制都有一個從理論上無法繞過的限制,那就是循環引用 (retain cycle) 的情況。
2.9.1 什麼是循環引用
class A: NSObject {
let b: B
override init() {
b = B()
super.init()
b.a = self
}
deinit {
print("A deinit")
}
}
class B: NSObject {
var a: A? = nil
deinit {
print("B deinit")
}
}
// A的b屬性引用了B,B的a屬性又引用了A,所以出現循環應用
2.9.2 在 Swift 裏防止循環引用
// 通過weak來解除循環引用
class B: NSObject {
weak var a: A? = nil
deinit {
print("B deinit")
}
}
// unowned 設置以後即使它原來引用的內容已經被釋放了,它仍然會保 持對被已經釋放了的對象的一個 "無效的" 引用,它不能是 Optional 值,也不會被指向 nil
// 關於兩者使用的選擇,Apple 給我們的建議是如果能夠確定在訪問時不會已被釋放的話,儘量使用 unowned ,如果存在被釋放的可能,那就選擇用 weak
2.9.3 閉包和循環引用
class Person {
let name: String
lazy var printName: ()->() = {
print("The name is \(self.name)")
}
init(personName: String) {
name = personName
}
deinit {
print("Person deinit \(self.name)")
}
}
var xiaoMing: Person? = Person(personName: "XiaoMing") xiaoMing!.printName()
xiaoMing = nil
// 輸出:
// The name is XiaoMing,沒有被釋放
// 修復方法
lazy var printName: ()->() = {
[weak self] in
if let strongSelf = self {
print("The name is \(strongSelf.name)")
}
}
2.10 @autoreleasepool
func autoreleasepool(code: () -> ())
func loadBigData() {
if let path = NSBundle.mainBundle().pathForResource("big", ofType: "jpg") {
for i in 1...10000 { autoreleasepool {
let data = NSData.dataWithContentsOfFile(path, options: nil, error: nil)
NSThread.sleepForTimeInterval(0.5)
}
}
}
2.11 值類型和引用類型
- 值類型(struct、enum、元組)
- 引用類型(class、方法、函數)
- 由於寫時複製技術,使值類型在很多時候性能都損失都較小,推薦一般使用值類型(函數式編程、面向協議編程、泛型編程都對值類型有偏好,特別是函數式編程)
- 對於經常變動的數組、字典,推薦使用NSMutableArray、NSMutableDictionary
2.12 String 還是 NSString
儘量使用String,原因有三
- Cocoa 所有的 API 都接受和返回 String 類型,沒有必要也不必給自己憑空添加麻煩去把框架中返回的字符串做一遍轉換
- 因爲在 Swift 中 String 是 struct,相比起 NSObject 的 NSString 類來說,更切合字符串的 "不變" 這一特性
- 因爲String實現了 Collection 這樣的協議,因此有些 Swift 的語法特性只有 String 才能 使用,而 NSString 是沒有的
2.13 UnsafePointer
爲了與龐大的 C 系帝國進行合作,Swift 定義了一套對 C 語言指針的訪問和轉換方法,那就是 UnsafePointer 和它的一系列變體。對於使用 C API 時如果遇到接受內存 地址作爲參數,或者返回是內存地址的情況,在 Swift 裏會將它們轉爲 UnsafePointer<Type> 的類型。
// C的方法
void method(const int *num) {
printf("%d",*num);
}
// 對應的Swift方法
func method(_ num: UnsafePointer<CInt>) {
print(num.pointee)
}
// 調用
var a: CInt = 123
method(&a) // 輸出 123
C API | Swift API |
---|---|
const Type * | UnsafePointer |
Type * | UnsafeMutablePointer |
2.14 C 指針內存管理
C 指針在 Swift 中被冠名以 unsafe 的另一個原因是無法對其進行自動的內存管理。和 Unsafe 類 的指針工作的時候,我們需要像 ARC 時代之前那樣手動地來申請和釋放內存,以保證程序不會出 現泄露或是因爲訪問已釋放內存而造成崩潰。
如果有C、C++的內管管理經驗的話,會比較好理解。
var pointer: UnsafeMutablePointer<MyClass>! // 聲明指針
pointer = UnsafeMutablePointer<MyClass>.allocate(capacity: 1)// 分配內存
pointer.initialize(to: MyClass())// 初始化
print(pointer.pointee.a)// 使用
pointer.deinitialize() // 反初始化
pointer.deallocate(capacity: 1)// 銷燬內存
pointer = nil// 防止懸浮指針
// 輸出: // 1
// deinit
2.15 COpaquePointer 和 C convention
應用場景很少,就不做介紹了。
2.16 GCD 和延時調用
let workingQueue = DispatchQueue(label: "my_queue")// 派發到剛創建的隊列中,GCD 會負責進行線程調度
workingQueue.async {
// 在 workingQueue 中異步進行
print("努力工作")
Thread.sleep(forTimeInterval: 2) // 模擬兩秒的執行時間
DispatchQueue.main.async {
// 返回到主線程更新 UI
print("結束工作,更新 UI")
}
}
// 延遲調用
let time: TimeInterval = 2.0 DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + time) {
print("2 秒後輸出")
}
2.17 獲取對象類型
let date = NSDate()
// 方法一
let name: AnyClass! = object_getClass(date)
print(name)
// 輸出:
// __NSDate
// 方法二
let name = type(of: date) print(name)
// 輸出:
// __NSDate
// 輸出Swift對象的類型
let string = "Hello"
let name = type(of: string) print(name)
// 輸出:
// String
2.18 自省
“我是誰”這個哲學問題,在程序設計中就叫做自省(Introspection)。
在絕大多數情況下,我們使用的 Swift 類型都應該是在編譯期間就確定 的。如果在你寫的代碼中經常需要檢查和確定 AnyObject 到底是什麼類的話,幾乎就意味着你的 代碼設計出了問題 (或者你正在寫一些充滿各種 "小技巧" 的代碼)。
在Swift中使用is類進行類型判斷。
2.19 KeyPath 和 KVO
結合KeyPath,KVO在Swift中好用了一些。
2.20 局部 scope
// 局部作用於的一種實現方法
func local(_ closure: ()->()) {
closure()
}
override func loadView() {
let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 480))
view.backgroundColor = .white
local {
let titleLabel = UILabel(frame: CGRect(x: 150, y: 30, width: 200, height: 40))
titleLabel.textColor = .red
titleLabel.text = "Title"
view.addSubview(titleLabel)
}
local {
let textLabel = UILabel(frame: CGRect(x: 150, y: 80, width: 200, height: 40))
textLabel.textColor = .red
textLabel.text = "Text"
view.addSubview(textLabel)
}
self.view = view
}
// 局部作用於的另外一種實現方法
do {
let textLabel = UILabel(frame: CGRect(x: 150, y: 80, width: 200, height: 40))
//...
}
// 局部作用於的另外一種實現方法——匿名閉包
let titleLabel: UILabel = {
let label = UILabel(frame: CGRect(x: 150, y: 30, width: 200, height: 40))
label.textColor = .red
label.text = "Title"
return label
}()
2.21 判等
沒啥可說的。
2.22 哈希
Hashable是個很重要的協議,Swift的基礎數據類型都實現了,對於字典也很重要。
2.22 類簇
class Drinking {
typealias LiquidColor = UIColor
var color: LiquidColor {
return .clear
}
class func drinking(name: String) -> Drinking {
var drinking: Drinking
switch name {
case "Coke":
drinking= Coke()
case "Beer":
drinking= Beer()
default:= Drinking()
drinking
}
return drinking
}
class Coke: Drinking {
override var color: LiquidColor {
return .black
}
}
class Beer: Drinking {
override var color: LiquidColor {
return .yellow
}
}
let coke = Drinking.drinking(name: "Coke")
coke.color // Black
let beer = Drinking.drinking(name: "Beer") beer.color // Yellow
2.23 調用 C 動態庫
// 在Swift中通過OC來調用C
// TargetName-Bridging-Header.h
#import <CommonCrypto/CommonCrypto.h>
// StringMD5.swift
extension String {
var MD5: String {
var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
if let data = data(using: .utf8) {
data.withUnsafeBytes {
(bytes: UnsafePointer<UInt8>) -> Void in
CC_MD5(bytes, CC_LONG(data.count), &digest)
}
}
var digestHex = ""
for index in 0..<Int(CC_MD5_DIGEST_LENGTH) {
digestHex += String(format: "%02x", digest[index])
}
return digestHex
}
}
// 測試 print("swifter.tips".MD5)
// 輸出
// dff88de99ff03d109de22fed4f71a273
2.24 輸出格式化
在 Swift 裏,我們在輸出時一般使用的 print 中是支持字符串插值的,而字符串插值時將直接使 用類型的 Streamable , Printable 或者 DebugPrintable 協議 (按照先後次序,前面的沒有實現的話 則使用後面的) 中的方法返回的字符串並進行打印。
let format = String(format:"%.2f",b)
print("double:\(format)")
// 輸出: double:1.23
// 如果想要實現簡單一點的寫法,可以如下
extension Double {
func format(_ f: String) -> String {
return String(format: "%\(f)f", self)
}
}
let f = ".2"
print("double:\(b.format(f))")
2.25 Options
沒啥可說的。
2.26 數組 enumerate
// 得到下標與元素值的同時進行遍歷
var result = 0
for (idx, num) in [1,2,3,4,5].enumerated() {
result += num
if idx == 2 {
break
}
}
print(result)
2.27 類型編碼 @encode
char *typeChar1 = @encode(int32_t);
char *typeChar2 = @encode(NSArray);
// typeChar1 = "i", typeChar2 = "{NSArray=#}"
2.28 C 代碼調用和 @asmname
我們甚至還有一種不需要藉助頭文件和 Bridging-Header 來導入 C 函數的方法,那就是使用 Swift 中的一個隱藏的符號 @asmname 。 @asmname 可以通過方法名字將某個 C 函數直接映射爲 Swift 中的函數。
//File.swift
//將 C 的 test 方法映射爲 Swift 的 c_test 方法
@asmname("test") func c_test(a: Int32) -> Int32
func testSwift(input: Int32) {
let result = c_test(input)
print(result)
}
testSwift(1)
// 輸出:2
2.29 delegate
protocol MyClassDelegate {
func method()
}
class MyClass {
weak var delegate: MyClassDelegate?
}
class ViewController: UIViewController, MyClassDelegate {
// ...
var someInstance: MyClass!
override func viewDidLoad() {
super.viewDidLoad()
someInstance = MyClass()
someInstance.delegate = self
}
func method() {
print("Do something")
}
//...
}
// weak var delegate: MyClassDelegate? 編譯錯誤
// 'weak' cannot be applied to non-class type 'MyClassDelegate'
// 這是因爲 Swift 的 protocol 是可以被除了 class 以外的其他類型遵守的,而對於像 struct 或是 enum 這樣的類型,本身就不通過引用計數來管理內存,所以也不可能用 weak 這樣的 ARC 的概念來進行修飾。
// 解決方案一
@objc protocol MyClassDelegate {
func method()
}
// 解決方案二
protocol MyClassDelegate: class {
func method()
}
// 解決方案三
protocol MyClassDelegate: AnyObject {
func method()
}
2.30 Associated Object
// MyClass.swift
class MyClass {
}
// MyClassExtension.swift
private var key: Void?
extension MyClass {
var title: String? {
get {
return objc_getAssociatedObject(self, &key) as? String
}
set {
objc_setAssociatedObject(self,&key, newValue,.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
// 測試
func printTitle(_ input: MyClass) {
if let title = input.title {
print("Title: \(title)")
} else {
print("沒有設置")
}
}
let a = MyClass() printTitle(a)
a.title = "Swifter.tips" printTitle(a)
// 輸出:
// 沒有設置
// Title: Swifter.tips
2.31 Lock
func myMethod(anObj: AnyObject!) {
objc_sync_enter(anObj)
// 在 enter 和 exit 之間持有 anObj 鎖
objc_sync_exit(anObj)
}
2.32 Toll-Free Bridging 和 Unmanaged
沒啥可說的。
三、Swift與開發環境及一些實踐
3.1 Swift命令行工具
swiftc --help 通過此命令,可以查看一些有用的命令。
3.2 隨機數生成
使用arc4random_uniform函數代替arc4random
3.3 print和debugPrint
如果想要自定義對象能夠在調試、字符串插值時輸出有意義的信息,可以實現CustomStringConvertible、CustomDebugStringConvertible協議。
3.4 錯誤和異常處理
講了swift對錯誤與異常的處理,講了Result類型。
3.5 斷言
簡單的對斷言進行了講解,沒有新的內容。
3.6 fatalError
@noreturn
func fatalError(@autoclosure message: () -> String = default,
file: StaticString = default,
line: UInt = default)
// 對於一些致命錯誤,可以使用此函數處理
3.7 代碼組織和 Framework
apple 爲了 iOS 平臺的安全性考慮,是不允許動態鏈接非系統的框架的。因此在 app 開發中我們所使用的第三方框架如果是以庫文件的方式提供的話,一定都是需要鏈接並打包進最後的二進制 可執行文件中的靜態庫。最初級和原始的靜態庫是以 .a 的二進制文件加上一些 .h 的頭文件進 行定義的形式提供的,這樣的靜態庫在使用時比較麻煩,我們除了將其添加到項目和配置鏈接 外,還需要進行指明頭文件位置等工作。這樣造成的結果不僅是添加起來比較麻煩,而且因爲頭 文件的路徑可能在不同環境下會存在不一樣的情況,而造成項目在換一個開發環境後就因爲配置 問題造成無法編譯。有過這種經歷的開發人員都知道,調配開發環境是一件非常讓人討厭和耗費時間的事情。
3.8 安全的資源組織方式
// 比直接使用字符串來標識資源方式更好,類似於OC中的宏
enum ImageName: String {
case myImage = "my_image"
}
enum SegueName: String {
case mySegue = "my_segue"
}
extension UIImage {
convenience init!(imageName: ImageName) {
self.init(named: imageName.rawValue)
}
}
extension UIViewController {
func performSegue(withName segueName: SegueName, sender: Any?) {
performSegue(withIdentifier: segueName.rawValue, sender: sender)
}
}
3.9 Playground 延時運行
此種用法很少,無需記錄。
3.10 Playground 與項目協作
此種用法很少,無需記錄。
3.11 Playground 可視化開發
此種用法很少,無需記錄。
3.12 數學和數字
- Darwin 裏的 math.h 定義了很多和數學相關的內容,它在 Swift 中也被進行了 module 映射,因此 在 Swift 中我們是可以直接使用的
- 在標準庫中對極限情況的數字做了一些約定
- Int.max 和 Int.min
- infinity(無窮) 和 NaN (Not a Number)
3.13 JSON 和 Codable
- 只要一個類型中所有的成員都實現了 Codable ,那麼這個類型也就可以自動滿足 Codable 的要 求
- 在 Swift 標準庫中,像是 String , Int , Double , Date 或者 URL 這樣的類型是默認就實現 了 Codable 的,因此我們可以簡單地基於這些常⻅類型來構建更復雜的 Codable 類型
- 另外,如果 JSON 中的 key 和類型中的變量名不一致的話 (這很常⻅,因爲 JSON 中往往使用下劃線命名 key 值,而 Swift 中的命名規則一般是駝峯式),我們還需要在對應類中聲明 CodingKeys 枚舉,並 用合適的鍵值覆蓋對應的默認值,上例中 Popup 和 MenuItem 都屬於這種情況
3.14 NSNull
在Swift中無需處理了。
3.15 文檔註釋
/**
A demo method
- parameter input: An Int number
- returns: The string represents the input number
*/
func method(input: Int) -> String {
return String(input)
}
3.16 性能考慮
大幅降低動態方法派發引起的方法查找性能損耗,來提高性能。
3.17 Log 輸出
符號 | 類型 | 描述 |
---|---|---|
#file | String | 包含這個符號的文件的路徑 |
#line | Int | 符號出現處的行號 |
#column | Int | 符號出現處的列 |
#function | String | 包含這個符號的方法名字 |
3.18 溢出
溢出發生時程序會發生崩潰,如果不想接受此默認方法,則可以使用以下運算符,通過他們來進行截斷而不是溢出。
- 溢出加法 ( &+ )
- 溢出減法 ( &- )
- 溢出乘法 ( &* )
- 溢出除法 ( &/ )
- 溢出求模 ( &% )
3.19 宏定義 define
以下是替代宏的一些做法:
- 使用合適作用範圍的 let 或者 get 屬性來替代原來的宏定義值
var M_PI: Double {
get {
3.24
}
}
- 通過#if等條件編譯取代宏的作用來實現條件編譯
3.20 屬性訪問控制
- internal 默認的訪問控制
- private 讓代碼只能在當前作用域或者同一文件中同一類型的作用域中被使用
- fileprivate 表示代碼可以在當前文件中被訪問,而不做類型限定
- open 開放給其他框架;在別的框架中被繼承或者重寫
- public 開放給其他框架
3.21 Swift 中的測試
無話可說
3.22 Core Data
無話可說
3.23 閉包歧義
講了很久之前的一個問題,現在已經不存在了。
3.23 泛型擴展
與爲普通的類型添加擴展不同的是,泛型類型在類型定義時就引入了類型標誌,我們可以直接使用。
3.24 兼容性
無話可說
3.25 列舉 enum 類型
枚舉值是可以自舉的。
3.26 尾遞歸
一般對於遞歸,解決棧溢出的一個好方法是採用尾遞歸的寫法。顧名思義,尾遞歸就是讓函數裏的最後一個動作是一個函數調用的形式,這個調用的返回值將直接被當前函數返回,從而避免在 棧上保存狀態。這樣一來程序就可以更新最後的棧幀,而不是新建一個,來避免棧溢出的發生。
// 非尾遞歸,可能堆棧溢出
func sum(_ n: UInt) -> UInt {
if n == 0 {
return 0
}
return n + sum(n - 1)
}
sum(4) // 10
sum(100) // 5050
// 尾遞歸版本
func tailSum(_ n: UInt) -> UInt {
func sumInternal(_ n: UInt, current: UInt) -> UInt {
if n == 0 {
return current
} else {
return sumInternal(n - 1, current: current + n)
}
}
return sumInternal(n, current: 0)
}
tailSum(1000000)