Swift學習(1)- 初•見(程式碼完善版)

Swift學習(1)- 初•見(程式碼完善版)

August 4, 2021·Jingyao Zhang
Jingyao Zhang

image

Swift簡介

Swift 是美國蘋果公司推出的程式語言,專門用於蘋果桌面作業系統 macOS 以及蘋果行動作業系統 iOS、iPadOS、watchOS 和 tvOS 的應用程式開發。Swift 在各方面都優於 Objective-C,也不會有那麼多複雜的符號和表示式。同時,Swift 更加快速、便利、高效且安全。此外,新的 Swift 語言依然與 Objective-C 相容。(更多關於 Swift 的資訊可以參考蘋果公司官方網站

一般來說,一行 Swift 程式碼就是一個完整的程式。

print("Hello, World!")
---
output: Hello, World!

簡單值

Swift 中,常數使用 let 修飾,常數值只能被賦值一次,可以在多個地方使用。

let constValue = 20
let constInteger: Int
print(constValue)
---
output: 20

Swift 中,變數使用 var 修飾,語法和 JavaScript 很像,變數可以多次賦值。

var variableValue = 20
variableValue = 30
print(variableValue)
---
output: 30

常數和變數的型別必須和賦給它的值一致,但不一定要明確宣告型別。當透過一個值來宣告常數或變數時,編譯器會自動推斷其型別。如果初始值沒有提供足夠資訊,或沒有初始值時,需要在變數/常數後面宣告型別,用冒號分隔。

let implicitInteger = 70
print("Implicit Integer:", implicitInteger)
let implicitDouble = 71.0
print("Implicit Double:", implicitDouble)
let explicitDouble: Double = 72
print("Explicit Double:", explicitDouble)
---
output: Implicit Integer: 70
Implicit Double: 71.0
Explicit Double: 72.0

小練習

建立一個常數,明確指定型別為 Float,並指定初始值為 4

參考答案:

let explicitFloat: Float = 100
print("Practice Result:", explicitFloat)
---
output: Practice Result: 100.0

值永遠不會被隱式轉換為其他型別,如果需要將一個值轉換成其他型別,請顯式轉型。

let label = "The width is "
let width = 94
let widthLabel = label + String(width)
print("Explicit Conversion Result:", widthLabel)
---
output: Explicit Conversion Result: The width is 94

小練習:

刪除上面倒數第二行中的 String,錯誤提示是什麼?

參考答案:

Binary operator '+' cannot be applied to operands of type 'String' and 'int'


有一種更簡單的方式可以將值轉換成字串:把值寫在括號中,並在括號前加上一個反斜線 \

let apples = 3
let oranges = 5
let fruitSummary = "I have \(apples) apples, and \(oranges) oranges. So I have \(apples + oranges) pieces of fruits totaly."
print(fruitSummary)
---
output: I have 3 apples, and 5 oranges. So I have 8 pieces of fruits totaly.

小練習:

使用 \() 把一個浮點數運算轉成字串,再加上某人的名字,和他打個招呼

參考答案:

let constScore = 301.92
let nameSocre = "Jensen \(constScore)"
print("Hello, ", nameSocre)
---
output: Hello,  Jensen 301.92

使用三個雙引號 """ 來包住多行字串內容,每行行首的縮排會被移除,直到和結尾引號的縮排相符。

let quotation = """
I said "I have \(apples) apples, and \(oranges) oranges"
And then I said "I have \(apples + oranges) pieces of fruits totaly."
"""
print(quotation)
---
output: I said "I have 3 apples, and 5 oranges"
And then I said "I have 8 pieces of fruits totaly."

使用方括號 [] 來建立陣列和字典,並使用索引或鍵(Key)來存取元素,最後一個元素後面允許有逗號。

var shoppingList = ["catfish", "water", "tulips", "blue paint", ]
print(shoppingList[0])
shoppingList[2] = "Jensen"
print(shoppingList)

var occupations = [
        "Country": "China",
        "City": "Hefei"
]
occupations["University"] = "HFUT"
print(occupations)
---
output: catfish
["catfish", "water", "Jensen", "blue paint"]
["University": "HFUT", "Country": "China", "City": "Hefei"]

陣列在新增元素時會自動變大。

shoppingList.append("apples")
print("陣列新增元素後的長度:", shoppingList.count)
---
output: 陣列新增元素後的長度: 5

使用初始化語法來建立一個空陣列或空字典。

let emptyArray: [String] = []
let emptyDicitionary: [String: Float] = [:]

如果變數的型別資訊可以被推斷出來,可以用 [][:] 來建立空陣列和空字典。

shoppingList = []
occupations = [:]

控制流程

使用 ifswitch 來進行條件操作,使用 for-inwhilerepeat-while 來進行迴圈,包住條件和迴圈變數的括號可以省略。在 if 敘述中,條件必須是布林表示式,這表示像 if score { ... } 這樣的程式碼會報錯,不會隱式與 0 做比較。

let individualScores = [75, 43, 103, 87, 12]
var teamScore = 0
for score in individualScores {
        if score > 50 {
                teamScore += 3
        } else {
                teamScore += 1
        }
}
print("The value of teamScore: ", teamScore)
---
output: The value of teamScore:  11

可以一起使用 iflet 來處理值缺失的情況。這些值可以用可選值來表示。可選值是一個具體的值或是 nil,以表示值缺失,在型別後面加上一個問號 ? 來標記這個變數的值是可選的。

var optionalString: String? = "Hello"
print(optionalString == nil)
optionalString = nil
print(optionalString == nil)

var optionalName: String? = "Jensen Jon"
var greeting = "Hello"
if let name = optionalName {
        greeting = "Hello, \(name)"
        print(greeting)
}
---
output: false
true
Hello, Jensen Jon

小練習:

把 optionalName 改成 nil,greeting 會是什麼?加上一個 else 敘述,當 optionalName 是 nil 時,給 greeting 賦予不同的值

參考答案: 如果變數的可選值是 nil,條件會判斷為 false,大括號中的程式碼會被跳過。如果不是 nil,會將值解包並賦給 let 後面的常數,這樣程式區塊中就可以使用這個值了


另一種處理可選值的方法是使用 ?? 運算子來提供一個預設值。如果可選值缺失的話,可以用預設值取代。

let nickName: String? = nil
let fullName: String = "Jensen Jon"
let informalGreeting = "Hi \(nickName ?? fullName)!"
print("informalGreeting: ", informalGreeting)
---
output: informalGreeting:  Hi Jensen Jon!

switch 支援任意型別的資料以及各種比較操作——不僅僅是整數以及測試相等。

let vegetable = "red pepper"  // Red Pepper 紅辣椒
switch vegetable {
case "celery":
        print("Add some raisins and make ants on a log.")  // Swift 中執行 switch 中匹配的 case 敘述後,程式會自動退出 switch,不需要 break
case "cucumber", "watercress":
        print("That would make a good tea sandwich.")
case let x where x.hasSuffix("pepper"):  // Warning: let 在上述例子中將匹配到的值賦給常數 x
        print("Is it a spicy \(x)?")
default:
        print("Everything tastes good in soup.")
}
---
output: Is it a spicy red pepper?

小練習:

刪除 default 敘述,看看會有什麼錯誤?

參考答案: Switch must be exhaustive


可以使用 for-in 來遍歷字典,需要一對變數來表示每個鍵值對,字典是無序集合,所以它們的鍵和值會以任意順序迭代。

let interestingNumbers = [
        "Prime": [2, 3, 5, 7, 11, 13],
        "Fibonacci": [1, 1, 2, 3, 5, 8],
        "Square": [1, 4, 9, 16, 25]
]
var largest = 0
for (_, numbers) in interestingNumbers {
        for number in numbers {
                if number > largest {
                        largest = number
                }
        }
}
print("interestingNumbers 字典中的最大值是:", largest)
---
output: interestingNumbers 字典中的最大值是: 25

小練習:

將 _ 替換成變數名稱,以確定哪種類型的值是最大的

參考答案:

var largCls = ""; largest = 0
for (varClass, numbers) in interestingNumbers {
        for number in numbers {
                if number > largest {
                        largest = number
                        largCls = varClass
                }
        }
}
print("字典中最大值的類型是:", largCls)
---
output: 字典中最大值的類型是: Square

使用 while 來重複執行一段程式直到條件改變。迴圈條件也可以放在結尾,保證至少會執行一次。

var n = 2
while n < 100 {
        n *= 2
}
print("n: ", n)

var m = 2
repeat {
        m *= 2
} while m < 100
print("m: ", m)
---
output: n:  128
m:  128

你可以在迴圈中使用 ..< 來表示索引範圍。

var total = 0
for i in 0..<5 {
        print(i)
        total += i
}
print("Total(..<): ", total)
---
output: 0
1
2
3
4
Total(..<):  10

使用 ..< 建立的範圍不包含上限,如果想包含的話需要使用 ...

total = 0
for i in 0...5 {
        print(i)
        total += i
}
print("Total(...): ", total)
---
output: 0
1
2
3
4
5
Total(...):  15

函式與閉包

使用 func 來宣告一個函式,使用名稱和參數來呼叫。使用 -> 來指定函式回傳值的型別。

func greet(person: String, day: String) -> String {
        return "Hello \(person), today is \(day)."
}
print(greet(person: "Jensen", day: "Thursday"))
---
output: Hello Jensen, today is Thursday.

小練習:

刪除 day 參數,在這個歡迎語中新增一個參數來表示今天的特餐

參考答案:

func greet(person: String, delicacy: String) -> String {
        return "Hello \(person), our special delicacy is \(delicacy) today!"
}
print(greet(person: "Jensen", delicacy: "beef"))
---
output: Hello Jensen, our special delicacy is beef today!

預設情況下,函式會使用它們的參數名稱作為參數標籤,在參數名稱前可以自訂參數標籤,或使用 _ 表示不使用參數標籤。

func greet(_ person: String, on day: String) -> String {
        return "Hello \(person), today is \(day)."
}
print(greet("Jensen", on: "Thursday"))
---
output: Hello Jensen, today is Thursday.

使用元組來產生複合值,例如讓一個函式回傳多個值,元組的元素可以用名稱或數字來取得。

func calculateStatistics(scores: [Int]) -> (min: Int, max: Int, sum: Int) {
        var min = scores[0]
        var max = scores[0]
        var sum = 0
        
        for score in scores {
                if score > max {
                        max = score
                } else if score < min {
                        min = score
                }
                sum += score
        }

        return (min, max, sum)
}
let statistics = calculateStatistics(scores: [5, 3, 100, -9, 0])
print("Max value is ", statistics.max, "\nMin value is ", statistics.min, "\nSum value is ", statistics.sum)
print(statistics.2)
---
output: Max value is  100 
Min value is  -9 
Sum value is  99
99

函式可以巢狀,巢狀的函式可以存取外層函式的變數。可以使用巢狀函式來重構過長或過於複雜的函式。

func returnFifteen() -> Int {
        var y = 10
        func add() {
                y += 5
        }
        add()
        return y
}
print("經過巢狀函式處理後的 y 值是:", returnFifteen())
---
output: 經過巢狀函式處理後的 y 值是: 15

函式是一等公民,這表示函式可以作為另一個函式的回傳值。

func makeIncrementer() -> ((Int) -> Int) {
        func addOne(number: Int) -> Int {
                return 1 + number
        }
        return addOne
}
var increment = makeIncrementer()
print("Increment is ", increment(7))
---
output: Increment is  8

函式也可以作為參數傳入另一個函式。

func hasAnyMatches(list: [Int], condition: (Int) -> Bool) -> Bool {
        for item in list {
                if condition(item) {  // if 條件必須是布林表示式
                        return true
                }
        }
        return false
}
func lessThanTen(number: Int) -> Bool {
        return number < 10
}
var numbers = [10, 20, 9, 2, 11]
print(hasAnyMatches(list: numbers, condition:  lessThanTen))
---
output: true

函式其實是一種特殊的閉包:它是一段可以之後被呼叫的程式碼。閉包中的程式碼可以存取閉包作用域中的變數和函式,即使閉包是在不同作用域被執行的,就像上面的巢狀函式。可以使用 {} 來建立匿名閉包,使用 in 將參數和回傳值型別的宣告與閉包主體分開。

var result = numbers.map({
        (number: Int) -> Int in
        let result = 3 * number
        return result
})
print("閉包值:", result)
---
output: 關包值: [30, 60, 27, 6, 33]

小練習:

重寫閉包,對所有奇數回傳 0

參考答案:

result = numbers.map({
        (number: Int) -> Int in
        if number % 2 != 0 {
                return 0
        }
        return number
})
print("閉包值:", result)
---
output: 閉包值: [10, 20, 0, 2, 0]

如果一個閉包的型別已知,例如作為代理的回呼,可以省略參數、回傳值,甚至兩者都省略。單一敘述的閉包會將其敘述的值作為結果回傳。

let mappedNumbers = numbers.map({number in 3 * number})
print("簡潔閉包:", mappedNumbers)
---
output: 簡潔閉包: [30, 60, 27, 6, 33]

可以透過參數位置而不是參數名稱來引用參數,這種方式在非常短的閉包中很有用,當閉包作為最後一個參數傳給函式時,可以直接跟在圓括號後面。當閉包是傳給函式的唯一參數時,可以完全省略圓括號。

let sortedNumbers = numbers.sorted {$0 > $1}
print("簡潔閉包(無圓括號):", sortedNumbers)
---
output: 簡潔閉包(無圓括號): [20, 11, 10, 9, 2]

類別與物件

使用 class 和類別名稱來建立一個類別。類別中的屬性宣告和變數、常數宣告一樣,不同的是類別中常/變數的上下文是類別。同樣地,方法和函式宣告也一樣。

class Shape {
        var numberOfSides = 0
        func simpleDescription() -> String {
                return "A shape with \(numberOfSides) sides."
        }
}
print(Shape().simpleDescription())
---
output: A shape with 0 sides.

小練習:

使用 let 新增一個常數屬性,再新增一個接收參數的方法

參考答案:

class Shapes {
        let constVal = 10
        var numberOfSize = 20
        func setter(_ number: Int) {
                numberOfSize = number
        }
        func shwoInfo() -> String {
                return "Class Shape has a const value constVal and a variable value numberOfSize, the value of constVal is \(constVal), the value of numberOfSize is \(numberOfSize)."
        }
}
var shapes = Shapes()
shapes.setter(100)
print(shapes.shwoInfo())
---
output: Class Shape has a const value constVal and a variable value numberOfSize, the value of constVal is 10, the value of numberOfSize is 100.

要建立一個類別的實例,在類別名稱後加上括號。使用點語法來存取實例的屬性和方法。

var shape = Shape()
shape.numberOfSides = 7
print("shape 物件已建立:", shape.simpleDescription())
---
output: shape 物件已建立: A shape with 7 sides.

上述的 Shape/Shapes 類別還缺少一個很重要的東西:建構子來初始化實例。可以使用 init 來建立建構子。

class NamedShape {
        var numberOfSides: Int = 0
        var name: String
        
        init(name: String) {
                self.name = name
        }
        
        func simpleDescription() -> String {
                return "A shape with \(numberOfSides) sides."
        }
}

注意,類似於 Python,在上述例子中 self 用來區分實例變數 name 和建構子參數 name。當需要建立實例時,就需要像傳入函式參數一樣給類別的建構子傳入參數。建立實例時,類別的每個屬性都需要賦值,無論是透過宣告還是建構子。

如果需要在物件釋放前進行一些清理工作,需要使用 deinit 建立解構子。

子類別的定義方式是在類別名稱後加上父類別名稱,並用冒號分隔。建立類別時,不一定需要一個標準的根類別(Root),所以可以依需求新增或忽略父類別。子類別如果需要覆寫父類別的方法,必須使用 override 關鍵字標記,否則編譯器會報錯。

class Square: NamedShape {
        var sideLength: Double
        
        init(sideLength: Double, name: String) {
                self.sideLength = sideLength
                super.init(name: name)
                numberOfSides = 4
        }
        
        func area() -> Double {
                return sideLength * sideLength
        }
        
        override func simpleDescription() -> String {  // 覆寫父類別方法,並呼叫父類別屬性
                return "A square with sides of length \(sideLength). Producted by \(name)."
        }
}
let test = Square(sideLength: 5.2, name: "Jensen")
print("Square: Area", test.area())
print(test.simpleDescription(), test.numberOfSides)
---
output: Square: Area 27.040000000000003
A square with sides of length 5.2. Producted by Jensen. 4

小練習:

建立 NamedShape 的另一個子類別 Circle,建構子接收兩個參數,一個是半徑,一個是名稱,在子類別中實作 area() 和 simpleDescription() 方法

參考答案:

class Circle: NamedShape {
        var radius: Double
        
        init(radius: Double, name: String) {
                self.radius = radius
                super.init(name: name)
                numberOfSides = 1
        }
        
        func area() -> Double {
                return radius * radius * Double.pi
        }

        override func simpleDescription() -> String {
                return "A circle has \(numberOfSides) side and with radius of \(radius), its area is \(area()). Producted by \(name)."
        }
}
let circle = Circle(radius: 5.2, name: "Jensen")
print(circle.simpleDescription())
---
output: A circle has 1 side and with radius of 5.2, its area is 84.94866535306801. Producted by Jensen.

除了簡單的儲存屬性,還有使用 gettersetter 的計算屬性。

class EquilateralTriangle: NamedShape {
        var sideLength: Double
        
        init(sideLength: Double, name: String) {
                self.sideLength = sideLength
                super.init(name: name)
                numberOfSides = 3
        }
        
        var perimeter: Double {
                get {
                        return 3.0 * sideLength
                }
                set {
                        sideLength = newValue  / 3.0
                }
        }
        
        override func simpleDescription() -> String {
                return "An equilateral triangle with sides of length \(sideLength)."
        }
}
var triangle = EquilateralTriangle(sideLength: 3.1, name: "Jensen")
print(triangle.simpleDescription())
triangle.perimeter = 9.9
print("Setter 後的新值:", triangle.sideLength)
---
output: An equilateral triangle with sides of length 3.1.
Setter 後的新值: 3.3000000000000003

perimetersetter 中,新值的名稱是 newValue,可以在 set 後的圓括號中明確設定名稱。注意,EquilateralTriangle 類別的建構子執行了三個步驟:

設定子類別宣告的屬性值

呼叫父類別的建構子

改變父類別定義的屬性值,其他工作如呼叫方法、getters 和 setters 也可以在這階段完成。

如果不需要計算屬性,但仍需在設定新值前或後執行程式碼,使用 willSetdidSet。這些程式碼會在屬性值改變時呼叫,但不包含在 init 中發生的值改變。

class TriangleAndSquare {
        var triangle: EquilateralTriangle {
                willSet {
                        square.sideLength = newValue.sideLength
                }
        }
        
        var square: Square {
                willSet {
                        triangle.sideLength = newValue.sideLength
                }
        }
        
        init(size: Double, name: String) {
                square = Square(sideLength: size, name: name)
                triangle = EquilateralTriangle(sideLength: size, name: name)
        }
}
var triangleAndSquare = TriangleAndSquare(size: 10, name: "Jensen")
print("第一次呼叫 > Square.sideLength:", triangleAndSquare.square.sideLength)
print("第一次呼叫 > EquilaterTriangle.sideLength:", triangleAndSquare.triangle.sideLength)
triangleAndSquare.square = Square(sideLength: 30, name: "Jensen")
print("第二次呼叫 > EquilaterTriangle.sideLength:", triangleAndSquare.triangle.sideLength)
---
output: 第一次呼叫 > Square.sideLength 10.0
第一次呼叫 > EquilaterTriangle.sideLength 10.0
第二次呼叫 > EquilaterTriangle.sideLength 30.0

處理變數的可選值時,可以在操作(如方法、屬性和子腳本)前加上 ?,如果 ? 前的值是 nil? 後的東西都會被忽略,整個表示式回傳 nil。否則可選值會被解包,之後的所有程式都會以解包後的值執行。在這兩種情況下,整個表示式的值也是一個可選值。

var optionalSquare: Square? = Square(sideLength: 2.5, name: "Jensen")
var sideLength = optionalSquare?.sideLength
print("有可選值:", sideLength)
optionalSquare = nil
sideLength = optionalSquare?.sideLength
print("無可選值:", sideLength)
---
output: 有可選值: Optional(2.5)
無可選值: nil
---
warning: Expression implicitly coerced from 'Double?' to 'Any'

列舉與結構體

使用 enum 來建立一個列舉,就像類別和其他所有命名型別一樣,列舉可以包含方法。


enum RankTest: Double {
        case ace
        case two, three, four, five, six, seven
}
print("列舉測試:", RankTest.two.rawValue)

enum Rank: Int {
        case ace = 1
        case two, three, four, five, six, seven, eight, nine, ten
        case jack, queen, king
        
        func simpleDescription() -> String {
                switch self {
                case .ace:
                        return "ace"
                case .jack:
                        return "jack"
                case .queen:
                        return "queen"
                case .king:
                        return "king"
                default:
                        return String(self.rawValue)
                }
        }
}
let ace = Rank.ace
let aceRawValue = ace.rawValue
print("ace: ", aceRawValue)
print("Rank.xx.simpleDescription(): ", Rank.king.simpleDescription())
---
output: 列舉測試: 1.0
ace:  1
Rank.xx.simpleDescription():  king

小練習:

寫一個函式,透過比較它們的原始值來比較兩個 Rank 值

參考答案:

enum Rank1: Int {
        case ace = 10
        case two, three
}

enum Rank2: Int {
        case ace = 7
        case next
}

func compareRanks(rankVal1: Int, rankVal2: Int) -> String {
        if rankVal1 < rankVal2 {
                return "Rank1"
        } else if rankVal2 < rankVal1 {
                return "Rank2"
        } else {
                return "Null"
        }
}

print("具有較大原始值的 Rank 是:", compareRanks(rankVal1: Rank1.ace.rawValue, rankVal2: Rank2.ace.rawValue))
---
output: 具有較大原始值的 Rank 是: Rank2

預設情況下,Swift 會從 0 開始每次加 1 為原始值賦值,原始值可以透過明確賦值來修改。當然,也可以選擇使用字串或浮點數作為列舉的原始值,使用 rawValue 屬性來存取列舉成員的原始值。

使用 init?(rawValue:) 初始化建構子從原始值建立一個列舉實例。如果存在於原始值相對應的列舉成員就回傳該列舉成員,否則回傳 nil

if let convertedRank = Rank(rawValue: 11) {
        let elevenDescription = convertedRank.simpleDescription()
        print(elevenDescription)
}
---
output: jack

列舉的關聯值是實際值,並不是原始值的另一種表示方式。事實上,如果沒有比較有意義的原始值,就不要提供原始值。

enum Suit {
        case spaeds, hearts, diamonds, clubs
        func simpleDescription() -> String {
                switch self {
                case .spaeds:
                        return "speedsDescription"
                case .hearts:
                        return "heartsDescription"
                case .diamonds:
                        return "diamondsDescription"
                case .clubs:
                        return "clubsDescription"
                }
        }
}
let hearts = Suit.hearts
print("Suit.hearts: ", hearts)
let heartsDescription = hearts.simpleDescription()
print("heartsDescription: ", heartsDescription)
---
output: Suit.hearts:  hearts
heartsDescription:  heartsDescription

小練習:

給 Suit 新增一個 color() 方法,對 spades 和 clubs 回傳 “black”,對 hearts 和 diamonds 回傳 “red”

參考答案:

enum SuitTest {
        case spaeds, hearts, diamonds, clubs
        func simpleDescription() -> String {
                switch self {
                case .spaeds:
                        return "speedsDescription"
                case .hearts:
                        return "heartsDescription"
                case .diamonds:
                        return "diamondsDescription"
                case .clubs:
                        return "clubsDescription"
                }
        }
        
        func color() -> String {
                switch self {
                case .spaeds:
                        return "black"
                case .clubs:
                        return "black"
                case .hearts:
                        return "red"
                case .diamonds:
                        return "red"
                }
        }
}
let clubs = SuitTest.clubs
print("Suits.clubs: ", clubs)
let clubsColor = clubs.color()
print("clubsColor: ", clubsColor)
---
output: Suits.clubs:  clubs
clubsColor:  black

注意在上面的例子中使用了兩種方式引用 hearts 列舉成員:

clubs 常數賦值時,列舉成員 Suits.clubs 需要使用全名來引用,因為常數沒有明確指定型別

switch 裡,列舉成員使用縮寫 .clubs 來引用,因為 self 的值已經是 Suits 型別

在任何已知變數型別的情況下都可以使用縮寫。

如果列舉成員的實例有原始值,這些值是在宣告時就已經決定,這表示不同列舉實例的列舉成員總會有相同的原始值。當然我們也可以為列舉成員設定關聯值,關聯值是在建立實例時決定的。這表示同一列舉成員不同實例的關聯值可以不同。

enum ServerResponse {
        case result(String, String)
        case failure(String)
}

var response = ServerResponse.result("6:00 am", "8:09 pm")
response = ServerResponse.failure("Out of memory.")

switch response {
case let .result(sunrise, sunset):
        print("Sunrise is at \(sunrise) and sunset is at \(sunset).")
case let .failure(message):
        print("Failure... \(message)")
}
---
output: Failure... Out of memory.

小練習:

給 ServerReponse 和 switch 新增第三種情況

參考答案:

enum ServerResponses {
        case result(String, String)
        case rain(String)
        case failure(String)
}

var responses = ServerResponses.result("6:00 am", "8:09 pm")
responses = ServerResponses.failure("Out of memory.")
responses = ServerResponses.rain("Today is rain.")

switch responses {
case let .result(sunrise, sunset):  // 注意 ServerResponses 的值在 switch 的分支匹配時,日出和日落時間是如何從該值中取出的
        print("Sunrise is at \(sunrise) and sunset is at \(sunset).")
case let .rain(message):
        print("Sorry! \(message)")
case let .failure(message):
        print("Failure... \(message)")
}
---
output: Sorry! Today is rain.

使用 struct 來建立一個結構體。結構體和類別有很多相同的地方,包括方法和建構子。結構體與類別最大的不同是結構體是值傳遞,類別是參考傳遞。

struct Card {
        var rank: Rank
        var suit: Suit
        func simpleDescription() -> String {
                return "The \(rank.simpleDescription()) of \(suit.simpleDescription())."
        }
}
let threeOfSpades = Card(rank: .three, suit: .spaeds)
let threeOfSpadesDescription = threeOfSpades.simpleDescription()
print("結構體傳值:", threeOfSpadesDescription)
---
output: 結構體傳值: The 3 of speedsDescription.

小練習:

寫一個方法,建立一副完整的撲克牌,這些牌是所有 rank 和 suit 的組合

參考答案:

enum Ranks: CaseIterable {
        case ace
        case two, three, four, five, six, seven, eight, nine, ten
        case jack, queen, king
}
enum Suits: CaseIterable {
        case spades, clubs, hearts, diamonds
}

struct Cards {
        var rank: Ranks
        var suit: Suits
}

func allCards() {
        for suit in Suits.allCases {
                for rank in Ranks.allCases {
                        Cards(rank: rank, suit: suit)
                }
        }
}

協定與擴充

使用 protocol 來宣告一個協定。

protocol ExampleProtocol {
        var simpleDescription: String { get }
        mutating func adjust()
}

類別、列舉和結構體都可以遵循協定,有點類似 Java 中的介面(Interface)。

class SimpleClass: ExampleProtocol {  // 類別遵循協定
        var simpleDescription: String = "A very simple class."
        var anotherProperty: Int = 69105
        func adjust() {
                simpleDescription += " Now 100% adjusted."
        }
}
var a = SimpleClass()
a.adjust()
print("Adjusted Class's Description: ", a.simpleDescription)

struct SimpleStructure: ExampleProtocol {
        var simpleDescription: String = "A simple structure."
        mutating func adjust() {  // 實作協定中的 mutating 方法時,若是類別型別,則不用寫 mutating 關鍵字,對於結構體和列舉(即值型別),則必須寫 mutating 關鍵字
                simpleDescription += " (adjusted)"
        }
}
var b = SimpleStructure()
b.adjust()
print("Adjusted Struct's Description: ", b.simpleDescription)
---
output: Adjusted Class's Description:  A very simple class. Now 100% adjusted.
Adjusted Struct's Description:  A simple structure. (adjusted)

小練習:

給 ExampleProtocol 再增加一個要求。需要怎麼改 SimpleClass 和 SimpleStructure 才能保證它們仍然遵循這個協定?

參考答案: 在 SimpleClass 和 SimpleStructure 增加一個方法實作該要求


注意宣告 SimpleStructure 時,mutating 關鍵字用來標記會修改結構體的方法。SimpleClass 的宣告不需要標記任何方法,因為類別中的方法通常可以修改類別的屬性(類別的性質)。

使用 extension 來為現有型別新增功能,例如新的方法和計算屬性。可以使用擴充讓某個在其他地方宣告的型別遵循某個協定,這同樣適用於從外部函式庫或框架引入的型別。

extension Int: ExampleProtocol {
        var simpleDescription: String {
                return "The number \(self)"
        }
        mutating func adjust() {
                self += 0
        }
}
print("Extension > Int: ", 7.simpleDescription)
---
output: Extension > Int:  The number 7

小練習:

給 Double 型別寫一個擴充,新增 roundValue 方法

參考答案:

protocol DoubleProtocol {
        func roundValue() -> Int
}

extension Double: DoubleProtocol {
        func roundValue() -> Int {
                return Int((self).rounded())
        }
}
print("Extension > Double: ", (3.14).roundValue())
---
output: Extension > Double:  3

可以像使用其他命名型別一樣使用協定名稱。例如,建立一個具有不同型別但都實作同一協定的物件集合。當處理型別是協定的值時,協定外定義的方法不可用。

let protocolValue: ExampleProtocol = a
print(protocolValue.simpleDescription)  
// print(protocolValue.anotherProperty)  // 如果取消前面的註解,這行程式會報錯
---
output: A very simple class. Now 100% adjusted.

即使 protocolValue 變數執行時的型別是 simpleClass,編譯器還是會把它的型別當作 ExampleProtocol,此時表示不能呼叫協定之外的方法或屬性。


錯誤處理

使用採用 Error 協定的型別來表示錯誤。

enum PrinterError: Error {
        case outofPaper
        case noToner
        case onFire
}

使用 throw 來拋出一個錯誤,使用 throws 來表示一個可以拋出錯誤的函式。如果在函式中拋出錯誤,這個函式會立刻回傳,並且呼叫該函式的程式會進行錯誤處理。

func send(job: Int, toPrinter printerName: String) throws -> String {
        if printerName == "Never has Toner" {
                throw PrinterError.noToner
        }
        return "\(printerName), Job sent."
}
print("ErrorDemo: ", try send(job: 10, toPrinter: "Okay"))
---
output: ErrorDemo:  Okay, Job sent.

有多種方式可以進行錯誤處理。一種方式是使用 do-catch。在 do 程式區塊中,使用 try 標記可以拋出錯誤的程式。在 catch 程式區塊中,除非另外命名,否則錯誤會自動命名為 error

do {
        let printerResponse = try send(job: 1040, toPrinter: "Jensen")
        print(printerResponse)
} catch {
        print(error)
}
---
output: Jensen, Job sent.

小練習:

將 printerName 改成 “Never has Toner” 使得 send(job:toPrinter:) 函式拋出錯誤

參考答案:

do {
        let printerResponse = try send(job: 0, toPrinter: "Never has Toner")
        print(printerResponse)
} catch {
        print(error)
}
---
output: noToner

可以使用多個 catch 區塊來處理特定錯誤,參照 switch 中的 case 風格來寫 catch

do {
        let printerResponse = try send(job: 0, toPrinter: "Never has Toner")
        print(printerResponse)
} catch PrinterError.onFire {
        print("I'll just put this over here, with the rest of the fire.")
} catch let printerError as PrinterError {
        print("Printer error: \(printerError)")
} catch {
        print(error)
}
---
output: Printer error: noToner

小練習:

在 do 程式區塊中新增拋出錯誤的程式,需要拋出哪種錯誤才能讓第一個 catch 區塊接收?怎麼讓第二個和第三個 catch 接收?

參考答案: 要讓第一個 catch 區塊接收,需要拋出 onFire 錯誤;拋出 PrinterError 中除 onFire 以外的其他錯誤即可被第二個 catch 區塊接收;拋出非 PrinterError 的錯誤即可被第三個 catch 區塊接收


另一種處理錯誤的方式是使用 try? 將結果轉換為可選值。如果函式拋出錯誤,該錯誤會被丟棄且結果為 nil。否則,結果會是一個包含函式回傳值的可選值。

let printerSuccess = try? send(job: 1884, toPrinter: "Jensen is handsome")
let printerFailure = try? send(job: 1885, toPrinter: "Never has Toner")
print("printerSuccess: \(printerSuccess)\nprinterFailure: \(printerFailure)")
---
output: printerSuccess: Optional("Jensen is handsome, Job sent.")
printerFailure: nil
---
warning: String interpolation produces a debug description for an optional value; did you mean to make this explicit?

使用 defer 程式區塊來表示在函式回傳前,函式中最後執行的程式。無論函式是否會拋出錯誤,這段程式都會執行。使用 defer,可以把函式呼叫之初就要執行的程式和函式呼叫結束時的收尾程式寫在一起,雖然這兩者的執行時機完全不同。

var fridgeIsOpen = false
let fridgeContent = ["milk", "eggs", "leftovers"]

func fridgeContains(_ food: String) -> Bool {
        fridgeIsOpen = true
        defer {
                fridgeIsOpen = false
        }
        
        let result = fridgeContent.contains(food)
        return result
}
print("Do this fridge contains banana? ", fridgeContains("banana"))
print("FridgeIsOpen: ", fridgeIsOpen)
---
output: Do this fridge contains banana?  false
FridgeIsOpen:  false

泛型

在尖括號裡寫一個名稱來建立泛型函式或型別。

func makeArray<Item>(repeating item: Item, numberOfTimes: Int) -> [Item] {
        var result: [Item] = []
        for _ in 0..<numberOfTimes {
                result.append(item)
        }
        return result
}
let repeatNum: [String] = makeArray(repeating: "Test", numberOfTimes: 5)
print(repeatNum)
---
output: ["Test", "Test", "Test", "Test", "Test"]

也可以建立泛型函式、方法、類別、列舉和結構體

// (重新實作 Swift 標準函式庫中的可選型別)
enum OptionalValue<Wrapped> {
        case none
        case some(Wrapped)
}
var possibleInteger: OptionalValue<Int> = .none
possibleInteger = .some(100)
print(possibleInteger)
---
output: some(100)

在型別名稱後面使用 where 來指定對型別的一系列需求,例如,限定型別實作某個協定,限定兩個型別相同,或限定某個類別必須有特定父類別。

func anyCommonElements<T: Sequence, U: Sequence>(_ lhs: T, _ rhs: U) -> Bool
where T.Element: Equatable, T.Element == U.Element  // 遵循 Equatable 協定可以包含對 == 和 != 的實作
{
        for lhsItem in lhs {
                for rhsItem in rhs {
                        if lhsItem == rhsItem {
                                return true
                        }
                }
        }
        return false
}
print("WHERE Test: ", anyCommonElements([1, 2 ,3], [3]))
---
output: WHERE Test:  true

小練習:

修改 anyCommonElements(_ :_ :) 函式來建立一個函式,回傳一個陣列,內容是兩個序列的共有元素

參考答案:

func anyCommonElementsInArray<T: Sequence, U: Sequence>(_ lhs: T, _ rhs: U) -> [T.Element]
where T.Element: Equatable, T.Element == U.Element
{
        var outArray: [T.Element] = []
        for lhsItem in lhs {
                for rhsItem in rhs {
                        if lhsItem == rhsItem {
                                outArray.append(lhsItem)
                        }
                }
        }
        return outArray
}
print("CommonArray: ", anyCommonElementsInArray([0, 1, 2, 3, 4], [0, 3, 4, 5, 6]))
---
output: CommonArray:  [0, 3, 4]

<T: Equatable><T> ... where T: Equatable 的寫法是等價的。


本篇僅對 Swift 語言做概括性介紹,後續將繼續分享 Swift 語言的詳細教學。

最後更新於