深入理解 Infix 中缀函数

107 阅读3分钟

前言

我们之前在创建 Map 时,多次使用了 A to B 这样的语法结构来创建键值对,这种写法相较于函数调用来说可读性更高。那么它是如何实现的?to 是一个关键字吗?

首先,to 并不是 Kotlin 中的一个关键字。我们之所以能够使用 A to B 这样的语法结构,多亏了 Kotlin 提供的强大语法糖特性:infix(中缀)函数

其实也不难理解,A to B 这样的写法,实际上等价于 A.to(B) 函数调用的写法,只不过更加简洁、更具有可读性。

我们来通过具体的例子来理解。

简单示例:改造 startsWith

String 类中有一个 startsWith() 方法,用于判断一个字符串是否以某个前缀开头。比如说:

val str1 = "Hello Kotlin"
val str2 = "Hello"
val startsWith = str1.startsWith(str2) // 标准的函数调用
println("str1 starts with str2: $startsWith") 

虽然这段代码已经很清晰了,但我们可以通过中缀函数,使其表达方式接近于自然语言。

首先,创建一个 infix.kt 文件,并定义一个扩展函数:

infix fun String.beginsWith(prefix: String) = startsWith(prefix)

我们定义了 String 类的扩展函数 beginsWith(),功能和 startsWith() 函数完全相同。关键在于,我们在函数声明前加上了 infix 关键字,将这个函数变为了中缀函数。这样,我们就可以使用新的方式来调用它

如下所示:

val str1 = "Hello Kotlin"
val str2 = "Hello"
val startsWith = str1 beginsWith str2 // 中缀调用
println("str1 starts with str2: $startsWith") 

可以看到,str1 beginsWith str2 的写法读起来像英文表达,代码更具有可读性。

Infix 函数的核心规则

定义一个 infix 函数,有以下要求:

  • infix 必须是某个类的成员函数或扩展函数。

  • infix 函数必须接收且只能接收一个参数。

其实这两点,很好理解。

中缀表示法 A func B 连接了两个操作数。A 就是函数的接收者,B 就是函数的参数。所以该函数只有是成员函数或扩展函数,才能有接收者。并且这种语法结构只有一个参数 B 的位置。

泛型示例:改造 contains

我们再来看一个更通用的例子。比如判断集合中是否包含某个元素,你可能会这样写:

val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val str = "Banana"
val contains = list.contains(str)
println("list contains $str: $contains")

同样地,我们可以使用 infix 函数来改造这段代码。在 infix.kt 文件中,我们定义一个泛型的中缀函数:

// 为任意集合 Collection<T> 定义一个 has 中缀扩展函数
infix fun <T> Collection<T>.has(element: T) = contains(element)

现在,我们的代码就可以变为:

val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val str = "Banana"
val contains = list has str // 中缀调用
println("list contains $str: $contains")

通过两个例子,想必你对于中缀函数已经了解很多了。

A to B 的源码

最后,我们来看看为什么 A to B 这样的写法能够构建键值对。进入 to() 函数的源码看看:

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

可以看到,to 是泛型的中缀扩展函数,可以被任何类型 A 的对象调用。接收一个任何类型 B 的参数 that。在其实现中,会创建并返回一个 Pair(this,that) 对象,Pair 对象的第一个值是函数的调用者,第二个值是函数的参数。

另外,中缀函数不要滥用,否则会降低代码的可读性。只有在函数的操作像动作,能够连接两个对象时,才考虑使用。