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

在 Kotlin,一个至少有一个参数的函数可以定义为一个常规的非成员函数,也可以定义为一个参数为接收器的 扩展函数扩展函数函数。

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

语言引用似乎不推荐在不同情况下使用常规函数或扩展函数。

所以,我的问题是: 什么时候扩展函数优于常规的非成员函数?当普通的延长?

foo.bar(baz, baq) vs bar(foo, baz, baq).

它是否只是函数语义的一个提示(接收方绝对是焦点) ,或者有使用扩展函数使代码更清晰或者打开机会的情况?

10900 次浏览

至少有一种情况下,扩展函数是必须调用的链接,也被称为“流畅风格”:

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

假设您想用自己的操作从 Java8扩展 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 时。扩展函数是通过添加新功能来更改类的惯用方法。可以添加 扩展函数扩展属性。请参阅 Jackson-Kotlin 舱中的一个示例,该示例将方法添加到 ObjectMapper类中,从而简化了对 TypeReference和泛型的处理。

  2. 为无法在 null上调用的新方法或现有方法添加 null 安全性。例如,字符串 String?.isNullOrBlank()的扩展函数允许您在 null字符串上使用该函数,而不必首先执行自己的 null检查。函数本身在调用内部函数之前进行检查。有关使用 Nullable Receiver 的扩展,请参阅 < a href = “ https://kotlinlang.org/docs/reference/extsions.html # Nullable-Receiver”rel = “ noReferrer”> 文档

强制性个案:

  1. 如果需要接口的内联默认函数,则必须使用扩展函数将其添加到接口中,因为在接口声明中不能这样做(内联函数必须是 final,这在当前接口中是不允许的)。当你需要内联的具体化函数时,这是非常有用的,例如这个来自 Injekt 的代码

  2. 当您希望向当前不支持该用法的类添加 for (item in collection) { ... }支持时。您可以添加遵循 For 循环文档中描述的规则的 iterator()扩展方法——即使返回的类似迭代器的对象也可以使用扩展来满足提供 next()hasNext()的规则。

  3. 将操作符添加到现有类中,如 +*(专门化为 # 1,但是不能以其他任何方式这样做,因此是强制性的)。请参阅 < a href = “ https://kotlinlang.org/docs/reference/operation-overloading.html”rel = “ norefrer”> 文档中的运算符重载

选择性个案:

  1. 您希望控制调用方可见的范围,因此只能在允许调用可见的上下文中扩展类。这是可选的,因为您可以只允许始终看到扩展。关于扩展函数的作用域,请参阅其他 SO 问题的答案

  2. 您有一个接口,希望简化所需的实现,同时仍然允许用户使用更简单的助手函数。您可以选择添加接口的默认方法来提供帮助,或者使用扩展函数来添加接口中不需要实现的部分。一个允许重写默认值,另一个不允许(除了扩展与成员的优先级)。

  3. 当您希望将函数与功能类别相关联时,扩展函数使用其接收方类作为查找它们的位置。它们的名称空间成为可以从中触发它们的类(或类)。而顶级函数将更难找到,并且将填充 IDE 代码完成对话框中的全局名称空间。还可以修复现有的库名称空间问题。例如,在 Java7中有 Path类,很难找到 Files.exist(path)方法,因为它的名称间隔很奇怪。这个函数可以直接放在 Path.exists()上。(@kirill)

优先权规则:

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

对于当前上下文中的每个隐式接收方,我们先尝试成员,然后是局部扩展函数(也是具有扩展函数类型的参数) ,然后是非局部扩展。

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

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

这是不可能写成一个“正常”的方法。