天天看點

Kotlin學習---函數的定義和調用(上)

上一篇文章中,我們對Kotlin中的類,屬性,函數,目錄結構有了一個大緻的了解,在這篇文章中,将對Kotlin中函數相關的特性做介紹。

文章目錄

    • 1.1 在Kotlin中建立集合
    • 1.2 讓函數更好調用
      • 1.2.1 命名參數
      • 1.2.2 預設參數值
      • 1.2.3 消除靜态工具類:頂層函數和屬性
    • 1.3 給别人的類添加方法:擴充函數和屬性
      • 1.3.1 導入和擴充函數
      • 1.3.2 從Java中調用擴充函數
      • 1.3.3 作為擴充函數的工具函數
      • 1.3.4 不可重寫的擴充函數
      • 1.3.5 擴充屬性

1.1 在Kotlin中建立集合

先來看一下Kotlin中幾種集合的建立方式

val set = hashSetOf(1, 7, 53)
val list = arrayListOf(1, 7, 53)
val map = hashMapOf(1 to "one", 7 to "seven", 53 to "fifty_three") // 裡面的 to 的用法并不是Kotlin裡面的特殊結構,它對應的隻是一個普通函數。這種用法叫做中綴調用,後面章節會有詳細的介紹。
           

這些建立集合的函數在Kotlin中都是 頂層函數 可以在任何地方調用。從函數名稱中就可以很容易的看出來我們建立的集合的類型。set 變量建立的集合類型在Java中對應的是 HashSet 類 ,list變量建立的集合類型在Java中對應的是 ArrayList 類 ,map變量建立的集合類型在Java中對應的是 HashMap 類。為了更加直覺的展示所建立的集合類型,我們運作下面的代碼看一下:

>>>  println(set.javaClass)  //Kotlin中的javaClass等價于Java中的getClass() 是一個擴充屬性
class java.util.HashSet


>>>  println(list.javaClass)
class java.util.ArrayList


>>>  println(map.javaClass)
class java.util.HashMap
           

可以看到跟我們上面分析的結果是一緻的。在Kotlin中沒有采用自己的集合類,而是使用了标準的Java集合類,這對于熟悉Java的開發者來說是一個好消息,所有你熟知的集合相關的知識都可以在Kotlin中使用并且不需要考慮相容性的問題。Kotlin中使用Java集合的而沒有使用自己專門的集合的原因也就在這裡。

盡管Kotlin中的集合使用的是标準Java中的集合,但是Kotlin對其做了很多擴充,以友善開發者的使用,例如可以通過下面的方式擷取一個清單中的最後一個元素,或者得到數字清單中的最大值:

val strings = listOf("first", "second", "fourteenth")
>>>  println(strings.lastIndex)
fourteenth


val numbers = setOf(1, 14, 2)
>>> println(numbers.max())
14
           

在Kotlin中還存在很多像 lastIndex 和 max() 這樣的擴充屬性和擴充方法。這些擴充元素提高了開發者在平時開發過程中的效率,并且使用起來是如此的簡單 。後續章節将會詳細的講到是如果實作的。接下來先讓我們看一下函數聲明。

1.2 讓函數更好調用

在Java集合中有一個預設的 toString 實作,但是它的輸出是固定的,結果往往不是我們想要的格式,這個時候我們通過會使用一些第三方庫,比如說 Guava 和 Apache Commons ,或者通過重寫對應的 toString() 函數來實作自己想要的格式。對于這個問題,Kotlin中專門有一個函數來解決。

本章節就讓我們一步一步的實作這個函數,在實作過程中我們會學習到很多函數相關的特性。

首先描述一下函數要實作的功能。下面的 joinToString 函數展現了通過元素中間添加分隔符号,在最前面添加字首,在最末尾添加字尾的方式把集合的元素逐個添加到一個 StringBuilder 的過程:

fun <T> joinToString(collection: Collection<T>,
                    separator: String,
                    prefix: String,
                    postfix: String): String {
    val result = StringBuilder(prefix)

    for ((index, element) in collection.withIndex()) {
        if ((index) > 0) result.append(separator)  //不會在第一個元素前面添加分隔符
        result.append(element)
    }

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

在這個函數中用到了泛型:它支援元素為任意類型的集合。這個函數運作起來的效果如下:

val list = listOf(1, 2, 3)
>>>  println(joinToString(list, "; ", "( ", ")"))
(1; 2; 3)
           

我們已經基本上實作了給集合添加分隔符,字首和字尾輸出的函數了。接下來我們會聚焦到它的聲明:要怎麼修改,讓這個函數調用更加的簡潔?有沒有什麼辦法可以讓我們不用每次都調用的時候都傳入四個參數呢?

1.2.1 命名參數

首先讓看一下這個函數的可讀性問題。舉個栗子,如果我這樣調用 joinToString :

你能夠第一時間看出來這些參數都代表什麼嗎?如果不去看函數聲明的話就很難回答這些問題。

在Kotlin中我們可以這樣處理:

在調用Kotlin定義的函數時,我們可以顯式的表明一些參數的名稱,同時這個時候并不關心原函數中的參數順序。需要注意的是,如果在調用一個函數時,指明了一個參數的名稱,為了避免混淆(編譯器會報錯),那它之後(之前的參數不受影響)的所有參數都需要标明名稱 。

當你調用Java函數時,不能采用命名參數,不管是JDK中的函數,還是Android架構的函數。把參數名稱存到.class檔案時是Java8及其更高版本的一個可選功能,而Kotlin需要保持和Java6的相容性。是以,編譯器不能識别出調用函數的參數名稱,然後把這些參數名對應到函數的定義的地方。

1.2.2 預設參數值

Java中另一個普通存在的問題就是一些類的重載函數實在是太多了。可以看一下 java.lang.Thread 類,裡面有8個構造方法,我的天!當然這些重載是為了向後相容,友善這些API的使用者,但是這就導緻了一個結果:重複。這些參數名稱和類型被重複一遍又一遍,同時如果你是一個良好公民的話,還必須每次重載的時候都重複大部分的文檔。真的是想想都麻煩。我自己在項目中也經常遇到這種情況,也是非常的無奈。

而在Kotlin中可以通過在聲明函數的時候,指定參數的預設值,這樣就可以避免建立重載的函數。讓我們使用預設參數值來改進一下 joinToString 函數。在大多數情況下,字元串可以不加字首或者字尾并用逗号分隔。是以,我們把這幾個參數都設定成預設值:

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

來看看該如何調用這個有預設值的函數:

>>>  println(joinToString(list))
1, 2, 3


>>>  print(joinToString(list, "; "))
1; 2; 3
           

使用起來是不是跟重載一個效果呢。使用 預設參數值 ,就不需要再寫那麼多的重載函數了。

當使用正常的文法時,必須按照函數聲明中定義的順序來給定參數,可以省略的隻有排在 末尾的參數。如果使用命名參數,可以省略中間的一些參數,也可以以你想要的任意順序隻給定你需要的參數:

>>>  joinToString(list, postfix = ";", prefix = '# ')
# 1, 2, 3;
           

需要注意,參數的預設值是被編碼到 調用的函數 中的,而不是調用的地方。

預設值和Java

如果你的方法是用Kotlin寫的,在Java中調用這個方法時,你不想每次調用都傳入所有的參數,希望能夠實作Java中寫多個重載函數一樣的效果,這個時候你可以在Kotlin的方法上添加 @JvmOverloads 注解。當有這個注解時,編譯器會自動生成Java重載函數,從最後一個開始省略每個參數。給 joinToString 函數添加這個注解的效果:

/* java */
String joinToString(Collection<T> collection, String separator, String prefix, String postfix);

String joinToString(Collection<T> collection, String separator, String prefix);

String joinToString(Collection<T> collection, String separator);

String joinToString(Collection<T> collection);
           

1.2.3 消除靜态工具類:頂層函數和屬性

讓我們回想一下,我們的項目中是不是存在很多 Util* 類檔案,很多時候可能一個類檔案裡面就隻有幾個方法,但是為了清楚的區分這些方法,我們也隻能這麼處理。這些檔案裡沒有執行個體函數或者這些類不包含任何的狀态,僅僅是一堆靜态函數的容器。對于這些類,Kotlin中可以直接放在代碼檔案的頂層,它不屬于任何一個類。可以通過import直接導入這些函數所在的檔案,而不需要在引入類這個層級。

讓我們把 joinToString 直接放到strings的包中試一下。依照下面這樣,建立一個join.kt檔案:

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

那麼這個檔案會被編譯成什麼樣呢?JVM中隻能執行類中的代碼,是以這個檔案最後會被編譯成一個跟Java中結構類似的檔案。為了友善了解,我們用一段Java代碼來表示join.kt編譯之後的結構:

/*Java*/
package string;
public class JoinKt{
    public static String joinToString(...) {...}
}
           

可以看到Kotlin檔案編譯生成的類的名稱,對應于包含函數的檔案的名稱。這個檔案中的所有頂層函數編譯為這個類的靜态函數。我們看一下Java是如何調用這段代碼的:

import strings.JoinKt;
...
JoinKt.joinToString(list, ", ", "", "");
           

修改檔案類名

如果要修改Kotlin頂層函數生成的類的名稱,可以為這個檔案添加 @JvmName 的注解,将其放到這個檔案的開頭,位于包名的前面:

@file:JvmName(“StringFunctions”) //注解指定類名

package strings //包的聲明跟在檔案注解之後

fun joinToString(…): String {…}

現在我們在Java中可以這樣調用這個函數:

import strings.StringFunctions;

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

頂層屬性

和頂層函數一樣,屬性也可以放到檔案的頂層。比如說可以使用 var 屬性來計算一些函數被執行的次數:

var opCount = 0  //聲明一個頂層屬性

fun performOperation() {
    opCount++  //改變該屬性的值
    //...
}

fun reportOperationCount() {
    println("Operation performed $opCount times") //讀取屬性值
}
           

也可以在代碼中用頂層屬性來定義常量:

預設情況下,頂層屬性和其他任意的屬性是一樣的,是通過 通路器 暴露給Java使用的(如果val就隻有一個getter,如果是var就對應一對getter和setter)。

為了友善使用,如果你想把一個常量以 public static final 的屬性暴露給Java,可以用 const 來修飾它(隻适用于所有的 基礎資料類型 的屬性,以及 String類型,其他類型的資料比如自定義的Bean類使用時會報 Const ‘val’ has type ‘Person’. Only primitives and String are allowed 錯誤)。我們從位元組碼層面來看一下有 const 和沒有 const 修飾的屬性有什麼不同(這裡不展示對應的位元組碼,大家可以自行去了解,這裡我隻展示最後分析的結果。如果不知道怎麼看Kotlin的位元組碼,可以看一下我的這篇文章 使用Android studio檢視Kotlin的位元組碼):

//Kotlin
const val UNIX_LINE_SEPARATOR = "\n"
//等同于
//Java
public static final String UNIX_LINE_SEPARATOR = "\n"


//Kotlin 
val UNIX_LINE_SEPARATOR = "\n"
//等同于
//Java
@NotNull
private static final String UNIX_LINE_SEPARATOR = "\n";

@NotNull
public static final String getUNIX_LINE_SEPARATOR() {
    return UNIX_LINE_SEPARATOR;
}


//簡單一句話概括,沒有 const 和有 const 差別在于,從調用方法上講一個通過getXXX來調用一個直接通過.XXX來調用。從結構上講,一個還是屬性,一個變成了字段。
           

我們已經對 joinToString 改進了很多了,接下來讓我們看看還有沒有其他方法能夠讓 joinToString 更加的好用呢。

1.3 給别人的類添加方法:擴充函數和屬性

一個Java項目,如果我們使用Kotlin的話肯定會遇到需要在原Java類中增加新函數的時候,當然你可以把原來的Java類用Kotlin重寫一遍,但是有沒有更好的辦法,在即不用改變原來Java類的基礎上又享受到Kotlin的文法特點呢?這就要說到Kotlin中的擴充函數了。

從理論上來講,擴充函數 非常的簡單,它就是一個類的 成員函數 ,不過這個成員函數是定義在類外部的。通過代碼來感受一下什麼是 擴充函數 。我們來寫一個擷取到一個字元串的最後一個字元的函數:

package strings

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

//String 叫做接收者類型
//this 叫做接收者對象
           

我們可以看到定義一個 擴充函數 十分的友善。我們隻需要在要定義的函數名稱前加上你要擴充的 類或者接口 的名稱就可以了。這個類名稱被叫做 接收者類型;用來調用這個擴充函數的對象,叫做 接收者對象,可以看代碼中的注釋。接收者類型是由擴充函數定義的,接收者對象是該類型的一個執行個體。

我們看一下該如何使用調用這個函數:

>>> println("Kotlin".lastChar())
n
           

在這個例子中 Kotlin 是接收對象者,String 是接收者類型。

從某種意義上來講,你已經為String類添加了自己的方法了。即使字元串不是代碼的一部分,也沒有類的源碼,你仍然可以在自己的項目中根據需要去擴充方法。

在擴充函數中可以像其他成員函數一樣用 this 。而且也可以像普通的成員函數一樣,省略它。

package strings 
fun String.lastChar(): Char = this[length - 1]
           

在擴充函數中,可以直接通路被擴充的類的其他方法和屬性,但是擴充函數是不允許你打破它的

封裝性

的。和定義在内部的方法不同的是,擴充函數不能通路私有的或者是受保護的成員。

1.3.1 導入和擴充函數

我們定義的擴充函數不會自動在整個項目範圍内生效,是以需要在用到的時候跟類和其他方法一樣需要導入。這樣是為了避免命名沖突。Kotlin中允許用和導入類一樣的文法來導入單個函數:

import strings.lastChar

val c = "Kotlin".lastChar()
           

當然也可以用 * 來導入:

import strings.*

val c = "Kotlin".lastChar()
           

可以使用關鍵詞 as 來修改導入的類或者函數的名稱:

import strings.lastChar as last

val c = "Kotlin".last()
           

在項目中可能存在不同包名,但是是重名的函數,這個時候如果同時導入了這兩個函數,那麼導入時給它重新命名就顯得很重要了。一般對于重名的函數還有一種處理方式就是:選擇用 全名來指出這個類或者函數。但是對于擴充函數,Kotlin的文法要求你用 簡短的名稱,是以這個時候關鍵詞 as 就是解決沖突的唯一方式了。

1.3.2 從Java中調用擴充函數

說完了Kotlin中怎麼使用擴充函數,接下來我們看一下在Java中該如何調用擴充函數。從本質上來講,擴充函數是 靜态函數 ,它把調用對象作為了它的第一個參數。調用擴充函數 不會建立适配的對象或者任何運作時的消耗。

是以Java中調用擴充函數就相當于:調用這個靜态函數,然後把接收者對象作為第一個參數傳進去。看一個例子:

/* Java */
char c = StringUtilKt.lastChar("Java");

//StringUtilKt 是lastChar這個擴充函數所在的Kotlin檔案名。
           

1.3.3 作為擴充函數的工具函數

前面鋪墊了這麼久,最後我們終于可以寫一個 joinToString 函數的終極版本了,它和你在Kotlin标準庫中看到的是一模一樣的。

fun <T> Collection<T>.joinToString(separator: String = ", ",    // 為 Collection<T> 聲明了一個擴充函數
                                   prefix: String = "",   //預設參數
                                   postfix: String = ""): String {
    val result = StringBuilder(prefix)
    for ((index, element) in this.withIndex()) {   //this指向接收者對象:T的集合
        if (index > 0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}

>>>  val list = listOf(1, 2, 3)
>>>  println(list.joinToString(separator = "; ", prefix = "(", postfix = ")"))
(1; 2; 3;)
           

本質上擴充函數無非就是靜态函數的一個 高效的文法糖 ,可以使用更具體的類型來作為接收者類型,而不是一個類。比如你像要一個 join 函數,隻能由字元串的集合來觸發。

fun Collection<String>.join(separator: String = ", ",
                            prefix: String = "",   
                            postfix: String = "") = joinToString (separator, prefix, postfix)
                            
>>>  println(listOf("one", "two", "eight").join(" "))
one two eight
           

如果用其他類型來調用,将會報錯:

>>>  listOf(1, 2, 8).join()

Error: Type mismatch: inferred type is List<Int> buy Collection<String> was expected.
           

擴充函數的靜态性質決定了擴充函數不能被子類重寫。

1.3.4 不可重寫的擴充函數

在Kotlin中,重寫成員函數是一件很平常的事情。但是不能重寫擴充函數。假設有兩個類,View和它的子類Button,然後Button重寫了父類的click函數。

open class View{
    open fun click() = println("View clicked")
}

class Button: View(){
    override fun click() = println("Button clicked")
}

>>>  val view: View = Button()
>>>  view.click()   //具體調用哪個方法,由實際的view的值來決定
Button clicked
           

我們可以看到當你聲明了類型為View的變量,同時它被指派為Button類型的對象,當你調用click函數時,會調用到Button中重寫的函數。但是對于擴充函數來說,并不是這樣。

擴充函數并不是類的一部分,它是聲明在類外的。雖然可以給基類和子類都定義一個同名的函數,但是最後調用的時候它是由該變量的 靜态類型 決定的,而不是這個變量的 運作時類型 。

fun View.showOff() = println("I'm a view!")
fun Button.showOff() = println("I'm a button!")

>>>  val view: View = Button()
>>>  view.showOff()
I'm a view!    //擴充函數被靜态地解析
//雖然view這個對象最後指派的是Button類的執行個體,但是在調用擴充函數時,仍然調用的是View類中的showOff擴充函數。
           

如你所見,擴充函數并不存在 重寫 ,因為Kotlin會把他們當作靜态函數對待。

注意:如果一個類的成員函數和擴充函數有相同的簽名,成員函數往往會被優先調用。

看完擴充函數,讓我們來看一下Kotlin中的擴充屬性。

1.3.5 擴充屬性

上一節中的lastChar函數我們把它轉換成一個屬性試試:

val String.lastChar: Char   //隻能定義成val屬性,因為沒有支援字段
    get() = this[length-1]
           
  • 和擴充函數一樣,擴充屬性也像接收者的一個普通的成員屬性一樣。
  • 必須定義getter()函數,因為沒有 支援字段(Backing Fields),是以沒有預設getter的實作。
  • 初始化也是不可以的:因為沒有地方存儲初始化值。

如果在StringBuilder上定義一個相同的屬性,可以置為var,因為StringBuilder的内容是可變的。

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

可以像通路成員屬性一樣通路它:

>>>  println("Kotlin".lastChar)
n
>>>  val sb = StringBuilder("Kotlin?")
>>>  sb.lastChar = '!'
>>>  println(sb)
Kotlin!
           

注意,當你需要從Java中通路擴充屬性的時候,應該顯式的調用它的getter函數:

關于擴充函數和擴充屬性相關的内容已經講完了,下面一篇文章将會詳細的講一講Kotlin中集合、字元串、正規表達式等等内容,如果感興趣小夥伴,記得關注一下哈~

對于文中有疑惑的地方,或者有任何意見和建議的地方都可以評論留言,我會第一時間回複~與君共勉。