1

揭秘 Kotlin 1.6.20 重磅功能 Context Receivers

 2 years ago
source link: https://blog.51cto.com/u_13238266/5285482
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

hi 大家好,我是 DHL。公众号:ByteCode ,专注分享有趣硬核原创内容,Kotlin、Jetpack、性能优化、系统源码、算法及数据结构、动画、大厂面经。

这篇文章我们一起来聊一下 Kotlin 1.6.20 的新功能 Context Receivers,来看看它为我们解决了什么问题。

通过这篇文章将会学习到以下内容:

  • 扩展函数的局限性
  • 什么是 Context Receivers,以及如何使用
  • Context Receivers 解决了什么问题
  • 引入 Context Receivers 会带来新的问题,我们如何解决
  • Context Receivers 应用范围及注意事项

扩展函数的局限性

在 Kotlin 中接受者只能应用在扩展函数或者带接受者 lambda 表达式中, 如下所示。

class Context {
    var density = 0f
}

// 扩展函数
inline fun Context.px2dp(value: Int): Float = value.toFloat() / density

接受者是 fun 关键字之后点之前的类型 Context,这里隐藏了两个知识点。

  • 我们可以像调用内部函数一样,调用扩展函数 px2dp(),通常结合 Kotlin 作用域函数 with , run , apply 等等一起使用。
with(Context()) {
    px2dp(100)
}
  • 在扩展函数内部,我们可以使用 this 关键字,或者隐藏关键字隐式访问内部的成员函数,但是我们不能访问私有成员

扩展函数使用起来很方便,我们可以对系统或者第三方库进行扩展,但是也有局限性。

  • 只能定义一个接受者,因此限制了它的可组合性,如果有多个接受者只能当做参数传递。比如我们调用 px2dp() 方法的同时,往 logcatfile 中写入日志。
class LogContext {
    fun logcat(message: Any){}
}
class FileContext {
    fun writeFile(message: Any) {}
}

fun printf(logContext: LogContext, fileContext: FileContext) {
    with(Context()) {
        val dp = px2dp(100)
        logContext.logcat("print ${dp} in logcat")
        fileContext.writeFile("write ${dp} in file")
    }
}
  • 在 Kotlin 中接受者只能应用在扩展函数或者带接受者 lambda 表达式中,却不能在普通函数中使用,失去了灵活性

Context Receivers 的出现带来新的可能性,它通过了组合的方式,将多个上下文接受者合并在一起,灵活性更高,应用范围更广。

什么是 Context Receivers

Context Receivers 用于表示一个基本约束,即在某些情况下需要在某些范围内才能完成的事情,它更加的灵活,可以通过组合的方式,组织上下文,将系统或者第三方类组合在一起,实现更多的功能。

如果想在项目中使用 Context Receivers,需要将 Kotlin 插件升级到 1.6.20 ,并且在项目中开启才可以使用。

plugins {
    id 'org.jetbrains.kotlin.jvm' version '1.6.20'
}
// ......
kotlinOptions {
    freeCompilerArgs = ["-Xcontext-receivers"]
}

如何使用 Context Receivers

当我们完成上述配置之后,就可以在项目中使用 Context Receivers,现在我们将上面的案例改造一下。

context(LogContext, FileContext)
fun printf() {
    with(Context()) {
        val dp = px2dp(100)
        logContext.logcat("print ${dp} in logcat")
        fileContext.writeFile("write ${dp} in file")
    }
}

我们在 printf() 函数上,使用 context() 关键字,在 context() 关键字括号中,声明上下文接收者类型的列表,多个类型用逗号分隔。但是列出的类型不允许重复,它们之间不允许有子类型关系。

通过 context() 关键字来限制它的作用范围,在这个函数中,我们可以调用上下文 LogContextFileContext 内部的方法,但是使用的时候,只能通过 Kotlin 作用域函数嵌套来传递多个接受者,也许在未来可能会提供更加优雅的方式。

with(LogContext()) {
    with(FileContext()) {
        printf("I am DHL")
    }
}

引入 Context Receivers 导致可读性问题

如果我们在 LogContextFileContext 中声明了多个相同名字的变量或者函数,我们只能通过 this@Lable 语句来解决这个问题。

context(LogContext, FileContext)
fun printf(message: String) {
    logcat("print message in logcat ${[email protected]}")
    writeFile("write message in file ${[email protected]}")
}

正如你所见,在 LogContextFileContext 中都有一个名为 name 的变量,我们只能通过 this@Lable 语句来访问,但是这样会引入一个新的问题,如果有大量的同名的变量或者函数,会导致 this 关键字分散到处都是,造成可读性很差。所以我们可以通过接口隔离的方式,来解决这个问题。

interface LogContextInterface{
    val logContext:LogContext
}
interface FileContextInterface{
    val fileContext:FileContext
}

context(LogContextInterface, FileContextInterface)
fun printf(message: String) {
    logContext.logcat("print message in logcat ${logContext.name}")
    fileContext.writeFile("write message in file ${fileContext.name}")
}

通过接口隔离的方式,我们就可以解决 this 关键字导致的可读性差的问题,使用的时候需要实例化接口。

val logContext = object : LogContextInterface {
    override val logContext: LogContext = LogContext()
}
val fileContext = object : FileContextInterface {
    override val fileContext: FileContext = FileContext()
}

with(logContext) {
    with(fileContext) {
        printf("I am DHL")
    }
}

Context Receivers 应用范围及注意事项

当我们重写带有上下文接受者的函数时,必须声明为相同类型的上下文接受者。

interface Canvas
interface Shape {
    context(Canvas)
    fun draw()
}

class Circle : Shape {
    context(Canvas)
    override fun draw() {
    }
}

我们重写了 draw() 函数,声明的上下文接受者必须是相同的,Context Receivers 不仅可以作用在扩展函数、普通函数上,而且还可以作用在类上。

context(LogContextInterface, FileContextInterface)
class LogHelp{
    fun printf(message: String) {
        logContext.logcat("print message in logcat ${logContext.name}")
        fileContext.writeFile("write message in file ${fileContext.name}")
    }
}

在类 LogHelp 上使用了 context() 关键字,我们就可以在 LogHelp 范围内任意的地方使用 LogContext 或者 FileContex

val logHelp = with(logContext) {
    with(fileContext) {
        LogHelp()
    }
}
logHelp.printf("I am DHL")

Context Receivers 除了作用在扩展函数、普通函数、类上,还可以作用在属性 gettersetter 以及 lambda 表达式上。

context(View)
val Int.dp get() = this.toFloat().dp

// lambda 表达式
fun save(block: context(LogContextInterface) () -> Unit) {
}

最后我们来看一下,来自社区 Context Receivers 实践的案例,扩展 Json 工具类。

fun json(build: JSONObject.() -> Unit) = JSONObject().apply { build() }

context(JSONObject)
infix fun String.by(build: JSONObject.() -> Unit) = put(this, JSONObject().build())

context(JSONObject)
infix fun String.by(value: Any) = put(this, value)

fun main() {
    val json = json {
        "name" by "Kotlin"
        "age" by 10
        "creator" by {
            "name" by "JetBrains"
            "age" by "21"
        }
    }
}
  • Context Receivers 提供一个基本的约束,可以在指定范围内,通过组合的方式实现更多的功能
  • Context Receivers 可以作用在扩展函数、普通函数、类、属性 gettersetterlambda 表达式
  • Context Receivers 允许在不需要继承的情况,通过组合的方式,组织上下文,将系统或者第三方类组合在一起,实现更多的功能
  • 通过 context() 关键字声明,在 context() 关键字括号中,声明上下文接收者类型的列表,多个类型用逗号分隔
  • 如果大量使用 this 关键字会导致可读性变差,我们可以通过接口隔离的方式来解决这个问题
  • 当我们重写带有上下文接受者的函数时,必须声明为相同类型的上下文接受者

全文到这里就结束了,感谢你的阅读,如果有帮助,欢迎 在看点赞收藏分享 给身边的朋友。

真诚推荐你关注我,公众号:ByteCode ,持续分享硬核原创内容,Kotlin、Jetpack、性能优化、系统源码、算法及数据结构、动画、大厂面经。


近期必读热门文章


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK