Swift學習(8)- 函式(程式碼完善版)

Swift學習(8)- 函式(程式碼完善版)

August 22, 2021·Jingyao Zhang
Jingyao Zhang

image

函式是一段完成特定任務的獨立程式碼片段。可以透過函式名稱來標示某個函式的功能,這個名稱可以在需要時「呼叫」該函式來完成它的任務。

Swift統一的函式語法非常彈性,可以用來表示任何函式,從最簡單沒有參數名稱的C風格函式,到複雜的帶有區域與外部參數名稱的Objective-C風格函式。參數可以提供預設值,以簡化函式呼叫。參數也可以同時作為傳入與傳出參數,也就是說,一旦函式執行結束,傳入的參數值將會被修改。

Swift中,每個函式都有一個由函式參數型別與回傳值型別組成的型別,可以將函式型別當作其他一般變數型別一樣處理,這樣就可以更簡單地將函式作為其他函式的參數,也可以從其他函式中回傳函式。函式的定義可以寫在其他函式定義中,這樣可以在巢狀函式範圍內實現功能封裝。


函式的定義與呼叫

當定義一個函式時,可以定義一個或多個有名稱與型別的值,作為函式的輸入,稱為參數,也可以定義某種型別的值作為函式執行結束時的輸出,稱為回傳型別。

每個函式都有函式名稱,用來描述函式執行的任務。要使用一個函式時,用函式名稱來「呼叫」這個函式,並傳給它對應的輸入值(稱為實參)。函式的實參必須與函式參數表中的參數順序一致。

下例中的函式名稱是greet(person:),之所以叫這個名字,是因為這個函式用一個人的名字作為輸入,並回傳向這個人問候的語句。為了完成這個任務,需要定義一個輸入參數:一個叫做personString值,以及一個包含給這個人問候語的String型別回傳值。

func greet(person: String) -> String {
        let greeting = "Hello, " + person + "!"
        return greeting
}

所有這些資訊彙總起來稱為函式的定義,並以func作為前綴。指定函式回傳型別時,用回傳箭頭->(一個連字號後接一個右尖括號)後面接回傳型別的名稱來表示。

這個定義描述了函式的功能,它期望接收什麼作為參數以及執行結束時它回傳的結果是什麼型別。這樣的定義使得函式可以在其他地方以清楚的方式被呼叫:

print(greet(person: "Jensen"))  // 輸出"Hello, Jensen!"
print(greet(person: "Morris"))  // 輸出"Hello, Morris!"
---
output: Hello, Jensen!
Hello, Morris!

呼叫greet(person:)函式時,在小括號中傳給它一個String型別的實參,例如greet(person: "Anna")。如上所示,因為這個函式回傳一個String型別的值,所以greet可以被包含在print(_:separator:terminator:)的呼叫中,用來輸出這個函式的回傳值。

注意

print(_:separator:terminator:)函式的第一個參數並沒有設定標籤,而其他參數因為已經有預設值,因此也是可選的。關於這些函式語法上的變化詳見下方關於函式參數標籤與參數名稱以及預設參數值。

greet(person:)的函式體中,先定義了一個新的名為greetingString常數,同時,把對personName的問候訊息賦值給greeting。然後用return關鍵字把這個問候回傳出去。一旦return greeting被呼叫,該函式結束執行並回傳greeting的當前值。

為了簡化這個定義,可以將問候訊息的建立與回傳寫成一句:

func greetAgain(person: String) -> String {
        return "Hello again, " + person + "!"
}
print(greetAgain(person: "Jensen"))  // 輸出"Hello again, Jensen!"
---
output: Hello again, Jensen!

函式參數與回傳值

函式參數與回傳值在Swift中非常彈性。可以定義任何型別的函式,從只帶一個未命名參數的簡單函式到複雜的帶有表達性參數名稱與不同參數選項的複雜函式。

無參數函式

函式可以沒有參數,下面這個函式就是一個無參數函式,當被呼叫時,它回傳固定的String訊息:

func sayHelloWorld() -> String {
        return "Hello, World!"
}
print(sayHelloWorld())  // 輸出"Hello, World!"
---
output: Hello, World!

即使這個函式沒有參數,定義中在函式名稱後面還是需要一對小括號。當被呼叫時,也需要在函式名稱後面寫一對小括號。

多參數函式

函式可以有多個輸入參數,這些參數被包含在函式的小括號中,以逗號分隔。

下面這個函式用一個人名和是否已經打過招呼作為輸入,並回傳對這個人的適當問候語:

func greet(person: String, alreadyGreeted: Bool) -> String {
        if alreadyGreeted {
                return greetAgain(person: person)
        } else {
                return greet(person: person)
        }
}
print(greet(person: "Jensen", alreadyGreeted: true))  // 輸出"Hello again, Jensen!"
---
output: Hello again, Jensen!

可以透過在括號內使用逗號分隔來傳遞一個String參數值和一個標示為alreadyGreetedBool值,來呼叫greet(person:alreadyGreeted:)函式。注意這個函式和上面的greet(person:)是不同的。雖然它們都有同樣的名稱greet,但greet(person:alreadyGreeted:)需要兩個參數,而greet(person:)只需要一個參數。

無回傳值函式

函式可以沒有回傳值。下面是greet(person:)函式的另一個版本greets,這個函式直接列印一個String值,而不是回傳它:

func greets(person: String) {
        print("Hello, \(person)!")
}
greets(person: "Jensen")  // 輸出"Hello, Jensen!"
---
output: Hello, Jensen!

因為這個函式不需要回傳值,所以這個函式的定義中沒有回傳箭頭->與回傳型別。

注意

嚴格來說,即使沒有明確定義回傳值,該greet(person:)函式依然回傳一個值。沒有明確定義回傳型別的函式會回傳一個Void型別的特殊值,該值為一個空元組,寫成()。

呼叫函式時,可以忽略該函式的回傳值:

func printAndCount(string: String) -> Int {
        print(string)
        return string.count
}
func printWithoutCounting(string: String) {
        let _ = printAndCount(string: string)
}
printAndCount(string: "Hello, World")  // 輸出"Hello, World",並且回傳值12
printWithoutCounting(string: "Hello, World")  // 輸出"Hello, World",但沒有任何回傳值
---
output: Hello, World
Hello, World

第一個函式printAndCount(string:),輸出一個字串並回傳Int型別的字元數。第二個函式printWithoutCounting(string:)呼叫了第一個函式,但卻忽略了它的回傳值,當第二個函式被呼叫時,訊息依然會由第一個函式輸出,但回傳值不會被用到。

注意

回傳值可以被忽略,但定義了回傳值的函式必須回傳一個值,如果在函式定義底部沒有回傳任何值,將導致編譯錯誤。

多重回傳值函式

可以使用元組(tuple)型別讓多個值作為一個複合值從函式中回傳。

下例中定義了一個名為minMax(array:)的函式,作用是在一個Int型別的陣列中找出最小值與最大值。

func minMax(array: [Int]) -> (min: Int, max: Int) {
        var currentMin = array[0]
        var currentMax = array[0]
        for value in array[1..<array.count] {
                if value < currentMin {
                        currentMin = value
                } else if value > currentMax {
                        currentMax = value
                }
        }
        return (currentMin, currentMax)
}

minMax(array:)函式回傳一個包含兩個Int值的元組,這些值被標記為minmax,以便查詢函式的回傳值時可以透過名稱來存取它們。

minMax(array:)的函式體中,一開始設定兩個工作變數currentMincurrentMax的值為陣列中的第一個數。然後函式會遍歷陣列中剩餘的值並檢查該值是否比currentMincurrentMax更小或更大。最後陣列中的最小值和最大值作為一個包含兩個Int值的元組回傳。

因為元組的成員值已經被命名,因此可以透過.語法存取檢索到的最小值與最大值:

let bounds = minMax(array: [8, -6, 2, 109, 3, -71])
print("Min is \(bounds.min) and max is \(bounds.max).")  // 輸出"Min is -71 and max is 109."
---
output: Min is -71 and max is 109.

需要注意的是,元組的成員不需要在元組從函式中回傳時命名,因為它們的名稱已經在函式回傳型別中指定了。

可選元組回傳型別

如果函式回傳的元組型別有可能整個元組都「沒有值」,可以使用可選的元組回傳型別反映整個元組可以是nil的事實。可以透過在元組型別的右括號後面加上一個問號來定義一個可選元組,例如(Int, Int)?(String, Int, Bool)?

注意

可選元組型別如(Int, Int)?與元組包含可選型別如(Int?, Int?)是不同的。可選的元組型別,整個元組是可選的,而不只是元組中每個元素值。

前面的minMax(array:)函式回傳了一個包含兩個Int值的元組。但函式不會對傳入的陣列執行任何安全檢查,如果array參數是一個空陣列,如上定義的minMax(array:)在試圖存取array[0]時會觸發一個執行時錯誤。

為了安全地處理這個「空陣列」問題,將minMax(array:)函式改寫為使用可選元組回傳型別,並且當陣列為空時回傳nil

func MinMax(array: [Int]) -> (min: Int, max: Int)? {
        if array.isEmpty {
                return nil
        }
        var currentMin = array[0]
        var currentMax = array[0]
        for value in array[1..<array.count] {
                if value < currentMin {
                        currentMin = value
                } else if value > currentMax {
                        currentMax = value
                }
        }
        return (currentMin, currentMax)
}

可以使用可選綁定來檢查minMax(array:)函式回傳的是一個存在的元組值還是nil

if let bounds = MinMax(array: [8, -6, 2, 109, 3, -71]) {
        print("Min is \(bounds.min) and max is \(bounds.max).")
}  // 輸出"Min is -71 and max is 109."
if let bounds = MinMax(array: []) {
        print("Min is \(bounds.min) and max is \(bounds.max).")
} else {
        print("Array is null.")
}
---
output: Min is -71 and max is 109.
Array is null.

隱式回傳的函式

如果一個函式的整個函式體是一個單行運算式,這個函式可以隱式地回傳這個運算式。例如,以下的函式有著同樣的作用:

func greeting(for person: String) -> String {
        "Hello, " + person + "!"
}
print(greeting(for: "Jensen"))  // 輸出"Hello, Jensen!"
func anotherGreeting(for person: String) -> String {
        return "Hello, " + person + "!"
}
print(anotherGreeting(for: "Jensen"))  // 輸出"Hello, Jensen!"
---
output: Hello, Jensen!
Hello, Jensen!

greeting(for:)函式的完整定義是回傳打招呼內容,這表示它能使用隱式回傳這種更簡短的形式。anotherGreeting(for:)函式回傳同樣的內容,卻因為return關鍵字讓函式更長。任何可以寫成一行return語句的函式都可以省略return

如同在「簡略的Getter宣告」中會看到的,一個屬性的getter也可以使用隱式回傳的形式。


函式參數標籤與參數名稱

每個函式參數都有一個參數標籤(Argument Label)以及一個參數名稱(Parameter Name)。參數標籤在呼叫函式時使用;呼叫時需要將函式的參數標籤寫在對應的參數前面。參數名稱在函式的實作中使用,預設情況下,函式參數使用參數名稱作為它們的參數標籤。

func someFunction(firstParameterName: Int, secondParameterName: Int) {
        // 在函式體內,firstParameterName和secondParameterName代表參數中第一個和第二個參數值
}
someFunction(firstParameterName: 1, secondParameterName: 2)

所有參數都必須有一個唯一的名稱。雖然多個參數擁有同樣的參數標籤是有可能的,但唯一的參數標籤能讓程式碼更具可讀性。

指定參數標籤

可以在參數名稱前指定它的參數標籤,中間以空格分隔:

func someFunction(argumentLabel parameterName: Int) {
        // 在函式體內,parameterName代表參數值
}

下面版本的greet(person:)函式,接收一個人的名字和他的家鄉,並回傳一句問候:

func greet(person: String, from hometown: String) -> String {
        "Hello \(person)! Glad you could visit from \(hometown)."
}
print(greet(person: "Jensen", from: "Hefei"))  // 輸出"Hello Jensen! Glad you could visit from Hefei."
---
output: Hello Jensen! Glad you could visit from Hefei.

參數標籤的使用能讓一個函式在呼叫時更具表達力,更接近自然語言,同時仍然保持函式內部的可讀性與清晰意圖。

忽略參數標籤

如果不希望為某個參數添加標籤,可以使用一個底線_來取代不明確的參數標籤。

func someFunction(_ firstParameterName: Int, secondParameterName: Int) {
        // 在函式體內,firstParameterName和secondParameterName代表參數中第一個和第二個參數值
}
someFunction(1, secondParameterName: 2)

如果一個參數有標籤,那麼在呼叫時必須使用參數標籤來標示這個參數。

預設參數值

可以在函式體中透過給參數賦值來為任意一個參數定義預設值(Default Value),當預設值被定義後,呼叫這個函式時可以忽略這個參數。

func someFunction(parameterWithoutDefault: Int, parameterWithDeafult: Int = 12) {
        // 如果在呼叫時不傳第二個參數,parameterWithDefault會被預設賦值為12傳入到函式體中。
}
someFunction(parameterWithoutDefault: 1)  // parameterWithDefault的值為12
someFunction(parameterWithoutDefault: 2, parameterWithDeafult: 6)  // parameterWithDefault的值為6

將不帶有預設值的參數放在函式參數列表的最前面,一般來說,沒有預設值的參數更為重要,將不帶預設值的參數放在最前面可以確保在函式呼叫時,非預設參數的順序是一致的,同時也讓相同的函式在不同情境下呼叫時顯得更為清楚。

可變參數

一個可變參數(Variadic Parameter)可以接受零個或多個值。函式呼叫時,可以用可變參數來指定函式參數可以被傳入不確定數量的輸入值。透過在變數型別名稱後面加上...的方式來定義可變參數。

可變參數的傳入值在函式體中會變成此型別的一個陣列。例如,一個叫做numbersDouble...型可變參數,在函式體內可以當作一個numbers[Double]型陣列常數。

下面這個函式用來計算一組任意長度數字的算術平均數(Arithmetic Mean):

func arithmeticMean(_ numbers: Double...) -> Double {
        var total: Double = 0
        for number in numbers {
                total += number
        }
        return total / Double(numbers.count)
}
print(arithmeticMean(1, 2, 3, 4, 5))  // 輸出3.0,是這五個數的平均數
print(arithmeticMean(3, 8.25, 18.75))  // 輸出10.0,是這三個數的平均數
---
output: 3.0
10.0

一個函式能擁有多個可變參數。可變參數後的第一個參數前必須加上參數標籤。參數標籤用於區分參數是傳遞給可變參數還是後面的參數。

輸入輸出參數

函式參數預設是常數。試圖在函式體中更改參數值會導致編譯錯誤。這表示不能錯誤地更改參數值。如果想要一個函式可以修改參數的值,並且希望這些修改在函式呼叫結束後仍然存在,那就需要把這個參數定義為輸入輸出參數(In-Out Parameters)。

定義一個輸入輸出參數時,在參數定義前加上inout關鍵字,一個輸入輸出參數有傳入函式的值,這個值被函式修改,然後被傳出函式,取代原來的值。想了解更多關於輸入輸出參數的細節與相關的編譯器最佳化,請參考輸入輸出參數一節。

只能傳遞變數給輸入輸出參數,不能傳入常數或字面值,因為這些值是不能被修改的。當傳入的參數作為輸入輸出參數時,需要在參數名稱前加上&符號,表示這個值可以被函式修改。

注意

輸入輸出參數不能有預設值,而且可變參數不能用inout標記。

下例中,swapTwoInts(_:_:)函式有兩個分別叫做ab的輸入輸出參數:

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
        let temporaryA = a
        a = b
        b = temporaryA
}

swapTwoInts(_:_:)函式簡單地交換ab的值。該函式先將a的值存到一個暫存常數temporaryA中,然後將b的值賦給a,最後將temporaryA賦值給b

可以用兩個Int型變數來呼叫swapTwoInts(_:_:)。需要注意的是,someIntanotherInt在傳入swapTwoInts(_:_:)函式前,都加了&的前綴。

var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("SomeInt is now \(someInt), and anotherInt is now \(anotherInt)!")  // 輸出"SomeInt is now 107, and anotherInt is now 3!"
---
output: SomeInt is now 107, and anotherInt is now 3!

從上面這個例子可以看到,someIntanotherInt的原始值在swapTwoInts(_:_:)函式中被修改,儘管它們的定義在函式體外。

注意

輸入輸出參數和回傳值是不一樣的。上面的swapTwoInts函式並沒有定義任何回傳值,但仍然修改了someInt和anotherInt的值。輸入輸出參數是函式對函式體外產生影響的另一種方式。


函式型別

每個函式都有一種特定的函式型別,函式的型別由函式的參數型別與回傳型別組成。例如:

func addTwoInts(_ a: Int, _ b: Int) -> Int {
        a + b
}
func multiplyTwoInts(_ a: Int, _ b: Int) -> Int {
        a * b
}

這個例子中定義了兩個簡單的數學函式:addTwoIntsmultiplyTwoInts。這兩個函式都接收兩個Int值,回傳一個Int值。

這兩個函式的型別是(Int, Int) -> Int,可以解讀為:

  • 「這個函式型別有兩個Int型的參數並回傳一個Int型的值」。

下面是另一個例子,一個沒有參數,也沒有回傳值的函式:

func printHelloWorld() {
        print("Hello, world!")
}

這個函式的型別是:() -> Void,也就是「沒有參數,並回傳Void型別的函式」。

使用函式型別

Swift中,使用函式型別就像使用其他型別一樣。例如,可以定義一個型別為函式的常數或變數,並將適當的函式賦值給它。

var mathFunc: (Int, Int) -> Int = addTwoInts

這段程式碼可以解讀為:

  • 「定義一個叫做mathFunc的變數,型別是『一個有兩個Int型參數並回傳一個Int型值的函式』,並讓這個新變數指向addTwoInts函式」。

addTwoIntsmathFunc有相同的型別,所以這個賦值過程在Swift型別檢查中是允許的。

現在可以使用mathFunc來呼叫被賦值的函式了:

print("Result: \(mathFunc(2, 3)).")  // 輸出"Result: 5."
---
output: Result: 5.

有相同匹配型別的不同函式可以被賦值給同一個變數,就像非函式型別的變數一樣:

mathFunc = multiplyTwoInts
print("Result: \(mathFunc(2, 3)).")  // 輸出"Result: 6."
---
output: Result: 6.

就像其他型別一樣,當賦值一個函式給常數或變數時,可以讓Swift來推斷其函式型別:

let anotherMathFunc = addTwoInts
// anotherMathFunc被推斷為(Int, Int) -> Int型別

函式型別作為參數型別

可以用(Int, Int) -> Int這樣的函式型別作為另一個函式的參數型別,這樣可以將函式的一部分實作留給函式的呼叫者來提供:

func printMathResult(_ mathFunc: (Int, Int) -> Int, _ a: Int, _ b: Int) {
        print("Result: \(multiplyTwoInts(a, b)). ")
}
printMathResult(mathFunc, 8, 2)  // 輸出"Result: 16."
---
output: Result: 16. 

這個例子定義了printMathResult(_:_:_:)函式,它有三個參數:第一個參數叫mathFunc,型別是(Int, Int) -> Int,可以傳入任何這種類型的函式;第二個和第三個參數叫ab,它們的型別都是Int,這兩個值作為已給出的函式的輸入值。

printMathResult(_:_:_:)被呼叫時,它被傳入multiplyTwoInts函式和整數8與2。它用傳入的8和2呼叫multiplyTwoInts,並輸出結果16。

printMathResult(_:_:_:)函式的作用就是輸出另一個適當型別的數學函式的呼叫結果。它不關心傳入函式是如何實作的,只關心傳入的函式是不是正確的型別,這讓printMathResult(_:_:_:)能以型別安全的方式將一部分功能交給呼叫者實作。

函式型別作為回傳型別

可以用函式型別作為另一個函式的回傳型別,需要做的是在回傳箭頭->後寫一個完整的函式型別。

下面這個例子中定義了兩個簡單函式,分別是stepForward(_:)stepBackward(_:)stepForward(_:)函式回傳一個比輸入值大1的值,stepBackward(_:)回傳一個比輸入值小1的值,這兩個函式的型別都是(Int) -> Int

func stepForward(_ input: Int) -> Int {
        input + 1
}
func stepBackward(_ input: Int) -> Int {
        input - 1
}

如下名為chooseStepFunc(backward:)的函式,它的回傳型別是(Int) -> Int型別的函式。chooseStepFunc(backward:)函式根據布林值backward來回傳stepForward(_:)stepBackward(_:)函式:

func chooseStepFunc(backward: Bool) -> (Int) -> Int {
        backward ? stepBackward : stepForward
}

現在可以用chooseStepFunc(backward:)來取得兩個函式其中之一:

var  currentValue = 3
let moveNearerToZero = chooseStepFunc(backward: currentValue > 0)

上面這個例子中計算出currentValue逐漸接近到0時需要往正數走還是往負數走。currentValue的初始值是3,這表示currentValue > 0為真,這讓chooseStepFunc(backward:)回傳stepBackward(_:)函式。一個指向回傳函式的參考被儲存在moveNearerToZero常數中。

現在,moveNearerToZero指向了正確的函式,它可以被用來數到零:

print("Counting to zero:")
// Counting to zero:
while currentValue != 0 {
        print("\(currentValue)...")
        currentValue = moveNearerToZero(currentValue)
}
print("Zero!")  // 3...  // 2...  // 1...  // Zero!
---
output: Counting to zero:
3...
2...
1...
Zero!

巢狀函式

到目前為止,本章中所見到的所有函式都叫全域函式(Global Functions),它們定義在全域範圍中。也可以將函式定義在其他函式體內,稱為巢狀函式(Nested Functions)。

預設情況下,巢狀函式對外界是不可見的,但可以被它們的外圍函式(Enclosing Function)呼叫。一個外圍函式也可以回傳它的某個巢狀函式,使得這個函式可以在其他範圍中被使用。

可以用回傳巢狀函式的方式去重寫chooseStepFunc(backward:)函式(chooseStepFunction()):

func chooseStepFunction(backward: Bool) -> (Int) -> Int {
        func stepForward(input: Int) -> Int { return input + 1 }
        func stepBackward(input: Int) -> Int { return input - 1 }
        return backward ? stepBackward(input:) : stepForward(input:)
}
var CurrentValue = -4
let MoveNearerToZero = chooseStepFunction(backward: currentValue > 0)  // moveNearerToZero 現在指向巢狀的 stepForward() 函式
while CurrentValue != 0 {
        print("\(CurrentValue)...")
        CurrentValue = MoveNearerToZero(CurrentValue)
}
print("Zero!")  // -4...  // -3...  // -2...  // -1...  // Zero!
---
output: -4...
-3...
-2...
-1...
Zero!
最後更新於