Tema 10
Ampliación de Estructuras de Control
Antes de pasar al estudio de las estructuras de datos complejas, se completa el repertorio de estructuras de control más frecuentes en los lenguajes imperativos, detallando las construcciones adoptados en Modula-2.
Se presentan algunas variantes para realizar la iteración y la selección, derivadas de las estructuras fundamentales de la Unidad Didáctica anterior, mostrando la posibilidad que existe de programarlas en función de ellas.
10.1 Estructuras complementarias de iteración
Como se explicó en el Tema 5, la programación estructurada propone la utilización del esquema WHILE como sentencia iterativa fundamental. Esta sentencia basta para poder realizar cualquier programa. Sin embargo, en el mismo Tema 5 también fue introducida la sentencia FOR cuya utilidad fundamental es programar un número de iteraciones que se conoce a priori y que no depende de la evolución de los cálculos realizados en cada iteración. La razón de que exista la sentencia FOR prácticamente en todos los lenguajes, es precisamente lo habitual de este tipo de iteraciones y la facilidad que proporciona el uso de la sentencia FOR en estos casos.
Las estructuras iterativas que se explican en este apartado están también disponibles habitualmente en todos los lenguajes y pretenden facilitar la programación de situaciones concretas pero muy frecuentes en cualquier programa. Utilizaremos para las explicaciones las sentencias disponibles en Modula-2: REPEAT y LOOP. Para posibilitar la terminación de la iteración utilizando una sentencia LOOP es necesario utilizar otra sentencia: EXIT. Esta última sentencia será explicada junto a la sentencia LOOP dado que está ligada por completo a ella y no se puede utilizar nunca de forma aislada.
10.1.1 Sentencia REPEAT
A veces resulta más adecuado comprobar la condición que controla las iteraciones al finalizar cada una de ellas, en lugar de hacerlo al comienzo de las mismas. En este caso, como se muestra en la Figura 10.1, siempre se ejecuta al menos una primera iteración.
––
Figura 10.1. Esquema de repetición
El formato de esta sentencia en Modula-2 es el siguiente:
REPEAT Secuencia_de_sentencias UNTIL Condición
la Condición que controla las repeticiones es una expresión cuyo resultado es un valor de tipo BOOLEAN. Si el resultado es FALSE se vuelve a ejecutar la Secuencia_de_sentencias y cuando el resultado es TRUE finaliza la ejecución de la sentencia.
Una situación típica en que resulta cómodo el empleo de esta sentencia es la que se produce cuando al finalizar cada iteración se pregunta al operador si desea continuar con una nueva. En todos estos casos, el programa siempre ejecuta la primera iteración y pregunta si se desea o no realizar otra más. Por ejemplo:
REPEAT
. . . .
… << operación >> …
. . . .
WriteString( "¿Otra operación (S/N)?" );
Read( tecla );
UNTIL tecla = "N"
En todos los programas realizados en los Temas anteriores se ha podido programar esta forma de operar utilizando la sentencia WHILE. Sin embargo, en los esquemas desarrollados ha sido necesario forzar la primera iteración inicializando la variable que controla la iteración a un valor distinto del necesario para la finalización. Para el mismo ejemplo anterior, esto se realizaría de la siguiente forma:
tecla := "S";
WHILE tecla <> "N" DO
. . . .
… << operación >> …
. . . .
WriteString( "¿Otra operación (S/N)?" );
Read(tecla)
END;
Esta solución es menos elegante y además tiene como incoveniente la necesidad de inicializar la variable de control, con lo que en caso de olvido, la ejecución puede ser impredecible. Como se sabe, las variables pueden tomar aleatoriamente cualquier valor inicial y dependiendo del mismo, se ejecutará o no la primera iteración. Para estas situaciones es muy aconsejable la utilización de la sentencia REPEAT que evita todos estos problemas.
También resulta adecuado el empleo del esquema REPEAT cuando solamente son válidos unos valores concretos para una determinada respuesta. Si la respuesta no es correcta se solicitará de nuevo y no se continuará hasta obtener una respuesta dentro de los valores válidos. La filosofía en este caso es prácticamente la misma del caso anterior, por ejemplo:
REPEAT
WriteString("¿Mes actual?");
ReadInt(mes)
UNTIL (mes >0) AND (mes <= 12);
Evidentemente la utilidad de la sentencia REPEAT no está ligada exclusivamente a los casos indicados. En general, es aconsejable su uso cuando se sepa que al menos es necesaria una iteración y por tanto utilizando la sentencia WHILE es necesario forzar las condiciones para que dicha iteración se produzca.
10.1.2 Sentencias LOOP y EXIT
Todas las estructuras iterativas explicadas hasta ahora se construyen mediante una única sentencia del lenguaje. Esto contribuye a que los programas resultantes sean fáciles de entender. La estructura LOOP que se explica en este apartado, sin embargo, emplea dos sentencias independientes para programar un esquema iterativo, que puede ser más complejo y, si no se programa con cierta disciplina, también más confuso. Por tanto, convendrá reservar el empleo de esta construcción para los casos que resulte artificioso emplear alguna de las otras sentencias iterativas explicadas anteriormente.
––
Figura 10.2. Bucle indefinido con salida intermedia
Con las sentencias WHILE y REPEAT se puede evaluar la condición para acabar el bucle de iteraciones bien al comienzo o ben al final de la ejecución de la secuencia de sentencias. Con frecuencia los bucles se pueden expresar de una de estas dos formas de manera sencilla. Sin embargo, hay ocasiones en que la condición para terminar las iteraciones se ha de examinar inevitablemente en un punto intermedio distinto al comienzo o final de la iteración. En este caso se tiene la situación que se muestra en la Figura 10.2.
Este tipo de condiciones están ligadas a causas que impiden una continuación normal de la iteración en curso y aconsejan abandonar dicha iteración. Por ejemplo cuando se obtiene un resultado parcial sin sentido: peso, volumen, etc. de valor negativo, que invalida los posibles cálculos posteriores. Además, dentro de este tipo de bucle de iteración suelen existir varias condiciones distintas en distintos puntos por las que se debe abandonar la ejecución del bucle.
Para facilitar la programación de esta estructura se dispone de una sentencia para programar un bucle indefinido sin expresar la condición de terminación, y otra sentencia distinta para poder salir de dicho bucle en el momento deseado. La sentencia de Modula-2 para el bucle indefinido es la siguiente:
LOOP Secuencia_de_sentencias END
que indica que se ejecute siempre de forma repetitiva e incondicional las sentencias agrupadas entre las palabras clave LOOP y END. Para poder finalizar el bucle indefinido es necesario que dentro del mismo exista alguna sentencia de salida. Esta sentencia en Modula-2 contiene simplemente la palabra clave:
EXIT
y su ejecución provoca la salida inmediata desde el interior del bucle indefinido del que ella misma forma parte. La ejecución del programa continúa con la sentencia inmediatamente a continuación del END de dicho bucle.
La sentencia EXIT es incondicional, es decir, no contiene en sí misma el examen de ninguna condición. En un esquema de iteración normal esta sentencia deberá estar anidada dentro de un esquema condicional. En caso contrario, sólo se ejecutaría la parte de la primera iteración anterior a la sentencia EXIT, y al llegar a ella se terminaría inmediatamente el bucle. Un esquema aproximado de la combinación LOOP – EXIT se muestra en la Figura 10.3.
––
Figura 10.3. Sentencias LOOP y EXIT
Una sentencia EXIT sólo se puede usar dentro de otra tipo LOOP y se produce un error de compilación cuando se trata de usar fuera de un LOOP.
Un ejemplo típico de esta estructura de iteración es:
LOOP
WriteString( "¿Mes Actual?");
RedInt(mes);
IF (mes > 0) AND (mes <= 12) THEN EXIT END;
WriteString( "Dato fuera de rango; repita" );
END
En este ejemplo se ha reescrito el bucle para leer como dato el número de un mes, hasta que se introduzca un dato correcto, y añadiendo la escritura de un mensaje de advertencia en el caso de que el dato introducido no sea aceptable.
Dado que existe la posibilidad de anidar sentencias, la sentencia EXIT puede estar anidada dentro de otra u otras sentencias iterativas WHILE, REPEAT o FOR anidadas a su vez dentro del bucle indefinido. Por ejemplo:
LOOP
……
FOR i := 1 TO 10 DO
……
WHILE x > 23 DO
……
IF z = 15 THEN EXIT END;
……
END; (* Final del WHILE *)
…..
END; (* Final del FOR *)
…..
END; (* Final del LOOP *)
Independientemente de la forma y el número de anidamientos, la ejecución de la sentencia EXIT provoca la salida y finalización inmediata del bucle indefinido que lo contenga, sin tener en cuenta para nada las otras sentencias. En el ejemplo anterior, cuando el valor de la variable z es igual a 15 al ejecutar la sentencia IF, se acaban todas las iteraciones incluidas dentro del bucle indefinido, aunque la variable x sea mayor de 23 y el índice de i no haya alcanzado todavía el valor 10.
Cuando se anidan dos o mas sentencias LOOP, cada sentencia EXIT se refiere siempre al bucle más interno de todos aquellos en los que pueda estar anidada. Por ejemplo:
LOOP (* Nivel 1º *)
…..
… EXIT … (* Salida del nivel 1º *)
LOOP (* Nivel 2º *)
…..
LOOP (* Nivel 3º *)
… EXIT … (* Salida del nivel 3º *)
……
END; (* Nivel 3º *)
……
… EXIT … (* Salida del nivel 2º *)
END (* Nivel 2º *)
……
END (* Nivel 1º *)
La posibilidad de emplear un número indeterminado de sentencias de salida y el efecto tan drástico que produce cada una de ellas, hace que resulte mucho más difícil de entender un programa en que se abuse de este tipo de estructura iterativa. En cualquier caso conviene evitar el uso de varios bucles indefinidos anidados.
Sólo es aconsejable utilizar esta estructura cuando resulte realmente apropiada (caso de manejo de errores o similar) o siempre que la utilización de las otras estructuras den lugar a un programa artificioso que pueda ser simplificado mediante el empleo de un bucle indefinido y sus correspondientes salidas.
En el apartado de programas completos se muestra un ejemplo para el manejo de errores sintácticos donde resulte más sencillo emplear un bucle indefinido que utilizar otro tipo de sentencia de iteración. El manejo de errores es un caso típico de tratamiento de excepciones según se vio en el apartado 8.3.2 del Tema 8. Las sentencias LOOP y EXIT constituyen un esquema muy adecuado para la programación del tratamiento de excepciones, cuando no interesa utilizar un procedimiento o función con múltiples sentencias de retorno.
10.2 Estructuras complementarias de selección
Para la selección entre varias alternativas es suficiente disponr de la sentencia IF, estudiada en el Tema 5. De hecho, existen lenguajes en los que la única sentencia disponible para la selección es la propuesta por la programación estructurada, que permite solamente la selección entre dos alternativas. La falta de claridad cuando se utilizan varias selecciones anidadas aconseja disponer de una sentencia de selección general como la estudiada en el Tema 5 para el lenguaje Modula-2.
Por las mismas razones de claridad y sencillez es habitual disponer de una sentencia que permite una selección por casos. Esta sentencia se conoce en la mayoría de los lenguajes con la palabra clave que la introduce: CASE. Este apartado está dedicado exclusivamente a dicha sentencia, estudiando la sintaxis y semántica que tiene en Modula-2.
10.2.1 Sentencia CASE
Cuando la selección entre varios casos alternativos depende del valor que toma una determinada variable o del resultado final de una expresión, es necesario realizar comparaciones de esa misma variable o expresión con todos los valores que pueda tomar, uno por uno, para decidir el camino a elegir. Así, en el programa para el cálculo del día de la semana del tema anterior con la variable M que guarda el mes teníamos:
IF M = Enero THEN IncreDias := 0
ELSIF M = Febrero THEN IncreDias := 3
ELSIF M = Marzo THEN IncreDias := 3
ELSIF M = Abril THEN IncreDias := 6
……
ELSIF M = Octubre THEN IncreDias := 0
ELSIF M = Noviembre THEN IncreDias := 3
ELSE IncreDias := 5
END;
lo que supone una sentencia larga y reiterativa, si se tiene en cuenta que además para algunos casos, por ejemplo los meses de Febrero, Marzo y Noviembre, se tiene que ejecutar la misma acción.
––
Figura 10.4. Selección por Casos
Si lo que se necesita es comparar el resultado de una expresión, es la misma expresión la que se debe evaluar tantas veces como comparaciones se deben realizar. En este caso y por razones de simplicidad y eficiencia es aconsejable guardar el resultado de la expresión en una variable auxiliar y realizar las comparaciones con la variable.
Si el tipo de valor que determina la selección es un tipo ordinal: INTEGER, CARDINAL, CHAR, enumerao o subrango, se dispone en Modula-2 de la sentencia CASE cuya estructura se muestra en la Figura 10.4, y en la que se agrupan los casos que tienen el mismo tratamiento y se evalúa solamente una vez la expresión x.
Las distintas vías de ejecución están asociadas a grupos de valores que pueda tomar la expresión o variable x: A con el valor v1, B con los valores de v2 a v4, C con los valores v5, v6 y v7 y establecieno como vía alternativa la acción H para el resto de los valores distintos de los anteriores.
El esquema de esta sentencia es:
CASE valor OF
valores : acción |
valores : acción |
. . . .
ELSE
acción por defecto
END
La sentencia comienza con la palabra clave CASE y a continuación se indica la expresión o variable cuyo valor fija los casos que se quieren analizar, seguida de la palabra clave OF. Para cada vía de ejecución posible se detallan primeramente los valores que debe tomar la variable, separados por comas (,). Estos valores también se pueden expresar en forma de subrango separados por dos puntos seguidos (..). La secuencia de sentencias que se deben ejecutar se detallan a continuación de los valores y separadas de estos por dos puntos (:). Las distintas vías alternativas de ejecución y sus correspondientes valores se separan unos de otros por el símbolo de barra (|). La alternativa para el resto de los valores es opcional, y va precedida de la palabra clave ELSE. La sentencia finaliza con la palabra clave END.
Esta sentencia no se puede utilizar cuando la variable o el resultado de la expresión que controla la selección sea del tipo REAL u otro tipo no simple como los conjuntos estudiados en el tema anterior. En estos casos no queda más remedio que emplear la sentencia de selección general.
Como ejemplo se reescribe la selección anterior, según el mes:
CASE M OF
Enero, Octubre : IncreDias := 0 |
Mayo : IncreDias := 1 |
Agosto : IncreDias := 2 |
Febrero .. Marzo, Noviembre : IncreDias := 3 |
Junio : IncreDias := 4 |
Septiembre, Diciembre : IncreDias := 5 |
ELSE
IncreDias := 6
END;
Evidentemente esta sentencia da lugar a un fragmento de programa más corto y fácil de entender.
En la sentencia CASE se deben incluir todos los posibles valores que pueda tomar la variable o expresión. Cuando se obtiene un valor que no está asociado a ninguna vía (y no hay alternativa ELSE), el programa finaliza por error. Si lo que sucede es que existen valores para los que no se debe realizar ninguna acción, entonces estos valores se deben declarar asociados a una secuencia de sentencias vacía. Por ejemplo:
CASE M OF
Enero..Mayo: |
Julio..Noviembre: |
Junio, Diciembre: Sueldo := Sueldo + Extra
END;
otra forma de conseguir esto mismo es mediante una alternativa ELSE vacía. Por ejemplo:
CASE M OF
Junio, Diciembre: Sueldo := Sueldo + Extra
ELSE
END;
La diferencia entre ambas es que en el primer caso se produciría un error que finalizaría la ejecución del programa si por cualquier causa la variable M toma un valor fuera del rango de mes declarado de Enero..Diciembre. En el segundo caso no se distingue la situación errónea de la que no lo es.
Si el programa está completamente probado, es muy probable que tal situación no se produzca nunca. Sin embargo, cuando un programa está todavía en fase de prueba es importante conocer todos los errores para analizar sus causas y corregirlos.
Por otro lado hay que tener en cuenta que un mismo valor nunca puede estar asociado a distintas alternativas dado que la ejecución resultaría ambigua. Este error puede producirse por una confusión, sobre todo cuando el mismo valor en un caso se indica dentro de un subrango y en otro de forma explícita. Por ejemplo:
CASE M OF
Enero, Octubre: IncreDias := 0 |
Mayo: IncreDias := 1 |
Agosto: IncreDias := 2 |
Febrero..Marzo, Noviembre IncreDias := 3 |
Junio: IncreDias := 4 |
Septiembre..Diciembre: IncreDias := 5 |
ELSE
IncreDias := 6
END;
Esta sentencia resulta ambigua para los meses de Octubre y Noviembre y es, por tanto, errónea.
Para acabar este apartado se detalla la sintasix formal de la sentencia CASE de Modula-2:
Sentencia_CASE ::= CASE Expresión OF
Caso { | Caso }
[ ELSE Secuencia_de_sentencias ]
END
Caso ::= Lista_de_valores : Secuencia_de_sentencias
Lista_de_valores ::= Valores { , Valores }
Valores ::= Expresión_constante [ .. Expresión_constante ]
El tipo del resultado de la Expresión y los Valores de cada caso deben ser compatibles.