Swift學習:2.6 函數

參考Swift 官方教程《The Swift Programming Language》中文版

函數(Functions)


函數是用來完成特定任務的獨立的代碼塊。你給一個函數起一個合適的名字,用來標識函數做什麼,並且當函數需要執行的時候,這個名字會被“調用”。

Swift 統一的函數語法足夠靈活,可以用來表示任何函數,包括從最簡單的沒有參數名字的 C 風格函數,到複雜的帶局部和外部參數名的 Objective-C 風格函數。參數可以提供默認值,以簡化函數調用。參數也可以既當做傳入參數,也當做傳出參數,也就是說,一旦函數執行結束,傳入的參數值可以被修改。

在 Swift 中,每個函數都有一種類型,包括函數的參數值類型和返回值類型。你可以把函數類型當做任何其他普通變量類型一樣處理,這樣就可以更簡單地把函數當做別的函數的參數,也可以從其他函數中返回函數。函數的定義可以寫在在其他函數定義中,這樣可以在嵌套函數範圍內實現功能封裝。

函數的定義與調用(Defining and Calling Functions)

當你定義一個函數時,你可以定義一個或多個有名字和類型的值,作爲函數的輸入(稱爲參數,parameters),也可以定義某種類型的值作爲函數執行結束的輸出(稱爲返回類型)。

每個函數有個函數名,用來描述函數執行的任務。要使用一個函數時,你用函數名“調用”,並傳給它匹配的輸入值(稱作實參,arguments)。一個函數的實參必須與函數參數表裏參數的順序一致。

在下面例子中的函數叫做"greetingForPerson",之所以叫這個名字是因爲這個函數用一個人的名字當做輸入,並返回給這個人的問候語。爲了完成這個任務,你定義一個輸入參數-一個叫做 personName 的 String值,和一個包含給這個人問候語的 String 類型的返回值:

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

所有的這些信息彙總起來成爲函數的定義,並以 func 作爲前綴。指定函數返回類型時,用返回箭頭 ->(一個連字符後跟一個右尖括號)後跟返回類型的名稱的方式來表示。

該定義描述了函數做什麼,它期望接收什麼和執行結束時它返回的結果是什麼。這樣的定義使的函數可以在別的地方以一種清晰的方式被調用:

println(sayHello("Anna"))
// prints "Hello, Anna!"
println(sayHello("Brian"))
// prints "Hello, Brian!"

調用 sayHello 函數時,在圓括號中傳給它一個 String 類型的實參。因爲這個函數返回一個 String 類型的值,sayHello 可以被包含在 println 的調用中,用來輸出這個函數的返回值,正如上面所示。

在 sayHello 的函數體中,先定義了一個新的名爲 greeting 的 String 常量,同時賦值了給 personName的一個簡單問候消息。然後用 return 關鍵字把這個問候返回出去。一旦 return greeting 被調用,該函數結束它的執行並返回 greeting 的當前值。

你可以用不同的輸入值多次調用 sayHello。上面的例子展示的是用"Anna""Brian"調用的結果,該函數分別返回了不同的結果。

爲了簡化這個函數的定義,可以將問候消息的創建和返回寫成一句:

func sayHelloAgain(personName: String) -> String {
    return "Hello again, " + personName + "!"
}
println(sayHelloAgain("Anna"))
// prints "Hello again, Anna!"

函數參數與返回值(Function Parameters and Return Values)

函數參數與返回值在Swift中極爲靈活。你可以定義任何類型的函數,包括從只帶一個未名參數的簡單函數到複雜的帶有表達性參數名和不同參數選項的複雜函數。

多重輸入參數(Multiple Input Parameters)

函數可以有多個輸入參數,寫在圓括號中,用逗號分隔。

下面這個函數用一個半開區間的開始點和結束點,計算出這個範圍內包含多少數字:

func halfOpenRangeLength(start: Int, end: Int) -> Int {
    return end - start
}
println(halfOpenRangeLength(1, 10))
// prints "9"

無參函數(Functions Without Parameters)

函數可以沒有參數。下面這個函數就是一個無參函數,當被調用時,它返回固定的 String 消息:

func sayHelloWorld() -> String {
    return "hello, world"
}
println(sayHelloWorld())
// prints "hello, world"

儘管這個函數沒有參數,但是定義中在函數名後還是需要一對圓括號。當被調用時,也需要在函數名後寫一對圓括號。

無返回值函數(Functions Without Return Values)

函數可以沒有返回值。下面是 sayHello 函數的另一個版本,叫 waveGoodbye,這個函數直接輸出 String值,而不是返回它:

func sayGoodbye(personName: String) {
    println("Goodbye, \(personName)!")
}
sayGoodbye("Dave")
// prints "Goodbye, Dave!"

因爲這個函數不需要返回值,所以這個函數的定義中沒有返回箭頭(->)和返回類型。

注意: 嚴格上來說,雖然沒有返回值被定義,sayGoodbye 函數依然返回了值。沒有定義返回類型的函數會返回特殊的值,叫 Void。它其實是一個空的元組(tuple),沒有任何元素,可以寫成()

被調用時,一個函數的返回值可以被忽略:

func printAndCount(stringToPrint: String) -> Int {
    println(stringToPrint)
    return countElements(stringToPrint)
}
func printWithoutCounting(stringToPrint: String) {
    printAndCount(stringToPrint)
}
printAndCount("hello, world")
// prints "hello, world" and returns a value of 12
printWithoutCounting("hello, world")
// prints "hello, world" but does not return a value

第一個函數 printAndCount,輸出一個字符串並返回 Int 類型的字符數。第二個函數printWithoutCounting調用了第一個函數,但是忽略了它的返回值。當第二個函數被調用時,消息依然會由第一個函數輸出,但是返回值不會被用到。

注意: 返回值可以被忽略,但定義了有返回值的函數必須返回一個值,如果在函數定義底部沒有返回任何值,這將導致編譯錯誤(compile-time error)。

多重返回值函數(Functions with Multiple Return Values)

你可以用元組(tuple)類型讓多個值作爲一個複合值從函數中返回。

下面的這個例子中,count 函數用來計算一個字符串中元音,輔音和其他字母的個數(基於美式英語的標準)。

func count(string: String) -> (vowels: Int, consonants: Int, others: Int) {
    var vowels = 0, consonants = 0, others = 0
    for character in string {
        switch String(character).lowercaseString {
        case "a", "e", "i", "o", "u":
            ++vowels
        case "b", "c", "d", "f", "g", "h", "j", "k", "l", "m",
          "n", "p", "q", "r", "s", "t", "v", "w", "x", "y", "z":
            ++consonants
        default:
            ++others
        }
    }
    return (vowels, consonants, others)
}

你可以用 count 函數來處理任何一個字符串,返回的值將是一個包含三個 Int 型值的元組(tuple):

let total = count("some arbitrary string!")
println("\(total.vowels) vowels and \(total.consonants) consonants")
// prints "6 vowels and 13 consonants"

需要注意的是,元組的成員不需要在函數中返回時命名,因爲它們的名字已經在函數返回類型中有了定義。

函數參數名稱(Function Parameter Names)

以上所有的函數都給它們的參數定義了參數名(parameter name)

func someFunction(parameterName: Int) {
    // function body goes here, and can use parameterName
    // to refer to the argument value for that parameter
}

但是,這些參數名僅在函數體中使用,不能在函數調用時使用。這種類型的參數名被稱作局部參數名(local parameter name),因爲它們只能在函數體中使用。

外部參數名(External Parameter Names)

有時候,調用函數時,給每個參數命名是非常有用的,因爲這些參數名可以指出各個實參的用途是什麼。

如果你希望函數的使用者在調用函數時提供參數名字,那就需要給每個參數除了局部參數名外再定義一個外部參數名。外部參數名寫在局部參數名之前,用空格分隔。

func someFunction(externalParameterName localParameterName: Int) {
    // function body goes here, and can use localParameterName
    // to refer to the argument value for that parameter
}

注意: 如果你提供了外部參數名,那麼函數在被調用時,必須使用外部參數名。

以下是個例子,這個函數使用一個結合者(joiner)把兩個字符串聯在一起:

func join(s1: String, s2: String, joiner: String) -> String {
    return s1 + joiner + s2
}

當你調用這個函數時,這三個字符串的用途是不清楚的:

join("hello", "world", ", ")
// returns "hello, world"

爲了讓這些字符串的用途更爲明顯,我們爲 join 函數添加外部參數名:

func join(string s1: String, toString s2: String, withJoiner joiner: String) -> String {
    return s1 + joiner + s2
}

在這個版本的 join 函數中,第一個參數有一個叫 string 的外部參數名和 s1 的局部參數名,第二個參數有一個叫 toString 的外部參數名和 s2 的局部參數名,第三個參數有一個叫 withJoiner 的外部參數名和joiner 的局部參數名。

現在,你可以使用這些外部參數名以一種清晰地方式來調用函數了:

join(string: "hello", toString: "world", withJoiner: ", ")
// returns "hello, world"

使用外部參數名讓第二個版本的 join 函數的調用更爲有表現力,更爲通順,同時還保持了函數體是可讀的和有明確意圖的。

注意: 當其他人在第一次讀你的代碼,函數參數的意圖顯得不明顯時,考慮使用外部參數名。如果函數參數名的意圖是很明顯的,那就不需要定義外部參數名了。

簡寫外部參數名(Shorthand External Parameter Names)

如果你需要提供外部參數名,但是局部參數名已經定義好了,那麼你不需要寫兩次參數名。相反,只寫一次參數名,並用井號(#)作爲前綴就可以了。這告訴 Swift 使用這個參數名作爲局部和外部參數名。

下面這個例子定義了一個叫 containsCharacter 的函數,使用井號(#)的方式定義了外部參數名:

func containsCharacter(#string: String, #characterToFind: Character) -> Bool {
    for character in string {
        if character == characterToFind {
            return true
        }
    }
    return false
}

這樣定義參數名,使得函數體更爲可讀,清晰,同時也可以以一個不含糊的方式被調用:

let containsAVee = containsCharacter(string: "aardvark", characterToFind: "v")
// containsAVee equals true, because "aardvark" contains a "v”

默認參數值(Default Parameter Values)

你可以在函數體中爲每個參數定義默認值。當默認值被定義後,調用這個函數時可以忽略這個參數。

注意: 將帶有默認值的參數放在函數參數列表的最後。這樣可以保證在函數調用時,非默認參數的順序是一致的,同時使得相同的函數在不同情況下調用時顯得更爲清晰。

以下是另一個版本的join函數,其中joiner有了默認參數值:

func join(string s1: String, toString s2: String, withJoiner joiner: String = " ") -> String {
    return s1 + joiner + s2
}

像第一個版本的 join 函數一樣,如果 joiner 被賦值時,函數將使用這個字符串值來連接兩個字符串:

join(string: "hello", toString: "world", withJoiner: "-")
// returns "hello-world"

當這個函數被調用時,如果 joiner 的值沒有被指定,函數會使用默認值(" "):

join(string: "hello", toString:"world")
// returns "hello world"

默認值參數的外部參數名(External Names for Parameters with Default Values)

在大多數情況下,給帶默認值的參數起一個外部參數名是很有用的。這樣可以保證當函數被調用且帶默認值的參數被提供值時,實參的意圖是明顯的。

爲了使定義外部參數名更加簡單,當你未給帶默認值的參數提供外部參數名時,Swift 會自動提供外部名字。此時外部參數名與局部名字是一樣的,就像你已經在局部參數名前寫了井號(#)一樣。

下面是 join 函數的另一個版本,這個版本中並沒有爲它的參數提供外部參數名,但是 joiner 參數依然有外部參數名:

func join(s1: String, s2: String, joiner: String = " ") -> String {
    return s1 + joiner + s2
}

在這個例子中,Swift 自動爲 joiner 提供了外部參數名。因此,當函數調用時,外部參數名必須使用,這樣使得參數的用途變得清晰。

join("hello", "world", joiner: "-")
// returns "hello-world"

注意: 你可以使用下劃線(_)作爲默認值參數的外部參數名,這樣可以在調用時不用提供外部參數名。但是給帶默認值的參數命名總是更加合適的。

可變參數(Variadic Parameters)

一個可變參數(variadic parameter)可以接受一個或多個值。函數調用時,你可以用可變參數來傳入不確定數量的輸入參數。通過在變量類型名後面加入(...)的方式來定義可變參數。

傳入可變參數的值在函數體內當做這個類型的一個數組。例如,一個叫做 numbers 的 Double... 型可變參數,在函數體內可以當做一個叫 numbers 的 Double[] 型的數組常量。

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

func arithmeticMean(numbers: Double...) -> Double {
    var total: Double = 0
    for number in numbers {
        total += number
    }
    return total / Double(numbers.count)
}
arithmeticMean(1, 2, 3, 4, 5)
// returns 3.0, which is the arithmetic mean of these five numbers
arithmeticMean(3, 8, 19)
// returns 10.0, which is the arithmetic mean of these three numbers

注意: 一個函數至多能有一個可變參數,而且它必須是參數表中最後的一個。這樣做是爲了避免函數調用時出現歧義。

如果函數有一個或多個帶默認值的參數,而且還有一個可變參數,那麼把可變參數放在參數表的最後。

常量參數和變量參數(Constant and Variable Parameters)

函數參數默認是常量。試圖在函數體中更改參數值將會導致編譯錯誤。這意味着你不能錯誤地更改參數值。

但是,有時候,如果函數中有傳入參數的變量值副本將是很有用的。你可以通過指定一個或多個參數爲變量參數,從而避免自己在函數中定義新的變量。變量參數不是常量,你可以在函數中把它當做新的可修改副本來使用。

通過在參數名前加關鍵字 var 來定義變量參數:

func alignRight(var string: String, count: Int, pad: Character) -> String {
    let amountToPad = count - countElements(string)
    if amountToPad < 1 {
        return string
    }
    let padString = String(pad)
    for _ in 1...amountToPad {
        string = padString + string
    }
    return string
}
let originalString = "hello"
let paddedString = alignRight(originalString, 10, "-")
// paddedString is equal to "-----hello"
// originalString is still equal to "hello"

這個例子中定義了一個新的叫做 alignRight 的函數,用來右對齊輸入的字符串到一個長的輸出字符串中。左側空餘的地方用指定的填充字符填充。這個例子中,字符串"hello"被轉換成了"-----hello"

alignRight 函數將參數 string 定義爲變量參數。這意味着 string 現在可以作爲一個局部變量,用傳入的字符串值初始化,並且可以在函數體中進行操作。

該函數首先計算出多少個字符需要被添加到 string 的左邊,以右對齊到總的字符串中。這個值存在局部常量 amountToPad 中。這個函數然後將 amountToPad 多的填充(pad)字符填充到 string 左邊,並返回結果。它使用了 string 這個變量參數來進行所有字符串操作。

注意: 對變量參數所進行的修改在函數調用結束後便消失了,並且對於函數體外是不可見的。變量參數僅僅存在於函數調用的生命週期中。

輸入輸出參數(In-Out Parameters)

變量參數,正如上面所述,僅僅能在函數體內被更改。如果你想要一個函數可以修改參數的值,並且想要在這些修改在函數調用結束後仍然存在,那麼就應該把這個參數定義爲輸入輸出參數(In-Out Parameters)。

定義一個輸入輸出參數時,在參數定義前加 inout 關鍵字。一個輸入輸出參數有傳入函數的值,這個值被函數修改,然後被傳出函數,替換原來的值。

你只能將變量作爲輸入輸出參數。你不能傳入常量或者字面量(literal value),因爲這些量是不能被修改的。當傳入的參數作爲輸入輸出參數時,需要在參數前加&符,表示這個值可以被函數修改。

注意: 輸入輸出參數不能有默認值,而且可變參數不能用 inout 標記。如果你用 inout 標記一個參數,這個參數不能被 var 或者 let 標記。

下面是例子,swapTwoInts 函數,有兩個分別叫做 a 和 b 的輸入輸出參數:

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

這個 swapTwoInts 函數僅僅交換 a 與 b 的值。該函數先將 a 的值存到一個暫時常量 temporaryA 中,然後將 b 的值賦給 a,最後將 temporaryA 幅值給 b

你可以用兩個 Int 型的變量來調用 swapTwoInts。需要注意的是,someInt 和 anotherInt 在傳入swapTwoInts 函數前,都加了 & 的前綴:

var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
println("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// prints "someInt is now 107, and anotherInt is now 3”

從上面這個例子中,我們可以看到 someInt 和 anotherInt 的原始值在 swapTwoInts 函數中被修改,儘管它們的定義在函數體外。

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

函數類型(Function Types)

每個函數都有種特定的函數類型,由函數的參數類型和返回類型組成。

例如:

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

這個例子中定義了兩個簡單的數學函數:addTwoInts 和 multiplyTwoInts。這兩個函數都傳入兩個 Int類型, 返回一個合適的Int值。

這兩個函數的類型是 (Int, Int) -> Int,可以讀作“這個函數類型,它有兩個 Int 型的參數並返回一個Int 型的值。”。

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

func printHelloWorld() {
    println("hello, world")
}

這個函數的類型是:() -> (),或者叫“沒有參數,並返回 Void 類型的函數”。沒有指定返回類型的函數總返回 Void。在Swift中,Void 與空的元組是一樣的。

使用函數類型(Using Function Types)

在 Swift 中,使用函數類型就像使用其他類型一樣。例如,你可以定義一個類型爲函數的常量或變量,並將函數賦值給它:

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

這個可以讀作:

“定義一個叫做 mathFunction 的變量,類型是‘一個有兩個 Int 型的參數並返回一個 Int 型的值的函數’,並讓這個新變量指向 addTwoInts 函數”。

addTwoInts 和 mathFunction 有同樣的類型,所以這個賦值過程在 Swift 類型檢查中是允許的。

現在,你可以用 mathFunction 來調用被賦值的函數了:

println("Result: \(mathFunction(2, 3))")
// prints "Result: 5"

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

mathFunction = multiplyTwoInts
println("Result: \(mathFunction(2, 3))")
// prints "Result: 6"

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

let anotherMathFunction = addTwoInts
// anotherMathFunction is inferred to be of type (Int, Int) -> Int

函數類型作爲參數類型(Function Types as Parameter Types)

你可以用(Int, Int) -> Int這樣的函數類型作爲另一個函數的參數類型。這樣你可以將函數的一部分實現交由給函數的調用者。

下面是另一個例子,正如上面的函數一樣,同樣是輸出某種數學運算結果:

func printMathResult(mathFunction: (Int, Int) -> Int, a: Int, b: Int) {
    println("Result: \(mathFunction(a, b))")
}
printMathResult(addTwoInts, 3, 5)
// prints "Result: 8”

這個例子定義了 printMathResult 函數,它有三個參數:第一個參數叫 mathFunction,類型是(Int, Int) -> Int,你可以傳入任何這種類型的函數;第二個和第三個參數叫 a 和 b,它們的類型都是 Int,這兩個值作爲已給的函數的輸入值。

當 printMathResult 被調用時,它被傳入 addTwoInts 函數和整數35。它用傳入35調用addTwoInts,並輸出結果:8

printMathResult 函數的作用就是輸出另一個合適類型的數學函數的調用結果。它不關心傳入函數是如何實現的,它只關心這個傳入的函數類型是正確的。這使得 printMathResult 可以以一種類型安全(type-safe)的方式來保證傳入函數的調用是正確的。

函數類型作爲返回類型(Function Type as Return Types)

你可以用函數類型作爲另一個函數的返回類型。你需要做的是在返回箭頭(->)後寫一個完整的函數類型。

下面的這個例子中定義了兩個簡單函數,分別是 stepForward 和stepBackwardstepForward 函數返回一個比輸入值大一的值。stepBackward 函數返回一個比輸入值小一的值。這兩個函數的類型都是 (Int) -> Int

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

下面這個叫做 chooseStepFunction 的函數,它的返回類型是 (Int) -> Int 的函數。chooseStepFunction 根據布爾值 backwards 來返回 stepForward 函數或 stepBackward 函數:

func chooseStepFunction(backwards: Bool) -> (Int) -> Int {
    return backwards ? stepBackward : stepForward
}

你現在可以用 chooseStepFunction 來獲得一個函數,不管是那個方向:

var currentValue = 3
let moveNearerToZero = chooseStepFunction(currentValue > 0)
// moveNearerToZero now refers to the stepBackward() function

上面這個例子中計算出從 currentValue 逐漸接近到0是需要向正數走還是向負數走。currentValue 的初始值是3,這意味着 currentValue > 0 是真的(true),這將使得 chooseStepFunction 返回stepBackward 函數。一個指向返回的函數的引用保存在了 moveNearerToZero 常量中。

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

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

嵌套函數(Nested Functions)

這章中你所見到的所有函數都叫全局函數(global functions),它們定義在全局域中。你也可以把函數定義在別的函數體中,稱作嵌套函數(nested functions)。

默認情況下,嵌套函數是對外界不可見的,但是可以被他們封閉函數(enclosing function)來調用。一個封閉函數也可以返回它的某一個嵌套函數,使得這個函數可以在其他域中被使用。

你可以用返回嵌套函數的方式重寫 chooseStepFunction 函數:

func chooseStepFunction(backwards: Bool) -> (Int) -> Int {
    func stepForward(input: Int) -> Int { return input + 1 }
    func stepBackward(input: Int) -> Int { return input - 1 }
    return backwards ? stepBackward : stepForward
}
var currentValue = -4
let moveNearerToZero = chooseStepFunction(currentValue < 0)
// moveNearerToZero now refers to the nested stepForward() function
while currentValue != 0 {
    println("\(currentValue)... ")
    currentValue = moveNearerToZero(currentValue)
}
println("zero!")
// -4...
// -3...
// -2...
// -1...
// zero!

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