3. Condiciones y bucles
Secciones
Ajustes de lectura
Ninguna voz para este idioma en este navegador.
Más voces
Código y términos técnicos en inglés
Desactivado: la voz de la página lo lee todo, sin pausa entre dos voces.
Ninguna voz para este idioma en este navegador.
Más voces
Más voces: Microsoft Edge ofrece voces naturales en línea, y Windows (Configuración › Hora e idioma › Voz), macOS y Android instalan otras.
Código: los ejemplos examples/l03_*.cs, los fragmentos rechazados compile_fail/l03_*.cs y las soluciones exercises/l03_*.cs.
Hasta ahora, los programas ejecutaban cada línea una vez, de arriba abajo. Esta lección cambia eso: una condición ejecuta algunas líneas solo en algunos casos, y un bucle ejecuta líneas varias veces.
Comparaciones y bool
Sección titulada «Comparaciones y bool»Una comparación hace una pregunta cuya respuesta es un bool: true (verdadero) o false (falso).
int fret = 12;
// Una comparación da un bool: true o falseConsole.WriteLine(fret == 12);Console.WriteLine(fret != 12);Console.WriteLine(fret > 5 && fret < 10); // && : las dos deben ser verdaderasConsole.WriteLine(fret < 1 || fret > 11); // || : al menos una debe ser verdaderaConsole.WriteLine(!(fret > 5)); // ! : lo contrarioTrueFalseFalseTrueFalse| Operador | Significado | Operador | Significado | |
|---|---|---|---|---|
== |
igual a | && |
y | |
!= |
distinto de | || |
o | |
<, <= |
menor que, o igual | ! |
no | |
>, >= |
mayor que, o igual |
Cuidado con ==, dos signos igual, que compara, y =, un solo signo, que asigna. Las páginas de operadores de comparación y de operadores lógicos booleanos tienen las reglas completas. El y y el o se detienen en cuanto conocen la respuesta: en fret > 5 && fret < 10, cuando la primera comparación es false, la segunda ni siquiera se calcula.
if, else if, else
Sección titulada «if, else if, else»if ejecuta un bloque, las líneas entre llaves, solo cuando su condición es true:
// if ejecuta un bloque solo cuando la condición es verdaderaif (fret == 0){ Console.WriteLine("Open string");}else if (fret == 12){ Console.WriteLine("One octave above the open string");}else{ Console.WriteLine("Somewhere else on the neck");}
// Las cadenas se comparan por su contenidostring tuning = "E A D G B E";if (tuning == "E A D G B E"){ Console.WriteLine("Standard tuning");}
// Una variable declarada dentro de un bloque solo existe en ese bloqueif (fret > 7){ int distance = fret - 7; Console.WriteLine($"{distance} frets above the 7th");}One octave above the open stringStandard tuning5 frets above the 7thLas condiciones se comprueban en orden, y solo se ejecuta el primer bloque cuya condición es true. else recoge todos los demás casos. Tanto else if como else son opcionales.
La condición debe ser un bool. Se rechazan dos errores clásicos. Escribir un solo signo igual en lugar de dos:
int fret = 5;if (fret = 12){ Console.WriteLine("Octave");}l03_assign_in_if.cs(2,5): error CS0029: Cannot implicitly convert type 'int' to 'bool'l03_assign_in_if.cs(1,5): warning CS0219: The variable 'fret' is assigned but its value is never usedfret = 12 es una asignación, cuyo valor es el int 12, no un bool. En algunos otros lenguajes esto compila y cambia fret en silencio. Una cadena tampoco es una condición:
string answer = "yes";if (answer)l03_string_condition.cs(2,5): error CS0029: Cannot implicitly convert type 'string' to 'bool'Escribe if (answer == "yes").
Una variable declarada dentro de un bloque solo existe en ese bloque, y en los bloques que contiene. Esa zona es su ámbito:
int fret = 9;if (fret > 7){ int distance = fret - 7;}Console.WriteLine(distance);l03_out_of_scope.cs(6,19): error CS0103: The name 'distance' does not exist in the current contextPara usar distance después del if, declárala antes del if.
Cuando un valor se compara con una lista de valores posibles, un switch se lee mejor que una cadena de else if.
int semitone = 7;
// La instrucción switch: un case por valor, cada case termina con breakswitch (semitone){ case 0: Console.WriteLine("Unison"); break; case 7: Console.WriteLine("Perfect fifth"); break; case 12: Console.WriteLine("Octave"); break; default: Console.WriteLine("Another interval"); break;}Perfect fifthLa instrucción switch salta al case que coincide, o a default cuando ninguno coincide. Cada sección debe terminar con break (o return, lección 4). En C y JavaScript, un break que falta deja que el programa caiga en el caso siguiente; C# lo rechaza:
switch (semitone){ case 7: Console.WriteLine("Perfect fifth"); case 12: Console.WriteLine("Octave"); break;}l03_fall_through.cs(4,5): error CS0163: Control cannot fall through from one case label ('case 7:') to anotherLa expresión switch
Sección titulada «La expresión switch»A menudo, cada caso solo calcula un valor. La expresión switch lo dice en menos líneas: el valor, switch, y luego un brazo por caso, pattern => result (patrón y resultado), separados por comas.
// La expresión switch calcula un valor: un brazo por patrón, _ coincide con todo lo demásstring name = semitone switch{ 0 => "C", 1 => "C#", 2 => "D", 3 => "D#", 4 => "E", 5 => "F", 6 => "F#", 7 => "G", 8 => "G#", 9 => "A", 10 => "A#", 11 => "B", _ => "not a semitone between 0 and 11",};Console.WriteLine($"Semitone {semitone} above C is {name}");
// Los patrones pueden comparar: gana el primer brazo que coincideforeach (int fret in new[] { 0, 3, 7, 12, 17, 30 }){ string zone = fret switch { 0 => "open string", < 5 => "first position", < 12 => "middle of the neck", 12 => "octave", <= 24 => "high on the neck", _ => "no such fret", }; Console.WriteLine($"Fret {fret}: {zone}");}Semitone 7 above C is GFret 0: open stringFret 3: first positionFret 7: middle of the neckFret 12: octaveFret 17: high on the neckFret 30: no such fret- Los brazos se prueban de arriba abajo, y el primero que coincide da el valor. El traste 3 coincide con el brazo
< 5, menor que 5, antes de probar el brazo menor que 12. - Un patrón puede ser un valor, como
12, o una comparación, como< 5, menor que 5. La página de patrones muestra los demás:and,or,noty muchos más. _, el descarte, coincide con todo: es eldefaultde una expresión switch.- El bucle
foreach (int fret in new[] { … })ejecuta el bloque una vez por cada número; los bucles vienen a continuación.
Sin _, un valor con el que no coincide ningún brazo no tiene nada que producir. El compilador avisa, y el programa falla cuando ocurre:
int stringNumber = 7;
// Ningún brazo para 7, y ningún brazo _: el compilador avisa y el programa falla en ejecuciónstring open = stringNumber switch{ 1 => "E4", 2 => "B3", 3 => "G3", 4 => "D3", 5 => "A2", 6 => "E2",};Console.WriteLine(open);l03_switch_warning.cs(4,28): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern '0' is not covered.Unhandled exception. System.Runtime.CompilerServices.SwitchExpressionException: Non-exhaustive switch expression failed to match its input.Unmatched value was 7. at <PrivateImplementationDetails>.ThrowSwitchExpressionException(Object unmatchedValue) at Program.<Main>$(String[] args) in C:\Users\spare\source\repos\learn\code\csharp-beginner\examples\l03_switch_warning.cs:line 4Una advertencia (warning), a diferencia de un error, no detiene la compilación: el programa se ejecuta, y aquí se detiene con una excepción, un error que ocurre mientras el programa se ejecuta. Las líneas que empiezan por at son la pila de llamadas (stack trace): dónde estaba el programa. La última apunta a la línea 4 del archivo. La lección 8 trata de las excepciones.
Dos cosas que saber sobre las advertencias. Primero, léelas: esta anunciaba el fallo. Segundo, dotnet run solo las muestra cuando compila; si vuelves a ejecutar el mismo archivo sin cambios, no compila, y la advertencia no se imprime otra vez. dotnet clean l03_switch_warning.cs olvida el programa compilado, y la siguiente ejecución vuelve a mostrar la advertencia. El check.sh del curso hace eso antes de cada ejecución.
Un bucle repite un bloque. C# tiene cuatro instrucciones de iteración:
// while: repite mientras la condición sea verdaderaint countdown = 3;while (countdown > 0){ Console.WriteLine($"{countdown}..."); countdown--; // lo mismo que countdown = countdown - 1}Console.WriteLine("Play!");
// for: inicio; condición; pasofor (int fret = 0; fret <= 12; fret += 3){ Console.Write($"{fret} ");}Console.WriteLine();
// foreach: cada elemento de una colección, aquí cada carácter de una cadenaforeach (char letter in "EADGBE"){ Console.Write($"[{letter}]");}Console.WriteLine();
// break sale del bucle, continue pasa a la vuelta siguientefor (int i = 1; i <= 10; i++){ if (i % 2 == 0) { continue; // salta los números pares } if (i > 7) { break; // para después de 7 } Console.Write($"{i} ");}Console.WriteLine();
// do-while: el bloque se ejecuta al menos una vez, la condición se comprueba despuésint tries = 0;do{ tries++; Console.WriteLine($"Try {tries}");} while (tries < 2);3...2...1...Play!0 3 6 9 12[E][A][D][G][B][E]1 3 5 7Try 1Try 2| Bucle | Úsalo cuando | Comprueba la condición |
|---|---|---|
while (condition) |
no sabes de antemano cuántas vueltas | antes de cada vuelta |
do { … } while (condition); |
el bloque debe ejecutarse al menos una vez | después de cada vuelta |
for (start; condition; step) |
cuentas: de 0 a 12, de 3 en 3 | antes de cada vuelta |
foreach (type item in collection) |
recorres cada elemento de una colección | — |
countdown--resta 1,i++suma 1, yfret += 3suma 3: formas cortas decountdown = countdown - 1yfret = fret + 3.- Un
fortiene tres partes separadas por punto y coma: lo que se hace una vez al principio (int fret = 0), la condición comprobada antes de cada vuelta (fret <= 12), y lo que se hace después de cada vuelta (fret += 3). Su variablefretsolo existe dentro del bucle. foreachtoma cada elemento por turno. Una cadena es una colección dechar; los arrays y las listas, en la lección 4, también son colecciones.continuesalta el resto del bloque y empieza la vuelta siguiente;breaksale del bucle de inmediato.
Un while cuya condición nunca pasa a false se ejecuta para siempre. Si un programa parece atascado, pulsa Ctrl+C en la terminal para detenerlo.
Bucles dentro de bucles: el mástil
Sección titulada «Bucles dentro de bucles: el mástil»Un bucle puede contener otro bucle. Para cada cuerda, el bucle interior recorre cada traste. Este programa imprime las notas de los cinco primeros trastes de una guitarra con afinación estándar, el Tuning.Default de Guitar Alchemist:
// Los cinco primeros trastes de una guitarra con afinación estándar (E2 A2 D3 G3 B3 E4, Tuning.Default en GA).// Cada nota es un número de semitonos: C es 0, C# es 1, ... B es 11, y el C siguiente vuelve a ser 12.for (int stringNumber = 6; stringNumber >= 1; stringNumber--){ // Semitonos de la cuerda al aire por encima de C int open = stringNumber switch { 6 => 4, // E 5 => 9, // A 4 => 2, // D 3 => 7, // G 2 => 11, // B _ => 4, // E (cuerda 1) };
Console.Write($"String {stringNumber}:"); for (int fret = 0; fret <= 5; fret++) { int semitone = (open + fret) % 12; // % 12 devuelve 12 a 0: la octava string name = semitone switch { 0 => "C", 1 => "C#", 2 => "D", 3 => "D#", 4 => "E", 5 => "F", 6 => "F#", 7 => "G", 8 => "G#", 9 => "A", 10 => "A#", _ => "B", }; Console.Write($" {name,-2}"); } Console.WriteLine();}String 6: E F F# G G# AString 5: A A# B C C# DString 4: D D# E F F# GString 3: G G# A A# B CString 2: B C C# D D# EString 1: E F F# G G# ALos guitarristas numeran las cuerdas de la más aguda, la cuerda 1, a la más grave, la cuerda 6; el bucle exterior cuenta hacia atrás para que la cuerda 6 se imprima primero. En la cuerda Si (B), el traste 1 es (11 + 1) % 12, que vale 0: C, es decir, do. El operador % hace que los números den la vuelta como las horas de un reloj, que es exactamente como se repiten los nombres de las notas en cada octava. El curso de teoría musical va mucho más lejos con la misma idea.
Las mismas notas en el mástil: cada línea es una cuerda, con la cuerda 6 arriba como en la salida, y cada columna es un traste. El traste 0 es la cuerda al aire, a la izquierda de la cejuela, la barra donde empiezan las cuerdas; el C resaltado es el traste 1 de la cuerda Si.
Leer hasta que la respuesta sea correcta
Sección titulada «Leer hasta que la respuesta sea correcta»Un bucle while puede seguir pidiendo datos hasta obtener lo que necesita:
// Adivina el traste: el bucle lee respuestas hasta que una es correctaint secret = 7;int tries = 0;bool found = false;
while (!found){ Console.Write("Which fret? "); string? line = Console.ReadLine(); if (line == null) { Console.WriteLine("No more input."); break; } if (!int.TryParse(line, out int guess)) { Console.WriteLine($"\"{line}\" is not a number."); continue; }
tries++; if (guess < secret) { Console.WriteLine("Higher."); } else if (guess > secret) { Console.WriteLine("Lower."); } else { found = true; Console.WriteLine($"Found in {tries} tries."); }}Tecleando 12, five, 5 y 7:
Which fret? 12Lower.Which fret? five"five" is not a number.Which fret? 5Higher.Which fret? 7Found in 3 tries.continue vuelve a la pregunta sin contar la respuesta incorrecta como intento. Cuando la entrada termina, ReadLine devuelve null y break sale del bucle: sin él, un programa cuya entrada viene de un archivo, como en la CI, preguntaría para siempre. Con el teclado, Ctrl+Z y luego Enter en Windows, o Ctrl+D en Linux y macOS, terminan la entrada.
Encontrar un error con el depurador
Sección titulada «Encontrar un error con el depurador»Cuando un programa da un resultado incorrecto, necesitas ver lo que hace, línea a línea. La forma más sencilla es añadir llamadas a Console.WriteLine que impriman las variables, volver a ejecutar y quitarlas después. Funciona en todas partes.
Un depurador lo hace mejor: pausa el programa en una línea que eliges, un punto de interrupción, muestra el valor de cada variable y te deja ejecutar una línea cada vez. Los tres editores de la lección 1 tienen uno:
| Acción | Visual Studio Code | Visual Studio | Rider |
|---|---|---|---|
| Poner o quitar un punto de interrupción | F9, o clic a la izquierda del número de línea | F9 | clic a la izquierda de la línea |
| Iniciar con el depurador | F5 | F5 | el icono del insecto |
| Ejecutar la línea siguiente | F10 (step over) | F10 | step over |
| Entrar en un método llamado en esta línea | F11 (step into) | F11 | step into |
| Continuar hasta el siguiente punto de interrupción | F5 | F5 | resume |
Pruébalo con el programa del mástil: pon un punto de interrupción en la línea int semitone = (open + fret) % 12;, inicia con el depurador y mira stringNumber, open y fret en el panel Variables o Locals. Cada vez que continúas, el programa vuelve a detenerse en el traste siguiente. También puedes escribir una expresión como (open + fret) % 12 en el panel Watch para ver su valor.
Las teclas de esta tabla son las predeterminadas de cada editor en Windows y Linux; el mapa de teclas de Rider en macOS es distinto. Consulta la depuración en VS Code, el depurador de Visual Studio para principiantes y la depuración en Rider. Por verificar: todavía no he depurado estos ejemplos en cada uno de los tres editores, ni como aplicaciones de un solo archivo ni dentro de un proyecto creado con dotnet new console.
Puntos clave
Sección titulada «Puntos clave»- Una comparación da un
bool; y (&&), o (||) y no (!) las combinan. Dos signos igual comparan, un solo signo igual asigna. if,else ifyelseejecutan el primer bloque cuya condición estrue; una variable declarada en un bloque solo existe en ese bloque.- Una instrucción
switchnecesita unbreakal final de cada caso; una expresiónswitchcalcula un valor, prueba sus brazos de arriba abajo y debería terminar con_. whilerepite mientras una condición es verdadera,forcuenta,foreachrecorre cada elemento de una colección;breaksale del bucle ycontinuesalta a la vuelta siguiente.- Una advertencia no detiene la compilación, pero a menudo anuncia un problema; una excepción no controlada detiene el programa e imprime dónde ocurrió.
- Un depurador se detiene en los puntos de interrupción y muestra las variables, una línea cada vez.
Ejercicios
Sección titulada «Ejercicios»- Imprime los números del 1 al 15 en una línea, pero imprime
Fizzen lugar de los múltiplos de 3,Buzzen lugar de los múltiplos de 5, yFizzBuzzpara los múltiplos de ambos. Pista: una expresión switch puede mirar dos valores a la vez, escrita(a, b) switch.
Solución
// Ejercicio 1: FizzBuzz de 1 a 15, con una expresión switch sobre dos restosfor (int i = 1; i <= 15; i++){ string text = (i % 3, i % 5) switch { (0, 0) => "FizzBuzz", (0, _) => "Fizz", (_, 0) => "Buzz", _ => i.ToString(), }; Console.Write($"{text} ");}Console.WriteLine();1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz(i % 3, i % 5) agrupa los dos restos en una tupla, y cada brazo comprueba ambos. (0, _) significa «divisible por 3, sea cual sea el segundo resto». El orden importa: (0, 0) debe ir primero, o 15 imprimiría Fizz. Con if, escribirías primero if (i % 3 == 0 && i % 5 == 0), luego else if (i % 3 == 0), y así sucesivamente.
- Pide el nombre de una nota (
C,C#, …B) e imprime las 13 notas de la escala cromática desde esa nota hasta la misma nota una octava más arriba. Imprime un mensaje si la nota es desconocida.
Solución
exercises/l03_ex_chromatic.cs:
// Ejercicio 2: leer el nombre de una nota e imprimir las 13 notas de la escala cromática desde ella hasta su octavaConsole.Write("Starting note? ");string? input = Console.ReadLine();
int start = input switch{ "C" => 0, "C#" => 1, "D" => 2, "D#" => 3, "E" => 4, "F" => 5, "F#" => 6, "G" => 7, "G#" => 8, "A" => 9, "A#" => 10, "B" => 11, _ => -1,};
if (start == -1){ Console.WriteLine($"Unknown note: {input}");}else{ for (int step = 0; step <= 12; step++) { string name = ((start + step) % 12) switch { 0 => "C", 1 => "C#", 2 => "D", 3 => "D#", 4 => "E", 5 => "F", 6 => "F#", 7 => "G", 8 => "G#", 9 => "A", 10 => "A#", _ => "B", }; Console.Write($"{name} "); } Console.WriteLine();}Con A:
Starting note? AA A# B C C# D D# E F F# G G# ALa primera expresión switch convierte el nombre en un número, donde -1 significa «desconocida»: una expresión switch también funciona con cadenas. El bucle se ejecuta 13 veces, step de 0 a 12, y % 12 devuelve al principio los números mayores que 11. Escribir dos veces los mismos nombres de notas no es ideal: la lección 4 los guarda una sola vez, en un array.
- Lee tiempos de práctica en minutos, uno por línea, hasta una línea vacía o el final de la entrada. Salta las líneas que no sean números enteros o que sean negativas, indicándolo. Después imprime el número de días válidos, el total en minutos y el total en horas y minutos.
Solución
// Ejercicio 3: sumar los minutos de práctica escritos uno por línea; una línea vacía termina la entradaint total = 0;int days = 0;while (true){ string? line = Console.ReadLine(); if (line == null || line == "") { break; } if (!int.TryParse(line, out int minutes) || minutes < 0) { Console.WriteLine($"Skipped: {line}"); continue; } total += minutes; days++;}Console.WriteLine($"{days} days, {total} minutes, {total / 60} h {total % 60} min");Con las líneas 30, 45, forty, -5, 60, una línea vacía y luego 90 (input/l03_ex_total.txt), la salida es:
Skipped: fortySkipped: -53 days, 135 minutes, 2 h 15 minwhile (true) nunca se detiene por sí solo: decide el break de dentro. El 90 después de la línea vacía nunca se lee. En !int.TryParse(line, out int minutes) || minutes < 0, el || calcula minutes < 0 solo cuando TryParse tuvo éxito, así que minutes siempre contiene el valor leído cuando se compara.
Fuentes
Sección titulada «Fuentes»- Instrucciones de selección:
if,elseyswitch, la expresiónswitch, patrones - Instrucciones de iteración:
for,foreach,doywhile, instrucciones de salto:breakycontinue - Operadores de comparación, operadores lógicos booleanos
- Advertencia del compilador CS8509,
SwitchExpressionException - Depurar C# en VS Code, recorrido por las características del depurador de Visual Studio, Rider: depurar código