[译]Kotlin珍品 4 - 可能是你没见过的函数类型调用方式

1,213 阅读5分钟

翻译说明: 翻译水平有限,文章内可能会出现不准确甚至错误的理解,请多多包涵!欢迎批评和指正.每篇文章会结合自己的理解和例子,希望对大家学习Kotlin起到一点帮助.

原文地址: [Kotlin Pearls 4] It’s an Object… It’s a Function… It’s an Invokable

原文作者: Uberto Barbini

前言

在我开始去看其他人写的Kotlin代码的时候,我当时在看到下面的class时非常震撼:

class ReceiptText(val template: String): (Int) -> String {
  override fun invoke(amount: Int): String =
        template.replace("%", amount.toString())
}

什么?一个继承函数类型的类!可以这样玩的么?我当时严重懵逼了!同学,如果你也有点懵了的话,那我觉得看完这篇文章就应该能搞(geng)懂(meng)了.

正文

不要懵.函数类型跟Kotlin中其他的类型一个尿性;你可以把它们当参数,变量和其他的用法,所以为毛不能继承它呢?先不过多纠结这个问题了啊,可以继承!但是如果你继承了一个函数类型的话,编译器会强制让你重载一个跟函数类型里面的参数和返回类型相对应的invoke方法.例如函数类型(Int) -> String那么invoke就是invoke(xx:Int) : String

这个invoke方法是什么呢?它就是个没有名字的方法,所以我们在这个类的对象的后面加上括号就能用了(如果有参数在括号里面带上对应类型的参数).

在函数式编程中,函数类型被称为箭头类型,因为它们表达了从一种类型到另一种类型的态射.像泛型一样,它们属于复合类型集合的一部分.

好了.目前来说这个知识量够了.现在让我们看看能用函数类型做什么了!

  • 类型别名

    在这个系列的前面的文章中我都刻意的避免使用这种类型别名的用法,就是为了让这个例子更能给大家加深印象.通常我个人是会经常在我的项目中使用类型别名的.

    typealias FInt2String = (Int) -> String
    
    class InheritFromFunctionTypes(val template: String) : FInt2String {
       override fun invoke(p1: Int): String {
           return template.replace("%", p1.toString())
       }
    
    }
    

    作为一个例子,假设我们需要从一个收据邮件里面打印金额信息.并且我们把文案模版的配置交给用户.

    首先我们不用函数类型的方式写一个两个参数的方法:

    fun receipt(template: String, amount: Int) = 
        template.replace("%", amount.toString())
    

    然后像这样用起来:
    val text = receipt(readTemplateFromConf(), amount)

    这样一看我们不用函数类型的方式也可以实现啊!但是大家想一下,这种方式的实现同时需要传入文字配置范本和金额两个参数.但是在实际场景中我们只需要在启动的时候获取一次模版,然后在我们读取金额的时候再做打印操作.这里如果用函数类型的方式,我们可以轻松的把读取模版和金额的动作分开来做:

    class ReceiptText(val template: String): (Int) -> String {
        override fun invoke(amount: Int): String =
        template.replace("%", amount.toString())
    }
    val receipt = ReceiptText("Thank you for you donation of $%!")//模版
    //做些其他的事情...
    val text = receipt(123) // 金额 
    
    //"Thank you for you donation of $123!"
    
  • lambda表达式
    上面的例子还有一种解法就是用lambda去让方法返回函数类型的实例.

    fun receiptText(template: String):(Int)->String ={
        amount -> template.replace("%", amount.toString())//通过lambda得到函数类型的实例
    }
    
    val text2 = receiptText("Thank you for you donation of %!")
    
    
    println(text2(2))
    
    //"Thank you for you donation of 2!"
    
    
    

    比较这两种实现方式,我认为,第一个基于类的,可读性更强并且更适合复杂逻辑的支持.第二个属于高阶函数,更方便但是有可读性有一点差,所以我倾向于仅仅在简单的逻辑下使用.

  • 配合operator的使用
    objects 和 classes都可以有多个operator invoke方法.我发现它在处理密封类时尤其有用.

     fun receiptText(template: String):(Int)->String ={
        amount -> template.replace("%", amount.toString())//通过lambda得到函数类型的实例
    }
    
    sealed class TemplateString
    
    //an object with invoke operator
    object ReceiptTextObj : TemplateString() {
    operator fun invoke(amount: Int): String = receiptText("My receipt for $%")(amount)
    operator fun invoke(template: String, amount: Int): String = receiptText(template)(amount)
    }
    

    注意,虽然object ReceiptTextObj没有继承我们的函数类型(Int) -> String,不过它的invoke 方法跟我们的类型一致.

  • 泛型类型
    方法类型可以像普通类型一样工作,那么泛型类型呢?答案是 可以的啊!

    拿集合泛型举个例子.我们可以通过把上面三个列子放进一个集合,然后传入不同的金额参数再去去调用:

    val functions = mutableListOf<(Int) -> String>()
    functions.add(receiptText("TA %"))//lambda获取函数类型实例
    functions.add(ReceiptText("Thank you for $%!"))//继承函数类型的实例类
    functions.add(ReceiptTextObj::invoke)//密封单例类的invoke
    val receipts = functions.mapIndexed{i, f -> f(i+100) }
    //["TA 100", "Thank you for $101!", "My receipt for $102"]
    

    我很想知道你是如何使用它们的,以及其他可能的用途。请大家在评论区留言或者给我私信.

  • Builder模式
    我们可以用它在建造者模式中构造一个复杂的对象:

    data class Person(val name: String, val age: Int, val weight: Double)
    val personBuilder: (String) -> (Int) -> (Double) -> Person = 
    { name ->
        { age ->
            { weight ->
                Person(name, age, weight)
            }
        }
    }
    

    使用起来像这样:

    val frank = personBuilder("Frank")(32)(78.5)
    //Person(name=Frank, age=32, weight=78.5)")
    val names = listOf("Joe", "Mary", "Bob", "Alice")
    val people: List<Person> = names
            .map { personBuilder(it) } //名字
            .map { it(nextInt(80)) } //随机年龄
            .map { it(nextDouble(100.0)) } //随机体重
    //4 个随机 Person 实例
    
  • DSL

    object Console {
        operator fun invoke (block: (String) -> String): Nothing {
            while (true) {
                val l = readLine()
                if (!l.isNullOrBlank())
                    println(block(l))
            }
        }
    }
    

    上面这段代码的逻辑是一个控制台无限循环的读取输入流,然后把读取内容交给一个函数类型去处理.可以用到一个用户输入问题,并且控制台返回答案的场景,就想这样:

    fun main() {
        println ("Write your command!")
        Console {
            val parts = it.split(' ')
            when (parts[0]) {
                "go" -> "going ${parts[1]}"
                "eat" -> "eating ${parts[1]}"
                "quit" -> throw InterruptedException("Program has been terminated by user")
                else -> "I don't think so..."
            }
        }
    }
    

结尾

我希望你们能喜欢这篇文章,如果喜欢的话 请关注原作者或者在掘金关注我并给我点个赞吧.