Operadores

    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 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 las variables 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 += asigna       a su primer operando el valor que tenía más el de su segundo operando (o sea, le suma el 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; // 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 puede traducirlos en una única instrucción en código máquina[5].        

  • 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 más adelante

  • 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 su correspondiente tema.

  • 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
                            

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:


int
* p = stackalloc[100]; // p apunta a una tabla de 100 enteros.

stackalloc
sólo puede usarse para inicializar punteros a objetos de tipos valor declarados como variables locales.

  • 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:

    •  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 aplicará  mucho más rápido en los casos donde no sea necesario convertir.

    • En caso de que se solicite hacer una conversión inválida as devuelve null mientras que () produce una excepción System.InvalidCastException.

Operadores
José Antonio González Seco

José Antonio es experto en tecnologias Microsoft. Imparte cursos y conferencias en congresos sobre C# y .NET en Universidades de toda España (Sevilla, Barcelona, San Sebastián, Valencia, Oviedo, etc.) en representación de grandes empresas como Microsoft.
Fecha de alta:03/10/2006
Última actualizacion:03/10/2006
Visitas totales:64249
Valorar el contenido:
Últimas consultas realizadas en los foros
Últimas preguntas sin contestar en los foros de devjoker.com