readLine(): leer datos por teclado en Kotlin

En el lenguaje de programación Kotlin la lectura de datos por teclado se realiza con la función readLine(); pero existen 11 formas para leer datos con esta función; ¿en que se diferencian? Ya lo veremos.

Las 11 formas se dividen en:

* «Casi» seguras con el operador ?., levantando una excepción NumberFormatException.

* Manejando correctamente el NumberFormatException, lectura con el operador ?.

* Para los amantes del NullPointerException con el operador !!, casi, casi y ya veremos.

* Con el operador !!, complaciendo a los amantes del NullPointerException en dos formas.

* Lectura realmente segura con el operador ?., produciendo un verdadero null en lugar de un NFE.

* Para los amantes del NullPointerException con el operador !!, un verdadero NPE directo.

Es importante aclarar que aquí se verán las 9 formas de lectura sobre valores numéricos ya que estos son los más conflictivos comparado con los valores de tipo string debido a que no generan un NullPointerException.



Conceptos básicos a tener en cuenta:

* Operador de llamada segura ?.: permite el acceso a métodos y propiedades de forma segura sólo si no es null, de lo contrario retorna null o levanta una excepción, dependiendo del caso.

* Operador de Aserción !!: le indica al compilador que, los métodos y propiedades a las que vamos a acceder no son nulos o referencias nulas; es una mentira piadosa.

* El guión del olvido _: no tiene un nombre oficial pero le llamó así por una sola razón, este guión se usa en los casos en donde deseamos ignorar un valor o parámetro ya que no lo vamos a usar en todo el programa. Dicho valor se pierde ya que este guión no se puede usar como una variable si se desea usar en el futuro.

1. Lectura de datos de formas«casi» seguras con el operador ?., levantando una excepción NumberFormatException:

Le llamo forma «casi» segura no porque genere un NullPointerException o genere un null manejable sino que genera un NumberFormatException cuando el valor ingresado no es un valor numérico sino un string. Aquí voy a exponer cada caso, su respectiva explicación y formas viables para mejores prácticas de programación.

Las formas más sencilla para leer datos de forma «casi» segura con la función readLine(), son los siguientes:

1) Con el operador as:

package ingresando.datos

fun main(arg: Array<String>) {
    var num: Int

    print("Ingrese un numero: ")
    num = readLine()?.toInt() as Int

    println(num)
}

En la función main() hay 4 líneas de códigos, pero dependiendo de lo que se ingrese por teclado entonces se ejecutara o no la cuarta línea y es aquí en donde viene el «casi» segura. Vamos a ver el porqué.

En la primera línea se declara una variable de tipo Int muy normal, en la segunda línea se imprime en pantalla un mensaje indicando al usuario que ingrese un número y hasta aquí todo va normal; pero, en la tercera línea el programa espera a que el usuario ingrese el número ya sea positivo o negativo, con la función readLine().

La función readLine() lee todo como un tipo String? y se requiere convertir dicho valor a un tipo Int, en Kotlin esto se hace con la función .toInt(). Con el operador ?. estamos llamando a dicha función de forma segura para realizar la conversión de String a Int garantizando que no se va a generar un alguna excepción solo si el valor ingresado es compatible, lo hace perfectamente y el tipo resultante es Int?, un tipo anulable; pero, ¿qué ocurre si no se ingresa un número ya sea positivo o negativo? ¿que ocurre si ingreso un string o presiono Enter sin ingresar nada? se va a generar un NumberFormatException.

Después de la función toInt() viene el operador as, el permite realizar la conversión de tipos a un tipo no nulo y para este caso es obligatoria la conversión ya que el tipo resultante es un tipo anulable (en inglés, nullable) y la variable es un tipo no nulo, ¿como se distingue a un tipo es nullable? se distingue porque el tipo lleva el signo «?» y veremos una forma de leer datos con él, de forma segura.

2) Sin el operador as:

package ingresando.datos

fun main(arg: Array<String>) {
    var num: Int?

    print("Ingrese un numero: ")
    num = readLine()?.toInt()

    println(num)
}

Este pequeño programa es exactamente igual que el anterior, funciona igual y genera el mismo error, con un par de diferencias muy notables; la primera es que en la declaración de la variable el tipo tiene el signo «?» y esto nos indica que es un tipo anulable, es decir, puede tener un valor null y la segunda es que en la lectura de datos, al final de la función readLine(), no está el operador de conversión as y esto se debe a que la variable es un tipo anulable y no se requiere realizar conversión a un tipo no nulo como en el anterior ítem.

Al observar ambas formas, uno esperaría un comportamiento diferente para cada una, siendo más razonable que la primera levantara un NumberFormatException debido a que la variable es un tipo no nulo mientras que en la segunda sería más razonable que generara un valor null debido a que esta si es una variable que es anulable; pero las dos formas generar la misma excepción NumberFormatException y aunque no tiene la misma gravedad que el NullPointerException, tambien podria arruinar el correcto funcionamiento de los programas ya que este causaría la terminación temprana del programa si no se maneja correctamente.

2.  Manejando correctamente el NumberFormatException, lectura con el operador ?. de forma segura:

Aquí voy a plantear un par de formas de ingresar datos manejando el NumberFormatException para el caso de la primera forma expuesta anteriormente e inducir un valor null para el caso de la segunda forma anterior.

1) Con el operador as + expresión try {…} catch() {…} = sin null de forma segura:

package ingresando.datos

fun main(arg: Array<String>) {
    var num: Int

    print("Ingrese un numero: ")
    num = try { 
        readLine()?.toInt() as Int 
    } catch (_: NumberFormatException) { 
        0 
    }

    println(num)
}

Este programa es exactamente el igual que el primero de la forma anterior, la diferencia radica en que se usa una expresión try {…} catch() {…}; pero, ¿cómo funciona? Toda expresión produce un resultado cuyo valor se puede asignar a una variable, en este caso el valor es la última expresión o línea de código en su interior.

La función readLine() está al interior de try {…} esperando a que el usuario ingrese un número y una vez es presionado Enter, el valor es asignado a la variable num; pero si de lo contrario se ingresa un valor diferente a un número entero ya sea positivo o negativo, se produce un NumberFormatException que es capturado por catch() {…} retornando y asignando a num un cero (0) como valor. Un detalle a notar es que ni en try ni en catch existe una sentencia return y esto se debe a que es una expresión como se explicó anteriormente.

Aclaración: el número 0 es opcional, pude haber puesto un 100, es decisión de cada programador; pero ¿por qué un 0? Bien, lo puse como una banderilla para procesarlo con otras estructuras de control indicando que no se ingreso un valor válido ya que el 0 por lo general se asocia a la ausencia de un valor, booleano False e incluso nulos. A continuación un fragmento de código procesando la variable num con una sentencia if().

if (num != 0) {
    println("El numero ingresado es: $num")
} else {
    println("Error: numero no valido!")
}

Si num es distinto a cero se imprime el número ingresado, de lo contrario imprime un mensaje de error.

2) Sin el operador as + expresión try {…} catch() {…} = un null de forma segura:

package ingresando.datos

fun main(arg: Array<String>) {
    var num: Int?

    print("Ingrese un numero: ")
    num = try {
        readLine()?.toInt()
    } catch (_: NumberFormatException) {
        null
    }
}

Esta es la forma para leer datos ingresados por teclado de tipo anulable manejando la excepción generada al ingresar un dato de tipo erroneo (NumberFormatException) correctamente, se parece mucho a la anterior forma pero con 3 diferencias notables; la variable num es un tipo Int anulable, la función readLine() vienen sin el operador as junto con el tipo y por último, el bloque catch() {..} retorna null en lugar de 0.



A diferencia de su antecesor, aquí si podemos aprovechar que la variable es anulable para asignarle un null en lugar de un 0 si el valor ingresado no es un número el cual puede ser usado para un futuro manejo con la sentencia if() o when() de la siguiente forma:

if(num != null) {
    println("El numero ingresado es: $num")
} else {
    println("Se ha asignado -$num- a la variable")
}

Si num es distinto a null entonces entonces se imprime en mensaje con el número ingresado, de lo contrario se informa al usuario que se le ha asignado null a la variable.

3) Para los amantes del NullPointerException con el operador !!, casi, casi:

En realidad el título «Para los amantes de…..» es un poco engañoso que a primera vista puede despertar el interés de los NPE’s Lovers, por eso al final se le agrega «casi, casi» y ya veremos el porqué de todo esto.

Breve descripción: el operador de aserción !! esta diseñado para garantizar que el acceso a referencias nulas produzcan una excepcion NullPointerException natural ya que en Kotlin el sistema de tipos nos protege de este tipo de fallos de forma predeterminada. Un acceso a referencia nula podría ser el de intentar convertir un tipo de dato a otro tipo (ej: de String a Int), si es nulo o dato incompatible, produce un NPE; por ejemplo:

var x = null
var y: Int?

y = x!!.toInt()

Este fragmento de código produce un hermoso NPE sin importar si la variable –y– es o no es anulable «?», el operador de aserción !! está diseñado para esta tarea y con él le estamos asegurando al compilador que la variable –x– no contiene un valor null (es una mentira piadosa) cosa que no es verdad.

1) Con o sin operador as, expectativa vs realidad:

package ingresando.datos

fun main(arg: Array<String>) {
    var num: Int

    print("Ingrese un numero: ")
    num = readLine()!!.toInt()
    println(num)
}

Expectativa vs realidad, nada es lo que parece. No importa si la variable num es o no es anulable «?» ya que estamos usando el operador de aserción !! para asegurarle al compilador que el valor ingresado jamás será null, ¿como funciona? la función readLine() espera a que el usuario ingrese un número y presione Enter, una vez se ingresa el valor, pasa a ser convertido a tipo Int pero en la mitad está el operador !! y se supone que si el valor es null se va a producir un NullPonterExeption. Esta es la expectativa pero la realidad es otra.

La realidad con la que se encontrarán los amantes del NullPointerException es una sola, no se va a producir un NPE de forma natural; pero se puede producir de forma «artificial» y eso lo veremos después con un par de formas. Lo que se produce en este caso es un NumberFormatException como ocurre los anteriores formas.

4) Con el operador !!, complaciendo a los amantes del NullPointerException:

Aquí expongo un par de formas para manejar apropiadamente la excepción NumberFormatException para generar un NullPointerException, complaciendo a los amantes de esta excepción. Son formas relativamente cortas, muy similares a las anteriores pero enfocadas no a la seguridad de referencias nulas sino a los NPEs.

1) Sin el operador as + expresión try {…} catch {…} + throw = un placer inducido para los NPE’s Lovers:

package ingresando.datos

fun main(arg: Array<String>) {
    var num: Int

    print("Ingrese un numero: ")
    num = try {
        readLine()!!.toInt()
    } catch (_: NumberFormatException) {
        throw NullPointerException("NullPointerException: " +
                "no se puede conververtir una referencia null en tipo Int")
    }

    println(num)
}

Esta es una forma con un manejo más adecuado y aunque el NullPointerException sea producido de forma inducida, cumple con las expectativas de los amantes de esta excepción. Anteriormente ya había explicado para qué sirve el operador de aserción !! o cual es la razón por la cual existe en Kotlin.

Bien, en este caso la expresión try {…} contiene a la función readLine() que espera a que el usuario ingrese un numero entero y presione Enter, una vez se ingresa el dato, se evalúa si es un tipo válido para ser convertido y asignado a la variable num; pero en caso contrario se genera un NumberFormatException que es capturado por el bloque catch {…} y luego levanta un bello NPE con un mensaje indicando el porqué ocurre la excepción.

Para generar excepciones de forma intencionalmente, se usa la palabra clave throw seguida por el nombre de la excepción y entre paréntesis un mensaje opcional, como se hizo aquí.




2) Con el operador as + expresión try {…} catch {…} + null explícito = un placer aún más natural NPE’s Lovers:

package ingresando.datos

fun main(arg: Array<String>) {
    var num: Int?

    print("Ingrese un numero: ")
    num = try {
        readLine()!!.toInt()
    } catch (_: NumberFormatException) {
        null
    }

    num!!
    println(num)
}

Al principio del apartado #3 hice una breve descripción sobre este tema junto con un ejemplo ilustrativo mostrando cómo se produce un NullPointerException, pues bien, aqui pondremos en práctica ese ejemplo.

Este programa es el mismo que el anterior, con solo 3 diferencias que se explican aquí; la primera tiene que ver con la variable num, ahora es anulable «?», es decir, ahora se le puede asignar null; la segunda tiene que ver con el bloque catch() {…}, retorna null en lugar de si el valor ingresado es de tipo incompatible y falla la conversión y la tercera diferencia tiene que ver con la expresión num!!, le estamos asegurando al compilador que esta variable no contiene una referencia nula (mentira piadosa); pero, si de lo contrario es nula, entonces genere una excepción NPE. Por eso le llamo «un placer aún más natural NPE’s Lovers».

5) Lectura realmente segura con el operador ?., produciendo un verdadero null en lugar de un NFE:

Después de un largo camino explicando las diferentes formas de ingresar datos por teclado, he dejado para el final la forma más corta, sencilla, concisa y segura para ingresar datos por teclado en Kotlin.

1) Función .toIntOrNull() + operador ?. = forma reducida, concisa y segura:

Breve descripción: La función .toIntOrNull() reduce todo lo que explico en el apartado #2 ingreso de datos forma «casi» segura en una sola línea de código, esta es la forma segura; esta función intentara convertir el dato ingresado a tipo entero o Int pero si dicha conversión falla debido a un tipo incompatible o referencia nula, entonces retorna null.

package ingresando.datos

fun main(arg: Array<String>) {
    var num: Int?

    print("Ingrese un numero: ")
    num = readLine()?.toIntOrNull()

    println(num)
}

En el apartado #2 ingreso de datos forma «casi» segura, en el ítem #2, he tocado el tema de ingreso de datos de forma segura usando una expresión try {…} catch() {…} para manejar el NumberFormatException que se produce al ingresar un dato que no puede ser convertido a tipo Int y asignar null a la variable.

Para poder hacer uso de la función .toIntOrNull() se requiere obligatoriamente que la variable sea de tipo anulable, es decir, debe llevar el signo «?» acompañando el tipo de dato como se observa en la variable num; por ejemplo, Int?. Con esto ya le estamos indicando al compilador que num está preparada para recibir un valor numérico o un null sin problema alguno en tiempo de ejecución, si no lo hacemos, no va a compilar.

Con la función readLine() el programa queda en espera hasta que el usuario ingrese un valor y presione Enter; una vez que es ingresado el valor y se presiona Enter, se intentara convertir dicho valor a un tipo Int como se explicó antes; pero si esta conversión falla entonces retorna null y es asignado a num.

2) Con el operador as + operador ?. = una excepción TypeCastException que se escapa:

Hay una forma de leer datos «casi» segura con la función .toIntOrNull() que podemos manejar directamente con una expresión try {…} catch() {…} como se hizo en el apartado #2, ítem #1 y funciona de la misma forma.

package ingresando.datos

fun main(arg: Array<String>) {
    var num: Int

    print("Ingrese un numero: ")
    num = readLine()?.toIntOrNull() as Int

    println(num)
}

Esta forma de leer datos aunque se vea segura en realidad causara un TypeCastException debido a que no se puede convertir null a un tipo Int, ¿de dónde sale ese null? Sale del intento de conversión del valor leído con la función readLine() usando la función .toIntOrNull() llamada de forma segura con el operador «?.», con lo cual cuando le estamos indicando al compilador que el valor a ingresar puede ser null o referencia nula.

La función .toIntOrNull() evalua si el valor ingresado de forma segura es un valor numérico, entonces retorna dicho valor convertido como un tipo Int; de lo contrario retorna null. La variable num no es anulable, asi que hay convertir el valor ingresado por el usuario a un valor no nulo usando el operador as seguido por el tipo al que será convertido, en este caso es Int y es aquí en donde ocurre la excepción anteriormente mencionada intentar convertir un valor null a un tipo Int u otros tipos.

Manejando la excepción TypeCastException con la expresión try {…} catch() {..}:

package ingresando.datos

fun main(arg: Array<String>) {
    var num: Int

    print("Ingrese un numero: ")
    num = try {
        readLine()?.toIntOrNull() as Int
    } catch (_: TypeCastException) {
        0
    }

    println(num)
}

Al igual que en la versión expuesta en el apartado #2 item #1, aquí también se usa la expresión try .. catch para manejar una excepción que en este caso se genera al tratar de convertir null a un tipo entero Int. Es de recordar que el guión «_» se usa para los casos en donde no necesitaremos un parámetro o un valor, con el evitamos dejar variables innecesarias sueltas.

 6) Para los amantes del NullPointerException con el operador !!, un verdadero NPE directo:

Asi como hay una versión segura para leer datos por teclado, tambien hay una version pura para los amantes del NullPointerException la cual tambien es una versión más reducida y concisa que sus antecesores.

1) Operador de aserción !! + función .toIntOrNull() = un verdadero NullPointerException:

Breve descripción: la función .toIntOrNull() también es compatible con el operador de aserción !!, reduciendo todo lo visto en el apartado #4 a una sola línea de código cumpliendo con la expectativa del programador. En este caso su comportamiento es totalmente lo opuesto a la anterior forma, esta función intentara convertir el valor ingresado a un tipo Int pero si dicha conversión falla debido a un tipo incompatible o referencia nula, se produce un NullPointerException en lugar de un null.

package ingresando.datos

fun main(arg: Array<String>) {
    var num: Int

    print("Ingrese un numero: ")
    num = readLine()!!.toIntOrNull()!!

    println(num)
}

En el apartado #4, he tocado el tema de ingreso de datos no segura usando una expresión try {…} catch() {…} para manejar el NumberFormatException que se produce al ingresar un dato que no puede ser convertido a tipo Int y levantar un NullPointerException en lugar de un null para asignar a una variable.

Para poder usar la función .toIntOrNull() en esta ocasión no es obligatorio que la variable sea anulable ya que es indiferente, siempre va a producir un NullPointerException si la conversión falla; pero dicha función debe ir siempre acompañada con el operador de aserción !! para asegurarle al compilador que el valor que se está convirtiendo no es null (una mentira piadosa). Para el caso de la función readLine(), el operador !! funciona igual, le estamos asegurando al compilador que el valor que vamos a leer no es null o una referencia nula.

Con la función readLine() el programa queda en espera hasta que el usuario ingrese un valor y presione Enter; una vez que es ingresado el valor y se presiona Enter, se intentara convertir dicho valor a un tipo Int como se explicó antes; pero si esta conversión falla, se produce el famoso NullPointerException.

2) Operador ?. + operador !! + función .toIntOrNull() = de null a NullPointerException a un solo !!:

Esta es la forma más curiosa que he visto pues podemos pasar de una lectura segura que genera un null si el valor a convertir o al que se accede, es incompatible o una referencia nula a un NullPointerException en un solo paso con solo agregar el operador de aserción !! junto a la función .toIntOrNull(). Creería que es la forma más sencilla y sutil, vamos a ver.

package ingresando.datos

fun main(arg: Array<String>) {
    var num: Int?

    print("Ingrese un numero: ")
    num = readLine()?.toIntOrNull()!!

    println(num)
}

Y aqui esta, el programa descrito en el apartado #5 pero con una sola diferencia notable, lleva el operador de asercion !! después de la función .toIntOrNull(). Pero, ¿como funciona? Básicamente funciona de la siguiente forma, con el operador «?.» le estamos indicando al compilador que el valor que va a ingresar el usuario tal vez sea null o referencia nula; con esto podemos acceder de forma segura a la función .toIntOrNull() para evaluar si el valor ingresado es numérico o un null. Si es un valor numérico, entonces lo convierte a Int, de lo contrario retornar null; pero, ¿qué ocurre con el operador !!? Con este operador le indicamos al compilador que el valor a convertir no es null, si esto es asi entonces todo saldra bien pero de lo contrario se produce un NullPointerException cumpliendo las expectativas del programador.



Lista de funciones para trabajar con tipos de datos numéricos:

Todo lo que he publicado aqui, también aplica para los diferentes tipos de datos numéricos existentes en el lenguaje de programación Kotlin. Existen varias funciones destinadas para cada uno de los tipos.

Las funciones .toFloat(), .toDouble(), .toLong(), .toShort() y .toByte() cumplen exactamente la misma labor que cumple la función .toInt() convirtiendo los valores a float, double, long, short y byte respectivamente sólo si dicho valor es válido, de lo contrario, se levanta una excepción NumberFormatException.

Mientras tanto, las funciones .toFloatOrNull(), .toDoubleOrNull(), .toLongOrNull(), .toShortOrNull() y .toByteOrNull() cumplen exactamente la misma labor que cumple la función .toIntOrNull() convirtiendo los valores a float, double, long, short y byte respectivamente sólo si dicho valor es válido, de lo contrario, se devuelve null. Al combinar los operadores «?.» y «!!» como se vio en el último caso, también se produce la misma excepción TypeCastException y se maneja de la misma forma.

Espero que les sea de utilidad esta pequeña publicación para comprender mejor el ingreso de datos por teclado en el lenguaje de programación Kotlin y que sea de su agrado para todos los lectores.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *