Subido por RODRIGO DANIEL CORI GALVEZ

EXPRESIONES (1)

Anuncio
Expresiones y declaraciones
de asignación
7.1
Introducción
7.2
Expresiones aritméticas
7.3
Operadores sobrecargados
7.4
Conversiones de tipo
7.5
Expresiones relacionales y booleanas
7.6
Evaluación del cortocircuito
7.7
Declaraciones de asignación
7.8
Asignación de modo mixto
Como indica el título, el tema de este capítulo son las expresiones y las
sentencias de asignación. En primer lugar, se tratan las reglas semánticas que
determinan el orden de evaluación de los operadores en las expresiones. A
continuación, se explica
de un problema potencial con el orden de evaluación de los operandos cuando
las funciones pueden tener efectos secundarios. A continuación, se analizan los
operadores sobrecargados, tanto los predefinidos como los definidos por el
usuario, junto con sus efectos en las expresiones de los programas. A
continuación, se describen y evalúan las expresiones de modo mixto. Esto lleva
a la definición y evaluación de las conversiones de tipo de ampliación y
reducción, tanto implícitas como explícitas. A continuación, se discuten las
expresiones relacionales y booleanas, incluyendo el proceso de evaluación de
cortocircuito.
Por último, se cubre la sentencia de asignación, desde su forma más simple
hasta todas sus variaciones, incluyendo las asignaciones como expresiones y
las asignaciones de modo mixto.
Las expresiones de concordancia de patrones de cadenas de caracteres se
trataron como parte del material sobre cadenas de caracteres en el capítulo 6,
por lo que no se mencionan en este capítulo.
7.1
Introducción
Las expresiones son el medio fundamental para especificar los cálculos en un
lenguaje de programación. Es crucial para un programador entender tanto la
sintaxis como la semántica de las expresiones del lenguaje que utiliza. En el
capítulo 3 se presentó un mecanismo formal (BNF) para describir la sintaxis de
las expresiones. En este capítulo se analiza la semántica de las expresiones.
Para entender la evaluación de expresiones, es necesario conocer los órdenes
de evaluación de operadores y operandos. El orden de evaluación de los
operadores en las expresiones viene dictado por las reglas de asociatividad y
precedencia del lenguaje. Aunque el valor de una expresión a veces depende de
ello, el orden de evaluación de los operandos en las expresiones a menudo no
es indicado por los diseñadores del lenguaje. Esto permite a los
implementadores elegir el orden, lo que lleva a la posibilidad de que los
programas produzcan resultados diferentes en diferentes implementaciones.
Otros problemas en la semántica de las expresiones son los desajustes de tipo,
las coerciones y la evaluación en cortocircuito.
La esencia de los lenguajes de programación imperativos es el papel dominante
de las sentencias de asignación. El propósito de estas sentencias es causar el
efecto secundario de cambiar los valores de las variables, o el estado, del
programa. Así que una parte integral de todos los lenguajes imperativos es el
concepto de variables cuyos valores cambian durante la ejecución del programa.
Los lenguajes funcionales utilizan variables de otro tipo, como los parámetros de
las funciones. Estos lenguajes también tienen declaraciones que vinculan
valores a nombres. Estas declaraciones son similares a las declaraciones de
asignación, pero no tienen efectos secundarios.
7.2
Expresiones aritméticas
La evaluación automática de expresiones aritméticas similares a las de la
matemática, la ciencia y la ingeniería fue uno de los principales objetivos de los
primeros lenguajes de programación de alto nivel. La mayoría de las
características de la aritmética
Las expresiones en los lenguajes de programación se heredaron de las
convenciones que habían evolucionado en las matemáticas. En los lenguajes de
programación, las expresiones aritméticas constan de operadores, operandos,
paréntesis y llamadas a funciones. Un operador puede ser unario, es decir, con
un solo operando, binario, es decir, con dos operandos, o ternario, es decir,
con tres operandos.
En la mayoría de los lenguajes de programación, los operadores binarios son
infijos, lo que significa que aparecen entre sus operandos. Una excepción es
Perl, que tiene algunos operadores que son prefijos, lo que significa que
preceden a sus operandos.
El propósito de una expresión aritmética es especificar una computación
aritmética. Una implementación de tal computación debe causar dos acciones:
obtener los operandos, normalmente de la memoria, y ejecutar operaciones
aritméticas en esos operandos. En las siguientes secciones, investigamos los
detalles de diseño comunes de las expresiones aritméticas.
A continuación se exponen los principales problemas de diseño de las
expresiones aritméticas, todos ellos analizados en esta sección:
●
¿Cuáles son las reglas de precedencia de los operadores?
●
●
●
●
●
¿Cuáles son las reglas de asociatividad de los operadores?
¿Cuál es el orden de evaluación de los operandos?
¿Existen restricciones en los efectos secundarios de la evaluación de
operandos?
¿Permite el lenguaje la sobrecarga de operadores definida por el
usuario?
¿Qué mezcla de tipos se permite en las expresiones?
7.2.1 Orden de evaluación del operador
Las reglas de precedencia y asociatividad de los operadores de un lenguaje
dictan el orden de evaluación de sus operadores.
7.2.1.1
Precedencia
El valor de una expresión depende, al menos en parte, del orden de evaluación
de los operadores en la expresión. Considere la siguiente expresión:
a+b*c
Supongamos que las variables a, b y c tienen los valores 3, 4 y 5,
respectivamente. Si se evalúa de izquierda a derecha (primero la suma y luego
la multiplicación), el resultado es 35. Si se evalúa de derecha a izquierda, el
resultado es 23.
En lugar de evaluar simplemente los operadores de una expresión de izquierda
a derecha o de derecha a izquierda, los matemáticos desarrollaron hace tiempo
el concepto de colocar los operadores en una jerarquía de prioridades de
evaluación y basar el orden de evaluación de las expresiones en parte en esta
jerarquía. Por ejemplo, en matemáticas, la multiplicación se considera más
prioritaria que la suma, quizás debido a su mayor nivel de complejidad. Si esa
convención se aplicará en la expresión del ejemplo anterior, como sería el caso
en la mayoría de los lenguajes de programación, la multiplicación se haría
primero.
Las reglas de precedencia de operadores para la evaluación de expresiones
definen parcialmente el orden en que se evalúan los operadores de diferentes
niveles de precedencia. Las reglas de precedencia de los operadores para las
expresiones se basan en la jerarquía de las prioridades de los operadores,
según el diseñador del lenguaje. Las reglas de precedencia de operadores de
los lenguajes imperativos comunes son casi todas iguales, porque se basan en
las de las matemáticas. En estos lenguajes, la exponenciación tiene la mayor
precedencia (cuando es proporcionada por el lenguaje), seguida de la
multiplicación y la división en el mismo nivel, seguida de la suma y la resta
binarias en el mismo nivel.
Muchos lenguajes también incluyen versiones unarias de la suma y la resta. La
suma unaria se denomina operador de identidad porque normalmente no tiene
ninguna operación asociada y, por tanto, no tiene ningún efecto sobre su
operando. Ellis y Stroustrup (1990, p. 56), hablando de C++, lo llaman un
accidente histórico y lo califican correctamente de inútil. El menos unario, por
supuesto, cambia el signo de su operando. En Java y C#, el menos unario
también provoca la conversión implícita de los operandos short y byte a tipo int.
En todos los lenguajes imperativos comunes, el operador unario menos puede
aparecer en una expresión, ya sea al principio o en cualquier parte de la misma,
siempre que esté entre paréntesis para evitar que esté junto a otro operador. Por
ejemplo,
a + (- b) * c
es legal, pero
a+-b*c
normalmente no lo es.
A continuación, considera las siguientes expresiones:
●
●
●
a/b
a*b
a ** b
En los dos primeros casos, la precedencia relativa del operador menos unario y
del operador binario es irrelevante: el orden de evaluación de los dos
operadores no tiene ningún efecto sobre el valor de la expresión. En el último
caso, sin embargo, sí importa.
De los lenguajes de programación comunes, sólo Fortran, Ruby, Visual Basic y
Ada tienen el operador de exponenciación. En los cuatro, la exponenciación
tiene mayor precedencia que el menos unario, por lo que
●
A ** B
equivale a
●
(A ** B)
La precedencia es sólo una parte de las reglas para el orden de evaluación de
los operadores; las reglas de asociatividad también la afectan.
7.2.1.2
Asociatividad
Considera la siguiente expresión:
a-b+c-d
Si los operadores de suma y resta tienen el mismo nivel de precedencia, como
ocurre en los lenguajes de programación, las reglas de precedencia no dicen
nada sobre el orden de evaluación de los operadores en esta expresión.
Cuando una expresión contiene dos ocurrencias adyacentes de operadores con
el mismo nivel de precedencia, la cuestión de qué operador se evalúa primero
se responde con las reglas de asociatividad del lenguaje. Un operador puede
tener asociatividad izquierda o derecha, lo que significa que cuando hay dos
operadores adyacentes con la misma precedencia, se evalúa primero el
operador izquierdo o el derecho, respectivamente.
La asociatividad en los lenguajes comunes es de izquierda a derecha, excepto
que el operador de exponenciación (cuando se proporciona) a veces asocia de
derecha a izquierda. En la expresión de Java
a-b+c
El operador de la izquierda se evalúa primero.
La exponenciación en Fortran y Ruby es asociativa a la derecha, por lo que en
la expresión
A ** B ** C
El operador derecho se evalúa primero.
En Visual Basic, el operador de exponenciación, ^, es asociativo a la izquierda.
Las reglas de asociatividad para algunos lenguajes comunes se dan aquí:
Regla de asociatividad lingüística
Lenguajes basados en C Izquierda: *, /, %, binario +, binario A la derecha: ++, --, unario -, unario +
Como se indica en el apartado 7.2.1.1, en APL todos los operadores tienen el
mismo nivel de precedencia. Por lo tanto, el orden de evaluación de los
operadores en las expresiones APL está determinado completamente por la
regla de asociatividad, que es de derecha a izquierda para todos los operadores.
Por ejemplo, en la expresión
A×B+C
el operador de adición se evalúa primero, seguido del operador de multiplicación
( * es el operador de multiplicación de APL). Si A fuera 3, B fuera 4 y C fuera 5,
entonces el valor de esta expresión APL sería 27.
Muchos compiladores para los lenguajes comunes hacen uso del hecho de que
algunos operadores aritméticos son matemáticamente asociativos, lo que
significa que las reglas de asociatividad no tienen impacto en el valor de una
expresión que contenga sólo esos operadores. Por ejemplo, la suma es
matemáticamente asociativa, por lo que en matemáticas el valor de la expresión
A+B+C
no depende del orden de evaluación del operador. Si las operaciones de punto
flotante para las operaciones matemáticas asociativas fueran también
asociativas, el compilador podría utilizar este hecho para realizar algunas
optimizaciones simples. En concreto, si el compilador puede reordenar la
evaluación de los operadores, podría producir un código ligeramente más rápido
para la evaluación de expresiones. Los compiladores suelen realizar este tipo de
optimizaciones.
Por desgracia, en un ordenador, tanto las representaciones en coma flotante
como las operaciones aritméticas en coma flotante son sólo aproximaciones de
sus homólogos matemáticos (debido a las limitaciones de tamaño). El hecho de
que un operador matemático sea asociativo no implica necesariamente que la
correspondiente operación en coma flotante sea asociativa. De hecho, sólo si
todos los operandos y resultados intermedios pueden representarse
exactamente en notación de punto flotante, el proceso será precisamente
asociativo. Por ejemplo, hay situaciones patológicas en el que la suma de
enteros en un ordenador no es asociativa. Por ejemplo, supongamos que un
programa debe evaluar la expresión
A+B+C+D
y que A y C son números positivos muy grandes, y B y D son números negativos
con valores absolutos muy grandes. En esta situación, añadir B a A no provoca
una excepción de desbordamiento, pero añadir C a A sí. Del mismo modo, la
adición de C a B no provoca un desbordamiento, pero la adición de D a B sí.
Debido a las limitaciones de la aritmética computacional, la suma es
catastróficamente no asociativa en este caso. Por lo tanto, si el compilador
reordena estas operaciones de adición, afecta al valor de la expresión. Este
problema, por supuesto, puede ser evitado por el programador, asumiendo que
los valores aproximados de las variables son conocidos. El programador puede
especificar la expresión en dos partes (en dos sentencias de asignación),
asegurándose de evitar el desbordamiento. Sin embargo, esta situación puede
surgir de formas mucho más sutiles, en las que es menos probable que el
programador se dé cuenta de la dependencia de orden.
7.2.1.3
Paréntesis
Los programadores pueden alterar las reglas de precedencia y asociatividad
colocando paréntesis en las expresiones. Una parte con paréntesis de una
expresión tiene precedencia sobre sus partes adyacentes sin paréntesis. Por
ejemplo, aunque la multiplicación tiene precedencia sobre la suma, en la
expresión
(A + B) * C
la suma se evaluará primero. Matemáticamente, esto es perfectamente natural.
En esta expresión, el primer operando del operador de multiplicación no está
disponible hasta que se evalúe la adición en la subexpresión entre paréntesis.
Además, la expresión del apartado 7.2.1.2 podría especificarse como
(A + B) + (C + D)
para evitar el desbordamiento.
Los lenguajes que permiten los paréntesis en las expresiones aritméticas
podrían prescindir de todas las reglas de precedencia y asociar simplemente
todos los operadores de izquierda a derecha o de derecha a izquierda. El
programador especificaría el orden de evaluación deseado con paréntesis. Este
enfoque sería sencillo porque ni el autor ni los lectores de los programas
tendrían que recordar ninguna regla de precedencia o asociatividad. La
desventaja de este esquema es que hace más tediosa la escritura de
expresiones, y también compromete seriamente la legibilidad del código. Sin
embargo, esta fue la elección de Ken Iverson, el diseñador de APL.
7.2.1.4
Expresiones condicionales
Las sentencias if-then-else pueden utilizarse para realizar una asignación de
expresión condicional. Por ejemplo, considere
si (cuenta == 0) media = 0;
si no
media = suma / recuento;
En los lenguajes basados en C, este código se puede especificar más
convenientemente en una sentencia de asignación utilizando una expresión
condicional, que tiene la siguiente forma:
expresión_1 ? expresión_2 : expresión_3
donde expresión_1 se interpreta como una expresión booleana. Si la
expresión_1 se evalúa como verdadera, el valor de la expresión completa es el
valor de la expresión_2; en caso contrario, es el valor de la expresión_3. Por
ejemplo, el efecto del ejemplo if-then-else puede conseguirse con la siguiente
sentencia de asignación, utilizando una expresión condicional:
media = (cuenta == 0) ? 0 : suma / recuento;
En efecto, el signo de interrogación denota el comienzo de la cláusula then, y
los dos puntos marcan el comienzo de la cláusula else. Ambas cláusulas son
obligatorias. Tenga en cuenta que ? Se utiliza en las expresiones condicionales
como operador ternario.
Las expresiones condicionales pueden utilizarse en cualquier parte de un
programa (en un lenguaje basado en C) donde pueda utilizarse cualquier otra
expresión. Además de los lenguajes basados en C, las expresiones
condicionales se ofrecen en Perl, JavaScript y Ruby.
7.2.2 Orden de evaluación del operando
Una característica de diseño de las expresiones que se discute menos es el
orden de evaluación de los operandos. Las variables de las expresiones se
evalúan obteniendo sus valores de la memoria. Las constantes a veces se
evalúan de la misma manera. En otros casos, una constante puede ser parte de
la instrucción del lenguaje de máquina y no requerir una búsqueda en memoria.
Si un operando es una expresión entre paréntesis, todos los operadores que
contiene deben ser evaluados antes de que su valor pueda ser utilizado como
operando.
Si ninguno de los operandos de un operador tiene efectos secundarios, el orden
de evaluación de los operandos es irrelevante. Por lo tanto, el único caso
interesante se produce cuando la evaluación de un operando tiene efectos
secundarios.
7.2.2.1
Efectos secundarios
Un efecto secundario de una función, naturalmente llamado efecto secundario
funcional, ocurre cuando la función cambia uno de sus parámetros o una
variable global. (Una variable global se declara fuera de la función pero es
accesible en la función).
Considera la siguiente expresión:
a + fun(a)
Si fun no tiene el efecto secundario de cambiar a, entonces el orden de
evaluación de los dos operandos, a y fun(a), no tiene efecto sobre el valor de la
expresión. Sin embargo, si fun cambia a, hay un efecto. Consideremos la
siguiente situación: fun devuelve 10 y cambia el valor de su parámetro a 20.
Supongamos que tenemos lo siguiente:
a = 10;
b = a + fun(a);
Entonces, si el valor de a se obtiene primero (en el proceso de evaluación de la
expresión), su valor es 10 y el valor de la expresión es 20. Pero si el segundo
operando se evalúa primero, entonces el valor del primer operando es 20 y el
valor de la expresión es 30.
El siguiente programa en C ilustra el mismo problema cuando una función
cambia una variable global que aparece en una expresión:
int a = 5;
int fun1() {
a = 17;
volver 3;
}/* fin de fun1 */
void main() {
a = a + fun1();
}/* fin de main */
El valor calculado para a en main depende del orden de evaluación de los
operandos en la expresión a + fun1(). El valor de a será 8 (si a se evalúa
primero) o 20 (si la llamada a la función se evalúa primero).
Obsérvese que las funciones en matemáticas no tienen efectos secundarios,
porque no existe la noción de variables en matemáticas. Lo mismo ocurre con
los lenguajes de programación funcionales. Tanto en las matemáticas como en
los lenguajes de programación funcional, las funciones son mucho más fáciles
de razonar y entender que las de los lenguajes imperativos, porque su contexto
es irrelevante para su significado.
Hay dos posibles soluciones al problema del orden de evaluación de los
operandos y los efectos secundarios. En primer lugar, el diseñador del lenguaje
podría impedir que la evaluación de la función afecte al valor de las expresiones,
simplemente desestimando los efectos secundarios de la función. En segundo
lugar, la definición del lenguaje podría establecer que los operandos de las
expresiones deben evaluarse en un orden determinado y exigir que los
implementadores garanticen ese orden.
No permitir los efectos secundarios funcionales en los lenguajes imperativos es
difícil, y elimina cierta flexibilidad para el programador. Consideremos el caso de
C y C++, que sólo tienen funciones, lo que significa que todos los subprogramas
devuelven un valor. Para eliminar los efectos secundarios de los parámetros
bidireccionales y seguir proporcionando subprogramas que devuelvan más de
un valor, habría que colocar los valores en una estructura y devolver la
estructura. El acceso a los globales en las funciones también tendría que ser
desautorizado. Sin embargo, cuando la eficiencia es importante, utilizar el
acceso a variables globales para evitar el paso de parámetros es un método
importante para aumentar la velocidad de ejecución. En los compiladores, por
ejemplo, el acceso global a datos como la tabla de símbolos es habitual.
El problema de tener un orden de evaluación estricto es que algunas técnicas de
optimización de código utilizadas por los compiladores implican la reordenación
de las evaluaciones de los operandos. Un orden garantizado no permite esos
métodos de optimización cuando hay llamadas a funciones. Por lo tanto, no
existe una solución perfecta, como demuestran los diseños de lenguajes reales.
La definición del lenguaje Java garantiza que los operandos aparezcan
evaluados en orden de izquierda a derecha, eliminando el problema discutido en
esta sección.
7.2.2.2
Transparencia referencial y efectos secundarios
El concepto de transparencia referencial está relacionado con los efectos
secundarios funcionales y se ve afectado por ellos. Un programa tiene la
propiedad de transparencia referencial si dos expresiones cualesquiera del
programa que tengan el mismo valor pueden ser sustituidas la una por la otra en
cualquier parte del programa, sin afectar a la acción del programa. El valor de
una función con transparencia referencial depende totalmente de sus
parámetros. La conexión entre la transparencia referencial y los efectos
secundarios funcionales se ilustra con el siguiente ejemplo:
resultado1 = (fun(a) + b) / (fun(a) - c); temp = fun(a);
resultado2 = (temp + b) / (temp - c);
Si la función fun no tiene efectos secundarios, el resultado1 y el resultado2
serán iguales, porque las expresiones asignadas a ellos son equivalentes. Sin
embargo, supongamos que fun tiene el efecto secundario de añadir 1 a b o a c.
Entonces el resultado1 no sería igual al resultado2. Entonces, ese efecto
secundario viola la transparencia referencial del programa en el que aparece el
código.
Los programas transparentes desde el punto de vista de la referencia tienen
varias ventajas. La más importante es que la semántica de estos programas es
mucho más fácil de entender que la de los programas que no son transparentes
desde el punto de vista de la referencia. La transparencia referencial hace que
una función sea equivalente a una función matemática, en términos de facilidad
de comprensión.
Al no tener variables, los programas escritos en lenguajes funcionales puros son
transparentes desde el punto de vista referencial. Las funciones en un lenguaje
funcional puro no pueden tener estado, que se almacenaría en variables locales.
Si una función de este tipo utiliza un valor de fuera de la función, ese valor debe
ser una constante, ya que no hay variables. Por lo tanto, el valor de la función
depende de los valores de sus parámetros.
La transparencia referencial se analizará con más detalle en el capítulo 15.
7.3
Operadores sobrecargados
Los operadores aritméticos se utilizan a menudo para más de un propósito. Por
ejemplo, + suele utilizarse para especificar la suma de enteros y la suma de
puntos flotantes. Algunos lenguajes -Java, por ejemplo- también lo utilizan para
la catenización de cadenas. Este uso múltiple de un operador se denomina
sobrecarga de operadores y, en general, se considera aceptable, siempre que
no se resienta la legibilidad ni la fiabilidad.
Como ejemplo de los posibles peligros de la sobrecarga, considere el uso del
ampersand (& ) en C++. Como operador binario, especifica una operación lógica
AND a nivel de bits. Como operador unario, sin embargo, su significado es
totalmente diferente. Como operador unario con una variable como operando, el
valor de la expresión es la dirección de esa variable. En este caso, el
ampersand se llama operador de dirección. Por ejemplo, la ejecución de
x = &y;
hace que la dirección de y se coloque en x. Hay dos problemas con este uso
múltiple del ampersand. En primer lugar, el uso del mismo símbolo para dos
operaciones completamente no relacionadas es perjudicial para la legibilidad. En
segundo lugar, el simple error de tecleado de omitir el primer operando para una
operación AND a nivel de bits puede no ser detectado por el compilador, porque
se interpreta como un operador de dirección. Este error puede ser difícil de
diagnosticar.
Prácticamente todos los lenguajes de programación tienen un problema menos
grave pero similar, que a menudo se debe a la sobrecarga del operador menos.
El problema es sólo que el compilador no puede saber si el operador debe ser
binario o unario. 5 Así que, una vez más, el compilador no puede detectar como
error el hecho de no incluir el primer operando cuando el operador se supone
que es binario. Sin embargo, los significados de las dos operaciones, unaria y
binaria, están al menos estrechamente relacionados, por lo que la legibilidad no
se ve afectada negativamente.
Algunos lenguajes que admiten tipos de datos abstractos (véase el capítulo 11),
por ejemplo, C++, C# y F#, permiten al programador sobrecargar aún más los
símbolos de los operadores. Por ejemplo, supongamos que un usuario quiere
definir el operador * entre un entero escalar y un array de enteros para significar
que cada elemento del array debe ser multiplicado por el escalar. Este operador
podría definirse escribiendo un subprograma de función llamado * que realice
esta nueva operación. El compilador elegirá el significado correcto cuando se
especifique un operador sobrecargado, basándose en los tipos de los
operandos, como ocurre con los operadores sobrecargados definidos por el
lenguaje. Por ejemplo, si esta nueva definición de * se define en un programa de
C#, el compilador de C# utilizará la nueva definición de * siempre que el
operador * aparezca con un entero simple como operando izquierdo y una
matriz de enteros como operando derecho. Cuando se utiliza con sensatez, la
sobrecarga de operadores definida por el usuario puede ayudar a la legibilidad.
Por ejemplo, si + y * están sobrecargados para un tipo de datos abstracto de
matriz y A, B, C y D son variables de ese tipo, entonces
A*B+C*D
puede utilizarse en lugar de
MatrixAdd(MatrixMult(A, B), MatrixMult(C, D))
Por otro lado, la sobrecarga definida por el usuario puede ser perjudicial para la
legibilidad. Por un lado, nada impide que un usuario defina + para significar
multiplicación. Además, al ver un operador * en un programa, el lector debe
encontrar tanto los tipos de los operandos como la definición del operador para
determinar su significado. Cualquiera de estas definiciones o todas ellas podrían
estar en otros archivos.
Otra consideración es el proceso de construcción de un sistema de software a
partir de módulos creados por diferentes grupos. Si los distintos grupos
sobrecargan los mismos operadores de forma diferente, es obvio que habrá que
eliminar estas diferencias antes de montar el sistema.
7.4
Conversiones de tipo
Las conversiones de tipos son de estrechamiento o de ampliación. Una
conversión de estrechamiento convierte un valor a un tipo que no puede
almacenar ni siquiera aproximaciones de todos los valores del tipo original. Por
ejemplo, convertir un double a un float en Java es una conversión de
estrechamiento, porque el rango de double es mucho mayor que el de float.
Una conversión de ampliación convierte un valor a un tipo que puede incluir al
menos aproximaciones de todos los valores del tipo original. Por ejemplo,
convertir un int en un float en Java es una conversión de ampliación. Las
conversiones de ampliación son casi siempre seguras, lo que significa que se
mantiene la magnitud aproximada del valor convertido. Las conversiones de
estrechamiento no siempre son seguras: a veces la magnitud del valor
convertido cambia en el proceso. Por ejemplo, si el valor de punto flotante
1,3E25 se convierte en un entero en un programa Java, el resultado no tendrá
ninguna relación con el valor original. Aunque las conversiones de
ensanchamiento suelen ser seguras, pueden dar lugar a una reducción de la
precisión. En muchas implementaciones de lenguajes, aunque las conversiones
de enteros a punto flotante son conversiones de ampliación, se puede perder
algo de precisión. Por ejemplo, en muchos casos, los enteros se almacenan en
32 bits, lo que permite al menos 9 dígitos decimales de precisión. Pero los
valores en coma flotante también se almacenan en 32 bits, con sólo unos siete
dígitos decimales de precisión (debido al espacio utilizado para el exponente).
Por tanto, la ampliación de enteros a punto flotante puede provocar la pérdida
de dos dígitos de precisión.
Las coerciones de los tipos no primitivos son, por supuesto, más complejas. En
el capítulo 5, se discutieron las complicaciones de la compatibilidad de
asignación de los tipos de array y de registro. También está la cuestión de qué
tipos de parámetros y tipos de retorno de un método le permiten anular un
método de una superclase, sólo cuando los tipos son los mismos, o también
algunas otras situaciones. Esa cuestión, así como el concepto de subclases
como subtipos, se tratan en el capítulo 12.
Las conversiones de tipo pueden ser explícitas o implícitas. En las dos
siguientes subsecciones se analizan estos tipos de conversiones de tipo.
7.4.1 Coerción en las expresiones
Una de las decisiones de diseño relativas a las expresiones aritméticas es si un
operador puede tener operandos de diferentes tipos. Los lenguajes que
permiten este tipo de expresiones, que se denominan expresiones de modo
mixto, deben definir convenciones para las conversiones implícitas de tipos de
operandos porque los ordenadores no tienen operaciones binarias que tomen
operandos de diferentes tipos. La coerción se definió como una conversión de
tipos implícita que es iniciada por el compilador o el sistema de tiempo de
ejecución. Las conversiones de tipo solicitadas explícitamente por el
programador se denominan conversiones explícitas, o castings, y no coerciones.
Aunque algunos símbolos de operador pueden estar sobrecargados,
suponemos que un sistema informático, ya sea en hardware o en algún nivel de
simulación de software, tiene una operación para cada tipo de operando y
operador definido en el lenguaje. 6 En el caso de los operadores sobrecargados en
un lenguaje que utiliza la vinculación estática de tipos, el compilador elige el tipo
correcto de operación basándose en los tipos de los operandos. Cuando los dos
operandos de un operador no son del mismo tipo y eso es legal en el lenguaje,
el compilador debe elegir uno de ellos para ser coercionado y generar el código
para esa coerción. En la siguiente discusión, se examinan las opciones de
diseño de coerción de varios lenguajes comunes.
Los diseñadores de lenguajes no están de acuerdo con la cuestión de las
coerciones en las expresiones aritméticas. Los que están en contra de una
amplia gama de coerciones están preocupados por los problemas de fiabilidad
que pueden resultar de dichas coerciones, porque reducen los beneficios de la
comprobación de tipos. Los que prefieren incluir un amplio rango de coerciones
están más preocupados por la pérdida de flexibilidad que resulta de las
restricciones. La cuestión es si los programadores deben preocuparse por esta
categoría de errores o si el compilador debe detectarlos.
Como simple ilustración del problema, considere el siguiente código Java:
int a;
float b, c, d;
...
d = b * a;
Supongamos que el segundo operando del operador de multiplicación debía ser
c, pero debido a un error de tecleado se tecleó como a. Como las expresiones
de modo mixto son legales en Java, el compilador no detectaría esto como un
error. Simplemente insertaría código para coaccionar el valor del operando int, a,
a float. Si las expresiones de modo mixto no fueran legales en Java, el
compilador habría detectado este error de tecleado como un error de tipo.
Dado que la detección de errores se reduce cuando se permiten expresiones de
modo mixto, F# y ML no las permiten. Por ejemplo, no permiten mezclar
operandos enteros y de punto flotante en las expresiones.
En la mayoría de los demás lenguajes comunes, no hay restricciones para las
expresiones aritméticas de modo mixto.
Los lenguajes basados en C tienen tipos enteros más pequeños que el tipo int.
En Java, estos son byte y short. Los operandos de todos estos tipos se
convierten en int cuando se les aplica prácticamente a cualquier operador. Por
lo tanto, aunque los datos pueden ser almacenados en variables de estos tipos,
no pueden ser manipulados antes de la conversión a un tipo mayor. Por
ejemplo, considere el siguiente código Java:
byte a, b, c;
...
a = b + c;
Los valores de b y c se coaccionan a int y se realiza una suma de int. A
continuación, la suma se convierte a byte y se coloca en a. Dado el gran
tamaño de las memorias de los ordenadores contemporáneos, hay pocos
incentivos para utilizar byte y short, a menos que haya que almacenar un gran
número de ellos.
7.4.2 Conversión explícita de tipos
La mayoría de los lenguajes proporcionan alguna capacidad para realizar
conversiones explícitas, tanto de ampliación como de reducción. En algunos
casos, se producen mensajes de advertencia cuando una conversión explícita
de estrechamiento provoca un cambio significativo en el valor del objeto que se
está convirtiendo. En los lenguajes basados en C, las conversiones de tipo
explícitas se llaman castings. Para especificar una conversión, el tipo deseado
se coloca entre paréntesis
justo antes de la expresión a convertir, como en
(int) ángulo
Una de las razones de los paréntesis alrededor del nombre del tipo en estas
conversiones es que el primero de estos lenguajes, C, tiene varios nombres de
tipo de dos palabras, como long int.
En ML y F#, los castings tienen la sintaxis de las llamadas a funciones. Por
ejemplo, en F# podríamos tener lo siguiente:
float(suma)
7.4.3 Errores en las expresiones
Durante la evaluación de una expresión pueden producirse varios errores. Si el
lenguaje requiere una comprobación de tipo, ya sea estática o dinámica,
entonces no pueden producirse errores de tipo de operando. Los errores que
pueden ocurrir debido a las coerciones de los operandos en las expresiones ya
han sido discutidos. Los otros tipos de errores se deben a las limitaciones de la
aritmética del ordenador y a las limitaciones inherentes a la aritmética. El error
más común ocurre cuando el resultado de una operación no puede ser
representado en la celda de memoria donde debe ser almacenado. Esto se
denomina desbordamiento o subdesbordamiento, dependiendo de si el
resultado es demasiado grande o demasiado pequeño. Una limitación de la
aritmética es que la división por cero no está permitida. Por supuesto, el hecho
de que no esté permitida matemáticamente no impide que un programa intente
hacerla.
El desbordamiento de punto flotante, el desbordamiento por debajo de la línea y
la división por cero son ejemplos de errores en tiempo de ejecución, que a veces
se llaman excepciones. Las facilidades del lenguaje que permiten a los
programas detectar y tratar las excepciones se discuten en el Capítulo 14.
7.5
Expresiones relacionales y booleanas
Además de las expresiones aritméticas, los lenguajes de programación admiten
expresiones relacionales y booleanas.
7.5.1 Expresiones relacionales
Un operador relacional es un operador que compara los valores de sus dos
operandos. Una expresión relacional tiene dos operandos y un operador
relacional. El valor de una expresión relacional es booleano, excepto cuando
booleano no es un tipo incluido en el lenguaje. Los operadores relacionales
suelen estar sobrecargados para una variedad de tipos. La operación que
determina la verdad o la falsedad de un
La expresión relacional depende de los tipos de operandos. Puede ser simple,
como en el caso de los operandos enteros, o compleja, como en el caso de los
operandos de cadenas de caracteres. Normalmente, los tipos de operandos que
pueden utilizarse para los operadores relacionales son los tipos numéricos, las
cadenas y los tipos de enumeración.
La sintaxis de los operadores relacionales para la igualdad y la desigualdad
difiere entre algunos lenguajes de programación. Por ejemplo, para la
desigualdad, los lenguajes basados en C utilizan !=, Lua utiliza ~=, Fortran 95+
utiliza .NE. o <> , y ML y F# utilizan <>. JavaScript y PHP tienen dos operadores
relacionales adicionales, === y !==. Son similares a sus parientes, == y !=, pero
evitan que sus operandos sean coaccionados. Por ejemplo, la expresión
"7" == 7
es cierto en JavaScript, porque cuando una cadena y un número son los
operandos de un operador relacional, la cadena se convierte en un número. Sin
embargo,
"7" === 7
es falso, porque no se realiza ninguna coerción sobre los operandos de este
operador.
Ruby utiliza == para el operador relacional de igualdad que utiliza coerciones, y
eql? para la igualdad sin coerciones. Ruby usa === sólo en la cláusula when de
su sentencia case, como se discute en el Capítulo 8.
Los operadores relacionales tienen siempre una precedencia menor que los
operadores aritméticos, por lo que en expresiones como
a+1>2*b
las expresiones aritméticas se evalúan primero.
7.5.2 Expresiones booleanas
Las expresiones booleanas están formadas por variables booleanas, constantes
booleanas, expresiones relacionales y operadores booleanos. Los operadores
suelen incluir los de las operaciones AND, OR y NOT, y a veces los de OR
exclusivo y equivalencia. Los operadores booleanos suelen tomar sólo
operandos booleanos (variables booleanas, literales booleanos o expresiones
relacionales) y producen valores booleanos.
En las matemáticas de las álgebras booleanas, los operadores OR y AND deben
tener la misma precedencia. Sin embargo, los lenguajes basados en C asignan
una mayor precedencia a AND que a OR. Tal vez esto se deba a la correlación
sin fundamento de la multiplicación con AND y de la suma con OR, que
naturalmente asignaría mayor precedencia a AND.
Dado que las expresiones aritméticas pueden ser los operandos de las
expresiones relacionales, y las expresiones relacionales pueden ser los
operandos de las expresiones booleanas, las tres categorías de operadores
deben colocarse en diferentes niveles de precedencia, uno respecto del otro.
La precedencia de los operadores aritméticos, relacionales y booleanos en los
lenguajes basados en C es la siguiente:
Highest post fix
++, -unario +, unario -, prefijo ++, --, !
*, /, %
binario +, binario < , > , <=, >=
=, !=
&&
||
Las versiones de C anteriores a C99 son extrañas entre los lenguajes
imperativos populares en el sentido de que no tienen un tipo booleano y, por
tanto, no tienen valores booleanos. En su lugar, se utilizan valores numéricos
para representar valores booleanos. En lugar de las operaciones booleanas, se
utilizan variables escalares (numéricas o de caracteres) y constantes, donde el
cero se considera falso y todos los valores distintos de cero se consideran
verdaderos. El resultado de la evaluación de una expresión de este tipo es un
número entero, con el valor 0 si es falso y 1 si es verdadero. Las expresiones
aritméticas también pueden utilizarse para las expresiones booleanas en C99 y
C++.
Un resultado extraño del diseño de expresiones relacionales de C es que la
siguiente expresión es legal:
a>b>c
El operador relacional más a la izquierda se evalúa primero porque los
operadores relacionales de C son asociativos a la izquierda, produciendo 0 o 1.
Luego este resultado se compara con la variable c. Luego, este resultado se
compara con la variable c. Nunca hay una comparación entre b y c en esta
expresión.
Algunos lenguajes, incluyendo Perl y Ruby, proporcionan dos conjuntos de
operadores lógicos binarios, && y para AND y || y or para OR. Una diferencia
entre && y and (y || y or) es que las versiones deletreadas tienen menor
precedencia. Además, and y or tienen la misma precedencia, pero && tiene
mayor precedencia que ||.
Si se incluyen los operadores no aritméticos de los lenguajes basados en C, hay
más de 40 operadores y al menos 14 niveles diferentes de precedencia. Esto es
una prueba clara de la riqueza de las colecciones de operadores y de la
complejidad de las expresiones posibles en estos lenguajes.
La legibilidad dicta que un lenguaje debe incluir un tipo booleano, como se dijo
en el Capítulo 6, en lugar de utilizar simplemente tipos numéricos en las
expresiones booleanas. Se pierde algo de detección de errores en el uso de
tipos numéricos para los operandos booleanos, porque cualquier expresión
numérica, ya sea intencionada o no, es un operando legal para un operador
booleano. En los otros lenguajes imperativos, cualquier expresión no booleana
utilizada como operando de un operador booleano se detecta como un error.
7.6 Evaluación del cortocircuito
Una evaluación en cortocircuito de una expresión es aquella en la que se
determina el resultado sin evaluar todos los operandos y/o operadores. Por
ejemplo, el valor de la expresión aritmética
(13 * a) * (b / 13 - 1)
es independiente del valor de (b / 13 - 1) si a es 0, porque 0 * x = 0 para
cualquier x. Así, cuando a es 0, no es necesario evaluar (b / 13 - 1) ni realizar la
segunda multiplicación. Sin embargo, en las expresiones aritméticas, este atajo
no se detecta fácilmente durante la ejecución, por lo que nunca se toma.
El valor de la expresión booleana
(a >= 0) && (b < 10)
es independiente de la segunda expresión relacional si a < 0, porque la
expresión (FALSE && (b < 10)) es FALSE para todos los valores de b. Por lo
tanto, cuando a es menor que cero, no hay necesidad de evaluar b, la constante
10, la segunda expresión relacional o la operación &&. A diferencia del caso de
las expresiones aritméticas, este atajo puede descubrirse fácilmente durante la
ejecución.
Para ilustrar un problema potencial con la evaluación sin cortocircuito de las
expresiones booleanas, suponga que Java no utiliza la evaluación con
cortocircuito. Se podría escribir un bucle de búsqueda de tablas utilizando la
sentencia while. Una versión simple de código Java para tal búsqueda,
asumiendo que list, que tiene elementos listlen, es el array a buscar y key es el
valor buscado, es
índice = 0;
while ((index < listlen) && (list[index] != key)) index = index + 1;
Si la evaluación no es un cortocircuito, ambas expresiones relacionales en la
expresión booleana de la sentencia while se evalúan, independientemente del
valor de la expresión primero. Por lo tanto, si la clave no está en la lista, el
programa terminará con una excepción de subíndice fuera de rango. La misma
iteración que tiene índice == listlen hará referencia a list[listlen], lo que provoca
el error de indexación porque list está declarada para tener listlen-1 como valor
de subíndice de límite superior.
Si un lenguaje proporciona una evaluación en circuito corto de las expresiones
booleanas y se utiliza, esto no es un problema. En el ejemplo anterior, un
esquema de evaluación en circuito corto evaluaría el primer operando del
operador AND, pero omitiría el segundo operando si el primero es falso.
Un lenguaje que proporciona evaluaciones de cortocircuito de expresiones
booleanas y también tiene efectos secundarios en las expresiones permite que
se produzcan errores sutiles. Supongamos que la evaluación en cortocircuito se
utiliza en una expresión y parte de la expresión que contiene un efecto
secundario no se evalúa; entonces el efecto secundario se producirá sólo en las
evaluaciones completas de toda la expresión. Si la corrección del programa
depende del efecto secundario, la evaluación en cortocircuito puede dar lugar a
un error grave. Por ejemplo, considere la expresión de Java
(a > b) || ((b++) / 3)
En esta expresión, b se cambia (en la segunda expresión aritmética) sólo
cuando a <= b. Si el programador asumió que b se cambiaría cada vez que esta
expresión se evalúa durante la ejecución (y la corrección del programa depende
de ello), el programa fallará.
En los lenguajes basados en C, los operadores AND y OR habituales, && y ||,
respectivamente, son de circuito corto. Sin embargo, estos lenguajes también
tienen operadores AND y OR a nivel de bits, & y |, respectivamente, que pueden
utilizarse con operandos de valor booleano y no son de cortocircuito. Por
supuesto, los operadores a nivel de bits sólo son equivalentes a los operadores
booleanos habituales si todos los operandos están restringidos a ser 0 (para
falso) o 1 (para verdadero).
Todos los operadores lógicos de Ruby, Perl, ML, F# y Python se evalúan en
corto circuito .
7.7 Declaraciones de asignación
Como hemos dicho anteriormente, la sentencia de asignación es una de las
construcciones centrales en los lenguajes imperativos. Proporciona el
mecanismo por el cual el usuario puede cambiar dinámicamente los enlaces de
los valores a las variables. En la siguiente sección, se discute la forma más
simple de asignación. Las secciones siguientes describen una variedad de
alternativas.
7.7.1 Asignaciones sencillas
Casi todos los lenguajes de programación que se utilizan actualmente utilizan el
signo de igualdad para el operador de asignación. Todos ellos deben utilizar algo
diferente al signo igual para el operador relacional de igualdad para evitar
confusiones con su operador de asignación.
ALGOL 60 fue pionero en el uso de := como operador de asignación, que evita
la confusión de la asignación con la igualdad. Ada también utiliza este operador
de asignación.
Las opciones de diseño de cómo se utilizan las asignaciones en un lenguaje han
variado mucho. En algunos lenguajes, como Fortran y Ada, una asignación sólo
puede aparecer como una sentencia independiente, y el destino está restringido
a una sola variable. Sin embargo, hay muchas alternativas.
7.7.2 Objetivos condicionales
Perl permite objetivos condicionales en las sentencias de asignación. Por
ejemplo, considere
($flag ? $cuenta1 : $cuenta2) = 0;
lo que equivale a
if ($flag) {
$count1 = 0;
} si no {
$count2 = 0;
}
7.7.3 Operadores de asignación compuestos
Un operador de asignación compuesta es un método abreviado para
especificar una forma de asignación comúnmente necesaria. La forma de
asignación que se puede abreviar con esta técnica tiene la variable de destino
que también aparece como el primer operando en la expresión del lado derecho,
como en
a=a+b
Los operadores de asignación compuestos fueron introducidos por ALGOL 68,
posteriormente fueron adoptados en una forma ligeramente diferente por C, y
forman parte de los demás lenguajes basados en C, así como de Perl,
JavaScript, Python y Ruby. La sintaxis de estos operadores de asignación es la
catenación del operador binario deseado al operador =. Por ejemplo,
suma += valor;
equivale a
suma = suma + valor;
Los lenguajes que soportan operadores de asignación compuestos tienen
versiones para la mayoría de sus operadores binarios.
7.7.4 Operadores de asignación unarios
Los lenguajes basados en C, Perl y JavaScript incluyen dos operadores
aritméticos unarios especiales que en realidad son asignaciones abreviadas.
Combinan las operaciones de incremento y decremento con la asignación. Los
operadores ++ para el incremento y -- para el decremento pueden utilizarse en
expresiones o para formar sentencias de asignación de un solo operador.
Pueden aparecer como operadores prefijos, lo que significa que preceden a los
operandos, o como operadores postfijos, lo que significa que siguen a los
operandos. En la sentencia de asignación
suma = ++ cuenta;
el valor de count se incrementa en 1 y luego se asigna a sum. Esta operación
también se podría plantear como
cuenta = cuenta + 1; suma = cuenta;
Si el mismo operador se utiliza como operador postfijo, como en
suma = cuenta ++;
la asignación del valor de count a sum ocurre primero; luego count se
incrementa. El efecto es el mismo que el de las dos sentencias
suma = cuenta;
cuenta = cuenta + 1;
Un ejemplo del uso del operador de incremento unario para formar una
sentencia de asignación completa es
cuenta ++;
que simplemente incrementa la cuenta. No parece una asignación, pero
ciertamente lo es. Es equivalente a la sentencia
cuenta = cuenta + 1;
Cuando dos operadores unarios se aplican al mismo operando, la asociación es
de derecha a izquierda. Por ejemplo, en
- cuenta ++ cuenta
se incrementa primero y luego se niega. Por lo tanto, es equivalente a
- (cuenta ++)
en lugar de
(- recuento) ++
7.7.5 Asignación como expresión
En los lenguajes basados en C, Perl y JavaScript, la sentencia de asignación
produce un resultado, que es el mismo que el valor asignado al objetivo. Por
tanto, puede utilizarse como expresión y como operando en otras expresiones.
Este diseño trata el operador de asignación como cualquier otro operador
binario, excepto que tiene el efecto secundario de cambiar su operando
izquierdo. Por ejemplo, en C, es común escribir sentencias como
while ((ch = getchar()) != EOF) { ... }
En esta sentencia, el siguiente carácter del archivo de entrada estándar,
normalmente el teclado, se obtiene con getchar y se asigna a la variable ch. El
resultado, o valor asignado, se compara entonces con la constante EOF. Si ch
no es igual a EOF, se ejecuta la sentencia compuesta {...}. Tenga en cuenta que
la asignación debe estar entre paréntesis -en los lenguajes que soportan la
asignación como una expresión, la precedencia del operador de asignación es
menor que la de los operadores relacionales. Sin los paréntesis, el nuevo
carácter se compararía primero con EOF. La desventaja de permitir que las
sentencias de asignación sean operandos en las expresiones es que
proporciona otro tipo de efecto secundario de la expresión. Este tipo de efecto
secundario puede llevar a expresiones que son difíciles de leer y entender. Una
expresión con cualquier tipo de efecto secundario tiene esta desventaja. Una
expresión de este tipo no puede leerse como una expresión, que en
matemáticas es una denotación de un valor, sino sólo como una lista de
instrucciones con un orden de ejecución impar.
Por ejemplo, la expresión a = b + (c = d / b) - 1 denota las instrucciones
Asigna d / b a c Asigna b + c a temp Asigna temp - 1 a
Obsérvese que el tratamiento del operador de asignación como cualquier otro
operador binario permite el efecto de las asignaciones con múltiples objetivos,
como
suma = cuenta = 0;
en el que primero se asigna el cero a count y luego se asigna el valor de count a
sum. Esta forma de asignación de múltiples objetivos también es legal en
Python.
Hay una pérdida de detección de errores en el diseño en C de la operación de
asignación que frecuentemente conduce a errores en el programa. En particular,
si escribimos
si (x = y) ...
en lugar de
si (x == y) ...
que es un error fácil de cometer, no es detectable como error por el com- pilador.
En lugar de comprobar una expresión relacional, se comprueba el valor que se
asigna a x (en este caso, es el valor de y el que llega a esta sentencia). Esto es
en realidad el resultado de dos decisiones de diseño: permitir que la asignación
se comporte como un operador binario ordinario y utilizar dos operadores muy
similares, y
para tener significados completamente diferentes. Este es otro ejemplo de las
deficiencias de seguridad de los programas C y C++. Tenga en cuenta que Java
y C# sólo permiten expresiones booleanas en sus sentencias if, descartando
este problema.
7.7.6 Asignaciones múltiples
Varios lenguajes de programación recientes, como Perl, Ruby y Lua,
proporcionan estados de asignación de múltiples objetivos y fuentes. Por
ejemplo, en Perl se puede escribir
($primero, $segundo, $tercero) = (20, 40, 60);
La semántica es que 20 se asigna a $primero, 40 a $segundo y 60 a $tercero. Si
hay que intercambiar los valores de dos variables, se puede hacer con una sola
asignación, como con
($primero, $segundo) = ($segundo, $primero);
Esto intercambia correctamente los valores de $first y $second, sin el uso de
una variable temporal (al menos una creada y gestionada por el programador).
La sintaxis de la forma más simple de la asignación múltiple de Ruby es similar
a la de Perl, excepto los lados izquierdo y derecho
no están entre paréntesis. Además, Ruby incluye algunas versiones más
elaboradas de las asignaciones múltiples, que no se discuten aquí.
7.7.7 Asignación en lenguajes de programación funcional
Todos los identificadores utilizados en los lenguajes funcionales puros y algunos
de ellos utilizados en otros lenguajes funcionales son sólo nombres de valores.
Como tales, sus valores nunca cambian. Por ejemplo, en ML, los nombres están
ligados a valores con la declaración val, cuya forma se ejemplifica en lo
siguiente:
val coste = cantidad * precio;
Si el coste aparece en el lado izquierdo de una declaración de val posterior, esa
declaración crea una nueva versión del nombre coste, que no tiene relación con
la versión anterior, que queda oculta.
F# tiene una declaración algo similar que utiliza la palabra reservada let. La
diferencia entre let de F# y val de ML es que let crea un nuevo ámbito, mientras
que val no. De hecho, las declaraciones de val a menudo se anidan en
construcciones de let en ML. let y val se discuten más a fondo en el Capítulo
15.
7.8 Asignación de modo mixto
Las expresiones de modo mixto se trataron en el apartado 7.4.1. Con
frecuencia, las sentencias de asignación también son de modo mixto. La
cuestión de diseño es: ¿El tipo de la expresión tiene que ser el mismo que el
tipo de la variable que se asigna, o puede utilizarse la coerción en algunos
casos de desajuste de tipos?
C, C++ y Perl utilizan reglas de coerción para la asignación de modo mixto que
son similares a las que utilizan para las expresiones de modo mixto; es decir,
muchas de las posibles mezclas de tipos son legales, con la coerción aplicada
libremente. 7
En un claro alejamiento de C++, Java y C# permiten la asignación en modo
mixto sólo si la coerción requerida se amplía. 8 Así, un valor int puede asignarse
a una variable float, pero no a la inversa. Desautorizar la mitad de las posibles
asignaciones de modo mixto es una forma sencilla pero eficaz de aumentar la
fiabilidad de Java y C#, en relación con C y C++.
Por supuesto, en los lenguajes funcionales, donde las asignaciones sólo se
utilizan para nombrar valores, no existe la asignación de modo mixto.
CUESTIONARIO
1.
2.
3.
4.
5.
6.
7.
Definir la precedencia y la asociatividad de los operadores.
¿Qué es un operador unario?
¿Qué es un operador infijo?
¿Qué operador suele tener asociatividad izquierda?
¿Cuándo llamamos a los operadores "adyacentes"?
¿Cómo afectan los paréntesis a la regla de precedencia?
Dar una solución al problema del orden de evaluación de los operandos y
los efectos secundarios.
8. ¿Qué es un operador sobrecargado?
9. Definir las conversiones de estrechamiento y ensanchamiento.
10.¿Qué es una expresión de modo mixto?
11. ¿Cómo se relaciona la transparencia referencial con los efectos
secundarios funcionales?
12.¿Cuáles son las ventajas de la transparencia referencial?
13.¿Cómo interactúa el orden de evaluación de los operandos con los
efectos secundarios funcionales?
14.¿Qué es la evaluación del cortocircuito?
15.¿Para qué sirve un operador de asignación compuesta?
16.¿Cuál es una posible desventaja de tratar el operador de asignación como
si fuera un operador aritmético?
Descargar