Curso de iniciación a la programación con C# |
OPERADORES
Los operadores sirven, como su propio nombre indica, para efectuar operaciones con uno o más parámetros (sumar, restar, comparar...) y retornar un resultado. Se pueden agrupar de varios modos, pero yo te los voy a agrupar por primarios, unitarios y binarios. Aquí tienes una tabla con los operadores de C#, y luego te los explico todos con calma:
Operadores |
Descripción |
Tipo |
Asociatividad |
(expresión) objeto.miembro método(argumento, argumento, ...) array[indice] var++, var-- new typeof sizeof checked, unchecked + - ! ~ ++var, --var (conversión) var *, / % +, - <<, >> <, >, <=, >=, is, ==, != & ^ | && || ? : =, *=, /=, %=, +=, -=, <<=, >>=, &=, ^=, |= |
Control de precedencia Acceso a miembro de objeto Enumeración de argumentos Elemento de un array Postincremento y postdecremento Creación de objeto Recuperación de tipo (reflexión) Recuperación de tamaño Comprobación de desbordamiento Operando en forma original Cambio de signo Not lógico Complemento bit a bit Preincremente y predecremento Conversión de tipos Multiplicación, división Resto de división Suma, resta Desplazamiento de bits Relacionales AND a nivel de bits XOR a nivel de bits OR a nivel de bits AND lógico OR lógico QUESTION De asignación |
Primario Primario Primario Primario Primario Primario Primario Primario Primario Unitario Unitario Unitario Unitario Unitario Unitario Binario Binario Binario Binario Binario Binario Binario Binario Binario Binario Binario Binario |
Ninguna Ninguna Ninguna Ninguna Ninguna Ninguna Ninguna Ninguna Ninguna Ninguna Ninguna Ninguna Ninguna Ninguna Ninguna Izquierda Izquierda Izquierda Izquierda Izquierda Izquierda Izquierda Izquierda Izquierda Izquierda Izquierda Derecha |
Están puestos en orden de precedencia, es decir, en el caso de haber una expresión con varios de ellos, se ejecutan por ese orden. Ya te explicaré esto con más detalles.
Los operadores primarios son operadores de expresión. Vamos siguiendo la tabla y te cuento de qué van:
En el caso de “(expresión)”, los operadores son realmente los paréntesis. Sirven para modificar la precedencia. ¿Qué es eso? Tranquilo, más adelante.
En “objeto.miembro”, el operador es el punto. Sirve para especificar un miembro de una clase (sea una variable, una propiedad o un método).
En “método(argumento, argumento, ...)”, los operadores vuelven a ser los paréntesis. En este caso, sirven para especificar la lista de argumentos de un método.
En array[índice], los operadores son los corchetes. Sirven para indicar el elemento de un array o un indizador. ¿Qué son los arrays y los indizadores? Calma, lo dejamos para más adelante.
Los operadores de incremento (++) y decremento (--) sirven para incrementar o disminuir el valor de una variable en una unidad. Por ejemplo:
num++;
hará que num incremente su valor en una unidad, es decir, si valía 10 ahora vale 11. Los operadores de incremento y decremento se pueden poner delante (preincremento ó predecremento) o bien detrás (postincremento ó postdecremento), teniendo comportamientos distintos. Me explico: si hay un postincremento o postdecremento, primero se toma el valor de la variable y después se incrementa o decrementa. En caso contrario, si lo que hay es un preincremento o un predecremento, primero se incrementa o decrementa la variable y después se toma el valor de la misma. En una línea como la anterior esto no se ve claro, porque, además, el resultado sería el mismo que poniendo ++num. Sin embargo, veamos este otro ejemplo (num vale 10):
a = ++num;
b = a--;
Después de ejecutar la primera línea, tanto a como num valdrían 11, ya que el preincremento hace que primero se incremente num y después se tome su valor, asignándolo así a la variable a. Ahora bien, después de ejecutar la segunda línea, b valdrá 11, y a valdrá 10. ¿Por qué? Porque el postdecremento de a hace que primero se asigne su valor actual a b y después se decremente el suyo propio.
El operador “new” sirve para instanciar objetos. A estas alturas nos hemos hartado de verlo, y como vamos a seguir hartándonos no me voy a enrollar más con él.
El operador “typeof” es un operador de reflexión, y la reflexión es la posibilidad de recuperar información de un tipo determinado en tiempo de ejecución. Más adelante hablaremos largo y tendido sobre la reflexión. De momento me basta con que sepas que typeof devuelve el tipo de un objeto.
“sizeof” devuelve el tamaño en bytes que ocupa un tipo determinado. Ahora bien, solamente se puede utilizar sizeof con tipos valor y en contextos de código inseguro. Aunque no vamos a explicar ahora este tipo de contexto, sí puedes ver cómo funciona en ejemplo "OperadoresPrimarios", que se incluye con esta entrega.
“checked” y “unchecked” sirven para controlar si una expresión provoca o no desbordamiento. Me explico con un ejemplo: sabemos que las variables de tipo byte pueden almacenar valores entre 0 y 255. Si escribimos el siguiente código:
byte i=253;
checked {i+=10;}
Console.WriteLine(i);
El programa se compila, pero al ejecutar se produce un error de desbordamiento, ya que la variable i es de tipo byte y no puede almacenar valores mayores que 255. Sin embargo, si cambiamos checked por unchecked:
byte i=253;
unchecked {i+=10;}
Console.WriteLine(i);
El programa no produciría error de desbordamiento, ya que unchecked hace que se omitan estos errores. En su lugar, i toma el valor truncado, de modo que después de esa línea valdría 7. Un asunto importante en el que quiero que te fijes: checked y unchecked son bloques de código, como se deduce al ver que está escrito con llaves, de modo que puedes incluir varias líneas en un mismo bloque checked o unchecked.
Sí, sí, ya sé que no entiendes eso del valor truncado. Veamos: las variables numéricas tienen un rango de datos limitado, es decir, una variable de tipo byte, por ejemplo, no puede almacenar valores menores que 0 ni mayores que 255. Cuando se trunca el valor, lo que se hace es, para que me entiendas, colocar seguidos en una lista todos los valores que la variable acepta, empezando de nuevo por el primero cuando el rango acaba, y después se va recorriendo esta lista hasta terminar la operación de suma. Como dicho así resulta bastante ininteligible, fíjate en la siguiente tabla y lo verás enseguida:
... |
250 |
251 |
252 |
253 |
254 |
255 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
... |
(253 + 10) |
+1 |
+2 |
+3 |
+4 |
+5 |
+6 |
+7 |
+8 |
+9 |
+10 |
+11 |
+12 |
+13 |
... |
En la primera fila están la lista de valores que acepta la variable, y en negrilla el valor que contiene. Como ves, a continuación del último que acepta vuelve a estar el primero. Al sumarle 10 (segunda fila) es como si se fueran contando valores posibles hacia adelante, de modo que i ahora vale 7. Otro ejemplo usando el tipo sbyte (que acepta valores entre -128 y 127):
sbyte i=126;
unchecked {i+=10;}
Console.WriteLine(i);
... |
125 |
126 |
127 |
-128 |
-127 |
-126 |
-125 |
-124 |
-123 |
-122 |
-121 |
-120 |
-119 |
-118 |
-117 |
... |
(126 + 10) |
+1 |
+2 |
+3 |
+4 |
+5 |
+6 |
+7 |
+8 |
+9 |
+10 |
+11 |
+12 |
+13 |
... |
De modo que i valdría -120 después de la suma.
Los operadores unitarios + y - sirven sencillamente para mantener o cambiar el signo de un operando. Si se desea mantener el signo de un operando sin cambios, el + se puede omitir. Por ejemplo:
int i=10;
int b=-i;
Console.WriteLine("Valor de i: {0} Valor de b: {1}", i, b);
La variable i valdría 10, y la variable b valdría -10. Por lo tanto, la salida en la consola sería:
Valor de i: 10 Valor de b: -10
El operador unitario ! es un not lógico, es decir, invierte el valor de un dato de tipo boolean. En el siguiente ejemplo, i valdría true y b valdría false:
bool i=true;
bool b=!i;
El operador unitario ~ es de complemento a nivel de bits, o sea, que devuelve el valor complementario al operando al que afecta. Para entender esto usaremos una variable de tipo int y escribiremos tanto su valor como el de su complementario en hexadecimal:
uint i=10;
Console.WriteLine("Valor de i: {0:X8} Valor de ~i: {1:X8}", i, ~i);
La salida en la consola sería la siguiente:
Valor de i: 0000000A Valor de ~i: FFFFFFF5
Como sabes, el número hexadecimal A equivale al 10 en base decimal, por eso escribe A como valor de i. El número FFFFFFF5 es el complementario en hexadecimal de 10, y en base decimal equivale a 4294967285. Veamos estos números en código binario (que es como se almacenan en la memoria):
A: 0000 0000 0000 0000 0000 0000 0000 1010
FFFFFFF5: 1111 1111 1111 1111 1111 1111 1111 0101
Así lo ves perfectamente: el operador ~ ha cambiado todos los bits, pondiendo un 1 donde había un 0 y un 0 donde había un uno. ¿Y por qué ha rellenado tantos números? Sabía que me preguntarías eso. Muy simple: ¿Cuánto espacio se reserva en memoria para una variable de tipo int? 4 bytes, ¿no?. Pues bien, independientemente del valor de la variable, esta siempre ocupa 4 bytes ( 32 bits), de modo que si, por ejemplo, le asignamos 10 (que en binario es 1010) tendrá que colocar 28 ceros delante para ocupar los 28 bits que le faltan. El operador ~ solamente es aplicable a variables de tipo int, uint, long y ulong.
En el operador (conversion), lo que ha de ir entre paréntesis es el tipo al que se quiere convertir (int), (uint), (long)... Ya lo explicamos con anterioridad cuando hablamos del sistema de tipos.
Los opeardores * y / son, respectivamente, para multiplicar y dividir. Es muy sencillo. Si, por ejemplo, tenemos la siguiente expresión: 4*6/2, el resultado sería el que se supone: 12.
El operador % devuelve el resto de una división. Por ejemplo, 8 % 3 devolvería 2.
Los operadores + y – (binarios) son para sumar o restar. 4+7-3 devolvería 8.
Los operadores << y >> efectúan un desplazamiento de bits hacia la izquierda o hacia la derecha. Ya sé que esto de los bits puede que resulte algo confuso para alguno, así que me extenderé un poquito. Veamos el siguiente ejemplo, también usando números hexadecimales:
int i=15;
int b;
int c;
Console.WriteLine("Valor de i: {0:X}", i);
b = i >> 1;
Console.WriteLine("Ejecutado b = i >> 1;");
Console.WriteLine("Valor de b: {0:X}", b);
c = i << 1;
Console.WriteLine("Ejecutado c = i << 1;");
Console.WriteLine("Valor de c: {0:X}", c);
Veamos la salida en la consola y después la examinamos:
Valor de i: F
Ejecutado b = i >> 1;
Valor de b: 7
Ejecutado b = i << 1;
Vaor de b: 1E
Variable | Valor hex. | Valor binario |
i | 0000000F | 0000 0000 0000 0000 0000 0000 0000 1111 |
b | 00000007 | 0000 0000 0000 0000 0000 0000 0000 0111 |
c | 0000001E | 0000 0000 0000 0000 0000 0000 0001 1110 |
Como puedes apreciar, a la variable b le asignamos lo que vale i desplazando sus bits hacia la derecha en una unidad. El 1 que había más a la derecha se pierde. En la variable c hemos asignado lo que valía i desplazando sus bits hacia la izquierda también en una unidad. Como ves, el la parte derecha se rellena el hueco con un cero.
Los operadores relacionales < (menor que), > (mayor que), <= (menor o igual que), >= (mayor o igual que), is, == (igual que), != (distinto de) establecen una comparación entre dos valores y devuelven como resultado un valor de tipo boolean (true o false). Veamos un ejemplo:
int i;
int b;
Console.Write("Escribe el valor de i: ");
i=Int32.Parse(Console.ReadLine());
Console.Write("Escribe el valor de b: ");
b=Int32.Parse(Console.ReadLine());
Console.WriteLine("i<b devuelve: {0}", (i<b));
Console.WriteLine("i<=b devuelve: {0}", (i<=b));
Console.WriteLine("i>b devuelve: {0}", (i>b));
Console.WriteLine("i>=b devuelve: {0}", (i>=b));
Console.WriteLine("i==b devuelve: {0}", (i==b));
Console.WriteLine("i!=b devuelve: {0}", (i!=b));
La salida de estas líneas de programa sería la siguiente (en rojo está lo que se ha escrito durante la ejecución de las mismas):
Escribe el valor de i: 2
Escribe el valor de b: 3
i<b devuelve: True
i<=b devuelve: True
i>b devuelve: False
i>=b devuelve: False
i==b devuelve: False
i!=b devuelve: True
El resultado es muy obvio cuando se trata con números (o, mejor dicho, con tipos valor). Sin embargo, ¿Qué ocurre cuando utilizamos variables de un tipo referencia?
Circunferencia c = new Circunferencia(4);
Circunferencia d = new Circunferencia(4);
Console.WriteLine("c==d devuelve: {0}", (c==d));
El resultado de comparar c==d sería False. Sí, sí, False. A pesar de que ambos objetos sean idénticos el resultado es, insisto, False. ¿Por qué? Porque una variable de un tipo referencia no retorna internamente un dato específico, sino un puntero a la dirección de memoria donde está almacenado el objeto. De este modo, al comparar, el sistema compara los punteros en lugar de los datos del objeto, y, por lo tanto, devuelve False, puesto que las variables c y d no apuntan a la misma dirección de memoria. Para eso tendríamos que utilizar el método Equals heredado de la clase object (en C#, todas las clases que construyas heredan automáticamente los miembros de la clase base System.Object), así:
Circunferencia c = new Circunferencia(4);
Circunferencia d = new Circunferencia(4);
Console.WriteLine("c.Equals(d) devuelve: {0}", c.Equals(d));
Ahora, el resultado sí sería True.
El operador is devuelve un valor boolean al comparar si un objeto (de un tipo referencia) es compatible con una clase. Por ejemplo:
Circunferencia c=new Circunferencia();
Console.WriteLine("El resultado de c is Circunferencia es: {0}", (c is Circunferencia));
La salida de estas dos líneas sería la que sigue:
El resultado de c is Circunferencia es: True
Al decir si el objeto es compatible con la clase me refiero a que se pueda convertir a esa clase. Por otro lado, si el objeto contiene una referenica nula (null) o si no es compatible con la clase, el operador is retornará false.
Los operadores & (and a nivel de bits), | (or a nivel de bits) y ^ (xor -o exclusivo- a nivel de bits) hacen una comparación binaria (bit a bit) de dos números devolviendo el resultado de dicha comparación como otro número. Vamos con un ejemplo para que veas que no te engaño:
int i=10;
int b=7;
int res;
res = i & b;
Console.WriteLine("{0} & {1} retorna: Decimal: {2} Hexadecimal: {3:X}", i, b, res, res);
res = (i | b);
Console.WriteLine("{0} | {1} retorna: Decimal: {2} Hexadecimal: {3:X}", i, b, res, res);
res = (i ^ b);
Console.WriteLine("{0} ^ {1} retorna: Decimal: {2} Hexadecimal: {3:X}", i, b, res, res);
La salida en pantalla de este fragmento sería la que sigue:
10 & 7 retorna: Decimal: 2 Hexadecimal: 2
10 | 7 retorna: Decimal: 15 Hexadecimal: F
10 ^ 7 retorna: Decimal: 13 Hexadecimal: D
Fíjate primero en que en ninguno de los casos se ha hecho una suma normal de los dos números. Veamos estas tres operaciones de un modo algo más claro. Marcaremos en negrilla los valores que provocan que el bit resultante en esa posición valga 1:
Operación: i & b
Variable |
Valor dec. |
Valor hex. |
Valor binario |
i |
10 |
A |
0000 0000 0000 0000 0000 0000 0000 1010 |
b |
7 |
7 |
0000 0000 0000 0000 0000 0000 0000 0111 |
Resultado |
2 |
2 |
0000 0000 0000 0000 0000 0000 0000 0010 |
Operación: i | b
Variable |
Valor dec. |
Valor hex. |
Valor binario |
i |
10 |
A |
0000 0000 0000 0000 0000 0000 0000 1010 |
b |
7 |
7 |
0000 0000 0000 0000 0000 0000 0000 0111 |
Resultado |
15 |
F |
0000 0000 0000 0000 0000 0000 0000 1111 |
Operación: i ^ b
Variable |
Valor dec. |
Valor hex. |
Valor binario |
i |
10 |
A |
0000 0000 0000 0000 0000 0000 0000 1010 |
b |
7 |
7 |
0000 0000 0000 0000 0000 0000 0000 0111 |
Resultado |
13 |
D |
0000 0000 0000 0000 0000 0000 0000 1101 |
Vamos operación por operación. En la primera de ellas, i & b, el resultado es 0010 porque el operador & hace una comparación bit a bit, devolviendo uno cuando ambos bits comparados también valen uno (se ve claramente que tanto para i como para b, el segundo bit por la derecha vale 1), y 0 cuando alguno de ellos (o los dos) es 0.
En la segunda operación, i | b, el resultado es 1111 porque el operador | devuelve 1 cuando al menos uno de los bits comparados es 1, y 0 cuando ambos bits comparados son también 0.
En el tercer caso, i ^ b, el resultado es 1101 porque el operador ^ devuelve 1 cuando uno y sólo uno de los bits comparados vale 1, y cero cuando ambos bits valen 1 o cuando ambos bits valen 0.
Los operadores && (AND lógico) y || (OR lógico) se ocupan de comparar dos valores de tipo boolean y retornan como resultado otro valor de tipo boolean. El operador && devuelve true cuando ambos operandos son true, y false cuando uno de ellos o los dos son false. El operador || devuelve true cuando al menos uno de los operandos es true (pudiendo ser también true los dos), y false cuando los dos operandos son false. Suelen combinarse con los operaciones relacionales para establecer condiciones más complejas. Por ejemplo, la siguiente expresión devolvería true si un número es mayor que 10 y menor que 20:
(num > 10) && (num < 20)
Para que veas otro ejemplo, la siguiente expresión devolvería true si el número es igual a 10 o igual a 20:
(num == 10) || (num == 20)
El operador ? : (question) evalúa una expresión como true o false y devuelve un valor que se le especifique en cada caso (Si programabas en Visual Basic, equivale más o menos a la función iif). Vamos a verlo con un ejemplo:
string mensaje = (num == 10) ? "El número es 10": "El número no es 10";
Fíjate bien. Delante del interrogante se pone la expresión que debe retornar un valor boolean. Si dicho valor es true, el operador retornará lo que esté detrás del interrogante, y si es false retornará lo que esté detrás de los dos puntos. No tiene por qué retornar siempre un string. Puede retornar un valor de cualquier tipo. Por lo tanto, si en este ejemplo num valiera 10, la cadena que se asignaría a mensaje sería "El número es 10", y en caso contrario se le asignaría "El número no es 10".
El operador de asignación (=) (sí, sí, ya sé que nos hemos hartado de usarlo, pero vamos a verlo con más profundidad) asigna lo que hay a la derecha del mismo en la variable que está a la izquierda. Por ejemplo, la expresión a = b asignaría a la variable a lo que valga la variable b. La primera norma que no debes olvidar es que ambas variables han de ser compatibles. Por ejemplo, no puedes asignar un número a una cadena, y viceversa, tampoco puedes asignar una cadena a una variable de algún tipo numérico.
Sobre esto hay que tener en cuenta una diferencia importante entre la asignación de una variable de tipo valor a otra (también de tipo valor, obviamente) y la asignación de una variable de tipo referencia a otra. Veamos el siguiente fragmento de código:
int a=5;
int b=a;
b++;
Tras la ejecución de estas tres líneas, efectivamente, a valdría 5 y b valdría 6. Ahora bien, ¿qué ocurre si usamos variables de tipo referencia? Veámoslo (la propiedad Radio es de lectura/escritura):
Circunferencia a=new Circunferencia();
a.Radio=4;
Circunferencia b=a;
b.Radio++;
Está claro que tanto a como b serán objetos de la clase circunferencia. Después de la ejecución de estas líneas, cuánto valdrá el radio de la circunferencia b? Efectivamente, 5. ¿Y el de la circunferencia a? ¡¡Sorpresa!! También 5. ¿Cómo es esto, si el valor que le hemos asignado al radio de la circunferencia a es 4? Volvamos a lo que decíamos sobre los tipos referencia: reservaban espacio en el montón y devolvían un puntero de tipo seguro a la dirección de memoria reservada. Lo que ha ocurrido aquí, por lo tanto, es que al hacer la asignación Circunferencia b=a; no se ha reservado un nuevo espacio en el montón para la circunferencia b: dado que a, al tratarse de una variable de tipo referencia, devuelve internamente un puntero a la dirección reservada para este objeto, es este puntero el que se ha asignado a la variable b, de modo que las variables a y b apuntan ambas al mismo espacio de memoria. Por lo tanto, cualquier modificación que se haga en el objeto usando alguna de estas variables quedará reflejado también en la otra variable, ya que, en realidad, son la misma cosa o, dicho de otro modo, representan al mismo objeto. Si queríamos objetos distintos, o sea espacios de memoria distintos en el montón, teníamos que haberlos instanciado por separado, y después asignar los valores a las propiedades una por una:
Circunferencia a=new Circunferencia();
a.Radio=4;
Circunferencia b=new Circunferencia();
b.Radio=a.Radio;
b.Radio++;
Ahora sí, el radio de la circunferencia a será 4 y el de la circunferencia b será 5. Cuidado con esto porque puede conducir a muchos errores. Si hubiéramos escrito el código de la siguiente forma:
Circunferencia a=new Circunferencia();
a.Radio=4;
Circunferencia b=new Circunferencia();
b=a;
b.Radio++;
Hubiera ocurrido algo parecido al primer caso. A pesar de haber instanciado el objeto b por su lado (reservando así un espacio de memoria para b en el montón distinto del de a), al asignar b=a, la referencia de b se destruye, asignándosele de nuevo la de a, de modo que ambas variables volverían a apuntar al mismo objeto dando el mismo resultado que en el primer caso, es decir, que el radio de ambas circunferencias (que en realidad son la misma) sería 5.
El resto de operadores de asignación son operadores compuestos a partir de otro operador y el operador de asignación. Veamos a qué equivalen los operadores *=, /=, %=, +=, -=, <<=, >>=, &=, ^=, |=
num *= 10; // Equivale a num = num * 10
num /= 10; // Equivale a num = num / 10
num %= 10; // Equivale a num = num % 10
num += 10; // Equivale a num = num + 10
num -= 10; // Equivale a num = num - 10
num <<= 10; // Equivale a num = num << 10
num >>= 10; // Equivale a num = num >> 10
num &= 10; // Equivale a num = num & 10
num ^= 10; // Equivale a num = num ^ 10
num |= 10; // Equivale a num = num | 10
La precedencia de operadores determina la prioridad con la que se ejecutan cuando hay varios de ellos en una misma expresión. Efectivamente, el resultado puede ser distinto en función del orden en que se ejecuten. Vamos con un ejemplo. La expresión 4 + 3 * 6 - 8 devolvería 14, ya que primero se hace la multiplicación y después las sumas y las restas. Si hubiéramos querido modificar dicha precedencia habría que haber usado paréntesis: (4+3)*6-8 devolvería 34, ya que primero se ejecuta lo que hay dentro del paréntesis, después la multiplicación y, por último, la resta. Como ves, la precedencia de operadores no cambia respecto de la que estudiamos en el colegio en las clases de matemáticas. En la tabla que tienes al principio de esta entrega, los operadores están colocados en orden de precedencia.
La asociatividad de los operadores indica el orden en que se ejecutan cuando tienen la misma precedencia. Obviamente, esto es aplicable solamente a los operadores binarios. Todos los operadores binarios son asociativos por la izquierda salvo los de asignación, que son asociativos por la derecha. Por ejemplo, en la expresión 4+3+2, primero se hace 4+3 y a este resultado se le suma el dos, dado que el operador + es asociativo por la izquierda. Sin embargo, en la expresión b = c = d, como el operador de asignación es asociativo por la derecha, primero se asigna a c el valor de d, y después se asigna a b el valor de c, es decir, que tras esta expresión las tres variables valdrían lo mismo.
Para esta entrega hay dos ejemplos: uno para ver el funcionamiento de los operadores typeof, sizeof, checked y unchecked y otro para ver el funcionamiento de los operadores de manejo de bits. Sigue este vínculo si quieres bajártelos.
No he confeccionado ejemplos de los otros porque los vamos a usar tanto en tantos ejemplos que no merece la pena hacer ejercicios específicos de ellos.