淺談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 = "{")// exampleval 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.ktpackage 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))// 可以直接runrun { 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 = ::Personval p = createPerson("Alice", 29)
還可以用同樣的方式引用擴充套件函式。
fun Person.isAdult() = age>= 21val predicate = Person::isAdult
接下來稍微探究下Lambda的原理。
9.Lambda表示式原理
自Kotlin 1.0起,每個Lambda表示式都會被編譯成一個匿名類,除非它是一個內聯Lambda。後續版本計劃支援生成Java 8位元組碼,一旦實現,編譯器就可以避免為每一個lambda表示式都生成一個獨立的.class檔案。
如果Lambda捕捉了變數,每個被捕捉的變數會在匿名類中有對應的欄位,而且每次呼叫都會建立一個這個匿名類的新例項。否則,一個單例就會被建立。類的名稱由Lambda宣告所在的函式名稱加上字尾衍生出來,這個例子中就是TestLambdaKt$main$1.class。
// TestLambda.ktpackage 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.InlineOnlypublic 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.InlineOnlypublic 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.InlineOnlypublic inline fun buildString(builderAction: StringBuilder.() -> Unit): String = StringBuilder().apply(builderAction).toString()
12.總結
本文只是說了Kotlin中關於函式的一點特性,當然也沒講全,比如行內函數,高階函式等,因為再寫下去太長了,所以後面再補充。從上面幾個例子也能大概感受到Kotlin的務實作風,提供了很多特性幫助開發者減少冗餘程式碼的編寫,可以提高效率,也能減少異常,讓程式猿早點下班,永葆頭髮烏黑靚麗。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69912579/viewspace-2662519/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 淺談Swift中的函式式Swift函式
- 淺談Kotlin中集合和函式式API完全解析-上篇(八)Kotlin函式API
- 淺談Kotlin語法篇之擴充套件函式(五)Kotlin套件函式
- 淺談eval函式函式
- 淺談生成函式函式
- 淺談Kotlin語法篇之頂層函式、中綴呼叫、解構宣告(四)Kotlin函式
- 淺談Kotlin語法篇之如何讓函式更好地呼叫(三)Kotlin函式
- 淺談Numpy中的shape、reshape函式的區別函式
- 淺談尤拉函式函式
- Kotlin中的高階函式Kotlin函式
- 淺談Kotlin中的Sequences原始碼解析(十)Kotlin原始碼
- 淺談php count()函式方法PHP函式
- 淺談C語言中函式的使用C語言函式
- 談談JS中的函式劫持JS函式
- 淺談js函式節流和函式防抖JS函式
- 淺談匿名函式和閉包函式
- 【Kotlin】函式Kotlin函式
- 淺談Kotlin的Checked Exception機制KotlinException
- kotlin常用函式Kotlin函式
- Kotlin中的also、let、run、with、apply函式的用法KotlinAPP函式
- Kotlin之“with”函式和“apply”函式Kotlin函式APP
- Kotlin 函式6 - 高階函式Kotlin函式
- 淺談Kotlin語法篇之Lambda表示式完全解析(六)Kotlin
- Kotlin 集合函式式ApiKotlin函式API
- 【原創】淺談指標(十一)alloca函式指標函式
- 淺談js中的正規表示式JS
- Kotlin高階函式Kotlin函式
- Kotlin教程(二)函式Kotlin函式
- kotlin 函式和 Lambda 表示式Kotlin函式
- 淺談JS變數宣告和函式宣告提升JS變數函式
- JS function 是函式也是物件, 淺談原型鏈JSFunction函式物件原型
- Kotlin中的幾個常用函式let with run also applyKotlin函式APP
- Kotlin擴充套件函式Kotlin套件函式
- Kotlin基礎之函式Kotlin函式
- 淺談JavaScript中的thisJavaScript
- Kotlin的幾個擴充套件函式Kotlin套件函式
- 【原創】淺談指標(八)字串相關函式(下集)指標字串函式
- Kotlin教程(八)高階函式Kotlin函式