Kotlin 中的 infix 关键字(中缀函数)

在 Kotlin 中,infix 是一个关键字,用于定义中缀函数(Infix Functions)。

中缀函数允许我们在调用函数的时候使用更加简洁的中缀符号(通常是一个操作符),而不是传统的点符号调用方式。中缀函数的作用是使代码更具可读性,特别是在编写某些领域特定语言(DSL)时非常有用。

在 mapOf 函数中允许我们用 A to B 这样的语法来构建键值对,它的具体实现是怎样的呢?以下是 to 函数的源码:

kotlin 复制代码
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.kt 文件中添加如下代码:

kotlin 复制代码
infix fun <A, B> A.with(that: B): Pair<A, B> = Pair(this, that)

这里只是将 to 函数改名为 with 函数,其他的实现逻辑是相同的,因为相信没有什么解释的必要。现在我们的项目中就可以使用 with 函数来构建键值对了,还可以将构建的键值对传入 mapOf 方法中:

kotlin 复制代码
val map = mapOf(
    "Apple" with 1, "Banana" with 2, "Orange" with 3, "Pear" with 4, "Grape" with 5
)

这就是 infix 函数(中缀函数),灵活运用它可以让语法变得更具可读性。

一个中缀函数的表达形式非常简单:

kotlin 复制代码
A 中缀方法 B

当我们要定义一个中缀函数的时候,它必须满足如下条件:

  • 该中缀函数必须是某个类型的扩展函数或者成员方法;
  • 该中缀函数只能有一个参数;
  • 虽然 Kotlin 的函数参数支持默认值,但中缀函数的参数不能有默认值,否则以上形式的 B 会缺失,从而对中缀表达式的语义造成破坏;

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

kotlin 复制代码
if ("Hello Kotlin".startsWith("Hello")){
	// 处理具体的逻辑
}

startWith 函数的用法虽然非常简单,但是借助 infix 函数,我们可以使用一种更具可读性的语法来表达这段代码。新建一个 infix.kt 文件:

kotlin 复制代码
infix fun String.beginsWith(prefix: String) = startsWith(prefix)

首先,去掉前面的 infix 关键字不谈,这就是一个 String 类的扩展函数。我们给 String 类添加了一个 beginsWith 函数,它也是用于判断一个字符串是否是以某个指定参数开头的,并且它的内部实现就是调用 String.startsWith 函数。

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

kotlin 复制代码
if ("Hello World" beginsWith("Hello")) { // 注意没有 .
		// 处理具体的逻辑
}

从这个例子中就能看出,infix 函数的语法规则并不复杂,上述的代码其实调用的 "Hello Kotlin" 这个字符的 beginsWith 函数,并传入 "Hello" 字符串作为参数。但是 infix 函数允许我们将函数调用的小数点、括号等计算机相关的语法去掉,从而使用一种更接近英语的语法来编写程序,使得代码看起来更具有可读性。

下面看一些复杂的例子。比如说这里有个集合,如果想要判断集合中是否包括某个指定元素,一般可以这样写:

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

我们仍然可以借助 infix 函数让这段代码变得更具可读性,在 infix.kt 文件中添加如下代码:

kotlin 复制代码
infix fun <T> Collection<T>.has(element: T) = contains(element)

可以看到,我们给 Collection 接口添加了一个扩展函数,这是因为 Collection 是 Java 以及 Kotlin 所有集合的总接口。因此给 Collection 添加了一个 has 函数,那么,所有集合的子类就都可以使用这个函数了。

另外,这里还用了泛型函数的定义方法,从而使得 has 函数可以接收任意类型具体类型的参数。而这个函数内部的实现就是调用了 Collection 接口中的 contians() 函数而已。也就是说,has 函数和 contains 函数的功能世纪上是一摸一样的,只是它多了一个 infix 关键字,从而拥有了 infix 函数的语法糖功能。

现在我们就可以使用如下的语法来判断集合中是否包括某个指定的元素:

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

将 infix.kt 反编译成 Java 代码:

java 复制代码
public final class InfixKt {
   public static final boolean beginsWith(@NotNull String $this$beginsWith, @NotNull String prefix) {
      Intrinsics.checkNotNullParameter($this$beginsWith, "$this$beginsWith");
      Intrinsics.checkNotNullParameter(prefix, "prefix");
      return StringsKt.startsWith$default($this$beginsWith, prefix, false, 2, (Object)null);
   }

   public static final boolean has(@NotNull Collection $this$has, Object element) {
      Intrinsics.checkNotNullParameter($this$has, "$this$has");
      return $this$has.contains(element);
   }

   @NotNull
   public static final Pair with(Object $this$with, Object that) {
      return new Pair($this$with, that);
   }
}

可以看到都是普通的函数调用,也就是说中缀函数的原理是编译器在语法层面给予了支持。Kotlin 的很多特性都是在语法和编译器上的优化。

相关推荐
腾讯TNTWeb前端团队3 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰6 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪6 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪7 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy7 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom8 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom8 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom8 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom8 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom8 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试