Kotlin 入門到進階(11) -- 函數(shù)詳細釋義

主要內(nèi)容:

  • 函數(shù)的定義
  • 頂級函數(shù)
  • 定義函數(shù)參數(shù)默認值
  • 調(diào)用函數(shù)時給定參數(shù)名
  • 單表達式函數(shù)
  • 中綴調(diào)用
  • 擴展函數(shù)
  • 局部函數(shù)
  • 匿名函數(shù)
  • 函數(shù)可變參數(shù)及原理分析
  • Java Arrays.asList 可變參數(shù)的坑和原理分析
  • 展開操作

前面我們已經(jīng)介紹了函數(shù)的定義和組成,下面在繼續(xù)分析函數(shù)的其他方面

更方便的函數(shù)調(diào)用

調(diào)用函數(shù)時指定參數(shù)的名字

假設我們有如下的函數(shù):

fun <T> joinToString(collection: Collection<T>,
    separator: String, 
    prefix: String, 
    postfix: String): String

然后調(diào)用該函數(shù)(為參數(shù)值指定參數(shù)名稱):

joinToString(collection, separator = " ", prefix = " ", postfix = ".")

為函數(shù)參數(shù)指定默認值

我們可以把 joinToString 定義改成如下形式:

fun <T> joinToString(collection: Collection<T>, 
    separator: String = ", ", 
    prefix: String = "", 
    postfix: String = "")

我們分別為函數(shù)的最后三個參數(shù)都設置了默認值却舀,我們可以這樣調(diào)用該函數(shù):

joinToString(list)
joinToString(list, prefix = "# ")

這樣也就間接的實現(xiàn)了Java中所謂的重載(overload)鸯屿,代碼也更簡潔虑稼,不用定義多個方法了

Parameter和Argument的區(qū)別

看過 《Kotlin In Action》 的英文原版細心的同學可能會發(fā)現(xiàn):書中的 3.2.1 章節(jié)是 Named Arguments

直譯過來是:為參數(shù)命名喉前。作者為什么沒有寫成 Named Parameters 呢哩掺?

下面我們就來看下 Parameter 和 Argument 的區(qū)別

簡而言之垄懂,就是在定義函數(shù)時候的參數(shù)稱之為 Parameter虚汛;調(diào)用函數(shù)傳入的參數(shù)稱之為 Argument

此外围来,除了 Parameter 和 Argument 跺涤,還有 Type Parameter 和 Type Argument

因為下面還要用到這兩個的概念,所以這里我們介紹下 Type Parameter 和 Type Argument

Type Parameter 和 Type Argument 的概念是在泛型類或者泛型函數(shù)的時候出現(xiàn)

頂級函數(shù)和屬性

在 Java 中我們需要把函數(shù)和屬性放在一個類中

在 Kotlin 中我們可以把某個函數(shù)或屬性直接放到某個 Kotlin 文件中

把這樣的函數(shù)或屬性稱之為 頂級(top level)函數(shù)或屬性

例如在 join.kt 文件中:

package strings

fun joinToString(...): String { 
    ... 
}

在 Java 代碼中如何調(diào)用該方法呢监透?因為 JVM 虛擬機只能執(zhí)行類中的代碼

所以 Kotlin 會生成一個名叫 JoinKt 的類桶错,并且頂級函數(shù)是靜態(tài)的

所以可以在 Java 中這樣調(diào)用頂級函數(shù):

JoinKt.joinToString(...)

在Kotlin中如何調(diào)用,如果在不同的包胀蛮,需要把這個頂級函數(shù)導入才能調(diào)用

//相當于 import strings.JoinKt.joinToString
import strings.joinToString 

//相當于 import strings.JoinKt.*
import strings.* 

所有的工具類都可以使用這樣的方式來定義

頂級屬性 同樣也是 static 靜態(tài)的

如果使用 var 來定義會生成對應的靜態(tài)setter院刁、getter函數(shù)

如果使用 val 來定義只會生成對應的靜態(tài)getter函數(shù)

我們知道頂級函數(shù)和屬性,最終還是會編譯放在一個類里面醇滥,這個類名就是頂級函數(shù)或屬性的 Kotlin文件名稱+Kt

如果所在的Kotlin文件名被修改黎比,編譯生成的類名也會被修改,可以通過注解的方式來固定編譯生成的類名:

@file:JvmName("StringFunctions")

package strings
fun joinToString(...): String { 
    ... 
}

調(diào)用的時候就可以這樣來調(diào)用:

import strings.StringFunctions; 

StringFunctions.joinToString(list, ", ", "", "");

擴展函數(shù)

何謂 擴展函數(shù) 鸳玩? 擴展函數(shù)是在類的外部定義阅虫,但是可以像類成員一樣調(diào)用該函數(shù)

其中 receiver type 就是我們擴展的目標類,receiver object 就是目標類的對象(哪個對象調(diào)用該擴展函數(shù)不跟,這個this就是哪個對象)

lastChar 就是我們?yōu)?String 類擴展的函數(shù)

package strings

fun String.lastChar(): Char = this.get(this.length - 1)

然后我們這樣來調(diào)用該擴展函數(shù):

println("Kotlin".lastChar())

如果擴展函數(shù)所在的包名和使用地方的包名不一樣的話颓帝,需要導入擴展函數(shù)

import strings.*
//或者
import strings.lastChar

val c = "Kotlin".lastChar()

擴展函數(shù)原理分析

擴展函數(shù)本質上是靜態(tài)函數(shù),如上面的擴展函數(shù) lastChar 反編譯后對應的 Java 代碼:

public static final char lastChar(@NotNull String $receiver) {
  Intrinsics.checkParameterIsNotNull($receiver, "receiver$0");
  return $receiver.charAt($receiver.length() - 1);
}

編譯的時候,會在調(diào)用的該擴展函數(shù)的地方使用 StringUtilsKt.lastChar("") 代替

所以购城,如果要在 Java 中使用 Kotlin 定義的擴展函數(shù)吕座,也是直接調(diào)用該靜態(tài)方法即可

并且擴展函數(shù)是不能被覆寫(override) 的,因為它本質上是一個靜態(tài)函數(shù)

擴展屬性

擴展屬性和擴展函數(shù)的定義非常相似:

val String.lastChar: Char 
    get() = this.get(length - 1)

我們必須為這個擴展屬性定義 getter 函數(shù)瘪板,因為擴展屬性沒有 backing field

擴展屬性在定義的時候吴趴,也會生成靜態(tài)方法:

public static final char getLastChar(@NotNull String $receiver) {
  Intrinsics.checkParameterIsNotNull($receiver, "receiver$0");
  return $receiver.charAt($receiver.length() - 1);
}

如果擴展屬性的 receiver object 可以被修改,可以把擴展屬性定義成 var

var StringBuilder.lastChar: Char
    get() = get(length - 1) 
    set(value: Char) {
        this.setCharAt(length - 1, value) 
    }

函數(shù)的可變參數(shù)和展開操作符

可變參數(shù)

在 Java 中通過三個點(...)來聲明可變參數(shù)侮攀,如:

public static <T> List<T> listOf(T... items) {
    System.out.println(items.getClass()); //數(shù)組類型
    return Arrays.asList(items);
}

Kotlin 和 Java 不一樣锣枝,Kotlin 使用 vararg 關鍵來定義可變參數(shù):

fun <T> listOf(vararg items: T): List<T> {
    println(items.javaClass)     //數(shù)組類型
    return Arrays.asList(*items) // * spread operator
}

對于可變參數(shù)的函數(shù),調(diào)用它的時候可以傳遞任意個參數(shù)

展開操作符

通過上面的兩段代碼比較我們發(fā)現(xiàn):Kotlin 需要顯示的將可變參數(shù)通過 * 展開兰英,然后傳遞給 asList 函數(shù)

這里的 * 就是 展開操作符(spread operator)撇叁,在 Java 中是沒有 展開操作符

下面我們再來看下,展開操作符的方便之處:

val intArr: Array<Int> = arrayOf(1, 2, 3, 4)
Arrays.asList(0, intArr).run {
    println("size = $size")
}

//輸出結果:
size = 2

可以發(fā)現(xiàn)畦贸,不用展示操作符的話陨闹,集合里面只有兩個元素

那我們把它改成使用 展開操作符 的情況:

val intArr: Array<Int> = arrayOf(1, 2, 3, 4)
Arrays.asList(0, *intArr).run {
    println("size = $size")
}

//輸出結果:
size = 5

Java中的Arrays.asList()的坑和原理分析

既然上面用到了 Java 中的 Arrays.asList() 函數(shù),下面來講下該函數(shù)的容易遇到的坑及原理分析:

public static void testArrays() {
    int[] intArr = {1, 2, 3};
    List list = Arrays.asList(intArr);
    println(list.size());   //size = 1
}

public static void testArrays2() {
    Integer[] intArr ={1, 2, 3};
    List list = Arrays.asList(intArr);
    println(list.size());  //size = 3
}

上面的 testArraystestArrays2 函數(shù)非常相似薄坏,只不過是數(shù)組的類型不同趋厉,導致 Arrays.asList(arr) 返回的集合大小不一樣

只要是 原始類型數(shù)組 Arrays.asList 返回的集合大小為 1,如果是 復雜類型的數(shù)組胶坠,Arrays.asList 返回的集合大小為數(shù)組的大小

為什么會產(chǎn)生這種情況呢觅廓?下面來分析下:

首先看下 Arrays.asList 是怎么定義的:

public static <T> List<T> asList(T... a)

Java 中的可變參數(shù)相當于數(shù)組:

public static <T> List<T> asList(T[] a)

我們知道 Java 中的泛型必須是復雜類型,所以這里的泛型 T 也必須是 復雜類型

當我們傳遞 int[] 數(shù)組的時候涵但,就會出現(xiàn)問題杈绸,因為 int 是原始類型,T 是復雜類型

所以 int[] 賦值給 T[] 是非法的矮瘟,當 一維原始類型的數(shù)組 當做給可變參數(shù)的時候瞳脓,編譯器會把這個可變參數(shù)編譯成一個 二維數(shù)組

就是為什么會出現(xiàn)上面情況的原因**

我們再來看下 Arrays.asList 完整源碼:


public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}

private static class ArrayList<E> extends AbstractList<E>
    implements RandomAccess, java.io.Serializable
{
    private static final long serialVersionUID = -2764017481108945198L;
    private final E[] a;

    ArrayList(E[] array) {
        a = Objects.requireNonNull(array);
    }
    //省略其他...
}

經(jīng)過上面的分析我們知道,如果是一維原始類型的數(shù)組傳遞給可變參數(shù)澈侠,這個可變參數(shù)就是 二維數(shù)組

然后把二維數(shù)組傳遞給內(nèi)部ArrayList的構造方法劫侧,通過 E[] 保存下來。這里的泛型 E 就相當于 int[]哨啃,E[] 相當于 int[][]

需要注意是 Java 不允許 將個二維數(shù)組 直接賦值 給一維的泛型數(shù)組:

int[][] intArray = {{1},{2}};
T[] t = intArray;  //非法

但是 Java 允許 把二維數(shù)組傳遞給參數(shù)是一維的泛型數(shù)組的函數(shù)烧栋,如:

public static <T> void testGeneric(T[] data){
}
int[][] intArray = {{1},{2}};
testGeneric(intArray);

Kotlin 展開操作符的原理分析

講到這里你可能迫不及待的想知道,為什么我們上面的代碼使用了展開操作符 Arrays.asList(*intArr) 返回的集合大小就是 5 呢拳球?

val intArr: Array<Int> = arrayOf(1, 2, 3, 4)
Arrays.asList(0, *intArr).run {
    println("size = $size")
}

//輸出結果:
size = 5

反編譯后對應的 Java 代碼如下:

Integer[] intArr2 = new Integer[]{1, 2, 3, 4};
SpreadBuilder var10000 = new SpreadBuilder(2);
var10000.add(0);             //第1個元素
var10000.addSpread(intArr2); //數(shù)組里的4個元素
List var2 = Arrays.asList((Integer[])var10000.toArray(new Integer[var10000.size()]));
int var7 = false;
String var5 = "size = " + var2.size();
System.out.println(var5);

原來會通過 SpreadBuilder 來處理展開操作符审姓,SpreadBuilder 里面維護了一個ArrayList

所有的元素都會保存到這個 ArrayList 中,然后把這個集合轉成 元素為復雜類型數(shù)組祝峻,再傳給 Arrays.asList(arr) 函數(shù)

根據(jù)上面我們對 Arrays.asList(arr) 的分析魔吐,我們就知道返回的集合大小是 5

中綴調(diào)用

我們都知道什么是前綴(prefix)扎筒,后綴(suffix)。那什么是函數(shù)的中綴(infix)調(diào)用呢酬姆?

使用關鍵字 infix 修飾的函數(shù)都能夠 中綴調(diào)用

被關鍵字 infix 修飾的函數(shù)只能有一個參數(shù)

Kotlin 中的 to 就是一個中綴函數(shù):

public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

下面我們來對比下 to 函數(shù)的常規(guī)調(diào)用和中綴調(diào)用:

1.to("one")  //普通的函數(shù)調(diào)用
1 to "one"   //函數(shù)的中綴調(diào)用

除了 to 函數(shù)嗜桌,還有我們介紹 循環(huán) 的時候講到的 until、downTo辞色、step 也是中綴函數(shù):

public infix fun Int.until(to: Int): IntRange {
    if (to <= Int.MIN_VALUE) return IntRange.EMPTY
    return this .. (to - 1).toInt()
}

public infix fun Int.downTo(to: Int): IntProgression {
    return IntProgression.fromClosedRange(this, to, -1)
}

public infix fun IntProgression.step(step: Int): IntProgression {
    checkStepIsPositive(step > 0, step)
    return IntProgression.fromClosedRange(first, last, if (this.step > 0) step else -step)
}


//使用示例:
for(i in 0 until 100){
}

for (i in 100 downTo 0 step 2) {
}

局部函數(shù)

局部函數(shù)(local function) 是在函數(shù)里面定義函數(shù)骨宠,局部函數(shù)只能在函數(shù)內(nèi)部使用j局部函數(shù)說白了就是函數(shù)嵌套,那什么時候使用局部函數(shù)呢相满?當一個函數(shù)里的邏輯很多重復的邏輯诱篷,可以把這些邏輯抽取到一個局部函數(shù)

以《Kotlin In Action》的代碼為例:

fun saveUser(user: User) {
    if (user.name.isEmpty()) {
        throw IllegalArgumentException("Cannot save user ${user.id}: Name is empty")
    }
    if (user.address.isEmpty()) { 
        throw IllegalArgumentException("Cannot save user ${user.id}: Address is empty")
    }
    // Save user to the database 
}

這個 saveUser 函數(shù)里面有些重復邏輯,如果 name 或 address 為空都會拋出異常

可以使用局部函數(shù)優(yōu)化下:

fun saveUser(user: User) {
    fun validate(value: String, fieldName: String) { 
        if (value.isEmpty()) {
            throw IllegalArgumentException("Can't save user ${user.id}: " + "$fieldName is empty")
        }
    }
    validate(user.name, "Name") 
    validate(user.address, "Address")
    // Save user to the database   
}

局部函數(shù)避免了模板代碼的出現(xiàn)雳灵。如果不使用局部函數(shù),我們需要把 validate函數(shù) 定義到外面去闸盔,但是這個函數(shù)只會被 saveUser函數(shù) 使用到悯辙,從而污染了外面的全局作用域。通過局部函數(shù)使得代碼更加清晰迎吵,可讀性更高躲撰。

需要注意的是,雖然 Kotlin 允許在函數(shù)內(nèi)部定義函數(shù)击费,但是不要嵌套太深拢蛋,否則會導致可讀性太差

匿名函數(shù)

匿名函數(shù)顧名思義就是沒有名字的函數(shù):如:

fun(x: Int, y: Int): Int {
    return x + y
}

匿名函數(shù)的返回類型的推導機制和普通函數(shù)一樣:

fun(x: Int, y: Int) = x + y

如果聲明了一個匿名函數(shù) ,如何調(diào)用呢蔫巩?

(fun(x: Int, y: Int): Int {
    val result = x + y
    println("sum:$result")
    return result
})(1, 9)

//輸出結果:
//sum:10
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末谆棱,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子圆仔,更是在濱河造成了極大的恐慌垃瞧,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件坪郭,死亡現(xiàn)場離奇詭異个从,居然都是意外死亡,警方通過查閱死者的電腦和手機歪沃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門嗦锐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人沪曙,你說我怎么就攤上這事奕污。” “怎么了液走?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵菊值,是天一觀的道長外驱。 經(jīng)常有香客問我,道長腻窒,這世上最難降的妖魔是什么昵宇? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮儿子,結果婚禮上瓦哎,老公的妹妹穿的比我還像新娘。我一直安慰自己柔逼,他們只是感情好蒋譬,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著愉适,像睡著了一般犯助。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上维咸,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天剂买,我揣著相機與錄音,去河邊找鬼癌蓖。 笑死瞬哼,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的租副。 我是一名探鬼主播坐慰,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼用僧!你這毒婦竟也來了结胀?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤责循,失蹤者是張志新(化名)和其女友劉穎把跨,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體沼死,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡着逐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了意蛀。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片耸别。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖县钥,靈堂內(nèi)的尸體忽然破棺而出秀姐,到底是詐尸還是另有隱情,我是刑警寧澤若贮,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布省有,位于F島的核電站痒留,受9級特大地震影響,放射性物質發(fā)生泄漏蠢沿。R本人自食惡果不足惜伸头,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望舷蟀。 院中可真熱鬧恤磷,春花似錦、人聲如沸野宜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽匈子。三九已至河胎,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間虎敦,已是汗流浹背游岳。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留原茅,地道東北人。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓堕仔,卻偏偏與公主長得像擂橘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子摩骨,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內(nèi)容