Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Clases en C#

Descargar como pdf o txt
Descargar como pdf o txt
Está en la página 1de 213

Clases en Microsoft Visual C#

Definición de clases

Conceptos de clase y objeto

C# es un lenguaje orientado a objetos puro[6], lo que significa que todo con lo que vamos a
trabajar en este lenguaje son objetos. Un objeto es un agregado de datos y de métodos que
permiten manipular dichos datos, y un programa en C# no es más que un conjunto de objetos que
interaccionan unos con otros a través de sus métodos.

Una clase es la definición de las características concretas de un determinado tipo de objetos. Es


decir, de cuáles son los datos y los métodos de los que van a disponer todos los objetos de ese
tipo. Por esta razón, se suele decir que el tipo de dato de un objeto es la clase que define las
características del mismo[7].

Sintaxis de definición de clases

La sintaxis básica para definir una clase es la que a continuación se muestra:

class <nombreClase>
{
<miembros>
}

De este modo se definiría una clase de nombre cuyos miembros son los definidos en .
Los miembros de una clase son los datos y métodos de los que van a disponer todos los objetos
de la misma. Un ejemplo de cómo declarar una clase de nombre A que no tenga ningún miembro
es la siguiente:

class A
{}

Una clase así declarada no dispondrá de ningún miembro a excepción de los implícitamente
definidos de manera común para todos los objetos que creemos en C#. Estos miembros los
veremos dentro de poco en este mismo tema bajo el epígrafe La clase primegina: System.Object.

Aunque en C# hay muchos tipos de miembros distintos, por ahora vamos a considerar que éstos
únicamente pueden ser campos o métodos y vamos a hablar un poco acerca de ellos y de cómo se
definen:

 Campos: Un campo es un dato común a todos los objetos de una determinada clase. Para definir
cuáles son los campos de los que una clase dispone se usa la siguiente sintaxis dentro de la zona
señalada como <miembros> en la definición de la misma:

<tipoCampo> <nombreCampo>;

1
Clases en Microsoft Visual C#

El nombre que demos al campo puede ser cualquier identificador que queramos siempre y cuando
siga las reglas descritas en elTema 4: Aspectos Léxicos para la escritura de identificadores y no
coincida con el nombre de ningún otro miembro previamente definido en la definición de clase.

Los campos de un objeto son a su vez objetos, y en <tipoCampo> hemos de indicar cuál es el tipo
de dato del objeto que vamos a crear. Éste tipo puede corresponderse con cualquiera que los
predefinidos en la BCL o con cualquier otro que nosotros hallamos definido siguiendo la sintaxis
arriba mostrada. A continuación se muestra un ejemplo de definición de una clase de nombre
Persona que dispone de tres campos:

class Persona
{
string Nombre;// Campo de cada objeto Persona que almacena su nombre
int Edad; // Campo de cada objeto Persona que almacena su edad
string NIF; // Campo de cada objeto Persona que almacena su NIF
}

Según esta definición, todos los objetos de clase Persona incorporarán campos que almacenarán
cuál es el nombre de la persona que cada objeto representa, cuál es su edad y cuál es su NIF. El
tipo int incluido en la definición del campo Edad es un tipo predefinido en la BCL cuyos objetos son
capaces de almacenar números enteros con signo comprendidos entre -2.147.483.648 y
2.147.483.647 (32 bits), mientras que string es un tipo predefinido que permite almacenar
cadenas de texto que sigan el formato de los literales de cadena visto en el Tema 4: Aspectos
Léxicos

Para acceder a un campo de un determinado objeto se usa la sintaxis:

<objeto>.<campo>

Por ejemplo, para acceder al campo Edad de un objeto Persona llamado p y cambiar su valor por
20 se haría:

p.edad = 20;

En realidad lo marcado como <objeto> no tiene porqué ser necesariamente el nombre de algún
objeto, sino que puede ser cualquier expresión que produzca como resultado una referencia no
nula a un objeto (si produjese null se lanzaría una excepción del tipo
predefinido System.NullPointerException)

 Metodos:Un método es un conjunto de instrucciones a las que se les asocia un nombre de modo
que si se desea ejecutarlas basta referenciarlas a través de dicho nombre en vez de tener que
escribirlas. Dentro de estas instrucciones es posible acceder con total libertad a la información
almacenada en los campos pertenecientes a la clase dentro de la que el método se ha definido, por
lo que como al principio del tema se indicó, los métodos permiten manipular los datos almacenados
en los objetos.

2
Clases en Microsoft Visual C#

La sintaxis que se usa en C# para definir los métodos es la siguiente:

<tipoDevuelto> <nombreMétodo> (<parametros>)


{
<instrucciones>
}

Todo método puede devolver un objeto como resultado de la ejecución de las instrucciones que lo
forman, y el tipo de dato al que pertenece este objeto es lo que se indica en <tipoDevuelto>. Si no
devuelve nada se indica void, y si devuelve algo es obligatorio finalizar la ejecución de sus
instrucciones con alguna instrucción return <objeto>; que indique qué objeto ha de devolverse.

Opcionalmente todo método puede recibir en cada llamada una lista de objetos a los que podrá
acceder durante la ejecución de sus instrucciones. En <parametros> se indica es cuáles son los
tipos de dato de estos objetos y cuál es el nombre con el que harán referencia las instrucciones del
método a cada uno de ellos. Aunque los objetos que puede recibir el método pueden ser diferentes
cada vez que se solicite su ejecución, siempre han de ser de los mismos tipos y han de seguir el
orden establecido en<parametros>.

Un ejemplo de cómo declarar un método de nombre Cumpleaños es la siguiente modificación de la


definición de la clase Persona usada antes como ejemplo:

class Persona
{
string Nombre;//Campo de cada objeto Persona que almacena su
nombre
int Edad; //Campo de cada objeto Persona que almacena su edad
string NIF; //Campo de cada objeto Persona que almacena su NIF

void Cumpleaños()//Incrementa en uno de la edad del objeto Persona


{
Edad++;
}
}

La sintaxis usada para llamar a los métodos de un objeto es la misma que la usada para llamar a
sus campos, sólo que ahora tras el nombre del método al que se desea llamar hay que indicar
entre paréntesis cuáles son los valores que se desea dar a los parámetros del método al hacer la
llamada. O sea, se escribe:

<objeto>.<método>(<parámetros>)

Como es lógico, si el método no tomase parámetros se dejarían vacíos los parámetros en la llamada
al mismo. Por ejemplo, para llamar al método Cumpleaños() de un objeto Persona llamado p se
haría:

3
Clases en Microsoft Visual C#

p.Cumpleaños(); // El método no toma parámetros,


// luego no le pasamos ninguno

Es importante señalar que en una misma clase pueden definirse varios métodos con el mismo
nombre siempre y cuando tomen diferente número o tipo de parámetros. A esto se le conoce
como sobrecarga de métodos, y es posible ya que el compilador sabrá a cual llamar a partir de
los <parámetros> especificados.

Sin embargo, lo que no se permite es definir varios métodos que únicamente se diferencien en su
valor de retorno, ya que como éste no se tiene porqué indicar al llamarlos no podría diferenciarse a
que método en concreto se hace referencia en cada llamada. Por ejemplo, a partir de la llamada:

p.Cumpleaños();

Si además de la versión de Cumpleaños() que no retorna nada hubiese otra que retornase un int,
¿cómo sabría entonces el compilador a cuál llamar?

Antes de continuar es preciso señalar que en C# todo, incluido los literales, son objetos del tipo de
cada literal y por tanto pueden contar con miembros a los que se accedería tal y como se ha
explicado. Para entender esto no hay nada mejor que un ejemplo:

string s = 12.ToString();

Este código almacena el literal de cadena “12” en la variable s, pues 12 es un objeto de


tipo int (tipo que representa enteros) y cuenta cuenta con el método común a todos los ints
llamado ToString() que lo que hace es devolver una cadena cuyos caracteres son los dígitos que
forman el entero representado por el int sobre el que se aplica; y como la variable s es de
tipostring (tipo que representa cadenas) es perfectamente posible almacenar dicha cadena en ella,
que es lo que se hace en el código anterior.

Creación de objetos
Operador new

Ahora que ya sabemos cómo definir las clases de objetos que podremos usar en
nuestras aplicaciones ha llegado el momento de explicar cómo crear objetos de una
determinada clase. Algo de ello ya se introdujo en el Tema 4: Aspectos Léxicos cuando se
comentó la utilidad del operador new, que precisamente es crear objetos y cuya sintaxis
es:

4
Clases en Microsoft Visual C#

new <nombreTipo>(<parametros>)

Este operador crea un nuevo objeto del tipo cuyo nombre se le indica y llama durante
su proceso de creación al constructor del mismo apropiado según los valores que se le
pasen en <parametros>, devolviendo una referencia al objeto recién creado. Hay que
resaltar el hecho de que new no devuelve el propio objeto creado, sino una referencia a
la dirección de memoria dinámica donde en realidad se ha creado.

El antes comentado constructor de un objeto no es más que un método definido en la


definición de su tipo que tiene el mismo nombre que la clase a la que pertenece el objeto
y no tiene valor de retorno. Como new siempre devuelve una referencia a la dirección
de memoria donde se cree el objeto y los constructores sólo pueden usarse como
operandos de new, no tiene sentido que un constructor devuelva objetos, por lo que no
tiene sentido incluir en su definición un campo <tipoDevuelto> y el compilador considera
erróneo hacerlo (aunque se indique void)

El constructor recibe ese nombre debido a que su código suele usarse precisamente
para construir el objeto, para inicializar sus miembros. Por ejemplo, a nuestra clase de
ejemplo Persona le podríamos añadir un constructor dejándola así:

class Persona
{
string Nombre; // Campo de cada objeto Persona que almacena su nombre
int Edad; // Campo de cada objeto Persona que almacena su edad
string NIF; // Campo de cada objeto Persona que almacena su NIF

void Cumpleaños() // Incrementa en uno la edad del objeto Persona


{
Edad++;
}

Persona (string nombre, int edad, string nif) // Constructor


{
Nombre = nombre;
Edad = edad;
NIF = nif;
}
}

Como se ve en el código, el constructor toma como parámetros los valores con los que
deseemos inicializar el objeto a crear. Gracias a él, podemos crear un objeto Persona de
nombre José, de 22 años de edad y NIF 12344321-A así:

new Persona("José", 22, "12344321-A")

5
Clases en Microsoft Visual C#

Nótese que la forma en que se pasan parámetros al constructor consiste en indicar los
valores que se ha de dar a cada uno de los parámetros indicados en la definición del
mismo separándolos por comas. Obviamente, si un parámetro se definió como de
tipo stringhabrá que pasarle una cadena, si se definió de tipo int habrá que pasarle un
entero y, en general, a todo parámetro habrá que pasarle un valor de su mismo tipo (o de
alguno convertible al mismo), produciéndose un error al compilar si no se hace así.

En realidad un objeto puede tener múltiples constructores, aunque para diferenciar a


unos de otros es obligatorio que se diferencien en el número u orden de los parámetros
que aceptan, ya que el nombre de todos ellos ha de coincidir con el nombre de la clase de
la que son miembros. De ese modo, cuando creemos el objeto el compilador podrá
inteligentemente determinar cuál de los constructores ha de ejecutarse en función de los
valores que le pasemos al new.

Una vez creado un objeto lo más normal es almacenar la dirección devuelta


por new en una variable del tipo apropiado para el objeto creado. El siguiente ejemplo -
que como es lógico irá dentro de la definición de algún método- muestra cómo crear una
variable de tipo Persona llamada p y cómo almacenar en ella la dirección del objeto que
devolvería la anterior aplicación del operador new:

Persona p; // Creamos variable p


p = new Persona("Jose", 22, "12344321-A");
// Almacenamos en p el objeto creado con new

A partir de este momento la variable p contendrá una referencia a un objeto de clase


Persona que representará a una persona llamada José de 22 años y NIF 12344321-A. O lo
que prácticamente es lo mismo y suele ser la forma comúnmente usada para decirlo: la
variable p representa a una persona llamada José de 22 años y NIF 12344321-A.

Como lo más normal suele ser crear variables donde almacenar referencias a objetos
que creemos, las instrucciones anteriores pueden compactarse en una sola así:

Persona p = new Persona("José", 22, "12344321-A");

De hecho, una sintaxis más general para la definición de variables es la siguiente:

<tipoDato> <nombreVariable> = <valorInicial>;

La parte = <valorInicial> de esta sintaxis es en realidad opcional, y si no se incluye la


variable declarada pasará a almacenar una referencia nula (contendrá el literal null)

6
Clases en Microsoft Visual C#

Constructor por defecto

No es obligatorio definir un constructor para cada clase, y en caso de que no definamos


ninguno el compilador creará uno por nosotros sin parámetros ni instrucciones. Es decir,
como si se hubiese definido de esta forma:

<nombreTipo>(){}

Gracias a este constructor introducido automáticamente por el compilador, si Coche es


una clase en cuya definición no se ha incluido ningún constructor, siempre será posible
crear uno nuevo usando el operador new así:

Coche c = new Coche(); //Crea coche c llamando al constructor por defecto de


Coche

Hay que tener en cuenta una cosa: el constructor por defecto es sólo incluido por el
compilador si no hemos definido ningún otro constructor. Por tanto, si tenemos una clase
en la que hayamos definido algún constructor con parámetros pero ninguno sin
parámetros no será válido crear objetos de la misma llamando al constructor sin
parámetros, pues el compilador no lo habrá definido automáticamente. Por ejemplo, con la
última versión de la clase de ejemplo Persona es inválido hacer:

Persona p = new Persona(); // ERROR:


// El único constructor de Persona
// toma 3 parámetros

Referencia al objeto actual con this

Dentro del código de cualquier método de un objeto siempre es posible hacer


referencia al propio objeto usando la palabra reservada this. Esto puede venir bien a la
hora de escribir constructores de objetos debido a que permite que los nombres que
demos a los parámetros del constructor puedan coincidir nombres de los campos del
objeto sin que haya ningún problema. Por ejemplo, el constructor de la clase Persona
escrito anteriormente se puede rescribir así usando this:

Persona (string Nombre, int Edad, string NIF)


{
this.Nombre = Nombre;
this.Edad = Edad;
this.NIF = NIF;

7
Clases en Microsoft Visual C#

Es decir, dentro de un método con parámetros cuyos nombres coincidan con campos,
se da preferencia a los parámetros y para hacer referencia a los campos hay que
prefijarlos con el this tal y como se muestra en el ejemplo.

El ejemplo anterior puede que no resulte muy interesante debido a que para evitar
tener que usar this podría haberse escrito el constructor tal y como se mostró en la
primera versión del mismo: dando nombres que empiecen en minúscula a los parámetros
y nombres que empiecen con mayúsculas a los campos. De hecho, ese es el convenio que
Microsoft recomienda usar. Sin embargo, como más adelante se verá sí que puede ser
útil this cuando los campos a inicializar a sean privados, ya que el convenio de escritura
de identificadores para campos privados recomendado por Microsoft coincide con el usado
para dar identificadores a parámetros (obviamente otra solución sería dar cualquier otro
nombre a los parámetros del constructor o los campos afectados, aunque así el código
perdería algo legibilidad)

Un uso más frecuente de this en C# es el de permitir realizar llamadas a un método de


un objeto desde código ubicado en métodos del mismo objeto. Es decir, en C# siempre es
necesario que cuando llamemos a algún método de un objeto precedamos al operador .de
alguna expresión que indique cuál es el objeto a cuyo método se desea llamar, y si éste
método pertenece al mismo objeto que hace la llamada la única forma de conseguir
indicarlo en C# es usando this.

Finalmente, una tercera utilidad de this es permitir escribir métodos que puedan
devolver como objeto el propio objeto sobre el que el método es aplicado. Para ello
bastaría usar una instrucción return this; al indicar el objeto a devolver.

Herencia y métodos virtuales


Concepto de herencia

El mecanismo de herencia es uno de los pilares fundamentales en los que se basa la


programación orientada a objetos. Es un mecanismo que permite definir nuevas clases a
partir de otras ya definidas de modo que si en la definición de una clase indicamos que
ésta deriva de otra, entonces la primera -a la que se le suele llamar clase hija- será
tratada por el compilador automáticamente como si su definición incluyese la definición de
la segunda –a la que se le suele llamar clase padre o clase base. Las clases que derivan
de otras se definen usando la siguiente sintaxis:

class <nombreHija>:<nombrePadre>
{

8
Clases en Microsoft Visual C#

<miembrosHija>
}

A los miembros definidos en <miembrosHijas> se le añadirán los que hubiésemos


definido en la clase padre. Por ejemplo, a partir de la clase Persona puede crearse una
clase Trabajador así:

class Trabajador:Persona
{
public int Sueldo;
public Trabajador (string nombre, int edad, string nif, int sueldo)
:base(nombre, edad, nif)
{
Sueldo = sueldo;
}
}

Los objetos de esta clase Trabajador contarán con los mismos miembros que los objetos
Persona y además incorporarán un nuevo campo llamado Sueldo que almacenará el dinero
que cada trabajador gane. Nótese además que a la hora de escribir el constructor de esta
clase ha sido necesario escribirlo con una sintaxis especial consistente en preceder la llave
de apertura del cuerpo del método de una estructura de la forma:

: base(<parametrosBase>)

A esta estructura se le llama inicializador base y se utiliza para indicar cómo


deseamos inicializar los campos heredados de la clase padre. No es más que una llamada
al constructor de la misma con los parámetros adecuados, y si no se incluye el compilador
consideraría por defecto que vale :base(), lo que sería incorrecto en este ejemplo debido
a que Persona carece de constructor sin parámetros.

Un ejemplo que pone de manifiesto cómo funciona la herencia es el siguiente:

using System;

class Persona
{
// Campo de cada objeto Persona que almacena su nombre

public string Nombre;


// Campo de cada objeto Persona que almacena su edad
public int Edad;
// Campo de cada objeto Persona que almacena su NIF

9
Clases en Microsoft Visual C#

public string NIF;

void Cumpleaños() // Incrementa en uno de edad del objeto Persona


{
Edad++;
}

// Constructor de Persona
public Persona (string nombre, int edad, string nif)
{
Nombre = nombre;
Edad = edad;
NIF = nif;
}
}

class Trabajador: Persona


{

// Campo de cada objeto Trabajador que almacena cuánto gana


public int Sueldo;

Trabajador(string nombre, int edad, string nif, int sueldo)


: base(nombre, edad, nif)
{ // Inicializamos cada Trabajador en base al constructor de Persona
Sueldo = sueldo;
}

public static void Main()


{
Trabajador p = new Trabajador("Josan", 22, "77588260-Z", 100000);
Console.WriteLine ("Nombre="+p.Nombre);
Console.WriteLine ("Edad="+p.Edad);
Console.WriteLine ("NIF="+p.NIF);
Console.WriteLine ("Sueldo="+p.Sueldo);
}
}

Nótese que ha sido necesario prefijar la definición de los miembros de Persona del
palabra reservada public. Esto se debe a que por defecto los miembros de una tipo sólo
son accesibles desde código incluido dentro de la definición de dicho tipo, e
incluyendopublic conseguimos que sean accesibles desde cualquier código, como el
método Main() definido en Trabajador. public es lo que se denomina un modificador de
acceso, concepto que se tratará más adelante en este mismo tema bajo el epígrafe
tituladoModificadores de acceso.

Llamadas por defecto al constructor base

Si en la definición del constructor de alguna clase que derive de otra no incluimos


inicializador base el compilador considerará que éste es :base() Por ello hay que estar
seguros de que si no se incluye base en la definición de algún constructor, el tipo padre
del tipo al que pertenezca disponga de constructor sin parámetros.

10
Clases en Microsoft Visual C#

Es especialmente significativo reseñar el caso de que no demos la definición de ningún


constructor en la clase hija, ya que en estos casos la definición del constructor que por
defecto introducirá el compilador será en realidad de la forma:

<nombreClase>(): base()
{}

Es decir, este constructor siempre llama al constructor sin parámetros del padre del tipo
que estemos definiendo, y si ése no dispone de alguno se producirá un error al compilar.

Métodos virtuales

Ya hemos visto que es posible definir tipos cuyos métodos se hereden de definiciones
de otros tipos. Lo que ahora vamos a ver es que además es posible cambiar dichar
definición en la clase hija, para lo que habría que haber precedido con la palabra
reservadavirtual la definición de dicho método en la clase padre. A este tipo de métodos
se les llama métodos virtuales, y la sintaxis que se usa para definirlos es la siguiente:

virtual <tipoDevuelto> <nombreMétodo>(<parámetros>)


{
<código>
}

Si en alguna clase hija quisiésemos dar una nueva definición del del método,
simplemente lo volveríamos a definir en la misma pero sustituyendo en su definición la
palabra reservada virtual por override. Es decir, usaríamos esta sintaxis:

override <tipoDevuelto> <nombreMétodo>(<parámetros>)


{
<nuevoCódigo>
}

Nótese que esta posibilidad de cambiar el código de un método en su clase hija sólo se
da si en la clase padre el método fue definido como virtual. En caso contrario, el
compilador considerará un error intentar redefinirlo.

El lenguaje C# impone la restricción de que toda redefinición de método que queramos


realizar incorpore la partícula override para forzar a que el programador esté seguro de
que verdaderamente lo que quiere hacer es cambiar el significado de un método
heredado. Así se evita que por accidente defina un método del que ya exista una
definición en una clase padre. Además, C# no permite definir un método
como override y virtual a la vez, ya que ello tendría un significado absurdo: estaríamos
dando una redefinición de un método que vamos a definir.

11
Clases en Microsoft Visual C#

Por otro lado, cuando definamos un método como override ha de cumplirse que en
alguna clase antecesora (su clase padre, su clase abuela, etc.) de la clase en la que se ha
realizado la definición del mismo exista un método virtual con el mismo nombre que el
redefinido. Si no, el compilador informará de error por intento de redefinición de método
no existente o no virtual. Así se evita que por accidente un programador crea que está
redefiniendo un método del que no exista definición previa o que redefina un método que
el creador de la clase base no desee que se pueda redefinir.

Para aclarar mejor el concepto de método virtual, vamos a mostrar un ejemplo en el


que cambiaremos la definición del método Cumpleaños() en los objetos Persona por una
nueva versión en la que se muestre un mensaje cada vez que se ejecute, y redefiniremos
dicha nueva versión para los objetos Trabajador de modo que el mensaje mostrado sea
otro. El código de este ejemplo es el que se muestra a continuación:

using System;
class Persona
{
// Campo de cada objeto Persona que almacena su nombre
public string Nombre;
// Campo de cada objeto Persona que almacena su edad
public int Edad;
// Campo de cada objeto Persona que almacena su NIF
public string NIF;
// Incrementa en uno de la edad del objeto Persona

public virtual void Cumpleaños()


{
Edad++;
Console.WriteLine("Incrementada edad de persona");
}

// Constructor de Persona
public Persona (string nombre, int edad, string nif)
{
Nombre = nombre;
Edad = edad;
NIF = nif;
}
}

class Trabajador: Persona


{
// Campo de cada objeto Trabajador que almacena cuánto gana

public int Sueldo;

Trabajador(string nombre, int edad, string nif, int sueldo)


: base(nombre, edad, nif)
{ // Inicializamos cada Trabajador en base al constructor de Persona
Sueldo = sueldo;
}

12
Clases en Microsoft Visual C#

public override void Cumpleaños()


{
Edad++;
Console.WriteLine("Incrementada edad de trabajador");
}

public static void Main()


{
Persona p = new Persona("Carlos", 22, "77588261-Z");
Trabajador t = new Trabajador("Josan", 22, "77588260-Z", 100000);
t.Cumpleaños();
p.Cumpleaños();
}
}

Nótese cómo se ha añadido el modificador virtual en la definición de Cumpleaños() en


la clase Persona para habilitar la posibilidad de que dicho método puede ser redefinido en
clase hijas de Persona y cómo se ha añado override en la redefinición del mismo dentro
de la clase Trabajador para indicar que la nueva definición del método es una redefinición
del heredado de la clase. La salida de este programa confirma que la implementación
de Cumpleaños() es distinta en cada clase, pues es de la forma:

ncrementada edad de trabajador

ncrementada edad de persona

También es importante señalar que para que la redefinición sea válida ha sido
necesario añadir la partícula public a la definición del método original, pues si no se
incluyese se consideraría que el método sólo es accesible desde dentro de la clase donde
se ha definido, lo que no tiene sentido en métodos virtuales ya que entonces nunca
podría ser redefinido. De hecho, si se excluyese el modificadorpublic el compilador
informaría de un error ante este absurdo. Además, este modificador también se ha
mantenido en la redefinición de Cumpleaños() porque toda redefinición de un método
virtual ha de mantener los mismos modificadores de acceso que el método original para
ser válida.

Clases abstractas

Una clase abstracta es aquella que forzosamente se ha de derivar si se desea que se


puedan crear objetos de la misma o acceder a sus miembros estáticos (esto último se verá
más adelante en este mismo tema) Para definir una clase abstracta se
anteponeabstract a su definición, como se muestra en el siguiente ejemplo:

public abstract class A

13
Clases en Microsoft Visual C#

{
public abstract void F();
}
abstract public class B: A
{
public void G() {}
}
class C: B
{
public override void F(){}
}

Las clases A y B del ejemplo son abstractas, y como puede verse es posible combinar
en cualquier orden el modificador abstractcon modificadores de acceso.

La utilidad de las clases abstractas es que pueden contener métodos para los que no se
dé directamente una implementación sino que se deje en manos de sus clases hijas darla.
No es obligatorio que las clases abstractas contengan métodos de este tipo, pero sí lo es
marcar como abstracta a toda la que tenga alguno. Estos métodos se definen precediendo
su definición del modificador abstract y sustituyendo su código por un punto y coma (;),
como se muestra en el método F() de la clase A del ejemplo (nótese que B también ha de
definirse como abstracta porque tampoco implementa el método F() que hereda de A)

Obviamente, como un método abstracto no tiene código no es posible llamarlo. Hay


que tener especial cuidado con esto a la hora de utilizar this para llamar a otros métodos
de un mismo objeto, ya que llamar a los abstractos provoca un error al compilar.

Véase que todo método definido como abstracto es implícitamente virtual, pues si no
sería imposible redefinirlo para darle una implementación en las clases hijas de la clase
abstracta donde esté definido. Por ello es necesario incluir el modificador override a la
hora de darle implementación y es redundante marcar un método
como abstract y virtual a la vez (de hecho, hacerlo provoca un error al compilar)

Es posible marcar un método como abstract y override a la vez, lo que convertiría al


método en abstracto para sus clases hijas y forzaría a que éstas lo tuviesen que
reimplementar si no se quisiese que fuesen clases abstractas.

La clase primegenia: System.Object

Ahora que sabemos lo que es la herencia es el momento apropiado para explicar que
en .NET todos los tipos que se definan heredan implícitamente de la
clase System.Object predefinida en la BCL, por lo que dispondrán de todos los miembros
de ésta. Por esta razón se dice que System.Object es la raíz de la jerarquía de objetos
de .NET.

A continuación vamos a explicar cuáles son estos métodos comunes a todos los objetos:

14
Clases en Microsoft Visual C#

 public virtual bool Equals(object o): Se usa para comparar el objeto sobre el que se
aplica con cualquier otro que se le pase como parámetro. Devuelve true si ambos objetos
son iguales y false en caso contrario.

La implementación que por defecto se ha dado a este método consiste en usar igualdad
por referencia para los tipos por referencia e igualdad por valor para los tipos por
valor. Es decir, si los objetos a comparar son de tipos por referencia sólo se
devuelve true si ambos objetos apuntan a la misma referencia en memoria dinámica, y si
los tipos a comparar son tipos por valor sólo se devuelve truesi todos los bits de ambos
objetos son iguales, aunque se almacenen en posiciones diferentes de memoria.

Como se ve, el método ha sido definido como virtual, lo que permite que los
programadores puedan redefinirlo para indicar cuándo ha de considerarse que son iguales
dos objetos de tipos definidos por ellos. De hecho, muchos de los tipos incluidos en la BCL
cuentan con redefiniciones de este tipo, como es el caso de string, quien aún siendo un
tipo por referencia, sus objetos se consideran iguales si apuntan a cadenas que sean
iguales carácter a carácter (aunque referencien a distintas direcciones de memoria
dinámica)

El siguiente ejemplo muestra cómo hacer una redefinición de Equals() de manera que
aunque los objetos Persona sean de tipos por referencia, se considere que dos Personas
son iguales si tienen el mismo NIF:

public override bool Equals(object o)


{
if (o==null)
return this==null;
else
return (o is Persona) && (this.NIF == ((Persona) o).NIF);
}

Hay que tener en cuenta que es conveniente que toda redefinición del
método Equals() que hagamos cumpla con una serie de propiedades que
muchos de los métodos incluidos en las distintas clases de la BCL esperan que se
cumplan. Estas propiedades son:

 Reflexividad: Todo objeto ha de ser igual a sí mismo. Es decir, x.Equals(x) siempre ha


de devolver true.
 Simetría: Ha de dar igual el orden en que se haga la comparación. Es decir, x.Equals(y)
ha de devolver lo mismo que y.Equals(x) .
 Transitividad: Si dos objetos son iguales y uno de ellos es igual a otro, entonces el
primero también ha de ser igual a ese otro objeto. Es decir, si x.Equals(y) e y.Equals(z)
entonces x.Equals(z) .
 Consistencia: Siempre que el método se aplique sobre los mismos objetos ha de
devolver el mismo resultado.

15
Clases en Microsoft Visual C#

 Tratamiento de objetos nulos: Si uno de los objetos comparados es nulo (null), sólo
se ha de devolver true si el otro también lo es.

Hay que recalcar que el hecho de que redefinir Equals() no implica que el operador de
igualdad (==) quede también redefinido. Ello habría que hacerlo de independientemente
como se indica en el Tema 11: Redefinición de operadores.

 public virtual int GetHashCode(): Devuelve un código de dispersión (hash) que


representa de forma numérica al objeto sobre el que el método es
aplicado. GetHashCode() suele usarse para trabajar con tablas de dispersión, y se
cumple que si dos objetos son iguales sus códigos de dispersión serán iguales, mientras
que si son distintos la probabilidad de que sean iguales es ínfima.

En tanto que la búsqueda de objetos en tablas de dispersión no se realiza únicamente


usando la igualdad de objetos (método Equals()) sino usando también la igualdad de
códigos de dispersión, suele ser conveniente redefinir GetHashCode() siempre que se
redefina Equals() De hecho, si no se hace el compilador informa de la situación con un
mensaje de aviso.

 public virtual string ToString(): Devuelve una representación en forma de cadena del
objeto sobre el que se el método es aplicado, lo que es muy útil para depurar aplicaciones
ya que permite mostrar con facilidad el estado de los objetos.

La implementación por defecto de este método simplemente devuelve una cadena de


texto con el nombre de la clase a la que pertenece el objeto sobre el que es
aplicado. Sin embargo, como lo habitual suele ser implementar ToString() en cada nueva
clase que es defina, a continuación mostraremos un ejemplo de cómo redefinirlo en la
clase Persona para que muestre los valores de todos los campos de los objetos Persona:

public override string ToString()


{
string cadena = "";
cadena += "DNI = " + this.DNI + "\n";
cadena += "Nombre = " + this.Nombre + "\n";
cadena += "Edad = " + this.Edad + "\n";
return cadena;
}

Es de reseñar el hecho de que en realidad los que hace el operador de concatenación


de cadenas (+) para concatenar una cadena con un objeto cualquiera es convertirlo
primero en cadena llamando a su método ToString() y luego realizar la concatenación de
ambas cadenas.

Del mismo modo, cuando a Console.WriteLine() y Console.Write() se les pasa como


parámetro un objeto lo que hacen es mostrar por la salida estándar el resultado de
convertirlo en cadena llamando a su método ToString(); y si se les pasa como
parámetros una cadena seguida de varios objetos lo muestran por la salida estándar esa
16
Clases en Microsoft Visual C#

cadena pero sustituyendo en ella toda subcadena de la forma {<número>} por el


resultado de convertir en cadena el parámetro que ocupe la posición <número>+2 en la
lista de valores de llamada al método.

 protected object MemberWiseClone(): Devuelve una copia shallow copy del objeto
sobre el que se aplica. Esta copia es una copia bit a bit del mismo, por lo que el objeto
resultante de la copia mantendrá las mismas referencias a otros que tuviese el objeto
copiado y toda modificación que se haga a estos objetos a través de la copia afectará al
objeto copiado y viceversa.

Si lo que interesa es disponer de una copia más normal, en la que por cada objeto
referenciado se crease una copia del mismo a la que referenciase el objeto clonado,
entonces el programador ha de escribir su propio método clonador pero puede servirse de
MemberwiseClone() como base con la que copiar los campos que no sean de tipos
referencia.

 public System.Type GetType(): Devuelve un objeto de clase System.Type que


representa al tipo de dato del objeto sobre el que el método es aplicado. A través de los
métodos ofrecidos por este objeto se puede acceder a metadatos sobre el mismo como
su nombre, su clase padre, sus miembros, etc. La explicación de cómo usar los miembros
de este objeto para obtener dicha información queda fuera del alcance de este documento
ya que es muy larga y puede ser fácilmente consultada en la documentación que
acompaña al .NET SDK.

 protected virtual void Finalize(): Contiene el código que se ejecutará siempre que
vaya ha ser destruido algún objeto del tipo del que sea miembro. La implementación dada
por defecto a Finalize() consiste en no hacer nada.

Aunque es un método virtual, en C# no se permite que el programador lo redefina


explícitamente dado que hacerlo es peligroso por razones que se explicarán en el Tema 8:
Métodos (otros lenguajes de .NET podrían permitirlo)

Aparte de los métodos ya comentados que todos los objetos heredan, la


clase System.Object también incluye en su definición los siguientes métodos de tipo:

 public static bool Equals(object objeto1, object objeto2) à Versión estática del
método Equals() ya visto. Indica si los objetos que se le pasan como parámetros son
iguales, y para compararlos lo que hace es devolver el resultado de
calcularobjeto1.Equals(objeto2) comprobando antes si alguno de los objetos
vale null (sólo se devolvería true sólo si el otro también lo es)

Obviamente si se da una redefinición al Equals() no estático, sus efectos también se


verán cuando se llame al estático.

 public static bool ReferenceEquals(object objeto1, object objeto2) à Indica si los


dos objetos que se le pasan como parámetro se almacenan en la misma posición de
memoria dinámica. A través de este método, aunque se hayan redefinido Equals() y el
operador de igualdad (==) para un cierto tipo por referencia, se podrán seguir realizando

17
Clases en Microsoft Visual C#

comparaciones por referencia entre objetos de ese tipo en tanto que redefinir de Equals()
no afecta a este método. Por ejemplo, dada la anterior redefinición de Equals() para
objetos Persona:

Persona p = new Persona("José", 22, "83721654-W");


Persona q = new Persona("Antonio", 23, "83721654-W");
Console.WriteLine(p.Equals(q));
Console.WriteLine(Object.Equals(p, q));
Console.WriteLine(Object.ReferenceEquals(p, q));
Console.WriteLine(p == q);

La salida que por pantalla mostrará el código anterior es:

True
True
False
False

En los primeros casos se devuelve true porque según la redefinición de Equals() dos
personas son iguales si tienen el mismo DNI, como pasa con los objetos p y q. Sin
embargo, en los últimos casos se devuelve false porque aunque ambos objetos tienen el
mismo DNI cada uno se almacena en la memoria dinámica en una posición distinta, que
es lo que comparan ReferenceEquals() y el operador== (éste último sólo por defecto)

Polimorfismo
Concepto de polimorfismo

El polimorfismo es otro de los pilares fundamentales de la programación orientada a


objetos. Es la capacidad de almacenar objetos de un determinado tipo en variables de
tipos antecesores del primero a costa, claro está, de sólo poderse acceder a través de
dicha variable a los miembros comunes a ambos tipos. Sin embargo, las versiones de los
métodos virtuales a las que se llamaría a través de esas variables no serían las definidas
como miembros del tipo de dichas variables, sino las definidas en el verdadero tipo de los
objetos que almacenan.

A continuación se muestra un ejemplo de cómo una variable de tipo Persona puede


usarse para almacenar objetos de tipo Trabajador. En esos casos el campo Sueldo del
objeto referenciado por la variable no será accesible, y la versión del método
Cumpleaños() a la que se podría llamar a través de la variable de tipo Persona sería la
definida en la clase Trabajador, y no la definida en Persona:

using System;

18
Clases en Microsoft Visual C#

class Persona
{
// Campo de cada objeto Persona que almacena su nombre
public string Nombre;
// Campo de cada objeto Persona que almacena su edad
public int Edad;
// Campo de cada objeto Persona que almacena su NIF
public string NIF;

// Incrementa en uno la edad del objeto Persona


public virtual void Cumpleaños()
{
Console.WriteLine("Incrementada edad de persona");
}

// Constructor de Persona
public Persona (string nombre, int edad, string nif)
{
Nombre = nombre;
Edad = edad;
NIF = nif;
}
}

class Trabajador: Persona


{
// Campo de cada objeto Trabajador que almacena cuánto gana
int Sueldo;

Trabajador(string nombre, int edad, string nif, int sueldo)


: base(nombre, edad, nif)
{// Inicializamos cada Trabajador en base al constructor de Persona
Sueldo = sueldo;
}

public override Cumpleaños()


{
Edad++;
Console.WriteLine("Incrementada edad de trabajador");
}

public static void Main()


{
Persona p = new Trabajador("Josan", 22, "77588260-Z", 100000);
p.Cumpleaños();
// p.Sueldo++; //ERROR: Sueldo no es miembro de Persona
}
}

El mensaje mostrado por pantalla al ejecutar este método confirma lo antes dicho
respecto a que la versión de Cumpleaños() a la que se llama, ya que es:

19
Clases en Microsoft Visual C#

Incrementada edad de trabajador

Métodos genéricos

El polimorfismo es muy útil ya que permite escribir métodos genéricos que puedan recibir
parámetros que sean de un determinado tipo o de cualquiera de sus tipos hijos. Es más,
en tanto que cómo se verá en el epígrafe siguiente, en C# todos los tipos derivan
implícitamente del tipo System.Object, podemos escribir métodos que admitan
parámetros de cualquier tipo sin más que definirlos como métodos que tomen parámetros
de tipo System.Object. Por ejemplo:

public void MétodoGenérico(object o)


{
// Código del método
}

Nótese que en vez de System.Object se ha escrito object, que es el nombre


abreviado incluido en C# para hacer referencia de manera compacta a un tipo tan
frecuentemente usado como System.Object.

Determinación de tipo. Operador is

Dentro de una rutina polimórifica que, como la del ejemplo anterior, admita parámetros
que puedan ser de cualquier tipo, muchas veces es conveniente poder consultar en el
código de la misma cuál es el tipo en concreto del parámetro que se haya pasado al
método en cada llamada al mismo. Para ello C# ofrece el operador is, cuya forma sintaxis
de uso es:

<expresión> is <nombreTipo>

Este operador devuelve true en caso de que el resultado de evaluar <expresión> sea del
tipo cuyo nombre es <nombreTipo> y falseen caso contrario[8]. Gracias a ellas podemos
escribir métodos genéricos que puedan determinar cuál es el tipo que tienen los
parámetros que en cada llamada en concreto se les pasen. O sea, métodos como:

public void MétodoGenérico(object o)


{
if (o is int)// Si o es de tipo int (entero)...
//...Código a ejecutar si el objeto o es de tipo int
else if (o is string) // Si no, si o es de tipo string (cadena)...
//...Código a ejecutar si o es de tipo string
//... Ídem para otros tipos

20
Clases en Microsoft Visual C#

El bloque if...else es una instrucción condicional que permite ejecutar un código u otro
en función de si la condición indicada entre paréntesis tras el if es cierta (true) o no
(false) Esta instrucción se explicará más detalladamente en el Tema 16: Instrucciones

Acceso a la clase base

Hay determinadas circunstancias en las que cuando redefinamos un determinado


método nos interese poder acceder al código de la versión original. Por ejemplo, porque el
código redefinido que vayamos a escribir haga lo mismo que el original y además algunas
cosas extras. En estos casos se podría pensar que una forma de conseguir esto sería
convirtiendo el objeto actual al tipo del método a redefinir y entonces llamar así a ese
método, como por ejemplo en el siguiente código:

using System;
class A
{
public virtual void F()
{
Console.WriteLine("A");
}
}
class B:A
{
public override void F()
{
Console.WriteLine("Antes");
((A) this).F(); // (2)
Console.WriteLine("Después");
}
public static void Main()
{
B b = new B();
b.F();
}
}

Pues bien, si ejecutamos el código anterior veremos que la aplicación nunca termina de
ejecutarse y está constantemente mostrando el mensaje Antes por pantalla. Esto se debe
a que debido al polimorfismo se ha entrado en un bucle infinito: aunque usemos el
operador de conversión para tratar el objeto como si fuese de tipo A, su verdadero tipo
sigue siendo B, por lo que la versión de F() a la que se llamará en (2) es a la de B de
nuevo, que volverá a llamarse así misma una y otra vez de manera indefinida.

Para solucionar esto, los diseñadores de C# han incluido una palabra reservada
llamada base que devuelve una referencia al objeto actual semejante a this pero con la
peculiaridad de que los accesos a ella son tratados como si el verdadero tipo fuese el de

21
Clases en Microsoft Visual C#

su clase base. Usando base, podríamos reemplazar el código de la redefinición de F() de


ejemplo anterior por:

public override void F()


{
Console.WriteLine("Antes");
base.F();
Console.WriteLine("Después");
}

Si ahora ejecutamos el programa veremos que ahora sí que la versión de F() en B llama a
la versión de F() en A, resultando la siguiente salida por pantalla:

Antes
A
Después

A la hora de redefinir métodos abstractos hay que tener cuidado con una cosa: desde el
método redefinidor no es posible usar basepara hacer referencia a métodos abstractos de
la clase padre, aunque sí para hacer referencia a los no abstractos. Por ejemplo:

abstract class A
{
public abstract void F();
public void G(){}
}
class B: A
{
public override void F()
{
base.G();// Correcto
base.F();// Error, base.F() es abstracto
}
}

Downcasting

Dado que una variable de un determinado tipo puede estar en realidad almacenando un
objeto que sea de algún tipo hijo del tipo de la variable y en ese caso a través de la
variable sólo puede accederse a aquellos miembros del verdadero tipo del objeto que sean
comunes con miembros del tipo de la variable que referencia al objeto, muchas veces nos
va a interesar que una vez que dentro de un método genérico hayamos determinado cuál
es el verdadero tipo de un objeto (por ejemplo, con el operador is) podamos tratarlo

22
Clases en Microsoft Visual C#

como tal. En estos casos lo que hay es que hacer una conversión del tipo padre al
verdadero tipo del objeto, y a esto se le llamadowncasting

Para realizar un downcasting una primera posibilidad es indicar preceder la expresión a


convertir del tipo en el que se la desea convertir indicado entre paréntesis. Es decir,
siguiendo la siguiente sintaxis:

(<tipoDestino>) <expresiónAConvertir>

El resultado de este tipo de expresión es el objeto resultante de convertir el resultado de


<expresiónAConvertir> a <tipoDestino>. En caso de que la conversión no se pudiese
realizar se lanzaría una excepción del tipo predefinido System.InvalidCastException

Otra forma de realizar el downcasting es usando el operador as, que se usa así:

<expresiónAConvertir> as <tipoDestino>

La principal diferencia de este operador con el anterior es que si ahora la conversión no


se pudiese realizar se devolvería null en lugar de lanzarse una excepción. La otra
diferencia es que as sólo es aplicable a tipos referencia y sólo a conversiones entre tipos
de una misma jerarquía (de padres a hijos o viceversa)

Los errores al realizar conversiones de este tipo en métodos genéricos se producen


cuando el valor pasado a la variable genérica no es ni del tipo indicado en <tipoDestino>
ni existe ninguna definición de cómo realizar la conversión a ese tipo (cómo definirla se
verá en el Tema 11: Redefinición de operadores)

Clases y métodos sellados

Una clase sellada es una clase que no puede tener clases hijas, y para definirla basta
anteponer el modificador sealed a la definición de una clase normal. Por ejemplo:

sealed class ClaseSellada


{}

Una utilidad de definir una clase como sellada es que permite que las llamadas a sus
métodos virtuales heredados se realicen tan eficientemente como si fuesen no virtuales,
pues al no poder existir clases hijas que los redefinan no puede haber polimorfismo y no
hay que determinar cuál es la versión correcta del método a la que se ha de llamar.
Nótese que se ha dicho métodos virtuales heredados, pues lo que no se permite es definir
miembros virtuales dentro de este tipo de clases, ya que al no poderse heredarse de ellas
es algo sin sentido en tanto que nunca podrían redefinirse.

23
Clases en Microsoft Visual C#

Ahora bien, hay que tener en cuenta que sellar reduce enormemente su capacidad de
reutilización, y eso es algo que el aumento de eficiencia obtenido en las llamadas a sus
métodos virtuales no suele compensar. En realidad la principal causa de la inclusión de
estas clases en C# es que permiten asegurar que ciertas clases críticas nunca podrán
tener clases hijas y sus variables siempre almacenarán objetos del mismo tipo. Por
ejemplo, para simplificar el funcionamiento del CLR y los compiladores se ha optado por
hacer que todos los tipos de datos básicos excepto System.Object estén sellados.

Téngase en cuenta que es absurdo definir simultáneamente una clase


como abstract y sealed, pues nunca podría accederse a la misma al no poderse crear
clases hijas suyas que definan sus métodos abstractos. Por esta razón, el compilador
considera erróneo definir una clase con ambos modificadores a la vez.

Aparte de para sellar clases, también se puede usar sealed como modificador en la
redefinición de un método para conseguir que la nueva versión del mismo que se defina
deje de ser virtual y se le puedan aplicar las optimizaciones arriba comentadas. Un
ejemplo de esto es el siguiente:

class A
{
public abstract F();
}

class B:A
{
public sealed override F() // F() deja de ser redefinible
{}
}

Ocultación de miembros
Hay ocasiones en las que puede resultar interesante usar la herencia únicamente como
mecanismo de reutilización de código pero no necesariamente para reutilizar miembros. Es
decir, puede que interese heredar de una clase sin que ello implique que su clase hija
herede sus miembros tal cuales sino con ligeras modificaciones.

Esto puede muy útil al usar la herencia para definir versiones especializadas de clases
de uso genérico. Por ejemplo, los objetos de la
clase System.Collections.ArrayList incluida en la BCL pueden almacenar cualquier
número de objetos System.Object, que al ser la clase primigenia ello significa que
pueden almacenar objetos de cualquier tipo. Sin embargo, al recuperarlos de este almacén
genérico se tiene el problema de que los métodos que para ello se ofrecen devuelven
objetos System.Object, lo que implicará que muchas veces haya luego que
reconvertirlos a su tipo original mediante downcasting para poder así usar sus métodos
específicos. En su lugar, si sólo se va a usar un ArrayList para almacenar objetos de un
cierto tipo puede resultar más cómodo usar un objeto de alguna clase derivada

24
Clases en Microsoft Visual C#

de ArrayList cuyo método extractor de objetos oculte al heredado de ArrayList y


devuelva directamente objetos de ese tipo.

Para ver más claramente cómo hacer la ocultación, vamos a tomar el siguiente
ejemplo donde se deriva de una clase con un método void F() pero se desea que en la
clase hija el método que se tenga sea de la forma int F():

class Padre
{
public void F()
{}
}
class Hija:Padre
{
public int F()
{return 1;}
}

Como en C# no se admite que en una misma clase hayan dos métodos que sólo se
diferencien en sus valores de retorno, puede pensarse que el código anterior producirá un
error de compilación. Sin embargo, esto no es así sino que el compilador lo que hará será
quedarse únicamente con la versión definida en la clase hija y desechar la heredada de la
clase padre. A esto se le conoce comoocultación de miembro ya que hace desparacer
en la clase hija el miembro heredado, y cuando al compilar se detecte se generará el
siguiente de aviso (se supone que clases.cs almacena el código anterior):

clases.cs(9,15): warning CS0108: The keyword new is required on 'Hija.F()' because it hides inherited
member 'Padre.F()'

Como generalmente cuando se hereda interesa que la clase hija comparta los mismos
miembros que la clase padre (y si acaso que añada miembros extra), el compilador emite
el aviso anterior para indicar que no se está haciendo lo habitual. Si queremos evitarlo
hemos de preceder la definición del método ocultador de la palabra reservada new para
así indicar explícitamente que queremos ocultar el F() heredado:

class Padre
{
public void F()
{}
}
class Hija:Padre
{
new public int F()
{return 1;}
}

25
Clases en Microsoft Visual C#

En realidad la ocultación de miembros no implica los miembros ocultados tengan que


ser métodos, sino que también pueden ser campos o cualquiera de los demás tipos de
miembro que en temas posteriores se verán. Por ejemplo, puede que se desee que un
campo X de tipo int esté disponible en la clase hija como si fuese de tipo string.

Tampoco implica que los miembros métodos ocultados tengan que diferenciarse de los
métodos ocultadores en su tipo de retorno, sino que pueden tener exactamente su mismo
tipo de retorno, parámetros y nombre. Hacer esto puede dar lugar a errores muy sutiles
como el incluido en la siguiente variante de la clase Trabajador donde en vez de
redefinirse Cumpleaños() lo que se hace es ocultarlo al olvidar incluir el override:

using System;
class Persona
{
// Campo de cada objeto Persona que almacena su nombre
public string Nombre;
// Campo de cada objeto Persona que almacena su edad
public int Edad;
// Campo de cada objeto Persona que almacena su NIF
public string NIF;

// Incrementa en uno la edad del objeto Persona


public virtual void Cumpleaños()
{
Console.WriteLine("Incrementada edad de persona");
}

// Constructor de Persona
public Persona (string nombre, int edad, string nif)
{
Nombre = nombre;
Edad = edad;
NIF = nif;
}
}

class Trabajador: Persona

{ // Campo de cada objeto Trabajador que almacena cuánto gana


int Sueldo;

Trabajador(string nombre, int edad, string nif, int sueldo)


: base(nombre, edad, nif)
{ // Inicializamos cada Trabajador en base al constructor de Persona
Sueldo = sueldo;
}

public Cumpleaños()
{
Edad++;
Console.WriteLine("Incrementada edad de trabajador");
}

26
Clases en Microsoft Visual C#

public static void Main()


{
Persona p = new Trabajador("Josan", 22, "77588260-Z", 100000);
p.Cumpleaños();
// p.Sueldo++; //ERROR: Sueldo no es miembro de Persona
}
}

Al no incluirse override se ha perdido la capacidad de polimorfismo, y ello puede


verse en que la salida que ahora mostrara por pantalla el código:

Incrementada edad de persona

Errores de este tipo son muy sutiles y podrían ser difíciles de detectar. Sin embargo, en
C# es fácil hacerlo gracias a que el compilador emitirá el mensaje de aviso ya visto por
haber hecho la ocultación sin new. Cuando el programador lo vea podrá añadirnew para
suprimirlo si realmente lo que quería hacer era ocultar, pero si esa no era su intención así
sabrá que tiene que corregir el código (por ejemplo, añadiendo el override olvidado)

Como su propio nombre indica, cuando se redefine un método se cambia su definición


original y por ello las llamadas al mismo ejecutaran dicha versión aunque se hagan a
través de variables de la clase padre que almacenen objetos de la clase hija donde se
redefinió. Sin embargo, cuando se oculta un método no se cambia su definición en la
clase padre sino sólo en la clase hija, por lo que las llamadas al mismo realizadas a través
de variables de la clase padre ejecutarán la versión de dicha clase padre y las realizadas
mediante variables de la clase hija ejecutarán la versión de la clase hija.

En realidad el polimorfismo y la ocultación no son conceptos totalmente antagónicos, y


aunque no es válido definir métodos que simultáneamente tengan los
modificadores override y new ya que un método ocultador es como si fuese la primera
versión que se hace del mismo (luego no puede redefinirse algo no definido), sí que es
posible combinar new y virtual para definir métodos ocultadores redefinibles. Por
ejemplo:

using System;
class A
{
public virtual void F() { Console.WriteLine("A.F"); }
}
class B: A
{
public override void F() { Console.WriteLine("B.F"); }
}
class C: B
{
new public virtual void F() { Console.WriteLine("C.F"); }
}
class D: C

27
Clases en Microsoft Visual C#

{
public override void F() { Console.WriteLine("D.F"); }
}
class Ocultación
{
public static void Main()
{
A a = new D();
B b = new D();
C c = new D();
D d = new D();
a.F();
b.F();
c.F();
d.F();
}
}

La salida por pantalla de este programa es:

B.F
B.F
D.F
D.F
Aunque el verdadero tipo de los objetos a cuyo método se llama en Main() es D, en las
dos primeras llamadas se llama al F() de B. Esto se debe a que la redefinición dada en B
cambia la versión de F() en A por la suya propia, pero la ocultación dada en C hace que
para la redefinición que posteriormente se da en D se considere que la versión original de
F() es la dada en C y ello provoca que no modifique la versiones de dicho método dadas
en A y B (que, por la redefinición dada en B, en ambos casos son la versión de B)

Un truco mnemotécnico que puede ser útil para determinar a qué versión del método
se llamará en casos complejos como el anterior consiste en considerar que el mecanismo
de polimorfismo funciona como si buscase el verdadero tipo del objeto a cuyo método se
llama descendiendo en la jerarquía de tipos desde el tipo de la variable sobre la que se
aplica el método y de manera que si durante dicho recorrido se llega a alguna versión del
método con new se para la búsqueda y se queda con la versión del mismo incluida en el
tipo recorrido justo antes del que tenía el método ocultador.

Hay que tener en cuenta que el grado de ocultación que proporcione new depende del
nivel de accesibilidad del método ocultador, de modo que si es privado sólo ocultará
dentro de la clase donde esté definido. Por ejemplo, dado:

using System;
class A
{
// F() es un método redefinible

28
Clases en Microsoft Visual C#

public virtual void F()


{
Console.WriteLine("F() de A");
}
}
class B: A
{
// Oculta la versión de F() de A sólo dentro de B
new private void F() {}
}

class C: B
{
// Válido, pues aquí sólo se ve el F() de A
public override void F()
{
base.F();
Console.WriteLine("F() de B");
}
public static void Main()
{
C obj = new C();
obj.F();
}
}

La salida de este programa por pantalla será:

F() de A

F() de B

Pese a todo lo comentado, hay que resaltar que la principal utilidad de poder indicar
explícitamente si se desea redefinir u ocultar cada miembro es que facilita enormemente
la resolución de problemas de versionado de tipos que puedan surgir si al derivar una
nueva clase de otra y añadirle miembros adicionales, posteriormente se la desea actualizar
con una nueva versión de su clase padre pero ésta contiene miembros que entran en
conflictos con los añadidos previamente a la clase hija cuando aún no existían en la clase
padre. En lenguajes donde implícitamente todos los miembros son virtuales, como Java,
esto da lugar a problemas muy graves debidos sobre todo a:

 Que por sus nombres los nuevos miembros de la clase padre entre en conflictos con los
añadidos a la clase hija cuando no existían. Por ejemplo, si la versión inicial de de la clase
padre no contiene ningún método de nombre F(), a la clase hija se le añade void F() y
luego en la nueva versión de la clase padre se incorporado int F(), se producirá un error
por tenerse en la clase hija dos métodos F()

En Java para resolver este problema una posibilidad sería pedir al creador de la clase
padre que cambiase el nombre o parámetros de su método, lo cual no es siempre posible
ni conveniente en tanto que ello podría trasladar el problema a que hubiesen derivado de

29
Clases en Microsoft Visual C#

dicha clase antes de volverla a modificar. Otra posibilidad sería modificar el nombre o
parámetros del método en la clase hija, lo que nuevamente puede llevar a
incompatibilidades si también se hubiese derivado de dicha clase hija.

 Que los nuevos miembros tengan los mismos nombres y tipos de parámetros que los
incluidos en las clases hijas y sea obligatorio que toda redefinición que se haga de ellos
siga un cierto esquema.

Esto es muy problemático en lenguajes como Java donde toda definición de método con
igual nombre y parámetros que alguno de su clase padre es considerado implícitamente
redefinición de éste, ya que difícilmente en una clase hija escrita con anterioridad a la
nueva versión de la clase padre se habrá seguido el esquema necesario. Por ello, para
resolverlo habrá que actualizar la clase hija para que lo siga y de tal manera que los
cambios que se le hagan no afecten a sus subclases, lo que ello puede ser más o menos
difícil según las características del esquema a seguir.

Otra posibilidad sería sellar el método en la clase hija, pero ello recorta la capacidad de
reutilización de dicha clase y sólo tiene sentido si no fue redefinido en ninguna subclase
suya.

En C# todos estos problemas son de fácil solución ya que pueden resolverse con sólo
ocultar los nuevos miembros en la clase hija y seguir trabajando como si no existiesen.

Miembros de tipo
En realidad, dentro la definición de un tipo de dato no tienen porqué incluirse sólo
definiciones de miembros comunes a todos sus objetos, sino también pueden definirse
miembros ligados al tipo como tal y no a los objetos del mismo. Para ello basta preceder
la definición de ese miembro de la palabra reservada static, como muestra este ejemplo:

class A
{
int x;
static int y;
}

Los objetos de clase A sólo van a disponer del campo x, mientras que el campo y va a
pertenecer a la clase A. Por esta razón se dice que los miembros con
modificador static son miembros de tipo y que los no lo tienen son miembros de
objeto.

Para acceder a un miembro de clase ya no es válida la sintaxis hasta ahora vista de


<objeto>.<miembro>, pues al no estar estos miembros ligados a ningún objeto no podría

30
Clases en Microsoft Visual C#

ponerse nada en el campo <objeto>. La sintaxis a usar para acceder a estos miembros
será <nombreClase>.<miembro>, como muestra ejemplo donde se asigna el valor 2 al
miembro y de la clase A definida más arriba:

A.y = 2;

Nótese que la inclusión de miembros de clase rompe con la afirmación indicada al


principio del tema en la que se decía que C# es un lenguaje orientado a objetos puro en
el que todo con lo que se trabaja son objetos, ya que a los miembros de tipo no se les
accede a través de objetos sino nombres de tipos.

Es importante matizar que si definimos una función como static, entonces el código de
la misma sólo podrá acceder implícitamente (sin sintaxis <objeto>.<miembro>) a otros
miembros static del tipo de dato al que pertenezca. O sea, no se podrá acceder a ni a los
miembros de objeto del tipo en que esté definido ni se podrá usar this ya que el método
no está asociado a ningún objeto. O sea, este código sería inválido:

int x;
static void Incrementa()
{
x++; //ERROR: x es miembro de objeto e Incrementa() lo es de clase.
}

También hay que señalar que los métodos estáticos no entran dentro del mecanismo
de redefiniciones descrito en este mismo tema. Dicho mecanismo sólo es aplicable a
métodos de objetos, que son de quienes puede declararse variables y por tanto puede
actuar el polimorfismo. Por ello, incluir los modificadores virtual, override o abstract al
definir un método static es considerado erróneo por el compilador. Eso no significa que
los miembros static no se hereden, sino tan sólo que no tiene sentido redefinirlos.

Encapsulación
Ya hemos visto que la herencia y el polimorfismo eran dos de los pilares fundamentales
en los que es apoya la programación orientada a objetos. Pues bien, el tercero y último es
la encapsulación, que es un mecanismo que permite a los diseñadores de tipos de datos
determinar qué miembros de los tipos creen pueden ser utilizados por otros
programadores y cuáles no. Las principales ventajas que ello aporta son:

 Se facilita a los programadores que vaya a usar el tipo de dato (programadores clientes) el
aprendizaje de cómo trabajar con él, pues se le pueden ocultar todos los detalles relativos
a su implementación interna y sólo dejarle visibles aquellos que puedan usar con
seguridad. Además, así se les evita que cometan errores por manipular inadecuadamente
miembros que no deberían tocar.

31
Clases en Microsoft Visual C#

 Se facilita al creador del tipo la posterior modificación del mismo, pues si los
programadores clientes no pueden acceder a los miembros no visibles, sus aplicaciones no
se verán afectadas si éstos cambian o se eliminan. Gracias a esto es posible crear
inicialmente tipos de datos con un diseño sencillo aunque poco eficiente, y si
posteriormente es necesario modificarlos para aumentar su eficiencia, ello puede hacerse
sin afectar al código escrito en base a la no mejorada de tipo.

La encapsulación se consigue añadiendo modificadores de acceso en las definiciones


de miembros y tipos de datos. Estos modificadores son partículas que se les colocan
delante para indicar desde qué códigos puede accederse a ellos, entendiéndose por
acceder el hecho de usar su nombre para cualquier cosa que no sea definirlo, como
llamarlo si es una función, leer o escribir su valor si es un campo, crear objetos o heredar
de él si es una clase, etc.

Por defecto se considera que los miembros de un tipo de dato sólo son accesibles
desde código situado dentro de la definición del mismo, aunque esto puede cambiarse
precediéndolos de uno los siguientes modificadores (aunque algunos de ellos ya se han
explicado a lo largo del tema, aquí se recogen todos de manera detallada) al definirlos:

 public: Puede ser accedido desde cualquier código.


 protected: Desde una clase sólo puede accederse a miembros protected de objetos de
esa misma clase o de subclases suyas. Así, en el siguiente código las instrucciones
comentadas con // Error no son válidas por lo escrito junto a ellas:

public class A
{
protected int x;
static void F(A a, B b, C c)
{
a.x = 1; // Ok
b.x = 1; // Ok
c.x = 1; // OK
}
}

public class B: A
{
static void F(A a, B b, C c)
{
//a.x = 1; // Error, ha de accederse a traves de objetos tipo B o C
b.x = 1; // Ok
c.x = 1; // Ok
}
}

public class C: B
{
static void F(A a, B b, C c)
{
//a.x = 1; // Error, ha de accederse a traves de objetos tipo C

32
Clases en Microsoft Visual C#

//b.x = 1; // Error, ha de accederse a traves de objetos tipo C


c.x = 1; // Ok
}
}

Obviamente siempre que se herede de una clase se tendrá total acceso en la clase hija –e
implícitamente sin necesidad de usar la sintaxis <objeto>.<miembro>- a los miembros
que ésta herede de su clase padre, como muestra el siguiente ejemplo:

using System;
class A
{
protected int x=5;
}
class B:A
{
B()
{
Console.WriteLine("Heredado x={0} de clase A", x);
}

public static void Main()


{
new B();
}
}

Como es de esperar, la salida por pantalla del programa de ejemplo será:

Heredado x=5 de clase A

A lo que no se podrá acceder desde una clase hija es a los miembros protegidos de otros
objetos de su clase padre, sino sólo a los heredados. Es decir:

using System;
class A
{
protected int x=5;
}
class B:A
{
B(A objeto)
{

33
Clases en Microsoft Visual C#

Console.WriteLine("Heredado x={0} de clase A", x);


Console.WriteLine(objeto.x); // Error, no es el x heredado
}

public static void Main()


{
new B(new A());
}
}

 private: Sólo puede ser accedido desde el código de la clase a la que pertenece. Es lo
considerado por defecto.
 internal: Sólo puede ser accedido desde código perteneciente al ensamblado en que se
ha definido.
 protected internal: Sólo puede ser accedido desde código perteneciente al ensamblado
en que se ha definido o desde clases que deriven de la clase donde se ha definido.

Si se duda sobre el modificador de visibilidad a poner a un miembro, es mejor ponerle


inicialmente el que proporcione menos permisos de accesos, ya que si luego detecta que
necesita darle más permisos siempre podrá cambiárselo por otro menos restringido. Sin
embargo, si se le da uno más permisivo de lo necesario y luego se necesita cambiar por
otro menos permisivo, los códigos que escrito en base a la versión más permisiva que
dependiesen de dicho miembro podrían dejar de funcionar por quedarse sin acceso a él.

Es importante recordar que toda redefinición de un método virtual o abstracto ha de


realizarse manteniendo los mismos modificadores que tuviese el método original. Es decir,
no podemos redefinir un método protegido cambiando su accesibilidad por pública, pues si
el creador de la clase base lo definió así por algo sería.

Respecto a los tipos de datos, por defecto se considera que son accesibles sólo desde
el mismo ensamblado en que ha sido definidos, aunque también es posible modificar esta
consideración anteponiendo uno de los siguientes modificadores a su definición:

 public: Es posible acceder a la clase desde cualquier ensamblado.


 internal: Sólo es posible acceder a la clase desde el ensamblado donde se declaró. Es lo
considerado por defecto.

También pueden definirse tipos dentro de otros (tipos internos) En ese caso serán
considerados miembros del tipo contenedor dentro de la que se hayan definido, por lo que
les serán aplicables todos los modificadores válidos para miembros y por defecto se
considerará que, como con cualquier miembro, son privados. Para acceder a estos tipos
desde código externo a su tipo contenedor (ya sea para heredar de ellos, crear objetos
suyos o acceder a sus miembros estáticos), además de necesitarse los permisos de acceso
necesarios según el modificador de accesibilidad al definirlos, hay que usar la notación
<nombreTipoContendor>.<nombreTipoInterno>, como muestra en este ejemplo:

// No lleva modificador, luego se considera que es internal

34
Clases en Microsoft Visual C#

class A
{
// Si ahora no se pusiese public se consideraría private
public class AInterna {}
}
// B deriva de la clase interna AInterna definida dentro de A.
// Es válido porque A.AInterna es pública
class B:A.AInterna
{}

Nótese que dado que los tipos externos están definidos dentro de su tipo externo,
desde ellos es posible acceder a los miembros estáticos privados de éste. Sin embargo,
hay que señalar que no pueden acceder a los miembros no estáticos de su tipo
contenedor.

Espacios de nombres

Concepto de espacio de nombres

Del mismo modo que los ficheros se organizan en directorios, los tipos de datos se
organizan en espacio de nombres.

Por un lado, esto permite tenerlos más organizados y facilita su localización. De hecho,
así es como se halla organizada la BCL, de modo que todas las clases más comúnmente
usadas en cualquier aplicación se hallan en el espacio de nombres llamado System, las de
acceso a bases de datos en System.Data, las de realización de operaciones de
entrada/salida en System.IO, etc.

Por otro lado, los espacios de nombres también permiten poder usar en un mismo
programa varias clases con igual nombre si pertenecen a espacios diferentes. La idea es
que cada fabricante defina sus tipos dentro de un espacio de nombres propio para que así
no haya conflictos si varios fabricantes definen clases con el mismo nombre y se quieren
usar a la vez en un mismo programa. Obviamente para que esto funcione no han de
coincidir los nombres los espacios de cada fabricante, y una forma de conseguirlo es
dándoles el nombre de la empresa fabricante, o su nombre de dominio en Internet, etc.

Definición de espacios de nombres

Para definir un espacio de nombres se utiliza la siguiente sintaxis:

namespace <nombreEspacio>
{
<tipos>
}

35
Clases en Microsoft Visual C#

Los así definidos pasarán a considerase miembros del espacio de nombres llamado .
Como veremos más adelante, aparte de clases estos tipos pueden ser también interfaces,
estructuras, tipos enumerados y delegados. A continuación se muestra un ejemplo en el
que definimos una clase de nombre ClaseEjemplo perteneciente a un espacio de nombres
llamado EspacioEjemplo:

namespace EspacioEjemplo
{
class ClaseEjemplo
{}
}

El verdadero nombre de una clase, al que se denomina nombre completamente


calificado, es el nombre que le demos al declararla prefijado por la concatenación de todos
los espacios de nombres a los que pertenece ordenados del más externo al más interno y
seguido cada uno de ellos por un punto (carácter .) Por ejemplo, el verdadero nombre de
la clase ClaseEjemplo antes definida es EspacioEjemplo.ClaseEjemplo. Si no definimos una
clase dentro de una definición de espacio de nombres -como se ha hecho en los ejemplos
de temas previos- se considera que ésta pertenece al llamado espacio de nombres global y
su nombre completamente calificado coincidirá con el identificador que tras la palabra
reservada class le demos en su definición (nombre simple)

Aparte de definiciones de tipo, también es posible incluir como miembros de un espacio


de nombres a otros espacios de nombres. Es decir, como se muestra el siguiente ejemplo
es posible anidar espacios de nombres:

namespace EspacioEjemplo
{
namespace EspacioEjemplo2
{
class ClaseEjemplo
{}
}
}

Ahora ClaseEjemplo tendrá EspacioEjemplo.EspacioEjemplo2.ClaseEjemplo como


nombre completamente calificado. En realidad es posible compactar las definiciones de
espacios de nombres anidados usando esta sintaxis de calificación completa para dar el
nombre del espacio de nombres a definir. Es decir, el último ejemplo es equivalente a:

namespace EspacioEjemplo.EspacioEjemplo2
{

36
Clases en Microsoft Visual C#

class ClaseEjemplo
{}
}

En ambos casos lo que se ha definido es una clase ClaseEjemplo perteneciente al


espacio de nombres EspacioEjemplo2 que, a su vez, pertenece al espacio EspacioEjemplo.

Importación de espacios de nombres


Sentencia using

En principio, si desde código perteneciente a una clase definida en un cierto espacio de


nombres se desea hacer referencia a tipos definidos en otros espacios de nombres, se ha
de referir a los mismos usando su nombre completamente calificado. Por ejemplo:

namespace EspacioEjemplo.EspacioEjemplo2
{
class ClaseEjemplo
{}
}
class Principal // Pertenece al espacio de nombres global
{
public static void Main()
{
EspacioEjemplo.EspacioEjemplo2.ClaseEjemplo c =
new EspacioEjemplo.EspacioEjemplo2.ClaseEjemplo();
}
}

Como puede resultar muy pesado tener que escribir nombres tan largos cada vez que
se referencie a tipos así definidos, C# incluye un mecanismo de importación de espacios
de nombres que simplifica la tarea y se basa en una sentencia que usa la siguiente
sintaxis:

using <espacioNombres>;

Estas sentencias siempre han de aparecer en la definición de espacio de nombres antes


que cualquier definición de miembros de la misma. Permiten indicar cuáles serán los
espacios de nombres que se usarán implícitamente dentro de ese espacio de nombres. A

37
Clases en Microsoft Visual C#

los miembros de los espacios de nombres así importados se les podrá referenciar sin usar
calificación completa. Así, aplicando esto al ejemplo anterior quedaría:

using EspacioEjemplo.EspacioEjemplo2;
namespace EspacioEjemplo.EspacioEjemplo2
{
class ClaseEjemplo
{}
}
// (1)
class Principal // Pertenece al espacio de nombres global
{
public static void Main()
{
// EspacioEjemplo.EspacioEjemplo2. está implícito
ClaseEjemplo c = new ClaseEjemplo();
}
}

Nótese que la sentencia using no podría haberse incluido en la zona marcada en el


código como (1) porque entonces se violaría la regla de que todo using ha aparecer en un
espacio de nombres antes que cualquier definición de miembro (la definición del espacio
de nombres EspacioEjemplo.EspacioEjemplo2 es un miembro del espacio de nombres
global) Sin embargo, el siguiente código sí que sería válido:

namespace EspacioEjemplo.EspacioEjemplo2
{
class ClaseEjemplo
{}
}
namespace Principal
{
using EspacioEjemplo.EspacioEjemplo2;
class Principal // Pertenece al espacio de nombres global
{
public static void Main()
{
ClaseEjemplo c = new ClaseEjemplo();
}
}
}

En este caso el using aparece antes que cualquier otra definición de tipos dentro del
espacio de nombres en que se incluye (Principal) Sin embargo, ahora la importación hecha
con el using sólo será válida dentro de código incluido en ese mismo espacio de nombres,
mientras que en el caso anterior era válida en todo el fichero al estar incluida en el
espacio de nombres global.

38
Clases en Microsoft Visual C#

Debe tenerse en cuenta que si una sentencia using importa miembros de igual nombre
que miembros definidos en el espacio de nombres en que se incluye, no se produce error
alguno pero se dará preferencia a los miembros no importados. Un ejemplo:

namespace N1.N2
{
class A {}
class B {}
}
namespace N3
{
using N1.N2;
class A {}
class C: A {}
}

Aquí C deriva de N3.A en vez de N1.N2.A. Si queremos lo contrario tendremos que


referenciar a N1.N2.A por su nombre completo al definir C o, como se explica a
continuación, usar un alias.

Especificación de alias

Aún en el caso de que usemos espacios de nombres distintos para diferenciar clases
con igual nombre pero procedentes de distintos fabricantes, podrían darse conflictos si
usamos sentencias using para importar los espacios de nombres de dichos fabricantes ya
que entonces al hacerse referencia a una de las clases comunes con tan solo su nombre
simple el compilador no podrá determinar a cual de ellas en concreto nos referimos.

Por ejemplo, si tenemos una clase de nombre completamente calificado A.Clase, otra
de nombre B.Clase, y hacemos:

using A;
using B;
class EjemploConflicto: Clase {}

¿Cómo sabrá el compilador si lo que queremos es derivar de A.Clase o de B.Clase? Pues


en realidad no podrá determinarlo y producirá un error informando de que existe una
referencia ambigua a Clase en el código.

Para resolver ambigüedades de este tipo podría hacerse referencia a los tipos en
conflicto usando siempre sus nombres completamente calificados, pero ello puede llegar a
ser muy fatigoso sobre todo si sus nombres son muy largos. Para solucionarlo sin escribir
tanto, C# permite definir alias para cualquier tipo de dato, que son sinónimos que se les
definen utilizando la siguiente sintaxis:

39
Clases en Microsoft Visual C#

using <alias> = <nombreCompletoTipo>;

Como cualquier otro using, las definiciones de alias sólo pueden incluirse al principio
de las definiciones de espacios de nombres y sólo tienen validez dentro de las mismas.

Definiendo alias distintos para los tipos en conflictos se resuelven los problemas de
ambigüedades. Por ejemplo, el problema del ejemplo anterior podría resolverse así:

using A;
using B;
using ClaseA = A.Clase;
class EjemploConflicto: ClaseA
{} // Heredamos de A.Clase

Los alias no tienen porqué ser sólo referentes a tipos, sino que también es posible
escribir alias de espacios de nombres. Por ejemplo:

namespace N1.N2
{
class A {}
}
namespace N3
{
using R1 = N1;
using R2 = N1.N2;
class B
{
N1.N2.A a; // Campo de nombre completamente calificado N1.N2.A
R1.N2.A b; // Campo de nombre completamente calificado N1.N2.A
R2.A c; // Campo de nombre completamente calificado N1.N2.A
}
}

Al definir alias hay que tener cuidado con no definir en un mismo espacio de nombres
varios con igual nombre o cuyos nombres coincidan con los de miembros de dicho espacio
de nombres. También hay que tener en cuenta que no se pueden definir unos alias en
función de otro, por lo que códigos como el siguiente son incorrectos:

namespace N1.N2 {}
namespace N3
{
using R1 = N1;
using R2 = N1.N2;
using R3 = R1.N2; // ERROR: No se puede definir R3 en función de R1
}

40
Clases en Microsoft Visual C#

Espacio de nombres distribuidos


Si hacemos varias definiciones de un espacio de nombres en un mismo o diferentes
ficheros y se compilan todas juntas, el compilador las fusionará en una sola definición
cuyos miembros serán la concatenación de los de definición realizada. Por ejemplo:

namespace A // (1)
{
class B1 {}
}
namespace A // (2)
{
class B2 {}
}

Una definición como la anterior es tratada por el compilador exactamente igual que:

namespace A
{
class B1 {}
class B2 {}
}

Y lo mismo ocurriría si las definiciones marcadas como (1) y (2) se hubiesen hecho en
ficheros separados que se compilasen conjuntamente.

Hay que tener en cuenta que las sentencias using, ya sean de importación de
espacios de nombres o de definición de alias, no son consideradas miembros de los
espacios de nombres y por tanto no participan en sus fusiones. Así, el siguiente código es
inválido:

namespace A
{
class ClaseA {}
}
namespace B
{
using A;
}
namespace B
{
// using A;
class Principal: ClaseA {}

41
Clases en Microsoft Visual C#

Este código no es correcto porque aunque se importa el espacio de nombres A al


principio de una definición del espacio de nombres donde se ha definido Principal, no se
importa en la misma definición donde se deriva Principal de A.ClaseA. Para que el código
compilase habría que descomentar la línea comentada.

Definición de variables
Una variable puede verse simplemente como un hueco en el que se puede almacenar
un objeto de un determinado tipo al que se le da un cierto nombre. Para poderla utilizar
sólo hay que definirla indicando cual erá su nombre y cual será el tipo de datos que podrá
almacenar, lo que se hace siguiendo la siguiente intaxis:

<tipoVariable> <nombreVariable>;

Una variable puede ser definida dentro de una definición de clase, en cuyo caso se
correspondería con el tipo de miembro que hasta ahora hemos denominado campo.
También puede definirse como un variable local a un método, que es una variable definida
dentro del código del método a la que sólo puede accederse desde dentro de dicho
código. Otra posibilidad es definirla como parámetro de un método, que son variables que
almacenan los valores de llamada al método y que, al igual que las variables locales, sólo
puede ser accedidas desde código ubicado dentro del método. El siguiente ejemplo
muestra como definir variables de todos estos casos:

class A
{
int x, z;
int y;
void F(string a, string b)
{
Persona p;
}
}

En este ejemplo las variables x, z e y son campos de tipo int, mientras que p es una
variable local de tipo Persona y a y b son parámetros de tipo string. Como se muestra en
el ejemplo, si un método toma varios parámetros las definiciones de éstos se separan
mediante comas (carácter ,), y si queremos definir varios campos o variables locales (no
válido para parámetros) de un mismo tipo podemos incluirlos en una misma definición
incluyendo en sus nombres separados por comas.
42
Clases en Microsoft Visual C#

Con la sintaxis de definición de variables anteriormente dada simplemente definimos


variables pero no almacenamos ningún objeto inicial en ellas. El compilador dará un valor
por defecto a los campos para los que no se indique explícitamente ningún valor según se
explica en el siguiente apartado. Sin embargo, a la variables locales no les da ningún valor
inicial, pero detecta cualquier intento de leerlas antes de darles valor y produce errores de
compilación en esos casos.

Ya hemos visto que para crear objetos se utiliza el operador new. Por tanto, una forma
de asignar un valor a la variable p del ejemplo anterior sería así:

Persona p;
p = new Persona("José", 22, "76543876-A");

Sin embargo, C# también proporciona una sintaxis más sencilla con la que podremos
asignar un objeto a una variable en el mismo momento se define. Para ello se la ha de
definir usando esta otra notación:

<tipoVariable> <nombreVariable> = <valorInicial>;

Así por ejemplo, la anterior asignación de valor a la variable p podría rescribirse de esta
otra forma más compacta:

Persona p = new Persona("José", 22, "76543876-A");

La especificación de un valor inicial también combinarse con la definición de múltiples


variables separadas por comas en una misma línea. Por ejemplo, las siguientes
definiciones son válidas:

Persona p1 = new Persona("José", 22, "76543876-A"),


p2 = new Persona("Juan", 21, "87654212-S");

Y son tratadas por el compilador de forma completamente equivalentes a haberlas


declarado como:

Persona p1 = new Persona("José", 22, "76543876-A");

43
Clases en Microsoft Visual C#

Persona p2 = new Persona("Juan", 21, "87654212-S");

Tipos de datos básicos


Los tipos de datos básicos son ciertos tipos de datos tan comúnmente utilizados en la
escritura de aplicaciones que en C# se ha incluido una sintaxis especial para tratarlos. Por
ejemplo, para representar números enteros de 32 bits con signo se utiliza el tipo de dato
System.Int32 definido en la BCL, aunque a la hora de crear un objeto a de este tipo que
represente el valor 2 se usa la siguiente sintaxis:

System.Int32 a = 2;

Como se ve, no se utiliza el operador new para crear objeto System.Int32, sino que
directamente se indica el literal que representa el valor a crear, con lo que la sintaxis
necesaria para crear entero de este tipo se reduce considerablemente. Es más, dado lo
frecuente que es el uso de este tipo también se ha predefinido en C# el alias int para el
mismo, por lo que la definición de variable anterior queda así de compacta:

int a = 2;

System.Int32 no es el único tipo de dato básico incluido en C#. En el espacio de


nombres System se han incluido todos estos:

Tipo Descripción Bits Rango de valores Alias

SByte Bytes con signo 8 [-128, 127] sbyte


Byte Bytes sin signo 8 [0, 255] byte
Int16 Enteros cortos con signo 16 [-32.768, 32.767] short
UInt16 Enteros cortos sin signo 16 [0, 65.535] ushort
[-2.147.483.648,
Int32 Enteros normales 32 int
2.147.483.647]
UInt32 Enteros normales sin signo 32 [0, 4.294.967.295] uint
[-9.223.372.036.854.775.808,
Int64 Enteros largos 64 long
9.223.372.036.854.775.807]
[0-
UInt64 Enteros largos sin signo 64 ulong
18.446.744.073.709.551.615]
Single Reales con 7 dígitos de precisión 32 [1,5×10-45 - 3,4×1038] float
Reales de 15-16 dígitos de
Double 64 [5,0×10-324 - 1,7×10308] double
precisión

44
Clases en Microsoft Visual C#

Reales de 28-29 dígitos de


Decimal 128 [1,0×10-28 - 7,9×1028] decimal
precisión
Boolean Valores lógicos 32 true, false bool
Char Caracteres Unicode 16 [„\u0000‟, „\uFFFF‟] char
String Cadenas de caracteres Variable El permitido por la memoria string
Object Cualquier objeto Variable Cualquier objeto object

Tabla 5 : Tipos de datos básicos

Pese a su sintaxis especial, en C# los tipos básicos son tipos del mismo nivel que
cualquier otro tipo del lenguaje. Es decir, heredan de System.Object y pueden ser tratados
como objetos de dicha clase por cualquier método que espere un System.Object, lo que es
muy útil para el diseño de rutinas genéricas que admitan parámetros de cualquier tipo y
es una ventaja importante de C# frente a lenguajes similares como Java donde los tipos
básicos no son considerados objetos.

El valor que por defecto se da a los campos de tipos básicos consiste en poner a cero
todo el área de memoria que ocupen. Esto se traduce en que los campos de tipos básicos
numéricos se inicializan por defecto con el valor 0, los de tipo bool lo hacen con false, los
de tipo char con „\u0000‟, y los de tipo string y object con null.

Ahora que sabemos cuáles son los tipos básicos, es el momento de comentar cuáles
son los sufijos que admiten los literales numéricos para indicar al compilador cuál es el
tipo que se ha de considerar que tiene. Por ejemplo, si tenemos en una clase los métodos:

public static void F(int x)


{...}
public static void F(long x)
{...}

Ante una llamada como F(100), ¿a cuál de los métodos se llamara? Pues bien, en
principio se considera que el tipo de un literal entero es el correspondiente al primero de
estos tipos básicos que permitan almacenarlo: int, uint, long, ulong, por lo que en el caso
anterior se llamaría al primer F() Para llamar al otro podría añadirse el sufijo L al literal y
hacer la llamada con F(100L) En la Tabla 6 se resumen los posibles sufijos válidos:

Sufijo Tipo del literal entero


ninguno Primero de: int, uint, long, ulong
L ó l[9] Primero de: long, ulong
Uóu Primero de: int, uint
UL, Ul, uL, ul, LU, Lu, lU ó lu ulong

Tabla 6: Sufijos de literales enteros

45
Clases en Microsoft Visual C#

Por su parte, en la Tabla 7 se indican los sufijos que admiten los literales reales:

Sufijo Tipo del literal real


Fóf float
ninguno, D ó d double
Móm decimal

Tabla 7: Sufijos de literales reales

Tablas unidimensionales
Una tabla unidimensional o vector es un tipo especial de variable que es capaz de
almacenar en su interior y de manera ordenada uno o varios datos de un determinado
tipo. Para declarar tablas se usa la siguiente sintaxis:

<tipoDatos>[] <nombreTabla>;

Por ejemplo, una tabla que pueda almacenar objetos de tipo int se declara así:

int[] tabla;

Con esto la tabla creada no almacenaría ningún objeto, sino que valdría null. Si se
desea que verdaderamente almacene objetos hay que indicar cuál es el número de
objetos que podrá almacenar, lo que puede hacerse usando la siguiente sintaxis al
declararla:

<tipoDatos>[] <nombreTabla> = new <tipoDatos>[<númeroDatos>];

Por ejemplo, una tabla que pueda almacenar 100 objetos de tipo int se declara así:

int[] tabla = new int[100];

Aunque también sería posible definir el tamaño de la tabla de forma separada a su


declaración de este modo:

int[] tabla;
tabla = new int[100];

46
Clases en Microsoft Visual C#

Con esta última sintaxis es posible cambiar dinámicamente el número de elementos de


una variable tabla sin más que irle asignando nuevas tablas. Ello no significa que una tabla
se pueda redimensionar conservando los elementos que tuviese antes del cambio de
tamaño, sino que ocurre todo lo contrario: cuando a una variable tabla se le asigna una
tabla de otro tamaño, sus elementos antiguos son sobreescritos por los nuevos.

Si se crea una tabla con la sintaxis hasta ahora explicada todos sus elementos tendrían
el valor por defecto de su tipo de dato. Si queremos darles otros valores al declarar la
tabla, hemos de indicarlos entre llaves usando esta sintaxis:

<tipoDatos>[] <nombreTabla> = new <tipoDatos>[] {<valores>};

Han de especificarse tantos <valores> como número de elementos se desee que tenga
la tabla, y si son más de uno se han de separar entre sí mediante comas (,) Nótese que
ahora no es necesario indicar el número de elementos de la tabla (aunque puede hacerse
si se desea), pues el compilador puede deducirlo del número de valores especificados. Por
ejemplo, para declarar una tabla de cuatro elementos de tipo int con valores 5,1,4,0 se
podría hacer lo siguiente:

int[] tabla = new int[] {5,1,4,0};

Incluso se puede compactar aún más la sintaxis declarando la tabla así:

int[] tabla = {5,1,4,0};

También podemos crear tablas cuyo tamaño se pueda establecer dinámicamente a


partir del valor de cualquier expresión que produzca un valor de tipo entero. Por ejemplo,
para crear una tabla cuyo tamaño sea el valor indicado por una variable de tipo int(luego
su valor será de tipo entero) se haría:

int i = 5;
...
int[] tablaDinámica = new int[i];

A la hora de acceder a los elementos almacenados en una tabla basta indicar entre
corchetes, y a continuación de la referencia a la misma, la posición que ocupe en la tabla
el elemento al que acceder. Cuando se haga hay que tener en cuenta que en C# las tablas
se indexan desde 0, lo que significa que el primer elemento de la tabla ocupará su
posición 0, el segundo ocupará la posición 1, y así sucesivamente para el resto de

47
Clases en Microsoft Visual C#

elementos. Por ejemplo, aunque es más ineficiente, la tabla declarada en el último


fragmento de código de ejemplo también podría haberse definido así:

int[] tabla = new int[4];


tabla[0] = 5;
// Por defecto se inicializó a 0,
// luego ahora el valor de tabla[1] pasa a ser 1
tabla[1]++;
tabla[2] = tabla[0] – tabla[1];
// tabla[2] pasa a valer 4, pues 5-4 = 1
// El contenido de la tabla será {5,1,4,0},
// pues tabla[3] se inicializó por defecto a 0.

Hay que tener cuidado a la hora de acceder a los elementos de una tabla ya que si se
especifica una posición superior al número de elementos que pueda almacenar la tabla se
producirá una excepción de tipo System.OutOfBoundsException. En el Tema 16:
Instrucciones se explica qué son las excepciones, pero por ahora basta considerar que son
objetos que informan de situaciones excepcionales (generalmente errores) producidas
durante la ejecución de una aplicación. Para evitar este tipo de excepciones puede
consultar el valor del campo[10] de sólo lectura Length que está asociado a toda tabla y
contiene el número de elementos de la misma. Por ejemplo, para asignar un 7 al último
elemento de la tabla anterior se haría:

tabla[tabla.Length – 1] = 7;
// Se resta 1 porque tabla.Length devuelve 4 pero el último
// elemento de la tabla es tabla[3]

Tablas dentadas

Una tabla dentada no es más que una tabla cuyos elementos son a su vez tablas,
pudiéndose así anidar cualquier número de tablas. Para declarar tablas de este tipo se usa
una sintaxis muy similar a la explicada para las tablas unidimensionales, sólo que ahora se
indican tantos corchetes como nivel de anidación se desee. Por ejemplo, para crear una
tabla de tablas de elementos de tipo intformada por dos elementos, uno de los cuales
fuese una tabla de elementos de tipo int formada por los elementos de valores 1,2 y el
otro fuese una tabla de elementos de tipo int y valores 3,4,5, se puede hacer:

int[][] tablaDentada = new int[2][] {new int[] {1,2}, new int[] {3,4,5}};

Como se indica explícitamente cuáles son los elementos de la tabla declarada no hace
falta indicar el tamaño de la tabla, por lo que la declaración anterior es equivalente a:

48
Clases en Microsoft Visual C#

int[][] tablaDentada = new int[][] {new int[] {1,2}, new int[] {3,4,5}};

Es más, igual que como se vió con las tablas unidimensionales también es válido hacer:

int[][] tablaDentada = {new int[] {1,2}, new int[] {3,4,5}};

Si no quisiésemos indicar cuáles son los elementos de las tablas componentes,


entonces tendríamos que indicar al menos cuál es el número de elementos que podrán
almacenar (se inicializarán con valores por defecto) quedando:

int[][] tablaDentada = {new int[2], new int[3]};

Si no queremos crear las tablas componentes en el momento de crear la tabla dentada,


entonces tendremos que indicar por lo menos cuál es el número de tablas componentes
posibles (cada una valdría null), con lo que quedaría:

int[][] tablaDentada = new int[2][];

Es importante señalar que no es posible especificar todas las dimensiones de una tabla
dentada en su definición si no se indica explícitamente el valor inicial de éstas entre llaves.
Es decir, esta declaración es incorrecta:

int[][] tablaDentada = new int[2][5];

Esto se debe a que el tamaño de cada tabla componente puede ser distinto y con la
sintaxis anterior no se puede decir cuál es el tamaño de cada una. Una opción hubiese
sido considerar que es 5 para todas como se hace en Java, pero ello no se ha
implementado en C# y habría que declarar la tabla de, por ejemplo, esta manera:

int[][] tablaDentada = {new int[5], new int[5]);

Finalmente, si sólo queremos declarar una variable tabla dentada pero no queremos
indicar su número de elementos, (luego la variable valdría null), entonces basta poner:

int[][] tablaDentada;

49
Clases en Microsoft Visual C#

Hay que precisar que aunque en los ejemplos hasta ahora presentes se han escrito
ejemplos basados en tablas dentadas de sólo dos niveles de anidación, también es
posible crear tablas dentadas de cualquier número de niveles de anidación. Por ejemplo,
para una tabla de tablas de tablas de enteros de 2 elementos en la que el primero fuese
una tabla dentada formada por dos tablas de 5 enteros y el segundo elemento fuese una
tabla dentada formada por una tabla de 4 enteros y otra de 3 se podría definir así:

int[][][] tablaDentada = new int[][][]


{ new int[][] {new int[5], new int[5]},
new int[][] {new int[4], new int[3]}};

A la hora de acceder a los elementos de una tabla dentada lo único que hay que hacer
es indicar entre corchetes cuál es el elemento exacto de las tablas componentes al que se
desea acceder, indicándose un elemento de cada nivel de anidación entre unos corchetes
diferentes pero colocándose todas las parejas de corchetes juntas y ordenadas de la tabla
más externa a la más interna. Por ejemplo, para asignar el valor 10 al elemento cuarto de
la tabla que es elemento primero de la tabla que es elemento segundo de la tabla dentada
declarada en último lugar se haría:

tablaDentada[1][0][3] = 10;

Tablas multidimensionales
Una tabla multidimensional o matriz es aquella cuyos elementos se encuentran
organizados en una estructura de varias dimensiones. Para definirlas se utiliza una sintaxis
similar a la usada para declarar tablas unidimensionales pero separando las diferentes
dimensiones mediante comas (,) Por ejemplo, una tabla multidimensional de elementos de
tipo int que conste de 12 elementos puede tener sus elementos distribuidos en dos
dimensiones formando una estructura 3x4 similar a una matriz de la forma:

1 2 3

4 5 6

7 8 9

Esta tabla se podría declarar así:

int[,] tablaMultidimensional = new int[3,4] {{1,2,3,4},


{5,6,7,8},
{9,10,11,12}};

50
Clases en Microsoft Visual C#

En realidad no es necesario indicar el número de elementos de cada dimensión de la tabla


ya que pueden deducirse de los valores explícitamente indicados entre llaves, por lo que la
definición anterior es similar a esta:

int[,] tablaMultidimensional = new int[,] {{1,2,3,4},


{5,6,7,8},
{9,10,11,12}};

Incluso puede reducirse aún más la sintaxis necesaria quedando tan sólo:

int[,] tablaMultidimensional = {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}};

Si no queremos indicar explícitamente los elementos de la tabla al declararla, podemos


obviarlos pero aún así indicar el tamaño de cada dimensión de la tabla (a los elementos se
les daría el valor por defecto de su tipo de dato) así:

int[,] tablaMultidimensional = new int[3,4];

También podemos no especificar ni siquiera el número de elementos de la tabla de esta


forma (tablaMultidimensional contendría ahora null):

int[,] tablaMultidimensional;

Aunque los ejemplos de tablas multidimensionales hasta ahora mostrados son de tablas
de dos dimensiones, en general también es posible crear tablas de cualquier número de
dimensiones. Por ejemplo, una tabla que almacene 24 elementos de tipo int y valor 0 en
una estructura tridimensional 3x4x2 se declararía así:

int[,,] tablaMultidimensional = new int[3,4,2];

El acceso a los elementos de una tabla multidimensional es muy sencillo: sólo hay que
indicar los índices de la posición que ocupe en la estructura multidimensional el elemento
al que se desee acceder. Por ejemplo, para incrementar en una unidad el elemento que
ocupe la posición (1,3,2) de la tabla anterior se haría (se indiza desde 0):

51
Clases en Microsoft Visual C#

tablaMultidimensional[0,2,1]++;

Nótese que tanto las tablas dentadas como las tablas multidimensionales pueden ser
utilizadas tanto para representar estructuras matriciales como para, en general,
representar cualquier estructura de varias dimensiones. La diferencia entre ambas son:

 Como las tablas dentadas son tablas de tablas, cada uno de sus elementos puede ser
una tabla de un tamaño diferente. Así, con las tablas dentadas podemos representar
matrices en las que cada columna tenga un tamaño distinto (por el aspecto “aserrado”
de este tipo de matrices es por lo que se les llama tablas dentadas), mientras que
usando tablas multidimensionales sólo es posible crear matrices rectangulares o
cuadradas. Las estructuras aserradas pueden simularse usando matrices
multidimensionales con todas sus columnas del tamaño de la columna más grande
necesaria, aunque ello implica desperdiciar mucha memoria sobre todo si los tamaños de
cada columna son muy diferentes y la tabla es grande. De todos modos, las estructuras
más comunes que se usan en la mayoría de aplicaciones suelen ser rectangulares o
cuadradas.
 Los tiempos que se tardan en crear y destruir tablas dentadas son superiores a los que
se tardan en crear y destruir tablas multidimensionales. Esto se debe a que las primeras
son tablas de tablas mientras que las segundas son una única tabla, Por ejemplo, para
crear una tabla dentada [100][100] hay que crear 101 tablas (la tabla dentada más las
100 tablas que contiene), mientras que para crear una crear una tabla bidimensional
[100,100] hay que crear una única tabla.
 Las tablas dentadas no forman parte del CLS, por lo que no todos los lenguajes
gestionados los tienen porqué admitir. Por ejemplo Visual Basic.NET no las admite, por
lo que al usarlas en miembros públicos equivale a perder interoperabilidad con estos
lenguajes.

Tablas mixtas

Una tabla mixta es simplemente una tabla formada por tablas multidimensionales y
dentadas combinadas entre sí de cualquier manera. Para declarar una tabla de este tipo
basta con tan solo combinar las notaciones ya vistas para las multidimensionales y
dentadas. Por ejemplo, para declarar una tabla de tablas multidimensionales cuyos
elementos sean tablas unidimensionales de enteros se haría lo siguiente:

int[][,][] tablaMixta;

Covarianza de tablas

La covarianza de tablas es el resultado de llevar el polimorfismo al mundo de las tablas.


Es decir, es la capacidad de toda tabla de poder almacenar elementos de clases hijas de la
clase de elementos que pueda almacenar. Por ejemplo, en tanto que todas clases son
hijas de System.Object, la siguiente asignación es válida:

52
Clases en Microsoft Visual C#

string[] tablaCadenas = {"Manolo", "Paco", "Pepe"};


object[] tablaObjetos = tablaCadenas;

Hay que tener en cuenta que la covarianza de tablas sólo se aplica a objetos de tipos
referencia y no a objetos de tipos valor Por ejemplo, la siguiente asignación no sería válida
en tanto que int es un tipo por valor:

int[] tablaEnteros = {1, 2, 3};


object[] tablaObjetos = tablaEnteros;

La clase System.Array
En realidad, todas las tablas que definamos, sea cual sea el tipo de elementos que
contengan, son objetos que derivan deSystem.Array. Es decir, van a disponer de todos
los miembros que se han definido para esta clase, entre los que son destacables:

 Length: Campo[11] de sólo lectura que informa del número total de elementos que
contiene la tabla. Si la tabla tiene más de una dimensión o nivel de anidación indica el
número de elementos de todas sus dimensiones y niveles. Por ejemplo:

int[] tabla = {1,2,3,4};


int[][] tabla2 = {new int[] {1,2}, new int[] {3
int[,] tabla3 = {{1,2},{3,4,5,6}};

Console.WriteLine(tabla.Length); //Imprime 4
Console.WriteLine(tabla2.Length); //Imprime 5
Console.WriteLine(tabla3.Length); //Imprime 6

 Rank: Campo de sólo lectura que almacena el número de dimensiones de la tabla.


Obviamente si la tabla no es multidimensional valdrá 1. Por ejemplo:

int[] tabla = {1,2,3,4};


int[][] tabla2 = {new int[] {1,2}, new int[] {3,4,5}};
int[,] tabla3 = {{1,2},{3,4,5,6}};

Console.WriteLine(tabla.Rank); //Imprime 1
Console.WriteLine(tabla2.Rank); //Imprime 1
Console.WriteLine(tabla3.Rank); //Imprime 2

53
Clases en Microsoft Visual C#

 int GetLength(int dimensión): Método que devuelve el número de elementos de la


dimensión especificada. Las dimensiones se indican empezando a contar desde cero, por
lo que si quiere obtenerse el número de elementos de la primera dimensión habrá que
usar GetLength(0), si se quiere obtener los de la segunda habrá que usar GetLength(1),
etc. Por ejemplo:

int[,] tabla = {{1,2}, {3,4,5,6}};


Console.WriteLine(tabla.GetLength(0)); // Imprime 2
Console.WriteLine(tabla.GetLength(1)); // Imprime 4

 void CopyTo(Array destino, int posición): Copia todos los elementos de la tabla
sobre la que se aplica en la tabla destinoa partir de la posición de ésta indicada. Por
ejemplo:

int[] tabla1 = {1,2,3,4};


int[] tabla2 = {5,6,7,8, 9};
tabla1.CopyTo(tabla2,0);
// A partir de ahora, tabla2 contendrá {5,1,2,3,4}

Ambas tablas deben ser unidimensionales, la tabla de destino hade ser de un tipo que
pueda almacenar los objetos de la tabla origen, el índice especificado ha de ser válido
(mayor o igual que cero y menor que el tamaño de la tabla de destino) y no ha de
valernull ninguna de las tablas. Si no fuese así, saltarían excepciones de diversos tipos
informando del error cometido (en la documentación del SDK puede ver cuáles son en
concreto)

Aparte de los miembros aquí señalados, System.Array también cuenta con muchos
otros que facilitan realizar tareas tan frecuentes como búsquedas de elementos,
ordenaciones, etc. Para más información sobre ellos puede consultarse la documentación
del SDK.

Constantes
Una constante es una variable cuyo valor puede determinar el compilador durante la
compilación y puede aplicar optimizaciones derivadas de ello. Para que esto sea posible se
ha de cumplir que el valor de una constante no pueda cambiar durante la ejecución, por lo
que el compilador informará con un error de todo intento de modificar el valor inicial de
una constante. Las constantes se definen como variables normales pero precediendo el
nombre de su tipo del modificador const y dándoles siempre un valor inicial al
declararlas. O sea, con esta sintaxis:

const <tipoConstante> <nombreConstante> = <valor>;

54
Clases en Microsoft Visual C#

Así, ejemplos de definición de constantes es el siguiente:

const int a = 123;


const int b = a + 125;

Dadas estas definiciones de constantes, lo que hará el compilador será sustituir en el


código generado todas las referencias a las constantes a y b por los valores 123 y 248
respectivamente, por lo que el código generado será más eficiente ya que no incluirá el
acceso y cálculo de los valores de a y b. Nótese que puede hacer esto porque en el código
se indica explícitamente cual es el valor que siempre tendrá a y, al ser este un valor fijo,
puede deducir cuál será el valor que siempre tendrá b. Para que el compilador pueda
hacer estos cálculos se ha de cumplir que el valor que se asigne a las constantes en su
declaración sea una expresión constante. Por ejemplo, el siguiente código no es válido en
tanto que el valor de x no es constante:

// x es una variable normal, no una constante


int x = 123;
// Error: x no tiene porqué tener valor constante (aunque aquí lo tenga)
const int y = x +123;

Debido a la necesidad de que el valor dado a una constante sea precisamente


constante, no tiene mucho sentido crear constantes de tipos de datos no básicos, pues a
no ser que valgan null sus valores no se pueden determinar durante la compilación sino
únicamente tras la ejecución de su constructor. La única excepción a esta regla son los
tipos enumerados, cuyos valores se pueden determinar al compilar como se explicará
cuando los veamos en el Tema 14: Enumeraciones

Todas las constantes son implícitamente estáticas, por lo se considera erróneo incluir el
modificador static en su definición al no tener sentido hacerlo. De hecho, para leer su
valor desde códigos externos a la definición de la clase donde esté definida la constante,
habrá que usar la sintaxis <nombreClase>.<nombreConstante> típica de los
campos static.

Por último, hay que tener en cuenta que una variable sólo puede ser definida como
constante si es una variable local o un campo, pero no si es un parámetro.

Variables de sólo lectura

Dado que hay ciertos casos en los que resulta interesante disponer de la capacidad de
sólo lectura que tienen las constantes pero no es posible usarlas debido a las restricciones
que hay impuestas sobre su uso, en C# también se da la posibilidad de definir variables
que sólo puedan ser leídas. Para ello se usa la siguiente sintaxis:

55
Clases en Microsoft Visual C#

readonly <tipoConstante> <nombreConstante> = <valor>;

Estas variables superan la mayoría de las limitaciones de las constantes. Por ejemplo:

 No es obligatorio darles un valor al definirlas, sino que puede dárseles en el constructor.


Ahora bien, una vez dado un valor a una variable readonly ya no es posible volverlo a
modificar. Si no se le da ningún valor ni en su constructor ni en su definición tomará el
valor por defecto correspondiente a su tipo de dato.

 No tienen porqué almacenar valores constantes, sino que el valor que almacenen puede
calcularse durante la ejecución de la aplicación.

 No tienen porqué definirse como estáticas, aunque si se desea puede hacerse.

 Su valor se determina durante la ejecución de la aplicación, lo que permite la actualización


de códigos cliente sin necesidad de recompilar. Por ejemplo, dado:

namespace Programa1
{
public class Utilidad
{
public static readonly int X = 1;
}
}
namespace Programa2
{
class Test
{
public static void Main()
{
System.Console.WriteLine(Programa1.Utilidad.X);
}
}
}

En principio, la ejecución de este programa producirá el valor 1. Sin embargo, si cada


espacio de nombres se compilan en módulos de código separados que luego se enlazan
dinámicamente y cambiamos el valor de X, sólo tendremos que recompilar el módulo
donde esté definido Programa1.Utilidad y Programa2.Test podrá ejecutarse usando el
nuevo valor de X sin necesidad de recompilarlo.

Sin embargo, pese a las ventajas que las variables de sólo lectura ofrecen respecto a
las constantes, tienen dos inconvenientes respecto a éstas: sólo pueden definirse como
campos (no como variables locales) y con ellas no es posible realizar las optimizaciones de
código comentadas para las constantes.

56
Clases en Microsoft Visual C#

Orden de inicialización de variables


Para deducir el orden en que se inicializarán las variables de un tipo de dato basta
saber cuál es el momento en que se inicializa cada una y cuando se llama a los
constructores:

 Los campos estáticos sólo se inicializan la primera vez que se accede al tipo al que
pertenecen, pero no en sucesivos accesos. Estos accesos pueden ser tanto para crear
objetos de dicho tipo como para acceder a sus miembros estáticos. La inicialización se
hace de modo que en primer lugar se dé a cada variable el valor por defecto
correspondiente a su tipo, luego se dé a cada una el valor inicial especificado al definirlas,
y por último se llame al constructor del tipo. Un constructor de tipo es similar a un
constructor normal sólo que en su código únicamente puede accederse a
miembros static (se verá en el Tema 8: Métodos)

 Los campos no estáticos se inicializan cada vez que se crea un objeto del tipo de dato
al que pertenecen. La inicialización se hace del mismo modo que en el caso de los campos
estáticos, y una vez terminada se pasa a ejecutar el código del constructor especificado al
crear el objeto. En caso de que la creación del objeto sea el primer acceso que se haga al
tipo de dato del mismo, entonces primero se inicializarán los campos estáticos y luego los
no estáticos.

 Los parámetros se inicializan en cada llamada al método al que pertenecen con los
valores especificados al llamarlo.

 Las variables locales se inicializan en cada llamada al método al cual pertenecen pero
tras haberse inicializado los parámetros definidos para el mismo. Si no se les da valor
inicial no toman ninguno por defecto, considerándose erróneo todo acceso de lectura que
se haga a las mismas mientras no se les escriba algún valor.

Hay que tener en cuenta que al definirse campos estáticos pueden hacerse definiciones
cíclicas en las que el valor de unos campos dependa del de otros y el valor de los
segundos dependa del de los primeros. Por ejemplo:

class ReferenciasCruzadas
{
static int a = b + 1;
static int b = a + 1;

public static void Main()


{
System.Console.WriteLine("a = {0}, b = {1}", a, b);
}
}

57
Clases en Microsoft Visual C#

Esto sólo es posible hacerlo al definir campos estáticos y no entre campos no estáticas
o variables locales, ya que no se puede inicializar campos no estáticos en función del valor
de otros miembros no estáticos del mismo objeto porque el objeto aún no estaría
inicializado, y no se pueden inicializar variables locales en función del valor de otras
variables locales definidas más adelante porque no se pueden leer variables no
inicializadas. Además, aunque las constantes sean implícitamente estáticas tampoco puede
hacerse definiciones cíclicas entre constantes.

En primer lugar, hay que señalar que escribir un código como el del ejemplo anterior no
es un buen hábito de programación ya que dificulta innecesariamente la legibilidad del
programa. Aún así, C# admite este tipo de códigos y para determinar el valor con que se
inicializarán basta tener en cuenta que siempre se inicializan primero todos los campos
con sus valores por defecto y luego se inicializan aquellos que tengan valores iniciales con
dichos valores iniciales y en el mismo orden en que aparezcan en el código fuente. De
este modo, la salida del programa de ejemplo anterior será:

a = 1, b = 2

Nótese que lo que se ha hecho es inicializar primero a y b con sus valores por defecto
(0 en este caso), luego calcular el valor final de a y luego calcular el valor final de b. Como
b vale 0 cuando se calcula el valor final de a, entonces el valor final de a es 1; y como
avale 1 cuando se calcula el valor final de b, entonces el valor final de b es 2.

Concepto de método
Un método es un conjunto de instrucciones a las que se les da un determinado
nombre de tal manera que sea posible ejecutarlas en cualquier momento sin tenerlas que
rescribir sino usando sólo su nombre. A estas instrucciones se les denomina cuerpo del
método, y a su ejecución a través de su nombre se le denomina llamada al método.

La ejecución de las instrucciones de un método puede producir como resultado un


objeto de cualquier tipo. A este objeto se le llamavalor de retorno del método y es
completamente opcional, pudiéndose escribir métodos que no devuelvan ninguno.

La ejecución de las instrucciones de un método puede depender del valor de unas


variables especiales denominadas parámetrosdel método, de manera que en función del
valor que se dé a estas variables en cada llamada la ejecución del método se pueda
realizar de una u otra forma y podrá producir uno u otro valor de retorno.

Al conjunto formado por el nombre de un método y el número y tipo de sus parámetros


se le conoce como signatura del método. La signatura de un método es lo que
verdaderamente lo identifica, de modo que es posible definir en un mismo tipo varios
métodos con idéntico nombre siempre y cuando tengan distintos parámetros. Cuando esto
ocurre se dice que el método que tiene ese nombre estásobrecargado.

58
Clases en Microsoft Visual C#

Definición de métodos

Para definir un método hay que indicar tanto cuáles son las instrucciones que forman
su cuerpo como cuál es el nombre que se le dará, cuál es el tipo de objeto que puede
devolver y cuáles son los parámetros que puede tomar. Esto se indica definiéndolo así:

<tipoRetorno> <nombreMétodo>(<parámetros>)
{
<cuerpo>
}

En <tipoRetorno> se indica cuál es el tipo de dato del objeto que el método devuelve,
y si no devuelve ninguno se ha de escribirvoid en su lugar.

Como nombre del método se puede poner en <nombreMétodo> cualquier identificador


válido. Como se verá más adelante en elTema 15: Interfaces, también es posible incluir en
<nombreMétodo> información de explicitación de implementación de interfaz, pero por
ahora podemos considerar que siempre será un identificador.

Aunque es posible escribir métodos que no tomen parámetros, si un método los toma
se ha de indicar en <parámetros> cuál es el nombre y tipo de cada uno, separándolos con
comas si son más de uno y siguiendo la sintaxis que más adelante se explica.

El <cuerpo> del método también es opcional, pero si el método retorna algún tipo de
objeto entonces ha de incluir al menos una instrucción return que indique cuál objeto.

La sintaxis anteriormente vista no es la que se usa para definir métodos abstractos.


Como ya se vio en el Tema 5: Clases, en esos casos lo que se hace es sustituir el cuerpo
del método y las llaves que lo encierran por un simple punto y coma (;) Más adelante en
este tema veremos que eso es también lo que se hace para definir métodos externos.

A continuación se muestra un ejemplo de cómo definir un método de nombre Saluda


cuyo cuerpo consista en escribir en la consola el mensaje “Hola Mundo” y que devuelva
un objeto int de valor 1:

int Saluda()
{
Console.WriteLine("Hola Mundo");
return 1;
}

59
Clases en Microsoft Visual C#

Llamada a métodos

La forma en que se puede llamar a un método depende del tipo de método del que se
trate. Si es un método de objeto (método no estático) se ha de usar la notación:

<objeto>.<nombreMétodo>(<valoresParámetros>)

El <objeto> indicado puede ser directamente una variable del tipo de datos al que
pertenezca el método o puede ser una expresión que produzca como resultado una
variable de ese tipo (recordemos que, debido a la herencia, el tipo del <objeto> puede ser
un subtipo del tipo donde realmente se haya definido el método); pero si desde código de
algún método de un objeto se desea llamar a otro método de ese mismo objeto, entonces
se ha de dar el valor this a <objeto>.

En caso de que sea un método de tipo (método estático), entones se ha de usar:

<tipo>.<nombreMétodo>(<valoresParámetros>)

Ahora en <tipo> ha de indicarse el tipo donde se haya definido el método o algún


subtipo suyo. Sin embargo, si el método pertenece al mismo tipo que el código que lo
llama entonces se puede usar la notación abreviada:

<nombreMétodo>(<valoresParámetros>)

El formato en que se pasen los valores a cada parámetro en <valoresParámetros> a


aquellos métodos que tomen parámetros depende del tipo de parámetro que sea. Esto se
explica en el siguiente apartado.

Tipos de parámetros. Sintaxis de definición


La forma en que se define cada parámetro de un método depende del tipo de
parámetro del que se trate. En C# se admiten cuatro tipos de parámetros: parámetros de
entrada, parámetros de salida, parámetros por referencia y parámetros de número
indefinido.

Parámetros de entrada

Un parámetro de entrada recibe una copia del valor que almacenaría una variable
del tipo del objeto que se le pase. Por tanto, si el objeto es de un tipo valor se le pasará
una copia del objeto y cualquier modificación que se haga al parámetro dentro del cuerpo
del método no afectará al objeto original sino a su copia; mientras que si el objeto es de
un tipo referencia entonces se le pasará una copia de la referencia al mismo y cualquier
60
Clases en Microsoft Visual C#

modificación que se haga al parámetro dentro del método también afectará al objeto
original ya que en realidad el parámetro referencia a ese mismo objeto original.

Para definir un parámetro de entrada basta indicar cuál el nombre que se le desea dar
y el cuál es tipo de dato que podrá almacenar. Para ello se sigue la siguiente sintaxis:

<tipoParámetro> <nombreParámetro>

Por ejemplo, el siguiente código define un método llamado Suma que toma dos
parámetros de entrada de tipo int llamados par1 y par2 y devuelve un int con su suma:

int Suma(int par1, int par2)


{
return par1+par2;
}

Como se ve, se usa la instrucción return para indicar cuál es el valor que ha de
devolver el método. Este valor es el resultado de ejecutar la expresión par1+par2; es
decir, es la suma de los valores pasados a sus parámetros par1 y par2 al llamarlo.

En las llamadas a métodos se expresan los valores que se deseen dar a este tipo de
parámetros indicando simplemente el valor deseado. Por ejemplo, para llamar al método
anterior con los valores 2 y 5 se haría <objeto>.Suma(2,5), lo que devolvería el valor 7.

Todo esto se resume con el siguiente ejemplo:

using System;
class ParámetrosEntrada
{
public int a = 1;
public static void F(ParametrosEntrada p)
{
p.a++;
}
public static void G(int p)
{
p++;
}
public static void Main()
{
int obj1 = 0;
ParámetrosEntrada obj2 = new ParámetrosEntrada();
G(obj1);
F(obj2);
Console.WriteLine("{0}, {1}", obj1, obj2.a);

61
Clases en Microsoft Visual C#

}
}

Este programa muestra la siguiente salida por pantalla:

0, 2

Como se ve, la llamada al método G() no modifica el valor que tenía obj1 antes de
llamarlo ya que obj1 es de un tipo valor (int) Sin embargo, como obj2 es de un tipo
referencia (ParámetrosLlamadas) los cambios que se le hacen dentro de F() al pasárselo
como parámetro sí que le afectan.

Parámetros de salida

Un parámetro de salida se diferencia de uno de entrada en que todo cambio que se


le realice en el código del método al que pertenece afectará al objeto que se le pase al
llamar dicho método tanto si éste es de un tipo por valor como si es de un tipo referencia.
Esto se debe a que lo que a estos parámetros se les pasa es siempre una referencia al
valor que almacenaría una variable del tipo del objeto que se les pase.

Cualquier parámetro de salida de un método siempre ha de modificarse dentro del


cuerpo del método y además dicha modificación ha de hacerse antes que cualquier lectura
de su valor. Si esto no se hiciese así el compilador lo detectaría e informaría de ello con un
error. Por esta razón es posible pasar parámetros de salida que sean variables no
inicializadas, pues se garantiza que en el método se inicializarán antes de leerlas. Además,
tras la llamada a un método se considera que las variables que se le pasaron como
parámetros de salida ya estarán inicializadas, pues dentro del método seguro que se las
inicializa.

Nótese que este tipo de parámetros permiten diseñar métodos que devuelvan múltiples
objetos: un objeto se devolvería como valor de retorno y los demás se devolverían
escribiéndolos en los parámetros de salida.

Los parámetros de salida se definen de forma parecida a los parámetros de entrada


pero se les ha de añadir la palabra reservadaout. O sea, se definen así:

out <tipoParámetro> <nombreParámetro>

Al llamar a un método que tome parámetros de este tipo también se ha preceder el


valor especificado para estos parámetros del modificador out. Una utilidad de esto es
facilitar la legibilidad de las llamadas a métodos. Por ejemplo, dada una llamada de la
forma:

62
Clases en Microsoft Visual C#

a.f(x, out z)

Es fácil determinar que lo que se hace es llamar al método f() del objeto a pasándole x
como parámetro de entrada y z como parámetro de salida. Además, también se puede
deducir que el valor de z cambiará tras la llamada.

Sin embargo, la verdadera utilidad de forzar a explicitar en las llamadas el tipo de paso
de cada parámetro es que permite evitar errores derivados de que un programador pase
una variable a un método y no sepa que el método la puede modificar. Teniéndola que
explicitar se asegura que el programador sea consciente de lo que hace.

Parámetros por referencia

Un parámetro por referencia es similar a un parámetro de salida sólo que no es


obligatorio modificarlo dentro del método al que pertenece, por lo que será obligatorio
pasarle una variable inicializada ya que no se garantiza su inicialización en el método.

Los parámetros por referencia se definen igual que los parámetros de salida pero
sustituyendo el modificador out por el modificadorref. Del mismo modo, al pasar valores
a parámetros por referencia también hay que precederlos del ref.

Parámetros de número indefinido

C# permite diseñar métodos que puedan tomar cualquier número de parámetros. Para
ello hay que indicar como último parámetro del método un parámetro de algún tipo de
tabla unidimensional o dentada precedido de la palabra reservada params. Por ejemplo:

static void F(int x, params object[] extras)


{}

Todos los parámetros de número indefinido que se pasan al método al llamarlo han de
ser del mismo tipo que la tabla. Nótese que en el ejemplo ese tipo es la clase
primigenia object, con lo que se consigue que gracias al polimorfismo el método pueda
tomar cualquier número de parámetros de cualquier tipo. Ejemplos de llamadas válidas
serían:

F(4);// Pueden pasarse 0 parámetros indefinidos


F(3,2);
F(1, 2, "Hola", 3.0, new Persona());
F(1, new object[] {2,"Hola", 3.0, new Persona});

63
Clases en Microsoft Visual C#

El primer ejemplo demuestra que el número de parámetros indefinidos que se pasen


también puede ser 0. Por su parte, los dos últimos ejemplos son totalmente equivalentes,
pues precisamente la utilidad de palabra reservada params es indicar que se desea que la
creación de la tabla object[] se haga implícitamente.

Es importante señalar que la prioridad de un método que incluya el params es inferior


a la de cualquier otra sobrecarga del mismo. Es decir, si se hubiese definido una
sobrecarga del método anterior como la siguiente:

static void F(int x, int y)


{}

Cuando se hiciese una llamada como F(3,2) se llamaría a esta última versión del
método, ya que aunque la del params es también aplicable, se considera que es menos
prioritaria.

Sobrecarga de tipos de parámetros

En realidad los modificadores ref y out de los parámetros de un método también


forman parte de lo que se conoce como signatura del método, por lo que esta clase es
válida:

class Sobrecarga
{
public void f(int x)
{}
public void f(out int x)
{}
}

Nótese que esta clase es correcta porque cada uno de sus métodos tiene una signatura
distinta: el parámetro es de entrada en el primero y de salida en el segundo.

Sin embargo, hay una restricción: no puede ocurrir que la única diferencia entre la
signatura de dos métodos sea que en uno un determinado parámetro lleve el
modificador ref y en el otro lleve el modificador out. Por ejemplo, no es válido:

class SobrecargaInválida
{
public void f(ref int x)
{}
public void f(out int x)

64
Clases en Microsoft Visual C#

{}
}

Métodos externos
Un método externo es aquél cuya implementación no se da en el fichero fuente en
que es declarado. Estos métodos se declaran precediendo su declaración del
modificador extern. Como su código se da externamente, en el fuente se sustituyen las
llaves donde debería escribirse su cuerpo por un punto y coma (;), quedando una sintaxis
de la forma:

extern <nombreMétodo>(<parámetros>);

La forma en que se asocie el código externo al método no está definida en la


especificación de C# sino que depende de la implementación que se haga del lenguaje. El
único requisito es que no pueda definirse un método como abstracto y externo a la vez,
pero por todo lo demás puede combinarse con los demás modificadores, incluso
pudiéndose definir métodos virtuales externos.

La forma más habitual de asociar código externo consiste en preceder la declaración del
método de un atributo de tipoSystem.Runtime.InteropServices.DllImport que
indique en cuál librería de enlace dinámico (DLL) se ha implementado. Este atributo
requiere que el método externo que le siga sea estático, y un ejemplo de su uso es:

using System.Runtime.InteropServices; // Aquí está definido DllImport

public class Externo


{
[DllImport("kernel32")]
public static extern void CopyFile(string fuente, string destino);

public static void Main()


{
CopyFile("fuente.dat", "destino.dat");
}
}

El concepto de atributo se explica detalladamente en el Tema 14:Atributos. Por ahora


basta saber que los atributos se usan de forman similar a los métodos sólo que no están
asociados a ningún objeto ni tipo y se indican entre corchetes ([]) antes de declaraciones
de elementos del lenguaje. En el caso concreto de DllImport lo que indica el parámetro

65
Clases en Microsoft Visual C#

que se le pasa es cuál es el fichero (por defecto se considera que su extensión es .dll)
donde se encuentra la implementación del método externo a continuación definido.

Lo que el código del ejemplo anterior hace es simplemente definir un método de


nombre CopyFile() cuyo código se corresponda con el de la función CopyFile() del fichero
kernel32.dll del API Win32. Este método es llamado en Main() para copiar el fichero de
nombre fuente.dat en otro de nombre destino.dat. Nótese que dado que CopyFile() se ha
declarado como static y se le llama desde la misma clase donde se ha declarado, no es
necesario precederlo de la notación <nombreClase>. para llamarlo.

Como se ve, la utilidad principal de los métodos externos es permitir hacer llamadas a
código nativo desde código gestionado, lo que puede ser útil por razones de eficiencia o
para reutilizar código antiguamente escrito pero reduce la portabilidad de la aplicación.

Constructores
Concepto de constructores

Los constructores de un tipo de datos son métodos especiales que se definen como
miembros de éste y que contienen código a ejecutar cada vez que se cree un objeto de
ese tipo. Éste código suele usarse para labores de inicialización de los campos del objeto a
crear, sobre todo cuando el valor de éstos no es constante o incluye acciones más allá de
una asignación de valor (aperturas de ficheros, accesos a redes, etc.)

Hay que tener en cuenta que la ejecución del constructor siempre se realiza después de
haberse inicializado todos los campos del objeto, ya sea con los valores iniciales que se
hubiesen especificado en su definición o dejándolos con el valor por defecto de su tipo.

Aparte de su especial sintaxis de definición, los constructores y los métodos normales


tienen una diferencia muy importante: los constructores no se heredan.

Definición de constructores

La sintaxis básica de definición de constructores consiste en definirlos como cualquier


otro método pero dándoles el mismo nombre que el tipo de dato al que pertenecen y no
indicando el tipo de valor de retorno debido a que nunca pueden devolver nada. Es decir,
se usa la sintaxis:

<modificadores> <nombreTipo>(<parámetros>)
{
<código>
}

Un constructor nunca puede devolver ningún tipo de objeto porque, como ya se ha


visto, sólo se usa junto al operador new, que devuelve una referencia al objeto recién
creado. Por ello, es absurdo que devuelva algún valor ya que nunca podría ser capturado

66
Clases en Microsoft Visual C#

en tanto que new nunca lo devolvería. Por esta razón el compilador considera erróneo
indicar algún tipo de retorno en su definición, incluso aunque se indique void.

Llamada al constructor

Al constructor de una clase se le llama en el momento en que se crea algún objeto de


la misma usando el operador new. De hecho, la forma de uso de este operador es:

new <llamadaConstructor>

Por ejemplo, el siguiente programa demuestra cómo al crearse un objeto se ejecutan


las instrucciones de su constructor:

class Prueba
{
Prueba(int x)
{
System.Console.Write("Creado objeto Prueba con x={0}",x);
}
public static void Main()
{
Prueba p = new Prueba(5);
}
}

La salida por pantalla de este programa demuestra que se ha llamado al constructor del
objeto de clase Prueba creado en Main(), pues es:

Creado objeto Prueba con x=5;

Llamadas entre constructores

Al igual que ocurre con cualquier otro método, también es posible sobrecargar los
constructores. Es decir, se pueden definir varios constructores siempre y cuando éstos
tomen diferentes números o tipos de parámetros. Además, desde el código de un
constructor puede llamarse a otros constructores del mismo tipo de dato antes de ejecutar
las instrucciones del cuerpo del primero. Para ello se añade un inicializador this al
constructor, que es estructura que precede a la llave de apertura de su cuerpo tal y como
se muestra en el siguiente ejemplo:

67
Clases en Microsoft Visual C#

class A
{
int total;

A(int valor): this(valor, 2); // (1)


{
}

A(int valor, int peso) // (2)


{
total = valor*peso;
}
}

El this incluido hace que la llamada al constructor (1) de la clase A provoque una
llamada al constructor (2) de esa misma clase en la que se le pase como primer parámetro
el valor originalmente pasado al constructor (1) y como segundo parámetro el valor 2. Es
importante señalar que la llamada al constructor (2) en (1) se hace antes de ejecutar
cualquier instrucción de (1)

Nótese que la sobrecarga de constructores -y de cualquier método en general- es un


buen modo de definir versiones más compactas de métodos de uso frecuente en las que
se tomen valores por defecto para parámetros de otras versiones menos compactas del
mismo método. La implementación de estas versiones compactas consistiría en hacer una
llamada a la versión menos compacta del método en la que se le pasen esos valores por
defecto (a través del this en el caso de los constructores) y si acaso luego (y/o antes, si
no es un constructor) se hagan labores específicas en el cuerpo del método compacto.

Del mismo modo que en la definición de un constructor de un tipo de datos es posible


llamar a otros constructores del mismo tipo de datos, también es posible hacer llamadas a
constructores de su tipo padre sustituyendo en su inicializador la palabra
reservada thispor base. Por ejemplo:

class A
{
int total;

A(int valor, int peso)


{
total = valor*peso;
}
}
class B:A
{
B(int valor):base(valor,2)
{}
}

68
Clases en Microsoft Visual C#

En ambos casos, los valores pasados como parámetros en el inicializador no pueden


contener referencias a campos del objeto que se esté creando, ya que se considera que
un objeto no está creado hasta que no se ejecute su constructor y, por tanto, al llamar al
inicializador aún no está creado. Sin embargo, lo que sí pueden incluir son referencias a
los parámetros con los que se llamó al constructor. Por ejemplo, sería válido hacer:

A(int x, int y): this(x+y)


{}

Constructor por defecto

Todo tipo de datos ha de disponer de al menos un constructor. Cuando se define un


tipo sin especificar ninguno el compilador considera que implícitamente se ha definido
uno sin cuerpo ni parámetros de la siguiente forma:

public <nombreClase>(): base()


{}

En el caso de que el tipo sea una clase abstracta, entonces el constructor por defecto
introducido es el que se muestra a continuación, ya que el anterior no sería válido porque
permitiría crear objetos de la clase a la que pertenece:

protected <nombreClase>(): base()


{}

En el momento en se defina explícitamente algún constructor el compilador dejará de


introducir implícitamente el anterior. Hay que tener especial cuidado con la llamada que
este constructor por defecto realiza en su inicializador, pues pueden producirse errores
como el del siguiente ejemplo:

class A
{
public A(int x)
{}
}
class B:A
{
public static void Main()
{
B b = new B(); // Error: No hay constructor base
}

69
Clases en Microsoft Visual C#

En este caso, la creación del objeto de clase B en Main() no es posible debido a que el
constructor que por defecto el compilador crea para la clase B llama al constructor sin
parámetros de su clase base A, pero A carece de dicho constructor porque no se le ha
definido explícitamente ninguno con esas características pero se le ha definido otro que ha
hecho que el compilador no le defina implícitamente el primero.

Otro error que podría darse consistiría en que aunque el tipo padre tuviese un
constructor sin parámetros, éste fuese privado y por tanto inaccesible para el tipo hijo.

También es importante señalar que aún en el caso de que definamos nuestras propios
constructores, si no especificamos un inicializador el compilador introducirá por nosotros
uno de la forma :base() Por tanto, en estos casos también hay que asegurarse de que el
tipo donde se haya definido el constructor herede de otro que tenga un constructor sin
parámetros no privado.

Llamadas polimórficas en constructores

Es conveniente evitar en la medida de lo posible la realización de llamadas a métodos


virtuales dentro de los constructores, ya que ello puede provocar errores muy difíciles de
detectar debido a que se ejecuten métodos cuando la parte del objeto que manipulan aún
no se ha sido inicializado. Un ejemplo de esto es el siguiente:

using System;
public class Base
{
public Base()
{
Console.WriteLine("Constructor de Base");
this.F();
}

public virtual void F()


{
Console.WriteLine("Base.F");
}
}
public class Derivada:Base
{
Derivada()
{
Console.WriteLine("Constructor de Derivada");
}

public override void F()


{
Console.WriteLine("Derivada.F()");
}

70
Clases en Microsoft Visual C#

public static void Main()


{
Base b = new Derivada();
}
}

La salida por pantalla mostrada por este programa al ejecutarse es la siguiente:

Constructor de Base

Derivada.F()

Constructor de Derivada
Lo que ha ocurrido es lo siguiente: Al crearse el objeto Derivada se ha llamado a su
constructor sin parámetros, que como no tiene inicializador implícitamente llama al
constructor sin parámetros de su clase base. El constructor de Base realiza una llamada al
método virtual F(), y como el verdadero tipo del objeto que se está construyendo es
Derivada, entonces la versión del método virtual ejecutada es la redefinición del mismo
incluida en dicha clase. Por último, se termina llamando al constructor de Derivada y
finaliza la construcción del objeto.

Nótese que se ha ejecutado el método F() de Derivada antes que el código del
constructor de dicha clase, por lo que si ese método manipulase campos definidos en
Derivada que se inicializasen a través de constructor, se habría accedido a ellos antes de
inicializarlos y ello seguramente provocaría errores de causas difíciles de averiguar.

Constructor de tipo

Todo tipo puede tener opcionalmente un constructor de tipo, que es un método


especial que funciona de forma similar a los constructores ordinarios sólo que para lo que
se usa es para inicializar los campos static del tipo donde se ha definido.

Cada tipo de dato sólo puede tener un constructor de tipo. Éste constructor es llamado
automáticamente por el compilador la primera vez que se accede al tipo, ya sea para crear
objetos del mismo o para acceder a sus campos estáticos. Esta llamada se hace justo
después de inicializar los campos estáticos del tipo con los valores iniciales especificados al
definirlos (o, en su ausencia, con los valores por defecto de sus tipos de dato), por lo que
el programador no tiene forma de controlar la forma en que se le llama y, por tanto, no
puede pasarle parámetros que condicionen su ejecución.

Como cada tipo sólo puede tener un constructor de tipo no tiene sentido poder
usar this en su inicializador para llamar a otro. Y además, tampoco tiene sentido
usar base debido a que éste siempre hará referencia al constructor de tipo sin parámetros
de su clase base. O sea, un constructor de tipo no puede tener inicializador.

71
Clases en Microsoft Visual C#

Además, no tiene sentido darle modificadores de acceso ya que el programador nunca


lo podrá llamar sino que sólo será llamado automáticamente y sólo al accederse al tipo por
primera vez. Como es absurdo, el compilador considera un error dárselos.

La forma en que se define el constructor de tipo es similar a la de los constructores


normales, sólo que ahora la definición ha de ir prefijada del modificador static y no puede
contar con parámetros ni inicializador. O sea, se define de la siguiente manera:

static <nombreTipo>()
{
<código>
}

En la especificación de C# no se ha recogido cuál ha de ser el orden exacto de las


llamadas a los constructores de tipos cuando se combinan con herencia, aunque lo que sí
se indica es que se ha de asegurar de que no se accede a un campo estático sin haberse
ejecutado antes su constructor de tipo. Todo esto puede verse más claro con un ejemplo:

using System;
class A
{
public static X;
static A()
{
Console.WriteLine("Constructor de A");
X=1;
}
}
class B:A
{
static B()
{
Console.WriteLine("Constructor de B");
X=2;
}

public static void Main()


{
B b = new B();
Console.WriteLine(B.X);
}
}

La salida que muestra por pantalla la ejecución de este programa es la siguiente:

Inicializada clase B

72
Clases en Microsoft Visual C#

Inicializada clase A

En principio la salida de este programa puede resultar confusa debido a que los
primeros dos mensajes parecen dar la sensación de que la creación del objeto b provocó
que se ejecutase el constructor de la clase hija antes que al de la clase padre, pero el
último mensaje se corresponde con una ejecución en el orden opuesto. Pues bien, lo que
ha ocurrido es lo siguiente: como el orden de llamada a constructores de tipo no está
establecido, el compilador de Microsoft ha llamado antes al de la clase hija y por ello el
primer mensaje mostrado es Inicializada clase B. Sin embargo, cuando en este constructor
se va a acceder al campo X se detecta que la clase donde se definió aún no está
inicializada y entonces se llama a su constructor de tipo, lo que hace que se muestre el
mensaje Incializada clase A. Tras esta llamada se machaca el valor que el constructor de A
dió a X (valor 1) por el valor que el constructor de B le da (valor 2) Finalmente, el último
WriteLine() muestra un 2, que es el último valor escrito en X.

Destructores
Al igual que es posible definir métodos constructores que incluyan código que gestione
la creación de objetos de un tipo de dato, también es posible definir un destructor que
gestione cómo se destruyen los objetos de ese tipo de dato. Este método suele ser útil
para liberar recursos tales como los ficheros o las conexiones de redes abiertas que el
objeto a destruir estuviese acaparando en el momento en que se fuese a destruir.

La destrucción de un objeto es realizada por el recolector de basura cuando realiza una


recolección de basura y detecta que no existen referencias a ese objeto ni en pila, ni en
registros ni desde otros objetos sí referenciados. Las recolecciones se inician
automáticamente cuando el recolector detecta que queda poca memoria libre o que se va
a finalizar la ejecución de la aplicación, aunque también puede forzarse llamando al
método Collect() de la clase System.GC

La sintaxis que se usa para definir un destructor es la siguiente:

~<nombreTipo>()
{
<código>
}

Tras la ejecución del destructor de un objeto de un determinado tipo siempre se llama


al destructor de su tipo padre, formándose así una cadena de llamadas a destructores que
acaba al llegarse al destructor de object. Éste último destructor no contiene código
alguno, y dado que object no tiene padre, tampoco llama a ningún otro destructor.

73
Clases en Microsoft Visual C#

Los destructores no se heredan. Sin embargo, para asegurar que la cadena de llamadas
a destructores funcione correctamente si no incluimos ninguna definición de destructor en
un tipo, el compilador introducirá en esos casos una por nosotros de la siguiente forma:

~<nombreTipo>()
{}

El siguiente ejemplo muestra como se definen destructores y cómo funciona la cadena


de llamada a destructores:

using System;
class A
{
~A()
{
Console.WriteLine("Destruido objeto de clase A");
}
}
class B:A
{
~B()
{
Console.WriteLine("Destruido objeto de clase B");
}
public static void Main()
{
new B();
}
}

El código del método Main() de este programa crea un objeto de clase B pero no
almacena ninguna referencia al mismo. Luego finaliza la ejecución del programa, lo que
provoca la actuación del recolector de basura y la destrucción del objeto creado llamando
antes a su destructor. La salida que ofrece por pantalla el programa demuestra que tras
llamar al destructor de B se llama al de su clase padre, ya que es:

Destruido objeto de clase B

Destruido objeto de clase A

74
Clases en Microsoft Visual C#

Nótese que aunque no se haya guardado ninguna referencia al objeto de tipo B creado
y por tanto sea inaccesible para el programador, al recolector de basura no le pasa lo
mismo y siempre tiene acceso a los objetos, aunque sean inútiles para el programador.

Es importante recalcar que no es válido incluir ningún modificador en la definición de


un destructor, ni siquiera modificadores de acceso, ya que como nunca se le puede llamar
explícitamente no tiene ningún nivel de acceso para el programador. Sin embargo, ello no
implica que cuando se les llame no se tenga en cuenta el verdadero tipo de los objetos a
destruir, como demuestra el siguiente ejemplo:

using System;
public class Base
{
public virtual void F()
{
Console.WriteLine("Base.F");
}

~Base()
{
Console.WriteLine("Destructor de Base");
this.F();
}
}
public class Derivada:Base
{
~Derivada()
{
Console.WriteLine("Destructor de Derivada");
}

public override void F()


{
Console.WriteLine("Derivada.F()");
}

public static void Main()


{
Base b = new Derivada();
}
}

La salida mostrada que muestra por pantalla este programa al ejecutarlo es:

Destructor de Derivada

Destructor de Base

Derivada.F()

75
Clases en Microsoft Visual C#

Como se ve, aunque el objeto creado se almacene en una variable de tipo Base, su
verdadero tipo es Derivada y por ello se llama al destructor de esta clase al destruirlo.
Tras ejecutarse dicho destructor se llama al destructor de su clase padre siguiéndose la
cadena de llamadas a destructores. En este constructor padre hay una llamada al método
virtual F(), que como nuevamente el objeto que se está destruyendo es de tipo Derivada,
la versión de F() a la que se llamará es a la de la dicha clase.

Nótese que una llamada a un método virtual dentro de un destructor como la que se
hace en el ejemplo anterior puede dar lugar a errores difíciles de detectar, pues cuando se
llama al método virtual ya se ha destruido la parte del objeto correspondiente al tipo
donde se definió el método ejecutado. Así, en el ejemplo anterior se ha ejecutado
Derivada.F() tras Derivada.~F(), por lo que si en Derivada.F() se usase algún campo
destruido en Derivada.~F() podrían producirse errores difíciles de detectar.

Concepto de propiedad
Una propiedad es una mezcla entre el concepto de campo y el concepto de método.
Externamente es accedida como si de un campo normal se tratase, pero internamente es
posible asociar código a ejecutar en cada asignación o lectura de su valor. Éste código
puede usarse para comprobar que no se asignen valores inválidos, para calcular su valor
sólo al solicitar su lectura, etc.

Una propiedad no almacena datos, sino sólo se utiliza como si los almacenase. En la
práctica lo que se suele hacer escribir como código a ejecutar cuando se le asigne un
valor, código que controle que ese valor sea correcto y que lo almacene en un campo
privado si lo es; y como código a ejecutar cuando se lea su valor, código que devuelva el
valor almacenado en ese campo público. Así se simula que se tiene un campo público sin
los inconvenientes que estos presentan por no poderse controlar el acceso a ellos.

Definición de propiedades

Para definir una propiedad se usa la siguiente sintaxis:

<tipoPropiedad> <nombrePropiedad>
{
set
{
<códigoEscritura>
}
get
{
<códigoLectura>
}
}

76
Clases en Microsoft Visual C#

Una propiedad así definida sería accedida como si de un campo de tipo


<tipoPropiedad> se tratase, pero en cada lectura de su valor se ejecutaría el
<códigoLectura> y en cada escritura de un valor en ella se ejecutaría <códigoEscritura>

Al escribir los bloques de código get y set hay que tener en cuenta que dentro del
código set se puede hacer referencia al valor que se solicita asignar a través de un
parámetro especial del mismo tipo de dato que la propiedad llamado value (luego
nosotros no podemos definir uno con ese nombre en <códigoEscritura>); y que dentro del
código get se ha de devolver siempre un objeto del tipo de dato de la propiedad.

En realidad el orden en que aparezcan los bloques de código set y get es irrelevante.
Además, es posible definir propiedades que sólo tengan el bloque get (propiedades de
sólo lectura) o que sólo tengan el bloque set (propiedades de sólo escritura) Lo que
no es válido es definir propiedades que no incluyan ninguno de los dos bloques.

Las propiedades participan del mecanismo de polimorfismo igual que los métodos,
siendo incluso posible definir propiedades cuyos bloques de código get o set sean
abstractos. Esto se haría prefijando el bloque apropiado con un modificador abstract y
sustituyendo la definición de su código por un punto y coma. Por ejemplo:

using System;
abstract class A
{
public abstract int PropiedadEjemplo
{
set;
get;
}
}
class B:A
{
private int valor;

public override int PropiedadEjemplo


{
get
{
Console.WriteLine("Leído {0} de PropiedadEjemplo", valor);
return valor;
}

set
{
valor = value;
Console.WriteLine("Escrito {0} en PropiedadEjemplo", valor);
}
}
}

77
Clases en Microsoft Visual C#

En este ejemplo se ve cómo se definen y redefinen propiedades abstractas. Al igual que


abstract y override, también es posible usar cualquiera de los modificadores relativos a
herencia y polimorfismo ya vistos: virtual, new y sealed.

Nótese que aunque en el ejemplo se ha optado por asociar un campo privado valor a la
propiedad PropiedadEjemplo, en realidad nada obliga a que ello se haga y es posible
definir propiedades que no tengan campos asociados. Es decir, una propiedad no se tiene
porqué corresponder con un almacén de datos.

Acceso a propiedades

La forma de acceder a una propiedad, ya sea para lectura o escritura, es exactamente


la misma que la que se usaría para acceder a un campo de su mismo tipo. Por ejemplo, se
podría acceder a la propiedad de un objeto de la clase B del ejemplo anterior con:

B obj = new B();


obj.PropiedadEjemplo++;

El resultado que por pantalla se mostraría al hacer una asignación como la anterior
sería:

Leído 0 de PropiedadEjemplo;

Escrito 1 en PropiedadEjemplo;

Nótese que en el primer mensaje se muestra que el valor leído es 0 porque lo que
devuelve el bloque get de la propiedad es el valor por defecto del campo privado valor,
que como es de tipo int tiene como valor por defecto 0.

Implementación interna de propiedades

En realidad la definición de una propiedad con la sintaxis antes vista es convertida por
el compilador en la definición de un par de métodos de la siguiente forma:

<tipoPropiedad> get_<nombrePropiedad>()
{// Método en que se convierte en bloque get
<códigoLectura>
}
void set_<nombrePropiedad> (<tipoPropiedad> value)
{// Método en que se convierte en bloque set
<códigoEscritura>
}

78
Clases en Microsoft Visual C#

Esto se hace para que desde lenguajes que no soporten las propiedades se pueda
acceder también a ellas. Si una propiedad es de sólo lectura sólo se generará el
método get_X(), y si es de sólo escritura sólo se generará el set_X() Ahora bien, en
cualquier caso hay que tener cuidado con no definir en un mismo tipo de dato métodos
con signaturas como estas si se van a generar internamente debido a la definición de una
propiedad, ya que ello provocaría un error de definición múltiple de método.

Teniendo en cuenta la implementación interna de las propiedades, es fácil ver que el


último ejemplo de acceso a propiedad es equivalente a:

B b = new B();
obj.set_PropiedadEjemplo(obj.get_Propiedad_Ejemplo()++);

Como se ve, gracias a las propiedades se tiene una sintaxis mucho más compacta y
clara para acceder a campos de manera controlada. Se podría pensar que la contrapartida
de esto es que el tiempo de acceso al campo aumenta considerablemente por perderse
tiempo en hacer las llamada a métodos set/get. Pues bien, esto no tiene porqué ser así
ya que el compilador de C# elimina llamadas haciendo inlining (sustitución de la llamada
por su cuerpo) en los accesos a bloques get/set no virtuales y de códigos pequeños, que
son los más habituales.

Nótese que de la forma en que se definen los métodos generados por el compilador se
puede deducir el porqué del hecho de que en el bloque set se pueda acceder a través
de value al valor asignado y de que el objeto devuelto por el código de un
bloque gettenga que ser del mismo tipo de dato que la propiedad a la que pertenece.

Indizadores
Concepto de indizador

Un indizador es una definición de cómo se puede aplicar el operador de acceso a


tablas ([ ]) a los objetos de un tipo de dato. Esto es especialmente útil para hacer más
clara la sintaxis de acceso a elementos de objetos que puedan contener colecciones de
elementos, pues permite tratarlos como si fuesen tablas normales.

Los indizadores permiten definir código a ejecutar cada vez que se acceda a un objeto
del tipo del que son miembros usando la sintaxis propia de las tablas, ya sea para leer o
escribir. A diferencia de las tablas, los índices que se les pase entre corchetes no tiene
porqué ser enteros, pudiéndose definir varios indizadores en un mismo tipo siempre y
cuando cada uno tome un número o tipo de índices diferente.

Definición de indizador

A la hora de definir un indizador se usa una sintaxis parecida a la de las propiedades:

79
Clases en Microsoft Visual C#

<tipoIndizador> this[<índices>]
{
set
{
<códigoEscritura>
}

get
{
<códigoLectura>
}
}

Las únicas diferencias entre esta sintaxis y la de las propiedades son:

 El nombre dado a un indizador siempre ha de ser this, pues carece de sentido poder darle
cualquiera en tanto que a un indizador no se accede por su nombre sino aplicando el
operador [ ] a un objeto. Por ello, lo que diferenciará a unos indizadores de otros será el
número y tipo de sus <índices>.

 En <índices> se indica cuáles son los índices que se pueden usar al acceder al indizador.
Para ello la sintaxis usada es casi la misma que la que se usa para especificar los
parámetros de un método, sólo que no se admite la inclusión de
modificadoresref, out o params y que siempre ha de definirse al menos un parámetro.
Obviamente, el nombre que se dé a cada índice será el nombre con el que luego se podrá
acceder al mismo en los bloques set/get.

 No se pueden definir indizadores estáticos, sino sólo indizadores de objetos.

Por todo lo demás, la sintaxis de definición de los indizadores es la misma que la de las
propiedades: pueden ser de sólo lectura o de sólo escritura, da igual el orden en que se
definan sus bloques set/get, dentro del bloque set se puede acceder al valor a escribir a
través del parámetro especial value del tipo del indizador, el código del bloque get ha de
devolver un objeto de dicho tipo, etc.

A continuación se muestra un ejemplo de definición de una clase que consta de dos


indizadores: ambos permiten almacenar elementos de tipo entero, pero uno toma como
índice un entero y el otro toma dos cadenas:

using System;

public class A
{
public int this[int índice]
{

80
Clases en Microsoft Visual C#

set
{
Console.WriteLine("Escrito {0} en posición {1}", value, índice);
}
get
{
Console.WriteLine("Leído 1 de posición {0}", índice);
return 1;
}
}

public int this[string cad1, string cad2]


{
set
{
Console.WriteLine("Escrito {0} en posición ({1},{2})"
, value, cad1, cad2);
}
get
{
Console.WriteLine("Leído 2 de posición ({0},{1})", cad1, cad2);
return 2;
}
}

Acceso a indizadores

Para acceder a un indizador se utiliza exactamente la misma sintaxis que para acceder
a una tabla, sólo que los índices no tienen porqué ser enteros sino que pueden ser de
cualquier tipo de dato que se haya especificado en su definición. Por ejemplo, accesos
válidos a los indizadores de un objeto de la clase A definida en el epígrafe anterior son:

A obj = new A();


obj[100] = obj["barco", "coche"];

La ejecución de la asignación de este ejemplo producirá esta salida por pantalla:

Leído 2 de posición (barco, coche)

Escrito 2 en posición 100

Implementación interna de indizadores

Al igual que las propiedades, para facilitar la interoperabilidad entre lenguajes los
indizadores son también convertidos por el compilador en llamadas a métodos cuya
definición se deduce de la definición del indizador. Ahora los métodos son de la forma:

81
Clases en Microsoft Visual C#

<tipoIndizador> get_Item(<índices>)
{
<códigoLectura>
}
void set_Item(<índices>, <tipoIndizador> value)
{
<códigoEscritura>
}

Nuevamente, hay que tener cuidado con la signatura de los métodos que se definan en
una clase ya que como la de alguno coincida con la generada automáticamente por el
compilador para los indizadores se producirá un error de ambigüedad.

Redefinición de operadores
Concepto de redefinición de operador

Un operador en C# no es más que 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. En el Tema 4: Aspectos Léxicos ya hemos visto que C# cuenta con un buen
número de operadores que permiten realizar con una sintaxis clara e intuitiva las
operaciones comunes a la mayoría de lenguajes (aritmética, lógica, etc.) así como otras
operaciones más particulares de C# (operador is, operador stackalloc, etc.)

En C# viene predefinido el comportamiento de sus operadores cuando se aplican a


ciertos tipos de datos. Por ejemplo, si se aplica el operador + entre dos
objetos int devuelve su suma, y si se aplica entre dos objetos string devuelve su
concatenación. Sin embargo, también se permite que el programador pueda definir el
significado la mayoría de estos operadores cuando se apliquen a objetos de tipos que él
haya definido, y esto es a lo que se le conoce como redefinición de operador.

Nótese que en realidad la posibilidad de redefinir un operador no aporta ninguna nueva


funcionalidad al lenguaje y sólo se ha incluido en C# para facilitar la legibilidad del código.
Por ejemplo, si tenemos una clase Complejo que representa números complejos
podríamos definir una función Sumar() para sus objetos de modo que a través de ella se
pudiese conseguir la suma de dos objetos de esta clase como muestra este ejemplo:

Complejo c1 = new Complejo(3,2); // c1 = 3 + 2i


Complejo c2 = new Complejo(5,2); // c2 = 5 + 2i
Complejo c3 = c1.Sumar(c2); // c3 = 8 + 4i

Sin embargo, el código sería mucho más legible e intuitivo si en vez de tenerse que
usar el método Sumar() se redefiniese el significado del operador + para que al aplicarlo
entre objetos Complejo devolviese su suma. Con ello, el código anterior quedaría así:

82
Clases en Microsoft Visual C#

Complejo c1 = new Complejo(3,2); // c1 = 3 + 2i


Complejo c2 = new Complejo(5,2); // c2 = 5 + 2i
Complejo c3 = c1 + c2; // c3 = 8 + 4i

Ésta es precisamente la utilidad de la redefinición de operadores: hacer más claro y


legible el código, no hacerlo más corto. Por tanto, cuando se redefina un operador es
importante que se le dé un significado intuitivo ya que si no se iría contra de la filosofía de
la redefinición de operadores. Por ejemplo, aunque sería posible redefinir el
operador * para que cuando se aplicase entre objetos de tipo Complejo devuelva su suma
o imprimiese los valores de sus operandos en la ventana de consola, sería absurdo hacerlo
ya que más que clarificar el código lo que haría sería dificultar su comprensión.

De todas formas, suele ser buena idea que cada vez que se redefina un operador en un
tipo de dato también se dé una definición de un método que funcione de forma
equivalente al operador. Así desde lenguajes que no soporten la redefinición de
operadores también podrá realizarse la operación y el tipo será más reutilizable.

Definición de redefiniciones de operadores


Sintaxis general de redefinición de operador

La forma en que se redefine un operador depende del tipo de operador del que se
trate, ya que no es lo mismo definir un operador unario que uno binario. Sin embargo,
como regla general podemos considerar que se hace definiendo un método público y
estático cuyo nombre sea el símbolo del operador a redefinir y venga precedido de la
palabra reservada operator. Es decir, se sigue una sintaxis de la forma:

public static <tipoDevuelto> operator <símbolo>(<operandos>)


{
<cuerpo>
}

Los modificadores public y static pueden permutarse si se desea, lo que es


importante es que siempre aparezcan en toda redefinición de operador. Se pueden
redefinir tanto operadores unarios como binarios, y en <operandos> se ha de incluir
tantos parámetros como operandos pueda tomar el operador a redefinir, ya que cada uno
representará a uno de sus operandos. Por último, en <cuerpo> se han de escribir las
instrucciones a ejecutar cada vez que se aplique la operación cuyo operador es <símbolo>
a operandos de los tipos indicados en <operandos>.

<tipoDevuelto> no puede ser void, pues por definición toda operación tiene un
resultado, por lo que todo operador ha de devolver algo. Además, permitirlo complicaría

83
Clases en Microsoft Visual C#

innecesariamente el compilador y éste tendría que admitir instrucciones poco intuitivas


(como a+b; si el + estuviese redefinido con valor de retorno void para los tipos de a y b)

Además, los operadores no pueden redefinirse con total libertad ya que ello también
dificultaría sin necesidad la legibilidad del código, por lo que se han introducido las
siguientes restricciones al redefinirlos:

 Al menos uno de los operandos ha de ser del mismo tipo de dato del que sea miembro la
redefinición del operador. Como puede deducirse, ello implica que aunque puedan
sobrecargarse los operadores binarios nunca podrá hacerse lo mismo con los unarios ya
que su único parámetro sólo puede ser de un único tipo (el tipo dentro del que se defina)
Además, ello también provoca que no pueden redefinirse las conversiones ya incluidas en
la BCL porque al menos uno de los operandos siempre habrá de ser de algún nuevo tipo
definido por el usuario.
 No puede alterarse sus reglas de precedencia, asociatividad, ubicación y número de
operandos, pues si ya de por sí es difícil para muchos recordarlas cuando son fijas, mucho
más lo sería si pudiesen modificarse según los tipos de sus operandos.
 No puede definirse nuevos operadores ni combinaciones de los ya existentes con nuevos
significados (por ejemplo ** para representar exponenciación), pues ello complicaría
innecesariamente el compilador, el lenguaje y la legibilidad del código cuando en realidad
es algo que puede simularse definiendo métodos.
 No todos los operadores incluidos en el lenguaje pueden redefinirse, pues muchos de ellos
(como ., new, =, etc.) son básicos para el lenguaje y su redefinición es inviable, poco útil
o dificultaría innecesariamente la legibilidad del código. Además, no todos los redefinibles
se redefinen usando la sintaxis general hasta ahora vista, aunque en su momento se irán
explicando cuáles son los redefinibles y cuáles son las peculiaridades de aquellos que
requieran una redefinición especial.

A continuación se muestra cómo se redefiniría el significado del operador + para los


objetos Complejo del ejemplo anterior:

class Complejo;
{

public float ParteReal;


public float ParteImaginaria;

public Complejo (float parteReal, float parteImaginaria)


{
this.ParteReal = parteReal;
this.ParteImaginaria = parteImaginaria;
}

public static Complejo operator +(Complejo op1, Complejo op2)


{
Complejo resultado = new Complejo();
resultado.ParteReal = op1.ParteReal + op2.ParteReal;
resultado.ParteImaginaria = op1.ParteImaginaria
+ op2.ParteImaginaria;

84
Clases en Microsoft Visual C#

return resultado;
}
}

Es fácil ver que lo que en el ejemplo se ha redefinido es el significado del


operador + para que cuando se aplique entre dos objetos de clase Complejo devuelva un
nuevo objeto Complejo cuyas partes real e imaginaria sea la suma de las de sus
operandos.

Se considera erróneo incluir la palabra reservada new en la redefinición de un


operador, ya que no pueden ocultarse redefiniciones de operadores en tanto que estos no
se aplican utilizando el nombre del tipo en que estén definidos. Las únicas posibles
coincidencias se darían en situaciones como la del siguiente ejemplo:

using System;
class A
{
public static int operator +(A obj1, B obj2)
{
Console.WriteLine("Aplicado + de A");
return 1;
}
}
class B:A
{
public static int operator +(A obj1, B obj2)
{
Console.WriteLine("Aplicado + de B");
return 1;
}

public static void Main()


{
A o1 = new A();
B o2 = new B();
Console.WriteLine("o1+o2={0}", o1+o2);
}
}

Sin embargo, más que una ocultación de operadores lo que se tiene es un problema de
ambigüedad en la definición del operador +entre objetos de tipos A y B, de la que se
informará al compilar ya que el compilador no sabrá cuál versión del operador debe usar
para traducir o1+o2 a código binario.

Redefinición de operadores unarios

Los únicos operadores unarios redefinibles son: !, +, -, ~, ++, --, true y false, y toda
redefinición de un operador unario ha de tomar un único parámetro que ha de ser del
mismo tipo que el tipo de dato al que pertenezca la redefinición.

85
Clases en Microsoft Visual C#

Los operadores ++ y -- siempre ha de redefinirse de manera que el tipo de dato del


objeto devuelto sea el mismo que el tipo de dato donde se definen. Cuando se usen de
forma prefija se devolverá ese objeto, y cuando se usen de forma postifja el compilador lo
que hará será devolver el objeto original que se les pasó como parámetro en lugar del
indicado en el return. Por ello es importante no modificar dicho parámetro si es de un
tipo referencia y queremos que estos operadores tengan su significado tradicional. Un
ejemplo de cómo hacerlo es la siguiente redefinición de ++ para el tipo Complejo:

public static Complejo operator ++ (Complejo op)


{
Complejo resultado = new Complejo(op.ParteReal + 1, op.ParteImaginaria);
return resultado;
}

Nótese que si hubiésemos redefinido el ++ de esta otra forma:

public static Complejo operator ++ (Complejo op)


{
op.ParteReal++;
return op;
}

Entonces el resultado devuelto al aplicárselo a un objeto siempre sería el mismo tanto


si fue aplicado de forma prefija como si lo fue de forma postifija, ya que en ambos casos
el objeto devuelto sería el mismo. Sin embargo, eso no ocurriría si Complejo fuese una
estructura, ya que entonces op no sería el objeto original sino una copia de éste y los
cambios que se le hiciesen en el cuerpo de la redefinición de ++ no afectarían al objeto
original, que es el que se devuelve cuando se usa ++ de manera postfija.

Respecto a los operadores true y false, estos indican respectivamente, cuando se ha


de considerar que un objeto representa el valor lógico cierto y cuando se ha de considerar
que representa el valor lógico falso, por lo que sus redefiniciones siempre han de devolver
un objeto de tipo bool que indique dicha situación. Además, si se redefine uno es
obligatorio redefinir también el otro, pues siempre es posible usar indistintamente uno u
otro para determinar el valor lógico que un objeto de ese tipo represente.

En realidad los operadores true y false no pueden usarse directamente en el código


fuente, sino que redefinirlos para un tipo de dato es útil porque permiten utilizar objetos
de ese tipo en expresiones condicionales tal y como si de un valor lógico se tratase. Por
ejemplo, podemos redefinir estos operadores en el tipo Complejo de modo que consideren
cierto a todo complejo distinto de 0 + 0i y falso a 0 + 0i:

public static bool operator true(Complejo op)

86
Clases en Microsoft Visual C#

{
return (op.ParteReal != 0 || op.ParteImaginaria != 0);
}
public static bool operator false(Complejo op)
{
return (op.ParteReal == 0 && op.ParteImaginaria == 0);
}

Con estas redefiniciones, un código como el que sigue mostraría por pantalla el
mensaje Es cierto:

Complejo c1 = new Complejo(1, 0); // c1 = 1 + 0i


if (c1)
System.Console.WriteLine("Es cierto");

Redefinición de operadores binarios

Los operadores binarios redefinibles son +, -


, *, /, %, &, |, ^, <<, >>, ==, !=, >, <, >= y <= Toda redefinición que se haga de
ellos ha de tomar dos parámetros tales que al menos uno sea del mismo tipo que el tipo
de dato del que es miembro la redefinición.

Hay que tener en cuenta que aquellos de estos operadores que tengan complementario
siempre han de redefinirse junto con éste. Es decir, siempre que se redefina en un tipo el
operador > también ha de redefinirse en él el operador <, siempre que se redefina >=ha
de redefinirse <=, y siempre que se redefina == ha de redefinirse !=.

También hay que señalar que, como puede deducirse de la lista de operadores binarios
redefinibles dada, no es redefinir directamente ni el operador de asignación = ni los
operadores compuestos (+=, -=, etc.) Sin embargo, en el caso de estos últimos dicha
redefinición ocurre de manera automática al redefinir su parte “no =” Es decir, al
redefinir + quedará redefinido consecuentemente +=, al redefinir * lo hará *=, etc.

Por otra parte, también cabe señalar que no es posible redefinir directamente los
operadores && y ||. Esto se debe a que el compilador los trata de una manera especial
que consiste en evaluarlos perezosamente. Sin embargo, es posible simular su redefinición
redefiniendo los operadores unarios true y false, los operadores binarios & y | y
teniendo en cuenta que && y || se evalúan así:

 &&: Si tenemos una expresión de la forma x && y, se aplica primero el operador false a
x. Si devuelve false, entonces x && y devuelve el resultado de evaluar x; y si no,
entonces devuelve el resultado de evaluar x & y

 ||: Si tenemos una expresión de la forma x || y, se aplica primero el operador true a x.


Si devuelve true, se devuelve el resultado de evaluar x; y si no, se devuelve el de evaluar
x | y.

87
Clases en Microsoft Visual C#

Redefiniciones de operadores de conversión

En el Tema 4: Aspectos Léxicos ya vimos que para convertir objetos de un tipo de dato
en otro se puede usar un operador de conversión que tiene la siguiente sintaxis:

(<tipoDestino>) <expresión>

Lo que este operador hace es devolver el objeto resultante de convertir al tipo de dato
de nombre <tipoDestino> el objeto resultante de evaluar <expresión> Para que la
conversión pueda aplicarse es preciso que exista alguna definición de cómo se ha de
convertir a <tipoDestino> los objetos del tipo resultante de evaluar <expresión> Esto
puede indicarse introduciendo como miembro del tipo de esos objetos o del tipo
<tipoDestino> una redefinición del operador de conversión que indique cómo hacer la
conversión del tipo del resultado de evaluar <expresión> a <tipoDestino>

Las redefiniciones de operadores de conversión pueden ser de dos tipos:

 Explícitas: La conversión sólo se realiza cuando se usen explícitamente los operadores de


conversión antes comentado.

 Implícitas: La conversión también se realiza automáticamente cada vez que se asigne un


objeto de ese tipo de dato a un objeto del tipo <tipoDestino>. Estas conversiones son
más cómodas que las explícitas pero también más peligrosas ya que pueden ocurrir sin
que el programador se dé cuenta. Por ello, sólo deberían definirse como implícitas las
conversiones seguras en las que no se puedan producir excepciones ni perderse
información al realizarlas.

En un mismo tipo de dato pueden definirse múltiples conversiones siempre y cuando el


tipo origen de las mismas sea diferente. Por tanto, no es válido definir a la vez en un
mismo tipo una versión implícita de una cierta conversión y otra explícita.

La sintaxis que se usa para hacer redefinir una operador de conversión es parecida a la
usada para cualquier otro operador sólo que no hay que darle nombre, toma un único
parámetro y hay que preceder la palabra reservada operator con las palabras
reservadasexplicit o implicit según se defina la conversión como explícita o implícita. Por
ejemplo, para definir una conversión implícita de Complejo a float podría hacerse:

public static implicit operator float(Complejo op)


{
return op.ParteReal;
}

Nótese que el tipo del parámetro usado al definir la conversión se corresponde con el
tipo de dato del objeto al que se puede aplicar la conversión (tipo origen), mientras que

88
Clases en Microsoft Visual C#

el tipo del valor devuelto será el tipo al que se realice la conversión (tipo destino) Con
esta definición podrían escribirse códigos como el siguiente:

Complejo c1 = new Complejo(5,2); // c1 = 5 + 2i


float f = c1; // f = 5

Nótese que en la conversión de Complejo a float se pierde información (la parte


imaginaria), por lo que sería mejor definir la conversión como explícita sustituyendo en su
definición la palabra reservada implicit por explicit. En ese caso, el código anterior
habría de cambiarse por:

Complejo c1 = new Complejo(5,2); // c1 = 5 + 2i


float f = (float) c1; // f = 5

Por otro lado, si lo que hacemos es redefinir la conversión de float a Complejo con:

public static implicit operator Complejo(float op)


{
return (new Complejo(op, 0));
}

Entonces se podría crear objetos Complejo así:

Complejo c2 = 5; // c2 = 5 + 0i

Véase que en este caso nunca se perderá información y la conversión nunca fallará, por
lo que es perfectamente válido definirla como implícita. Además, nótese como
redefiniendo conversiones implícitas puede conseguirse que los tipos definidos por el
usuario puedan inicializarse directamente a partir de valores literales tal y como si fuesen
tipos básicos del lenguaje.

En realidad, cuando se definan conversiones no tiene porqués siempre ocurrir que el


tipo destino indicado sea el tipo del que sea miembro la redefinición, sino que sólo ha de
cumplirse que o el tipo destino o el tipo origen sean de dicho tipo. O sea, dentro de un
tipo de dato sólo pueden definirse conversiones de ese tipo a otro o de otro tipo a ese. Sin
embargo, al permitirse conversiones en ambos sentidos hay que tener cuidado porque ello
puede producir problemas si se solicitan conversiones para las que exista una definición de
cómo realizarlas en el tipo fuente y otra en el tipo destino. Por ejemplo, el siguiente
código provoca un error al compilar debido a ello:
89
Clases en Microsoft Visual C#

class A
{
static void Main(string[] args)
{
A obj = new B(); // Error: Conversión de B en A ambigua
}

public static implicit operator A(B obj)


{
return new A();
}
}
class B
{
public static implicit operator A(B obj)
{
return new A();
}
}

El problema de este tipo de errores es que puede resulta difícil descubrir sus causas en
tanto que el mensaje que el compilador emite indica que no se pueden convertir los
objetos A en objetos B pero no aclara que ello se deba a una ambigüedad.

Otro error con el que hay que tener cuidado es con el hecho de que puede ocurrir que
al mezclar redefiniciones implícitas con métodos sobrecargados puedan haber
ambigüedades al determinar a qué versión del método se ha de llamar. Por ejemplo, dado
el código:

using System;
class A
{
public static implicit operator A(B obj)
{
return new A();
}

public static void MétodoSobrecargado(A o)


{
Console.WriteLine("Versión que toma A");
}

public static void MétodoSobrecargado(C o)


{
Console.WriteLine("Versión que toma C");
}

static void Main(string[] args)


{
MétodoSobrecargado(new B());

90
Clases en Microsoft Visual C#

}
}
class B
{
public static implicit operator C(B obj)
{
return new C();
}
}
class C
{}

Al compilarlo se producirá un error debido a que en la llamada a MétodoSobrecargado()


el compilador no puede deducir a qué versión del método se desea llamar ya que existen
conversiones implícitas de objetos de tipo B en cualquiera de los tipos admitidos por sus
distintas versiones. Para resolverlo lo mejor especificar explícitamente en la llamada la
conversión a aplicar usando el operador ()Por ejemplo, para usar usar la versión del
método que toma como parámetro un objeto de tipo A se podría hacer:

MétodoSobrecargado ( (A) new B());

Sin embargo, hay que tener cuidado ya que si en vez del código anterior se tuviese:

class A
{
public static implicit operator A(B obj)
{
return new A();
}
public static void MétodoSobrecargado(A o)
{
Console.WriteLine("Versión que toma A");
}

public static void MétodoSobrecargado(C o)


{
Console.WriteLine("Versión que toma C");
}

static void Main(string[] args)


{
MétodoSobrecargado(new B());
}
}
class B
{
public static implicit operator A(B obj)
{

91
Clases en Microsoft Visual C#

return new A();


}

public static implicit operator C(B obj)


{
return new C();
}
}
class C
{}

Entonces el fuente compilaría con normalidad y al ejecutarlo se mostraría el siguiente


mensaje que demuestra que se ha usado la versión del método que toma un objeto C.

Finalmente, hay que señalar que no es posible definir cualquier tipo de conversión, sino
que aquellas para las que ya exista un mecanismo predefinido en el lenguaje no son
válidas. Es decir, no pueden definirse conversiones entre un tipo y sus antecesores (por el
polimorfismo ya existen), ni entre un tipo y él mismo, ni entre tipos e interfaces por ellos
implementadas (las interfaces se explicarán en el Tema 15: Interfaces)

Delegados y eventos
Concepto de delegado

Un delegado es un tipo especial de clase cuyos objetos pueden almacenar referencias


a uno o más métodos de tal manera que a través del objeto sea posible solicitar la
ejecución en cadena de todos ellos.

Los delegados son muy útiles ya que permiten disponer de objetos cuyos métodos
puedan ser modificados dinámicamente durante la ejecución de un programa. De hecho,
son el mecanismo básico en el que se basa la escritura de aplicaciones de ventanas en la
plataforma .NET. Por ejemplo, si en los objetos de una clase Button que represente a los
botones estándar de Windows definimos un campo de tipo delegado, podemos conseguir
que cada botón que se cree ejecute un código diferente al ser pulsado sin más que
almacenar el código a ejecutar por cada botón en su campo de tipo delegado y luego
solicitar la ejecución todo este código almacenado cada vez que se pulse el botón.

Sin embargo, también son útiles para muchísimas otras cosas tales como asociación de
código a la carga y descarga de ensamblados, a cambios en bases de datos, a cambios en
el sistema de archivos, a la finalización de operaciones asíncronas, la ordenación de
conjuntos de elementos, etc. En general, son útiles en todos aquellos casos en que
interese pasar métodos como parámetros de otros métodos.

Además, los delegados proporcionan un mecanismo mediante el cual unos objetos


pueden solicitar a otros que se les notifique cuando ocurran ciertos sucesos. Para ello,
bastaría seguir el patrón consistente en hacer que los objetos notificadores dispongan de
algún campo de tipo delegado y hacer que los objetos interesados almacenen métodos

92
Clases en Microsoft Visual C#

suyos en dichos campos de modo que cuando ocurra el suceso apropiado el objeto
notificador simule la notificación ejecutando todos los métodos así asociados a él.

Definición de delegados

Un delegado no es más que un tipo especial de subclase System.MulticastDelegate.


Sin embargo, para definir estas clases no se puede utilizar el mecanismo de herencia
normal sino que ha de seguirse la siguiente sintaxis especial:

<modificadores> delegate <tipoRetorno> <nombreDelegado> (<parámetros>);

<nombreDelegado> será el nombre de la clase delegado que se define, mientras que


<tipoRetorno> y <parámetros> se corresponderán, respectivamente, con el tipo del valor
de retorno y la lista de parámetros de los métodos cuyos códigos puede almacenar en su
interior los objetos de ese tipo delegado (objetos delegados)

Un ejemplo de cómo definir un delegado de nombre Deleg cuyos objetos puedan


almacenar métodos que devuelvan un string y tomen como parámetro un int es:

delegate void Deleg(int valor);

Cualquier intento de almacenar en este delegado métodos que no tomen sólo


un int como parámetro o no devuelvan un stringproducirá un error de compilación o, si
no pudiese detectarse al compilar, una excepción de
tipo System.ArgumentNullException en tiempo de ejecución. Esto puede verse con el
siguiente programa de ejemplo:

using System;
using System.Reflection;
public delegate void D();
public class ComprobaciónDelegados
{
public static void Main()
{
Type t = typeof(ComprobaciónDelegados);
MethodInfo m = t.GetMethod("Método1");
D obj = (D) Delegate.CreateDelegate(typeof(D), m);
obj();
}

public static void Método1()


{
Console.WriteLine("Ejecutado Método1");
}

93
Clases en Microsoft Visual C#

public static void Método2(string s)


{
Console.WriteLine("Ejecutado Método2");
}
}

Lo que se hace en el método Main() de este programa es crear a partir del


objeto Type que representa al tipo ComprobaciónDelegados un
objeto System.Reflection.MethodInfo que representa a su método Método1. Como se
ve, para crear el objeto Type se utiliza el operador typeof ya estudiado, y para obtener el
objeto MethodInfo se usa su método GetMethod() que toma como parámetro una
cadena con el nombre del método cuyo MethodInfo desee obtenerse. Una vez
conseguido, se crea un objeto delegado de tipo D que almacene una referencia al método
por él representado a través del método CreateDelegate() de la clase Delegate y se
llama dicho objeto, lo que muestra el mensaje:

Ejecutando Método1

Aunque en vez de obtener el MethodInfo que representa al Método1 se hubiese


obtenido el que representa al Método2 el compilador no detectaría nada raro al compilar
ya que no es lo bastante inteligente como para saber que dicho objeto no representa a un
método almacenable en objetos delegados de tipo D. Sin embargo, al ejecutarse la
aplicación el CLR sí que lo detectaría y ello provocaría una ArgumentNullException

Esto es un diferencia importante de los delegados respecto a los punteros a función de


C/C++ (que también pueden almacenar referencias a métodos), ya que con estos últimos
no se realizan dichas comprobaciones en tiempo de ejecución y puede terminar ocurriendo
que un puntero a función apunte a un método cuya signatura o valor de retorno no se
correspondan con los indicados en su definición, lo que puede ocasionar que el programa
falle por causas difíciles de detectar.

Las definiciones de delegados también pueden incluir cualquiera de los modificadores


de accesibilidad válidos para una clase, ya que al fin y al cabo los delegados son clases. Es
decir, todos pueden incluir los modificadores public e internal, y los se definan dentro de
otro tipo también pueden incluir protected, private y protected internal.

Manipulación de objetos delegados

Un objeto de un tipo delegado se crea exactamente igual que un objeto de cualquier


clase sólo que en su constructor ha de pasársele el nombre del método cuyo código
almacenará. Este método puede tanto ser un método estático como uno no estático. En el
primer caso se indicaría su nombre con la sintaxis <nombreTipo>.<nombreMétodo>, y en
el segundo se indicaría con <objeto>.<nombreMétodo>

94
Clases en Microsoft Visual C#

Para llamar al código almacenado en el delegado se usa una sintaxis similar a la de las
llamadas a métodos, sólo que no hay que prefijar el objeto delegado de ningún nombre de
tipo o de objeto y se usa simplemente <objetoDelegado>(<valoresParámetros>)

El siguiente ejemplo muestra cómo crear un objeto delegado de tipo D, asociarle el


código de un método llamado F y ejecutar dicho código a través del objeto delegado:

using System;
delegate void D(int valor);
class EjemploDelegado
{
public static void Main()
{
D objDelegado = new D(F);
objDelegado(3);
}

public static void F(int x)


{
Console.WriteLine( "Pasado valor {0} a F()", x);
}
}

La ejecución de este programa producirá la siguiente salida por pantalla:

Pasado valor 3 a F()

Nótese que para asociar el código de F() al delegado no se ha indicado el nombre de


este método estático con la sintaxis <nombreTipo>.<nombreMétodo> antes comentada.
Esto se debe a que no es necesario incluir el <nombreTipo>. cuando el método a asociar
a un delegado es estático y está definido en el mismo tipo que el código donde es
asociado

En realidad un objeto delegado puede almacenar códigos de múltiples métodos tanto


estáticos como no estáticos de manera que una llamada a través suya produzca la
ejecución en cadena de todos ellos en el mismo orden en que se almacenaron en él.
Nótese que si los métodos devuelven algún valor, tras la ejecución de la cadena de
llamadas sólo se devolverá el valor de retorno de la última llamada.

Además, cuando se realiza una llamada a través de un objeto delegado no se tienen en


cuenta los modificadores de visibilidad de los métodos que se ejecutarán, lo que permite
llamar desde un tipo a métodos privados de otros tipos que estén almacenados en un
delegado por accesible desde el primero tal y como muestra el siguiente ejemplo:

using System;

95
Clases en Microsoft Visual C#

public delegate void D();


class A
{
public static D obj;
public static void Main()
{
B.AlmacenaPrivado();
obj();
}
}
class B
{
private static void Privado()
{
Console.WriteLine("Llamado a método privado");
}

public static void AlmacenaPrivado()


{
A.obj += new D(Privado);
}
}

La llamada a AlmacenaPrivado en el método Main() de la clase A provoca que en el


campo delegado obj de dicha clase se almacene una referencia al método privado
Privado() de la clase B, y la instrucción siguiente provoca la llamada a dicho método
privado desde una clase externa a la de su definición como demuestra la salida del
programa:

Llamado a método privado

Para añadir nuevos métodos a un objeto delegado se le aplica el


operador += pasándole como operando derecho un objeto delegado de su mismo tipo
(no vale de otro aunque admita los mismos tipos de parámetros y valor de retorno) que
contenga los métodos a añadirle, y para quitárselos se hace lo mismo pero con el
operador -=. Por ejemplo, el siguiente código muestra los efectos de ambos operadores:

using System;

delegate void D(int valor);

class EjemploDelegado
{
public string Nombre;
EjemploDelegado(string nombre)
{
Nombre = nombre;
}

96
Clases en Microsoft Visual C#

public static void Main()


{
EjemploDelegado obj1 += new EjemploDelegado("obj1");
D objDelegado = new D(f);
objDelegado += new D(obj1.g);
objDelegado(3);
objDelegado -= new D(obj1.g);
objDelegado(5);
}

public void g(int x)


{
Console.WriteLine("Pasado valor {0} a g() en objeto {1}", x, Nombre);
}

public static void f(int x)


{
Console.WriteLine( "Pasado valor {0} a f()", x);
}

La salida producida por pantalla por este programa será:

Pasado valor 3 a f()

Pasado valor 3 a g() en objeto obj1

Pasado valor 5 a f()

Como se ve, cuando ahora se hace la llamada objDelegado(3) se ejecutan los códigos
de los dos métodos almacenados en objDelegado, y al quitársele luego uno de estos
códigos la siguiente llamada sólo ejecuta el código del que queda. Nótese además en el
ejemplo como la redefinición de + realizada para los delegados permite que se pueda
inicializar objDelegado usando += en vez de =. Es decir, si uno de los operandos
de + vale null no se produce ninguna excepción, sino que tan sólo no se añade ningún
método al otro.

Hay que señalar que un objeto delegado vale null si no tiene ningún método asociado,
ya sea porque no se ha llamado aún a su constructor o porque los que tuviese asociado se
le hayan quitado con -=. Así, si al Main() del ejemplo anterior le añadimos al final:

objDelegado -= new D(f);


objDelegado(6);

97
Clases en Microsoft Visual C#

Se producirá al ejecutarlo una excepción de


tipo System.NullReferenceException indicando que se ha intentado acceder a una
referencia nula.

También hay que señalar que para que el operador -= funcione se le ha de pasar como
operador derecho un objeto delegado que almacene algún método exactamente igual al
método que se le quiera quitar al objeto delegado de su lado izquierdo. Por ejemplo, si se
le quiere quitar un método de un cierto objeto, se le ha de pasar un objeto delegado que
almacene ese método de ese mismo objeto, y no vale que almacene ese método pero de
otro objeto de su mismo tipo. Por ejemplo, si al Main() anterior le añadimos al final:

objDelegado -= new g(obj1.g);


objDelegado(6);

Entonces no se producirá ninguna excepción ya que el -= no eliminará ningún método


de objDelegado debido a que ese objeto delegado no contiene ningún método g()
procedente del objeto obj1. Es más, la salida que se producirá por pantalla será:

Pasado valor 3 a f()

Pasado valor 3 a g() en objeto obj1

Pasado valor 5 a f()

Pasado valor 6 a f()

La clase System.MulticastDelegate
Ya se ha dicho que la sintaxis especial de definición de delegados no es más que una
forma especial definir subclases deSystem.MulticastDelegate. Esta clase a su vez
deriva de System.Delegate, que representa a objetos delegados que sólo puede
almacenar un único método. Por tanto, todos los objetos delegado que se definan
contarán con los siguientes miembros comunes heredados de estas clases:

 object Target: Propiedad de sólo lectura que almacena el objeto al que pertenece el
último método añadido al objeto delegado. Si es un método de clase vale null.
 MethodInfo Method: Propiedad de sólo lectura que almacena un
objeto System.Reflection.MethodInfo con información sobre el último método añadido
al objeto (nombre, modificadores, etc.) Para saber cómo acceder a estos datos puede
consultar la documentación incluida en el SDK sobre la clase MethodInfo
 Delegate[] getInvocationList(): Permite acceder a todos los métodos almacenados en
un delegado, ya que devuelve una tabla cuyos elementos son delegados cada uno de los
cuales almacenan uno, y sólo uno, de los métodos del original. Estos delegados se

98
Clases en Microsoft Visual C#

encuentran ordenados en la tabla en el mismo orden en que sus métodos fueron fue
almacenados en el objeto delegado original.

Este método es especialmente útil porque a través de la tabla que retorna se pueden
hacer cosas tales como ejecutar los métodos del delegado en un orden diferente al de su
almacenamiento, procesar los valores de retorno de todas las llamadas a los métodos del
delegado original, evitar que una excepción en la ejecución de uno de los métodos impida
la ejecución de los demás, etc.

Aparte de estos métodos de objeto, la clase System.MulticastDelegate también


cuenta con los siguientes métodos de tipo de uso frecuente:

 static Delegate Combine(Delegate fuente, Delegate destino): Devuelve un nuevo


objeto delegado que almacena la concatenación de los métodos de fuente con los de
destino. Por tanto, nótese que estas tres instrucciones son equivalentes:

objDelegado += new D(obj1.g);


objDelegado = objDelegado + new D(obj1.g);
objDelegado = (D) MulticastDelegate.Combine(objDelegado, new D(obj1.g);

Es más, en realidad el compilador de C# lo que hace es convertir toda aplicación


del operador + entre delegados en una llamada a Combine() como la mostrada.

Hay que tener cuidado con los tipos de los delegados a combinar ya que han de ser
exactamente los mismos o si no se lanza una System.ArgumentException, y ello
ocurre aún en el caso de que dichos sólo se diferencien en su nombre y no en sus tipos de
parámetros y valor de retorno.

 static Delegate Combine(Delegate[] tabla): Devuelve un nuevo delegado cuyos


métodos almacenados son la concatenación de todos los de la lista que se le pasa como
parámetro y en el orden en que apareciesen en ella. Es una buena forma de crear
delegados con muchos métodos sin tener que aplicar += varias veces. Todos los objetos
delegados de la tablahan de ser del mismo tipo, pues si no se produciría
una System.ArgumentException.

 static Delegate Remove(Delegate original, Delegate aBorrar): Devuelve un nuevo


delegado cuyos métodos almacenados son el resultado de eliminar de original los que
tuviese aBorrar. Por tanto, estas instrucciones son equivalentes:

objDelegado -= new D(obj1.g);


objDelegado - objDelegado - new D(obj1.g);
objDelegado = (D) MulticastDelegate.Remove(objDelegado, new D(obj1.g);

99
Clases en Microsoft Visual C#

Nuevamente, lo que hace el compilador de C# es convertir toda aplicación del


operador - entre delegados en una llamada aRemove() como la mostrada. Por tanto, al
igual que con -=, para borrar métodos de objeto se ha de especificar en aBorrar un objeto
delegado que contenga referencias a métodos asociados a exactamente los mismos
objetos que los almacenados en original.

 static Delegate CreateDelegate (Type tipo, MehodInfo método): Ya se usó este


método en el ejemplo de comprobación de tipos del epígrafe “Definición de delegados” de
este mismo tema. Como recordará permite crear dinámicamente objetos delegados, ya
que devuelve un objeto delegado del tipo indicado que almacena una referencia al método
representado por su segundo parámetro.

Llamadas asíncronas
La forma de llamar a métodos que hasta ahora se ha explicado realiza la llamada de
manera síncrona, lo que significa que la instrucción siguiente a la llamada no se ejecuta
hasta que no finalice el método llamado. Sin embargo, a todo método almacenado en un
objeto delegado también es posible llamar de manera asíncrona a través de los métodos
del mismo, lo que consiste en que no se espera a que acabe de ejecutarse para pasar a la
instrucción siguiente a su llamada sino que su ejecución se deja en manos de un hilo
aparte que se irá ejecutándolo en paralelo con el hilo llamante.

Por tanto los delegados proporcionan un cómodo mecanismo para ejecutar cualquier
método asíncronamente, pues para ello basta introducirlo en un objeto delegado del tipo
apropiado. Sin embargo, este mecanismo de llamada asíncrona tiene una limitación, y es
que sólo es válido para objetos delegados que almacenen un único método.

Para hacer posible la realización de llamadas asíncronas, aparte de los métodos


heredados de System.MulticastDelegate todo delegado cuenta con estos otros dos que
el compilador define a su medida en la clase en que traduce la definición de su tipo:

IAsyncResult BeginInvoke(<parámetros>, AsyncCallback cb, Object o)

<tipoRetorno> EndInvoke(<parámetrosRefOut>, IASyncResult ar)

BeginInvoke() crea un hilo que ejecutará los métodos almacenados en el objeto


delegado sobre el que se aplica con los parámetros indicados en <parámetros> y
devuelve un objeto IAsyncResult que almacenará información relativa a ese hilo (por
ejemplo, a través de su propiedad de sólo lectura bool IsComplete puede consultarse si
ha terminado su labor) Sólo tiene sentido llamarlo si el objeto delegado sobre el que se
aplica almacena un único método, pues si no se lanza unaSystem.ArgumentException.

El parámetro cb de BeginInvoke() es un objeto de tipo delegado que puede


almacenar métodos a ejecutar cuando el hilo antes comentado finalice su trabajo. A estos

100
Clases en Microsoft Visual C#

métodos el CLR les pasará automáticamente como parámetro el IAsyncResult devuelto


porBeginInvoke(), estando así definido el delegado destinado a almacenarlos:

public delegate void ASyncCallback(IASyncResult obj);

Por su parte, el parámetro o de BeginInvoke puede usarse para almacenar cualquier


información adicional que se considere oportuna. Es posible acceder a él a través de la
propiedad object AsyncState del objeto IAsyncResult devuelto por BeginInvoke()

En caso de que no se desee ejecutar ningún código especial al finalizar el hilo de


ejecución asíncrona o no desee usar información adicional, puede darse sin ningún tipo de
problema el valor null a los últimos parámetros de BeginInvoke() según corresponda.

Finalmente, EndInvoke() se usa para recoger los resultados de la ejecución asíncrona


de los métodos iniciada a travésBeginInvoke() Por ello, su valor de retorno es del
mismo tipo que los métodos almacenables en el objeto delegado al que pertenece y en
<parámetrosRefOut> se indican los parámetros de salida y por referencia de dichos
métodos. Su tercer parámetro es el IAsyncResult devuelto por el BeginInvoke() que
creó el hilo cuyos resultados se solicita recoger y se usa precisamente para identificarlo. Si
ese hilo no hubiese terminado aún de realizar las llamadas, se esperará a que lo haga.

Para ilustrar mejor el concepto de llamadas asíncronas, el siguiente ejemplo muestra


cómo encapsular en un objeto delegado un método F() para ejecutarlo asíncronamente:

D objDelegado = new D (F);


IAsyncResult hilo = objDelegado.BeginInvoke(3,
new AsyncCallback(M),
"prueba");
// ... Hacer cosas
objDelegado.EndInvoke(hilo);

Donde el método M ha sido definido en la misma clase que este código así:

public static void M(IAsyncResult obj)


{
Console.WriteLine("Llamado a M() con {0}", obj.AsyncState);
}

Si entre el BeginInvoke() y el EndInvoke() no hubiese habido ninguna escritura en


pantalla, la salida del fragmento de código anterior sería:

101
Clases en Microsoft Visual C#

Pasado valor 3 a F()

Llamado a M() con prueba

La llamada a BeginInvoke() lanzará un hilo que ejecutará el método F() almacenado


en objDelegado, pero mientras tanto también seguirá ejecutándose el código del hilo
desde donde se llamó a BeginInvoke() Sólo tras llamar a EndInvoke() se puede
asegurar que se habrá ejecutado el código de F(), pues mientras tanto la evolución de
ambos hilos es prácticamente indeterminable ya que depende del cómo actúe el
planificador de hilos.

Aún si el hilo llamador modifica el valor de alguno de los parámetros de salida o por
referencia de tipos valor, el valor actualizado de éstos no será visible para el hilo llamante
hasta no llamar a EndInvoke() Sin embargo, el valor de los parámetros de tipos
referencia sí que podría serlo. Por ejemplo, dado un código como:

int x=0;
Persona p = new Persona("Josan", "7361928-E", 22);
IAsyncResult res = objetoDelegado.BeginInvoke(ref x, p, null, null);
// Hacer cosas...
objetoDelegado.EndInvoke(ref x, res);

Si en un punto del código comentado con // Hacer cosas..., donde el hilo asíncrono ya
hubiese modificado los contenidos de x y p, se intentase leer los valores de estas
variables, sólo se leería el valor actualizado de p. El de x no se vería hasta después de la
llamada aEndInvoke()

Por otro lado, hay que señalar que si durante la ejecución asíncrona de un método se
produce alguna excepción, ésta no sería notificada pero provocaría que el hilo asíncrono
abortase. Si posteriormente se llamase a EndInvoke() con el IAsyncResult asociado a
dicho hilo, se relanzaría la excepción que produjo el aborto y entonces podría tratarse.

Para optimizar las llamadas asíncronas es recomendable marcar con el


atributo OneWay definido enSystem.Runtime.Remoting.Messaging los métodos
cuyo valor de retorno y valores de parámetros de salida no nos importen, pues ello indica
a la infraestructura encargada de hacer las llamadas asíncronas que no ha de considerar.
Por ejemplo:

[OneWay] public void Método()


{}

102
Clases en Microsoft Visual C#

Ahora bien, hay que tener en cuenta que hacer esto implica perder toda posibilidad de
tratar las excepciones que pudiese producirse al ejecutar asíncronamente el método
atribuido, pues con ello llamar a EndInvoke() dejaría de relanzar la excepción producida.

Por último, a modo de resumen a continuación se indican cuáles son los patrones que
pueden seguirse para recoger los resultados de una llamada asíncrona:

1. Detectar si la llamada asíncrona ha finalizado mirando el valor de la


propiedad IsComplete del objeto IAsyncResult devuelto
por BeginInvoke() Cuando sea así, con EndInvoke() puede recogerse sus
resultados.
2. Pasar un objeto delegado en el penúltimo parámetro de BeginInvoke() con el
método a ejecutar cuando finalice el hilo asíncrono, lo que liberaría al hilo llamante
de la tarea de tener que andar mirando si ha finalizado o no.

Si desde dicho método se necesitase acceder a los resultados del método llamado
podría accederse a ellos a través de la propiedad AsyncDelegate del
objeto IAsyncResult que recibe. Esta propiedad contiene el objeto delegado al
que se llamó, aunque se muestra a continuación antes de acceder a ella hay que
convertir el parámetro IAsyncResult de ese método en unAsyncResult:

public static void M(IAsyncResult iar)


{
D objetoDelegado = (D) ((AsyncResult iar)).AsyncDelegate;
// A partir de aquí podría llamarse a EndInvoke()
// a través de objetoDelegado
}

Implementación interna de los delegados


Cuando hacemos una definición de delegado de la forma:

<modificadores> delegate <tipoRetorno> <nombre>(<parámetros>);

El compilador internamente la transforma en una definición de clase de la forma:

<modificadores> class <nombre>:System.MulticastDelegate


{
private object _target;
private int _methodPtr;
private MulticastDelegate _prev;

103
Clases en Microsoft Visual C#

public <nombre>(object objetivo, int punteroMétodo)


{...}

public virtual <tipoRetorno> Invoke(<parámetros>)


{...}

public virtual IAsyncResult BeginInvoke(<parámetros>,


AsyncCallback cb, Object o)
{...}

public virtual <tipoRetorno> EndInvoke(<parámetrosRefOut>,


IASyncResult ar)
{...}
}

Lo primero que llama la atención al leer la definición de esta clase es que su


constructor no se parece en absoluto al que hemos estado usando hasta ahora para crear
objetos delegado. Esto se debe a que en realidad, a partir de los datos especificados en la
forma de usar el constructor que el programador utiliza, el compilador es capaz de
determinar los valores apropiados para los parámetros del verdadero constructor, que son:

 object objetivo contiene el objeto al cual pertenece el método especificado, y su valor


se guarda en el campo _target. Si es un método estático almacena null.

 int punteroMétodo contiene un entero que permite al compilador determinar cuál es el


método del objeto al que se desea llamar, y su valor se guarda en el campo _methodPtr.
Según donde se haya definido dicho método, el valor de este parámetro procederá de las
tablas MethodDef o MethodRef de los metadatos.

El campo privado _prev de un delegado almacena una referencia al delegado previo al


mismo en la cadena de métodos. En realidad, en un objeto delegado con múltiples
métodos lo que se tiene es una cadena de objetos delegados cada uno de los cuales
contiene uno de los métodos y una referencia (en _prev) a otro objeto delegado que
contendrá otro de los métodos de la cadena.

Cuando se crea un objeto delegado con new se da el valor null a su


campo _prev para así indicar que no pertenece a una cadena sino que sólo contiene un
método. Cuando se combinen dos objetos delegados (con + o Delegate.Combine()) el
campo _prev del nuevo objeto delegado creado enlazará a los dos originales; y cuando se
eliminen métodos de la cadena (con – oDelegate.Remove()) se actualizarán los
campos _prev de la cadena para que salten a los objetos delegados que contenían los
métodos eliminados.

Cuando se solicita la ejecución de los métodos almacenados en un delegado de manera


asíncrona lo que se hace es llamar al método Invoke() del mismo. Por ejemplo, una
llamada como esta:

104
Clases en Microsoft Visual C#

objDelegado(49);

Es convertida por el compilador en:

objDelegado.Invoke(49);

Aunque Invoke() es un método público, C# no permite que el programador lo llame


explícitamente. Sin embargo, otros lenguajes gestionados sí que podrían permitirlo.

El método Invoke() se sirve de la información almacenada


en _target, _methodPtr y _prev, para determinar a cuál método se ha de llamar y en
qué orden se le ha de llamar. Así, la implementación de Invoke() será de la forma:

public virtual <tipoRetorno> Invoke(<parámetros>)


{
if (_prev!=null)
_prev.Invoke(<parámetros>);
return _target._methodPtr(<parámetros>);
}

Obviamente la sintaxis _target._methodPtr no es válida en C#, ya


que _methodPtr no es un método sino un campo. Sin embargo, se ha escrito así para
poner de manifiesto que lo que el compilador hace es generar el código apropiado para
llamar al método perteneciente al objeto indicado en _target e identificado con el valor
de _methodPtr

Nótese que la instrucción if incluida se usa para asegurar que las llamadas a los
métodos de la cadena se hagan en orden: si el objeto delegado no es el último de la
cadena. (_prev!=null) se llamará antes al método Invoke() de su predecesor.

Por último, sólo señalar que, como es lógico, en caso de que los métodos que el objeto
delegado pueda almacenar no tengan valor de retorno (éste sea void), el cuerpo
de Invoke() sólo varía en que la palabra reservada return es eliminada del mismo.

Eventos
Concepto de evento

Un evento es una variante de las propiedades para los campos cuyos tipos sean
delegados. Es decir, permiten controlar la forman en que se accede a los campos

105
Clases en Microsoft Visual C#

delegados y dan la posibilidad de asociar código a ejecutar cada vez que se añada o
elimine un método de un campo delegado.

Sintaxis básica de definición de eventos

La sintaxis básica de definición de un evento consiste en definirlo como cualquier otro


campo con la única peculiaridad de que se le ha de anteponer la palabra
reservada event al nombre de su tipo (que será un delegado) O sea, se sigue la sintaxis:

<modificadores> event <tipoDelegado> <nombreEvento>;

Por ejemplo, para definir un evento de nombre Prueba y tipo delegado D se haría:

public event D Prueba;

También pueden definirse múltiples eventos en una misma línea separando sus
nombres mediante comas. Por ejemplo:

public event D Prueba1, Prueba2;

Desde código ubicado dentro del mismo tipo de dato donde se haya definido el evento
se puede usar el evento tal y como si de un campo delegado normal se tratase. Sin
embargo, desde código ubicado externamente se imponen una serie de restricciones que
permiten controlar la forma en que se accede al mismo:

 No se le puede aplicar los métodos heredados de System.MulticastDelegate.

 Sólo se le puede aplicar dos operaciones: añadido de métodos con += y eliminación de


métodos con -=. De este modo se evita que se use sin querer = en vez de += ó -= y se
sustituyan todos los métodos de la lista de métodos del campo delegado por otro que en
realidad se le quería añadir o quitar (si ese otro valiese null, ello incluso podría provocar
unaSystem.NullReferenceException)

 No es posible llamar a los métodos almacenados en un campo delegado a través del


mismo. Esto permite controlar la forma en que se les llama, ya que obliga a que la
llamada tenga que hacerse a través de algún método público definido en la definición del
tipo de dato donde el evento fue definido.

Sintaxis completa de definición de eventos

106
Clases en Microsoft Visual C#

La verdadera utilidad de un evento es que permite controlar la forma en que se asocian y


quitan métodos de los objetos delegados con+= y -=. Para ello se han de definir con la
siguiente sintaxis avanzada:

<modificadores> event <tipoDelegado> <nombreEvento>


{
add
{
<códigoAdd>
}

remove
{
<códigoRemove>
}
}

Con esta sintaxis no pueden definirse varios eventos en una misma línea como ocurría
con la básica. Su significado es el siguiente: cuando se asocie un método con += al
evento se ejecutará el <códigoAdd>, y cuando se le quite alguno con –= se ejecutará el
<códigoRemove>. Esta sintaxis es similar a la de los bloques set/get de las propiedades
pero con una importante diferencia: aunque pueden permutarse las
secciones add y remove, es obligatorio incluir siempre a ambas.

La sintaxis básica es en realidad una forma abreviada de usar la avanzada. Así, la


definición public event D Prueba(int valor); la interpretaría el compilador como:

private D prueba
public event D Prueba
{
[MethodImpl(MethodImlOptions.Synchronized)]
add
{
prueba = (D) Delegate.Combine(prueba, value);
}
[MethodImpl(MethodImlOptions.Synchronized)]
remove
{
prueba = (D) Delegate.Remove(prueba, value);
}
}

Es decir, el compilador definirá un campo delegado privado y códigos


para add y remove que hagan que el uso de += y -= sobre el evento tenga el efecto
que normalmente tendrían si se aplicasen directamente sobre el campo privado. Como se
ve, dentro de estos métodos se puede usar value para hacer referencia al operando
derecho de los operadores += y -=. El
107
Clases en Microsoft Visual C#

atributoSystem.Runtime.InteropServices.MethodImpl que precede a los


bloques add y remove sólo se incluye para asegurar que un cambio de hilo no pueda
interrumpir la ejecución de sus códigos asociados.

Las restricciones de uso de eventos desde códigos externos al tipo donde se han
definido se deben a que en realidad éstos no son objetos delegados sino que el objeto
delegado es el campo privado que internamente define el compilador. El compilador
traduce toda llamada al evento en una llamada al campo delegado. Como este es privado,
por eso sólo pueda accederse a él desde código de su propio tipo de dato.

En realidad, el compilador internamente traduce las secciones add y remove de la


definición de un evento en métodos de la forma:

void add_<nombreEvento>(<tipoDelegado> value)


void remove_<nombreEvento>(<tipoDelegado> value)

Toda aplicación de += y -= a un evento no es convertida en una llamada al campo


privado sino en una llamada al métodoadd/remove apropiado, como se puede observar
analizando el MSIL de cualquier fuente donde se usen += y -= sobre eventos. Además,
como estos métodos devuelven void ése será el tipo del valor devuelto al aplicar += ó -=
(y no el objeto asignado), lo que evitará que código externo al tipo donde se haya definido
el evento pueda acceder directamente al campo delegado privado.

Si en vez de la sintaxis básica usamos la completa no se definirá automáticamente un


campo delegado por cada evento que se defina, por lo que tampoco será posible hacer
referencia al mismo desde código ubicado en la misma clase donde se ha definido. Sin
embargo ello permite que el programador pueda determinar, a través de
secciones add y remove, cómo se almacenarán los métodos. Por ejemplo, para ahorrar
memoria se puede optar por usar un diccionario donde almacenar los métodos asociados a
varios eventos de un mismo objeto en lugar de usar un objeto delegado por cada uno.

Dado que las secciones add y remove se traducen como métodos, los eventos
también podrán participar en el mecanismo de herencia y redefiniciones típico de los
métodos. Es decir, en <modificadores> aparte de modificadores de acceso y el
modificadorstatic, también se podrán incluir los modificadores relativos a herencia. En
este sentido hay que precisar algo: un evento definido comoabstract ha de definirse
siempre con la sintaxis básica (no incluirá secciones add o remove)

Estructuras
Concepto de estructura

Una estructura es un tipo especial de clase pensada para representar objetos ligeros.
Es decir, que ocupen poca memoria y deban ser manipulados con velocidad, como objetos
que representen puntos, fechas, etc. Ejemplos de estructuras incluidas en la BCL son la

108
Clases en Microsoft Visual C#

mayoría de los tipos básicos (excepto string y object), y de hecho las estructuras junto
con la redefinición de operadores son la forma ideal de definir nuevos tipos básicos a los
que se apliquen las mismas optimizaciones que a los predefinidos.

Diferencias entre clases y estructuras

A diferencia de una clase y fiel a su espíritu de “ligereza”, una estructura no puede


derivar de ningún tipo y ningún tipo puede derivar de ella. Por estas razones sus
miembros no pueden incluir modificadores relativos a herencia, aunque con una
excepción: pueden incluir override para redefinir los miembros de System.Object.

Otra diferencia entre las estructuras y las clases es que sus variables no almacenan
referencias a zonas de memoria dinámica donde se encuentran almacenados objetos sino
directamente referencian a objetos. Por ello se dice que las clases son tipos referencia y
las estructuras son tipos valor, siendo posible tanto encontrar objetos de estructuras en
pila (no son campos de clases) como en memoria dinámica (son campos de clases)

Una primera consecuencia de esto es que los accesos a miembros de objetos de tipos
valor son mucho más rápidos que los accesos a miembros de pilas, ya que es necesario
pasar por una referencia menos a la hora de acceder a ellos. Además, el tiempo de
creación y destrucción de estructuras también es inferior. De hecho, la destrucción de los
objetos almacenados en pila es prácticamente inapreciable ya que se realiza con un simple
decremento del puntero de pila y no interviene en ella el recolector de basura.

Otra consecuencia de lo anterior es que cuando se realicen asignaciones entre variables


de tipos valor, lo que se va a copiar en la variable destino es el objeto almacenado por la
variable fuente y no la dirección de memoria dinámica a la que apuntaba ésta. Por
ejemplo, dado el siguiente tipo (nótese que las estructuras se definen igual que las clases
pero usando la palabra reservada struct en vez de class):

struct Point
{
public int x, y;

public Point(int x, int y)


{
this.x = x;
this.y = y;
}
}

Si usamos este tipo en un código como el siguiente:

Punto p = new Punto(10,10);


Punto p2 = p;

109
Clases en Microsoft Visual C#

p2.x = 100;
Console.WriteLine(p.x);

Lo que se mostrará por pantalla será 10. Esto se debe a que el valor de x modificado es
el de p2, que como es una copia de p los cambios que se le hagan no afectarán a p. Sin
embargo, si Punto hubiese sido definido como una clase entonces sí que se hubiese
mostrado por pantalla 100, ya que en ese caso lo que se habría copiado en p2 habría sido
una referencia a la misma dirección de memoria dinámica referenciada por p, por lo que
cualquier cambio que se haga en esa zona a través de p2 también afectará a p.

De lo anterior se deduce que la asignación entre objetos de tipos estructuras es mucho


más lenta que la asignación entre objetos de clases, ya que se ha de copiar un objeto
completo y no solo una referencia. Para aliviar esto al pasar objetos de tipos estructura
como parámetros, se da la posibilidad de pasarlos como parámetros por referencia
(modificador ref) o parámetros de salida (out) en vez de como parámetros de entrada.

Todas las estructuras derivan implícitamente del tipo System.ValueType, que a su


vez deriva de la clase primigeniaSystem.Object. ValueType tiene los mismos miembros
que su padre, y la única diferencia señalable entre ambos es que enValueType se ha
redefinido Equals() de modo que devuelva true si los objetos comparados tienen el
mismo valor en todos sus campos y false si no. Es decir, la comparación entre estructuras
con Equals() se realiza por valor.

Respecto a la implementación de la igualdad en los tipos definidos como estructuras,


también es importante tener muy en cuenta que el operador == no es en principio
aplicable a las estructuras que defina el programador. Si se desea que lo tenga ha de
dársele explícitamente una redefinición al definir dichas estructuras.

Boxing y unboxing
Dado que toda estructura deriva de System.Object, ha de ser posible a través del
polimorfismo almacenar objetos de estos tipos en objetos object. Sin embargo, esto no
puede hacerse directamente debido a las diferencias semánticas y de almacenamiento que
existen entre clases y estructuras: un object siempre ha de almacenar una referencia a
un objeto en memoria dinámica y una estructura no tiene porqué estarlo. Por ello ha de
realizársele antes al objeto de tipo valor una conversión conocida como boxing.
Recíprocamente, al proceso de conversión de un object que contenga un objeto de un
tipo valor al tipo valor original se le denominaunboxing.

El proceso de boxing es muy sencillo. Consiste en envolver el objeto de tipo valor en un


objeto de un tipo referencia creado específicamente para ello. Por ejemplo, para un objeto
de un tipo valor T, el tipo referencia creado sería de la forma:

class T_Box

110
Clases en Microsoft Visual C#

{
T value;
T_Box(T t)
{
value = t;
}
}

En realidad todo esto ocurre de forma transparente al programador, el cual


simplemente asigna el objeto de tipo valor a un objeto de tipo referencia como si de
cualquier asignación polimórfica se tratase. Por ejemplo:

int p = new Punto(10,10);


object o = p; // boxing. Es equivalente a object o = new Punto_Box(p);

En realidad la clase envoltorio arriba escrita no se crea nunca, pero conceptualmente es


como si se crease. Esto se puede comprobar viendo a través del siguiente código que el
verdadero tipo del objeto o del ejemplo anterior sigue siendo Punto (y no Punto_Box):

Console.WriteLine((p is Punto));

La salida por pantalla de este código es True, lo que confirma que se sigue
considerando que en realidad p almacena un Punto (recuérdese que el operador is sólo
devuelve true si el objeto que se le pasa como operando izquierdo es del tipo que se le
indica como operando derecho)

El proceso de unboxing es también transparente al programador. Por ejemplo, para


recuperar como Punto el valor de tipo Punto almacenado en el objeto o anterior se haría:

p = (Punto) o; // Es equivalente a ((Punto_Box) o).value

Obviamente durante el unboxing se hará una comprobación de tipo para asegurar que
el objeto almacenado en o es realmente de tipo Punto. Esta comprobación es tan estricta
que se ha de cumplir que el tipo especificado sea exactamente el mismo que el tipo
original del objeto, no vale que sea un compatible. Por tanto, este código es inválido:

int i = 123;
object o = i;
long l = (long) o // Error: o contiene un int, no un long

111
Clases en Microsoft Visual C#

Sin embargo, lo que si sería válido es hacer:

long l = (long) (int) o;

Como se puede apreciar en el constructor del tipo envoltorio creado, durante el boxing
el envoltorio que se crea recibe una copia del valor del objeto a convertir, por lo que los
cambios que se le hagan no afectarán al objeto original. Por ello, la salida del siguiente
código será 10:

Punto p = new Punto(10,10);


object o = p; // boxing
p.X = 100;
Console.WriteLine( ((Punto) o).X); // unboxing

Sin embargo, si Punto se hubiese definido como una clase entonces sí que se mostraría
por pantalla un 100 ya que entonces no se haría boxing en la asignación de p a o sino que
se aplicaría el mecanismo de polimorfismo normal, que consiste en tratar p a través de o
como si fuese de tipo object pero sin realizarse ninguna conversión.

El problema del boxing y el unboxing es que son procesos lentos, ya que implican la
creación y destrucción de objetos envoltorio. Por ello puede interesar evitarlos en aquellas
situaciones donde la velocidad de ejecución de la aplicación sea crítica, y para ello se
proponen varias técnicas:

 Si el problema se debe al paso de estructuras como parámetros de métodos genéricos que


tomen parámetros de tipo object, puede convenir definir sobrecargas de esos métodos
que en lugar de tomar objects tomen objetos de los tipos estructura que en concreto la
aplicación utiliza

A partir de la versión 2.0 de C#, se pueden utilizar las


denominadas plantillas o genéricos, que no son más que definiciones de tipos de datos
en las que no se indica cuál es el tipo exacto de ciertas variables sino que se deja en
función de parámetros a los que puede dárseles distintos valores al crear cada objeto de
ese tipo. Así, en vez de crearse objetos con métodos que tomen parámetrosobject, se
podrían ir creando diferentes versiones del tipo según la estructura con la se vaya a
trabajar. El Tema 21: Novedades de C# 2.0 explica esto más detalladamente.

 Muchas veces conviene hacer unboxing para poder acceder a miembros específicos de
ciertas estructuras almacenadas enobjects, aunque a continuación vuelva a necesitarse
realmacenar la estructura en un object. Para evitar esto una posibilidad sería almacenar
en el objecto no directamente la estructura sino un objeto de una clase envolvente creada
a medida por el programador y que incluya los miembros necesarios para hacer las

112
Clases en Microsoft Visual C#

operaciones anteriores. Así se evitaría tener que hacer unboxing, pues se convertiría
de object a esa clase, que no es un tipo valor y por tanto no implica unboxing.

 Con la misma idea, otra posibilidad sería que el tipo estructura implementase ciertas
interfaces mediante las que se pudiese hacer las operaciones antes comentadas. Aunque
las interfaces no se tratarán hasta el Tema 15: Interfaces, por ahora basta saber que las
interfaces son también tipos referencia y por tanto convertir de object a un tipo interfaz
no implica unboxing.

Constructores de estructuras
Los constructores de las estructuras se comportan de una forma distinta a los de las
clases. Por un lado, no pueden incluir ningún inicializador base debido a que como no
puede haber herencia el compilador siempre sabe que ha de llamar al constructor sin
parámetros de System.ValueType. Por otro, dentro de su cuerpo no se puede acceder a
sus miembros hasta inicializarlos, pues para ahorrar tiempo no se les da ningún valor
inicial antes de llamar al constructor.

Sin embargo, la diferencia más importante entre los constructores de ambos tipos se
encuentra en la implementación del constructor sin parámetros: como los objetos
estructura no pueden almacenar el valor por defecto null cuando se declaran sin usar
constructor ya que ese valor indica referencia a posición de memoria dinámica
indeterminada y los objetos estructura no almacenan referencias, toda estructura siempre
tiene definido un constructor sin parámetros que lo que hace es darle en esos casos un
valor por defecto a los objetos declarados. Ese valor consiste en poner a cero toda la
memoria ocupada por el objeto, lo que tiene el efecto de dar como valor a cada campo el
cero de su tipo[12]. Por ejemplo, el siguiente código imprime un 0 en pantalla:

Punto p = new Punto();


Console.WriteLine(p.X);

Y el siguiente también:

using System;
struct Punto
{
public int X,Y;
}
class EjemploConstructorDefecto
{
Punto p;
public static void Main()
{
Console.WriteLine(p.X);
}

113
Clases en Microsoft Visual C#

Sin embargo, el hecho de que este constructor por defecto se aplique no implica que se
pueda acceder a las variables locales sin antes inicializarlas con otro valor. Por ejemplo, el
siguiente fragmento de código de un método sería incorrecto:

Punto p;
Console.WriteLine(p.X); // X no inicializada

Sin embrago, como a las estructuras declaradas sin constructor no se les da el valor por
defecto null, sí que sería válido:

Punto p;
p.X = 2;
Console.WriteLine(p.X);

Para asegurar un valor por defecto común a todos los objetos estructura, se prohibe a
los programadores darles su propia definición del constructor sin parámetros. Mientras que
en las clases es opcional implementarlo y si no se hace el compilador introduce uno por
defecto, en las estructuras no es válido hacerlo. Además, aún en el caso de que se definan
otros constructores, el constructor sin parámetros seguirá siendo introducido
automáticamente por el compilador a diferencia de cómo ocurría con las clases donde en
ese caso el compilador no lo introducía.

Por otro lado, para conseguir que el valor por defecto de todos los objetos estructuras
sea el mismo, se prohíbe darles una valor inicial a sus campos en el momento de
declararlos, pues si no el constructor por defecto habría de tenerlos en cuenta y su
ejecución sería más ineficiente. Por esta razón, los constructores definidos por el
programador para una estructura han de inicializar todos sus miembros no estáticos en
tanto que antes de llamarlos no se les da ningún valor inicial.

Nótese que debido a la existencia de un constructor por defecto cuya implementación


escapa de manos del programador, el código de los métodos de una estructura puede
tener que considerar la posibilidad de que se acceda a ellos con los valores resultantes de
una inicialización con ese constructor. Por ejemplo, dado:

struct A
{
public readonly string S;

public A(string s)
{

114
Clases en Microsoft Visual C#

if (s==null)
throw (new ArgumentNullException());
this.S = s;
}
}

Nada asegura que en este código los objetos de clase A siempre se inicialicen con un
valor distinto de null en su campo S, pues aunque el constructor definido para
A comprueba que eso no ocurra lanzando una excepción en caso de que se le pase una
cadena que valga null, si el programador usa el constructor por defecto creará un objeto
en el que S valga null. Además, ni siquiera es válido especificar un valor inicial a S en su
definición, ya que para inicializar rápidamente las estructuras sus campos no estáticos no
pueden tener valores iniciales.

Enumeraciones
Concepto de enumeración

Una enumeración o tipo enumerado es un tipo especial de estructura en la que los


literales de los valores que pueden tomar sus objetos se indican explícitamente al definirla.
Por ejemplo, una enumeración de nombre Tamaño cuyos objetos pudiesen tomar los
valores literales Pequeño, Mediano o Grande se definiría así:

enum Tamaño
{
Pequeño,
Mediano,
Grande
}

Para entender bien la principal utilidad de las enumeraciones vamos a ver antes un
problema muy típico en programación: si queremos definir un método que pueda imprimir
por pantalla un cierto texto con diferentes tamaños, una primera posibilidad sería dotarlo
de un parámetro de algún tipo entero que indique el tamaño con el que se desea mostrar
el texto. A estos números que los métodos interpretan con significados específicos se les
suele denominar números mágicos, y su utilización tiene los inconvenientes de que
dificulta la legibilidad del código (hay que recordar qué significa para el método cada valor
del número) y su escritura (hay que recordar qué número ha pasársele al método para
que funcione de una cierta forma)

Una alternativa mejor para el método anterior consiste en definirlo de modo que tome
un parámetro de tipo Tamaño para que así el programador usuario no tenga que recordar
la correspondencia entre tamaños y números. Véase así como la llamada (2) del ejemplo
que sigue es mucho más legible que la (1):

115
Clases en Microsoft Visual C#

obj.MuestraTexto(2); // (1)
obj.MuestraTexto(Tamaño.Mediano); // (2)

Además, estos literales no sólo facilitan la escritura y lectura del código sino que
también pueden ser usados por herramientas de documentación, depuradores u otras
aplicaciones para sustituir números mágicos y mostrar textos muchos más legibles.

Por otro lado, usar enumeraciones también facilita el mantenimiento del código. Por
ejemplo, si el método (1) anterior se hubiese definido de forma que 1 significase tamaño
pequeño, 2 mediano y 3 grande, cuando se quisiese incluir un nuevo tamaño intermedio
entre pequeño y mediano habría que darle un valor superior a 3 o inferior a 1 ya que los
demás estarían cogidos, lo que rompería el orden de menor a mayor entre números y
tamaños asociados. Sin embargo, usando una enumeración no importaría mantener el
orden relativo y bastaría añadirle un nuevo literal.

Otra ventaja de usar enumeraciones frente a números mágicos es que éstas participan
en el mecanismo de comprobación de tipos de C# y el CLR. Así, si un método espera un
objeto Tamaño y se le pasa uno de otro tipo enumerado se producirá, según cuando se
detecte la incoherencia, un error en compilación o una excepción en ejecución. Sin
embargo, si se hubiesen usado números mágicos del mismo tipo en vez de enumeraciones
no se habría detectado nada, pues en ambos casos para el compilador y el CLR serían
simples números sin ningún significado especial asociado.

Definición de enumeraciones

Ya hemos visto un ejemplo de cómo definir una enumeración. Sin embargo, la sintaxis
completa que se puede usar para definirlas es:

enum <nombreEnumeración> : <tipoBase>


{
<literales>
}

En realidad una enumeración es un tipo especial de estructura


(luego System.ValueType será tipo padre de ella) que sólo puede tener como miembros
campos públicos constantes y estáticos. Esos campos se indican en , y como sus
modificadores son siempre los mismos no hay que especificarlos (de hecho, es erróneo
hacerlo)

El tipo por defecto de las constantes que forman una enumeración es int, aunque
puede dárseles cualquier otro tipo básico entero
(byte, sbyte, short, ushort, uint, int, long o ulong) indicándolo en <tipoBase>.
Cuando se haga esto hay que tener muy presente que el compilador de C# sólo admite

116
Clases en Microsoft Visual C#

que se indiquen así los alias de estos tipos básicos, pero no sus nombres reales
(System.Byte,System.SByte, etc.)

Si no se especifica valor inicial para cada constante, el compilador les dará por defecto
valores que empiecen desde 0 y se incrementen en una unidad para cada constante según
su orden de aparición en la definición de la enumeración. Así, el ejemplo del principio del
tema es equivalente a:

enum Tamaño:int
{
Pequeño = 0,
Mediano = 1,
Grande = 2
}

Es posible alterar los valores iniciales de cada constante indicándolos explícitamente


como en el código recién mostrado. Otra posibilidad es alterar el valor base a partir del
cual se va calculando el valor de las siguientes constantes como en este otro ejemplo:

enum Tamaño
{
Pequeño,
Mediano = 5,
Grande
}

En este último ejemplo el valor asociado a Pequeño será 0, el asociado a Mediano será
5, y el asociado a Grande será 6 ya que como no se le indica explícitamente ningún otro
se considera que este valor es el de la constante anterior más 1.

Obviamente, el nombre que se de a cada constante ha de ser diferente al de las demás


de su misma enumeración y el valor que se de a cada una ha de estar incluido en el rango
de valores admitidos por su tipo base. Sin embargo, nada obliga a que el valor que se de
a cada constante tenga que ser diferente al de las demás, y de hecho puede especificarse
el valor de una constante en función del valor de otra como muestra este ejemplo:

enum Tamaño
{
Pequeño,
Mediano = Pequeño,
Grande = Pequeño + Mediano
}

En realidad, lo único que importa es que el valor que se dé a cada literal, si es que se le
da alguno explícitamente, sea una expresión constante cuyo resultado se encuentre en el

117
Clases en Microsoft Visual C#

rango admitido por el tipo base de la enumeración y no provoque definiciones circulares.


Por ejemplo, la siguiente definición de enumeración es incorrecta ya que en ella los
literales Pequeño y Mediano se han definido circularmente:

enum TamañoMal
{
Pequeño = Mediano,
Mediano = Pequeño,
Grande
}

Nótese que también la siguiente definición de enumeración también sería incorrecta ya


que en ella el valor de B depende del de A implícitamente (sería el de A más 1):

enum EnumMal
{
A = B,
B
}

Uso de enumeraciones

Las variables de tipos enumerados se definen como cualquier otra variable (sintaxis
<nombreTipo> <nombreVariable>) Por ejemplo:

Tamaño t;

El valor por defecto para un objeto de una enumeración es 0, que puede o no


corresponderse con alguno de los literales definidos para ésta. Así, si la t del ejemplo
fuese un campo su valor sería Tamaño.Pequeño. También puede dársele otro valor al
definirla, como muestra el siguiente ejemplo donde se le da el valor Tamaño.Grande:

Tamaño t = Tamaño.Grande; // Ahora t vale Tamaño.Grande

Nótese que a la hora de hacer referencia a los literales de una enumeración se usa la
sintaxis <nombreEnumeración>.<nombreLiteral>, como es lógico si tenemos en cuenta
que en realidad los literales de una enumeración son constantes publicas y estáticas, pues
es la sintaxis que se usa para acceder a ese tipo de miembros. El único sitio donde no es
necesario preceder el nombre del literal de <nombreEnumeración>. es en la propia
definición de la enumeración, como también ocurre con cualquier constante estática.

En realidad los literales de una enumeración son constantes de tipos enteros y las
variables de tipo enumerado son variables del tipo entero base de la enumeración. Por eso

118
Clases en Microsoft Visual C#

es posible almacenar valores de enumeraciones en variables de tipos enteros y valores de


tipos enteros en variables de enumeraciones. Por ejemplo:

int i = Tamaño.Pequeño; // Ahora i vale 0


Tamaño t = (Tamaño) 0; // Ahora t vale Tamaño.Pequeño (=0)
t = (Tamaño) 100; // Ahora t vale 100, que no se corresponde
// con ningún literal

Como se ve en el último ejemplo, también es posible darle a una enumeración valores


enteros que no se correspondan con ninguno de sus literales.

Dado que los valores de una enumeración son enteros, es posible aplicarles muchas de
las operaciones que se pueden aplicar a los mismos: ==, !=, <, >, <=, >=, +, -
, ^, &, |, ~, ++, -- y sizeof. Sin embargo, hay que concretar que los operadores
binarios +y – no pueden aplicarse entre dos operandos de enumeraciones, sino que al
menos uno de ellos ha de ser un tipo entero; y que |, & y^ sólo pueden aplicarse entre
enumeraciones.

La clase System.Enum

Todos los tipos enumerados derivan de System.Enum, que deriva


de System.ValueType y ésta a su vez deriva de la clase primigenia System.Object.
Aparte de los métodos heredados de estas clases padres ya estudiados, toda enumeración
también dispone de otros métodos heredados de System.Enum, los principales de los
cuales son:

 static Type getUnderlyingType(Type enum): Devuelve un objeto System.Type con


información sobre el tipo base de la enumeración representada por el
objeto System.Type que se le pasa como parámetro[13].

 string ToString(string formato): Cuando a un objeto de un tipo enumerado se le


aplica el método ToString() heredado deobject, lo que se muestra es una cadena con el
nombre del literal almacenado en ese objeto. Por ejemplo (nótese queWriteLine() llama
automáticamente al ToString() de sus argumentos no string):

Tamaño t = Color.Pequeño;
Console.WriteLine(t); // Muestra por pantalla la cadena "Pequeño"

Como también puede resultar interasante obtener el valor numérico del literal, se ha
sobrecargado System.Enum el método anterior para que tome como parámetro una
cadena que indica cómo se desea mostrar el literal almacenado en el objeto. Si esta
cadena es nula, vacía o vale "G" muestra el literal como si del

119
Clases en Microsoft Visual C#

método ToString() estándar se tratase, pero si vale "D" o "X" lo que muestra es su valor
numérico (en decimal si vale "D" y en hexadecimal si vale "X") Por ejemplo:

Console.WriteLine(t.ToString("X")); // Muestra 0
Console.WriteLine(t.ToString("G")); // Muestra Pequeño

En realidad, los valores de formato son insensibles a la capitalización y da igual si en vez


de "G" se usa "g" o si en vez de "X" se usa "x".

 static string Format(Type enum, object valorLiteral, string formato): Funciona de


forma parecida a la sobrecarga deToString() recien vista, sólo que ahora no es necesario
disponer de ningún objeto del tipo enumerado cuya representación de literal se desea
obtener sino que basta indicar el objeto Type que lo representa y el número del literal a
obtener. Por ejemplo:

Console.Write(Enum.Format(typeof(Tamaño), 0, "G"); // Muestra Pequeño

Si el valorLiteral indicado no estuviese asociado a ningún literal del tipo enumerador


representado por enum, se devolvería una cadena con dicho número. Por el contrario, si
hubiesen varios literales en la enumeración con el mismo valor numérico asociado, lo que
se devolvería sería el nombre del declarado en último lugar al definir la enumeración.

 static object Parse(Type enum, string nombre, bool mayusculas?): Crea un objeto
de un tipo enumerado cuyo valor es el correspondiente al literal de nombre asociado
nombre. Si la enumeración no tuviese ningún literal con ese nombre se lanzaría
una ArgumentException, y para determinar cómo se ha de buscar el nombre entre los
literales de la enumeración se utiliza el tercer parámetro (es opcional y por defecto
vale false) que indica si se ha de ignorar la capitalización al buscarlo. Un ejemplo del uso
de este método es:

Tamaño t = (Tamaño) Enum.Parse(typeof(Tamaño), "Pequeño");


Console.WriteLine(t) // Muestra Pequeño

Aparte de crear objetos a partir del nombre del literal que almacenarán, Parse() también
permite crearlos a partir del valor numérico del mismo. Por ejemplo:

Tamaño t = (Tamaño) Enum.Parse(typeof(Tamaño), "0");


Console.WriteLine(t) // Muestra Pequeño

120
Clases en Microsoft Visual C#

En este caso, si el valor indicado no se correspondiese con el de ninguno de los literales


de la enumeración no saltaría ninguna excepción, pero el objeto creado no almacenaría
ningún literal válido. Por ejemplo:

Tamaño t = (Tamaño) Enum.Parse(typeof(Tamaño), "255");


Console.WriteLine(t) // Muestra 255

 static object[] GetValues(Type enum): Devuelve una tabla con los valores de todos
los literales de la enumeración representada por el objeto System.Type que se le pasa
como parámetro. Por ejemplo:

object[] tabla = Enum.GetValues(typeof(Tamaño));


Console.WriteLine(tabla[0]); // Muestra 0, pues Pequeño = 0
Console.WriteLine(tabla[1]); // Muestra 1, pues Mediano = 1
Console.WriteLine(tabla[2]); // Muestra 1, pues Grande = Pequeño+Mediano

 static string GetName(Type enum, object valor): Devuelve una cadena con el
nombre del literal de la enumeración representada por enum que tenga el valor
especificado en valor. Por ejemplo, este código muestra Pequeño por pantalla:

Console.WriteLine(Enum.GetName(typeof(Tamaño), 0)); //Imprime Pequeño

Si la enumeración no contiene ningún literal con ese valor devuelve null, y si tuviese
varios con ese mismo valor devolvería sólo el nombre del último. Si se quiere obtener el
de todos es mejor usar GetNames(), que se usa como GetName() pero devuelve
unstring[] con los nombres de todos los literales que tengan el valor indicado ordenados
según su orden de definición en la enumeración.

 static bool isDefined (Type enum, object valor): Devuelve un booleano que indica si
algún literal de la enumeración indicada tiene el valor indicado.

Enumeraciones de flags

Muchas veces interesa dar como valores de los literales de una enumeración
únicamente valores que sean potencias de dos, pues ello permite que mediante
operaciones de bits & y | se puede tratar los objetos del tipo enumerado como si
almacenasen simultáneamente varios literales de su tipo. A este tipo de enumeraciones las
llamaremos enumeraciones de flags, y un ejemplo de ellas es el siguiente:

enum ModificadorArchivo
{

121
Clases en Microsoft Visual C#

Lectura = 1,
Escritura = 2,
Oculto = 4,
Sistema = 8
}

Si queremos crear un objeto de este tipo que represente los modificadores de un


archivo de lectura-escritura podríamos hacer:

ModificadorArchivo obj =
ModificadorArchivo.Lectura | ModificadorArchivo.Escritura

El valor del tipo base de la enumeración que se habrá almacenado en obj es 3, que es
el resultado de hacer la operación OR entre los bits de los valores de los literales Lectura y
Escritura. Al ser los literales de ModificadorArchivo potencias de dos sólo tendrán un único
bit a 1 y dicho bit será diferente en cada uno de ellos, por lo que la única forma de
generar un 3 (últimos dos bits a 1) combinando literales de ModificadorArchivo es
combinando los literales Lectura (último bit a 1) y Escritura (penúltimo bit a 1) Por tanto,
el valor de obj identificará unívocamente la combinación de dichos literales.

Debido a esta combinabilidad no se debe determinar el valor literal de los objetos


ModificadorArchivo tal y como si sólo pudiesen almacenar un único literal, pues su valor
numérico no tendría porqué corresponderse con el de ningún literal de la enumeración Por
ejemplo:

bool permisoLectura = (obj == ModificadorArchivo.Lectura);// false

Aunque los permisos representados por obj incluían permiso de lectura, se


devuelve false porque el valor numérico de obj es 3 y el del ModificadorArchivo.Lectura es
1. Si lo que queremos es comprobar si obj contiene permiso de lectura, entonces habrá
que usar el operador de bits & para aislarlo del resto de literales combinados que
contiene:

bool permisoLectura =
(ModificadorArchivo.Lectura == (obj & ModificadorArchivo.Lectura));//true

O, lo que es lo mismo:

bool permisoLectura = ( (obj & ModificadorArchivo.Lectura) != 0);//true

122
Clases en Microsoft Visual C#

Asimismo, si directamente se intenta mostrar por pantalla el valor de un objeto de una


enumeración que almacene un valor que sea combinación de literales, no se obtendrá el
resultado esperado (nombre del literal correspondiente a su valor) Por ejemplo, dado:

Console.Write(obj); // Muestra 3

Se mostrará un 3 por pantalla ya que en realidad ningún literal de ModificadorArchivo


tiene asociado dicho valor. Como lo natural sería que se desease obtener un mensaje de la
forma Lectura, Escritura, los métodos ToString() y Format() de las enumeraciones ya
vistos admiten un cuarto valor "F" para su parámetro formato (su nombre viene de flags)
con el que se consigue lo anterior. Por tanto:

Console.Write(obj.ToString("F")); // Muestra Lectura, Escritura

Esto se debe a que cuando Format() detecta este indicador (ToString() también,
pues para generar la cadena llama internamente a Format()) y el literal almacenado en
el objeto no se corresponde con ninguno de los de su tipo enumerado, entonces lo que
hace es mirar uno por uno los bits a uno del valor numérico asociado de dicho literal y
añadirle a la cadena a devolver el nombre de cada literal de la enumeración cuyo valor
asociado sólo tenga ese bit a uno, usándo como separador entre nombres un carácter de
coma.

Nótese que nada obliga a que los literales del tipo enumerado tengan porqué haberse
definido como potencias de dos, aunque es lo más conveniente para que "F" sea útil, pues
si la enumeración tuviese algún literal con el valor del objeto de tipo enumerado no se
realizaría el proceso anterior y se devolvería sólo el nombre de ese literal.

Por otro lado, si alguno de los bits a 1 del valor numérico del objeto no tuviese el
correspondiente literal con sólo ese bit a 1 en la enumeración no se realizaría tampoco el
proceso anterior y se devolvería una cadena con dicho valor numérico.

Una posibilidad más cómoda para obtener el mismo efecto que con "F" es marcar la
definición de la enumeración con el atributoFlags, con lo que ni siquiera sería necesario
indicar formato al llamar a ToString() O sea, si se define ModificadorArchivo así:

[Flags]
enum ModificadorArchivo
{
Lectura = 1,
Escritura = 2,
Oculto = 4,
Sistema = 8
}

123
Clases en Microsoft Visual C#

Entonces la siguiente llamada producirá como salida Lectura, Escritura:

Console.Write(obj); // Muestra Lectura, Escritura

Esto se debe a que en ausencia del modificador "F", Format() mira dentro de los
metadatos del tipo enumerado al que pertenece el valor numérico a mostrar si éste
dispone del atributo Flags. Si es así funciona como si se le hubiese pasado "F".

También cabe destacar que, para crear objetos de enumeraciones cuyo valor sea una
combinación de valores de literales de su tipo enumerado, el método
método Parse() de Enum permite que la cadena que se le especifica como segundo
parámetro cuente con múltiples literales separados por comas. Por ejemplo, un objeto
ModificadorArchivo que represente modificadores de lectura y ocultación puede crearse
con:

ModificadorArchivo obj =
(ModificadorArchivo) Enum.Parse(typeof(ModificadorArchivo)
,"Lectura,Oculto"));

Hay que señalar que esta capacidad de crear objetos de enumeraciones cuyo valor
almacenado sea una combinación de los literales definidos en dicha enumeración es
totalmente independiente de si al definirla se utilizó el atributo Flags o no.

Interfaces
Concepto de interfaz

Una interfaz es la definición de un conjunto de métodos para los que no se da


implementación, sino que se les define de manera similar a como se definen los métodos
abstractos. Es más, una interfaz puede verse como una forma especial de definir clases
abstractas que tan sólo contengan miembros abstractos.

Como las clases abstractas, las interfaces son tipos referencia, no puede crearse
objetos de ellas sino sólo de tipos que deriven de ellas, y participan del polimorfismo. Sin
embargo, también tienen numerosas diferencias con éstas:

 Es posible definir tipos que deriven de más de una interfaz. Esto se debe a que los
problemas que se podrían presentar al crear tipos que hereden de varios padres se deben
a la difícil resolución de los conflictos derivados de la herencia de varias implementaciones
diferentes de un mismo método. Sin embargo, como con las interfaces esto nunca podrá
ocurrir en tanto que no incluyen código, se permite la herencia múltiple de las mismas.

 Las estructuras no pueden heredar de clases pero sí de interfaces, y las interfaces no


pueden derivar de clases, pero sí de otras interfaces.

124
Clases en Microsoft Visual C#

 Todo tipo que derive de una interfaz ha de dar una implementación de todos los miembros
que hereda de esta, y no como ocurre con las clases abstractas donde es posible no darla
si se define como abstracta también la clase hija. De esta manera queda definido un
contrato en la clase que la hereda que va a permitir poder usarla con seguridad en
situaciones polimórficas: toda clase que herede una interfaz implementará todos los
métodos de la misma. Por esta razón se suele denominarimplementar una interfaz al
hecho de heredar de ella.

Nótese que debido a esto, no suele convenir ampliar interfaces ya definidas e


implementadas, puesto que cualquier añadido invalidará sus implementaciones hasta que
se defina en las mismas un implementación para dicho añadido. Sin embargo, si se hereda
de una clase abstracta este problema no se tendrá siempre que el miembro añadido a la
clase abstracta no sea abstracto.

 Las interfaces sólo pueden tener como miembros métodos normales, eventos, propiedades
e indizadores; pero no pueden incluir definiciones de campos, operadores, constructores,
destructores o miembros estáticos. Además, todos los miembros de las interfaces son
implícitamente públicos y no se les puede dar ningún modificador de acceso (ni
siquiera public, pues se supone)

Definición de interfaces

La sintaxis general que se sigue a la hora de definir una interfaz es:

<modificadores> interface <nombre>:<interfacesBase>


{
<miembros>
}

Los <modificadores> admitidos por las interfaces son los mismos que los de las clases
Es decir, public, internal, private, protected, protected internal o new (e
igualmente, los cuatro últimos sólo son aplicables a interfaces definidas dentro de otros
tipos)

El <nombre> de una interfaz puede ser cualquier identificador válido, aunque por
convenio se suele usar I como primer carácter del mismo (IComparable, IA, etc)

Los <miembros> de las interfaces pueden ser definiciones de métodos, propiedades,


indizadores o eventos, pero no campos, operadores, constructores o destructores. La
sintaxis que se sigue para definir cada tipo de miembro es la misma que para definirlos
como abstractos en una clase pero sin incluir abstract por suponerse implícitamente:

 Métodos: <tipoRetorno> <nombreMétodo>(<parámetros>);

 Propiedades: <tipo> <nombrePropiedad> {set; get;}

125
Clases en Microsoft Visual C#

Los bloques get y set pueden intercambiarse y puede no incluirse uno de ellos
(propiedad de sólo lectura o de sólo escritura según el caso), pero no los dos.

 Indizadores: <tipo> this[<índices>] {set; get;}

Al igual que las propiedades, los bloques set y get pueden intercambiarse y obviarse
uno de ellos al definirlos.

 Eventos: event <delegado> <nombreEvento>;

Nótese que a diferencia de las propiedades e indizadores, no es necesario indicar nada


sobre sus bloques add y remove. Esto se debe a que siempre se han de implementar
ambos, aunque si se usa la sintaxis básica el compilador les da una implementación por
defecto automáticamente.

Cualquier definición de un miembro de una interfaz puede incluir el


modificador new para indicar que pretende ocultar otra heredada de alguna interfaz
padre. Sin embargo, el resto de modificadores no son válidos ya que implícitamente
siempre se considera que son public y abstract. Además, una interfaz tampoco puede
incluir miembros de tipo, por lo que es incorrecto incluir el modificador static al definir
sus miembros.

Cada interfaz puede heredar de varias interfaces, que se indicarían en


<interfacesBase> separadas por comas. Esta lista sólo puede incluir interfaces, pero no
clases o estructuras; y a continuación se muestra un ejemplo de cómo definir una interfaz
IC que hereda de otras dos interfaces IA y IB:

public delegate void D (int x);


interface IA
{
int PropiedadA{get;}
void Común(int x);
}
interface IB
{
int this [int índice] {get; set;}
void Común(int x);
}
interface IC: IA, IB
{
event D EventoC;
}

Nótese que aunque las interfaces padres de IC contienen un método común no hay
problema alguno a la hora de definirlas. En el siguiente epígrafe veremos cómo se
resuelven las ambigüedades que por esto pudiesen darse al implementar IC.

126
Clases en Microsoft Visual C#

Implementación de interfaces

Para definir una clase o estructura que implemente una o más interfaces basta incluir
los nombres de las mismas como si de una clase base se tratase -separándolas con comas
si son varias o si la clase definida hereda de otra clase- y asegurar que la clase cuente con
definiciones para todos los miembros de las interfaces de las que hereda -lo que se puede
conseguir definiéndolos en ella o heredándolos de su clase padre.

Las definiciones que se den de miembros de interfaces han de ser siempre públicas y
no pueden incluir override, pues como sus miembros son implícitamente abstract se
sobreentiende. Sin embargo, sí pueden dársele los modificadores
como virtual ó abstracty usar override en redefiniciones que se les den en clases hijas
de la clase que implemente la interfaz.

Cuando una clase deriva de más de una interfaz que incluye un mismo miembro, la
implementación que se le dé servirá para todas las interfaces que cuenten con ese
miembro. Sin embargo, también es posible dar una implementación diferente para cada
una usando una implementación explícita, lo que consiste en implementar el miembro
sin el modificador public y anteponiendo a su nombre el nombre de la interfaz a la que
pertenece seguido de un punto (carácter .)

Cuando un miembro se implementa explícitamente, no se le pueden dar modificadores


como en las implementaciones implícitas, ni siquiera virtual o abstract. Una forma de
simular los modificadores que se necesiten consiste en darles un cuerpo que lo que haga
sea llamar a otra función que sí cuente con esos modificadores.

El siguiente ejemplo muestra cómo definir una clase CL que implemente la interfaz IC:

class CL:IC
{
public int PropiedadA
{
get {return 5;}
set {Console.WriteLine("Asignado{0} a PropiedadA", value);}
}

void IA.Común(int x)
{
Console.WriteLine("Ejecutado Común() de IA");
}

public int this[int índice]


{
get { return 1;}
set { Console.WriteLine("Asignado {0} a indizador", value); }
}

void IB.Común(int x)
{

127
Clases en Microsoft Visual C#

Console.WriteLine("Ejecutado Común() de IB");


}

public event D EventoC;


}

Como se ve, para implementar la interfaz IC ha sido necesario implementar todos sus
miembros, incluso los heredados de IA y IB, de la siguiente manera:

 Al EventoC se le ha dado la implementación por defecto, aunque si se quisiese se podría


haber dado una implementación específica a sus bloques add y remove.

 Al método Común() se le ha dado una implementación para cada versión heredada de una
de las clases padre de IC, usándose para ello la sintaxis de implementación explícita antes
comentada. Nótese que no se ha incluido el modificador public en la implementación de
estos miembros.

 A la PropiedadA se le ha dado una implementación con un bloque set que no aparecía en


la definición de PropiedadA en la interfaz IA. Esto es válido hacerlo siempre y cuando la
propiedad no se haya implementado explícitamente, y lo mismo ocurre con los indizadores
y en los casos en que en vez de set sea get el bloque extra implementado.

Otra utilidad de las implementaciones explícitas es que son la única manera de


conseguir poder dar implementación a métodos ocultados en las definiciones de
interfaces. Por ejemplo, si tenemos:

interface IPadre
{
int P{get;}
}
interface IHija:Padre
{
new int P();
}

La única forma de poder definir una clase donde se dé una implementación tanto para
el método P() como para la propiedad P, es usando implementación explícita así:

class C: IHija
{
void IPadre.P {}
public int P() {…}
}

128
Clases en Microsoft Visual C#

O así:

class C: IHija
{
public void P() {}
int IHija.P() {}
}

O así:

class C: IHija
{
void IPadre.P() {}
int IHija.P() {}
}

Pero como no se puede implementar es sin ninguna implementación explícita, pues se


produciría un error al tener ambos miembros las misma signatura. Es decir, la siguiente
definición no es correcta:

class C: IHija
{
public int P() {} // ERROR: Ambos miembros tienen la misma signatura
public void P(){}
}

Es posible reimplementar en una clase hija las definiciones que su clase padre diese
para los métodos que heredó de una interfaz. Para hacer eso basta hacer que la clase hija
también herede de esa interfaz y dar en ella las definiciones explícitas de miembros de la
interfaz que se estimen convenientes, considerándose que las implementaciones para los
demás serán las heredadas de su clase padre. Por ejemplo:

using System;

interface IA
{
void F();
}

class C1: IA
{
public void F()

129
Clases en Microsoft Visual C#

{
Console.WriteLine("El F() de C1");
}
}

class C2: C1, IA


{// Sin implementación explícita no redefiniría, sino ocultaría
void IA.F()
{
Console.WriteLine("El F() de C2");
}

public static void Main()


{
IA obj = new C1();
IA obj2 = new C2();
obj.F();
obj2.F();
}

Reimplementar un miembro de una interfaz de esta manera es parecido a redefinir los


miembros reimplementados, sólo que ahora la redefinición sería solamente accesible a
través de variables del tipo de la interfaz. Así, la salida del ejemplo anterior sería:

El F() de C1

El F() de C2

Hay que tener en cuenta que de esta manera sólo pueden hacerse reimplementaciones
de miembros si la clase donde se reimplementa hereda directamente de la interfaz
implementada explícitamente o de alguna interfaz derivada de ésta. Así, en el ejemplo
anterior sería incorrecto haber hecho:

class C2:C1 //La lista de herencias e interfaces implementadas por C2 sólo incluye a C1
{
void IA.f(); // ERROR: Aunque C1 herede de IA, IA no se incluye directamente
// en la lista de interfaces implementadas por C2
}

Es importante señalar que el nombre de interfaz especificado en una implementación


explícita ha de ser exactamente el nombre de la interfaz donde se definió el miembro
implementado, no el de alguna subclase de la misma. Por ejemplo:

130
Clases en Microsoft Visual C#

interface I1
{
void F()
}
interface I2:I1
{}
class C1:I2
{
public void I2.F(); //ERROR: habría que usar I1.F()
}

En el ejemplo anterior, la línea comentada contiene un error debido a que F() se definió
dentro de la interfaz I1, y aunque también pertenezca a I2 porque ésta lo hereda de I1, a
la hora de implementarlo explícitamente hay que prefijar su nombre de I1, no de I2.

Acceso a miembros de una interfaz

Se puede acceder a los miembros de una interfaz implementados en una clase de


manera no explícita a través de variables de esa clase como si de miembros normales de
la misma se tratase. Por ejemplo, este código mostraría un cinco por pantalla:

CL c = new CL();
Console.WriteLine(c.PropiedadA);

Sin embargo, también es posible definir variables cuyo tipo sea una interfaz. Aunque no
existen constructores de interfaces, estas variables pueden inicializarse gracias al
polimorfismo asignándoles objetos de clases que implementen esa interfaz. Así, el
siguiente código también mostraría un cinco por pantalla:

IA a = new CL();
Console.WriteLine(a.PropiedadA);

Nótese que a través de una variable de un tipo interfaz sólo se puede acceder a
miembros del objeto almacenado en ella que estén definidos en esa interfaz. Es decir, los
únicos miembros válidos para el objeto a anterior serían PropiedadA y Común()

En caso de que el miembro al que se pretenda acceder haya sido implementado


explícitamente, sólo puede accederse a él a través de variables del tipo interfaz al que
pertenece y no a través de variables de tipos que hereden de ella, ya que la definición de
estos miembros es privada al no llevar modificador de acceso. Por ejemplo:

CL cl = new CL();

131
Clases en Microsoft Visual C#

IA a = cl;
IB b = cl;
// Console.WriteLine(cl.Común()); // Error: Común()
// fue implementado explícitamente
Console.WriteLine(a.Común());
Console.WriteLine(b.Común());
Console.WriteLine(((IA) cl).Común());
Console.WriteLine(((IB) cl).Común());

Cada vez que se llame a un método implementado explícitamente se llamará a la


versión del mismo definida para la interfaz a través de la que se accede. Por ello, la salida
del código anterior será:

Ejecutado Común() de IA

Ejecutado Común() de IB

Ejecutado Común() de IA

Ejecutado Común() de IB

Se puede dar tanto una implementación implícita como una explícita de cada miembro
de una interfaz. La explícita se usará cuando se acceda a un objeto que implemente esa
interfaz a través de una referencia a la interfaz, mientras que la implícita se usará cuando
el acceso se haga a través de una referencia del tipo que implementa la interfaz. Por
ejemplo, dado el siguiente código:

interface I
{object Clone();}
class Clase:I
{
public object Clone()
{
Console.WriteLine("Implementación implícita");
}
public object IClonable.Clone()
{
Console.WriteLine("Implementación explícita");
}

public static void Main()


{
Clase obj = new Clase();
((I) obj).Clone();
obj.Clone();
}
}

132
Clases en Microsoft Visual C#

El resultado que por pantalla se mostrará tras ejecutarlo es:

Implementación explícita

Implementación implícita

Acceso a miembros de interfaces y boxing

Es importante señalar que aunque las estructuras puedan implementar interfaces tal y
como lo hacen las clases, el llamarlas a través de referencias a la interfaz supone una gran
pérdida de rendimiento, ya que como las interfaces son tipos referencia ello implicaría la
realización del ya visto proceso de boxing. Por ejemplo, en el código:

using System;

interface IIncrementable
{ void Incrementar();}

struct Estructura:IIncrementable
{
public int Valor;

public void Incrementar()


{
Valor++;
}

static void Main()


{
Estructura o = new Estructura();
Console.WriteLine(o.Valor);
((IIncrementable) o).Incrementar();
Console.WriteLine(o.Valor);
o.Incrementar();
Console.WriteLine(o.Valor);
}
}

La salida obtenida será:

133
Clases en Microsoft Visual C#

Donde nótese que el resultado tras la primera llamada a Incrementar() sigue siendo el
mismo ya que como se le ha hecho a través de un referencia a la interfaz, habrá sido
aplicado sobre la copia de la estructura resultante del boxing y no sobre la estructura
original. Sin embargo, la segunda llamada sí que se aplica a la estructura ya que se realiza
directamente sobre el objeto original, y por tanto incrementa su campo Valor.

Otras instrucciones
Las instrucciones vistas hasta ahora son comunes a muchos lenguajes de
programación. Sin embargo, en C# también se ha incluido un buen número de nuevas
instrucciones propias de este lenguaje. Estas instrucciones se describen en los siguientes
apartados:

Instrucciones checked y unchecked

Las instrucciones checked y unchecked permiten controlar la forma en que tratarán


los desbordamientos que ocurran durante la realización de operaciones aritméticas con
tipos básico enteros. Funcionan de forma similar a los
operadores checked y uncheckedya vistos en el Tema 4: Aspectos léxicos, aunque a
diferencia de éstos son aplicables a bloques enteros de instrucciones y no a una única
expresión. Así, la instrucción checked se usa de este modo:

checked
<instrucciones>

Todo desbordamiento que se produzca al realizar operaciones aritméticas con enteros


en <instrucciones> provocará que se lance una excepción System.OverflowException.
Por su parte, la instrucción unchecked se usa así:

unchecked
<instrucciones>

En este caso, todo desbordamiento que se produzca al realizar operaciones aritméticas


con tipos básicos enteros en <instrucciones> será ignorado y lo que se hará será tomar el
valor resultante de quedarse con los bits menos significativos necesarios.

Por defecto, en ausencia de estas instrucciones las expresiones constantes se evalúan


como si se incluyesen dentro de una instrucción checked y las que no constantes como si
se incluyesen dentro de una instrucción unchecked. Sin embargo, a través de la
opción /checked del compilador es posible tanto hacer que por defecto se comprueben
los desbordamientos en todos los casos para así siempre poder detectarlos y tratarlos.

134
Clases en Microsoft Visual C#

Desde Visual Studio.NET, la forma de controlar el tipo de comprobaciones que por


defecto se harán es a través de View à Propety Pages à Configuration
Settings à Build à Check for overflow underflow.

El siguiente código muestra un ejemplo de cómo usar ambas instrucciones:

using System;

class Unchecked
{
static short x = 32767; // Valor maximo del tipo short

public static void Main()


{
unchecked
{
Console.WriteLine((short) (x+1)); // (1)
Console.WriteLine((short) 32768); // (2)
}
}

En un principio este código compilaría, pero los desbordamientos producidos por el


hecho de que 32768 no es un valor que se pueda representar con un short (16 bits con
signo) provocarían que apareciese por pantalla dicho valor truncado, mostrándose:

-32768

-32678

Sin embargo, si sustituyésemos la instrucción unchecked por checked, el código


anterior ni siquiera compilaría ya que el compilador detectaría que se va a producir un
desbordamiento en (2) debido a que 32768 es constante y no representable con unshort.

Si eliminamos la instrucción (2) el código compilaría ya que (x+1) no es una expresión


constante y por tanto el compilador no podría detectar desbordamiento al compilar. Sin
embargo, cuando se ejecutase la aplicación se lanzaría
una System.OverflowException.

Instrucción lock

La instrucción lock es útil en aplicaciones concurrentes donde múltiples hilos pueden


estar accediendo simultáneamente a un mismo recurso, ya que lo que hace es garantizar
que un hilo no pueda acceder a un recurso mientras otro también lo esté haciendo. Su
sintaxis es la siguiente:

135
Clases en Microsoft Visual C#

lock (<objeto>)
<instrucciones>

Su significado es el siguiente: ningún hilo puede ejecutar las <instrucciones> del


bloque indicado si otro las está ejecutando, y si alguno lo intenta se quedará esperando
hasta que acabe el primero. Esto también afecta a bloques de <instrucciones> de
cualquier otro lock cuyo <objeto> sea el mismo. Este <objeto> ha de ser de algún tipo
referencia.

En realidad, la instrucción anterior es equivalente a hacer:

System.Threading.Monitor.Enter(<objeto>);
try
{
<instrucciones>
}
finally
{
System.Threading.Monitor.Exit(<objeto>);
}

Sin embargo, usar lock tiene dos ventajas: es más compacto y eficiente (<objeto> sólo
se evalúa una vez)

Una buena forma de garantizar la exclusión mutua durante la ejecución de un método


de un cierto objeto es usando this como <objeto> En el caso de que se tratase de un
método de tipo, en tanto que this no tiene sentido dentro de estos métodos estáticos una
buena alternativa sería usar el objeto System.Type que representase a ese tipo. Por
ejemplo:

class C
{
public static void F()
{
lock(typeof(C))
{
// ... Código al que se accede exclusivamente
}
}
}

Instrucción using

136
Clases en Microsoft Visual C#

La instrucción using facilita el trabajo con objetos que tengan que ejecutar alguna
tarea de limpieza o liberación de recursos una vez que termine de ser útiles. Aunque para
estos menesteres ya están los destructores, dado su carácter indeterminista puede que en
determinadas ocasiones no sea conveniente confiar en ellos para realizar este tipo de
tareas. La sintaxis de uso de esta instrucción es la siguiente:

using (<tipo> <declaraciones>)


<instrucciones>

En <declaraciones> se puede indicar uno o varios objetos de tipo <tipo> separados


por comas. Estos objetos serán de sólo lectura y sólo serán accesibles desde
<instrucciones>. Además, han de implementar la interfaz System.IDisposable definida
como sigue:

interface IDisposable
{
void Dispose()
}

En la implementación de Dispose() se escribiría el código de limpieza necesario, pues


el significado de using consiste en que al acabar la ejecución de <instrucciones>, se
llama automáticamente al método Dispose() de los objetos definidos en
<declaraciones>.

Hay que tener en cuenta que la llamada a Dispose() se hace sea cual sea la razón de
que se deje de ejecutar las <instrucciones> Es decir, tanto si se ha producido una
excepción como si se ha acabado su ejecución normalmente o con una instrucción de
salto,Dispose() es siempre llamado. En realidad una instrucción using como:

using (R1 r1 = new R1())


{
r1.F();
}

Es tratada por el compilador como:

{
R1 r1 = new R1()
try
{

137
Clases en Microsoft Visual C#

r1.F();
}
finally
{
if (r1!=null)
((IDisposable) r1).Dispose();
}
}

Si se declarasen varios objetos en <declaraciones>, a Dispose() se le llamaría en el


orden inverso a como fueron declarados. Lo mismo ocurre si se anidasen varias
instrucciones using: primero se llamaría al Dispose() de las variables declaradas en
los usinginternos y luego a las de los externos. Así, estas dos instrucciones son
equivalentes:

using (Recurso obj = new Recurso(), obj2= new Recurso())


{
r1.F();
r2.F();
}
using (Recurso obj = new Recurso())
{
using (Recurso obj2= new Recurso())
{
r1.F();
r2.F();
}
}

El siguiente ejemplo resume cómo funciona la sentencia using:

using System;
class A:IDisposable
{
public void Dispose()
{
Console.WriteLine("Llamado a Dispose() de {0}", Nombre);
}

public A(string nombre)


{
Nombre = nombre;
}
string Nombre;
}
class Using
{
public static void Main()
{

138
Clases en Microsoft Visual C#

A objk = new A("objk");


using (A obj1 = new A("obj1"), obj2 = new A("objy"))
{
Console.WriteLine("Dentro del using");
}
Console.WriteLine("Fuera del using");
}
}

La salida por pantalla resultante de ejecutar este ejemplo será:

Dentro del using

Llamando a Dispose() de objy

Llamando a Dispose() de obj1

Fuera del using

Como se deduce de los mensajes de salida obtenidos, justo antes de salirse


del using se llama a los métodos Dispose() de los objetos declarados en la sección
<declaraciones> de dicha instrucción y en el mismo orden en que fueron declarados.

Instrucción fixed

La instrucción fixed se utiliza para fijar objetos en memoria de modo que el


recolector de basura no pueda moverlos durante la ejecución de un cierto bloque de
instrucciones.

Esta instrucción sólo tiene sentido dentro de regiones de código inseguro, concepto que
se trata en el Tema 18: Código inseguro, por lo que será allí es donde se explique a fondo
cómo utilizarla. Aquí sólo diremos que su sintaxis de uso es:

fixed(<tipoPunteros> <declaracionesPunterosAFijar>)
<instrucciones>

Atributos
Concepto de atributo

Un atributo es información que se puede añadir a los metadatos de un módulo de


código. Esta información puede ser referente tanto al propio módulo o el ensamblado al
que pertenezca como a los tipos de datos definidos en él, sus miembros, los parámetros

139
Clases en Microsoft Visual C#

de sus métodos, los bloques set y get de sus propiedades e indizadores o los
bloques add y remove de sus eventos.

En C# se incluyen numerosos modificadores que nos permiten asociar información a los


metadatos de un módulo. Por ejemplo, con los
modificadores public, protected, private, internal o protected internal podemos
añadir información sobre la visibilidad de los tipos del módulo y de sus miembros. Pues
bien, los atributos pueden verse como un mecanismo mediante el cual el programador
puede crear sus propios modificadores.

Un ejemplo de atributo podría ser uno llamado Ayuda que pudiese prefijar las
definiciones de miembros de tipos e indicase cuál es la URL donde se pudiese encontrar
información detallada con ayuda sobre el significado del miembro prefijado.

Utilización de atributos

Para colocar un atributo a un elemento basta prefijar la definición de dicho elemento


con una estructura de esta forma:

[<nombreAtributo>(<parámetros>)]

Esta estructura ha de colocarse incluso antes que cualquier modificador que pudiese
acompañar la definición del elemento a atribuir.

Los parámetros de un atributo pueden ser opcionales, y si se usa sin especificar valores
para sus parámetros no hay porqué que usar paréntesis vacíos como en las llamadas a
métodos, sino que basta usar el atributo indicando sólo la sintaxis [<nombreAtributo>]

Los parámetros de un atributo pueden ser de dos tipos:

 Parámetros sin nombre: Se usan de forma similar a los parámetros de los métodos,
sólo que no pueden contar con modificadores ref u out.

 Parámetros con nombre: Son opcionales y pueden colocarse en cualquier posición en la


lista de <parámetros> del atributo. Lo último se debe a que para darles valor se usa la
sintaxis <nombreParámetro>=<valor>, con lo el compilador no dependerá de la posición
en que se les pasen los valores para determinar a qué parámetro se le está dando cada
valor, sino que recibe explícita su nombre.

Para evitar conflictos entre parámetros con nombre y parámetros sin nombre, los
primeros siempre se han de incluir después de los segundos, no siendo posible mezclarlos
indiscriminadamente.

Si se desean especificar varios atributos para un mismo elemento se pueden indicar


todos ellos entre unos mismos corchetes separados por comas. Es decir, de la forma:

140
Clases en Microsoft Visual C#

[<atributo1>(<parametros1>), <atributo2>(<parámetros2>), ...]

Aunque también sería posible especificarlos por separado. O sea, de esta otra forma:

[<atributo1>(<parametros1>)] [<atributo2>(<parámetros2>)] ...

Hay casos en los que por la ubicación del atributo no se puede determinar de manera
unívoca a cuál elemento se le desea aplicar, ya que podría ser aplicable a varios. En esos
casos, para evitar ambigüedades lo que se hace es usar el atributo prefijando su nombre
de un indicador de tipo de elemento, quedando así la sintaxis a usar:

[<indicadorElemento>:<nombreAtributo>(<parámetros>)]

Aunque cada implementación de C# puede incluir sus propios indicadores de tipo de


elemento, todas ellas incluirán al menos los siguientes:

 assembly: Indica que el atributo se aplica al ensamblado en que se compile el código


fuente que lo contenga. Al definir atributos de ensamblado es obligatorio incluir este
indicador, ya que estos atributos se colocan precediendo cualquier definición de clase o
espacio de nombres y si no se incluyesen se confundiría con atributos de tipo, que se
colocan en el mismo sitio.

 module: Indica que el atributo se aplica al módulo en que se compile el código fuente
que lo contenga. Al igual que el indicadorassembly, hay que incluirlo siempre para definir
este tipo de atributos porque si no se confundirían con atributos de tipo, ya que también
se han de ubicar precediendo las definiciones de clases y espacios de nombres.

 type: Indica que el atributo se aplica al tipo cuya definición precede. En realidad no hace
falta utilizarlo, pues es lo que por defecto se considera para todo atributo que preceda a
una definición de tipo. Sin embargo, se ha incluido por consistencia con el resto de
indicadores de tipo de atributo y porque puede resultar conveniente incluirlo ya que
explicitarlo facilita la lectura del código.

 return: Indica que el atributo se aplica a un valor de retorno de un método, operador,


bloque get, o definición de delegado. Si no se incluyese se consideraría que se aplica a la
definición del método, operador, bloque get o delegado, ya que estos atributos se colocan
antes de la misma al igual que los atributos de valores de retorno.

 param: Indica que el atributo se aplica a un parámetro de un método. Si no se incluyese


al definir bloques set, add o removese consideraría que el atributo se refiere a los
bloques en sí y no al parámetro value en ellos implícito.

141
Clases en Microsoft Visual C#

 method: Indica que el atributo se aplica al método al que precede. En realidad no es


necesario usarlo porque, como se dice en la explicación de los
indicadores param y return, es lo que se considera por defecto. Sin embrago, y como
pasaba con type, se incluye por consistencia y porque puede ser buena idea incluirlo para
facilitar la legibilidad del código con su explicitación.

 event: Indica que el atributo se aplica al evento a cuya definición precede. En realidad no
es necesario incluirlo porque es lo que se considera por defecto, pero nuevamente se ha
incluido por consistencia y para facilitar la lectura del código.

 property: Indica que el atributo se aplica a la propiedad a cuya definición precede. Éste
también es un indicador innecesario e incluido tan sólo por consistencia y para facilitar la
legibilidad del código.

 field: Indica que el atributo se aplica al cuya definición precede. Como otros indicadores,
sólo se incluye por consistencia y para hacer más legible el código

Definición de nuevos atributos


Especificación del nombre del atributo

Se considera que un atributo es toda aquella clase que derive de System.Attribute.


Por tanto, para definir un nuevo tipo de atributo hay que crear una clase que derive de
ella. Por convenio, a este tipo de clases suele dárseles nombres acabados en Attribute,
aunque a la hora de usarlas desde C# es posible obviar dicho sufijo. Un ejemplo de cómo
definir un atributo llamado Ayuda es:

using System;
class AyudaAttribute:Attribute
{}

Y ejemplos de cómo usarlo prefijando la definición de clases son:

[Ayuda]
class A
{}
[AyudaAttribute]
class B
{}

Puede darse la circunstancia de que se haya definido un atributo con un cierto nombre
sin sufijo Attribute y otro que si lo tenga. Como es lógico, en ese caso cuando se use el
atributo sin especificar el sufijo se hará referencia a la versión sin sufijo y cuando se use
con sufijo se hará referencia a la versión con sufijo.

142
Clases en Microsoft Visual C#

Especificación del uso de un atributo

Por defecto cualquier atributo que se defina puede preceder la definición de cualquier
elemento del lenguaje. Si se desea limitar a qué definiciones puede preceder es necesario
prefijar la clase que lo define con un atributo especial llamado System.AttributeUsage.
Este atributo consta de los siguientes parámetros con nombre:

 AllowMultiple: Por defecto cada atributo sólo puede aparecer una vez prefijando a cada
elemento. Dándole el valor true a este parámetro se considerará que puede aparecer
múltiples veces.

 Inherited: Por defecto los atributos aplicados a una clase no son heredados en sus clases
hijas. Dándole el valor true a este parámetro se consigue que sí lo sean.

Aparte de estos dos parámetros, AttributeUsage también puede contar con un


parámetro opcional sin nombre que indique a qué tipos de definiciones puede preceder.
Por defecto se considera que un atributo puede preceder a cualquier elemento, lo que es
equivalente a darle el valor AttributeTargets.All a este parámetro. Sin embrago es
posible especificar otras posibilidades dándole valores de la
enumeración System.AttributeTargets, que son los que se recogen en la Tabla:

Valor de AttributeTargets Significa que el atributo puede preceder a...


All Cualquier definición
Assembly Definiciones de espacio de nombres, considerándose que el
atributo se refiere al ensamblado en general.
Module Definiciones de espacio de nombres, considerándose que el
atributo se refiere al módulo en su conjunto.
Class Definiciones de clases
Delegate Definiciones de delegados
Interface Definiciones de interfaces
Struct Definiciones de estructuras
Enum Definiciones de enumeraciones
Field Definiciones de campos
Method Definiciones de métodos
Constructor Definiciones de constructores
Property Definiciones de propiedades o indizadores
Event Definiciones de eventos
Parameter Definiciones de parámetros de métodos
ReturnValue Definiciones de valores de retorno de métodos

Tabla 9: Valores de AttributeTargets

Es posible combinar varios de estos valores mediante operaciones lógicas "or" (carácter
| ) Por ejemplo, si queremos definir el atributo Ayuda anterior de modo que sólo pueda
ser usado para prefijar definiciones de enumeraciones o de clases se haría:

143
Clases en Microsoft Visual C#

[AttributeUsage(AttributeTargets.Class | AttributeTargetes.Enum)]
class Ayuda:Attribute
{}

Es importante resaltar que AttributeUsage sólo puede incluirse precediendo


definiciones de otros atributos (o sea, de clases derivadas de System.Attribute)

Especificación de parámetros válidos

Se considera que los parámetros sin nombre que puede tomar un atributo son aquellos
que se especifiquen como parámetros en el constructor del tipo que lo define, y que sus
parámetros con nombre serán las propiedades y campos públicos, no estáticos y de
lectura/escritura definidos en dicho tipo.

Un ejemplo de cómo definir el atributo Ayuda anterior de modo que tome un parámetro
sin nombre con la URL que indique dónde encontrar la ayuda sobre el miembro o clase al
que precede y un parámetro con nombre llamado Autor que indique quién es el autor de
esa documentación es:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Enum)]
class Ayuda:Attribute
{
private string autor;
private string url;

public Ayuda(string URL)


{ url=URL; }

public string Autor


{
set {autor = value;}
get {return autor;}
}
}

Ejemplos de usos válidos de este atributo son:

[Ayuda("http://www.josan.com/Clases/A.html")]
class A {}
[Ayuda("http://www.josan.com/Clases/B.html", Autor="José Antonio")]
class B {}

Los tipos válidos de parámetros, tanto con nombre como sin él, que puede tomar un
atributo son: cualquier tipo básico exceptodecimal y los tipos enteros sin signo, cualquier
144
Clases en Microsoft Visual C#

enumeración pública, System.Type o tablas unidimensionales de elementos de


cualquiera de los anteriores tipos válidos.

Lectura de atributos en tiempo de ejecución


Para acceder a los metadatos de cualquier ensamblado se utilizan las clases del espacio
de nombres System.Reflection. Este espacio de nombres es inmenso y explicar cómo
utilizarlo queda fuera del alcance de este libro, aunque de todos modos a continuación se
darán unas ideas básicas sobre cómo acceder a través de sus tipos a los atributos
incluidos en los ensamblados.

La clave para acceder a los atributos se encuentra en el método estático de la


clase System.Attribute llamado Attribute[] GetCustomAttributes(<x>
objetoReflexivo), donde <x> es el tipo de System.Reflection que representa a los
elementos cuyos atributos se desea obtener. Los posibles tipos son: Assembly, que
representa ensamblados, Module que representa módulos,MemberInfo que representa
miembros (incluidos tipos, que al fin y al cabo son miembros de espacios de nombres),
yParameterInfo que representa parámetros. El parámetro tomado por este método será
el objeto que represente al elemento en concreto cuyos metadatos se quieren obtener.

Como se ve, GetCustomAttributes() devuelve una tabla con los atributos en forma
de objetos Attribute, que es la clase base de todos los atributos, por lo que si a partir de
ellos se desease acceder a características específicas de cada tipo de atributo habría que
aplicar downcasting como se comentó en el Tema 5: Clases (para asegurase de que las
conversiones se realicen con éxito recuérdese que se puede usar el operador is para
determinar cuál es el verdadero tipo de cada atributo de esta tabla)

Para obtener el objeto Assembly que representa al ensamblado al que pertenezca el


código que se esté ejecutando se usa el método Assembly
GetExecutingAssembly() de la clase Assembly, que se usa tal y como se muestra:

Assembly ensamblado = Assembly.GetExecutingAssembly();

Otra posibilidad sería obtener ese objeto Assembly a partir del nombre del fichero
donde se encuentre almacenado el ensamblado. Para ello se usa el método Assembly
LoadFrom(string rutaEnsamblado) de la clase Assembly como se muestra:

Assembly ensamblado = Assembly.LoadFrom("josan.dll");

Una vez obtenido el objeto que representa a un ensamblado, pueden obtenerse los
objetos Module que representan a los módulos que lo forman a través de su
método Module[] GetModules().

145
Clases en Microsoft Visual C#

A partir del objeto Module que representa a un módulo puede obtenerse los
objetos Type que representan a sus tipos a través de su método Type[]
GetTypes() Otra posibilidad sería usar el operador typeof ya visto para obtener
el Type que representa a un tipo en concreto sin necesidad de crear
objetos Module o Assembly.

En cualquier caso, una vez obtenido un objeto Type, a través de sus


métodos FieldInfo[] GetFields(), MethodInfo[] GetMethods(), ConstructorInfo[]
GetConstructors(), EventInfo[] GetEvents[] y PropertyInfo[]
GetProperties() pueden obtenerse los objetos reflexivos que representan, de manera
respectiva, a sus campos, métodos, constructores, eventos y propiedades o indizadores.
Tanto todos estos objetos como los objetos Type derivan de MemberInfo, por lo que
pueden ser pasados como parámetros de GetCustomAttributes() para obtener los
atributos de los elementos que representan.

Por otro lado, a través de los objetos MethodInfo y ConstructorInfo, es posible


obtener los tipos reflexivos que representan a los parámetros de métodos y constructores
llamando a su método ParameterInfo[] GetParameters() Además, en el caso de los
objetos MethodInfo también es posible obtener el objeto que representa al tipo de
retorno del método que representan mediante su propiedad Type ReturnType {get;}.

En lo referente a las propiedades, es posible obtener los objetos MethodInfo que


representan a sus bloques get y set a través de los métodos MethodInfo
GetSetMethod() y MethodInfo GetSetMethod() de los objetos PropertyInfo que
las representan. Además, para obtener los objetos reflexivos que representen a los índices
de los indizadores también se dispone de un métodoParamterInfo[]
GetIndexParameters()

Y en cuanto a los eventos, los objetos EventInfo disponen de métodos MethodInfo


GetAddMethod() y MethodInfo GetRemoveMethod() mediante los que es posible
obtener los objetos reflexivos que representan a sus bloques add y remove.

A continuación se muestra un programa de ejemplo que lo que hace es mostrar por


pantalla el nombre de todos los atributos que en él se hayan definido:

using System.Reflection;
using System;

[assembly: EjemploEnsamblado]
[module: EjemploModulo]
[AttributeUsage(AttributeTargets.Method)]
class EjemploMétodo:Attribute
{}

[AttributeUsage(AttributeTargets.Assembly)]
class EjemploEnsamblado:Attribute
{}

146
Clases en Microsoft Visual C#

[AttributeUsage(AttributeTargets.Module)]
class EjemploModulo:Attribute
{}

[AttributeUsage(AttributeTargets.Class)]
class EjemploTipo:Attribute
{}

[AttributeUsage(AttributeTargets.Field)]
class EjemploCampo:Attribute
{}

[EjemploTipo]
class A
{
public static void Main()
{
Assembly ensamblado = Assembly.GetExecutingAssembly();

foreach(Attribute atributo in Attribute.GetCustomAttributes(ensamblado))


Console.WriteLine("ENSAMBLADO: {0}",atributo);

foreach(Module modulo in ensamblado.GetModules())


{
foreach(Attribute atributo in Attribute.GetCustomAttributes(modulo))
Console.WriteLine("MODULO: {0}", atributo);

foreach (Type tipo in modulo.GetTypes())


{
foreach(Attribute atributo in Attribute.GetCustomAttributes(tipo))
Console.WriteLine("TIPO: {0}", atributo);
foreach (FieldInfo campo in tipo.GetFields())
muestra("CAMPO", campo);
foreach (MethodInfo metodo in tipo.GetMethods())
muestra("METODO", metodo);
foreach (EventInfo evento in tipo.GetEvents())
muestra("EVENTO", evento);
foreach (PropertyInfo propiedad in tipo.GetProperties())
muestra("PROPIEDAD", propiedad);
foreach (ConstructorInfo constructor in tipo.GetConstructors())
muestra("CONSTRUCTOR",constructor);
}
}
}

static private void muestra(string nombre, MemberInfo miembro)


{
foreach (Attribute atributo in Attribute.GetCustomAttributes(miembro))
Console.WriteLine("{0}: {1}", nombre, atributo);
}
}

Lo único que hace el Main() de este programa es obtener el Assembly que


representa el ensamblado actual y mostrar todos sus atributos de ensamblado. Luego
obtiene todos los Modules que representa a los módulos de dicho ensamblado, y muestra

147
Clases en Microsoft Visual C#

todos los atributos de módulo de cada uno. Además, de cada módulo se obtienen todos
los Types que representan a los tipos en él definidos y se muestran todos sus atributos; y
de cada tipo se obtienen los objetos reflexivos que representan a sus diferentes tipos de
miembros y se muestran los atributos de cada miembro.

Aparte del método Main() en el ejemplo se han incluido definiciones de numerosos


atributos de ejemplo aplicables a diferentes tipos de elemento y se han diseminado a lo
largo del fuente varios usos de estos atributos. Por ello, la salida del programa es:

ENSAMBLADO: EjemploEnsamblado

ENSAMBLADO: System.Diagnostics.DebuggableAttribute

MODULO EjemploModulo

TIPO: System.AttributeUsageAttribute

TIPO: System.AttributeUsageAttribute

TIPO: System.AttributeUsageAttribute

TIPO: System.AttributeUsageAttribute

TIPO: System.AttributeUsageAttribute

TIPO: EjemploTipo

METODO: EjemploMétodo

Nótese que aparte de los atributos utilizados en el código fuente, la salida del
programa muestra que el compilador ha asociado a nivel de ensamblado un atributo extra
llamado Debuggable. Este atributo incluye información sobre si pueden aplicarse
optimizaciones al compilar JIT el ensamblado o si se ha de realizar una traza de su
ejecución. Sin embargo, no conviene fiarse de su implementación ya que no está
documentado por Microsoft y puede cambiar en futuras versiones de la plataforma .NET.

Atributos de compilación
Aunque la mayoría de los atributos son interpretados en tiempo de ejecución por el CLR
u otras aplicaciones, hay una serie de atributos que tienen un significado especial en C# y
condicionan el proceso de compilación. Estos son los que se explican a continuación.

Atributo System.AttributeUsage

148
Clases en Microsoft Visual C#

Ya hemos visto en este mismo tema que se usa para indicar dónde se pueden colocar
los nuevos atributos que el programador defina, por lo que no se hará más hincapié en él.

Atributo System.Obsolete

Puede preceder a cualquier elemento de un fichero de código fuente para indicar que
ha quedado obsoleto. Admite los siguientes dos parámetros sin nombre:

 Un primer parámetro de tipo string que contenga una cadena con un mensaje a mostrar
cuando al compilar se detecte que se ha usado el elemento obsoleto.
 Un segundo parámetro de tipo bool que indique si se ha de producir un aviso o un error
cuando se detecte el uso del elemento obsoleto. Por defecto se muestra un aviso, pero si
se da valor true a este parámetro, el mensaje será de error.

El siguiente ejemplo muestra como utilizar este atributo:

using System;

class Obsoleta
{
[Obsolete("No usar f(), que está obsoleto.", true)]
public static void f()
{}

public static void Main()


{
f();
}
}

Cuando se compile este programa, el compilador emitirá el siguiente mensaje de error:

obsolete.cs(11,17): error CS0619: „Obsoleta.f()‟ is obsolete: No usar f(), que está obsoleto.

Si no se hubiese especificado a Obsolete su segundo parámetro, entonces el mensaje


sería de aviso en vez de error:

obsolete.cs(11,17): warning CS0618: „Obsoleta.f()‟ is obsolete: No usar f(), que está obsoleto.

Atributo System.Diagnostics.Conditional

Este atributo sólo puede prefijar definiciones de métodos, y permite definir si las
llamadas al método prefijado se han de compilar o no. Puede usarse múltiples veces
prefijando a un mismo método y toma un parámetro sin nombre de tipo string. Sólo se

149
Clases en Microsoft Visual C#

compilarán aquellas llamadas al método tales que en el momento de hacerlas esté


definida alguna directiva de preprocesado con el mismo nombre que el parámetro de
alguno de los atributos Conditional que prefijen la definición de ese método.

Como se ve, este atributo es una buena forma de simplificar la escritura de código que
se deba compilar condicionalmente, ya que evita tener varias directivas #if que encierren
cada llamada al método cuya ejecución se desea controlar. Sin embargo, Conditionalno
controla la compilación de ese método, sino sólo las llamadas al mismo.

El siguiente ejemplo muestra cómo usar Conditional:

using System;
using System.Diagnostics;

class Condicional
{
[Conditional("DEBUG")]
public static void F()
{
Console.WriteLine("F()");
}

public static void Main()


{
F();
}
}

Sólo si compilamos el este código definiendo la constante de preprocesado DEBUG se


mostrará por pantalla el mensaje F() En caso contrario, nunca se hará la llamada a F()

Hay que precisar que en realidad Conditional no puede preceder a cualquier definición
de método, sino que en su colocación hay impuestas ciertas restricciones especiales:

 El método ha de tener un tipo de retorno void. Esto se debe a que si tuviese otro se
podría usar su valor de retorno como operando en expresiones, y cuando no fuesen
compiladas sus llamadas esas expresiones podrían no tener sentido y producir errores de
compilación.

 Si se aplica a un método virtual todas sus redefiniciones lo heredan, siendo erróneo


aplicárselo explícitamente a una de ellas. Esto debe a que en tiempo de compilación puede
no saberse cuál es el verdadero tipo de un objeto, y si unas redefiniciones pudiesen ser
condicionales y otras no, no podría determinarse al compilar si es condicional la versión
del método a la que en cada caso se llame.

 No puede atribuirse a métodos definidos en interfaces ni a implementaciones de métodos


de interfaces, pues son también virtuales y podrían reimplementarse.

150
Clases en Microsoft Visual C#

Atributo System.ClsCompliant

Permite especificar que el compilador ha de asegurarse de que el ensamblado, tipo de


dato o miembro al que se aplica es compatible con el CLS. Ello se le indica (ya sea por
nombre o posicionalmente) a través de su único parámetro bool IsCompliant, tal y
como se muestra en el siguiente código ejemplo:

using System;

[assembly:CLSCompliant(true)]
public class A
{
public void F(uint x)
{}

public static void Main()


{}
}

Si intenta compilarlo tal cual, obtendrá el siguiente mensaje de error:

error CS3001: El tipo de argumento 'uint' no es compatible con CLS

Esto se debe a que el tipo uint no forma parte del CLS, y en el código se está
utilizando como parte de un método público aún cuando mediante el
atributo CLSCompliant se está indicando que se desea que el código sea compatible con
el CLS. Si se le quitase este atributo, o se diese el valor false a su parámetro, o se
definiesen la clase A o el método F() como privados, o se cambiase el tipo del parámetro x
a un tipo perteneciente al CLS (pe, int), entonces sí que compilaría el código.

Nótese que cuando el atributo CLSCompliant se aplica a nivel de todo un


ensamblado, se comprueba la adecuación al CLS de todos sus tipos de datos, mientras
que si solamente se aplica a un tipo de dato, sólo se comprobará la adecuación al CLS del
mismo; y si tan sólo se aplica a un miembro, únicamente se comprobará la adecuación al
CLS de éste.

Pseudoatributos
La BCL proporciona algunos atributos que, aunque se usan de la misma manera que el
resto, se almacenan de manera especial en los metadatos, como lo harían modificadores
como virtual o private. Por ello se les denomina pseudoatributos, y no se pueden
recuperar mediante el ya visto método GetCustomAttributes(), aunque para algunos de
ellos se proporcionan otro mecanismos de recuperación específicos. Un ejemplo es el
atributo DllImport que ya se ha visto que se usa para definición de métodos externos.
Así, dado el siguiente código:

151
Clases en Microsoft Visual C#

using System.Reflection;
using System.Runtime.InteropServices;
using System;
using System.Diagnostics;

class A
{
[DllImport("kernel32")][Conditional("DEBUG")]

public static extern void CopyFile(string fuente, string destino);

public static void Main()


{

MethodInfo método = typeof(A).GetMethod("CopyFile");


foreach (Attribute atributo in método.GetCustomAttributes(false))
Console.WriteLine(atributo);
}
}

La salida que se obtendría al ejecutarlo es la siguiente:

System.Diagnostics.ConditionalAttribute

Donde como se puede ver, no se ha recuperado el pseudoatributo DllImport mientras


que el otro (Conditional), que es un atributo normal, sí que lo ha hecho.

Código inseguro
Concepto de código inseguro

Código inseguro es todo aquél fragmento de código en C# dentro del cual es posible
hacer uso de punteros.

Un puntero en C# es una variable que es capaz de almacenar direcciones de memoria.


Generalmente suele usarse para almacenar direcciones que almacenen objetos, por lo que
en esos casos su significado es similar al de variables normales de tipos referencia. Sin
embargo, los punteros no cuentan con muchas de las restricciones de éstas a la hora de
acceder al objeto. Por ejemplo, al accederse a los elementos de una tabla mediante un
puntero no se pierde tiempo en comprobar que el índice especificado se encuentre dentro
de los límites de la tabla, lo que permite que el acceso se haga más rápidamente.

152
Clases en Microsoft Visual C#

Aparte de su mayor eficiencia, también hay ciertos casos en que es necesario disponer
del código inseguro, como cuando se desea hacer llamadas a funciones escritas en
lenguajes no gestionados cuyos parámetros tengan que ser punteros.

Es importante señalar que los punteros son una excepción en el sistema de tipos de
.NET, ya que no derivan de la clase primigeniaSystem.Object, por lo que no dispondrán
de los métodos comunes a todos los objetos y una variable object no podrá almacenarlos
(tampoco existen procesos similares al boxing y unboxing que permitan simularlo)

Compilación de códigos inseguros

El uso de punteros hace el código más proclive a fallos en tanto que se salta muchas de
las medidas incluidas en el acceso normal a objetos, por lo que es necesario incluir ciertas
medidas de seguridad que eviten la introducción accidental de esta inseguridad

La primera medida tomada consiste en que explícitamente hay que indicar al


compilador que deseamos compilar código inseguro. Para ello, al compilador de línea de
comandos hemos de pasarle la opción /unsafe, como se muestra el ejemplo:

csc códigoInseguro.cs /unsafe

Si no se indica la opción unsafe, cuando el compilador detecte algún fuente con código
inseguro producirá un mensaje de error como el siguiente:

códigoInseguro(5,23): error CS0277: unsafe code may only appear if compiling with /unsafe

En caso de que la compilación se vaya a realizar a través de Visual Studio.NET, la forma


de indicar que se desea compilar código inseguro es activando la casilla View à Property
Pages à Configuration Properties à Build à Allow unsafe code blocks

Marcado de códigos inseguros

Aparte de forzarse a indicar explícitamente que se desea compilar código inseguro, C#


también obliga a que todo uso de código inseguro que se haga en un fichero fuente tenga
que ser explícitamente indicado como tal. A las zonas de código donde se usa código
inseguro se les denomina contextos inseguros, y C# ofrece varios mecanismos para
marcar este tipo de contextos.

Una primera posibilidad consiste en preceder un bloque de instrucciones de la palabra


reservada unsafe siguiendo la siguiente sintaxis:

unsafe <instrucciones>

153
Clases en Microsoft Visual C#

En el código incluido en <instrucciones> podrán definirse variables de tipos puntero y


podrá hacerse uso de las mismas. Por ejemplo:

public void f()


{
unsafe
{
int *x;
}
}

Otra forma de definir contextos inseguros consiste en añadir el modificador unsafe a la


definición de un miembro, caso en que dentro de su definición se podrá hacer uso de
punteros. Así es posible definir campos de tipo puntero, métodos con parámetros de tipos
puntero, etc. El siguiente ejemplo muestra cómo definir dos campos de tipo puntero.
Nótese sin embargo que no es posible definir los dos en una misma línea:

struct PuntoInseguro
{
public unsafe int *X; // No es válido hacer public unsafe int *X, Y;
public unsafe int *Y; // Tampoco lo es hacer public unsafe int *X, *Y;
}

Obviamente, en un método que incluya el modificador unsafe no es necesario


preceder con dicha palabra sus bloques de instrucciones inseguros.

Hay que tener en cuenta que el añadido de modificadores unsafe es completamente


inocuo. Es decir, no influye para nada en cómo se haya de redefinir y si un
método Main() lo tiene sigue siendo un punto de entrada válido.

Una tercera forma consiste en añadir el modificador unsafe en la definición de un tipo,


caso en que todas las definiciones de miembros del mismo podrán incluir código inseguro
sin necesidad de añadir a cada una el modificador unsafe o preceder sus bloques de
instrucciones inseguras de la palabra reservada unsafe. Por ejemplo:

unsafe struct PuntoInseguro


{
public int * X, *Y;
}

154
Clases en Microsoft Visual C#

Definición de punteros
Para definir una variable puntero de un determinado tipo se sigue una sintaxis parecida
a la usada para definir variables normales sólo que al nombre del tipo se le postpone un
símbolo de asterisco (*) O sea, un puntero se define así:

<tipo> * <nombrePuntero>;

Por ejemplo, una variable puntero llamada a que pueda almacenar referencias a
posiciones de memoria donde se almacenen objetos de tipo int se declara así:

int * a;

En caso de quererse declarar una tabla de punteros, entonces el asterisco hay que
incluirlo tras el nombre del tipo pero antes de los corchetes. Por ejemplo, una tabla de
nombre t que pueda almacenar punteros a objetos de tipo int se declara así:

int*[] t;

Hay un tipo especial de puntero que es capaz de almacenar referencias a objetos de


cualquier tipo. Éstos punteros se declaran indicando void como <tipo>. Por ejemplo:

void * punteroACualquierCosa;

Hay que tener en cuenta que en realidad lo que indica el tipo que se dé a un puntero
es cuál es el tipo de objetos que se ha de considerar que se almacenan en la dirección de
memoria almacenada por el puntero. Si se le da el valor void lo que se está diciendo es
que no se desea que se considere que el puntero apunta a ningún tipo específico de
objeto. Es decir, no se está dando información sobre el tipo apuntado.

Se pueden declarar múltiples variables locales de tipo puntero en una misma línea. En
ese caso el asterisco sólo hay que incluirlo antes del nombre de la primera. Por ejemplo:

int * a, b; // a y b son de tipo int * No sería válido haberlas definido como


int *a, *b;

155
Clases en Microsoft Visual C#

Hay que tener en cuenta que esta sintaxis especial para definir en una misma definición
varios punteros de un mismo tipo sólo es válida en definiciones de variables locales. Al
definir campos no sirve y hay que dar para cada campo una definición independiente.

El recolector de basura no tiene en cuenta los datos a los que se referencie con
punteros, pues ha de conocer cuál es el objeto al referenciado por cada variable y un
puntero en realidad no tiene porqué almacenar referencias a objetos de ningún tipo en
concreto. Por ejemplo, pueden tenerse punteros int * que en realidad apunten a
objeto char, o punteros void * que no almacenen información sobre el tipo de objeto al
que debería considerarse que apuntan, o punteros que apunte a direcciones donde no
hayan objetos, etc.

Como el recolector de basura no trabaja con punteros, no es posible definir punteros


de tipos que se almacenen en memoria dinámica o contengan miembros que se
almacenen en memoria dinámica, ya que entonces podría ocurrir que un objeto sólo
referenciado a través de punteros sea destruido por considerar el recolector que nadie le
referenciaba. Por ello, sólo es válido definir punteros de tipos cuyos objetos se puedan
almacenar completamente en pila, pues la vida de estos objetos no está controlada por el
recolector de basura sino que se destruyen cuando se abandona el ámbito donde fueron
definidos.

En concreto, los únicos punteros válidos son aquellos que apunten a tipos valor básicos,
enumeraciones o estructuras que no contengan campos de tipos referencias. También
pueden definirse punteros a tipos puntero, como se muestra en el siguiente ejemplo de
declaración de un puntero a punteros de tipo int llamando punteroApunteros:

int ** punteroApunteros;

Obviamente la anidación puede hacerse a cualquier nivel de profundidad, pudiéndose


definir punteros a punteros a punteros, o punteros a punteros a punteros a punteros, etc.

Manipulación de punteros
Obtención de dirección de memoria. Operador &

Para almacenar una referencia a un objeto en un puntero se puede aplicar al objeto el


operador prefijo &, que lo que hace es devolver la dirección que en memoria ocupa el
objeto sobre el que se aplica. Un ejemplo de su uso para inicializar un puntero es:

int x =10;
int * px = &x;

156
Clases en Microsoft Visual C#

Este operador no es aplicable a expresiones constantes, pues éstas no se almacenan en


ninguna dirección de memoria específica sino que se incrustan en las instrucciones. Por
ello, no es válido hacer directamente:

int px = &10; // Error 10 no es una variable con dirección propia

Tampoco es válido aplicar & a campos readonly, pues si estos pudiesen ser apuntados
por punteros se correría el riesgo de poderlos modificar ya que a través de un puntero se
accede a memoria directamente, sin tenerse en cuenta si en la posición accedida hay
algún objeto, por lo que mucho menos se considerará si éste es de sólo lectura.

Lo que es sí válido es almacenar en un puntero es la dirección de memoria apuntada


por otro puntero. En ese caso ambos punteros apuntarían al mismo objeto y las
modificaciones a éste realizadas a través de un puntero también afectarían al objeto visto
por el otro, de forma similar a como ocurre con las variables normales de tipos referencia.
Es más, los operadores relacionales típicos (==, !=, <,>, <= y >=) se han redefinido
para que cuando se apliquen entre dos punteros de cualesquiera dos tipos lo que se
compare sean las direcciones de memoria que estos almacenan. Por ejemplo:

int x = 10;
int px = &x;
int px2 = px; // px y px2 apuntan al objeto almacenado en x
Console.WriteLine( px == px2); // Imprime por pantalla True

En realidad las variables sobre las que se aplique & no tienen porqué estar inicializadas.
Por ejemplo, es válido hacer:

private void f()


{
int x;
unsafe
{ int px = &x;}
}

Esto se debe a que uno de los principales usos de los punteros en C# es poderlos pasar
como parámetros de funciones no gestionadas que esperen recibir punteros. Como
muchas de esas funciones han sido programadas para inicializar los contenidos de los
punteros que se les pasan, pasarles punteros inicializados implicaría perder tiempo
innecesariamente en inicializarlos.

Acceso a contenido de puntero. Operador *

Un puntero no almacena directamente un objeto sino que suele almacenar la dirección


de memoria de un objeto (o sea, apunta a un objeto) Para obtener a partir de un puntero

157
Clases en Microsoft Visual C#

el objeto al que apunta hay que aplicarle al mismo el operador prefijo *, que devuelve el
objeto apuntado. Por ejemplo, el siguiente código imprime en pantalla un 10:

int x = 10;
int * px= &x;
Console.WriteLine(*px);

Es posible en un puntero almacenar null para indicar que no apunta a ninguna


dirección válida. Sin embargo, si luego se intenta acceder al contenido del mismo a través
del operador * se producirá generalmente una excepción de
tipo NullReferenceException(aunque realmente esto depende de la implementación del
lenguaje) Por ejemplo:

int * px = null;
Console.WriteLine(*px); // Produce una NullReferenceException

No tiene sentido aplicar * a un puntero de tipo void * ya que estos punteros no


almacenan información sobre el tipo de objetos a los que apuntan y por tanto no es
posible recuperarlos a través de los mismos ya que no se sabe cuanto espacio en memoria
a partir de la dirección almacenada en el puntero ocupa el objeto apuntado y, por tanto,
no se sabe cuanta memoria hay que leer para obtenerlo.

Acceso a miembro de contenido de puntero. Operador ->

Si un puntero apunta a un objeto estructura que tiene un método F() sería posible
llamarlo a través del puntero con:

(*objeto).F();

Sin embargo, como llamar a objetos apuntados por punteros es algo bastante habitual,
para facilitar la sintaxis con la que hacer esto se ha incluido en C# el operador ->, con el
que la instrucción anterior se escribiría así:

objeto->f();

Es decir, del mismo modo que el operador . permite acceder a los miembros de un
objeto referenciado por una variable normal, ->permite acceder a los miembros de un
objeto referenciado por un puntero. En general, un acceso de la forma O -> M es
equivalente a hacer (*O).M. Por tanto, al igual que es incorrecto aplicar * sobre punteros
de tipo void *, también lo es aplicar ->

158
Clases en Microsoft Visual C#

Conversiones de punteros

De todo lo visto hasta ahora parece que no tiene mucho sentido el uso de punteros de
tipo void * Pues bien, una utilidad de este tipo de punteros es que pueden usarse como
almacén de punteros de cualquier otro tipo que luego podrán ser recuperados a su tipo
original usando el operador de conversión explícita. Es decir, igual que los objetos de
tipo object pueden almacenar implícitamente objetos de cualquier tipo, los punteros void
* pueden almacenar punteros de cualquier tipo y son útiles para la escritura de métodos
que puedan aceptar parámetros de cualquier tipo de puntero.

A diferencia de lo que ocurre entre variables normales, las conversiones entre punteros
siempre se permiten, al realizarlas nunca se comprueba si son válidas. Por ejemplo:

char c = 'A';
char* pc = &c;
void* pv = pc;
int* pi = (int*)pv;
int i = *pi;// Almacena en 16 bits del char de pi + otros 16
// indeterminados
Console.WriteLine(i);
*pi = 123456;// Machaca los 32 bits apuntados por pi

En este código pi es un puntero a un objeto de tipo int (32 bits), pero en realidad el
objeto al que apunta es de tipo char (16 bits), que es más pequeño. El valor que se
almacene en i es en principio indefinido, pues depende de lo que hubiese en los 16 bits
extras resultantes de tratar pv como puntero a int cuando en realidad apuntaba a
un char.

Del mismo modo, conversiones entre punteros pueden terminar produciendo que un
puntero apunte a un objeto de mayor tamaño que los objetos del tipo del puntero. En
estos casos, el puntero apuntaría a los bits menos significativos del objeto apuntado.

También es posible realizar conversiones entre punteros y tipos básicos enteros. La


conversión de un puntero en un tipo entero devuelve la dirección de memoria apuntada
por el mismo. Por ejemplo, el siguiente código muestra por pantalla la dirección de
memoria apuntada por px:

int x = 10;
int *px = &x;
Console.WriteLine((int) px);

Por su parte, convertir cualquier valor entero en un puntero tiene el efecto de devolver
un puntero que apunte a la dirección de memoria indicada por ese número. Por ejemplo,
el siguiente código hace que px apunte a la dirección 1029 y luego imprime por pantalla la
dirección de memoria apuntada por px (que será 1029):

159
Clases en Microsoft Visual C#

int *px = (int *) 10;


Console.WriteLine((int) px);

Nótese que aunque en un principio es posible hacer que un puntero almacene


cualquier dirección de memoria, si dicha dirección no pertenece al mismo proceso que el
código en que se use el puntero se producirá un error al leer el contenido de dicha
dirección. El tipo de error ha producir no se indica en principio en la especificación del
lenguaje, pero la implementación de Microsoft lanza una
referencia NullReferenceException. Por ejemplo, el siguiente código produce una
excepción de dicho tipo al ejecurtase:

using System;

class AccesoInválido
{
public unsafe static void Main()
{
int * px = (int *) 100;
Console.Write(*px); // Se lanza NullReferenceException
}
}

Aritmética de punteros

Los punteros se suelen usar para recorrer tablas de elementos sin necesidad de tener
que comprobarse que el índice al que se accede en cada momento se encuentra dentro de
los límites de la tabla. Por ello, los operadores aritméticos definidos para los punteros
están orientados a facilitar este tipo de recorridos.

Hay que tener en cuenta que todos los operadores aritméticos aplicables a punteros
dependen del tamaño del tipo de dato apuntado, por lo que no son aplicables a
punteros void * ya que estos no almacenan información sobre dicho tipo. Esos
operadores son:

 ++ y --: El operador ++ no suma uno a la dirección almacenada en un puntero, sino que


le suma el tamaño del tipo de dato al que apunta. Así, si el puntero apuntaba a un
elemento de una tabla pasará a apuntar al siguiente (los elementos de las tablas se
almacenan en memoria consecutivamente) Del mismo modo, -- resta a la dirección
almacenada en el puntero el tamaño de su tipo de dato. Por ejemplo, una tabla de 100
elementos a cuyo primer elemento inicialmente apuntase pt podría recorrerse así:

for (int i=0; i<100; i++)


Console.WriteLine("Elemento{0}={1}", i, (*p)++);

160
Clases en Microsoft Visual C#

El problema que puede plantear en ciertos casos el uso de ++ y -- es que hacen que al
final del recorrido el puntero deje de apuntar al primer elemento de la tabla. Ello podría
solucionarse almacenando su dirección en otro puntero antes de iniciar el recorrido y
restaurándola a partir de él tras finalizarlo.

 + y -: Permiten solucionar el problema de ++ y -- antes comentado de una forma más


cómoda basada en sumar o restar un cierto entero a los punteros. + devuelve la dirección
resultante de sumar a la dirección almacenada en el puntero sobre el que se aplica el
tamaño del tipo de dicho puntero tantas veces como indique el entero sumado. - tiene el
mismo significado pero r estando dicha cantidad en vez de sumarla. Por ejemplo,
usando + el bucle anterior podría rescribirse así:

for (int i=0; i<100; i++)


Console.WriteLine("Elemento{0}={1}", i, *(p+i));

El operador - también puede aplicarse entre dos punteros de un mismo tipo, caso en
que devuelve un long que indica cuántos elementos del tipo del puntero pueden
almacenarse entre las direcciones de los punteros indicados.

 []: Dado que es frecuente usar + para acceder a elementos de tablas, también se ha
redefinido el operador [] para que cuando se aplique a una tabla haga lo mismo y
devuelva el objeto contenido en la dirección resultante. O sea *(p+i) es equivalente a
p[i], con lo que el código anterior equivale a:

for (int i=0; i<100; i++)


Console.WriteLine("Elemento{0}={1}", i, p[i]);

No hay que confundir el acceso a los elementos de una tabla aplicando [] sobre una
variable de tipo tabla normal con el acceso a través de un puntero que apunte a su primer
elemento. En el segundo caso no se comprueba si el índice indicado se encuentra dentro
del rango de la tabla, con lo que el acceso es más rápido pero también más proclive a
errores difíciles de detectar.

Finalmente, respecto a la aritmética de punteros, hay que tener en cuenta que por
eficiencia, en las operaciones con punteros nunca se comprueba si se producen
desbordamientos, y en caso de producirse se truncan los resultados sin avisarse de ello
mediante excepciones. Por eso hay que tener especial cuidado al operar con punteros no
sea que un desbordamiento no detectado cause errores de causas difíciles de encontrar.

Operadores relacionados con código inseguro


Operador sizeof. Obtención de tamaño de tipo

161
Clases en Microsoft Visual C#

El operador unario y prefijo sizeof devuelve un objeto int con el tamaño en bytes del
tipo de dato sobre el que se aplica. Sólo puede aplicarse en contextos inseguros y sólo a
tipos de datos para los que sea posible definir punteros, siendo su sintaxis de uso:

sizeof(<tipo>)

Cuando se aplica a tipos de datos básicos su resultado es siempre constante. Por ello,
el compilador optimiza dichos usos de sizeofsustituyéndolos internamente por su valor
(inlining) y considerando que el uso del operador es una expresión constante. Estas
constantes correspondientes a los tipos básicos son las indicadas en la Tabla 10:

Tipos Resultado
sbyte, byte, bool 1
short, ushort, char 2
int, uint, float 4
long, ulong, double 8

Tabla 10: Resultados de sizeof para tipos básicos

Para el resto de tipos a los que se les puede aplicar, sizeof no tiene porqué devolver
un resultado constante sino que los compiladores pueden alinear en memoria las
estructuras incluyendo bits de relleno cuyo número y valores sean en principio
indeterminado. Sin embargo, el valor devuelto por sizeof siempre devolverá el tamaño en
memoria exacto del tipo de dato sobre el que se aplique, incluyendo bits de relleno si los
tuviese.

Nótese que es fácil implementar los operadores de aritmética de punteros


usando sizeof. Para ello, ++ se definiría como añadir a la dirección almacenada en el
puntero el resultado de aplicar sizeof a su tipo de dato, y -- consistiría en restarle dicho
valor. Por su parte, el operador + usado de la forma P + N (P es un puntero de tipo T y N
un entero) lo que devuelve es el resultado de añadir al puntero sizeof(T)*N, y P – N
devuelve el resultado de restarle sizeof(T)*N. Por último, si se usa - para restar dos
punteros P1 y P2 de tipo T, ello es equivalente a calcular (((long)P1) -
((long)P2)))/sizeof(T)

Operador stackalloc. Creación de tablas en pila.

Cuando se trabaja con punteros puede resultar interesante reservar una zona de
memoria en la pila donde posteriormente se puedan ir almacenando objetos.
Precisamente para eso está el operador stackalloc, que se usa siguiéndose la siguiente
sintaxis:

stackalloc <tipo>[<número>]

162
Clases en Microsoft Visual C#

stackalloc reserva en pila el espacio necesario para almacenar contiguamente el


número de objetos de tipo <tipo> indicado en <número> (reserva
sizeof(<tipo>)*<número> bytes) y devuelve un puntero a la dirección de inicio de ese
espacio. Si no quedase memoria libre suficiente para reservarlo se produciría una
excepción System.StackOverflowException.

stackalloc sólo puede usarse para inicializar punteros declarados como variables
locales y sólo en el momento de su declaración.. Por ejemplo, un puntero pt que apuntase
al principio de una región con capacidad para 100 objetos de tipo int se declararía con:

int * pt = stackalloc int[100];

Sin embargo, no sería válido hacer:

int * pt;
pt = stackalloc int[100];//ERROR:Sólo puede usarse stackalloc
//en declaraciones

Aunque pueda parecer que stackalloc se usa como sustituto de new para crear tablas
en pila en lugar de en memoria dinámica, no hay que confundirse: stackalloc sólo
reserva un espacio contiguo en pila para objetos de un cierto tipo, pero ello no significa
que se cree una tabla en pila. Las tablas son objetos que heredan de System.Array y
cuentan con los miembros heredados de esta clase y de object, pero regiones de
memoria en pila reservadas por stackalloc no. Por ejemplo, el siguiente código es
inválido.

int[] tabla;
int * pt = stackalloc int[100];
tabla = *pt; //ERROR: El contenido de pt es un int, no una tabla (int[])
Console.WriteLine(pt->Length); // ERROR: pt no apunta a una tabla

Sin embargo, gracias a que como ya se ha comentado en este tema el operador [] está
redefinido para trabajar con punteros, podemos usarlo para acceder a los diferentes
objetos almacenados en las regiones reservadas con stackalloc como si fuesen tablas.
Por ejemplo, este código guarda en pila los 100 primeros enteros y luego los imprime:

class Stackalloc
{
public unsafe static void Main()
{
int * pt = stackalloc int[100];
for (int i=0; i<100; i++)

163
Clases en Microsoft Visual C#

pt[i] = i;
for(int i=0; i<100; i++)
System.Console.WriteLine(pt[i]);
}
}

Nótese que, a diferencia de lo que ocurriría si pt fuese una tabla, en los accesos con
pt[i] no se comprueba que i no supere el número de objetos para los que se ha reservado
memoria. Como contrapartida, se tiene el inconveniente de que al no ser pt una tabla no
cuenta con los métodos típicos de éstas y no puede usarse foreach para recorrerla.

Otra ventaja de la simulación de tablas con stackalloc es que se reserva la memoria


mucho más rápido que el tiempo que se tardaría en crear una tabla. Esto se debe a que
reservar la memoria necesaria en pila es tan sencillo como incrementar el puntero de pila
en la cantidad correspondiente al tamaño a reservar, y no hay que perder tiempo en
solicitar memoria dinámica. Además,stackalloc no pierde tiempo en inicializar con algún
valor el contenido de la memoria, por lo que la "tabla" se crea antes pero a costa de que
luego sea más inseguro usarla ya que hay que tener cuidado con no leer trozos de ella
antes de asignarles valores válidos.

Fijación de variables apuntadas


Aunque un puntero sólo puede apuntar a datos de tipos que puedan almacenarse
completamente en pila (o sea, que no sean ni objetos de tipos referencia ni estructuras
con miembros de tipos referencia), nada garantiza que los objetos apuntados en cada
momento estén almacenados en pila. Por ejemplo, las variables estáticas de tipo int o los
elementos de una tabla de tipo int se almacenan en memoria dinámica aún cuando son
objetos a los que se les puede apuntar con punteros.

Si un puntero almacena la dirección de un objeto almacenado en memoria dinámica y


el recolector de basura cambia al objeto de posición tras una compactación de memoria
resultante de una recolección, el valor almacenado en el puntero dejará de ser válido.
Para evitar que esto ocurra se puede usar la instrucción fixed, cuya sintaxis de uso es:

fixed(<tipo> <declaraciones>)
<instrucciones>

El significado de esta instrucción es el siguiente: se asegura que durante la ejecución


del bloque de <instrucciones> indicado el recolector de basura nunca cambie la dirección
de ninguno de los objetos apuntados por los punteros de tipo <tipo> declarados. Estas
<declaraciones> siempre han de incluir una especificación de valor inicial para cada
puntero declarado, y si se declaran varios se han de separar con comas.

Los punteros declarados en <declaraciones> sólo existirán dentro de <instrucciones>,


y al salir de dicho bloque se destruirán. Además, si se les indica como valor inicial una

164
Clases en Microsoft Visual C#

tabla o cadena que valga null saltará una NullReferenceException. También hay que
señalar que aunque sólo pueden declarase punteros de un mismo tipo en cada fixed, se
puede simular fácilmente la declaración de punteros de distintos tipos anidando
varios fixed.

Por otro lado, los punteros declarados en <declaraciones> son de sólo lectura, ya que
si no podría cambiárseles su valor por el de una dirección de memoria no fijada y conducir
ello a errores difíciles de detectar.

Un uso frecuente de fixed consiste en apuntar a objetos de tipos para los que se
puedan declarar punteros pero que estén almacenados en tablas, ya que ello no se puede
hacer directamente debido a que las tablas se almacenan en memoria dinámica. Por
ejemplo, copiar usando punteros una tabla de 100 elementos de tipo int en otra se haría
así:

class CopiaInsegura
{
public unsafe static void Main()
{
int[] tOrigen = new int[100];
int[] tDestino = new int[100];
fixed (int * pOrigen=tOrigen, pDestino=tDestino)
{
for (int i=0; i<100; i++)
pOrigen[i] = pDestino[i];
}
}
}

Como puede deducirse del ejemplo, cuando se inicializa un puntero con una tabla, la
dirección almacenada en el puntero en la zona <declaraciones> del fixed es la del primer
elemento de la tabla (también podría haberse hecho pOrigen = &tOrigen[0]), y luego es
posible usar la aritmética de punteros para acceder al resto de elementos a partir de la
dirección del primero ya que éstos se almacenan consecutivamente.

Al igual que tablas, también puede usarse fixed para recorrer cadenas. En este caso lo
que hay que hacer es inicializar un puntero de tipo char * con la dirección del primer
carácter de la cadena a la que se desee que apunte tal y como muestra este ejemplo en el
que se cambia el contenido de una cadena "Hola" por "XXXX":

class CadenaInsegura
{
public unsafe static void Main()
{
string s="Hola";
Console.WriteLine("Cadena inicial: {0}", s);
fixed (char * ps=s)

165
Clases en Microsoft Visual C#

{
for (int i=0;i<s.Length;i++)
ps[i] = 'A';
}
Console.WriteLine("Cadena final: {0}", s);
}
}

La salida por pantalla de este último programa es:

Hola

AAAA

La ventaja de modificar la cadena mediante punteros es que sin ellos no sería posible
hacerlo ya que el indizador definido para los objetos string es de sólo lectura.

Cuando se modifiquen cadenas mediante punteros hay que tener en cuenta que,
aunque para facilitar la comunicación con código no gestionado escrito en C o C++ las
cadenas en C# también acaban en el carácter „\0‟, no se recomienda confiar en ello al
recorrerlas con punteros porque „\0‟ también puede usarse como carácter de la cadena.
Por ello, es mejor hacer como en el ejemplo y detectar su final a través de su
propiedad Length.

Hay que señalar que como fixed provoca que no pueda cambiarse de dirección a
ciertos objetos almacenados en memoria dinámica, ello puede producir la generación de
huecos en memoria dinámica, lo que tiene dos efectos muy negativos:

 El recolector de basura está optimizado para trabajar con memoria compactada, pues si
todos los objetos se almacenan consecutivamente en memoria dinámica crear uno nuevo
es tan sencillo como añadirlo tras el último. Sin embargo, fixed rompe esta
consecutividad y la creación de objetos en memoria dinámica dentro de este tipo de
instrucciones es más lenta porque hay que buscar huecos libres.
 Por defecto, al eliminarse objetos de memoria durante una recolección de basura se
compacta la memoria que queda ocupada para que todos los objetos se almacenen en
memoria dinámica. Hacer esto dentro de sentencias fixed es más lento porque hay que
tener en cuenta si cada objeto se puede o no mover.

Por estas razones es conveniente que el contenido del bloque de instrucciones de una
sentencia fixed sea el mínimo posible, para que así el fixed se ejecute lo antes posible.

166
Clases en Microsoft Visual C#

Novedades de C# 2.0

Introducción

El 24 de Octubre de 2003 Microsoft hizo público el primer borrador de lo que sería la


versión 2.0 del lenguaje C#, incluida en la nueva versión del .NET Framework conocida
con el nombre clave Whidbey. En ella se introducía una importante novedad en el CLR
consistente en proporcionar soporte para tipos genéricos que se pudiesen usar como
plantillas en base a la que definir otros tipos. Esto lógicamente implicaba que a los
lenguajes .NET de Microsoft en primer lugar, y presumiblemente el resto después, se les
hiciesen también modificaciones orientadas a aprovechar esta nueva funcionalidad.

En este tema se explican las novedades para ello incluidas en la versión 2.0 de C#, así
como otras novedades no directamente relacionadas con los genéricos que también
incorpora: los iteradores para facilitar la implementación de las
interfaces IEnumerablee IEnumerator, los métodos anónimos y otros mecanismos
destinados a facilitar el trabajo con los delegados, la capacidad de dividir las definiciones
de las clases entre varios ficheros a través de clases parciales, la posibilidad de
asignar null a los tipos valor a través de los nuevos tipos valor anulables, etc.

En principio, las modificaciones introducidas en C# se han diseñado con la idea de


mantener el máximo nivel de compatibilidadcon códigos escritos para las anteriores
versiones del lenguaje –versiones 1.X-. Por ello, las nuevas palabras con significado
especial introducidas (where, yield, etc.) no se han clasificado como reservadas,
de modo que seguirán siendo válidos los identificadores que se hubiesen declarados con
sus nombres. Sólo se han introducido unas mínimas incompatibilidadesrelacionadas
con la sintaxis de los genéricos que se describen en el epígrafe Ambigüedades del tema.

Genéricos
Concepto de genéricos

C# 2.0 permite especificar los tipos utilizados en las definiciones de otros tipos de datos
y de métodos de forma parametrizada, de manera que en vez de indicarse exactamente
cuáles son se coloque en su lugar un parámetro –parámetro tipo- que se concretará en
el momento en que se vayan a usar (al crear un objeto de la clase, llamar al método,…) A
estas definiciones se les llama genéricos, y un ejemplo de una de ellas es el siguiente:

public class A<T>


{
T valor;
public void EstablecerValor(T valor)
{
this.valor = valor;

167
Clases en Microsoft Visual C#

}
}

En esta clase no se han concretando ni el tipo del campo privado valor ni el del único
parámetro del método EstablecerValor() En su lugar se le especificado un parámetro tipo
T que se concretará al utilizar la clase. Por ejemplo, al crear un objeto suyo:

A<int> obj = new A<int>();

Esto crearía un objeto de la clase genérica A con el parámetro tipo T concretizado con
el argumento tipo int. La primera vez que el CLR encuentre esta concretización de T
a int realizará un proceso de expansión o instanciación del genérico consistente en
generar una nueva clase con el resultado de sustituir en la definición genérica toda
aparición de los parámetros tipos por los argumentos tipo. Para el ejemplo anterior esta
clase sería:

public class A<int>


{
int valor;
public void EstablecerValor(int valor)
{
this.valor = valor;
}
}

A los tipos con parámetros tipo, como A<T>, se les llama tipos genéricos cerrados;
a los generados al concretárseles algún parámetro tipo se le llama tipos construidos; y
a los generados al concretárseles todos tipos genéricos abiertos. La relación
establecida entre ellos es similar a la establecida entre las clases normales y los objetos: al
igual que clases sirven de plantillas en base a las que crear objetos, los tipos genéricos
cerrados actúan como plantillas en base a las que crear tipos genéricos abiertos. Por eso,
en el C++ tradicional se llamaba plantillas a las construcciones equivalentes a los
genéricos.

La expansión la hace el CLR en tiempo de ejecución, a diferencia de lo que sucede en


otros entornos (pe, C++) en los que se realiza al compilar. Esto tiene varias ventajas:

 Ensamblados más pequeños: Como sólo almacenan el tipo genérico cerrado, que el
CLR ya expandirá en tiempo de ejecución, su tamaño es más pequeño y se evita el
problema del excesivo inflado del código binario generado (code bloat)

Además, para evitar el inflado de la memoria consumida, el CLR reutiliza gran parte del
MSIL generado para la primera expansión de un genérico por un tipo referencia en las

168
Clases en Microsoft Visual C#

siguientes expansiones del mismo por otros tipos referencia, ya que todas las referencias
son al fin y al cabo punteros que en memoria se representan igual.

 Metadatos ricos: Al almacenarse los tipos genéricos cerrados en los ensamblados, se


podrán consultar mediante reflexión y ser aprovechados por herramientas como el
IntelliSense de Visual Studio.NET para proporcionar ayuda sobre su estructura.

 Implementación fácil: Como es el propio CLR quien realiza gran parte del trabajo
necesario para dar soporte a los genéricos, la inclusión de los mismos en cualquiera de los
lenguajes .NET se simplifica considerablemente.

Usos de los genéricos

Los genéricos no son una novedad introducida por C# en el mundo de la programación,


sino que otros lenguajes como Ada, Eiffel o C++ (plantillas) ya las incluyen desde hace
tiempo. Su principal utilidad es, como su propio nombre indica, facilitar la creación de
código genérico que pueda trabajar con datos de cualquier tipo. Esto es especialmente útil
para crear tipos que actúen como colecciones (pilas, colas, listas, etc.), cosa que C# 1.X
sólo permitía crear definiéndolos en base a la clase base común object. Por ejemplo, una
cola que admitiese objetos de cualquier tipo había que declararla como sigue:

public class Cola


{
object[] elementos;
public int NúmeroElementos;

public void Encolar(object valor);


{…}
public object Desencolar()
{…}
}

El primer problema de esta solución es lo incómoda y proclive a errores que resulta su


utilización, pues a la hora de extraer valores de la cola habrá que convertirlos a su tipo
real si se quieren aprovechar sus miembros específicos. Es decir:

Cola miCola = new Cola();


miCola.Encolar("Esto es una prueba");
string valorDesencolado = (string) miCola.Desencolar();

Aparte de que el programador tenga que escribir (string) cada vez que quiera convertir
alguna de las cadenas extraídas de miCola a su tipo concreto, ¿qué ocurrirá si por error
introduce un valor que no es ni string ni de un tipo convertible a string (por ejemplo,

169
Clases en Microsoft Visual C#

unint) y al extraerlo sigue solicitando su conversión a string? Pues que el compilador no


se dará cuenta de nada y en tiempo de ejecución saltará una InvalidCastException.

Para resolver esto podría pensarse en derivar un tipo ColaString de Cola cuyos
métodos públicos trabajasen directamente con cadenas de textos (Encolar(string valor) y
string Desencolar()) Sin embargo, no es una solución fácil de reutilizar ya que para
cualquier otro tipo de elementos (pe, una cola de ints) habría que derivar una nueva clase
de Cola.

Otro problema de ambas soluciones es su bajo rendimiento, puesto que cada vez que
se almacene un objeto de un tipo referencia en la cola habrá que convertir su referencia a
una referencia a object y al extraerlo habrá que volverla a transformar en una referencia
a string. ¡Y para los tipos valor todavía es peor!, en tanto que habrá que realizar boxing y
unboxing, procesos que son mucho más lentos que las conversiones de referencias.

Si por el contrario se hubiese definido la cola utilizando genéricos tal y como sigue:

public class Cola<T>


{
T[] elementos;
public int NúmeroElementos;
public void Encolar(T valor)
{…}
public T Desencolar()
{…}
}

Entonces la extracción de objetos de la cola no requeriría de ningún tipo de conversión


y sería tan cómoda y clara como sigue:

Cola<string> miCola = new Cola<string>();


miCola.Encolar("Esto es una prueba");
string valorDesencolado = miCola.Desencolar();

Si ahora por equivocación el programador solicitase almacenar un objeto cuyo tipo no


fuese ni string ni convertible a él, obtendría un error al compilar informándole de ello y
evitando que el fallo pueda llegar al entorno de ejecución. Además, el rendimiento del
código es muy superior ya que no requerirá conversiones de referencias a/desde object.
Si realiza pruebas podrá comprobar que la utilización de genéricos ofrece mejoras en el
rendimiento entorno al 20% para los tipos referencia, ¡y al 200% para los tipos valor!

Sintaxis

170
Clases en Microsoft Visual C#

El CLR de .NET 2.0 permite definir genéricamente tanto clases como estructuras,
interfaces, delegados y métodos. Para ello basta con indicar tras el identificador de las
mismas su lista de sus parámetros genéricos entre símbolos < y > separados por comas.
Con ello, dentro de su definición (miembros de las clases, cuerpos de los métodos, etc.) se
podrá usar libremente esos parámetros en cualquier sitio en que se espere un nombre de
un tipo. La siguiente tabla muestra un ejemplo para cada tipo de construcción válida:

Ejemplo declaración Ejemplo uso

public class Nodo<T>


{ class Nodo8BitAvanzado: Nodo<byte>
public T Dato;
public Nodo<T> Siguiente; {...}
}

public struct Pareja<T,U> Pareja<int, string> miPareja;

{ miPareja.Valor1 = 1;

public T Valor1; miPareja.Valor2 = "Hola";

public U Valor2;

interface IComparable<T> class A: IComparable<Persona>

{ {

int CompararCon(T otroValor);

} public int
CompararCon(Persona persona)

{...}

delegate void Tratar<T>(T valor); Tratar<int> objTratar =


new Tratar<int>(F);

171
Clases en Microsoft Visual C#

public void F(int x)

{...}

void intercambiar<T>(ref T valor1, decimal d1 = 0, d2 = 1;

ref T valor2)

{ this.intercambiar<decimal>
(ref d1, ref d2);

T temp = valor1;
this.intercambiar(ref d1, ref d2);
valor1 = valor2;

valor2 = temp;

Tabla 19: Ejemplos de declaración de tipos y miembros genéricos

Nótese que todos los ejemplos de nombres de parámetros genéricos hasta ahora vistos
son única letra mayúsculas (T, U, etc.) Aunque obviamente no es obligatorio, sino que se
les puede dar cualquier identificador válido, es el convenio de nomenclatura utilizado en la
BCL del .NET Framework 2.0 y el que por tanto se recomienda seguir. Lo que sí es
obligatorio es no darles nunca un nombre que coincida con el del tipo o miembro al que
estén asociados o con el de alguno de los miembros de éste.

Fíjese además que la segunda llamada del ejemplo de utilización del método genérico
intercambiar() no explicita el tipo del parámetro genérico. Esto se debe a que C# puede
realizar inferencia de tipos y deducir que, como para todos parámetros del tipo T se ha
especificado un valor decimal, T debe concretarse como decimal.

Limitaciones

En principio, dentro de los tipos genéricos se puede declarar cualquier miembro que se
pueda declarar dentro de un tipo normal, aunque existe una limitación: no se pueden
declarar puntos de entrada (métodos Main())ya que a éstos los llama el CLR al iniciar la
ejecución del programa y no habría posibilidad de concretizarles los argumentos tipo.

172
Clases en Microsoft Visual C#

Por su parte, los parámetros tipo se pueden usar en cualquier sitio en que se espere
un tipo aunque también con ciertas limitaciones: No pueden usarse en
atributos, alias, punteros o métodos externos, ni en los nombres de las clases
bases o de las interfaces implementadas. Sin embargo, excepto en el caso de los
punteros, en estos sitios sí que se pueden especificar tipos genéricos cerrados; e incluso
en los nombres de las clases o de las interfaces base, también se pueden usar tipos
genéricos abiertos. Por ejemplo:

// class A<T>: T {} // Error. No se puede usar T como interfaz


// o clase base
class A<T> {}
interface I1<V> {}
class B<T>: A<T>, I1<string> {} // OK.

Debe tenerse cuidado al definir miembros con parámetros tipos ya que ello puede dar
lugar a sutiles ambigüedades tal y como la que se tendría en el siguiente ejemplo:

class A<T>
{
void F(int x, string s) {}
void F(T x, string s) {}
}

Si se solicitase la expansión A<int>, el tipo construido resultante acabaría teniendo dos


métodos de idéntica signatura. Aunque en principio, el compilador de C# 2.0 debería de
asegurar que tras cualquier expansión siempre se generen tipos genéricos abiertos
válidos, produciendo errores de compilación en caso contrario, por el momento no lo
hace y deja compilar clases como la anterior. Sencillamente, si llamamos al método F() de
un objeto A<int>, la versión del método que se ejecutará es la primera. Es decir, en las
sobrecargas los parámetros tipo tienen menos prioridad que los concretos.

Donde no resulta conveniente controlar que los parámetros genéricos puedan dar lugar
a métodos no válidos es en las redefiniciones de operadores de conversión, en concreto
en lo referente al control de que las expansiones puedan dar lugar a redefiniciones de las
conversiones predefinidas (como las de a/desde object), puesto que si se hiciese los
parámetros tipo nunca se podrían utilizar en las conversiones. Por ello, simplemente
se ignoran las conversiones a medida que al expandirse puedan entrar en
conflicto con las predefinidas. Por ejemplo, dado el siguiente código:

using System;
public class ConversionGenerica<T>
{

173
Clases en Microsoft Visual C#

public static implicit operator T(ConversionGenerica<T> objeto)


{
T obj = default(T);
Console.WriteLine("Operador de conversión implícita");
return obj;
}
}
public class Principal
{
public static void Main()
{
ConversionGenerica<Principal> objeto1=
new ConversionGenerica<Principal>();
Principal objeto2 = objeto1; // Conversión no predefinida (a Principal)
Console.WriteLine("Antes de conversión no predefinida");
ConversionGenerica<object> objeto3 = new ConversionGenerica<object>();
object objeto4 = objeto3; // Conversión predefinida (a object)
}
}

El resultado de su ejecución demuestra que la versión implícita de la conversión solo se


ejecuta ante la conversión no predefinida, puesto que su salida es la siguiente:

Operador de conversión implícita

Antes de conversión no predefinida

Donde nunca habrá ambigüedades es ante clases como la siguiente, ya que sea cual
sea el tipo por el que se concretice T siempre será uno y por tanto nunca se podrá dar el
caso de que la segunda versión de F() coincida en signatura con la primera:

class A<T>
{
void F(int x, string s) {}
void F(T x, T s) {}
}

Así mismo, también se admitirían dos métodos como los siguientes, ya que se
considera que los parámetros tipo forman parte de las signaturas:

class A
{
void F<T>(int x, string s) {}

174
Clases en Microsoft Visual C#

void F(int x, string s) {}


}

Y al redefinir métodos habrá que mantener el mismo número de parámetros tipo en la


clase hija que en la clase padre para así conservar la signatura. Esto, como en el caso de
los parámetros normales, no implica mantener los nombres de los parámetros tipo, sino
sólo su número. Por tanto, códigos como el siguiente serán perfectamente válidos:

public class A
{
protected virtual void F<T, U>(T parámetro1, U parámetro2)
{}
}
public class B:A
{
protected override void F<X, Y>(X parámetro1, Y parámetro2)
{}
}

Restricciones

Probablemente a estas alturas ya esté pensado que si en tiempo de diseño no conoce


el tipo concreto de los parámetros genéricos, ¿cómo podrá escribir código que los use y
funcione independientemente de su concretización? Es decir, dado un método como:

T Opera<T>(T valor1, T valor2)


{
int resultadoComparación = valor1.CompareTo(valor2);
if (resultadoComparación>0)
return valor1-valor2 ;
else if(resultadoComparación<0)
return Math.Pow(valor1, valor2) ;
else
return 2.0d;
}

Si se le llamase con Opera(2.0d, 3.2d) no habría problema, pues los


objetos double tanto cuentan con un método CompareTo()válido, como tienen definida
la operación de resta, se admiten como parámetros del método Pow() de la clase Math y
van a generar valores de retorno de su mismo tipo double. Pero, ¿y si se le llamase con
Opera("hola", "adiós")? En ese caso, aunque los objetosstring también cuentan con un
método CompareTo() válido, no admiten la operación de resta, no se puede pasar como
parámetros a Pow() y el valor que devolvería return 2.0d; no coincidiría con el tipo de
retorno del método.

175
Clases en Microsoft Visual C#

Esta inconsistencias son fáciles de solucionar en entornos donde la expansión se realiza


en tiempo de compilación, pues el compilador informa de ellas. Sin embargo, en los que
se hace en tiempo de ejecución serían más graves ya que durante la compilación pasarían
desapercibidas y en tiempo de ejecución podrían causar errores difíciles de detectar. Por
ello, C# en principio sólo permite que con los objetos de tipos genéricos se realicen las
operaciones genéricas a cualquier tipo: las de object. Por ejemplo, el código que sigue
sería perfectamente válido ya que tan sólo utiliza miembros de object:

public static bool RepresentacionesEnCadenaIguales<T,U>


(T objeto1, U objeto2)
{
return objeto1.ToString() == objeto2.ToString();
}

Obviamente, también compilará un código en el que los parámetros genéricos con los
que se realicen operaciones no comunes a todos los objects se conviertan antes a tipos
concretos que sí las admitan, aunque entonces si se le pasasen argumentos de tipos no
válidos para esa conversión saltaría una InvalidCastException en tiempo de ejecución.
Por ejemplo, el siguiente método compilará pero la ejecución fallará en tiempo de
ejecución cuando se le pasen parámetros no convertibles a int.

public static int Suma<T,U>(T valor1, U valor2)


{
return ((int) (object) valor1) + ((int) (object) valor2);
}

Nótese que por seguridad ni siquiera se permite la conversión directa de un parámetro


tipo a cualquier otro tipo que no sea object y ha sido necesario hacerla indirectamente.

Lógicamente, C# ofrece mecanismos con los que crear códigos genéricos que a la vez
sean seguros y flexibles para realizar otras operaciones aparte de las que válidas para
cualquier object. Estos mecanismos consisten en definir restricciones en el abanico de
argumentos tipo válidos para cada parámetro tipo, de modo que así se puedan realizar
con él las operaciones válidas para cualquier objeto de dicho subconjunto de tipos.

Las restricciones se especifican con


cláusulas where <parámetroGenérico>:<restricciones> tras la lista de parámetros de los
métodos y delegados genéricos, o tras el identificador de las clases, estructuras e
interfaces genéricas; donde <restricciones> pueden ser:

 Restricciones de clase base: Indican que los tipos asignados al parámetro genérico
deben derivar, ya sea directa o indirectamente, del indicado en <restricciones>. Así, en el
código genérico se podrán realizar con seguridad todas las operaciones válidas para los

176
Clases en Microsoft Visual C#

objetos dicho tipo padre, incluidas cosas como lanzarlos con sentencias throw o
capturarlos en bloques catch si ese padre deriva de Exception. Por ejemplo:

using System;
class A
{
public int Valor;
}
class B:A
{
public static int IncrementarValor<T>(T objeto) where T:A
{
return ++objeto.Valor;
}
static void Main()
{
Console.WriteLine(B.IncrementarValor(new B())); // Imprime 1
Console.WriteLine(B.IncrementarValor(new A())); // Imprime 1
// Console.WriteLine(B.IncrementarValor(new C())); // Error
}
}
class C {}

Esta restricción además permite asegurar que el argumento tipo siempre será un
tipo referencia, por lo que con podrá utilizar en los contextos en que sólo sean válidos
tipos referencia, como por ejemplo con el operador as. Así mismo, también permite
realizar conversiones directas del parámetro tipo a su tipo base sin tenerse que pasar
antes por la conversión a objectantes vista.

 Restricciones de interfaz: Son similares a las anteriores, sólo que en este caso lo que
indican es que los tipos que se asignen al parámetro tipo deben implementar las interfaces
que, separadas mediante comas, se especifiquen en <restricciones> Así se podrán usar en
todos aquellos contextos en los que se esperen objetos que las implementen, como por
ejemplo en sentenciasusing si implementan IDisposable.
 Restricciones de constructor: Indican que los tipos por los que se sustituya el
parámetro genérico deberán disponer de un constructor público sin parámetros. Se
declaran especificando new() en <restricciones>, y sin ellas no se permite instanciar
objetos del parámetro tipo dentro de la definición genérico. Es decir:

// Correcto, pues se indica que el tipo por el que se concretice T


// deberá de tener un constructor sin parámetros
class A<T> where T:new()
{
public T CrearObjeto()
{

177
Clases en Microsoft Visual C#

return new T();


}
}
/* Incorrecto, ya que ante el new T() no se sabe si el tipo por el
que se concretice T tendrá un constructor sin parámetros
class B<T>
{
public T CrearObjeto()
{
return new T();
}
}
*/

Cada genérico puede tener sólo una cláusula where por parámetro genérico, aunque en
ella se pueden mezclar restricciones de los tres tipos separadas por comas. Si se hace,
éstas han de aparecer en el orden en que se han citado: la de clase base primero y la de
constructor la última. Por ejemplo, el tipo por el que se sustituya el parámetro genérico
del siguiente método deberá derivar de la clase A, implementar las interfaces I1 e I2 y
tener constructor público sin parámetros:

void MiMétodo<T> (T objeto) where T: A, I1, I2, new()


{...}

Las restricciones no se heredan, por lo que si de una clase genérica con


restricciones se deriva otra clase genérica cuyos parámetros tipo se usen como
parámetros de la clase padre, también habrán de incluirse en ella dichas restricciones para
asegurase de que cualquier argumento tipo que se le pase sea válido. Igualmente, en las
redefiniciones habrá que mantener las restricciones específicas de cada método. O sea:

class A {}
class B<T> where T:A
{}
/* No válido, T debe ser de tipo A para poder ser pasado como argumento
tipo de B, y dicha restricción no la hereda automáticamente el T de C.

class C<T>:B<T>
{}
*/
class C<T>:B<T> where T:A // Válido
{}

Nótese que todas estas restricciones se basan en asegurar que los argumentos tipo
tengan determinadas características (un constructor sin parámetros o ciertos miembros

178
Clases en Microsoft Visual C#

heredados o implementados), pero no en las propias características de esos tipos. Por lo


tanto, a través de parámetros tipo no podrá llamarse a miembros estáticos ya
que no hay forma de restringir los miembros estáticos que podrán tener los argumentos
tipo.

Finalmente, cabe señalar que en realidad también existe una cuarta forma de
restringir el abanico de argumentos tipos de un tipo genérico, que además es mucho más
flexible que las anteriores y permite aplicar cualquier lógica para la comprobación de los
tipos. Consiste en incluir en el constructor estático del tipo genérico código que lance
alguna excepción cuando los argumentos tipo especificados no sean válidos, abortándose
así la expansión. Por ejemplo, un tipo genérico C<T> que sólo admita como argumentos
tipos objetos de las clases A o B podría crearse como sigue:

class C<T>
{
static C()
{
if ( !(typeof(T) == typeof(A)) && !(typeof(T) == typeof(B)))
throw new ArgumentException("El argumento tipo para T debe ser A o B");
}
}

O si se quisiese que también los admitiese de cualquier clase derivada de éstas, podría
aprovecharse como sigue el método bool IsAssignableForm(Type tipo) de los
objetos Type, que indica si el objeto al que se aplica representa a un tipo al que se le
pueden asignar objetos del tipo cuyo objeto System.Type se le indica como parámetro:

class C<T>
{
static C()
{
if ( !(typeof(B).IsAssignableFrom(typeof(T))) &&
!(typeof(A).IsAssignableFrom(typeof(T))))
throw new ArgumentException("El argumento tipo para T debe ser A o B");
}
}

El único problema de esta forma de restringir el abanico de tipos es que desde el


código del tipo genérico no se podrá acceder directamente a los miembros específicos del
tipo argumento, sino que antes habrá que convertirlos explícitamente a su tipo. Por
ejemplo:

179
Clases en Microsoft Visual C#

using System;
public class A
{
public void Método()
{
Console.WriteLine("Ejecutado Método() de objeto A");
}
}
public class B
{
public void Método()
{
Console.WriteLine("Ejecutado Método() de objeto B");
}
}
class C<T>
{
static C()
{
if ( !(typeof(T) == typeof(A)) && !(typeof(T) == typeof(B)))
throw new ArgumentException("El argumento tipo para T debe
ser A o B");
}
public void LlamarAMétodo(T objeto)
{
if (objeto is A)
(objeto as A).Método(); // Hay que hacer conversión explícita
else
(objeto as B).Método(); // Hay que hacer conversión explícita
}
}
class Principal
{
static void Main()
{
C<A> objetoCA = new C<A>();
objetoCA.LlamarAMétodo(new A());
C<B> objetoCB = new C<B>();
objetoCB.LlamarAMétodo(new B());
}
}

Valores por defecto

Cuando se trabaja con tipos genéricos puede interesar asignarles el valor por defecto
de su tipo, pero… ¿cuál será éste? Es decir, si el parámetro genérico se sustituye por un
tipo referencia, el valor por defecto de sus objetos sería null y valdrían códigos como:

180
Clases en Microsoft Visual C#

void F<T>()
{
T objeto = null ;
}

Sin embargo, si se sustituyese por un tipo valor no sería válido, por lo que este tipo de
asignaciones nunca se admitirán salvo que haya establecido una restricción de clase base
que asegure que el argumento tipo sea siempre un tipo referencia.

No obstante, C# 2.0 permite representar el valor por defecto de cualquier parámetro


tipo con la sintaxisdefault(<parámetroTipo>), lo que dejaría al ejemplo anterior como
sigue:

void F<T>()
{
T objeto = default(T);
}

En cada expansión, el CLR sustituirá la expresión default(T) por el valor por defecto del
tipo que se concrete para T (0, null,false,...), dando lugar a métodos como:

Expansión para int Expansión para string Expansión para bool

void F<int>() void F<string>() void F<bool>()


{ { {
int objeto =0; int objeto = null; int objeto = false;
} } }

Nótese pues que para comprobar si un cierto argumento tipo es un tipo valor o un tipo
referencia bastará con comprobar si sudefault devuelve null. Así por ejemplo, para crear
un tipo genérico que sólo admita tipos referencia se podría hacer:

class GenéricoSóloReferencias<T>
{
static GenéricoSóloReferencias()
{
if (default(T)!=null)
throw new ArgumentException("T debe ser un tipo referencia");
}
}

181
Clases en Microsoft Visual C#

Por su parte, y a diferencia de lo que ocurre con el operador de asignación, el operador


de igualdad (==) sí que puede aplicarse entre instancias de parámetros tipo y null, aún
cuando estos almacenen tipos valor y dicha comparación no sea válida. En dichos casos la
comparación simplemente retornaría false. Igualmente, también se les podrá aplicar el
operador de desigualdad (!=), que siempre devolverá true para los tipo valor.

Ambigüedades

El que los delimitadores de los argumentos tipo sean los mismos que los operadores de
comparación "mayor que" y "menor que" y utilicen como el mismo carácter separador que
se usa para separar los argumentos de las llamadas a métodos (la coma ,) puede dar
lugar a ambigüedades. Por ejemplo, una llamada como la siguiente:

F(G<A,B>(7));

Podría interpretarse de dos formas: Como una llamada a un método F() con el
resultado de evaluar G<A como primer argumento y el resultado de B>(7) como segundo,
o como una llamada a F() con el resultado de una llamada G<A,B>(7) a un método
genérico G()

Lo que hace el compilador es interpretar como llamadas a métodos genéricos cualquier


aparición del carácter > donde a este le sigan alguno de estos caracteres: (, ), ], >, ;, :,
,?, . ó ,. En el resto de casos serán tratados como operadores de comparación. Por tanto,
en el ejemplo anterior la expresión será interpretada como una llamada a un método
genérico, y para que se interpretase de la otra forma habría rescribirse como F(G<A,B>7);

Tipos parciales
C# 2.0 da la posibilidad de distribuir las definiciones de los tipos en múltiples ficheros
tales que si al compilar se pasan todos juntos al compilador, éste automáticamente los
fusionará en memoria y generará el MSIL correspondiente a su unión. A estos tipos se les
conoce como tipos parciales y pueden ser definiciones tanto de clases como
de estructuras e interfaces, pero no de enumeraciones o delegados ya que estas
suelen ser definiciones tan sencillas que dividirlas resultaría muy poco o nada útil.

Los tipos parciales facilitan separar los códigos obtenidos automáticamente


mediante generadores de código (como VS.NET 2005) de las modificaciones se les
realicen a mano tras su generación, pues permiten regenerarlos en cualquier momento sin
que con ello se pierdan dichos cambios. Además, también son útiles para agilizar el
desarrollo y mantenimiento de las tipos de datos, pues permiten que varias personas
puedan estar trabajando simultáneamente en diferentes secciones de un mismo tipo de
dato.

Para definir un tipo parcial simplemente con basta declararlo varias veces en el mismo
o en diferentes ficheros, añadiéndoles un nuevo modificador partial y manteniendo

182
Clases en Microsoft Visual C#

siempre el mismo nombre completo, parámetros tipo, modificadores de visibilidad y clase


base. Las restricciones de los parámetros tipos no tienen porqué aparecer en todas las
partes, pero si lo hace deben ser siempre las mismas. Por ejemplo, en un fichero
clientes.cs se podría tener:

interface I1
{
void F();
}
[MiAtributo1] [MiAtributo2("Test1")]
public partial class Clientes
{
public int X;
}

Y en otro fichero llamado clientes-ampliación.cs:

public partial class Clientes: I1


{}
interface I2
{
void G();
}
[MiAtributo2("Test2")]
public partial class Clientes: I2
{
void I1.F()
{}
public void G()
{}
}

Si al compilar se especifican ambas ambos ficheros simultáneamente, como en:

csc /t:library clientes.cs clientes-ampliación.cs

El compilador considerará que la definición de la clase Clientes es la siguiente:

[MiAtributo1, MiAtributo2("Test1"), MiAtributo2("Test2")]


public class Clientes: I1, I2
{
public int X;
void I1.F()
{}

183
Clases en Microsoft Visual C#

public void G()


{}
}

Nótese que en una parte de una definición parcial se podrán referenciar identificadores
(miembros, interfaces implementadas explícitamente, etc.) no declarados en ella sino en
otras partes de la definición, puesto que mientras al compilar se le pasen al compilador las
partes donde están definidos, la fusión producirá una clase válida. No obstante, hay que
señalar que esto tiene pequeñas algunas limitaciones y no es aplicable a:

 Modificador unsafe: Por seguridad, aplicarlo a la definición de una parte de un tipo no


implicará que se considere aplicado al resto y puedan utilizarse en ellas punteros, sino que
deberá de aplicarse por separado a cada parte en la que se vaya a hacer uso de
características inseguras.
 Directiva using: En cada parte de una definición parcial se puede utilizar diferentes
directivas using que importen diferentes espacios de nombres y definan diferentes alias.
Al analizar cada parte, el compilador tan sólo interpretará las directivas usingespecificadas
en ella, admitiéndose incluso que en varias partes se definan alias con el mismo nombre
para clases o espacios de nombres diferentes.

El uso de tipos parciales no obstante introduce el problema de que el mantenimiento de


código particionados puede ser más complicado al estar distribuida su implementación a lo
largo de múltiples ficheros. Sin embargo, herramientas como la Vista de Clases de
VS.NET 2005 solucionan esto proporcionando una vista unificada de la estructura de estos
tipos con el resultado de la fusión de todas sus partes.

Iteradores
Aprovechando las ventajas proporcionadas por los genéricos, en la BCL del .NET 2.0 se
ha optimizado la implementación de las colecciones introduciendo en un nuevo espacio de
nombres System.Collections.Generic dos nuevas interfaces
llamadasIEnumerable<T> e IEnumerator<T> que las colecciones podrán
implementar para conseguir que el acceso a las colecciones sea mucho más
eficiente que con las viejas IEnumerable e IEnumerator al evitarse el
boxing/unboxing o downcasting/upcasting que el tipo de retorno object de la
propiedad Current de IEnumerator implicaba. Además, en ellas se ha optimizado el
diseño eliminando el tan poco utilizado método Reset() y haciéndoles implementar en su
lugar la estandarizada interfaz IDisposable. En resumen, están definidas como sigue:

public interface IEnumerable<T>


{
IEnumerator<T> GetEnumerator();
}

public interface IEnumerator<T>: IDisposable


{

184
Clases en Microsoft Visual C#

T Current { get; }
bool MoveNext();
}

Nótese que en realidad C# 1.0 ya proporcionaba a través del patrón de colección un


mecanismo con que implementar colecciones fuertemente tipadas. Sin embargo, era algo
específico de este lenguaje, oculto y desconocido para muchos programadores y no
completamente soportado por otros lenguajes .NET (por ejemplo, Visual Basic.NET y su
sentencia For Each) Por el contrario, estas nuevas interfaces genéricas proporcionan
una solución más compatible y mejor formulada.

C# 2.0 proporciona además un nuevo mecanismo con el que es resultará mucho


sencillo crear colecciones que implementando directamente estas interfaces:
los iteradores. Son métodos que para generar objetos que implementen todas estas
interfaces se apoyan en una nueva sentencia yield. Han de tener como tipo de
retorno IEnumerable, IEnumerator, IEnumerable<T> eIEnumerator<T>, y su
cuerpo indicarán los valores a recorrer con sentencias yield return <valor>; en las que si
se el tipo de retorno del iterador es genérico, <valor> deberá ser de tipo T. Asimismo,
también se marcar el fin de la enumeración y forzar a queMoveNext() siempre
devuelva false mediante sentencias yield break;.

A partir de la definición de un iterador, el compilador anidará dentro de la clase en que


éste se definió una clase privada que implementará la interfaz de su tipo de retorno. El
código del iterador no se ejecutará al llamarle, sino en cada llamada realizada al
método MoveNext() del objeto que devuelve, y sólo hasta llegarse a algún yield return.
Luego la ejecución se suspenderá hasta la siguiente llamada a MoveNext(), que la
reanudará por donde se quedó. Por tanto, sucesivas llamadas a MoveNext() irán
devolviendo los valores indicados por los yield return en el mismo orden en que se éstos
se ejecuten. Así, en:

using System;
using System.Collections;

class PruebaIteradores
{
public IEnumerator GetEnumerator()
{
yield return 1;
yield return "Hola";
}

public IEnumerable AlRevés


{
get
{

185
Clases en Microsoft Visual C#

yield return "Hola";


yield return 1;
}
}

static void Main()


{
PruebaIteradores objeto = new PruebaIteradores();
foreach(object valor in objeto)
Console.WriteLine(valor);

foreach(object x in objeto.AlRevés)
Console.WriteLine(x);
}
}

La clase PruebaIteradores tendrán un método GetEnumerator() que devolverá un


objeto IEnumerator generado en base a las instrucciones yield que retornará como
primer elemento un 1 y como segundo la cadena "Hola", por lo que podrá ser recorrida a
través de la instrucción foreach. Del mismo modo, su propiedad AlRevés devolverá un
objeto que también dispone de dicho método y por tanto también podrá recorrese a
través de dicha sentencia, aunque en este caso hace el recorrido al revés. Por tanto, su
salida será:

Hola

Hola

Nótese que aunque los iteradores se puedan usar para definir métodos que devuelvan
tanto objetos IEnumerable comoIEnumerator, ello no significa que dichos tipos sean
intercambiables. Es decir, si en el ejemplo anterior hubiésemos definido la propiedad
AlRevés como de tipo IEnumerator, el código no compilaría en tanto que lo
que foreach espera es un objeto que implemente IEnumerable, y no un IEnumerator.

En realidad, si el tipo de retorno del iterador es IEnumerator o IEnumerator<T>, la


clase interna que se generará implementará tanto la versión genérica de esta interfaz
como la que no lo es. Lo mismo ocurre para el caso
de IEnumerable e IEnumerable<T>,en el que además, el compilador opcionalmente
(el de C# de Microsoft sí lo hace) también podrá implementar las
interfacesIEnumerator e IEnumerator<T>. Sin embargo, implementar las
interfaces IEnumerator no implica que se implementen lasIEnumerable, y de ahí el
problema descrito en el párrafo anterior.

186
Clases en Microsoft Visual C#

Por otro lado, hay que señalar que en el código que se puede incluir dentro de los
iteradores existen algunas limitacionesdestinadas a evitar rupturas descontroladas de la
iteración: no se permiten sentencias return, ni código inseguro ni parámetros por
referencia, y las sentencias yield no se puede usar dentro de
bloques try, catch o finally.

Internamente, a partir de la definición del iterador el compilador generará una clase


interna que implementará la interfaz del tipo de retorno de éste y sustituirá el código del
miembro de la colección donde se define el iterador por una instanciación de dicha clase a
la que le pasará como parámetro el propio objeto colección a recorrer (this) En ella, el
código del iterador se transformará en una implementación
del MoveNext() de IEnumerator basada en un algoritmo de máquina de estado, y la
posible limpieza de los recursos consumido por la misma se hará en el Dispose() O sea,
se generará algo como:

public class <colección>:<interfaz>


{
public virtual <interfaz> GetEnumerator()
{
return GetEnumerator$<númeroAleatorioÚnico>__IEnumeratorImpl impl =
new GetEnumerator$<númeroAleatorioÚnico>__IEnumeratorImpl(this);
}

class GetEnumerator$<númeroAleatorioÚnico>__IEnumeratorImpl:<interfaz>
{
public <colección> @this;
<tipoElementosColección> $_current;
string <interfaz>.Current { get { return $_current; } }

bool <interfaz>.MoveNext()
{
// Implementación de la máquina de estados
}

void IDisposable.Dispose()
{
// Posible limpieza de la máquina de estados
}
}
}

Nótese que para cada foreach se generará un nuevo objeto de la clase interna, por lo
que el estado de estos iteradores automáticamente generados no se comparte
entre foreachs y por tanto el antiguo método Reset() de IEnumerator se vuelve
innecesario. Es más, si la interfaz de retorno del iterador fuese IEnumerator, la
implementación realizada por el compilador en la clase interna para el obsoleto método de
la misma Reset() lanzará una NotSupportedException ante cualquier llamada que
explícitamente se le realice. No obstante, hay que tener cuidado con esto pues puede

187
Clases en Microsoft Visual C#

implicar la creación de numerosos objetos en implementaciones recursivas como la


siguiente, en la que se aprovechan los iteradores para simplificar la implementación de los
recorridos sobre un árbol binario:

using System.Collections.Generic;

public class Nodo<T>


{
public Nodo<T> Izquierdo, Derecho;
public T Valor;
}

public class ÁrbolBinario<T>


{
Nodo<T> raíz;
public IEnumerable<T> Inorden{get{return recorrerEnInorden(this.raíz); } }
IEnumerable<T> recorrerEnInorden(Nodo<T> nodo)
{
Nodo<T> nodoIzquierdo = nodo.Izquierdo;
if (nodoIzquierdo!=null)
foreach(T valor in recorrerEnInorden(nodoIzquierdo))
yield return valor;

yield return raíz.Valor;


Nodo<T> nodoDerecho= nodo.Derecho;
if (nodoDerecho!=null)
foreach(T valor in recorrerEnInorden(nodoDerecho))
yield return valor;
}

// Implementación de recorridos en Preorden y PostOrden y demás miembros


}

Mejoras en la manipulación de delegados


Inferencia de delegados

Mientras que en C# 1.X siempre era necesario indicar explícitamente el delegado del
objeto o evento al que añadir cada método utilizando el operador new, como en:

miObjeto.miEvento += new MiDelegado(miCódigoRespuesta);

En C# 2.0 el compilador es capaz de inferirlo automáticamente de la definición del


delegado en que se desea almacenar. Así, para el ejemplo anterior bastaría con escribir:

188
Clases en Microsoft Visual C#

miObjeto.miEvento += miCódigoRespuesta;

Sin embargo, asignaciones como la siguiente en la que el método no es asociado a un


delegado no permiten la deducción automática del mismo y fallarán al compilar:

object o = miCódigoRespuesta;

Aunque explicitando la conversión a realizar tal y como sigue sí que compilará:

object o = (MiDelegado) miCódigoRespuesta;

En general, la inferencia de delegados funciona en cualquier contexto en que se espere


un objeto delegado. Esto puede observarse en el siguiente ejemplo:

using System;
class A
{
delegate void MiDelegado(string cadena);

public static void Main()


{
// En C# 1.X: MiDelegado delegado = new MiDelegado(miMétodo);
MiDelegado delegado = miMétodo;
// En C# 1.X: delegado.EndInvoke(delegado.BeginInvoke("Hola",
// new AsyncCallback(métodoFin), null));
delegado.EndInvoke(delegado.BeginInvoke("Hola", métodoFin, null));
}

static void miMétodo(string cadena)


{
Console.WriteLine("MiMétodo(string {0})", cadena);
}
static void métodoFin(IAsyncResult datos)
{
//…
}
}

Métodos anónimos

C# 2.0 permite asociar código a los objetos delegados directamente, sin que para ello
el programador tenga que crear métodos únicamente destinados a acoger su cuerpo y no
a, como sería lo apropiado, reutilizar o clarificar funcionalidades. Se les llamamétodos
anónimos, pues al especificarlos no se les da un nombre sino que se sigue la sintaxis:

189
Clases en Microsoft Visual C#

delegate(<parámetros>) {<instrucciones>};

Y será el compilador quien internamente se encargue de declarar métodos con dichos


<parámetros> e <instrucciones> y crear un objeto delgado que los referencie. Estas
<instrucciones> podrán ser cualesquiera excepto yield, y para evitar colisiones con los
nombres de métodos creados por el programador el compilador dará a los métodos en
que internamente las encapsulará nombres que contendrán la subcadena reservada __.
Nótese que con esta sintaxis no se pueden añadir atributos a los métodos
anónimos.

Con métodos anónimos, las asignaciones de métodos a delegados podría compactarse


aún más eliminándoles la declaración explícita del método de respuesta. Por ejemplo:

miObjeto.miEvento += delegate(object parámetro1, int parámetro2)


{ Console.WriteLine ("Evento producido en miObjeto"); };

Incluso si, como es el caso, en el código de un método anónimo no se van a utilizar los
parámetros del delegado al que se asigna, puede omitirse especificarlos (al llamar a los
métodos que almacena a través suya habrá que pasarles valores cualesquiera) Así, la
asignación del ejemplo anterior podría compactarse aún más y dejarla en:

miObjeto.miEvento += delegate {
Console.WriteLine ("Evento producido en miObjeto"); };

En ambos casos, a partir de estas instrucciones el compilador definirá dentro de la clase


en que hayan sido incluidas un método privado similar al siguiente:

public void __AnonymousMethod$00000000(object parámetro1, int parámetro2)


{
Console.WriteLine ("Evento producido en miObjeto");
}

Y tratará la asignación del método anónimo al evento como si fuese:

miObjeto.miEvento += new MiDelegado(this,__AnonymousMethod$00000000);

No obstante, la sintaxis abreviada no se puede usar con delegados con


parámetros out, puesto que al no poderlos referenciar dentro de su cuerpo será imposible
asignarles en el mismo un valor tal y como la semántica de dicho modificador requiere.

190
Clases en Microsoft Visual C#

Fíjese que aunque a través de += es posible almacenar métodos anónimos en un


objeto delegado, al no tener nombre no será posible quitárselos con -= a no ser que
antes se hayan almacenado en otro objeto delegado, como en por ejemplo:

MiDelegado delegado = delegate(object párametro1, int parámetro2)


{ Console.WriteLine ("Evento producido en miObjeto"); };
miObjeto.miEvento += delegado;
miObjeto.miEvento -= delegado;

Los métodos anónimos han de definirse en asignaciones a objetos delegados o eventos


para que el compilador pueda determinar el delegado donde encapsularlo. Por tanto, no
será válido almacenarlos en objects mediante instrucciones del tipo:

object anónimo = delegate(object párametro1, int parámetro2)


{ Console.WriteLine ("Evento producido en miObjeto"); }; // Error

Aunque sí si se especificarse el delegado mediante conversiones explícitas, como en:

object anónimo = (MiDelegado) delegate(object parametro1, int parámetro2)


{ Console.WriteLine ("Evento producido en miObjeto"); };

Los métodos anónimos también pueden ser pasados como parámetros de los métodos
que esperen delegados, como en por ejemplo:

class A
{
delegate void MiDelegado();
public void MiMétodo()
{
LlamarADelegado(delegate() { Console.Write("Hola"); });
}
void LlamarADelegado(MiDelegado delegado)
{
delegado();
}
}

Nótese que si el método aceptase como parámetros objetos del tipo


genérico Delegate, antes de pasarle el método anónimo habría que convertirlo a algún
tipo concreto para que el compilador pudiese deducir la signatura del método a generar, y
el delegado no podría tomar ningún parámetro ni tener valor de retorno. Es decir:

191
Clases en Microsoft Visual C#

class A
{
public void MiMétodo()
{
LlamarADelegado((MiDelegado) delegate { Console.Write("Hola"); });
}

void LlamarADelegado(delegate delegado)


{
MiDelegado objeto = (MiDelegado) delegado;
objeto("LlamarADelegado");
}
}

Captura de variables externas

En los métodos anónimos puede accederse a cualquier elemento visible desde el punto
de su declaración, tanto a las variables locales de los métodos donde se declaren como a
los miembros de sus clases. A dichas variables se les denomina variables externas, y se
dice que los métodos anónimos las capturan ya que almacenarán una referencia a su
valor que mantendrán entre llamadas. Esto puede observarse en el siguiente ejemplo:

using System;
delegate int D(ref VariablesExternas parámetro);
class VariablesExternas
{
public int Valor = 100;
static D F()
{
VariablesExternas o = new VariablesExternas();
int x = 0;
D delegado = delegate(ref VariablesExternas parámetro)
{
if (parámetro==null)
parámetro = o;
else
parámetro.Valor++;
return ++x;
};
x += 2;
o.Valor+=2;
return delegado;
}
static void Main()
{
D d = F();
VariablesExternas objeto = null;
int valor = d(ref objeto);
Console.WriteLine("valor={0}, objeto.Valor={1}", valor, objeto.Valor);
valor = d(ref objeto);
Console.WriteLine("valor={0}, objeto.Valor={1}", valor, objeto.Valor);

192
Clases en Microsoft Visual C#

}
}

Cuya salida es:

valor=3, objeto.Valor=102

valor=4, objeto.Valor=103

Fíjese que aunque las variables x y o son locales al método F(), se mantienen entra las
llamadas que se le realizan a través del objeto delegado d ya que han sido capturadas por
el método anónimo que éste almacena.

A las variables capturadas no se les considera fijas en memoria, por lo que para
poderlas manipular con seguridad en código inseguro habrá que encerrarlas en la
sentencia fixed.

Debe señalarse que la captura de variables externas no funciona con los campos de
las estructuras, ya que dentro de un método anónimo no se puede referenciar
al this de las mismas. Sin embargo, la estructura siempre puede copiarse desde fuera del
método anónimo en una variable local para luego referenciarla en el mismo a través de
dicha copia. Eso sí, debe señalarse que en un campo de la estructura no se podrá realizar
esta copiar ya que ello causaría ciclos en la definición de ésta.

Covarianza y contravarianza de delegados

Los delegados también son más flexibles en C# 2.0 porque sus objetos admiten tanto
métodos que cumplan exactamente con sus definiciones, con valores de retorno y
parámetros de exactamente los mismos tipos indicados en éstas, como métodos que los
tomen de tipos padres de éstos. A esto se le conoce como covarianza para el caso de los
valores de retorno y contravarianza para el de los parámetros. Por ejemplo:

using System;
public delegate Persona DelegadoCumpleaños(Persona persona, int nuevaEdad);
public class Persona
{
public string Nombre;
private int edad;

public int Edad


{
get { return this.edad; }
}
public event DelegadoCumpleaños Cumpleaños;
public Persona(String nombre, int edad)
{
this.Nombre = nombre;
this.edad = edad;

193
Clases en Microsoft Visual C#

public void CumplirAños()


{
Cumpleaños(this, this.edad+1);
this.edad++;
}
}
public class Empleado:Persona
{
public uint Sueldo;

public Empleado(string nombre, int edad, uint sueldo):base(nombre, edad)


{
this.Sueldo = sueldo;
}
}
public class Covarianza
{
static void Main()
{
Empleado josan = new Empleado("Josan", 25, 500000);
josan.Cumpleaños += mostrarAños;
josan.CumplirAños();
}
static Persona mostrarAños(Persona josan, int nuevaEdad)
{
Console.WriteLine("{0} cumplió {1} años", josan.Nombre, nuevaEdad);
return josan;
}
}

Nótese que aunque en el ejemplo el delegado se ha definido para operar con objetos
del tipo Persona, se le han pasado objetos de su subtipo Empleado y devuelve un objeto
también de dicho tipo derivado. Esto es perfectamente seguro ya que en realidad los
objetos del tipo Empleado siempre tendrán los miembros que los objetos del Persona y
por lo tanto cualquier manipulación que se haga de los mismos en el código será
sintácticamente válida. Si lo ejecutamos, la salida que mostrará el código es la siguiente:

Josan cumplió 26 años

Tipos anulables
Concepto

En C# 2.0 las variables de tipos valor también pueden almacenar el valor especial null,
como las de tipos referencia. Por ello, a estas variables se les denomina tipos anulables.

194
Clases en Microsoft Visual C#

Esto les permite señalar cuando almacenan un valor desconocido o inaplicable, lo que
puede resultar muy útil a la hora de trabajar con bases de datos ya que en éstas los
campos de tipo entero, booleanos, etc. suelen permitir almacenar valores nulos. Así
mismo, también evita tener que definir ciertos valores especiales para los parámetros o el
valor de retorno de los métodos con los que expresar dicha semántica (pe, devolver -1 en
un método que devuelva la posición donde se haya un cierto elemento en una tabla para
indicar que no se ha encontrado en la misma), cosa que además puede implicar
desaprovechar parte del rango de representación del tipo valor o incluso no ser posible de
aplicar si todos los valores del parámetro o valor de retorno son significativos.

Sintaxis

La versión anulable de un tipo valor se representa igual que la normal pero con el
sufijo ?, y se le podrán asignar tanto valores de sutipo subyacente (el tipo normal, sin
el ?) como null. De hecho, su valor por defecto será null. Por ejemplo:

int? x = 1;
x = null;

En realidad el uso de ? no es más que una sintaxis abreviada con la que instanciar un
objeto del nuevo tipo genérico Nullable<T>incluido en el espacio de
nombres System de la BCL con su parámetro genérico concretizado al tipo subyacente.
Este tipo tiene un constructor que admite un parámetro del tipo genérico T, por lo que en
realidad el código del ejemplo anterior es equivalente a:

Nullable<int> x = new Nullable<int>(1);// También valdría


// Nullable<int>x = new int?(1);
x = null;

El tipo Nullable proporciona dos propiedades a todos los tipos anulables: bool
HasValue para indicar si almacena null, y T Value, para obtener su valor. Si una
variable anulable valiese null, leer su propiedad Value haría saltar
una InvalidOperationException. A continuación se muestra un ejemplo del uso de las
mismas:

using System;
class Anulables
{
static void Main()
{
int? x = null;
int? y = 123;
MostrarSiNulo(x, "x");
MostrarSiNulo(y, "y");

195
Clases en Microsoft Visual C#

}
static void MostrarSiNulo(int? x, string nombreVariable)
{
if (!x.HasValue)
Console.WriteLine("{0} es nula", nombreVariable);
else
Console.WriteLine("{0} no es nula. Vale {1}.",nombreVariable,x.Value);
}
}

En él, el método MostrarSiNulo() primero comprueba si la variable vale null, para si así
simplemente indicarlo y si no indicar cuál su valor. Su salida será:

x es nula

y no es nula. Vale 123.

En realidad nada fuerza a utilizar HasValue para determinar si una variable anulable
vale null, sino que en su lugar se puede usar el habitual operador de igualdad. Del mismo
modo, en vez de leer su valor a través de de la propiedad Value se puede obtener
simplemente accediendo a la variable como si fuese no anulable. Así, el anterior método
MostrarSiNulo() podría rescribirse como sigue, con lo que quedará mucho más legible:

static void MostrarSiNulo(int? x, string nombreVariable)


{
if (x==null)
Console.WriteLine("{0} es nula", nombreVariable);
else
Console.WriteLine("{0} no es nula. Vale {1}.", nombreVariable, x);
}

Finalmente, hay que señalar que el tipo subyacente puede ser a su vez un tipo
anulable, siendo válidas declaraciones del tipo int??, int???, etc. Sin embargo, no tiene
mucho sentido hacerlo ya que al fin y al cabo los tipos resultantes serían equivalentes y
aceptarían el mismo rango de valores. Por ejemplo, una instancia del tipo int??? podría
crearse de cualquier de estas formas:

int??? x = 1;
x = new int???(1);
x = new int???(new int??(1));
x = new int???(new int??(new int?(1)));

Y leerse como sigue:

196
Clases en Microsoft Visual C#

Console.WriteLine(x);
Console.WriteLine(x.Value);
Console.WriteLine(x.Value.Value);
Console.WriteLine(x.Value.Value.Value);

Conversiones

C# 2.0 es capaz realizar implícitamente conversiones desde un tipo subyacente a su


versión anulable, gracias alo que cualquier valor del tipo subyacente de una variable
anulable se puede almacenar en la misma. Por ejemplo:

int x = 123;
int? y = x;

Lo que no es posible realizar implícitamente es la conversión recíproca (de un tipo


anulable a su tipo subyacente) ya que si una variable anulable almacena el valor null este
no será válido como valor de su tipo subyacente. Por tanto, estas conversiones han de
realizarse explícitamente, tal y como a continuación se muestra:

int z = (int) y;

Lo que sí se permite también es realizar implícitamente entre tipos anulables todas


aquellas conversiones que serían válidas entre sus versiones no anulables. Por ejemplo:

double d = z;
double d2 = (double) y; // Se puede hacer la conversión directamente
// a double
d2 = (int) y; // o indirectamente desde int
z = (int) d;
y = (int?) d2; // Se puede hacer la conversión directamente
// a int?
y = (int) d2; // o indirectamente desde int

Operaciones con nulos

Obviamente, la posibilidad de introducir valores nulos en los tipos valor implica que se
tengan que modificar los operadores habitualmente utilizados al trabajar con ellos para
que tengan en cuenta el caso en que sus operandos valgan null.

Para los operadores relacionales, esto implicaría en principio la introducción de


una lógica trivaluada (valores true, false y null), como en las bases de datos SQL. Sin
embargo, en tanto que ello suele atentar contra la intuitividad y provocar "problemas
psicológicos" a los programadores, se ha preferido simplificar el funcionamiento de estos

197
Clases en Microsoft Visual C#

operadores y hacer que simplemente devuelvan true si sus dos operadores


valen null y false si solo uno de ellos lo hace, pero jamás devolverán null. Por ejemplo:

int? i = 1;
int? j = 2;
int? z = null;

Console.WriteLine(i > j);


// Imprime False, al ser el valor de i menor que el de j
Console.WriteLine(i > z);
// Imprime False, al ser z null.
Console.WriteLine(i > null);
// Imprime False. El compilador incluso avisa de que este
// tipo de comparaciones siempre retornan false por ser uno
// de sus operandos null.
Console.WriteLine(null > null); // Imprime False. Ídem al caso anterior.

Sin embargo, para el caso de los operadores lógicos sí que se ha optado por permitir la
devolución de null por similitud con SQL, quedando sus tablas de verdad definidas como
sigue según esta lógica trivaluada:

x y x && y x || y
true true true true
true false false true
true null null true
false false false false
false null false null
null null null null

Tabla 20: Tabla de verdad de los operadores lógicos en lógica trivaluada

Y en el caso de los aritméticos también se permite la devolución de null, aunque en


este caso su implementación es mucho más intuitiva y simplemente consiste en
retornar null si algún operador vale null y operar normalmente si no (como en SQL) Por
ejemplo:

int? i = 1;
int? j = 2;
int? z = null;
Console.WriteLine(i + j);
// Imprime 3, que es la suma de los valores de i y j
Console.WriteLine(i + z);
// No imprime nada, puesto que i+z devuelve null.

198
Clases en Microsoft Visual C#

En cualquier caso, debe tenerse en cuenta que no es necesario preocuparse por


cómo se comportarán los operadores redefinidos ante las versiones
anulables de las estructuras, pues su implementación la proporcionará automáticamente
el compilador. Por ejemplo, si se ha redefinido el operador + para una estructura, cuando
se aplique entre versiones anulables del tipo simplemente se mirará si alguno de los
operandos es null, devolviéndose null si es así y ejecutándose la redefinición del
operador si no.

Operador de fusión (??)

Para facilitar el trabajo con variables anulables, C# 2.0 proporciona un


nuevo operador de fusión ?? que retorna su operando izquierdo si este no es nulo y el
derecho si lo es. Este operador se puede aplicar tanto entre tipos anulables como entre
tipos referencia o entre tipos anulables y tipos no anulables. Ejemplos de su uso serían los
siguientes:

int z;
int? enteronulo = null;
int? enterononulo;
string s = null;
z = enteronulo ?? 123; // Válido entre tipo anulable y no anulable. z=123
enterononulo = enteronulo ?? 123; // enterononulo = 123.
z = (int) (enteronulo ?? enterononulo);//Válido entre tipos anulables.
//z=123
Console.WriteLine(s ?? "s nula");// Escribe s nula.

Lo que obviamente no se permite es aplicarlo entre tipos no anulables puesto que no


tiene sentido en tanto que nunca será posible que alguno de sus operandos valga null.
Tampoco es aplicable entre tipos referencia y tipos valor ya que el resultado de la
expresión podría ser de tipo valor o de tipo referencia dependiendo de los operandos que
en concreto se les pase en cada ejecución de la misma, y por tanto no podría almacenarse
con seguridad en ningún tipo de variable salvo object. Por tanto, todas las siguientes
asignaciones son incorrectas (excepto las de las inicializaciones, claro):

int x = 1;
int? y = 1;
int z = 0;
string s = null;
z = 1 ?? 1; // No válido entre tipos valor
z = x ?? 123; // De nuevo, no válido entre tipos valor
z = s ?? x; // No válido entre tipos referencia y tipos valor.
z = s ?? y; // Ni aunque el tipo valor sea anulable.

El operador ?? es asociativo por la derecha, por lo que puede combinarse como sigue:

199
Clases en Microsoft Visual C#

using System;
class OperadorFusión
{
static void Main()
{
string s = null, t = null;
Console.WriteLine(s ?? t ?? "s y t son cadenas nulas");
t = "Ahora t no es nula";
Console.WriteLine(s ?? t ?? "s y t son cadenas nulas");
s = "Ahora s no es nula";
Console.WriteLine(s ?? t ?? "s y t son cadenas nulas");
}
}

Siendo el resultado de la ejecución del código el siguiente:

s y t son cadenas nulas

Ahora t no es nula

Ahora s no es nula

Modificadores de visibilidad de bloques get y set


C# 2.0 permite definir diferentes modificadores de visibilidad para los
bloques get y set de las propiedades e indizadores, de manera que podrán tener
propiedades en las que el bloque get sea público pero el set protegido. Por ejemplo:

class A
{
string miPropiedad;
public string MiPropiedad
{
get { return miPropiedad; }
protected set { miPropiedad = value; }
}
}

Lógicamente, la visibilidad de los bloques get y set nunca podrá ser superior a los de
la propia propiedad o indizador al que pertenezcan. Por ejemplo, una propiedad protegida
nunca podrá tener uno de estos bloques público.

Además, aunque de este modo se puede configurar la visibilidad del bloque get o del
bloque set de una cierta propiedad o indizador, no se puede cambiar la de ambos. Si
interesase, ¿para qué se dio a la propiedad o indizador ese modificador de visibilidad?

200
Clases en Microsoft Visual C#

Clases estáticas
Para facilitar la creación de clases que no estén destinadas a ser instanciadas o
derivadas (abstract sealed) sino tan sólo a proporcionar ciertos métodos estáticos
(clases fábrica, utilizadas para crear ciertos tipos de objetos, clases utilidad que
ofrezcan acceso a ciertos servicios de manera cómoda, sin tener que instanciar objetos,
etc.), C# 2.0 permite configurar dicha semántica en la propia declaración de las mismas
incluyendo en ellas el modificador static. Así se forzará a que el compilador asegure el
seguimiento de dicha semántica. Por ejemplo, el código que a continuación se muestra
será válido:

static public class ClaseEstática


{
static public object CrearObjetoTipoA()
{ … }
static public object CrearObjetoTipoB()
{ … }
}

Por el contrario, el siguiente código no compilaría ya que la clase ClaseEstáticaConError


se ha definido como estática pero tiene un método no estático llamado MétodoInstancia,
se la está intentando instanciar, y se está intentando derivar de ella una clase Hija:

static public class ClaseEstáticaConError


{
static public object CrearObjetoTipoA()
{ … }
static public object CrearObjetoTipoB()
{ … }

public void MétodoInstancia(string texto) // Error: método no estático.


{
Console.WriteLine("MétodoInstancia({0})", texto);
}
static void Main()
{
// Error: Se está instanciado la clase estática
ClaseEstáticaConError objeto = new ClaseEstáticaConError();
objeto.MétodoInstancia("Test");
}
}
class Hija: ClaseEstáticaConError {}
// Error: Se está derivando de clase estática

201
Clases en Microsoft Visual C#

Referencias a espacios de nombres


Alias global y calificador ::

En C# 1.1 puede producirse conflictos a la hora de resolver las referencias a ciertas


clases cuando en anidaciones de espacios de nombres existan varias clases y/o espacios
de nombres homónimos. Por ejemplo, en el siguiente código:

using System;
namespace Josan
{
class A
{
static void Main(string[] args)
{
Método();
}
static void Método()
{
Console.WriteLine("Josan.System.A.Método()");
A.Método();
}
}
}
class A
{
static void Método()
{
Console.WriteLine("A.Método()");
}
}

Resulta imposible llamar desde el método Método() de la clase Josan.A a su homónimo


en la clase A del espacio de nombres global, por lo que la llamada A.Método() producirá
un bucle recursivo infinito y la salida del programa será del tipo:

Josan.System.A.Método()

Josan.System.A.Método()

Josan.System.A.Método()

Para solucionar esto, C# 2.0 introduce la posibilidad de hacer referencia al espacio de


nombres global a través de un alias predefinido denominado global y un calificador :: que
se usa de la forma <inicio>::<referencia> y permite indicar el punto de <inicio> en la
jerarquía de espacios de nombres a partir del que se ha de resolver la <referencia> al tipo
o espacio de nombres indicada. Estas referencias se podrán usar en cualquier parte en la

202
Clases en Microsoft Visual C#

que se espere un nombre de tipo o espacio de nombres, tal como una sentenciausing, la
creación de un objeto con new,... Así, el ejemplo anterior podría resolverse como sigue:

using System;
namespace Josan
{
class A
{
static void Main(string[] args)
{
Método();
}
static void Método()
{
Console.WriteLine("Josan.System.A.Método()");
global::A.Método();
}
}
}
class A
{
public static void Método()
{
Console.WriteLine("A.Método()");
}
}

Y ahora ya sí que se obtendría la salida buscada:

Josan.System.A.Método()

A.Método()

Alias externos

C# 2.0 va un paso más allá en lo referente a resolver conflictos de ambigüedades en


los nombres de los identificadores respecto a C# 1.X. Ahora no solo se pueden usar clases
con el mismo nombre siempre y cuando se hallen en espacios de nombres diferentes sino
que también se permite usar clases con el mismo nombre y espacio de nombres que se
hallen en diferentes ensamblados. Para ello se usan los denominados alias externos.

Antes de las importaciones de espacios de nombres (using) se puede incluir líneas con
la siguiente sintaxis:

extern alias <nombreAlias>;

Con esto se estará diciendo al compilador que durante las comprobaciones de sintaxis
admita a la izquierda del calificador :: las referencias al alias de espacio de nombres cuyo

203
Clases en Microsoft Visual C#

nombre se le indica.. Cada una de estas referencias se corresponderán con uno o varios
ensamblados externos dentro de cuyos espacios de nombres se intentarán resolver la
referencia indicada a la derecha del ::. La correspondencia entre estos ensamblados y sus
alias se indicarán al compilador de C# mediante parámetros que se podrán pasar en la
línea de comandos al hacerles referencia mediante la opción /r siguiendo la sintaxis
<nombreAlias>=<rutaEnsamblado> para los valores de la misma; o en el caso de VS.NET
2005, desde la ventana de propiedades de la referencia al ensamblado en la solución.

Por ejemplo, se puede tener un fichero miclase1.cs con el siguiente contenido:

using System;
public class MiClase
{
public void F()
{
Console.WriteLine("F() de mi miclase1.cs");
}
}

Y otro miclase2.cs con una clase homónima a MiClase:

using System;
public class MiClase
{
public void F()
{
Console.WriteLine("F() de mi miclase2.cs");
}
}

Para usar ambas clases a la vez en un fichero extern.cs bastaría escribirlo como sigue:

extern alias X;
extern alias Y;
class Extern
{
static void Main()
{
X::MiClase objeto = new X::MiClase();
objeto.F();
Y::MiClase objeto2 = new Y::MiClase();
objeto2.F();
}
}

204
Clases en Microsoft Visual C#

Y compilar todo el conjunto con:

csc extern.cs /r:X=miclase1.dll /r:Y=miclase2.dll

csc extern.cs /r:X=miclase1.dll /r:Y=miclase2.dll

Al ejecutarlo podrá comprobarse que la salida demuestra que cada llamada se ha hecho
a uno de los diferentes tipos MiClase:

F() de mi miclase1.cs

F() de mi miclase2.cs

Nótese que es perfectamente válido asociar un mismo alias a varios


ensamblados y varios alias a un mismo ensamblado. Lo que no se puede es incluir
varias definiciones de alias en una misma opción /r, sino que para ello habría que utilizar
opciones /rdiferentes tal como se ha hecho en el ejemplo. Es decir, la siguiente llamada al
compilador no sería correcta:

csc extern.cs /r:X=miclase1.dll,Y=miclase2.dll

Supresión temporal de avisos


Al conjunto de directivas del preprocesador de C# se ha añadido una nueva en C# 2.0
que permite desactivar la emisión de determinados avisos durante la compilación de
determinadas secciones del código fuente así como volverlo a activar. Su sintaxis es:

#pragma warning <estado> <códigoAviso>

Donde <códigoAviso> es el código del aviso a desactivar o reactivar, y <estado>


valdrá disable o restore según si lo que se desea es desactivarlo o rehabilitarlo. Por
ejemplo, al compilar el siguiente código el compilador sólo informará de que no se usa la
variable b, pero no se dirá nada de la variable debido a que se le ha suprimido dicho
mensaje de aviso (su código es el 649) a través de la directiva #pragma warning:

class ClaseConAvisos
{

205
Clases en Microsoft Visual C#

# pragma warning disable 649


public int a;
# pragma warning restore 649
public int b;
}

En cualquier caso, hay que señalar que como normal general no se recomienda
hacer uso de esta directiva, ya que el código final debería siempre escribirse de
manera que no genere avisos. Sin embargo, puede venir bien durante la depuración de
aplicaciones o la creación de prototipos o estructuras iniciales de código para facilitar el
aislamiento de problemas y evitar mensajes de aviso ya conocidos que aparecerán
temporalmente.

Atributos condicionales
El atributo Conditional que en C# 1.X permitía especificar si compilar ciertas llamadas
a métodos en función de valores de constantes de preprocesador ha sido ahora ampliado
para también poderse aplicar a la utilización de atributos. En este caso, si la utilización del
atributo para un determinado fichero no se compila por no estar definida la constante de
la que depende, ninguna clase del mismo a la se aplique lo almacenará entre sus
metadatos. Así, si tenemos un fichero test.cs con la siguiente definición de atributo:

using System;

using System.Diagnostics;

[Conditional("DEBUG")]
public class TestAttribute : Attribute {}

Y con él compilamos un fichero miclase.cs con el contenido:

#define DEBUG
[Test]
class MiClase {}

En el ensamblado resultante la clase MiClase incluirá el atributo Test entre sus


metadatos por estar definida la constante DEBUG dentro del fichero en que se usa el
atributo. Pero por el contrario, si además compilásemos otro fichero miclase2.cs como el
siguiente:

#undef DEBUG
[Test]
class MiClase2 {}

206
Clases en Microsoft Visual C#

Entonces la MiClase2 del ensamblado resultante no almacenaría el atributo Test entre


sus metadatos por no estar definida la constante DEBUG en el fichero donde se declaró.

Incrustación de tablas en estructuras


Al interoperar con código nativo puede ser necesario pasar a éste estructuras de un
tamaño fijo ya que el código nativo puede haber sido programado para esperar un cierto
tamaño concreto de las mismas. Hacer esto con estructuras que tengan tablas como
campos no era fácil en C# 1.X ya que al ser éstas objetos de tipo referencia, en realidad
sus campos almacenaban referencias a los datos de la tabla y no los propios datos.

C# 2.0 soluciona este problema dando la posibilidad de definir en las estructuras


campos de tipo tabla que se almacenará ocupando posiciones de memoria contiguas. Para
ello, basta preceder del modificador fixed la definición del campo. Por ejemplo:

public struct Persona


{
public unsafe fixed char Nombre[100];
public int Edad;
}

De este modo, los objetos de la estructura Persona siempre ocuparán 104 bytes (100
por la tabla correspondiente al nombre da persona y 4 por los bytes del tipo int de la
edad)

No obstante, hay que señalar que este uso del modificador fixed sólo funciona en las
definiciones de campos que sean tablas unidimensionales planas (vectores) de
estructuras en contextos inseguros, y no en campos de clases, ni en contextos
seguros, ni con tablas multidimensionales o dentadas.

Modificaciones en el compilador
Control de la versión del lenguaje

A partir de la versión 2.0 del .NET Framework, el compilador de C# proporciona una


nueva opción /langversion que permite restringirle las características del lenguaje que
se permitirán usar durante la compilación para solo permitir las de una cierta versión o
estándar del lenguaje. Por ahora, los valores que esta opción admite son los siguientes:

Valor Descripción
default Utilizar las características de la versión más actual del lenguaje para la que el compilador
se haya preparado. Es lo que se toma por defecto
ISO-1 Usar las características de la versión 1.X del lenguaje estandarizada por ISO. Por lo tanto,
no se permitirá el uso de genéricos, métodos anónimos, etc.[17]

207
Clases en Microsoft Visual C#

Tabla 21: Identificadores de versiones del lenguaje

Por ejemplo, dado el siguiente fichero version2.cs que usa tipos parciales:

partial class Version2


{
static void Main()
{
}
}

Si lo intentamos compilar como sigue:

csc version2.cs /langversion:iso-1

Obtendremos un mensaje de error indicándonos que el uso de tipos parciales no está


permitido en la versión 1.X estándar de C#:

Version2.cs(1,1): error CS1644: Feature 'partial types' cannot be used because it is not part of the
standardized ISO C# language specification

Control de la plataforma de destino

Dado que a partir de la versión 2.0 de Microsoft.NET se soportan las arquitecturas


de 64 bits de Intel y AMD, el compilador de C# admite una nueva
opción /platform por medio de la que se le puede indicar el tipo de plataforma hacia la
que se desea dirigir el código que se compila. Los valores que admite son los siguientes:

Valor Significado
Anycpu Compilar para cualquier plataforma. Es lo que por defecto se toma
X86 Compilar para los procesadores de 32 bits compatibles con Intel
X64 Compilar para los procesadores de 64 bits compatibles con AMD
Itanium Compilar para los procesadores de 64 bits Intel Itanium

Tabla 22: Identificadores de plataforma de destino

Gracias a esta opción, si el código se dirige hacia una plataforma de 64 bits y se intenta
ejecutar bajo una plataforma de 32 bits saltará un mensaje de error como el siguiente:

208
Clases en Microsoft Visual C#

[ Ampliar Imagen]

Por el contrario, si se dirige a plataformas de 32 bits y se intenta ejecutar en una de 64


bits, automáticamente Windows simulará la ejecución del mismo en un entorno de 32 bits
a través de su funcionalidad WOW (Windows On Windows)

Aunque en principio lo ideal es realizar el código lo más independiente posible


de la plataforma, se da esta opción porque cuando se opera con código inseguro se
pueden tener que realizar suposiciones sobre las características concretas de la plataforma
hacia la que se dirige el código, fundamentalmente en lo que respecta al tamaño ocupado
en memoria por ciertos tipos de datos que se manipulen mediante aritmética de punteros.

Envío automático de errores a Microsoft

A partir de .NET 2.0, el compilador de C# da la opción de enviar automáticamente a


Microsoft los errores internos del mismo que durante su uso pudiesen surgir para así
facilitar su detección y corrección a Microsoft (errores de csc.exe y no del código que se
pretenda compilar) Por ejemplo, si se intenta compilar el siguiente código con el
compilador de C# 2.0 de la Beta 1 del Microsoft.NET Framework SDK 2.0:

using System;

using System.Collections;

class Error
{
IEnumerator GetEnumerator()
{
try {}
catch { yield break; }
finally {}
}

static void Main()


{}
}

El compilador sufrirá un error interno que impedirá finalizar el proceso de compilación:

error CS0583: Internal Compiler Error (0xc0000005 at address 56D1EEC4): likely culprit is

209
Clases en Microsoft Visual C#

'TRANSFORM'.

An internal error has occurred in the compiler. To work around this problem, try simplifying or
changing the program near the locations listed below. Locations at the top of the list are closer to the
point at which the internal error occurred.

error.cs(6,14): error CS0584: Internal Compiler Error: stage 'TRANSFORM' symbol


'Error.GetEnumerator()'

error.cs(6,14): error CS0584: Internal Compiler Error: stage 'BIND' symbol 'Error.GetEnumerator()'

error.cs(6,14): error CS0584: Internal Compiler Error: stage 'COMPILE' symbol 'Error.GetEnumerator()'

error.cs(6,14): error CS0584: Internal Compiler Error: stage 'COMPILE' symbol 'Error.GetEnumerator()'

error.cs(6,14): error CS0584: Internal Compiler Error: stage 'COMPILE' symbol 'Error.GetEnumerator()'

error.cs(4,7): error CS0584: Internal Compiler Error: stage 'COMPILE' symbol 'Error'

error.cs(114297930,1): error CS0584: Internal Compiler Error: stage 'COMPILE' symbol '<global
namespace>'

error.cs: error CS0586: Internal Compiler Error: stage 'COMPILE'

error CS0587: Internal Compiler Error: stage 'COMPILE'

error CS0587: Internal Compiler Error: stage 'BEGIN'

Para realizar el envío del error a Microsoft se puede usar la opción /errorreport del
compilador, la cual admite los siguientes valores:

Valor Descripción
None No enviar el error a Microsoft. Es lo que se hace por defecto.
Prompt Preguntar si enviar el error. Esto hará que durante la compilación se muestre al usuario
una ventana como la siguiente en la que se le pide permiso para la realización del envío
y se le de la opción de inspeccionar la información que se enviará a Microsoft por si
desea asegurarse de que no se vayan a enviar datos sensibles sobre el equipo o el
código fuente compilado (puede que se les envíe parte del mismo para facilitarles la
detección del error):

210
Clases en Microsoft Visual C#

[ Ampliar Imagen]

Send Enviar automáticamente la notificación a Microsoft, sin necesidad de tener que


confirmarlo a través de la ventana que la opción anterior muestra.

Tabla 23: Opciones para el envío de errores a Microsoft

Lo ideal es combinar esta opción con la opción /bugreport ya conocida, para que
así el informe del error sea más rico y la detección de sus causas sea más sencilla. Como
en este caso la información que se enviará a Microsoft es mayor y por consiguiente puede
que se tarde mucho más enviársela a través de Internet (sobre todo si se usa un módem
convencional para acceder a la Red), se informará antes al usuario de esta circunstancia y
se le preguntará si está seguro de realizar el envío. Si confirma, aparecerá una ventana
como la siguiente con información sobre el progreso del envío del informe de error:

[ Ampliar Imagen]

Concretización de avisos a tratar como errores

La opción /warnaserror permite ahora que se le concreten los avisos que se desea
que se traten como errores. Por ejemplo, para decirle que sólo considere como errores los
avisos con código CS0028:

csc test.cs /warnaserror:28

Así mismo, a VS.NET se le ha añadido la posibilidad de configurar los avisos a tratar


como errores y los avisos a filtrar a través de la hoja de propiedades del proyecto. En

211
Clases en Microsoft Visual C#

concreto, desde sus controles Configuration Properties à Build à Treat Warnings as


Errors y Configuration Properties à Build à Suppress specific warnings.

Visibilidad de los recursos

Las opciones /linkres y /res permiten ahora especificar la visibilidad de los recursos
que se enlazarán o incrustarán en el ensamblado generado, de modo que se pueda
configurar no permitir acceder a los mismos desde otros ensamblados. Para ello, tras el
identificador del recurso se puede incluir una de estas partículas separada por una coma:

Valor Descripción
public Podrá accederse a los recursos del ensamblado libremente desde cualquier otro
ensamblado. Es lo que se toma por defecto
private Sólo podrá accederse a los recursos desde el propio ensamblado

Tabla 24: Indicadores de visibilidad de recursos

Por ejemplo:

csc test.cs /linkres:misrecursos.resources,recursos.cs,private

Firma de ensamblados

El compilador de C# 2.0 incorpora opciones relacionadas con la firma de ensamblados


que hasta ahora venían siendo proporcionada de manera externa al mismo, pues como al
fin y al cabo compilar un ensamblado con firma o sin ella no es más que una opción de
compilación, la forma más lógica de indicarlo es a través de opciones del compilador.

Para poder instalar un ensamblado en el GAC es necesario que tenga un nombre


seguro; es decir, que esté firmado. Este proceso de firma consiste en utilizar una clave
privada para cifrar con ella el resultado de aplicar un algoritmo de hashing al contenido del
ensamblado, e incluir en su manifiesto la clave pública con la que luego poder descifrar la
firma y comprobar que el ensamblado no se ha alterado y procede de quien se espera.

Para generar la pareja de claves pública-privada se puede utilizar la


herramienta sn.exe (singing tool) del SDK, que se puede usar como sigue para generar
aleatoriamente un par de claves y guardarlas en un fichero (por convenio se le suele dar
extensión .snk):

sn –k misClaves.snk

Luego, la firma del ensamblado se realizará especificando durante la compilación la ruta


del fichero en el que se almacenan las claves a utilizar para realizar la firma a través de la
nueva opción /keyfile incluida en el compilador de C# 2.0 para estos menesteres:

212
Clases en Microsoft Visual C#

csc test.cs /keyfile:misClaves.snk

En lugar de especificar las claves como fichero, también es posible instalarlas en


un contenedor público y referenciar al mismo durante la compilación. Para instalarlas
en el contenedor se usa de nuevo la herramienta sn.exe, indicándole ahora la opción –
iseguida de la ruta del fichero y el nombre del contenedor donde instalarla. Por ejemplo:

sn –i misClaves.snk contenedorJosan

Y al compilar se referenciaría al contenedor con la opción /keycontainer de csc:

csc test.cs /keyname:contenedorJosan

El uso de las opciones /keyfile y /keycontainer son ahora la práctica recomendada


por Microsoft para la firma de los ensamblados, en detrimento de los antiguos atributos de
ensamblado AssemblyKeyFile y AssemblyKeyName que antes se usaban para estas
labores. De hecho, si se insiste en seguir utilizándolos el compilador emitirá mensajes de
aviso informando de la inconveniencia de hacerlo. Lógicamente, en los ensamblados
generados utilizando estas opciones ya no estarán presentes dichos atributos.

Las opciones /keyfile y /keycontainer se pueden combinar con /delaysign para


conseguir que el proceso de firma no se haga al completo, sino que en vez de calcularse y
guardarse la firma del ensamblado entre sus metadatos sólo se reserve en los mismos el
espacio necesario para posteriormente incluírsela y se les añada la clave pública con la
que se podrán instalar en el GAC para realizarles pruebas. A esto se le conoce como firma
parcial, y para posteriormente realizar la firma completa al ensamblado se puede utilizar
la utilidad sn.exe para refirmarlo, ya sea en base a un fichero de claves o a un
contenedor público tal y como a continuación se muestra con los siguientes ejemplos:

sn.exe –R test.dll misClaves.snk

sn.exe –Rc test.dll contenedorJosan

Nuevamente, en .NET 1.X la firma retrasada de los ensamblados se realizaba a través


de un atributo de ensamblado (AssemblyDelaySign), cosa que ahora no se recomienda
y que provocará la aparición de mensajes de aviso durante la compilación si se utiliza.

213

También podría gustarte