CONTROL DE FLUJO: ESTRUCTURAS CONDICIONALES

 

Hemos visto hasta ahora que los programas van ejecutando las líneas de código con orden. Sin embargo, hay muchas situaciones en las que es preciso alterar ese orden, o bien puede ocurrir que sea necesario que se efectúen una serie de operaciones que pueden ser distintas en otras circunstancias. Por ejemplo, si el programa pide una clave de acceso, deberá continuar con la ejecución normal en caso de que la clave introducida por el usuario sea correcta, y deberá salir del mismo en caso contrario. Pues bien: para todas estas cuestiones que, por otra parte, son muy frecuentes, tenemos las estructuras de control de flujo.

 

En C# contamos con varias de estas estructuras, así que las iremos explicando con calma una a una, empezando en esta entrega con las estructuras condicionales. De nuevo he de avisar a los programadores de C/C++: el comportamiento de algunas de estas estructuras cambia ligeramente en C#, así que leed esta entrega atentamente, pues de lo contrario podéis encontraros con varios problemas a la hora de usarlas.

 

INSTRUCCIÓN IF...ELSE IF...ELSE

 

Empezaré diciendo que, para los que no sepan inglés, if significa si condicional, es decir, si te portas bien, te compro un helado y te dejo ver la tele. Pues bien, en programación, es más o menos lo mismo. Pongamos un poco de pseudo-código para que los principiantes se vayan haciendo a la idea:

 

Si (te portas bien)

{

    te compro un helado;

    te dejo ver la tele;

}

 

Está bastante claro, ¿verdad? En programación se evalúa a verdadero o falso la condición, que es lo que está dentro de los paréntesis (en negrilla en el pseudo-código). Si esta condición se evalúa a true (verdadero) se ejecutan las líneas del bloque, y si se evalúa a false (falso) no se ejecutan. Vamos a verlo, ahora sí, en C#:

 

if (num==10)

{

    Console.WriteLine("El número es igual a 10");

}

 

En este pequeño ejemplo, se evalúa como verdadero o falso lo que está dentro de los paréntesis, es decir, num==10. Por lo tanto, el operador == retornará true siempre que num valga 10, y false si vale otra cosa. Por cierto, ya que estamos, no confundas el operador de comparación == con el de asignación =. Digo esto porque en otros lenguajes (Visual Basic, por ejemplo) se usa el mismo operador (=) para ambas cosas, y es el compilador el que determina si es de comparación o de asignación según el contexto. No ocurre así en C#: == es de comparación siempre, y = es de asignación siempre. Por lo tanto, qué hubiera sucedido si hubiéramos escrito el ejemplo así:

 

if (num=10) //Incorrecto: se está usando = en lugar de ==

{

    Console.WriteLine("El número es igual a 10");

}

 

Los programadores de C ó C++ dirán que la expresión siempre se evaluaría a true, además de que se asignaría el valor 10 a la variable num. Pero este curso es de C#, así que los programadores de C ó C++ se han vuelto a equivocar: en C# se produciría un error, porque la expresión no se evalúa a true o false, sino que tiene que retornar true o false necesariamente. Es decir, el compilador de C# no evalúa números como valores boolean. Esto hace que sea imposible equivocarse de operador en expresiones de este tipo.

 

Bien, continuemos. Como puedes apreciar, la instrucción if ejecuta el código de su bloque siempre que la expresión que se evalúa retorne true. Sin embargo, no es necesario abrir el bloque en el caso de que solamente haya que ejecutar una sentencia. Así, podríamos haber escrito el ejemplo de esta otra forma:

 

if (num==10)

    Console.WriteLine("El número es igual a 10");

 

O bien:

 

if (num==10) Console.WriteLine("El número es igual a 10");

 

En cualquiera de los dos casos, hubiera funcionado igual porque, recordemos, el compilador entiende que todo es la misma instrucción mientras no encuentre un punto y coma o una llave de apertura de bloque.

 

También puede ocurrir que tengamos que ejecutar una serie de acciones si se da una condición y otras acciones en caso de que esa condición no se dé. Pues bien, para eso tenemos la instrucción else. Volviendo a la interpretación lingüística para favorecer todo esto a los principiantes, sería como un "de lo contrario", es decir, si te portas bien, te compro un helado y te dejo ver la tele; de lo contrario, te castigo en tu cuarto y te quedas sin cenar. ¿Quieres un poquito de pseudo-código para ver esto? Venga, aquí lo tienes:

 

Si (te portas bien)

{

    te compro un helado;

    te dejo ver la tele;

}

de lo contrario

{

    te castigo en tu cuarto;

    te quedas sin cenar;

}

 

Alguno debe estar partiéndose de risa (¡¡¡VAYA UN PSEUDO-CÓDIGO!!!). Pero la cuestión es que se entiende perfectamente, así que este pseudo-código es... bueno... dejémoslo en "estupendo" (ufff, casi se me escapa...). Bien, veamos algo de esto, ahora sí, en C#:

 

if (num==10)

{

    Console.WriteLine("El número es igual a 10");

}

else

{

    Console.WriteLine("El número no es igual a 10");

}

 

Esto es muy fácil, ¿no te parece? Si se cumple la condición, se ejecuta el código del bloque if, y si no se cumple se ejecuta el código del bloque else. Del mismo modo, si el bloque consta de una única línea, podemos ahorrarnos las llaves, así:

 

if (num==10)

    Console.WriteLine("El número es igual a 10");

else

    Console.WriteLine("El número no es igual a 10");

 

O bien:

 

if (num==10) Console.WriteLine("El número es igual a 10");

else Console.WriteLine("El número no es igual a 10");

 

Como veis, sucede lo mismo que cuando nos ahorramos las llaves anteriormente. Ahora bien, recordad que si no se ponen las llaves, tanto if como else afectan únicamente a la primera línea que se encuentre tras la condición. Vamos a ver estos dos ejemplos, para que os quede esto bien claro:

 

if (num==10)

    Console.WriteLine("El número es igual a 10");

    Console.WriteLine("He dicho");

else // Incorrecto: el compilador no sabe a qué if se refiere

    Console.WriteLine("El número no es igual a 10");

 

------------------------------------------------------

 

if (num==10)

    Console.WriteLine("El número es igual a 10");

else

    Console.WriteLine("El número no es igual a 10");

    Console.WriteLine("He dicho"); // Esta línea se ejecuta siempre

 

En el primer caso se produciría un error en tiempo de compilación, porque el compilador no sabría enlazar el else con el if ya que, al no haber llaves de bloque, da por terminada la influencia de este después de la primera línea que está tras él. En el segundo caso no se produciría un error, pero la última línea (la que escribe "He dicho" en la consola) se ejecutaría siempre, independientemente de si se cumple o no la condición, pues no hay llaves dentro del bloque else, por lo cual este afecta solamente a la línea que le sigue. Para que else afectara a estas dos líneas habría que haber escrito las llaves de bloque:

 

if (num==10)

    Console.WriteLine("El número es igual a 10");

else

{

    Console.WriteLine("El número no es igual a 10");

    Console.WriteLine("He dicho"); // Esta línea se ejecuta siempre

}

 

Ahora sí, en caso de cumplirse la condición se ejecutaría la línea que hay detrás del if, y en caso contrario se ejecutarían las dos líneas escritas dentro del bloque else.

 

También podría suceder que hubiera que enlazar varios if con varios else. Volvamos con otro ejemplo para ver si nos entendemos: si compras el libro te regalo el separador, de lo contrario, si compras la pluma te regalo el cargador, de lo contrario, si compras el cuaderno te regalo un llavero, y, de lo contario, no te regalo nada. Veamos de nuevo el pseudo-código de esto:

 

Si (compras el libro)

{

    te regalo el separador;

}

de lo contrario si (compras la pluma)

{

    te regalo el cargador;

}

de lo contrario si (compras el cuaderno)

{

    te regalo un llavero;

}

de lo contrario

{

    no te regalo nada;

}

 

O sea, queda claro lo que sucede: si se cumple alguna de las condiciones se producen una serie de consecuencias, y si no se cumple ninguna de las tres condiciones la consecuencia es que no hay regalo alguno. En este caso, sin embargo, hay que tomarse el pseudo-código al pie de la letra para que la relación con la programación sea exacta: ¿qué ocurre si se dan dos o las tres condiciones? Pues en la vida real, probablemente, te llevarías varios regalos, pero si lo tomamos al pie de la letra, solamente te podrías llevar el primer regalo en el que se cumpliese la condición, pues las demás están precedidas por la expresión "de lo contrario", que es, en sí misma, otra condición, es decir, si se cumple una las demás ya no se contemplan aunque también se cumplan. Esto es exactamente lo que ocurre en programación: el compilador no sigue analizando las demás condiciones en el momento en el que encuentre una que retorna true. Veamos algo de esto en C#:

 

if (num==10)

{

    Console.WriteLine("El número es igual a 10");

}

else if (num>5)

{

    Console.WriteLine("El número es mayor que 5");

}

else if (num>15)

{

    Console.WriteLine("El número es mayor que 15");

}

else

{

    Console.WriteLine("El número no es 10 ni mayor que 5");

}

 

Bien, examinemos las diferentes posibilidades. Si num vale 10, se ejecutará el bloque del primer if, por lo que la salida en la consola será "El número es igual a 10". Sin embargo, a pesar de que 10 es también mayor que cinco, no se ejecutará el bloque del primer else if, pues ni siquiera se llega a comprobar dado que ya se ha cumplido una condición en la estructura. Si num vale un número mayor que 5, menor que 15 y distinto de 10, o sea, 9, por ejemplo, se ejecuta el bloque del primer else if saliendo "el número es mayor que 5" en la consola. Ahora bien: ¿qué sucede si el número es mayor que 15? Pues sucede exactamente lo mismo, ya que, si es mayor que 15 también es mayor que 5, de modo que se ejecuta el bloque del primer else if y después se dejan de comprobar el resto de las condiciones. Por lo tanto, en este ejemplo, el bloque del segundo else if no se ejecutaría en ningún caso. Por último, el bloque del else se ejecutará siempre que num valga 5 o menos de 5, pues es el único caso en el que no se cumple ninguna de las condiciones anteriores.

 

Por otra parte, las condiciones que se evalúen en un if o en un else if no tienen por qué ser tan sencillas. Recuerda que en C# estas expresiones (las condiciones) han de retornar true o false necesariamente, por lo que podemos usar y combinar todo aquello que pueda retornar true o false, como variables de tipo bool, métodos o propiedades que retornen un tipo bool, condiciones simples o compuestas mediante los operadores lógicos && (AND lógico) || (OR lógico) y ! (NOT lógico), o incluso mezclar unas con otras. por ejemplo:

 

if ((Ficheros.Existe(Archivo) || Crear) && EspacioDisco>1000 )

{

    Console.WriteLine("Los datos se guardarán en el archivo");

}

else

{

    Console.WriteLine("El archivo no existe y no se puede crear o bien no hay espacio");

}

 

En este ejemplo, "Existe" es un método de la clase Ficheros que retorna true o false, "Crear" es una variable bool y "EspacioDisco" es una variable de tipo uint. Como ves, en una sola condición están combinados varios elementos que pueden retornar valores boolean. Se encierra entre paréntesis la expresión Ficheros.Existe(Archivo) || Crear porque necesitamos que se evalúe todo esto primero para después comparar con la otra expresión, ya que el operador && se ejecuta antes que el ||. Así esta expresión retornará true en caso de que el método Existe devuelva true, la variable crear valga true o sucedan ambas cosas. Posteriormente se establece el resultado de la otra expresión, es decir EspacioDisco>1000, y después se comparan los dos resultados con el operador &&, obteniendo el resultado final, que será true si ambos operandos valen true, y false si alguno de ellos o los dos valen false. Así, si el archivo existe o bien si se quiere crear en caso de que no exista se guardarán los datos si, además, hay espacio suficiente, y si, por el contrario, el archivo no existe y no se quiere crear o bien si no hay espacio, los datos no se guardarán.

 

Antes de terminar con la instrucción if, quiero puntualizar una cosa. Cuando queramos hacer una simple asignación a una variable dependiendo de un determinado valor, podemos hacerlo con if o bien podemos usar el operador Question (?:) (revisa cómo funciona este operador en la entrega 4), que, a mi entender, es más cómodo. Por ejemplo, tenemos un método en el que necesitamos saber, de entre dos números, cuál es el mayor y cuál el menor. Podemos hacerlo con if, así:

 

if (num1>num2)

{

    mayor=num1;

    menor=num2;

}

else

{

    mayor=num2;

    menor=num1

}

Correcto, pero también podemos hacer la lectura del código algo más cómoda si usamos el operador Question, así:

 

mayor=(num1>num2) ? num1: num2;

menor=(num1>num2) ? num2: num1;

 

Bien, creo que esto ya está suficientemente explicado. Entiendo que para los que se estén iniciando, esto puede resultar todavía un poco confuso, pero tranquilos, hay un modo infalible para hacerse con todo esto: práctica. Cuanto más lo uséis más seguros os hallaréis con esto, y como en cualquier programa el uso de if es el pan nuestro de cada día, os haréis con ello muy pronto.

 

INSTRUCCIÓN SWITCH

 

Una instrucción switch funciona de un modo muy similar a una construcción con if...else if... else. Sin embargo, hay un diferencia que es fundamental: mientras en las construcciones if...else if... else las condiciones pueden ser distintas en cada uno de los if ... else if, en un switch se evalúa siempre la misma expresión, comprobando todos los posibles resultados que esta pueda retornar. Un switch equivaldría a comprobar las diferentes situaciones que se pueden dar con respecto a una misma cosa. Por ejemplo, si te compras un coche y tienes varias opciones de financiación: En caso de usar la primera opción te descuento un 10 por ciento, en caso de usar la segunda opción te descuento un cinco por ciento, en caso de usar la tercera opción te descuento un dos por ciento, y en cualquier otro caso no te descuento nada. Como ves, se comprueba siempre el valor de un solo elemento, que en este caso sería la opción. Pongamos un poco de pseudo-código otra vez:

 

comprobemos (opcion)

{

    en caso de 1:

        te descuento un 10%;

        Nada más;

    en caso de 2:

        te descuento un 5%;

        Nada más;

    en caso de 3:

        te descuento un 2%;

        Nada más;

    en otro caso:

        no te descuento nada;

        Nada más;

}

 

Sí, ya sé que eso ni es pseudo-código ni es "na", pero lo que pretendo es que se entienda con facilidad, y creo que así se entiende mucho mejor que siendo estrictos, así que no seáis tan criticones, hombre...

 

Bueno, como veis, se comprueba siempre lo que vale "opcion"; si vale 1 sucede el primer caso, si vale 2 el segundo, si vale 3 el tercero, y si vale cualquier otra cosa sucede el último caso. Vamos a verlo en C#:

 

switch (opcion)

{

    case 1:

        descuento=10;

        break;

    case 2:

        descuento=5;

        break;

    case 3:

        descuento=2;

        break;

    default:

        descuento=0;

        break;

}

 

Hay algunas cosas importantes en las que quiero que te fijes especialmente: solamente se establece un bloque para la instrucción "switch", pero ninguno de los "case" abre ningún bloque (tampoco lo hace "default"). Una vez que se terminan las instrucciones para cada caso hay que poner "break" para que el compilador salga del switch (esto lo digo especialmente para los programadores de Visual Basic, ya que en VB no había que poner nada al final de cada Case en un Select Case). ¿Y qué ocurre si no se cierra el "case" con un "break"? Bien, pueden ocurrir dos cosas (los programadores de C/C++, por favor, que no se precipiten...). Veamos: si queremos que el programa haga las mismas cosas en distintos casos, habrá que poner todos estos casos y no cerrarlos con break. Por ejemplo, si el descuento es 5 tanto para la segunda como para la tercera opción, habría que hacerlo así:

 

switch (opcion)

{

    case 1:

        descuento=10;

        break;

    case 2:

    case 3:

        descuento=5;

        break;

    default:

        descuento=0;

        break;

}

 

Así, en caso de que opcion valiera 2 ó tres, el descuento sería del 5%, pues, si opcion vale 2, el flujo del programa entraría por case 2 y continuaría por case 3 ejecutando el código de este último al no haber cerrado el case 2 con break, y si opción vale 3 entraría por case 3 ejecutando, por lo tanto, el mismo código. Sin embargo, y aquí quiero que se fijen especialmente los programadores de C/C++, no hubiera sido válido establecer acciones para el case 2 sin cerrarlo con break. Vamos a ponerlo primero, y luego lo explico más detenidamente:

 

switch (opcion)

{

    case 1:

        descuento=10;

        break;

    case 2:

        regalo="Cargador de CD"

    case 3:

        descuento=5;

        break;

    default:

        descuento=0;

        break;

}

 

Voy a examinar esto como si estuviera escrito en C/C++: si opcion vale 2, el flujo entraría por case 2, estableciendo el regalo y seguiría por case 3 estableciendo también el descuento, dado que case 2 no ha sido cerrado con un break. Si opción vale 3 entraría solamente por case 3, estableciendo únicamente el descuento y no el regalo. Ciertamente, esto era muy cómodo si querías definir acciones específicas para un valor determinado y añadirles otras que fueran comunes para varios valores. Sin embargo, esto no se puede hacer en C#, dado que el compilador avisaría de un error, diciendo que hay que cerrar case 2. ¿Por qué? Pues bien, a pesar de la comodidad de esta construcción en C/C++ para determinadas circunstancias, lo cierto es que lo más común es que se omita el break por error que por intención, lo cual provoca muchos fallos que serían muy difíciles de detectar. Por este motivo, los diseñadores del lenguaje C# decidieron que el riesgo no merecía la pena, ya que esta funcionalidad se puede conseguir fácilmente con un simple if, así:

 

switch (opcion)

{

    case 1:

        descuento=10;

        break;

    case 2:

    case 3:

        if (opcion==2) regalo="Cargador de CD";

        descuento=5;

        break;

    default:

        descuento=0;

        break;

}

 

Es cierto que esto es un poco más incómodo, pero también es cierto que es mucho más seguro, ya que se evitan los problemas que sobrevenían en C/C++ cuando te olvidabas de poner un break.

 

Para terminar, la expresión que se ha de comprobar en un switch ha de ser, necesarimente, compatible con los tipos sbyte, byte, short, ushort, int, uint, long, ulong, char o string (sí, sí, para los programadores de Java, string también). Y recordad: al decir compatible quiero decir que sea de uno de esos tipos o que se pueda convertir a uno de ellos.

 

Para esta entrega tienes un ejemplo del switch (sigue este vínculo para bajártelo), pero no he diseñado ninguno sobre if, ya que lo vamos a usar constantemente a partir de ahora. Además, no quiero daros todo tan hecho, porque corremos el riesgo de que aprendáis mucha teoría pero luego no seáis capaces de llevarla a la práctica. Ciertamente, había pensado en modificar la clase Cuadrado (sí, la del ejercicio que propuse en la entrega anterior). Recuerda que uno de los constructores de esta clase, concretamente el que construía un cuadrado a partir de los vértices uno y tres, era muy inseguro, puesto que si dábamos componentes x menores para el vértice 3 que para el vértice 1, luego no nos coincidía la longitud del lado, además de que los vértices se colocaban al revés. Sin embargo he decidido complicaros un poco la vida, y poner esto como un ejercicio en vez de un ejemplo. Sí, sí, lo que lees, un ejercicio. Así matamos dos pájaros de un tiro: te devanas los sesos para hacerlo y, si no te sale, lo puedes ver hecho cuando llegue la próxima entrega. Aquí va el enunciado:

 

EJERCICIO 2

 

Veamos, uno de los constructores de la clase Cuadrado que hicimos en el ejercicio 1 era muy inseguro. Vamos a verlo gráficamente, para que todos nos hagamos una idea exacta de lo que estoy diciendo. Los vértices del cuadrado han de corresponderse exactamente igual a como están en esta imagen:

 

 

Ahora bien, teníamos un problema si, por ejemplo, pasábamos 6, 6 a las coordenadas para el vértice1 y 1,1 a las coordenadas para el vértice3, y es que no se calculaba bien la longitud del lado, además de que los vértices se nos cambiaban de sitio (se puede comprobar en la ejecución del ejemplo).

 

Pues bien, lo que hay que arreglar es esto, precisamente. El constructor debe aceptar dos argumentos del tipo Punto sin presuponer que alguno de ellos corresponde a algún vértice determinado, y después hacer las comprobaciones necesarias para calcular el vértice1 y la longitud del lado correctos. Ojo, que es un poco más complicado de lo que puede parecer a simple vista. Hala, al lío... Por cierto, no hay pistas, lo siento...

.