0

Control de flujo en C#

La gestión del flujo es uno de los conceptos fundamentales que he abordado al sumergirme en el lenguaje de programación C#. Esta publicación técnica en el blog, que planeo expandir en una serie conforme adquiera más conocimientos sobre C# y .NET durante mi proceso de aprendizaje. Mi meta es crear contenido que explore a fondo los principios básicos esenciales de C# y .NET.

¿Qué es el control de flujo?

El flujo de ejecución se refiere al orden en que se ejecuta o evalúa el código de un programa. En C#, existen diversas construcciones para escribir bucles y para determinar qué código ejecutar según el valor de las entradas.

El primer conjunto esencial de declaraciones de control de flujo incluye las declaraciones de selección: declaraciones if y declaraciones switch. Estas declaraciones eligen el código que se ejecutará de entre varias opciones según el valor de una expresión.

IF

Al emplear la instrucción if, es posible verificar condiciones para ramificar el flujo de ejecución. La instrucción if elegirá una instrucción para ejecutar según el valor de una expresión booleana.

La instrucción if solo se bifurcará dentro del bloque y ejecutará el código si la condición se evalúa como verdadera. Por ejemplo, la instrucción if a continuación solo ejecutará el código dentro del bloque y añadirá la calificación a la lista de calificaciones si el valor de la variable de calificación es menor o igual a 100:

namespace StudentGradeBook
{
    public class Book
    {
        public void AddGrade(double grade)
        {
            if (grade <= 100)
            {
                grades.Add(grade);
            }
        }

        private List<double> grades = new List<double>();
    }
}

Opcionalmente, las instrucciones if pueden incluir una instrucción else. La instrucción else solo se ejecuta cuando la expresión de las instrucciones if se evalúa como falsa. En el siguiente ejemplo, si la variable de calificación no es menor o igual a 100, se ejecutará la instrucción else:

namespace StudentGradeBook
{
    public class Book
    {
        public void AddGrade(double grade)
        {
            if (grade <= 100)
            {
                grades.Add(grade);
            }
            else
            {
                Console.WriteLine("You have entered an invalid grade value");
            }
        }

        private List<double> grades = new List<double>();
    }
}

En caso de necesitar comprobar múltiples condiciones, puede redactar else if de la siguiente manera:

namespace StudentGradeBook
{
    public class Book
    {
        public void AddGrade(double grade)
        {
            if (grade <= 100)
            {
                grades.Add(grade);
            }
            else if (grade >= 0)
            {
                grades.Add(grade);
            }
            else
            {
                Console.WriteLine("You have entered an invalid grade value");
            }
        }

        private List<double> grades = new List<double>();
    }
}

Una alternativa a escribir else if es utilizar operadores booleanos para combinar dos verificaciones lógicas en una sola expresión. Algunos ejemplos de operadores booleanos son:

  • && (operador lógico AND)
  • || (operador lógico OR)
  • == (operador de igualdad)
  • != (operador de desigualdad)

En el siguiente ejemplo, el lado derecho del operador && se evalúa solo si el lado izquierdo se evalúa como verdadero. Solo si ambas condiciones a cada lado del operador && se evalúan como verdaderas, se ejecutará el cuerpo de la instrucción if. Por lo tanto, solo si la calificación de entrada es menor que 100 y mayor o igual a 0.

namespace StudentGradeBook
{
    public class Book
    {
        public void AddGrade(double grade)
        {
            if (grade <= 100 &amp;&amp; grade >= 0)
            {
                grades.Add(grade);
            }
            else
            {
                Console.WriteLine("You have entered an invalid grade value");
            }
        }

        private List<double> grades = new List<double>();
    }
}

switch 

Aunque es posible concatenar declaraciones if utilizando declaraciones if anidadas con if y else if, C# ofrece la declaración switch como alternativa. La declaración switch elige uno de los varios bloques de código que se ejecutarán según el valor de una expresión de entrada.

En tiempo de ejecución, la declaración switch se evalúa una vez, comparando el valor de la expresión con el valor de cada caso. Si se encuentra una coincidencia, se ejecuta el bloque de código asociado a la declaración de caso coincidente. En el método AddLetterGrade() del siguiente ejemplo, si el valor de la variable de letra es igual a A, se ejecutará el cuerpo de la declaración case que representa la constante ‘A’ y se añade la calificación 90 a la colección de calificaciones.

Si el valor de la expresión switch no coincide con ninguno de estos casos, entonces no se ejecutará ninguna parte del código dentro de la declaración switch. No obstante, si deseamos tener un caso que se ejecute cuando ninguno de los otros casos se ejecute, podemos emplear la palabra clave default. Esta sección por defecto es opcional.

namespace StudentGradeBook
{
    public class Book
    {
        public void AddLetterGrade(char letter)
        {
            switch (letter)
            {
                case 'A':
                    AddGrade(90);
                    break;
                case 'B':
                    AddGrade(80);
                    break;
                case 'C':
                    AddGrade(70);
                    break;
                default:
                    AddGrade(0);
                    break;
            }
        }
        
        public void AddGrade(double grade)
        {
            if (grade <= 100 &amp;&amp; grade >= 0)
            {
                grades.Add(grade);
            }
            else
            {
                Console.WriteLine("You have entered an invalid grade value");
            }
        }
        
      	private List<double> grades = new List<double>();
    }
}

Como puede apreciarse en el ejemplo previo, entre cada declaración case se encuentra una declaración «break”. Cuando el compilador de C# encuentra la palabra clave break, la ejecución se desplazará al final de la declaración switch y saldrá del bloque switch.

El compilador de C# requiere una palabra clave break dentro de la declaración switch para controlar de manera explícita el flujo de ejecución. La norma que impone el compilador de C# es que no se puede acceder al punto final de la lista de declaraciones para cada caso. Esto se debe a que el compilador de C# quiere detener explícitamente que el código se ejecute desde una declaración case hasta la siguiente. En algunos lenguajes C, esto sería aceptable y se conoce como «fall-through».

En realidad, la gran mayoría de las secciones de casos no se caen, por lo que cuando ocurre una caída, suele ser accidental y tiene consecuencias no deseadas. Por lo tanto, la caída es ilegal en C#. Si se desea el «fall-through», se debe solicitar explícitamente mediante la palabra clave goto, como se muestra en el ejemplo siguiente:

namespace StudentGradeBook
{
    public class Book
    {
        public void AddLetterGrade(char letter)
        {
            switch (letter)
            {
                case 'A':
                    AddGrade(90);
                    goto case 'B';
                case 'B':
                    AddGrade(80);
                    break;
            }
        }
      
     	public void AddGrade(double grade)
        {
            if (grade <= 100 &amp;&amp; grade >= 0)
            {
                grades.Add(grade);
            }
            else
            {
                Console.WriteLine("You have entered an invalid grade value");
            }
        }
        
        private List<double> grades = new List<double>();
    }
}

Pattern matching con switch

La declaración switch puede evaluar patrones. La coincidencia de patrones es el proceso de examinar una expresión para determinar si presenta ciertas características.

En el ejemplo anterior, la declaración switch utilizaba el tipo de patrón constante. Cada caso en una declaración switch especifica un patrón: ese patrón es un valor de una constante. La expresión switch coincidirá con este patrón si comparte el mismo valor.

Otro uso popular para la coincidencia de patrones es verificar si una variable se ajusta a un tipo específico. Puede utilizar patrones de tipo en declaraciones switch. La declaración switch le permite declarar una variable en una instrucción case y realizar coincidencias de tipos o comparaciones de tipos con la expresión switch.

object grade = 90;

switch (grade)
{
  case int number:
    Console.WriteLine($"The grade achieved was {number.ToString("D2")}%");
    break;

  case string letter:
    Console.WriteLine($"The grade achieved was '{letter}'");
    break;
}

El siguiente grupo de instrucciones que rigen el flujo de control son las instrucciones de iteración. Las instrucciones de iteración, que incluyen bucles for, foreach, do y while, posibilitan la ejecución repetitiva de un bloque de código siempre que se cumpla una condición especificada.

while loop y do/while loop

El ciclo while itera de manera continua a través de un bloque de código mientras una condición específica se evalúe como verdadera. La condición determina cuándo se detendrá la ejecución del bucle. El código dentro del bucle se repetirá de manera constante hasta que la condición sea falsa.

En el siguiente ejemplo, el código del bucle se ejecutará repetidamente siempre que el índice de la variable sea menor que el número de calificaciones en la lista de calificaciones. El índice indica la posición en el recorrido de la lista de calificaciones. Aunque la colección de calificaciones tiene 4 elementos, el último elemento tendrá un índice de 3.

var grades = new List<double>() {13.2, 12.4, 7.8, 3.2};

var index = 0; // initialization

while (index < grades.Count) //condition
{
  Console.WriteLine(grades[index]);
  index += 1; // increment
}

Dado que una instrucción while evalúa su expresión antes de cada iteración, existe la posibilidad de que un bucle while no ejecute su cuerpo en absoluto. Por ejemplo, si el recuento de calificaciones es 0 debido a la ausencia de calificaciones en la lista, el bucle no ejecutará el código contenido en él.

No obstante, puede ser deseable ejecutar el cuerpo del bucle while al menos una vez. Esto se puede lograr mediante el bucle do/while, como se ejemplifica en el siguiente ejemplo. El bucle do/while siempre se ejecutará al menos una vez porque no verifica la condición del bucle hasta después de la primera iteración. La condición al final de la primera iteración indica al tiempo de ejecución que debe continuar con este bucle.

var grades = new List<double>() {13.2, 12.4, 7.8, 3.2};

var index = 0; // initialization

do
{
  Console.WriteLine(grades[index]);
  index += 1; // increment
} while (index < grades.Count); //condition

for 

El bucle for se ejecutará siempre que una condición booleana especificada se evalúe como verdadera. Este bucle, similar a un bucle while, introduce dos nuevas características en su expresión booleana. La sintaxis del bucle for se describe a continuación:

for (statement 1; statement 2; statement 3)
{ 
  // code block to be executed
}

La primera declaración es una expresión de inicialización que brinda un espacio para declarar y/o inicializar una variable que mantiene su alcance durante la duración del bucle. Esta instrucción se ejecuta una vez al inicio del bucle. En el siguiente ejemplo, he declarado una variable de índice de tipo int. Esta inicialización representa una variable de conteo de bucle.

La segunda declaración establece la condición que se debe verificar antes de ejecutar el cuerpo del bucle for. La instrucción condicional debe ser una expresión booleana. Esto opera de manera análoga a la condición en un bucle while. En el siguiente ejemplo, la condición a verificar es si el valor del índice es menor que la cantidad de calificaciones en la lista de calificaciones. El bucle continuará mientras esta condición sea verdadera.

Finalmente, la tercera instrucción define la operación que se realizará después de cada ejecución del cuerpo del bucle. En el ejemplo siguiente, la instrucción del iterador incrementa la variable de conteo. Después de cada iteración del bucle, sumaremos uno a la variable índice.

Podría preferirse un bucle for en lugar de un bucle while por motivos de claridad, ya que, a diferencia de un bucle while, la expresión de inicialización, la condición y el iterador se encuentran definidos en una sola línea.

var grades = new List<double>() {13.2, 12.4, 7.8, 3.2};

for (var index = 0; index < grades.Count; index += 1)
{
  Console.WriteLine(grades[index]);
}

foreach

El bucle foreach se utiliza para recorrer los elementos de una colección. Cada iteración de este bucle ejecutará el cuerpo del bucle una vez por cada calificación presente en la colección de calificaciones. El valor de cada calificación se utilizará como la variable de calificación en cada vuelta del bucle.

var grades = new List<double>() {13.2, 12.4, 7.8, 3.2};

foreach (double grade in grades)
{
  Console.WriteLine(grade);
};

Cómo foreach trabaja internamente: IEnumerable<T> and IEnumerator<T>

Muchas colecciones de C# implementan la interfaz de la biblioteca de clases de .NET. El bucle foreach puede recorrer en iteración cada elemento de estas colecciones una tras otra mediante el uso interno de métodos expuestos por la interfaz para enumerarlos.

IEnumerable<T> tiene un solo método que devuelve un como se muestra a continuación:GetEnumerator()IEnumerator<T>.

namespace System.Collections.Generic
{
    public interface IEnumerator<T> : IEnumerable
    {
        bool MoveNext();
        void Reset();
        T Current { get; }
    }
}

El IEnumerator<T> proporciona la capacidad de iterar a través de una colección mediante la exposición de una propiedad Current y los métodos MoveNext y Reset. El parámetro type <T> Define el tipo de objetos que se van a enumerar. La propiedad Current obtiene el elemento de la colección en la posición actual del enumerador. El método MoveNext() mueve el enumerador al siguiente elemento de la colección. Y, por último, el método Reset() establece el enumerador en su posición inicial, que es antes del primer elemento de la colección.

Entonces, el bucle foreach llama al método GetEnumerator() de la interfaz IEnumerable<T>, que luego devuelve el IEnumerator<T> de esa colección. Luego llama al método MoveNext(), que mueve el enumerador al primer elemento de la colección. Ese elemento será referenciado por la propiedad Current. Para cada iteración del bucle foreach, se llamará al método MoveNext(), que mueve el enumerador al siguiente elemento en la colección, que será referenciado por Current. Este proceso se repite hasta llegar al último elemento de la colección.

La declaración foreach no se limita únicamente a colecciones que implementan la interfaz IEnumerable<T>. Puedes utilizar un bucle foreach con cualquier colección que tenga un método GetEnumerator() que se asemeje al definido por la interfaz IEnumerable<T>, como se describió anteriormente.

Conclusiones

En resumen, en esta entrada del blog exploramos diversas sentencias para controlar el flujo de ejecución en un programa. Observamos cómo las sentencias de selección, como «if» y «switch», determinan qué código ejecutar en base al valor de una expresión. Además, examinamos las sentencias iterativas «for», «foreach», «do» y «while», que permiten ejecutar código en bucles mientras se cumpla una condición específica. Estas instrucciones de control de flujo son esenciales para guiar a los programas en la toma de decisiones sobre el orden de ejecución.

Fernando Sonego

Deja una respuesta

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