Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. Kotlin

Kotlin Desde Cero: Funciones Avanzadas

by
Difficulty:IntermediateLength:LongLanguages:
This post is part of a series called Kotlin From Scratch.
Kotlin From Scratch: More Fun With Functions
Kotlin From Scratch: Classes and Objects

Spanish (Español) translation by Rafael Chavarría (you can also view the original English article)

Kotlin es un lenguaje funcional, y eso significa que las funciones son prominentes. El lenguaje está empacado con características para hacer las funciones de código fáciles y expresivas. En este artículo, aprenderás sobre funciones de extensión, funciones de orden más alto, cierres, y funciones en línea en Kotlin.

En el artículo anterior, aprendiste sobre las funciones de nivel superior, expresiones lambda, funciones anónimas, funciones locales, funciones infijas, y finalmente funciones miembro en Kotlin. En este tutorial, continuaremos aprendiendo más sobre funciones en Kotlin excavando en:

  • funciones extensión
  • funciones de orden alto
  • cierres
  • funciones en línea

1. Funciones de Extensión

¿No sería bueno si el tipo String en Java tuviera un método para capitalizar la primera letra en una String--como ucfirst() en PHP? Podríamos llamar a este método upperCaseFirstLetter().

Para realizar esto, podrías crear una subclase String la cuál extienda el tipo String en Java. Pero recuerda que la clase String 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 entonces no podríamos hacer uso de la característica auto-completar del IDE para ver la lista de métodos disponibles para el tipo String. Lo que sería realmente agradable sería de algún modo agregar una función a una clase sin tener que heredar de esa clase.

Bueno, Kotlin nos ha cubierto con otra característica asombrosa: funciones extensión. Estas nos dan la habilidad de extender una clase con 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 extensión es declarada fuera de la clase que quiere extender. En otras palabras, también es una función de nivel superior (si quieres un recordatorio sobre funciones de nivel superior en Kotlin, visita el tutorial Más Diversión Con Funciones en esta serie).

Junto con funciones de extensión, Kotlin también soporta propiedades de extensión. En este artículo, discutiremos las funciones de extensión, y esperaremos hasta una publicación futura para discutir propiedades de extensión junto con clases en Kotlin.

Creando una Función de Extensión

Cómo puedes ver en el código de abajo, definimos una función de nivel superior de manera normal para nosotros para declarar 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, tienes que prefijar el nombre de la clase que estás extendiendo antes del nombre de la función. El nombre de la clase o el tipo en el cuál la extensión es definida es llamado tipo de recibidor, y el objeto recibidor es la instancia de clase o valor sobre el cuál la función de extensión es llamada.

Nota que la palabra clave this dentro del cuerpo de la función referencia al objeto recibidor o instancia.

Llamando a una Función de Extensión

Después de crear tu función de extensión, primero necesitarás importar la función extensión en otros paquetes o archivos a ser usados en ese archivo o paquete. Después, llamar a la función es igual que llamar a cualquier otro método de la clase de tipo de recibidor.

En el ejemplo de arriba, el tipo de recibidor es clase String, y el objeto recibidor es "chike". Si estás usando un IDE tal como IntelliJ IDEA que tiene la característica IntelliSense, verías tu nueva función de extensión sugerida entre la lista de otras funciones en un tipo String.

IntelliJ IDEA intellisense feature

Interoperabilidad Java

Nota que detrás de escenas, Kotlin creará un método estático. El primer argumento de este método estático es el objeto recibidor. Así que es sencillo para los llamadores Java llamar a este método estático y después pasar el objeto recibidor como un argumento.

Por ejemplo, si nuestra función de extensión fue declarada en un archivo StringUtils.kt, el compilador Kotlin debería crear una clase Java StringUtilsKt con un método estático upperCaseFirstLetter().

Esto significa que los llamadores Java pueden simplemente llamar al método referenciando su clase generada, justo como cualquier otro método estático.

Recuerda que este mecanismo interoperacional Java es similar a cómo funcionan las funciones de nivel superior en Kotlin, ¡como discutimos en el artículo Más Diversión Con Funciones!

Funciones de Extensión vs. Funciones Miembro

Nota que las funciones de extensión no pueden sobreescribir funciones que ya están declaradas en una clase o interfaz--conocida como funciones miembro (si quieres un recordatorio sobre funciones miembro en Kotlin, echa un vistazo al tutorial anterior en esta serie). Así qué, si has definido una función de extensión con exactamente la misma función firma---el mismo nombre de función y mismo número, tipos y orden de argumentos, independientemente del tipo de retorno---el compilador Kotlin no lo invocará. En el proceso de compilación, cuando una función es invocada, el compilador Kotlin primero buscará 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 es invocada o limitada. 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.

En el código de arriba, definimos un tipo llamado Student con dos funciones miembro: printResult() y expel(). Entonces definimos dos funciones de extensión que tienen los mismos nombres que las funciones miembro.

Llamemos a la función printResult() y veamos el resultado.

Como puedes ver, la función que fue invocada o limitada fue la función miembro y no la función de extensión con la misma firma de función (aunque IntilliJ IDEA aún te dará una pista sobre este).

Sin embargo, llamar a la función miembro expel() y la función de extensión expel(reason: String) producirá resultados diferentes porque las firmas de la función son diferentes.

Funciones de Extensión de Miembro

Declararás una función de extensión como una función de nivel superior la mayoría del tiempo, pero nota que también puedes declararlas como funciones miembro.

En el código de arriba, declaramos una función de extensión exFunction() de tipo ClassB dentro de otra clase ClassA. El recibidor de envío es la instancia de la clase en la cuál la extensión es declarada, y la instancia del tipo de recibidor del método de extensión es llamado el recibidor de extensión. Cuando hay un conflicto de nombre u opacamiento entre el recibidor de envío y recibidor de extensión, nota que el compilador elige el recibidor de extensión.

Así que en el código de ejemplo de abajo, el recibidor de extensión es una instancia de ClassB---así que significa que el método toString() es de tipo ClassB cuando se llama dentro de la función de extensión exFunction(). Para que invoquemos el método toString() del recibidor de función ClassA en su lugar, necesitamos usar un this calificado:

2. Funciones de Orden Más Alto

Una función de orden más alto es solo una función que toma otra función (o expresión lambda) como parámetro, devuelve una función, o hace ambos. La función colección last() es un ejemplo de una función de orden más alto desde la librería estándar.

Aquí pasamos una lambda a la función last para servir como un predicado para buscar dentro de un subconjunto de elementos. Ahora nos sumergiremos a crear nuestras propias funciones de orden más alto en Kotlin.

Creando una Función de Orden Más Alto

Viendo la función circleOperation() de abajo, esta tiene dos parámetros. El primero, radius, acepta un double, y el segundo, op, es una función que acepta un double como entrada y también devuelve un double como salida---podemos decir sucintamente que el segundo parámetro es "una función de double a double".

Observa que los tipos de parámetro de función op para la función están envueltos en paréntesis (), y el tipo de salida es separado por una flecha. La función circleOperation() es un ejemplo típico de una función de orden más alto que acepta una función como parámetro.

Invocando una Función de Orden Más Alto

En la invocación de esta función circleOperation(), pasamos otra función, calArea(), a esta. (Nota que si la firma del método de la función pasada no corresponde a lo que declara la función de orden más alto, el llamado de función no se compilará.)

Para pasar la función calArea() como un parámetro a circleOperation(), necesitamos prefijarlo con :: y omitir los paréntesis ().

Usar funciones de nivel más alto de manera sabia puede hacer a nuestro código más fácil de leer y más entendible.

Funciones Lambda y de Orden Más Alto

También podemos pasar una lambda (o literal de función) a una función de orden más alto directamente cuando invocamos la función:

Recuerda, para que evitemos nombrar el argumento de manera explícita, podemos usar el nombre de argumento auto-generado it para nosotros solo si la lambda tiene un argumento. (Si quieres un recordatorio sobre lambda en Kotlin, visita el tutorial Más Diversión Con Funciones en esta serie).

Devolviendo una Función

Recuerda que en adición a aceptar una función como un parámetro, las funciones de orden más alto también pueden devolver una función a los llamadores.

Aquí la función multiplier() devolverá una función que aplica el factor dado a cualquier número pasado a esta. Esta función devuelta es una lambda (o literal de función) de double a double (significando que el parámetro de entrada de la función devuelta es un tipo double, y la el resultado de salida también es un tipo doble).

Para probar esto, pasamos un factor de dos y asignamos la función devuelta a la variable doubler. Podemos invocar esto como una función normal, y sea cual sea el valor que pasemos a esta será doblado.

3. Cierres

Un cierre es una función que tiene acceso a variables y parámetros que son definidos en un alcance foráneo.

En el código de arriba, la lambda pasada a la función colección filter() usa el parámetro length de la función foránea printFilteredNamesByLength(). Nota que este parámetro es definido fuera del alcance de la lambda, pero que la lambda aún puede acceder el length. Este mecanismo es un ejemplo de cierre en programación funcional.

4. Funciones En Línea

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 escenas cuando se crean expresiones lambda.

Desafortunadamente, este mecanismo introduce gastos generales porque una clase anónima es creada bajo el capó cada vez que creamos una lambda. También, una lambda que usa el parámetro de función exterior o variable local con un cierre agrega su propia ubicación de memoria porque un nuevo objeto es asignado al montón con cada invocación.

Comparando Funciones En Línea Con Funciones Normales

Para prevenir estos gastos generales, el equipo Kotlin nos proporcionó el modificador inline para funciones. Una función de orden más alto con el modificador inline será alineado durante la compilación de código. En otras palabras, el compilador copiará la lambda (o literal de función) y también el cuerpo de la función de orden más alto  y la pegará en el sitio de la llamada.

Veamos un ejemplo práctico.

En el código de arriba, tenemos una función de orden más alto circleOperation() que no tiene el modificador inline. Ahora veamos el código byte Kotlin generado cuando compilamos y descoompilamos el código, y después lo comparamos con uno que tiene el modificador inline.

En el código byte Java generado arriba, puedes ver que el compilador llamó la función circleOperation() dentro del método main().

Ahora especifiquemos la función de orden más alto como inline en su lugar, y también veamos el código byte generado.

Para hacer una función de orden más alto en línea, tenemos que insertar el modificador inline antes de la palabra clave fun, justo como hicimos en el código de arriba. También revisemos el código byte generado para esta función en línea.

Viendo al código byte generado para la función en línea dentro de la función main(), puedes observar que en vez de llamar a la función circleOperation(), ahora ha copiado el cuerpo de la función circleOperation() incluyendo el cuerpo lambda y pegándolo como su sitio de llamada.

Con este mecanismo, nuestro código ha sido significativamente optimizado---no más creación de clases anónimas o asignaciones de memoria extra. Pero sé consciente de que hemos tenido un código byte más grande tras bambalinas que antes. Por esta razón, es altamente recomendable solo alinear funciones de orden más alto más pequeñas que acepten lambda como parámetros.

Muchas de las funciones de orden más alto de la librería estándar en Kotlin tienen el modificador en línea. Por ejemplo, si echas un vistazo a la colección de funciones de operación filter() y first(), verás que tienen el modificador inline y también son pequeñas en tamaño.

¡Recuerda no alinear funciones normales que no acepten una lambda como parámetro! Estas se compilarán, pero podría no haber mejoramiento significativo de desempeño (IntelliJ IDEA incluso dará una pista sobre esto).

El Modificador noinline

Si tienes más de dos parámetros lambda en una función, tienes la opción de decidir cuál lambda no alinear usando el modificador noinline sobre el parámetro. Esta funcionalidad es útil especialmente para un parámetro lambda que tomará mucho código. En otras palabras, el compilador Kotlin no copiará y pegará esa lambda en donde es llamada pero en su lugar creará una clase anónima tras bambalinas.

Aquí, insertamos el modificador noinline al segundo parámetro lambda. Nota que este modificador solo es válido si la función tiene el modificador inline.

Rastro de Pila en Funciones En Línea

Nota que cuando una excepción es arrojada dentro de una función en línea, el apilamiento de llamada de método en el rastro de pila es distinto a una función normal si el modificador inline. Esto es debido al mecanismo de copiar y pegar empleado por el compilador para funciones en línea. La cosa interesante es que IntelliJ IDEA nos ayuda a navegar fácilmente la pila de llamada de método en el rastro de pila para una función en línea. Veamos un ejemplo.

En el código de arriba, una excepción es lanzada deliberadamente dentro de la función en línea myFunc(). Veamos ahora el rastro de pila dentro de IntelliJ IDEA cuando el código es ejecutado. Viendo la captura de pantalla de abajo, puedes ver que nos dan dos opciones de navegación para elegir: el cuerpo de la función en línea o el sitio de llamado de la función en línea. Elegir el primero nos llevará al punto de excepción que fue arrojado en el cuerpo de la función, mientras que el segundo nos llevará al punto en el que el método fue llamado.

IntelliJ IDEA stack trace for inline function

Si la función no era una en línea, nuestro rastro de pila sería como con el que ya estarías familiarizado:

IntelliJ IDEA stack trace for normal function

Conclusión

En este tutorial, aprendiste incluso más cosas que puedes hacer con funciones en Kotlin. Cubrimos:

  • funciones de extensión
  • funciones de orden más alto
  • cierres
  • funciones en línea

En el siguiente tutorial en la serie Kotlin Desde Cero, indagaremos en programación orientada a objetos y comenzaremos a aprender cómo funcionan las clases en Kotlin. ¡Nos vemos pronto!

Para aprender más sobre el lenguaje Kotlin, recomiendo visitar la documentación Kotlin. ¡O revisa algunas de nustras otras publicaciones de desarrollo de aplicaciones Android aquí en Envato Tuts+!

Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.