Kotlin難點

2023-05-25 21:00:33

高階函數

高階函數是將函數用作引數或返回值的函數,還可以把函數賦值給一個變數。

所有函數型別都有一個圓括號括起來的引數型別列表以及一個返回型別:(A, B) -> C 表示接受型別分別為 A 與 B 兩個引數並返回一個 C 型別值的函數型別。 引數型別列表可以為空,如 () -> A,Unit 返回型別不可省略。

(Int) -> String

函數型別表示法可以選擇性地包含函數的引數名:(x: Int, y: Int) -> Point。 這些名稱可用於表明引數的含義。
(Button, ClickEvent) -> Unit
如需將函數型別指定為可空,請使用圓括號:((Int, Int) -> Int)?

    fun a(funParam: (Int) -> String): String {
        return funParam(1)
    }

    fun b(param: Int): String {
        return param.toString()
    }

呼叫

a(::b)
var d = ::b
b(1) // 呼叫函數
d(1) // 實際上會呼叫 d.invoke(1)
(::b)(1) // 用物件 :: b 後面加上括號來實現 b() 的等價操作, 實際上會呼叫 (::b).invoke(1)
b.invoke(1) // 報錯

物件是不能加個括號來呼叫的,但是函數型別的物件可以。為什麼?因為這其實是個假的呼叫,它是 Kotlin 的語法糖,實際上你對一個函數型別的物件加括號、加引數,它真正呼叫的是這個物件的 invoke() 函數

雙冒號

:: 建立一個函數參照或者一個類參照

函數參照

fun isOdd(x: Int) = x % 2 != 0

我們可以很容易地直接呼叫它(isOdd(5)),但是我們也可以將其作為一個函數型別的值,例如將其傳給另一個函數。為此,我們使用 :: 操作符:

val numbers = listOf(1, 2, 3)
println(numbers.filter(::isOdd))

這裡 ::isOdd 是函數型別 (Int) -> Boolean 的一個值。

如果我們需要使用類的成員函數或擴充套件函數,它需要是限定的,例如 String::toCharArray。

    val args: Array<String> = arrayOf("1", "2")
  args.filter(String::isNotEmpty) 
  
  class PdfPrinter {
        fun println(any: Any) {
            kotlin.io.println(any)  //重名了可以用包名呼叫
        }
    }
      val pdfPrinter = PdfPrinter()
      args.forEach(pdfPrinter::println)

類參照

val c = MyClass::class

該參照是 KClass 型別的值
請注意,Kotlin 類參照與 Java 類參照不同。要獲得 Java 類參照, 請在 KClass 範例上使用 .java 屬性。
平時寫的類,其資訊都可以在這個KClass來獲取

屬性參照

data class MediaItem(val title: String, val url: String)

var items= mutableListOf<MediaItem>()
items
    .sortedBy { it.title }
    .map { it.url }
    .forEach { print(it) }

items
    .sortedBy(MediaItem::title)
    .map(MediaItem::url)
    .forEach(::println)

匿名函數

沒有名字的函數
要傳一個函數型別的引數,或者把一個函數型別的物件賦值給變數,除了用雙冒號來拿現成的函數使用,你還可以直接把這個函數挪過來寫:

fun b(param: Int): String {
    return param.toString()
}

a(fun b(param: Int): String {
  return param.toString()
});

val d = fun b(param: Int): String {
  return param.toString()
}

//名字沒意義,省略
a(fun(param: Int): String {
  return param.toString()
});
val d = fun(param: Int): String {
  return param.toString()
}

如果你在 Java 裡設計一個回撥的時候是這麼設計的:

public interface OnClickListener {
  void onClick(View v);
}
public void setOnClickListener(OnClickListener listener) {
  this.listener = listener;
}

使用的時候是這麼用的:

view.setOnClickListener(new OnClickListener() {
  @Override
  void onClick(View v) {
    switchToNextPage();
  }
});

kotlin寫法

fun setOnClickListener(onClick: (View) -> Unit) {
  this.onClick = onClick
}
view.setOnClickListener(fun(v: View): Unit) {
  switchToNextPage()
})

Lambda寫法:

view.setOnClickListener({ v: View ->
  switchToNextPage()
})

Lambda 表示式

簡化匿名函數,程式碼更簡潔

    view.setOnClickListener({ v: View ->
        switchToNextPage()
    })

//如果 Lambda 是函數的最後一個引數,你可以把 Lambda 寫在括號的外面:
    view.setOnClickListener() { v: View ->
        switchToNextPage()
    }
//而如果 Lambda 是函數唯一的引數,你還可以直接把括號去了:
    view.setOnClickListener { v: View ->
        switchToNextPage()
    }
//另外,如果這個 Lambda 是單引數的,它的這個引數也省略掉不寫:
//根據上下文推導,根據最後一行程式碼來推斷出返回值型別
    view.setOnClickListener {
        switchToNextPage()
    }

Lambda 表示式的完整語法形式如下:

val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
val sum = { x: Int, y: Int -> x + y }

多引數例子:
fold函數:將所提供的操作應用於集合元素並返回累積的結果

val items = listOf(1, 2, 3, 4, 5)

// Lambdas 表示式是花括號括起來的程式碼塊。
items.fold(0, { 
    // 如果一個 lambda 表示式有引數,前面是引數,後跟「->」
    acc: Int, i: Int -> 
    print("acc = $acc, i = $i, ") 
    val result = acc + i
    println("result = $result")
    // lambda 表示式中的最後一個表示式是返回值:
    result
})

// lambda 表示式的引數型別是可選的,如果能夠推斷出來的話:
val joinedToString = items.fold("Elements:", { acc, i -> acc + " " + i })

輸出:

acc = 0, i = 1, result = 1
acc = 1, i = 2, result = 3
acc = 3, i = 3, result = 6
acc = 6, i = 4, result = 10
acc = 10, i = 5, result = 15
joinedToString = Elements: 1 2 3 4 5

總結:
函數不能直接傳遞或者賦給某個變數,需要函數型別範例化,有三種方式:

使用已有宣告的可呼叫參照
1.函數參照

使用函數位面值的程式碼塊

2.匿名函數
3.lambda 表示式

例子

實現介面

var onVideoStartCallBack: (() -> Unit)? = null

onVideoStartCallBack?.invoke()

videioView.onVideoStartCallBack = {

}

函數裡實現介面

object UploaderListHelper {

    fun startTaskUpload(activity: Activity, startCallBack: ((Int) -> Unit)?) {
        startCallBack.invoke(position)
    }
}

UploaderListHelper.startTaskUpload(activity) {
    refreshProgress(it)
}

作用域函數

Kotlin 標準庫包含幾個函數,它們的唯一目的是在物件的上下文中執行程式碼塊。當對一個物件呼叫這樣的函數並提供一個 lambda 表示式時,它會形成一個臨時作用域。在此作用域中,可以存取該物件而無需其名稱。這些函數稱為作用域函數。共有以下五種:let、run、with、apply 以及 also。

這些函數基本上做了同樣的事情:在一個物件上執行一個程式碼塊。不同的是這個物件在塊中如何使用,以及整個表示式的結果是什麼。
目的:簡潔

 val person = findPerson();
        //person是可null的,所以需要?
        println(person?.age)
        println(person?.name)
        //上面太麻煩,findPerson加了?,所以後面不需要了,減少的判空操作。let可以安全呼叫
        findPerson()?.let { person ->
            person.work()
            println(person.age)
        }
        //還可以更簡潔,person也不用寫
        findPerson()?.apply {
            work()
            println(age)
        }

使⽤時可以通過簡單的規則作出一些判斷
返回自身
返回值是它本身
從 apply 和 also 中選
作⽤域中使⽤ this 作為引數選擇 apply

val adam = Person("Adam").apply {
    age = 32
    city = "London"        
}
println(adam)

作⽤域中使⽤ it 作為引數選擇 also

val numbers = mutableListOf("one", "two", "three")
numbers
    .also { println("The list elements before adding new one: $it") }
    .add("four")

with 非擴充套件函數

val numbers = mutableListOf("one", "two", "three")
with(numbers) {
    println("'with' is called with argument $this")
    println("It contains $size elements")
}

不需要返回自身
從 run 和 let 中選擇
作用域中使用 this 作為引數,選擇 run
作用域中使用 it 作為引數,選擇 let, 適合配合空判斷的時候

val service = MultiportService("https://example.kotlinlang.org", 80)

val result = service.run {
    port = 8080
    query(prepareRequest() + " to port $port")
}

// 同樣的程式碼如果用 let() 函數來寫:
val letResult = service.let {
    it.port = 8080
    it.query(it.prepareRequest() + " to port ${it.port}")
}

it作為引數的好處
let 允許我們自定義引數名字,使可讀性更強,如果傾向可讀性可以選擇 T.let

參考文章
Kotlin 的高階函數、匿名函數和 Lambda 表示式
Kotlin官網