Kotlin函數與函數式編程淺析
如果你對Kotlin語法一無所知,推薦先閱讀官方文檔或者中文站(https://www.kotlincn.net/docs/reference/)之后再看這篇文章會有更深刻的理解。本篇文章主要介紹Kotlin函數的用法,以及自己對函數式編程的一些理解。并且會和Python,C++做一些比較。
下面是維基百科上對于函數式編程的定義:
函數式編程(英語:functional programming)或稱函數程序設計,又稱泛函編程,是一種編程范型,它將電腦運算視為數學上的函數計算,并且避免使用程序狀態以及易變對象。函數編程語言最重要的基礎是λ演算(lambda calculus)。而且λ演算的函數可以接受函數當作輸入(引數)和輸出(傳出值)。
下面是關于高階函數的定義:
在數學和計算機科學中,高階函數是至少滿足下列一個條件的函數:接受一個或多個函數作為輸入,輸出一個函數
不難推斷出函數式編程最重要的基礎是高階函數。也就是支持函數可以接受函數當作輸入(引數)和輸出(傳出值)。
函數作為Kotlin中的一級公民可以像其他對象一樣作為函數的輸入與輸出,這也就是Java程序員轉到Kotlin覺得變化***,最難理解的一點。如果你之前學過Python或者C++11可能會對此比較容易接受。這也是為什么本文以介紹Kotlin的函數及函數式編程為主。
Kotlin 函數
下面是Kotlin中一般的函數定義,和Java不同的是函數形參,返回值類型置后。函數體可以用等號賦值給函數定義,這里也可以看出函數和變量的平等性。
- fun main(args: Array) {
- var s = sum(1,2)
- var m = multi(2,3)
- var x = maxOf(3,4)
- }
- fun sum(a: Int, b: Int): Int {
- return a + b
- }
- fun multi(a: Int, b: Int): Int = a * b
- fun maxOf(a: Int, b: Int): Int = if (a > b) a else b
另外Kotlin還支持函數默認參數,拓展函數,中綴表達式,下面是簡單的例子:
- fun main(args: Array) {
- isBiggerThan(2)
- isBiggerThan(2, 5)
- var s = "a".isLetter()
- var a = 1 add 2
- }
- fun isBiggerThan(a: Int, b: Int = 0) {
- return a > b
- }
- //拓展函數
- fun String.isLetter(): Boolean {
- return matches(Regex("^[a-z|A-Z]$"))
- }
- //拓展函數,中綴表達式
- infix fun Int.add(x: Int): Int {
- return this + x
- }
支持默認參數的函數可以減小函數的重載。
String對象中本沒有判斷是否是字母的方法,在Java中我們一般會定義一些Utils方法,而在Kotlin中可以定義類的拓展函數。
第二個例子是給Int類定義了一個拓展函數,并且該拓展函數以中綴表達式表示,給予了開發者定義類似關鍵字的權利。
比如我們可以這樣創建一個map對象:
- val kv = mapOf("a" to 1, "b" to 2)
這里的to就是一個中綴表達式,定義如下:
- public infix fun<A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
Pair就是Map中存的對象,所以你也可以這樣創建
- val kv = mapOf(Pair("a", 1), Pair("b", 2))
在Python中如果我們想讓函數返回多個值,可以返回一個元組,Kotlin基于解構原則也可以實現類似的功能:
- fun main(args: Array) {
- val (index, count) = findWhere("abcabcabcabc", 'c')
- }
- fun findWhere(str: String, findChar: Char): Pair<Int, Int> {
- var index = -1
- var count = 0
- for ((i, v) in str.withIndex()) {
- if (v == findChar) {
- if (index == -1) {
- index = i
- }
- ++count
- }
- }
- return Pair(index, count)
- }
自定義對象如何支持解構請查看官方文檔,map支持解構,所以可以像下面這樣遍歷:
- for ((k, v) in map) {
- print("$k -> $v, ")
- }
高階函數與 Lambda 表達式
“Lambda 表達式”(lambda expression)是一個匿名函數,Lambda表達式基于數學中的λ演算得名,直接對應于其中的lambda抽象(lambda abstraction),是一個匿名函數,即沒有函數名的函數。Lambda表達式可以表示閉包(注意和數學傳統意義上的不同)。
Python中的lambda表達式:
- add = lambda x, y:x+y
C++中的lambda:
- [](int x, int y) -> int{ return x + y; }
Kotlin中的lambda:
- var add = {x: Int, y: Int -> x + y}
Kotlin 作為一個強類型語言還是比較簡潔的。
我們可以這樣使用一個lambda表達式:
- fun main(args: Array) {
- val sumLambda = {a: Int, b: Int -> a + b}
- sumLambda(1, 2)
- }
它可以像函數一樣使用()調用,在kotlin中操作符是可以重載的,()操作符對應的就是類的重載函數invoke()。
你還可以想下面這樣定義一個變量:
- val numFun: (a: Int, b: Int) -> Int
它不是一個普通的變量,它必須指向一個函數,并且函數簽名必須一致:
- fun main(args: Array) {
- val sumLambda = {a: Int, b: Int -> a + b}
- var numFun: (a: Int, b: Int) -> Int
- numFun = {a: Int, b: Int -> a + b}
- numFun = sumLambda
- numFun = ::sum
- numFun(1,2)
- }
- fun sum(a: Int, b: Int): Int {
- return a + b
- }
可以看到這個變量可以等于一個lambda表達式,也可以等于另一個lambda表達式變量,還可以等于一個普通函數,但是在函數名前需要加上(::)來獲取函數引用。
這個類似C++中的函數指針,然而在Python中可以直接使用函數名作為函數引用,下面是c++函數指針的例子:
- #include
- using namespace std;
- void swap(int &x, int &y);
- int main(int arg, char* args[]) {
- int x = 10;
- int y = 20;
- void (*methodPtr)(int &x, int &y);//聲明一個函數指針
- methodPtr = &swap; //函數指針賦值
- methodPtr = swap;//取地址符可省略,效果和上面一致
- methodPtr(x, y); //像給函數起了一個別名,可以直接使用()調用
- cout << "x:" << x << " y:" << y << endl; //x:20 y:10
- }
- void swap(int &x, int &y) {
- int tmp = x;
- x = y;
- y = tmp;
- }
回到Kotlin,我們還可以將一個函數傳遞給另一個函數,比如:
- //函數參數
- fun doMap(list: List, function: (it: T) -> Any) {
- for (item in list) {
- function(item)
- }
- }
***個參數是一個List,第二個參數是一個函數,目的就是將List中的每一個元素都執行一次第二個函數。使用方法如下:
- val strList = listOf("h" ,"e", "1", "a", "b", "2", " ", "", "c", "5", "7", "F")
- doMap(strList, {item ->print("item: ${upperLetter(item)}, ") })
- fun upperLetter(item: String): String {
- if (item.isLetter()) {
- return item.toUpperCase()
- }
- return item
- }
第二個參數直接傳進去了一個lambda表達式,當然也可以傳一個函數引用:
- val strList = listOf("h" ,"e", "1", "a", "b", "2", " ", "", "c", "5", "7", "F")
- doMap(strList, ::printUpperLetter)
- fun printUpperLetter(item: String) {
- print("item: ${upperLetter(item)}, ")
- }
- fun upperLetter(item: String): String {
- if (item.isLetter()) {
- return item.toUpperCase()
- }
- return item
- }
效果和上面的代碼一樣。
在C++中使用函數指針可以實現類似的效果:
- using namespace std;
- void mMap(vector list, void (*fun)(int item));
- int main(int arg, char* args[]) {
- vector list = {2,3,4,3,2,1,2};
- mMap(list, [](int item) -> void { cout << item << endl; });
- }
- void mMap(vector list, void (*fun)(int item)) {
- for(int it : list) {
- fun(it);
- }
- }
再次回到Kotlin,如果函數作為入參在入參列表的***一個,你還可以這樣做,直接寫在大括號內:
- fun main(args: Array) {
- log { sum(1,2) }
- }
- fun log(function: () -> T) {
- val result = function()
- println("result -> $result")
- }
是不是有點像gradle配置文件的寫法,所以Kotlin可以很方便的編寫 領域專用語言(DSL)
另外Kotlin還支持局部函數和函數作為返回值,看下面的代碼:
- fun main(args: Array) {
- val addResult = lateAdd(2, 4)
- addResult()
- }
- //局部函數,函數引用
- fun lateAdd(a: Int, b: Int): Function0 {
- fun add(): Int {
- return a + b
- }
- return ::add
- }
在lateAdd內部定義了一個局部函數,***返回了該局部函數的引用,對結果使用()操作符拿到最終的結果,達到延遲計算的目的。
函數作為一級公民當然可以像普通對象一樣放進map中,比如下面這樣:
- val funs = mapOf("sum" to ::sum)
- val mapFun = funs["sum"]
- if (mapFun != null) {
- val result = mapFun(1,2)
- println("sum result -> $result")
- }
- fun sum(a: Int, b: Int): Int {
- return a + b
- }
將一個函數引用作為value放進了map中,取出來之后使用()操作符調用,可以簡化一些if,else的場景。
基于以上函數式編程的特性,Kotlin可以像RxJava一樣很方便的進行相應式編程,比如:
- fun printUpperLetter(list: List) {
- list
- .filter (fun(item):Boolean {
- return item.isNotEmpty()
- })
- .filter { item -> item.isNotBlank()}
- .filter {
- item ->
- if (item.isNullOrEmpty()) {
- return@filter false
- }
- return@filter item.matches(Regex("^[a-z|A-Z]$"))
- }
- .filter { it.isLetter() }
- .map(String::toUpperCase)
- .sortedBy { it }
- .forEach { print("$it, ") }
- println()
- }
上面的代碼只是做演示,并無實際意義。具體語法請查看官方文檔。
我相信Kotlin作為一種強類型的現代化語言可以在保證穩定性的同時極大地提高開發者的開發效率。