天天看點

淺談Kotlin中的函數

本文首發于 vivo網際網路技術 微信公衆号 

連結:

https://mp.weixin.qq.com/s/UV23Uw_969oVhiOdo4ZKAw 作者:連淩能

Kotlin,已經被Android官方宣布 kotlin first 的存在,去翻 Android 官方文檔的時候,發現提供的示例代碼已經變成了 Kotlin。Kotlin的務實作風,提供了很多特性幫助開發者減少備援代碼的編寫,可以提高效率,也能減少異常。

本文簡單談下Kotlin中的函數,包括表達式函數體,命名參數,預設參數,頂層函數,擴充函數,局部函數,Lambda表達式,成員引用,with/apply函數等。從例子入手,從一般寫法到使用特性進行簡化,再到原了解析。

1.表達式函數體

通過下面這個簡單的例子看下函數聲明相關的概念,函數聲明的關鍵字是fun,嗯,比JS的function還簡單。

Kotlin中參數類型是放在變量:後面,函數傳回類型也是。

fun max(a: Int, b: Int) : Int {
    if (a > b) {
        return a 
    } else {
        return b
    }
}           

當然, Kotlin是有類型推導功能,如果可以根據函數表達式推導出類型,也可以不寫傳回類型。

但是上面的還是有點繁瑣,還能再簡單,在 Kotlin中if是表達式,也就是有傳回值的,是以可以直接return,另外判斷式中隻有一行一句也可以省略掉大括号:

fun max(a: Int, b: Int)  {
    return if (a > b) a else b
}           

還能在簡單點嗎?可以,if是表達式,那麼就可以通過表達式函數體傳回:

fun max(a: Int, b: Int)  = if(a > b)  a else b           

最終隻需要一行代碼。

Example

再看下面這個例子,後面會基于這個例子進行修改。這個函數把集合以某種格式輸出,而不是預設的toString()。

是泛型,在這裡形參集合中的元素都是T類型。傳回String類型。fun joinToString(

collection: Collection<T>,
        separator: String,
        prefix: String,
        postfix: String
): String {
    val sb = StringBuilder(prefix)
    for ((index, element) in collection.withIndex()) {
        if (index > 0) sb.append(separator)
        sb.append(element)
    }

    sb.append(postfix)
    return sb.toString()
}           

2.命名參數調用

先來看下函數調用,相比Java, Kotlin中可以類似于JavaScript中帶命名參數進行調用,而且可以不用按函數聲明中的順序進行調用,可以打亂順序,比如下面:

joinToString(separator = " ", collection = list, postfix = "}", prefix = "{")

// example
val list = arrayListOf("10", "11", "1001")
println(joinToString(separator = " ", collection = list, postfix = "}", prefix = "{"))

>>> {10 11 1001}           

3.預設參數

Java裡面有重載這一說,或者JavaScript有預設參數值這一說,Kotlin采用了預設參數值。調用的時候就不需要給有預設參數值的形參傳實參。上面的函數改成如下:

fun <T> joinToString(
        collection: Collection<T>,
        separator: String = " ",
        prefix: String = "[",
        postfix: String = "]"
): String {
    ...
}

// 
joinToString(list)           

那麼調用的時候如果預設參數值自己的滿足要求,就可以隻傳入集合list即可。

4.頂層函數

不同于Java中函數隻能定義在每個類裡面,Kotlin采用了JavaScript 中的做法,可以在檔案任意位置處定義函數,這種函數稱為頂層函數。

編譯後頂層函數會成為檔案類下的靜态函數,比如在檔案名是join.kt下定義的joinToString函數可以通過JoinKt.joinToSting調用,其中JoinKt是編譯後的類名。

// 編譯成靜态函數
// 檔案名 join.kt
package strings
fun joinToString() : String {...}

/* Java */
import strings.JoinKt;
JoinKt.joinToSting(....)           

看下上面函數編譯後的效果:// 編譯成class檔案後反編譯結果

@NotNull
public static final String joinToString(@NotNull Collection collection, @NotNull String separator, @NotNull String prefix, @NotNull String postfix) {
      Intrinsics.checkParameterIsNotNull(collection, "collection");
      Intrinsics.checkParameterIsNotNull(separator, "separator");
      Intrinsics.checkParameterIsNotNull(prefix, "prefix");
      Intrinsics.checkParameterIsNotNull(postfix, "postfix");
      StringBuilder sb = new StringBuilder(prefix);
      int index = 0;

      for(Iterator var7 = ((Iterable)collection).iterator(); var7.hasNext(); ++index) {
         Object element = var7.next();
         if (index > 0) {
            sb.append(separator);
         }

         sb.append(element);
      }

      sb.append(postfix);
      String var10000 = sb.toString();
      Intrinsics.checkExpressionValueIsNotNull(var10000, "sb.toString()");
      return var10000;
   }

// 預設函數值
public static String joinToString$default(Collection var0, String var1, String var2, String var3, int var4, Object var5) {
      if ((var4 & 2) != 0) {
         var1 = " ";
      }

      if ((var4 & 4) != 0) {
         var2 = "[";
      }

      if ((var4 & 8) != 0) {
         var3 = "]";
      }

      return joinToString(var0, var1, var2, var3);           

接下來看下Kotlin中很重要的一個特性,擴充函數。

5.擴充函數

  • 擴充函數是類的一個成員函數,不過定義在類的外面
  • 擴充函數不能通路私有的或者受保護的成員
  • 擴充函數也是編譯成靜态函數

是以可以在Java庫的基礎上通過擴充函數進行封裝,假裝好像都是在調用Kotlin自己的庫一樣,在Kotlin中Collection就是這麼幹的。

再對上面的joinToString來一個改造,終結版:

fun <T> Collection<T>.joinToString(
        separator: String = " ",
        prefix: String = "[",
        postfix: String = "]"
): String {
    val sb = StringBuilder(prefix)
    for ((index, element) in this.withIndex()) {
        if (index > 0) sb.append(separator)
        sb.append(element)
    }

    sb.append(postfix)
    return sb.toString()
}           

在這裡聲明成了Collection接口類的擴充函數,這樣就可以直接通過list進行調用, 在擴充函數裡面照常可以使用this,這裡的this就是指向接收者對象,在這裡就是list。

val list = arrayListOf("10", "11", "1001")
println(list.joinToString())

>>> [10 11 1001]           

經常我們需要對代碼進行重構,其中一個重要的措施就是減少重複代碼,在Java中可以抽取出獨立的函數,但這樣有時候對整體結構并不太好,Kotlin提供了局部函數來解決這個問題。

6.局部函數

顧名思義,局部函數就是可以在函數内部定義函數。先看下沒有使用局部函數的一個例子,這個例子先對傳進來的使用者名和位址進行校驗,隻有都不為空的情況下才存進資料庫:

class User(val id: Int, val name: String, val address: String)

fun saveUser(user: User) {
    if (user.name.isEmpty()) {
        throw IllegalArgumentException(
            "Can't save user ${user.id}: empty Name")
    }

    if (user.address.isEmpty()) {
        throw IllegalArgumentException(
            "Can't save user ${user.id}: empty Address")
    }

    // Save user to the database
}           

上面有重複的代碼,就是對name和address的校驗重複了,隻是入參的不同,是以可以抽出一個校驗函數,使用局部函數重寫:

fun saveUser(user: User) {
    fun validate(value: String, fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException(
                    "Can't save user ${user.id}: empty $fieldName")
        }
    }

    validate(user.name, "Name")
    validate(user.address, "Address")
}           

布局函數可以通路所在函數中的所有參數和變量。

如果不支援Lambda都不好意思稱自己是一門現代語言,來看看Kotlin中的表演。

7.Lambda表達式

Lambda本質上是可以傳遞給其他函數的一小段代碼,可以當成值到處傳遞

Lambda表達式以左大括号開始,以右大括号結束,箭頭->分割成兩邊,左邊是入參,右邊是函數體。

val sum = {x : Int, y : Int -> x + y}
println(sum(1, 2))

// 可以直接run
run { println(42)}           

如果Lambda表達式是函數調用的最後一個實參,可以放到括号外邊;

當Lambda是函數唯一實參時,可以去掉調用代碼中的空括号;

和局部變量一樣,如果Lambda參數的類型可以被推導出來,就不需要顯示的指定。

val people = listOf(User(1, "A", "B"), User(2, "C", "D"))
people.maxBy { it.id }           

如果在函數内部使用Lambda,可以通路這個函數的參數,還有在Lambda之前定義的局部變量。

fun printProblemCounts(responses: Collection<String>) {
    var clientErrors = 0
    var serverErrors = 0
    responses.forEach {
        if (it.startsWith("4")) {
            clientErrors++
        } else if (it.startsWith("5")) {
            serverErrors++
        }
    }
    println("$clientErrors client errors, $serverErrors server errors")
}           

考慮這麼一種情況,如果一個函數A接收一個函數類型參數,但是這個參數功能已經在其它地方定義成函數B了,有一種辦法就是傳入一個Lambda表達式給A,在這個表達式中調用B,但是這樣就有點繁瑣了,有沒有可以直接拿到B的方式呢?

我都說了這麼多了,肯定是有了。。。那就是成員引用。

8.成員引用

如果Lambda剛好是函數或者屬性的委托,可以用成員引用替換。

people.maxBy(User::id)           

Ps:不管引用的是函數還是屬性,都不要在成員引用的名稱後面加括号

引用頂層函數

fun salute() = println("Salute!")
run(::salute)           

如果Lambda要委托給一個接收多個參數的函數,提供成員引用代替會非常友善:fun sendEmail(person: Person, message: String) {

println("message: $message")
}

val action = { person: Person, message: String ->
        sendEmail(person, message)
}
// action可以簡化如下
val action = ::sendEmail
// 
action(p, "HaHa")           

可以用 構造方法引用 存儲或者延期執行建立類執行個體的動作,構造方法的引用的形式是在雙冒号後指定類名稱:

data class Person(val name: String, val age: Int)
val createPerson = ::Person
val p = createPerson("Alice", 29)           

還可以用同樣的方式引用擴充函數。

fun Person.isAdult() = age>= 21
val predicate = Person::isAdult           

接下來稍微探究下Lambda的原理。

9.Lambda表達式原理

自Kotlin 1.0起,每個Lambda表達式都會被編譯成一個匿名類,除非它是一個内聯Lambda。後續版本計劃支援生成Java 8位元組碼,一旦實作,編譯器就可以避免為每一個lambda表達式都生成一個獨立的.class檔案。

如果Lambda捕捉了變量,每個被捕捉的變量會在匿名類中有對應的字段,而且每次調用都會建立一個這個匿名類的新執行個體。否則,一個單例就會被建立。類的名稱由Lambda聲明所在的函數名稱加上字尾衍生出來,這個例子中就是TestLambdaKt$main$1.class。

// TestLambda.kt
package ch05

fun salute(callback: () -> Unit) = callback()

fun main(args: Array<String>) {
    salute { println(3) }
}           

編譯後,生成兩個檔案。

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----        2019/7/24     14:33           1239 TestLambdaKt$main$1.class
-a----        2019/7/24     14:35           1237 TestLambdaKt.class           

先看下TestLambdaKt$main$1.class, 構造一個靜态執行個體ch05.TestLambdaKt$main$1 INSTANCE,在類加載的時候進行指派,同時繼承接口Function0,實作invoke方法:

final class ch05.TestLambdaKt$main$1 extends kotlin.jvm.internal.Lambda implements kotlin.jvm.functions.Function0<kotlin.Unit>
  minor version: 0
  major version: 50
  flags: ACC_FINAL, ACC_SUPER
  Constant pool:...
{
  public static final ch05.TestLambdaKt$main$1 INSTANCE;
    descriptor: Lch05/TestLambdaKt$main$1;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL

  public java.lang.Object invoke();
    descriptor: ()Ljava/lang/Object;
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #12                 // Method invoke:()V
         4: getstatic     #18                 // Field kotlin/Unit.INSTANCE:Lkotlin/Unit;
         7: areturn

  public final void invoke();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_FINAL
    Code:
      stack=2, locals=2, args_size=1
         0: iconst_3
         1: istore_1
         2: getstatic     #24                 // Field java/lang/System.out:Ljava/io/PrintStream;
         5: iload_1
         6: invokevirtual #30                 // Method java/io/PrintStream.println:(I)V
         9: return
      LineNumberTable:
        line 6: 0
        line 6: 9
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lch05/TestLambdaKt$main$1;

  ch05.TestLambdaKt$main$1();
    descriptor: ()V
    flags:
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: iconst_0
         2: invokespecial #35                 // Method kotlin/jvm/internal/Lambda."<init>":(I)V
         5: return

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: new           #2                  // class ch05/TestLambdaKt$main$1
         3: dup
         4: invokespecial #56                 // Method "<init>":()V
         7: putstatic     #58                 // Field INSTANCE:Lch05/TestLambdaKt$main$1;
        10: return
}           

再看下另外一個類TestLambdaKt.class, 在main方法中傳入TestLambdaKt$main$1.INSTANCE給方法salute,在方法salute中調用接口方法invoke,見上面。

public final class ch05.TestLambdaKt
  minor version: 0
  major version: 50
  flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER
Constant pool:
  ...
{
  public static final void salute(kotlin.jvm.functions.Function0<kotlin.Unit>);
    descriptor: (Lkotlin/jvm/functions/Function0;)V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: ldc           #10                 // String callback
         3: invokestatic  #16                 // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
         6: aload_0
         7: invokeinterface #22,  1           // InterfaceMethod kotlin/jvm/functions/Function0.invoke:()Ljava/lang/Object;
        12: pop
        13: return
      LineNumberTable:
        line 3: 6
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      14     0 callback   Lkotlin/jvm/functions/Function0;
    Signature: #7                           // (Lkotlin/jvm/functions/Function0<Lkotlin/Unit;>;)V
    RuntimeInvisibleParameterAnnotations:
      0:
        0: #8()

  public static final void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: ldc           #27                 // String args
         3: invokestatic  #16                 // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
         6: getstatic     #33                 // Field ch05/TestLambdaKt$main$1.INSTANCE:Lch05/TestLambdaKt$main$1;
         9: checkcast     #18                 // class kotlin/jvm/functions/Function0
        12: invokestatic  #35                 // Method salute:(Lkotlin/jvm/functions/Function0;)V
        15: return
      LineNumberTable:
        line 6: 6
        line 7: 15
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      16     0  args   [Ljava/lang/String;
    RuntimeInvisibleParameterAnnotations:
      0:
        0: #8()
}           

Ps:Lambda内部沒有匿名對象那樣的的this:沒有辦法引用到Lambda轉換成的匿名類執行個體。從編譯器角度看,Lambda是一個代碼塊不是一個對象,不能把它當成對象引用。Lambda中的this引用指向的是包圍它的類。

如果在Lambda中要用到正常意義上this呢?這個就需要帶接收者的函數。看下比較常用的兩個函數with和apply。

10.with函數

直接上Kotlin的源碼,with在這裡聲明成内聯函數(後面找機會說), 接收兩個參數,在函數體裡面對接收者調用Lambda表達式。在Lambda表達式裡面可以通過this引用到這個receiver對象。

/**
 * Calls the specified function [block] with the given [receiver] as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()
           

看個例子:

fun alphabet(): String {
    val result = StringBuilder()
    for (letter in 'A'..'Z') {
         result.append(letter)
    }
    result.append("\nNow I know the alphabet!")
    return result.toString()
}           

with改造, 在with裡面就不用顯示通過StringBuilder進行append調用。

fun alphabet(): String {
    val result = StringBuilder()
    return with(result) {
        for (letter in  'A'..'Z') {
            append(letter)
        }
        append("\nNow I know the alphabet!")
        this.toString()
    }
}

// 再進一步
fun alphabet() = with(StringBuilder()) {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I know the alphabet!")
    toString()
}           

with傳回的值是執行Lambda代碼的結果,該結果是Lambda中的最後一個表達式的值。如果想傳回的是接收者對象,而不是執行Lambda的結果,需要用apply函數。

11.apply函數

apply函數幾乎和with函數一模一樣,唯一的差別就是apply始終傳回作為實參傳遞給它的對象,也就是接收者對象。

/**
 * Calls the specified function [block] with `this` value as its receiver and returns `this` value.
 */
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }           

apply被聲明稱一個擴充函數,它的接收者變成了作為實參傳入的Lambda的接收者。

fun alphabet() = StringBuilder().apply {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I know the alphabet!")
}.toString()           

可以調用庫函數再簡化:

fun alphabet() = buildString {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I know the alphabet!")
}

//
/**
 * Builds new string by populating newly created [StringBuilder] using provided [builderAction]
 * and then converting it to [String].
 */
@kotlin.internal.InlineOnly
public inline fun buildString(builderAction: StringBuilder.() -> Unit): String =
        StringBuilder().apply(builderAction).toString()           

12.總結

本文隻是說了Kotlin中關于函數的一點特性,當然也沒講全,比如内聯函數,高階函數等,因為再寫下去太長了,是以後面再補充。從上面幾個例子也能大概感受到Kotlin的務實作風,提供了很多特性幫助開發者減少備援代碼的編寫,可以提高效率,也能減少異常,讓程式猿早點下班,永葆頭發烏黑靓麗。