new - package kotlin
¿Cuándo debería uno preferir las funciones de extensión de Kotlin? (4)
En Kotlin, una función con al menos un argumento puede definirse como una función regular no miembro o como una función de extensión con un argumento como receptor.
En cuanto a la definición del alcance, parece que no hay diferencia: ambos pueden declararse dentro o fuera de las clases y otras funciones, y ambos pueden o no tener modificadores de visibilidad por igual.
La referencia del lenguaje parece no recomendar el uso de funciones regulares o funciones de extensión para diferentes situaciones.
Entonces, mi pregunta es: ¿ cuándo las funciones de extensión dan ventaja sobre las que no son miembros regulares? Y cuando los regulares sobre las extensiones?
foo.bar(baz, baq)
vs bar(foo, baz, baq)
.
¿Es solo una insinuación de semántica de una función (el receptor está definitivamente enfocado) o hay casos en que el uso de funciones de extensiones hace que el código sea mucho más limpio / abre oportunidades?
Hay al menos un caso en el que las funciones de extensión son imprescindibles: encadenamiento de llamadas, también conocido como "estilo fluido":
foo.doX().doY().doZ()
Supongamos que desea extender la interfaz de Stream desde Java 8 con sus propias operaciones. Por supuesto, puede usar funciones comunes para eso, pero se verá feo como el infierno:
doZ(doY(doX(someStream())))
Claramente, quiere usar funciones de extensión para eso. Además, no puede convertir infi funciones comunes, pero puede hacerlo con funciones de extensión:
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))
}
Hay casos donde tienes que usar métodos de extensión. Por ejemplo, si tiene alguna implementación de lista MyList<T>
, puede escribir un método de extensión como
fun Int MyList<Int>.sum() { ... }
Es imposible escribir esto como un método "normal".
Las funciones de extensión funcionan muy bien con el operador de llamada segura ?.
. Si espera que el argumento de la función a veces sea null
, en lugar de regresar pronto, conviértalo en el receptor de una función de extensión.
Función ordinaria:
fun nullableSubstring(s: String?, from: Int, to: Int): String? {
if (s == null) {
return null
}
return s.substring(from, to)
}
Función de extensión:
fun String.extensionSubstring(from: Int, to: Int) = substring(from, to)
Llamar al sitio:
fun main(args: Array<String>) {
val s: String? = null
val maybeSubstring = nullableSubstring(s, 0, 1)
val alsoMaybeSubstring = s?.extensionSubstring(0, 1)
Como puede ver, ambos hacen lo mismo, sin embargo, la función de extensión es más corta y en el sitio de llamadas, queda inmediatamente claro que el resultado será anulable.
Las funciones de extensión son útiles en algunos casos y obligatorias en otras:
Casos idiomáticos:
Cuando desee mejorar, extender o cambiar una API existente. Una función de extensión es la forma idiomática de cambiar una clase al agregar una nueva funcionalidad. Puede agregar funciones de extensión y propiedades de extensión . Vea un ejemplo en el Módulo Jackson-Kotlin para agregar métodos a la clase
ObjectMapper
simplificando el manejo deTypeReference
y genéricos.Agregar seguridad nula a métodos nuevos o existentes que no se pueden invocar en un
null
. Por ejemplo, la función de extensión para String ofString?.isNullOrBlank()
permite usar esa función incluso en una cadenanull
sin tener que hacer primero su propia comprobaciónnull
. La función en sí misma realiza la comprobación antes de llamar a las funciones internas. Consulte la documentación de las extensiones con receptor Nullable
Casos obligatorios:
Cuando desee una función predeterminada en línea para una interfaz, debe usar una función de extensión para agregarla a la interfaz porque no puede hacerlo dentro de la declaración de interfaz (las funciones en línea deben ser
final
y no están permitidas actualmente en una interfaz). Esto es útil cuando necesita funciones reificadas en línea, por ejemplo este código de InjektCuando desee agregar soporte
for (item in collection) { ... }
a una clase que actualmente no admite ese uso. Puede agregar un método de extensióniterator()
que siga las reglas descritas en la documentación de bucles for : incluso el objeto similar al iterador devuelto puede usar extensiones para satisfacer las reglas de proporcionarnext()
yhasNext()
.Agregar operadores a clases existentes como
+
y*
(especialización de # 1 pero no puede hacer esto de otra manera, por lo tanto es obligatorio). Ver documentación para sobrecarga del operador
Casos opcionales:
Desea controlar el alcance de cuando algo está visible para una persona que llama, por lo que amplía la clase solo en el contexto en el que permitirá que la llamada sea visible. Esto es opcional porque puedes permitir que las extensiones se vean siempre. ver respuesta en otra pregunta SO para funciones de extensión de alcance
Tiene una interfaz que desea simplificar la implementación requerida, al tiempo que permite más funciones de ayuda para el usuario. Opcionalmente, puede agregar métodos predeterminados para la interfaz para ayudar o usar funciones de extensión para agregar las partes de la interfaz que no se espera implementar. Uno permite anular los valores predeterminados, el otro no (excepto por la precedencia de las extensiones frente a los miembros).
Cuando desee relacionar funciones con una categoría de funcionalidad; las funciones de extensión usan su clase de receptor como un lugar desde donde encontrarlas. Su espacio de nombres se convierte en la clase (o clases) desde la que se pueden activar. Mientras que las funciones de nivel superior serán más difíciles de encontrar, y llenarán el espacio de nombre global en los diálogos de finalización del código IDE. También puede corregir los problemas de espacio existente en el nombre de la biblioteca. Por ejemplo, en Java 7 tiene la clase
Path
y es difícil encontrar elFiles.exist(path)
porque su nombre está espaciado de manera extraña. La función podría colocarse directamente enPath.exists()
lugar. (@kirill)
Reglas de precedencia:
Al extender las clases existentes, tenga en cuenta las reglas de precedencia. Se describen en KT-10806 como:
Para cada receptor implícito en el contexto actual, probamos los miembros, luego las funciones de extensión local (también los parámetros que tienen el tipo de función de extensión), y luego las extensiones no locales.