kotlin中有趣的用法,函數式編程

1.擴展函數

在 java中 api 定義許多好用的方法,但有些方法並不能解決我們的需求,經常看到項目中許多util類。
例如判斷一個字符串是不是郵箱格式java 代碼可能是這樣的

public static boolean isEmail(String email) {
        return Pattern.compile("^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$")
                .matcher(email).matches();
    }
   ……
    public static void main(String[] args) {
        String str = "";
        StringUtil.isEmail(str);
    }

在 kotlin 中可以利用擴展函數,實現上述代碼

//此方法不需要寫在某個類中,任意一個.kt文件即可
//爲某個類擴展,就在函數名前加上類名.最爲前綴,方法體中可以使用 this 關鍵字
//獲取到調用該方法的對象
fun String.isEmail(): Boolean =
    Pattern.compile("^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*\$")
        .matcher(this).matches()
fun main() {
    val str=""
    // 調用擴展函數,使用擴展類的對象就可以調用,看上去就像該類本身具有的成員方法
    println(str.isEmail())
}

kotlin 如何實現擴展函數的呢,通過產看 kotlin字節碼轉成 java 代碼你就明白了
下面是轉換之後的代碼

  public static final boolean isEmail(@NotNull String $receiver) {
      Intrinsics.checkParameterIsNotNull($receiver, "receiver$0");
      return Pattern.compile("^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$").matcher((CharSequence)$receiver).matches();
   }

發現代碼和我們以前的寫的 java 代碼差不多,只不過 kotlin 的語法可以實現擴展函數的樣子
kotlin api提供許多有用的擴展函數,如一些類型轉換的擴展函數,toInt()、toIntOrNull()、toFloat()、toLong、toXXX()……
例源碼中的 toInt()

public actual inline fun String.toInt(): Int = java.lang.Integer.parseInt(this)

2.kotlin 中的高階函數——函數可以當做參數或返回值

kotlin 是函數式編程,kotlin 中函數也是一種類型。例如

	//block1 無參數無返回值函數類型
    val block1: () -> Unit
    // block2 無參數返回值爲 String 的函數類型
    val block2: () -> String
    // block2 有一個 Int 類型的參數無返回值的函數類型
    val block3: (Int) -> Unit
    // block4 有2個參數返回值爲 String 的函數類型
    val block4: (Int, String) -> String

函數類型對應在kotlin.jvm.functions 中都有定義

package kotlin.jvm.functions

/** A function that takes 0 arguments. */
public interface Function0<out R> : Function<R> {
    /** Invokes the function. */
    public operator fun invoke(): R
}
/** A function that takes 1 argument. */
public interface Function1<in P1, out R> : Function<R> {
    /** Invokes the function with the specified argument. */
    public operator fun invoke(p1: P1): R
}
/** A function that takes 2 arguments. */
public interface Function2<in P1, in P2, out R> : Function<R> {
    /** Invokes the function with the specified arguments. */
    public operator fun invoke(p1: P1, p2: P2): R
}

這個文件中定義了從 Function0 到 Function22 接口,Function後面的數字代表可以有幾個參數,
Function0<out R>代表無參數返回值爲指定泛型 R,Function1<in P1, out R>代表1個參數返回值爲指定泛型 R, 同理Function22就是有22個參數返回值爲指定泛型 R。

//根據上述對應定義關係,block1、block2、block3、block4 就可以寫成下面的形式
	//block1 無參數無返回值函數類型
    val block1: Function0<Unit>
    // block2 無參數返回值爲 String 的函數類型
    val block2: Function0<Unit>
    // block2 有一個 Int 類型的參數無返回值的函數類型
    val block3: Function1<Int,Unit>
    // block4 有2個參數返回值爲 String 的函數類型
    val block4: Function2<Int,String,String>

在 kotlin.jvm.functions 中定義到 Function22 ,那麼函數類型最多隻能定義有22個參數的函數類型?(22 個參數還夠你用?)
在 kotlin1.3之前定義超過22個參數的函數類型確實不行,會報錯,在 kotlin1.3版本加入一個 FunctionN 可以對應參數大於22的函數類型。kotlin.jvm.functions 中定義的都是一些 Function 的接口,具體實現有興趣的可以去研究一下。
在 kotlin 中函數既然是一種類型,那麼定義函數的參數或返回值時,也可以把函數類型作爲參數或者返回值
如下面函數作爲參數

fun show(block: () -> Unit) {
    println("start")
    //調用傳入的函數,
    //從Function0定義中有一個operator 修飾的方法表示操作符重載的方法,
    // block()完整寫法 block.invoke()
    block()
    println("end")
}
fun main() {
//調用 show 方法傳如一個函數
    show {
        println("函數類型參數")
    }
}

輸出結果

start
函數參數
end

爲什麼show後面跟上一個{}可以就可以調用 show 方法?

先說說函數類型如何實例化吧
如定義val block:(Int) ->String 實例方式如下
通過lambda 表達式的方式實現

val block:(Int) ->String={params:Int-> "函數類型實例化"}

通過匿名函數實現

 val block: (Int) -> String = fun(params: Int): String { return "函數類型實例化" }

說完函數類型實例化再看 show 方法,show 方法需要的參數是 () -> Unit 那麼我們需要一個這鐘類型的實例

  //使用lambda 表達式創建函數類型實例
    val block = { print("函數類型參數") }
    //調用函數
    show(block)
    //在 kotlin中最後一個參數如果是 lambda 那麼把 lambda 表達式寫到()外面
    show() { print("函數類型參數") }
    //如果lambda前面括號沒有任何參數可以省去不寫,最終寫法就是
    show { print("函數類型參數") }

利用上述特性我們可以寫出許多有意思的代碼,例如 kotlin 可以使用很簡潔的方式開啓一個線程,代碼如下

//在{}寫以前 run 方法裏面寫的邏輯即可,再也不用擔心線程忘記start()了
 thread {
        println(Thread.currentThread().name)
    }

查看該方法源碼就會知道就利用函數類型作爲參數很簡單就實現了上面簡潔的寫法,附上thread 方法源碼

public fun thread(
    start: Boolean = true,
    isDaemon: Boolean = false,
    contextClassLoader: ClassLoader? = null,
    name: String? = null,
    priority: Int = -1,
    block: () -> Unit
): Thread {
    val thread = object : Thread() {
        public override fun run() {
            block()
        }
    }
    if (isDaemon)
        thread.isDaemon = true
    if (priority > 0)
        thread.priority = priority
    if (name != null)
        thread.name = name
    if (contextClassLoader != null)
        thread.contextClassLoader = contextClassLoader
    if (start)
        thread.start()
    return thread
}

3.閉包

閉包概念比較抽象,先看一段代碼

/**
 * @return ()->Int 返回函數類型
 */
fun test(): () -> Int {
    var count = 1
    return { count++ }//使用lambda表達式創建函數實例返回
}
fun main() {
    //調用test方法返回一個()->Int函數類型實現
    val show = test()
    //調用返回是函數,每次調用返回值不同
    println(show())//1
    println(show())//2
    println(show())//3
}

輸出結果

1
2
3

一般方法執行完就會出棧,方法中局部變量也會隨之釋放。但上面代碼看上去卻不是那樣的,很奇怪。test() 執行完後 再調用show()方法是count這個局部變量應該是找不到纔對,但運行的結果卻是每次調用結果都加一了。

下面將kotlin編譯成java應該就可以解釋着這個問題了

 @NotNull
   public static final Function0 test() {
      final IntRef count = new IntRef();
      count.element = 1;
      return (Function0)(new Function0() {
         // $FF: synthetic method
         // $FF: bridge method
         public Object invoke() {
            return this.invoke();
         }

         public final int invoke() {
            IntRef var10000 = count;
            int var1;
            var10000.element = (var1 = var10000.element) + 1;
            return var1;
         }
      });
   }

在kotlin中test()中定義的count變量在java中變成了對象引用。對象隨之方法的調用而創建,但是有引用指向對象,方法執行完,對象是不會被釋放的,所以每次調用改變的是的該對象的屬性值。

如果不轉換爲java代碼可以用閉包的概念理解這段代碼。count和函數{count++}在test()方法中創建,count是一個自由變量(非局部變量稱之爲自由變量,count不會隨着show方法的調用而消失。自由變量要比局部變量生命週期長)。函數{count++}引用自由變量count。在維基百科閉包是這個樣定義的是引用了自由變量的函數。這個被引用的自由變量將和這個函數一同存在,即使已經離開了創造它的環境也不例外。有另一種說法認爲閉包是由函數和與其相關的引用環境組合而成的實體。閉包在運行時可以有多個實例,不同的引用環境和相同的函數組合可以產生不同的實例。

按照維基百科的第一種說法{count++}就是閉包,它是應用了自由變量的函數。如何按第二種說法{count++}和它執行的引用環境整體是一個閉包。

參考鏈接
kotlin 函數
維基百科 閉包

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章