You are on page 1of 91

INSTITUTO TECNOLOGICO DE NOGALES

INGENIERIA EN SISTEMAS COMPUTACIONALES

APUNTES PARA LA MATERIA


DE PROGRAMACION ORIENTADA A OBJETOS

POR: M.C. LUCAS GALAZ VALLES

1
EJEMPLO................................................................................................................56

BLOQUE TRY..........................................................................................................80

BLOQUE CATCH....................................................................................................80

BLOQUE FINALLY.................................................................................................82

CAPTURA DE ERRORES NO CONTROLADOS...................................................83


........................................................................................................................................................................86

Clases usadas para E/S de archivos...........................................................................................................86

Clases usadas para leer y escribir en secuencias.......................................................................................86

Clases comunes de secuencias de E/S..............................................................................................................87

E/S y seguridad.................................................................................................................................................87

8.2 Operaciones básicas en archivos texto y binario.....................................................................................87


8.2.1 Crear y 8.2.2 Abrir................................................................................................................................87
8.2.4 Lectura y escritura, 8.2.5 Recorrer y 8.2.3 Cerrar................................................................................89

I Arreglos unidimensionales y multidimensionales

1.1 Arreglos unidimensionales listas (vectores)

Un arreglo (matriz, vector, lista) es un tipo especial de objeto compuesto por una
colección de elementos del mismo tipo de datos que se almacenan consecutivamente en
memoria. La Figura 1.1 es un arreglo de 10 elementos de tipo double y se representa por
un nombre, lista, con índices o subíndices.

lista [0] Lista es el nombre


lista [1] [ i ] es el índice
lista [2]
lista [3]
lista [4]
lista [5]
lista [6]
lista [7]
lista [8]

2
Figura 1.1. El arreglo lista de 10 elementos, con índices de 0 a 8.

Otra forma de representar gráficamente un arreglo es en forma de lista horizontal:

lista[0] lista[1] lista[2] lista[3] ……

Figura 1.2. Arreglo lista de 10 elementos.

Los arreglos pueden ser unidimensionales (Figuras 1.1 y 1.2) conocidos también como
listas o vectores y multidimensionales conocidos también como tablas o matrices, que
pueden tener dos o más dimensiones.

Ejemplo

El arreglo temperaturas de ocho elementos consta de los siguientes componentes:

temperaturas [0]
temperaturas [1]
temperaturas [2]
temperaturas [3]
temperaturas [4]
temperaturas [5]
temperaturas [6]
temperaturas [7]

Regla: Un arreglo tiene un nombre o identificador, un índice que es un entero encerrado


entre corchetes, un tamaño o longitud, que es el número de elementos que se pueden
almacenar en el arreglo cuando se le asigna espacio en memoria. Un arreglo se
representa por una variable arreglo y se debe declarar, crear, iniciar y utilizar.

Conceptos basicos

El proceso que se puede realizar con arreglos abarca las siguientes operaciones:
declaración, creación, inicialización y utilización. Las operaciones de declaración, creación
e inicialización son necesarias para poder utilizar un arreglo.

Declaración

La declaración de un arreglo es la operación mediante la cual se define su nombre con un


identificador válido y el tipo de los elementos del arreglo. La sintaxis para declarar un
arreglo adopta el siguiente formato:

tipoDato[ ] nombreArreglo

Ejemplo

3
double[ ] miLista; Se declara un arreglo miLista de tipo double.
float[ ] temperatura; Se declara un arreglo temperatura de tipo float

Las declaraciones no especifican el tamaño del arreglo que se especificará cuando se


cree el mismo.

Creación

Un arreglo C# es un tipo referencia derivado de la clase base System.Array y la


declaración no asigna espacio en memoria para el arreglo. No se pueden asignar
elementos al arreglo a menos que el arreglo esté ya creado. Después que se ha
declarado un arreglo se puede utilizar el operador new para crear el arreglo con la sintaxis
siguiente:

nombreArreglo = new tipoDato[tamaño];

nombreArreglo es el nombre del arreglo declarado previamente, tipoDato es el tipo de


dato de los elementos del arreglo y tamaño es la longitud o tamaño del arreglo y es una
expresión entera cuyo valor es el número de elementos del arreglo.

Ejemplo

miLista = new double [8]; // arreglo miLista de 8 elementos


temperatura = new float [30]; // arreglo temperatura de 30 elementos

Regla: Se pueden combinar la declaración y la creación de un arreglo con una sola


sentencia

tipoDato [ ] nomb,reArreglo = new tipoDato[tamaño};

Ejemplo

double[ ] miLista = new double[8];


float[ ] temperatura = new float[30];

Precaución: Una vez que un arreglo se ha creado su tamaño no se puede modificar.

Inicialización y utilización

Cuando se crea un arreglo, si no se inicializa en el momento en el que se declara, a los


elementos se les asigna su valor por defecto. A los elementos del arreglo se accede a
través del índice. Los índices del arreglo están en el rango de 0 a tamaño-1. Así miLista
contiene 8 elementos y sus índices son 0,1,2, . . . ,7.
Cada elemento del arreglo se representa con la siguiente sintaxis:

nombreArreglo[índice];

4
Ejemplo

miLista [7] representa el último elemento del arreglo

Regla: En C#, un índice del arreglo es siempre un entero que comienza en cero y termina
en tamaño-1.

Precaución: Tenga cuidado ya que, al contrario que en otros lenguajes de programación,


los índices siempre se encierran entre corchetes: temperaturas [15] .

Un arreglo completo se puede inicializar con una sintaxis similar a:

double[ ] miLista = { 1.5, 2.45, 3.15, 7.25, 8.4 };

esta sentencia crea el arreglo miLista que consta de cinco elementos. También podrían
haberse utilizado

double[ ] miLista = new int[5] { 1.5,2.45,3.15,7.25,8.4 };


double[ ] miLista = new int[ ] { 1.5, 2.45, 3.15, 7.25, 8.4 };

Cálculo del tamaño

El tamaño de un arreglo se obtiene con la propiedad Length. Así, por ejemplo, si se crea
un arreglo miLista, la sentencia miLista.Length devuelve el tamaño del arreglo miLista
(10, por ejemplo).

Utilización de los elementos del arreglo

Las variables que representan elementos de un arreglo se utilizan de igual forma que
cualquier otra variable. Por ejemplo:

int[ ] n = new int[50];


int j =0;
//...
j = n[j] + n[10];

Algunas sentencias permitidas en C#

temperatura [5] = 45;


temperatura [8] = temperatura [5] + 10;

Los elementos de un arreglo se suelen procesar mediante bucles (por ejemplo for) por las
siguientes razones:

• Todos los elementos del arreglo son del mismo tipo y tienen las mismas
propiedades; por esta razón, se procesan de igual forma y repetidamente utilizando
un bucle.

5
• El tamaño de un arreglo se conoce, por lo que resulta muy conveniente el empleo
de un bucle for.
• Otra forma muy adecuada para recorrer los elementos de un arreglo es la
instrucción foreach.

Ejemplos

1. El bucle for siguiente introduce valores en los elementos del arreglo. El tamaño del arreglo
se obtiene en miLista.Length.
for (int i = i; i < miLista.Length; i++)
miLista[i] = i;

2. foreach (double i in miLista)


{
MessageBox.Show("El valor es:"+ i);
}

3. int[ ] cuenta = new int[100];


int i;
for (i = 0; i < cuenta.Length; i++)
cuenta[i] = 0;

COPIA DE ARREGLOS

Con frecuencia se necesita duplicar un arreglo o bien una parte de un arreglo. Existen dos
métodos para copiar arreglos: copiar elementos individuales utilizando un bucle y utilizar
el método Arreglo.Copy.

Método 1

Un método para copiar arreglos es escribir un bucle que copia cada elemento desde el
arreglo origen al elemento correspondiente del arreglo destino.

Ejemplo

Este código copta arregloFuente en arregloDestino

Nota: Crear un proyecto, poner un botón en la forma, hacer doble clic en el botón,
declarar e inicializar los arreglos, escribir el siguiente código.

for (int i = 0; i < arregloFuente.Length; i++)


arregloDestino[i] = arregloFuente[i];

Método 2

Otra forma de copiar arreglos es usar el método Arreglo.Copy que tiene los siguientes
formatos:

public static void Copy(Array arregloOrigen,Array arregloDestino, int longitud)

6
public static void Copy (Array arregloOrigen, int pos_ini, Array arregloDestino, int pos_fin,
int longitud)

La sintaxis del método Copy ( ) es:

Array.Copy(arregloOrigen, arregloDestino, longitud);


Array.Copy(arregloFuente, pos_ini, arregloDestino, pos_fin, longitud) ;

Pos_ini = Posición donde comienza la copia.


Pos_fin = Posición donde comienza el almacenamiento.
Longitud = Número de elementos a ser copiados.

Ejemplo

object[ ] arregloOrigen = {4, 5, 1, 25, 100};


int[ ] arregloDestino1 = new int[arregloOrigen.Length];
Array.Copy(arregloOrigen,0, arregloDestino1,0, arregloOrigen.Length);
for (int i = 0; i < arregloDestino1.Length; i++)
MessageBox.Show(arregloDestino1[i]);

int[ ] arregloDestino2 = new int[arregloOrigen.Length];


for (int i = 0; i < arregloOrigen.Length; i++)
arregloDestino2[i] = (int)arregloOrigen[i];
for (int i = 0; i < arregloDestino2.Length; i++)
MessageBox.Show(arregloDestino2[i]);

PASO DE ARREGLOS COMO PARÁMETROS

Los arreglos pueden ser pasados como parámetros por valor y también como parámetros
por variable, mediante el uso de los modificadores ref o out. Cuando un arreglo se pasa a
un método como parámetro por valor es posible cambiar los valores de los elementos del
arreglo en el interior del método y devolverlos modificados al lugar desde donde se
efectuó la llamada. Esto es debido a que los arreglos son tipos referencia. y, al pasar un
arreglo por valor lo que se efectúa sólo es una copia de la referencia que señalaba al
arreglo.

Ejemplo

public class ArrComoParametros1


{
private static void Cambiar(int[ ] arr)
{
for (int i = 0; i < arr.Length; i++)
arr[i] = arr[i] + 2;
}

7
public void Principal( )
{
int[ ] arr = {1,4,5};
Cambiar(arr);
for (int i = 0; i < arr.Length; i++)
MessageBox.Show(“{0} “+arr[i]);
}
}

El resultado de la ejecución es: 3 6 7

Cuando se pasa un arreglo por referencia a un método, no sólo los cambios en los valores
de los elementos del arreglo, sino todos los cambios que tengan lugar dentro del método
afectan la matriz original.

Ejemplo

El siguiente ejemplo utiliza el método Añadir para incrementar el número de elementos de


un arreglo. Es necesario que al método Añadir se le pase el arreglo precedido por el
modificador ref, para que la referencia se pueda devolver modificada.

class ArrComoParametros2
{
static void Añadir (ref int[ ] arr, int elemento)
{
int[ ] otro = arr;
int nueva longitud = arr.Length+1;
// La referencia pasa a apuntar a un nuevo lugar
arr = new int[nuevalongitud];
arr[nuevalongitud-l] = elemento;
for (int i=0; i < otro.Length; i++)
arr[i] = otro[i];
}

public void Principal( )


{
int[ ] arr = {1,4,5};
Añadir(ref arr, 8);
for (int i=0; i<arr.Length; i++)
MessageBox.Show(“{0} “+arr[i]);
}
El resultado de la ejecución es: 1 4 5 8

1.2 Arreglo bidimensional

En C# los arreglos pueden tener más de una dimensión. Así, las tablas o matrices se
representan mediante arreglos bidimensionales. Un arreglo bidimensional se declara:

tipo [ , ] nombre;

8
y se crea o instancia mediante:

nombre = new tipo[valor1, valor2];

En la instanciación, a cada dimensión de la matriz se le asocia una longitud (valor1 y


valor2), que es un número entero mayor o igual que cero. También es posible combinar
ambas operaciones, declaración e instanciación, en una única sentencia.

tipo[ , ] nombre = new tipo[valor1, valor2];

Las tablas de valores constan de información dispuesta en filas y columnas. Para


identificar un elemento de una tabla concreta se deben especificar dos subíndices (por
convenio, el primero identifica la fila del elemento y el segundo identifica la columna del
elemento). Si la longitud de una dimensión es valor1 los subíndices pueden ir de cero a
valor1-1 en dicha dimensión.

0 1 2 3 4 5

(a) Arreglo de una dimensión

0 1 2 3
0
1
2
3
(b) Arreglo bidimensional

Figura 1.3. Arreglos: (a) Una dimensión; (b) Dos dimensiones.

La Figura 1.4 ilustra un arreglo de doble subíndice, a, que contiene tres filas y cuatro
columnas (un arreglo de 3 x 4). Un arreglo con m filas y n columnas se denomina arreglo
m_por _n.

Columna 0 Columna 1 Columna 2 Columna 3


Fila 0
Fila 1
Fila 2
a[0,0] a[0,1] a[0,2] a[0,3]
a[1,0] a[1,1] a[1,2] a[1,3]
a[2,0] a[2,1] a[2,2] a[2,3]

9
Figura 1.4. Un arreglo de dos dimensiones con tres filas y cuatro columnas.

Cada elemento del arreglo a se identifica como tal elemento mediante el formato a [i, j]; a
es el nombre del arreglo, e i,j son los subíndices que identifican unívocamente la fila y la
columna de cada elemento de a; obsérvese que todos los elementos de la primera fila
comienzan por un primer subíndice de 0 y los de la columna cuarta tienen un segundo
subíndice de 3 (4 -1).
La inicialización puede efectuarse en el momento de la declaración especificando los
valores iniciales entre { }.

Ejemplos

1. Un arreglo b de 2x2 dimensiones se puede declarar e inicializar con:

int[ , ] b= {{5,6}, {7,8}};

b [0, 0] b [0,1]
(5) (6)
b [1, 0] b [1,1]
(7) (8)

y también con

int[ , ] b=new int[2, 2] {{5, 6}, {7, 8}};

o con

int[ , ] b=new int[ , ] {{5, 6}, {7, 8}};

2. La declaración

int[ , ] b = {{4,5,6}, {7,8,9}};

crea un arreglo de dos filas y tres columnas, en el que la primera fila contiene 4 , 5, 6 .
Un medio rápido para asignar valores a los elementos de un arreglo de dos dimensiones
es utilizar bucles for

for (int x = 0; x < 3; ++x) {


for (int y = 0; y < 3; ++y) {
tabla[x , y] = 5;
}
}

Los bucles anidados funcionan del modo siguiente. El bucle externo, el bucle x, arranca
estableciendo x a 0. Como el cuerpo del bucle x es otro bucle, a continuación arranca
dicho bucle interior, bucle y, fijando y a 0. Todo esto lleva al programa a la línea que

10
inicializa el elemento del arreglo tabla[0, 0] al valor 5. A continuación el bucle interior
establece y a 1, y con ello tabla[0,1] toma el valor 5. Cuando termina el bucle interno el
programa bifurca de nuevo al bucle externo y establece x a 1. El bucle interno se repite de
nuevo, pero esta vez con x igual a 1, y tomando y valores que van de 0 a 2. Por último,
cuando ambos bucles terminan todos los elementos del arreglo habrán tomado el valor 5.

Ejemplo

1. Asignación de valores a cada uno de los elementos de un arreglo

0 1 2 3
0 1 2 3
4 5 6 7
8 9 10 11
12 13 14 15

Tras crear y declarar el arreglo tabla: int[ , ] tabla = new int[3,3];

El listado siguiente recorre el arreglo y asigna valores a sus elementos:

for (int x = 0; x < 3; ++x)


for (int y = 0; Y < 3; ++y)
tabla[x,y] = x*4 + y;

2. Declarar, crear y llenar un arreglo de 8 filas y 10 columnas con 80 enteros de valor


cero

int[ , ] numeros;
numeros = new int[8,10];
for (int x = 0; x < 8; ++x)
for (int y = 0; Y < 10; ++y)
numeros[x,y] = 0;

Análogamente, también es posible leer valores para cada uno de los elementos de un
arreglo de dos dimensiones anidando dos bucles for, uno externo y otro interno. El
recorrido de un arreglo para mostrar la información almacenada en él, también se efectúa
utilizando bucles for anidados, ya que los bucles facilitan la manipulación de cada uno de
los elementos de un arreglo.

Ejercicio

La siguiente aplicación crea e inicializa un arreglo de dos dimensiones con 10 columnas y


15 filas. A continuación presenta en pantalla el contenido del arreglo.

public class Tabla1 {

11
static int[ , ] tabla;

static void Llenar()


{
tabla = new int[15,10];
for (int x = 0; x < 15; x++)
for (int y = 0; y < 10; y++)
tabla[x,y] = x * 10 + y;
}

static void Mostrar()


{
for (int x = 0; x < 15; x++)
for (int y = 0; Y < 10; y++)
MessageBox.Show(tabla[x,y];
}

public static void Principal ()


{
//Creación, instanciación e inicialización en una única sentencia
Llenar() ;
Mostrar() ;
}

A continuación se muestra un formulario que crea e inicializa un arreglo de dos


dimensiones con 10 columnas y 15 filas, y presenta el contenido del arreglo, de forma que
se puede ver que el arreglo contiene realmente los valores a los que se ha inicializado.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace unFormulario
{
public partial class Form1 : Form
{
int[,] tabla;

public Form1()
{
InitializeComponent();
}

public void UnFormulario()


{
Inicializar();

12
Text = "2ª aplicación Windows";
Size = new Size(400, 400);
CenterToScreen();
}

private void Inicializar()


{
tabla = new int[15, 10];
for (int x = 0; x < 15; x++)
for (int y = 0; y < 10; y++)
tabla[x, y] = x * 10 + y;
}

protected override void OnPaint(PaintEventArgs e)


{
UnFormulario();
base.OnPaint(e);
Graphics g = e.Graphics;
for (int x = 0; x < 15; x++)
for (int y = 0; y < 10; y++)
{
string s = tabla[x, y].ToString();
Font fuente = new Font("Arial", 10);
SolidBrush pincel = new SolidBrush(Color.Green);
PointF lugar = new PointF(50 + y * 25, 50 + x * 15);
g.DrawString(s, fuente, pincel, lugar);
}
}

}
}

El paso como parámetros de las tablas o matrices se efectúa de forma análoga a


como se efectúa el de arreglos unidimensionales. Por ejemplo:

int[,] tabla = {{3,8}, {4, 7},{5, 6},{9, 2}};


Cambiar(tabla);

static void Cambiar(int[,] t)

//cabecera del método

Un arreglo multidimensional, con más de dos dimensiones, se declara, crea e inicializa de


la misma forma que un bidimensional. La asignación, lectura y presentación de cada uno
de los valores almacenados también se realiza mediante bucles anidados.

//Declaración: se usan comas para separar cada dimensión del arreglo.


tipo[,,] nombre; //arreglo tridimensional

//Instanciación
nombre = new tipo[valor1, valor2, valor3];

13
Ejemplo

//Creación, instanciación e inicialización en una única sentencia


int[,,] numeres = new int[2,3,4] {{{1,2,3,4},{2,3,4,5},{3,4,5,6}},{{6,S,4,3},{S,4,3,2},{4,3,2,1}}};
MessageBox.Show(numeros.Length); // Resultado: 24
MessageBox.Show(numeros.Rank); // Resultado: 3
MessageBox.Show(numeros.GetLength(0)); // Resultado: 2

Importante: La propiedad Arreglo.Rank devuelve el número de dimensiones de


un.arreglo.
El método Arreglo.GetLength devuelve el número de elementos en una determinada
dimensión de un arreglo.

1.3 Arreglo Multidimensional

Los elementos de un arreglo en C# pueden ser de cualquier tipo, incluidos otros arreglos.
En los arreglos de arreglos cada fila puede contener un número diferente de columnas y
se pueden asignar dinámicamente, como sigue:

int[ ] [ ] b;
b = new int[3] [ ]; // asigna filas
b[0] = new int[5]; // asigna 5 columnas a la fila 0
b[1] = new int[4]; // asigna 4 columnas a la fila 1
b[2] = new int[3]; // asigna 3 columnas a la fila 2

La expresión b.Length devuelve el número de filas del arreglo, mientras b[1].Length


devuelve el número de columnas de la fila 1 del arreglo. ,

MessageBox.Show(b.Length);
MessageBox.Show(b[1].Length);

//Resultado: 3 //Resultado: 4

Ejemplo

Declarar, crear e inicializar un arreglo de arreglos:

int[ ] [ ] matriz = new int[5] [ ];


matriz[0] = new int[ ]{1, 2, 3, 4};
matriz[1] = new int[ ]{3, 4, 5, 6};
matriz[2] = new int[ ]{3, 4};
matriz[3] = new int[ ]{4, 6, 8};
matriz[4] = new int[ ] {S, 8 ,1, 7, 3, 6, 6};

Se puede utilizar también una notación abreviada para declarar e inicializar un arreglo de
arreglos:

int[ ] [ ] matriz = new int [ ] [ ] {


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

14
new int[ ] {3, 4, 5, 6},
new int[ ] {3, 4},
new int[ ] {4, 6, 5},
new int[ ] {5, 8 ,1, 7, 3, 6, 6}
};

o bien:

int[ ] [ ] matriz =
{
new int[ ] {l, 2, 3, 4},
new int[ ] {3, 4, 5, 6},
new int[ ] {3, 4},
new int[ ] {4, 6, S},
new int [ ] {5, 8 ,1, 7, 3, 6, 6}
};

Es importante destacar que no se puede omitir el operador new de la inicialización de los


elementos.

La asignación de valores a un elemento específico se puede hacer con sentencias


similares a:

matriz [2] [0] = 9;

Para recorrer un arreglo de arreglos se puede usar una estructura for con una variable de
control, i, que recorra las filas (0 hasta a.Length-1) y otro bucle for con una variable de
control j que recorra los elementos de cada fila (0 hasta a[i].Length-1).

Ejemplo

La siguiente estructura declara, crea, inicializa y muestra el contenido del arreglo a, en el


que cada fila contiene un número diferente de columnas

int[ ] [ ] a;
a = new int[3] [ ]; // asigna filas
a[0] = new int[5];' // asigna 5 columnas a la fila 0
a[1] = new int[7]; // asigna 7 columnas a la fila 1
a[2] = new int[3]; // asigna 3 columnas a la fila 2
for (int i = 0; i < a.Length; i++)
for (int j = 0; j < a[i] .Length; j++)
a[i][j]=j+1;

15
II Métodos y Mensajes

2.0 Clases y Objetos

2.0.1 Clases

Las clases son estructuras o plantillas que sirven para definir un objeto. En C# una clase
puede tener los siguientes miembros: campos de datos (constantes, campos de sólo
lectura o variables), declaraciones de tipos anidadas, eventos, indexadores, operadores,
constructores, destructores, propiedades y métodos; y, habitualmente, una clase de un
objeto contiene una colección de métodos y definiciones de datos. Si se diseña una clase
que representa a un cliente, no se ha creado un objeto. Un objeto es una instancia
(ejemplar, caso) de la clase Cliente y, por consiguiente, puede, naturalmente, haber
muchos objetos de la clase Cliente. La creación de una variable específica de un tipo
particular de clase se conoce como instanciación (creación de instancias) de esa clase.
Una clase describe la constitución de un objeto y sirve como plantilla para construir
objetos, especificando la interfaz pública de un objeto. Una clase tiene un nombre y
especifica los miembros que pertenecen a la clase. Una vez que se define una clase, el
nombre de la clase se convierte en un nuevo tipo de dato y se puede utilizar para:

• declarar variables de ese tipo.


• crear objetos de ese tipo.

16
El siguiente ejemplo representa una clase Circulo que se utilizará para construir objetos
del tipo Círculo:

class Circulo
{
public double radio =5.0;
public double CaleularSuperficie()
{
return radio*radio*3.141592;
}
}
Esta clase Circulo es, simplemente, una definición que se utiliza para declarar y crear
objetos Circulo.

Las clases se declaran con el siguiente formato:

class Nombre
{
// cuerpo de la clase
}

El cuerpo de la clase define los miembros. Excepto en el caso de sobrecarga, todos los
miembros deben tener nombres distintos.

2.0.2 Objeto

Un objeto es una colección de datos y una serie de rutinas miembros, entre las que
destacan los métodos. Los objetos representan cosas físicas o abstractas, pero que
tienen un estado y un comportamiento. Por ejemplo, una mesa, un estudiante, un círculo,
una cuenta corriente, un préstamo, un automóvil,... se consideran objetos. Así, ciertas
propiedades definen a un objeto y ciertas propiedades definen lo que hace. Las
propiedades que definen el objeto se conocen como campos de datos y el
comportamiento de los objetos, es decir las acciones que deseamos que efectúen, se
define como métodos.

La sintaxis para declarar un objeto es:

NombreClase NombreObjeto;

Circulo myCirculo;

2.1 Atributos Const y Static

Es importante el lugar donde se efectúa la declaración de las variables, pues éste


determina su ámbito. Las variables miembros de una clase se denominan campos y,

17
cuando no llevan el modificador static, se asocian a una instancia de la clase, de forma
que cada instancia de la clase tiene una copia de dichas variables, por lo que reciben el
nombre de variables de instancia. Cuando los campos se declaran static se asocian con la
propia clase y reciben el nombre de variables de clase, esto quiere decir que no se hará
una copia de estas variables para cada uno de los objetos de la clase y, por lo tanto, su
valor será compartido por todas las instancias de la misma. Las variables que se declaran
en métodos, propiedades, o indexadores se denominan variables locales. Las variables
locales sólo existirán y se podrá hacer referencia a ellas dentro del cuerpo del método,
propiedad o indexador donde han sido declaradas. Además, en C# es posible agrupar
sentencias simples, encerrándolas entre llaves para formar bloques o sentencias
compuestas y efectuar declaraciones de variables dentro de dichos bloques, pero habrá
de tener en cuenta que su ámbito será el bloque en el que han sido declaradas u otros
interiores a él. Es decir, que si un bloque tuviera otros anidados, no se podrán declarar
variables en los bloques interiores con el nombre de otras de ámbito exterior.

Ejemplo

int i = 25;
double j = Math.Sqrt(20);
i++;
j += 5;
MessageBox.Show(i+" "+j);
// A continuación comienza un bloque
{
int aux = i;
i= (int) (j) ; //es necesaria una conversión explícita
j= aux; //se efectúa una conversión implícita
MessageBox.Show(i+" "+j+" "+aux);

}
//Fin del bloque
MessageBox.Show(i+" "+j); // aux aquí no está definida

Es necesario destacar que la ubicación en memoria de las variables depende no sólo de


su tipo, sino también del lugar donde han sido declaradas y las variables miembro de una
clase (campos), aunque sean de un tipo simple, se alojan en el montículo.
Variables fijas y móviles. En C# se dice que una variable es fija cuando no se ve afectada
por el recolector de basura y móvil en caso contrario. Las variables locales son variables
fijas, mientras que los campos de los objetos son variables móviles que el recolector de
basura puede reubicar o liberar.

CONSTANTES

Las constantes son datos cuyo valor se establece en tiempo de compilación y no puede
variar durante la ejecución de un programa. En un programa pueden aparecer constantes
de dos tipos: literales y simbólicas. Las constantes simbólicas o con nombre representan
datos permanentes que nunca cambian y se declaran como las variables, pero
inicializándose en el momento de la declaración y comenzando dicha declaración con la
palabra reservada const, que sirve para que el valor asignado no pueda ser modificado.

18
Pueden declararse constantes tanto campos como variables locales. El tipo de una
constante puede ser cualquiera, pero a una constante de un tipo referencia, que no sea
una cadena, sólo se le podrá asignar el valor null.

Cuando se necesite un nombre simbólico para un valor constante que no pueda


establecerse en tiempo de compilación, en lugar de una constante, se declarará una
variable de sólo lectura (readonly). Las constantes de clase se declaran en el cuerpo de la
clase y fuera de todos los métodos; la declaración de una constante puede ir acompañada
por un conjunto de atributos, el modificador new y alguno de los siguientes modificadores
de acceso: public, protected internal, protected, internal, o private. Las constantes son
miembros estáticos, pero no pueden llevar el modificador static. La sintaxis para declarar
una constante es:
const tipoDato NOMBRE CONSTANTE = valor;

Ejemplos

1. const double PI = 3.141592;


superficie = radio * radio * PI;

2. class Prueba

public const string CADENA = "O."; void Método ( )


{
/ / ...
/ / ...
}
3. using System;
class EjConstSLect
{
//Compara constantes y campos de sólo lectura
class ConstSLect
{
//no se puede modificar el valor de una constante
const int X = 1;
//se establece el valor en una declaración de inicialización
readonly int y = 200;
public readonly int z;

//Constructores
public ConstSLect()
{
// se establece su valor en el constructor
z = 6;
}

public ConstSLect(int pl, int p2)


{
// se anula el valor establecido en la declaración de inicialización
y = pl; z = p2;

19
}

public int Suma()


{
return x+y+z;
}
}

public static void Main()


{
ConstSLect instancial= new ConstSLect();
/* un campo de sólo lectura sólo puede ser asignado en un constructor o en una
declaración de inicialización y la sentencia siguiente sería incorrecta
instancial.z = 4; */
Console.WriteLine(instancial.Suma()) ;
ConstSLect instancia2 = new ConstSLect(2,3);
Console.WriteLine(instancia2.Suma());
}

Las constantes literales son valores de un determinado tipo escritos directamente en un


programa. Dichas constantes podrán ser enteras, reales, lógicas, carácter, cadena de
caracteres, o el valor null.

Constantes enteras

Las constantes enteras representan números enteros con y sin signo. La escritura de
constantes enteras en un programa debe seguir unas determinadas reglas:

• No utilizar comas ni signos de puntuación en números enteros. 123456 en lugar de


123.456.
• Cuando un literal entero es válido para varios tipos, su tipo se elige siguiendo el
siguiente orden int, uint, long, ulong.
• Puede añadirse una L o l al final del número para especificar que se trata de un
long 123456L.
• Puede añadirse una U o u al final del número para especificar su pertenencia a los
tipos uint o ulong.
• Si se trata de un número en base hexadecimal deberá comenzar por 0(cero) y
a continuación, llevar la letra x. 0x123 es una constante entera en hexadecimal.

Constantes reales

Una constante flotante representa un número real, siempre tiene signo y puede
representar tanto aproximaciones (coma flotante) como valores exactos (decimal). Las
constantes reales tienen el tipo double por defecto, aunque también pueden ir seguidas
por una da D que especifique su pertenencia a dicho tipo.

Cuando se les añade una f o F se obliga a que sean de tipo float y una m o M indican un
decimal.

20
double dr =-3.0m;

la sentencia anterior es incorrecta, no puede asignarse directamente un decimal a una


variable de tipo coma flotante

Constantes lógicas

Las constantes literales de tipo lógico disponibles en C# son true (verdadero) y false
(falso).

Constantes de tipo carácter

Una constante de tipo carácter es un carácter Unicode válido encerrado entre comillas
simples. Los caracteres que pueden considerarse válidos son:

• Los caracteres simples, excepto ', \ y el carácter de nueva línea.


• Secuencias de escape simples, que se usan para representar ciertas constantes de
tipo carácter: \' \" \\ \0 \a \b \f \n \r \t \v.
• Secuencias de escape hexadecimales: \x seguido por cuatro dígitos hexadecimales.
Por ejemplo: '\x0061'.
• Secuencias de escape Unicode: \u seguido por cuatro dígitos hexadecimales o \U
seguido por ocho dígitos hexadecimales. Por ejemplo: '\U00000061' o '\u0061'.

Constantes de tipo cadena

Una constante literal de tipo cadena en C# está constituida por una serie de caracteres
Unicode, entre los que pueden aparecer secuencias de escape, encerrados entre comillas
dobles o bien por un signo @ seguido de una serie de caracteres Unicode encerrados
entre comillas. En un literal de cadena que comienza por el signo @ no se procesan las
secuencias de escape y todos los caracteres especificados entre las dobles comillas son
interpretados literalmente.

Ejemplo

// se especifican secuencias de escape


string doslineas = "1 2 3\r\n4 5 6";
/*
se escribe la cadena literalmente
y precedida por el car cter @
*/
string otrasdos= @"1 2 3 4 5 6";

2.2 Concepto de método y 2.3 Declaración de métodos

21
Los métodos son los miembros de un tipo clase donde se especifican las acciones que se
realizan por un objeto. Una invocación a un método es una petición al método para que
ejecute su acción y lo haga con el objeto mencionado. La invocación de un método se
denominaría también llamar a un método y pasar un mensaje a un objeto.

Nota: Existen dos tipos de métodos: aquellos que devuelven un único valor y aquellos
que realizan alguna acción distinta de devolver un valor. Los métodos que realizan alguna
acción distinta de devolver un valor se denominan métodos void.

La implementación de los métodos podría ser como ésta:

public class CuentaCorriente


{
private double saldo;

public void Depositar(double cantidad)


{
saldo = saldo + cantidad;
}

public void Retirar(double cantidad)


{
saldo = saldo - cantidad;
}
public double ObtenerSaldo()
{
return saldo;
}
}

2.4 Llamadas a métodos.

La llamada o invocación a un método se puede realizar de dos formas, dependiendo de


que el método devuelva o no un valor.
1. Si el método devuelve un valor, la llamada al método se trata normalmente como un
valor. Por ejemplo:

Cantidad = myCuentaCorriente.ObtenerSaldo();

2. Si el método devuelve void, una llamada al método debe ser una sentencia. Por
ejemplo.

myCuentaCorriente.Depositar(5000)

Cuando un programa llama a un método, el control del programa se transfiere al método


llamado. Un método llamado devuelve el control al llamador cuando se ejecute su
sentencia return o cuando se alcance la llave de cierre ( } ).

22
2.5 Tipos de métodos.

2.5.1 Métodos Const y Static.

En C# no hay métodos Const.

Métodos Static:

Utilice el modificador static para declarar un miembro estático, que pertenece al propio
tipo en vez de a un objeto específico. El modificador static puede utilizarse con clases,
campos, métodos, propiedades operadores y eventos, pero no puede utilizarse con
indizadores, destructores o tipos que no sean clases. Por ejemplo, la siguiente clase se
declara como static y solo contiene métodos static:
static class CompanyEmployee
{
public static string GetCompanyName(string name) { ... }
public static string GetCompanyAddress(string address) { ... }
}

Comentario
• Una declaración de constante o tipo constituye, implícitamente, un miembro
estático.

• No se puede hacer referencia a un miembro estático por medio de una instancia.


En vez de ello, se debe hacer referencia por medio del nombre de tipo. Por
ejemplo, considere la siguiente clase:
public class MyBaseC
{
public struct MyStruct
{
public static int x = 100;
}
}

Para referirse al miembro estático x, use el nombre completo (a menos que sea accesible
desde el mismo ámbito):
MyBaseC.MyStruct.x

23
• Mientras que una instancia de una clase contiene una copia independiente de
todos los campos de instancia de la clase, sólo existe una copia de cada campo
estático.

• No es posible utilizar this para hacer referencia a descriptores de acceso de


propiedades o métodos static.

• Si la palabra clave static se aplica a una clase, todos los miembros de la clase
deben ser estáticos.

• Las clases, incluidas las clases estáticas, pueden tener constructores estáticos. Se
llama a los constructores estáticos en algún momento comprendido entre el inicio
del programa y la creación de instancias de la clase.

Para comprender el uso de miembros estáticos, considere una clase que representa al
empleado de una compañía. Suponga que la clase contiene un método que cuenta
empleados y un campo que almacena el número de empleados. Ni el método ni el campo
pertenecen a ninguna instancia de empleado. En vez de ello, pertenecen a la clase
compañía. Por tanto, se deberían declarar como miembros estáticos de la clase.

Ejemplo
Este ejemplo lee el nombre y el identificador de un nuevo empleado, incrementa en uno el
contador de empleados y muestra la información del nuevo empleado, así como el nuevo
número de empleados. Por motivos de simplicidad, el programa lee el número actual de
empleados desde el teclado. En una aplicación real, esta información se leería desde un
archivo.
En el siguiente ejemplo vamos a usar el modo consola solo para uso de explicación en
clase.

// cs_static_keyword.cs
using System;
public class Employee
{
public string id;
public string name;

public Employee() { }

public Employee(string name, string id)


{
this.name = name;
this.id = id;
}

public static int employeeCounter;


public static int AddEmployee()
{
return ++employeeCounter;
}

24
}

class MainClass : Employee


{
static void Main()
{
Console.Write("Enter the employee's name: "); string name = Console.ReadLine();
Console.Write("Enter the employee's ID: "); string id = Console.ReadLine();
// Create and configure the employee object:
Employee e = new Employee(name, id);
Console.Write("Enter the current number of employees: ");
String n = Console.ReadLine();
Employee.employeeCounter = Int32.Parse(n);
Employee.AddEmployee();
// Display the new information:
Console.WriteLine("Name: {0}", e.name);
Console.WriteLine("ID: {0}", e.id);
Console.WriteLine("NewNumber of Employees: {0}", Employee.employeeCounter);
}
}

Entrada
Tara Strahan
AF643G
15

Resultados del ejemplo


Enter the employee's name: Tara Strahan
Enter the employee's ID: AF643G
Enter the current number of employees: 15
Name: Tara Strahan
ID: AF643G
New Number of Employees: 16

2.6 Referencia This

La palabra clave this hace referencia a la instancia actual de la clase.


A continuación, se indican algunos usos comunes de this:
Obtener acceso a miembros con el fin de evitar ambigüedades con nombres similares, por
ejemplo:

public Employee(string name, string alias)


{
this.name = name;
this.alias = alias;
}

25
• Pasar un objeto como parámetro a otros métodos, por ejemplo, para:

CalcTax(this);

• Declarar indizadores, por ejemplo:

public int this [int param] {


get
{
return array[param];
}
set
{
array[param] = value;
}
}

Debido a que las funciones miembro estáticas existen en el nivel de clase y no como parte
de un objeto, no tienen un puntero this. Es un error hacer referencia a this en un método
estático.

Ejemplo
En este ejemplo, this se utiliza para calificar los miembros de la clase Employee, name y
alias, que presentan ambigüedad con nombres similares. También se utiliza para pasar un
objeto al método CalcTax, el cual pertenece a otra clase.

// keywords_this.cs
// this example
using System;
class Employee
{
private string name;
private string alias;
private decimal salary = 3000.00;
// Constructor:
public Employee(string name, string alias)
{
// Use this to qualify the fields, name and alias:
this.name = name;
this.alias = alias;
}

// Printing method:
public void printEmployee()
{

26
Console.WriteLine("Name: {0}\nAlias: {1}", name, alias);
// Passing the object to the CalcTax method by using this:
Console.WriteLine("Taxes: {0:C}", Tax.CalcTax(this));
}

public decimal Salary


{
get
{
return salary;
}
}
}

class Tax
{
public static decimal CalcTax(Employee E)
{
return 0.08m * E.Salary,
}
}

class MainClass {
static void Main()
{
// Create objects:
Employee E1 = new Employee("John M. Trainer", "jtrainer");
// Display results:
E1.printEmployee();
}
}

Resultados
Name: John M. Trainer
Alias: jtrainer
Taxes: $240.00

2.7 Forma de pasar argumentos.

La cabecera de un método especifica el número y tipo de parámetros formales requeridos.


En el interior de una clase, un método se identifica no sólo por su nombre, sino también
por su lista de parámetros formales. Por consiguiente, el mismo nombre de método se
puede definir más de una vez con diferentes parámetros formales para conseguir la
sobrecarga de métodos.
Cuando se llama a un método se deben proporcionar un número y tipo correctos de
argumentos. Los argumentos incluidos en la llamada a un método se conocen como

27
argumentos (parámetros) reales o simplemente argumentos. La llamada a un método
exige proporcionarle parámetros reales (actuales) que se deben dar en el mismo orden
que la lista de parámetros formales en la especificación del método. Esta regla se conoce
como asociación del orden de los parámetros. C# soporta llamadas a un método con un
número variable de argumentos cuando el parámetro formal correspondiente es de tipo
array, va precedido por el modificador params y es el último especificado en la cabecera
del método.

Ejemplos

1. El método ImprimirN imprime un mensaje n veces

void ImprimirN(String mensaje, int n)


{
for (int i=0; i<n; i++)
System.Console.WriteLine(mensaje);
}

a. Invocación correcta

Una invocación ImprimirN ("Carchelejo" , 4) imprime la palabra Carchelejo cuatro veces.


El proceso es el siguiente
La invocación a ImprimirN pasa el parámetro actual cadena, "Carchelejo", al parámetro
formal mensaje y el parámetro actual 4 a la variable n .
Se imprime 4 veces la frase Carchelejo.

b. Invocación incorrecta

La sentencia ImprimirN (4, "Carchelejo") es incorrecta, ya que el tipo de dato 4 no coincide


con el tipo del parámetro mensaje y, de igual modo, el segundo parámetro "Carchelejo"
tampoco coincide con el tipo del segundo parámetro formal n.
2. El método ImprimirNX imprime n veces todos los argumentos que se le pasen como
parámetros a continuación del húmero de veces a iterar, que deben ser de tipo ushort o
implícitamente convertibles a ushort.

class Prueba
{
// conversión implícita
static void ImprimirNX(int n, params ushort[ ] x)

for (int i=0; i<n; i++)


{
foreach(char j in x)
System.Console.Write("{0} ", j);
//conversión explícita
System.Console.WriteLine();

28
}

public static void Main () {


ImprimirNX (3, 'H', 79, 76, 'A');
ImprimirNX(2);
ImprimirNX (1, 65, 68, 73, 79, 83 );
}

Salida

HoLAHoLAHOLA

Tres veces la palabra HOLA

A DIO S

Dos líneas en blanco Una vez la palabra ADIOS

La operación de enlazar (binding) los parámetros reales a los formales se denomina


paso de parámetros. Cuando se llama a un método con más de un argumento, dichos
argumentos se evalúan de modo secuencial, de izquierda a derecha. Existen dos tipos de
paso de parámetros: por valor y por referencia.

PASO DE PARÁMETROS POR VALOR

En C#, los parámetros pueden ser pasados por valor o por referencia. Cuando un
parámetro se pasa por valor, sus valores se copian en nuevas posiciones, que se pasan a
la subrutina; como consecuencia de esto, si el parámetro cambia su valor dentro del
método, propiedad, indexador, operador o constructor, dicho valor no cambiará en el
programa llamador original. Hay que tener en cuenta que las variables que se pasan
como parámetros pueden ser de tipo valor o de tipo referencia. Una variable de un tipo
valor almacena directamente los datos, esto significa que cuando el valor de la variable se
pasa a un parámetro formal, los datos se copian y su cambio en el interior del método no
repercute en el exterior. Una variable de un tipo de referencia no contiene los datos
directamente, sino una referencia al lugar donde se encuentran almacenados los mismos;
esto implica que, cuando se pasa por valor una variable de tipo referencia, es posible
devolver cambiados a los datos apuntados por la referencia, pero no el valor de la propia
referencia.

PASO DE PARÁMETROS POR REFERENCIA

El paso por referencia permite a los métodos, propiedades, indexadores, operadores, y


constructores devolver el valor de los parámetros modificado. Cuando se pasa un
parámetro por referencia no se copia el valor del parámetro actual en una nueva posición
de memoria, sino que se establece un nuevo nombre para el parámetro actual. Por
defecto, los parámetros se pasan por valor y para pasar un parámetro por referencia es
necesario emplear las palabras reservadas out o ref.

Ejemplo

29
En el siguiente programa objetos de la clase Quebrado se pasan como argumentos a los
métodos Simplificar, Intercambiar y Mostrar. El programa consta de dos partes:

• Primero comienza por simplificar un quebrado para mostrar el funcionamiento del


paso de parámetros por valor cuando los argumentos son de tipo referencia.
• Después, intercambia dos quebrados para que pueda observarse el
funcionamiento del paso de parámetros por referencia con argumentos de tipo
referencia.

using System;
class Quebrado
{
int num, den;
// Constructor
public Quebrado(int x, int y)
{
num = x;
den = y;
}

/* Variable referencia pasada por valor


Permite que el objeto referenciado se devuelva modificado */
public static void Simplificar(Quebrado q)
{
int a = q.num;
int b = q.den;
/* Cálculo del máximo común divisor entre a y b.
El máximo común divisor se obtiene aplicando el algoritmo
de Euclides, que dice que para obtener el máximo común divisor
entre dos números, se dividen y si el resto es cero el divisor
es el máximo común divisor, si el resto no es cero se
intercambian dividendo por divisor y divisor por resto,
repitiéndose la operación hasta obtener un resto cero,
en cuyo caso el divisor será el máximo común divisor */

while (b > 0)
{
int r = a % b;
a = b;
b = r;
}
/* Al terminar el bucle el máximo común divisor se encuentra
almacenado en a.
Para simplificar el quebrado se divide numerador y denominador
por su máximo común divisor */
q.num = q.num / a;
q.den = q.den / a;
}

30
/* Variable referencia pasada por referencia.
Permite que la referencia se devuelva modificada */
public static void Intercambiar(ref Quebrado q1, ref Quebrado q2)
{
Quebrado auxi = q1;
q1 = q2;
q2 = auxi;
}

/* Uso de params para permitir llamadas con un número variable


de argumentos */
public static void Mostrar(params Quebrado[] arr)
{
foreach (Quebrado q in arr)
Console.Write(" {0}/{1} ", q.num, q.den);
Console.WriteLine();
}
}

class PasoDePR
{
public static void Main ()
{
Quebrado q1 = new Quebrado(18, 45);
Quebrado.Mostrar(q1);
Quebrado.Simplificar(q1);
Quebrado.Mostrar(q1);
Quebrado q2 = new Quebrado(1, 2);
Quebrado.Mostrar(q1, q2);
Quebrado.Intercambiar(ref q1, ref q2);
Quebrado.Mostrar(q1, q2);
}
}

Resultado
2/5
2/5 1/2
1/2 2/5

Como se ha podido observar en el caso de ref, los modificadores ref y out permiten que
los cambios hechos en el parámetro se reflejen en su variable asociada. La diferencia
entre ambos está en que un parámetro ref necesita que el parámetro actual
correspondiente sea previamente inicializado, mientras que esto no es necesario cuando
se trata de un parámetro out.

Ejemplo

El programa siguiente ordena 3 números leídos desde teclado, utilizando dos métodos
auxiliares: Leer3 e Intercambiar. El método Leer3 devuelve al método Main los 3 números
introducidos desde teclado y permite mostrar el funcionamiento del modificador out. El

31
método Intercambiar es similar al expuesto en el ejemplo anterior (en la clase Quebrado),
pero ahora se utiliza para mostrar el funcionamiento del modificador ref con argumentos
de tipo valor. En Leer3 se capturan las posibles excepciones generadas (try) y como
tratamiento de las mismas (catch) se efectúa su lanzamiento (throw e), para que en Main
vuelvan a ser consideradas. En Main la excepción se captura de nuevo y no se establece
ningún tratamiento para la misma, pero así, si se produce una excepción en Leer3, el
programa principal no hace nada.

using System;

class PasoDePV
{
static void Intercambiar(ref int pl, ref int p2)
{
int auxi = pl;
pl = p2;
p2 = auxi;
}

static void Leer3(out int a, out int b, out int e)


{
a = b = e = 0;
try
{
Console.Write("lO ");
a = int.Parse(Console.ReadLine());
Console.Write("2° ");
b = int.Parse(Console.ReadLine());
Console.Write("3° ");
e = int.Parse(Console.ReadLine());
}
catch (Exception e)
{
throw e;
}
}

public static void Main ()


{
int a, b, e;
try
{
Console.WriteLine("Introdu ea tres n meros");
Leer3(out a, out b, out e);
if (a > b)
Intereambiar(ref a, ref b);
if (b > c)
Intercambiar(ref b, ref c);
if (a > b)

32
Intercambiar(ref a, ref b);
Console.WriteLine("Ordenados: {0} {l} {2}",a,b ,e);

}
catch { }
}
}

2.8 Devolver un valor desde un método.

Los métodos pueden devolver un valor al llamador. Si el tipo de valor devuelto (el que
aparece antes del nombre de método) no es void, el método puede devolver el valor
mediante la palabra clave return. Una instrucción con la palabra clave return, seguida de
un valor que coincida con el tipo de valor devuelto, devolverá ese valor al llamador del
método. La palabra clave return también detiene la ejecución del método. Si el tipo de
valor devuelto es void, una instrucción return sin ningún valor sigue siendo útil para
detener la ejecución del método. Sin la palabra clave return, el método detendrá la
ejecución cuando llegue al fin del bloque de código. Es necesario que los métodos con un
tipo de valor devuelto no nulo utilicen la palabra clave return para devolver un valor. Por
ejemplo, estos dos métodos utilizan la palabra clave return para devolver enteros:

class SimpleMath
{
public int AddTwoNumbers(int number1, int number2)
{
return number1 + number2;
}

public int SquareANumber(int number)


{
return number * number;
}
}

Para emplear un valor devuelto por un método, el método de llamada puede utilizar la
propia llamada del método en cualquier parte donde un valor del mismo tipo sea
suficiente. El valor devuelto también se puede asignar a una variable. Por ejemplo, los dos
ejemplos de código siguientes logran el mismo objetivo:

int result = obj.AddTwoNumbers(1, 2);


obj.SquareANumber(result);

33
obj.SquareANumber(obj.AddTwoNumbers(1, 2));

El uso de una variable intermedia, en este caso result, para almacenar un valor es
opcional. La legibilidad del código puede ser útil o puede ser necesaria si el valor se va a
utilizar más de una vez.

III Constructor y destructor

3.1 Conceptos de métodos constructor y destructor / 3.2 Declaración de métodos


constructor y destructor

Los constructores son métodos especiales que permiten controlar el proceso de


inicialización. Se ejecutan después de que el programa empiece o se cree una instancia
de un tipo. A diferencia de otros miembros, los constructores no se heredan y no
introducen ningún nombre en el espacio de declaración de un tipo. A los constructores
sólo se les puede invocar con expresiones de creación de objetos o con .NET Framework.
Nunca se pueden invocar directamente.

Una clase puede tener varios constructores que toman argumentos diferentes. Los
constructores permiten al programador establecer valores predeterminados, limitar la
creación de instancias y escribir código flexible y fácil de leer.
Si no proporciona un constructor para el objeto, C# creará uno de forma predeterminada
que crea instancias del objeto y establecer las variables miembro con los valores

34
predeterminados indicados

Tipo de
valor Valor predeterminado

bool false
byte 0
char '\0'
decimal 0,0M
double 0,0D
enum El valor producido por la expresión (E)0, donde E es el identificador de la
enumeración.
float 0,0F
int 0
long 0L
sbyte 0
short 0
struct El valor obtenido al asignar los valores predeterminados a los campos de tipo
de valor y el valor null a los campos de tipo de referencia.
uint 0
ulong 0
ushort 0

Los constructores son métodos de clase que se ejecutan cuando se crea un objeto de un
tipo determinado. Los constructores tienen el mismo nombre que la clase y, normalmente,
inicializan los miembros de datos del nuevo objeto.
En el ejemplo siguiente, una clase denominada Taxi se define con un constructor simple.
Esta clase crea instancias con el operador new. El operador new invoca el constructor
Taxi inmediatamente después de asignar la memoria al nuevo objeto.

public class Taxi


{
public bool isInitialized;
public Taxi()
{
isInitialized = true;
}
}

35
class TestTaxi
{
static void Main()
{
Taxi t = new Taxi();
System.Console.WriteLine(t.isInitialized);
}
}

Destructores

Los destructores se utilizan para destruir instancias de clases.


Comentarios
Los destructores no se pueden definir en estructuras. Sólo se utilizan con clases.
Una clase sólo puede tener un destructor.
Los destructores no se pueden heredar ni sobrecargar.
No se puede llamar a los destructores. Se invocan automáticamente.
Un destructor no permite modificadores de acceso ni tiene parámetros.
Por ejemplo, el siguiente código muestra una declaración de un destructor para la clase
Car:

class Car
{
~ Car() // destructor
{
// cleanup statements...
}
}

El destructor llama implícitamente al método Finalize en la case base del objeto. Por lo
tanto, el código de destructor anterior se traduce implícitamente a:

protected override void Finalize()


{
try
{
// cleanup statements...
}
finally
{
base.Finalize();
}
}

36
Esto significa que se llama al método Finalize de forma recursiva para todas las instancias
de la cadena de herencia, desde la más derivada hasta la menos derivada.
El programador no puede controlar cuándo se llama al destructor, porque esto lo
determina el recolector de elementos no utilizados. El recolector de elementos no
utilizados comprueba si hay objetos que ya no están siendo utilizados por ninguna
aplicación. Si considera un objeto elegible para su destrucción, llama al destructor (si
existe) y reclama la memoria utilizada para almacenar el objeto. También se llama a los
destructores cuando se cierra el programa.
Es posible forzar la recolección de elementos no utilizados llamando al método Collect,
pero en la mayoría de los casos debe evitarse su uso por razones de rendimiento.

3.3 Aplicaciones de constructores y destructores / 3.4 Tipos de constructores y


destructores

En general, C# no requiere tanta administración de memoria como se necesita al


desarrollar con un lenguaje que no está diseñado para un motor en tiempo de ejecución
con recolección de elementos no utilizados. Esto es debido a que el recolector de
elementos no utilizados de .NET Framework administra implícitamente la asignación y
liberación de memoria para los objetos. Sin embargo, cuando la aplicación encapsule
recursos no administrados como ventanas, archivos y conexiones de red, debería utilizar
destructores para liberar dichos recursos. Cuando el objeto se marca para su destrucción,
el recolector de elementos no utilizados ejecuta el método Finalize.

Ejemplo
En el siguiente ejemplo se crean tres clases que forman una cadena de herencia. La clase
First es la clase base, Second se deriva de First y Third se deriva de Second. Las tres
tienen destructores. En Main(), se crea una instancia de la clase más derivada. Cuando
ejecute el programa, observe que se llama a los destructores de las tres clases
automáticamente y en orden, desde la más derivada hasta la menos derivada.

class First
{
~First()
{
System.Console.WriteLine("First's destructor is called");
}
}

class Second: First


{
~Second()
{
System.Console.WriteLine("Second's destructor is called");
}
}

class Third: Second


{

37
~Third()
{
System.Console.WriteLine("Third's destructor is called");
}
}

class TestDestructors
{
static void Main()
{
Third t = new Third();
}
}

Constructores de instancias

Los constructores de instancia se utilizan para crear e inicializar instancias. El constructor


de clase se invoca al crear un objeto nuevo, por ejemplo:

class CoOrds
{
public int x, y;

// constructor
public CoOrds()
{
x = 0;
y = 0;
}
}

Se llama a este constructor cada vez que se crea un objeto basado en la clase CoOrds.
Un constructor como éste, que no toma ningún argumento, se denomina constructor
predeterminado. Sin embargo, suele ser útil proporcionar constructores adicionales. Por
ejemplo, se puede agregar a la clase CoOrds un constructor que permita especificar los
valores iniciales de los miembros de datos:

// A constructor with two arguments:


public CoOrds(int x, int y)
{
this.x = x;
this.y = y;
}

Esto permite crear objetos CoOrd con valores iniciales concretos o predeterminados, del
modo siguiente:

CoOrds p1 = new CoOrds();


CoOrds p2 = new CoOrds(5, 3);

38
Si una clase no tiene un constructor predeterminado, se genera uno automáticamente y
se utilizan los valores predeterminados para inicializar los campos del objeto, por ejemplo,
un campo de tipo int se inicializa en 0. Por consiguiente, como el constructor
predeterminado de la clase CoOrds inicializa todos los miembros de datos en cero, se
puede quitar del todo sin cambiar el funcionamiento de la clase.
Constructores privados
Un constructor private es un caso especial de constructor de instancia. Se utiliza
normalmente en clases que contienen sólo miembros estáticos. Si una clase tiene uno o
más constructores privados y ningún constructor público, el resto de clases (excepto las
anidadas) no tiene permiso para crear instancias de esta clase. Por ejemplo:

class NLog
{
// Private Constructor:
private NLog() { }
public static double e = System.Math.E; //2.71828...
}
La declaración de un constructor vacío evita la generación automática de un constructor
predeterminado. Observe que si no utiliza un modificador de acceso en el constructor,
éste será private de manera predeterminada. Sin embargo, normalmente se utiliza el
modificador private de manera explícita para aclarar que no es posible crear una instancia
de la clase.
Los constructores privados se utilizan para evitar la creación de instancias de una clase
cuando no hay campos o métodos de instancia, por ejemplo la clase Math, o cuando se
llama a un método para obtener una instancia de una clase. Si todos los métodos de la
clase son estáticos, considere convertir la clase completa en estática.

public class Counter


{
private Counter() { }
public static int currentCount;
public static int IncrementCount()
{
return ++currentCount;
}
}

class TestCounter
{
static void Main()
{
// If you uncomment the following statement, it will generate
// an error because the constructor is inaccessible:
// Counter aCounter = new Counter(); // Error

Counter.currentCount = 100;
Counter.IncrementCount();
System.Console.WriteLine("New count: {0}", Counter.currentCount);
}
}

39
Constructores estáticos

Un constructor estático se utiliza para inicializar cualquier dato estático o realizar una
acción determinada que sólo debe realizarse una vez. Es llamado automáticamente antes
de crear la primera instancia o de hacer referencia a cualquier miembro estático.

class SimpleClass
{
// Static constructor
static SimpleClass()
{
//...
}
}
Los constructores estáticos tienen las propiedades siguientes:
Un constructor estático no permite modificadores de acceso ni tiene parámetros.
Se le llama automáticamente para inicializar la clase antes de crear la primera instancia o
de hacer referencia a cualquier miembro estático.
El constructor estático no puede ser llamado directamente.
El usuario no puede controlar cuando se ejecuta el constructor estático en el programa.
Los constructores estáticos se utilizan normalmente cuando la clase hace uso de un
archivo de registro y el constructor escribe entradas en dicho archivo.
Los constructores estáticos también son útiles al crear clases contenedoras para código
no administrado, cuando el constructor puede llamar al método LoadLibrary.

Ejemplo
En este ejemplo, la clase Bus tiene un constructor estático y un miembro estático, Drive().
Cuando se llama a Drive(), se invoca el constructor estático para inicializar la clase.

public class Bus


{
// Static constructor:
static Bus()
{
System.Console.WriteLine("The static constructor invoked.");
}

public static void Drive()


{
System.Console.WriteLine("The Drive method invoked.");
}
}

class TestBus
{
static void Main()
{
Bus.Drive();
}
}

40
IV Sobrecarga

4.1 Conversión de tipos

La conversión entre tipos de datos se puede hacer de forma explícita utilizando una
conversión de tipos; en algunos casos, se permiten conversiones implícitas. Por ejemplo:

static void TestCasting()


{
int i = 10;
float f = 0;
f = i; // An implicit conversion, no data will be lost.
f = 0.5F;
i = (int) f; // An explicit conversion. Information will be lost.
}

Una conversión de tipos invoca de forma explícita al operador de conversión de un tipo a


otro. En la conversión de tipos se producirá un error si no se ha definido ninguno de estos
operadores. Puede escribir operadores de conversión personalizados para convertir entre
los tipos definidos por el usuario.

class Test
{

41
static void Main()
{
double x = 1234.7;
int a;
a = (int)x; // cast double to int
System.Console.WriteLine(a);
}
}

Resultado
1234

Convert (Clase)

Convierte un tipo de datos base en otro tipo de datos base.


public static class Convert

Comentarios
Esta clase devuelve un tipo cuyo valor es equivalente al valor de un tipo especificado. Los
tipos base que se admiten son Boolean, Char, SByte, Byte, Int16, Int32, Int64, UInt16,
UInt32, UInt64, Single, Double, Decimal, DateTime y String.
Existe un método de conversión para convertir todos y cada uno de los tipos base en los
demás tipos base. Sin embargo, la operación de conversión real efectuada queda incluida
en tres categorías:
• La conversión de un tipo en sí mismo devuelve dicho tipo. No se lleva a cabo
realmente ninguna conversión.
• La conversión que no puede producir un resultado significativo produce una
excepción InvalidCastException. No se lleva a cabo realmente ninguna
conversión. Las conversiones de Char en Boolean, Single, Double, Decimal o
DateTime, y de estos tipos en Char producen una excepción. Las conversiones
de DateTime en cualquier tipo excepto String, y de cualquier tipo excepto String
en DateTime producen una excepción.
• Los tipos base no descritos anteriormente pueden ser objeto de conversiones a
y desde cualquier otro tipo base.

No se producirá una excepción si la conversión de un tipo numérico produce una pérdida


de precisión, es decir, la pérdida de algunos de los dígitos menos significativos. Sin
embargo, la excepción se producirá si el resultado es mayor de lo que puede representar
el tipo de valor devuelto del método de conversión.
Por ejemplo, cuando un tipo Double se convierte en un tipo Single, se puede producir
una pérdida de precisión pero no se produce ninguna excepción. Sin embargo, si la
magnitud del tipo Double es demasiado grande para que un tipo Single lo represente, se
produce una excepción de desbordamiento.
Existe un conjunto de métodos que admiten la conversión de una matriz de bytes en y
desde un tipo String o una matriz de caracteres Unicode formada por dígitos de base 64.
Los datos expresados como dígitos de base 64 se pueden transmitir fácilmente en
canales de datos que sólo pueden transmitir caracteres de 7 bits.

42
Algunos de los métodos de esta clase toman un objeto de parámetro que implementa la
interfaz IFormatProvider. Este parámetro puede proporcionar información de formato
específica de la referencia cultural para ayudar en el proceso de conversión. Los tipos de
valor base pasan por alto este parámetro, pero los tipos definidos por el usuario que
implementan IConvertible pueden tenerlo en cuenta.
Para obtener más información sobre los tipos de valor base, vea el tema correspondiente
que aparece en la sección Vea también.

Ejemplo
En el siguiente ejemplo de código, se muestran algunos de los métodos de conversión de
la clase Convert, entre los que se incluyen ToInt32, ToBoolean y ToString.

double dNumber = 23.15;

try {
// Returns 23
int iNumber = System.Convert.ToInt32(dNumber);
}
catch (System.OverflowException) {
System.Console.WriteLine(
"Overflow in double to int conversion.");
}
// Returns True
bool bNumber = System.Convert.ToBoolean(dNumber);

// Returns "23.15"
string strNumber = System.Convert.ToString(dNumber);

try {
// Returns '2'
char chrNumber = System.Convert.ToChar(strNumber[0]);
}
catch (System.ArgumentNullException) {
System.Console.WriteLine("String is null");
}
catch (System.FormatException) {
System.Console.WriteLine("String length is greater than 1.");
}

// System.Console.ReadLine() returns a string and it


// must be converted.
int newInteger = 0;
try {
System.Console.WriteLine("Enter an integer:");
newInteger = System.Convert.ToInt32(
System.Console.ReadLine());
}
catch (System.ArgumentNullException) {
System.Console.WriteLine("String is null.");
}

43
catch (System.FormatException) {
System.Console.WriteLine("String does not consist of an " +
"optional sign followed by a series of digits.");
}
catch (System.OverflowException) {
System.Console.WriteLine(
"Overflow in string to int conversion.");
}

System.Console.WriteLine("Your integer as a double is {0}",


System.Convert.ToDouble(newInteger));

En el ejemplo de código siguiente se muestran algunos de los métodos de conversión de


la clase Convert.

// Sample for the Convert class summary.


using System;

class Sample
{
public static void Main()
{
string nl = Environment.NewLine;
string str = "{0}Return the Int64 equivalent of the following base types:{0}";
bool xBool = false;
short xShort = 1;
int xInt = 2;
long xLong = 3;
float xSingle = 4.0f;
double xDouble = 5.0;
decimal xDecimal = 6.0m;
string xString = "7";
char xChar = '8'; // '8' = hexadecimal 38 = decimal 56
byte xByte = 9;

// The following types are not CLS-compliant.


ushort xUshort = 120;
uint xUint = 121;
ulong xUlong = 122;
sbyte xSbyte = 123;

// The following type cannot be converted to an Int64.


// DateTime xDateTime = DateTime.Now;

Console.WriteLine(str, nl);
Console.WriteLine("Boolean: {0}", Convert.ToInt64(xBool));
Console.WriteLine("Int16: {0}", Convert.ToInt64(xShort));
Console.WriteLine("Int32: {0}", Convert.ToInt64(xInt));
Console.WriteLine("Int64: {0}", Convert.ToInt64(xLong));
Console.WriteLine("Single: {0}", Convert.ToInt64(xSingle));
Console.WriteLine("Double: {0}", Convert.ToInt64(xDouble));

44
Console.WriteLine("Decimal: {0}", Convert.ToInt64(xDecimal));
Console.WriteLine("String: {0}", Convert.ToInt64(xString));
Console.WriteLine("Char: {0}", Convert.ToInt64(xChar));
Console.WriteLine("Byte: {0}", Convert.ToInt64(xByte));
Console.WriteLine("DateTime: There is no example of this conversion because");
Console.WriteLine(" a DateTime cannot be converted to an Int64.");
//
Console.WriteLine("{0}The following types are not CLS-compliant.{0}", nl);
Console.WriteLine("UInt16: {0}", Convert.ToInt64(xUshort));
Console.WriteLine("UInt32: {0}", Convert.ToInt64(xUint));
Console.WriteLine("UInt64: {0}", Convert.ToInt64(xUlong));
Console.WriteLine("SByte: {0}", Convert.ToInt64(xSbyte));
}
}
/*
This example produces the following results:

Return the Int64 equivalent of the following base types:

Boolean: 0
Int16: 1
Int32: 2
Int64: 3
Single: 4
Double: 5
Decimal: 6
String: 7
Char: 56
Byte: 9
DateTime: There is no example of this conversion because
a DateTime cannot be converted to an Int64.

The following types are not CLS-compliant.

UInt16: 120
UInt32: 121
UInt64: 122
SByte: 123
*/

4.2 Sobrecarga de métodos

• La sobrecarga de los métodos permite que una clase, estructura o interfaz declare
varios métodos con el mismo nombre, siempre que sus firmas sean únicas dentro de
esa clase, estructura o interfaz.

• La sobrecarga de los constructores de instancias permite que una clase o una


estructura declare varios constructores de instancias, a condición de que sus firmas
sean únicas dentro de esa clase o estructura.

45
• La sobrecarga de los indizadores permite que una clase, estructura o interfaz
declare varios indizadores, siempre que sus firmas sean únicas dentro de esa clase,
estructura o interfaz.

• La sobrecarga de los operadores permite que una clase o una estructura declare
varios operadores con el mismo nombre, siempre que sus firmas sean únicas dentro
de esa clase o estructura.

El siguiente ejemplo muestra un conjunto de declaraciones de métodos sobrecargados.

void F(); // F()

void F(int x); // F(int)

void F(ref int x); // F(ref int)

void F(int x, int y); // F(int, int)

int F(string s); // F(string)

int F(int x); // F(int) error

void F(string[] a); // F(string[])

void F(params string[] a);

Se debe tener en cuenta que los modificadores de parámetro ref y out forman parte de
una firma. Por lo tanto, F(int) y F(ref int) son firmas únicas. Asimismo, el tipo del valor
devuelto y el modificador params no forman parte de una firma, por lo que no es posible
sobrecargar basándose exclusivamente en el tipo de valor devuelto o en la inclusión o
exclusión del modificador params. Como tales, las declaraciones de los métodos F(int) y
F(params string[]) anteriormente identificadas producen un error en tiempo de
compilación.

4.3 Sobrecarga de operadores

Sobrecarga de operadores
Todos los operadores unarios y binarios tienen implementaciones predefinidas que están
disponibles automáticamente en cualquier expresión. Además de las implementaciones
predefinidas, pueden introducirse implementaciones definidas por el usuario si se incluyen
declaraciones operator en las clases y estructuras. Las implementaciones de operador
definidas por el usuario siempre tienen precedencia sobre las implementaciones de
operador predefinidas: sólo se consideran las implementaciones de operador predefinidas
cuando no existen implementaciones de operador definidas por el usuario que puedan
aplicarse.

Los operadores unarios sobrecargables son:

+ - ! ~ ++ -- true false

46
Aunque true y false no se utilizan explícitamente en las expresiones, se consideran
operadores porque se invocan en varios contextos de expresión: expresiones de tipo
Boolean, expresiones con condicionales y operadores lógicos condicionales.

Los operadores binarios sobrecargables son:

+ - * / % & | ^ << >> == != > < >= <=

Sólo los operadores mencionados pueden sobrecargarse. En concreto, no es posible


sobrecargar accesos a miembros, llamadas a métodos o los operadores =, &&, ||, ?:,
checked, unchecked, new, typeof, as e is.

Cuando se sobrecarga un operador binario, el operador correspondiente de asignación (si


existe) también se sobrecarga de modo implícito. Por ejemplo, una sobrecarga del
operador * también es una sobrecarga del operador *=. Debe tenerse en cuenta que el
propio operador de asignación (=) no se puede sobrecargar. Una asignación siempre
realiza una simple copia bit a bit de un valor en una variable.

Las operaciones de conversión de tipo, como (T)x, se sobrecargan proporcionando


conversiones definidas por el usuario.

El acceso a elementos, del tipo a[x], no se considera un operador sobrecargable. En lugar


de ello, se acepta la indización definida por el usuario mediante indizadores.

En las expresiones, las referencias a los operadores se realizan mediante la notación de


operadores y, en las declaraciones, las referencias a los operadores se realizan mediante
la notación funcional. En la tabla siguiente se muestra la relación entre las notaciones de
operador y funcional para los operadores unarios y binarios. En la primera entrada, op
denota cualquier operador de prefijo unario sobrecargable. En la segunda entrada, op
denota los operadores de sufijo unarios ++ y --. En la primera entrada, op denota
cualquier operador binario sobrecargable.

Notación de operador Notación funcional

op x operator op(x)

x op operator op(x)

x op y operator op(x, y)

Las declaraciones de operador definidas por el usuario siempre requieren que por lo
menos uno de los parámetros sea del tipo de la clase o estructura que contiene la
declaración del operador. Por lo tanto, no es posible que un operador definido por el
usuario tenga la misma firma que un operador predefinido.

Las declaraciones de operador definidas por el usuario no pueden modificar la sintaxis,


precedencia o asociatividad de un operador. Por ejemplo, el operador / siempre es un
operador binario, siempre tiene el nivel de precedencia especificado en la y siempre es
asociativo por la izquierda.

47
Aunque es posible que un operador definido por el usuario realice cualquier cálculo que le
interese, no se recomiendan las implementaciones que generan resultados distintos de
los que intuitivamente pueden esperarse. Por ejemplo, una implementación de operator
== debe comparar la igualdad de los dos operandos y devolver un resultado bool
apropiado.

La sobrecarga de operadores permite utilizar implementaciones de operadores definidas


por el usuario en operaciones en las que al menos uno de los operandos es de un tipo
estructura o clase definido por el usuario. El primer ejemplo muestra cómo utilizar la
sobrecarga de operadores para crear una clase de números complejos que define la
suma compleja. El segundo ejemplo muestra cómo utilizar la sobrecarga de operadores
para implementar un tipo lógico de tres valores.

Ejemplo 1

Este ejemplo muestra cómo utilizar la sobrecarga de operadores para crear una clase de
números complejos Complex que define la suma compleja. El programa muestra las
partes real e imaginaria de los números y el resultado de la suma mediante un método
sustituto del método ToString.

// complex.cs
using System;

public struct Complex


{
public int real;
public int imaginary;

public Complex(int real, int imaginary)


{
this.real = real;
this.imaginary = imaginary;
}

// Declare which operator to overload (+), the types


// that can be added (two Complex objects), and the
// return type (Complex):
public static Complex operator +(Complex c1, Complex c2)
{
return new Complex(c1.real + c2.real, c1.imaginary + c2.imaginary);
}

// Override the ToString method to display an complex number in the suitable format:
public override string ToString()
{
return(String.Format("{0} + {1}i", real, imaginary));
}

public static void Main()


{
Complex num1 = new Complex(2,3);

48
Complex num2 = new Complex(3,4);

// Add two Complex objects (num1 and num2) through the


// overloaded plus operator:
Complex sum = num1 + num2;

// Print the numbers and the sum using the overriden ToString method:
Console.WriteLine("First complex number: {0}",num1);
Console.WriteLine("Second complex number: {0}",num2);
Console.WriteLine("The sum of the two numbers: {0}",sum);

}
}

Resultado
First complex number: 2 + 3i
Second complex number: 3 + 4i
The sum of the two numbers: 5 + 7i

Ejemplo 2

Este ejemplo muestra cómo utilizar la sobrecarga de operadores para implementar un tipo
lógico de tres valores. Los valores posibles de este tipo son DBBool.dbTrue,
DBBool.dbFalse y DBBool.dbNull, donde el miembro dbNull indica un valor desconocido.
Nota Los operadores True y False definidos aquí sólo resultan de utilidad para tipos que
representan valores True, False y Null (ni True ni False), como los utilizados en bases de
datos.

// dbbool.cs
using System;

public struct DBBool


{
// The three possible DBBool values:
public static readonly DBBool dbNull = new DBBool(0);
public static readonly DBBool dbFalse = new DBBool(-1);
public static readonly DBBool dbTrue = new DBBool(1);
// Private field that stores -1, 0, 1 for dbFalse, dbNull, dbTrue:
int value;

// Private constructor. The value parameter must be -1, 0, or 1:


DBBool(int value)
{
this.value = value;
}

// Implicit conversion from bool to DBBool. Maps true to


// DBBool.dbTrue and false to DBBool.dbFalse:

49
public static implicit operator DBBool(bool x)
{
return x? dbTrue: dbFalse;
}

// Explicit conversion from DBBool to bool. Throws an


// exception if the given DBBool is dbNull, otherwise returns
// true or false:
public static explicit operator bool(DBBool x)
{
if (x.value == 0) throw new InvalidOperationException();
return x.value > 0;
}

// Equality operator. Returns dbNull if either operand is dbNull,


// otherwise returns dbTrue or dbFalse:
public static DBBool operator ==(DBBool x, DBBool y)
{
if (x.value == 0 || y.value == 0) return dbNull;
return x.value == y.value? dbTrue: dbFalse;
}

// Inequality operator. Returns dbNull if either operand is


// dbNull, otherwise returns dbTrue or dbFalse:
public static DBBool operator !=(DBBool x, DBBool y)
{
if (x.value == 0 || y.value == 0) return dbNull;
return x.value != y.value? dbTrue: dbFalse;
}

// Logical negation operator. Returns dbTrue if the operand is


// dbFalse, dbNull if the operand is dbNull, or dbFalse if the
// operand is dbTrue:
public static DBBool operator !(DBBool x)
{
return new DBBool(-x.value);
}

// Logical AND operator. Returns dbFalse if either operand is


// dbFalse, dbNull if either operand is dbNull, otherwise dbTrue:
public static DBBool operator &(DBBool x, DBBool y)
{
return new DBBool(x.value < y.value? x.value: y.value);
}

// Logical OR operator. Returns dbTrue if either operand is


// dbTrue, dbNull if either operand is dbNull, otherwise dbFalse:
public static DBBool operator |(DBBool x, DBBool y)
{
return new DBBool(x.value > y.value? x.value: y.value);
}

50
// Definitely true operator. Returns true if the operand is
// dbTrue, false otherwise:
public static bool operator true(DBBool x)
{
return x.value > 0;
}

// Definitely false operator. Returns true if the operand is


// dbFalse, false otherwise:
public static bool operator false(DBBool x)
{
return x.value < 0;
}

// Overload the conversion from DBBool to string:


public static implicit operator string(DBBool x)
{
return x.value > 0 ? "dbTrue"
: x.value < 0 ? "dbFalse"
: "dbNull";
}

// Override the Object.Equals(object o) method:


public override bool Equals(object o)
{
try
{
return (bool) (this == (DBBool) o);
}
catch
{
return false;
}
}

// Override the Object.GetHashCode() method:


public override int GetHashCode()
{
return value;
}

// Override the ToString method to convert DBBool to a string:


public override string ToString()
{
switch (value)
{
case -1:
return "DBBool.False";
case 0:
return "DBBool.Null";

51
case 1:
return "DBBool.True";
default:
throw new InvalidOperationException();
}
}
}

class Test
{
static void Main()
{
DBBool a, b;
a = DBBool.dbTrue;
b = DBBool.dbNull;

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


Console.WriteLine( "!{0} = {1}", b, !b);
Console.WriteLine( "{0} & {1} = {2}", a, b, a & b);
Console.WriteLine( "{0} | {1} = {2}", a, b, a | b);
// Invoke the true operator to determine the Boolean
// value of the DBBool variable:
if (b)
Console.WriteLine("b is definitely true");
else
Console.WriteLine("b is not definitely true");
}
}
Resultado
!DBBool.True = DBBool.False
!DBBool.Null = DBBool.Null
DBBool.True & DBBool.Null = DBBool.Null
DBBool.True | DBBool.Null = DBBool.True
b is not definitely true

En las reglas siguientes se describen las pautas para sobrecargar operadores:

• Defina los operadores en tipos de valor que sean tipos de lenguajes lógicos integrados,
como la Estructura System.Decimal.

• Proporcione métodos de sobrecarga de operadores sólo en la clase en la que se definen


los métodos. El compilador de C# cumple esta directriz.

• Utilice las convenciones de firma y de nomenclatura descritas en Common Language


Specification (CLS). El compilador de C# lo hace automáticamente.

• Utilice la sobrecarga de operadores en los casos en los que el resultado de la operación


es obvio. Por ejemplo, tiene sentido poder restar un valor Time de otro valor Time y

52
obtener un TimeSpan. No obstante, no es adecuado utilizar el operador or para crear la
unión de dos consultas a la base de datos, o utilizar shift para escribir una secuencia.

• Sobrecargue los operadores de forma simétrica. Por ejemplo, si sobrecarga el operador


de igualdad (==), también debe sobrecargar el operador distinto de (!=).

• Proporcione firmas alternativas. La mayoría de los lenguajes no son compatibles con la


sobrecarga de operadores. Por esta razón, es un requisito de CLS que todos los tipos que
sobrecargan operadores incluyan un método secundario con un nombre apropiado
específico al dominio que proporcione la funcionalidad equivalente. La inclusión de un
método secundario es un requisito de Common Language Specification (CLS). El ejemplo
siguiente es compatible con CLS.

public struct DateTime


{
public static TimeSpan operator -(DateTime t1, DateTime t2) { }
public static TimeSpan Subtract(DateTime t1, DateTime t2) { }
}

V Herencia

5.1 Introducción a la herencia

Las clases pueden heredar de otra clase. Para conseguir esto, se coloca un signo de dos
puntos después del nombre de la clase al declarar la clase y se denomina la clase de la
cual se hereda (la clase base) después del signo de dos puntos, del modo siguiente:

public class A
{
public A() { }
}

public class B : A
{
public B() { }
}

La nueva clase (la clase derivada) obtiene todos los datos no privados y el
comportamiento de la clase base, además de todos los demás datos y comportamientos
que define para sí misma. La nueva clase tiene dos tipos efectivos: el tipo de la nueva
clase y el tipo de la clase que hereda.

Una clase extendida hereda todos los miembros de su clase base, excepto constructores

53
y destructores, y añade nuevos miembros específicos. En esencia, una subclase hereda
las variables y métodos de su clase base y de todos sus ascendientes. La subclase puede
utilizar estos miembros, puede ocultar las variables miembro o anular (redefinir) los
métodos. La palabra reservada this permite hacer referencia a la propia clase, mientras
que base se utiliza para referenciar a la clase base y poder llamar a métodos de la misma
(aunque estén redefinidos). Ninguna de estas palabras reservadas pueden utilizarse
desde métodos estáticos.

5.2 Herencia simple

En C# sólo se permite la herencia simple. En otras palabras, una clase puede heredar la
implementación de una sola clase base. Sin embargo, una clase puede implementar más
de una interfaz. La siguiente tabla muestra ejemplos de herencia de clases e
implementación de interfaces:

Herencia Ejemplo

Ninguna class ClassA { }


Simple class DerivedClass: BaseClass { }
Ninguna, implementa dos interfaces class ImplClass: IFace1, IFace2 { }
Simple, implementa una interfaz class ImplDerivedClass: BaseClass, IFace1 { }
Los niveles de acceso protected y private sólo se permiten en clases anidadas.

Una clase puede contener declaraciones de los siguientes miembros:

• Constructores

• Destructores

• Constantes

• Campos

• Métodos

• Propiedades

• Indizadores

• Operadores

• Eventos

• Delegados

• Clases

• Interfaces

54
• Estructuras

Ejemplo
En el siguiente ejemplo se muestra la declaración de campos de clase, constructores y
métodos. También muestra la creación de instancias de objetos y la impresión de datos
de instancia. En este ejemplo, se declaran dos clases, la clase Kid, que contiene dos
campos privados (name y age) y dos métodos públicos. La segunda clase, MainClass, se
utiliza para contener Main.

// keyword_class.cs
// class example using System;
class Kid
{
private int age;
private string name;
/ / Default constructor:
public Kid() { name = "N/A"; }
// Constructor:
public Kid(string name, int age)
{
this.name = name;
this.age = age;
}

// Printing method:
public void PrintKid() {
Console.WriteLine("{0}, {1} years old.", name, age);
}
}

class MainClass
{
static void Main()
{
// Create objects
// Objects must be created using the new operator:
Kid kid1 = new Kid("Craig", 11);
Kid kid2 = new Kid("Sally", 10);
// Create an object using the default constructor:
Kid kid3 = new Kid();
// Display results:
Console.Write("Kid #1: ");
kid1.PrintKid(); Console.Write("Kid #2: ");
kid2.PrintKid();
Console.Write("Kid #3: "); kid3.PrintKid();
}
}

Regla: Los constructores y destructores no se heredan por la subclase.

55
5.4 Clase base y clase derivada

5.4.1 Definición y 5.4.2 Declaracion


La herencia se realiza a través de una derivación, lo que significa que una clase se
declara utilizando una clase base de la cual hereda los datos y el comportamiento. Una
clase base se especifica anexando dos puntos y el nombre de la clase base a
continuación del nombre de la clase derivada.

Ejemplo

En el ejemplo siguiente, se define una clase pública que contiene un campo único, un
método y un método especial denominado constructor. Luego se crean instancias de la
clase con la palabra clave new.

public class Person


{
// Field
public string name;

// Constructor
public Person()
{
name = "unknown";
}

// Method
public void SetName(string newName)
{
name = newName;
}
}
class TestPerson
{
static void Main()
{
Person person1 = new Person();
System.Console.WriteLine(person1.name);

person1.SetName("John Smith");
System.Console.WriteLine(person1.name);
}
}

5.5 Parte protegida

5.5.1 Propósito de la parte protegida


La palabra clave protected es un modificador de acceso a miembros. Un miembro
protegido es accesible dentro de su clase y por clases derivadas.
Un miembro protegido de una clase base es accesible en una clase derivada sólo si el
acceso se realiza a través del tipo de la clase derivada. Por ejemplo, considere el
siguiente segmento de código:

56
// protected_keyword.cs
using System;
class A {
protected int x = 123;
}

class B : A {
static void Main() {
A a = new A();
B b = new B();
// Error CS1540, because x can only be accessed by
// classes derived from A.
// a.x = 10;
// OK, because this class derives from A.
b.x = 10;
}
}

La instrucción a.x =10 genera un error, ya que A no se deriva de B.


Los miembros de una estructura no se pueden proteger, ya que la estructura no se puede
heredar.

Ejemplo
En este ejemplo, la clase DerivedPoint se deriva de Point; por lo tanto, puede obtener
acceso a los miembros protegidos de la clase base directamente desde la clase derivada.

// protected_keyword_2.cs
using System;
class Point
{
protected int x;
protected int y;
}

class DerivedPoint: Point


{
static void Main()
{
DerivedPoint dp = new DerivedPoint();
// Direct access to protected members:
dp.x = 10;
dp.y = 15;
Console.WriteLine("x = {0}, y = {1}", dp.x, dp.y);
}
}

Resultados

57
x = 10, y = 15

5.6 Redefinición de los miembros de las clases derivadas


Virtual
La palabra clave virtual se utiliza para modificar un método, propiedad, indizador o
declaración de evento y permite reemplazar a cualquiera de estos en una clase derivada.
En el siguiente ejemplo, cualquier clase que hereda este método puede reemplazarlo:

public virtual double Area() { return x * y; }

Comentarios
Cuando se invoca un método virtual, el tipo en tiempo de ejecución del objeto se
comprueba para ver si existe un miembro de reemplazo. Se realiza una llamada al
miembro de reemplazo que está en la clase de mayor derivación, el cual puede ser el
miembro original, si no existe ninguna clase derivada que haya reemplazado el miembro.
De forma predeterminada, los métodos son no virtuales. No se puede reemplazar un
método no virtual.
No puede utilizar el modificador virtual con los modificadores static, abstract y override.
Las propiedades virtuales funcionan como los métodos abstractos, salvo en lo que se
refiere a las diferencias en la sintaxis de las declaraciones e invocaciones.
• Es incorrecto utilizar el modificador virtual para una propiedad estática.
• Una propiedad virtual heredada se puede reemplazar en una clase derivada si
se incluye una declaración de propiedad que use el modificador override.
Ejemplo
En este ejemplo, la clase Dimensions contiene las dos coordenadas x, y, y el método
virtual Area(). Las clases de las diferentes figuras, como Circle, Cylinder y Sphere,
heredan la clase Dimensions, que permite calcular el área de la superficie de cada figura.
Cada clase derivada dispone de su propia implementación de Area() (método de
reemplazo). El programa calcula y muestra el área apropiada para cada implementación
del método Area() según el objeto asociado al método.
Observe que todas las clases heredadas Circle, Sphere y Cylinder utilizan constructores
que inicializan la clase base, por ejemplo:

public Cylinder(double r, double h): base(r, h) {}

// cs_virtual_keyword.cs
using System;
class TestClass {

public class Dimensions


{
public const double PI = Math.PI;
protected double x, y;
public Dimensions() { }
public Dimensions(double x, double y) { this.x = x; this.y = y; }
public virtual double Area() { return x * y; }
}

public class Circle : Dimensions {


public Circle(double r) : base(r, 0) { }

58
public override double Area() { return PI * x * x; }
}

class Sphere : Dimensions


{
public Sphere(double r) : base(r, 0) { }
public override double Area() { return 4 * PI * x * x; }
}

class Cylinder : Dimensions


{
public Cylinder(double r, double h) : base(r, h) { }
public override double Area() { return 2 * PI * x * x + 2 * PI * x * y; }
}

static void Main()


{
double r = 3.0, h = 5.0;
Dimensions c = new Circle(r);
Dimensions s = new Sphere(r);
Dimensions l = new Cylinder(r, h);
// Display results:
Console.WriteLine("Area of Circle = {0:F2}", c.Area());
Console.WriteLine("Area of Sphere = {0:F2}", s.Area());
Console.WriteLine("Area of Cylinder = {0:F2}", l.Area());
}
}

Resultados
Area of Circle = 28.27
Area of Sphere = 113.10
Area of Cylinder = 150.80

Override
El modificador override es necesario para ampliar o modificar la implementación
abstracta o virtual de un método, propiedad, indizador o evento heredado.
En este ejemplo, la clase Square debe proporcionar una implementación de reemplazo de
Area porque ésta se hereda de la clase abstracta ShapesClass:

abstract class ShapesClass


{
abstract public int Area();
}
class Square : ShapesClass
{
int x, y;
// Because ShapesClass.Area is abstract, failing to override
// the Area method would result in a compilation error.
public override int Area() { return x * y; }
}

59
Comentarios
El método override proporciona una nueva implementación de un miembro heredado de
una clase base. El método reemplazado por una declaración override se conoce como
método base reemplazado. El método base reemplazado debe tener la misma firma que
el método override. No se puede reemplazar un método estático o no virtual. El método
base reemplazado debe ser virtual, abstract u override.
Una declaración override no puede cambiar la accesibilidad del método virtual. El método
override y el método virtual deben tener el mismo modificador de nivel de acceso.
No se pueden utilizar los modificadores new, static, virtual o abstract para modificar un
método override.
Una declaración de propiedad de reemplazo debe especificar el mismo modificador de
acceso, tipo y nombre que la propiedad heredada, y la propiedad reemplazada debe ser
virtual, abstract u override.

Ejemplo
Este ejemplo define una clase base denominada Employee y una clase derivada
denominada SalesEmployee. La clase SalesEmployee incluye una propiedad adicional,
salesbonus, y reemplaza al método CalculatePay para tenerlo en cuenta.

using System;
class TestOverride
{
public class Employee
{
public string name;
// Basepay is defined as protected, so that it may be
// accessed only by this class and derrived classes. protected decimal basepay;
// Constructor to set the name and basepay values.
public Employee(string name, decimal basepay)
{
this.name = name;
this.basepay = basepay;
}
// Declared virtual so it can be overridden.
public virtual decimal CalculatePay() { return basepay; }
}

// Derive a new class from Employee.


public class SalesEmployee : Employee
{
// New field that will affect the base pay. private decimal salesbonus;
// The constructor calls the base-class version, and
// initializes the salesbonus field.
public SalesEmployee(string name, decimal basepay, decimal salesbonus) :
base(name, basepay)
{
this.salesbonus = salesbonus;
}

// Override the CalculatePay method


// to take bonus into account.

60
public override decimal CalculatePay()
{
return basepay + salesbonus;
}
}

static void Main()


{
// Create some new employees.
SalesEmployee employee1 = new SalesEmployee("Alice", 1000, 500);
Employee employee2 = new Employee("Bob", 1200);
Console.WriteLine("Employee " + employee1.name + " earned: " +
employee1.CalculatePay());
Console.WriteLine("Employee " + employee2.name + " earned: " +
employee2.CalculatePay());
}
}

Resultados
Employee Alice earned: 1500 Employee Bob earned: 1200

5.7 Clases virtuales y visibilidad


En C# no hay clases virtuales directamente.

5.8 Constructores y destructores en las clases derivadas


Constructores
Todos los constructores de instancia (excepto aquellos para la clase object) incluyen
implícitamente una invocación de otro constructor de instancia inmediatamente antes que
el cuerpo-del-constructor (constructor-body). El constructor que se invoca implícitamente
está determinado por el inicializador-de-constructor (constructor-initializer):
• Un inicializador de constructor de instancia de la forma base (lista de argumentos
opcional o argument-listopt) causa la invocación de un constructor de instancia desde
la clase base directa. El conjunto de constructores de instancia candidatos está
formado por todos los constructores de instancia accesibles que contiene la clase
base directa. Si el conjunto está vacío o no es posible identificar el mejor constructor
de instancia, se produce un error en tiempo de compilación.
• Un inicializador de constructor de instancia de la forma this(lista-de-argumentos-
opcional) causa la invocación de un constructor de instancia desde la propia clase. El
conjunto de constructores de instancia candidatos está formado por todos los
constructores de instancia accesibles declarados en la clase. Si el conjunto está
vacío o no es posible identificar el mejor constructor de instancia, se produce un error
en tiempo de compilación. Si la declaración de un constructor de instancia incluye un
inicializador de constructor que invoca al propio constructor, se produce un error en
tiempo de compilación.
Si un constructor de instancia no tiene inicializador de constructor, se proporciona
implícitamente uno de la forma base(). Por lo tanto, una declaración de constructor de
instancia de la forma
C(...) {...}
equivale exactamente a
C(...): base() {...}

61
El ámbito de los parámetros dados en la lista-de-parámetros-formales de una declaración
de constructor de instancia incluye el inicializador de constructor de dicha declaración. Por
lo tanto, un inicializador de constructor puede tener acceso a los parámetros del
constructor. Por ejemplo:

class A
{
public A(int x, int y) {}
}
class B: A
{
public B(int x, int y): base(x + y, x - y) {}
}

Un inicializador de constructor de instancia no puede tener acceso a la instancia que está


siendo creada. Por ello, se produce un error en tiempo de compilación si se hace
referencia a this en una expresión de argumento del inicializador del constructor, al igual
que se produce un error en tiempo de compilación si una expresión de argumento hace
referencia a cualquier miembro de instancia a través del nombre-simple.

Ejecución de constructor
Los inicializadores de variables se transforman en instrucciones de asignación, que se
ejecutan antes de la invocación del constructor de instancia de la clase base. Tal
ordenamiento garantiza que todos los campos de instancia son inicializados por sus
inicializadores de variable antes de que se ejecute cualquier instrucción que tenga acceso
a la instancia.
Dado el ejemplo:

using System;
class A
{
public A() {
PrintFields();
}
public virtual void PrintFields() {}
}
class B: A
{
int x = 1;
int y;
public B() {
y = -1;
}
public override void PrintFields() {
Console.WriteLine("x = {0}, y = {1}", x, y);
}
}

cuando se utiliza new B() para crear una instancia de B, el resultado que se produce es:
x = 1, y = 0

62
El valor de x es 1 porque el inicializador de variable se ejecuta antes de que se invoque el
constructor de instancia de la clase base. Sin embargo, el valor de y es 0 (el valor
predeterminado para un tipo int) porque la asignación a y no se ejecuta hasta que no
devuelve el control el constructor de la clase base.
Es útil considerar los inicializadores de variables de instancia y los inicializadores de
constructor como instrucciones que se insertan automáticamente antes del cuerpo-del-
constructor. El ejemplo

using System;
using System.Collections;
class A
{
int x = 1, y = -1, count;
public A() {
count = 0;
}
public A(int n) {
count = n;
}
}
class B: A
{
double sqrt2 = Math.Sqrt(2.0);
ArrayList items = new ArrayList(100);
int max;
public B(): this(100) {
items.Add("default");
}
public B(int n): base(n – 1) {
max = n;
}
}

contiene varios inicializadores de variable e inicializadores de constructor de ambas


formas (base y this). El código del ejemplo se corresponde con el código siguiente, donde
cada comentario indica una instrucción que se inserta automáticamente (la sintaxis
utilizada para invocar el constructor insertado automáticamente no es correcta, pero sirve
para ilustrar el mecanismo).

using System.Collections;
class A
{
int x, y, count;
public A() {
x = 1; // Variable initializer
y = -1; // Variable initializer
object(); // Invoke object() constructor
count = 0;
}
public A(int n) {
x = 1; // Variable initializer

63
y = -1; // Variable initializer
object(); // Invoke object() constructor
count = n;
}
}
class B: A
{
double sqrt2;
ArrayList items;
int max;
public B(): this(100) {
B(100); // Invoke B(int) constructor
items.Add("default");
}
public B(int n): base(n – 1) {
sqrt2 = Math.Sqrt(2.0); // Variable initializer
items = new ArrayList(100); // Variable initializer
A(n – 1); // Invoke A(int) constructor
max = n;
}

Destructores
Un destructor es un miembro que implementa las acciones necesarias para destruir una
instancia de una clase. Un destructor se declara utilizando una declaración-de-destructor

(destructor-declaration):
destructor-declaration:
attributesopt externopt ~ identifier ( ) destructor-body
destructor-body:
block
;
Una declaración-de-destructor puede incluir un conjunto de atributos (attributes).
El identificador de un declarador-de-destructor debe nombrar la clase en la que se declara
el destructor. Si se especifica cualquier otro nombre, se produce un error en tiempo de
compilación.
Cuando una declaración de destructor incluye un modificador extern, se dice que es un
destructor externo. Debido a que la declaración de destructor externo no proporciona una
implementación real, su cuerpo-de-destructor (destructor-body) consiste en un punto y
coma. Para el resto de destructores, el cuerpo-de-destructor consiste en un bloque que
especifica las instrucciones necesarias para destruir una instancia de la clase. Un cuerpo-
de-destructor corresponde exactamente al cuerpo-de-un-método de un método de
instancia con un tipo de valor devuelto void.
Los destructores no se heredan. Por lo tanto, una clase sólo tiene el destructor que se
puede declarar en la propia clase.
Como un destructor no puede tener parámetros, no puede ser sobrecargado; por lo tanto,
una clase sólo puede tener como máximo un destructor.
Los destructores se invocan automáticamente y no se pueden invocar explícitamente. Una
instancia se convierte en candidata para destrucción cuando ya ninguna parte de código

64
puede utilizarla. La ejecución del destructor de la instancia puede ocurrir en cualquier
momento una vez que la instancia se convierta en candidata para destrucción. Cuando se
destruye una instancia, se llama a los destructores de su cadena de herencia en orden, de
la más derivada a la menos derivada. Un destructor puede ejecutarse en cualquier
subproceso. Para leer una explicación más detallada de las reglas que controlan cómo y
cuándo se ejecuta un destructor. El resultado del ejemplo

using System;
class A
{
~A() {
Console.WriteLine("A's destructor");
}
}
class B: A
{
~B() {
Console.WriteLine("B's destructor");
}
}
class Test
{
static void Main() {
B b = new B();
b = null;
GC.Collect();
GC.WaitForPendingFinalizers();
}
}

B's destructor
A's destructor

ya que en una cadena de herencia los destructores se llaman en orden, de la más


derivada a la menos derivada.
Los destructores se implementan reemplazando el método virtual Finalize en
System.Object. Los programas de C# no permiten reemplazar este método o llamarlo
directamente (o a reemplazos del mismo). Por ejemplo, el programa

class A
{
override protected void Finalize() {} // error
public void F() {
this.Finalize(); // error
}
}

contiene dos errores.


El compilador se comporta como si este método, y sus reemplazos, no existieran. Por lo
tanto, este programa:

65
class A
{
void Finalize() {} // permitted
}

es válido, y el método mostrado oculta el método Finalize de System.Object.


Para leer una explicación del comportamiento producido cuando se inicia una excepción
desde un destructor.

VI Polimorfismo y reutilización

6.1 Concepto del polimorfismo


El polimorfismo ayuda al programador a escribir código más fácil de modificar y ampliar.
Polimorfismo es pues la capacidad de un objeto para responder a un mensaje basado en
su tipo y posición en la jerarquía de clases. Una respuesta apropiada implica la capacidad
del objeto para elegir la implementación del método que mejor se adapte a sus
características. Mediante técnicas polimórficas es posible escribir código que manipule
objetos de muchas clases diferentes de un modo uniforme y consistente, con
independencia de su tipo exacto. La flexibilidad y generalidad de las estructuras
polimórficas es una de las ventajas más significativas de la programación orientada a
objetos.
La estrategia para desarrollar una estructura polimórfica comienza con la identificación de
los métodos comunes a través de un grupo de tipos de objetos similares pero no idénticos
y organizando una jerarquía de clases donde los métodos comunes se sitúan en la clase
base, mientras que los restantes se organizan en clases derivadas, deducidas de esta
clase base. La clase base define una interfaz a través de la cual un objeto de cualquiera
de las subclases especificadas se puede manipular. Es importante considerar que los
métodos de la interfaz compartida se deben declarar en la clase base y han de ser de
virtuales. De esta forma las clases derivadas pueden redefinir dichos métodos mediante el
modificador override, se establece ligadura dinámica y, en tiempo de ejecución, se busca
cuál es la clase del objeto que ha recibido el mensaje y se decide qué método se ejecuta.

6.2 Clases Abstractas


6.2.1 Definición y 6.2.2 Redefinición

66
Las clases abstractas están estrechamente relacionadas con las interfaces. Son clases de
las que no es posible crear instancias; frecuentemente, están implementadas sólo
parcialmente o no están implementadas. Una de las principales diferencias entre las
clases abstractas y las interfaces es que una clase puede implementar un número
ilimitado de interfaces, pero sólo puede heredar de una clase abstracta (o de cualquier
otra clase). Una clase derivada de una clase abstracta conserva la capacidad de
implementar interfaces. Las clases abstractas son útiles para crear componentes, porque
permiten especificar un nivel invariable de funcionalidad para algunos métodos y aplazar
la implementación de otros hasta que se necesite una implementación específica de la
clase. También admiten bien el uso de versiones, porque, si se necesita una funcionalidad
adicional en las clases derivadas, se puede agregar a la clase base sin romper el código.

abstract class WashingMachine


{
public WashingMachine()
{
// Code to initialize the class goes here.
}

abstract public void Wash();


abstract public void Rinse(int loadSize);
abstract public long Spin(int speed);
}

class MyWashingMachine : WashingMachine


{
public MyWashingMachine()
{
// Initialization code goes here.
}

override public void Wash()


{
// Wash code goes here.
}

override public void Rinse(int loadSize)


{
// Rinse code goes here.

67
}

override public long Spin(int speed)


{
// Spin code goes here.
}
}

Cuando se implementa una clase abstracta, debe implementarse cada método abstracto
(MustOverride) de esta clase; cada método implementado debe recibir el mismo número y
tipo de argumentos y devolver el mismo valor que el método especificado en la clase
abstracta.
6.3 Definición de una interfaz y 6.4 Implementación de la definición de una interfaz.

Las interfaces se definen utilizando la palabra clave interface. Por ejemplo:

interface IComparable
{
int CompareTo(object obj);
}

Las interfaces describen un grupo de comportamientos relacionados que pueden


pertenecer a cualquier clase o estructura. Las interfaces pueden estar compuestas de
métodos, propiedades, eventos, indizadores o cualquier combinación de estos cuatro
tipos de miembros. Una interfaz no puede contener campos. Los miembros de interfaz
son automáticamente públicos.
Las clases y estructuras se pueden heredar de interfaces de manera similar a como las
clases pueden heredar una clase base o estructura, con dos excepciones:
• Una clase o estructura puede heredar más de una interfaz.
• Cuando una clase o estructura hereda una interfaz, hereda definiciones de
miembro, pero no implementaciones. Por ejemplo:

public class Minivan : Car, IComparable


{
public int CompareTo(object obj)
{
//implementation of CompareTo
return 0; //if the Minivans are equal
}
}

68
Para implementar un miembro de interfaz, el miembro correspondiente de la clase debe
ser público, no estático y tener el mismo nombre y la misma firma que el miembro de
interfaz. Las propiedades e indizadores de una clase pueden definir descriptores de
acceso adicionales para una propiedad o indizador definidos en una interfaz. Por ejemplo,
una interfaz puede declarar una propiedad con un descriptor de acceso get, pero la clase
que implementa la interfaz puede declarar la misma propiedad con descriptores de acceso
get y set. Sin embargo, si la propiedad o el indizador utiliza una implementación explícita,
los descriptores de acceso deben coincidir.
Las interfaces y los miembros de interfaz son abstractos; las interfaces no proporcionan
una implementación predeterminada.
La interfaz IComparable informa al usuario del objeto de que éste se puede comparar con
otros objetos del mismo tipo y el usuario de la interfaz no necesita saber cómo se
implementa.
Las interfaces pueden heredar otras interfaces. Es posible que una clase herede una
interfaz varias veces, a través de las clases base o interfaces que hereda. En ese caso, la
clase sólo puede implementar la interfaz una vez, siempre que ésta se declare como parte
de la nueva clase. Si la interfaz heredada no está declarada como parte de la nueva
clase, la clase base que la declaró proporcionará su implementación. Es posible que una
clase base implemente miembros de interfaz a través de miembros virtuales. En ese caso,
la clase que hereda la interfaz puede cambiar el comportamiento de la interfaz
reemplazando los miembros virtuales.

Información general sobre interfaces


Una interfaz tiene las siguientes propiedades:
• Una interfaz es similar a una clase base abstracta. Cualquier tipo no abstracto
que hereda la interfaz debe implementar todos sus miembros.
• No se pueden crear instancias directamente de una interfaz.
• Las interfaces pueden contener eventos, métodos, indizadores y propiedades.
• Las interfaces no contienen implementaciones de métodos.
• Las clases y estructuras se pueden heredar de más de una interfaz.
• Una interfaz se puede heredar de varias interfaces.

6.5 Reutilización de la definición de una interfaz.


Implementación explícita de interfaz
Si una clase implementa dos interfaces que contienen un miembro con la misma firma, la
implementación de ese miembro en la clase hará que ambas interfaces usen ese miembro
como implementación. Por ejemplo:

interface IControl
{
void Paint();
}
interface ISurface
{

69
void Paint();
}
class SampleClass : IControl, ISurface
{
// Both ISurface.Paint and IControl.Paint call this method.
public void Paint()
{
}
}

Sin embargo, los miembros de dos interfaces no realizan la misma función, esto puede
llevar a una implementación incorrecta de una o ambas interfaces. Es posible implementar
un miembro de interfaz explícitamente, creando un miembro de clase que sólo se llama a
través de la interfaz y es específico de ésta. Esto se puede llevar a cabo asignando al
miembro de clase el nombre de la interfaz y un punto. Por ejemplo:

public class SampleClass : IControl, ISurface


{
void IControl.Paint()
{
System.Console.WriteLine("IControl.Paint");
}
void ISurface.Paint()
{
System.Console.WriteLine("ISurface.Paint");
}
}

El miembro de clase IControl.Paint sólo está disponible a través de la interfaz IControl,


mientras que ISurface.Paint sólo está disponible a través de ISurface. Ambas
implementaciones de método son independientes y ninguna está directamente disponible
en la clase. Por ejemplo:

SampleClass obj = new SampleClass();


//obj.Paint(); // Compiler error.

IControl c = (IControl)obj;
c.Paint(); // Calls IControl.Paint on SampleClass.

ISurface s = (ISurface)obj;
s.Paint(); // Calls ISurface.Paint on SampleClass.

La implementación explícita también se usa para resolver casos donde cada una de las
dos interfaces declara miembros diferentes del mismo nombre como propiedad y método:

interface ILeft
{
int P { get;}
}
interface IRight
{

70
int P();
}

Para implementar ambas interfaces, una clase tiene que utilizar implementación explícita,
ya sea para la propiedad P, el método P o ambos, con el fin de evitar un error del
compilador. Por ejemplo:

class Middle : ILeft, IRight


{
public int P() { return 0; }
int ILeft.P { get { return 0; } }
}

Propiedades de interfaces
A continuación se muestra un ejemplo de descriptor de acceso de un indizador de
interfaz:

public interface ISampleInterface


{
// Property declaration:
string Name
{
get;
set;
}
}

Un identificador de acceso de una propiedad de interfaz no tiene cuerpo. Así, el propósito


de los descriptores de acceso es indicar si la propiedad es de lectura y escritura, de sólo
lectura o de sólo escritura.

Ejemplo
En este ejemplo, la interfaz IEmployee tiene una propiedad de lectura y escritura, Name, y
una propiedad de sólo lectura, Counter. La clase Employee implementa la interfaz
IEmployee y utiliza las dos propiedades. El programa lee el nombre de un empleado
nuevo y el número actual de empleados, y muestra como resultado el nombre del
empleado y el nuevo número de empleados calculado.
Se podría utilizar el nombre completo de la propiedad, que hace referencia a la interfaz en
la que se declara el miembro. Por ejemplo:

string IEmployee.Name
{
get { return "Employee Name"; }
set { }
}

Por ejemplo, si la clase Employee implementa dos interfaces ICitizen y IEmployee, y


ambas tienen la propiedad Name, será necesario implementar explícitamente el miembro
de interfaz. Es decir, la siguiente declaración de propiedad:

71
string IEmployee.Name
{
get { return "Employee Name"; }
set { }
}

implementa la propiedad Name en la interfaz IEmployee, mientras que la declaración:

string ICitizen.Name
{
get { return "Citizen Name"; }
set { }
}

implementa la propiedad Name en la interfaz ICitizen.

interface IEmployee
{
string Name
{
get;
set;
}

int Counter
{
get;
}
}

public class Employee : IEmployee


{
public static int numberOfEmployees;

private string name;


public string Name // read-write instance property
{
get
{
return name;
}
set
{
name = value;
}
}

private int counter;


public int Counter // read-only instance property
{
get

72
{
return counter;
}
}

public Employee() // constructor


{
counter = ++counter + numberOfEmployees;
}
}

class TestEmployee
{
static void Main()
{
System.Console.Write("Enter number of employees: ");
Employee.numberOfEmployees = int.Parse(System.Console.ReadLine());

Employee e1 = new Employee();


System.Console.Write("Enter the name of the new employee: ");
e1.Name = System.Console.ReadLine();

System.Console.WriteLine("The employee information:");


System.Console.WriteLine("Employee number: {0}", e1.Counter);
System.Console.WriteLine("Employee name: {0}", e1.Name);
}
}

Indizadores en interfaces
Los descriptores de acceso de los indizadores de interfaz se diferencian de los
descriptores de acceso de los indizadores de clase en los siguientes aspectos:
• Los descriptores de acceso de interfaz no utilizan modificadores.
• Un identificador de acceso de interfaz no tiene cuerpo.
Así, el propósito del descriptor de acceso es indicar si el indizador es de lectura y
escritura, de sólo lectura o de sólo escritura.
A continuación se muestra un ejemplo de descriptor de acceso de un indizador de
interfaz:

public interface ISomeInterface


{
//...

// Indexer declaration:
string this[int index]
{
get;
set;
}
}

73
La firma de un indizador debe ser diferente de las firmas de los demás indizadores
declarados en la misma interfaz.
Ejemplo
En el ejemplo siguiente se muestra cómo se implementan indizadores de interfaz:
// Indexer on an interface:

public interface ISomeInterface


{
// Indexer declaration:
int this[int index]
{
get;
set;
}
}

// Implementing the interface.


class IndexerClass : ISomeInterface
{
private int[] arr = new int[100];
public int this[int index] // indexer declaration
{
get
{
// Check the index limits.
if (index < 0 || index >= 100)
{
return 0;
}
else
{
return arr[index];
}
}
set
{
if (!(index < 0 || index >= 100))
{
arr[index] = value;
}
}
}
}

class MainClass
{
static void Main()
{
IndexerClass test = new IndexerClass();

74
// Call the indexer to initialize the elements #2 and #5.
test[2] = 4;
test[5] = 32;
for (int i = 0; i <= 10; i++)
{
System.Console.WriteLine("Element #{0} = {1}", i, test[i]);
}
}
}

Declarar un evento en una interfaz e implementarlo en una clase


Este ejemplo muestra que es posible declarar un evento en una interfaz e implementarlo
en una clase.

public delegate void TestDelegate(); // delegate declaration

public interface ITestInterface


{
event TestDelegate TestEvent;
void FireAway();
}

public class TestClass : ITestInterface


{
public event TestDelegate TestEvent;

public void FireAway()


{
if (TestEvent != null)
{
TestEvent();
}
}
}

public class MainClass


{
static private void F()
{
System.Console.WriteLine("This is called when the event fires.");
}

static void Main()


{
ITestInterface i = new TestClass();

i.TestEvent += new TestDelegate(F);


i.FireAway();
}
}

75
VII Excepciones

7.1 Definición
7.1.1 Que son las excepciones
Las características de control de excepciones del lenguaje C# proporcionan una manera
de afrontar cualquier situación inesperada o excepcional que se presente mientras se
ejecuta un programa. El control de excepciones utiliza las palabras clave try, catch y
finally para intentar acciones que podrían no realizarse correctamente, controlar errores y
limpiar los recursos después. Common Language Runtime (CLR), las bibliotecas de otro
fabricante o el código de aplicación que utiliza la palabra clave throw pueden generar
excepciones.
En este ejemplo, el método hace una prueba para realizar una división por cero y detecta
el error. Sin el control de excepciones, este programa finalizaría con un error
DivideByZeroException no controlado.

int SafeDivision(int x, int y)


{
try
{
return (x / y);
}
catch (System.DivideByZeroException dbz)

76
{
System.Console.WriteLine("Division by zero attempted!"); return 0;
}
}

Información general sobre excepciones


Las excepciones tienen las propiedades siguientes:
• Cuando la aplicación encuentra una circunstancia excepcional, como una
división por cero o una advertencia de que no hay suficiente memoria, se
genera una excepción.
• Cuando se produce una excepción, el flujo de control salta inmediatamente a
un controlador de excepciones asociado, si hay alguno presente.
• Si no hay un controlador de excepciones para una excepción determinada, el
programa deja de ejecutarse y presenta un mensaje de error.
• Las acciones que pueden producir una excepción se ejecutan con la palabra
clave try.
• Un controlador de excepciones es un bloque de código que se ejecuta cuando
se produce una excepción. En C#, la palabra clave catch se utiliza para definir
un controlador de excepciones.
• Un programa que utiliza la palabra clave throw puede generar explícitamente
excepciones.
• Los objetos de excepción contienen información detallada sobre el error que
incluye el estado de la pila de llamadas y una descripción de texto del error.
• El código se ejecuta en un bloque finally aunque se produzca una excepción,
permitiendo así que el programa libere recursos.

7.1.2 Clases de excepciones


Excepciones predefinidas por el lenguaje.
El Common Language Runtime (CLR) de .NET Framework produce automáticamente
algunas excepciones como resultado de operaciones básicas en las que se produce un
error. A continuación se muestran estas excepciones y sus condiciones de error.

Excepción Descripción

ArithmeticException Clase base de las excepciones producidas durante


operaciones aritméticas, como DivideByZeroException y
OverflowException.
ArrayTypeMismatchException Se produce cuando una matriz no puede almacenar un
elemento dado porque el tipo real del elemento es
incompatible con el tipo real de la matriz.
DivideByZeroException Se produce cuando tiene lugar un intento de dividir un
valor integral por cero.
IndexOutOfRangeException Se produce cuando tiene lugar un intento de indizar una
matriz cuando el índice es menor que cero o se encuentra

77
fuera de los límites de la matriz.
InvalidCastException Se produce cuando tiene lugar un error en tiempo de
ejecución en una conversión explícita de un tipo base a
una interfaz o a un tipo derivado.
NullReferenceException Se produce cuando se utiliza una referencia null de
manera que hace obligatorio el objeto al que se hace
referencia.
OutOfMemoryException Se produce cuando tiene lugar un error al intentar asignar
memoria mediante new.
OverflowException Se produce cuando una operación aritmética en un
contexto checked produce un desbordamiento.
StackOverflowException Se produce cuando se agota la pila de excepciones debido
a la existencia de demasiadas llamadas al método
pendientes; normalmente, suele indicar un nivel de
recursividad muy profundo o infinito.
TypeInitializationException Se produce cuando un constructor estático produce una
excepción sin que haya cláusulas catch compatibles para
capturarla.

7.1.3 Propagación
La instrucción throw (throw-statement) inicia una excepción.
throw-statement:
throw expressionopt ;

Una instrucción throw con expresión inicia el valor resultante de evaluar la expresión. La
expresión debe denotar un valor del tipo de clase System.Exception o de un tipo de clase
que derive de System.Exception. Si la expresión evaluada devuelve null, se inicia
entonces System.NullReferenceException.
Una instrucción throw sin expresión sólo se puede utilizar en un bloque catch, en cuyo
caso esta instrucción volverá a iniciar la excepción que esté controlando en ese momento
el bloque catch.
Como una instrucción throw transfiere incondicionalmente el control a otra parte del
código, el punto final de una instrucción throw nunca es alcanzable.
Cuando se inicia una excepción, el control se transfiere a la primera cláusula catch de una
instrucción try envolvente que pueda controlar la excepción. El proceso que tiene lugar
desde el punto de inicio de la excepción hasta el punto en que se transfiere el control a un
controlador de excepciones adecuado es conocido con el nombre de propagación de
excepción. La propagación de una excepción consiste en evaluar repetidamente los

78
siguientes pasos hasta encontrar una cláusula catch que coincida con la excepción. En
esta descripción, el punto de inicio es la ubicación desde la que se inicia la excepción.
• En el miembro de función actual, se examina cada instrucción try que
envuelve al punto de inicio. Se evalúan los siguientes pasos para cada
instrucción S, comenzando con la instrucción try más interna y terminando con la
más externa:
• Si el bloque try de S encierra al punto de inicio y S tiene una o varias
cláusulas catch, se examinan las cláusulas catch en orden de aparición hasta
encontrar un controlador adecuado para la excepción. La primera cláusula catch
que especifique el tipo de excepción o un tipo base del tipo de excepción se
considera una coincidencia. Una cláusula catch general es una coincidencia para
cualquier tipo de excepción. Si se encuentra una cláusula catch coincidente,
termina la propagación de excepción y se transfiere el control al bloque de la
cláusula catch.
• En caso contrario, si el bloque try o un bloque catch de S encierra el punto
de inicio y S tiene un bloque finally, el control se transfiere al bloque finally. Si el
bloque finally inicia otra excepción, finaliza el procesamiento de la excepción
actual. Si no, cuando el control alcanza el punto final del bloque finally, continúa
con el procesamiento de la excepción actual.
• Si no se encontró un controlador de excepciones en la llamada del
miembro de función actual, finaliza la llamada al miembro de función. Los pasos
anteriores se repiten para el llamador del miembro de función con el punto de
inicio que corresponda a la instrucción desde la que se invocó al miembro de
función.
• Si el procesamiento de la excepción finaliza todas las llamadas a miembros
de función del subproceso actual indicando que el subproceso no ha encontrado
controlador para la excepción, dicho subproceso finaliza. El impacto de esta
terminación se define según la implementación.

// throw example using System;


public class ThrowTest
{
static void Main()
{
string s = null;
if (s == null)
{
throw new ArgumentNullException();
}
Console.Write("The string s is null"); // not executed
}
}

7.2 Gestión de excepciones


7.2.1 Manejo de excepciones

Manejo de excepciones estructuradas

79
Las excepciones en C# las podemos controlar usando las instrucciones try / catch / finally.
Estas instrucciones realmente son bloques de instrucciones, y por tanto estarán
delimitadas con un par de llaves.
Cuando queramos controlar una parte del código que puede producir un error lo incluimos
dentro del bloque try, si se produce un error, éste lo podemos detectar en el bloque catch,
por último, independientemente de que se produzca o no una excepción, podemos
ejecutar el código que incluyamos en el bloque finally.
Cuando creamos una estructura de control de excepciones no estamos obligados a usar
los tres bloques, aunque el primero: try si es necesario, ya que es el que le indica al
compilador que tenemos intención de controlar los errores que se produzcan. Por tanto
podemos crear un "manejador" de excepciones usando los tres bloques, usando try y
catch o usando try y finally.
Veamos ahora con más detalle cada uno de estos bloques y que es lo que podemos
hacer en cada uno de ellos.
Bloque try
En este bloque incluiremos el código en el que queremos comprobar los errores. El código
a usar será un código normal, es decir, no tenemos que hacer nada en especial, ya que
en el momento que se produzca el error se usará (si hay) el código del bloque catch.
Bloque catch
Si se produce una excepción, ésta la capturamos en un bloque catch.
En el bloque catch podemos indicar que tipo de excepción queremos capturar, para ello
usaremos una variable de tipo Exception, la cual puede ser del tipo de error específico
que queremos controlar o de un tipo genérico.
Por ejemplo, si sabemos que nuestro código puede producir un error al trabajar con
ficheros, podemos usar un código como éste:

Nota

try
{
// código para trabajar con ficheros, etc.
}
catch(System.IO.IOException ex)
{
// el código a ejecutar cuando se produzca ese error
}
Si nuestra intención es capturar todos los errores que se produzcan, es decir, no
queremos hacer un filtro con errores específicos, podemos usar la clase Exception como
tipo de excepción a capturar. La clase Exception es la más genérica de todas las clases
para manejo de excepciones, por tanto capturará todas las excepciones que se
produzcan.
Nota

80
try
{
// código que queremos controlar
}
catch(System.Exception ex)
{
// el código a ejecutar cuando se produzca cualquier error
}
Aunque si no vamos usar la variable indicada en el bloque Catch, pero queremos que no
se detenga la aplicación cuando se produzca un error, podemos hacerlo de esta forma:
Nota

try
{
// código que queremos controlar
}
catch
{
// el código a ejecutar cuando se produzca cualquier error
}
La variable indicada en el bloque catch la podemos usar para mostrar un mensaje al
usuario o para obtener información extra sobre el error, pero no siempre vamos a hacer
uso de esa variable, en ese caso podemos utilizar el código anterior, en el que no se usa
una variable y tampoco se indica el tipo de error que queremos interceptar. Pero es
posible que nuestra intención sea capturar errores de un tipo concreto sin necesidad de
utilizar una variable, en ese caso podemos crear un bloque catch como el siguiente, en el
que solo se indica el tipo de excepción:

Nota

try
{
// código que queremos controlar
}
catch(FormatException)
{
// interceptar los errores del tipo FormatException
}
Varias capturas de errores en un mismo bloque try/catch
En un mismo try/catch podemos capturar diferentes tipos de errores, para ello podemos
incluir varios bloques catch, cada uno de ellos con un tipo de excepción diferente.
Es importante tener en cuenta que cuando se produce un error y usamos varios bloques
catch, el CLR de .NET buscará la captura que mejor se adapte al error que se ha
producido, pero siempre lo hará examinando los diferentes bloques catch que hayamos
indicado empezando por el indicado después del bloque try, por tanto deberíamos poner
las más genéricas al final, de forma que siempre nos aseguremos de que las capturas de

81
errores más específicas se intercepten antes que las genéricas. Aunque el propio
compilador de C# detectará si hay capturas de errores genéricas antes que las más
específicas, avisándonos de ese hecho.
En el siguiente código capturamos un error específico y también uno genérico, con idea
de que tengamos siempre controlado cualquier error que se produzca:
Nota

try
{
// código que queremos controlar
}
catch(FormatException)
{
// captura de error de formato
}
catch(Exception ex)
{
// captura del resto de errores
Bloque finally
En este bloque podemos indicar las instrucciones que queremos que se ejecuten, se
produzca o no una excepción. De esta forma nos aseguramos de que siempre se
ejecutará un código, por ejemplo para liberar recursos, se haya producido un error o no.
En este código tenemos tres capturas de errores diferentes y un bloque finally que
siempre se ejecutará, se produzca o no un error:

Nota

int i, j;
//
try
{
Console.Write("Un numero ");
i = Convert.ToInt32(Console.ReadLine());
Console.Write("Otro numero ");
j = Convert.ToInt32(Console.ReadLine());
int r = i / j;
Console.WriteLine("El resultado es: {0}", r);
}
catch (FormatException)
{
Console.WriteLine("No es un número válido");
// Salimos de la función, pero se ejecutará el finally
return;
}
catch (DivideByZeroException)
{

82
Console.WriteLine("La división por cero no está permitida.");
}
catch (Exception ex)
{
// Captura del resto de excepciones
Console.WriteLine(ex.Message);
}
finally
{
// Este código siempre se ejecutará
Console.WriteLine("Se acabó");
}
Nota

Nota: Hay que tener en cuenta de que incluso si usamos return dentro de un bloque de
control de errores, se ejecutará el código indicado en el bloque finally.
Captura de errores no controlados
Como es lógico, si no controlamos las excepciones que se puedan producir en nuestras
aplicaciones, éstas serán inicialmente controladas por el propio runtime de .NET, en estos
casos la aplicación se detiene y se muestra el error al usuario. Pero esto es algo que no
deberíamos consentir, por tanto siempre deberíamos detectar todos los errores que se
produzcan en nuestras aplicaciones, pero a pesar de que lo intentemos, es muy probable
que no siempre podamos conseguirlo.
Una forma de hacerlo es iniciando nuestra aplicación dentro de un bloque try/catch, de
esta forma, cuando se produzca el error, se capturará en ese bloque catch, porque
cuando el runtime de .NET se encuentra con una excepción, lo que hace es revisar "la
pila" de llamadas y buscar algún try/catch, si lo encuentra, lo utiliza, y si no lo encuentra,
se encarga de lanzar la excepción deteniendo el programa.
Esto es importante saberlo, no ya por detectar esos errores que no hemos tenido la
previsión de controlar, sino porque es posible que si un error se produce dentro de un
método en el que no hay captura de errores, pero antes de llamar a ese método hemos
usado un try/catch, el error será interceptado por ese catch, aunque posiblemente ni
siquiera lo pusimos pensando que podía capturar errores producidos en otros niveles más
profundos de nuestra aplicación.

7.2.2 Lanzamiento de excepciones

El propósito de una instrucción finally es asegurarse de que la limpieza necesaria de


objetos, por lo general objetos que contienen recursos externos, se produzca
inmediatamente, incluso cuando se produce una excepción. Un ejemplo de esta limpieza
es llamar a Close en FileStream inmediatamente después de su uso en lugar de esperar
que el objeto sea recolectado como elemento no utilizado por Common Language
Runtime, de la siguiente manera:

static void CodeWithoutCleanup()


{
System.IO.FileStream file = null;
System.IO.FileInfo fileInfo = new System.IO.FileInfo("C:\\file.txt");

83
file = fileInfo.OpenWrite();
file.WriteByte(0xF);

file.Close();
}

Ejemplo
Para convertir el código anterior en una instrucción try-catch-finally, el código de limpieza
está separado del código activo como se muestra a continuación.

static void CodeWithCleanup()


{
System.IO.FileStream file = null;
System.IO.FileInfo fileInfo = null;

try
{
fileInfo = new System.IO.FileInfo("C:\\file.txt");

file = fileInfo.OpenWrite();
file.WriteByte(0xF);
}
catch(System.Exception e)
{
System.Console.WriteLine(e.Message);
}
finally
{
if (file != null)
{
file.Close();
}
}
}

Como puede producirse una excepción en cualquier momento dentro del bloque try antes
de la llamada a OpenWrite() o la propia llamada a OpenWrite() podría producir un error,
no se garantiza que el archivo esté abierto cuando se intenta cerrarlo. El bloque finally
agrega una comprobación para asegurarse de que el objeto FileStream no es null antes
de llamar al método Close. Sin la comprobación de null, el bloque finally podría iniciar su
propia excepción NullReferenceException, pero se debería evitar producir excepciones en
bloques finally si es posible.
Una conexión de base de datos también es un elemento que debería cerrarse en un
bloque finally. Dado que el número de conexiones permitido a un servidor de base de
datos está limitado a veces, es importante cerrar las conexiones de base de datos tan
rápido como sea posible. Si se produce una excepción antes de poder cerrar la conexión,
se trata de otro caso en el que es preferible utilizar el bloque finally a esperar a la
recolección de elementos no utilizados.

84
VIII Flujos y archivos

8.1 Definición de Archivos de texto y archivos binarios


La clase base abstracta Stream es compatible con bytes de lectura y escritura. Stream
tiene compatibilidad asincrónica. Sus implementaciones predeterminadas definen lecturas
y escrituras asincrónicas según sus correspondientes métodos asincrónicos, y viceversa.
Todas las clases que representan secuencias se derivan de la clase Stream. La clase
Stream y sus clases derivadas proporcionan una visión genérica de los orígenes de datos
y los repositorios, aislando al programador de los detalles específicos del sistema
operativo y sus dispositivos subyacentes.
Las secuencias comprenden estas operaciones fundamentales:
• En las secuencias se puede leer. La lectura es la transferencia de datos desde una
secuencia a una estructura de datos, como, por ejemplo, una matriz de bytes.
• En las secuencias se puede escribir. La escritura consiste en la transferencia de
información desde un origen de datos a una secuencia.
• Las secuencias pueden admitir operaciones de búsqueda. Las operaciones de
búsqueda consisten en la consulta y modificación de la posición actual en una
secuencia.
Según el origen de datos o repositorio subyacente, las secuencias pueden admitir sólo
algunas de estas características. Por ejemplo, NetworkStreams no admite operaciones de

85
búsqueda. Las propiedades CanRead, CanWrite y CanSeek de la clase Stream y sus
clases derivadas determinan qué operaciones admiten las diversas secuencias.

Clases usadas para E/S de archivos


FileObject proporciona una representación de un archivo de texto. Para realizar las
operaciones más comunes con archivos de texto, tales como leer, escribir, agregar,
copiar, suprimir, mover o cambiar el nombre, se debe usar la clase FileObject. También se
puede usar esta clase para examinar y, en algunos casos, establecer atributos de archivo,
codificación e información de ruta de acceso.
Directory proporciona métodos estáticos para crear, mover y enumerar archivos en
directorios y subdirectorios. La clase DirectoryInfo proporciona métodos de instancia.
DirectoryInfo proporciona métodos de instancia para crear, mover y enumerar archivos en
directorios y subdirectorios. La clase Directory proporciona métodos estáticos.
File proporciona métodos estáticos para crear, copiar, eliminar, mover y abrir archivos y
contribuye a la creación de FileStream. La clase FileInfo proporciona métodos de
instancia.
FileInfo proporciona métodos de instancia para crear, copiar, eliminar, mover y abrir
archivos y contribuye a la creación de FileStream. La clase File proporciona métodos
estáticos.
FileStream admite el acceso aleatorio a archivos mediante el método Seek. FileStream
abre los archivos de forma sincrónica de manera predeterminada, pero también admite
operaciones asincrónicas. File contiene métodos estáticos y FileInfo contiene métodos de
instancia.
FileSystemInfo es la clase base abstracta para FileInfo y DirectoryInfo.
Path proporciona métodos y propiedades para procesar cadenas de directorio de una
plataforma a otra.
File, FileInfo, Path, Directory y DirectoryInfo son clases sealed (en Microsoft Visual Basic,
NotInheritable). Se pueden crear nuevas instancias de estas clases, pero no pueden tener
clases derivadas.

Clases usadas para leer y escribir en secuencias


BinaryReader y BinaryWriter leen y escriben cadenas codificadas y tipos de datos
primitivos desde y hacia las clases Stream.
StreamReader lee los caracteres de Streams, usando Encoding para convertir los
caracteres en bytes y a partir de bytes. StreamReader tiene un constructor que trata de
comprobar cuál es el Encoding correcto para una Stream dada, basándose en la
presencia de un preámbulo específico de Encoding, por ejemplo una marca de orden de
bytes.
StreamWriter escribe los caracteres en Streams, usando Encoding para convertir los
caracteres en bytes.
StringReader lee los caracteres de Strings. StringReader permite tratar Strings con la
misma API, de modo que la salida puede ser una Stream en cualquier codificación o una
String.

86
StringWriter escribe los caracteres en Strings. StringWriter permite tratar Strings con la
misma API, de modo que la salida puede ser una Stream en cualquier codificación o una
String.
TextReader es la clase base abstracta para StreamReader y StringReader. Mientras que
las implementaciones de la clase abstracta Stream están diseñadas para la entrada y
salida de bytes, las de TextReader están diseñadas para la entrada de caracteres
Unicode.
TextWriter es la clase base abstracta para StreamWriter y StringWriter. Mientras que las
implementaciones de la clase abstracta Stream están diseñadas para la entrada y salida
de bytes, las de TextWriter están diseñadas para la salida de caracteres Unicode.

Clases comunes de secuencias de E/S


Una BufferedStream es una Stream que agrega almacenamiento en búfer a otra Stream,
por ejemplo una NetworkStream. FileStream tiene almacenamiento en búfer internamente,
y MemoryStream no necesita almacenamiento en búfer. BufferedStream se puede
componer en torno a ciertos tipos de secuencias para mejorar el rendimiento de lectura y
escritura. Un búfer es un bloque de bytes de la memoria utilizado para almacenar datos
en la memoria caché y, de este modo, reducir el número de llamadas al sistema operativo.
Una CryptoStream vincula secuencias de datos a transformaciones criptográficas. Aunque
CryptoStream deriva de Stream, no forma parte del espacio de nombres System.IO, sino
del espacio de nombres System.Security.Cryptography.
Una MemoryStream es una secuencia no almacenada en búfer cuyos datos encapsulados
son accesibles directamente en la memoria. Esta secuencia no tiene almacén de respaldo
y puede resultar útil como búfer temporal.
Una NetworkStream representa una Stream a través de una conexión de red. Aunque
NetworkStream deriva de Stream, no forma parte del espacio de nombres System.IO, sino
de System.Net.Sockets.

E/S y seguridad
Cuando se utilizan las clases del espacio de nombres System.IO se deben satisfacer
requisitos de seguridad del sistema operativo como las listas de control de acceso (ACL)
para que se permita el acceso. Este requisito es adicional a cualquier requisito
FileIOPermission.
Precaución La directiva de seguridad predeterminada de Internet y de una intranet no
permite el acceso a archivos. Por ello, no use las clases de E/S de almacenamiento no
aislado normales al escribir código que se vaya a descargar a través de Internet. En su
lugar, utilice Almacenamiento aislado.
Precaución Al abrir un archivo o una secuencia de red, sólo se realiza una comprobación
de seguridad cuando se construye la secuencia. Por lo tanto, se debe tener cuidado al
entregar estas secuencias a dominios de aplicaciones o código que no sean de confianza.

8.2 Operaciones básicas en archivos texto y binario

8.2.1 Crear y 8.2.2 Abrir.


Los ejemplos de código siguientes muestran cómo escribir texto en un archivo de texto.

87
El primer ejemplo muestra cómo agregar texto a un archivo existente. El segundo ejemplo
indica cómo crear un nuevo archivo de texto y escribir una cadena en él. Los métodos
WriteAllText pueden proporcionar una funcionalidad parecida.

Ejemplo 1:
using System;
using System.IO;

class Test
{
public static void Main()
{
// Create an instance of StreamWriter to write text to a file.
// The using statement also closes the StreamWriter.
using (StreamWriter sw = new StreamWriter("TestFile.txt"))
{
// Add some text to the file.
sw.Write("This is the ");
sw.WriteLine("header for the file.");
sw.WriteLine("-------------------");
// Arbitrary objects can also be written to the file.
sw.Write("The date is: ");
sw.WriteLine(DateTime.Now);
}
}
}

Ejemplo 2:
using System;
using System.IO;
public class TextToFile
{
private const string FILE_NAME = "MyFile.txt";
public static void Main(String[] args)
{
if (File.Exists(FILE_NAME))
{
Console.WriteLine("{0} already exists.", FILE_NAME);
return;
}
using (StreamWriter sw = File.CreateText(FILE_NAME))
{
sw.WriteLine ("This is my file.");
sw.WriteLine ("I can write ints {0} or floats {1}, and so on.", 1, 4.2);
sw.Close();
}
}
}

88
8.2.4 Lectura y escritura, 8.2.5 Recorrer y 8.2.3 Cerrar

Texto
Los ejemplos de código siguientes muestran cómo leer texto desde un archivo de texto. El
segundo ejemplo ofrece una notificación cuando se detecta el fin del archivo. Esta
funcionalidad también se puede conseguir utilizando los métodos ReadAllLines o
ReadAllText.

Ejemplo
using System;
using System.IO;

class Test
{
public static void Main()
{
try
{
// Create an instance of StreamReader to read from a file.
// The using statement also closes the StreamReader.
using (StreamReader sr = new StreamReader("TestFile.txt"))
{
String line;
// Read and display lines from the file until the end of
// the file is reached.
while ((line = sr.ReadLine()) != null)
{
Console.WriteLine(line);
}
}
}
catch (Exception e)
{
// Let the user know what went wrong.
Console.WriteLine("The file could not be read:");
Console.WriteLine(e.Message);
}
}
}

using System;
using System.IO;
public class TextFromFile
{
private const string FILE_NAME = "MyFile.txt";
public static void Main(String[] args)
{
if (!File.Exists(FILE_NAME))
{
Console.WriteLine("{0} does not exist.", FILE_NAME);
return;

89
}
using (StreamReader sr = File.OpenText(FILE_NAME))
{
String input;
while ((input=sr.ReadLine())!=null)
{
Console.WriteLine(input);
}
Console.WriteLine ("The end of the stream has been reached.");
sr.Close();
}
}

Binario
8.2.4 Lectura y escritura, 8.2.5 Recorrer y 8.2.3 Cerrar
Las clases BinaryWriter y BinaryReader se usan para escribir y leer datos, en lugar de
cadenas de caracteres. En el siguiente ejemplo de código se muestra cómo se escriben y
se leen datos en una nueva secuencia de archivos vacía (Test.data). Después de crear el
archivo de datos en el directorio actual, se crean las clases BinaryWriter y BinaryReader
asociadas, y BinaryWriter se usa para escribir los enteros de 0 a 10 en Test.data, que
deja el puntero de archivo al final del archivo. Después de volver a establecer el puntero
de archivo en el origen, BinaryReader lee el contenido especificado.

using System;
using System.IO;
class MyStream
{
private const string FILE_NAME = "Test.data";
public static void Main(String[] args)
{
// Create the new, empty data file.
if (File.Exists(FILE_NAME))
{
Console.WriteLine("{0} already exists!", FILE_NAME);
return;
}
FileStream fs = new FileStream(FILE_NAME, FileMode.CreateNew);
// Create the writer for data.
BinaryWriter w = new BinaryWriter(fs);
// Write data to Test.data.
for (int i = 0; i < 11; i++)
{
w.Write( (int) i);
}
w.Close();
fs.Close();
// Create the reader for data.
fs = new FileStream(FILE_NAME, FileMode.Open, FileAccess.Read);
BinaryReader r = new BinaryReader(fs);
// Read data from Test.data.

90
for (int i = 0; i < 11; i++)
{
Console.WriteLine(r.ReadInt32());
}
w.Close();
}
}

Test.data ya existe en el directorio actual, se inicia una IOException. Use FileMode.Create


para crear siempre un archivo nuevo sin iniciar una IOException.

Bibliografía

Titulo: C# Manual de programación


Autor: Joyanes Fernandez
Editorial: McGraw Hill

Titulo: Microsoft C# Lenguaje y aplicaciones


Autor: Francisco Javier Ceballos
Editorial: Ra-ma

WWW.MICROSOFT.COM.MX

91

You might also like