Loading

Kotlin中的作用域函数 —— 精通Kotlin的作用域函数和高阶函数

在学习Android这几年欠下的债的过程中,我发现,Kotlin中很多地方有着难以理解的写法,比如R.() -> Unit、比如协程的coroutineScope是怎么来的,这在写惯了Java那种本分老实的语言的我眼中着实是非常难以理解的。我在网络上搜索时,发现一切的一切都要从一个叫做作用域函数的东西说起,但无奈,很多基础概念没有弄清,学起来也学不懂,直到我找到了一篇文章......

本文是Mastering Kotlin Scoped and Higher-Order Functions的翻译,由于水平问题,翻译可能很多地方不精准,如有问题请提出,十分感谢。

由于译者是边学边把这篇文章翻译了,所以有些地方可能有点生硬,有时间我会将这篇文章润色,当然,你也可以提出自己的想法~~~

从下面一行开始,就是原文了。

开始

学习如何构建,不是如何使用。

如果你正在使用Kotlin,你一定已经看过下面的表格了:

从现在起,忘掉这个表格讲了什么,丢掉它,让我们重新开始。

声明:这将是一篇很长的文章(想要掌握一些东西需要付出努力)。我将文章分割成几部分,然后添加休息标记,当你看到一个休息标题时就去休息一下吧。

休息一下 💆

网络上已经有许多文章解释什么是作用域函数以及如何使用它。这篇文章将更进一步,我们不仅要理解它们做了什么,我们还将“掌握概念”。

在踏上成为一个Master的道路之前,你需要理解两个基础且简单的概念。

  1. 扩展函数(Extension Functions)
  2. 高阶函数和Lambda(Higer-Order Function and Lambda)

如果你想知道为什么这两个概念和作用域函数相关(let、apply、also、run等),一会儿你就知道了。现在我们将快速的复习一下这两个主题。如果你从没用过这两个概念,我推荐你先去查阅官方文档或更多文章。

扩展函数

根据官方文档,Kotlin提供了用新功能扩展一个类的能力,你无需非要继承这个类或是使用类似装饰器(Decorator)这样的设计模式。

简单来说,你可以不用修改类的源代码就在类中定义一个新函数,并且(在你定义的函数中)通过这个类的实例去调用该类的(其他)函数。你甚至不用访问这个类的源代码。

语法如下:

fun AnyClass.yourNewFunction(optionalParamIfNeeded : Type) {
   // 在这里施展你的魔法并使用`this`代表`AnyClass`的实例
}

比如说,你想要一个String类型的方法,它能将字符串转换成驼峰命名(camelcase),你可以定义一个函数:

fun String.toCamelCase() { 
// 有些神奇,String实例可以通过`this`来获取
  return camelCased;
}
fun String.toCamelCase() {
   this.substring(1)
   return camelCased;
}

高阶函数和Lambda

官方文档所说,高阶函数是接收函数作为参数或者返回一个函数的函数。我们来看个例子:

fun doSomething(block: () -> Unit) {
}

这个doSomething函数接收一个无参数,返回Unit的函数()->Unit。Kotlin中没有void的概念,如果一个函数什么都不返回,它返回Unit(编译器会帮你加上这行return语句)。这个函数在参数中被传入,并且参数名是block。你可以用任何你想的名字命名它,甚至xyz(还是要遵循Kotlin语法规则的啊)

fun doSomething(xyz: () -> Unit) { // valid, xyz is the param name
}

doSomething函数应该如何调用参数中传入的函数呢?语法非常简单:

fun doSomething(xyz: () -> Unit) {
    xyz() // 就像普通函数一样调用它
}

休息一下 💆

让我们看看另一个高阶函数的例子:

fun doSomething(block: () -> String) {
    val someString:String = block()  // 参数中的函数返回String类型
}

这一次,参数中的函数返回一个String

fun doSomething(block: (i:Int) -> String) { 
    // param funtion returns String
    val someString:String = block(1)  
}

这一次,参数中的函数接收一个Int,返回一个String。

fun doSomething(block: () -> String):String {
    return block()  // param funtion returns String
}

这一次,doSomething使用block返回一个他想要的String,因为block也返回一个String。

将函数作为参数传递

fun doSomething(block: () -> Unit) {
}

doSomething想要一个没有任何参数,返回Unit的函数。

有几种方式来调用doSomething并传递一个函数。

第一种——传递一个结构匹配的已有函数

比如我们有一个函数:

fun randomFunction() { println("I am random function") }

randomFunction没有参数并且什么都没返回(实际上是返回Unit),它的结构与doSomething的结构相匹配。

我们可以调用:

doSomething(::randomFunction)

这里我们调用doSomething函数并将randomFunction作为一个参数传入,::语法用来引用一个函数。

fun randomFunction(){ println("I am random") }

fun doSomething(block: () -> Unit) {
   block() // 将会打印 "I am random"
}

doSomething(::randomFunction) 
fun randomFunction(){ println("I am random") }

fun doSomething(block: (i:Int) -> Unit) {}

// 不会通过编译 ❌ 由于randomFunction不接收Int
doSomething(::randomFunction)

方法二——传入一个匿名函数

fun doSomething(block: (i:Int) -> Unit) {}

我们可以传入一个匿名函数,如下所示:

doSomething (fun(x:Int){
    
})

注意我们创建了一个匿名函数并且将它作为参数传递了出去。如果doSomething调用了block(99),那么我们传入的函数将被调用,并且x的值是99。

休息一下 💆

方法三——创建一个继承函数类型(function type)的类,并且将这个类的实例传入

示例1

interface CustomFunctionType : () -> Unit {
    override fun invoke()
}

这里,我们创建了一个接口继承自一个函数类型。这个函数的定义是不接受参数并且返回Unit。

上面的定义等于这个函数:

fun customFunction() {}

不同时我们可以创建一个CustomFunctionType的实例并且在运行时提供一个实现,如下所示:

// 这里我们创建了一个匿名类
val dynamic = object : CustomFunctionType{
    override fun invoke() {
      // 在这里施展你的魔法
    }
}

如果我们有一个函数接收具有类似签名的函数,我们可以传递我们上面创建的对象

fun doSomething(block: () -> Unit) {
   block() // # 将打印 "I am random"
}
doSomething(dynamic)

可能第一眼看上去有些困难。关键点是,我们创建了一个接口/类来继承一个由包含参数和返回类型的签名构成的函数类型,比如() -> Unit。并且我们可以将这个接口/类的实例作为一个参数传递到方法签名匹配的地方。

例子2

interface CustomFunctionType : (Int) -> Unit {
    override fun invoke(i:Int)
}

这里我们创建了另一个扩展自函数类型的接口,这个函数的定义是,它接收一个整数参数并且什么都不返回。

例子3

interface CustomFunctionType : (Int, Int) -> String {
    override fun invoke(i:Int, j:Int):String
}

这个等价于一个接收两个整数返回一个字符串的函数。

加分项

我猜你的脑子一定开始嗡嗡了。我们知道一个高阶函数也可以被赋值给一个变量,这是一个例子:

fun doSum(i:Int, j:Int):Int{ 
  return i + j 
}
val sum: (Int, Int) -> Int = ::doSum

变量sum的类型是接收两个int参数并返回一个int的函数类型。我们使用::引用将doSum函数赋值给它。

现在我们可以这样调用:

sum(1, 1) // 等价于doSum(1,1)

休息一下 💆

方法四——进入Lambda的世界

就像我们上面看到的,我们可以继承一个函数类型并传递这个类的实例。Kotlin具有相同的简略语法。这就是lambda。

我们可以在任何需要函数类型的地方使用lambda。语法是花括号,接下来就是参数,再接下来就是方法体。

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

等号右侧就是lambda,我们知道Koltin具有类型推导功能,所以我们可以省略参数类型:

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

我们知道我们可以在任何需要函数类型的地方使用lambda。这意味着我们可以在那些使用一个函数作为另一个函数的参数的地方使用一个lambda。我们看一个相同的例子:

fun doSomething(block: () -> Unit) {
}

同样,doSomething需要一个函数,并且我们刚刚说过,我们可以在这里使用一个lambda。这个函数不接受任何参数并且不返回任何参数(实际上是Unit),如下是一个使用lambda的调用

doSomething({})

没错,就这么简单。

我们该如何使用lambda接收参数呢?同样非常简单,我们可以输入参数名,然后是参数类型。

fun doSomething(block: (i:Int, j:Int) -> Unit) {
}

// 使用lambda调用
doSomething({x:Int, y:Int -> })

但是,我们如何通过一个lambda返回一个值? 我们知道Kotlin非常聪明,lambda的最后一行被推导成返回值,并且类型必须和需要的类型相匹配。比如:

fun doSomething(block: (i:Int, j:Int) -> String) {
}

doSomething({x:Int, y:Int -> 
    "TheStringToReturn"
})

在上面的例子中,我们返回了"TheStringToReturn",因为doSomething需要一个接收两个Int并返回String的东西。当然,这是Kotlin,所以我们不需要如此啰嗦。我们可以忽略lambda表达式的参数类型

doSomething({x, y -> 
    "TheStringToReturn"
})

特殊的Lambda语法

如果你正在调用的函数接收一个lambda(函数类型),你可以在方法后面敲这个lambda。第一眼看起来很奇怪,但是这是一个简写语法。我们可以像这样编写上面的lambda调用:

doSomething(){x, y -> 
// do some magic here 
}

特殊的Lambda语法——另一个技巧

如果这里没有其它参数,只有lambda,我们可以省略函数的圆括号(这里原文给的是curly braces,应该是写错了)。所以上面的例子变成了:

doSomething {x, y -> 
// do some magic here 
}

是的,这时合法的并且真的很短。

特殊的Lambda语法——最后一个技巧

如果Lambda里只有一个参数,你可以忽略它的名字。默认的名字是it,是的,你可以在lambda中通过it来访问这个参数。

这是一个例子:

fun doSomething(block: (i:Int) -> String) { 
  block(5)
}

doSomething {
    "Hello, the value of single parameter passed is : $it"
}

上面的代码片段调用接收一个函数类型的doSomething。我们使用一个Kotlin提供的lambda表达式的最短形式。我们丢掉了圆括号,丢掉了参数名,因为这只有一个。我们通过$it来引用它。Lambda通过它的最后一行返回一个String,不需要返回语句。

实际上使用lambda来调用并不是什么魔法,Kotlin创建了一个FunctionXX类型的匿名类,请查阅Functions.kt,它在kotlin.jvm.function

休息一下 💆

接收者类型和Lambda

在我们跳进作用域函数(如letapplyalso)之前,我们需要理解最后一个概念,接收者类型(Receiver Type)。

现在我们知道扩展函数和函数类型参数了,在这个上下文中,我们考虑接收者类型是两者的结合。我们可以定义一个使用了扩展函数语法的函数类型参数。

fun doSomething(block: String.() -> Unit) {
}

在上面的例子中,我们在函数类型参数前使用了String.。它代表,新函数成为String类的匿名扩展,并且要调用这个函数类型的话,需要一个String类的实例。下面是一个例子:

fun doSomething(block: String.() -> Unit) {

  block() 
  // ❌ 不允许,因为函数类型是String类型的扩展

  "Gaurav".block() 
  // ✅ 允许,我们使用了一个String类型来调用
}

// 最短语法
doSomething {
 this.substring(1) 
 // `this`引用到一个String实例,在这个例子中是`Gaurav`
}

我们从上面的例子中看到了String接收者类型和一个函数类型参数的使用,并在String类上创建了一个匿名扩展函数。这个函数将被赋值给参数名,并且只能在我们访问block参数的位置调用。但是如果我们不想要硬编码接收者类型,或者,仅仅是返回类型的时候怎么办呢?你可以猜猜。

使用泛型的接收者类型

无论是在Kotlin还是Java中,泛型都是编译时类型,但是它们们很神奇,并且编译器做了杰出的工作来制造它们是运行时类型的假象。关于泛型的细节超出了本文的讨论范围,如果你没有了解过的话,这是官方文档,并且我建议你阅读一些博客。接下来,泛型实战!!!:

fun<R> doSomething(block: String.(i: Int) -> R) {
    println("Gaurav".block(1))
}

这次我们在我们的函数类型参数上使用了泛型 + 接收者类型。我们可以使用<Type>调用doSomething,如下所示:

doSomething<Int> {
  // do something here
  100
}

上面的调用使用<Int>,所以编译器将控制那个函数类型返回Int。上面的lambda返回100。

doSomething<String> {
  // do something here
  "Gaurav Khanna"
}

现在,我们使用<String>并且返回一个String。但请记住,Kotlin很聪明,并且能推导类型。那么我们为什么还声明它? Kotlin能够从lambda的最后一条语句为你推导返回值类型:

doSomething {
  // do something here
  "Gaurav Khanna"
}

上面的调用等价于使用<String>的调用。

类似的,我们可以在接收者类型上使用泛型:

fun<T,R> doSomething(t:T, block: T.() -> R) {
    println(t.block())
}

这里,我们宣布了函数类型具有一个泛型的接收者类型,这意味着这里的类型将由调用者定义。这是我们如何调用它:

doSomething<String, Int>("Gaurav") {
    this.substring(1)
    "from inside"
    1
}

我们明确的指出,调用者的类型是字符串,返回值类型是Int。我们需要传入一个调用者类型的实例(在这里是String)。否则,函数doSomething不能调用函数类型参数,因为它需要由调用者定义的类型T的实例。是的,我们可以再次忽略类型,编译器将会为我们推导。

doSomething("Gaurav") {
    this.substring(1)
    "from inside"
    1
}

上面的函数调用等价于使用<String, Int>的调用。

泛型和接收者类型的最后一个例子:

fun<T,R> T.doSomething(block: T.() -> R) {
    println(block())
}

🥺🥺如果你的脑袋正在嗡嗡作响,挺住,不要放弃。

这里,doSomething被定义成一个泛型T的扩展函数(T.doSomething),这意味着这是一个任意调用者的扩展函数。是的,从字面上理解,每个实例都可以调用的,那就是一个成员函数了。

而且这个函数类型参数block也是一个类型T的匿名扩展函数(T.() -> R),这再次意味着每一个类的每一个实例。

为什么在上面的例子中我们不需要一个T的实例去调用block?答案是doSomething也是一个扩展函数,block也是一个扩展函数。直接来说,它们都是类型T的成员函数了,而且一个类的两个成员函数能互相直接调用或者是像如下所示一样使用this关键字:

fun<T,R> T.doSomething(block: T.() -> R) {
    println(this.block())
}

休息一下然后复习上面的总结 💆💆

最后,让我们深入Kotlin提供的作用域函数!

或者...再等一下

我想,我该用一些例子给你展示我们之前学习的东西有多么强大。

fun <T> T.callMyAnonymousLambda(block: (T) -> Unit) {
   block(this)
}

在上面的代码段里,我们定义了一个具有泛型的扩展函数,它接收一个和调用者具有相同类型的参数并什么都不返回(Unit)的函数类型参数。

使用block(this)来调用lambda是因为lambda需要一个T类型的参数,this就是指向来自泛型<T>的,函数callMyAnonymousLambda的调用者。下面是一个我们如何调用上面函数的例子:

"Gaurav".callMyAnonymousLambda {name ->
    println("My name is $name")
}

现在T变成了String,并且lambda有了一个String类型参数,它被作为name传递。

记住,如果我们没有在单参数lambda中给定一个明确的参数名,我们可以使用it

"Gaurav".callMyAnonymousLambda {
    println("My name is $it")
}

我的朋友,我们刚刚创建了我们自己的let函数,它与Kotlin中的非常相似

这是let函数的代码:

fun <T, R> T.let(block: (T) -> R): R {
    return block(this)
}

let通过泛型T被定义成了任何类的扩展函数。(在let函数中)调用者将变成T类型,并且返回类型可以被显式声明或者可以被编译器推导出来。这个函数类型参数block接收一个T类型的参数(也就是调用者类型)并返回R类型。let函数也返回R,但是let函数并不知道R是什么,所以它使用了block函数来返回R(因为R具体是什么只有block函数才知道)。

"Gaurav".let{ name->
    println("My name is $name")
}

当然,我们可以忽略单参数名,通过it引用它:

"Gaurav".let{ 
    println("My name is $it")
}

还记得上面表格中的let吗?

let是一个扩展函数,并且lambda表达式具有一个参数,并且它返回lambda表达式的最后一条语句,因为let函数return block(this)

让我们创建其它的自定义函数

fun <T> T.callExtensionLambdaAndReturnSelf(block: T.() -> Unit) {
    block()
}

这里,callExtensionLambdaAndReturnSelf通过泛型<T>被定义,所以调用者在这里(callExtensionLambdaAndReturnSelf中)变成了T。它接收一个匿名的,函数类型的扩展函数,这个扩展函数不接受参数,并且什么也不返回。同样的我们的自定义函数callExtensionLambdaAndReturnSelf也什么都不返回(Unit)。

block()被单独调用,而没有使用任何对象(如T.block),是因为callExtensionLambdaAndReturnSelfblock同时都是T类型的扩展成员,它们不用显式的引用。你可以使用this.block(),它们的意义相同。

emm...我的朋友,我们刚刚又创建了我们自己的apply函数,他和Kotlin中的很像

这是apply函数的代码:

fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this
}

apply是一个<T>类型的扩展函数,并且函数类型参数也是一个<T>类型的匿名扩展函数。apply函数使用隐式的this来调用block,并且它返回调用者this

这是一个例子:

class Workshop(val car:Car){}

workshop = Workshop(Ferrari())

workshop.apply<Workshop> {  }

OR

workshop.apply{ 
// do some magic here
// 调用者是`this`,一个具有ferrari Car成员的Workshop
}

我相信你已经抓住了概念并且你不仅仅是尝试记忆这张表了,下面是其它的作用域函数。

Aslo

fun <T> T.also(block: (T) -> Unit): T {
    block(this)
    return this
}

also函数也是一个泛型T的扩展函数,它接收一个只有一个参数,参数类型与调用者相同的函数类型参数。apply函数返回调用者类型,你能在传入的lambda中使用it的原因是also内部对block(this)的调用,你可以自由的对这个it命名。

Run

fun <T, R> T.run(block: T.() -> R): R {
    return block()
}

run函数也是一个泛型T的扩展函数,所以任何调用者都可以像调用一个成员函数一样调用它,它接收一个同样是T类型的匿名扩展函数的lambda。run函数委托lambda返回类型R。我们可以像访问this一样访问调用者因为它是一个扩展函数。

With

with(receiver: T, block: T.() -> R): R {
    return receiver.block()
}

with不是一个扩展函数,但是是一个具有函数类型参数的函数。这次我们传递T作为一个参数receiver,并且使用它调用block,剩下的(和之前)都没什么差别。

但是,何时使用?使用哪个?

这篇文章旨在教你作用域函数的内部原理和基本概念。不可否认这些函数让人第一次看起来很迷惑。只要它能在你的项目中带来好处,您可以自由的使用它们,但是,请让我分享一些使用案例:

apply通常被用于装载一个对象,或者用于你对调用者对象的返回类型感兴趣的地方。

dialog.apply { 
  setOkButton("Ok")
  setCancelButton("cancel")
}

let通常用于改变作用域,以及用于我们对Lambda表达式的结果感兴趣时。它在可空对象上表现甚好:

fun doMagic(car:Car?) {  
  car?.let {
    it.start() 
  }
}

run有两个变体。第一个只是改变作用域,所以你可以尽情使用你的变量和返回一些东西。另外,run被定义成了一个具有调用方接收者类型参数的扩展函数。

语义上,also可以用来链接函数调用

car.also{ // do soem magic }.also{ // do more magic }

with可以像apply那样使用,一个使用案例就是不用this和点就能修改属性:

with(car) { model ="x6" manu ="bmw" }

结论

我希望你从这篇文章中学到了东西。我的意图是让你真正掌握,所以你不能仅仅只是会如何使用作用域函数,而是你要可以创建自己的作用域函数。这一切都与基础有关。我们看到了扩展函数和函数类型参数扮演了很重要的角色。作用域函数仅仅是让你生活更加美好(make your life easy)的工具。不需要背诵任何表格,你只需要缕清基础然后,明智的使用它们。不用因为是使用this还是it疑惑,而是理解,它们从哪儿来的以及背后的原理。最后,实践!实践!实践!

I write about Android, Java, Kotlin and Software. If you reached here and enjoyed/learned, share this article among your circle. Follow me on Twitter OR Medium OR visit my website

最后,休息一下 💆💆💆

完~~~

posted @ 2022-01-05 15:07  yudoge  阅读(787)  评论(2编辑  收藏  举报