Está en la página 1de 121

Desarrollo Profesional de Aplicaciones

Introducción al lenguaje de programación C#

Características del lenguaje C#


Aspectos Léxicos
Tipos de datos
Variables y constantes
Operadores y expresiones
Estructuras de control
E/S básica
C# (leído en inglés "C Sharp" y en español "C Almohadilla") es el nuevo lenguaje de propósito
general diseñado por Microsoft para su plataforma .NET.
Aunque es posible escribir código para la plataforma .NET en muchos otros lenguajes, C# es el
único que ha sido diseñado específicamente para ser utilizado en ella, por lo que programarla
usando C# es mucho más sencillo e intuitivo que hacerlo con cualquiera de los otros lenguajes ya
que C# carece de elementos heredados innecesarios en .NET. Por esta razón, se suele decir que
C# es el lenguaje nativo de .NET
Características del lenguaje C#
Aunque es pronto para entrar con detenimiento en el lenguaje C# podemos adelantar las
características más relevantes de este lenguaje, características que se describen con profundidad
posteriormente, durante el estudio detallado de los elementos del lenguaje.
 Es autocontenido. Un programa en C# no necesita de ficheros adicionales al
propio código fuente, como los ficheros de cabecera (.h) de C++, lo que simplifica
la arquitectura de los proyectos software desarrollados con C++.
 Es homogeneo. El tamaño de los tipos de datos básicos es fijo e independiente
del compilador, sistema operativo o máquina en la que se compile (no ocurre lo
que en C++), lo que facilita la portabilidad del código.
 Es actual. C# incorpora en el propio lenguaje elementos que se han demostrado
ser muy útiles para el desarrollo de aplicaciones como el tipo básico decimal que
representa valores decimales con 128 bits, lo que le hace adecuado para cálculos
financieros y monetarios, incorpora la instrucción foreach, que permite una cómoda
iteración por colecciones de datos, proporciona el tipo básico string, permite definir
cómodamente propiedades (campos de acceso controlado), etc.
 Está orientado a objetos. C# soporta todas las características propias del
paradigma de la programación orientada a objetos: encapsulación, herencia y
polimorfismo.
o Encapsulación: además de los modificadores de accceso convencionales:
public, private y protected, C# añade el modificador internal, que limita el
acceso al proyecto actual.
o C# sólo admite herencia simple.
o Todos los métodos son, por defecto, sellados, y los métodos redefinibles
han de marcarse, obligatoriamente, con el modificador virtual.
 Delega la gestión de memoria. Como todo lenguaje de .NET, la gestión de la
memoria se realiza automáticamente ya que tiene a su disposición el recolector de
basura del CLR. Esto hace que el programador se desentienda de la gestión
directa de la memoria (petición y liberación explícita) evitando que se cometan los
errores habituales de este tipo de gestión en C++, por ejemplo.
En principio, en C# todo el código incluye numerosas restricciones para asegurar
su seguridad no permite el uso de punteros, por ejemplo. Sin embargo, y a
diferencia de Java, en C# es posible saltarse dichas restricciones manipulando
objetos a través de punteros. Para ello basta marcar regiones de código como
inseguras (modificador unsafe) y podrán usarse en ellas punteros de forma similar
a cómo se hace en C++, lo que puede resultar vital para situaciones donde se
necesite una eficiencia y velocidad procesamiento muy grandes.
 Emplea un sistema de tipos unificado. Todos los tipos de datos (incluidos los
definidos por el usuario) siempre derivarán, aunque sea de manera implícita, de
una clase base común llamada System.Object, por lo que dispondrán de todos los
miembros definidos en ésta clase. Esto también es aplicable, lógicamente, a los
tipos de datos básicos.
 Proporciona seguridad con los tipos de datos. C# no admiten ni funciones ni
variables globales sino que todo el código y datos han de definirse dentro de
definiciones de tipos de datos, lo que reduce problemas por conflictos de nombres
y facilita la legibilidad del código.
C# incluye mecanismos que permiten asegurar que los accesos a tipos de datos
siempre se realicen correctamente:
o No pueden usarse variables que no hayan sido inicializadas.
o Sólo se admiten conversiones entre tipos compatibles
o Siempre se comprueba que los índices empleados para acceder a los
elementos de una tabla (vector o matriz) se encuentran en el rango de
valores válidos.
o Siempre se comprueba que los valores que se pasan en una llamada a
métodos que pueden admitir un número indefinido de parámetros (de un
cierto tipo) sean del tipo apropiado.
 Proporciona instrucciones seguras. En C# se han impuesto una serie de
restricciones para usar las instrucciones de control más comunes. Por ejemplo,
toda condición está controlada por una expresión condicional, los casos de una
instrucción condicional múltiple (switch) han de terminar con una instrucción break
o goto, etc.
 Facilita la extensibilidad de los operadores. C# permite redefinir el significado
de la mayoría de los operadores -incluidos los de conversión, tanto para
conversiones implícitas como explícitas- cuando se aplican a diferentes tipos de
objetos.
 Permite incorporar modificadores informativos sobre un tipo o sus
miembros. C# ofrece, a través del concepto de atributos, la posibilidad de añadir,
a los metadatos del módulo resultante de la compilación de cualquier fuente,
información sobre un tipo o sus miembros a la generada por el compilador que
luego podrá ser consultada en tiempo ejecución a través de la biblioteca de
reflexión de .NET. Esto, que más bien es una característica propia de la
plataforma .NET y no de C#, puede usarse como un mecanismo para definir
nuevos modificadores.
 Facilita el mantenimiento (es "versionable"). C# incluye una política de
versionado que permite crear nuevas versiones de tipos sin temor a que la
introducción de nuevos miembros provoquen errores difíciles de detectar en tipos
hijos previamente desarrollados y ya extendidos con miembros de igual nombre a
los recién introducidos.
 Apuesta por la compatibilidad. C# mantiene una sintaxis muy similar a C++ o
Java que permite, bajo ciertas condiciones, incluir directamente en código escrito
en C# fragmentos de código escrito en estos lenguajes.
En resumen, podemos concluir que:
 Es un lenguaje orientado al desarrollo de componentes (módulos independientes
de granularidad mayor que los objetos) ya que los componentes son objetos que
se caracterizan por sus propiedades, métodos y eventos y estos aspectos de los
componentes están presentes de manera natural en C#.
 En C# todo son objetos: desaparece la distinción entre tipos primitivos y objetos de
lenguajes como Java o C++ (sin penalizar la eficiencia como en LISP o Smalltalk).
 El software es robusto y duradero: el mecanismo automático de recolección de
basura, la gestión de excepciones, la comprobación de tipos, la imposibilidad de
usar variables sin inicializar y hacer conversiones de tipo (castings) no seguras,
gestión de versiones, etc. ayudan a desarrollar software fácilmente mantenible y
poco propenso a errores.
 Además, no hay que olvidar el aspecto económico: la posibilidad de utilizar C++
puro (código no gestionado o inseguro), la facilidad de interoperabilidad (XML,
SOAP, COM, DLLs...) junto con un aprendizaje relativamente sencillo (para los que
ya conocen otros lenguajes de programación) hace que el dominio y uso del
lenguaje junto a otras tecnologías sea muy apreciado.
Aspectos Léxicos
En esta sección presentaremos las reglas sintácticas básicas que deben cumplir los programas
escritos en C# y veremos los elementos fundamentales de cualquier programa en C#
(identificadores, comentarios, palabras reservadas, etc.). Se trata, en definitiva, de la parte
instrumental y básica, además de imprescindible, de cualquier manual de programación.
Es costumbre desde la época dorada del lenguaje C (quizá una de las pocas costumbres que se
mantienen en el mundo de la informática) que se presente un lenguaje de programación
empleando un programa que muestra en la consola el mensaje ¡Hola, mundo! (para ser más
precisos deberíamos decir Hello, world!). Sigamos manteniendo esta costumbre:
¡Hola, mundo!

/*
Fichero: Saludo.cs
Fecha: Enero de 2004
Autores: F. Berzal, F.J. Cortijo y J.C.Cubero
Comentarios:
Primer programa escrito en C#
*/

using System;

public class SaludoAlMundo


{
public static void Main( )
{
// Mostrar en la consola el mensaje: ¡Hola, mundo!

Console.WriteLine("¡Hola, mundo!");
Console.ReadLine(); // Enter para terminar.
}
}
Lo primero que hay que resaltar es que C# es un lenguaje sensible a las mayúsculas, por lo que,
por ejemplo, Main es diferente a main, por lo que deberá prestar atención a la hora de escribir el
código ya que la confusión entre mayúsculas y minúsculas provocará errores de compilación.
Todas las órdenes acaban con el símbolo del punto y coma (;). Los bloques de órdenes (parte
iterativa de un ciclo, partes dependientes de una instrucción condicional -parte if y parte else-,
código de un método, etc.) se encierran entre llaves {} y no se escribe el ; después de la llave de
cierre (observar en el ejemplo el método Main).
Si el lector ha programado en C++ no habrá tenido dificultad en localizar los comentarios que
hemos insertado en el programa ya que la sintaxis es idéntica en ambos lenguajes: existen
comentarios de línea, cuyo comienzo se especifica con los caracteres // y comentarios de formato
libre, delimitados por /* y */.
C# es un lenguaje orientado a objetos y todo programa debe pertenecer a una clase; en este caso
la clase se llama SaludoAlMundo.
La línea using System declara que se va a usar el espacio de nombres (namespace) llamado
System. Esta declaración no es igual a un #include de C#, tan solo evita escribir el prefijo System
cada vez que se use un elemento de ese espacio de nombres. Por ejemplo, Console es un objeto
del espacio de nombres System; en lugar de escribir su nombre completo (System.Console)
podemos escribir solamente Console al haber declarado que se va a emplear el espacio de
nombres System.
Cuando este programa se compile y se proceda a su ejecución, el primera función (estrictamente
hablando, método) en ejecutarse será Main. Los programadores de C++ deberán tener especial
cuidado y no confundirlo con main().
La función Main() tiene los siguientes prototipos válidos:
 Si la función no devuelve ningún valor, puede usarse:
 public static void Main( )
 public static void Main(string [] args)
La diferencia está en la posibilidad de suministrar argumentos en la llamada a la
ejecución del programa. Main() procesan los argumentos tomándolos de la lista de
cadenas args (más adelante se detalla cómo).
 Si la función devuelve un valor al proceso que invoca la ejecución del programa, el
valor debe ser entero (int) y, como en el caso anterior, puede usarse:
 public static int Main( )
 public static int Main(string [] args)
La convención es que el valor 0 indica que el programa termina correctamente.
En cuanto a las instrucciones que efectivamente se ejecutan, el método Main() llama a los métodos
WriteLine() y ReadLine() del objeto Console. El primero se encargan de mostrar una cadena en la
consola (Símbolo de Sistema, en Windows XP) y el segundo de tomar una cadena de la consola
(teclado). Aunque esta última instrucción pueda parecer innecesaria, de no escribirla se mostraría
la cadena ¡Hola, mundo! y se cerraría inmediatamente la consola, por lo que no podríamos
contemplar el resultado de la ejecución. La última instrucción detiene la ejecución del programa
hasta que se introduce una cadena (pulsar ENTER es suficiente) y a continuación termina la
ejecución del programa y se cierra la consola. Así, cuando usemos aplicaciones de consola
siempre terminaremos con esta instrucción.
A continuacón detallaremos ciertos aspectos léxicos importantes de los programas escritos en C#,
algunos de los cuales ya se han comentado brevemente como explicación al programa
introductorio.
Comentarios
Los comentarios tienen como finalidad ayudar a comprender el código fuente y están destinados,
por lo tanto, a los programadores. No tienen efecto sobre el código ejecutable ya que su contenido
es ignorado por el compilador (no se procesa). La sintaxis de los comentarios en C# es idéntica a
la de C++ y se distinguen dos tipos de comentarios:
 Comentarios de línea. Están precedidos de la construcción // y su efecto (ámbito)
termina en la línea en la que está inmerso.
 Comentarios de formato libre. Están delimitados por las construcciones /* y */ y
pueden extenderse por varias líneas.
Ejemplos de comentarios

// En una línea, al estilo de C++

/*
En múltiples líneas, como se viene
haciendo desde "los tiempos de C"
*/

/* Este tipo de comentario ya no es habitual */


Identificadores
Un identificador es un nombre con el que nos referimos a algún elemento de nuestro programa:
una clase, un objeto, una variable, un método, etc. Se imponen algunas restricciones acerca de los
nombres que pueden emplearse:
 Deben comenzar por una letra letra o con el carácter de subrayado (_), que está
permitido como carácter inicial (como era tradicional en el lenguaje C).
 No pueden contener espacios en blanco.
 Pueden contener caracteres Unicode, en particular secuencias de escape Unicode.
 Son sensibles a mayúsculas/minúsculas.
 No pueden coincidir con palabras reservadas (a no ser que tengan el prefijo @ que
habilita el uso de palabras clave como identificadores).
Los identificadores con prefijo @ se conocen como identificadores literales.
Aunque el uso del prefijo @ para los identificadores que no son palabras clave está
permitido, no se recomienda por regla de estilo.
Palabras reservadas
Las palabras reservadas son identificadores predefinidos reservados que tienen un significado
especial para el compilador por lo que no se pueden utilizar como identificadores en un programa a
menos que incluyan el carácter @ como prefijo.
Las palabras reservadas en C# son, por orden alfabético:
abstract, as, base, bool, break, byte, case, catch, char, checked, class, const, continue, decimal,
default, delegate, do, double, else, enum, event, explicit, extern, false, finally, fixed, float, for,
foreach, goto, if, implicit, in, int, interface, internal, is, lock, long, namespace, new, null, object,
operator, out, override, params, private, protected, public, readonly, ref, return, sbyte, sealed, short,
sizeof, stackalloc, static, string, struct, switch, this, throw, true, try, typeof, uint, ulong, unchecked,
unsafe, ushort, using, virtual, void, volatile, while
Literales
Un literal es una representación en código fuente de un valor. Todo literal tiene asociado un tipo,
que puede ser explícito (si se indica en el literal, mediante algún sufijo, por ejemplo) o implícito (se
asume uno por defecto).
Los literales pueden ser:
 Literales lógicos.
Existen dos valores literales lógicos: true y false. El tipo de un literal lógico es bool.
 Literales enteros.
Permiten escribir valores de los tipos enteros: int, uint, long y ulong. Los literales
enteros tienen dos formatos posibles: decimal y hexadecimal. Los literales
hexadecimales tienen el sufijo 0x.
El tipo de un literal entero se determina como sigue:
o Si no tiene sufijo, su tipo es int, uint, long o ulong.
o Si tiene el sufijo U o u, su tipo es uint o ulong.
o Si tiene el sufijo L o l, su tipo es long o ulong.
o Si tiene el sufijo UL, Ul, uL, ul, LU, Lu, lU o lu es de tipo ulong.
A la hora de escribir literales de tipo long se recomienda usar L en lugar
de l para evitar confundir la letra l con el dígito 1.
Literales enteros

123 // int
0x7B // hexadecimal
123U // unsigned
123ul // unsigned long
123L // long
 Literales reales.
Los literales reales permiten escribir valores de los tipos float, double y decimal.
El tipo de un literal real se determina como sigue:
o Si no se especifica sufijo, el tipo es double.
o Si el sufijo es F o f es de tipo float.
o Si el sufijo es D o d es de tipo double.
o Si el sufijo es M o m es de tipo decimal.
Hay que tener en cuenta que, en un literal real, siempre son necesarios
dígitos decimales tras el punto decimal. Por ejemplo, 3.1F es un literal
real, pero no así 1.F.
Literales reales
1f, 1.5f, 1e10f, 123.456F, 123f y 1.23e2f // float
1d, 1.5d, 1e10d, 123.456D, 123.0 y 123D // double
1m, 1.5m, 1e10m, 123.456M, 123.456m y 12.3E1M // decimal.
 Literales de caracteres.
Un literal de caracteres representa un carácter único y normalmente está
compuesto por un carácter entre comillas simples, por ejemplo 'A'.
Una secuencia de escape sencilla representa una codificación de caracteres
Unicode y está formada por el carácter \ seguido de otro carácter. Las secuencias
válidas se describe en la siguiente tabla.
Secuencia de escape Nombre del carácter Codificación Unicode
\' Comilla simple 0x0027
\" Comilla doble 0x0022
\\ Barra invertida 0x005C
\0 Null 0x0000
\a Alerta 0x0007
\b Retroceso 0x0008
\f Avance de página 0x000C
\n Nueva línea 0x000A
\r Retorno de carro 0x000D
\t Tabulación horizontal 0x0009
\v Tabulación vertical 0x000B
El tipo de un literal de caracteres es char.
Literales de caracteres

'A' // caracter sencillo


'\u0041' // caracter Unicode
'\x0041' // unsigned short hexadecimal
'\n' // caracter de escape: CR+LF
 Literales de cadena.
C# admite dos formatos de literales de cadena: literales de cadena típicos y
literales de cadena textuales. El tipo de un literal de cadena es string.
Un literal típico de cadena consta de cero o más caracteres entre comillas dobles y
puede incluir secuencias de escape sencillas y secuencias de escape
hexadecimales y Unicode.
Literales tipicos de cadena

"!Hola, mundo!" // !Hola, mundo!


"!Hola, \t mundo!" // !Hola, mundo!
"" // La cadena vacia
Un literal de cadena textual consta del carácter @ seguido de un carácter de
comillas dobles, cero o más caracteres y un carácter de comillas dobles de cierre.
Por ejemplo, @"Hola". En estos literales los caracteres se interpretan de manera
literal y no se procesan las secuencias de escape, con la única excepción de la
secuencia \". Un literal de cadena textual puede estar en varias líneas.
Literales de cadena textuales

@"!Hola, \t mundo!" // !Hola, \t mundo!


"Me dijo \"Hola\" y me asustó" // Me dijo "Hola" y me asustó
@"Me dijo ""Hola"" y me asustó" // Me dijo "Hola" y me asustó
"\\\\servidor\\share\\file.txt" // \\servidor\share\file.txt
@"\\servidor\share\file.txt" // \\servidor\share\file.txt
@"uno // Esta es una cadena distribuida
dos" // en dos lineas.
 Literal null.
Su único valor es null y su tipo es el tipo null.
Órdenes
 Delimitadas por punto y coma (;) como en C, C++ y Java.
 Los bloques { ... } no necesitan punto y coma al final.
Tipos de datos
Los tipos de datos ofrecidos por C# al programador forman parte de un sistema unificado en el que
todos los tipos de datos (incluidos los definidos por el usuario) derivan, aunque sea de manera
implícita, de la clase System.Object. Por herencia dispondrán de todos los miembros definidos en
ésta clase, en particular los útiles métodos Equals(), GetHashCode(), GetType() y ToString() que
describiremnos más adelante.
C# proporciona seguridad con los tipos de datos. C# no admiten ni funciones ni variables globales
sino que todo el código y datos han de definirse dentro de definiciones de tipos de datos, lo que
reduce problemas por conflictos de nombres y facilita la legibilidad del código. C# incluye
mecanismos que permiten asegurar que los accesos a tipos de datos siempre se realicen
correctamente:
 No pueden usarse variables que no hayan sido iniciadas.
 El tipo asignado restringe los valores que puede almacenar y las operaciones en
las que puede intervenir.
 Siempre se comprueba que los índices empleados para acceder a los elementos
de una tabla (vector o matriz) se encuentran en el rango de valores válidos.
 Sólo se admiten conversiones de tipo entre tipos compatibles y entre aquellos que
se hayan definido explícitamente el mecanismo de conversión (En C# puede
implementarse la manera en que se realiza la conversión implícita y explícita entre
tipos)
Los tipos de datos en C# pueden clasificarse en dos grandes categorías:
 tipos valor
 tipos referencia
y pueden caracterizarse como sigue:
Tipos valor Tipos referencia
La variable contiene un valor La variable contiene una referencia
El dato se almacena en la pila El dato se almacena en el heap
El dato siempre tiene valor El dato puede no tener valor null
Una asignación copia el valor Una asignación copia la referencia

int i = 123; // tipo valor

string s = "Hello world"; // tipo referencia

El comportamiento cuando se copian o modifican objetos de estos tipos es muy diferente.


Tipos valor
Los tipos básicos son tipos valor. Si una variable es de un tipo valor contiene únicamente un valor
del tipo del que se ha declarado.
Los tipos predefinidos de C# son tipos disponibles en la plataforma .NET y que, por comodidad, en
C# se emplean usando un alias. En la tabla siguiente enumeramos los tipos simples detallando su
nombre completo, su alias, una breve descripción, el número de bytes que ocupan y el rango de
valores.
Tamañ
Nombre (.NET Descripció
Alias o Rango
Framework) n
(bytes)
System.Sbyte sbyte Bytes con 1 -128 ... 127
signo
Enteros
System.Int16 short 2 -32.768 ... 32.767
cortos
-2.147.483.648 ...
System.Int32 int Enteros 4
2.147.483.647
-
Enteros 9.223.372.036.854.775.808
System.Int64 long 8
largos ...
9.223.372.036.854.775.807
Bytes (sin
System.Byte byte 1 0 ... 255
signo)
Enteros
System.Uint16 ushort cortos (sin 2 0 ... 65.535
signo)
0 ...
Enteros (sin
System.UInt32 uint 4 18.446.744.073.709.551.61
signo)
5
Enteros 0 ...
System.Uint64 ulong largos (sin 8 18.446.744.073.709.551.61
signo) 5
Reales (7 ±1.5 x 10-45 ... ±3.4 x
System.Single float 4
decimales) 10+38
Reales (15-
±5.0 x 10-324 ... ±1.7 x
System.Double double 16 8
10+308
decimales)
Reales (28-
System.Decima decima ±1.0 x 10-28 ... ±7.9 x
29 12
l l 10+28
decimales)
Caracteres
System.Char char 2 Cualquier carácter Unicode
Unicode
System.Boolea Valores
bool 1 true ó false
n lógicos
El comportamiento de los datos de tipos valor es el esperado cuando se inician o reciben un valor
por asignación a partir de otro dato de tipo valor (son independientes).
Tipos referencia
Si un dato es de un tipo referencia contiene la dirección de la información, en definitiva, una
referencia al objeto que contiene los datos y/o métodos. En definitiva, distinguimos:
 La referencia o un nombre por el que nos referimos al objeto y que utilizamos para
manipularlo.
 El objeto referenciado, que ocupa lugar en memoria (en el heap) y que almacenará
el valor efectivo del objeto.
En definitiva: la variable y su contenido "lógico" están en posiciones de memoria diferentes. El valor
almacenado en una variable de tipo referencia es la dirección de memoria del objeto referenciado
(es una referencia) o tiene el valor null (no referencia a nada). Observe que pueden existir dos
variables que referencien al mismo objeto (pueden existir dos referencias a la misma zona de
memoria).
C# proporciona dos tipos referencia predefinidos: object y string. Todos los demás tipos
predefinidos son tipos valor.
El tipo object es el tipo base del cual derivan todos los tipos básicos predefinidos y los creados por
el usuario. Pueden crearse nuevos tipos referencia usando declaraciones de clases (class),
interfaces (interface) y delegados (delegate), y nuevos tipos valor usando estructuras struct.
Los objetos de las clases creadas por el usuario son siempre de tipo referencia. El operador new
permite la creación de instancias de clases definidas por el usuario. new es muy diferente en C# y
en C++:
 En C++ indica que se pide memoria dinámica.
 En C# indica que se llama al constructor de una clase.
El efecto, no obstante, es similar ya que como la variable es de un tipo referencia, al llamar al
constructor se aloja memoria en el heap de manera implícita.
Considere el siguiente fragmento de código, en el que todas las variables son del mismo tipo:
ObjetoDemo).
Tipos referencia

class ObjetoDemo
{
public int Valor;
}

class AppDemoRef
{

static void Main(string[] args)


{
ObjetoDemo o1 = new ObjetoDemo(); // new llama a un constructor
o1.Valor = 10;
ObjetoDemo o2 = new ObjetoDemo(); // new llama a un constructor
o2 = o1; // La memoria que ocupaba el objeto refernciado por "o2"
// se pierde: actuará el recolector de basura.
PintaDatos ("o1", "o2", o1, o2);

ObjetoDemo o3 = new ObjetoDemo();// new llama a un constructor


o3.Valor = 10;
ObjetoDemo o4 = o3; // "o4" contiene la misma direccion de memoria
que "o3"
o4.Valor = 20; // Igual que hacer o3.Valor = 20;
PintaDatos ("o3", "o4", o3, o4);

ObjetoDemo o5 = new ObjetoDemo(); // new llama a un constructor


o5.Valor = 10;
ObjetoDemo o6 = new ObjetoDemo(); // new llama a un constructor
o6.Valor = o5.Valor;
PintaDatos ("o5", "o6", o5, o6);

Console.ReadLine();
}

static void PintaDatos (string st1, string st2, ObjetoDemo ob1,


ObjetoDemo ob2)
{
Console.Write ("{0} = {1}, {2} = {3} ", st1, ob1.Valor, st2, ob2.Valor);
if (ob1==ob2)
Console.WriteLine ("{0} == {1}", st1, st2);
else
Console.WriteLine ("{0} != {1}", st1, st2);
}
}
El tipo string es un tipo especial de tipo referencia. De hecho, parece más un tipo valor ante la
asignación. Observe el ejemplo:

string s1 = "Hola";
string s2 = s1;
En este punto s2 referencia al mismo objeto que s1. Sin embargo, cuando el valor de s1 es
modificado, por ejemplo con:

s1 = "Adios";
lo que ocurre es que se crea un nuevo objeto string referenciado por s1. De esta forma, s1 contiene
"Adios" mientras que s2 mantiene el valor "Hola". Esto es así porque los objetos string son
immutables, por lo que, para cambiar lo que referencia una variable string debe crearse un nuevo
objeto string.
Variables y constantes
Variables
Una variable permite el almacenamiento de datos en la memoria. Es una abstracción que permite
referirnos a una zona de memoria mediante un nombre (su identificador). Todas las variables
tienen asociadas un tipo que determina los valores que pueden almacenarse y las operaciones que
pueden efectuarse con los datos de ese tipo. Además, el término variable indica que el contenido
de esa zona de memoria puede modificarse durante la ejecución del programa.
Nombres de variables
Los nombres que pueden asignarse a las variables deben regirse por unas normas básicas:
 Pueden contener letras, dígitos y el caracter de subrayado (_).
 No pueden empezar con un número: deben comenzar por una letra letra o con el
carácter de subrayado (_).
Finalmente, recordar que, como identificador que es, el nombre de una variable es sensible a las
mayúsculas y no pueden coincidir con una palabra reservada a no ser que tenga el prefijo @,
aunque no es una práctica recomendada.
Declaración de variables
Antes de usar una variable se debe declarar. La declaración de una variable indica al compilador
el nombre de la variable y su tipo. Una declaración permite que se pueda reservar memoria para
esa variable y restringir el espacio (cantidad de memoria) que requiere, los valores que pueden
asignarsele y las operaciones en las que puede intervenir.
La sintaxis de una declaración es sencilla: tan sólo hay que especificar el tipo de la variable y el
nombre que se le asocia. La declaración debe concluir con el carácter punto y coma. Por ejemplo,
si vamos a emplear una variable para guardar en ella el valor del área de un círculo debemos:
 Darle un nombre significativo: Area
 Asociarle un tipo: dado que puede tener decimales y no se requiere una gran
precisión, bastará con el tipo float.

float Area;
Cuando se van a declarar múltiples variables del mismo tipo no es necesario que cada declaración
se haga por separado, pueden agruparse en la misma línea compartiendo el tipo. Por ejemplo, las
declaraciones:
float Radio;
float Area;
pueden simplificarse en una línea:

float Radio, Area;


De la misma manera pueden declararse e inicializarse variables en una sola línea:

int a=1, b=2, c, d=4;


No existe ninguna zona predeterminada en el código para la declaración de variables, la única
restricción es que la declaración debe realizarse antes de su uso.
No es conveniente abusar de la declaración múltiple de variables en una
línea. Desde el punto de vista de la legibilidad es preferible, por regla
general, que cada variable se declare separadamente y que la
declaración vaya acompañada de un breve comentario:

float Radio; // Radio del circulo del cual se calcula el


area.
float Area; // Area del circulo
Acceso a variables
Una variable se usa para asignarle un valor (acceso de escritura) o para utilizar el valor
almacenado (acceso de lectura).
Una vez declarada una variable debe recibir algún valor (es su misión, después de todo). Este valor
lo puede recibir de algún dispositivo (flujo de entrada) o como resultado de evaluar una expresión.
La manera más simple de proporcionar un valor a una variable es emplear la instrucción de
asignación:

Radio = 10.0F;
En el ejemplo se asigna el valor (literal entero) 10 a la variable Radio. El valor que tuviera
almacenado la variable Radio se pierde, quedando fijado a 10.
En la misma línea de la declaración puede asignarse un valor a la variable, por lo que declaración e
inicialización:

float Radio; // Declaracion


Radio = 10.0F; // Inicializacion
pueden simplificarse en una sola línea:

float Radio = 10.0F; // Declaracion e Inicializacion


La manera más simple de leer el valor de una variable es emplearla como parte de una expresión,
por ejemplo, en la parte derecha de una instrucción de asignación:

Area = 2 * 3.1416F * Radio * Radio;


La variable Radio (su valor) se emplea en la expresión 2 * 3.1416 * Radio * Radio para calcular el
área de un círculo de radio Radio. Una vez calculado el valor de la expresión, éste se asigna a la
variable Area.
Otra manera de acceder al valor de una variable para lectura es emplearla como el argumento de
una instrucción de escritura WriteLine. Por ejemplo,
Console.WriteLine(Area);
mostrará en la consola el valor de la variable Area.
Leer el valor de una variable no modifica el contenido de la variable.
A modo de resumen, un programa que calcula y muestra el área de un círulo de radio 10 es el
siguiente:
Calculo del área de un círculo (1)

using System;
class Area1
{
static void Main(string[] args)
{
float Radio = 10.0F;
float Area = Area = 2 * 3.1416F * Radio * Radio;
Console.WriteLine(Area);
Console.ReadLine();
}
}
Como puede parecer evidente, emplear una variable que no ha sido declarada produce un error en
tiempo de compilación. En C#, además, hay que asignarle un valor antes de utilizarla. Si no se
hace se genera un error en tiempo de compilación ya que esta comprobación (igual que ocurre en
Java) se efectúa por el compilador.

void f()
{
int i;
Console.WriteLine(i); // Error: uso de la variable local no asignada 'i'
}
Constantes
Una constante se define de manera parecida a una variable: modeliza una zona de memoria que
puede almacenar un valor de un tipo determinado. La diferencia reside en que esa zona de
memoria (su contenido) no puede modificarse en la ejecución del programa. El valor de la
constante se asigna en la declaración.
Sintácticamente se especifica que un dato es constante al preceder con la palabra reservada const
su declaración. Por ejemplo, para declarar un a constante de tipo float llamada PI y asignarle el
valor (constante) 3.1416 se escribirá:

const float PI = 3.1416;


Solo se puede consultar el valor de una constante, nunca se debe intentar modificarlo porque se
produciría un error en tiempo de compilación. Por ejemplo, la siguiente instrucción:
Area = 2 * PI * Radio * Radio;
utiliza la constante PI declarada anteriormente, usando su valor para evaluar una expresión.
Podemos modificar el programa anterior para que utilice la constante PI:
Calculo del área de un círculo (2)

using System;

class Area2
{
static void Main(string[] args)
{
const float PI = 3.1416F;
float Radio = 10.0F;
float Area = 2 * PI * Radio * Radio;
Console.WriteLine(Area);
Console.ReadLine();
}
}
Ámbito de variables y constantes
El ámbito (del inglés scope) de una variable y/o constante indica en qué partes del código es lícito
su uso.
En C# el ámbito abarca desde el lugar de su declaración hasta donde termina el bloque en el que
fue declarada.
En el ámbito de una variable no puede declararse otra variable con el mismo nombre (aunque sea
en un bloque interno, algo que si está permitido en C++):

static void Main(string[] args)


{
float Radio = 10.0F;
...
if (Radio > 0){
float Radio = 20.0F; // Error: No se puede declarar una variable
// local denominada 'Radio' en este ámbito.
}
...
}
Operadores y expresiones
Un operador está formado por uno o más caracteres y permite realizar una determinada operación
entre uno o más datos y produce un resultado. Es una manera simbólica de expresar una
operación sobre unos operandos.
C# proporciona un conjunto fijo, suficiente y completo de operadores. El significado de cada
operador está perfectamente definido para los tipos predefinidos, aunque algunos de ellos pueden
sobrecargarse, es decir, cambiar su significado al aplicarlos a un tipo definido por el usuario.
C# dispone de operadores aritméticos, lógicos, relacionales, de manipulación de bits, asignación,
para acceso a tablas y objetos, etc. Los operadores pueden presentarse por diferentes criterios,
por ejemplo, por su funcionalidad:
Categorías Operadores
Aritméticos +-*/%
Lógicos (booleanos y bit a bit) & | ^ ! ~ && ||
Concatenación de cadenas +
Incremento y decremento ++ --
Desplazamiento << >>
Relacionales == != < > <= >=
= += -= *= /= %= &= |= ^= <<=
Asignación
>>=
Acceso a miembros .
Acceso por índices []
Conversión de tipos explícita ()
Conditional ?:
Creación de objetos new
Información de tipos as is sizeof typeof
Control de excepciones de
checked unchecked
desbordamiento
Direccionamiento indirecto y dirección * -> [] &
 Los operadores aritméticos de C# son los que se emplean comúnmente en otros
lenguajes: + (suma), - (resta), * (multiplicación), / (división) y % (módulo o resto de
la división).
Son operadores binarios y se colocan entre los argumentos sobre los que se
aplican, proporcionando un resultado numérico (Por ejemplo, 7+3.5, 66 % 4).
 Los operadores lógicos proporcionan un resultado de tipo lógico (bool) y los
operadores a nivel de bit actúan sobre la representación interna de sus
operandos (de tipos enteros) y proporcionan resultados de tipo numérico.
Los operadores binarios son: & (operación Y lógica entre argumentos lógicos u
operación Y bit a bit entre operandos numéricos), | (operación O, lógica ó bit a bit,
dependiendo del tipo de los argumentos) , ^ (O exclusivo, lógico ó bit a bit), && (Y
lógico, que evalúa el segundo operando solo cuando es necesario) y || (O lógico,
que evalúa el segundo operando solo cuando es necesario).
Los operadores unarios son: ! (negación o complemento de un argumento lógico) y
~ (complemento bit a bit de un argumento numérico).
 El operador + para la concatenación de cadenas es un operador binario. Cuando
al menos uno de los operandos es de tipo string este operador actúa uniendo las
representaciones de tipo string de los operandos.
 Operadores de incremento y decremento. El operador de incremento (++)
incrementa su operando en 1 mientras que el de decremento (--) decrementa su
operando en 1. Puede aparecer antes de su operando: ++v (incremento prefijo) o
después: v++ (incremento postfijo).
El incremento prefijo hace que el resultado sea el valor del operando después de
haber sido incrementado y el postfijo hace que el resultado sea valor del operando
antes de haber sido incrementado.
Los tipos numéricos y de enumeración poseen operadores de incremento y
decremento predefinidos.
 Los operadores de esplazamiento son operadores binarios. Producen un
desplazamiento a nivel de bits de la representación interna del primer operando
(de un tipo entero), a la izquierda (<<) o a la derecha (>>) el número de bits
especificado por su segundo operando.
 Los operadores relacionales proporcionan un resultado lógico, dependiendo de si
sus argumentos son iguales (==), diferentes (!=), o del orden relativo entre ellos (<,
>, <= y >=).
 La asignación simple (=) almacena el valor del operando situado a su derecha en
una variable (posición de memoria) indicada por el operando situado a su
izquierda.
Los operandos deben ser del mismo tipo o el operando de la derecha se debe
poder convertir implícitamente al tipo del operando de la izquierda).
El operador de asignación = produce los siguientes resultados:
o En tipos simples el funcionamiento es similar al de C++, copia el contenido
de la expresión de la derecha en el objeto que recibe el valor.
o En datos struct realiza una copia directa del contenido, como en C++.
o En clases se copia la referencia, esto es, la dirección del objeto,
provocando que el objeto sea referenciado por más de una referencia.
Este comportamiento es distinto al que se produce en C++, en el que se
copia el contenido del objeto. Si el objeto contiene estructuras más
complejas, C++ requiere normalmente la sobrecarga del operador para
personalizar la manera en que se realiza la copia para que cada instancia
de la clase (fuente y destino)maqneje su propia zona de memoria. En C#
no se permite la sobrecarga del operador =
Los otros operadores de esta categoría realizan, además de la asignación otra
operación previa a la asignación. Por ejemplo,

a += 22;
equivale a

a = a + 22;
o sea, primero se calcula la expresión a+22 y posteriormente,ese valor se
almacena en a.
De la misma manera actúan los demás operadores de asignación: -=, *=, /=, %=,
&=, |=, ^=, <<= y >>=.
Para asignar instancias de clases (en el sentido clásico de C++) se debe redefinir
el método MemberwiseCopy() que es heredado por todas las clases desde
System.Object (todas las clases heredan, en última instancia de System.Object).
 El operador de acceso a miembros es el operador punto (.) y se emplea para
acceder a los miembros (componentes) de una clase, estructura o espacio de
nombres.
 El operador de acceso por índices es el tradicional, formado por la pareja de
caracteres [ y ]. En C# también se emplea para especificar atributos.
 El operador de conversión explícita de tipos (casting) es el clásico, formado por
la pareja de caracteres ( y ).
 El operador ternario condicional evalúa una condición lógica y devuelve uno de
dos valores.
Se utiliza en expresiones de la forma:

cond ? expr1 : expr2


de manera que si cond es verdad, se evalúa expr1 y se devuelve como resultado;
si cond es falsa se evalúa expr1 y se devuelve como resultado.
 Creación de objetos: new ObjetoDemo o1 = new ObjetoDemo()
 Información de tipos: as is sizeof typeof
El operador as se utiliza para realizar conversiones entre tipos compatibles. El
operador as se utiliza en expresiones de la forma:
<expresion> as type
donde <expresion> es una expresión de un tipo de referencia, y type es un tipo de
referencia.

// Ejempo de uso del operador as

using System;

class Clase1 {}

class Clase2 {}

public class Demo


{
public static void Main()
{
object [] Objetos = new object[6];
Objetos[0] = new Clase1();
Objetos[1] = new Clase1();
Objetos[2] = "Hola";
Objetos[3] = 123;
Objetos[4] = 123.4;
Objetos[5] = null;

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


{
string s = Objetos[i] as string;
Console.Write ("{0}:", i);
if (s != null)
Console.WriteLine ( "'" + s + "'" );
else
Console.WriteLine ( "no es string" );
}
Console.ReadLine();
}
}
El resultado es:
0:no es string
1:no es string
2:'Hola'
3:no es string
4:no es string
5:no es string
El operador is se utiliza para comprobar si el tipo en tiempo de ejecución de un
objeto es compatible con un tipo dado. El operador is se utiliza en expresiones de
la forma:
<expresion> is type
donde <expresion> es una expresión de un tipo de referencia, y type es un tipo de
referencia.

// Ejempo de uso del operador is

using System;

class Clase1 {}

class Clase2 {}

public class Demo


{
public static void Prueba (object o)
{
Clase1 a;
Clase2 b;

if (o is Clase1)
{
Console.WriteLine ("o es de Clase1");
a = (Clase1)o;
// trabajar con a
}

else if (o is Clase2)
{
Console.WriteLine ("o es de Clase2");
b = (Clase2)o;
// trabajar con b
}

else
{
Console.WriteLine ("o no es ni de Clase1 ni de Clase2.");
}
}
public static void Main()
{
Clase1 c1 = new Clase1();
Clase2 c2 = new Clase2();
Prueba (c1);
Prueba (c2);
Prueba ("Un string");

Console.ReadLine();
}
}
El resultado es:
o es de Clase1
o es de Clase2
o no es ni de Clase1 ni de Clase2.
El operador typeof se utiliza para obtener el objeto System.Type para un tipo. Una
expresión typeof se presenta de la siguiente forma:
typeof (tipo)
donde tipo es el tipo cuyo objeto System.Type se desea obtener.

// Ejempo de uso del operador typeof

using System;

class Clase1 {}

class Clase2 {}

public class Demo


{
public static void Prueba (object o)
{
Clase1 a;
Clase2 b;

Type t;

if (o is Clase1)
{
a = (Clase1)o;
Console.WriteLine ("--> {0}", typeof(Clase1).FullName);
t = typeof(Clase1);
}
else
{
b = o as Clase2;
t = typeof(Clase2);
}
Console.WriteLine (t.FullName);
}
public static void Main()
{
Clase1 c1 = new Clase1();
Clase2 c2 = new Clase2();
Prueba (c1);
Prueba (c2);

Console.ReadLine();
}
}
El resultado es:
--> Clase1
Clase1
Clase2
 Control de excepciones de desbordamiento: checked y unchecked
C# proporciona la posibilidad de realizar operaciones de moldeado (casting) y
aritméticas en un contexto verificado. En otras palabras, el entorno de ejecución
.NET detecta cualquier situación de desbordamiento y lanza una excepción
(OverFlowException) si ésta se manifiesta.
En un contexto verificado (checked), si una expresión produce un valor fuera del
rango del tipo de destino, el resultado depende de si la expresión es constante o
no. Las expresiones constantes producen errores de compilación, mientras que las
expresiones no constantes se evalúan en tiempo de ejecución y producen
excepciones.
Si no se especifican ni checked ni unchecked, las expresiones constantes utilizan
la verificación de desbordamiento predeterminada en tiempo de compilación, que
es checked. De lo contrario, si la expresión no es constante, la verificación de
desbordamiento en tiempo de ejecución depende de otros factores tales como las
opciones del compilador y la configuración del entorno. En el siguiente ejemplo, el
valor por defecto es unchecked:

using System;

class PruebaDesbordamiento
{
static short x = 32767; // Maximo valor short
static short y = 32767;

public static int UnCh()


{
int z = unchecked((short)(x + y));
return z; // -2
}
public static int UnCh2()
{
int z = (short)(x + y); // Por defecto es unchecked
return z; // -2
}
public static void Main()
{
Console.WriteLine("Valor -unchecked-: {0}", UnCh());
Console.WriteLine("Valor por defecto: {0}", UnCh2());
Console.ReadLine();
}
}
El resultado es:
Valor -unchecked-: -2
Valor por defecto: -2
Si añadimos la función:

public static int Ch()


{
int z = checked((short)(x + y));
return z;
}
y la llamada:
Console.WriteLine("Valor -checked- : {0}", Ch());
la ejecución del programa provoca el lanzamiento de una excepción
System.OverflowException que detiene la ejecución del programa, al no estar
controlada.
También podrían presentarse por precedencia. En la tabla siguiente los enumeramos de mayor a
menor precedencia:
Categorías Operadores
Primarios Paréntesis: (x)
Acceso a miembros: x.y

Llamada a métodos: f(x)

Acceso con índices: a[x]

Post-incremento: x++

Post-decremento: x--

Llamada a un constructor: new

Consulta de tipo: typeof

Control de desbordamiento activo: checked

Control de desbordamiento inactivo: unchecked

Valor positivo: +

Valor negative: -

No: !

Complemento a nivel de bit: ~


Unarios
Pre-incremento: ++x

Post-decremento: --x

Conversión de tipo -cast-: (T)x

Multiplicación: *

División: /
Multiplicativos
Resto: %

Suma: +
Aditivos
Resta: -

Desplazamiento de bits a la izquierda: <<


Desplazamiento
Desplazamiento de bits a la derecha: >>

Relacionales Menor: <

Mayor: >

Menor o igual: <=

Mayor o igual: >=

Igualdad o compatibilidad de tipo: is


Conversión de tipo: as

Igualdad ==
Desigualdad !=
Bitwise AND &
Bitwise XOR ^
Bitwise OR |
Logical AND &&
Logical OR ||
Condicional ternario ?:
Asignación =, *=, /=, %=, +=, -=, <<=, >>=, &=, ^=, |=
Asociatividad
Como siempre, es mejor utilizar paréntesis para controlar el orden de evaluación

x = y = z se evalúa como x = (y = z)

x + y + z se evalúa como (x + y) + z
Estructuras de control
Las estructuras de control de C# son similares a las de C y C++. La diferencia más notable radica
en que la instrucción condicional if y los ciclos while y do están controlados por una expresión
lógica (tipo Boolean).
Esta restricción hace que las instrucciones sean más seguras al evitar posibles fuentes de error, o
al menos, facilitan la legibilidad del código. Por ejemplo, en la siguiente instrucción (válida en C++):

if (a)
la expresión a podría ser una expresión boolean pero también de tipo int, char, float *... y la
condición se evalúa como true cuando a es distinto de cero (valor entero 0, carácter 0 ó puntero
nulo, en los ejemplos). En C# se clarifica esta ambigüedad y sólo se admiten expresiones lógicas.
De esta manera, la instrucción anterior será válida sólo cuando a sea una expresión boolean.
A modo de resumen, las características propias de las estructuras de control de C# son:
 goto no puede saltar dentro de un bloque (da igual, de todas formas no lo
usaremos NUNCA).
 switch funciona como en Pascal (no como en C).
 Se añade una nueva estructura de control iterativa: foreach.
Estructuras condicionales
if, if-else
La estructura condicional tiene la sintaxis clásica, con la diferencia indicada anteriormente acerca
del tipo de la expresión. Si debe ejecutar más de una instrucción, se encierran en un bloque,
delimitado por las llaves { y }.
Si sólo se actúa cuando la condición es cierta:

if (a > 0) if (a > 0) {
Console.WriteLine ("Positivo"); Console.WriteLine ("Positivo");
ContPositivos++;
}
Si se actúa cuando la condición es falsa se emplea la palabra reservada else:

if (a > 0) if (a > 0) {
Console.WriteLine ("Positivo"); Console.WriteLine ("Positivo");
else ContPositivos++;
Console.WriteLine ("No Positivo"); }
else {
Console.WriteLine ("No Positivo");
ContNoPositivos++;
}
Puede escribirse una instrucción condicional dentro de otra instrucción condicional, lógicamente:

if (a > 0) {
Console.WriteLine ("Positivo");
ContPositivos++;
}
else {
if (a < 0) {
Console.WriteLine ("Negativo");
ContNegativos++;
}
else {
Console.WriteLine ("Cero");
ContCeros++;
}
}
La concordancia entre if y else se establece de manera sencilla: cada else se asocia al último if que
no tenga asociado un bloque else.
switch
La estructura de selección múltiple switch funciona sobre cualquier tipo predefinido (incluyendo
string) o enumerado (enum) y debe indicar explícitamente cómo terminar cada caso (generalmente,
con break en situaciones "normales" ó throw en situaciones "anormales", aunque es posible -pero
no recomendable- emplear goto case ó return ):

using System;

class HolaMundoSwitch
{
public static void Main(String[] args)
{
if (args.Length > 0)
switch(args[0])
{
case "José":
Console.WriteLine("Hola José. Buenos días");
break;
case "Paco":
Console.WriteLine("Hola Paco. Me alegro de verte");
break;
default: Console.WriteLine("Hola {0}", args[0]);
break;
}
else
Console.WriteLine("Hola Mundo");
}
}
Especificar los parámetros al programa en Visual Studio: Utilizar el explorador de soluciones para
configurar las propiedades del proyecto.
Un ejemplo que usa un datos string para controlar la selección:

using System;

namespace ConsoleApplication14
{
class Class1
{
static int Test(string label)
{
int res;

switch(label)
{
case null:
goto case "A"; // idem case "B" o case "A"
case "B":
case "C":
res = 1;
break;
case "A":
res = 2;
break;
default:
res = 0;
break;
}
return res;
}

static void Main(string[] args)


{
Console.WriteLine (Test("")); // 0
Console.WriteLine (Test("A")); // 2
Console.WriteLine (Test("B")); // 1
Console.WriteLine (Test("C")); // 1
Console.WriteLine (Test("?")); // 0
Console.ReadLine();
}
}
}
Estructuras repetitivas
Las estructuras repetitivas de C# (while, do...while, for) no presentan grandes diferencias respecto
a las de otros lenguajes, como C++. La aportación fundamental es la de la estructura iterativa en
colecciones foreach.
while

int i = 0;

while (i < 5) {
...
i++;
}
do...while

int i = 0;

do {
...
i++;
} while (i < 5);
for

int i;

for (i=0; i < 5; i++) {


...
}
foreach
Un ciclo foreach itera seleccionando todos los miembros de un vector, matriz u otra colección sin
que se requiera explicitar los índices que permiten acceder a los miembros.
El siguiente ejemplo muestra todos los argumentos recibidos por el programa cuando se invoca su
ejecución (argumentos en la línea de órdenes):

public static void Main(string[] args)


{
foreach (string s in args)
Console.WriteLine(s);
}
El vector (la colección) que se utiliza para iterar es args. Es un vector de datos string. El ciclo
foreach realiza tantas iteraciones como cadenas contenga el vector args, y en cada iteración toma
una y la procesa a través de la variable de control s.
El ejemplo siguiente realiza la misma función:

public static void Main(string[] args)


{
for (int i=0; i < args.Lenght; i++)
Console.WriteLine(args[i]);
}
El ciclo foreach proporciona acceso de sólo lectura a los elementos de la colección sobre la que
se itera. Por ejemplo, el código de la izquierda no compilará, aunque el de la derecha sí lo hará (v
es un vector de int):

foreach (int i in v) for (int i=0; i < v.Length; i++)


i *= 2; v[i] *= 2;
El ciclo foreach puede emplearse en vectores y colecciones. Una colección es una clase que
implementa el interfaz IEnumerable. Sobre las colecciones dicutiremos más adelante.
Saltos
goto
Aunque el lenguaje lo permita, nunca escribiremos algo como lo que aparece a continuación:

using System;

namespace DemoGoto
{
class MainClass
{
static void Busca(int val, int[,] vector, out int fil, out int col)
{
int i, j;
for (i = 0; i < vector.GetLength(0); i++)
for (j = 0; j < vector.GetLength(1); j++)
if (vector[i, j] == val) goto OK;
throw new InvalidOperationException("Valor no encontrado");
OK:
fil = i; col = j;
}

static void Main(string[] args)


{
int [,] coleccion = new int [2,3] {{1,0,4},{3,2,5}};
int f,c;

int valor = Convert.ToInt32(args[0]);

Busca (valor, coleccion, out f, out c);


Console.WriteLine ("El valor {0} esta en [{1},{2}]", valor,f,c);
Console.ReadLine();
}
}
}

break
Lo usaremos únicamente en sentencias switch.
continue
Mejor no lo usamos.
return
Procuraremos emplearlo sólo al final de un método para facilitar la legibilidad del código.
E/S básica
Las operaciones de entrada y salida tienen como objetivo permitir que el usuario pueda introducir
información al programa (operaciones de entrada) y que pueda obtener información de éste
(operaciones de salida). En definitiva, tratan de la comunicación entre el usuario y el programa.
La manera más simple de comunicación es mediante la consola. La consola ha sido el modo
tradicional de comunicación entre los programas y el usuario por su simplicidad. Las aplicaciones
basadas en ventanas resultan mucho más atractivas y cómodas para el usuario y es éste, sin
duda, el tipo de comunicación que deberemos emplear para productos finales. Los programas que
no requieran una mucha interacción con el usuario, no obstante, se construyen y se ponen en
explotación mucho más rápidamente si se utiliza la consola como medio de comunicación.
Aplicaciones en modo consola
Estas aplicaciones emplean la consola para representar las secuencias de entrada, salida (y error)
estándar.
Una aplicación de consola se crea en Visual Studio .NET seleccionando Archivo, Nuevo y
Proyecto. Cuando aparece la ventana Nuevo proyecto se selecciona Proyectos de Visual C# y
Aplicación de consola:

El acceso a la consola lo facilita la clase Console, declarada en el espacio de nombres System.


Esa clase proporciona la compatibilidad básica para aplicaciones que leen y escriben caracteres en
la consola. No es necesario realizar ninguna acción para poder obtener datos de la consola a partir
de la entrada estándar (teclado) o presentarlos en la salida estándar (consola) ya que estos flujos
(junto con el del error estándar) se asocian a la consola de manera automática, como ocurre en C+
+, por ejemplo, con cin, cout y cerr.
Los métodos básicos de la clase Console son WriteLine y ReadLine, junto con sus variantes Write
y Read:
 WriteLine escribe una línea en la salida estándar, entendiendo que escribe el
terminador de línea actual (por defecto la cadena "\r\n").
La versión más simple de este método recibe un único argumento (una cadena)
cuyo valor es el que se muestra:
Console.WriteLine ("!Hola, " + "mundo!");
// Escribe: !Hola, mundo!
Otra versión permite mostrar variables de diferentes tipos (sin necesidad de
convertirlas a string. La llamada tiene un número indeterminado de argumentos: el
primero es una cadena de formato en la que las variables a mostrar se indican con
{0}, {1}, etc. y a continuación se enumeran las variables a mostrar, entendiendo
que la primera se "casa" con {0}, la segunda con {1}, etc. Esta manera de mostrar
los datos recuerda a la instrucción printf de C, que cayó en desuso con C++ ...

int TuEdad = 25;


string TuNombre = "Pepe";
Console.WriteLine ("Tienes {0} años, {1}.", TuEdad, TuNombre);
// Escribe: Tienes 25 años, Pepe.
 El método Write hace lo mismo que WriteLine aunque no escribe el terminador de
línea.
 ReadLine lee la siguiente línea de caracteres de la secuencia de entrada estándar
(el teclado, por defecto), eliminando del buffer de entrada el terminador de línea.
Devuelve la cadena leida, que no contiene el carácter o los caracteres de
terminación.
 Read lee el siguiente carácter de la secuencia de entrada estándar y devuelve un
valor de tipo int. La lectura se realiza del buffer de entrada y no se termina (no
devuelve ningún valor) hasta que se encuentra al caracter de terminación (cuando
el usuario presionó la tecla ENTER). Si existen datos disponibles en el buffer, la
secuencia de entrada contiene los datos introducidos por el usuario, seguidos del
carácter de terminación.
Veamos un sencillo ejemplo sobre el uso de estos métodos.
E/S simple

using System;

class PideNombre
{
static void Main(string[] args)
{
Console.Write ("Introduzca su nombre: "); // 1
string nombre = Console.ReadLine(); // 2

Console.WriteLine ("Su nombre es: " + nombre); // 3


Console.ReadLine(); // 4
}
}
La instrucción 1 muestra la cadena Introduzca su nombre: pero no avanza a la siguiente línea de la
consola, por lo que cuando se ejecuta la instrucción 2 lo que escribe el usuario se muestra a
continuación, en la misma línea. La cadena que escribe el usuario se guarda en la variable nombre
y se elimina del buffer de entrada el terminador de línea. Cuando se valida la entrada (al pulsar
ENTER) se avanza a la siguiente línea. La instrucción 3 muestra una cadena, resultado de
concatenar un literal y la cadena introducida por el usuario. Finalmente, la instrucción 4 es
necesaria para detener la ejecución del programa (realmente, la finalización del mismo) hasta que
el usuario pulse ENTER. Observar que aunque el método Readline devuelva una cadena, éste
valor devuelto no es usado. En la siguiente figura mostramos dos ejemplos de ejecución.

Aplicaciones Windows
Una aplicación basada en ventanas (aplicación Windows, en lo que sigue) utilizan ventanas y
componentes específicos para interactuar con el usuario. Las peticiones de datos se realizan con
componentes de entrada de texto (por ejemplo, con un TextBox) o mediante la selección en una
lista de posibilidades (por ejemplo, con un ComboBox). Las salidas pueden realizarse de múltiples
maneras, empleando componentes Label, ventanas de mensajes MessageBox, etc.
Por ejemplo, en la figura siguiente mostramos una aplicación que responde mostrando una ventana
de mensaje (MessageBox) cuando se pincha sobre el botón titulado Saludo. Basta con ejecutar
este código cada vez que se pinche en dicho botón:
MessageBox.Show ("¡Hola, mundo!", "Un saludo típico");
(en realidad, System.Windows.Forms.MessageBox.Show (...);)

Una aplicación más compleja podría pedir el nombre del usuario en un componente TextBox y
mostrarlo empleando un componente Label cuando se pincha en el botón titulado Saludo:

Una aplicación de ventanas se crea fácilmente en Visual Studio .NET seleccionando Archivo,
Nuevo y Proyecto. En la ventana Nuevo proyecto se selecciona ahora Proyectos de Visual C# y
Aplicación para Windows.

Tipos de datos

Tipos básicos
El sistema unificado de tipos. El tipo Object
Cadenas de caracteres
Vectores y matrices
Estructuras
Enumeraciones
Cuando definimos un objeto debemos especificar su tipo. El tipo determina qué valores puede
almacenar ese objeto (clase y rango) y las operaciones que pueden efectuarse con él.
Como cualquier lenguaje de programación, C# proporciona una serie de tipos predefinidos (int,
byte, char, string, ...) y mecanismos para que el usuario cree sus propios tipos (class y struct).
La estructura de tipos de C# es una gran novedad ya que establece una relación jerárquica entre
éstos, de manera que todos los tipos son clases y se construyen por herencia de la clase base
Objet. Esta particularidad hace que la creación y gestión de tipos de datos en C# sea una de las
principales novedades y potencialidades del lenguaje.
Tipos básicos
Los tipos de datos básicos son los tipos de datos más comúnmente utilizados en programación.
Los tipos predefinidos en C# están definidos en el espacio de nombres System, que es el espacio
de nombres más numeroso (e importante) de la plataforma .NET. Por ejemplo, para representar
números enteros de 32 bits con signo se utiliza el tipo de dato System.Int32 y a la hora de crear un
objeto a de este tipo que represente el valor 2 se usa la siguiente sintaxis:
System.Int32 a = 2;
Al ser un tipo valor no se utiliza el operador new para crear objetos System.Int32, sino que
directamente se indica el literal que representa el valor a crear, con lo que la sintaxis necesaria
para crear entero de este tipo se reduce considerablemente. Es más, dado lo frecuente que es el
uso de este tipo también se ha predefinido en C# el alias int para el mismo, por lo que la definición
de variable anterior queda así de compacta:

int a = 2;
System.Int32 no es el único tipo de dato básico incluido en C#. En el espacio de nombres System
se han incluido los siguientes tipos:
C# Tipo en System Características Símbolo
sbyte System.Sbyte entero, 1 byte con signo
byte System.Byte entero, 1 byte sin signo
short System.Short entero, 2 bytes con signo
ushort System.UShort entero, 2 bytes sin signo
int System.Int32 entero, 4 bytes con signo
uint System.UInt32 entero, 4 bytes sin signo U
long System.Int64 entero, 8 bytes con signo L
ulong System.ULong64 entero, 8 bytes sin signo UL
float System.Single real, IEEE 754, 32 bits F
double System.Double real, IEEE 754, 64 bits D
decimal System.Decimal real, 128 bits (28 dígitos significativos) M
bool System.Boolean (Verdad/Falso) 1 byte
char System.Char Carácter Unicode, 2 bytes ´´
string System.String Cadenas de caracteres Unicode ""
Cualquier objeto (ningún tipo
object System.Object
concreto)
Los tipos están definidos de manera muy precisa y no son dependientes del compilador o de la
plataforma en la que se usan.
La tabla anterior incorpora la columna Símbolo para indicar cómo debe interpretarse un literal. Por
ejemplo, 28UL debe interpretarse como un entero largo sin signo (ulong).
Todos los tipos enumerados son tipos valor, excepto string (que no debe confundirse con un vector
de caracteres) y Object que son tipos referencia. Recuerde, no obstante, que los datos string se
comportaban como un tipo valor ante la asgnación.
El sistema unificado de tipos. El tipo Object
En C# desaparecen las variables y funciones globales: todo el código y todos los datos de una
aplicación forman parte de objetos que encapsulan datos y código (como ejemplo, recuerde
cómo Main() es un método de una clase). Como en otros lenguajes orientados a objetos, en C# un
tipo puede incluir datos y métodos. De hecho, hasta los tipos básicos predefinidos incluyen
métodos, que, como veremos, heredan de la clase base object, a partir de la cual se construyen
implícita o explícitamente todos los tipos de datos. Por ejemplo:
int i = 10;
string c = i.ToString();
e incluso:
string c = 10.ToString();
Esta manera por la que podemos manipular los datos básicos refleja la íntima relación entre C# y la
biblioteca de clase de .NET. De hecho, C# compila sus tipos básicos asociándolos a sus
correspondientes en .NET; por ejemplo, hace corresponder al tipo string) con la clase
System.String, al tipo int) con la clase System.Int32, etc. Así, se confirma que todo es un objeto en
C# (por si aún había alguna duda).
Aunque no comentaremos todos los métodos disponibles para los tipos básicos, destacaremos
algunos de ellos.
 Todos los tipos tienen un método ToString() que devuelve una cadena (string) que
representa su valor textual.
 char tiene propiedades acerca de su contenido (IsLetter, IsNumber, etc.) además
de métodos para realizar conversiones (ToUpper(), ToLower(), etc.)
 El tipo básico string dispone, como puede suponerse, de muchos métodos
específicos, aún más que la clase string de la biblioteca estándar de C++.
Algunos métodos estáticos y propiedades particularmente interesantes son:
 Los tipos numéricos enteros tipos tienen las propiedades MinValue y MaxValue
(p.e. int.MaxValue).
 Los tipos float y double tienen la propiedad Epsilon, que indica el mínimo valor
positivo que puede representarse en un dato de su tipo (p.e. float.Epsilon).
 Los tipos float y double tienen definidos los valores NaN (no es un número, no está
definido), PositiveInfinite y NegativeInfinity, valores que son pueden ser devueltos
al realizar ciertos cálculos.
 Muchos tipos, incluyendo todos los tipos numéricos, proporcionan el método
Parse() que permite la conversión desde un dato string (p.e. double d =
double.Parse("20.5"))
Conversiones de tipos
Una conversión de tipo (casting) puede ser implícita o explícita.
Implícitas Explícitas
Ocurren automáticamente Requieren un casting
Siempre tienen éxito Pueden fallar
No se pierde información Se puede perder información

int x = 123456;
long y = x; // implicita
short z = (short) x; // explicita (riesgo)

float f1 = 40.0F;
long l1 = (long) f1; // explicita (riesgo por redondeo)
short s1 = (short) l1; // explicita (riesgo por desbordamiento)
int i1 = s1; // implicita, no hay riesgo
uint i2 = (uint) i1; // explicita (riesgo de error por signo)
En C# las conversiones de tipo (tanto implícitas como explícitas) en las que intervengan tipos
definidos por el usuario pueden definirse y particularizarse.
Recordemos que pueden emplearse los operadores checked y unchecked para realizar
conversiones (y operaciones aritméticas) en un contexto verificado: el entorno de ejecución .NET
detecta cualquier situación de desbordamiento y lanza una excepción OverFlowException si ésta
se manifiesta.
El tipo object
Por el sistema unificado de tipos de C#, todo es un objeto. C# tiene predefinido un tipo referencia
llamado object y cualquier tipo (valor o referencia, predefinido o definido por el usuario) es en
última instancia, de tipo object (con otras palabras puede decirse que hereda todas las
características de ese tipo).
El tipo object se basa en System.Object de .NET Framework. Las variables de tipo object pueden
recibir valores de cualquier tipo. Todos los tipos de datos, predefinidos y definidos por el usuario,
heredan de la clase System.Object. La clase Object es la superclase fundamental de todas las
clases de .NET Framework; la raíz de la jerarquía de tipos.
Normalmente, los lenguajes no precisan una clase para declarar la herencia de Object porque está
implícita.
Por ejemplo, dada la siguiente declaración:
object o;
todas estas instrucciones son válidas:
o = 10; // int
o = "Una cadena para el objeto"; // cadena
o = 3.1415926; // double
o = new int [24]; // vector de int
o = false; // boolean
Dado que todas las clases de .NET Framework se derivan de Object, todos los métodos definidos
en la clase Object están disponibles en todos los objetos del sistema. Las clases derivadas pueden
reemplazar, y de hecho reemplazan, algunos de estos métodos, entre los que se incluyen los
siguientes:
 public void Object() Inicializa una nueva instancia de la clase Object. Los
constructores llaman a este constructor en clases derivadas, pero éste también
puede utilizarse para crear una instancia de la clase Object directamente.
 public bool Equals(object obj) Determina si el objeto especificado es igual al objeto
actual.
 protected void Finalize() Realiza operaciones de limpieza antes de que un objeto
sea reclamado automáticamente por el recolector de elementos no utilizados.
 public int GetHashCode() Sirve como función hash para un tipo concreto,
apropiado para su utilización en algoritmos de hash y estructuras de datos como
las tablas hash.
 public string ToString() Devuelve un objeto string que representa al objeto actual.
Se emplea para crear una cadena de texto legible para el usuario que describe una
instancia de la clase. La implementación predeterminada devuelve el nombre
completo del tipo del objeto Object.
En el último ejemplo, si cada vez que asignamos un valor a o ejecutamos
Console.WriteLine (o.ToString()) o simplemente Console.WriteLine (o);
obtendremos este resultado:
10
Una cadena para el objeto
3,1415926
System.Int32[]
False
 public Type GetType() Obtiene el objeto Type que representa el tipo exacto en
tiempo de ejecución de la instancia actual.
En el último ejemplo, si cada vez que asignamos un valor a o ejecutamos
Console.WriteLine (o.getType()) obtendremos este resultado:
System.Int32
System.String
System.Double
System.Int32[]
System.Boolean
 protected object MemberwiseClone() Crea una copia superficial del objeto Object
actual. No se puede reemplazar este método; una clase derivada debe
implementar la interfaz ICloneable si una copia superficial no es apropiada.
MemberwiseClone() está protegido y, por tanto, sólo es accesible a través de esta
clase o de una clase derivada. Una copia superficial crea una nueva instancia del
mismo tipo que el objeto original y, después, copia los campos no estáticos del
objeto original. Si el campo es un tipo de valor, se realiza una copia bit a bit del
campo. Si el campo es un tipo de referencia, la referencia se copia, pero no se
copia el objeto al que se hace referencia; por lo tanto, la referencia del objeto
original y la referencia del punto del duplicado apuntan al mismo objeto. Por el
contrario, una copia profunda de un objeto duplica todo aquello a lo que hacen
referencia los campos del objeto directa o indirectamente.
Polimorfismo -boxing y unboxing-
Boxing (y su operación inversa, unboxing) permiten tratar a los tipos valor como objetos (tipo
referencia). Los tipos de valor, incluidos los struct y los predefinidos, como int, se pueden convertir
al tipo object (boxing) y desde el tipo object (unboxing).
La posibilidad de realizar boxing permite construir funciones polimórficas: pueden realizar una
operación sobre un objeto sin conocer su tipo concreto.

void Polim(object o)
{
Console.WriteLine(o.ToString());
}
...
Polim(42);
Polim("abcd");
Polim(12.345678901234M);
Polim(new Point(23,45));
Boxing
Boxing es una conversión implícita de un tipo valor al tipo object. Cuando se realiza boxing de un
valor, se asigna una instancia de objeto y se copia el valor en el nuevo objeto.
Por ejemplo, considere la siguiente declaración de una variable de tipo de valor:
int i = 123;
La siguiente instrucción aplica implícitamente la operación de boxing sobre la variable i:
object o = i;
El resultado de esta instrucción es crear un objeto o en la pila que hace referencia a un valor del
tipo int alojado en el heap. Este valor es una copia del valor del tipo de valor asignado a la variable
i. La diferencia entre las dos variables, i y o se muestra en la siguiente figura:

En definitiva, el efecto del boxing es el de cualquier otro tipo de casting pero:


 el contenido de la variable se copia al heap
 se crea una referencia a ésta copia
Aunque no es necesario, también es posible realizar el boxing explícitamente como en el siguiente
ejemplo:
int i = 123;
object o = (object) i;
El siguiente ejemplo convierte una variable entera i a un objeto o mediante boxing. A continuación,
el valor almacenado en la variable i se cambia de 123 a 456. El ejemplo muestra que el objeto
mantiene la copia original del contenido, 123.

// Boxing de una variable int


using System;
class TestBoxing
{
public static void Main()
{
int i = 123;
object o = i; // boxing implicito
i = 456; // Modifica el valor de i
Console.WriteLine("Valor (tipo valor) = {0}", i);
Console.WriteLine("Valor (tipo object)= {0}", o);
}
}
El resultado de su ejecución es:
Valor (tipo valor) = 456
Valor (tipo object)= 123
Unboxing
Una vez que se ha hecho boxing sobre un dato y disponemos de un object no puede hacerse
demasiado con él ya que no pueden emplearse métodos o propiedades del tipo original: el
compilador no puede conocer el tipo original sobre el que se hizo boxing.
string s1 = "Hola";
object o = s1; // boxing
...
if (o.Lenght > 0) // ERROR
Una vez que se ha hecho boxing puede deshacerse la conversión (unboxing) haciendo casting
explícito al tipo de dato inicial.
string s2 = (string) o; // unboxing
Afortunadamente es posible conocer el tipo, de manera que si la conversión no es posible se lanza
una excepción. Pueden utilizarse los operadores is y as para determinar la corrección de la
conversión:
string s2;
if (o is string)
s2 = (string) o; // unboxing
o alternativamente:
string s2 = o as string; // conversion
if (s2 != null) // OK, la conversion funciono
Conclusiones
Ventajas del sistema unificado de tipos: las colecciones funcionan sobre cualquier tipo.

Hashtable t = new Hashtable();

t.Add(0, "zero");
t.Add(1, "one");
t.Add(2, "two");

string s = string.Format("Your total was {0} on {1}", total, date);


Desventajas del sistema unificado de tipos: Eficiencia.
La necesidad de utilizar boxing disminuirá cuando el CLR permita genéricos (algo similar a los
templates en C++).
Cadenas de caracteres
Una cadena de caracteres no es más que una secuencia de caracteres Unicode. En C# se
representan mediante objetos del tipo string, que no es más que un alias del tipo System.String
incluido en la BCL.
El tipo string es un tipo referencia. Representa una serie de caracteres inmutable. Se dice que una
instancia de String es inmutable porque no se puede modificar su valor una vez creada. Los
métodos que aparentemente modifican una cadena devuelven en realidad una cadena nueva que
contiene la modificación.
Recuerde la particularidad de este tipo sobre el operador de asignación: su funcionamiento es
como si fuera un tipo valor. Este es, realmente, el funcionamiento obtenido con el método Copy().
Si no se desea obtener una copia (independiente) sino una copia en el sentido de un tipo valor
emplee el método Clone().
Puede accederse a cada uno de los caracteres de la cadena mediante índice, como ocurre
habitualmente en otros lenguajes, siendo la primera posición asociada al índice cero. Este acceso,
no obstante, sólo está permitido para lectura.
string s = "!!Hola, mundo!!";;
for (int i=0; i < s.Length; i++)
Console.Write (s[i]);
Este ejemplo muestra todos los caracteres que componen la cadena, uno a uno. El ciclo realiza
tantas iteraciones como número de caracteres forman la cadena (su longitud) que se consulta
usando la propiedad Length.
Por definición, un objeto String, incluida la cadena vacía (""), es mayor que una referencia nula y
dos referencias nulas son iguales entre sí. El carácter null se define como el hexadecimal 0x00.
Puede consultarse si una cadena es vacía empleando la propiedad estática (sólo lectura) Empty: el
valor de este campo es la cadena de longitud cero o cadena vacía, "". Una cadena vacía no es
igual que una cadena cuyo valor sea null.
Los procedimientos de comparación y de búsqueda distinguen mayúsculas de minúsculas de forma
predeterminada. Pueden emplearse los métodos Compare() y Equals() para realizar referencias
combinadas y comparaciones entre valores de instancias de Object y String. Los operadores
relacionales == y != se implementan con el método Equals().
El método Concat() concatena una o más instancias de String o las representaciones de tipo String
de los valores de una o más instancias de Object. El operador + está sobrecargado para realizar la
concatenación. Por ejemplo, el siguiente código:
string s1 = "Esto es una cadena... como en C++";
string s2 = "Esto es una cadena... ";
string s3 = "como en C++";
string s4 = s2 + s3;
string s5 = String.Concat(s2, s3);

Console.WriteLine ("s1 = {0}", s1);


Console.WriteLine ("s4 = {0}", s4);
Console.WriteLine ("s5 = {0}", s5);

if ((s1 == s4) && (s1.Equals(s5)))


Console.WriteLine ("s1 == s4 == s5");
produce este resultado:
s1 = Esto es una cadena... como en C++
s4 = Esto es una cadena... como en C++
s5 = Esto es una cadena... como en C++
s1 == s4 == s5
Un método particularmente útil es Split(), que devuelve un vector de cadenas resultante de "partir"
la cadena sobre la que se aplica en palabras:
string linea;
string [] palabras;
...
palabras = linea.Split (null); // null indica dividir por espacios
En definitiva, la clase String incluye numerosos métodos, que pueden emplease para:
 Realizar búsquedas en cadenas de caracteres: IndexOf(), LastIndexOf(),
StartsWith(), EndsWith()
 Eliminar e insertar espacios en blanco: Trim(), PadLeft(), PadRight()
 Manipular subcadenas: Insert(), Remove(), Replace(), Substring(), Join()
 Modificar cadenas de caracteres: ToLower(), ToUpper(), Format() (al estilo del
printf de C, pero seguro).
Vectores y matrices
Un vector (matriz) en C# es radicalmente diferente a un vector (matriz) en C++: más que una
colección de variables que comparten un nombre y accesibles por índice, en C# se trata de una
instancia de la clase System.Array, y en consecuencia se trata de una colección que se almacena
en el heap y que está bajo el control del gestor de memoria.
Todas las tablas que definamos, sea cual sea el tipo de elementos que contengan, son objetos que
derivan de System.Array. Ese espacio de nombres proporciona métodos para la creación,
manipulación, búsqueda y ordenación de matrices, por lo tanto, sirve como clase base para todas
las matrices de la CLR (Common Language Runtime).
En C# las tablas pueden ser multidimensionales, se accede a los elementos por índice, siendo el
índice inicial de cada dimensión 0.
Siempre se comprueba que se esté accediendo dentro de los límites. Si se intenta acceder a un
elemento de un vector (matriz) especificando un índice fuera del rango, se detecta en tiempo de
ejecución y se lanza una excepción IndexOutOfBoundsException.
La sintaxis es ligeramente distinta a la del C++ porque las tablas son objetos de tipo referencia:

double [] array; // Declara un a referencia


// (no se instancia ningún objeto)
array = new double[10]; // Instancia un objeto de la clase
// System.Array y le asigna 10 casillas.
que combinadas resulta en (lo habitual):

double [] array = new double[10];


 El tamaño del vector se determina cuando se instancia, no es parte de la
declaración.
 string [] texto; // OK
 string [10] texto; // Error
 La declaración emplea los paréntesis vacíos [ ] entre el tipo y el nombre para
determinar el número de dimensiones (rango).
 string [] Mat1D; // 1 dimension
 string [,] Mat2D; // 2 dimensiones
 string [,,] Mat3D; // 3 dimensiones
 string [,,,] Mat4D; // 4 dimensiones
 ......
 En C# el rango es parte del tipo (es obligatorio en la declaración). El número de
elementos no lo es (está asociado a la instancia concreta).
Otros ejemplos de declaración de vectores:

string[] a = new string[10]; // "a" es un vector de 10 cadenas


int[] primes = new int[9]; // "primes" es un vector de 9 enteros
Un vector puede inicializarse a la misma vez que se declara. Las tres definiciones siguientes son
equivalentes:

int[] prime1 = new int[10] {1,2,3,5,7,11,13,17,19,23};


int[] prime2 = new int[] {1,2,3,5,7,11,13,17,19,23};
int[] prime3 = {1,2,3,5,7,11,13,17,19,23};
Los vectores pueden dimensionarse dinámicamente (en tiempo de ejecución). Por ejemplo, el
siguiente código:

Console.Write ("Num. casillas: ");


string strTam = Console.ReadLine();
int tam = Convert.ToInt32(strTam);

int[] VecDin = new int[tam];


for (int i=0; i<tam; i++) VecDin[i]=i;

Console.Write ("Contenido de VecDin = ");


foreach (int i in VecDin)
Console.Write(i + ", ");
Console.ReadLine();

produce este resultado:


Num. casillas: 6
Contenido de VecDin = 0, 1, 2, 3, 4, 5,
Esta facilidad es una gran ventaja, aunque una vez que el constructor actúa y se crea una instancia
de la clase System.Array no es posible redimensionarlo. Si se desea una estructura de datos con
esta funcionalidad debe emplearse una estructura colección disponible en System.Collections (por
elemplo, System.Collections.ArrayList).
En el siguiente ejemplo observe cómo el vector palabras se declara como un vector de string. Se
instancia y se inicia después de la ejecución del método Split(), que devuelve un vector de string.

string frase = "Esto es una prueba de particion";


string [] palabras; // vector de cadenas (no tiene tamaño asignado)

palabras = frase.Split (null);

Console.WriteLine ("Frase = {0}", frase);


Console.WriteLine ("Hay = {0} palabras", palabras.Length);

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


Console.WriteLine (" Palabra {0} = {1}", i, palabras[i]);
El resultado es:

Frase = Esto es una prueba de particion


Hay = 6 palabras
Palabra 0 = Esto
Palabra 1 = es
Palabra 2 = una
Palabra 3 = prueba
Palabra 4 = de
Palabra 5 = particion
Matrices
Las diferencias son importantes respecto a C++ ya que C# permite tanto matrices rectangulares
(todas las filas tienen el mismo número de columnas) como matrices dentadas o a jirones.

int [,] array2D; // Declara un a referencia


// (no se instancia ningún objeto)
array2D = new int [2,3]; // Instancia un objeto de la clase
// System.Array y le asigna 6 casillas
// (2 filas con 3 columnas cada una)
que combinadas (y con inicialización) podría quedar:

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


El número de dimensiones puede ser, lógicamente, mayor que dos:

int [,,] array3D = new int [2,3,2];


El acceso se realiza con el operador habitual [ ], aunque el recorrido se simplica y clarifica en C#
con el ciclo foreach:

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


vector1[i] = vector2[i];
...
foreach (float valor in vector2)
Console.Wtite (valor);

Una matriz dentada no es más que una tabla cuyos elementos son a su vez tablas, pudiéndose
así anidar cualquier número de tablas. Cada tabla puede tener un número propio de casillas. En el
siguiente ejemplo se pide el número de filas, y para cada fila, el número de casillas de ésa.

Console.WriteLine ("Introduzca las dimensiones: ");

// Peticion de numero de filas (num. vectores)


Console.Write ("Num. Filas: ");
string strFils = Console.ReadLine();
int Fils = Convert.ToInt32(strFils);

// Declaracion de la tabla dentada: el numero de


// casillas de cada fila es deconocido.
int[][] TablaDentada = new int[Fils][];

// Peticion del numero de columnas de cada vector


for (int f=0; f<Fils; f++)
{
Console.Write ("Num. Cols. de fila {0}: ", f);
string strCols = Console.ReadLine();
int Cols = Convert.ToInt32(strCols);

// Peticion de memoria para cada fila


TablaDentada[f] = new int[Cols];
}

// Rellenar todas las casillas de la tabla dentada


for (int f=0; f<TablaDentada.Length; f++)
for (int c=0; c<TablaDentada[f].Length; c++)
TablaDentada[f][c]=((f+1)*10)+(c+1);

// Mostrar resultado
Console.WriteLine ("Contenido de la matriz: ");
for (int f=0; f<TablaDentada.Length; f++)
{
for (int c=0; c<TablaDentada[f].Length; c++)
Console.Write (TablaDentada[f][c] + " ");
Console.WriteLine();
}

Console.ReadLine();

El resultado es:
Introduzca las dimensiones:
Num. Filas: 4
Num. Cols. de fila 0: 2
Num. Cols. de fila 1: 5
Num. Cols. de fila 2: 3
Num. Cols. de fila 3: 7
Contenido de la matriz:
11 12
21 22 23 24 25
31 32 33
41 42 43 44 45 46 47
Estructuras
Una estructura (struct) se emplea para definir nuevos tipos de datos. Los nuevos tipos así
definidos son tipos valor (se almacenan en la pila).
La sintaxis para la definición de estructuras es similar a la empleada para las clases (se emplea la
palabra reservada struct en lugar de class). No obstante,
 La herencia y aspectos relacionados (p.e. métodos virtuales, métodos abstractos)
no se admiten en los struct.
 No se puede especificar (explícitamente) un constructor sin parámetros. Los datos
struct tienen un constructor sin parámetros predefinido y no puede reemplazarse,
sólo se permiten constructores con parámetros. En este sentido son diferentes a
los struct en C++.
 En el caso de especificar algún constructor con parámetros deberíamos
asegurarnos de iniciar todos los campos.
En el siguiente ejemplo se proporciona un único constructor con parámetros:

public struct Point


{
public int x, y;

public Point(int x, int y)


{
this.x = x;
this.y = y;
}
public string Valor()
{
return ("[" + this.x + "," + this.y + "]");
}
}
Sobre esta declaración de tipo Point, observe estas declaraciones de datos Point:

Point p = new Point(2,5);


Point p2 = new Point();
Ambas crean una instancia de la clase Point en la pila y asigna los valores oportunos a los campos
del struct con el constructor:
 En el primer caso actúa el constructor suministrado en la implementación del tipo.
 En el segundo actúa el constructor por defecto, que inicia los campos numéricos a
cero, y los de tipo referencia a null.
Puede comprobarse fácilmente:
Console.WriteLine ("p = " + p.Valor());
Console.WriteLine ("p2 = " + p2.Valor());
produce como resultado:
p = [2,5]
p2 = [0,0]
En cambio, la siguiente declaración:
Point p3;
crea una instancia de la clase Point en la pila sin iniciar (cuidado: no inicia p3 a null, no es un tipo
referencia). Por lo tanto, la instrucción:
Console.WriteLine ("p3 = " + p3.Valor());
produce un error de compilación, al intentar usar una variable no asignada.
Observe las siguientes operaciones con struct. Su comportamiento es previsible:
p.x += 100;
int px = p.x; // p.x==102
p3.x = px; // p3.x==102

p2 = p; // p2.x==102, p2.y==5
p2.x += 100; // p2.x==202, p2.y==5
p3.y = p.y + p2.y; // p3.y==10

Console.WriteLine ("p = " + p.Valor());


Console.WriteLine ("p2 = " + p2.Valor());
Console.WriteLine ("p3 = " + p3.Valor());
el resultado es:

p = [102,5]
p2 = [202,5]
p3 = [102,10]
Enumeraciones
Una enumeración o tipo enumerado es un tipo especial de estructura en la que los literales de
los valores que pueden tomar sus objetos se indican explícitamente al definirla.
La sintaxis es muy parecida a la empleada en C++:
enum State { Off, On };
aunque el punto y coma final es opcional ya que una enumeración es una definición de un struct y
ésta no requiere el punto y coma:
enum State { Off, On }
Al igual que en C++ y en C, C# numera a los elementos de la enumeración con valores enteros
sucesivos, asignando el valor 0 al primero de la enumeración, a menos que se especifique
explícitamente otra asignación:
enum Tamanio {
Pequeño = 1,
Mediano = 3,
Grande = 5
Inmenso
}
En el ejemplo, el valor asociado a Inmenso es 6.
Para acceder a los elementos de una enumeración debe cualificarse completamente:
Tamanio Ancho = Tamanio.Grande;
lo que refleja que cada enumeración es, en última instancia, un struct.
Si no se declara ningún tipo subyacente de forma explícita, se utiliza Int32. En el siguiente ejemplo
se especifica el tipo base byte:
enum Color: byte {
Red = 1,
Green = 2,
Blue = 4,
Black = 0,
White = Red | Green | Blue
}
La clase Enum (System.Enum) proporciona la clase base para las enumeraciones. Proporciona
métodos que permiten comparar instancias de esta clase, convertir el valor de una instancia en su
representación de cadena, convertir la representación de cadena de un número en una instancia
de esta clase y crear una instancia de una enumeración y valor especificados. Por ejemplo:
Color c1 = Color.Black;

Console.WriteLine((int) c1); // 0
Console.WriteLine(c1); // Black
Console.WriteLine(c1.ToString()); // Black
Pueden convertirse explícitamente en su valor entero (como en C). Admiten determinados
operadores aritméticos (+,-,++,--) y lógicos a nivel de bits (&,|,^,~). ejemplo:
Color c2 = Color.White;

Console.WriteLine((int) c2); // 7
Console.WriteLine(c2.ToString()); // White
Otro ejemplo más complejo:
enum ModArchivo
{
Lectura = 1,
Escritura = 2,
Oculto = 4,
Sistema = 8
}
...
ModArchivo st = ModArchivo.Lectura | ModArchivo.Escritura;
...
Console.WriteLine (st.ToString("F")); // Lectura, Escritura
Console.WriteLine (Enum.Format(typeof(ModArchivo), st, "G")); // 3
Console.WriteLine (Enum.Format(typeof(ModArchivo), st, "X")); // 00000003
Console.WriteLine (Enum.Format(typeof(ModArchivo), st, "D")); // 3

Clases (1)
Introducción
Modificadores de acceso
Variables de instancia y miembros estáticos
Campos, constantes, campos de sólo lectura y propiedades
Métodos
Constructores y "destructores"
Sobrecarga de operadores
Conversiones de tipo
Clases y structs

Introducción
Un objeto es un agregado de datos y de métodos que permiten manipular dichos datos, y un
programa en C# no es más que un conjunto de objetos que interaccionan unos con otros a
través de sus métodos.

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


objetos: cuáles son los datos y los métodos de los que van a disponer todos los objetos de
ese tipo. Se dice que los datos y los métodos son los miembros de la clase. En C# hay
muchos tipos de miembros, que podemos clasificar de manera muy genérica en las dos
categorías antes mencionadas: datos y métodos.

 Datos
o campos
o constantes y campos de sólo lectura
o propiedades
 Métodos
o métodos generales
o métodos constructores
o métodos de sobrecarga de operadores e indexadores

Un campo es un dato común a todos los objetos de una clase. La declaración de un dato se
realiza, sintácticamente, como cualquier variable.

Un método es una función, un conjunto de instrucciones al que se le asocia un nombre.

La palabra reservada this es una variable predefinida disponible dentro de las funciones no
estáticas de un tipo que se emplea, en un método de una clase, para acceder a los miembros
del objeto sobre el que se está ejecutando el método en cuestión. En definitiva, permite
acceder al objeto "activo". El siguiente ejemplo muestra de manera clara su aplicación:

Una clase muy sencilla (uso de this)

class Persona // Clase Persona


{
private string nombre; // campo privado

public Persona (string nombre) // Constructor


{
this.nombre = nombre; // acceso al campo privado
}

public void Presenta (Persona p) // Metodo


{
if (p != this) // Una persona no puede presentarse
a si misma
Console.WriteLine("Hola, " + p.nombre + ", soy
" + this.nombre);
}
}
...
Persona yo = new Persona ("YO");
Persona tu = new Persona ("TU");
yo.Presenta(tu);
tu.Presenta(yo);
yo.Presenta(yo); // Sin efecto
tu.Presenta(tu); // Sin efecto

La ejecución del código anterior produce el siguiente resultado:

Hola, TU, soy YO


Hola, YO, soy TU
En el siguiente ejemplo mostramos más miembros de una clase:

Una clase sencilla (CocheSimple)

public class CocheSimple


{
private int VelocMax; // Campo
private string Marca; // Campo
private string Modelo; // Campo

// Método constructor sin argumentos


public CocheSimple () {
this.VelocMax = 0;
this.Marca = "Sin marca";
this.Modelo = "Sin modelo";
}

// Método constructor con argumentos


public CocheSimple (string marca, string mod,
int velMax)
{
this.VelocMax = velMax;
this.Marca = marca;
this.Modelo = mod;
}

// Método
public void MuestraCoche ()
{
Console.WriteLine (this.Marca + " " +
this.Modelo +
" (" + this.VelocMax + " Km/h)");
}

} // class CocheSimple

recordemos que el operador new se emplea para crear objetos de una clase especificada.
Cuando se ejecuta se llama a un método especial llamado constructor y devuelve una
referencia al objeto creado. Si no se especifica ningún constructor C# considera que existe
un constructor por defecto sin parámetros. Una buena costumbre es proporcionar siempre
algún constructor.

Una aplicación que usa la clase CocheSimple

class CocheSimpleApp
{
static void Main(string[] args)
{
// "MiCoche" y "TuCoche" son variables de tipo
"CocheSimple"
// que se inicializan llamando al constructor.
CocheSimple MiCoche = new CocheSimple
("Citröen", "Xsara", 220);
CocheSimple TuCoche = new CocheSimple ("Opel",
"Corsa", 190);

Console.Write ("Mi coche: ");


MiCoche.MuestraCoche(); // LLamada al método
"MuestraCoche()"

Console.Write ("El tuyo: ");


TuCoche.MuestraCoche(); // LLamada al método
"MuestraCoche()"

Console.ReadLine ();

} // Main

} // class CocheSimpleApp

Modificadores de acceso
Los modificadores de acceso nos permiten especificar quién puede usar un tipo o un
miembro del tipo, de forma que nos permiten gestionar la encapsulación de los objetos en
nuestras aplicaciones:

 Los tipos de nivel superior (aquéllos que se encuentran directamente en un


namespace) pueden ser public o internal
 Los miembros de una clase pueden ser public, private, protected,
internal o protected internal
 Los miembros de un struct pueden ser public, private o internal

Modificador de Un miembro del tipo T definido en el


acceso assembly A es accesible...
public desde cualquier sitio
private (por
sólo desde dentro de T (por defecto)
defecto)
protected desde T y los tipos derivados de T
internal desde los tipos incluidos en A
protected desde T, los tipos derivados de T y los tipos
internal incluidos en A

Variables de instancia y miembros estáticos


Por defecto, los miembros de una clase son variables de instancia: existe una copia de los
datos por cada instancia de la clase y los métodos se aplican sobre los datos de una
instancia concreta.
Se pueden definir miembros estáticos que son comunes a todas las instancias de la clase.
Lógicamente, los métodos estáticos no pueden acceder a variables de instancia, ni a la
variable this que hace referencia al objeto actual.

using System;

class Mensaje {
public static string Bienvenida = "¡Hola!, ¿Cómo
está?";
public static string Despedida = "¡Adios!, ¡Vuelva
pronto!";
}

class MiembrosStaticApp
{
static void Main()
{
Console.WriteLine(Mensaje.Bienvenida);
Console.WriteLine(" Bla, bla, bla ... ");
Console.WriteLine(Mensaje.Despedida);
Console.ReadLine();
}
}

De cualquier forma, no conviene abusar de los miembros estáticos, ya que son básicamente
datos y funciones globales en entornos orientados a objetos.

Campos, constantes, campos de sólo lectura y


propiedades
Campos

Un campo es una variable que almacena datos, bien en una una clase, bien en una
estructura.

Constantes (const)

Una constante es un dato cuyo valor se evalúa en tiempo de compilación y, por tanto, es
implícitamente estático (p.ej. Math.PI).
public class MiClase
{
public const string version = "1.0.0";
public const string s1 = "abc" + "def";
public const int i3 = 1 + 2;
public const double PI_I3 = i3 * Math.PI;
public const double s = Math.Sin(Math.PI); //ERROR
...
}

Campos de sólo lectura (readonly)

Similares a las constantes, si bien su valor se inicializa en tiempo de ejecución (en su


declaración o en el constructor). A diferencia de las constantes, si cambiamos su valor no
hay que recompilar los clientes de la clase. Además, los campos de sólo lectura pueden ser
variables de instancia o variables estáticas.

public class MiClase


{
public static readonly double d1 =
Math.Sin(Math.PI);
public readonly string s1;

public MiClase(string s)
{
s1 = s;
}
}
......
MiClase mio = new MiClase ("Prueba");
Console.WriteLine(mio.s1);
Console.WriteLine(MiClase.d1);
......

Produce como resultado:

Prueba
1,22460635382238E-16

Propiedades

Las propiedades son campos virtuales, al estilo de Delphi o C++Builder. Su aspecto es el


de un campo (desde el exterior de la clase no se diferencian) pero están implementadas con
código, como los métodos. Pueden ser de sólo lectura, de sólo escritura o de lectura y
escritura.

Considere de nuevo la clase CocheSimple. Podemos añadir la propiedad MaxSpeed:


// Propiedad
public float MaxSpeed
{
get { return VelocMax / 1.6F; }
set { VelocMax = (int) ((float) value * 1.6F);}
}
de manera que si las siguientes líneas se añaden al final del método main en
CocheSimpleApp:
Console.WriteLine ();
Console.WriteLine ("My car's Max Speed: "
+ MiCoche.MaxSpeed +" Mph" ); // get

Console.WriteLine ("Tunning my car...");


MiCoche.MaxSpeed = 200; // set
Console.WriteLine ("After tunning my car (Incr. max Speed to 200
Mph");
Console.WriteLine ("My car's Max Speed: "
+ MiCoche.MaxSpeed + " Mph"); // get

Console.WriteLine ();
Console.Write ("Mi coche: ");
MiCoche.MuestraCoche(); // LLamada al método "MuestraCoche()"
el resultado obtenido es:
Mi coche: Citröen Xsara (220 Km/h)
El tuyo: Opel Corsa (190 Km/h)

My car's Max Speed: 137,5 Mph


Tunning my car...
After tunning my car (Incr. max Speed to 200 Mph
My car's Max Speed: 200 Mph

Mi coche: Citröen Xsara (320 Km/h)

Un ejemplo con campos, métodos y propiedades

using System;

class Coche
{
// Campos
protected double velocidad=0;
public string Marca;
public string Modelo;
public string Color;
public string NumBastidor;

// Método constructor
public Coche(string marca, string modelo,
string color, string numbastidor)
{
this.Marca=marca;
this.Modelo=modelo;
this.Color=color;
this.NumBastidor=numbastidor;
}

// Propiedad (solo lectura)


public double Velocidad
{
get { return this.velocidad; }
}

// Método
public void Acelerar(double c)
{
Console.WriteLine("--> Incrementando veloc. en {0}
km/h", c);
this.velocidad += c;
}

// Método
public void Girar(double c)
{
Console.WriteLine("--> Girando {0} grados", c);
}

// Método
public void Frenar(double c)
{
Console.WriteLine("--> Reduciendo veloc. en {0}
km/h", c);
this.velocidad -= c;
}

// Método
public void Aparcar()
{
Console.WriteLine("-->Aparcando coche");
this.velocidad = 0;
}

} // class Coche

class EjemploCocheApp
{

static void Main(string[] args)


{

Coche MiCoche = new Coche("Citröen", "Xsara


Picasso",
"Rojo","1546876");

Console.WriteLine("Los datos de mi coche son:");


Console.WriteLine(" Marca: {0}", MiCoche.Marca);
Console.WriteLine(" Modelo: {0}",
MiCoche.Modelo);
Console.WriteLine(" Color: {0}", MiCoche.Color);
Console.WriteLine(" Número de bastidor: {0}",
MiCoche.NumBastidor);
Console.WriteLine();

MiCoche.Acelerar(100);
Console.WriteLine("La velocidad actual es de {0}
km/h",
MiCoche.Velocidad);

MiCoche.Frenar(75);
Console.WriteLine("La velocidad actual es de {0}
km/h",
MiCoche.Velocidad);

MiCoche.Girar(45);

MiCoche.Aparcar();
Console.WriteLine("La velocidad actual es de {0}
km/h",
MiCoche.Velocidad);

Console.ReadLine();
}

} // class EjemploCocheApp

Métodos
Implementan las operaciones que se pueden realizar con los objetos de un tipo concreto.
Constructores, destructores y operadores son casos particulares de métodos. Las
propiedades y los indexadores se implementan con métodos (get y set).

Como en cualquier lenguaje de programación, los métodos pueden tener parámetros,


contener órdenes y devolver un valor (con return).
Por defecto, los parámetros se pasan por valor (por lo que los tipos "valor" no podrían
modificarse en la llamada a un método). El modificador ref permite que pasemos
parámetros por referencia. Para evitar problemas de mantenimiento, el modificador ref hay
que especificarlo tanto en la definición del método como en el código que realiza la
llamada. Además, la variable que se pase por referencia ha de estar inicializada
previamente.

void RefFunction (ref int p)


{
p++;
}
......

int x = 10;

RefFunction (ref x); // x vale ahora 11

El modificador out permite devolver valores a través de los argumentos de un método. De


esta forma, se permite que el método inicialice el valor de una variable. En cualquier caso,
la variable ha de tener un valor antes de terminar la ejecución del método. Igual que antes,
Para evitar problemas de mantenimiento, el modificador out hay que especificarlo tanto en
la definición del método como en el código que realiza la llamada.

void OutFunction(out int p)


{
p = 22;
}
......

int x; // x aún no está inicializada

OutFunction (out x);

Console.WriteLine(x); // x vale ahora 22

Sobrecarga de métodos: Como en otros lenguajes, el identificador de un método puede


estar sobrecargado siempre y cuando las signaturas de las distintas implementaciones del
método sean únicas (la signatura tiene en cuenta los argumentos, no el tipo de valor que
devuelven).

void Print(int i);


void Print(string s);
void Print(char c);
void Print(float f);

int Print(float f); // Error: Signatura duplicada


Vectores de parámetros: Como en C, un método puede tener un número variable de
argumentos. La palabra clave params permite especificar un parámetro de método que
acepta un número variable de argumentos. No se permiten parámetros adicionales después
de la palabra clave params, ni varias palabras clave params en una misma declaración de
método.

El siguiente código emplea una función que suma todos los parámetros que recibe. El
número de éstos es indeterminado, aunque debe asegurarse que sean de tipo int:

public static int Suma(params int[] intArr)


{
int sum = 0;
foreach (int i in intArr) sum += i;
return sum;
}
......
int sum1 = Sum(13,87,34); // sum1 vale 134
Console.WriteLine(sum1);
int sum2 = Sum(13,87,34,6); // sum2 vale 140
Console.WriteLine(sum2);
produce el siguiente resultado:
134
140

El siguiente código es algo más complejo.

public static void UseParams1(params int[] list)


{
for ( int i = 0 ; i < list.Length ; i++ )
Console.Write(list[i] + ", ");
Console.WriteLine();
}

public static void UseParams2(params object[] list)


{
for ( int i = 0 ; i < list.Length ; i++ )
Console.Write((object)list[i] + ", ");
Console.WriteLine();
}
......
UseParams1(1, 2, 3);
UseParams2(1, 'a', "test");

int[] myarray = new int[3] {10,11,12};


UseParams1(myarray);
Observe su ejecución:
1, 2, 3,
1, a, test,
10, 11, 12,

Constructores y "destructores"
Los constructores son métodos especiales que son invocados cuando se instancia una clase
(o un struct).

 Se emplean habitualmente para inicializar correctamente un objeto.


 Como cualquier otro método, pueden sobrecargarse.
 Si una clase no define ningún constructor se crea un constructor sin
parámetros (ímplícito).
 No se permite un constructor sin parámetros para los struct.

C# permite especificar código para inicializar una clase mediante un constructor estático.
El constructor estático se invoca una única vez, antes de llamar al constructor de una
instancia particular de la clase o a cualquier método estático de la clase. Sólo puede haber
un constructor estático por tipo y éste no puede tener parámetros.

Destructores: Se utilizan para liberar los recursos reservados por una instancia (justo antes
de que el recolector de basura libere la memoria que ocupe la instancia).

A diferencia de C++, la llamada al destructor no está garantizada por lo que tendremos


que utilizar una orden using e implementar el interfaz IDisposable para asegurarnos de
que se liberan los recursos asociados a un objeto). Sólo las clases pueden tener destructores
(no los struct).

class Foo
{
~Foo()
{
Console.WriteLine("Destruido {0}", this);
}
}

Sobrecarga de operadores
Como en C++, se pueden sobrecargar (siempre con un método static) algunos operadores
unarios (+, -, !, ~, ++, --, true, false) y binarios (+, -, *, /, %, &, |, ^, ==, !=, <, >, <=, >=,
<, >).

No se puede sobrecargar el acceso a miembros, la invocación de métodos, el operador de


asignación ni los operadores sizeof, new, is, as, typeof, checked, unchecked, &, || y
?:.

Los operadores & y || se evalúan directamente a partir de los operadores & y |.

La sobrecarga de un operador binario (v.g. *) sobrecarga implícitamente el operador de


asignación correspondiente (v.g. *=).
using System;

class OverLoadApp
{

public class Point


{
int x, y; // Campos

public Point() // Constructor sin parámetros


{
this.x = 0;
this.y = 0;
}
public Point(int x, int y) // Constructor común
{
this.x = x;
this.y = y;
}
public int X // Propiedad
{
get { return x; }
set { x = value; }
}
public int Y // Propiedad
{
get { return y; }
set { y = value; }
}

// Operadores de igualdad

public static bool operator == (Point p1, Point


p2)
{
return ((p1.x == p2.x) && (p1.y == p2.y));
}
public static bool operator != (Point p1, Point
p2)
{
return (!(p1==p2));
}

// Operadores aritméticos

public static Point operator + (Point p1, Point


p2)
{
return new Point(p1.x+p2.x, p1.y+p2.y);
}
public static Point operator - (Point p1, Point
p2)
{
return new Point(p1.x-p2.x, p1.y-p2.y);
}
}

static void Main(string[] args)


{
Point p1 = new Point(10,20);
Point p2 = new Point();
p2.X = p1.X;
p2.Y = p1.Y;

Point p3 = new Point(22,33);

Console.WriteLine ("p1 es: ({0},{1})", p1.X,


p1.Y);
Console.WriteLine ("p2 es: ({0},{1})", p2.X,
p2.Y);
Console.WriteLine ("p3 es: ({0},{1})", p3.X,
p3.Y);

if (p1 == p2) Console.WriteLine ("p1 y p2 son


iguales");
else Console.WriteLine ("p1 y p2 son diferentes");

if (p1 == p3) Console.WriteLine ("p1 y p3 son


iguales");
else Console.WriteLine ("p1 y p3 son diferentes");

Console.WriteLine ();

Point p4 = p1 + p3;
Console.WriteLine ("p4 (p1+p3) es: ({0},{1})",
p4.X, p4.Y);
Point p5 = p1 - p1;
Console.WriteLine ("p5 (p1-p1) es: ({0},{1})",
p5.X, p5.Y);

Console.WriteLine ();
Console.WriteLine ("p1 es: ({0},{1})", p1.X,
p1.Y);
Console.WriteLine ("p2 es: ({0},{1})", p2.X,
p2.Y);
Console.WriteLine ("p3 es: ({0},{1})", p3.X,
p3.Y);
Console.WriteLine ("p4 es: ({0},{1})", p4.X,
p4.Y);
Console.WriteLine ("p5 es: ({0},{1})", p5.X,
p5.Y);

Console.ReadLine ();
}
}
Para asegurar la compatibilidad con otros lenguajes de .NET:

// Operadores aritméticos

public static Point operator + (Point p1, Point p2)


{
return SumaPoints (p1, p2);
}
public static Point operator - (Point p1, Point p2)
{
return RestaPoints (p1, p2);
}
public static Point RestaPoints (Point p1, Point
p2) {
return new Point(p1.x-p2.x, p1.y-p2.y);
}
public static Point SumaPoints (Point p1, Point p2)
{
return new Point(p1.x+p2.x, p1.y+p2.y);
}

Y respecto a los operadores de comparación:

// Operadores de igualdad

public static bool operator == (Point p1, Point p2)


{
return (p1.Equals(p2));
}
public static bool operator != (Point p1, Point p2)
{
return (!p1.Equals(p2));
}
public override bool Equals (object o) { // Por
valor
if ( (((Point) o).x == this.x) &&
(((Point) o).y == this.y) )
return true;
else return false;
}
public override int GetHashCode() {
return (this.ToString().GetHashCode());
}

La sobrecarga de los operadores relacionales obliga a implementar la interface


IComparable, concretamente el método CompareTo:

public class Point : IComparable {

......

// Operadores relacionales

public int CompareTo(object o) {


Point tmp = (Point) o;
if (this.x > tmp.x) return 1;
else
if (this.x < tmp.x) return -1;
else return 0;
}

public static bool operator < (Point p1, Point p2)


{
IComparable ic1 = (IComparable) p1;
return (ic1.CompareTo(p2) < 0);
}
public static bool operator > (Point p1, Point p2)
{
IComparable ic1 = (IComparable) p1;
return (ic1.CompareTo(p2) > 0);
}
public static bool operator <= (Point p1, Point p2)
{
IComparable ic1 = (IComparable) p1;
return (ic1.CompareTo(p2) <= 0);
}
public static bool operator >= (Point p1, Point p2)
{
IComparable ic1 = (IComparable) p1;
return (ic1.CompareTo(p2) >= 0);
}

......
} // class Point

static void Main(string[] args)


{
......
if (p1 > p2) Console.WriteLine ("p1 > p2");
if (p1 >= p2) Console.WriteLine ("p1 >= p2");
if (p1 < p2) Console.WriteLine ("p1 < p2");
if (p1 <= p2) Console.WriteLine ("p1 <= p2");
if (p1 == p2) Console.WriteLine ("p1 == p2");
Console.WriteLine ();

if (p1 > p3) Console.WriteLine ("p1 > p3");


if (p1 >= p3) Console.WriteLine ("p1 >= p3");
if (p1 < p3) Console.WriteLine ("p1 < p3");
if (p1 <= p3) Console.WriteLine ("p1 <= p3");
if (p1 == p3) Console.WriteLine ("p1 == p3");
Console.WriteLine ();
}
}

El operador de asignación (=) cuando se aplica a clases copia la referencia, no el contenido.


Si se desea copiar instancias de clases lo habitual en C# es redefinir (overrride) el método
MemberwiseCopy() que heredan, por defecto, todas las clases en C# de System.Object.

Conversiones de tipo
Pueden programarse las conversiones de tipo, tanto explícitas como implícitas):

using System;

class ConversionesApp
{

public class Euro


{
private int cantidad; // Campo

public Euro (int v) // Constructor común


{ cantidad = v;}

public int valor // Propiedad


{ get { return cantidad; } }

// Conversión implícita "double <-- Euro"


public static implicit operator double (Euro x)
{
return ((double) x.cantidad);
}
// Conversión explícita "Euro <-- double"
public static explicit operator Euro(double x)
{
double arriba = Math.Ceiling(x);
double abajo = Math.Floor(x);
int valor = ((x+0.5 >= arriba) ? (int)arriba :
(int)abajo);
return new Euro(valor);
}

} // class Euro

static void Main(string[] args)


{
double d1 = 442.578;
double d2 = 123.22;

Euro e1 = (Euro) d1; // Conversión explícita a


"Euro"
Euro e2 = (Euro) d2; // Conversión explícita a
"Euro"

Console.WriteLine ("d1 es {0} y e1 es {1}",


d1, e1.valor);
Console.WriteLine ("d2 es {0} y e2 es {1}",
d2, e2.valor);

double n1 = e1; // Conversión implícita "double


<--Euro"
double n2 = e2; // Conversión implícita "double
<--Euro"

Console.WriteLine ("n1 es {0}", n1);


Console.WriteLine ("n2 es {0}", n2);

Console.ReadLine ();
}
}

C# no permite definir conversiones entre clases que se relacionan mediante herencia.


Dichas conversiones están ya disponibles: de manera implícita desde una clase derivada a
una antecesora y de manera explícita a la inversa.
Clases y structs
Tanto las clases como los structs permiten al usuario definir sus propios tipos, pueden
implementar múltiples interfaces y pueden contener datos (campos, constantes, eventos...),
funciones (métodos, propiedades, indexadores, operadores, constructores, destructores y
eventos) y otros tipos internos (clases, structs, enums, interfaces y delegados).

Observe las similitudes entre ambas en el siguiente ejemplo.

struct SPoint - class CPoint

using System;

struct SPoint
{
private int x, y; // Campos

public SPoint(int x, int y) // Constructor


{
this.x = x;
this.y = y;
}
public int X // Propiedad
{
get { return x; }
set { x = value; }
}
public int Y // Propiedad
{
get { return y; }
set { y = value; }
}
}

class CPoint
{
private int x, y; // Campos

public CPoint(int x, int y) // Constructor


{
this.x = x;
this.y = y;
}
public int X // Propiedad
{
get { return x; }
set { x = value; }
}
public int Y // Propiedad
{
get { return y; }
set { y = value; }
}
}

class Class2App
{
static void Main(string[] args)
{
SPoint sp = new SPoint(2,5);
sp.X += 100;
int spx = sp.X; // spx = 102

CPoint cp = new CPoint(2,5);


cp.X += 100;
int cpx = cp.X; // cpx = 102

Console.WriteLine ("spx es: {0} ", spx); // 102


Console.WriteLine ("cpx es: {0} ", cpx); // 102
Console.ReadLine ();
}
}

Aunque las coincidencias son muchas, existen, no obstante, existen algunas diferencias
entre ellos:

Cuando se crea un objeto struct mediante el operador new, se crea y se llama al constructor
apropiado. A diferencia de las clases, se pueden crear instancias de las estructuras sin
utilizar el operador new. Si no se utiliza new, los campos permanecerán sin asignar y el
objeto no se podrá utilizar hasta haber inicializado todos los campos.
Clase Struct
Tipo referencia Tipo valor
Para las estructuras no existe herencia: una
estructura no puede heredar de otra estructura o
Puede heredar de
clase, ni puede ser la base de una clase. Sin
otro tipo (que no
embargo, las estructuras heredan de la clase base
esté "sellado")
Object. Una estructura puede implementar
interfaces del mismo modo que las clases.
Puede tener un
constructor sin No puede tener un constructor sin parámetros
parámetros
No pueden Se pueden crear instancias de las estructuras sin
crearse instancias utilizar el operador new, pero los campos
sin emplear el permanecerán sin asignar y el objeto no se podrá
operador new. utilizar hasta haber iniciado todos los campos.
Puede tener un
No puede tener destructor
destructor

Supongamos la clase MiClase y el struct de tipo MiStruct. Según sabemos de C#, las
instancias de MiClase se almacenan en el heap mientras las instancias de MiStruct se
almacenan en la pila:
MiClase cl;
Declara una referencia (igual a la declaración de
un puntero no inicializado en C++).
Crea una instancia de MiClase. LLama al
cl = new
MiClase(); constructor sin parámetros de la clase. Además,
reserva memoria en el heap.
Crea una instancia de MiStruct pero no llama a
MiStruct st; ningún constructor. Los campos de st quedan sin
iniciar.
Llama al constructor: se inicializan los campos
st = new
MiStruct(); con los valores por defecto. No se reserva
memoria porque st ya existe en la pila.

Herencia
Concepto de herencia
Clases abstractas
Clases selladas
Tipos anidados
Operadores especiales

Concepto de herencia
El mecanismo de herencia es uno de los pilares fundamentales en los que se basa la
programación orientada a objetos. Es un mecanismo que permite definir nuevas clases a
partir de otras ya definidas. Si en la definición de una clase indicamos que ésta deriva de
otra, entonces la primera -a la que se le suele llamar clase hija o clase derivada- será
tratada por el compilador automáticamente como si su definición incluyese la definición de
la segunda -a la que se le suele llamar clase padre o clase base.

Las clases que derivan de otras se definen usando la siguiente sintaxis:

class <claseHija> : <clasePadre>


{
<miembrosHija>
}

A los miembros definidos en la clase hija se le añadirán los que hubiésemos definido en la
clase padre: la clase derivada "hereda" de la clase base.
La palabra clave base se utiliza para obtener acceso a los miembros de la clase base desde
una clase derivada.

C# sólo permite herencia simple.

Herencia de constructores

Los objetos de una clase derivada contarán con los mismos miembros que los objetos de la
clase base y además incorporarán nuevos campos y/o métodos. El constructor de una clase
derivada puede emplear el constructor de la clase base para inicializar los campos
heredados de la clase padre con la construcción base. En realidad se trata de una llamada al
constructor de la clase base con los parámetros adecuados.

: base(<parametrosBase>)

Si no se incluye el compilador consideraría que vale :base(), lo que provocaría un error si


la clase base carece de constructor sin parámetros.

Ejemplo de "herencia" de constructores

public class B
{
private int h; // Campo

public B () { // Constructor sin parámetros


this.h = -1;
}
public B (int h) // Constructor con parámetro
{
this.h = h;
}
public int H // Propiedad
{
get { return h; }
set { h = value; }
}
} // class B

public class D : B // "D" hereda de "B"


{
private int i; // Campo

public D () : this(-1) {} // Constructor sin


parámetros

public D (int i) { // Constructor con un parámetro


this.i = i;
}
public D (int h, int i) : base(h) { // Constructor
con
this.i = i; // dos
parámetros
}

public int I // Propiedad


{
get { return i; }
set { i = value; }
}

} // class D

......
B varB1 = new B(); // Const. sin parámetros de B
B varB2 = new B(5); // Const. con 1 parámetro de B
Console.WriteLine("varB1 : (H={0})", varB1.H);
Console.WriteLine("varB2 : (H={0})\n", varB2.H);

D varD1 = new D(); // Const. sin parámetros de


D
D varD2 = new D(15); // Const. con 1 parámetro
de D
D varD3 = new D(25, 11); // Const. con 2 parámetros
de D

Console.WriteLine("varD1 : (I={0},H={1})", varD1.I,


varD1.H);
Console.WriteLine("varD2 : (I={0},H={1})", varD2.I,
varD2.H);
Console.WriteLine("varD3 : (I={0},H={1})", varD3.I,
varD3.H);
Console.ReadLine();
......

En el siguiente ejemplo se muestra cómo puede extenderse la clase CocheSimple vista


anteriormente para construir, a partir de ella, la clase Taxi. Observar como se emplea la
construcción base para referenciar a un constructor de la clase base y que cuando actúa el
constructor sin parámetros de la clase Taxi se llama implícitamente al constructor sin
parámetros de la clase CocheSimple.
Ejemplo: herencia sobre la clase CocheSimple

using System;

namespace DemoHerencia {

class CocheSimple
{
private int VelocMax;
private string Marca;
private string Modelo;

public CocheSimple () {
this.VelocMax = 0;
this.Marca = "??";
this.Modelo = "??";
}
public CocheSimple (string marca, string mod, int
velMax)
{
this.VelocMax = velMax;
this.Marca = marca;
this.Modelo = mod;
}

public void MuestraCoche () {


Console.WriteLine (this.Marca + " " +
this.Modelo +
" (" + this.VelocMax + "
Km/h)");
}

} // class CocheSimple

class Taxi : CocheSimple

private string CodLicencia;

public Taxi () {}
public Taxi (string marca, string mod, int vel,
string lic) : base (marca, mod,
vel)
{
this.CodLicencia = lic;
}
public string Licencia {
get { return this.CodLicencia; }
}
} // class Taxi

class DemoHerenciaApp {

static void Main(string[] args) {

CocheSimple MiCoche =
new CocheSimple ("Citröen", "Xsara Picasso",
220);
CocheSimple TuCoche =
new CocheSimple ("Opel", "Corsa", 190);
CocheSimple UnCoche = new CocheSimple ();

Console.Write ("Mi coche: ");


MiCoche.MuestraCoche();
Console.Write ("El tuyo: ");
TuCoche.MuestraCoche();
Console.Write ("Un coche sin identificar: ");
UnCoche.MuestraCoche();

Console.WriteLine();

Taxi ElTaxiDesconocido = new Taxi ();


Console.Write ("Un taxi sin identificar: ");
ElTaxiDesconocido.MuestraCoche();

Taxi NuevoTaxi= new Taxi ("Ford", "KA", 150,


"GR1234");
Console.Write ("Un taxi nuevo: ");
NuevoTaxi.MuestraCoche();
Console.Write (" Licencia: {0}",
NuevoTaxi.Licencia);

Console.ReadLine ();

} // Main

} // class DemoHerenciaApp

} // namespace DemoHerencia

Redefinición de métodos

Siempre que se redefine un método que aparece en la clase base, hay que utilizar
explícitamente la palabra reservada override y, de esta forma, se evitan redefiniciones
accidentales (una fuente de errores en lenguajes como Java o C++).

Sabemos que todos los objetos (incluidas las variables de los tipos predefinidos) derivan, en
última instancia, de la clase Object. Esta clase proporciona el método ToString que crea
una cadena de texto legible para el usuario que describe una instancia de la clase. Si
dejamos sin redefinir este método y empleando la clase CocheSimple las siguientes
instrucciones:

CocheSimple MiCoche =
new CocheSimple ("Citröen", "Xsara Picasso",
220);

Console.WriteLine ("Mi coche: " +


MiCoche.ToString());

producen el siguiente resultado:

Mi coche: DemoHerencia.CocheSimple

lo que nos invita a redefinir el método ToString en la clase CocheSimple:

class CocheSimple
{
...
public override string ToString()
{
return (this.Marca + " " + this.Modelo +
" (" + this.VelocMax + " Km/h)");
}
...
}

Las dos instrucciones siguientes son equivalentes:

Console.WriteLine ("Mi coche: " +


MiCoche.ToString());

Console.WriteLine ("Mi coche: " + MiCoche);

por lo que podemos sutituir las instrucciones que muestran los datos de los objetos
CocheSimple por:

Console.WriteLine ("Mi coche: " + MiCoche);


Console.WriteLine ("El tuyo: " + TuCoche);
Console.WriteLine ("Un coche sin identificar: " +
UnCoche);

y eliminamos el (innecesario) método MuestraCoche, el resultado de la ejecución del


programa anterior es:

La palabra reservada base sirve para hacer referencia a los miembros de la clase base que
quedan ocultos por otros miembros de la clase actual. Por ejemplo, podríamos redefinir
también el método ToString de la clase Taxi empleando el método redefinido ToString
de la clase base CocheSencillo:

class CocheSimple
{
...
public override string ToString()
{
return (this.Marca + " " + this.Modelo +
" (" + this.VelocMax + " Km/h)");
}
...
}
class Taxi : CocheSimple
{
...
public override string ToString()
{
return (base.ToString() + "\n Licencia: " +
this.Licencia);
}
...
}

......
Taxi ElTaxiDesconocido = new Taxi ();
Console.WriteLine ("Un taxi sin identificar: " +
ElTaxiDesconocido);

Taxi NuevoTaxi= new Taxi ("Citröen", "C5", 250,


"GR1234");
Console.WriteLine ("Un taxi nuevo: " + NuevoTaxi);
......

y el resultado es:
En la sección dedicada a la classes.xml#Sobrecarga

sobrecarga de operadores

introdujimos la clase Point. No había ningún método que mostrara los datos de interés de
un objeto de tipo Point. Podemos sobreescribir el método ToString de manera que fuera:

public class Point


{
...
public override string ToString()
{
return ("["+this.X+", "+this.Y+"]");
}
...
}

Ahora las instrucciones de escritura se convierten en llamadas a este método, por ejemplo:

Console.WriteLine ("p1 es: " + p1);


// Console.WriteLine ("p1 es: " + p1.ToString()

El resultado de la ejecución de ese programa será:


Métodos virtuales

Un método es virtual si puede redefinirse en una clase derivada. Los métodos son no
virtuales por defecto.

 Los métodos no virtuales no son polimórficos (no pueden reemplazarse) ni


pueden ser abstractos.
 Los métodos virtuales se definen en una clase base (empleando la palabra
reservada virtual) y pueden ser reemplazados (empleando la palabra
reservada override) en las subclases (éstas proporcionan su propia
-específica- implementación).
 Generalmente, contendrán una implementación por defecto del método (si
no, se deberían utilizar métodos abstractos).

class Shape // Clase base


{
// "Draw" es un método virtual
public virtual void Draw() { ... }
}

class Box : Shape


{
// Reemplaza al método Draw de la clase base
public override void Draw() { ... }
}

class Sphere : Shape


{
// Reemplaza al método Draw de la clase base
public override void Draw() { ... }
}

void HandleShape(Shape s)
{
...
s.Draw(); // Polimorfismo
...
}

HandleShape(new Box());
HandleShape(new Sphere());
HandleShape(new Shape());

NOTA: Propiedades, indexadores y eventos también pueden ser virtuales.

Clases abstractas
Una clase abstracta es una clase que no puede ser instanciada. Se declara empelando la
palabra reservada abstract.
Permiten incluir métodos abstractos y métodos no abstractos cuya implementación hace
que sirvan de clases base (herencia de implementación). Como es lógico, no pueden estar
"selladas".

Métodos abstractos

Un método abstracto es un método sin implementación que debe pertenecer a una clase
abstracta. Lógicamente se trata de un método virtual forzoso y su implementación se
realizará en una clase derivada.

abstract class Shape // Clase base abstracta


{
public abstract void Draw(); // Método abstracto
}

class Box : Shape


{
public override void Draw() { ... }
}

class Sphere : Shape


{
public override void Draw() { ... }
}

void HandleShape(Shape s)
{
...
s.Draw();
...
}

HandleShape(new Box());
HandleShape(new Sphere());
HandleShape(new Shape()); // Error !!!

Clases selladas
Una clase sellada (sealed), es una clase de la que no pueden derivarse otras clases (esto es,
no puede utilizarse como clase base). Obviamente, no puede ser una clase abstracta.

Los struct en C# son implícitamente clases selladas.

¿Para qué sirve sellar clases? Para evitar que se puedan crear subclases y optimizar el
código (ya que las llamadas a las funciones de una clase sellada pueden resolverse en
tiempo de compilación).

Tipos anidados
C# permite declarar tipos anidados, esto es, tipos definidos en el ámbito de otro tipo. El
anidamiento nos permite que el tipo anidado pueda acceder a todos los miembros del tipo
que lo engloba (independientemente de los modificadores de acceso) y que el tipo esté
oculto de cara al exterior (salvo que queramos que sea visible, en cuyo caso habrá que
especificar el nombre del tipo que lo engloba para poder acceder a él).

Operadores especiales
is

Se utiliza para comprobar dinámicamente si el tipo de un objeto es compatible con un tipo


especificado (instanceof en Java).

No conviene abusar de este operador (es preferible diseñar correctamente una jerarquía de
tipos).

static void DoSomething(object o)


{
if (o is Car) ((Car)o).Drive();
}

as

Intenta convertir de tipo una variable (al estilo de los casts dinámicos de C++). Si la
conversión de tipo no es posible, el resultado es null. Es más eficiente que el operador is,
si bien tampoco es conveniente abusar del operador as.

static void DoSomething(object o)


{
Car c = o as Car;

if (c != null) c.Drive();
}

typeof

El operador typeof devuelve el objeto derivado de System.Type correspondiente al tipo


especificado. De esta forma se puede hacer reflexión para obtener dinámicamente
información sobre los tipos (como en Java).

...
Console.WriteLine(typeof(int).FullName);
Console.WriteLine(typeof(System.Int).Name);
Console.WriteLine(typeof(float).Module);
Console.WriteLine(typeof(double).IsPublic);
Console.WriteLine(typeof(Point).MemberType);
...

Clases (2)
Indexadores (indexers)
Interfaces
Delegados

Indexadores (indexers)
C# no permite, hablado con rigor, la sobrecarga del operador de acceso a tablas [ ]. Si
permite, no obstante, definir lo que llama un indexador para una clase que permite la
misma funcionalidad.

Los indexadores permiten definir código a ejecutar cada vez que se acceda a un objeto del
tipo del que son miembros usando la sintaxis propia de las tablas, ya sea para leer o
escribir. Esto es especialmente útil para hacer más clara la sintaxis de acceso a elementos
de objetos que puedan contener colecciones de elementos, pues permite tratarlos como si
fuesen tablas normales.

A diferencia de las tablas, los índices que se les pase entre corchetes no tiene porqué ser
enteros, pudiéndose definir varios indexadores en un mismo tipo siempre y cuando cada
uno tome un número o tipo de índices diferente.

La sintaxis empleada para la definición de un indexador es similar a la de la definición de


una propiedad.

public class MiClase


{
...
public string this[int x]
{
get {
// Obtener un elemento
}
set {
// Fijar un elemento
}
}
...
}
...
MiClase MiObjeto = new MiClase();

El código que aparece en el bloque get se ejecuta cuando la expresión MiObjeto[x]


aparece en la parte derecha de una expresión mientras que el código que aparece en el
bloque set se ejecuta cuando MiObjeto[x] aparece en la parte izquierda de una expresión.

Algunas consideraciones finales:

 Igual que las propiedades, pueden ser de sólo lectura, de sólo escritura o de
lectura y escritura.
 El nombre dado a un indexador siempre ha de ser this.
 Lo que diferenciará a unos indexadores de otros será el número y tipo de sus
índices.

Ejemplo de indexador

using System;

namespace IndexadorCoches
{
public class Coche
{
private int VelocMax;
private string Marca;
private string Modelo;

public Coche (string marca, string modelo, int


velocMax)
{
this.VelocMax = velocMax;
this.Marca = marca;
this.Modelo = modelo;
}

public override string ToString () {


return (this.Marca + " " + this.Modelo +
" (" + this.VelocMax + " Km/h)");
}

} // class Coche

public struct DataColeccionCoches


{
public int numCoches;
public int maxCoches;
public Coche[] vectorCoches;

public DataColeccionCoches (int max)


{
this.numCoches = 0;
this.maxCoches = max;
this.vectorCoches = new Coche[max];
}
} // struct DataColeccionCoches

public class Coches


{
// Los campos se encapsulan en un struct
DataColeccionCoches data;

// Constructor sin parámetros


public Coches()
{
this.data = new DataColeccionCoches(10);
// Si no se pone un valor se llamará al
// constructor sin parámetros (por defecto)
del
// struct y tendremos problemas: no se puede
// explicitar el constructor sin parámetros
// para un struct.
}

// Constructor con parámetro


public Coches(int max)
{
this.data = new DataColeccionCoches(max);
}

public int MaxCoches


{
set { data.maxCoches = value; }
get { return (data.maxCoches); }
}
public int NumCoches
{
set { data.numCoches = value; }
get { return (data.numCoches); }
}
public override string ToString () {
string str1 = " --> Maximo= " +
this.MaxCoches;
string str2 = " --> Real = " +
this.NumCoches;
return (str1 + "\n" + str2);
}

// El indexador devuelve un objeto Coche de


acuerdo
// a un índice numérico
public Coche this[int pos]
{
// Devuelve un objeto del vector de coches
get
{
if(pos < 0 || pos >= MaxCoches)
throw new
IndexOutOfRangeException("Fuera de
rango");
else
return (data.vectorCoches[pos]);
}
// Escribe en el vector
set { this.data.vectorCoches[pos] = value;}
}

} // class Coches

class IndexadorCochesApp
{
static void Main(string[] args)
{
// Crear una colección de coches
Coches MisCoches = new Coches (); // Por
defecto (10)

Console.WriteLine ("***** Mis Coches *****");


Console.WriteLine ("Inicialmente: ");
Console.WriteLine (MisCoches);

// Añadir coches. Observar el acceso con []


("set")
MisCoches[0] = new Coche ("Opel", "Zafira",
200);
MisCoches[1] = new Coche ("Citröen", "Xsara",
220);
MisCoches[2] = new Coche ("Ford", "Focus",
190);

MisCoches.NumCoches = 3;

Console.WriteLine ("Despues de insertar 3


coches: ");
Console.WriteLine (MisCoches);

// Mostrar la colección
Console.WriteLine ();
for (int i=0; i<MisCoches.NumCoches; i++)
{
Console.Write ("Coche Num.{0}: ", i+1);
Console.WriteLine (MisCoches[i]); //
Acceso ("get")
}
Console.ReadLine ();

//
*********************************************
// Crear una colección de coches
Coches TusCoches = new Coches (4);

Console.WriteLine ("***** Tus Coches *****");


Console.WriteLine ("Inicialmente: ");
Console.WriteLine (TusCoches);

// Añadir coches. Observar el acceso con []


TusCoches[TusCoches.NumCoches++] =
new Coche ("Opel", "Corsa", 130);
TusCoches[TusCoches.NumCoches++] =
new Coche ("Citröen", "C3", 140);

Console.WriteLine ("Despues de insertar 2


coches: ");
Console.WriteLine (TusCoches);

// Mostrar la colección
Console.WriteLine ();
for (int i=0; i<TusCoches.NumCoches; i++)
{
Console.Write ("Coche Num.{0}: ", i+1);
Console.WriteLine (TusCoches[i]); //
Acceso ("get")
}

Console.ReadLine ();

} // Main

} // class IndexadorCochesApp

} // namespace IndexadorCoches
Interfaces
Un interfaz define un contrato semántico que ha de respetar cualquier clase (o struct) que
implemente el interfaz.

La interfaz no contiene implementación alguna. La clase o struct que implementa el interfaz


es la que tiene la funcionalidad especificada por el interfaz.

Una interfaz puede verse como una forma especial de definir clases que sólo cuenten con
miembros abstractos. Sin embargo, todo tipo que derive de una interfaz ha de dar una
implementación de todos los miembros que hereda de esta, y no como ocurre con las clases
abstractas donde es posible no darla si se define como abstracta también la clase hija.

 La especificación del interfaz puede incluir métodos, propiedades,


indexadores y eventos, pero no campos, operadores, constructores o
destructores.
 Aunque solo se permite la herencia simple de clases, como ocurre en Java,
se permite y herencia múltiple de interfaces. Esto significa que es posible
definir tipos que deriven de más de una interfaz.
 Los interfaces (como algo separado de la implementación) permiten la
existencia del polimorfismo, al poder existir muchas clases o structs que
implementen el interfaz.

Ejemplo de polimorfismo

using System;

namespace Interface1
{
class Interface1App
{
// Definición de una interface

public interface IDemo


{
void MetodoDeIDemo ();
}

// "Clase1" y "Clase2" implementan la interface

public class Clase1 : IDemo


{
public void MetodoDeIDemo()
{
Console.WriteLine ("Método de Clase1");
}
}

public class Clase2 : IDemo


{
public void MetodoDeIDemo()
{
Console.WriteLine ("Método de Clase2");
}
}

static void Main(string[] args)


{
Clase1 c1 = new Clase1();
Clase2 c2 = new Clase2();
IDemo demo; // objeto de una interface

// Ejemplo de polimorfismo
demo = c1;
demo.MetodoDeIDemo();

demo = c2;
demo.MetodoDeIDemo();

Console.ReadLine();
}
}
}

Otro ejemplo:

Ejemplo de interface "heredada"

using System;

class InterfaceApp
{

interface IPresentable
{
void Presentar();
}

class Triangulo : IPresentable


{
private double b, a;
public Triangulo(double Base, double altura)
{
this.b=Base;
this.a=altura;
}
public double Base
{
get { return b; }
}
public double Altura
{
get { return a; }
}
public double Area
{
get { return (Base*Altura/2); }
}
public void Presentar()
{
Console.WriteLine("Base del triángulo: {0}",
Base);
Console.WriteLine("Altura del triángulo: {0}",
Altura);
Console.WriteLine("Área del triángulo: {0}",
Area);
}
}
class Persona : IPresentable
{
private string nbre, apell, dir;
public Persona (string nombre, string apellidos,
string direccion)
{
this.nbre = nombre;
this.apell = apellidos;
this.dir = direccion;
}
public string Nombre
{
get { return nbre; }
}
public string Apellidos
{
get { return apell; }
}
public string Direccion
{
get { return dir; }
}
public void Presentar()
{
Console.WriteLine("Nombre: {0}", Nombre);
Console.WriteLine("Apellidos: {0}", Apellidos);
Console.WriteLine("Dirección: {0}", Direccion);
}
}
static void VerDatos(IPresentable IP)
{
IP.Presentar();
}

static void Main(string[] args)


{
Triangulo t=new Triangulo(10,5);
Persona p=new Persona ("Paco", "Pérez", "su
casa");

Console.WriteLine("Ya se han creado los objetos");


Console.WriteLine("\nINTRO para
VerDatos(triangulo)");
Console.ReadLine();
VerDatos(t);

Console.WriteLine("\nINTRO para
VerDatos(proveedor)");
Console.ReadLine();
VerDatos(p);

Console.ReadLine();
}
}

El principal uso de las interfaces es indicar que una clase implementa ciertas características.
Por ejemplo, el ciclo foreach trabaja internamente comprobando que la clase sobre la que
se aplica implementa el interfaz IEnumerable y llamando a los métodos definidos en esa
interfaz.

Herencia múltiple

La plataforma .NET no permite herencia múltiple de implementación, aunque sí se puede


conseguir herencia múltiple de interfaces. Clases, structs e interfaces pueden heredar de
múltiples interfaces (como en Java).
Herencia de interfaces

using System;

namespace Interface2
{
class Interface2App
{
// Definición de interfaces

public interface IDemo1


{
void Metodo1DeInterface1 ();
string Metodo2DeInterface1 ();
}

public interface IDemo2


{
void Metodo1DeInterface2 ();
}

public interface IDemo3 : IDemo1


{
void Metodo1DeInterface3 (string mensaje);
}

// "Clase1" implementan la interface "IDemo1"

public class Clase1 : IDemo1


{
public void Metodo1DeInterface1()
{ Console.WriteLine ("Mét1 de Int1 en
Clase1"); }

public string Metodo2DeInterface1()


{ return ("En Mét2 de Int1 en Clase1"); }

// "Clase1" implementan las interfaces


// "IDemo1" e "IDemo2"

public class Clase2 : IDemo1, IDemo2


{
public void Metodo1DeInterface1()
{ Console.WriteLine ("Mét1 de Int1 en
Clase2"); }

public string Metodo2DeInterface1()


{ return ("En Mét2 de Int1 en Clase2"); }

public void Metodo1DeInterface2()


{ Console.WriteLine ("Mét1 de Int2 en
Clase2"); }
}
// "Clase3" implementan la interface "IDemo3", la
// cual ha heredado de "IDemo1"

public class Clase3 : IDemo3


{
public void Metodo1DeInterface1()
{ Console.WriteLine ("Mét1 de Int1 en
Clase3"); }

public string Metodo2DeInterface1()


{ return ("En Mét2 de Int1 en Clase3"); }

public void Metodo1DeInterface3 (string m)


{ Console.WriteLine (m + "Mét1 de Int3 en
Clase3"); }

}
static void Main(string[] args)
{
Clase1 c1 = new Clase1();
Clase2 c2 = new Clase2();
Clase3 c3 = new Clase3();

IDemo1 i1;
IDemo2 i2;
IDemo3 i3;

c1.Metodo1DeInterface1();
Console.WriteLine(c1.Metodo2DeInterface1());
Console.WriteLine();

i1 = c3;
Console.WriteLine("Cuando i1 = c3 ");
i1.Metodo1DeInterface1();
Console.WriteLine(i1.Metodo2DeInterface1());
Console.WriteLine();

i3 = c3;
Console.WriteLine("Cuando i3 = c3 ");
i3.Metodo1DeInterface1();
Console.WriteLine(i3.Metodo2DeInterface1());
i3.Metodo1DeInterface3("Aplicado a i3: ");
Console.WriteLine();

i1 = c2;
Console.WriteLine("Cuando i1 = c2 ");
i1.Metodo1DeInterface1();
Console.WriteLine(i1.Metodo2DeInterface1());
i2 = c2;
Console.WriteLine("Ahora i2 = c2 ");
i2.Metodo1DeInterface2();
Console.WriteLine();

Console.ReadLine();
}
}
}

Resolución de conflictos de nombres

Si dos interfaces tienen un método con el mismo nombre, se especifica explícitamente el


interfaz al que corresponde la llamada al método para eliminar ambigüedades

interface IControl
{
void Delete();
}

interface IListBox: IControl


{
void Delete();
}

interface IComboBox: ITextBox, IListBox


{
void IControl.Delete();
void IListBox.Delete();
}

Delegados
Un delegado es un tipo especial de clase que define la signatura de un método. Su función
es similar a la de los punteros a funciones en lenguajes como C y C++ (C# no soporta
punteros a funciones).
Los delegados pueden pasarse a métodos y pueden usarse para llamar a los métodos de los
que contienen referencias.

Los delegados proporcionan polimorfismo para las llamadas a funciones:

Un "tipo" delegado

using System;

class Delegate1App
{
// Declaración del "tipo delegado" llamado
// "Del": funciones que devuelven un double
// y reciben un double.
delegate double Del (double x);

static double incremento (double x)


{ return (++x); }

static void Main(string[] args)


{
// Instanciación
Del del1 = new Del (Math.Sin);
Del del2 = new Del (Math.Cos);
Del del3 = new Del (incremento);

// Llamadas
Console.WriteLine (del1(0)); // 0
Console.WriteLine (del2(0)); // 1
Console.WriteLine (del3(10)); // 11

Console.ReadLine();
}
}

Para hacer más evidente el polimorfismo el método Main podría escribirse como:

static void Main(string[] args)


{
Del f1 = new Del (Math.Sin);
Del f2 = new Del (Math.Cos);
Del f3 = new Del (incremento);

Del f;
f = f1; Console.WriteLine (f(0)); // 0
f = f2; Console.WriteLine (f(0)); // 1
f = f3; Console.WriteLine (f(10)); // 11
}
o bien así:

static void Main(string[] args)


{
Del[] d = new Del[3];

d[0] = new Del (Math.Sin);


d[1] = new Del (Math.Cos);
d[2] = new Del (incremento);

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


Console.WriteLine (d[i](0)); // 0, 1
Console.WriteLine (d[2](10)); // 11
}

Los delegados son muy útiles ya que permiten disponer de objetos cuyos métodos puedan
ser modificados dinámicamente durante la ejecución de un programa.

En general, son útiles en todos aquellos casos en que interese pasar métodos como
parámetros de otros métodos.

Los delegados son la base sobre la que se monta la gestión de eventos en la plataforma
.NET.

Multicasting

Un delegado es un tipo especial de clase cuyos objetos pueden almacenar referencias a uno
o más métodos, de tal manera que a través del objeto sea posible solicitar la ejecución en
cadena de todos ellos.

Un delegado puede contener e invocar múltiples métodos. De esta forma se puede hacer
multicasting de una forma sencilla y elegante. Para que un delegado pueda contener varios
métodos, éstos no pueden devolver ningún valor (si lo intentasen devolver, se generaría una
excepción en tiempo de ejecución).

"Multicasting"

using System;

class Delegate2App
{
delegate void SomeEvent (int x, int y);

static void Func1(int x, int y)


{ Console.WriteLine(" Desde Func1"); }
static void Func2(int x, int y)
{ Console.WriteLine(" Desde Func2"); }

static void Main(string[] args)


{
SomeEvent func = new SomeEvent(Func1);

func += new SomeEvent(Func2);

Console.WriteLine("Llamada a func");
func(1,2); // Se llama tanto a Func1 como a Func2

func -= new SomeEvent(Func1);

Console.WriteLine("Llamada a func");
func(2,3); // Sólo se llama a Func2

Console.ReadLine();
}
}

Cada delegado tiene una lista ordenada de métodos que se invocan secuencialmente (en el
mismo orden en el que fueron añadidos al delegado). Los operadores += y -= se utilizan
para añadir y eliminar métodos de la lista asociada a un delegado.

Delegados vs. interfaces

Siempre se pueden utilizar interfaces en vez de delegados. Los interfaces son más
versátiles, ya que pueden encapsular varios métodos y permiten herencia, si bien los
delegados resultan más adecuados para implementar manejadores de eventos. Con los
delegados se escribe menos código y se pueden implementar fácilmente múltiples
manejadores de eventos en una única clase.

Eventos

Muchas aplicaciones actuales se programan en función de eventos. Cuando se produce


algún hecho de interés para nuestra aplicación, éste se notifica mediante la generación de
un evento, el cual será procesado por el manejador de eventos correspondiente (modelo
"publish-subscribe"). Los eventos nos permiten enlazar código personalizado a
componentes creados previamente (mecanismo de "callback").

El callback consiste en que un cliente notifica a un servidor que desea ser informado
cuando alguna acción tenga lugar. C# usa los eventos de la misma manera que Visual Basic
usa los mensajes.

Las aplicaciones en Windows se programan utilizando eventos, pues los eventos resultan
especialmente indicados para la implementación de interfaces interactivos. Cuando el
usuario hace algo (pulsar una tecla, hacer click con el ratón, seleccionar un dato de una
lista...), el programa reacciona en función de la acción del usuario.

El uso de eventos, no obstante, no está limitado a la implementación de interfaces. También


son útiles en el desarrollo de aplicaciones que deban realizar operaciones periódicamente o
realizar operaciones de forma asíncrona (p.ej. llegada de un correo electrónico, terminación
de una operación larga...).

El lenguaje C# da soporte a los eventos mediante el uso de delegados. Al escribir nuestra


aplicación, un evento no será más que un campo que almacena un delegado. Los usuarios
de la clase podrán registrar delegados (mediante los operadores += y -=), pero no podrán
invocar directamente al delegado.

Eventos en C#

public delegate void EventHandler ( object sender,


EventArgs e);

public class Button


{
public event EventHandler Click;

protected void OnClick (EventArgs e)


{
// This is called when button is clicked
if (Click != null) Click(this, e);
}
}

public class MyForm: Form


{
Button okButton;

static void OkClicked(object sender, EventArgs e)


{
ShowMessage("You pressed the OK button");
}

public MyForm()
{
okButton = new Button(...);
okButton.Caption = "OK";
okButton.Click += new EventHandler(OkClicked);
}
}

Aspectos avanzados de C#
Excepciones
Reflexión
Atributos

Excepciones
Las excepciones ofrecen varias ventajas respecto a otros métodos de notificación de error,
como los códigos devueltos (órdenes return) ya que ningún error pasa desapercibido (las
excepciones no pueden ser ignoradas) y no tienen por qué tratarse en el punto en que se
producen. Los valores no válidos no se siguen propagando por el sistema. No es necesario
comprobar los códigos devueltos. Es muy sencillo agregar código de control de
excepciones para aumentar la confiabilidad del programa.

Una excepción es cualquier situación de error o comportamiento inesperado que encuentra


un programa en ejecución. Las excepciones se pueden producir a causa de un error en el
código o en código al que se llama (como una biblioteca compartida), que no estén
disponibles recursos del sistema operativo, condiciones inesperadas que encuentra
Common Language Runtime (por ejemplo, código que no se puede comprobar), etc. La
aplicación se puede recuperar de algunas de estas condiciones, pero de otras no.

Las excepciones pueden generarse en un proceso o hebra de nuestra aplicación (con la


sentencia throw) o pueden provenir del entorno de ejecución de la plataforma .NET.

En .NET Framework, una excepción es un objeto derivado de la clase Exception. El


mecanismo de control de excepciones en C# es muy parecido al de C++ y Java: la
excepción se inicia en un área del código en que se produce un problema. La excepción
asciende por la pila hasta que la aplicación la controla o el programa se detiene.

El proceso es el siguiente:

 La sentencia throw lanza una excepción (una instancia de una clase derivada
de System.Exception, que contiene información sobre la excepción:
Message, StackTrace, HelpLink, InnerException...).
 El bloque try delimita código que podría generar una excepción.
 El bloque catch indica cómo se manejan las excepciones. Se puede relanzar
la excepción capturada o crear una nueva si fuese necesario. Se pueden
especificar distintos bloques catch para capturar distintos tipos de
excepciones. En ese caso, es recomendable poner primero los más
específicos (para asegurarnos de que capturamos la excepción concreta).
 El bloque finally incluye código que siempre se ejecutará (se produzca o
no una excepción).

Ejemplo 1

El siguiente ejemplo pone de manifiesto el flujo de ejecución que sigue un programa que
lanza y procesa una excepción.

try {
Console.WriteLine("try");
throw new Exception("Mi excepcion");
}
catch {
Console.WriteLine("catch");
}
finally {
Console.WriteLine("finally");
}

La ejecución de este programa produce el siguiente resultado:

try
catch
finally

Ejemplo 2

Se puede profundizar en el tratamiento de la excepción, por ejemplo, comprobando alguna


propiedad del objeto Exception generado.

La clase Exception es la clase base de la que derivan las excepciones. La mayoría de los
objetos de excepción son instancias de alguna clase derivada de Exception, pero se puede
iniciar cualquier objeto derivado de la clase Object como excepción. En casi todos los
casos, es recomendable iniciar y detectar sólo objetos Exception.

La clase Exception tiene varias propiedades que facilitan la comprensión de una


excepción. Entre éstas destacamos la propiedad Message. Esta propiedad proporciona
información sobre la causa de una excepción. Veamos cómo se utiliza:

try {
Console.WriteLine("try");
throw new Exception("Mi excepcion");
}
catch (Exception e)
{
Console.WriteLine("catch");
Console.WriteLine("Excepción detectada: " +
e.Message);
}
catch {
Console.WriteLine("catch");
}
finally {
Console.WriteLine("finally");
}

La ejecución de este programa produce el siguiente resultado:

try
catch
Excepción detectada: Mi excepcion
finally

La mayoría de las clases derivadas de la clase Exception no implementan miembros


adicionales ni proporcionan más funcionalidad, simplemente heredan de Exception. Por
ello, la información más importante sobre una excepción se encuentra en la jerarquía de
excepciones, el nombre de la excepción y la información que contiene la excepción.

Ejemplo 3

El siguiente ejemplo muestra cómo el uso de execpciones puede controlar un número


importante de situaciones de error.

static void Main(string[] args)


{
int numerador = 10;
Console.WriteLine ("Numerador es = {0}",
numerador);
Console.Write ("Denominador = ");
string strDen = Console.ReadLine();

int denominador, cociente;

try
{
Console.WriteLine("--> try");
denominador = Convert.ToInt16(strDen);
cociente = numerador / denominador;
Console.WriteLine ("Cociente = {0}", cociente);
}

catch (ArithmeticException e)
{
Console.WriteLine("--> catch");
Console.WriteLine("Excep. aritmética");
Console.WriteLine("ArithmeticException Handler:
{0}",
e.ToString());
}
catch (ArgumentNullException e)
{
Console.WriteLine("--> catch");
Console.WriteLine("Excep. de argumento nulo");
Console.WriteLine("ArgumentNullException
Handler: {0}",
e.ToString());
}
catch (Exception e)
{
Console.WriteLine("--> catch");
Console.WriteLine("generic Handler: {0}",
e.ToString());
}
finally
{
Console.WriteLine("--> finally");
}

Console.ReadLine();
}
}

Cuando todo funciona sin problemas:

Cuando se intenta dividir por cero:


Cuando se produce desbordamiento:

Cuando se produce otro problema (cadena vacía, por ejemplo):

Ejemplo 4

Hemos visto que pueden conocerse los detalles de la excepción que se haya producido.
Podemos conocer más detalles usando la propiedad StackTrace. Esta propiedad contiene
un seguimiento de pila que se puede utilizar para determinar dónde se ha producido un
error. El seguimiento de pila contiene el nombre del archivo de código fuente y el número
de la línea del programa si está disponible la información de depuración.

using System;
using System.Diagnostics;

class ExcepApp
{
static void Main(string[] args)
{
int numerador = 10;
Console.WriteLine ("Numerador es = {0}",
numerador);

Console.Write ("Denominador = ");


string strDen = Console.ReadLine();

int denominador, cociente;

try
{
Console.WriteLine("--> try");
denominador = Convert.ToInt16(strDen);
cociente = numerador / denominador;
Console.WriteLine ("Cociente = {0}", cociente);
}

catch (Exception e)
{
Console.WriteLine("--> catch");
Console.WriteLine("Generic Handler: {0}",
e.ToString());
Console.WriteLine();

StackTrace st = new StackTrace(e, true);

Console.WriteLine("Traza de la pila:");
for (int i = 0; i < st.FrameCount; i++) {
StackFrame sf = st.GetFrame(i);
Console.WriteLine(" Pila de llamadas, Método:
{0}",
sf.GetMethod() );
Console.WriteLine(" Pila de llamadas, Línea :
{0}",
sf.GetFileLineNumber());
}

Console.WriteLine();
}

finally
{
Console.WriteLine("--> finally");
}

Console.ReadLine();
}
}
Reflexión
La capacidad de reflexión de la plataforma .NET (similar a la de la plataforma Java) nos
permite explorar información sobre los tipos de los objetos en tiempo de ejecución.

La instrucción GetType() obtiene el objeto Type de la instancia actual sobre el que se


aplica. El valor devuelto es representa el tipo exacto, en tiempo de ejecución, de la instancia
actual.
Un sencillo ejemplo con Type y GetType()

public class Test


{
private int n;

public Test (int n)


{
this.n = n;
}
}

// Acceso a información acerca de una clase

public static void Main(string[] args)


{
Type tipoClase = typeof(Test);
Console.WriteLine("El nombre del tipo de tipoClase
es: {0}",
tipoClase.Name);

Test t = new Test(0);


Type tipoVariable = t.GetType();
Console.WriteLine("El tipo de la variable t es:
{0}",
tipoVariable.Name);
}

El programa anterior muestra como resultado:

El nombre del tipo de tipoClase es: Test


El tipo de la variable t es: Test
Otro ejemplo del uso de Type y GetType():

Un ejemplo más complejo con Type y GetType()

Using System;

public class ClaseBase : Object {}

public class ClaseDerivada : ClaseBase {}

public class Test {

public static void Main() {

ClaseBase ibase = new ClaseBase();


ClaseDerivada iderivada = new ClaseDerivada();
Console.WriteLine("ibase: Type is {0}",
ibase.GetType());
Console.WriteLine("iderivada: Type is {0}",
iderivada.GetType());

object o = iderivada;
ClaseBase b = iderivada;
Console.WriteLine("object o = iderivada: Type is
{0}",
o.GetType());
Console.WriteLine("ibase b = iderivada: Type is
{0}",
b.GetType());
Console.ReadLine();
}
}

La reflexión puede emplearse para examinar los métodos, propiedades, ... de una clase:
Métodos de una clase

using System;
using System.Reflection;

class ReflectApp
{
public class Test
{
private int n;

public Test (int n) {


this.n = n;
}
public void Metodo1DeTest (int n) {
// .....
}
public int Metodo2DeTest (int a, float b, string
c) {
// .....
return 0;
}
}

public static void Main(string[] args)


{
Type t = typeof(Test);

MethodInfo[] MetInf = t.GetMethods();

foreach (MethodInfo m in MetInf) {


Console.WriteLine ();
Console.WriteLine ("Método: " + m.Name );
Console.WriteLine (" Características: "
+ ((m.IsPublic) ? " (public)" : "")
+ ((m.IsVirtual) ? " (virtual)" :
""));

// Parámetros

ParameterInfo[] ParInf = m.GetParameters();

if (ParInf.Length > 0) {
Console.WriteLine (" Parámetros: " );
foreach (ParameterInfo p in ParInf)
Console.WriteLine(" " + p.ParameterType
+ " " + p.Name);
}
}
Console.ReadLine ();
}
}

Atributos
Un atributo es información que se puede añadir a los metadatos de un módulo de código.
Los atributos nos permiten "decorar" un elemento de nuestro código con información
adicional.

C# es un lenguaje imperativo, pero, como todos los lenguajes de esta categoría, contiene
algunos elementos declarativos. Por ejemplo, la accesibilidad de un método de una clase se
especifica mediante su declaración como public, protected, private o internal. C#
generaliza esta capacidad permitiendo a los programadores inventar nuevas formas de
información declarativa, anexarlas a distintas entidades del programa y recuperarlas en
tiempo de ejecución. Los programas especifican esta información declarativa adicional
mediante la definición y el uso de atributos.

Esta información puede ser referente tanto al propio módulo o el ensamblado al que
peretenezca, como a los tipos de datos definidos en él, sus miembros, los parámetros de sus
métodos, los bloques set y get de sus propiedades e indexadores o los bloques add y
remove de sus eventos. Se pueden emplear en ensamblados, módulos, tipos, miembros,
valores de retorno y parámetros.

Atributos predefinidos

Si bien el programador puede definir cuantos atributos considere necesarios, algunos


atributos ya están predefinidos en la plataforma .NET.

Atributo Descripción
Browsable
Propiedades y eventos que deben mostrarse en el
inspector de objetos.
Clases y estructuras que pueden "serializarse" (esto
Serializable es, volcarse en algún dispositivo de salida, p.ej.
disco), como en Java.
Obsolete
El compilador se quejará si alguien los utiliza
(deprecated en Java).
ProgId COM Prog ID
Transaction Características transaccionales de una clase.

Observar como al marcar como obsoleta la clase A se genera un error al compilar el módulo
ya que se emplea en la línea comentada.

...

[Obsolete("Clase A desfasada. Usar B en su lugar")]


class A {
public void F() {}
}
class B {
public void F() {}
}

class SimpleAtrPredefApp
{

static void Main(string[] args)


{

A a = new A(); // Avisos


a.F();
...
}
}
Declarar una clase atributo

Declarar un atributo en C# es simple: se utiliza la forma de una declaración de clase que


hereda de System.Attribute y que se ha marcado con el atributo AttributeUsage, como
se indica a continuación:

// La clase HelpAttribute posee un parámetro


posicional (url)
// de tipo string y un parámetro con nombre -opcional-
(Topic)
// de tipo string.

[AttributeUsage(AttributeTargets.All)]
public class HelpAttribute: Attribute
{
public string Topic = null;
private string url;

public HelpAttribute(string url)


{
this.url = url;
}
public string Url {
get { return url; }
}
public override string ToString() {
string s1 = " Url = " + this.Url;
string dev = (this.Topic != null) ?
(s1 + " - Topic: " + this.Topic) : s1;
return (dev);
}
} // class HelpAttribute
 El atributo AttributeUsage especifica los elementos del lenguaje a los que
se puede aplicar el atributo.
 Las clases de atributos son clases públicas derivadas de System.Attribute
que disponen al menos de un constructor público.
 Las clases de atributos tienen dos tipos de parámetros:
o Parámetros posicionales, que se deben especificar cada vez que se
utiliza el atributo. Los parámetros posicionales se especifican como
argumentos de constructor para la clase de atributo. En el ejemplo
anterior, url es un parámetro posicional.
o Parámetros con nombre, los cuales son opcionales. Si se especifican
al usar el atributo, debe utilizarse el nombre del parámetro. Los
parámetros con nombre se definen mediante un campo o una
propiedad no estáticos. En el ejemplo anterior, Topic es un
parámetro con nombre.
 Los parámetros de un atributo sólo pueden ser valores constantes de los
siguientes tipos:
o Tipos simples (bool, byte, char, short, int, long, float y
double)
o string
o System.Type
o enumeraciones
o object (El argumento para un parámetro de atributo del tipo object
debe ser un valor constante de uno de los tipos anteriores.)
o Matrices unidimensionales de cualquiera de los tipos anteriores

Parámetros para el atributo AttributeUsage

El atributo AttributeUsage proporciona el mecanismo subyacente mediante el cual los


atributos se declaran.

AttributeUsage tiene un parámetro posicional:

 AllowOn, que especifica los elementos de programa a los que se puede


asignar el atributo (clase, método, propiedad, parámetro, etc.). Los valores
aceptados para este parámetro se pueden encontrar en la enumeración
System.Attributes.AttributeTargets de .NET Framework. El valor
predeterminado para este parámetro es el de todos los elementos del
programa (AttributeElements.All).

AttributeUsage tiene un parámetro con nombre:

 AllowMultiple, valor booleano que indica si se pueden especificar varios


atributos para un elemento de programa. El valor predeterminado para este
parámetro es False.
Utilizar una clase atributo

A continuación, se muestra un breve ejemplo de uso del atributo declarado en la sección


anterior:

[Help("http://decsai.ugr.es/Clase1.htm")]
class Clase1
{
/* Bla, bla, bla... */
}

[Help("http://decsai.ugr.es/Clase2.htm",
Topic="Atributos")]
class Clase2
{
/* Bla, bla, bla... */
}

En este ejemplo, el atributo HelpAttribute está asociado con las clases Clase1 y Clase2.

Nota: Por convención, todos los nombres de atributo finalizan con la palabra "Attribute"
para distinguirlos de otros elementos de .NET Framework. No obstante, no tiene que
especificar el sufijo de atributo cuando utiliza atributos en el código (véase el ejemplo).

Acceder a los atributos por reflexión

Los atributos de un tipo o de un miembro de un tipo pueden ser examinados en tiempo de


ejecución (reflexión), heredan de la clase System.Attribute y sus argumentos se
comprueban en tiempo de compilación.

Los principales métodos de reflexión para consultar atributos se encuentran en la clase


System.Reflection.MemberInfo. El método clave es GetCustomAttributes, que
devuelve un vector de objetos que son equivalentes, en tiempo de ejecución, alos atributos
del código fuente.

Ejemplo 1

El siguiente ejemplo muestra la manera básica de utilizar la reflexión para obtener acceso a
los atributos:

class AtributosSimpleApp
{
static void Main(string[] args)
{
MemberInfo info1 = typeof(Clase1);
object[] attributes1 =
info1.GetCustomAttributes(true);
for (int i = 0; i < attributes1.Length; i ++) {
System.Console.WriteLine(attributes1[i]);
}
MemberInfo info2 = typeof(Clase2);
object[] attributes2 =
info2.GetCustomAttributes(true);
for (int i = 0; i < attributes2.Length; i ++) {
System.Console.WriteLine(attributes2[i]);
}
Console.ReadLine();

} // Main ()
} // class AtributosSimpleApp

Ejemplo 2

Este ejemplo amplía el anterior añadiendo muchas más posibilidades.

Atributos y reflexión

using System;
using System.Diagnostics;
using System.Reflection;

// La clase IsTested es una clase de atributo


// definida por el usuario.
// Puede aplicarse a cualquier definición, incluyendo:
// - tipos (struct, class, enum, delegate)
// - miembros (métodos, campos, events, properties,
indexers)
// Se usa sin argumentos.

[AttributeUsage(AttributeTargets.All)]
public class IsTestedAttribute : Attribute {
public override string ToString() { return ("
REVISADO"); }
}

// La clase HelpAttribute posee un parámetro


posicional (url)
// de tipo string y un parámetro con nombre -opcional-
(Topic)
// de tipo string.

[AttributeUsage(AttributeTargets.All)]
public class HelpAttribute: Attribute {
public string Topic = null;
private string url;

public HelpAttribute(string url) {


this.url = url;
}
public string Url {
get { return url; }
}
public override string ToString()
{
string s1 = " Url = " + this.Url;
string dev = (this.Topic != null) ?
(s1 + ". Topic = " + this.Topic) : s1;
return (dev);
}

// La clase CodeReviewAttribute es una clase de


atributo
// definida por el usuario.
// Puede aplicarse en clases y structs únicamente.
// Toma dos argumentos de tipo string (el nombre del
// revisor y la fecha de revisión) además de permitir
otro
// argumento opcional (Comment) de tipo string.

[AttributeUsage(AttributeTargets.Class |
AttributeTargets.Struct)]
public class CodeReviewAttribute: System.Attribute
{
public CodeReviewAttribute(string reviewer, string
date) {
this.reviewer = reviewer;
this.date = date;
this.comment = "";
}

public string Comment {


get { return(comment); }
set { comment = value; }
}

public string Date {


get { return(date); }
}

public string Reviewer {


get { return(reviewer); }
}
public override string ToString()
{
string st1 = " Revisor : " + Reviewer +
"\n";
string st2 = " Fecha: " + Date;
string st3;
if (Comment.Length != 0)
st3 = "\n" + " NOTAS: " + Comment;
else st3 = "";
return (st1 + st2 + st3);
}

string reviewer;
string date;
string comment;
}

[CodeReview("Pepe", "01-12-2002", Comment="Codigo


mejorable")]
[Help("http://decsai.ugr.es/Clase1.htm")]
[IsTested]
class Clase1
{
int c1c1;
int c2c1;

public Clase1 (int n1, int n2)


{
this.c1c1 = n1;
this.c2c1 = n2;
}

[IsTested]
public override string ToString()
{
return (this.c1c1.ToString() +
this.c2c1.ToString());
}
}

[CodeReview("Juani", "12-11-2002",
Comment="Excelente")]
[Help("http://decsai.ugr.es/Clase3.htm",
Topic="Atributos")]
class Clase2
{
string c1c2;

public Clase2 (string s) {


this.c1c2 = s;
}

[IsTested]
public char Met1Clase2 ()
{
return (this.c1c2[0]);
}
}

[CodeReview("Pepe", "12-11-2002"), IsTested()]


class Clase3
{
int c1c3;

[IsTested]
public Clase3 (int n1) {
this.c1c3 = n1;
}
}

class Atributos1App
{
private static bool IsMemberTested (MemberInfo
member)
{
foreach (object attr in
member.GetCustomAttributes(true))
if (attr is IsTestedAttribute) return true;
return false;
}

private static string InfoRevision (MemberInfo


member)
{
if (IsMemberTested(member)) return ("REVISADO");
else return ("NO REVISADO");
}

private static void DumpAttributes(MemberInfo


member)
{
Console.WriteLine();
Console.WriteLine("Información de: " +
member.Name);
/*
object[] arr =

member.GetCustomAttributes(typeof(HelpAttribute),
true);
if (arr.Length == 0)
Console.WriteLine("Esta clase no tiene
ayuda.");
else {
HelpAttribute ha = (HelpAttribute) arr[0];
Console.WriteLine (ha.ToString());
}
*/
foreach (object attribute in
member.GetCustomAttributes(true))
{
if (attribute is HelpAttribute)
Console.WriteLine (" Atributos de
ayuda:");
if (attribute is CodeReviewAttribute)
Console.WriteLine (" Atributos de
Revisión:");
if (attribute is IsTestedAttribute)
Console.WriteLine (" Atributos de
Actualización:");

Console.WriteLine(attribute);
}
}

static void Main(string[] args)


{
// t es un vector de tipos
Type [] t = new Type[3];

t[0] = typeof(Clase1);
t[1] = typeof(Clase2);
t[2] = typeof(Clase3);

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

DumpAttributes(t[i]);

Console.WriteLine (" Información de los


métodos:");

foreach (MethodInfo m in (t[i]).GetMethods())


{
if (IsMemberTested(m)) {
Console.WriteLine(" Método {0}
REVISADO",
m.Name);
}
else {
Console.WriteLine(" Método {0} NO
REVISADO",
m.Name);
}
}

}
Console.ReadLine();
}
}

Programas en C#
Organización lógica de los tipos
Organización física de los tipos
Ejecución de aplicaciones
Código no seguro
Preprocesador
Documentación

El compilador de C# se ocupa de abstraer al programador de la localización (ficheros) de


los tipos y otros elementos. Es irrelevante el hecho de colocar el código en un único fichero
o de disponerlo en varios, así como de usar una clase antes de declararla: el compilador se
encargará de encontrar la localización de los elementos. La consecuencia es que no existe,
propiamente, el concepto de enlace (linking): el compilador compila el código a un
ensamblado (o simplemente a un módulo).

Organización lógica de los tipos


Del mismo modo que los ficheros se organizan en directorios, los tipos de datos se
organizan en espacios de nombres (del inglés, namespaces).

Los espacios de nombres son mecanismos para controlar la visibilidad (ámbito) de los
nombres empleados e un programa. Su propósito es el de facilitar la combinación de los
componentes de un programa (que pueden provenir de varias fuentes) minimizando los
conflictos entre identificadores.

 Por un lado estos espacios permiten tener más organizados los tipos de
datos, lo que facilita su localización. Así es como está organizada la BCL:
todas las clases más comúnmente usadas en cualquier aplicación pertenecen
al espacio de nombres llamado System, las de acceso a bases de datos en
System.Data, las de realización de operaciones de entrada/salida en
System.IO, etc.
 Por otro lado, los espacios de nombres también permiten poder usar en un
mismo programa clases homónimas, siempre que pertenezcan a espacios de
nombres diferentes y queden perfectamente cualificadas.

En definitiva: Los espacios de nombres proporcionan una forma unívoca de identificar


un tipo. Eliminan cualquier tipo de ambigüedad en los nombres de los símbolos empleados
en un programa.

El siguiente ejemplo trabaja con un espacio de nombres que incluye la declaración de un


clase:

namespace Graficos2D
{
public class Point { ... }
}
Los componentes del espacio de nombres no son visibles directamente desde fuera del
espacio en el que están inmersos (al igual que los componentes de un struct, de una
enumeración, de una clase, ...) a no ser que se cualifiquen completamente:

namespace Graficos2D
{
public class Point { ... }
}

class MainClass
{
Graficos2D.Point p;

static void Main(string[] args)


{
...
}
}

La declaración

Point p;
produciría un error de compilación. En el siguiente ejemplo se trabaja con dos espacios de
nombres que incluyen una clase homónima (Point) en cada uno de ellos. Observe el uso
correcto de cada una de las dos clases cuando se cualifican completamente:

using System;

namespace Graficos2D
{
public class Point { ... }
}

namespace Graficos3D
{
public class Point { ... }
}

class MainClass
{
Graficos2D.Point p1;
Graficos3D.Point p2;

static void Main(string[] args)


{
...
}
}

No existe relación entre espacios de nombres y ficheros (a diferencia de Java).


Los espacios de nombres se pueden anidar. Observe en el siguiente ejemplo cómo la
cualificación evita cualquier duda acerca de la clase de los objetos:

namespace N1
{
public class C1
{
public class C2 {}
}
namespace N2
{
public class C2 {}
}
}

class MainClass
{
N1.C1 o1;
N1.C1.C2 o2;
N1.N2.C2 o3;

static void Main(string[] args)


{
...
}
}

La directiva using

La directiva using se utiliza para permitir el uso de tipos en un espacio de nombres, de


modo que no sea necesario especificar el uso de un tipo en ese espacio de nombres
(directiva using).

using System;

namespace N1
{
public class C1
{
public class C2 {}
}
namespace N2
{
public class C2 {}
}
}

namespace DemoNamespace
{
using N1;
class MainClass
{
C1 o1; // N1 es implícito (N1.C1)
N1.C1 o1_bis; // Tipo totalmente cualificado

//C2 c; // ¡Error! C2 no está


definida en N1
C1.C2 o2; // Idem a: N1.C1.C2 o2;
N1.N2.C2 o3; // Tipo totalmente
cualificado
N2.C2 o3_bis; // Idem a: N1.N2.C2 o3_bis;

static void Main(string[] args)


{
...
}
}
}

La directiva using también puede usarse para crear un alias para un espacio de nombres
(alias using).

using Cl2 = N1.N2.C2;


using NS1 = N1.N2;

Cl2 ob1; // O sea, N1.N2.C2


NS1.C2 ob2; // O sea, N1.N2.C2

Observe cómo, en ocasiones, el uso de alias simplifica y clarifica el código:

Uso de alias

using System;

namespace Graficos2D
{
public class Point { }
}

namespace Graficos3D
{
public class Point { }
}

namespace DemoNamespace
{
using Point2D = Graficos2D.Point;
using Point3D = Graficos3D.Point;

class MainClass
{
Point2D p1;
Point3D p2;

static void Main(string[] args)


{

}
}
}

Cualquier declaración -propia- que use un nombre empelado en un espacio de nombres


oscurece la declaración del espacio de nombres en el bloque en el que ha sido declarada.

using System;

namespace Graficos2D
{
public class Point { }
public class Almacen {}
}

namespace DemoNamespace
{
using Graficos2D;

class MainClass
{
Point p1;
int[] Almacen = new int[20];

static void Main(string[] args)


{

}
}
}

Organización física de los tipos


Los tipos se definen en ficheros

 Un fichero puede contener múltiples tipos.


 Cada tipo está definido en un único fichero (declaración y definición
coinciden).
 No existen dependencias de orden entre los tipos.

Los ficheros se compilan en módulos.

En .NET existen dos tipos de módulos de código compilado:

 Ejecutables (extensión .exe)


 Bibliotecas de enlace dinámico (extensión .dll)

Ambos son ficheros que contienen definiciones de tipos de datos. Se diferencian en que
sólo los primeros (.exe) disponen de un método especial que sirve de punto de entrada a
partir del que es posible ejecutar el código que contienen haciendo una llamada desde la
línea de órdenes del sistema operativo.

Los módulos se agrupan en ensamblados o assemblies:

Un ensamblado consiste en un bloque constructivo reutilizable, versionable y


autodescriptivo de una aplicación de tipo Common Language Runtime. Los ensamblados
proporcionan la infraestructura que permite al motor de tiempo de ejecución comprender
completamente el contenido de una aplicación y hacer cumplir las reglas del control de
versiones y de dependencia definidas por la aplicación. Estos conceptos son cruciales para
resolver el problema del control de versiones y para simplificar la implementación de
aplicaciones en tiempo de ejecución.

Un ensamblado es una agrupación lógica de uno o más módulos o ficheros de recursos


(ficheros .GIF, .HTML, etc.) que se engloban bajo un nombre común. Un programa puede
acceder a información o código almacenados en un ensamblado sin tener que conocer cuál
es el fichero en concreto donde se encuentran, por lo que los ensamblados nos permiten
abstraernos de la ubicación física del código que ejecutemos o de los recursos que usemos.

Hay dos tipos de ensamblados: ensamblados privados y ensamblados compartidos.


También para evitar problemas, se pueden mantener múltiples versiones de un mismo
ensamblado. Así, si una aplicación fue compilada usando una cierta versión de un
determinado ensamblado compartido, cuando se ejecute sólo podrá hacer uso de esa versión
del ensamblado y no de alguna otra más moderna que se hubiese instalado. De esta forma
se soluciona el problema del infierno de las DLL.

Referencias

En Visual Studio se utilizan referencias para identificar assemblies particulares, p.ej.


compilador de C#
csc HelloWorld.cs /reference:System.WinForms.dll

Los espacios de nombres son una construcción del lenguaje para abreviar nombres,
mientras que las referencias son las que especifican qué assembly utilizar.

Un ejercicio

Sobre un proyecto nuevo, trabajaremos con dos ficheros de código: uno contendrá el
método Main() y el otro un espacio de nombres con una clase que se empelará en Main().

 1. Añadir al proyecto un módulo de código (clase). Escribir una clase "útil"


e insertarla en un espacio de nombres. Supongamos se llama MiClase.cs
 2. Compilar el espacio de nombres y obtener una DLL:

csc /t:library MiClase.cs

 3. Escribir en Main() código que use la clase. Supongamos que el fichero se


llama Ppal.cs
 4. Compilar el módulo principal con el espacio de nombres y crear un
ejecutable:

csc /r:MiClase.dll Ppal

Observar el uso de una referencia en la llamada al compilador.

Ejecución de aplicaciones
Gestión de memoria

C# utiliza un recolector de basura para gestionar automáticamente la memoria, lo que


elimina quebraderos de cabeza y una de las fuentes más comunes de error pero conlleva
una finalización no determinísitica (no se ofrece ninguna garantía respecto a cuándo se
llama a un destructor, ni siquiera podemos afirmar que llegue a llamarse el destructor).

Los objetos que deban eliminarse tras ser utilizados deberían implementar la interfaz
System.IDisposable (escribiendo en el método Dispose todo aquello que haya de
realizarse para liberar un objeto). El método Dispose siempre se invoca al terminar una
sentencia using:

public class MyResource : IDisposable


{
public void MyResource()
{
// Acquire valuble resource
}
public void Dispose()
{
// Release valuble resource
}

public void DoSomething()


{
...
}

using (MyResource r = new MyResource())


{
r.DoSomething();
} // se llama a r.Dispose()

Código no seguro
En ocasiones necesitamos tener un control total sobre la ejecución de nuestro código
(cuestiones de rendimiento, compatibilidad con código existente, uso de DLLs...), por lo
que C# nos da la posibilidad de marcar fragmentos de código como código no seguro
(unsafe) y así poder emplear C/C++ de forma nativa: punteros, aritmética de punteros,
operadores -> y *, ... sin recolección de basura. La instrucción stackalloc reserva
memoria en la pila de manera similar a como malloc lo hace en C o new en C++.

public unsafe void MiMetodo () // Método no seguro


{ ... }

unsafe class MiClase // Clase (struct) no segura


{ ... } // Todos los miembros son no
seguros

struct MiStruct
{
private unsafe int * pX; // Campo de tipo puntero
no seguro
...
}

unsafe
{
// Instrucciones que usan punteros
}
En caso de que la compilación se vaya a realizar a través de Visual Studio .NET, la forma
de indicar que se desea compilar código inseguro es activando la casilla Proyecto |
Propiedades de (proyecto) | Propiedades de Configuración | Generar | Permitir bloques
de código no seguro | True.

class StackallocApp
{
public unsafe static void Main()
{
const int TAM = 10;
int * pt = stackalloc int[TAM];

for (int i=0; i<TAM; i++) pt[i] = i;

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


System.Console.WriteLine(pt[i]);

Console.ReadLine ();
}
}

Para asegurarnos de que el recolector de basura no mueve nuestros datos tendremos que
utilizar la sentencia fixed. El recolector puede mover los datos de tipo referencia, por lo
que si un puntero contiene la dirección de un dato de tipo referencia podría apuntar a una
dirección incorrecta después de que el recolector de basura trabajara. Si un conjunto de
instrucciones se encierra en un bloque fixed se previene al recolector de basura para que
no mueva el objeto al que se referencia mientras dura el bloque fixed.

Esta capacidad tiene su coste: al emplear punteros, el código resultante es inseguro ya que
éste no se puede verificar. De modo que tendremos que extremar las precauciones si alguna
vez tenemos que usar esta capacidad del lenguaje C#.

Preprocesador
C# proporciona una serie de directivas de preprocesamiento con distintas funciones.
Aunque se le sigue llamando preprocesador (como en C o C++), el preprocesador no es
independiente del compilador y se han eliminado algunas directivas como #include (para
mejorar los tiempos de compilación, se utiliza el esquema de lenguajes como Java o Delphi
en lugar de los ficheros de cabecera típicos de C/C++) o las macros de #define (para
mejorar la claridad del código).

Directiva Descripción
#define, #undef
Definición de símbolos para la
compilación condicional.
#if, #elif, #else,
#endif Compilación condicional.
#error, #warning Emisión de errores y avisos.
#region, #end Delimitación de regiones.
#line Especificación de números de línea.

Aserciones

Las aserciones nos permiten mejorar la calidad de nuestro código. Esencialmente, las
aserciones no son más que pruebas de unidad que están incluidas en el propio código
fuente. Las aserciones nos permiten comprobar precondiciones, postcondiciones e
invariantes. Las aserciones sólo se habilitan cuando se compila el código para depurarlo, de
forma que su correcto funcionamiento se compruebe continuamente. Cuando distribuyamos
nuestras aplicaciones, las aserciones se eliminan para no empeorar la eficiencia de nuestro
código.

El método Assert() comprueba una condición y muestra un mensaje si ésta es falsa. Puede
emplearse cualquiera de estas versiones:

 public static void Assert(bool) comprueba una condición y envía la


pila de llamadas si ésta es falsa.
 public static void Assert(bool, string) Comprueba una condición
y muestra un mensaje si ésta es falsa.
 ublic static void Assert(bool, string, string Comprueba una
condición y muestra ambos mensajes si es false.

Compilación condicional: Aserciones

public class Debug


{
public static void Assert(bool cond, String s)
{
if (!cond) {
throw new AssertionException(s);
}
}
void DoSomething()
{
...
Assert((x == y), "X debería ser igual a Y");
...
}
}

Documentación
A los programadores no les suele gustar documentar código, por lo que resulta conveniente
suministrar un mecanismo sencillo que les permita mantener su documentación actualizada.
Al estilo de doxygen o Javadoc, el compilador de C# es capaz de generarla
automáticamente a partir de los comentarios que el progamador escriba en los ficheros de
código fuente. Los comentarios a partir de los cuales se genera la documentación se
escriben en XML.

El hecho de que la documentación se genere a partir de los fuentes permite evitar que se
tenga que trabajar con dos tipos de documentos por separado (fuentes y documentación)
que deban actualizarse simultáneamente para evitar incosistencias entre ellos derivadas de
que evolucionen de manera separada ya sea por pereza o por error.

El compilador genera la documentación en XML con la idea de que sea fácilmente legible
para cualquier aplicación. Para facilitar su legibilidad a humanos bastaría añaderle una hoja
de estilo XSL o usar alguna aplicación específica encargada de leerla y mostrarla de una
forma más cómoda para humanos.

Los comentarios XML se denotan con una barra triple (///) y nos permiten generar la
documentación del código cuando compilamos con la opción /doc.

csc programa.cs /doc:docum_programa.xml

El formato de los comentarios viene definido en un esquema XML, si bien podemos añadir
nuestras propias etiquetas para personalizar la documentación de nuestras aplicaciones.
Algunas de las etiquetas predefinidas se verifican cuando generamos la documentación
(parámetros, excepciones, tipos...).

Estos comentarios han preceder las definiciones de los elementos a documentar. Estos
elementos sólo pueden ser definiciones de miembros, ya sean tipos de datos (que son
miembros de espacios de nombres) o miembros de tipos datos, y han de colocarse incluso
incluso antes que sus atributos.

Etiqueta XML Descripción


<summary> Descripción breve de tipos y miembros.
<remarks> Descripción detallada de tipos y miembros.
<para> Delimita párrafos.
<example> Ejemplo de uso.
<see>
<seealso> Referencias cruzadas. Usa el atributo cref
<c> <code> Código de ejemplo (verbatim).
<param> Parámetros de métodos. Usa el atributo name.
<paramref>
Referencia a parámetros de metodos. Usa el
atributo name.
<returns> Valor devuelto por el método.
<exception> Descripciçon de Excepciones.
<value> Descripción de propiedades.
<list> Generar listas. Usa el atriibuto (opcional) type.
<item>
Generar listas. Usa el atriibuto (opcional) type
(puede ser: bullet, number o table).
<permission> Permisos.

Veamos un ejemplo detallado:

using System;

namespace Geometria {

/// <summary>
/// Clase Punto.
/// </summary>
/// <remarks>
/// Caracteriza a los puntos de un espacio
bidimensional.
/// Tiene múltiples aplicaciones....
/// </remarks>

class Punto {

/// <summary>
/// Campo que contiene la coordenada X de un punto
/// </summary>
/// <remarks>
/// Es de solo lectura
/// </remarks>

public readonly uint X;

/// <summary>
/// Campo que contiene la coordenada Y de un punto
/// </summary>
/// <remarks>
/// Es de solo lectura
/// </remarks>
public readonly uint Y;

/// <summary>
/// Constructor de la clase
/// </summary>
/// <param name="x">Coordenada x</param>
/// <param name="y">Coordenada y</param>

public Punto(uint x, uint y) {


this.X=x;
this.Y=y;
}

} // fin de class Punto

/// <summary>
/// Clase Cuadrado. Los objetos de esta clase son
polígonos
/// cerrados de cuatro lados de igual longitud y que
/// forman ángulos rectos.
/// </summary>
/// <remarks>
/// Los cuatro vértices pueden numerarse de manera
que
/// el vértice 1 es el que tiene los menores
valores de
/// las coordenadas X e Y.
///
/// Los demás vértices se numeran a partir de éste
recorriendo
/// el cuadrado en sentido antihorario.
/// </remarks>

class Cuadrado {

/// <summary>
/// Campo que contiene las coordenadas del vértice
1.
/// </summary>
///
protected Punto vertice1;

/// <summary>
/// Campo que contiene la longitud del lado.
/// </summary>
protected uint lado;

/// <summary>
/// Constructor de la clase.
/// Construye un cuadrado a partir del vértice 1 y
de
/// la longitud del lado.
/// </summary>
/// <param name="vert1">Coordenada del vértice
1</param>
/// <param name="lado">Longitud del lado</param>
///
public Cuadrado(Punto vert1, uint lado) {
this.vertice1=vert1;
this.lado=lado;
}

/// <summary>
/// Constructor de la clase.
/// Construye un cuadrado a partir de los vértices
1 y 3.
/// </summary>
/// <param name="vert1">Coordenada del vértice
1</param>
/// <param name="vert3">Coordenada del vértice
3</param>
/// <remarks>
/// Habría que comprobar si las componentes del
vértice 3
/// son mayores o menores que las del vértice1.
/// Vamos a presuponer que las componentes del
vértice 3 son
/// siempre mayores que las del uno.
/// </remarks>
public Cuadrado(Punto vert1, Punto vert3) {
this.vertice1=vert1;
this.lado=(uint) Math.Abs(vert3.X-vert1.X);
}

/// <summary>
/// Propiedad que devuelve el punto que representa
a
/// las coordenadas del vértice 1.
/// </summary>
public Punto Vertice1 {
get {
return this.vertice1;
}
}

/// <summary>
/// Propiedad que devuelve el punto que representa
a
/// las coordenadas del vértice 2.
/// </summary>
public Punto Vertice2 {
get {
Punto p=new Punto(this.vertice1.X +
this.lado,this.vertice1.Y);
return p;
}
}

......

} // Fin de class Cuadrado


} // Fin de namespace Geometria

namespace PruebaGeometria {
using Geometria;
class GeometriaApp {
....

Para generar la documentación en Visual Studio .NET seleccionaremos el proyecto en el


explorador de soluciones y daremos el nombre del fichero XML que contendrá la
documentación: Ver | Páginas de propiedades | Propiedades de configuración |
Generar | Archivo de documentación XML y darle el nombre: DocumentacionGeometia,
por ejemplo.

Para ver el resultado: Herramientas | Generar páginas Web de comentarios. Unos


ejemplos:

También podría gustarte