什么时候应该更喜欢 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)
如您所见,两者做同样的事情,但是扩展函数更短并且在调用站点上,很明显结果可以为空。
扩展函数在少数情况下有用,在其他情况下是强制性的:
惯用格:
当您想要增强、扩展或更改现有的 API 时。扩展函数是通过添加新功能来更改 class 的惯用方法。您可以添加 extension functions and extension properties. See an example in the Jackson-Kotlin Module 以向 ObjectMapper
class 添加方法,从而简化 TypeReference
和泛型的处理。
为无法在 null
上调用的新方法或现有方法添加 null 安全性。例如,String?.isNullOrBlank()
字符串的扩展函数允许您甚至在 null
字符串上使用该函数,而无需先进行自己的 null
检查。函数本身在调用内部函数之前进行检查。参见 documentation for extensions with Nullable Receiver
必填案例:
当你想要一个接口的内联默认函数时,你必须使用扩展函数将其添加到接口中,因为你不能在接口声明中这样做(内联函数必须是final
目前在接口中是不允许的)。这在需要内联具体化函数时很有用,for example this code from Injekt
当您想要为当前不支持该用法的 class 添加 for (item in collection) { ... }
支持时。您可以添加遵循 for loops documentation 中描述的规则的 iterator()
扩展方法——甚至返回的类似迭代器的对象也可以使用扩展来满足提供 next()
和 [=20] 的规则=].
向现有 class 添加运算符,例如 +
和 *
(#1 的专业化,但您不能以任何其他方式执行此操作,所以是强制性的)。参见 documentation for operator overloading
可选案例:
您想控制调用者何时可以看到某些内容的范围,因此您仅在允许调用可见的上下文中扩展 class。这是可选的,因为您可以只允许始终看到扩展。
您有一个界面,您希望简化所需的实现,同时仍允许为用户提供更简单的辅助功能。您可以选择为接口添加默认方法来提供帮助,或者使用扩展函数来添加接口的非预期实现部分。一个允许覆盖默认值,另一个不允许(扩展与成员的优先级除外)。
当您想要将功能与功能类别相关联时;扩展函数使用它们的接收器 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"方法。
在 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)
如您所见,两者做同样的事情,但是扩展函数更短并且在调用站点上,很明显结果可以为空。
扩展函数在少数情况下有用,在其他情况下是强制性的:
惯用格:
当您想要增强、扩展或更改现有的 API 时。扩展函数是通过添加新功能来更改 class 的惯用方法。您可以添加 extension functions and extension properties. See an example in the Jackson-Kotlin Module 以向
ObjectMapper
class 添加方法,从而简化TypeReference
和泛型的处理。为无法在
null
上调用的新方法或现有方法添加 null 安全性。例如,String?.isNullOrBlank()
字符串的扩展函数允许您甚至在null
字符串上使用该函数,而无需先进行自己的null
检查。函数本身在调用内部函数之前进行检查。参见 documentation for extensions with Nullable Receiver
必填案例:
当你想要一个接口的内联默认函数时,你必须使用扩展函数将其添加到接口中,因为你不能在接口声明中这样做(内联函数必须是
final
目前在接口中是不允许的)。这在需要内联具体化函数时很有用,for example this code from Injekt当您想要为当前不支持该用法的 class 添加
for (item in collection) { ... }
支持时。您可以添加遵循 for loops documentation 中描述的规则的iterator()
扩展方法——甚至返回的类似迭代器的对象也可以使用扩展来满足提供next()
和 [=20] 的规则=].向现有 class 添加运算符,例如
+
和*
(#1 的专业化,但您不能以任何其他方式执行此操作,所以是强制性的)。参见 documentation for operator overloading
可选案例:
您想控制调用者何时可以看到某些内容的范围,因此您仅在允许调用可见的上下文中扩展 class。这是可选的,因为您可以只允许始终看到扩展。
您有一个界面,您希望简化所需的实现,同时仍允许为用户提供更简单的辅助功能。您可以选择为接口添加默认方法来提供帮助,或者使用扩展函数来添加接口的非预期实现部分。一个允许覆盖默认值,另一个不允许(扩展与成员的优先级除外)。
当您想要将功能与功能类别相关联时;扩展函数使用它们的接收器 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"方法。