Kotlin:使用infix函数构建更可读的语法

我不是罗大锤 2021年11月23日 256次浏览

我们已经多次使用过A to B这样的语法结构,包括Kotlin自带的 mapOf()函数,这种语法可读性高,相比调用一个函数,它更接近于使用英语的语法来编写。

to并不是Kotlin语言中的一个关键字,之所以我们能使用A to B这种语法结构,是因为Kotlin提供了一种高级语法糖特性:infix函数,它只是把编程语言函数调用的语法规则调整了一下,比如A to B这样额写法,实际上等价于A.to(B)的写法。

下面通过两个具体的例子来学习一下infix函数的用法。

一、简单例子

String类有一个startsWith()函数,用于判断一个字符串是否以某个指定参数开头,比如下面的这段代码判断结果一定会是true:

if ("Hello Kotlin".startsWith("Hello")) {
    // 处理具体逻辑
}

借助infix函数,可以使用一种更具可读性的语法来表达这段代码:

// String类扩展函数,给String添加beginsWith函数,通过调用startsWith实现
infix fun String.beginsWith(prefix: String) = startsWith(prefix)

加上infix关键字之后,beginsWith()函数就变成了一个infix函数,这样除了传统的函数调用方法之外,还可以使用一种特殊的语法糖格式调用beginsWith()函数,如下所示:

// infix允许我们将调用函数的小数点、括号等相关语法去掉
// 从而使用一种更接近英语的语法来编写程序
if ("Hello Kotlin" beginsWith "Hello") {
    // 处理具体逻辑
}

由于infix函数的特殊性,有两个比较严格的限制:首先,infix函数不能定义成顶层函数,它必须是某个类的成员函数,可以使用扩展函数将它定义到某个类当中;其次,infix函数必须且只能接收一个参数,至于参数类型是没有限制的。

二、复杂例子

这里有一个集合,如果想要判断集合中是否包含某个指定元素,一般可以这样写:

val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
if (list.contains("Banana")) {
    // 处理具体逻辑
}

使用infix函数让这段代码更具有可读性:

infix fun <T> Collection.<T>.has(element: T) = contains(element)

val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
if (list has "Banana")) {
    // 处理具体逻辑
}

三、模仿to()函数实现

先看一下to()函数的源码:

public infix fun <A,B> A.to(that: B): Pair<A,B> = Pair(this,that)

可以看到这里使用定义泛型函数的方式将to()函数定义到了A类型下,并且接收一个B类型的参数,因此A和B可以是两种不同类型的泛型,也就使得我们可以构建出字符串to整形这样的键值对。

to()函数的具体实现也非常简单,就是创建并返回了一个Pair对象,也就是说A to B这样的语法结构实际上得到的是一个包含A、B数据的Pair对象,而mapOf()函数实际上接收的正式一个Pair类型的可变参数列表。

下面模仿to()函数实现我们自己的一个键值对函数:

infix fun <A,B> A.with(that: B): Pair<A,B> = Pair(this, that)

val map = mapOf("Apple" with 1, "Banana" with 2,
    "Organge" with 3, "Pear" with 4)