Kotlin es un lenguaje funcional, y eso significa que las funciones son frontales y centrales. El lenguaje está repleto de características para hacer que las funciones de codificación sean fáciles y expresivas. En esta publicación, aprenderá sobre funciones de extensión, funciones de orden superior, cierres y funciones en línea en Kotlin.
En el artículo anterior, aprendió sobre funciones de nivel superior, expresiones lambda, funciones anónimas, funciones locales, funciones de infijo y, finalmente, funciones miembro en Kotlin. En este tutorial, continuaremos aprendiendo más sobre las funciones en Kotlin al profundizar en:
¿No sería bueno si el Cuerda
tipo en Java tenía un método para poner en mayúscula la primera letra de una Cuerda
-me gusta ucfirst ()
en PHP? Podríamos llamar a este método. upperCaseFirstLetter ()
.
Para darte cuenta de esto, podrías crear un Cuerda
subclase que extiende la Cuerda
escriba en java. Pero recuerda que el Cuerda
La clase en Java es final, lo que significa que no puedes extenderla. Una posible solución para Kotlin sería crear funciones de ayuda o funciones de nivel superior, pero esto podría no ser ideal porque no podríamos utilizar la función de autocompletar IDE para ver la lista de métodos disponibles para el Cuerda
tipo. Lo que sería realmente bueno sería agregar de alguna manera una función a una clase sin tener que heredar de esa clase.
Bueno, Kotlin nos ha cubierto con otra característica impresionante: funciones de extensión. Esto nos permite extender una clase con una nueva funcionalidad sin tener que heredar de esa clase. En otras palabras, no necesitamos crear un nuevo subtipo o alterar el tipo original.
Una función de extensión se declara fuera de la clase que quiere extender. En otras palabras, también es una función de nivel superior (si desea un repaso de funciones de nivel superior en Kotlin, visite el tutorial Más diversión con funciones de esta serie).
Junto con las funciones de extensión, Kotlin también soporta propiedades de extensión. En esta publicación, analizaremos las funciones de extensión y esperaremos hasta una publicación futura para analizar las propiedades de extensión junto con las clases en Kotlin.
Como puede ver en el código a continuación, definimos una función de nivel superior como normal para que declaremos una función de extensión. Esta función de extensión está dentro de un paquete llamado com.chike.kotlin.strings
.
Para crear una función de extensión, debe prefijar el nombre de la clase que está extendiendo antes del nombre de la función. El nombre de la clase o el tipo en el que se define la extensión se llama tipo de receptor, y el objeto receptor es la instancia de clase o valor en el que se llama la función de extensión.
paquete com.chike.kotlin.strings fun String.upperCaseFirstLetter (): String return this.substring (0, 1) .toUpperCase (). plus (this.substring (1))
Tenga en cuenta que el esta
palabra clave dentro del cuerpo de la función hace referencia al objeto receptor o instancia.
Después de crear la función de extensión, primero deberá importar la función de extensión a otros paquetes o archivos que se utilizarán en ese archivo o paquete. Entonces, llamar a la función es lo mismo que llamar a cualquier otro método de la clase de tipo de receptor.
paquete com.chike.kotlin.packagex import com.chike.kotlin.strings.upperCaseFirstLetter print ("chike" .upperCaseFirstLetter ()) // "Chike"
En el ejemplo anterior, tipo de receptor es clase Cuerda
, y el objeto receptor es "chike"
. Si está utilizando un IDE como IntelliJ IDEA que tiene la función IntelliSense, verá su nueva función de extensión sugerida entre la lista de otras funciones en una Cuerda
tipo.
Tenga en cuenta que detrás de las escenas, Kotlin creará un método estático. El primer argumento de este método estático es el objeto receptor. Así que es fácil para los llamadores de Java llamar a este método estático y luego pasar el objeto receptor como un argumento.
Por ejemplo, si nuestra función de extensión fue declarada en un StringUtils.kt archivo, el compilador Kotlin crearía una clase Java StringUtilsKt
con un método estático upperCaseFirstLetter ()
.
/ * Java * / package com.chike.kotlin.strings public class StringUtilsKt public static String upperCaseFirstLetter (String str) return str.substring (0, 1) .toUpperCase () + str.substring (1);
Esto significa que los llamadores de Java pueden simplemente llamar al método haciendo referencia a su clase generada, al igual que para cualquier otro método estático.
/ * Java * / print (StringUtilsKt.upperCaseFirstLetter ("chike")); // "Chike"
Recuerde que este mecanismo de interoperabilidad de Java es similar a cómo funcionan las funciones de nivel superior en Kotlin, como hemos comentado en la publicación Más diversión con funciones!
Tenga en cuenta que las funciones de extensión no pueden anular las funciones ya declaradas en una clase o interfaz conocida como funciones miembro (si desea actualizar las funciones de miembro en Kotlin, consulte el tutorial anterior de esta serie). Por lo tanto, si ha definido una función de extensión con exactamente la misma firma de función, el mismo nombre de función y el mismo número, tipos y orden de argumentos, independientemente del tipo de retorno, el compilador de Kotlin no lo invocará. En el proceso de compilación, cuando se invoca una función, el compilador Kotlin buscará primero una coincidencia en las funciones miembro definidas en el tipo de instancia o en sus superclases. Si hay una coincidencia, entonces esa función miembro es la que se invoca o vincula. Si no hay coincidencia, entonces el compilador invocará cualquier función de extensión de ese tipo.
Así que en resumen: las funciones miembro siempre ganan.
Veamos un ejemplo práctico..
class Student fun printResult () println ("Impresión del resultado del estudiante") fun expel () println ("Expelling student from school") fun Student.printResult () println ("Función de extensión printResult ()") fun Student.expel (reason: String) println ("Expelling student from School. Reason: \" $ reason \ "")
En el código anterior, definimos un tipo llamado Estudiante
con dos funciones miembro: printResult ()
y expulsar()
. Luego definimos dos funciones de extensión que tienen los mismos nombres que las funciones miembro.
Llamemos al printResult ()
Funcionar y ver el resultado..
val student = Student () student.printResult () // Impresión del resultado del alumno
Como puede ver, la función que se invocó o enlazó fue la función miembro y no la función de extensión con la misma firma de función (aunque IntelliJ IDEA aún le daría una pista al respecto).
Sin embargo, llamando a la función miembro expulsar()
y la función de extensión expulsar (motivo: cadena)
Producirá diferentes resultados porque las firmas de la función son diferentes.
student.expel () // Expelling student from school student.expel ("robó dinero") // Expelling student from School. Razón: "robó dinero"
La mayoría de las veces declarará una función de extensión como una función de nivel superior, pero tenga en cuenta que también puede declararlas como funciones miembro..
clase ClassB clase ClassA fun ClassB.exFunction () print (toString ()) // llama a ClassB toString () fun callExFunction (classB: ClassB) classB.exFunction () // llama a la función de extensión
En el código anterior, declaramos una función de extensión. exFunction ()
de Clase B
escribe dentro de otra clase Clase A
. los receptor de despacho es la instancia de la clase en la que se declara la extensión, y la instancia del tipo de receptor del método de extensión se llama extensión del receptor. Cuando hay un conflicto de nombres o una sombra entre el receptor de despacho y el receptor de extensión, tenga en cuenta que el compilador elige el receptor de extensión.
Así que en el ejemplo de código anterior, extensión del receptor es una instancia de Clase B
-por lo que significa el Encadenar()
el metodo es de tipo Clase B
cuando se llama dentro de la función de extensión exFunction ()
. Para nosotros invocar el Encadenar()
método de la receptor de despacho Clase A
en su lugar, tenemos que utilizar un calificado esta
:
// ... fun ClassB.extFunction () print ([email protected] ()) // ahora llama a ClassA toString () method // ...
Una función de orden superior es solo una función que toma otra función (o expresión lambda) como parámetro, devuelve una función o realiza ambas cosas. los último()
La función de recopilación es un ejemplo de una función de orden superior de la biblioteca estándar..
val stringList: lista= listOf ("in", "the", "club") print (stringList.last it.length == 3) // "the"
Aquí pasamos una lambda a la último
función para servir como predicado para buscar dentro de un subconjunto de elementos. Ahora nos dedicaremos a crear nuestras propias funciones de orden superior en Kotlin..
Mirando la funcion operación de círculo ()
A continuación, tiene dos parámetros. El primero, radio
, acepta un doble, y el segundo, op
, es una función que acepta un doble como entrada y también devuelve un doble como salida; podemos decir más brevemente que el segundo parámetro es "una función del doble al doble".
Observar que el op
Los tipos de parámetros de función para la función se incluyen entre paréntesis ()
, y el tipo de salida está separado por una flecha. La función operación de círculo ()
es un ejemplo típico de una función de orden superior que acepta una función como parámetro.
diversión calCircumference (radio: Doble) = (2 * Math.PI) * radio fun calArea (radio: Doble): Double = (Math.PI) * Math.pow (radio, 2.0) fun circleOperation (radio: Double, op: (Doble) -> Doble): Doble resultado de val = op (radio) resultado de retorno
En la invocación de este operación de círculo ()
función, pasamos otra función, calArea ()
, a eso (Tenga en cuenta que si la firma del método de la función pasada no coincide con lo que declara la función de orden superior, la función call no se compilará).
Pasar el calArea ()
funciona como un parámetro para operación de círculo ()
, necesitamos prefijarlo con ::
y omita el ()
soportes.
print (circleOperation (3.0, :: calArea)) // 28.274333882308138 print (circleOperation (3.0, calArea)) // no compilará print (circleOperation (3.0, calArea ())) // no compilará print (circleOperation) 6.7, :: calCircumference)) // 42.09734155810323
El uso inteligente de funciones de orden superior puede hacer que nuestro código sea más fácil de leer y más comprensible.
También podemos pasar un lambda (o función literal) a una función de orden superior directamente cuando invocamos la función:
circleOperation (5.3, (2 * Math.PI) * it)
Recuerde, para que evitemos nombrar explícitamente el argumento, podemos usar el eso
el nombre del argumento se genera automáticamente para nosotros solo si la lambda tiene un argumento. (Si desea una actualización de lambda en Kotlin, visite el tutorial Más diversión con funciones de esta serie).
Recuerde que además de aceptar una función como parámetro, las funciones de orden superior también pueden devolver una función a las personas que llaman.
multiplicador de diversión (factor: doble): (doble) -> doble = número -> número * factor
Aquí el multiplicador()
La función devolverá una función que aplica el factor dado a cualquier número que se le pase. Esta función devuelta es una lambda (o función literal) de doble a doble (lo que significa que el parámetro de entrada de la función devuelta es un tipo doble, y el resultado de salida también es un tipo doble).
val doubler = multiplicador (2) print (doubler (5.6)) // 11.2
Para probar esto, pasamos un factor de dos y asignamos la función devuelta al duplicador de variables. Podemos invocar esto como una función normal, y cualquier valor que le demos se duplicará.
Un cierre es una función que tiene acceso a variables y parámetros que se definen en un ámbito externo.
fun printFilteredNamesByLength (length: Int) val names = arrayListOf ("Adam", "Andrew", "Chike", "Kechi") val filterResult = names.filter it.length == length println (filterResult) printFilteredNamesByLength ( 5) // [Chike, Kechi]
En el código anterior, la lambda pasa a la filtrar()
La función de recolección usa el parámetro. longitud
de la función exterior printFilteredNamesByLength ()
. Tenga en cuenta que este parámetro está definido fuera del alcance de la lambda, pero que lambda todavía puede acceder a la longitud
. Este mecanismo es un ejemplo de cierre en programación funcional..
En Más diversión con funciones, mencioné que el compilador Kotlin crea una clase anónima en versiones anteriores de Java detrás de escena al crear expresiones lambda..
Desafortunadamente, este mecanismo introduce una sobrecarga porque una clase anónima se crea bajo el capó cada vez que creamos un lambda. Además, un lambda que utiliza el parámetro de la función externa o la variable local con un cierre agrega su propia sobrecarga de asignación de memoria porque un nuevo objeto se asigna al montón con cada invocación.
Para evitar estos gastos generales, el equipo de Kotlin nos proporcionó la en línea
Modificador para funciones. Una función de orden superior con el en línea
El modificador estará en línea durante la compilación del código. En otras palabras, el compilador copiará la lambda (o función literal) y también el cuerpo de la función de orden superior y los pegará en el sitio de la llamada.
Veamos un ejemplo práctico..
fun circleOperation (radio: Doble, op: (Doble) -> Doble) println ("El radio es $ radius") resultado val = op (radio) println ("El resultado es $ result") fun main (args: Array) circleOperation (5.3, (2 * Math.PI) * it)
En el código anterior, tenemos una función de orden superior operación de círculo ()
eso no tiene el en línea
modificador Ahora veamos el código de bytes de Kotlin generado cuando compilamos y descompilamos el código, y luego lo comparamos con uno que tiene la en línea
modificador.
public final class InlineFunctionKt public static final void circleOperation (doble radio, @NotNull Function1 op) Intrinsics.checkParameterIsNotNull (op, "op"); Cadena var3 = "El radio es" + radio; System.out.println (var3); doble resultado = ((Número) op.invoke (radio)). doubleValue (); String var5 = "El resultado es" + resultado; System.out.println (var5); public static final void main (@NotNull String [] args) Intrinsics.checkParameterIsNotNull (args, "args"); circleOperation (5.3D, (Function1) null.INSTANCE);
En el bytecode Java generado anteriormente, puede ver que el compilador llamó a la función operación de círculo ()
dentro de principal()
método.
Especifiquemos ahora la función de orden superior como en línea
En su lugar, y también ver el código de bytes generado..
inline fun circleOperation (radio: Doble, op: (Doble) -> Doble) println ("El radio es $ radius") resultado val = op (radio) println ("El resultado es $ resultado") fun main (args: Formación) circleOperation (5.3, (2 * Math.PI) * it)
Para hacer una función de orden superior en línea, tenemos que insertar el en línea
modificador antes de la divertido
palabra clave, al igual que hicimos en el código anterior. También verifiquemos el bytecode generado para esta función en línea.
public static final voO circleOperation (doble radio, @NotNull Function1 op) Intrinsics.checkParameterIsNotNull (op, "op"); Cadena var4 = "El radio es" + radio; System.out.println (var4); doble resultado = ((Número) op.invoke (radio)). doubleValue (); String var6 = "El resultado es" + resultado; System.out.println (var6); public static final void main (@NotNull String [] args) Intrinsics.checkParameterIsNotNull (args, "args"); doble radio $ iv = 5.3D; Cadena var3 = "El radio es" + radio $ iv; System.out.println (var3); resultado doble $ iv = 6.283185307179586D * radio $ iv; String var9 = "El resultado es" + resultado $ iv; System.out.println (var9);
Mirando el bytecode generado para la función en línea dentro del principal()
función, se puede observar que en lugar de llamar a la operación de círculo ()
función, ahora ha copiado el operación de círculo ()
Cuerpo de la función que incluye el cuerpo lambda y lo pegó en su sitio de llamada.
Con este mecanismo, nuestro código se ha optimizado significativamente, no más creación de clases anónimas o asignaciones de memoria extra. Pero tenga en cuenta que tendríamos un código de bytes más grande detrás de las escenas que antes. Por este motivo, se recomienda encarecidamente solo incluir funciones de orden superior más pequeñas que acepten lambda como parámetros.
Muchas de las funciones estándar de la biblioteca de orden superior en Kotlin tienen el modificador en línea. Por ejemplo, si echa un vistazo a las funciones de la operación de colección filtrar()
y primero()
, verás que tienen el en línea
Modificador y también son pequeños en tamaño.
diversión pública en líneaIterable .filtro (predicado: (T) -> Booleano): Lista return filterTo (ArrayList (), predicado) público en línea diversión Iterable .primero (predicado: (T) -> booleano): T para (elemento en esto) si (predicado (elemento)) devuelve el elemento lanzado NoSuchElementException ("La colección no contiene ningún elemento que coincida con el predicado.")
¡Recuerde no incluir las funciones normales que no aceptan un lambda como parámetro! Se compilarán, pero no habrá una mejora significativa en el rendimiento (IntelliJ IDEA incluso daría una pista sobre esto).
noinline
ModificadorSi tiene más de dos parámetros lambda en una función, tiene la opción de decidir qué lambda no debe estar en línea usando el noinline
Modificador en el parámetro. Esta funcionalidad es útil especialmente para un parámetro lambda que tendrá una gran cantidad de código. En otras palabras, el compilador de Kotlin no copiará ni pegará esa lambda donde se llama, sino que creará una clase anónima detrás de la escena..
inline fun myFunc (op: (Doble) -> Doble, noinline op2: (Int) -> Int) // realizar operaciones
Aquí, insertamos el noinline
Modificador al segundo parámetro lambda. Tenga en cuenta que este modificador solo es válido si la función tiene la en línea
modificador.
Tenga en cuenta que cuando se lanza una excepción dentro de una función en línea, la pila de llamadas al método en el seguimiento de pila es diferente de una función normal sin la en línea
modificador Esto se debe al mecanismo de copiar y pegar empleado por el compilador para las funciones en línea. Lo bueno es que IntelliJ IDEA nos ayuda a navegar fácilmente la pila de llamadas de método en el seguimiento de pila para una función en línea. Veamos un ejemplo.
inline fun myFunc (op: (Double) -> Double) throw Exception ("message 123") fun main (args: Array) myFunc (4.5)
En el código anterior, se lanza una excepción deliberadamente dentro de la función en línea myFunc ()
. Ahora veamos el seguimiento de la pila dentro de IntelliJ IDEA cuando se ejecuta el código. Mirando la captura de pantalla a continuación, puede ver que tenemos dos opciones de navegación para elegir: el cuerpo de la función en línea o el sitio de llamada de función en línea. Elegir el primero nos llevará al punto en que se lanzó la excepción en el cuerpo de la función, mientras que el segundo nos llevará al punto en que se llamó el método..
Si la función no era una en línea, nuestra traza de pila sería como la que ya conoces:
En este tutorial, aprendiste aún más cosas que puedes hacer con las funciones en Kotlin. Cubrimos
En el siguiente tutorial de la serie Kotlin From Scratch, profundizaremos en la programación orientada a objetos y comenzaremos a aprender cómo funcionan las clases en Kotlin. Te veo pronto!
Para obtener más información sobre el idioma Kotlin, recomiendo visitar la documentación de Kotlin. O echa un vistazo a algunas de nuestras otras publicaciones de desarrollo de aplicaciones para Android aquí en Envato Tuts+!