什么时候应该更喜欢 Kotlin 扩展函数?

When should one prefer Kotlin extension functions?

在 Kotlin 中,具有至少一个参数的函数可以定义为常规非成员函数或 extension function,其中一个参数是接收者。

至于作用域,似乎没有区别:两者都可以在 类 和其他函数的内部或外部声明,并且都可以或不能具有相同的可见性修饰符。

语言参考似乎不建议针对不同情况使用正则函数或扩展函数。

所以,我的问题是:扩展函数什么时候比普通非成员函数更有优势?什么时候普通函数比扩展函数更有优势?

foo.bar(baz, baq) 对比 bar(foo, baz, baq).

它只是函数语义的提示(接收者肯定是焦点)还是在某些情况下使用扩展函数可以使代码更简洁或打开机会?

至少在一种情况下扩展函数是必须的 - 调用链,也称为 "fluent style":

foo.doX().doY().doZ()

假设您想使用自己的操作从 Java 8 扩展 Stream 接口。当然,你可以用普通的函数来实现,但它看起来很难看:

doZ(doY(doX(someStream())))

显然,您想为此使用扩展函数。 另外,你不能让普通函数中缀,但是你可以用扩展函数来做:

infix fun <A, B, C> ((A) -> B).`|`(f: (B) -> C): (A) -> C = { a -> f(this(a)) }

@Test
fun pipe() {
    val mul2 = { x: Int -> x * 2 }
    val add1 = { x: Int -> x + 1 }
    assertEquals("7", (mul2 `|` add1 `|` Any::toString)(3))
}

扩展函数与安全调用运算符配合得非常好?.。如果您希望函数的参数有时会是 null,而不是提前返回,请将其作为扩展函数的接收者。

普通函数:

fun nullableSubstring(s: String?, from: Int, to: Int): String? {
    if (s == null) {
        return null
    }

    return s.substring(from, to)
}

扩展函数:

fun String.extensionSubstring(from: Int, to: Int) = substring(from, to)

呼叫站点:

fun main(args: Array<String>) {
    val s: String? = null

    val maybeSubstring = nullableSubstring(s, 0, 1)
    val alsoMaybeSubstring = s?.extensionSubstring(0, 1)

如您所见,两者做同样的事情,但是扩展函数更短并且在调用站点上,很明显结果可以为空。

扩展函数在少数情况下有用,在其他情况下是强制性的:

惯用格:

  1. 当您想要增强、扩展或更改现有的 API 时。扩展函数是通过添加新功能来更改 class 的惯用方法。您可以添加 extension functions and extension properties. See an example in the Jackson-Kotlin Module 以向 ObjectMapper class 添加方法,从而简化 TypeReference 和泛型的处理。

  2. 为无法在 null 上调用的新方法或现有方法添加 null 安全性。例如,String?.isNullOrBlank() 字符串的扩展函数允许您甚至在 null 字符串上使用该函数,而无需先进行自己的 null 检查。函数本身在调用内部函数之前进行检查。参见 documentation for extensions with Nullable Receiver

必填案例:

  1. 当你想要一个接口的内联默认函数时,你必须使用扩展函数将其添加到接口中,因为你不能在接口声明中这样做(内联函数必须是final 目前在接口中是不允许的)。这在需要内联具体化函数时很有用,for example this code from Injekt

  2. 当您想要为当前不支持该用法的 class 添加 for (item in collection) { ... } 支持时。您可以添加遵循 for loops documentation 中描述的规则的 iterator() 扩展方法——甚至返回的类似迭代器的对象也可以使用扩展来满足提供 next() 和 [=20] 的规则=].

  3. 向现有 class 添加运算符,例如 +*(#1 的专业化,但您不能以任何其他方式执行此操作,所以是强制性的)。参见 documentation for operator overloading

可选案例:

  1. 您想控制调用者何时可以看到某些内容的范围,因此您仅在允许调用可见的上下文中扩展 class。这是可选的,因为您可以只允许始终看到扩展。

  2. 您有一个界面,您希望简化所需的实现,同时仍允许为用户提供更简单的辅助功能。您可以选择为接口添加默认方法来提供帮助,或者使用扩展函数来添加接口的非预期实现部分。一个允许覆盖默认值,另一个不允许(扩展与成员的优先级除外)。

  3. 当您想要将功能与功能类别相关联时;扩展函数使用它们的接收器 class 作为找到它们的地方。他们的名字 space 成为可以触发他们的 class(或 classes)。而顶级函数将更难找到,并且会在 IDE 代码完成对话框中填充全局名称 space。您还可以修复现有的库名称 space 问题。例如,在 Java 7 中有 Path class 并且很难找到 Files.exist(path) 方法,因为它的名称很奇怪 spaced。该函数可以直接放在 Path.exists() 上。 (@基里尔)

优先规则:

扩展现有的 classes 时,请牢记优先规则。它们在 KT-10806 中被描述为:

For each implicit receiver on current context we try members, then local extension functions(also parameters which have extension function type), then non-local extensions.

有些情况下您必须使用扩展方法。例如。如果你有一些列表实现MyList<T>,你可以写一个扩展方法,比如

fun Int MyList<Int>.sum() { ... }

不可能把这个写成"normal"方法。