Un operador en C# es un símbolo formado por uno o más caracteres que
permite realizar una determinada operación entre uno o más datos y produce un
resultado.
A continuación se describen
cuáles son los operadores incluidos en el lenguaje clasificados según el tipo
de operaciones que permiten realizar, aunque hay que tener en cuenta que C#
permite la redefinición del significado de la mayoría de los operadores según
el tipo de dato sobre el que se apliquen,
por lo que lo que aquí se cuenta se corresponde con los usos más comunes
de los mismos:
- Operaciones
aritméticas: Los operadores aritméticos incluidos en C# son
los típicos de suma (+), resta (-),
producto (*), división (/) y
módulo (%) También se incluyen operadores de “menos
unario” (–) y “más unario” (+)
Relacionados
con las operaciones aritméticas se encuentran un par de operadores llamados checked y unchecked que
permiten controlar si se desea detectar los desbordamientos que puedan
producirse si al realizar este tipo de
operaciones el resultado es superior a la capacidad del tipo de datos de
sus operandos. Estos operadores se usan así:
checked
(<expresiónAritmética>)
unchecked(<expresiónAritmética>)
Ambos
operadores calculan el resultado de <expresiónAritmética> y lo
devuelven si durante el cálculo no se produce ningún desbordamiento. Sin
embargo, en caso de que haya desbordamiento cada uno actúa de una forma
distinta: checked
provoca un error de compilación si <expresiónAritmética>
es una expresión constante y una excepción System.OverflowException si no lo es, mientras
que unchecked
devuelve el resultado de la expresión aritmética truncado para modo que quepa
en el tamaño esperado.
Por defecto,
en ausencia de los operadores checked y unchecked
lo que se hace es evaluar las operaciones aritméticas entre datos constantes
como si se les aplicase checked y
las operaciones entre datos no constantes como si se les hubiese aplicado unchecked.
- Operaciones
lógicas: Se incluyen operadores que permiten realizar las
operaciones lógicas típicas: “and”
(&& y
&), “or” (|| y |),
“not” (!)
y “xor” (^)
Los
operadores &&
y ||
se diferencia de &
y |
en que los primeros realizan evaluación perezosa y los segundos no. La
evaluación perezosa consiste en que si el resultado de evaluar el primer
operando permite deducir el resultado de la operación, entonces no se evalúa el
segundo y se devuelve dicho resultado directamente, mientras que la evaluación
no perezosa consiste en evaluar siempre ambos operandos. Es decir, si el primer
operando de una operación &&
es falso se devuelve false
directamente, sin evaluar el segundo; y si el primer operando de una || es cierto se devuelve true directamente, sin
evaluar el otro.
- Operaciones
relacionales: Se han incluido los tradicionales operadores de
igualdad (==), desigualdad (!=),
“mayor que” (>), “menor que” (<),
“mayor o igual que” (>=) y “menor o igual
que” (<=)
- Operaciones
de manipulación de bits: Se han incluido operadores que
permiten realizar a nivel de bits operaciones “and” (&),
“or” (|),
“not” (~),
“xor” (^),
desplazamiento a izquierda (<<)
y desplazamiento a derecha (>>)
El operador << desplaza a izquierda rellenando con
ceros, mientras que el tipo de relleno realizado por >>
depende del tipo de dato sobre el que se aplica: si es un dato con signo
mantiene el signo, y en caso contrario rellena con ceros.
- Operaciones
de asignación: Para realizar asignaciones se usa en C# el operador
=,
operador que además de realizar la asignación que se le solicita devuelve
el valor asignado. Por ejemplo, la
expresión a = b
asigna a la variable a
el valor de la variable b
y devuelve dicho valor, mientras que la expresión c = a = b asigna a c y a el valor de b
(el operador = es asociativo por la
derecha)
También se
han incluido operadores de asignación compuestos que permiten ahorrar tecleo
a la hora de realizar asignaciones tan
comunes como:
temperatura
= temperatura + 15; // Sin usar
asignación compuesta
temperatura
+= 15; // Usando asignación compuesta
Las dos
líneas anteriores son equivalentes, pues el operador compuesto += lo que hace
es asignar a su primer operando el valor que tenía más el valor de su segundo
operando. Como se ve, permite compactar bastante el código.
Aparte del
operador de asignación compuesto +=, también se ofrecen operadores de asignación
compuestos para la mayoría de los operadores binarios ya vistos. Estos son: +=, -=, *=, /=, %=, &=, |=, ^=, <<= y >>=.
Nótese que no hay versiones compuestas para los operadores binarios &&
y ||.
Otros dos
operadores de asignación incluidos son los de incremento(++) y decremento (--)
Estos operadores permiten, respectivamente, aumentar y disminuir en una unidad
el valor de la variable sobre el que se aplican. Así, estas líneas de código
son equivalentes:
temperatura = temperatura +
1; // Sin usar asignación compuesta ni
incremento temperatura
+= 1; // Usando
asignación compuesta
temperatura++; // Usando incremento
Si el
operador ++
se coloca tras el nombre de la variable (como en el ejemplo) devuelve el valor
de la variable antes de incrementarla, mientras que si se coloca antes,
devuelve el valor de ésta tras incrementarla; y lo mismo ocurre con el operador
--.
Por ejemplo:
c
= b++; // Se asigna a c el valor de b y luego se incrementa b c = ++b;
c
= ++b; // Se incrementa el valor de b y luego se asigna a c
La ventaja de
usar los operadores ++ y -- es que en muchas máquinas son más eficientes
que el resto de formas de realizar sumas o restas de una unidad, pues el compilador
traducirlos en una única instrucción en código máquina.
- Operaciones
con cadenas: Para realizar operaciones de concatenación de cadenas
se puede usar el mismo operador que para realizar sumas, ya que en C# se
ha redefinido su significado para que cuando se aplique entre operandos
que sean cadenas o que sean una cadena y un carácter lo que haga sea
concatenarlos. Por ejemplo, ″Hola″+″
mundo″ devuelve ″Hola
mundo″, y ″Hola
mund″ + ′o′ también.
- Operaciones
de acceso a tablas: Una tabla
es un conjunto de ordenado de objetos de tamaño fijo. Para acceder a
cualquier elemento de este conjunto se aplica el operador postfijo []
sobre la tabla para indicar entre corchetes la posición que
ocupa el objeto al que se desea acceder dentro del conjunto. Es decir, este
operador se usa así:
[<posiciónElemento>]
Un ejemplo de
su uso en el que se asigna al elemento que ocupa la posición 3 en una tabla de
nombre tablaPrueba el valor del
elemento que ocupa la posición 18 de dicha tabla es el siguiente:
tablaPrueba[3]
= tablaPrueba[18];
Las
tablas se estudian detenidamente en el Tema
7: Variables y tipos de datos
- Operador
condicional: Es el único operador incluido en C# que toma 3
operandos, y se usa así:
<condición>
? <expresión1> : <expresión2>
El significado
del operando es el siguiente: se evalúa <condición>
Si es cierta se devuelve el resultado de evaluar <expresión1>,
y si es falsa se devuelve el resultado de evaluar <condición2>.
Un ejemplo de su uso es:
b
= (a>0)? a : 0; // Suponemos a y
b de tipos enteros
En este
ejemplo, si el valor de la variable a es
superior a 0 se asignará a b el valor de a,
mientras que en caso contrario el valor que se le asignará será 0.
Hay que tener
en cuenta que este operador es asociativo por la derecha, por lo que una
expresión como a?b:c?d:e es
equivalente a a?b:(c?d:e)
No hay que confundir este
operador con la instrucción condicional if que se tratará en el Tema 8:Instrucciones, pues aunque su
utilidad es similar al de ésta, ? devuelve un valor e if no.
- Operaciones
de delegados: Un delegado
es un objeto que puede almacenar en referencias a uno o más métodos y a
través del cual es posible llamar a estos métodos. Para añadir objetos a
un delegado se usan los operadores + y +=,
mientras que para quitárselos se usan los operadores – y -=.
Estos conceptos se estudiarán detalladamente en el Tema 13: Eventos y delegados
- Operaciones
de acceso a objetos: Para acceder a los miembros de un objeto se
usa el operador ., cuya sintaxis es:
<objeto>.<miembro>
Si a es un objeto, ejemplos de cómo llamar a diferentes
miembros suyos son:
a.b = 2; // Asignamos a su propiedad a el valor 2
a.f(); // Llamamos a su método f()
a.g(2); // Llamamos a su método g()
pasándole como parámetro el valor entero 2
a.c += new adelegado(h) //
Asociamos a su evento c el código del método h() de //“tipo” adelegado
No se
preocupe si no conoce los conceptos de métodos, propiedades, eventos y
delegados en los que se basa este ejemplo, pues se explican detalladamente en
temas posteriores.
- Operaciones
con punteros: Un puntero es una variable que almacena una
referencia a una dirección de memoria. Para obtener la dirección de
memoria de un objeto se usa el operador &,
para acceder al contenido de la dirección de memoria almacenada en un
puntero se usa el operador *, para acceder a un
miembro de un objeto cuya dirección se almacena en un puntero se usa ->,
y para referenciar una dirección de memoria de forma relativa a un puntero
se le aplica el operador [] de la forma puntero[desplazamiento].
Todos estos conceptos se explicarán más a fondo en el Tema 18: Código inseguro.
- Operaciones
de obtención de información sobre tipos: De todos los operadores
que nos permiten obtener información sobre tipos de datos el más
importante es typeof, cuya forma de
uso es:
typeof(<nombreTipo>)
Este operador
devuelve un objeto de tipo System.Type con información sobre el tipo de
nombre <nombreTipo> que
podremos consultar a través de los miembros
ofrecidos por dicho objeto. Esta información incluye detalles tales como
cuáles son sus miembros, cuál es su tipo padre o a qué espacio de nombres
pertenece.
Si
lo que queremos es determinar si una determinada expresión es de un tipo u otro, entonces el operador a usar es is,
cuya sintaxis es la siguiente:
<expresión>
is <nombreTipo>
El
significado de este operador es el siguiente: se evalúa <expresión>.
Si el resultado de ésta es del tipo cuyo nombre se indica en <nombreTipo>
se devuelve true; y si no, se devuelve false. Como se verá en
el Tema 5: Clases, este operador
suele usarse en métodos polimórficos.
Finalmente,
C# incorpora un tercer operador que permite obtener información sobre un tipo
de dato: sizeof Este operador permite obtener el número de bytes
que ocuparán en memoria los objetos de un tipo, y se usa así:
sizeof(<nombreTipo>)
sizeof sólo puede usarse dentro de código inseguro, que
por ahora basta considerar que son zonas de código donde es posible usar punteros. No será
hasta el Tema 18: Código inseguro
cuando lo trataremos en profundidad.
Además, sizeof sólo se puede aplicar
sobre nombres de tipos de datos cuyos objetos se puedan almacenar directamente
en pila. Es decir, que sean estructuras (se verán en el Tema 13) o tipos enumerados (se verán en el Tema 14)
- Operaciones
de creación de objetos: El operador más típicamente usado para
crear objetos es new, que se usa así:
new
<nombreTipo>(<parametros>)
Este operador
crea un objeto de <nombreTipo>
pasándole a su método constructor los parámetros indicados en <parámetros> y devuelve una referencia al
mismo. En función del tipo y número de estos parámetros se llamará a uno u otro
de los constructores del objeto. Así, suponiendo que a1 y a2 sean variables de tipo Avión,
ejemplos de uso del operador new son:
Avión
a1 = new Avión(); // Se llama al constructor sin parámetros de Avión
Avión
a2 = new Avión(“Caza”); // Se llama al constructor de Avión que toma
// como parámetro una cadena
En caso de que
el tipo del que se haya solicitado la creación del objeto sea una clase, éste
se creará en memoria dinámica, y lo que new devolverá
será una referencia a la dirección de pila donde se almacena una referencia a
la dirección del objeto en memoria dinámica. Sin embargo, si el objeto a crear
pertenece a una estructura o a un tipo enumerado, entonces éste se creará
directamente en la pila y la referencia devuelta por el new se referirá
directamente al objeto creado. Por estas razones, a las clases se les conoce
como tipos referencia ya que de sus
objetos en pila sólo se almacena una referencia a la dirección de memoria
dinámica donde verdaderamente se encuentran; mientras que a las estructuras y
tipos enumerados se les conoce como tipos
valor ya sus objetos se almacenan directamente en pila.
C# proporciona otro operador que también nos
permite crear objetos. Éste es stackalloc, y se usa así:
stackalloc
<nombreTipo>[<nElementos>]
Este operador
lo que hace es crear en pila una tabla de tantos elementos de tipo <nombreTipo> como indique <nElementos> y devolver la dirección de memoria
en que ésta ha sido creada. Por ejemplo:
stackalloc sólo puede usarse para inicializar punteros a
objetos de tipos valor declarados como variables locales. Por ejemplo:
int
* p = stackalloc[100]; // p apunta a una tabla de 100 enteros.
- Operaciones
de conversión: Para convertir unos objetos en otros se utiliza el
operador de conversión, que no consiste más que en preceder la expresión a
convertir del nombre entre paréntesis del tipo al que se desea convertir
el resultado de evaluarla. Por ejemplo, si l
es una variable de tipo long y se desea almacenar
su valor dentro de una variable de tipo int llamada i, habría que convertir
previamente su valor a tipo int
así:
i
= (int) l; // Asignamos a i el resultado
de convertir el valor de l a tipo int
Los tipos int y long están
predefinidos en C# y permite almacenar valores enteros con signo. La capacidad
de int
es de 32 bits, mientras que la de long es de 64 bits. Por tanto, a no ser que
hagamos uso del operador de conversión, el compilador no nos dejará hacer la
asignación, ya que al ser mayor la capacidad de los long, no todo valor
que se pueda almacenar en un long tiene
porqué poderse almacenar en un int. Es decir, no es válido:
i =
l; //ERROR: El valor de l no tiene porqué caber en i
Esta
restricción en la asignación la impone el compilador debido a que sin ella
podrían producirse errores muy difíciles de detectar ante truncamientos no
esperados debido al que el valor de la variable fuente es superior a la
capacidad de la variable destino.
Existe otro
operador que permite realizar operaciones de conversión de forma muy similar al
ya visto. Éste es el operador as, que se usa así:
<expresión>
as <tipoDestino>
Lo que hace
es devolver el resultado de convertir el resultado de evaluar <expresión> al tipo indicado en <tipoDestino> Por ejemplo, para almacenar en
una variable p el resultado
de convertir un objeto t a tipo tipo Persona se haría:
p = t as Persona;
Las únicas diferencias
entre usar uno u otro operador de conversión son:
q as
sólo es aplicable a tipos referencia y sólo a aquellos casos en que existan
conversiones predefinidas en el lenguaje. Como se verá más adelante, esto sólo
incluye conversiones entre un tipo y tipos padres suyos y entre un tipo y tipos hijos suyos.
Una
consecuencia de esto es que el programador puede definir cómo hacer
conversiones de tipos por él definidos y otros mediante el operador (),
pero no mediante as.
Esto se debe a
que as únicamente indica que se desea que una referencia a un objeto en memoria
dinámica se trate como si el objeto fuese de otro tipo, pero no implica
conversión ninguna. Sin embargo, () sí que implica conversión si el <tipoDestino> no es compatible con el tipo del
objeto referenciado. Obviamente, el operador se aplirará mucho más rápido en los casos donde no sea
necesario convertir.
q En
caso de que se solicite hacer una conversión inválida as devuelve null mientras
que ()
produce una excepción System.InvalidCastException.