本文首發(fā)于 vivo互聯(lián)網(wǎng)技術(shù) 微信公眾號(hào)?
鏈接:https://mp.weixin.qq.com/s/UV23Uw_969oVhiOdo4ZKAw
作者:連凌能創(chuàng)新互聯(lián)公司是一家專注于成都做網(wǎng)站、網(wǎng)站設(shè)計(jì)與策劃設(shè)計(jì),朝陽網(wǎng)站建設(shè)哪家好?創(chuàng)新互聯(lián)公司做網(wǎng)站,專注于網(wǎng)站建設(shè)十多年,網(wǎng)設(shè)計(jì)領(lǐng)域的專業(yè)建站公司;建站業(yè)務(wù)涵蓋:朝陽等地區(qū)。朝陽做網(wǎng)站價(jià)格咨詢:18980820575
Kotlin,已經(jīng)被Android官方宣布?kotlin first?的存在,去翻 Android 官方文檔的時(shí)候,發(fā)現(xiàn)提供的示例代碼已經(jīng)變成了 Kotlin。Kotlin的務(wù)實(shí)作風(fēng),提供了很多特性幫助開發(fā)者減少冗余代碼的編寫,可以提高效率,也能減少異常。
本文簡單談下Kotlin中的函數(shù),包括表達(dá)式函數(shù)體,命名參數(shù),默認(rèn)參數(shù),頂層函數(shù),擴(kuò)展函數(shù),局部函數(shù),Lambda表達(dá)式,成員引用,with/apply函數(shù)等。從例子入手,從一般寫法到使用特性進(jìn)行簡化,再到原理解析。
通過下面這個(gè)簡單的例子看下函數(shù)聲明相關(guān)的概念,函數(shù)聲明的關(guān)鍵字是fun,嗯,比JS的function還簡單。
Kotlin中參數(shù)類型是放在變量:后面,函數(shù)返回類型也是。
fun max(a: Int, b: Int) : Int {
if (a > b) {
return a
} else {
return b
}
}
當(dāng)然, Kotlin是有類型推導(dǎo)功能,如果可以根據(jù)函數(shù)表達(dá)式推導(dǎo)出類型,也可以不寫返回類型。
但是上面的還是有點(diǎn)繁瑣,還能再簡單,在 Kotlin中if是表達(dá)式,也就是有返回值的,因此可以直接return,另外判斷式中只有一行一句也可以省略掉大括號(hào):
fun max(a: Int, b: Int) {
return if (a > b) a else b
}
還能在簡單點(diǎn)嗎?可以,if是表達(dá)式,那么就可以通過表達(dá)式函數(shù)體返回:
fun max(a: Int, b: Int) = if(a > b) a else b
最終只需要一行代碼。
Example
再看下面這個(gè)例子,后面會(huì)基于這個(gè)例子進(jìn)行修改。這個(gè)函數(shù)把集合以某種格式輸出,而不是默認(rèn)的toString()。
collection: Collection,
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()
}
先來看下函數(shù)調(diào)用,相比Java, Kotlin中可以類似于JavaScript中帶命名參數(shù)進(jìn)行調(diào)用,而且可以不用按函數(shù)聲明中的順序進(jìn)行調(diào)用,可以打亂順序,比如下面:
joinToString(separator = " ", collection = list, postfix = "}", prefix = "{")
// example
val list = arrayListOf("10", "11", "1001")
println(joinToString(separator = " ", collection = list, postfix = "}", prefix = "{"))
>>> {10 11 1001}
Java里面有重載這一說,或者JavaScript有默認(rèn)參數(shù)值這一說,Kotlin采用了默認(rèn)參數(shù)值。調(diào)用的時(shí)候就不需要給有默認(rèn)參數(shù)值的形參傳實(shí)參。上面的函數(shù)改成如下:
fun joinToString(
collection: Collection,
separator: String = " ",
prefix: String = "[",
postfix: String = "]"
): String {
...
}
//
joinToString(list)
那么調(diào)用的時(shí)候如果默認(rèn)參數(shù)值自己的滿足要求,就可以只傳入集合list即可。
不同于Java中函數(shù)只能定義在每個(gè)類里面,Kotlin采用了JavaScript 中的做法,可以在文件任意位置處定義函數(shù),這種函數(shù)稱為頂層函數(shù)。
編譯后頂層函數(shù)會(huì)成為文件類下的靜態(tài)函數(shù),比如在文件名是join.kt下定義的joinToString函數(shù)可以通過JoinKt.joinToSting調(diào)用,其中JoinKt是編譯后的類名。
// 編譯成靜態(tài)函數(shù)
// 文件名 join.kt
package strings
fun joinToString() : String {...}
/* Java */
import strings.JoinKt;
JoinKt.joinToSting(....)
看下上面函數(shù)編譯后的效果:// 編譯成class文件后反編譯結(jié)果
@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;
}
// 默認(rèn)函數(shù)值
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中很重要的一個(gè)特性,擴(kuò)展函數(shù)。
擴(kuò)展函數(shù)是類的一個(gè)成員函數(shù),不過定義在類的外面
擴(kuò)展函數(shù)不能訪問私有的或者受保護(hù)的成員
所以可以在Java庫的基礎(chǔ)上通過擴(kuò)展函數(shù)進(jìn)行封裝,假裝好像都是在調(diào)用Kotlin自己的庫一樣,在Kotlin中Collection就是這么干的。
再對(duì)上面的joinToString來一個(gè)改造,終結(jié)版:
fun Collection.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接口類的擴(kuò)展函數(shù),這樣就可以直接通過list進(jìn)行調(diào)用, 在擴(kuò)展函數(shù)里面照常可以使用this,這里的this就是指向接收者對(duì)象,在這里就是list。
val list = arrayListOf("10", "11", "1001")
println(list.joinToString())
>>> [10 11 1001]
經(jīng)常我們需要對(duì)代碼進(jìn)行重構(gòu),其中一個(gè)重要的措施就是減少重復(fù)代碼,在Java中可以抽取出獨(dú)立的函數(shù),但這樣有時(shí)候?qū)φw結(jié)構(gòu)并不太好,Kotlin提供了局部函數(shù)來解決這個(gè)問題。
顧名思義,局部函數(shù)就是可以在函數(shù)內(nèi)部定義函數(shù)。先看下沒有使用局部函數(shù)的一個(gè)例子,這個(gè)例子先對(duì)傳進(jìn)來的用戶名和地址進(jìn)行校驗(yàn),只有都不為空的情況下才存進(jìn)數(shù)據(jù)庫:
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
}
上面有重復(fù)的代碼,就是對(duì)name和address的校驗(yàn)重復(fù)了,只是入?yún)⒌牟煌?,因此可以抽出一個(gè)校驗(yàn)函數(shù),使用局部函數(shù)重寫:
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")
}
布局函數(shù)可以訪問所在函數(shù)中的所有參數(shù)和變量。
如果不支持Lambda都不好意思稱自己是一門現(xiàn)代語言,來看看Kotlin中的表演。
Lambda本質(zhì)上是可以傳遞給其他函數(shù)的一小段代碼,可以當(dāng)成值到處傳遞
Lambda表達(dá)式以左大括號(hào)開始,以右大括號(hào)結(jié)束,箭頭->分割成兩邊,左邊是入?yún)?,右邊是函?shù)體。
val sum = {x : Int, y : Int -> x + y}
println(sum(1, 2))
// 可以直接run
run { println(42)}
如果Lambda表達(dá)式是函數(shù)調(diào)用的最后一個(gè)實(shí)參,可以放到括號(hào)外邊;
當(dāng)Lambda是函數(shù)唯一實(shí)參時(shí),可以去掉調(diào)用代碼中的空括號(hào);
和局部變量一樣,如果Lambda參數(shù)的類型可以被推導(dǎo)出來,就不需要顯示的指定。
val people = listOf(User(1, "A", "B"), User(2, "C", "D"))
people.maxBy { it.id }
如果在函數(shù)內(nèi)部使用Lambda,可以訪問這個(gè)函數(shù)的參數(shù),還有在Lambda之前定義的局部變量。
fun printProblemCounts(responses: Collection) {
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")
}
考慮這么一種情況,如果一個(gè)函數(shù)A接收一個(gè)函數(shù)類型參數(shù),但是這個(gè)參數(shù)功能已經(jīng)在其它地方定義成函數(shù)B了,有一種辦法就是傳入一個(gè)Lambda表達(dá)式給A,在這個(gè)表達(dá)式中調(diào)用B,但是這樣就有點(diǎn)繁瑣了,有沒有可以直接拿到B的方式呢?
我都說了這么多了,肯定是有了。。。那就是成員引用。
如果Lambda剛好是函數(shù)或者屬性的委托,可以用成員引用替換。
people.maxBy(User::id)
Ps:不管引用的是函數(shù)還是屬性,都不要在成員引用的名稱后面加括號(hào)
引用頂層函數(shù)
fun salute() = println("Salute!")
run(::salute)
如果Lambda要委托給一個(gè)接收多個(gè)參數(shù)的函數(shù),提供成員引用代替會(huì)非常方便: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")
可以用 構(gòu)造方法引用 存儲(chǔ)或者延期執(zhí)行創(chuàng)建類實(shí)例的動(dòng)作,構(gòu)造方法的引用的形式是在雙冒號(hào)后指定類名稱:
data class Person(val name: String, val age: Int)
val createPerson = ::Person
val p = createPerson("Alice", 29)
還可以用同樣的方式引用擴(kuò)展函數(shù)。
fun Person.isAdult() = age>= 21
val predicate = Person::isAdult
接下來稍微探究下Lambda的原理。
自Kotlin 1.0起,每個(gè)Lambda表達(dá)式都會(huì)被編譯成一個(gè)匿名類,除非它是一個(gè)內(nèi)聯(lián)Lambda。后續(xù)版本計(jì)劃支持生成Java 8字節(jié)碼,一旦實(shí)現(xiàn),編譯器就可以避免為每一個(gè)lambda表達(dá)式都生成一個(gè)獨(dú)立的.class文件。
如果Lambda捕捉了變量,每個(gè)被捕捉的變量會(huì)在匿名類中有對(duì)應(yīng)的字段,而且每次調(diào)用都會(huì)創(chuàng)建一個(gè)這個(gè)匿名類的新實(shí)例。否則,一個(gè)單例就會(huì)被創(chuàng)建。類的名稱由Lambda聲明所在的函數(shù)名稱加上后綴衍生出來,這個(gè)例子中就是TestLambdaKt$main$1.class。
// TestLambda.kt
package ch05
fun salute(callback: () -> Unit) = callback()
fun main(args: Array) {
salute { println(3) }
}
編譯后,生成兩個(gè)文件。
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, 構(gòu)造一個(gè)靜態(tài)實(shí)例ch05.TestLambdaKt$main$1 INSTANCE,在類加載的時(shí)候進(jìn)行賦值,同時(shí)繼承接口Function0,實(shí)現(xiàn)invoke方法:
final class ch05.TestLambdaKt$main$1 extends kotlin.jvm.internal.Lambda implements kotlin.jvm.functions.Function0
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."":(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 "":()V
7: putstatic #58 // Field INSTANCE:Lch05/TestLambdaKt$main$1;
10: return
}
再看下另外一個(gè)類TestLambdaKt.class, 在main方法中傳入TestLambdaKt$main$1.INSTANCE給方法salute,在方法salute中調(diào)用接口方法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);
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;)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內(nèi)部沒有匿名對(duì)象那樣的的this:沒有辦法引用到Lambda轉(zhuǎn)換成的匿名類實(shí)例。從編譯器角度看,Lambda是一個(gè)代碼塊不是一個(gè)對(duì)象,不能把它當(dāng)成對(duì)象引用。Lambda中的this引用指向的是包圍它的類。
如果在Lambda中要用到常規(guī)意義上this呢?這個(gè)就需要帶接收者的函數(shù)??聪卤容^常用的兩個(gè)函數(shù)with和apply。
直接上Kotlin的源碼,with在這里聲明成內(nèi)聯(lián)函數(shù)(后面找機(jī)會(huì)說), 接收兩個(gè)參數(shù),在函數(shù)體里面對(duì)接收者調(diào)用Lambda表達(dá)式。在Lambda表達(dá)式里面可以通過this引用到這個(gè)receiver對(duì)象。
/**
* Calls the specified function [block] with the given [receiver] as its receiver and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun with(receiver: T, block: T.() -> R): R = receiver.block()
看個(gè)例子:
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進(jìn)行append調(diào)用。
fun alphabet(): String {
val result = StringBuilder()
return with(result) {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
this.toString()
}
}
// 再進(jìn)一步
fun alphabet() = with(StringBuilder()) {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
toString()
}
with返回的值是執(zhí)行Lambda代碼的結(jié)果,該結(jié)果是Lambda中的最后一個(gè)表達(dá)式的值。如果想返回的是接收者對(duì)象,而不是執(zhí)行Lambda的結(jié)果,需要用apply函數(shù)。
apply函數(shù)幾乎和with函數(shù)一模一樣,唯一的區(qū)別就是apply始終返回作為實(shí)參傳遞給它的對(duì)象,也就是接收者對(duì)象。
/**
* Calls the specified function [block] with `this` value as its receiver and returns `this` value.
*/
@kotlin.internal.InlineOnly
public inline fun T.apply(block: T.() -> Unit): T { block(); return this }
apply被聲明稱一個(gè)擴(kuò)展函數(shù),它的接收者變成了作為實(shí)參傳入的Lambda的接收者。
fun alphabet() = StringBuilder().apply {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
}.toString()
可以調(diào)用庫函數(shù)再簡化:
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()
本文只是說了Kotlin中關(guān)于函數(shù)的一點(diǎn)特性,當(dāng)然也沒講全,比如內(nèi)聯(lián)函數(shù),高階函數(shù)等,因?yàn)樵賹懴氯ヌL了,所以后面再補(bǔ)充。從上面幾個(gè)例子也能大概感受到Kotlin的務(wù)實(shí)作風(fēng),提供了很多特性幫助開發(fā)者減少冗余代碼的編寫,可以提高效率,也能減少異常,讓程序猿早點(diǎn)下班,永葆頭發(fā)烏黑靚麗。