Kotlin Desde Cero: Funciones Avanzadas
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.
1 |
package com.chike.kotlin.strings |
2 |
|
3 |
fun String.upperCaseFirstLetter(): String { |
4 |
return this.substring(0, 1).toUpperCase().plus(this.substring(1)) |
5 |
}
|
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.
1 |
package com.chike.kotlin.packagex |
2 |
|
3 |
import com.chike.kotlin.strings.upperCaseFirstLetter |
4 |
|
5 |
print("chike".upperCaseFirstLetter()) // "Chike" |
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.



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().
1 |
/* Java */
|
2 |
package com.chike.kotlin.strings |
3 |
|
4 |
public class StringUtilsKt { |
5 |
|
6 |
public static String upperCaseFirstLetter(String str) { |
7 |
return str.substring(0, 1).toUpperCase() + str.substring(1); |
8 |
}
|
9 |
}
|
Esto significa que los llamadores Java pueden simplemente llamar al método referenciando su clase generada, justo como cualquier otro método estático.
1 |
/* Java */
|
2 |
print(StringUtilsKt.upperCaseFirstLetter("chike")); // "Chike" |
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.
1 |
class Student { |
2 |
|
3 |
fun printResult() { |
4 |
println("Printing student result") |
5 |
}
|
6 |
|
7 |
fun expel() { |
8 |
println("Expelling student from school") |
9 |
}
|
10 |
}
|
11 |
|
12 |
fun Student.printResult() { |
13 |
println("Extension function printResult()") |
14 |
}
|
15 |
|
16 |
fun Student.expel(reason: String) { |
17 |
println("Expelling student from School. Reason: \"$reason\"") |
18 |
}
|
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.
1 |
val student = Student() |
2 |
student.printResult() // Printing student result |
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.
1 |
student.expel() // Expelling student from school |
2 |
student.expel("stole money") // Expelling student from School. Reason: "stole money" |
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.
1 |
class ClassB { |
2 |
|
3 |
}
|
4 |
|
5 |
class ClassA { |
6 |
|
7 |
fun ClassB.exFunction() { |
8 |
print(toString()) // calls ClassB toString() |
9 |
}
|
10 |
|
11 |
fun callExFunction(classB: ClassB) { |
12 |
classB.exFunction() // call the extension function |
13 |
}
|
14 |
}
|
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:
1 |
// ...
|
2 |
fun ClassB.extFunction() { |
3 |
print(this@ClassA.toString()) // now calls ClassA toString() method |
4 |
}
|
5 |
// ...
|
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.
1 |
val stringList: List<String> = listOf("in", "the", "club") |
2 |
print(stringList.last{ it.length == 3}) // "the" |
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.
1 |
fun calCircumference(radius: Double) = (2 * Math.PI) * radius |
2 |
|
3 |
fun calArea(radius: Double): Double = (Math.PI) * Math.pow(radius, 2.0) |
4 |
|
5 |
fun circleOperation(radius: Double, op: (Double) -> Double): Double { |
6 |
val result = op(radius) |
7 |
return result |
8 |
}
|
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 ().
1 |
print(circleOperation(3.0, ::calArea)) // 28.274333882308138 |
2 |
print(circleOperation(3.0, calArea)) // won't compile |
3 |
print(circleOperation(3.0, calArea())) // won't compile |
4 |
print(circleOperation(6.7, ::calCircumference)) // 42.09734155810323 |
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:
1 |
circleOperation(5.3, { (2 * Math.PI) * it }) |
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.
1 |
fun multiplier(factor: Double): (Double) -> Double = { number -> number*factor } |
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).
1 |
val doubler = multiplier(2) |
2 |
print(doubler(5.6)) // 11.2 |
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.
1 |
fun printFilteredNamesByLength(length: Int) { |
2 |
val names = arrayListOf("Adam", "Andrew", "Chike", "Kechi") |
3 |
val filterResult = names.filter { |
4 |
it.length == length |
5 |
}
|
6 |
println(filterResult) |
7 |
}
|
8 |
|
9 |
printFilteredNamesByLength(5) // [Chike, Kechi] |
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.
1 |
fun circleOperation(radius: Double, op: (Double) -> Double) { |
2 |
println("Radius is $radius") |
3 |
val result = op(radius) |
4 |
println("The result is $result") |
5 |
}
|
6 |
|
7 |
fun main(args: Array<String>) { |
8 |
circleOperation(5.3, { (2 * Math.PI) * it }) |
9 |
}
|
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.
1 |
public final class InlineFunctionKt { |
2 |
public static final void circleOperation(double radius, @NotNull Function1 op) { |
3 |
Intrinsics.checkParameterIsNotNull(op, "op"); |
4 |
String var3 = "Radius is " + radius; |
5 |
System.out.println(var3); |
6 |
double result = ((Number)op.invoke(radius)).doubleValue(); |
7 |
String var5 = "The result is " + result; |
8 |
System.out.println(var5); |
9 |
}
|
10 |
|
11 |
public static final void main(@NotNull String[] args) { |
12 |
Intrinsics.checkParameterIsNotNull(args, "args"); |
13 |
circleOperation(5.3D, (Function1)null.INSTANCE); |
14 |
}
|
15 |
}
|
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.
1 |
inline fun circleOperation(radius: Double, op: (Double) -> Double) { |
2 |
println("Radius is $radius") |
3 |
val result = op(radius) |
4 |
println("The result is $result") |
5 |
}
|
6 |
|
7 |
fun main(args: Array<String>) { |
8 |
circleOperation(5.3, { (2 * Math.PI) * it }) |
9 |
}
|
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.
1 |
public static final void circleOperation(double radius, @NotNull Function1 op) { |
2 |
Intrinsics.checkParameterIsNotNull(op, "op"); |
3 |
String var4 = "Radius is " + radius; |
4 |
System.out.println(var4); |
5 |
double result = ((Number)op.invoke(radius)).doubleValue(); |
6 |
String var6 = "The result is " + result; |
7 |
System.out.println(var6); |
8 |
}
|
9 |
|
10 |
public static final void main(@NotNull String[] args) { |
11 |
Intrinsics.checkParameterIsNotNull(args, "args"); |
12 |
double radius$iv = 5.3D; |
13 |
String var3 = "Radius is " + radius$iv; |
14 |
System.out.println(var3); |
15 |
double result$iv = 6.283185307179586D * radius$iv; |
16 |
String var9 = "The result is " + result$iv; |
17 |
System.out.println(var9); |
18 |
}
|
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.
1 |
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> { |
2 |
return filterTo(ArrayList<T>(), predicate) |
3 |
}
|
4 |
|
5 |
public inline fun <T> Iterable<T>.first(predicate: (T) -> Boolean): T { |
6 |
for (element in this) if (predicate(element)) return element |
7 |
throw NoSuchElementException("Collection contains no element matching the predicate.") |
8 |
}
|
¡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.
1 |
inline fun myFunc(op: (Double) -> Double, noinline op2: (Int) -> Int) { |
2 |
// perform operations
|
3 |
}
|
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.
1 |
inline fun myFunc(op: (Double) -> Double) { |
2 |
throw Exception("message 123") |
3 |
}
|
4 |
|
5 |
fun main(args: Array<String>) { |
6 |
myFunc({ 4.5 }) |
7 |
}
|
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.



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



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+!


Android SDKJava vs. Kotlin: ¿Deberías Estar Usando Kotlin para Desarrollo Android?Jessica Thornsby

Android SDKIntroducción a Componentes de Arquitectura AndroidTin Megali

Android SDKCómo Usar la API Google Cloud Vision en Aplicaciones AndroidAshraff Hathibelagal

Android SDK¿Qué Son Aplicaciones Instantáneas Android?Jessica Thornsby



