函數
函數是執行特定任務的自包含代碼塊。給定函數一個名稱作為標識,并在需要的時候通過調用其名稱來執行任務。
Swift 的統一函數語法十分靈活,可以表示簡單的無參數的 C 風格函數,也可以表示有本地參數及外部參數的復雜 Objective-C 風格函數。參數可以為簡單的函數調用提供默認值,也可以作為輸入/輸出參數進行傳遞,并在函數執行完成的時候改變參數值。
Swift 中每個函數都有一種類型,由函數的參數類型及返回值類型組成。并且可以像使用 Swift 中的其他類型那樣來使用函數類型,因此將函數作為參數傳遞給其他函數,或者將函數作為函數返回值就變得很容易。函數可以定義在其他函數作用域中,用以封裝一段有用功能。
8.1 定義和調用函數
當定義一個函數時,你可以任意定義一個或多個給定命名及類型的值作為輸入(稱為形參),同時可以給定一個當函數執行結束后的返回值類型(稱為返回類型)。
每個函數都有一個函數名,用來描述函數所執行的任務。當要使用一個函數時,你可以調用其名稱并且傳遞與參數類型相符的值。一個函數的實參必須與形參順序一致。
例如下面例子中的函數 sayHello ,它以一個人名作為輸入參數,返回對這個人的一句問候語。為了實現這個功能,需要定義一個名為 personName 的字符串輸入參數,以及一個包含了對這個人名的問候語的字符串返回值類型:
<此處添加代碼2.6.1 - 1>
所有這些信息匯集到一個以 func 關鍵字作為前綴的函數的定義中。使用符號 -> 來指明返回值類型(一個連字符后面跟一個右箭頭),后面跟返回類型名稱。
函數的定義描述了函數的功能,它期望接收什么,以及當它執行結束時會返回什么。函數的定義使其可以清晰并且無歧義地在你的代碼中被調用:
<此處添加代碼2.6.1 - 2>
你可以通過在圓括號內傳遞一個字符串變量來調用 sayHello 函數,例如 sayHello(“Anna”)。由于該函數返回一個字符串值,sayHello 可以被包含在 println 函數中來打印其返回值,如上所示。
sayHello 函數主體首先定義了一個名為 greeting 的字符串常量,并且將其設置為對 personName 的一句問候語。之后使用關鍵字 return 來將?greeting 作為函數返回值。當 return greeting 語句被調用的時候,函數的執行結束并且返回當前 greeting 的值。
你可以傳入不同的值來調用 sayHello 函數。上面的例子展示了當傳入值為 “Anna” 以及 “Brian” 時函數的行為。對于不同的情況函數返回相對應的問候語。
為了簡化函數主體,可以將消息創建和返回合并到一條語句中:
<此處添加代碼2.6.1 - 3>
8.2 函數的形參和返回值
在 Swift 中,函數的形參和返回值具有很高的靈活性。你可以定義任何事情,無論是僅具有一個形參的簡單工具函數,還是具有豐富形參和不同形參選項的復雜函數。
多輸入形參
如下函數接收一個半開區間的開始和結束位置,然后計算在此區間內有多少個元素:
<此處添加代碼2.6.2 - 1>
無形參函數
函數并為要求要定義輸入形參。如下是一個無形參函數,任何時候調用都返回相同的字符串消息:
<此處添加代碼2.6.2 - 2>
函數定義時仍需要在函數名后面跟一對圓括號,即使它不帶有任何參數。當函數被調用是,函數名后面同樣需要跟一對圓括號。
無返回值的函數
函數并未要求要定義返回類型。如下是一個無返回值版本的 sayHello 函數,我們稱它為 sayGoodbye,它只會打印自己的字符串值而不是返回它:
<此處添加代碼2.6.2 - 3>
因為它并不返回任何值,函數定義也不需要包含返回剪頭(->)及返回類型。
注意:
嚴格來說,即使沒有定義任何返回值,函數 sayGoodbye 仍然返回了一個值。沒有定義返回類型的函數返回一個特殊類型的值 Void。這是一個空的元組,沒有包含任何元素,可以被寫作 ()。
當一個函數被調用時,它的返回類型可以被忽略:
<此處添加代碼2.6.2 - 4>
第一個函數 printAndCount,打印一個字符串,并以 Int 類型返回其字符數。第二個函數 printWithoutCounting,調用第一個函數,但忽略其返回值。當第二個函數被調用時,消息仍由第一個函數打印,但并未使用它的返回值。
注意:
返回值可以被忽略,但一個定義了返回值的函數必須有返回值。一個定義了返回類型的函數,不允許在控制流結束不返回值,這樣會導致編譯錯誤。
多返回值的函數
你可以使用一個元組作為函數的返回類型,來返回一個由多個返回值組成的復合返回值。
下面的例子定義了一個名為minMax的函數,用來查找到int類型數組中的最小值和最大值:
<此處添加代碼2.6.2 - 5>
minMax 函數返回包含兩個 int 類型值的一個元組。這兩個值的標簽為 min 和 max,當解包函數返回值的時候可以利用名字分別訪問到它們。
minMax 函數主體首先將兩個工作變量 currentMin 和 currentMax 設置為數組中的第一個數。然后函數遍歷數組中剩余的所有值以查看是否有比 currentMin 小或比 currentMax 大的值。最后,所有值中的最大值最小值組成元組返回。
元組的成員值已經作為函數返回類型的一部分,因此可以使用點語法訪問到最小值和最大值:
<此處添加代碼2.6.2 - 6>
需要注意的是元組的成員不需要被命名,因為元組是由函數返回的,他們的名字已經被指定為函數返回類型的一部分。
可選元組返回類型
如果函數返回的元組類型可能出現整個元組為空值的情況,你可以使用一個可選元組返回類型來表征整個元組可能為 nil。在元組類型的圓括號后面添加一個問號可以表示可選元組返回類型,例如 (Int, Int)? 或 (String, Int, Bool)?。
注意:
一個可選元組類型例如 (Int, Int)? 和一個包含可選類型的元組例如 (Int?, Int?) 是不同的。 可選元組類型的整個元組是可選的,并不只是元組中單獨的值可選。
上面 minMax 函數返回一個包含兩個 Int 值的元組。但是,函數并不會對傳入的數組進行任何安全檢查。如果數組參數中包含有空數組 — 元素數為零的數組 — 上面定義的 minMax 函數將會在訪問一個元素為空的數組時觸發運行時錯誤。
要安全處理 “空數組” 的場景,可以將 minMax 函數寫成返回可選元組,并在數組為空的時候返回nil:
<此處添加代碼2.6.2 - 7>
你可以使用可選元祖來檢查此版本的 minMax 函數是否返回一個真正的元組或者是返回nil:?
<此處添加代碼2.6.2 - 8>
函數可以有多個輸入參數,寫到函數的括號內,以逗號隔開。
如下函數接收一個半開區間的開始和結束位置,然后計算在此區間內有多少個元素:
<此處添加代碼2.6.2 - 9>
8.3 函數形參名
上面的所有函數都為其形參定義了行參名:
<此處添加代碼2.6.3 - 1>
但是,這些參數的名稱僅能在函數本身主體內部使用,不能在調用函數時使用。這些形參名被稱為本地形參名,因為他們僅能在函數主體內部使用。
外部形參名?
有時,當你調用一個函數的時候,將每一個形參進行命名是非常有用的,這樣可以清晰地表明每一個傳入參數的含義和目的。
如果希望調用你的函數的用戶提供形參名,除了定義形參名,還需要為每個參數定義外部參數名。外部形參名書寫在本地形參名之前,用一個空格隔開:
<此處添加代碼2.6.3 - 2>
注意:
如果你為一個參數提供外部形參名,外部形參名必須在調用時使用。
舉一個例子,考慮下面這個函數,在兩個字符串中間插入第三個字符串 “joiner” 來連接它們:
<此處添加代碼2.6.3 - 3>
當你調用這個函數的時候,你傳入的三個參數目的并不是非常明確:
<此處添加代碼2.6.3 - 4>
為了讓這些參數值的目的更清晰,為每一個join函數的參數定義一個外部形參名:
<此處添加代碼2.6.3 - 5>
這個版本的 join 函數,第一個形參的外部名稱是 string,內部名稱是 s1;第二個參數的外部名稱是 tostring,內部名稱是 s2;第三個參數的外部名稱是 withJoiner,內部名稱是 joiner。
現在你可以使用這些外部形參名來明確的調用這個函數:
<此處添加代碼2.6.3 - 6>
使用了外部形參名的 join 函數,能更形象、更像語句一樣被用戶調用,同時還使得函數主體可讀性更強、意圖更明確。
注意:
當別人第一次閱讀你的代碼可能不知道函數參數的含義時,就需要考慮使用外部形參名了。如果每個形參的目的清晰的話,就不需要指定外部形參名了。
外部形參名簡寫
如果你想為一個函數形參提供外部形參名,而本地形參名已經使用了一個合適的名稱,那你就不需要為這個參數書寫兩次形參名。只需要書寫一次形參名,并用一個 “#” 符號作為前綴。著能告訴 Swift 使用這個名稱同時作為本地形參名和外部形參名。
下面的例子定義了一個名為 containsCharacter 的函數,通過在本地形參名前添加 # 符號來定義外部形參名:
<此處添加代碼2.6.3 - 7>
這個函數對于形參名的選擇使得函數主體更加清晰易讀,并且在函數調用是不會有歧義:
<此處添加代碼2.6.3 - 8>
默認形參值
你可以為每一個形參定義一個默認值作為函數定義的一部分。如果定義了默認值,那么在調用時就可以省略該形參。
注意:
將帶默認值的形參寫在函數參數列表的末尾。這能確保函數的所有調用都使用相同的無默認值形參順序,并且在每種情況下都清晰的調用函數。
如下是一個先前的 join 函數,它為形參 joiner 提供了默認值:
<此處添加代碼2.6.3 - 9>
如果 join 函數被調用時為 joiner 提供了字符串值,那么字符串值將用于連接兩個字符串,跟之前一樣:
<此處添加代碼2.6.3 - 10>
但如果函數被調用時沒有為 joiner 提供任何值,就會使用默認值空格符 “ ”:
<此處添加代碼2.6.3 - 11>
帶默認值的外部形參
在大多數情況下,為所有形參提供一個帶默認值的外部名稱是很有用。這樣可以確保在函數被調用時形參提供的值所對應的實參有明確的目的。
為了讓這個過程更簡單,Swift 可為每個定義的形參提供一個自動外部名稱。這個自動外部名和本地形參名是相同的,就如同在本地形參名前面添加 # 號一樣。
如下是一個先前的 join 函數,沒有為任何形參提供外部名稱,但仍為 joiner 形參提供了默認值:
<此處添加代碼2.6.3 - 12>
在這個例子中,Swift 自動為帶默認值的形參 joiner 提供了外部名稱。在函數被調用的時候外部名稱必須提供,這使得函數的調用清晰無歧義:
<此處添加代碼2.6.3 - 13>
可變形參
一個可變形參可以接受零個或多個指定類型的值。當函數被調用時,可變形參可以用來指定傳入參數數量不固定。在參數類型名后面寫三個句號 (…) 。
傳遞給可變形參的值在函數體內部以符合的類型存儲在數組中。例如一個名為 numbers 類型為 double 的可變形參,在函數體就成為一個名為 numbers 類型為 double[] 的常量數組。
下面的例子可以計算任意長度數字數組的平均數:
<此處添加代碼2.6.3 - 14>
注意:函數最多只能有一個可變形參,并且它必須出現在參數表的最后,以避免多個參數引發歧義。如果你的函數中有一個或多個帶有默認值的參數,則將可變形參放在參數列表的最后面。
常量形參和變量形參
函數的形參默認是常量。試圖在函數體內修改形參的值將引發編譯錯誤。這確保了你不會誤修改一個形參的值。
但是,有時函數的形參的變量副本是非常有用的。這樣可以避免你自己為一個或多個參數申明變量。變量參數是變量而非常量,它能提供給函數一個可以修改的值拷貝。
在形參名前面添加 var 關鍵字來申明變量參數:
<此處添加代碼2.6.3 - 15>
這個例子定義了一個新的方法稱為 alignRight,將出入的字符串與另一個更長的字符串進行右對齊。左側的空白使用指定的占位符來填充。在這里例子中,字符串 “Hello” 被轉換為字符串 “——Hello”。
函數 alignRight 函數將輸入參數 string 定義為變量參數。這意味著 string 可以作為本地變量來使用,初始化為傳入字符串值, 并且可以在函數體內被修改。
函數首先計算出需要在左邊添加幾個字符,以使得整個字符串右對齊。這個值存儲在本地的常量 amountToPad 中。如果不需要填充字符(即當 amountToPad 小于1)時,函數直接將輸入值 string 返回。
否則,函數在現有字符串左邊填充 amountToPad 個 pad 字符并返回。在字符串值修改的時候其一直使用 string 變量類型。
注意:
對變量參數的修改效果不會超過函數調用范圍,并且在函數體外部不可見。變量參數僅存在于函數調用生命周期內。
In-Out 形參
如上所述,變量形參僅能在函數內部被修改。如果你想讓一個函數改變形參值,并且讓修改效果延續到函數調用結束之后,那么可以將參數定義為 in-out 形參。
在形參定義前添加 in-out 關鍵字來定義一個 in-out 形參。In-Out 形參有一個傳遞進函數的值,在函數中被修改,并傳回出函數以替換原來的值。
你只能傳遞一個變量作為 in-out 形參對應的實參。不能傳遞常量或者字面量作為實參,因為常量和字面量不能被修改。當你傳遞一個變量作為 in-out 形參的實參時,需要在變量前面直接加上 & 符號以指示函數可以修改其值。
注意:
in-out 參數不能有默認值,并且可變形參不能被標記為 in-out 形參。如果你標記一個形參為in-out,就不能將其標記為 var 或著 let。
如下是一個示例函數稱為 swapTwoInts,其有兩個 in-out 整形形參 a 和 b:
<此處添加代碼2.6.3 - 16>
swapTwoInts 函數只是簡單地交換 a,b 的值。它將 a 的值儲存在一個稱為 temporaryA 的臨時常量中,將 b 的值賦給 a,然后將 temporaryA 的值賦給 b。
你可以使用兩個 Int 型變量調用 swapTwoInts 函數來交換他們的值。需要注意的是,當他們被傳遞給 swapTwoInts 函數時,someInt 和 anotherInt 名稱前面需要添加 & 符號:
<此處添加代碼2.6.3 - 17>
上面的例子展示了 someInt 和 anotherInt 的原始值被 swapTwoInts 函數改變,即使他們定義在函數的外部。
8.4 函數類型
每一個函數都有一個特定的函數類型,由參數類型和返回值類型組成。
例如:
<此處添加代碼2.6.4 - 1>
這個例子中定義了兩個簡單的數學函數 addTwoInts 和 multiplyTwoInts。每個函數都接受兩個 Int 值,并返回一個 Int 值,執行適當的數學運算并返回結果。
這兩個函數的類型都是 (Int, Int) -> Int。可以這樣理解:
“ 這是一個擁有兩個 Int 型參數,且返回一個 Int 型值的函數類型。”
下面是另一個例子,該函數沒有參數和返回值:
<此處添加代碼2.6.4 - 2>
這個函數的類型是 () -> () ,即 “一個沒有形參的函數,并且返回 Void”。沒有指明返回類型的函數會返回 Void,在 Swift 中相當于一個空元組,寫為 ()。
使用函數類型
你可以像使用 Swift 中的其他類型一樣使用函數類型。例如,你可以定義一個常量或變量作為函數類型,并為變量指定一個函數:
<此處添加代碼2.6.4 - 3>
這可以解讀為:
“ 定義一個名為 mathFunction 的變量,該變量的類型是為 ‘一個接受兩個 Int 型參數,返回一個 Int 值的函數’,設置這個新的變量為 addTwoInts 函數。”
addTwoInts 函數和 mathFunction 具有相同的類型,因此 Swift 在賦值時進行類型檢查。
現在你可以使用 mathFunction 來調用指定的函數:
<此處添加代碼2.6.4 - 4>
具有匹配的相同類型的函數可以被賦值給同一個變量,就跟非函數類型一樣:
<此處添加代碼2.6.4 - 5>
如同其他類型一樣,當你把函數賦值給一個常量或者變量時,你可以讓 Swift 自行去判斷其類型:
<此處添加代碼2.6.4 - 6>
作為行參的函數類型
你可以使用形如 (Int, Int) -> Int 的函數類型,來作為另一個函數的形參。這使得當函數被調用時,保留了一些功能交給調用函數去實現。
如下的例子打印出了上面數學函數的結果:
<此處添加代碼2.6.4 - 7>
這個例子定義了一個有三個形參名為 printMathResult 的函數。第一個形參稱為 mathFunction,類型是 (Int, Int) -> Int。你可以傳遞任何此類型的函數作為第一個形參的實參。第二和第三個形參成為 a 和 b,都是 Int 類型。用作數學函數的兩個輸入值。
當 printMathResult 被調用時,傳遞進了 addTwoInt 函數,整數 3 和 5。然后函數用 3 和 5 調用了傳入的函數,打印出結果 8。
printMathResult 函數的功能是打印出傳入的特定類型數學函數的結果。它不關心傳入函數的實現,只關心傳入函數的類型是否正確。這使得 printMathResult 函數能以類型的安全(type-safe)的方式將部分功能轉交給調用者。
作為返回類型的函數類型
你可以將一個函數類型作為另一個函數的返回類型。可以在返回函數的返回剪頭 (->) 后面添加完整的函數類型來實現。
下面的例子定義了兩個簡單的函數稱為 stepForward 和 stepBackward。stepForward 函數返回輸入值加一的結果,stepBackword 返回輸入值減一的結果。這兩個函數的類型都是 (Int) -> Int:
<此處添加代碼2.6.4 - 8>
這個名為 chooseStepFunction 的函數,它返回一個類型為 (Int) -> Int 的函數,其根據布爾類型傳入參數 backwards 的值來決定返回 stepForward 還是 stepBackward:
<此處添加代碼2.6.4 - 9>
現在你可以使用 chooseStepFunction 來獲取一個遞增或者遞減函數:
<此處添加代碼2.6.4 - 10>
上面的例子可以計算出是否需要通過遞增或者遞減來讓 currentValue 變量趨于 0. currentValue 的初始值是3,因此當 currentValue > 0 時返回真,這使 chooseStepFunction 返回 stepBackward 函數。返回函數的引用存儲在名為 moveNearerToZero 的常量中。
從而 moveNearerToZero 可以執行正確的功能,可以用來計數到 0:
<此處添加代碼2.6.4 - 11>
8.5 嵌套函數
前面所有章節中所涉及的函數都是全局函數,定義在全局作用域中。你也可以在函數體中定義函數,被稱為嵌套函數。
嵌套函數默認對外界不可見,但仍可以通過其包裹函數(enclosing Function)調用它。 包裹函數可以通過返回一個嵌套函數使得這個嵌套函數可以被外部使用。
重寫上面的 chooseStepFunction 例子以返回嵌套函數:
<此處添加代碼2.6.5- 1>
Tips:
1. 函數參數名稱的省略:
Objective-C 的函數在命名上用幾乎完整的英文表述了函數名和詳細的參數名,這使得函數的可讀性極其優秀,如:UIBarButton的工廠方法:- initWithImage: style:? target: action:。Swift中也延續了這個優點,并且提供了“_” 和 “#” 符號來要求調用時添加或者不添加參數名稱,為了達到和Objective-C完全一致的風格,可以在第一個參數前添加“_”符號。
2.可變參數函數的推薦寫法:
Swift 和 Objective-C 中都限制最多只能有一組可變參數,可變參數必須只能作為方法中最后一個參數來使用,并且可變參數都必須是同一個類型。當需要處理多種類型的場景下,可以使用 Any 作為參數類型,這類似于 Objective-C 中的 id 類型。處理可變參數時推薦使用“_”符號,是可變參數看起來是一個匿名的參數列表,風格就和 Objective-C 統一。
3. 函數的參數修飾符需要注意的問題:
函數的參數默認是用 let 修飾的,也就是說函數內不可改變參數的值。在函數內改變值會產生編譯錯誤,如:
func functionName(variable: Int) -> Int {
return variable *= 2;
}
如需要改變參數值,需要顯示指定參數修飾符為 var:
func functionName(var variable: Int) -> Int {
return variable *= 2;
}
并且需要注意的是,參數的修飾是具有傳遞限制的,參數在傳遞調用的場景下必須保持七參數。
4. Swift 1.2變化部分及與 Objective-C 的關聯:
Swift 1.2版本發布同時為 Objective-C API 中可以表示參數,返回值,屬性,變量等等的“nullability”屬性。這個nullability標示符影響了Objective-C API在Swift的可選類型值。
