Capítulo 4: control El control se refiere al orden en que se ejecutan los programas. Hay 3 tipos de mecanismos de control: - orden de evaluación de las expresiones - orden de ejecución de las instrucciones - invocación de procedimientos/funciones/subprogramas/métodos Evaluación de expresiones ------------------------- Hay dos tendencias - especificar el orden en que se evalúa una expresión. Por ejemplo, para evaluar una expresión exp en Java: · Si exp es básica, i.e. una constante, una variable, se entrega simplemente el valor asociado. . Si exp es de la forma exp1 operador exp2, entonces primero se evalúa exp1 y después exp2, excepto cuando el operador tiene evaluación condicional. . Si exp es una invocación de un método obj.met(arg0, arg1, ...), primero se evalúa la expresión obj, luego arg0, ..., etc. y por último se realiza la invocación. int a= 9; a += (a=3); // <=> a= a+ (a=3) siempre resulta en a= 12 · La ventaja es que los programas hacen lo mismo sin importar las optimizaciones que pueda hacer el compilador. · La desventaja es que se hace más difícil optimizar un programa: en ocasiones reodenar la evaluación de un programa puede resultar en programas más eficientes, pero esto debe hacerse sin alterar el resultado que daría el orden de evaluación especificado. - no especificar el orden en que se evalúa una expresión · Ejemplo en C: la expresión anterior puede ser tanto 12 como 6! · En Scheme tampoco existe una especificación para el orden de evaluación de argumentos de los procedimientos. · La ventaja es que es más fácil optimizar · La desventaja es que un programa puede entregar resultados distintos según el compilador utilizado. · Esta opción queda descartada en Java debido al objetivo "run once run anywhere". Definiciones - Evaluación impaciente (eager): los argumentos de una operación siempre se evalúan. Ejemplos: +, -, * - Evaluación perezosa (lazy): sólo se evalúan cuando se necesitan. Ejemplos en Java: &&, ||, ? · a && f(x), f(x) sólo se invoca si a es verdadera · a ? b=1 : b=2 sólo se ejecuta una de las asignaciones En lenguajes como C y Java esto se llama más bien evaluácion condicional. Obs.: - En Pascal la evaluación de los operandos and y or es impaciente, i.e. la expresión i0 goto 60 20 K= (I+J)/2 IF( A(K)-X) 40, 30, 50 30 BUSCAR= K RETURN 40 I= K+1 GOTO 10 50 J= K-1 GOTO 10 60 BUSCAR= -1 RETURN END Observaciones: + Los espacios en la zona del programa son superfluos + No hay indentación + Las columnas 73-80 se destinan para enumeración de las tarjetas perforadas. + No hay if/then/else o while + En Fortran IV se introduce: · IF (X.LE.10) GOTO 10 expresiones booleanas en el IF y GOTO · IF (X.GT.10) Y= 0.0 asignación en vez de GOTO · DO 100 I= 1, 100 Ciclos, el 100 indica el final ... 100 CONTINUE CONTINUE es la instrucción nula + Cuidado: DO 100 I= 1. 100 ¡es una asignación de la variable DO100I! + Ventajas: simple de explicar y entender la semántica + Desventajas: programas ilegibles - Programación estructuras: el goto se reemplaza por instrucciones de control estructurada que permiten escribir programas legibles. + En Pascal el código de los procedimientos se escribe en base a las abstracciones: · secuenciación: dadas las instrucciones I1 ; I2 primero se ejecuta I1 y luego I2 · alternancia: if (exp) I1 else I2 si exp es verdadera se ejecuta I1, si no I2 · iteración: while (exp) I mientras exp sea verdadera se ejecuta I · composición: begin I1 ; I2 ... In end se comporta como una instrucción + Se demuestra que cualquier programa no estructurado se puede escribir en forma estructurada + En un comienzo no se suprime goto completamente: C, C++ y PASCAL tienen goto, en Java goto es una palabra reservada, pero no existe la instrucción + Azucar sintáctico: for, do ... while. + Desventajas: burocrático con el manejo de errores. + Extensiones al programación estructura: · escapes léxicos: ejemplo en Java calculo: { ... if (...) break calculo; } ... // si la condición es verdadera se salta directamente acá · excepciones: escapes inter métodos. En Java throw/try/catch. En C setjmp/longjmp. Ejemplo: void P() { void Q() { ... ... try { if (...) ... throw new MyException(); Q(); ... (***) ... ... (*) ... } } catch (MyException e) { ... (**) ... } ... } Si ninguna excepción es lanzada, se ejecuta (*), (***) pero no (**) Cuando se lanza una excepción se realiza una búsqueda en la estructura de try´s en progreso de algún catch que calze con la excepción lanzada (un objeto de tipo Throwable). Entonces se ejecuta (**) pero no (*) ni (***) en el ejemplo. La búsqueda se hace desde el try más interno hacia el try más externo. Un catch (ExceptionType e) calza con un objeto lanzado o si se cumple o instanceof ExceptionType. · Lamentablemente en Java el manejo de excepciones es ineficiente, por lo que solo se usan en caso de errores. Invocación de procedimientos ---------------------------- - Definiciones invocación: f(x, y) declaración: double f(double a, double b) { ... } parámetros reales o argumentos: x e y parámetros formales o simplemente parámetros: a y b. - Semántica: Regla de la copia Una llamada a un procedimiento es equivalente a substituir la llamada por el código que implementa el procedimiento, reemplazando los parámetros por los argumentos. Problema: con llamadas recursivas el código es infinito, pero no es relevante porque sólo se trata de una semántica de referencia, no se implementa de esa forma. - Política de paso de parámetros: + Por referencia: el parámetro recibido es la variable pasada como argumento, no su valor. Ej.: void swap(int &a, int &b) { int tmp= a; a= b; b= tmp; } int x= 1, y= 2; <=> int x= 1, y= 2; swap (x, y); { int tmp0=x; x= y; y= tmp0; } (Las variables locales se renombran para evitar los conflictos.) Lenguajes: Pascal, C++ -> opcional Fortran -> siempre Problema: ¿qué hace swap(x+1, y)? Pascal, C++: error Fortran: crea z= x+1 e invoca swap(z, y) ¡silenciosamente! + Por valor: el parámetro es el valor del argumento, no la variable. con: void swap(int a, int b) ... la llamada es equivalente a: { int a0= x, b0= y; int tmp0= a0; a0= b0; b0= tmp0; } y por supuesto no sirve para intercambiar el valor de los argumentos. + Por valor/resultado: se pasan los valores, pero al final se copian de nuevo a los argumentos. con: void swap(in out int a, in out int b) { ... } // leng. hipotético { int a0= x, b0= y; int tmp0= a0; a0= b0; b0= tmp0; x= a0; y= b0; } + Por nombre: se substituye la expresión completa (en desuso). void compute(boolean x byname, double exp byname) { // leng. hipotético while (x) exp; } int i=1; double prod= 1; compute(i while (i error, no hay candidato Si #C= 1 => ese es el método que se invoca Si #C>1 => error, hay ambigüedad - Tipo estático de una expresión: el tipo que se determina en tiempo de compilación para esa expresión. - Tipo dinámico de una expresión: el tipo real en tiempo de ejecución Ej. Figura f[]= new Figura[] { new Circ(), new Linea(), new Text() }; for (int i= 0; i<3; i++) f[i].dibujar(); Para la iteración en que i= 1, tipo estátido de f[i]= Figura tipo dinámico de f[i]= Linea Siempre se cumple que tipo dinámico<=tipo estático - Enlace dinámico: En una invocación o.m(...) en donde o es una expresión, el método que se invoca en tiempo de ejecución es el que se defina para el tipo dinámico de o. => en el ejemplo, si tanto Figura y Linea definen dibujar, el que se invoca es el definido en Linea. Si Linea no definiese un dibujar entonces lo hereda de Figura y sería el método invocado.