Kotlin 高階函數與特性,讓代碼飛起

Google 在 2017 年的時候就開始推廣 Kotlin 語言作爲 Android 開發的首選語言,現在都 1202 了,發現身邊不少小夥伴還是全部用 Java 作爲開發語言,對 Kotlin 的理解還停留在空指針判斷上。我覺得有必要向他們安利一下我爲什麼選擇 Kotlin 作爲首選的開發語言

Kotlin 對 Java 的優勢:

一、編譯時判空機制

Kotlin 在編譯時會對可能會導致空指針異常的地方進行了強制判斷,幫助我們規避掉絕大多數的空指針異常。主要體現在我們在聲明變量或者參數的時候就必須顯示給定這個變量或參數是否可以爲空。例如:

 class User {
     fun sayHello(who: String) {
         println("Hello $who")
     }
 }

var user : User? = null //聲明一個可以爲空的user變量

在聲明 user 變量的時候,通過在 User 類型後面加 ”?“ 來表示這個 user 對象是可能爲空的,如果我們在使用 user 的時候不進行判空操作,編譯器將直接拋出異常。

user.sayHello("Mark") //直接使用user對象無法編譯通過

錯誤如下:

Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type User?

要想使用可能爲空的對象,有一下兩種方案:

user?.sayHello("Mark") //對象後面加”?“,表示如果user對象不爲空,纔會執行後面的sayHello方法
//或者
user!!.sayHello("Mark") //強行認定user不爲空,當user爲空時會拋出NullPointException

如果我們在聲明變量或參數的時候,在類型後面不加 "?",則表示這個對象不允許爲空,變量的聲明如下:

var user : User = User() // 由於user不能爲空,我們無法給user賦值爲null
//或者
lateinit var user : User // 先聲明出user,延遲對user進行初始化操作

注意:如果使用 lateinit 延遲對象的初始化的話,在使用這個對象的時候要確保先對其進行初始化操作,否則會拋出異常:

kotlin.UninitializedPropertyAccessException:lateinit property user has not been initialized

以上是變量中的空判斷,作爲參數的時候也是一樣的道理:

fun somebody1(user : User?){} // 允許傳入的參數user爲空
fun somebody2(user : User){}  // 不允許傳入的參數user爲空

切記,在 Kotlin 中 User 和 User? 是兩個不相同的類型,因此在 somebody2 中傳入 User? 類型的參數的話,是無法編譯通過的。也正是因爲這樣,開發過程中存在的空指針隱患能在編譯階段被暴露出來,能幫助我們規避絕大多數的 NullPointException。

以上就是 Kotlin 在規避空指針方面的應用。

小夥伴:就這?我多加幾個 if(user != null) 的判斷不也一樣,爲啥還要學一門新的語言?

打工人:別急,Kotlin 的特性遠遠不止如此,且聽我慢慢道來。

二、可選參數與具名參數

重載是我們開發過程經常見到,以前用 Java 的時候是如何進行方法的重載呢?

public void sayHello(String name){
  sayHello(name,null,null,null)
}

public void sayHello(String name,Integer age){
  sayHello(name,age,null,null)
}

public void sayHello(String name,Integer age,Integer tall){
  sayHello(name,age,tall,null)
}

public void sayHello(String name,Integer age,Integer tall,String interest){
  //balabala
}

上面是比較常見的重載,但是如果再想要重載一個只要 name 和 tall 參數的方法,怎麼重載?

Kotlin 的方法重載比 Java 要簡潔許多:

//給age、tall、interest分別
fun sayHello(name:String,age:Int? = null,tall:Int? = null,interest:String? = null){
 //balabala
}

這一個方法變實現了上面四個方法的功能,並且還一不小心做了增強。再出現上面那個只要 name 和 tall 的尷尬問題,只需要使用具名調用的方式就可以了。

sayHello("Mark",18,180,"大寶劍") //傳入了所有參數
sayHello("Mark",18) // 傳入了name和age
sayHello("Mark",age = 18) //同上,顯示的指定了age參數
sayHello("Mark",tall = 180) //傳入了name和tall
sayHello("Mark",interest = "大寶劍") //直接大寶劍也是可以的

是不是既滿足了 Java 重載的多個方法的功能,又增強了不少?

但有一點需要注意:如果傳入的可選參數是連續的,Kotlin 可以推演出參數的類型,對號入座,但如果不連續,就需要用具名參數的方式指明要傳入哪個參數。

同樣和作用於構造方法的重載。

三、函數作爲參數或返回值

雖然說 JDK 1.8 的時候,也支持了把函數作爲參數傳遞的特性,但是卻很少在項目中有見到過這麼用的,具體是什麼原因我也不太清楚,猜測是 Java 語言給開發者們留下的舊有印象吧。Java 的實現這裏就不做討論了,重點介紹一下再 Kotlin 中是如何使用這一特性的。

我們現在實現一個最簡單的函數參數方法,再討論使用場景。

fun launch(block : () -> Unit){
 //do something
}

在上面的代碼中,我們定義了一個名爲 launch 的方法,而這個方法的參數是一個無參無返回值的函數,在調用方法的時候傳入具體的函數。

如果這個函數參數是定義方法的最後一個參數,函數體可以寫到括號表達式外部,如果括號表達式裏面沒有參數,則可以省略括號表達式,所以可以用如下形式調用 launch 方法:

launch{
 // 這是函數參數裏面的具體實現
}

可能看起來有點抽象,我們可以描述一個需求,然後再實現這個需求,理解起來就更清楚一些。

需求:

A、B 兩人分別負責各自的運算邏輯對 m 和 n 兩個數據進行處理,但是處理的最終結果需要在 show 方法裏面展示出來,我們當然可以用 if else 的邏輯實現這個 show 方法,僞代碼如下:

fun show(m,n){
  int v = if(A){//如果是A處理,返回m+n
     m+n
  } else if(B){//如果是B處理,返回m-n
     m-n
  } 
 showIt(v) //對結果進行最終處理
}

這樣的寫法雖然可以滿足功能,但是如果有十幾個人都要實現自己的邏輯呢?程序將進入 if else 的地獄。還有,如果 show 方法是在基礎模塊中定義,A 和 B 又都是業務模塊,把業務模塊中的邏輯沉澱到基礎模塊是不合理的,在 Java 中遇到這種需求通常就要考慮面向對象了,基礎模塊中抽象出接口,A、B 中對接口做實現,但這樣的實現未免太複雜,我們是不是可以這麼寫呢:

//基礎模塊 block函數需要兩個Int類型參數並返回值爲Int類型
fun show(m:Int,n:Int,block : (m :Int, n:Int) -> Int){
  showIt(block(m,n))
}

//A 的模塊
var blockA = fun (m :Int, n:Int){
  return m+n
}
show(m,n,blockA) //A 的模塊調用基礎模塊的show方法

//B 的模塊
var blockB = fun (m :Int, n:Int){
  return m-n
}
show(m,n,blockB) //B 的模塊調用基礎模塊的show方法

相比抽象成接口的方式,是不是簡潔許多?

函數參數的另一個常用場景就是代替 Callback,作爲 Java 程序員,每天都在與回調地獄做鬥爭,從 RxJava 的火爆程度上便可見一斑。

通常我們異步方法結果的獲取都是通過 Callback 實現的,由於太過耳熟能詳,這裏就不再舉例子了,只簡單描述下如何通過接口參數簡化回調地獄。

// 定義一個異步方法
fun asyncExecute(param:Any,result:(data:String) -> Unit){
  var result = //balabala 異步處理操作
  //balalba切回主線程
  result(data)
}

//調用異步方法,並把結果show出來
asyncExecute(param){data ->
  showData(data)
}

從代碼觀感層面來說,簡直不要太友好啊!

函數除了作爲參數,同樣也可以作爲返回值:

fun testMethod() : (str : String) -> Int{
 return { str ->
    str.length
  }
}

上面的代碼定義了一個返回值爲一個函數,該函數需要一個 String 類型的參數並返回一個 Int 型數據。不過函數作爲返回值的場景,我基本是沒有用到過,有使用過的盆友可以介紹一下心得~

四、拓展函數

資本家:小打,給我們的 String 新增一個方法 helloWorker 的方法,調用這個方法可以打印” 打工人,打工魂,打工的都是人上人!“這句話。

打工人:(... 什麼腦殘需求..)老闆英明,只需要重寫 String 新增這個方法就可以了,這樣我們就有自己的 String 了。

資本家:重寫 String 方法?你去財務領下工資,明天不用來了!

由於 String 是 final 類型,我們沒法通過重寫該類爲它新增方法,一般操作 String 都是使用靜態方法來處理,但有時候我們偏偏想不起來那個該死的靜態方法在哪個類裏面了...

沒關係,有了拓展方法,再也不用擔心這個問題了,請看:

//爲String新增拓展方法
fun String.helloWorker(){
 println("打工人,打工魂,打工的都是人上人!")
}

"Jack".helloWorker()
"Mark".helloWorker()
//Jack在工地搬磚,Mark在做Android開發,他們都有光明的未來~

拓展方法的可以近似面向切面的方式爲某個類新增方法,而不需要通過繼承來實現。即使是被 final 修飾的類也可以進行拓展,因爲它本身就是編譯工具的一個障眼法(感興趣的可以研究一下拓展函數的原理),但這並不妨礙我們在開發中的使用。

拓展函數的用途非常廣泛,而且拓展方法對被拓展的類以及其子類都能生效。它的使用場景非常廣泛,除了爲某些類提供額外的方法外(爲 View 提供統一的防連點,爲 Int 提供 dp2px 的轉換等等),還可以對業務功能龐大的類進行拓展,把業務複雜的業務邏輯梳理到拓展類裏面,提高代碼的可讀性。

不得不說,自從用了拓展方法,喫飯都有勁兒了。

五、內聯函數

Kotlin 提供了一系列的內聯函數:let,with,run,apply,also,能幫我們提高代碼的整潔度,比如:

user?.username = "Mark"
user?.age = 18
user?.tall = 180
//使用let函數
user?.let{
  it.username = "Mark"
  it.age = 18
  it.tall = 180
}

user?.sayHello()
user?.sayBye()
user?.sayGood()
//使用with函數
user?.with{
  sayHello()
  sayBye()
  sayGood()
}

//多數場景都是使用run,因爲run是let和with的結合

關於 let,with,run,apply,also 的區別如下:

| 函數名 | 函數體內使用的對象 | 返回值 | 適用的場景 | | --- | --- | --- | --- | | let | it 指代當前對象 | 閉包 | 合併多處判斷 null 的操作 | | with | this 指代當前對象,可省略 | 閉包 | 合併多個方法調用 | | run | this 指代當前對象,可省略 | 閉包 | let 和 with 的結合體 | | apply | this 指代當前對象,可省略 | this | 能用 run 的地方就能用 apply,多用於初始化對象 | | also | it 指代當前對象 | this | 能用 let 就能用 also,可用於函數鏈式調用 |

除了上訴 Kotlin 爲我們提供的內斂函數外,我們也可以自定義內聯函數。內聯函數比普通函數的優勢在於,內聯函數會把函數體複製到到調用函數的地方,避免了棧幀的入棧出棧操作。

六、字符串處理

字符串內可以引用變量或表達式,相當實用的一個功能,許多語言也有這個特性,但不知道 Java 一直不跟進。

var str = "My name is ${user.name},I am ${user.age} years old."
//如果直接使用變量可以省略大括號
var str2 = "My name is $username"

Kotlin 也支持多行字符串:

var str = """
 Hello everybody,
 my name is ${user.name},
 I am ${user.age}
""".trimIndent()

多行字符串在某些特殊的場景裏面還是比較好用的。

七、結語

以上就是 Kotlin 部分常用的特性,當 Kotlin 好用的特性遠不止這些,流式操作、懶加載、無需 findViewById 就能拿到 xml 裏面的對象等等。當然還有最重要的:協程

看了這麼多,還想在猶豫猶豫嗎?

轉自:掘金  王遠道

https://juejin.cn/post/6949128725900296200

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/B7FdHNYUp-jHJaL-RNQi8A