Swift底層原理探索1----函數

函數的定義

//無參函數
func pi() -> Double {
    return 3.14
}
//帶參函數
func sum(v1: Int, v2: Int) -> Int { // 形參默認是let, 而且只能是let,因此不用糾結let or var
    return v1 + v2
}
//函數的調用
sum(v1: 10, v2: 20)

//無返回值 下面三種等價
func sayHello() -> Void {
    print("Hello")
}
func sayHello2() -> () {
    print("Hello")
}
func sayHello3() {
    print("Hello")
}
複製代碼

隱式返回

若是整個函數體是一個單一表達式,那麼函數會隱式(自動)返回這個表達式編程

//隱式返回
func sum2(v1: Int, v2: Int) -> Int { // 函數體內只有一條語句,即可以不用寫return, 若是有多條語句,則必須經過 return關鍵字來返回
     v1 + v2
}
//函數的調用
sum2(v1: 10, v2: 20)
複製代碼

返回元組:實現多返回值

//經過元祖實現多返回值,將多個返回值整理到一個元祖數據結構中進行一塊返回
func calculate(v1: Int, v2: Int) -> (sum: Int, difference: Int, average: Int) {
    let sum = v1 + v2
    return (sum, v1 - v2, sum >> 1)
}

let result = calculate(v1: 20, v2: 10)
result.sum
result.difference
result.average
複製代碼

函數文檔的註釋

/// 求和【概述】
///
/// 將2個整數相加【更詳細的描述】
///
/// - Parameter v1: 第一個整數
/// - Parameter v2: 第二個整數
/// - Returns: 2個整數的和
///
/// - Note: 傳入兩個整數便可【批註】
func sum(_ v1: Int, _ v2: Int) -> Int {
    v1 + v2
}

複製代碼

函數文檔的註釋須要嚴格按照上面的模版來填寫。蘋果官方很是建議咱們對函數進行詳細的文檔注視,有助於提升代碼的可讀性。註釋生成的效果以下,經過option鍵+點擊函數名稱調出。swift

image.png

更詳細的關於函數文檔註釋請參考蘋果官方提供的接口設計規範api


參數標籤

//函數定義裏面,經過形參time的語義,很容易理解傳進來參數的性質或者做用
func goToWork(at time: String) {
    print("This time is \(time)") 
}
//函數調用的時候,實際參數取代了time,
goToWork(at: "10:00")
複製代碼

上例中的goToWork函數,參數擁有兩個標籤attime,其中time做爲爲形參,在函數體內部實現中被用來傳遞參數,而at則是在函數調用的時候使用。 上面的示例可感受到,經過參數標籤,使得函數的定義和調用,都很是符合口語習慣,利用蘋果的提供的這個特性,參照咱們正常的語言習慣來合理地設置參數標籤,能夠很好地提高代碼的可讀性。這也符合了蘋果的API設計準則。markdown

func sum(_ v1: Int, _ v2: Int) -> Int {
    v1 + v2
}
sum(10, 20)
複製代碼

經過_來省略參數標籤,能夠是的函數的調用無需參數數名,是的代碼很精簡。可是對於這個一點的使用須要結合實際狀況,不要爲了精簡代碼而影響到代碼的可讀性,從而給後期的維護帶來不便。數據結構


默認參數值

首先看一下帶默認值函數的範例編程語言

func check(name: String = "nobody", age: Int, job: String = "none") {
    print("name=\(name), age=\(age), job=\(job)")
}
check(name: "Jack", age: 20, job: "Docter")
check(name: "Rose", age: 18)
check(age: 10, job: "Batman")
check(age: 15)
複製代碼

範例中可見,check的三個參數中,參數age是沒有默認值的,然後的幾種調用過程,能夠得出的結論是,age必須傳值之外,其他帶默認值的參數很隨便,傳不傳都行,並且沒有特別的順序要求ide

你可能不理解我前面說的這個順序要求是什麼意思。若是你接觸過C++,那麼就知道C++ 的函數也是能夠給參數設置默認值的,可是要求必須從右向左依次設置,不能間隔。好比下面這個test的函數函數

void test1(int a, int b, int c = 10, int d = 20){
}//在參數列表中,從右向左分別給`d`和`c`設置了默認值,能夠編譯經過✔️

void test2(int a = 10, int b, int c, int d = 20){
}//在參數列表中,從右向左分別給`d`和`a`設置了默認值,中間跳過了`c`和`b`,沒法編譯經過✖️
複製代碼

不一樣於swiftC++ 並無參數標籤這種特性,那麼C++ 就只能將實參按照傳入順序,在形參列表裏面,從左向右進行賦值。那麼test1(50, 60)就很好理解,加上本來帶有默認值的參數,就等價於test1(50, 60, 10, 20)oop

可是test2(50, 60)就沒法被計算機理解了,由於按照C++ 的解析規則,5060會分別賦值給參數ab,可是實際上咱們是想賦值給參數bc,因爲這裏出現了二義性,所以test2在實際中是沒法被使用的,其實編程語言的各類奇怪限制,不少就是爲了消除代碼的二義性,要知道其實計算機是很笨的。測試

回看swift的參數標籤特性,由於調用函數須要帶上參數標籤,所以swift在解析的時候,能夠根據參數標籤,把實參和形參對應綁定起來。所以咱們在給swift函數設置參數默認值的時候,能夠不考慮順序。可是,若是咱們給函數裏面的參數都加上_,效果上就至關於C++函數了,以下

void testCpp(int a, int b, int c , int d ){
}
複製代碼

等價於

func testSwift(_ a: Int, _ b: Int, _ c: Int, _ d: Int){
}
複製代碼

在這裏插入圖片描述 看的出來,雖然咱們給testSwift部分參數設置了默人蔘數,可是由於設置順序問題,致使實際上必須給全部參數傳值,才能成功調用,也就是說默認參數沒法起到應有的效果。若是參照C++ 的作法,從右向左依次設置默認值,則能夠經過只傳非默認值參數來進行函數調用,如上看的test


可變參數(Variadic Parameter)

func sum(_ numbers: Int...) -> Int {
    var total = 0
    for number in numbers {
        total += number
    }
    return total
}
sum(1,3,4,5,50,90)
sum(5,88,2)
複製代碼
  • 一個函數最多隻能有1個可變參數
  • 緊跟在可變參數後面的參數不能省略參數標籤,這麼要求的目的很容易理解,就是爲了藉助可變參數以後的那個參數的參數標籤來肯定可變參數結束的位置。
//參數string不能省略標籤
func test(_ numbers: Int..., string: String, _other: String) {
    
}
test(10,20,40, string: "jack", _other: "rose")
複製代碼

Swift自帶的print函數

Swift自帶的print函數就是一個可變參數運用的範例

/// - Parameters:
/// - items: Zero or more items to print.
/// - separator: A string to print between each item. The default is a single
/// space (`" "`).
/// - terminator: The string to print after all items have been printed. The
/// default is a newline (`"\n"`).
public func print_copy(_ items: Any..., separator: String = " ", terminator: String = "\n"){
    //系統實現
    }
複製代碼

# 輸入輸出參數(In-Out Parameter)
  • 能夠用inout定義一個輸入輸出參數:能夠在函數內部修改外部實參的值
  • 可變參數不能標記爲inout
    1. inout參數的本質是地址傳遞(引用傳遞)
    2. inout參數不能有默認值
    3. inout參數只能傳入能夠被屢次賦值的
func swapValue(_ v1: inout Int, _ v2: inout Int) {
    let tmp = v1
    v1 = v2
    v2 = tmp
}
var num1 = 10
var num2 = 20
swapValue(&num1, &num2)

func swapValue2(_ v1: inout Int, _ v2: inout Int) {
//利用元組來實現
    (v1, v2) = (v2, v1)
}
複製代碼

如今經過彙編手段,來研究一下inout的實現原理。在C語言裏,咱們經過&能夠訪問變量的地址,但在Swift裏面,這個功能被屏蔽了,咱們只有在傳inout參數的時候,纔可使用&,其餘地方使用會直接編譯器報錯。

首先咱們須要新建一個命令行項目 image

準備以下測試代碼

var number = 10
func test(_ num: inout Int) {
    num = 20
}
func test2(_ num: Int) {
}

test(&number)
test2(number)
複製代碼

加個斷點,進入彙編 在這裏插入圖片描述 在這裏插入圖片描述 圖中展現了test()test1()這兩個函數的彙編嗎,經過傳遞參數所使用的指令,咱們就獲得告終果

  • leaqtest函數用的這個指令是進行地址傳遞的,也就是說test函數接受了一個內存地址做爲參數
  • movqtest2函數用的這個指令是進行復制操做的,做用就是將參數的值傳入函數內部

根據上面的發現,說明了,inout參數被傳入的其實是外部變量的地址。這樣咱們就理解了爲何上面還要求inout參數不能有默認值,這裏傳入的是一個內存地址,默認值沒有任何意義,而且剛纔說了Swift不容許咱們獲取內存地址,所以其實沒有手段能夠將內存地址設定成默認值。


函數重載(Function Overload)

  • 規則
    1. 函數名相同
    2. 參數個數不一樣 || 參數類型不一樣 || 參數標籤不一樣
func add(v1: Int, v2: Int) -> Int {
    v1 + v2
}
func add(v1: Int, v2: Int, v3: Int) -> Int {//參數個數不一樣
    v1 + v2 + v3
}
func add(v1: Int, v2: Double) -> Double {//參數類型不一樣
    Double(v1) + v2
}

func add(_ v1: Int, _ v2: Int) -> Int {//參數標籤不一樣
    v1 + v2
}
func add(a: Int, b: Int) -> Int {//參數標籤不一樣
    a + b
}
複製代碼

函數重載注意點

  • 返回值類型與函數重載無關
  • 默認參數值和函數重載一塊兒使用產生二義性時,編譯器並不會報錯(在C++中是會報錯的)
  • 可變參數、省略參數標籤、函數重載一塊兒使用產生二義性是,編譯器有可能會報錯

內聯函數

  • 若是開啓了編譯器優化(Release模式會默認開始優化),編譯器會自動將某些函數編程內聯函數,將函數調用展開成函數體
  • 如下狀況不會被自動內聯
    1. 函數體比較長
    2. 包含遞歸調用的函數
    3. 包含動態派發的函數
    4. ......

@inline

Release模式下,編譯器已經開啓優化,會自動決定哪些函數比內聯展開,所以不必手動使用@inline將函數調用展開成函數體,看以下代碼

func test() {
    print("test")
}
test()
複製代碼

當前的優化設置以下 image

說明當前Debug模式是沒有優化的,咱們看下彙編狀況image 能夠清晰看到,調用了test()函數,在進一步的驗證,咱們能夠在test()內部加上斷點 image 其彙編以下 image 能夠看到print()打印語句確實是在test()函數內部執行的。咱們調整一下Debug模式下的優化策略爲Optimize for Speed在看下test()函數有沒有被調用,狀況以下 image

發現test()沒有被調用,函數就結束了。可是print("test")語句卻執行了,所以追蹤一下print語句的執行環境 image image 能夠看到,print語句是在main函數裏被調用的,Swiftmain函數是會自動生成的,在這裏就是咱們當前代碼所在的文件。所以說明通過優化,代碼test(),並無進行函數調用,而是將它內部的語句print("test")直接展開到當前的做用域進行執行,由於不用再調用test()函數,因此就節省了函數調用所須要的棧空間的開闢以及銷燬等一些操做開銷,從而提高了程序的運行速度。

Swift編譯器設計得很巧妙,能自動判斷哪些函數須要進行內聯展開,哪些不須要。一般,如下幾種狀況不須要進行內聯

(1) 內部語句較多的函數 image image 這裏在編譯器有優化的狀況下,maintest()進行了調用,編譯器並無內聯展開test()函數,這很容易理解,由於此時test()內部的語句太多,被內聯展開以後會產生大量的代碼(轉換到底層就是大量的010101字節碼),若是程序內部大量調用test()函數,那麼對齊內聯展開的代價顯然大過了進行正常調用的開銷,所以編譯器選擇不對其進行內聯展開

(2) 存在遞歸調用的函數,這個也很好理解,可想而知,若是要展開一個遞歸函數,那麼也是一層套一層的循環展開,這個代價顯然也是大於了對函數的直接調用

(3) 存在動態派發(動態綁定)的函數,這個跟上面兩個狀況不一樣,並非不想,而是不能。由於對一個函數進行內聯展開的前提,能在編譯階段就肯定改函數內部要執行的代碼是什麼。若是是以下狀況 image 能夠看到,如上的多肽狀況下,在編譯階段是沒法肯定p.test()具體調用的是哪一個類裏面的test()。只能等到程序運行階段才能決定,所以編譯器的內聯優化在這裏沒法實現。

如下再瞭解一下Swift給咱們提供的一些內聯的使用方法(若是須要的話),通常來講,大部分狀況下咱們基本都用不着@inline

//永遠不會被內聯(即便開啓了編譯器優化)
@inline(never) func test() {
    print("test")
}

//開啓編譯器優化後,即便代碼很長,也會被內聯(遞歸調用,動態派發函數出以外)
@inline(__always) func test1() {
    print("test")
}
複製代碼

函數類型(Function Type)

每個函數都是有類型的,函數類型由形式參數類型返回值類型組成

func test2() {}  // () -> Void or () -> ()
func sum1(a: Int, b: Int) -> Int {
    a + b
} //(Int, Int) - Int

//定義變量
var fn: (Int, Int) -> Int = sum1
fn(2, 3) //調用時不須要參數標籤
複製代碼

函數類型做爲函數參數

func sum4(v1: Int, v2: Int) -> Int {
    v1 + v2
}
func difference(v1: Int, v2: Int) -> Int {
    v1 - v2
}
func printResult(_ mathFn: (Int, Int) -> Int, _ a: Int, _ b: Int) {
    print("Result: \(mathFn(a, b))")
}
printResult(sum4, 5, 2) //Result: 7
printResult(difference, 5, 2) //Resule : 3
複製代碼

函數類型做爲函數返回值

func next(_ input: Int) -> Int {
    input + 1
}
func previous(_ input: Int) -> Int {
    input - 1
}
func forward(_ forward: Bool) -> (Int) -> Int {
    forward ? next : previous
}
forward(true)(3) // 4
forward(false)(3) // 2
複製代碼

順便了解一下,如上的forward函數就是所謂的高階函數(將一個函數做爲返回值的函數)


typealias(別名)

  • typealias用來給類起別名
//基本數據類型別名
typealias Byte = Int8
typealias Short = Int16
typealias Long = Int64
//元祖別名
typealias Date = (year: Int, month: Int, day: Int)
func test3(_ date: Date) {
    print(date.0)
    print(date.year)
}
test3((2020, 2, 29))
//函數別名
typealias IntFn = (Int, Int) -> Int
func difference1(v1: Int, v2: Int) -> Int {
    v1 - v2
}
let fn1: IntFn = difference1
fn1(20, 10)

func setFn(_ fn:IntFn) {}
setFn(difference1)
func getFn() -> IntFn {difference1}
複製代碼
  • 按照Swift標準庫的定義,Void就是空元組()
public typealias Void = ()
複製代碼

嵌套函數(Nested Function)

func forward1(_ forward: Bool) -> (Int) -> Int {
    func next(_ input: Int) -> Int {
        input + 1
    }
    func previous(_ input: Int) -> Int {
        input - 1
    }
    return forward ? next : previous
}
forward1(true)(2)
forward1(false)(2)
複製代碼

若是有些函數的實現過程你不想暴露給別人,好比上面的next()previous()函數,那麼能夠經過上述的方法將他們隱藏在一個殼函數(forward())內部,並經過控制條件(_ forward: Bool)來調用。

已上就是關於Swift函數的整理。

相關文章
相關標籤/搜索