Estructuras de control

if, else, while, do-while, for...

Estructuras de control


if, else, while, do-while, for...

« Volver al inicio

Operaciones relacionales

Hemos visto ya que en C#, al igual que en muchos lenguajes de programación, se puede trabajar con operadores matemáticos. Ahora toca ver otros tipos de operadores diferentes que no realizan operaciones aritméticas. Esto es importante conocerlo antes de empezar a aprender las instrucciones más básicas de control que existen en C#.

También recordarás haber visto en el colegio que cuando un número es mayor que otro, utilizamos el símbolo >, o bien el símbolo < si el miembro de la izquierda es menor que el de la derecha.

1 < 2
6 > 3
203 < 406
110 = 110

En programación estos símbolos se conocen como operadores relacionales y es muy importante tenerlos en cuenta, sobre todo a la hora de evaluar condiciones en las instrucciones de control que veremos más adelante.

Estos son los operadores relacionales que soporta C#:

OperadorOperación
<Menor que
>Mayor que
<=Menor o igual que
>=Mayor o igual que
==Igual a
!=No es igual a

A la hora de realizar una operación relacional, esta siempre nos devolverá un valor lógico: verdadero o falso. En C# esto se traduce a true si es cierto o false si es falso.

Vamos a ver unos cuantos ejemplos:

OperaciónResultado
1 < 2true
3 > 0true
4 >= 2true
8 <= 2false
5 >= 5true
8 < 2false
10 == 10true
7 != 7false
3 != 2true

El resultado se devuelve en un tipo de datos que no hemos visto aún que se llama bool, donde sólo hay dos valores posibles: verdadero o falso.

Es importante no confundir el operador relacional de doble igual (==) con el símbolo de asignación (=). Si nos confundimos, el programa nos puede dar error u otros resultados no esperados.

int numero;

numero == 1; // Dará error aquí, porque estamos comparando
             // una variable sin asignar con un número.

El uso correcto sería:

int numero;

numero = 1;

Asimismo, si nos confundimos de símbolo en un if:

int numero = 1;

if(numero = 1) // Nos puede dar error aquí, porque en lugar de comparar,
               // estamos asignando a numero un valor.
  System.Console.WriteLine("El número es uno.");

Operaciones lógicas

Hemos visto los operadores aritméticos, los operadores relacionales, y ahora nos queda conocer los operadores lógicos. Los operadores lógicos sirven para juntar varias condiciones a la vez. Estas se pueden juntar de varias maneras, en función de lo que necesitemos programar.

Como se trata de algo que no se da antes de los ciclos de grado superior (quizá sí en grado medio), vamos a entender primero cómo funcionan los operadores lógicos más básicos. De hecho, aparte de los que se verán aquí, hay más, pero de momento no las hemos visto en clase, por lo que no voy a anotarlas por aquí todavía para no liar la perdiz.

AND (&&)

Por ejemplo, imaginémonos que queremos ir al cine, pero para eso necesitamos haber comprado la entrada y no tiene que llover. Es decir, se tiene que cumplir que haga sol y que tenga la entrada si queremos ir al cine. Si no se cumple cualquiera de esas dos condiciones, no podremos ir al cine (con posibilidad de devolver la entrada).

Condición 1Condición 2Resultado
Tengo la entradaHace solVoy al cine
No tengo la entradaHace solNo voy al cine
Tengo la entradaNo hace solNo voy al cine
No tengo la entradaNo hace solNo voy al cine

 

Si esto lo traducimos a variables y operadores lógicos de C#:

Condición 1Condición 2Voy al cine?
!entrada!hace_solfalse
!entradahace_solfalse
entrada!hace_solfalse
entradahace_soltrue

 

Aquí estamos aplicando una operación lógica llamada AND, que se traduce como Y, y en C# se representa con && (doble ampersand). Para que sea cierta una premisa con el operador &&, ambas condiciones tienen que ser ciertas y ninguna puede quedar como falsa.

Vamos a traducir el caso anterior en lo que se llama tabla de verdad:

aba && b
falsefalsefalse
falsetruefalse
truefalsefalse
truetruetrue

 

OR (||)

Vamos a ver otro caso. Al final vamos al cine, pero resulta que existe una promoción en el que te pueden hacer descuento si tienes hasta 25 años o si eres estudiante. Puede cumplirse cualquiera de las dos condiciones para que se te aplique el descuento, pero no si eres mayor de 25 y además no eres estudiante.

Condición 1Condición 2Resultado
No tengo <= 25 añosNo soy estudianteNo tengo descuento
No tengo <= 25 añosSoy estudianteTengo descuento
Tengo <= 25 añosNo soy estudianteTengo descuento
Tengo <= 25 añosSoy estudianteTengo descuento

 

Condición 1Condición 2Voy al cine?
!hasta_25!estudiantefalse
!hasta_25estudiantetrue
hasta_25estudiantetrue
hasta_25estudiantetrue

 

Aquí estamos aplicando una operación lógica llamada OR, que se traduce como O, y en C# se representa con || (doble tubería). Para que sea cierta una premisa con dicho operador, se tiene que cumplir al menos una de las condiciones. Sólo si ambas no son ciertas, el resultado será falso.

aba || b
falsefalsefalse
falsetruetrue
truefalsetrue
truetruetrue

 

NOT (!)

Habréis observado que estamos usando la exclamación (!) para marcar las condiciones negativas. En C# resulta que ! es un operador lógico de negación (NOT) y ya la hemos observado en el apartado anterior con el operador relacional !=, que es equivalente a negar el operador ==:

a != b

// Es igual a:

!(a == b)

Sin embargo, para estos casos es preferible emplear la primera forma a != b siempre que sea posible para facilitar la legibilidad y para gastar menos caracteres.

La tabla de verdad del operador ! sería:

a!a
falsetrue
truefalse

 

También podemos negar dos, tres o infinitas veces una variable o condición.

a!a!!a!!!a
falsetruefalsetrue
truefalsetruefalse
!a

// Es igual a:
!!!a               //3 exclamaciones

// O igual a:
!!!!!!!!!!!!!!!!!a //17 exclamaciones

Es algo que en la práctica se tiene que evitar, pero sin embargo tenemos que tener en cuenta a la hora de solucionar problemas. Al final cuando una variable true se niega un número par de veces, siempre resulta true, mientras que un número impar de ! equivale a negarlo una sola vez.

Condicionales

Vistos ya los operadores relacionales y los operadores lógicos fundamentales, estamos ya listos para empezar a trabajar con estructuras de control. En esta sección en particular le daremos una vuelta a las instrucciones condicionales if y switch.

if

Se traduce como “si” (sin acento, condicional, no “sí” de afirmación). Tiene la siguiente estructura:

if(condición) {
	código;
}

Traducido a nuestro lenguaje, esto sería:

//Empiezo aquí
si(se cumple esta condición) {
	ejecuto
	estas
	líneas
	de código
}
//Si no se cumple, continúo aquí

Podemos escribir los bloques if de una forma más abreviada si dentro queremos ejecutar una sola línea de código, aunque es menos recomendable si estamos empezando a programar:

if(condición)
	código;  // Podemos prescindir de llaves si nuestro
	         // código dentro del if sólo ocupará una línea.

¿Por qué la forma abreviada es menos recomendable para principiantes? Vamos a ver este ejemplo:

int resultado = 2 + 3;

if(resultado == 3)
	Console.WriteLine("Resultado de la suma:");
	Console.WriteLine("El resultado es tres");

En este ejemplo muy simple, nuestra intención es imprimir por pantalla el resultado de la suma sólo si nos da 3. Si no, no queremos imprimir nada.

Sin embargo, el resultado de ejecutar este código no será el esperado y nos imprimirá “El resultado es tres” aunque no sea cierto. Esto se debe a que con if o con cualquier otra instrucción de bloque sin llaves, el compilador sólo incluirá la línea inmediata dentro de su bloque. Esto es, que la línea que imprime “Resultado de la suma:” se imprimiría en caso de que el resultado de la suma fuera 3, y la siguiente línea se imprimiría independientemente del resultado de la suma porque está fuera del if.

Para verlo más claro, el código equivalente al ejemplo anterior sería el siguiente:

int resultado = 2 + 3;

if(resultado == 3) {
    Console.WriteLine("Resultado de la suma:");  // Esta línea es inmediata, así que entra en el if
}
Console.WriteLine("El resultado es tres");	// Esta línea queda fuera del if

Al loro, porque esto suele ser una pregunta trampa en los exámenes de tipo test.

if-else

Con else podemos añadir un “anti-if”, es decir, un bloque de código que se ejecutará sólo si no se cumple lo que había en el if:

if(condición) {
	código si condición es verdadero;
}
else {
	código si condición es negativa;
}

… que traducido en nuestro lenguaje, sería algo así:

si(se cumple esta condición) {
	ejecuto
	estas
	líneas
	de código
}
si no {
	ejecuto
	estas
	líneas
	de aquí
	en su lugar
}

Además, podemos encadenar varios if usando else if de la siguiente forma:

if(condición1) {
	código;
}
else if(condición2) {
	otro código;
}
else if(condición3) {
	otro código;
}
else {
	código si ninguno se cumple ninguna de las anteriores;
}

Es importante respetar el siguiente orden: if (el que lo comienza todo) → else ifelse, pudiendo agregar tantos else if como necesitemos siempre y cuando estén entre el bloque if y el bloque else. No podemos empezar un bloque else o else if sin haber antes un bloque if.

Podemos entender mejor cómo funciona if e if-else si emulamos el comportamiento interno del programa marcando qué hace el programa paso por paso: Simulación de if-else Si nos fijamos bien, no entramos en ningún sub-bloque de if hasta llegar al else if de la línea 14, ya que es el único donde se cumple la condición. Así que, la salida por consola de ese programa será: Tres.

También es muy importante saber usar correctamente las estructuras if, else if y else en todo momento, ya que podemos crear con ello estructuras anidadas con diferente comportamiento, pudiendo comportar a bugs si no controlamos bien la lógica de nuestro código:

if(condición1) {
	if(condición2) {	// si condición1 y condición2 se cumplen
		código;
	}
	else {			// si condición1 se cumple pero no condición2
		otro código;
	}
}
else if(condición3) {	// ni condición1 ni condición2 se cumplen, pero sí condición3
	más código;
}
else {
	otro código más;	// si no se cumple ninguna de las condiciones anteriores
}

if(condición4) {	// se cumple condición4 independientemente de si se cumplen o no las otras 3
	...
}

También es sumamente importante saber que el compilador se saldrá de un bloque if - else if - else después de entrar en uno o ninguno de los bloques. Es decir, si se cumple la condición de un if, se ignorará lo que haya en los else if o else que le sucedan:

int numero = 2;

if(numero == 1) {
    Console.WriteLine("El resultado es 1");
}
else if(numero == 2) {
    Console.WriteLine("El resultado es 2");
}
else if(numero == 3) {
    Console.WriteLine("El resultado es 3");
}
else {
    Console.WriteLine("El resultado es otro");
}

Console.WriteLine("Fin del if");

En este ejemplo, primero evaluamos if(numero == 1). Como no es cierto, pasamos al siguiente else if(numero == 2). Como este sí que es cierto, imprimimos la línea "El resultado es 2" y salimos del if. Su salida en consola sería:

El resultado es 2
Fin del if

Insisto, es súper importante dominar el comportamiento de los if antes de seguir entendiendo el comportamiento del resto de estructuras de control, ya que es la fuente de bugs más común entre los programadores noveles y a la vez la que más quebraderos de cabeza suelen dar a la hora de depurar la lógica de estos programas, sobre todo cuando empecemos a engordar los if con múltiples condiciones.

switch

El código del gif anterior se podría reescribir perfectamente con switch:

using System;

class a {
	static void Main() {
		
		int num = 3;
		
		switch(num) {		
		case 1:
			Console.WriteLine("Uno");
			break;
		case 2:
			Console.WriteLine("Dos");
			break;
		case 3:
			Console.WriteLine("Tres");
			break;
		}
		//Fin del programa
	}
}

La estructura básica de un switch sería:

switch(variable) {
	case valor:
		código;
		break;
	case otro_valor:
		otro_código;
		break;
	default:	// equivale a else
		otro_código;
		break;
}

Y en nuestro idioma:

//Empiezo aquí
dada(esta_variable) {
	en caso de que valga X:
		ejecutar este código;
		interrumpir;
	en caso de que valga Y:
		ejecutar este otro código;
		interrumpir;
	si no se da ningún caso:
		finalmente ejecutar este código;
		interrumpir;
}
//Al final de switch o a la orden de interrumpir, continúo por aquí

Lo que hace break, que hemos traducido como interrumpir, es salirse de todo el bloque switch cuando hayamos ejecutado el código perteneciente a cada caso. Si no colocamos el break, el programa seguirá leyendo las instrucciones del siguiente case hasta llegar al siguiente break o hasta que se llegue al final del bloque switch. Tenemos que pensar que el break dentro del switch actúa como el cierre de llaves en un if, else if o else.

Vamos a ver cómo se comporta un switch de forma más gráfica con el mismo ejemplo de antes (ignorad la mala indentación del código): Simulación de switch

Aquí nos encontramos con que switch seleccionará automáticamente el primer caso que tenga el mismo valor que tiene la variable dentro de los paréntesis (variable num). Si añadimos una etiqueta default dentro del switch, actuará exactamente igual que un else a secas: se ejecutará en caso de que num no tenga ninguno de los demás valores.

Como num vale 3 en el ejemplo, se ejecutará el bloque de código que hay dentro de case 3: y la orden break hará que salte hasta el final del bloque switch, de forma similar a cómo saltamos hacia afuera de un else if una vez que terminemos de ejecutar lo que estaba dentro.

Bucles

¿Qué es un bucle? Vamos a recordar este gag chanante:

— Correcto, me llaman Bucle.
— ¿Y nos puede explicar por qué?
— Pronto se dará cuenta.
— ¿Me daré cuenta de que le llaman Bucle?
— Correcto, me llaman Bucle.
— ¿Y nos puede explicar por qué?
— Pronto se dará cuenta.

Bucle es, por ejemplo, ponerte tu playlist favorita de Spotify una y otra vez. Cada vez que se repita esa playlist entera, estás haciendo una iteración. Te la has puesto tantas veces en bucle, que cuando termina una canción, sabes cuál será la siguiente que viene a continuación.

Pero un bucle no tiene por qué ser algo que se repita de forma indefinida. También es un bucle querer ver un vídeo en YouTube, y que antes de ello te obligue a ver un anuncio. Y luego otro anuncio. Y después, un tercer anuncio, ya llevamos tres iteraciones. Pero ahora puedes salirte del bucle al pulsar “Saltar” para así por fin poder ver el vídeo que deseabas ver hace cinco minutos y poner fin a ese bucle de anuncios.

Con esto debe quedarnos claro que existen los bucles definidos e indefinidos, es decir, bucles con infinitas iteraciones y bucles con un número finito de iteraciones.

Los bucles van siempre acompañados de una condición lógica que confirme si se va a realizar la siguiente iteración o no. Si escribimos una condición lógica que siempre sea cierta, nos quedará un bucle indefinido.

En esta sección vamos a repasar los distintos bucles que existen:

while

La estructura básica de un while (mientras) es la siguiente:

while(condición) {
	sentencia1;
	sentencia2;
	sentencia3;
	...
	sentenciaN;
}

O bien la siguiente si queremos definirla en una línea (sin llaves):

while(condición)
	sentencia;

Hablando en cristiano:

//Empiezo aquí
mientras que se cumpla (esta condición) {
	repetiré este bloque de código;
}
// Si no se cumple, continúo aquí

Cuando se ejecute un bucle while, lo que hará será:

  1. Evaluar que la condición lógica sea correcta (el resultado sea true).
  2. Si lo primero se cumple, ejecutar cada una de las líneas dentro del bucle ordenadamente. Si no se cumple, saltar al paso 4.
  3. Repetir el paso 1.
  4. Fin del bucle.

Lo que queremos decir con el punto 1 y 2 es que la expresión lógica que escribas en condición se comparará con true (verdadero). Si tu expresión lógica es igual a true, el paso 2 hará que entremos en el bucle. Si en cambio la condición es falsa (false), el paso 2 hará que el programa se salte el código de todo el bloque del bucle y seguirá ejecutándose por debajo de ese bloque while.

Visto de una forma gráfica, así funciona internamente un bucle while: Simulación de while

Si la condición se evalúa siempre como verdadera y no se hace nada para gestionar su salida, estaremos creando un bucle infinito. Por ejemplo, si creamos un bucle while(true), internamente se evaluará como: mientras que (true) sea igual a true, repetir el código. Como no hay condición de salida y la condición es una constante, creamos un bucle indefinido.

while(true) {
	Console.WriteLine("Hola");
}

La salida por consola de este comando, por tanto, será:

Hola
Hola
Hola
Hola
Hola
Hola
Hola
Hola

… y así continuamente hasta que matemos el proceso o se apague el ordenador.

do-while

Un do-while (hacer-mientras) tiene esta estructura:

do {
	sentencia1;
	sentencia2;
	sentencia3;
	...
	sentenciaN;
}
while(condición);

Traducido a pseudocódigo:

hacer {
	este bloque de código;
}
y repetir mientras se cumpla (esta condición);
  1. Se ejecutará primero el código que hay en el bloque.
  2. Cuando finalice el bloque, se evaluará si se sigue cumpliendo la condición.
  3. Si se cumple la condición anterior (resulta true), vuelve al paso 1. Si no, se sale del bucle.

Si queremos visualizar los pasos de ejecución para ver cómo se comporta este bucle: Simulación de do-while

¿Observamos las diferencias entre while y do-while?

  • while evalúa primero la condición y luego ejecuta el bloque de código.
    • Esto significa que el bloque de código puede que no se llegue a ejecutar nunca (si no se cumple la condición).
  • do-while ejecuta primero el bloque de código y luego evalúa la condición.
    • Esto significa que el bloque de código se ejecutará al menos una vez, pues estamos indicando al compilador que nos ejecute primero el código, pase lo que pase.

El do-while nos puede servir por ejemplo para pedirle una contraseña a un usuario, puesto que primero queremos primero imprimir un texto que pida la contraseña al usuario y que a continuación la valide:

 1using System;
 2
 3class a {
 4  static void Main() {
 5    string  password, // Contraseña introducida
 6            passCorrecta = "déjame entrar"; // Contraseña correcta
 7		
 8    do {
 9      // Pregunta al menos una vez por la contraseña.
10      Console.Write("Introduce la contraseña: ");
11      password = Console.ReadLine();
12      
13      // Si la contraseña no es correcta, muestra error.
14      if(password != passCorrecta) {
15      Console.WriteLine("Acceso denegado");
16      }
17    }
18    // Si la contraseña NO es correcta, seguir preguntando
19    //(hasta que se introduzca la correcta).
20    while(password != passCorrecta); 
21    
22    // El programa seguirá por aquí si se ha introducido bien.
23    Console.Write("Acceso concedido");
24  }
25}

Salida:

Introduce la contraseña: 1234
Acceso denegado
Introduce la contraseña: qazwsx123
Acceso denegado
Introduce la contraseña: déjame entrar
Acceso concedido

Si esto lo transformamos a while, vamos a tener que hacer algunos cambios en el código anterior:

  • Tenemos que asignar la variable password antes del bucle porque si la dejamos sin asignar, a la hora de evaluarse al comienzo del bucle, el compilador nos devolverá error. Con dejarlo preasignado con una cadena vacía, bastaría.
 1using System;
 2
 3class a {
 4  static void Main() {
 5    string  password = "", // Contraseña introducida
 6    passCorrecta = "déjame entrar"; // Contraseña correcta
 7    
 8    // Primero evalúa si la contraseña es correcta, luego hace el código de dentro
 9    while (password != passCorrecta) {
10      Console.Write("Introduce la contraseña: ");
11      password = Console.ReadLine();
12      
13      if(password != passCorrecta) {
14        Console.WriteLine("Acceso denegado");
15      }
16    }
17    
18    Console.Write("Acceso concedido");
19  }
20}

for

Si queremos que una parte del programa se repita X veces, el bucle for será una buena herramienta para ello.

for tiene una estructura un poco más compleja que los anteriores tipos de bucle. A diferencia de while y do-while, for tiene tres campos. Se estructura así:

for(campo1; campo2; campo3) {
	código;
}
  • campo1 será un valor contador que se ejecutará una sola vez al principio del bucle.
  • campo2 será donde escribiremos qué condición queremos que se cumpla en cada iteración (lo mismo que en while).
  • campo3 será donde tendrá lugar un incremento del valor contador.
for(contador = n; condición; incremento) {
	sentencia1;
	sentencia2;
	sentencia3;
	...
	sentenciaN;
}

En pseudocódigo:

Partiendo de contador = n;
Mientras se cumpla la condición {
	ejecutar el bloque;
	
	...
} incrementando en cada iteración

Por ejemplo, si queremos hacer un bucle con contador para mostrar por pantalla los números del 1 al 5 en cada línea:

 1using System;
 2
 3class a {
 4	static void Main() {
 5		int contador;
 6
 7		for(contador = 1; contador <= 5; contador = contador + 1) {
 8			Console.WriteLine(contador);
 9		}
10		
11		//Fin del programa
12	}
13}

Nos devolverá esta salida:

1
2
3
4
5

Simulándolo gráficamente, se ejecutará el código así: Simulación de for

Tenemos que meternos en la cabeza que:

  1. Lo primero que se ejecutará será siempre el primer campo de contador, y sólo se ejecutará una vez.
  2. Lo siguiente que hará será comprobar que la condición especificada en el campo 2 se cumple.
  3. Ejecutar el código del for si el paso 2 es cierto, o bien continuar por debajo del bucle for si el paso 2 resulta falso.
  4. Al final de cada iteración, se ejecuta el campo 3 para incrementar el valor del contador y se vuelve al paso 2.

Al final, el bucle for es equivalente a un while, y de hecho el código anterior se puede reescribir de la siguiente forma:

 1using System;
 2
 3class a {
 4	static void Main() {
 5		int contador = 1;
 6
 7		while(contador <= 5) {
 8			Console.WriteLine(contador);
 9			
10			contador = contador + 1;
11		}
12		
13		//Fin del programa
14	}
15}

… pero nos ocuparía más líneas, porque tenemos que declarar e inicializar la variable contador fuera del bucle, y tenemos que hacer el incremento después de la última línea de código dentro del bucle.

Más cosas a tener en cuenta de los bucles for:

  1. En el campo de inicialización (el primero), podemos declarar y asignar la variable a la vez. Esto es útil si queremos usar una variable i que sólo tenga uso dentro del bucle for:
for(int i = 0; i <= 10; i = i + 1) {
	Console.WriteLine(i);
}

En esta situación, la variable i será exclusiva de ese bucle for y sólo podrá usarse dentro. De lo contrario, nos devolverá error de compilación:

for(int i = 0; i <= 10; i = i + 1) {
	Console.WriteLine(i);
}
i = 0; // Esto nos dará error porque i no existe fuera del for.

Si queremos usar i fuera, tendríamos que al menos declararlo fuera del for y dejar el primer campo del for únicamente para inicializarlo:

int i; // Declaramos fuera

// Inicializamos i a 0 cuando entremos en el for
for(i = 0; i <= 10; i = i + 1) {
	Console.WriteLine(i);
}

// Se sale del bucle con i == 11
i = 0; // Esto funcionará.
  1. En el campo de incremento, que es el tercero, en realidad podemos hacer que un valor sume, reste, se multiplique, se divida, o se pueda realizar cualquier operación sobre el contador. Por ejemplo, podemos mostrar los números pares del 2 al 10 de esta forma:
for(int i = 2; i <= 10; i = i + 2) {
	Console.WriteLine(i);
}

Por cierto, también tienes que saber ya que podemos abreviar el i = i + 1 como i++, así como también podemos abreviar el i = i - 1 con i--.

De la misma forma, si queremos incrementar de 2 en 2, podemos usar i += 2, o bien i -= 2 si queremos reducir de dos en dos. Existen más operadores similares que veremos más adelante.

  1. Si en el for escribimos sólo en el campo de condición (después del primer punto y coma), dejando vacíos el campo de inicialización y el de variación, el for se convierte funcionalmente en un bucle while:
int i = 1;
for(; i <= 10;) {
	Console.WriteLine(i);
	i++;
}

Equivale a:

int i = 1;
while(i <= 10) {
	Console.WriteLine(i);
	i++;
}

Bucles for anidados

A veces nos podemos encontrar con bucles anidados como este:

for (int i = 0; i < 5; i++) {
	for (int j = 0; j < 5; j++) {
		Console.WriteLine("{0} {1}", i, j);
	}
	Console.WriteLine();
}

Este código devolverá esta salida:

0 0
0 1
0 2
0 3
0 4

1 0
1 1
1 2
1 3
1 4

2 0
2 1
2 2
2 3
2 4

3 0
3 1
3 2
3 3
3 4

4 0
4 1
4 2
4 3
4 4

Aquí ya nos solemos hacer un lío, sobre todo a la hora de intentar seguir los pasos de ejecución, lo cual será imprescindible para solucionar problemas donde necesitemos usar un for dentro de otro.

Vamos a simplificar un poco el código para leer mejor cómo se comporta un if dentro de otro, y en nuestro lenguaje:

Partiendo de i = 0, mientras i sea menor que 5:
   Partiendo de j = 0, mientras que j sea menor que 5:
      Imprimir por consola el valor de i y de j
   Incrementar j y fin del bucle
   Imprimir un salto de línea
Incrementar i y fin del bucle

Básicamente, tenemos un for bidimensional.

  1. Inicio del programa.
  2. Primero nos metemos en el bucle con contador i
  3. Ejecutamos todo el código que hay dentro del bucle del contador i
  4. Resulta que dentro de ese bloque de código hay otro bucle for con contador en j
  5. Ejecutamos todo el código que hay dentro del bucle del contador j
  6. Cuando finalicemos la iteración actual de j, incrementamos el valor de j y volvemos al paso 4 si se verifica que j < 5.
  7. Cuando j llegue a 5, salimos del bucle j y continuamos por debajo.
  8. Imprimimos una línea nueva.
  9. Cuando finalicemos la iteración actual de i, incrementamos el valor de i y volvemos al paso 2 si se verifica que i < 5.
  10. Cuando i llegue a 5, salimos del bucle i y continuamos por debajo.
  11. Fin del programa.

Si esto sigue siendo un lío, no te preocupes, que también me he tomado la molestia de hacer un gráfico de estos chulos para que lo visualices más claramente: Simulación de for anidado

La clave para entenderlo mejor es la siguiente:

  • La iteración de más afuera no finalizará hasta que finalice primero el bucle o los bucles que estén más al interior.

En el ejemplo, por cada pasada que hagamos en i, hacemos cinco pasadas del bucle j (contando desde 0). Cuando finalice el bucle que está más adentro (j), continuará o finalizará el bucle inmediato que lo contenga (i).

Entendiendo esto, ya podemos hacer un cuadrado con asteriscos cambiando tan sólo una línea del código anterior:

for (int i = 0; i < 5; i++) {
	for (int j = 0; j < 5; j++) {
		Console.Write("*"); // Cambiamos las coordenadas por un asterisco
                            // y hacemos que imprima sin saltar de línea
	}
	Console.WriteLine();
}

Se ejecutará así (pero mucho más rápido): Simulación de ejecución

foreach

foreach es un bucle especial que nos permite iterar dentro de listas o variables compuestas, que las veremos más adelante. Por ahora, vamos a ver un ejemplo con cadenas de texto.

Sabemos que el string es un conjunto de caracteres (varios char seguidos en una misma variable):

char letra1 = 'H',
     letra2 = 'o',
     letra3 = 'l',
     letra4 = 'a';
     
string frase = letra1.ToString()
             + letra2.ToString()
             + letra3.ToString()
             + letra4.ToString();
             
Console.WriteLine(frase);

Este código devolverá por consola: Hola porque hemos concatenado cada uno de los chars en un solo string— de una forma un poco sucia, pero esto es un ejemplo para que se entienda cómo funciona internamente un string.

Sabiendo esto, sabremos que la función foreach lo que hará es recorrer cada uno de los elementos del string, carácter por carácter, y guardarlo en una variable auxiliar. Por ejemplo:

string frase = "Hola";

foreach(char letra in frase) {
	Console.WriteLine(letra);
}

Traducido a nuestro lenguaje, esto es:

Inicializamos la variable frase: "Hola";

Para cada (carácter en la frase) {
	Imprimimos por pantalla una línea con cada letra;
}

Lo que hacemos cuando entremos en esa estructura foreach básicamente es, paso por paso:

  1. Declarar una variable letra de tipo char, que se usará como una variable auxiliar.
  2. Almacenar en esa variable auxiliar la letra que esté en la posición de frase correspondiente a la iteración actual. Tenemos que tener en cuenta de que los caracteres de este string se cuentan así, elemento por elemento:
0123
Hola

  3. Por cada letra de frase, imprimir una línea por pantalla.

La salida por consola será:

H
o
l
a

Básicamente, con foreach estamos partiendo nuestra frase (string) en trozos, y cada uno de estos trozos será cada una de las letras (de tipo char) que compone la frase.

También podemos tener exactamente la misma salida con un bucle for, pero nos quedaría un poco más larga:

string frase = "Hola";

for(int i = 0; i < frase.Length; i++) {
    Console.WriteLine(frase[i]);
}

La diferencia es que usaremos foreach si queremos sólo leer el contenido de una variable compuesta, mientras que for nos permitirá leer y modificar el contenido de una variable compuesta.

A saber también que los string son un tipo inmutable, pero esto sería adelantarnos un poco de temario. Ya hablaremos de cadenas de texto y de arrays (variables compuestas) más adelante, así como otros casos de uso interesantes de foreach.

break en los bucles

El break dentro de los bucles (for, while, do-while, foreach…) es una instrucción para ordenar salir del bucle en el que se encuentre. Mientras que con el break que hemos visto en el switch interrumpimos la instrucción switch en la que nos encontremos, el break lo hace en un bucle sin repetir el resto de iteraciones.

Por ejemplo, tenemos este código:

using System;

class a {
	static void Main() {
		for(int i = 0; i < 3; i++) {
			for(int j = 0; j < 3; j++) {
				if(j == 2) {
					break;
				}
				Console.Write("{0}", j);
			}
			Console.WriteLine();
		}
	}
}

La salida por consola será:

01
01
01

¿Por qué ocurre esto? Porque break se sale del bucle j y únicamente de ese bucle en el momento en el que esta variable vale 2. Luego el bucle i sigue su curso porque no hemos colocado ningún break en él.

Vamos a verlo paso por paso.

Demostración de break en un bucle anidado

Lo que nos tiene que quedar claro aquí es que break sólo se saldrá del bucle en el que se encuentre, aunque esté metido en un if, y no afectará a otros bucles exteriores.

Ahora bien, es muy importante saber también que, si el código anterior lo intentamos convertir a switch de la siguiente manera:

using System;

class a {
	static void Main() {
		for(int i = 0; i < 3; i++) {
			for(int j = 0; j < 3; j++) {
				switch(j) {
					case 2:
						break;
				}
				Console.Write("{0}", j);
			}
			Console.WriteLine();
		}
	}
}

Obtendremos la salida:

012
012
012

¿Por qué sucede esto? Porque es importante saber que si colocamos un break dentro de un switch, aunque este switch esté contenido en un bucle, el break primero hará que nos salgamos solamente de la instrucción switch que lo contenga, por lo que switch no se puede utilizar en este contexto si queremos interrumpir el bucle j con break, y reitero que esto es importante tenerlo presente a la hora de depurar posibles errores en nuestro código en un futuro.

Es por eso que se desaconseja usar break en los bucles sobre todo si estamos empezando a programar, ya que puede llevar a comportamientos inesperados y dificulta la trazabilidad de un programa.

continue y diferencias con break

continue es otra herramienta que nos puede ser muy útil si lo sabemos utilizar correctamente. Nos permitirá pasar a la siguiente iteración ignorando el resto de líneas que sigan a continuación. A diferencia del break:

  • break fuerza la salida del control actual.
  • continue fuerza la salida de la iteración actual, pero continúa con la siguiente iteración.

Por ejemplo, con este código:

using System;

class a {
	static void Main() {
		for(int i = 0; i < 5; i++) {
			for(int j = 0; j < 5; j++) {
				if(j == 2) {
					// Pasar a la siguiente cuando j valga 2
					continue;
				}
				Console.Write("{0}", j);
			}
			Console.WriteLine();
		}
	}
}

Obtendremos la salida:

0134
0134
0134
0134
0134

… porque estamos interrumpiendo la iteración en el momento en el que entramos en el if cuando j vale 2, y estamos forzando a continuar en la siguiente iteración, cuando j vale 3, hasta el final del bucle.

comments powered by Disqus