Logo Passei Direto

Guia_practica_del_desarrollo_aplicaciones_windows_en_net_free

User badge image
Alex Bustamante

en

Herramientas de estudio

Material
¡Estudia con miles de materiales!

Vista previa del material en texto

Guía práctica del 
desarrollo de 
aplicaciones 
Windows en .NET
Alberto Población, Luis Alfonso Rey, 
Jorge Cangas & José Vicente Sánchez 
© Danysoft 
Libro para José Mora 
DERECHOS RESERVADOS 
El contenido de esta publicación tiene todos los derechos reservados, por lo que no 
se puede reproducir, transcribir, transmitir, almacenar en un sistema de 
recuperación o traducir a otro idioma de ninguna forma o por ningún medio 
mecánico, manual, electrónico, magnético, químico, óptico, o de otro modo. La 
persecución de una reproducción no autorizada tiene como consecuencia la cárcel 
y/o multas. 
LIMITACIÓN DE LA RESPONSABILIDAD 
Tanto el autor como en Danysoft hemos revisado el texto para evitar cualquier tipo 
de error, pero no podemos prometerle que el libro esté siempre libre de errores. Por 
ello le rogamos nos remita por e-mail sus comentarios sobre el libro en 
attcliente@danysoft.com 
DESCUENTOS ESPECIALES 
Recuerde que Danysoft ofrece descuentos especiales a centros de formación y en 
adquisiciones por volumen. Para más detalles, consulte con Danysoft. 
MARCAS REGISTRADAS 
Todos los productos y marcas se mencionan únicamente con fines de identificación y 
están registrados por sus respectivas compañías. 
Autores: Alberto Población, José Vicente Sánchez, Jorge Cangas, y Luis Alfonso Rey 
Publicado por Danysoft 
Avda. de la Industria, 4 Edif. 1 3º 
28108 Alcobendas, Madrid. España. 
902 123146 | www.danysoft.com 
ISBN: 978-84-939910-3-6 
Depósito Legal: M-15718-2012 
Libro para José Mora 
Si no es ese usuario rogamos nos lo comente a attcliente @ danysoft.com 
IMPRESO EN ESPAÑA 
© Danysoft | Madrid, 2012 
 
 
 Tabla De 
Contenidos 
TABLA DE CONTENIDOS ................................................................................................ 3 
APARTADO I: PLATAFORMA .NET Y LENGUAJE C# POR ALBERTO 
POBLACIÓN .......................................................................................................... 11 
PRÓLOGO.................................................................................................................... 13 
INTRODUCCIÓN ........................................................................................................... 15 
A CONTINUACIÓN ............................................................................................................... 16 
EL FRAMEWORK .......................................................................................................... 17 
SERVICIOS DEL CLR ............................................................................................................. 18 
LIBRERÍAS DEL FRAMEWORK ................................................................................................. 19 
Espacios de nombres ................................................................................................. 21 
ALGUNAS CLASES DE USO FRECUENTE...................................................................................... 23 
Interfaces de usuario ................................................................................................. 23 
Aplicaciones de escritorio .......................................................................................... 23 
Aplicaciones para Internet ........................................................................................ 24 
Aplicaciones de consola ............................................................................................ 25 
Servicios Windows..................................................................................................... 25 
Colecciones ................................................................................................................ 26 
Entradas/Salidas ....................................................................................................... 26 
Acceso a datos .......................................................................................................... 27 
Globalización ............................................................................................................. 28 
Manipulación de texto .............................................................................................. 28 
Multihilo .................................................................................................................... 29 
4 | Windows Forms 
Libro para José Mora 
Reflexión ................................................................................................................... 30 
A CONTINUACIÓN ............................................................................................................... 30 
EL LENGUAJE C# .......................................................................................................... 33 
NUESTRO PRIMER PROGRAMA ............................................................................................... 34 
EXAMINANDO EL PROGRAMA EJEMPLO .................................................................................... 38 
La directiva using ...................................................................................................... 38 
La declaración del namespace .................................................................................. 39 
La clase ...................................................................................................................... 39 
El método Main ......................................................................................................... 40 
La sentencia de salida ............................................................................................... 40 
A CONTINUACIÓN ............................................................................................................... 41 
ELEMENTOS SINTÁCTICOS BÁSICOS ............................................................................. 42 
COMENTARIOS ................................................................................................................... 43 
Comentarios XML ...................................................................................................... 43 
SENTENCIAS ....................................................................................................................... 45 
Bloques de sentencias ............................................................................................... 46 
SENTENCIAS DE CONTROL DE FLUJO ........................................................................................ 47 
If ................................................................................................................................ 47 
Switch ........................................................................................................................ 48 
While ......................................................................................................................... 50 
Do .............................................................................................................................. 50 
For ............................................................................................................................. 51 
Foreach ..................................................................................................................... 52 
Goto .......................................................................................................................... 52 
Break ......................................................................................................................... 53 
Continue .................................................................................................................... 53 
EXCEPCIONES ..................................................................................................................... 53 
La instrucción throw .................................................................................................. 54 
Las instrucciones try...catch...finally......................................................................... 55 
Desbordamientos aritméticos ................................................................................... 56 
OPERADORES ..................................................................................................................... 57 
Prioridad de los operadores ...................................................................................... 59 
Los operadores is y as ............................................................................................... 59 
A CONTINUACIÓN ............................................................................................................... 60 
SISTEMA DE TIPOS Y DECLARACIONES DE VARIABLES .................................................. 62 
EL SISTEMA COMÚN DE TIPOS ................................................................................................ 63 
Tipos Valor ................................................................................................................ 64 
Tipos Referencia ........................................................................................................ 64 
TIPOS SIMPLES ................................................................................................................... 64 
NOMBRES DE VARIABLES ...................................................................................................... 66 
Reglas ........................................................................................................................ 66 
Sugerencias de buen estilo ........................................................................................ 66 
Windows Forms | 5 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
VARIABLES LOCALES ............................................................................................................ 67 
VARIABLES MIEMBRO DE CLASS O STRUCT ................................................................................ 67 
CONSTANTES Y VARIABLES DE SÓLO‐LECTURA ........................................................................... 68 
Constantes de tipo carácter y cadena ....................................................................... 68 
Constantes de tipo numérico .................................................................................... 69 
MODIFICADORES DE ALCANCE ............................................................................................... 69 
ENUMERACIONES ............................................................................................................... 70 
CASTING ........................................................................................................................... 71 
STRUCTS ........................................................................................................................... 72 
ARREGLOS ......................................................................................................................... 73 
Declaración ............................................................................................................... 73 
Acceso a los elementos ............................................................................................. 74 
Propiedades y métodos de los arreglos .................................................................... 74 
Algunas consideraciones sobre los arreglos .............................................................. 75 
LOS TIPOS VAR ................................................................................................................... 76 
LOS TIPOS DYNAMIC ............................................................................................................ 78 
LOS TIPOS NULLABLE ........................................................................................................... 80 
A CONTINUACIÓN ............................................................................................................... 80 
MÉTODOS ................................................................................................................... 82 
DECLARACIÓN .................................................................................................................... 82 
LLAMADA A LOS MÉTODOS ................................................................................................... 83 
SOBRECARGAS.................................................................................................................... 84 
PARÁMETROS OPCIONALES ................................................................................................... 85 
PARÁMETROS CON NOMBRE ................................................................................................. 86 
PARÁMETROS DE ENTRADA Y SALIDA ....................................................................................... 86 
NÚMERO VARIABLE DE ARGUMENTOS ..................................................................................... 88 
A CONTINUACIÓN ............................................................................................................... 89 
PROPIEDADES ............................................................................................................. 91 
DECLARACIÓN .................................................................................................................... 92 
INVOCACIÓN ...................................................................................................................... 93 
COMPARATIVA ................................................................................................................... 93 
PROPIEDADES AUTOMÁTICAS ................................................................................................ 93 
INDEXADORES .................................................................................................................... 94 
A CONTINUACIÓN ............................................................................................................... 95 
DELEGADOS Y EVENTOS .............................................................................................. 96 
DELEGADOS ....................................................................................................................... 97 
Declaración de un tipo de delegado .......................................................................... 97 
Declaración de una instancia  de un delegado ......................................................... 98 
Llamada a un método  a través de un delegado ....................................................... 99 
Delegados anónimos ............................................................................................... 100 
EVENTOS ......................................................................................................................... 101 
Declaración ............................................................................................................. 102 
6 | Windows Forms 
Libro para José Mora 
Suscripción .............................................................................................................. 102 
Disparo .................................................................................................................... 103 
Patrón convencional ............................................................................................... 104 
Accesores para los eventos ..................................................................................... 105 
A CONTINUACIÓN ............................................................................................................. 106 
ORIENTACIÓN A OBJETOS .......................................................................................... 107 
DATOS ESTÁTICOS ............................................................................................................. 108 
CREACIÓN DE INSTANCIAS ...................................................................................................109 
Constructores .......................................................................................................... 110 
Constructores estáticos ........................................................................................... 112 
Destructores ............................................................................................................ 113 
La sentencia using ................................................................................................... 114 
HERENCIA DE CLASES ......................................................................................................... 116 
Clases selladas ........................................................................................................ 117 
SOBRESCRITURA ............................................................................................................... 118 
POLIMORFISMO ................................................................................................................ 120 
CLASES ABSTRACTAS .......................................................................................................... 121 
INTERFACES ..................................................................................................................... 122 
A CONTINUACIÓN ............................................................................................................. 124 
SOBRECARGA DE OPERADORES ................................................................................. 125 
FORMA DE REALIZAR LA SOBRECARGA ................................................................................... 126 
Operadores restringidos.......................................................................................... 127 
OPERADORES DE CONVERSIÓN ............................................................................................. 128 
A CONTINUACIÓN ............................................................................................................. 131 
GENÉRICOS ............................................................................................................... 133 
EL PROBLEMA .................................................................................................................. 133 
Boxing y Unboxing .................................................................................................. 134 
LA SOLUCIÓN ................................................................................................................... 135 
DECLARACIÓN DE RESTRICCIONES ......................................................................................... 137 
A CONTINUACIÓN ............................................................................................................. 139 
EXTENSORES, LAMBDAS Y LINQ ................................................................................. 141 
MÉTODOS DE EXTENSIÓN ................................................................................................... 141 
EXPRESIONES LAMBDA ....................................................................................................... 145 
Árboles de expresiones ............................................................................................ 146 
LINQ ............................................................................................................................. 147 
A CONTINUACIÓN ............................................................................................................. 151 
OTRAS CARACTERÍSTICAS .......................................................................................... 153 
ATRIBUTOS ...................................................................................................................... 153 
CLASES PARCIALES ............................................................................................................ 155 
Métodos parciales ................................................................................................... 156 
Windows Forms | 7 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
INICIALIZADORES DE COLECCIONES ........................................................................................ 158 
ENUMERADORES .............................................................................................................. 159 
COVARIANCIA Y CONTRAVARIANCIA ...................................................................................... 160 
CONCLUSIÓN ................................................................................................................... 163 
APARTADO II: ADO Y LINQ POR JORGE L.CANGAS .......................... 165 
¡CONECTADO! ........................................................................................................... 167 
SQLCONNECTION .............................................................................................................. 171 
DBPROVIDERFACTORY ....................................................................................................... 173 
RECORDATORIO ................................................................................................................ 176 
EL MODO CONECTADO .............................................................................................. 177 
DBCONNECTION ............................................................................................................... 179 
DBCOMMAND ................................................................................................................. 180 
IDATAREADER .................................................................................................................. 182 
IDBDATAPARAMETER ........................................................................................................ 185 
DBCOMMAND “RELOADED” ............................................................................................... 186 
DBTRANSACTION .............................................................................................................. 190 
RECORDATORIO ................................................................................................................ 192 
EL MODO DESCONECTADO ........................................................................................ 195 
DATATABLE ..................................................................................................................... 195 
DATASET ........................................................................................................................ 197 
DATAVIEW ...................................................................................................................... 201 
DATABINDING.................................................................................................................. 202 
TYPEDDATASET ................................................................................................................ 204 
CONCLUSIÓN ................................................................................................................... 209 
LINQ ......................................................................................................................... 211 
CONSULTAS CON LINQ ....................................................................................................... 211 
EXPRESIONES LAMBDA ....................................................................................................... 213 
EVALUACIÓN PEREZOSA...................................................................................................... 214 
OPERADORES DE CONSULTA ................................................................................................ 215 
LINQ PARA DATASET ......................................................................................................... 218 
LINQ PARA XML ...............................................................................................................219 
APARTADO III: WINDOWS FORMS POR JOSÉ VICENTE SÁNCHEZ
 ................................................................................................................................. 221 
INTRODUCCIÓN ......................................................................................................... 223 
VISUAL STUDIO ................................................................................................................ 224 
¿POR QUÉ C#? ................................................................................................................ 225 
PRIMEROS PASOS CON WINDOWS FORMS ................................................................ 227 
UN REPASO A LOS CONTROLES BÁSICOS DE WINDOWS FORMS ................................................... 235 
8 | Windows Forms 
Libro para José Mora 
UN EJEMPLO MÁS COMPLETO: WORDPAD ................................................................ 241 
AGREGANDO ALGO DE CÓDIGO ............................................................................................ 249 
GUARDANDO LOS CAMBIOS DEL ARCHIVO .............................................................................. 255 
AGREGANDO UN MENÚ Y UNA BARRA DE ESTADO .................................................................... 258 
FUENTES DE TEXTO ................................................................................................... 267 
LAS FUENTES .................................................................................................................... 267 
CONCLUSIÓN ................................................................................................................... 277 
USANDO ELEMENTOS DE TERCEROS .......................................................................... 279 
USANDO COM ................................................................................................................ 281 
CONCLUSIÓN ................................................................................................................... 289 
APARTADO IV: WINDOWS PRESENTATION FOUNDATION POR 
LUIS ALFONSO REY ......................................................................................... 291 
PRÓLOGO APARTADO IV ........................................................................................... 293 
EL MODELO DE APLICACIÓN ...................................................................................... 295 
APLICACIONES DE WINDOWS .............................................................................................. 295 
APLICACIONES DE NAVEGACIÓN ........................................................................................... 296 
CONCLUSIÓN ................................................................................................................... 298 
A CONTINUACIÓN ............................................................................................................. 299 
XAML ........................................................................................................................ 301 
XML .............................................................................................................................. 302 
Representación ....................................................................................................... 302 
Sistema de propiedades y eventos .......................................................................... 304 
Controles y sus propiedades más comunes ............................................................. 308 
CONCLUSIÓN ................................................................................................................... 314 
A CONTINUACIÓN ............................................................................................................. 314 
PANELES Y LAYOUT ................................................................................................... 315 
PANELES ......................................................................................................................... 316 
CONCLUSIÓN ................................................................................................................... 319 
A CONTINUACIÓN ............................................................................................................. 319 
DATABINDING Y RECURSOS ....................................................................................... 321 
LOS INTERFACES ............................................................................................................... 321 
LAS EXPRESIONES DE BINDING ............................................................................................. 324 
DATACONTEXT ................................................................................................................. 326 
EL BINDING MÚLTIPLE ........................................................................................................ 327 
CONVERSORES ................................................................................................................. 330 
VALIDACIÓN .................................................................................................................... 333 
INTEGRACIÓN CON VISUAL STUDIO ....................................................................................... 337 
DATAPROVIDERS Y COLLECCIONVIEWSOURCE ........................................................................ 339 
Windows Forms | 9 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
RECURSOS ....................................................................................................................... 343 
CONCLUSIÓN ................................................................................................................... 344 
A CONTINUACIÓN ............................................................................................................. 344 
COMANDOS .............................................................................................................. 345 
REDEFINIR UN COMANDO ................................................................................................... 345 
CREAR UN COMANDO NUEVO .............................................................................................. 347 
COMANDOS EN LOS NUEVOS CONTROLES ............................................................................... 349 
COMANDOS EN 4.0 ........................................................................................................... 351 
A CONTINUACIÓN ............................................................................................................. 352 
A CONTINUACIÓN ............................................................................................................. 352 
ESTILOS Y PLANTILLAS ............................................................................................... 353 
ESTILOS .......................................................................................................................... 354 
PLANTILLAS DE DATOS ........................................................................................................ 357 
PLANTILLAS DE CONTROLES ................................................................................................. 359 
TRIGGERS ........................................................................................................................ 361 
RECURSOS COMPARTIDOS Y TEMAS ...................................................................................... 362 
CONCLUSIÓN ................................................................................................................... 364 
A CONTINUACIÓN ............................................................................................................. 364 
GRÁFICOS Y ANIMACIONES ....................................................................................... 365 
GRAFICOS Y RENDERIZADO.................................................................................................. 365 
RENDERIZADO 3‐D ........................................................................................................... 372 
ANIMACIÓN ..................................................................................................................... 375 
VISUAL STATE MANAGER ................................................................................................... 376 
TRATAMIENTO DE MEDIOS .................................................................................................. 378 
CONCLUSIÓN ................................................................................................................... 379 
A CONTINUACIÓN ............................................................................................................. 379 
DOCUMENTOS .......................................................................................................... 381 
DOCUMENTOS EN WPF ..................................................................................................... 381 
DOCUMENTOS DE FLUJO .................................................................................................... 382 
SERIALIZACIÓN Y ALMACENAJE DE DOCUMENTOS ..................................................................... 384 
ANOTACIONES.................................................................................................................. 386 
CONCLUSIÓN ................................................................................................................... 387 
A CONTINUACIÓN ............................................................................................................. 387 
CONTROLES ............................................................................................................... 389 
CONTROLES DE USUARIO .................................................................................................... 389 
LA JERARQUÍA DE OBJETOS EN WPF ..................................................................................... 390 
PASOS PARA DESARROLLAR UN NUEVO CONTROL ..................................................................... 391 
CONCLUSIÓN ................................................................................................................... 395 
A CONTINUACIÓN ............................................................................................................. 395 
LOCALIZACIÓN E INTER‐OPERABILIDAD ..................................................................... 397 
10 | Windows Forms 
Libro para José Mora 
LOCALIZACIÓN Y GLOBALIZACIÓN ......................................................................................... 398 
LOCALIZANDO UNA APLICACIÓN ........................................................................................... 398 
INTEROPERABILIDAD .......................................................................................................... 402 
CONCLUSIÓN ................................................................................................................... 403 
ÍNDICE .................................................................................................................. 405 
SITIOS WEB RELACIONADOS ..................................................................... 412 
 
 
 
 
 
Apartado I: 
Plataforma .NET 
y Lenguaje C# 
por Alberto Población 
 
 
 
 
 
 
 
 
 
 
Plataforma .NET y Lenguaje C# | 13 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Prólogo 
Este libro trata sobre la plataforma .NET en su versión 4.0, así como el lenguaje C# 
también en su versión 4.0 y Visual Studio 2010. Donde sea oportuno, 
mencionaremos aquellas características que son exclusivas de estas últimas 
versiones, con el fin de que el lector sea consciente de que no se pueden usar con 
otras versiones más antiguas. 
No se trata de un manual básico de programación, sino que presume que el lector ya 
sabe programar en algún otro lenguaje y entorno tradicional, mencionando 
únicamente las peculiaridades de la sintaxis del lenguaje C#, sin detenerse a 
explicar conceptos tales como qué es o para qué sirve un bucle o una variable. 
Igualmente, en lo que se refiere al Framework de .NET, no pretende ser una 
referencia de todas las librerías e infraestructura provistas por dicha plataforma, 
sino una introducción general del tipo “qué es y para qué sirve”. 
 
Espero que el libro te resulte útil, y que obtengas buenos resultados desarrollando 
aplicaciones con Visual Studio bajo la plataforma .NET. 
 
14 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Plataforma .NET y Lenguaje C# | 15 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Introducción 
Cuando hablamos de la plataforma “punto net”, nos referimos a una serie de 
elementos de infraestructura que se usan para desarrollar aplicaciones informáticas 
que funcionan principalmente sobre Microsoft Windows. 
Desde nuestro punto de vista como desarrolladores de software, se trata 
básicamente de un conjunto de librerías que nos facilitan el desarrollo de 
aplicaciones porque aportan una gran cantidad de funcionalidad que en otros 
entornos de programación sería necesario construir a mano o adquirir de terceras 
partes. Por ejemplo, podemos construir una aplicación de escritorio para Windows 
sin preocuparnos por la bomba de mensajes o por la declaración de las interfaces de 
programación (APIs) previstas por el sistema, porque .NET nos proporciona ya una 
serie de clases que encapsulan y abstraen esos detalles. Lo mismo ocurre con las 
aplicaciones para Web o con los servicios para Windows, cuyo desarrollo se ve en 
todos los casos simplificado gracias a las librerías incluidas en el Framework de 
.NET. 
El conjunto de librerías de clases se conoce como Framework Class Libraries 
(FCL). Entre otras cosas, soporta múltiples tipos de interfaces de usuario, acceso a 
datos, criptografía, algoritmos numéricos, protocolos de comunicación, codificación 
y análisis de texto, manejo del sistema de archivos, etc., etc. Además, se soportan 
diversos lenguajes de programación, todos los cuales pueden hacer uso de estas 
librerías además de interactuar entre sí gracias a una librería de tipos comunes. 
Cuando se compila un programa escrito para .NET, se genera lo que se denomina 
“código gestionado”, que rueda bajo un entorno de ejecución llamado Common 
Language Runtime (CLR). El CLR controla, limita y gestiona el funcionamiento del 
programa compilado. Este entorno aporta diversos servicios, tales como la 
seguridad, gestión de memoria y de excepciones, y la interacción con el código no-
gestionado (especialmente las APIs nativas de Windows). El “Framework” de .NET 
está constituido por el conjunto de las FCL más el CLR. 
Además del soporte que nos proporciona el Framework de .NET, otra de las 
ventajas a la hora de desarrollar aplicaciones sobre esta plataforma consiste en la 
disponibilidad de herramientas de desarrollo muy potentes y versátiles. 
16 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Probablemente la herramienta más conocida (aunque no la única) sea Visual 
Studio, que aporta un entorno integrado de desarrollo desde el que se pueden 
editar, compilar, ejecutar y depurar los programas. No obstante, debe quedarnos 
claro que .NET no es Visual Studio sino una infraestructura independiente de dicho 
producto. Es posible desarrollar programas para .NET sin usar en ningún momento 
Visual Studio. 
A continuación 
Veremos seguidamente una breve presentación general del Framework y sus 
librerías, para pasar después a explorar las características del lenguaje C#. 
Plataforma .NET y Lenguaje C# | 17 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
El Framework 
Como ya hemos mencionado, el Framework proporciona infraestructura de soporte 
para compilar y ejecutar aplicacionesbasadas en .NET. 
Las aplicaciones deben apoyarse en un sistema operativo. A lo largo de este texto 
presumiremos que se trata de aplicaciones para Windows, pese a que el lenguaje C# 
en sí mismo es independiente del sistema, y de hecho permite generar aplicaciones 
para otros entornos, tales como MONO, Silverlight, o el Micro Framework para 
sistemas embebidos. 
El diagrama que se acompaña muestra los diversos bloques del Framework apilados 
sobre la infraestructura del sistema operativo. 
 
 
Muchas de las librerías del sistema, tales como Windows Management 
Instrumentation (WMI) o colas de mensajes (MSMQ) se encuentran encapsuladas 
18 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
en las librerías del Framework. De esta manera, nuestros programas de .NET 
pueden utilizarlas de una manera sistemática y uniforme, sin necesidad de conocer 
los entresijos y complicaciones de cada una de estas APIs por separado. 
El Common Language Runtime (CLR), que ya hemos mencionado, proporciona un 
entorno de ejecución que a veces se conoce como “entorno gestionado”. Aporta a las 
aplicaciones una serie de servicios tales como el Garbage Collector, seguridad de 
acceso a código y verificación de código, que contribuyen a la estabilidad y 
simplicidad de despliegue de los programas de .NET. 
El diagrama muestra también varios bloques correspondientes a las librerías de 
clases del Framework (FCL), incluyendo entre otras cosas el acceso a datos o los 
distintos tipos de interfaces de usuario. 
Finalmente, todo ello puede ser controlado desde diversos lenguajes de 
programación, entre los que se encuentra C#, que será el lenguaje estudiado en este 
texto. 
Servicios del CLR 
Estos son algunos de los múltiples servicios aportados por el CLR: 
 Cargador de clases – Gestiona la carga en memoria de las clases y sus
metadatos.
 Compilador de código intermedio a nativo – Los compiladores de .Net
generan archivos ejecutables que en realidad contienen en su interior un
código intermedio que se denomina MSIL (“Microsoft Intermediate
Language”). Al cargarlo en memoria, el CLR traduce el MSIL en código
nativo optimizado para la CPU en la que se esté ejecutando el programa
en ese momento. También se conoce como el compilador JIT (“Just-In-
Time”).
 Gestor de código – Administra la ejecución.
 Recogemigas (“Garbage Collector”) – Gestiona el ciclo de vida de los
objetos, liberándolos cuando ya no se encuentran en uso. Hablaremos
de este tema con más detalle en la sección dedicada a los destructores de
clases, en el capítulo “Orientación a Objetos”.
 Motor de seguridad – Recopila una serie de “pruebas” (evidence)
basándose en el origen del código, y en base a esos datos decide los
Plataforma .NET y Lenguaje C# | 19 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
permisos que tendrá el código. Es la base del mecanismo que se conoce 
como CAS (“Code Access Security”). 
 Motor de depuración – Permite conectar una herramienta de 
depuración (como por ejemplo el propio Visual Studio) con el código en 
ejecución, para seguirlo paso a paso, examinar las variables y depurar la 
aplicación. 
 Verificador de tipos – Al cargar el código, verifica que los tipos (clases, 
estructuras, enumeraciones, etc.) no contienen ninguna instrucción 
MSIL que los pueda volver inseguros, como por ejemplo variables sin 
inicializar o accesos a elementos de un arreglo sin comprobar que el 
índice se encuentra dentro del rango válido. Es posible ejecutar código 
no verificado, pero requiere privilegios elevados. El código MSIL 
generado por C# es siempre verificable a condición de que no se utilice 
la instrucción unsafe. 
 Gestor de excepciones – Se encarga de la gestión estructurada de 
excepciones, que pueden fluir desde código desarrollado en un lenguaje 
a otro. En el caso de C#, la construcción que permite tratar estas 
excepciones es try...catch (explicada más adelante). 
 Soporte de hilos de ejecución – Se dispone de infraestructura para dar 
soporte a los programas que utilizan múltiples hilos de ejecución. 
 Transportador para COM – Permite transportar la ejecución entre 
código gestionado de .NET y código no-gestionado implementado por 
medio de COM (Component Object Model). Puede funcionar en ambas 
direcciones, es decir, un programa de .NET puede realizar una llamada 
a un objeto COM, y un consumidor de objetos COM puede consumir un 
componente programado en .NET siempre que se configure 
adecuadamente para permitir esta interacción. 
Librerías del Framework 
Hay un gran número de librerías incluidas en el Framework. Desde nuestros 
programas podemos hacer referencia a las diferentes DLLs para acceder a las clases 
que contienen. Cuando esto se hace desde Visual Studio, se presenta un cuadro que 
enumera las librerías y que reproducimos aquí para dar una idea del aspecto que 
tienen: 
20 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Por ejemplo, la ventana anterior nos ofrece la opción de utilizar la librería 
System.Core.dll en versión 4.0.0.0. ¿Dónde se encuentra ubicada esta DLL? 
Durante la instalación del Framework, la mayor parte de las librerías de sistema se 
instalan en una zona reservada que se denomina “caché global de ensamblados” 
(Global Assembly Cache – GAC). 
Las librerías que se instalan aquí se encuentran ya precompiladas (a código nativo 
en lugar de MSIL) y con el código verificado, por lo que el proceso de carga es más 
rápido que las que no han pasado por este proceso. Además, las librerías que se 
instalan en el GAC son encontradas automáticamente por todos los programas de 
.NET que se ejecuten en el sistema, por lo que no es necesario hacer nada en 
especial para indicarle al programa la ubicación en la que se encuentran en tiempo 
de ejecución. 
Físicamente, el GAC está formado por una serie de carpetas por debajo de la ruta 
c:\Windows\Assembly. El Explorador de Windows conoce esta peculiaridad, y 
muestra el contenido del GAC en un listado continuo con independencia de las 
carpetas utilizadas internamente. 
Plataforma .NET y Lenguaje C# | 21 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
Es lícito tener instaladas a la vez varias copias de una misma librería siempre que 
tengan distintas versiones. En tiempo de ejecución, cada programa carga 
automáticamente la versión de la librería con la que fue compilado, por lo que 
desaparece el infame “infierno de las DLLs” que teníamos que sufrir con tecnologías 
anteriores (COM) cuando instalábamos simultáneamente más de un programa con 
versiones diferentes de una misma librería, que se “pisaban” unas a otras. 
En realidad, el mecanismo de carga de DLLs es sumamente sofisticado, y se conoce 
como fusion en la documentación original. Por mediación de los archivos de 
configuración se puede alterar este proceso, para lograr (por ejemplo) que un 
programa cargue una versión más reciente de una librería que aquella con la que se 
compiló. 
Espacios de nombres 
Las clases que hay dentro de las librerías vienen clasificadas en lo que se 
denominan espacios de nombres. Por ejemplo, hay una clase llamada 
SqlConnection cuyo nombre completo es en realidad 
22 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
System.Data.SqlClient.SqlConnection. La parte que viene antes del último 
punto (System.Data.SqlClient) es el espacio de nombres de la clase. 
Los espacios de nombres se usan para clasificar y eliminar ambigüedades en los 
nombres de tipos que hay en las librerías de clases. Clasifican los nombres de tipos 
en el sentido de que las diversas clases que tienen funcionalidad similar, o 
relacionada entre sí, se categorizan bajo un mismo espacio de nombres para que 
puedan ser fácilmente encontradas y reconocidas. Eliminan ambigüedades porque 
gracias al espacio de nombres es posible tener varias clases que se llamen igual, 
aunque no tengan nada que ver una con otra. Por ejemplo, hay al menos cuatro 
copias de la clase Timer: 
 System.Windows.Forms.Timer System.Threading.Timer
 System.Timers.Timer
 System.Web.Extensions.Timer
Es lícito que dentro de una DLL se definan clases que pertenezcan a espacios de 
nombres diferentes, y también es lícito que dos DLLs definan clases del mismo 
espacio de nombres. Sin embargo, en la mayor parte de los casos, los espacios de 
nombres y las DLLs coinciden, lo que facilita escoger las referencias que deben 
añadirse al programa. En cualquier caso, la documentación de cada clase anuncia al 
principio cuál es la librería que la contiene. 
En la sección dedicada al lenguaje C# mostraremos cómo emplear la directiva 
using para declarar en un programa fuente los espacios de nombres que se van a 
utilizar, evitando de esa manera tenerlos que escribir cada vez que se haga 
referencia a una de las clases. 
Plataforma .NET y Lenguaje C# | 23 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Algunas clases de uso frecuente 
Dentro de este apartado vamos a mencionar, a título de ejemplo, algunas de las 
librerías que vienen incluidas en el Framework y que se usan con cierta frecuencia, 
dependiendo del tipo de aplicaciones que se desarrollen. 
Interfaces de usuario 
El Framework de .NET permite desarrollar aplicaciones de distintos tipos, desde 
aplicaciones de consola hasta aplicaciones web, pasando por distintas modalidades 
de aplicaciones de escritorio. Estos son los principales tipos de aplicaciones que 
soportan las librerías: 
Aplicaciones de escritorio 
Pueden ser de dos tipos: 
El modelo más clásico se conoce como Windows Forms o abreviadamente 
Winforms. Desde Visual Studio se dibuja la estética de las ventanas que se 
presentarán en pantalla, y esto genera sentencias de código que al ser ejecutadas 
producen el mismo resultado que se preparó en tiempo de diseño. Dentro de las 
ventanas se ubican componentes que son instancias de clases con una serie de 
interfaces predefinidas que les permiten “incrustarse” en el dibujo de la pantalla. 
Para realizar la interacción con el usuario, estos componentes utilizan un modelo de 
eventos, por el que se conectan con ellos una serie rutinas desarrolladas por el 
programador conocidas como “manejadores de eventos”. Estos manejadores se 
añaden a las clases generadas por el diseñador, típicamente en un archivo separado 
que se denomina code behind (“código por detrás”). 
Desde el punto de vista de las librerías del Framework, las clases que representan 
las ventanas en pantalla heredan de la clase System.Windows.Forms.Form. 
Similarmente, los componentes que ubicamos en pantalla se encuentran también 
definidos en ese mismo espacio de nombres, System.Windows.Forms, y las clases 
correspondientes están almacenadas en la librería System.Windows.Forms.dll. 
El segundo tipo de aplicación de escritorio utiliza una tecnología denominada 
Windows Presentation Foundation (WPF). Aunque en teoría es posible crear estas 
aplicaciones con técnicas similares a las de Winforms (mediante sentencias de 
código que dibujen las pantallas), habitualmente siguen un paradigma distinto. Al 
dibujar las ventanas desde el diseñador de Visual Studio, lo que se genera es un 
archivo que contiene XML, siguiendo un esquema que Microsoft denomina XAML 
24 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
(Extensible Application Markup Language). Este archivo define el contenido 
visible de la ventana de forma similar a la definición de una página Web realizada 
mediante HTML. Al igual que en el caso de Winforms, existe un modelo de eventos 
(diferente del de Winforms) que permite conectar con los controles que hay en 
pantalla las rutinas que escribimos en el code behind. Además, XAML incorpora 
potentes capacidades de manejo de recursos, plantillas, vínculos de datos, 
desencadenadores, animaciones y gráficos, que en muchos casos permiten 
establecer en tiempo de diseño complejos comportamientos y cambios estéticos que 
habrían requerido implementar eventos y escribir código si se hubiera utilizado el 
modelo Winforms. 
En cuanto a las librerías del Framework que implementan todo lo anterior, en su 
mayor parte se trata de PresentationFramework.dll y 
PresentationCore.dll. Éstas se apoyan a su vez en una librería llamada 
MilCore.dll, que contiene código no-gestionado y es la que se encarga de la 
interfaz gráfica a bajo nivel. Los controles que se dibujan en pantalla están en el 
espacio de nombres System.Windows.Controls. 
Aplicaciones para Internet 
Al igual que en el caso de las aplicaciones de escritorio, disponemos en .NET de dos 
tecnologías diferentes para generar desde el lado servidor páginas web dinámicas. 
Ambas en su conjunto forman parte de ASP.NET (“Active Server Pages.NET”). 
Por una parte, tenemos la más antigua de las dos tecnologías, conocida como 
WebForms. En tiempo de diseño funciona de forma parecida a Winforms y WPF, es 
decir, se diseña una página con el diseñador de Visual Studio, el cual genera 
(además de código HTML para representar la página) una clase en la que se crean 
objetos que representan el contenido de la pantalla. Un modelo de eventos conecta 
estos objetos con las subrutinas que se programan en el code behind para 
interactuar con la pantalla. Internamente, dispone de varios automatismos para 
simular en Web el comportamiento de los formularios de Windows, de forma que 
haciendo llamadas de ida y vuelta (“postbacks”) desde el navegador al servidor, 
desde el punto de vista del usuario la página web aparenta comportarse como un 
formulario. 
El espacio de nombres en el que se encuentra implementada esta funcionalidad es 
System.Web, incluyendo los espacios que cuelgan de él, tales como 
System.Web.UI.WebControls y System.Web.UI.HtmlControls, que contienen 
las clases que definen los controles que se utilizan en pantalla. 
La otra tecnología que se utiliza para desarrollar páginas web dinámicas se 
denomina ASP.NET MVC. Las siglas MVC corresponden a Model View Controller, 
que es un patrón de programación muy conocido. Las librerías proveen una 
Plataforma .NET y Lenguaje C# | 25 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
infraestructura especialmente diseñada para dar soporte precisamente a este patrón 
de programación. En este caso no se dispone de un mecanismo que directamente 
simule formularios, pero a cambio se tiene un control muy preciso sobre qué es 
exactamente lo que se procesa y se genera en cada momento ante cada interacción 
del usuario con la página. 
Las librerías (y espacios de nombres) que dan soporte a esta funcionalidad son 
System.Web.Mvc y System.Web.Routing. 
Aplicaciones de consola 
Se trata, probablemente, de las aplicaciones con la interfaz de usuario más simple 
de todas. En pantalla, simplemente abren una ventana con líneas de caracteres, y 
toda la entrada/salida de la aplicación se realiza en modo carácter. 
A pesar de lo poco “vistosas” que son estas aplicaciones, es común utilizarlas 
cuando se desea crear herramientas administrativas que funcionen en modo “línea 
de comandos”, o cuando deban automatizarse para ejecutarlas sin interacción con el 
usuario (puesto que sus textos de entrada y salida pueden redirigirse a archivos). 
Cuando se aprende por primera vez un lenguaje de programación, es común 
realizar las primeras pruebas sobre aplicaciones de consola, para concentrarse en el 
propio lenguaje y no distraerse con las complejidades de la interfaz de usuario. De 
hecho, cuando más adelante en este libro comencemos a tratar el lenguaje C#, 
escribiremos nuestro primer ejemplo como aplicación de consola por este mismo 
motivo. 
En la librería System.dll se dispone de la clase System.Console, que contiene 
los métodos necesarios para realizar las entradas/salidas en consola. 
Servicios Windows 
Los Servicios Windows no tienen una interfaz de usuario propiamente dicha 
(funcionan “ocultos” con independencia del usuario que haya hecho “login”). No 
obstante, los clasificamos dentro de este apartado porque de cara al desarrollador 
siguen un patrónsimilar: el diseñador de Visual Studio genera una clase heredada a 
partir de una clase base que implementa este tipo de aplicaciones, y luego se 
“conecta” código sobre esta clase. 
La librería que da soporte a este tipo de aplicaciones es 
System.ServiceProcess.dll, y la clase de la que heredan los servicios, 
System.ServiceProcess.ServiceBase. 
26 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Colecciones 
En el espacio de nombres System.Collections hay diversas interfaces y clases 
que definen colecciones de objetos, tales como listas, colas, tablas de hash y 
diccionarios. 
Similarmente, el espacio de nombres System.Collections.Generic incluye 
también diversas colecciones, pero en este caso los elementos almacenados no son 
“Objects”, sino que se pueden personalizar para que tomen un tipo de datos 
concreto. Utilizan para ello un mecanismo llamado “Genéricos”, al que dedicaremos 
posteriormente un capítulo. 
Algunas de las clases correspondientes a los espacios de nombres 
System.Collections y System.Collections.Generic se definen dentro de la 
librería Mscorlib.dll, que constituye un núcleo “básico” que siempre se enlaza 
con las aplicaciones de .NET. Entre otras cosas, Mscorlib.dll define la clase 
System.Object, de la que heredan todas las demás, y tiene la distinción de ser la 
única librería que el CLR requiere que siempre se cargue dentro de todo proceso de 
código gestionado. Por este motivo, no es necesario añadir expresamente una 
referencia a esta DLL en nuestros proyectos, ya que siempre se enlaza de forma 
predeterminada. 
Otras de las clases de estos espacios de nombres se albergan en System.dll y 
System.Core.dll. 
Entradas/Salidas 
En el espacio de nombres System.IO se dispone de clases que permiten leer y 
escribir en archivos y streams, así como manejar archivos y carpetas en disco. 
En el capítulo dedicado a la programación orientada a objetos nos detendremos 
momentáneamente a examinar la clase Stream, ya que la usaremos como ejemplo 
de clase abstracta. Mientras tanto, valga decir que este es en .NET el instrumento 
principal para grabar y leer información desde un archivo u otra ubicación. Por 
ejemplo, entre las clases hijas de Stream se encuentra el FileStream, que permite 
leer y grabar archivos, NetworkStream, que permite enviar y recibir secuencias de 
bytes a un socket de red, MemoryStream, que permite enviarlos a un búfer en 
memoria, etc. 
El Stream representa un flujo de bytes. Podemos pensar en él como una “tubería” 
en la que se inyectan bytes por un extremo y salen por el otro. En un FileStream, 
uno de los extremos se conecta con un archivo en disco. Pero también hay Streams 
que están pensados para conectarlos con otro Stream a continuación, como si 
Plataforma .NET y Lenguaje C# | 27 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
empalmásemos dos tuberías. De esta manera, tenemos el CryptoStream, que sirve 
para cifrar los datos que lo atraviesan, el GZipStream, que sirve para comprimir 
datos, o el BufferedStream, que sirve para intercalar una memoria tampón. Así, 
por ejemplo, si conectásemos un GZipStream con un CryptoStream y luego con 
un FileStream, podríamos grabar un archivo con el contenido comprimido y 
cifrado (el orden es importante: si primero lo cifrásemos, no podríamos después 
comprimirlo). 
Las clases derivadas de Stream a veces son incómodas de manejar, ya que operan 
con arreglos de bytes, mientras que nuestros programas normalmente lo que 
manejan son tipos primitivos tales como números enteros o cadenas de caracteres. 
Para simplificar la lectura y escritura de estos datos, se dispone de unas clases 
auxiliares que se interponen entre los Streams y nuestro código, realizando de 
forma interna las conversiones entre los datos que les aportamos y las secuencias de 
bytes. Entre ellas están las clases abstractas TextReader y TextWirter, de las que 
heredan StreamReader y StreamWriter (que trabajan con cadenas de 
caracteres), así como las clases BinaryReader y BinaryWriter, que trabajan con 
datos tales como números enteros o de coma flotante. 
Finalmente, y para terminar de hablar sobre el espacio de nombres System.IO, 
mencionemos que también contiene (entre otras) las clases File y FileInfo, que 
permiten hacer operaciones tales como copiar, borrar y renombrar archivos, y 
también Directory y DirectoryInfo, que hacen lo mismo con los directorios, 
además de permitir enumerar los subdirectorios y archivos que contienen. 
Las clases de este espacio de nombres están en las librerías Mscorlib.dll y 
System.dll. 
Acceso a datos 
El espacio de datos System.Data contiene clases que forman la arquitectura de 
ADO.NET. Estas clases permiten gestionar datos provenientes de diferentes 
orígenes, como por ejemplo bases de datos SQL Server u Oracle, e incluso archivos 
de texto u hojas Excel, siempre que se use el “driver” adecuado. 
Adicionalmente, en las librerías del Framework se dispone de otras tecnologías que, 
apoyándose sobre ADO.NET, aportan funcionalidad más sofisticada. 
Concretamente, LINQ-to-SQL permite incrustar consultas similares a SQL dentro 
del lenguaje C#, y Entity Framework permite crear un mapeo objeto-relacional 
(ORM). 
28 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Más adelante en este libro hay una pequeña sección sobre el funcionamiento de 
LINQ, desde el punto de vista de su interacción con el lenguaje (pero no con la base 
de datos). 
Aquí no vamos a hablar más acerca del acceso a datos, ya que sobre este tema existe 
una sección específica en este libro. 
Globalización 
El espacio de nombres System.Globalization contiene clases que definen la 
información relativa a las distintas culturas, tales como el idioma, país o región, 
calendario, formatos para las fechas, la moneda y los números, etc. 
Estas clases son útiles para escribir aplicaciones que funcionen en múltiples países. 
Nos permiten determinar, entre otras muchas cosas, si los números se deben 
escribir con punto o coma decimal, y si las fechas son del tipo día/mes/año o 
mes/día/año. 
También hay métodos para comparar cadenas de texto teniendo en cuenta las 
peculiaridades lingüísticas de distintos idiomas, y para procesar calendarios 
diferentes del gregoriano. 
Con independencia de las capacidades de System.Globalization, es conveniente 
saber también que tanto las aplicaciones de tipo Winforms como las de Webforms 
tienen ya previstos mecanismos automáticos para traducir a distintos idiomas los 
textos presentados al usuario, extrayéndolos de archivos de recursos en los que se 
incorporan las traducciones. 
Manipulación de texto 
El espacio de nombres System.Text contiene clases que representan distintas 
codificaciones de caracteres, tales como ASCII o UTF8. Permite realizar 
conversiones entre bloques de caracteres y bloques de bytes (aplicando la 
codificación correspondiente). Las clases codificadoras tienen principalmente la 
finalidad de convertir desde y hacia Unicode. Internamente, las cadenas de .NET 
utilizan Unicode (codificado como UTF-16), pero no siempre que se lee o escribe un 
archivo se desea grabar el texto en este formato. Utilizando las clases de 
System.Text, como por ejemplo System.Text.Endoding.UTF8, se pueden hacer 
las conversiones oportunas al formato deseado. 
Plataforma .NET y Lenguaje C# | 29 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
En este espacio de nombres también se encuentra la clase StringBuilder, que 
permite manipular el contenido de las cadenas de caracteres sin tener que crear 
instancias intermedias de la clase String. Esto merece una explicación adicional: 
En .NET, los objetos de la clase String son invariables, es decir, una vez que se han 
creado, no se les puede cambiar el valor. Cuando de forma aparente se cambia el 
valor de un string, por ejemplo, concatenándole un carácter al final, lo que ocurre 
internamente es que se construye un nuevo string, se le copia el nuevo valor, y se 
destruye el antiguo string.Estas creaciones y destrucciones son costosas, por lo 
que resulta muy lento un bucle de este tipo: 
string s=""; 
for (int i = 0; i < 10000; i++) 
{ 
 s = s + "x"; 
} 
 
El remedio consiste en emplear un StringBuilder, que asigna internamente un 
búfer grande en el que ir manipulando los caracteres, y luego permite convertir el 
resultado en un string una vez terminados los cambios: 
StringBuilder sb = new StringBuilder(); 
for (int i = 0; i < 10000; i++) 
{ 
 sb.Append("x"); 
} 
string s = sb.ToString(); 
 
Esta clase se encuentra definida dentro de la librería Mscorlib.dll. 
Aprovechamos para mencionar también el espacio de nombres 
System.Text.RegularExpressions (en System.dll), en el que se define la 
clase Regex. Gracias a ella podemos procesar las denominadas expresiones 
regulares. Se pueden usar para analizar con rapidez grandes cantidades de texto 
buscando patrones de caracteres, y extraer, editar, reemplazar o borrar subcadenas. 
Multihilo 
El espacio de nombres System.Threading (implementado en Mscorlib.dll) 
pone a nuestra disposición una serie de clases e interfaces que permiten escribir 
programas con múltiples hilos de ejecución. 
Además de permitir lanzar distintos hilos (mediante la clase Thread), contiene los 
elementos necesarios para sincronizar la ejecución de los mismos, por ejemplo, 
semáforos de mutua exclusión (mutex). También se encuentra aquí la clase 
30 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
TreadPool, que permite acceder al pool de hilos del sistema, y una clase Timer 
para generar eventos periódicos. 
Reflexión 
El espacio de nombres System.Reflection contiene clases que recuperan 
información acerca de los ejecutables, clases, miembros, parámetros y otras 
entidades almacenadas en el código gestionado. Lo consiguen gracias a los 
metadatos que se almacenan dentro del archivo ejecutable al compilar el código 
fuente. 
Las clases de System.Reflection también permiten manipular instancias 
cargadas en memoria, por ejemplo, para conectarse a eventos, cambiar valores de 
propiedades, o invocar métodos (incluso los marcados como privados, no solo los 
públicos). También se pueden crear nuevas clases sobre la marcha, por mediación 
de las clases del espacio de nombres System.Reflection.Emit. 
A pesar de la potencia que tienen estas herramientas de reflexión, no se recomienda 
abusar de ellas porque resultan lentas (en comparación con la ejecución “normal”, 
directa, del código), y porque perjudican la legibilidad y facilidad de mantenimiento 
del programa ya que se “saltan” las características de encapsulación de la 
programación orientada a objetos. 
A continuación 
Después de haber visto estos pocos ejemplos del tipo de funcionalidad que nos 
proporcionan las librerías del Framework, en los próximos capítulos veremos los 
elementos fundamentales del lenguaje C#, apoyándonos inicialmente en un sencillo 
programa ejemplo. 
Seguirá un estudio de las distintas construcciones del lenguaje, para terminar 
examinando algunas de las características periféricas, tales como los Atributos para 
adjuntar metadatos o la infraestructura que da soporte a las consultas integradas en 
el lenguaje. 
Plataforma .NET y Lenguaje C# | 31 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
32 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Plataforma .NET y Lenguaje C# | 33 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
El Lenguaje C# 
El lenguaje C# (que se pronuncia “Ce Sharp”) fue diseñado por Microsoft para 
construir todo tipo de aplicaciones bajo la plataforma .NET. Supone una evolución 
de otros lenguajes anteriores de la familia del C y C++. Según la propia Microsoft, 
C# es “simple, moderno, seguro en cuanto a tipos, y orientado a objetos”. 
El código fuente de C# se compila a “código manejado”, lo que significa que se 
beneficia de los servicios del CLR que ya hemos mencionado con anterioridad. 
Dentro de Visual Studio, a veces se hace referencia a este lenguaje como “Visual 
C#”. El entorno de trabajo incluye plantillas de proyecto, diseñadores, páginas de 
propiedades, asistentes para generación de código y otras características que nos 
ayudan durante la escritura de nuestros programas. 
Algunas de las ventajas de desarrollar con C# y Visual Studio son las siguientes: 
 C# un lenguaje sencillo, seguro en cuanto a tipos, orientado a objetos, y 
muy versátil para generar distintos tipos de aplicaciones. 
 Permite integrarse con código ya existente, a través de COM-Interop y 
Platform/Invoke, técnicas que permiten llamar a objetos COM o a APIs 
convencionales (de código no-manejado). 
 Dispone de una potente gestión de memoria, que evita muchos de los 
problemas encontrados en otros entornos más antiguos, tales como los 
“memory leaks” (pérdidas de memoria) que a veces ocurren en C++ o la 
falta de liberación de objetos con referencias circulares que ocurren en 
VB. 
 La Seguridad de Acceso a Código (CAS) permite determinar los 
permisos de una aplicación mediante un mecanismo basado en 
“pruebas” que determina la confianza que tenemos en ese código. 
 Soporta metadatos extensibles, en forma de datos que se adjuntan al 
ejecutable pero no constituyen código que se ejecute. 
34 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
 Puede interactuar con otros lenguajes, con otras plataformas, y con
datos almacenados en distinto formatos y ubicaciones.
Nuestro primer programa 
Cuando se enseña un nuevo lenguaje de programación, es tradicional comenzar 
escribiendo un pequeño programa denominado “Hola, Mundo”, que se limita a 
escribir en pantalla un texto y a continuación terminar. El propósito es el de 
enseñar desde el principio cuál es la estructura básica de un programa mínimo y la 
forma de compilarlo y ejecutarlo. 
Veamos sin más preliminares cómo se escribe el “Hola Mundo” en C#: 
using System; 
class Program 
{ 
 static void Main() 
 { 
 Console.WriteLine("Hola, Mundo."); 
 } 
} 
Este bloque de código podemos teclearlo dentro del NOTEPAD y salvarlo a disco 
con un nombre acabado en “.cs”, como por ejemplo “HolaMundo.cs”. 
Para compilarlo, basta con invocar desde una línea de comandos al compilador de 
C#, llamado CSC.EXE, pasándole el nombre archivo: 
C:\Prueba\> CSC HolaMundo.cs 
El compilador CSC.EXE se encuentra en la misma carpeta en la que se ha instalado 
el Framework. Para la versión 4.0, la ruta predeterminada es 
C:\Windows\Microsoft.NET\Framework\v4.0.30319. Si no queremos escribir esta 
ruta manualmente para llamar al CSC, podemos añadirla a la variable PATH dentro 
de una ventana de comandos. De hecho, para facilitarnos esta operación, al instalar 
Visual Studio se crea una entrada en el menú de Inicio de Windows llamada “Visual 
Studio Command Prompt” que nos abre una ventana de comandos con al PATH ya 
configurado. Si usamos esa ventana de comandos para compilar, bastará con 
escribir “CSC” sin tener que escribir la ruta completa. 
Plataforma .NET y Lenguaje C# | 35 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
Después de compilar nuestro HolaMundo.cs, se crea en la misma carpeta un 
archivo HolaMundo.exe, que representa ya el ejecutable final de nuestro programa. 
Podemos lanzarlo sin más que teclear su nombre, obteniendo como resultado el 
mensaje esperado: 
 
Al igual que suele ocurrir con otras herramientas de línea de comandos, CSC 
dispone de múltiples opciones que se pueden añadir para compilar juntos múltiples 
archivos fuente, añadir referencias a librerías, cambiar el nombre y tipo del 
36 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
ejecutable, etc. Sin embargo, no nos vamos a detener ahora a estudiar esas opciones 
porque normalmente no operaremos de esta manera. 
Nota: El resultado de la compilación se denomina “ensamblado”, o assembly en la 
documentación en inglés. El ensamblado es la unidad mínima de instalación y 
despliegue de .Net, y en términos prácticos es prácticamentesiempre un EXE o una 
DLL. En teoría, se puede producir un ensamblado con múltiples archivos (que
acaban en la extensión .netmodule), pero esta opción no es comúnmente utilizada
en la práctica.
Aunque está bien saber que es posible escribir un programa de .NET con el 
NOTEPAD y compilarlo desde línea de comandos sin utilizar en ningún momento 
Visual Studio, la realidad es que en la práctica trabajaremos casi todo el tiempo con 
esta última herramienta y seleccionaremos desde la interfaz gráfica las opciones 
necesarias. 
No es objetivo de este capítulo enseñar al lector a manejar Visual Studio, sino 
concentrarnos en el lenguaje de programación propiamente dicho. No obstante, y 
por si acaso alguno de nuestros lectores no estuviera familiarizado con la 
herramienta, vamos a indicar muy brevemente los pasos necesarios para crear 
nuestro “Hola Mundo” desde Visual Studio: 
Tras abrir Visual Studio, seleccionaremos en el menú la opción “Archivo -> Nuevo -
> Proyecto”, que nos presentará una lista de plantillas para seleccionar el tipo de
proyecto deseado.
Plataforma .NET y Lenguaje C# | 37 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
En nuestro caso concreto, vamos a seleccionar la plantilla para aplicaciones de 
Consola. Este es el tipo de aplicación que tiene la interfaz de usuario más simple, ya 
que funciona en una ventana de consola escribiendo y leyendo líneas de caracteres. 
De esta manera, podremos concentrarnos en los elementos del lenguaje de 
programación C#, sin distraernos con detalles de manejo de la interfaz de usuario, 
que no son relevantes de cara al estudio del lenguaje en sí mismo. 
Después de seleccionar la plantilla y asignar un nombre al proyecto, se abre en 
Visual Studio la pantalla de edición de texto del archivo principal del proyecto (que 
de forma predeterminada se llama Program.cs pero podemos renombrarlo si es 
necesario). El código fuente presenta un “esqueleto” de programa en el que 
podemos insertar las líneas que deseemos. 
38 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Examinando el código que nos proporciona la plantilla, vemos que es muy similar al 
que habíamos escrito a mano con el NOTEPAD, con algunas adiciones que 
comentaremos seguidamente. 
Examinando el programa ejemplo 
La directiva using 
Fijándonos en el HolaMundo.cs, vemos que al principio hay una o más directivas 
“using”. Nuestro propio ejemplo sólo contiene un using System, pero la plantilla 
de Visual Studio aporta varias líneas de este tipo. 
Detrás de la palabra using viene un espacio de nombres (“namespace”). Sabemos 
ya que los espacios de nombres se usan para clasificar y eliminar ambigüedades en 
los nombres de tipos que hay en las librerías de clases. La directiva using anuncia 
al compilador que vamos a utilizar ese espacio de nombres en las llamadas a 
distintas clases que vienen más abajo en el código fuente. 
Por ejemplo, nuestro programa contiene la sentencia Console.WriteLine(...). 
Console es una clase que se encuentra programada dentro del espacio de nombres 
System, por lo que su nombre completo es System.Console. Aunque es lícito 
escribir System.Console.WriteLine(...) cada vez que sea necesario hacer esta 
llamada en el programa, esto se vuelve molesto en programas largos con múltiples 
llamadas a múltiples clases dentro de espacios de nombres con nombres 
complicados. Gracias a la directiva using, podemos declarar al principio que vamos 
a usar System, y luego prescindir de dicho prefijo en las llamadas a las clases de 
este espacio de nombres. 
La plantilla predefinida para aplicaciones de consola presume que vamos a utilizar 
varios espacios de nombres, cuyas declaraciones incluye automáticamente. 
Podemos, por supuesto, borrar las declaraciones que nos sobren si no vamos a usar 
esos espacios de nombres. 
Esta utilización de directivas using para introducir en nuestro código las 
declaraciones de espacios de nombres es frecuentísima, y nos la encontraremos 
prácticamente en todos los programas fuente escritos con C#. Por este motivo 
hemos dejado el using System en nuestro programa ejemplo, pese a que no es 
imprescindible usar esta directiva en un programa mínimo. 
Plataforma .NET y Lenguaje C# | 39 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
La declaración del namespace 
La plantilla de Visual Studio incorpora a continuación la línea “namespace 
HolaMundo”, y seguidamente encierra entre llaves el resto del código. Eso significa 
que al compilar, todo ese código quedará asignado al espacio de nombres que se 
indica. Por ejemplo, la clase Program será en realidad HolaMundo.Program. Esto 
deberá ser tenido en cuenta desde otras partes del programa que deban hacer 
llamadas a nuestra clase Program, ya que tendrán que escribir 
“HolaMundo.Program” o incluir una directiva “using HolaMundo”, como vimos 
en el apartado anterior. 
El programa ejemplo que nosotros escribimos desde NOTEPAD no llevaba esta 
directiva, puesto que no resulta imprescindible asignar un espacio de nombres para 
un programa mínimo de este tipo. Sin embargo, en programas más grandes, lo 
habitual es que cada uno de los archivos fuente declare el espacio de nombres del 
código que contiene. 
De manera predeterminada, las plantillas empleadas por Visual Studio introducen 
un namespace que coincide con el nombre del proyecto. Este valor predeterminado 
puede cambiarse desde la ventana de Propiedades del proyecto, pero únicamente 
afectará a los nuevos archivos fuente que en el futuro se añadan al proyecto; los que 
ya estén creados habrá que modificarlos manualmente si queremos cambiarles el 
espacio de nombres. 
La clase 
La siguiente línea que encontramos en el programa fuente es “class Program”, 
seguida del resto del contenido encerrado entre llaves. Esta línea declara que vamos 
a compilar una clase, y el código que va entre las llaves es el contenido de la clase. 
A diferencia de otros lenguajes anteriores tales como C o C++, en C# no podemos 
escribir un método directamente en el programa sin que forme parte de una clase. 
Incluso aunque no vayamos a instanciar la clase Program en ningún momento, es 
obligatorio declararla para poder ubicar en su interior el método Main que contiene 
la funcionalidad de nuestro programa. 
Aprovechamos para mencionar que los saltos de línea y los espacios en blanco son 
irrelevantes en C#, y se emplean sólo por razones estéticas y para facilitar la lectura 
del programa fuente. Este programa compilaría exactamente igual aunque las llaves 
se encontrasen (por ejemplo) en las mismas líneas que el código fuente abarcado 
por ellas. 
40 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
El método Main 
Dentro de la clase se ha escrito un método que lleva el nombre Main. Este método 
es por convención el “punto de arranque” del programa, o en otras palabras, el lugar 
por el que comenzará a ejecutarse cuando se invoque desde el sistema operativo. 
Para quienes estén acostumbrados a programar en C o C++, nótese que en C# la M 
de Main va en mayúsculas a diferencia de lo que ocurre en los lenguajes más 
antiguos. Y ya que estamos en ello, aprovechamos también para mencionar que C# 
es sensible a mayúsculas y minúsculas, por lo que el método main es distinto del 
método Main. 
Otra observación es que la plantilla de Visual Studio ha añadido como argumento 
de Main un arreglo de cadenas declarado como string[] args. Este arreglo 
recibe los parámetros que se tecleen en la línea de comandos al llamar al 
HolaMundo.exe. En nuestro propio ejemplo de NOTEPAD omitimos este 
argumento porque no hacía uso de los mencionados parámetros. 
Una vez más, los lectores que trabajen en C o C++ notarán una diferencia, ya que 
dichos lenguajes no sólo requieren que main reciba el arreglo de parámetros, sino 
también el número de elementos que contiene el arreglo. En C# esto no es 
necesario, ya que cada arreglo contiene una propiedad llamada Length que 
devuelve el número de elementos que contiene. Másadelante, en el capítulo 
dedicado a los arreglos, estudiaremos con detenimiento las propiedades que 
exhiben. 
El método Main de nuestro programa va precedido de la palabra void, que indica 
que “no devuelve nada”, y de la palabra static, que indica que puede invocarse sin 
necesidad de instanciar la clase que lo contiene. Más adelante tenemos un capítulo 
dedicado a los métodos en el que estudiaremos la forma de declarar y devolver 
valores desde ellos, y otro capítulo dedicado a orientación a objetos, en el que 
trataremos con más detenimiento el uso de la declaración static. 
La sentencia de salida 
Después de todo lo anterior, llegamos por fin al código que verdaderamente realiza 
el trabajo de nuestra aplicación. En este ejemplo concreto, se trata de la única 
sentencia “Console.WriteLine("Hola, Mundo."); ”. Básicamente, consiste en 
una llamada al método estático WriteLine de la clase System.Console, 
pasándole como argumento la cadena “Hola, Mundo”. 
La clase System.Console está definida dentro de una de las librerías del 
Framework, concretamente mscorlib.dll. Normalmente, cuando llamamos a una 
clase que está dentro de una DLL es necesario indicar al invocar al CSC cuál es esa 
Plataforma .NET y Lenguaje C# | 41 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
DLL para que pueda resolver las referencias a la misma. Pero mscorlib.dll es un 
caso excepcional porque contiene funcionalidad básica que siempre debe estar 
disponible al ejecutar una aplicación de .NET, y por ese motivo no fue necesario 
indicar su uso en el momento de compilar. 
Como observación final, fijémonos en que la sentencia termina en un punto y coma. 
En C#, el punto y coma es un terminador de sentencias, no un separador de 
sentencias, por lo que todas las sentencias deben ir seguidas de un punto y coma 
incluso aunque detrás no venga otra sentencia. 
A continuación 
Conocemos ahora en cierto detalle cómo está formado un programa mínimo y qué 
significa cada una de las partes que lo componen. También hemos visto cómo 
compilarlo y ejecutarlo. En los capítulos que siguen iremos estudiando cada uno de 
los elementos sintácticos que componen el lenguaje C#. 
42 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Elementos 
Sintácticos 
Básicos 
A continuación vamos a estudiar algunos de los elementos que constituyen la 
sintaxis básica del lenguaje C#. 
Plataforma .NET y Lenguaje C# | 43 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Comentarios 
Puesto que este texto va dirigido a desarrolladores que ya han programado con 
algún otro lenguaje, nos imaginamos que no es necesario que hagamos ningún 
hincapié acerca de la utilidad de comentar el código fuente. Así que veamos sin más 
cómo se escriben los comentarios: 
Static void Main() //Esto es un comentario 
{ 
 /* Esto es otro comentario */ 
 Console.WriteLine(“Hola”); 
} 
 
Como podemos observar, hay dos estilos distintos para los comentarios: 
Los que empiezan por dos barras (//) declaran como comentario todo lo que venga 
detrás, hasta el final de la línea. 
Los comentarios que van entre /* y */ pueden ocupar varias líneas o insertarse en 
medio de una línea. 
Comentarios XML 
Una característica especial de Visual Studio es que permite generar de forma 
semiautomática bloques de comentarios estandarizados que contienen información 
sobre los elementos más comunes del código fuente (tales como clases, propiedades 
o métodos) codificada en forma de XML. 
Para generar un bloque de comentarios de este tipo, basta con teclear tres barras 
(///) por encima del elemento que queremos comentar. Al terminar de teclear la 
tercera barra, Visual Studio escribe inmediatamente un bloque de comentarios de 
varias líneas listo para que lo rellenemos. Por ejemplo, si escribimos las tres barras 
por encima del método Main de nuestro ejemplo “Hola, mundo”, se genera lo 
siguiente: 
/// <summary> 
/// 
/// </summary> 
/// <param name="args"></param> 
static void Main(string[] args) 
 
44 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Entre medias de los “tags” XML (que variarán dependiendo del tipo de objeto que 
estemos comentando) escribiremos los comentarios oportunos, por ejemplo: 
/// <summary> 
/// Punto principal de entrada al programa 
/// </summary> 
/// <param name="args">Argumentos de la línea de 
comandos</param> 
static void Main(string[] args) 
{ 
 Console.WriteLine("Hola, Mundo."); 
} 
Como vemos, se trata de varias líneas que empiezan por tres barras. Dado que 
cualquier texto que empieza por dos barras es considerado como comentario por el 
compilador, todas estas líneas son consideradas comentarios, y no tienen efecto 
sobre el ejecutable compilado. 
En la ventana de Propiedades del Proyecto de Visual Studio existe una casilla de 
selección que permite configurarlo de manera que extraiga todos los comentarios de 
este tipo que existan en todos los fuentes del proyecto a un archivo de tipo .xml en 
la misma carpeta del ejecutable. 
Plataforma .NET y Lenguaje C# | 45 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Este archivo sirve dos propósitos: 
Por una parte, puede emplearse para generar la documentación del código fuente. 
Existen diversas herramientas disponibles públicamente que son capaces de 
convertir este XML en formatos legibles tales como Word o HTML. Estos archivos 
pueden entregarse como documentación final de los fuentes del programa. 
Si el archivo XML se copia junto con el código resultante de la compilación (que en 
estos casos típicamente será una DLL) a una carpeta desde donde sea referenciada 
esa DLL, entonces mientras tecleamos el código fuente de estos nuevos programas 
Visual Studio será capaz de mostrarnos automáticamente el texto de esos 
comentarios XML. De esta manera, dispondremos “en línea” de la documentación 
correspondiente a las DLLs que estamos empleando. Las propias DLLs del 
Framework vienen documentadas de esta manera, gracias a lo cual Visual Studio 
nos muestra su ayuda interactiva (denominada intellisense) mientras vamos 
tecleando. 
 
Este mismo efecto puede lograrse con las librerías escritas por nosotros si 
habilitamos la opción de “Generar el archivo de documentación XML” y copiamos 
ese archivo junto con la DLL de nuestra librería. 
Nota: En el caso de llamar a los métodos con comentarios XML desde el mismo 
proyecto en el que se han definido, los comentarios aparecen en intellisense sin que 
sea necesario generar en disco el archivo XML. 
Sentencias 
Las sentencias conforman la lógica del programa, indicando las instrucciones que 
deben ejecutarse. El programa consiste en una secuencia de sentencias, que se 
ejecutan de arriba a abajo mientras no se altere expresamente la secuencia de 
ejecución mediante una instrucción apropiada. 
Normalmente las sentencias en C# terminan con un punto y coma. Opcionalmente, 
se pueden escribir bloques de sentencias y también usar ciertas construcciones del 
46 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
lenguaje (bucles, condiciones, etc.) para controlar la secuencia de ejecución de las 
sentencias. 
Por ejemplo, la siguiente línea constituye una sentencia válida: 
Console.WriteLine("Hola, Mundo."); 
Bloques de sentencias 
Se pueden agrupar varias sentencias dentro de un bloque encerrándolas entre 
llaves. Esto es útil, sobre todo, cuando todo el bloque en su conjunto se somete a 
una misma operación de control (por ejemplo, un “if” que afecta a varias 
sentencias). 
{ 
 Sentencia1(); 
 Sentencia2(); 
 //... 
} 
Se pueden anidar los bloques, al igual que ocurre en otros lenguajes similares. Sin 
embargo, en C# hay una diferencia importante respecto a otros lenguajes de la 
familia del C: no es lícito que dos bloques anidados contengan una misma 
declaración de variable. Por ejemplo, la siguiente combinación produce un error al 
compilarse: 
{ 
 int i; 
 //... 
 { 
 int i; 
 //... 
 } 
} 
Esta restricción se introdujo paraevitar el error frecuente que se produce cuando el 
programador accede a la variable del bloque interno creyendo que usa la del bloque 
externo. 
Por el contrario, es perfectamente lícito que dos bloques del mismo nivel declaren la 
misma variable: 
{ 
 int i; 
 //... 
} 
//... 
{ 
Plataforma .NET y Lenguaje C# | 47 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 int i; 
 //... 
} 
Nota: esto no afecta a las variables miembro de una clase, que sí que se pueden 
llamar igual que las variables locales de un método de esa clase. Aunque 
visualmente aparenten estar escritas en dos bloques anidados, conceptualmente se 
trata de una estructura distinta de la que estamos tratando aquí. 
Sentencias de control de flujo 
Bajo esta denominación genérica, agrupamos varios tipos de sentencias: 
Sentencias de selección: if y switch. 
Sentencias de iteración: while, do, for y foreach. 
Sentencias de salto: goto, break y continue. 
If 
La sentencia if permite ejecutar código condicionalmente. En su variante más 
sencilla, tiene este aspecto: 
if (condicion) sentencia; 
Opcionalmente, admite una cláusula else para introducir una sentencia que se 
ejecuta cuando no se cumple la condición: 
if (condicion) sentencia1; else sentencia2; 
En cualquier caso, donde va una sentencia también puede ir un bloque: 
if (condicion) 
{ 
 Sentencias; 
 //... 
} 
else 
{ 
 Sentencias; 
 //... 
} 
48 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Algunas observaciones, destinadas sobre todo a quienes provengan de programar 
en otros lenguajes: 
 Recuérdese que C# es sensible a mayúsculas y minúsculas. Las palabras
clave deben escribirse obligatoriamente en minúsculas. Por ejemplo, no es
válido escribir If en lugar de if.
 La condición que sigue al if necesariamente tiene que ir encerrada entre
paréntesis.
 La condición tiene que devolver un resultado booleano; no es válido (como
ocurría en el antiguo C) emplear una expresión que devuelva un valor
entero, esperando que se considere verdadero si es distinto de cero.
 Aunque examinaremos los operadores más adelante, merece la pena señalar
que en C# el comparador de igualdad está formado por dos símbolos de “=”,
en lugar de uno sólo como ocurre en otros lenguajes. Por ejemplo para
ejecutar una sentencia si la variable i vale cero, debe escribirse así:
if (i == 0) sentencia; 
Switch 
La sentencia switch permite elegir una rama de código entre muchas, en función 
del valor que adquiera una variable o expresión. Este es un ejemplo: 
switch (miVariable) 
{ 
 case 1: 
 sentenciaA; 
 break; 
 case 2: 
 sentenciaB; 
 break; 
 case 3: 
 sentenciaC; 
 break; 
 case 4: 
 sentenciaD; 
 break; 
 default: 
 sentenciaE; 
 break; 
} 
Plataforma .NET y Lenguaje C# | 49 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Si miVariable vale 1, pasan a ejecutarse las sentencias que hay debajo del “case 
1”, si vale 2, las del “case 2”, etc. Si no tiene ninguno de los valores indicados en 
los case, se ejecuta el default. 
Observaciones: 
 El default es opcional. Si se omite, no se ejecuta nada en caso de que 
la variable de control no coincida con ninguno de los valores previstos 
en los case. 
 En C# no se permite que la ejecución fluya de un case al siguiente, 
como ocurría en C tradicional. En este último lenguaje, se producían 
con frecuencia errores cuando el programador olvidaba escribir la 
instrucción break que detiene el salto al siguiente case. Los 
desarrolladores de C# optaron por hacer que el compilador genere un 
error en caso de omitir el case (u otra instrucción que detenga el 
avance, como por ejemplo un return o un throw). 
 Si tenemos que migrar a C# algún código antiguo proveniente de C, y 
para ello realmente necesitamos que la ejecución fluya de un case a otro, 
podemos lograrlo mediante una instrucción del tipo “goto case 2;” 
(por ejemplo). 
 Puesto que ningún case puede fluir al siguiente, es lícito recolocarlos en 
cualquier orden sin que afecte a la semántica del switch. 
 La expresión que controla el switch no puede ser de un tipo arbitrario. 
Sólo se permiten enteros, char, enum o string. Nótese que los strings 
sí que se permiten en C#, contrariamente a lo que ocurre en otros 
lenguajes de la misma familia (C, C++ o Java). Cuando se comparan 
strings, es lícito que uno de los valores de los case sea null. 
 Se pueden juntar varios case uno a continuación de otro, sin código 
intermedio, si se desea que todos ejecuten las mismas sentencias. 
 En un sólo case se pueden escribir varias sentencias sin necesidad de 
que vayan entre llaves para formar un “bloque”. 
 Una pregunta frecuente en los foros de programación se refiere a cómo 
conseguir en C# un switch con comparaciones complejas en los case, 
tales como rangos de valores. Aunque esto se puede hacer en VB, el 
switch de C# únicamente permite comparaciones de igualdad. Para 
lograr otros tipos de comparación es necesario sustituir el switch por 
una secuencia de if...else if. Aunque visualmente no resulta tan 
elegante como el switch, esto no supone una merma en el rendimiento 
del programa compilado, ya que en cualquier caso el compilador habría 
50 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
tenido que convertir los case con comparaciones complejas en una 
secuencia de if. 
While 
Se utiliza para repetir un bloque de sentencias mientras se cumpla una condición. 
La sintaxis tiene este aspecto: 
while (condicion) sentencia; 
En circunstancias normales, es necesario que durante la ejecución de la sentencia 
(que puede ser un bloque entre llaves) se modifique algún dato que forme parte de 
la condición. De lo contrario el bucle no terminaría nunca. Por ejemplo, el siguiente 
fragmento de código escribe en consola los números del 1 al 9: 
int i = 1; 
while (i < 10) 
{ 
 Console.WriteLine(i); 
 i++; 
} 
Son de aplicación las mismas observaciones que se hicieron al hablar de la 
condición en la sentencia if. 
Do 
Es similar al while, pero en este caso la condición se evalúa al final del bucle, en 
lugar del principio. Las sentencias que hay dentro del do siempre se ejecutan al 
menos una vez, mientras que el while podría no ejecutar nada en caso de que la 
condición sea falsa desde el principio. 
Por ejemplo, el siguiente bucle escribe los números del 1 al 9: 
int i = 1; 
do 
{ 
 Console.WriteLine(i); 
 i++; 
} while (i < 10); 
Plataforma .NET y Lenguaje C# | 51 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
For 
El bucle for en C# es muy potente, ya que permite seleccionar cualquier sentencia 
para ejecutar al principio del bucle, a cada iteración, y como condición de 
terminación. De forma genérica, la sintaxis es esta: 
for ( inicialización ; condición ; actualización ) 
 sentencia; 
Uno de los casos de uso más simples es el que utiliza una variable de tipo entero 
para repetir un número fijo de iteraciones. Por ejemplo, este bucle escribe los 
mismos resultados que nuestros dos ejemplos anteriores: 
for (int i = 1; i < 10; i++) 
{ 
 Console.WriteLine(i); 
} 
 
Obsérvese las tres partes separadas por puntos y comas: 
La primera, i=1, representa la acción que se toma en el momento de entrar en el 
bucle. A propósito, nótese que este ejemplo aprovecha para declarar la variable i en 
la misma sentencia. Esto es opcional; también puede declararse fuera, pero lo 
mencionamos por ser novedoso respecto a otras variantes de C que no admiten esta 
posibilidad. 
La segunda, i<10, es una condición booleana que se evalúa antes de ejecutar las 
sentencias del bucle. Si resulta ser falsa desde el principio, el bucle no se ejecuta 
ninguna vez. 
La tercera, i++ (que como veremos después significa “incrementar la variable i”) se 
ejecuta al final del bucle antes de volver a comprobar la condición del principio. 
Pese a que esta es una utilización muy común del for, hay muchas otras formas de 
usarlo. Por ejemplo, para recorrer todaslas fechas desde hoy hasta un mes más 
tarde, se puede usar este bucle: 
for (DateTime dia = DateTime.Now; 
 dia < DateTime.Now.AddMonths(1); 
 dia = dia.AddDays(1)) 
{ 
 Console.WriteLine(dia); 
} 
Se puede omitir cualquiera de las partes que van separadas por punto y coma. Por 
ejemplo, el siguiente bucle es “infinito” (no terminará mientras no se ejecute en su 
interior una sentencia que lo abandone, como por ejemplo el break que veremos 
más adelante): 
52 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
for (;;) 
{ 
 //... 
} 
Foreach 
Este tipo de bucle se utiliza para iterar sobre las colecciones de objetos, devolviendo 
a cada iteración uno de los miembros de la colección. Aunque todavía no hemos 
avanzado en este texto lo suficiente como para definir con precisión qué se entiende 
por una “colección” dentro de este contexto, a grosso modo podemos pensar en ella 
como una entidad de software cuyo propósito es contener otras entidades. A estos 
efectos, un vector o una matriz con colecciones; también el conjunto de caracteres 
de un string, y numerosas clases definidas en las librerías del Framework, como 
por ejemplo List<T> o Hashtable. 
El siguiente ejemplo recorre uno por uno los caracteres de la cadena “Hola, 
mundo”, escribiendo cada uno en una línea: 
string s = "Hola, mundo"; 
foreach (char c in s) 
{ 
 Console.WriteLine(c); 
} 
De manera general la sintaxis es: 
foreach (variable in coleccion) sentencia; 
Como ya hemos visto, es lícito (pero no obligatorio) declarar la variable localmente 
dentro del propio bucle. Se produce un error si el tipo de la variable no concuerda 
con el tipo de los datos contenidos en la colección. 
Goto 
Aunque la tendencia dentro de las técnicas de programación estructurada es 
despreciar esta instrucción, goto sigue estando disponible en el lenguaje C#. Para 
quienes tengan curiosidad por su sintaxis, esta es la forma de utilizarla en C#: 
 //... 
 goto etiqueta; 
 //... 
etiqueta: ; 
 //... 
Plataforma .NET y Lenguaje C# | 53 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Break 
La sentencia break hace que se abandone el bloque switch, while, do, for o 
foreach más próximo dentro del que se encuentre. Por ejemplo, el siguiente 
bloque for se abandona a la cuarta iteración: 
for (int i = 0; i < 10; i++) 
{ 
 //... 
 if (i == 4) break; 
 //... 
} 
Continue 
La sentencia continue hace que se pase a la siguiente iteración del bucles while, 
do, for o foreach, omitiendo la ejecución de las líneas que vienen debajo dentro 
del bucle. 
for (int i = 0; i < 10; i++) 
{ 
 //[Esto se ejecuta 10 veces] 
 
 if (i == 4) continue; 
 
 //[Esto se ejecuta 9 veces; se omite cuando i vale 4] 
} 
Excepciones 
Durante le ejecución de un programa pueden producirse errores, que normalmente 
desearemos interceptar y tratar adecuadamente. C# implementa (en combinación 
con el CLR) el sistema que se conoce como “gestión estructurada de excepciones”, 
que existe también en otras plataformas y entornos de trabajo. 
Típicamente, en alguna subrutina escrita por nosotros o por terceras partes, se 
detecta una situación imprevista. Por ejemplo, la subrutina debe leer un archivo en 
disco, pero el archivo no existe. En este caso, se desea informar al llamante de esta 
situación anómala. El llamante, que probablemente será a su vez otra subrutina, 
puede tener previsto código para tratar este error; si no lo hace, el error (que se 
denomina “excepción”) continúa propagándose a través de la pila de llamadas hasta 
54 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
llegar a alguna rutina que lo intercepte. Si no es así, y el error llega a “salirse” del 
Main, la ejecución del programa se interrumpe y se presenta un mensaje (bastante 
poco amigable) al usuario. 
La instrucción throw 
En la parte del código donde debe generarse la excepción se utiliza la instrucción 
throw. Detrás de ella se añade una instancia de un objeto que herede de la clase 
System.Exception. Aunque todavía no hemos examinado la sintaxis que se utiliza 
para instanciar clases (usando el operador new), no tiene demasiada dificultad 
entender el ejemplo que sigue: 
if (elArchivoNoExiste) 
{ 
 throw new Exception("El archivo no existe"); 
} 
Al llegar a la instrucción throw, la ejecución de la subrutina se interrumpe, y se 
pasa la excepción al llamante, sin que se lleguen a ejecutar las líneas escritas debajo 
del throw. 
Nota: No se deben utilizar las excepciones como mecanismo para transmitir 
intencionadamente información desde una subrutina a su llamante. Debe 
reservarse para situaciones imprevistas, en las que ocurre una circunstancia que no 
debería ocurrir nunca durante la ejecución normal del programa. 
En lugar de “new Exception” se puede utilizar cualquier otra clase hija de 
Exception, como por ejemplo DivideByZeroException. En las librerías del 
Framework vienen ya predefinidas múltiples excepciones, que siguen un criterio 
común de nomenclatura consistente en que el nombre siempre termina en el sufijo 
Exception. Las propias rutinas del Framework hacen un throw de estas 
excepciones cuando es pertinente, y también podemos lanzarlas nosotros en 
Plataforma .NET y Lenguaje C# | 55 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
nuestro propio código. Si necesitamos distinguir en nuestro código alguna 
excepción específica que no viene ya prevista en el Framework, podemos crear para 
ello nuestra propia clase heredando de System.Exception. 
Las instrucciones try...catch...finally 
Estas son las instrucciones que se utilizan en el código que antes hemos 
denominado “llamante” para interceptar las excepciones devueltas desde las 
subrutinas a las que llama. Veamos primero un ejemplo: 
try 
{ 
 AbrirConexiones(); 
 LlamarAUnMétodo(); 
 LlamarAOtroMétodo(); 
} 
catch (Exception ex) 
{ 
 Console.WriteLine("Error: " + ex.Message); 
} 
finally 
{ 
 CerrarConexiones(); 
} 
 
En el bloque try hay una serie de sentencias que se intenta ejecutar en secuencia. 
Si alguna de ellas produce una excepción, la ejecución del try se interrumpe y se 
pasa a ejecutar las sentencias que haya dentro del catch. Si todo el try termina 
con éxito, el catch no se ejecuta. Tanto en un caso como en el otro, al terminar la 
ejecución del try o del catch, se ejecuta el bloque finally (que puede omitirse si 
no se necesita). 
El bloque finally se ejecuta siempre, incluso aunque se abandone el try mediante 
una instrucción return. 
Es lícito introducir múltiples bloques catch, de forma que cada uno intercepte una 
excepción diferente, y por lo tanto se les pueda dar un tratamiento diferenciado. En 
este caso, se deben escribir por orden de forma que si una excepción es la clase 
madre de las otras, la madre vaya al final (de lo contrario, siempre se interceptaría 
ésta y las hijas no servirían para nada). 
try 
{ 
 //... 
} 
catch (OverflowException) 
56 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
{ 
 Console.WriteLine("Desbordamiento"); 
} 
catch (Exception ex) 
{ 
 Console.WriteLine("Error imprevisto: " + ex.Message); 
} 
Nótese que la variable que hemos llamado “ex”, que recibe una copia de la instancia 
lanzada con el throw, puede omitirse si no se usa dentro del catch. 
Si dentro del catch se necesita volver a lanzar la misma excepción para que “suba” 
con todos sus datos al siguiente llamante, puede hacerse escribiendo simplemente: 
throw; 
Como observación final para quienes vengan de programar de otros lenguajes del 
estilo de Java, hay que señalar que en C# no se declaran de ninguna manera cuáles 
son las excepciones que puede lanzar un determinado módulo. Simplemente se van 
escribiendo los throw que sean necesarios, y no se deja a nivel global ninguna 
constancia de cuáles son éstos. 
Desbordamientos aritméticos 
En C#, de manera predeterminada no se comprueba si las operaciones entre 
enteros producen un desbordamiento. Por ejemplo,supongamos que un valor de 
tipo int tiene ya el máximo valor permisible, y que lo incrementamos: 
int i = int.MaxValue; 
i++; 
Console.WriteLine(i); 
El resultado es -2147483648, que probablemente no es lo que se deseaba obtener. 
Sin embargo, no se produce ningún error. Si deseamos que el compilador genere 
código para detectar estas situaciones, podemos encerrar las instrucciones dentro 
de un bloque “checked”: 
checked 
{ 
 i++; 
} 
En este caso sí que se produce una excepción del tipo OverflowException al 
exceder el valor máximo del int. 
Plataforma .NET y Lenguaje C# | 57 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Por simetría, existe también una instrucción unchecked, que usualmente no es 
necesario utilizar dado que este es el comportamiento predeterminado. El 
comportamiento predeterminado para el control de desbordamientos aritméticos 
puede modificarse cuando se compila desde línea de comandos, añadiendo 
/checked+ o /checked- tras el CSC. 
Las instrucciones checked y unchecked también se pueden usar como si fueran 
métodos además de usarlas como sentencias. Esto permite construir expresiones en 
las que se comprueban los desbordamientos al evaluarlas: 
Console.WriteLine(checked(++i)); 
Operadores 
Las expresiones se crean mediante operadores y operandos. Los operadores indican 
cuáles son las operaciones que se aplican a los operandos. En C#, se usan algunos 
operadores (como suma, resta, multiplicación y división) comunes con otros 
lenguajes de programación, y algunos otros no tan comunes y que sólo resultarán 
familiares para quienes hayan trabajado con otros lenguajes de la misma familia. 
La tabla siguiente resume los operadores más comunes: 
 Igualdad: == (igual a), != (distinto de) 
 Comparación: <, >, <=, >= 
 Condicionales: && (y), || (o), ?: (operador ternario) 
 Incremento y decremento: ++, -- 
 Aritméticos: +, -, *, /, % 
 Asignaciones: =, +=, *=, /=, etc. 
 Desplazamiento de bits: <<, >> 
Quienes hayan trabajado con C, C++ o Java encontrarán estos operadores 
familiares, ya que en C# se utilizan de la misma manera. Para quienes provengan de 
otros entornos, pasamos a comentar algunos de los operadores menos evidentes. 
• Para realizar las operaciones lógicas (and y or) hay dos tipos de operadores: 
“&” y “&&” para el “and”, y “|” y “||” para el “or”. Para devolver un resultado 
booleano se usan “&&” y “||”, mientras que “&” y “|” se usan para realizar 
operaciones binarias entre enteros, aplicando bit a bit el “and” o el “or”. 
if (i == 1 && j < 7) 
58 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
{ 
 //... 
} 
Estas comparaciones abandonan la ejecución en cuanto se conoce el 
resultado. En el ejemplo anterior, si i no es igual a 1, se sale del if sin llegar 
a comparar la j con 7. Esto puede tener su importancia en caso de que 
alguna de las comparaciones contenga una llamada a un método con efectos 
colaterales (cosa que las buenas prácticas recomiendan evitar, pero no 
siempre se hace). 
• El operador ternario es similar al IIF de Visual Basic. Permite elegir entre
dos valores en función de una expresión booleana.
string resultado = i < 3 ? "Si" : "No";
• El operador de asignación (“=”) devuelve como resultado el valor asignado,
por lo que puede a continuación volverse a usar el resultado. Por ejemplo, la
siguiente línea asigna un 8 a las variables i, j y k:
i = j = k = 8;
• Las asignaciones compuestas permiten aplicar el operador sobre la propia
variable a la que se asigna el resultado. Por ejemplo, esta expresión:
i *= 2;
es equivalente a esta:
i = i * 2;
• El operador “%” realiza la operación que en otros lenguajes de programación
se conoce como “módulo”, refiriéndose al resto de la división. Por ejemplo,
11%3 devuelve 2 porque el resto de dividir 11 entre 3 es 2.
• Los operadores ++ y – incrementan o decrementan el valor de la variable a
la que se aplican, y devuelven el valor de la misma. Si se pone el ++ antes de
una variable, primero se incrementa y luego se devuelve el valor
incrementado, mientras que si se pone detrás, se devuelve el valor que tenía
antes de incrementarse.
• Otro operador poco conocido es “??”. Se utiliza para comprobar si una
expresión se evalúa como null, y devolver en ese caso un valor alternativo.
Por ejemplo:
string s1 = null;
//...
string s2 = s1 ?? "nada";
La última instrucción asigna a s2 la cadena s1 a no ser que ésta sea null, en
cuyo caso se le asigna la cadena “nada”.
Plataforma .NET y Lenguaje C# | 59 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Prioridad de los operadores 
El orden de prioridad que se emplea al evaluar los operadores es el mismo al que 
estamos habituados en otros lenguajes de programación. Por ejemplo, la 
multiplicación se realiza antes que la suma, por lo que la siguiente expresión arroja 
como resultado 11: 
1 + 2 * 3 + 4 
Por supuesto, en caso de que se deseen otras prioridades, se pueden usar paréntesis 
al igual que en la mayoría de los lenguajes. 
Los operadores is y as 
Estos dos operadores no estaban en la lista anterior porque son un poco diferentes a 
los demás: se utilizan para comparar objetos con tipos. 
El operador is compara un objeto con un tipo, y devuelve true o false según que 
el objeto sea o no de ese tipo. También devuelve true si el objeto es de una clase hija 
de la indicada (o si se indica una interfaz y el objeto la implementa). Por ejemplo: 
object obj = 123; 
//... 
if (obj is int) 
{ 
 int n = (int)obj; 
 //... 
} 
 
El operador as intenta convertir un objeto a un tipo concreto, y devuelve el 
resultado de la conversión si ésta es lícita, o null si no lo consigue. 
object obj = "Hola"; 
//... 
string s = obj as string; 
if (s != null) 
{ 
 // usar s ... 
} 
 
Sólo se puede hacer esto con un tipo-referencia (explicado en el próximo capítulo), 
ya que los tipos-valor no pueden contener null. Por eso hemos escrito el ejemplo 
de as con string en lugar de int. 
60 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Tanto el operador is como el as se utilizan frecuentemente cuando se trabaja con 
herencia de clases, que se cubre en este libro más adelante. 
A continuación 
Tras haber visto algunos elementos básicos del lenguaje, en el siguiente capítulo 
estudiaremos los distintos tipos de datos que se pueden declarar, así como la forma, 
características y variantes de dichas declaraciones. 
Plataforma .NET y Lenguaje C# | 61 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
62 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Sistema De Tipos 
Y Declaraciones 
De Variables 
Dado que el libro va dirigido a desarrolladores que ya tienen experiencia utilizando 
algún otro lenguaje de programación, entendemos que no es necesario explicar qué 
es y para qué sirve una variable. Aprender a declararlas es relativamente sencillo: 
en C# se escribe simplemente el tipo de dato seguido del nombre de la variable: 
int i; 
Esta declaración admite diversas modificaciones. Por ejemplo, se puede inicializar 
la variable en la misma línea que la declara: 
int i = 1; 
Y también se pueden declarar a la vez varias variables del mismo tipo: 
int i, j, k; 
Para escribir estas declaraciones necesitamos conocer cuáles son los tipos de 
variable que se pueden declarar, qué reglas deben seguirse para escribir los 
nombres, y cuál es el lugar del código en el que pueden escribirse las variables. En 
los siguientes apartados daremos unas indicaciones genéricas al respecto, sin entrar 
en muchos detalles para no alargarnos en exceso. 
Plataforma .NET y Lenguaje C# | 63 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
El sistema común de tipos 
Usualmente se abrevia como CTS por sus siglas en inglés. Forma parte del CLR, y lo 
comparten todos los lenguajes y herramientas de desarrollo de .NET. El CTS es un 
modelo que define las reglas que se aplican al declarar, utilizar y gestionar los tipos 
de datos. Es unainfraestructura que permite la interoperabilidad entre lenguajes de 
programación, así como la seguridad en el manejo de datos (“type safety”). 
El sistema de tipos de .NET define dos clases distintas de variables: las 
denominadas “tipo-valor” (value type) y las “tipo-referencia” (reference type). La 
principal diferencia es que las variables tipo-valor guardan directamente el valor 
que se les asigna en la dirección de memoria reservada para la variable. En cambio, 
las tipo-referencia lo que guardan es una especie de puntero (conocido como 
“referencia”) que señala a una zona de memoria asignada dinámicamente (conocida 
como el heap) que es la ubicación en la que realmente se guardan los datos. 
Estas diferencias entre tipos valor y tipos referencia influyen en el comportamiento 
de las variables a la hora de trabajar con ellas. Por ejemplo, si se copia una variable 
tipo valor en otra del mismo tipo, se realiza realmente una copia del valor 
almacenado. Pero si se copia una de tipo referencia, lo que se copia no es el valor 
(que está en otro sitio) sino la referencia que está almacenada dentro de la variable. 
Esto hace que la segunda variable tenga una copia de la misma referencia, y por 
tanto apunte a la misma copia de los datos a la que apuntaba la primera variable. Si 
modificamos los datos a través de la segunda variable, también cambiarán los datos 
que se ven a través de la primera variable. Este comportamiento puede sorprender a 
quienes no hayan trabajado con este tipo de datos, por lo que es importante conocer 
cuál de los dos tipos tiene cada una de nuestras variables. 
Esta diferencia de comportamiento puede ilustrarse con un par de ejemplos. El 
primero usa variables de tipo-valor, y se comporta como cabe esperar: 
int i, j; 
i = 5; //Valor a la primera variable 
j = i; //Copiamos en la segunda 
i = 6; //Cambiamos la primera 
//La segunda no ha cambiado: 
Console.WriteLine(j); //Escribe 5 
 
El segundo es muy similar, pero las variables son de tipo-referencia: 
int[] a = new int[1], b; 
a[0] = 5; //Valor a la primera variable 
b = a; //Copiamos en la segunda 
a[0] = 6; //Cambiamos la primera 
//La segunda cambia: 
64 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Console.WriteLine(b[0]); //Escribe 6 
En este caso las variables que hemos usado son de tipo “arreglo” (los arreglos se 
estudian un poco más adelante), que en .NET resulta ser un tipo-referencia, incluso 
aunque los elementos contenidos dentro del arreglo sean de tipo-valor. 
Los mismos ejemplos anteriores permiten observar también otra diferencia entre 
los tipos-valor y los tipos-referencia: Los tipos-referencia han de inicializarse 
mediante el operador new, que asigna memoria en el heap y devuelve una referencia 
a dicha memoria. Esta operación no se necesita para los tipos-valor, que reservan 
directamente la memoria necesaria para contener sus valores. 
Tipos Valor 
Los tipos-valor pueden ser de dos clases: 
Los tipos simples, como por ejemplo int, float, double, decimal, bool, etc. 
Los definidos mediante código, que pueden ser enums o structs. 
Tipos Referencia 
Se definen mediante la palabra clave class, que estudiaremos con algo más de 
detenimiento en la parte dedicada a orientación a objetos. Por ahora, valga 
simplemente un ejemplo: 
class Ejemplo 
{ 
 public int Dato; 
} 
//... 
Ejemplo miVariable = new Ejemplo(); 
miVariable.Dato = 7; 
Console.WriteLine(miVariable.Dato); 
Tipos simples 
En el siguiente cuadro se resumen los tipos simples más corrientes: 
Plataforma .NET y Lenguaje C# | 65 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Tipo Alias Descripción 
System.SByte sbyte Un byte con signo 
System.Byte byte Un byte sin signo 
System.Int16 short Entero con signo de 16 bits 
System.UInt16 ushort Como el short pero sin signo 
System.Int32 int Entero con signo de 32 bits 
System.UInt32 uint Como el int pero sin signo 
System.Int64 long Entero con signo de 64 bits 
System.UInt64 ulong Como el long pero sin signo 
System.Char char Un carácter Unicode 
System.Single float Número en coma flotante, 32 bits 
System.Double double Número en coma flotante, 64 bits 
System.Boolean bool Verdadero/falso 
System.Decimal decimal Número exacto en 128 bits 
 
Como se ve en la tabla, por cada tipo definido en el CTS (tal como System.Int32) 
hay un “alias” en C# tal como int. Da exactamente igual escribir en el código fuente 
cualquiera de las dos cosas; es una cuestión de preferencia personal, el compilador 
produce en ambos casos el mismo código ejecutable. 
Dado que habitualmente tenemos un “using System” al principio del código 
fuente, se puede omitir dicho prefijo en el código, escribiendo (por ejemplo) 
Double en lugar de System.Double. Su alias es double (con minúsculas), por lo 
que al final resulta que podemos escribir Double o double en el fuente y en los dos 
casos el resultado de la compilación es el mismo. Sin embargo, algunos 
desarrolladores se sorprenden al ver que estas dos palabras se muestran en distinto 
color en el IDE de Visual Studio. El motivo es que uno es un identificador y el otro 
es una palabra reservada. 
66 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Nombres de variables 
Reglas 
 Los nombres de variable deben comenzar por una letra, o el carácter “_”,
pero no por un número. El código fuente se puede salvar como Unicode,
lo que permite usar como “letras” caracteres de otros alfabetos (chino,
ruso, griego, etc.) También se admiten, por supuesto, eñes y vocales con
tilde.
 Después del primer carácter se pueden usar también números.
 No se puede usar como nombre de variable una palabra reservada del
lenguaje (como while, for, break, etc.)
Sugerencias de buen estilo 
 No utilizar variables que empiecen por “_”.
 No escribir los nombres todos en mayúsculas.
 Usar palabras completas para designar las variables, en lugar de
abreviaturas.
 No utilizar “notación polaca” (prefijos para indicar el tipo y alcance de la
variable). Con Visual Studio, el propio IDE presenta dinámicamente
esta información, por lo que no se necesita “contaminar” con ella los
nombres.
 Cuando los nombres contengan varias palabras, aplicar el estilo “Pascal”
(las palabras juntas poniendo en mayúsculas la inicial de cada una, por
ejemplo, estoEsUnaVariable). No separar las palabras con caracteres
de subrayado.
 Aunque es legal, no es buena idea realizar declaraciones de este tipo:
double DOUBLE; 
Plataforma .NET y Lenguaje C# | 67 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Variables locales 
Se pueden declarar variables dentro de cualquier bloque de sentencias rodeado de 
llaves. En ese caso, son locales a ese bloque y desaparecen al salir del mismo. Por 
ejemplo: 
for (int i = 0; i < 10; i++) 
{ 
 int j; 
 j = i + 1; 
 //... 
} 
 
Tanto la i como la j son locales al bucle, y desaparecen al salir del mismo. 
Como ya mencionamos con anterioridad, no es lícito en C# definir una variable con 
el mismo nombre dentro de otro bloque anidado en el anterior. 
Variables miembro de class o 
struct 
Dentro de una clase o estructura se pueden definir variables, que son visibles desde 
todos los miembros de la clase. Por ejemplo, obsérvese la variable m en este 
ejemplo: 
class Ejemplo 
{ 
 int m; 
 
 void metodo1() 
 { 
 m = 1; 
 } 
 void metodo2() 
 { 
 Console.WriteLine(m); 
 } 
} 
 
68 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Constantes y variables de sólo-
lectura 
Para declarar constantes en el código, se usa la misma sintaxis que al declarar una 
variable, pero se antepone la palabra clave const: 
const double e = 2.718281828; 
Los valores de las constantes se calculan en tiempo de compilación (una constante 
puede depender de otras constantes), y luego no pueden ser modificados. 
Para declarar variables de sólo-lectura, se usa la palabra clave readonly: 
readonly ArrayList lista; 
Las variables de tipo readonly se puedeninicializar en el constructor de la clase 
que las contiene (ver el capítulo dedicado a orientación a objetos), pero una vez 
terminada la ejecución del constructor ya no se les puede cambiar el valor. 
Constantes de tipo carácter y cadena 
Los valores constantes de tipo char se escriben entre comillas simples: 
char c = ‘A’; 
Las constantes de tipo string se escriben entre comillas dobles: 
string s = “ABC”; 
Dentro estas constantes, el carácter “\” se usa como “escape” para indicar que el 
carácter que le sigue tiene un significado especial. Por ejemplo, “\n” es un salto de 
línea, “\r” es un retorno de carro, y “\t” es un carácter de tabulación. 
Si queremos escribir realmente una “\”, tenemos que duplicarla: “\\”. Esto se vuelve 
molesto cuando hay que escribir una cadena larga con muchas de estas 
contrabarras, por ejemplo, una ruta de un archivo en disco. En estos casos, existe 
un truco para deshabilitar el uso de la barra como carácter de escape, que consiste 
en anteponer una “@” a la cadena: 
string s = @”c:\carpeta1\carpeta2\fichero.txt”; 
Plataforma .NET y Lenguaje C# | 69 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Constantes de tipo numérico 
Los valores numéricos se escriben directamente mediante una secuencia de dígitos. 
Si son valores de tipo “unsigned”, se añade al final una U: 
int i = 2000000000; 
uint n = 3000000000u; 
 
Similarmente, existen otros sufijos para especificar el tipo de valor. Para los float 
se usa una F, ya que de lo contrario una secuencia de dígitos con un punto decimal 
se considera un double. También se usa la letra M para indicar un valor tipo 
decimal. 
double d = 22.33; 
float f = 123.45f; 
decimal d = 67.89m; 
 
Se pueden expresar valores hexadecimales anteponiendo el prefijo 0x (cero equis): 
int crlf = 0x0d0a; 
 
Modificadores de Alcance 
Las variables “miembro” de una clase o estructura pueden ir precedidas de un 
identificador de alcance para modificar su visibilidad, por ejemplo: 
class Ejemplo 
{ 
 public int UnDato; 
 //... 
} 
Los modificadores disponibles son: 
private – Indica que la variable sólo es visible en el interior de la clase que la 
define. 
internal – Indica que la variable es visible desde cualquier otra clase que se 
compile dentro del mismo assembly junto con la clase que define la variable. 
public – Indica que la variable es visible desde cualquier otra clase, tanto si se 
encuentra en el mismo assembly como si se compila por separado. 
70 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
protected – Indica que la variable es visible desde las clases hijas de la clase que 
la define (tanto si dichas clases hijas se compilan en el mismo assembly como si 
no). 
protected internal – es un “or” de protected e internal, es decir, la variable 
es visible desde las clases hijas (estén o no en el mismo assembly) y también desde 
las otras clases del mismo assembly, sean o no clases hijas. 
Enumeraciones 
Hemos mencionado antes que entre los tipos-valor se encontraban las 
enumeraciones y las estructuras. Cuando hablamos de “enumeraciones”, nos 
referimos a tipos de datos que permiten almacenar una serie de valores discretos a 
los que se asigna un nombre. Veamos un ejemplo: 
enum Colores 
{ 
 Rojo = 1, 
 Azul = 2, 
 Verde = 3 
} 
El bloque anterior define un tipo de dato que se llama Colores, y que puede recibir 
los valores Colores.Rojo, Colores.Azul y Colores.Verde. Dichos valores se 
almacenan internamente dentro de un int, asignándole los valores 1, 2 y 3. Es 
opcional escribir dichos números; si no los especificamos, el compilador asigna 
valores consecutivos empezando por cero. 
Para declarar una variable de este tipo, se hace lo mismo que con cualquier otro 
tipo-valor, es decir, se escribe el nombre del tipo (“Color”) seguido del nombre de 
la variable. El siguiente fragmento declara una variable, le asigna un valor, y luego 
lo escribe. 
Colores c; 
c = Colores.Azul; 
Console.WriteLine(c); //Escribe "Azul" 
Nótese que al escribir la variable, a diferencia de lo que ocurre en otros lenguajes, se 
escribe el nombre del valor y no el número. También es posible realizar esta 
operación a la inversa mediante el método System.Enum.Parse: 
string s = "Verde"; 
c = (Colores)Enum.Parse(typeof(Colores), s); 
Console.WriteLine(c); 
Plataforma .NET y Lenguaje C# | 71 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Los enums tienen algunas características adicionales en las que no nos vamos a 
detener, como por ejemplo la posibilidad de marcarlos con el atributo [Flags], que 
permite que los distintos valores almacenados representen combinaciones de bits. 
Mencionemos, simplemente, que el uso de enums es corrientísimo en las librerías 
del Framework, y que el intellisense de Visual Studio nos ofrece automáticamente 
los valores de la enumeración cuando accedemos a un argumento de uno de estos 
tipos. 
 
En la imagen anterior, MessageBoxButtons es un enum, y la pantalla nos presenta 
los valores que puede tomar. 
Casting 
En el anterior ejemplo referido al método Enum.Parse hicimos uso de una 
construcción que no habíamos visto hasta ahora, y que se conoce en Inglés como 
cast (“lanzamiento”). Consiste en escribir entre paréntesis un nombre de tipo, por 
delante de una expresión. Lo que hace es convertir la expresión al tipo indicado. 
resultado = (tipo)expresión; 
En nuestro caso concreto, el método Parse tal como está definido en las librerías 
del Framework devuelve un System.Object, que es la clase madre de todas las 
demás y puede por tanto contener cualquier cosa. Pero dado que el resultado 
queremos guardarlo dentro de un Color, y que C# es un lenguaje robusto en cuanto 
a tipos (“type-safe”), necesitamos “forzar” esta conversión desde la clase madre a 
la hija. Esto se consigue gracias al cast, en el que conviene fijarse porque es una 
construcción que se usa muy a menudo al programar en C#. 
72 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Structs 
Mediante la palabra clave struct podemos definir un tipo-valor que contenga en 
su interior los datos escogidos por el desarrollador. El bloque siguiente presenta un 
ejemplo sencillo: 
struct Punto 
{ 
 public double X; 
 public double Y; 
} 
Esto define un struct que contiene dos campos públicos llamados X e Y. Para usar 
este tipo, se declara una variable en la manera habitual, y se accede a los campos 
interiores mediante el operador ”.”: 
Punto P; 
P.X = 1.0;
P.Y = 2.0;
Console.WriteLine("({0}, {1})", P.X, P.Y); //"(1, 2)"
Nótese que, a diferencia de lo que ocurriría si estuviéramos usando un tipo-
referencia, no es necesario asignar memoria mediante el operador new. Cuando 
declaramos “Punto p”, ya se asigna una variable con espacio suficiente para 
contener la X y la Y. 
En este ejemplo hemos empleado una sobrecarga del método WriteLine que 
recibe un especificador de formato y luego varios datos para ser insertados en el 
interior de esa cadena. Dentro de la cadena de formato, se marcan con {0}, {1}, 
etc. las posiciones en las que debe acomodarse uno de los parámetros que vienen 
detrás. Para quienes vengan de programar en C o C++, esto es el equivalente de lo 
que haría el método printf con sus especificadores del tipo %f dentro de la cadena 
de formato. Vale la pena recordar esta técnica porque resulta útil para dar formato 
de salida a los datos no sólo en aplicaciones de consola, sino también en cualquier 
otro tipo de aplicación gracias al método Format de la clase String, que funciona 
de la misma manera. 
Aunque el ejemplo que hemos indicado es muy simple, y es la forma más habitual 
de definir una estructura, en realidad los struct son mucho más sofisticados y 
pueden contener los mismos miembros que una clase: métodos, propiedades, 
eventos, etc. La principal restricción es que no se permite la herencia de structs, 
sino sólo la de clases. 
Plataforma .NET y Lenguaje C# | 73 
 
Guía práctica de desarrollo de aplicaciones Windows en .NETArreglos 
Un arreglo (“array”) sirve para contener una secuencia ordenada de elementos del 
mismo tipo. Los arreglos en C# pueden ser unidimensionales (que en ocasiones se 
conocen como “vectores”) o multidimensionales (“matrices”). 
En otros lenguajes de la misma familia (C o C++), la notación de los arreglos es 
intercambiable con la de punteros. En C#, en general no se hace uso de esta 
característica. De hecho, en C# no se pueden manejar punteros, salvo que se 
habiliten expresamente mediante una sentencia unsafe. Esto es tan poco usual, y 
tiene tantos inconvenientes, que ni siquiera vamos a mencionar cómo se hace. 
Declaración 
Para declarar un arreglo hay que indicar el tipo de los elementos, seguido del rango 
del arreglo (número de dimensiones), y el nombre de la variable. Por ejemplo, la 
siguiente línea declara un arreglo de strings de una sola dimensión: 
string[] cadenas; 
Y la siguiente declaración corresponde a un arreglo bidimensional: 
int[,] numeros; 
Nótese que en ninguno de los casos se indica el tamaño del arreglo. Estas variables 
no sirven para contener los datos, sino únicamente una referencia al lugar en el que 
se encuentran los datos (el heap). Antes de poder asignarle datos, será necesario 
asignar memoria en el heap mediante el operador new: 
cadenas = new string[10]; 
numeros = new int[3, 3]; 
 
Las anteriores sentencias reservan en el heap espacio para sendos arreglos de 10 
cadenas y de 3x3 enteros, respectivamente, y devuelven la referencia 
correspondiente para ser almacenada en la variable. Los arreglos en .NET son tipos-
referencia. 
Antes de seguir adelante, y para evitar que alguien se confunda, mencionemos un 
par de construcciones que se permiten en otros lenguajes de la familia de C, pero no 
en C#: 
tipo nombre[]; //No se permite en C# 
tipo[10] nombre; //Tampoco se permite C# 
 
74 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Se puede inicializar un arreglo en el momento de declararlo suministrando entre 
llaves una lista de valores: 
int[] lista = new int[3] { 7, 14, -1 }; 
Cuando se realiza la inicialización de esta manera, no es necesario escribir el “new”. 
La siguiente instrucción es equivalente a la anterior: 
int[] lista = { 7, 14, -1 }; 
Cuando el arreglo es multidimensional, se usa para la inicialización una sintaxis 
análoga pero anidando las filas entre llaves: 
int[,] matriz = { { 1, 2 }, { 3, 4 } }; 
Cuando se usa esta construcción es necesario aportar valores para todos los 
elementos; no se puede dejar parte de la matriz sin inicializar. 
Acceso a los elementos 
Para leer o grabar un elemento del arreglo, se escribe entre corchetes el valor del 
índice por cada una de las dimensiones del arreglo: 
int i = lista[2]; 
int j = matriz[0, 1]; 
En C#, los índices siempre empiezan a contarse desde cero. 
Los arreglos son de lectura/escritura: 
lista[0] = 55; 
Pero la instrucción anterior dará un error en tiempo de ejecución si el 
almacenamiento del arreglo no se ha inicializado antes mediante el operador new. 
Propiedades y métodos de los arreglos 
En C#, los arreglos son objetos, y exhiben propiedades y métodos a los que se puede 
acceder mediante el operador “.”. Por ejemplo, el tamaño de un arreglo puede 
consultarse mediante la propiedad Length:
int[] lista = { 1, 0, 0 }; 
int[,] matriz = new int[4,6]; 
Console.WriteLine(lista.Length); //Escribe 3 
Console.WriteLine(matriz.Length); //Escribe 24 
Plataforma .NET y Lenguaje C# | 75 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Si se desea conocer una por una las dimensiones de un arreglo multidimensional, 
puede usarse el método GetLength: 
int[,,] matriz = new int[2, 4, 6]; 
Console.WriteLine(matriz.Rank); //Escribe 3 
Console.WriteLine(matriz.GetLength(0)); //Escribe 2 
Console.WriteLine(matriz.GetLength(1)); //Escribe 4 
Console.WriteLine(matriz.GetLength(2)); //Escribe 6 
 
Algunas consideraciones sobre los 
arreglos 
 No se puede redimensionar un arreglo. Si es necesario cambiarle el tamaño, 
hay que inicializar un nuevo arreglo (con new), copiar los elementos del 
antiguo al nuevo, y luego asignar a la variable original la referencia al nuevo 
arreglo, permitiendo que el Garbage Collector destruya el antiguo. Ni que 
decir tiene que esto es muy poco eficiente, especialmente si hay que hacerlo 
de manera repetida en un bucle. Si se necesita un arreglo que pueda agregar 
o eliminar elementos dinámicamente, es preferible usar una colección que 
mantenga los elementos enlazados mediante punteros. En El Framework 
hay múltiples colecciones a nuestra disposición, como por ejemplo 
List<tipo>. 
 Los elementos del arreglo tienen que ser del mismo tipo. Desde luego, podría 
crearse un arreglo de Object, lo que permitiría almacenar cualquier clase 
de dato en cada posición del arreglo, pero esto tiene varios inconvenientes, 
como por ejemplo que se necesitan casts cada vez que se recupera un 
elemento, que el compilador no verifica la validez del tipo de cada uno, y 
que si son tipos-valor se producen operaciones de boxing y unboxing 
(explicadas más adelante). 
 No se pueden crear arreglos de solo-lectura. Si se necesita esta 
funcionalidad, hay colecciones del Framework que permiten obtenerla. 
 Recuérdese lo ya comentado cuando se trataron los objetos tipo-valor y tipo-
referencia, en el sentido de que si se copia una variable que contiene un 
arreglo, en realidad lo que se copia es la referencia a los datos, y por lo tanto 
los dos arreglos (original y copiado) apuntan a los mismos datos. En caso 
necesario, puede emplearse el método Clone del arreglo para copiar los 
elementos: 
int[,,] matriz2 = (int[,,])matriz.Clone(); 
76 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
 En el caso de arreglos unidimensionales, también se puede usar el método
CopyTo para copiar elementos de un arreglo a otro (ya inicializado).
Los tipos var 
En la versión 3.0 de C# se introdujo la posibilidad de declarar variables sin escribir 
expresamente su tipo. En lugar de ello, se usa la palabra var, y se inicializa la 
variable en la misma sentencia. El compilador deduce el tipo de dato a partir del 
valor con el que se inicializa la variable, y aplica ese tipo a la variable. Por ejemplo, 
consideremos las siguientes declaraciones: 
var i = 5; 
var s = "Hola"; 
var a = new ArrayList(); 
var c = new List<IEnumerable<decimal>>(); 
Después de compilar, el código que se genera es el mismo que si hubiéramos 
declarado las variables de esta manera: 
int i = 1; 
string s = "Hola"; 
ArrayList a = new ArrayList(); 
List<IEnumerable<decimal>> c = 
 new List<IEnumerable<decimal>>(); 
Como podemos ver, en algún caso la sintaxis puede simplificarse al usar var, sobre 
todo cuando los tipos tienen cierta complejidad, como es el caso de la última de las 
declaraciones anteriores. Sin embargo, el uso excesivo de var en lugares donde 
resulta innecesario disminuye la claridad del código ya que no es fácil ver el tipo que 
tienen las variables. El siguiente ejemplo presenta un caso extremo: 
var z = MiClase.MiMetodo(); //¿Qué tipo tiene z? 
Aunque este tipo de declaraciones son lícitas, pueden producir confusión, por lo que 
se recomienda no abusar de las declaraciones con var. 
Para simplificar declaraciones complejas como la de la variable c del ejemplo 
anterior, podemos crear un “alias” para su tipo de dato mediante la directiva using 
(la misma que hasta ahora usábamos para importar espacios de nombres) añadida 
al principio del archivo fuente: 
using lista = List<IEnumerable<decimal>>; 
//... 
lista c = new lista(); 
Plataforma .NET y Lenguaje C# | 77 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Dado que la palabra var puede introducir confusión y hacer que el código pierda 
claridad, y además hay una alternativa que permite evitarla, ¿por qué los 
diseñadores del lenguaje decidieron introducirla en la versión 3.0? La razón está en 
que en esa misma versión del lenguaje se introdujeron también los tiposanónimos, 
que permiten declarar clases sin asignarles un nombre. Dado que no tienen 
nombre, no es posible declarar variables de esa clase en la forma habitual 
(escribiendo el nombre de la clase), por lo que la única forma de declararlas es 
usando la palabra var. 
Aunque no es este el lugar idóneo para introducir los tipos anónimos, para 
comprender en qué consisten veamos un breve ejemplo que muestra cómo declarar 
un objeto de este tipo: 
var persona = new { Nombre="Pepe", Apellido="Pérez" }; 
 
Esta sentencia declara una clase con dos propiedades llamadas Nombre y 
Apellido, crea una instancia de la misma asignando los valores Pepe y Pérez a las 
propiedades, y asigna a la variable persona una referencia a la instancia creada. 
Como vemos, no es posible declarar persona anteponiendo el nombre de la clase, 
puesto que el nombre de la clase no aparece por ningún lado; el compilador genera 
un nombre interno que no es accesible desde el código fuente. 
Estos tipos anónimos resultarán especialmente útiles más adelante, cuando 
estudiemos las consultas integradas en el lenguaje (LINQ). Llegados a ese punto, 
nos encontraremos manejando con frecuencia la palabra var para poder declarar 
las variables que reciben los resultados de estas consultas. 
Algunas observaciones sobre var: 
 Sólo se puede declarar una variable con var si se inicializa en la misma 
línea. A estos efectos, inicializarla con null no es suficiente, puesto que 
no se puede deducir el tipo de la variable a partir de null. 
 No sirve para declarar variables débilmente tipadas, es decir, que 
permitan asignar distintos tipos de datos. Esto sucede, por ejemplo, con 
el var de JavaScript, donde una variable inicializada con un número 
entero permite más adelante asignarle un string. No es este el caso de 
C#. Una variable de tipo var inicializada con un valor entero se 
comporta exactamente igual que si la hubiésemos declarado de tipo 
int, y el compilador arroja un error si se le intenta asignar un string. 
Por lo tanto, la variable está igual de fuertemente tipada que si su tipo se 
hubiera declarado explícitamente en lugar de usar var. 
 Por el mismo motivo, no equivale al Variant de VB6, ni tampoco al Dim 
de VB6 (Aunque en VB.NET sí que hay una forma de usar Dim que 
78 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
equivale al var de C#). Tampoco equivale a usar un System.Object en 
.NET (a un object se le puede asignar un valor de cualquier tipo, pero 
no queda fuertemente tipado). 
Los tipos Dynamic 
En la versión 4.0 de C# (Visual Studio 2010) se introdujo un nuevo tipo de datos, 
que se designa con la palabra clave dynamic. Los objetos de este tipo permiten que 
se escriba en el fuente una llamada a cualquier miembro que se desee, pero no se 
verifica en tiempo de compilación la existencia del miembro correspondiente. En 
otras palabras, en tiempo de compilación se presume que un objeto dynamic 
soporta cualquier tipo de operación que escriba el desarrollador. Esto permite usar 
la variable de tipo dynamic para referirse de manera sencilla a datos desconocidos 
al compilar, por ejemplo los que vienen a través de una interacción con objetos 
COM, o del DOM de HTML, o extraídos de una clase de .NET mediante las APIs de 
System.Reflection. Lógicamente, el inconveniente es que si el objeto en cuestión 
no soporta realmente el miembro al que estamos llamando, se producirá un error 
en tiempo de ejecución. 
Por ejemplo, consideremos una clase como la del siguiente bloque de código, y una 
llamada como la que hay escrita debajo. Al compilar este código se producirá un 
error porque el compilador detecta que Metodo2 no existe. 
class Ejemplo 
{ 
 public Metodo1() { } 
} 
//... 
Ejemplo ej = new Ejemplo(); 
ej.Metodo2(); 
Sin embargo, podríamos declarar la misma variable como dynamic, según se 
muestra a continuación, y el código compilará correctamente. 
class Ejemplo 
{ 
 public Metodo1() { } 
} 
//... 
Plataforma .NET y Lenguaje C# | 79 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
dynamic ej = new Ejemplo(); 
ej.Metodo2(); 
 
Obviamente, este ejemplo concreto no tiene ningún sentido; aunque compila sin 
errores, inevitablemente fallará al ejecutarlo. Uno de los casos en que se pone de 
manifiesto la utilidad del tipo dynamic ocurre cuando la variable se conecta en 
tiempo de ejecución con un objeto que no era conocido en el momento de compilar. 
 
Un caso típico se presenta cuando tratamos de controlar desde nuestro código en 
.NET a través de COM una aplicación externa, como por ejemplo Word o Excel. 
Aunque no es este el lugar adecuado para hablar de COM/Interop, mencionemos 
simplemente que se trata de la tecnología que nos permite comunicar desde .NET 
con los objetos que utilizan el anterior estándar para componentes de software, 
conocido como COM (Component Object Model). Antes de la versión 4.0 de C#, nos 
encontrábamos con frecuencia escribiendo código de este tipo: 
Excel.Application app = new Excel.Application(); 
//... 
((Excel.Range)app.Cells[3, 3]).Value = "Ejemplo"; 
Excel.Range rango = (Excel.Range)app.Cells[3, 3]; 
 
El motivo de tener que hacer estos casts es que muchos de los métodos expuestos a 
través de COM aceptan diversas combinaciones de argumentos, por lo que están 
implementados como Object. Gracias a dynamic, el código queda más limpio y 
sencillo de escribir: 
dynamic app = new Excel.Application(); 
//... 
app.Cells[3, 3].Value = "Ejemplo"; 
Excel.Range rango = app.Cells[3, 3]; 
 
En realidad, los tipos dynamic son mucho más sofisticados que lo que se pone de 
manifiesto en esta pequeña explicación. Por ejemplo, se puede operar entre tipos 
dynamic, y el resultado en general es también dynamic. También se puede 
programar una clase de forma que al ser llamada desde un tipo dynamic, la clase 
sepa cuál fue el método llamado, y pueda ejecutar código en consecuencia aunque 
ese método concreto no exista en la clase. Todo ello se sale del alcance de este 
pequeño texto introductorio. 
80 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Los tipos Nullable 
Los tipos-referencia permiten asignarles el valor null para indicar que no 
contienen en ese momento ninguna referencia. Esto puede resultar útil en muchos 
casos. Por ejemplo, cuando se trae información desde una base de datos a variables 
en memoria, si el campo de la base de datos es NULL, se puede meter null en la 
variable para reflejar esa “falta de dato”. 
Sin embargo, los tipos-valor no permiten asignarles null. Estos tipos contienen 
directamente su valor final, no una referencia, por lo que no es lícito “anular” esa 
referencia que no existe. Si en algún caso necesitamos simbolizar tipos-valor que no 
contienen nada, podemos convertirlos en tipos-referencia por mediación de un tipo 
genérico que se llama Nullable<T>. Aunque aún no hemos estudiado los tipos 
genéricos, que se tratan más adelante en este libro, no necesitamos saber 
prácticamente nada sobre ellos para poderlos utilizar. 
Por ejemplo, supongamos que necesitamos un int que acepte null. Podemos 
declararlo así: 
Nullable<int> i; 
i = 1; //OK; 
i = null; //OK, pese a que “int n=null” no se admite. 
//Para usarlo: 
if (i.HasValue) //Comprueba si es null 
{ 
 int n = i.Value; //Saca el int del Nullable<int> 
} 
Es tan común el uso de Nullable que el lenguaje prevé una sintaxis abreviada para 
declararlos, consistente en añadir una interrogación detrás del nombre del tipo: 
int? i = null; //Equivale a Nulable<int>i = null; 
A continuación 
Hemos visto que las variables y arreglos, además de poder declararse localmente 
dentro de un bloque de sentencias, pueden ser también directamente miembros de 
una clase o estructura, en cuyo caso se conocen como campos (“fields” en la 
documentación en inglés). En el siguiente capítulo estudiaremos otro tipo de 
Plataforma .NET y Lenguaje C# | 81 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
miembros: los métodos, que permiten estructurarnuestro programa agrupando las 
sentencias en bloques con nombre. 
 
82 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Métodos 
Normalmente las aplicaciones se escriben subdividiéndolas en unidades 
funcionales, y no en un único bloque monolítico de sentencias consecutivas. En C#, 
los fragmentos unitarios se denominan métodos. Cada método se escribe dentro de 
una clase (o struct), y sirve para realizar una acción o calcular un valor. 
En algunos otros lenguajes, se denominan Subrutinas y Funciones, según que 
realicen una acción o calculen un valor. En C# no se hace distinción; la diferencia 
estriba en que en el primer caso el valor devuelto se designa como void. 
Declaración 
Un método consiste en una serie de sentencias agrupadas, a las que se asigna un 
nombre: 
tipodevuelto NombreDelMetodo(argumentos) 
{ 
 //sentencias... 
} 
Si el método no devuelve nada, se escribe void para el tipo devuelto. Si no se recibe 
ningún argumento, se escriben los paréntesis sin nada entre ellos: 
void MiMetodo() 
{ 
 Sentencia1; 
 Sentencia2; 
 //Este método no recibe ni devuelve nada 
} 
Si recibe argumentos, se escriben entre los paréntesis los tipos y nombres: 
int Sumar(int a, int b, int[] matriz) 
{ 
Plataforma .NET y Lenguaje C# | 83 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 int suma = a + b; 
 foreach (int c in matriz) suma += c; 
 return suma; 
} 
 
En este ejemplo vemos dos argumentos de tipo entero, y uno de tipo “arreglo de 
enteros”. De paso, vemos también cómo se devuelve el resultado, a saber, utilizando 
la sentencia return. 
Llamada a los métodos 
Para llamar a un método, se escribe su nombre seguido de los valores de los 
argumentos entre paréntesis. Opcionalmente, puede también recogerse el valor de 
salida en caso de que el método devuelva alguno. 
int resultado = Sumar(1, 2, new int[] { 3, 4 }); 
 
Console.WriteLine(resultado); //Escribe 10 
Si se necesita llamar a un método desde fuera de la clase que lo define, se antepone 
el nombre de la instancia de la clase, o el propio nombre de la clase en caso de que 
el método sea esté marcado como static: 
class Clase1 
{ 
 public static void MetodoEstatico() 
 { 
 Console.WriteLine("Hola 1"); 
 } 
 public void MetodoDeInstancia() 
 { 
 Console.WriteLine("Hola 2"); 
 } 
} 
 
class Clase2 
{ 
 public void Llamadas() 
 { 
 Clase1.MetodoEstatico(); 
 
 Clase1 x = new Clase1(); 
 x.MetodoDeInstancia(); 
 } 
} 
84 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
El ejemplo anterior muestra los métodos marcados como public para que puedan 
ser llamados desde fuera de la clase. Se aplican los mismos modificadores de 
accesibilidad que mencionamos cuando hablamos de la declaración de variables. 
Sobrecargas 
En C# es lícito declarar lo que se denominan “sobrecargas” (overloads) de un 
procedimiento. Consisten en múltiples declaraciones de procedimiento utilizando 
en todas el mismo nombre. A la hora de realizar las llamadas, el compilador deduce 
cuál de las sobrecargas debe ser invocada en función de la combinación de 
parámetros que se incluyan en la llamada. Por ejemplo, consideremos estas dos 
declaraciones: 
static int Sumar(int a, int b) 
{ 
 return a + b; 
} 
static int Sumar(int a, int b, int c) 
{ 
 return a + b + c; 
} 
Ahora es lícito realizar estas dos llamadas: 
resultado = Sumar(1, 2); 
resultado = Sumar(3, 4, 5); 
El compilador decide a cuál de los dos métodos llamar gracias al distinto número de 
argumentos. Es más, no sólo es lícito distinguir en función del número, sino 
también del tipo de los argumentos: 
static void Imprimir(decimal valor) 
{ 
 //... 
} 
static void Imprimir(string valor) 
{ 
 //... 
} 
Ahora se pueden hacer las dos llamadas siguientes, que se diferencian por el tipo de 
argumento: 
Imprimir(1); 
Plataforma .NET y Lenguaje C# | 85 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Imprimir("Hola"); 
 
Nótese que en el primer caso hemos pasado un valor de tipo int. El compilador 
“sabe” que hay una conversión predeterminada de int a decimal, y en 
consecuencia es capaz de resolver esa llamada invocando al procedimiento que 
recibe un valor tipo decimal como argumento. 
Lo que no es lícito es añadir sobrecargas que sólo se diferencien en el nombre (pero 
no el tipo) de los argumentos, o que se diferencien por el tipo del valor devuelto. En 
estos casos, el compilador no es capaz de resolver la ambigüedad en las llamadas. 
El uso de sobrecargas es sumamente común en las librerías del Framework. Cuando 
tecleamos el nombre de un método en Visual Studio, el intellisense nos ofrece una 
lista de sobrecargas junto con un par de flechas que nos permiten verlas una por 
una. 
 
Este mismo mecanismo de sobrecargas también está disponible en los 
constructores, que estudiaremos en el capítulo de orientación a objetos. 
Parámetros opcionales 
Esta característica es nueva de la versión 4.0 de C#. En versiones anteriores, si se 
necesitaba llamar a un método con distintas combinaciones de argumentos, era 
necesario sobrecargar el método. En la versión 4.0, tenemos además la posibilidad 
de declarar argumentos opcionales, cosa que se consigue asignándoles un valor 
predeterminado: 
int HacerAlgo(int a, int b = 1) 
{ 
 return a + b; 
} 
Cuando un argumento dispone de valor predeterminado, puede omitirse en la 
llamada al método. El siguiente ejemplo muestra dos llamadas a nuestro método 
HacerAlgo, donde vemos que el parámetro b se puede omitir: 
int n = HacerAlgo(1, 2); 
Console.WriteLine(n); //Escribe 3 
 
86 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
int m = HacerAlgo(5); 
Console.WriteLine(m); //Escribe 6 
Parámetros con nombre 
Otra característica también nueva en la versión 4.0 es la posibilidad de especificar el 
nombre de los argumentos al llamar a un método, en lugar de distinguirlos 
únicamente por su posición dentro de la lista de argumentos. Esto resulta 
especialmente útil cuando tenemos una declaración de un método con numerosos 
argumentos opcionales, y sólo queremos especificar el valor de algunos de ellos. 
void PresentarMensaje( 
 string texto = "Alerta", 
 string color = "Red", 
 int X = 10, 
 int Y = 10, 
 bool negrita = false) 
{ 
 //... 
} 
El método anterior podría invocarse, por ejemplo, así: 
PresentarMensaje(Y: 20, X: 0, texto: "Hola"); 
Obsérvese que los argumentos se reconocen por su nombre, y que el orden en el que 
se escriben es indiferente. 
Parámetros de entrada y salida 
De manera predeterminada, en C# los parámetros de los métodos se pasan por 
valor. Esto quiere decir que se pasa al método una copia del valor original que se 
escribió como argumento al realizar la llamada. Si el método modifica dicho valor, 
el programa llamante no recibe ese cambio, puesto que se realiza sobre una copia. 
static void EscribirMás(int numero) 
{ 
 numero += 1; 
 Console.WriteLine(numero); 
} 
Plataforma .NET y Lenguaje C# | 87 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
//... 
 
int numero = 7; 
EscribirMás(numero); //Escribe 8 
Console.WriteLine(numero); //Sigue siendo 7 
A causa de este comportamiento, no es posible devolver resultados desde el método 
al código llamante por mediación de estos parámetros. Por supuesto, si el método 
sólo ha de devolver un único dato, puede hacerlo a través del valor de retorno. Pero 
si debe devolver varios, tendremos que configurar uno o más parámetros para que 
pasen por referencia. 
En los lenguajes C o C++ realizaríamos este tipo de paso declarando el parámetro 
como puntero, y tomando la dirección del valor a pasar mediante el operador &. 
Pero ya hemos comentado que en C# normalmente no se manejan punteros. En su 
lugar, tomaremos referencias, que sirven al mismo objetivo pero son generadas 
automáticamente por el compilador. Se usan para ello las palabras clave ref y out. 
static int HacerAlgo(int num, 
 refint unDato, out int resultado) 
{ 
 unDato += num; 
 resultado = 5; 
 return 3; 
} 
 
//... 
 
int a = 2; 
int b; 
int n = HacerAlgo(10, ref a, out b); 
//a=10, b=5, n=3 
 
Como vemos, es necesario escribir ref o out tanto en la declaración del método 
como en la instrucción que lo llama. Ambas palabras ocasionan que se pase una 
referencia a la variable original. La diferencia es que cuando se usa out estamos 
informando al compilador de que esperamos recibir un resultado a través de ese 
parámetro, y en consecuencia el compilador genera un error si se nos olvida 
asignarle un valor dentro del método. 
En el ejemplo anterior, los datos que hemos pasado al método eran tipos-valor. 
Cuando se pasa un tipo-referencia, usualmente no es necesario usar ref, ya que lo 
que queremos es pasar “el valor de la referencia”. Los datos a los que apunta esa 
referencia sí que pueden ser modificados desde dentro del método. Si usáramos la 
palabra clave ref, estaríamos pasando “la referencia a la referencia”, cosa que nos 
permitiría modificar dentro del método no sólo el contenido de la variable, sino 
88 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
también la propia referencia en sí misma, por ejemplo, para realizarle una nueva 
asignación de memoria con new. 
Desde luego, lo anterior es bastante “lioso” y en general no se suele hacer. El paso 
de parámetros con out o ref se hace en muy contadas ocasiones en C#. Un caso 
concreto en el que se usa out dentro de las librerías del Framework es el método 
TryParse que existe en varias clases y cuyo objetivo es analizar un String y 
convertirlo a la clase correspondiente. Por ejemplo: 
string s = Console.ReadLine(); 
int n; 
bool ok = int.TryParse(s, out n); 
if (ok) 
{ 
 //... 
} 
Aquí tratamos de convertir a entero una cadena introducida por el usuario. Por 
supuesto, el usuario puede introducir algo que no se reconozca como número 
entero. El método TryParse intenta realizar la conversión, y devuelve true o false 
dependiendo de si lo consigue. En caso de tener éxito, el valor convertido se 
devuelve a través del parámetro tipo out. Vale la pena destacar la importancia de 
que el parámetro sea out en lugar de ref. Si fuera ref y tratáramos de usar el valor 
de n más abajo en el código, el compilador señalaría un error de “acceso a variable 
posiblemente no inicializada”. Gracias al out, el compilador sabe que esa variable 
será forzosamente inicializada dentro de TryParse, cosa que no estaría garantizada 
si se hubiera declarado como ref. 
Número variable de argumentos 
Es posible declarar un método que acepte un número arbitrario de argumentos. 
Esto se logra mediante la palabra clave params. Desde el punto de vista del 
llamante, el método aparenta tener un número ilimitado de sobrecargas, aceptando 
cualquier cantidad de parámetros. Desde el punto de vista del método, se recibe un 
único parámetro de tipo arreglo, que en su interior contiene todos los valores 
pasados en cada llamada. 
static void Sumar(string texto, params int[] sumandos) 
{ 
 int suma = 0; 
Plataforma .NET y Lenguaje C# | 89 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 for (int i = 0; i < sumandos.Length; i++) 
 { 
 suma += sumandos[i]; 
 } 
 Console.WriteLine("{0} {1}", texto, suma); 
} 
 
//... 
 
Sumar("Hola", 1, 2, 3); 
Sumar("Adios", 2, 4, 6, 8, 10, -1); 
 
Este ejemplo demuestra que además de los argumentos variables es también 
posible declarar otros argumentos fijos (string texto en este ejemplo). 
Hay varios métodos en las librerías del Framework que utilizan este mecanismo, 
como por ejemplo el Console.WriteLine que ya conocemos, o el método Split 
de la clase String, que permite trocear una cadena por un número arbitrario de 
caracteres que se aceptan como argumentos. 
 
 
A continuación 
Llevamos vistos dos tipos de miembros de las clases y estructuras: los campos y los 
métodos. En el capítulo que sigue estudiaremos un tercer tipo, que se denomina 
propiedades. Como veremos, las propiedades nos permiten exponer el estado de 
una clase de tal manera que se puedan interceptar los accesos de lectura y de 
escritura a sus miembros. Aprovecharemos también para examinar los indexadores, 
que se declaran usando una sintaxis derivada de la de las propiedades. 
 
90 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Plataforma .NET y Lenguaje C# | 91 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Propiedades 
Las propiedades constituyen un concepto tal vez novedoso para quienes hayan 
programado en otros lenguajes como C o C++ en los que no existe esta 
funcionalidad. En cambio en Visual Basic las propiedades vienen siendo de uso 
común desde hace ya muchas versiones. 
Desde el punto de vista del código que accede a las propiedades de una clase, éstas 
aparentan ser campos (variables públicas), ya que la sintaxis para acceder a ellas es 
la misma. Sin embargo, en el lugar en el que se encuentran definidas, las 
propiedades se diferencian de los campos en que pueden contener código que se 
ejecuta al asignarles valores y recuperar los mismos. De esta manera, las 
propiedades permiten validar los datos que se les asignan, y también realizar 
acciones cuando se recupera o modifica su valor. 
Desde luego, lo mismo se podría hacer empleando métodos, tales como GetDato() 
y SetDato() para leer y grabar los valores, y de hecho así se suele hacer en otros 
lenguajes en los que no existen las propiedades. Sin embargo, gracias a las 
propiedades se simplifican en muchas ocasiones el código. Comparemos las dos 
líneas siguientes: 
miClase.SetDato(miClase.GetDato() + 1); 
miClase.Dato++; 
Ambas líneas pueden realizar las mismas operaciones, a condición de que Dato sea 
una propiedad. 
Las propiedades son extremadamente comunes en las clases del Framework, e 
igualmente deberían de serlo en las clases desarrolladas por nosotros mismos. De 
hecho, las recomendaciones de estilo sugieren que nunca se añadan a las clases 
campos públicos. En su lugar, el estado de la clase debe exponerse al exterior 
exclusivamente a través de propiedades. 
A continuación veremos cómo se declaran y utilizan las propiedades. 
92 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Declaración 
Una propiedad es un miembro de una clase que proporciona acceso al estado de la 
misma de manera similar a como lo haría un campo de la clase. La declaración de la 
propiedad consiste en un tipo de dato y un nombre, igual que un campo, pero a 
continuación trae entre llaves uno o dos fragmentos de código que se conocen como 
accesores. Los accesores se llaman get y set, y sirven respectivamente para 
contener el código que se ejecuta al leer la propiedad y al asignarle un valor. 
Los accesores son análogos a la declaración de un método, pero no llevan 
argumentos. No es obligatorio que una propiedad contenga los dos accesores. Por 
ejemplo, se podría escribir únicamente el get, dando lugar a una propiedad de solo-
lectura. 
El siguiente código define una propiedad llamada Texto: 
private string texto; 
public string Texto 
{ 
 get 
 { 
 return texto; 
 } 
 set 
 { 
 texto = value; 
 } 
} 
Esta propiedad es pública y lo único que hace es almacenar y devolver un valor en 
un campo privado (que en la documentación en inglés se suele llamar backing field 
y podemos interpretar como “campo de almacenamiento”). 
Nótese que hemos escrito el nombre de la propiedad con la inicial mayúscula, 
mientras que su campo de almacenamiento se llama igual pero escrito todo en 
minúsculas. Este estilo es controvertido, ya que en general se considera 
desaconsejable declarar en un mismo fuente dos nombres iguales que difieran sólo 
en las mayúsculas o minúsculas. Sin embargo, este caso concreto en que se usa la 
inicial para distinguir la propiedad de su backing field suele ser comúnmente 
aceptado. 
Definida de esta manera, la propiedad vista desde fuera se comporta igual que si 
fuera uncampo público. Sin embargo, tiene la ventaja de que si es necesario se 
puede insertar código adicional, por ejemplo, para comprobar la validez de los 
valores asignados, sin afectar al código que hace llamadas a la propiedad. 
Plataforma .NET y Lenguaje C# | 93 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Invocación 
El accesor get es automáticamente invocado cuando se hace una lectura sobre la 
propiedad. Similarmente, se llama al set cuando se asigna a la propiedad un valor. 
string s = Texto; //Esto obtiene s a partir del get 
Texto = "abcd"; //Esto pasa "abcd" al value del set 
Dentro del accesor set, value es una palabra reservada que se usa para 
representar el valor asignado a la propiedad. 
Comparativa 
Las propiedades son similares a los campos desde el punto de vista de que la 
sintaxis para acceder a ellas es la misma. Pero se diferencian en que no representan 
directamente una ubicación de memoria, por lo que no se puede tomar una 
referencia a las mismas. Por tanto, no se puede pasar una propiedad como 
parámetro out o ref, mientras que un campo sí puede pasarse de esta manera. 
También son similares a los métodos en el sentido de que ambos pueden contener 
código ejecutable, sirven para ocultar al exterior los detalles de la implementación, 
y se pueden suplantar en una clase hija. Sin embargo, se diferencian en que la 
sintaxis de llamada es distinta, las propiedades no pueden recibir argumentos, y no 
pueden ser de tipo void. 
Al igual que los métodos y los campos, las propiedades pueden marcarse como 
static, en cuyo caso pueden ser llamadas a través del nombre de la clase que las 
contiene, sin necesidad de crear una instancia de la misma. Si estas propiedades se 
usan para acceder a un campo de almacenaje, entonces éste también tendrá que ser 
estático, por lo que únicamente existirá una copia “permanente” del mismo, en 
lugar de una copia por cada instancia de la clase. 
Propiedades automáticas 
El patrón del ejemplo anterior (una propiedad que en el get y el set simplemente 
lee o almacena su valor en un campo de almacenamiento) es de uso frecuentísimo 
cuando se definen clases. Para evitar repeticiones innecesarias de código, en la 
94 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
versión 3.0 de C# se introdujo una sintaxis alternativa que permite definir una 
propiedad de este tipo con menos líneas de código. Esta es la forma de escribirlas: 
public int MiPropiedad { get; set; } 
Como puede verse, el get y el set simplemente terminan en punto y coma y no 
contienen código. El compilador automáticamente declara un backing field (con un 
nombre interno que no es accesible para nosotros) y genera el código que lo lee y 
graba. 
Tan común es escribir propiedades de esta manera, que Visual Studio dispone de un 
mecanismo abreviado para generarlas. Basta con escribir prop y pulsar dos veces el 
tabulador para que se genere el bloque de código de la propiedad automática. 
Internamente, esta técnica funciona gracias a los snippets, bloques de código que se 
pueden insertar tecleando una abreviatura. Esta es una característica del editor de 
texto de Visual Studio que no tiene nada que ver con el lenguaje C# en sí mismo, y 
que se puede aplicar para generar muchos bloques de código de distintos tipos. No 
obstante, la mencionamos aquí por lo frecuente y cómodo que resulta su uso a la 
hora de definir propiedades. 
Indexadores 
Un indexador es un miembro de una clase que permite acceder a ella con una 
sintaxis similar a la que se usa para acceder a los arreglos. 
El motivo de incluirlos dentro de este capítulo es que la sintaxis que se usa para 
definir los indexadores es la misma que para las propiedades, con la peculiaridad de 
que la propiedad se tiene que llamar this, y que puede tomar uno o más 
argumentos (que luego reciben lo que serían los “índices” del arreglo cuando se 
hace la llamada al indexador). Este es un ejemplo: 
class MiClase 
{ 
 public string this[int índice] { 
 get { return BuscarDato(índice); } 
 set { GuardarDato(índice, value); } 
 } 
} 
Para llamar al indexador, se usa la clase como si fuera un arreglo: 
MiClase m = new MiClase(); 
m[27] = "Hola"; 
Plataforma .NET y Lenguaje C# | 95 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
string s = m[-1]; 
 
Obsérvese que el “índice” entre corchetes puede ser cualquier cosa. Si fuese un 
arreglo de verdad, el índice nunca podría valer -1, porque los arreglos siempre 
empiezan a indexarse a partir de cero. En el caso del indexador, ese valor se pasa a 
la variable índice, y es el código escrito en los accesores el que debe evaluarlo y 
procesarlo como se considere oportuno. 
Los indexadores son similares a los arreglos en el sentido de que se usa la misma 
sintaxis a la hora de llamarlos. Pero presentan múltiples diferencias: 
 Los indexadores pueden aceptar argumentos que no sean enteros, e incluso 
aunque sean enteros, pueden aceptar valores que no serían lícitos para un 
arreglo. 
 Los indexadores se pueden sobrecargar igual que los métodos. Es decir, se 
puede definir varias veces la propiedad this con distinto número o tipo de 
argumentos, y el compilador distingue entre ellas gracias a los índices 
suministrados entre corchetes al hacer la llamada. 
 Al igual que las propiedades, los indexadores no designan direcciones de 
almacenamiento, por lo que no se pueden pasar como parámetros out o 
ref (mientras que un elemento de de un arreglo sí que se puede pasar de 
esta manera). 
 A diferencia de los arreglos (y las propiedades), los indexadores no pueden 
ser de tipo static. 
A continuación 
Hasta ahora hemos hablado de las clases, estructuras y enumeraciones. Para 
terminar con todos los tipos (types) que se pueden definir en .NET, todavía nos 
faltan los delegados y eventos, que se presentan a continuación. 
 
96 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Delegados Y 
Eventos 
Los delegados y eventos permiten ejecutar código que se conecta de forma 
“dinámica”, es decir, sin que el código llamado y el código llamante queden 
enlazados de forma “fija” en el momento de compilar. Para ello, se interpone entre 
ellos una variable (el delegado o evento), que recibe en tiempo de ejecución la 
“dirección” del código de destino. Cuando el llamante quiere invocar al código 
llamado, obtiene el sitio de destino a partir de esa variable, y por lo tanto no tiene 
por qué llamar siempre al mismo sitio, ya que la variable puede cambiar durante la 
ejecución del programa. 
Para hacernos una idea del tipo de problemas que se pueden resolver mediante 
estas construcciones, consideremos el siguiente ejemplo: 
En las librerías del Framework se dispone de una clase llamada Cache que puede 
ser empleada por las aplicaciones Web para guardar temporalmente en memoria 
datos que resultan lentos de obtener desde una ubicación externa. La ventaja de 
usar el caché en lugar de guardar esos datos en una variable “normal” es que el 
caché opera de forma inteligente, y libera la memoria ocupada en caso de que el 
servidor la requiera para otro fin. El programa que llama al caché para almacenar 
un dato puede desear ser informado de cuándo el caché se ve obligado a descartar 
dicho dato. Por “ser informado” se entiende que debe ejecutarse algún método del 
código llamante, que se suele conocer como “retrollamada” (“callback”). Pero 
cuando los desarrolladores de Microsoft escribieron la clase Cache, no existía 
todavía el programa del que estamos hablando; no hay forma de que la clase Cache 
pueda contener ya compilada una llamada al método de retrollamada. Aquí es 
donde interviene el delegado: al llamar al Cache, el código llamante le pasa un 
argumento (de tipo delegado) que apunta a la rutina de retrollamada. El objeto 
Plataforma .NET y Lenguaje C# | 97 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Cache llama a dicho código de forma indirecta a través del delegado, como 
comentábamos al principio.Dentro de las librerías del Framework es muy corriente el uso de delegados, 
existiendo numerosas clases y métodos que hacen uso de ellos. Por supuesto, 
también es lícito e incluso frecuente declarar y utilizar estos tipos de dato en 
nuestro propio código. 
Los eventos se construyen empleando delegados, y aportan frente a los anteriores 
un mecanismo estandarizado para “conectar” y “desconectar” el código de destino a 
los delegados. Dentro de las librerías del Framework, y por supuesto también 
dentro del código que nosotros desarrollamos, el uso de eventos es (si cabe) aún 
más frecuente que el de los delegados, por lo que resulta conveniente familiarizarse 
cuidadosamente con su forma de empleo. 
Delegados 
Quienes vengan de programar en los lenguajes C o C++ pueden pensar en un 
delegado como un puntero a una función. Sin embargo, mientras que en C o C++ 
los punteros a funciones son inseguros, en el sentido de que se les puede asignar un 
valor que no apunte a una función del tipo adecuado, en cambio en C# el 
compilador sólo permite apuntar el delegado a un método que tenga la estructura 
adecuada. 
Declaración de un tipo de delegado 
Los delegados se declaran usando la palabra clave delegate, y escribiendo a 
continuación una declaración con la misma estructura con la que declararíamos 
uno de los métodos a los que puede apuntar este delegado. En el lugar donde 
escribiríamos el nombre del método, se escribe el nombre del delegado. Por 
ejemplo, el siguiente delegado sirve para apuntar a métodos que reciben como 
argumento un string y devuelven un int: 
delegate int MiTipoDelegado(string s); 
 
Al igual que las definiciones de clases se pueden realizar directamente dentro de un 
espacio de nombres, o se pueden anidar dentro de otras clases, también los 
delegados se pueden definir de forma independiente, o en el interior de una clase. 
98 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
En los dos casos, pueden opcionalmente acompañarse de una especificación de 
alcance. Por ejemplo, las tres declaraciones siguientes son válidas: 
public delegate void Delegado1(); 
class Clase1 
{ 
 private delegate void Delegado2(int n); 
 delegate int Delegado3(string s); 
} 
Desde fuera de Clase1, el tipo Delegado3 se llama Clase1.Delegado3, anidando 
los nombres de la misma manera que los anidaríamos si se tratara de una clase 
definida dentro de otra. 
Declaración de una instancia 
de un delegado 
Al igual que es importante distinguir entre la declaración de una clase y cada una de 
las instancias de dicha clase, cuando manejamos delegados ocurre lo mismo: por 
una parte declaramos los tipos de los delegados (como hemos visto en el apartado 
anterior), y luego creamos una o más instancias de dicho tipo. 
Las instancias se declaran exactamente igual que las variables ordinarias que hemos 
venido manejando hasta el momento, es decir, escribiendo el nombre del tipo 
seguido del nombre de la variable. Por ejemplo: 
Delegado1 miVariable; 
Además de declarar la variable, típicamente desearemos inicializarla, de forma que 
el delegado apunte a alguna rutina en concreto. La sintaxis que se utiliza es, una vez 
más, muy similar a la que usaríamos para crear una instancia de una clase, 
empleando el operador new: 
Delegado1 miVariable = new Delegado1(MiMetodo); 
//... 
void MiMetodo() 
{ 
 //... 
} 
Recordemos la definición que escribimos antes para Delegado1. Este tipo sirve 
para apuntar a métodos que no toman ningún argumento y no devuelven ningún 
resultado. Por lo tanto, el compilador nos permite apuntarlo a MiMetodo, que tiene 
precisamente esas características. 
Plataforma .NET y Lenguaje C# | 99 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
En la versión 2.0 de C# si introdujo una funcionalidad que se conoce como 
inferencia automática de tipos de delegados. Gracias a ella, es lícito omitir el new y 
el tipo de delegado al inicializar una variable de este tipo, a condición de que exista 
suficiente información de contexto para que el compilador pueda inferir cuál es el 
tipo que corresponde. Así, el ejemplo anterior se puede simplificar como sigue: 
Delegado1 miVariable = MiMetodo; 
//... 
void MiMetodo() 
{ 
 //... 
} 
 
Llamada a un método 
a través de un delegado 
Para concluir con el uso de los delegados, nos falta ejecutar el método al que apunta 
el delegado. Para ello basta con escribir el nombre de la variable en el mismo sitio 
en que normalmente escribiríamos el nombre del método si lo estuviéramos 
llamando directamente sin interponer un delegado: 
miVariable(); //Esto ejecuta MiMetodo 
 
Este ejemplo concreto usa un delegado que no recibe ningún argumento ni devuelve 
ningún resultado, pero por supuesto el mecanismo sigue siendo válido cuando el 
delegado es más complejo: 
class MiClase 
{ 
 delegate double Estadistica(double[] datos); 
 
 public void HacerCalculos() 
 { 
 double[] datosDePrueba = { 2, 3, 6 }; 
 
 Estadistica miCalculo = new Estadistica(Media); 
 double resultado1 = miCalculo(datosDePrueba); 
 
 miCalculo = Mediana; 
 double resultado2 = miCalculo(datosDePrueba); 
 //... 
 Console.WriteLine(resultado1); 
 Console.WriteLine(resultado2); 
 } 
 
100 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
 private double Media(double[] datos) 
 { 
 return datos.Average(); 
 } 
 private double Mediana(double[] datos) 
 { 
 //Hay que arreglar este método para que funcione 
 // bien... pero eso no afecta al mecanismo 
 // de llamada por mediación de un delegado. 
 Array.Sort(datos); 
 return datos[datos.Length/2]; 
 } 
} 
En este ejemplo hemos declarado una instancia del delegado Estadistica llamada 
miCalculo, y la hemos apuntado sucesivamente a dos métodos distintos (usando 
una de las dos veces la inferencia automática de tipos). Después lo hemos invocado 
pasando unos datos de prueba. Nótese que aunque las dos invocaciones son 
idénticas, los resultados son diferentes puesto que internamente el delegado apunta 
a una subrutina distinta en cada caso. 
Aunque esta es la forma más común de usar un delegado, los delegados también se 
pueden invocar a través de su método Invoke: 
resultado1 = miCalculo.Invoke(datosDePrueba); 
Alternativamente, se puede realizar la llamada mediante BeginInvoke, que 
permite ejecutar de forma asíncrona el método al que apunta el delegado. 
Delegados anónimos 
A partir de la versión 2.0 de C# se introdujo la posibilidad de inicializar delegados 
escribiendo directamente el código al que se conectan, sin necesidad de encapsular 
dicho código en un método y luego asignar al delegado el nombre del método. Dado 
que en este caso no existe en ningún sitio un nombre con el que referirse al método 
que hemos conectado, esta construcción se conoce como “delegado anónimo” o más 
propiamente “método anónimo”. 
Por ejemplo, esta es la forma tradicional de inicializar un delegado, usando un 
nombre de procedimiento: 
public delegate void MiTipoDeDelegado(int dato); 
public MiTipoDeDelegado MiDelegado; 
//... 
Plataforma .NET y Lenguaje C# | 101 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
MiDelegado = new MiTipoDeDelegado(MiMetodo); 
//... 
 
private void MiMetodo(int n) 
{ 
 Console.WriteLine(n); 
} 
 
Y esta es la forma de lograr el mismo resultado, omitiendo la declaración 
independiente del método: 
public delegate void MiTipoDeDelegado(int dato); 
public MiTipoDeDelegado MiDelegado; 
//... 
 
MiDelegado = delegate(int n){ Console.WriteLine(n); }; 
 
Como vemos, se usa la palabra reservada delegate, seguida de los argumentos si 
los hubiera, y luego entre paréntesis las sentencias que antes estaban dentro del 
método. En caso de que las sentencias no hagan uso de los argumentos, se puede 
omitir la declaración de los mismos que sigue a la palabra delegate. 
Una ventaja de esta construcción, aparte de la mayor brevedad del programa 
fuente, esque permite realizar lo que se denomina captura de variables. Sin 
detenernos a estudiar este tema, mencionemos simplemente que consiste en la 
posibilidad de acceder desde el cuerpo del método anónimo a las variables locales 
que se encuentran definidas dentro del método que inicializa el delegado. Esto no 
sería posible si ese mismo código estuviera escrito en un método separado, para el 
que no serían visibles dichas variables. 
Eventos 
Los eventos logran un efecto similar al de los delegados, en el sentido de que 
permiten establecer una conexión variable entre el código llamante y el código 
llamado. El caso de uso más típico consiste en que un objeto expone un evento al 
que otros objetos se suscriben y por su mediación, el que exponía el evento notifica 
a los demás la ocurrencia de algún cambio. 
Es muy común el uso de eventos en relación con interfaces gráficas de usuario tales 
como Windows Forms. Se dispone de una serie de clases agrupadas en librerías que 
encapsulan los controles que hay en pantalla. Cuando se produce un cambio de 
estado en la pantalla, por ejemplo, se pulsa un botón o se mueve el ratón por 
102 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
encima de una imagen, las rutinas de la librería disparan eventos que nuestro 
código “cliente” puede recibir para ejecutar acciones en respuesta a esos cambios de 
estado. 
A pesar de que los eventos se usan comúnmente en este tipo de aplicaciones, el 
lenguaje no los reserva para dicho uso, y es perfectamente legítimo declararlos y 
consumirlos en cualquier tipo de aplicación o librería, aunque no tenga nada que 
ver con ninguna interfaz gráfica de usuario. 
Declaración 
Para declarar un evento, se usa la palabra clave event, seguida del tipo del 
delegado que define el evento, y finalmente el nombre de variable que se le asigna. 
class MiClase 
{ 
 public delegate void MiDelegado(int dato); 
 public event MiDelegado MiEvento; 
} 
Como de costumbre, es opcional anteponer una especificación de alcance, aunque lo 
más típico es que los eventos sean públicos puesto que se suelen usar para notificar 
al exterior los cambios producidos dentro de la clase. 
Una vez realizada la declaración anterior, desde otros puntos del código se pueden 
realizar suscripciones a ese evento. 
Suscripción 
El código que se suscribe al evento utiliza el operador “+=” para conectar un 
método con el evento: 
MiClase x = new MiClase(); 
x.MiEvento += new MiClase.MiDelegado(x_MiEvento);
//...
static void x_MiEvento(int dato)
{
 //... 
} 
 Si fuera necesario desconectar el evento, puede utilizarse de manera análoga el 
operador “-=” para romper la conexión entre el evento y el método que le habíamos 
conectado. 
Plataforma .NET y Lenguaje C# | 103 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Si el delegado es de tipo void, es decir, no se espera que el método devuelva nada, 
entonces se dice que es de tipo multicast. En este caso, se permite repetir más de 
una vez la conexión con “+=”, de forma que más de un método quede conectado al 
evento. Al dispararse éste, se ejecutan por orden todas las rutinas que se le 
conectaron. 
Disparo 
Para completar el circuito, nos falta disparar el evento desde la clase que lo declaró, 
para que se ejecuten los métodos conectados desde la clase o clases cliente. Para 
ello, basta con llamarlo por su nombre como si fuera un método, añadiendo los 
argumentos que requiera. 
private void DispararElEvento() 
{ 
 if (MiEvento != null) 
 { 
 MiEvento(123); 
 } 
} 
 
En el ejemplo podemos observar que antes de llamar al evento hemos comprobado 
que no sea null. Se trata de una precaución para no tratar de llamarlo en caso de 
que no se le haya conectado ninguna rutina cliente, cosa que provocaría un error. 
Nótese que este código no es seguro ante la utilización en multi-hilo. Si nuestro 
programa tuviera múltiples hilos de ejecución, sería posible que un hilo pasara por 
el if encontrando que MiEvento no es null, y justo antes de hacerle la llamada a 
MiEvento, otro hilo lo desconectase dejándolo en null, con lo que la llamada 
fallaría. 
Hay varias formas de remediar esta situación. Una que se usa con cierta frecuencia 
consiste en inicializar el evento conectándolo a una rutina vacía, con lo que nunca 
es null: 
public delegate void MiDelegado(int dato); 
public event MiDelegado MiEvento = delegate(int i){ }; 
 
private void DispararElEvento() 
{ 
 MiEvento(123); 
} 
104 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Patrón convencional 
Aunque los eventos pueden tomar cualquier número de argumentos de cualquier 
tipo, las librerías del Framework de .NET siguen una convención específica a la 
hora de definirlos. Resulta muy recomendable seguir en nuestro código el mismo 
patrón cuando declaremos nuestros propios eventos, con el fin de facilitar la lectura 
y seguimiento de los fuentes a quien esté familiarizado con el criterio de librerías. 
Concretando: los eventos se definen empleando siempre dos argumentos, el 
primero de tipo object, y el segundo de una clase derivada de 
System.EventArgs. 
private void rutina(object sender, EventArgs e) 
{ 
 //... 
} 
En el primer argumento, que convencionalmente se llama sender (“remitente”), se 
devuelve siempre la instancia de la clase que disparó el evento (this en el código 
fuente). De esta manera, se puede usar una única rutina de tratamiento de eventos 
para conectarla a distintos objetos capaces de disparar el mismo evento, y así el 
código escrito en la rutina puede distinguir de dónde proviene la llamada. Un 
ejemplo típico sería el de ubicar varios botones sobre una interfaz gráfica, y 
conectar el evento Click de todos ellos a un mismo método: 
private void Button_Click(object sender, EventArgs e) 
{ 
 Button b = (Button)sender; 
 Console.WriteLine(b.Text); //texto del botón pulsado 
} 
Si el evento debe trasladar uno o más datos al suscriptor, se encapsulan todos esos 
datos dentro de una clase hija de EventArgs, y se pasa una instancia de esa clase 
dentro del segundo parámetro del evento, usualmente llamado “e”. 
Convencionalmente, esa clase hija suele llamarse igual que el evento, añadiendo el 
sufijo “EventArgs”: 
delegate void RatonClickHandler( 
 object sender, RatonClickEventArgs e); 
class MiClase 
{ 
 public event RatonClickHandler RatonClick; 
 void LanzarEvento(int X, int Y, bool dch) 
 { 
 if (RatonClick != null) 
Plataforma .NET y Lenguaje C# | 105 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 { 
 RatonClickEventArgs arg = 
 new RatonClickEventArgs { 
 CoordenadaX = X, 
 CoordenadaY = Y, 
 EsClickDerecho = dch }; 
 RatonClick(this, arg); 
 } 
 } 
} 
 
class RatonClickEventArgs : EventArgs 
{ 
 public int CoordenadaX { get; set; } 
 public int CoordenadaY { get; set; } 
 public bool EsClickDerecho { get; set; } 
} 
 
 
En este ejemplo observamos además otra convención, consistente en que el 
delegado que se usa para definir el evento lleva el mismo nombre pero añadiendo el 
sufijo “Handler”. 
Por supuesto, desde el punto de vista del lenguaje C# ninguna de estas 
convenciones es obligatoria. Al compilador le da lo mismo el texto que se use para 
los nombres y el número de argumentos que lleve el evento. Estas reglas aportan 
simplemente una norma de estilo que permite que nuestros eventos presenten un 
aspecto similar al que tienen los que vienen predefinidos en el Framework. 
Tan común es este patrón, que en las librerías del Framework a partir de la versión 
2.0 se incluyó una declaración genérica llamada EventHandler<T> para evitar 
tener que declarar por separado cada uno de los delegados que se usan con este tipo 
de eventos. No vamos a presentar un ejemplo, ya que aún no hemos introducido los 
tipos genéricos, que se tratan más adelante en este libro. 
Accesores para los eventos 
Hemos visto yaque las clases pueden tener campos públicos, pero que es más 
común exponer al exterior el estado de la clase mediante propiedades. Las 
propiedades vistas desde fuera tienen el mismo aspecto que los campos, pero 
aportan la ventaja de que se puede introducir código en los accesores get y set 
para ser ejecutado al leer o asignar la propiedad. 
106 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Los eventos disponen de una construcción similar, en la que los accesores se llaman 
add y remove, y permiten interponer código que se ejecuta al conectar o 
desconectar delegados al evento. Este es el aspecto que tiene su sintaxis: 
class MiClase2 
{ 
 private EventHandler miEvento; 
 public event EventHandler MiEvento 
 { 
 add 
 { 
 miEvento = (EventHandler) 
 Delegate.Combine(miEvento, value); 
 } 
 remove 
 { 
 miEvento = (EventHandler) 
 Delegate.Remove(miEvento, value); 
 } 
 } 
 //... 
} 
En este ejemplo hemos usado los métodos Combine y Remove para conectar y 
desconectar delegados. Esta sintaxis es un poco engorrosa, pero afortunadamente 
existe una forma abreviada de expresar lo mismo que consiste en aplicar los 
operadores “+=” y “-=” como vimos con anterioridad. 
Así como el uso de propiedades públicas en lugar de campos públicos es muy 
común, en cambio es muy poco frecuente emplear accesores para definir los 
eventos. Esta construcción sólo se utiliza en casos muy especializados, como por 
ejemplo al definir eventos enrutados en WPF (Windows Presentation Foundation). 
A continuación 
El capítulo que viene a continuación se titula “Orientación a objetos”, y nos 
enseñará cuáles son los elementos del lenguaje C# que nos permiten definir clases y 
controlar la herencia. También introduce el concepto de interfaz y su sintaxis en C#. 
Plataforma .NET y Lenguaje C# | 107 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Orientación a 
Objetos 
En este capítulo trataremos los elementos del lenguaje que permiten definir clases y 
trabajar con ellas. No pretende ser una introducción a la programación orientada a 
objetos, dado que presumimos que los lectores ya tienen experiencia previa 
utilizando otros lenguajes de desarrollo con esta característica. Por lo tanto, no nos 
detendremos a explicar conceptos tales como abstracción, encapsulación, herencia y 
polimorfismo, sino que directamente expondremos la sintaxis del lenguaje C# que 
se emplea para trabajar con ellos. 
Recapitulando lo que ya hemos visto: 
Las clases se definen mediante la palabra clave class, encerrando entre llaves el 
contenido de la clase. En su interior pueden contener campos, métodos, eventos, y 
otras definiciones de clases, enumeraciones, estructuras o delegados. Todos ellos 
pueden ir precedidos de un indicador de alcance, tal como public, internal o 
private. 
public class MiClase 
{ 
 internal string miCampo; 
 public int MiPropiedad { get; set; } 
 public void MiMetodo() { } 
 public event EventHandler MiEvento; 
 private class OtraClase { } 
} 
 
Las clases definen tipos-referencia, lo que implica que las variables del tipo de la 
clase no contienen directamente los valores internos (esencialmente, los que se 
encuentran almacenados en los campos de la clase). En cambio, contienen una 
referencia a la zona de memoria donde realmente se almacenan los datos (el heap), 
108 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
que debe ser asignada mediante el operador new. Se accede a los miembros de la 
clase a través de la referencia aplicando el operador “.”. 
MiClase m = new MiClase(); 
m.miCampo = "abcd";
No existe ningún operador para deshacer el efecto del new. Cuando ya no queda 
ninguna referencia a la instancia que se asignó en el heap, ésta queda 
automáticamente a disposición del recogemigas (Garbage Collector), que la libera 
cuando el CLR lo considera oportuno, sin que normalmente tengamos que 
ocuparnos de ello en nuestro código. Para anular expresamente una referencia, se le 
puede asignar el valor null: 
m = null; 
Si la variable m es local a un método, normalmente es innecesario asignarle null; 
simplemente desaparece al terminar el método. Aunque en otros lenguajes tales 
como Visual Basic 6 se recomienda anular las referencias asignándoles Nothing, 
incluso aunque sean variables locales, esto es superfluo en .NET. 
Aunque lo expuesto hasta aquí es suficiente para definir clases, así como crear, 
utilizar y liberar instancias, para aprovechar toda la funcionalidad que aportan las 
clases se requiere utilizar características adicionales, que se exponen en los 
epígrafes siguientes. 
Datos estáticos 
Se puede añadir el calificador static a los datos que se declaran dentro de una 
clase. Cuando se hace esto, las variables así declaradas se benefician de la 
encapsulación dentro de la clase, pero se asocian con la propia clase en sí y no con 
cada instancia de la clase. En otras palabras, existe una única copia estática de esos 
datos, incluso aunque nunca se cree una instancia de la clase. Los datos estáticos no 
se guardan en el heap y no se requiere una referencia para acceder a ellos. En su 
lugar, se accede a través del nombre de la clase. 
Igualmente, se puede aplicar la palabra clave static a los métodos y propiedades, 
en cuyo caso se invocan también a través del nombre de la clase y no de una 
instancia de la misma. Veamos un ejemplo: 
public class MiClase 
{ 
 private static string miCampo; 
Plataforma .NET y Lenguaje C# | 109 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
 public static string ObtenerDato() 
 { 
 return miCampo; 
 } 
} 
//... 
string algo = MiClase.ObtenerDatos(); 
 
Los métodos y propiedades de tipo static sólo pueden acceder directamente a 
campos estáticos, o llamar a otros métodos y propiedades también estáticos (si 
pudieran llamar a uno de instancia, ¿qué instancia se tomaría?). Por el contrario, 
las propiedades y métodos de instancia sí que pueden llamar a campos, propiedades 
y métodos estáticos. 
Es perfectamente razonable definir una clase que únicamente contenga métodos 
estáticos, y que no contenga ningún dato de instancia. Esto se hace, por ejemplo, 
cuando se desea crear una clase que contenga métodos utilitarios, que operen sobre 
los argumentos recibidos y no necesiten almacenar información cambiante según la 
instancia. En estos casos, la propia clase puede marcarse como static. Con ello, el 
compilador sabe que sólo se desea introducir en ella contenido estático, y nos señala 
el error en caso de que añadamos algún miembro de instancia. 
public static class Mates 
{ 
 public static double Pi { get { return 3.14; } } 
 
 public static double Seno(double ángulo) 
 { 
 //... 
 } 
 
 public static double Coseno(double ángulo) 
 { 
 //... 
 } 
} 
Creación de instancias 
Como ya sabemos, cuando se declara una variable del tipo de una clase, no se crea 
una instancia o un objeto de esa clase. Tan solo se reserva espacio en memoria para 
guardar una referencia a una instancia. Para crear la instancia y obtener una 
110 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
referencia a la misma, se usa la palabra clave new, que ya hemos mencionado con 
anterioridad. 
MiClase variable; //Esto no crea una instancia 
variable = new MiClase(); //Esto sí 
Al crear la instancia, se reserva memoria en el heap, se inicializa toda con ceros, y se 
devuelve una referencia a la misma, que en el ejemplo anterior hemos almacenado 
dentro de variable. 
Aunque lo anterior es suficiente para crear una instancia en los casos más simples, 
en muchas ocasiones desearemos inicializar con valores específicos ciertos datos de 
la instancia. Estos datos pueden pasarse como argumentos a un constructor 
definido en la clase. 
Constructores 
Un constructor se define creando un método con el mismo nombre que la clase, 
pero sin ningún valor devuelto (ni siquiera void): 
public class MiClase 
{public MiClase() 
 { 
 } 
 //... 
} 
En el ejemplo anterior, el constructor no recibe ningún dato ni realiza ninguna 
operación en concreto. Si únicamente necesitamos este constructor, no es necesario 
escribirlo, porque el compilador automáticamente lo genera por nosotros. Gracias a 
ello funcionaban los ejemplos que hemos visto hasta ahora, en los que no 
creábamos ningún constructor. 
Es lícito escribir código entre las llaves que abren y cierran el constructor. En ese 
caso, se ejecutan cuando se crea una instancia de la clase mediante new. También se 
pueden sobrecargar los constructores igual que los métodos, añadiendo entre los 
paréntesis distintas combinaciones de parámetros. 
public class Punto 
{ 
 public int x { get; set; } 
 public int y { get; set; } 
 public Punto() 
Plataforma .NET y Lenguaje C# | 111 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 { 
 x = 1; y = 1; 
 } 
 
 public Punto(int x, int y) 
 { 
 this.x = x; this.y = y; 
 } 
} 
//... 
Punto p1 = new Punto(); //equivale a (1,1) 
Punto p2 = new Punto(3, 7); 
 
En el caso de que definamos en nuestro código cualquier constructor, entonces el 
compilador ya no genera de forma automática el constructor predeterminado. Si 
necesitamos el predeterminado, tendremos que escribirlo expresamente como en el 
ejemplo que vimos más arriba. 
Aprovechemos para mencionar una construcción peculiar que aparece en el ejemplo 
anterior. Dentro del constructor que recibe los parámetros x e y, hemos inicializado 
las propiedades de la clase mediante this.x = x; this.y = y;. La palabra 
clave this hace siempre referencia a la propia instancia desde la que se llama a 
this. Por lo tanto, this.x se refiere a la “x” que existe dentro de la clase como 
variable de instancia. Esto nos permite diferenciarla de la “x” que se recibe como 
parámetro del constructor. 
Además de esta utilización de this para resolver la ambigüedad en los nombres de 
variables, se utiliza esta palabra clave cuando es necesario llamar a un método 
pasándole una referencia a una instancia de una clase, y queremos pasar la propia 
instancia que realiza la llamada. Vimos ya un ejemplo cuando hablábamos de los 
eventos, y pasamos this en el sender al disparar un evento. 
El ejemplo anterior muestra también cómo hacer uso de un constructor no 
predeterminado. Simplemente, se pasan los parámetros detrás de la llamada a new, 
igual que si se tratase de un método. 
Otra observación sobre el ejemplo anterior es que presenta duplicidad de código. 
Concretamente, la primera sobrecarga del constructor asigna (1,1) a las variables 
internas, y la segunda asigna dos valores arbitrarios a las mismas variables. 
Podríamos suprimir la primera copia del código haciendo que un constructor llame 
al otro. Sin embargo, los constructores no se pueden invocar directamente como si 
fueran métodos. En su lugar, se usa this en la propia definición del constructor: 
public class Punto 
{ 
 public int x { get; set; } 
 public int y { get; set; } 
112 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
 public Punto() : this(1,1) 
 { 
 } 
 public Punto(int x, int y) 
 { 
 this.x = x; this.y = y; 
 } 
} 
Estos dos constructores funcionan igual que los del caso anterior, pero ahora ya no 
hay código duplicado en el primero de ellos. Por supuesto, en este ejemplo tan 
simple la duplicidad era trivial, pero en casos reales en los que el constructor realice 
un número significativo de operaciones es conveniente evitar repeticiones usando 
este tipo de construcción. 
Aunque todavía no hemos hablado de herencia de clases, adelantemos que en caso 
de que nuestra clase heredase de otra, y el constructor tuviese que llamar al 
constructor de la clase madre, se usa para ello la palabra clave base, en lugar de la 
palabra this del ejemplo anterior. 
Constructores estáticos 
Se puede declarar un constructor con la palabra static. En este caso, no se usa 
para inicializar instancias, sino que el CLR lo ejecuta automáticamente para 
inicializar la clase. Por este motivo, los constructores estáticos a veces se llaman 
también constructores de clase. 
public class Poligono 
{ 
 static Graphics graphics; 
 static Poligono() 
 { 
 graphics = contenedor.CreateGraphics(...); 
 } 
 //... 
} 
Los constructores estáticos no pueden recibir parámetros ya que no habría ninguna 
oportunidad para pasárselos puesto que no los llamamos nunca desde nuestro 
código. Por la misma razón, tampoco llevan indicador de accesibilidad (public, 
internal, etc.). 
Plataforma .NET y Lenguaje C# | 113 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Destructores 
Un destructor se define con una sintaxis análoga al constructor, pero precedido de 
una tilde: 
public class Punto 
{ 
 //... 
 ~Punto() 
 { 
 //Liberar recursos 
 //... 
 } 
} 
 
No puede tener parámetros, ya que no hay oportunidad de pasárselos. Nunca 
llamamos explícitamente al destructor, sino que el CLR lo llama internamente de 
manera automática. El destructor a veces también se conoce como finalizador, ya 
que internamente se compila como si fuera un override del método Finalize 
heredado de System.Object. 
El proceso de destrucción es distinto del que estamos acostumbrados a ver en otros 
entornos de desarrollo, ya que los objetos no se destruyen inmediatamente cuando 
terminan de utilizarse. Veamos a grandes rasgos cómo funciona el mecanismo 
internamente: 
Cuando un objeto se convierte en inalcanzable (es decir, no queda ya ninguna 
referencia que apunte a ese objeto), automáticamente queda a disposición del 
Garbage Collector (GC). El GC de momento no hace nada. Cuando más adelante se 
detecta una situación de falta de memoria, se realiza una “pasada” liberando los 
objetos que ya no están en uso. 
Si un objeto tiene un finalizador, el proceso se ve ligeramente modificado, ya que al 
perderse la última referencia el objeto se pasa a una cola de destrucción. Por lo 
tanto, deja de ser inalcanzable, puesto que tiene una referencia dentro de la cola, y 
en consecuencia, el GC no puede liberar la memoria que ocupa. Se lanza un hilo de 
ejecución separado que va ejecutando los finalizadores de los objetos que hay en la 
cola, y tras ser ejecutados, se eliminan de la cola y entonces sí que se convierten en 
inalcanzables, pudiendo ser liberados en una segunda pasada del GC. 
Por lo tanto, añadir un destructor en una clase tiene la consecuencia de que los 
objetos de esa clase tardan más en ser liberados que si no tuvieran destructor. 
También tenemos que tener en cuenta que los destructores pueden ser llamados en 
cualquier orden (según les corresponda en la cola), por lo que cada uno no puede 
confiar en que existan o no existan todavía otros objetos relacionados con la clase 
114 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
que se está destruyendo en ese momento. Y también es conveniente tener presente 
que la destrucción se produce desde otro hilo de ejecución, cosa que puede tener su 
importancia dependiendo de cómo esté escrito el código y los objetos a los que 
acceda. 
Nota: El proceso de GC es más complejo de lo que da a entender la explicación 
anterior. Entre los factores que lo complican podemos contar los siguientes: 
• Puede haber varias colas de destrucción en máquinas con varias CPUs.
• Puede ocurrir que un objeto “resucite” desde la cola de destrucción si el
destructor vuelve a crear una referencia.
• El heap tiene varios niveles, y en cada pasada del destructor los objetos
van subiendo de nivel, y la liberación de memoria sólo se realiza sobre el
nivel más bajo (a no ser que sea insuficiente, en cuyo caso sube de
nivel).
• Hay un heap independiente para objetos de gran tamaño en el que se
evita moverlos de ubicación cuando se libera memoria.
En suma, el proceso de GC es complejo internamente y aquí sólo hemos arañado la 
superficie. Lo que conviene recordar es que, mientras sea posible,es preferible no 
incluir destructores en las clases de .NET. En general, sólo se necesitan cuando la 
clase inicializa recursos no-gestionados que se deban luego liberar. Mientras la clase 
sólo use recursos gestionados (escritos íntegramente con .NET, sin realizar accesos 
a recursos externos), normalmente nunca será necesario escribir un destructor. 
La sentencia using 
En los casos en los que una clase realmente asigna recursos no-gestionados que han 
de liberarse, es preferible liberarlos mediante un método en lugar de un destructor. 
Aunque el método podría llamase de cualquier manera, la convención consiste en 
llamarlo Dispose. Es tan común hacer esto que el Framework define para ello una 
interfaz llamada IDisposable, que únicamente contiene un método llamado 
Dispose. Teniendo en cuenta todo lo anterior, resulta que las clases que requieren 
destrucción se suelen escribir siguiendo un patrón que (de forma simplificada) 
resulta análogo al siguiente: 
class MiClase : IDisposable 
{ 
 public MiClase() 
 { 
 //Constructor: reserva recursos externos 
Plataforma .NET y Lenguaje C# | 115 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 } 
 
 public void Dispose() 
 { 
 LiberarRecursos(); 
 GC.SuppressFinalize(this); 
 } 
 
 ~MiClase() 
 { 
 LiberarRecursos(); 
 } 
 
 private void LiberarRecursos() 
 { 
 //Liberar los recursos externos 
 } 
} 
 
Como vemos, la clase implementa el método Dispose, que es el que normalmente 
invocaremos para liberar los recursos. Dentro de Dispose, se llama al método 
SupressFinalize del Garbage Collector, que suprime el funcionamiento del 
destructor. De esa manera, después de llamar a Dispose el objeto se comporta 
como si no tuviese destructor, evitándose así los inconvenientes que ya hemos visto 
que éste ocasiona. En caso de que el desarrollador olvidase llamar a Dispose, 
eventualmente se ejecutaría el destructor, con lo que los recursos se liberarían de 
todas maneras, aunque ello ocurra más tarde que si se hubiera llamado a Dispose. 
Así pues, la creación y uso de objetos de la clase debería seguir este modelo: 
MiClase m = new MiClase(); 
//Llamar a los métodos de m 
//... 
m.Dispose(); 
¿Qué pasaría si en una revisión posterior del código se introdujera (por ejemplo) un 
return antes de llamar a Dispose? 
MiClase m = new MiClase(); 
//... 
if (condicion) return; 
//... 
m.Dispose(); 
En este caso, cada vez que se cumpliese la condición se omitiría la llamada a 
Dispose, con el consiguiente perjuicio para el funcionamiento del programa. 
Para evitar este tipo de errores, el lenguaje dispone de una sentencia llamada using 
(no debe ser confundida con la directiva using que se introduce al principio del 
código para declarar espacios de nombres). Se utiliza así: 
116 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
using (MiClase m = new MiClase()) 
{ 
 //... 
 if (condicion) return; 
 //... 
} 
Las sentencias se escriben entre llaves, y cuando se abandona ese bloque, 
automáticamente se llama a Dispose sobre el objeto indicado dentro de los 
paréntesis. Esto ocurre incluso aunque el bloque se abandone antes de llegar al 
final, tal como simboliza el return de nuestro ejemplo. 
Para que se pueda usar using, es necesario que la clase implemente IDisposable, 
ya que el compilador traduce el bloque de esta manera: 
MiClase m = new MiClase(); 
try 
{ 
 //... 
 if (condicion) return; 
 //... 
} 
finally 
{ 
 ((IDisposable)m).Dispose(); 
} 
Normalmente no escribiremos este código, ya que resulta mucho más claro y simple 
emplear la sentencia using, especialmente cuando hay que anidar varias de estas 
construcciones unas dentro de otras. 
Herencia de clases 
En C# la herencia de clases se representa escribiendo el nombre de la clase madre 
detrás de la hija, separado por dos puntos. Por ejemplo, consideremos la clase 
Punto que ya habíamos definido en un ejemplo anterior y creemos una clase 
Punto3D heredando de ella: 
public class Punto 
{ 
 public int x { get; set; } 
 public int y { get; set; } 
} 
Plataforma .NET y Lenguaje C# | 117 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
class Punto3D : Punto 
{ 
 public int z { get; set; } 
} 
 
Aquí la clase Punto3D tiene las mismas dos propiedades x e y que tenía Punto y 
además la propiedad z que le hemos añadido. Esto es extensible a los campos, 
métodos y eventos, que igualmente se heredan desde la clase madre a la hija. 
Es corrientísimo heredar no sólo a partir de nuestras propias clases, sino también 
de las clases del Framework. Vimos ya un ejemplo al hablar de los eventos, cuando 
hicimos una clase que heredaba de System.EventArgs. 
En el Framework hay múltiples clases que están previstas precisamente para 
heredar de ellas, y que típicamente siguen la convención de tener un nombre que 
termine en el sufijo Base. Un ejemplo es la clase 
System.Collections.CollectionBase, que se usaba en las primeras versiones 
del Framework para heredar de ella y crear colecciones fuertemente tipadas, en 
lugar de usar las colecciones predefinidas de tipo System.Object (en versiones 
modernas es preferible crear estas colecciones mediante Genéricos, que 
estudiaremos más adelante). 
Nota: C# únicamente admite herencia simple (a diferencia de otros lenguajes como 
C++ que admiten herencia múltiple). Esto significa que cada clase hija sólo puede 
heredar de una única clase madre. Si se necesita heredar de más de una, puede 
simularse un comportamiento hasta cierto punto análogo gracias a la herencia de 
interfaz, que trataremos un poco más adelante. 
Clases selladas 
Se puede “sellar” una clase para impedir que se herede de ella usando la palabra 
clave sealed: 
public sealed class Punto 
{ 
 //... 
} 
Un ejemplo de clase sellada en el Framework es la clase String. Si intentamos 
heredar de ella, el compilador nos responde con un mensaje de error. 
118 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Sobrescritura 
La clase hija puede realizar lo que se conoce como sobrescribir o suplantar un 
método u otro miembro heredado de la clase madre (overriding en inglés). Para 
ello, primero el método tiene que estar marcado en la clase madre con la palabra 
clave virtual, indicando que se permite sobrescribirlo. A continuación, se 
sobrescribe en la clase hija marcándolo con la palabra override: 
public class Punto 
{ 
 //... 
 public virtual void Escribir() 
 { 
 Console.WriteLine("({0},{1})", x, y); 
 } 
} 
class Punto3D : Punto 
{ 
 //... 
 public override void Escribir() 
 { 
 Console.WriteLine("({0},{1},{2})", x, y, z); 
 } 
} 
Tras haber realizado las anteriores declaraciones, podemos escribir código como el 
siguiente: 
Punto p1 = new Punto { x = 1, y = 2 }; 
Punto3D p2 = new Punto3D { x = 1, y = 2, z = 3 }; 
p1.Escribir(); // Escribe (1,2) 
p2.Escribir(); // Escribe (1,2,3) 
Para hacer esto, no habríamos necesitado suplantar el método Escribir, habría 
bastado con agregar un método distinto en cada clase para lograr el mismo 
resultado. La utilidad del override se verá en el próximo apartado, cuando 
hablemos de polimorfismo. 
Si hubiéramos añadido el método Escribir en las dos clases sin marcarlo como 
virtual y override, Visual Studio habría generado un aviso diciendo que el 
método de la clase hija “esconde” el método de la clase madre. Si realmente es eso 
lo que deseamos hacer, podemos eliminar el aviso marcando el método de la clase 
hija con la palabra new (que en este caso no tiene nada que ver con el new que se 
emplea para instanciar clases): 
Plataforma .NET y Lenguaje C# | 119 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
public class Punto 
{ 
 //... 
 public void Escribir() 
 { 
 Console.WriteLine("({0},{1})", x, y); 
 } 
} 
 
class Punto3D : Punto 
{ 
 //... 
 public new void Escribir() 
 { 
 Console.WriteLine("({0},{1},{2})", x, y, z);} 
} 
 
Cuando se usa new para forzar que un método “esconda” el de la clase madre, el 
método ya no es polimórfico. A efectos del ejemplo anterior, en el que llamábamos a 
Escribir sobre p1 y p2, no se nota ninguna diferencia. Únicamente se notará 
cuando introduzcamos p2 dentro de p1 y entonces llamemos a Escribir sobre p1, 
en cuyo caso los resultados variarán dependiendo de que hayamos usado override 
o new. 
Aunque no tiene nada que ver con la sobrescritura de métodos, vamos a realizar un 
inciso para fijarnos en un pequeño detalle de los ejemplos anteriores. Observemos 
la sintaxis que hemos empleado para inicializar las instancias de Punto y Punto3D: 
Punto p1 = new Punto { x = 1, y = 2 }; 
En lugar de escribir entre paréntesis los parámetros del constructor, hemos 
indicado entre llaves una lista de asignaciones de valores a las propiedades del 
objeto. Internamente, el compilador crea una instancia del objeto, como si 
hubiéramos llamado a new Punto(), y luego asigna una por una sus propiedades. 
El efecto es el mismo que si las hubiéramos asignado a mano, pero el código fuente 
queda más breve escribiéndolo de esta manera. Esta construcción a veces se conoce 
como inicializador de clase, y es una novedad introducida con la versión 3.0 de C#. 
El inicializador de clase es compatible con el paso de parámetros al constructor, es 
decir, se pueden poner entre paréntesis los parámetros del constructor y además 
después añadir entre llaves los valores para algunas propiedades adicionales que 
también se desee inicializar. 
120 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Polimorfismo 
Una de las razones más importantes por las que se utiliza herencia de clases es para 
implementar el polimorfismo. 
En el contexto en el que nos movemos, y hablando en términos muy generales, 
“polimorfismo” se refiere a la capacidad que tienen ciertos objetos de responder de 
forma distinta ante la invocación de uno de sus miembros, a pesar de que a que el 
miembro invocado sea aparentemente el mismo en todos los casos. 
En términos de C#, dispondremos de varias clases hijas de una misma clase madre. 
En tiempo de compilación, se escribe en el programa una llamada a un método o a 
una propiedad de una variable que es del tipo de la clase madre. Sin embargo, en 
tiempo de ejecución, el método que realmente se ejecuta es el de la clase hija que se 
ha almacenado dentro de esa variable. Por eso se dice que el método es polimórfico 
(“tiene muchas formas”), ya que se ejecuta distinto código pese a que la llamada 
escrita en el programa se realiza a un método concreto definido en una clase madre. 
Para comprender cómo funciona, veamos un ejemplo. Vamos a emplear las mismas 
clases Punto y Punto3D que ya hemos visto en ejemplos anteriores y que 
reproducimos a continuación, y después vamos a instanciar un objeto de la clase 
hija y almacenarlo en una variable del tipo de la clase madre: 
public class Punto 
{ 
 //... 
 public virtual void Escribir() 
 { 
 Console.WriteLine("({0},{1})", x, y); 
 } 
} 
class Punto3D : Punto 
{ 
 //... 
 public override void Escribir() 
 { 
 Console.WriteLine("({0},{1},{2})", x, y, z); 
 } 
} 
//... 
Punto3D p2 = new Punto3D { x = 1, y = 2, z = 3 }; 
Punto p1 = p2; 
p1.Escribir(); // Escribe (1,2,3) 
Plataforma .NET y Lenguaje C# | 121 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Hemos llamado al método Escribir del objeto p1. Este objeto es de la clase 
Punto, cuyo método Escribir sólo escribe (x,y). Sin embargo, al ejecutar este 
código se escribe (x,y,z), que resulta ser el resultado del método Escribir de la 
clase Punto3D. Es decir, nuestro código fuente tiene escrita una llamada a un 
método de una clase madre, pero en realidad el método que se ejecuta es el de la 
clase hija. Se dice en este caso que el método Escribir es polimórfico porque se 
comporta de distintas formas dependiendo del objeto almacenado dentro de la 
variable sobre la que lo invocamos. 
El comportamiento polimórfico sólo ocurre cuando el método está marcado como 
virtual en la clase madre y override en la hija. Si hubiéramos usado en la clase 
hija la palabra clave new en lugar de override, habríamos interrumpido la cadena 
de polimorfismo, y se habría escrito únicamente (1,2) en lugar de (1,2,3). 
Clases abstractas 
Una clase abstracta declara uno o más miembros que son a su vez abstractos, lo cual 
significa que sólo se declaran pero no llevan escrita la implementación. La clase 
abstracta no se puede instanciar; sólo sirve como “modelo” para heredar de ella. En 
las clases hijas, se hace un override de los miembros abstractos, que entonces 
pasan ya a tener una implementación concreta. 
public abstract class Poligono 
{ 
 public abstract void Dibujar(); 
} 
public class Triangulo : Poligono 
{ 
 public override void Dibujar() 
 { 
 graphics.DrawLines(...); 
 } 
} 
 
En este ejemplo vemos que la clase Poligono contiene un método Dibujar 
declarado como abstract. Esto fuerza a que la propia clase se declare a su vez 
como abstract. Dibujar simplemente termina en punto y coma, no lleva código 
entre llaves (implementación). Esto lo convierte en un método abstracto. 
La clase hija, Triangulo, hereda de Poligono y realiza un override de Dibujar, 
que esta vez sí que lleva implementación. Se puede hacer un new de Triangulo y 
122 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
llamar a su método Dibujar, pero si tratásemos de hacer un new de Poligono el 
compilador generaría un error. 
Las clases abstractas sirven para actuar como base de una jerarquía de clases, 
proporcionando parte de la implementación y dejando que las clases hijas 
implementen el resto de los miembros marcados como abstractos. 
Un ejemplo de clase abstracta dentro de las librerías del Framework es la clase 
Stream, que define (entre otras cosas) tres métodos Read, Write y Seek para leer, 
grabar y posicionarse en un flujo secuencial de bytes. No se puede instanciar 
directamente un Stream, pero sí se puede crear un método que acepte un 
parámetro de tipo Stream (y de hecho, numerosos métodos del Framework así lo 
hacen). Dentro de uno de esos métodos, se puede llamar a Read, Write y Seek 
sobre el argumento, y en realidad se ejecutará Read, Write o Seek sobre la clase 
hija de Stream que se haya pasado dentro del argumento. En las librerías vienen ya 
predefinidas varias clases hijas de Stream, tales como FileStream para leer y 
grabar archivos en disco, o MemoryStream para leer y grabar bytes en un búfer en 
memoria. También podemos escribir nuestras propias clases heredando de Stream, 
poniéndose así de manifiesto su utilidad como clase base. 
Interfaces 
Una interfaz representa un “contrato” sintáctico y semántico que deben cumplir las 
clases que la implementan. 
Veamos, para comenzar, cómo se declara una interfaz: 
public interface IGuardar 
{ 
 void Grabar(string fichero); 
 bool Leer(string fichero); 
} 
Como vemos, se usa la palabra clave interface, y luego se declaran los miembros 
igual que si se tratase de una clase, pero escribiendo sólo las declaraciones, sin 
incluir ninguna implementación. Por convención, se suele utilizar para las 
interfaces un nombre que comience con la letra I. 
Los miembros de la interfaz son siempre públicos. No es lícito agregarles 
expresamente un modificador de acceso tal como public. 
Se habla indistintamente de “implementar” o “heredar” una interfaz. Una clase 
puede implementar una o más interfaces. También se permite que una interfaz 
Plataforma .NET y Lenguaje C# | 123 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
herede de otra. La sintaxis que se emplea para indicar que una clase implementa 
una interfaz es la misma que para indicar la herencia de clases, es decir, se escribe 
el nombre de la interfaz detrás del de la clase separado por dos puntos. 
public class MiClase : IGuardar 
{ 
} 
 
Al declarar que la clase implementa la interfaz, automáticamentenos obligamos a 
definir dentro de la clase todos los miembros declarados por la interfaz. Aunque 
podemos escribirlos a mano, si trabajamos con Visual Studio hay un truco para 
generar automáticamente el “esqueleto” de todos los miembros. Para ello, basta con 
hacer clic sobre el smart tag (una pequeña rayita azul) que aparece debajo del 
nombre de la interfaz. 
 
Seleccionando la opción “Implementar interfaz” se escriben automáticamente 
dentro de la clase los miembros que aún no existan: 
public class MiClase : IGuardar 
{ 
 public void Grabar(string fichero) 
 { 
 throw new NotImplementedException(); 
 } 
 
 public bool Leer(string fichero) 
 { 
 throw new NotImplementedException(); 
 } 
} 
 
Lógicamente, tendremos que editar la implementación de estos métodos para 
introducir el código oportuno en lugar del “throw new 
NotImplementedException”. 
La otra opción del smart tag, que nos ofrece implementar la interfaz 
explícitamente, genera este código: 
public class MiClase : IGuardar 
{ 
 void IGuardar.Grabar(string fichero) 
124 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
 { 
 throw new NotImplementedException(); 
 } 
 bool IGuardar.Leer(string fichero) 
 { 
 throw new NotImplementedException(); 
 } 
} 
Como vemos, difiere del anterior en que los métodos de la interfaz llevan como 
prefijo el nombre de la interfaz. Cuando se declaran de esta manera, no pueden ser 
invocados directamente sobre la clase, sino que siempre tienen que llamarse a 
través de la interfaz. Sin embargo, puede ser que tengamos que usar este tipo de 
declaración explícita en caso de que la clase implemente a la vez más de una 
interfaz, y se dé la coincidencia de que dos de ellas contengan un método con el 
mismo nombre. 
Para hacer uso de las interfaces, se usa un mecanismo análogo al que utilizábamos 
con la herencia de clases: se declara una variable del tipo de la interfaz (que 
equivale a lo que antes era la clase madre), se introduce dentro de la variable una 
clase que implemente la interfaz (equivale a la clase hija), y se ejecutan los métodos 
de la interfaz (equivale a hacer llamadas polimórficas sobre la clase madre). 
IGuardar x = new MiClase(); 
x.Leer("C:\\archivo1.txt");
//...
x.Grabar("c:\\archivo2.txt");
El uso de interfaces en el Framework es corrientísimo, empleándose para declarar 
funcionalidades de clases que no tienen nada que ver entre sí, pero a las que se 
desea dotar de un determinado comportamiento común. Ya vimos un ejemplo 
cuando mencionamos la sentencia using, que se puede aplicar sobre las clases que 
implementen la interfaz IDisposable. 
A continuación 
El próximo capítulo nos enseña cómo sobrecargar los operadores de C#, de forma 
que nos permitan escribir líneas de código en las que se realicen operaciones de 
“suma” o de otro tipo, no sobre los tipos de datos primitivos de C#, sino sobre 
instancias de clases definidas por nosotros. 
Plataforma .NET y Lenguaje C# | 125 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Sobrecarga De 
Operadores 
Los operadores existen con el fin de que las expresiones sean más claras y fáciles de 
entender. Sería posible crear un lenguaje de programación sin ningún operador, 
empleando en su lugar llamadas a métodos para realizar todas las operaciones. Por 
ejemplo, si no existiese el operador “+”, podríamos tener a cambio un método 
“Sumar” que realizase la misma operación. En otras palabras, en lugar de escribir 
a=b+c escribiríamos a=Sumar(b,c). Sin embargo, esto se vuelve molesto y poco 
claro cuando hay que hacer una operación más compleja: 
a = Multiplicar(Sumar(b, c), Sumar(Sumar(d, e), f)); 
a = (b + c) * (d + e + f); 
 
Las dos líneas anteriores realizan la misma operación, pero es más legible y más 
sencilla de escribir la segunda variante. 
Este ejemplo utilizaba los operadores “+” y “*” para representar la suma y 
multiplicación aritmética. Esta forma de usar esos operadores viene ya incorporada 
“de fábrica” en el lenguaje. Pero es también posible redefinir los operadores para 
que operen sobre objetos programados por nosotros. Por ejemplo, podríamos tener 
una clase “Pedido” que contuviera diversos artículos pedidos por un cliente, y 
podríamos redefinir el operador “+” para que operase entre objetos del tipo 
“Pedido”. Esto nos permitiría escribir instrucciones del tipo: 
PedidoUnificado = Pedido1 + Pedido2; 
La simplicidad de esta notación, frente al uso de un método tal como 
JuntarPedidos(pedido1, pedido2) se pondría de manifiesto sobre todo si 
tuviéramos instrucciones largas con sumas y restas para añadir y retirar pedidos. 
Algunos operadores predefinidos tienen ya varias sobrecargas. Por ejemplo, el 
operador “+” realiza una suma aritmética si se intercala entre valores numéricos, 
126 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
pero si se intercala entre cualquier objeto y un string realiza una llamada al 
método ToString del objeto, y a continuación una concatenación de la cadena 
resultante con el string. 
De la misma manera, podemos redefinir operadores para que realicen las 
operaciones de nuestra elección al aplicarlos sobre instancias de nuestras propias 
clases. 
Forma de realizar la sobrecarga 
En otros lenguajes como C++, para redefinir un operador se crea un método de 
instancia dentro de una clase, que recibe un argumento sobre el que operar, y 
realiza la operación contra el this (la instancia en la que está escrito el método). 
Los creadores de C# consideraron que este mecanismo resultaba demasiado 
confuso, y decidieron que la sobrecarga se realizaría mediante métodos estáticos, 
que reciben los dos argumentos sobre los que actúa el operador. 
Veamos directamente un primer ejemplo: 
class Pedido 
{ 
 public List<Articulo> Articulos { get; set; } 
 //... 
 public static Pedido operator+(Pedido p1, Pedido p2) 
 { 
 List<Articulo> resultado = 
 new List<Articulo>(p1.Articulos); 
 resultado.AddRange(p2.Articulos); 
 return new Pedido { Articulos = resultado }; 
 } 
} 
En la clase Pedido, que contiene una lista de objetos del tipo Articulo, hemos 
definido un método estático que en lugar de tener un nombre “corriente” tiene el 
nombre “operator+”. El método recibe dos argumentos de tipo Pedido, y 
devuelve un resultado también del tipo Pedido. Una vez definido este método, 
desde fuera de la clase podemos hacer operaciones como estas: 
Pedido pedido1 = new Pedido(...); 
Pedido pedido2 = new Pedido(...); 
Pedido total = pedido1 + pedido2; 
Es decir, podemos aplicar la suma entre dos pedidos para dar lugar a un nuevo 
pedido. El detalle principal que hay que tener en cuenta es que el nombre del 
Plataforma .NET y Lenguaje C# | 127 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
método tiene que usar la palabra clave operator, seguida del símbolo del operador 
que se desea sobrecargar. 
Es lícito sobrecargar varias veces un operador, de la misma forma que 
sobrecargaríamos cualquier método, es decir, cambiando los tipos de los 
parámetros. Por ejemplo, podríamos añadir otra sobrecarga al “+” que nos permita 
añadirle un nuevo Artículo a un Pedido: 
public static Pedido operator +(Pedido p1, Articulo a) 
{ 
 List<Articulo> resultado = 
 new List<Articulo>(p1.Articulos); 
 resultado.Add(a); 
 return new Pedido { Articulos = resultado }; 
} 
 
Ahora se pueden escribir operaciones como estas: 
Pedido pedido1 = new Pedido(...); 
Pedido pedido2 = new Pedido(...); 
Articulo art1 = new Articulo(...); 
 
Pedido total = pedido1 + art1; 
total += pedido2; 
 
Nótese que al sobrecargar el “+”, automáticamente ha quedado también 
sobrecargado el “+=”. 
Operadores restringidos 
No todos los operadores se pueden sobrecargar con sencillez de la misma forma que 
hemos sobrecargado el “+”. Algunos presentan ciertas restricciones. 
 Los operadores “&&” y “||” no se pueden sobrecargar directamente, pero 
sí se pueden sobrecargar los operadores “&” y “|”,que de manera 
indirecta influyen en el resultado de “&&” y “||”. 
 Los operadores de comparación, “<” y “>” siempre deben sobrecargarse 
los dos juntos, de forma que cuando se cumpla que a>b, también se 
debe cumplir que b<a. Lo mismo pasa con “<=” y “>=”. 
 Los operadores “==” y “!=”, análogamente, deben definirse en pareja, de 
forma que su interpretación de cuándo dos objetos son iguales sea la 
misma. Para evitar confusiones entre los desarrolladores, es 
conveniente además hacer un override del método Equals (heredado 
128 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
de System.Object) para que también haga la misma interpretación de 
igualdad. 
 Y aunque no tiene nada que ver con la sobrecarga de operadores,
aprovechamos para mencionar que cuando se hace esto último (un
override de Equals), también se debe hacer un override del método
GethashCode de tal manera que dos objetos que se consideren iguales
tengan siempre códigos de hash iguales. De lo contrario, fallarían las
clases que almacenan objetos en tablas de hash, tales como Hashtable
o Dictionary<K,T>.
Operadores de conversión 
Existen conversiones implícitas entre ciertos tipos de datos. Por ejemplo, siempre 
podemos asignar un valor de tipo int a uno de tipo long: 
int entero = 7; 
long largo = entero; 
También existen conversiones explícitas entre tipos de dato, que usan la sintaxis 
que conocemos como cast: 
long largo = 0; 
int entero = (int)largo; 
Las conversiones de los dos ejemplos anteriores vienen ya predefinidas, pero 
también podemos agregar a nuestras clases conversiones implícitas y explícitas para 
que resulte más simple en el código fuente convertir unas en otras. Por supuesto, 
todas las conversiones se podrían realizar mediante métodos, pero como ya vimos 
un poco más arriba, el propósito de los operadores, incluyendo los de conversión, es 
simplificar el código y hacerlo más legible. 
Los operadores de conversión se definen mediante la palabra clave operator, igual 
que la sobrecarga de operadores aritméticos, pero además requieren que se indique 
la palabra explicit o implicit para especificar, respectivamente, si la 
conversión es explícita o implícita. 
public static explicit operator Pedido(Articulo a) 
{ 
 return new Pedido(a); 
} 
public static implicit operator Articulo(string nombre) 
Plataforma .NET y Lenguaje C# | 129 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
{ 
 return new Articulo { Nombre = nombre }; 
} 
 
Después de escribir las anteriores declaraciones, se pueden ejecutar sentencias 
como estas: 
//Conversión implícita: 
Articulo art2 = "Foxtrot"; 
 
//Conversión explícita: 
Pedido pedido3 = (Pedido)art2; 
 
Si se añade una conversión de una clase a string, es conveniente hacer también un 
override del método ToString heredado de System.Object, de forma que 
ambos mecanismos de conversión a string devuelvan el mismo resultado. 
Reproducimos, finalmente, el ejemplo completo con las dos clases Pedido y 
Articulo incluyendo constructores, operadores y conversiones. 
class Program 
{ 
 static void Main(string[] args) 
 { 
 Pedido pedido1 = new Pedido("Alfa", "Bravo"); 
 Pedido pedido2 = new Pedido("Gamma", "Delta"); 
 Articulo art1 = new Articulo() { Nombre = "Echo" }; 
 
 //Sobrecarga de suma: 
 Pedido total = pedido1 + art1; 
 total += pedido2; 
 
 Console.WriteLine(total); 
 
 //Conversión implícita: 
 Articulo art2 = "Foxtrot"; 
 
 //Conversión explícita: 
 Pedido pedido3 = (Pedido)art2; 
 
 Console.WriteLine(pedido3); 
 
 Console.ReadLine(); 
 } 
} 
 
class Pedido 
{ 
 public Pedido(params Articulo[] art) 
 { 
130 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
 this.Articulos = new List<Articulo>(art); 
 } 
 public List<Articulo> Articulos { get; set; } 
 //Primera sobrecarga de suma: Pedido+Pedido 
 public static Pedido operator +(Pedido p1, Pedido p2) 
 { 
 List<Articulo> resultado = 
 new List<Articulo>(p1.Articulos); 
 resultado.AddRange(p2.Articulos); 
 return new Pedido { Articulos = resultado }; 
 } 
 //Segunda sobrecarga de suma: Pedido+Articulo 
 public static Pedido operator +(Pedido p1, Articulo a) 
 { 
 List<Articulo> resultado = 
 new List<Articulo>(p1.Articulos); 
 resultado.Add(a); 
 return new Pedido { Articulos = resultado }; 
 } 
 //Convertir un artículo en pedido. 
 // Devuelve un pedido que contiene ese único artículo 
 public static explicit operator Pedido(Articulo a) 
 { 
 return new Pedido(a); 
 } 
 public override string ToString() 
 { 
 string s = ""; 
 foreach (Articulo a in this.Articulos) 
 s += a.ToString() + "\n"; 
 return s; 
 } 
} 
class Articulo 
{ 
 public string Nombre { get; set; } 
 //Convertir un string en artículo 
 // Devuelve un artículo cuyo Nombre es 
 // el valor del string 
 public static implicit operator Articulo(string nombre) 
 { 
 return new Articulo { Nombre = nombre }; 
 } 
 public static explicit operator string(Articulo a) 
Plataforma .NET y Lenguaje C# | 131 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 { 
 return a.ToString(); 
 } 
 public override string ToString() 
 { 
 return Nombre; 
 } 
} 
A continuación 
En el siguiente capítulo pasaremos a ver algo completamente independiente de lo 
que hemos visto en este. Se trata de los tipos genéricos, que proporcionan un 
mecanismo para definir clases que operen sobre cualquier tipo de dato, que se 
aporta como argumento al declarar variables de esa clase. 
132 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Plataforma .NET y Lenguaje C# | 133 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Genéricos 
El problema 
Hemos mencionado ya con anterioridad que en las primeras versiones del 
Framework las colecciones (tales como listas, colas, pilas, etc.) utilizaban el tipo de 
datos Object, con el fin de que pudiesen operar con cualquier tipo de dato (ya que 
todos se pueden almacenar dentro de un Object). 
El ejemplo que sigue presenta un ejemplo en el que se maneja una de estas 
colecciones: 
using System.Collections; 
//... 
//Crear una colección e insertar un elemento 
ArrayList personas = new ArrayList(); 
personas.Add(new Persona()); 
 
//Acceder a la colección 
//Requiere conversión explícita 
Persona p = (Persona)personas[0]; 
 
//Lo mismo con un tipo-valor 
ArrayList numeros = new ArrayList(); 
 
//Esto ocasiona Boxing 
numeros.Add(123); 
 
//Esto ocasiona Unboxing 
int n = (int)numeros[0]; 
 
//Aqui hay un error pero el compilador lo permite 
numeros.Add(new Persona()); 
 
//Esto compila pero falla en tiempo de ejecución 
int m = (int)numeros[1]; 
 
134 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Podemos observar aquí un par de inconvenientes que presenta esta forma de 
trabajar: En primer lugar, no se comprueba en tiempo de compilación el tipo de 
objetos que se almacena en la colección, por lo que se pueden producir errores en 
tiempo de ejecución al tratar de extraer un elemento del tipo incorrecto (señalado 
con un comentario en el ejemplo anterior). También se puede apreciar en este 
ejemplo la necesidad de realizar casts cada vez que se accede a un objeto de la 
colección, cosa que complica el código fuente y además ocupa ciclos de CPU en 
tiempo de ejecución. 
Adicionalmente, en el caso de que los objetos almacenados sean de tipo-valor se 
presenta un inconveniente más que no resulta obvio a la vista del código fuente: se 
trata de las operaciones conocidas como boxing y unboxing, que podríamos 
traducir libremente como "encajar" y "desencajar". 
Boxing y Unboxing 
Para comprender en qué consisten, es necesario pensar en la forma y lugar en que 
se almacenan los datos. Recordemos que los tipos-valor se almacenan dentro de la 
propia variable, mientras que los tipos-referencia se almacenan en el heap y la 
variable guarda una referencia a esa dirección. Eltipo Object es un tipo-referencia, 
por lo que los datos que almacena están en el heap. Cuando le asignamos un tipo-
valor, el contenido tiene que transferirse de la variable que lo contenía a un "cajón" 
dentro del heap. Y similarmente, cuando luego lo extraemos, tiene que salir de ese 
"cajón" y guardarse en la variable tipo-valor que lo recibe. Estas dos operaciones de 
meter y sacar datos tipo-valor en el heap son las que se conocen como boxing y 
unboxing. 
Las operaciones de boxing y unboxing tienen un coste considerable. Así como una 
copia de un tipo-valor a otro simplemente requiere copiar las palabras de memoria 
que forman el dato, y copiar un tipo-referencia a otro simplemente requiere copiar 
la referencia, en cambio copiar un tipo-valor a un tipo-referencia requiere múltiples 
operaciones internas. Hay que asignar una zona de almacenamiento en el heap, lo 
cual requiere buscar un hueco suficientemente grande y posiblemente reorganizar 
la memoria si no lo hubiera, y realizar el control y seguimiento de esos datos en el 
heap. Y después, cuando hacemos el unboxing, se requiere liberar la memoria 
asignada en el heap, que una vez más tiene un coste interno no despreciable. 
Por las razones anteriores, usar colecciones de Object para almacenar tipos-valor 
es menos eficiente que crear colecciones que directamente almacenen la 
información en los adecuados tipos-valor. 
Plataforma .NET y Lenguaje C# | 135 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
La solución 
En la versión 2.0 de C# se introdujo en el lenguaje la posibilidad de definir lo que de 
denomina "genéricos" ("generics" en la documentación en inglés). Gracias a este 
mecanismo, se definen clases y otros objetos que se "parametrizan" con un tipo de 
dato. Al instanciarlos, se asigna un tipo a ese "parámetro genérico" y esto 
"personaliza" la clase para que trabaje con ese tipo de dato. 
Esta personalización la entiende el propio CLR y realmente trabaja de forma 
interna con el tipo de dato indicado. No se trata de una "cola sintáctica" para el 
compilador que luego se implementa internamente mediante Object. El resultado 
es que se suprimen las operaciones de boxing y unboxing, con el correspondiente 
incremento de eficiencia. Adicionalmente, el compilador conoce el tipo con el que se 
está trabajando, por lo que se puede comprobar en tiempo de compilación la 
corrección de los tipos asignados, y además desaparecen los molestos casts. 
El ejemplo que sigue presenta el mismo código de nuestro ejemplo inicial, reescrito 
para que use uno de los tipos genéricos incluidos con el Framework. 
using System.Collections.Generic; 
//... 
//Crear una colección e insertar un elemento 
List<Persona> personas = new List<Persona>(); 
personas.Add(new Persona()); 
 
//Acceder a la colección 
//No requiere conversión 
Persona p = personas[0]; 
 
//Lo mismo con un tipo-valor 
List<int> numeros = new List<int>(); 
 
//Esto NO ocasiona Boxing 
numeros.Add(123); 
 
//Esto NO ocasiona Unboxing 
int n = numeros[0]; 
 
//Aqui el compilador detecta el error 
numeros.Add(new Persona()); 
 
//Esto ya no puede fallar en tiempo de ejecución 
int m = numeros[1]; 
 
Aunque aquí hemos usado la clase List<T>, que viene con el Framework, este 
mecanismo es perfectamente válido para utilizarlo en las clases que nosotros 
136 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
mismos escribamos. El siguiente ejemplo muestra cómo definir y utilizar una clase 
de tipo genérico. 
public class Pila<T> 
{ 
 private T[] elementos; 
 private int siguiente = 0; 
 public Pila(int tamaño) 
 { 
 elementos = new T[tamaño]; 
 } 
 public void Meter(T item) 
 { 
 elementos[siguiente++] = item; 
 } 
 public T Sacar() 
 { 
 return elementos[--siguiente]; 
 } 
} 
//Ejemplo de Uso: 
void Prueba() 
{ 
 Pila<int> laPila = new Pila<int>(10); 
 laPila.Meter(7); 
 //... 
 int n = laPila.Sacar(); 
 //... 
} 
A grandes rasgos, podemos observar que se indica en la definición inicial un 
nombre de parámetro genérico entre “<” y “>” (que en el ejemplo anterior se 
llamaba “T”), y luego ese parámetro se usa en los distintos lugares en los que 
normalmente usaríamos el nombre de un tipo. Es lícito escribir un genérico que 
contenga más de un parámetro. 
Para usar la clase, se declara el tipo concreto que se va a utilizar escribiéndolo en 
lugar de la T, como por ejemplo Pila<int>, y ese texto se usa como nombre del 
tipo a todos los efectos. 
En lugar de <int>, podríamos haber usado <string>, y la clase genérica seguiría 
siendo válida. Nótese que int es tipo-valor y string es tipo-referencia, y en ambos 
casos el código final funciona sin boxing ni unboxing. 
Plataforma .NET y Lenguaje C# | 137 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Los genéricos pueden ser no sólo clases sino también structs, interfaces, métodos y 
delegados. Este es un ejemplo en el que se declara un método de tipo genérico: 
static T PrimerValor<T>(IEnumerable<T> lista) 
{ 
 IEnumerator<T> enu = lista.GetEnumerator(); 
 enu.MoveNext(); 
 return enu.Current; 
} 
 
//... 
 
int[] x = new int[] { 2, 4, 6, 8 }; 
int n = PrimerValor(x); 
Console.WriteLine(n); //Escribe 2 
 
El ejemplo también demuestra cómo llamar al método genérico. Nótese que no ha 
sido necesario invocarlo como PrimerValor<int>, sino que ha bastado con 
escribir PrimerValor. El compilador ha deducido el tipo de genérico a partir del 
argumento “x” pasado al método. 
Declaración de restricciones 
En nuestro último ejemplo, el parámetro T podía ser absolutamente cualquier tipo. 
Esto está bien desde el punto de vista de que la clase Pila vale para contener 
cualquier clase de objeto. Pero a cambio, dentro de la clase se pueden hacer muy 
pocas cosas con esos objetos, porque el compilador no tiene manera de saber qué 
características van a tener los objetos que le pasemos al utilizar la clase. 
Si sabemos que todos los T que vamos a usar cumplen algún requisito concreto, por 
ejemplo, heredan todos de una misma clase madre, entonces podemos indicarlo al 
definir la clase genérica. Después de eso, no podremos usarla pasándole un tipo que 
no herede de la clase en cuestión, pero a cambio dentro de la clase podremos llamar 
sobre las instancias de T a los métodos y propiedades de esa clase madre. 
Cuando indicamos alguno de esos “requisitos” del parámetro genérico, se dice que 
estamos añadiendo una restricción (constraint). Se permiten tres tipos de 
restricciones: 
• Restricción de Clase – Obliga a que los T deriven de determinada clase 
base. 
138 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
• Restricción de Interfaz – Obliga a que los T implementen una
determinada interfaz
• Restricción de Constructor – Obliga a que T tenga un constructor
público predeterminado
Además de estas restricciones, también se puede forzar que T sea un tipo-valor o un 
tipo-referencia agregando una de las palabras clave struct o class, 
respectivamente, en la lista de restricciones. 
El siguiente ejemplo muestra cómo introducir las restricciones (mediante la palabra 
clave where), y de paso nos enseña una clase con más de un parámetro genérico: 
class MiGenerico<U, V> 
 where U : IComparable 
 where V : ClaseMadre, new() 
{ 
 // ... 
} 
class ClaseMadre { } 
class UnaClase : ClaseMadre { } 
class OtraClase : ClaseMadre 
{ 
 //Al definir este constructor 
 // ya no existe el predeterminado 
 public OtraClase(string s) { } 
} 
Estas son algunas declaraciones que podríamos tratar de realizar con nuestra clase 
MiGenerico. El compilador rechazará todas las que no cumplen las restricciones: 
//Funciona: 
MiGenerico <int, UnaClase> ejemplo1; 
//Falla porque string no hereda de ClaseMadre 
MiGenerico <int, string> ejemplo2; 
//Falla por culpa del constructor de OtraClase 
MiGenerico <int, OtraClase> ejemplo3; 
//Falla porque Point no implementa IComparable 
MiGenerico <System.Drawing.Point, UnaClase> ejemplo4; 
Gracias a la restricción,se pueden ejecutar instrucciones como esta: 
class MiGenerico<U> where U : IComparable 
{ 
Plataforma .NET y Lenguaje C# | 139 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 U x, y; 
 //... 
 if (x.CompareTo(y) > 0) //... 
} 
 
De no haber introducido la restricción, no podríamos haber llamado a 
x.CompareTo(y) porque el compilador no tendría constancia de que la variable x 
(de tipo U) implementase dicho método. En cambio, al indicar que U implementa la 
interfaz IComparable, sabemos que se puede llamar al método CompareTo, 
definido dentro de dicha interfaz. 
A continuación 
Tras este capítulo dedicado a los Genéricos, vamos a estudiar las consultas 
integradas en el lenguaje (LINQ), así como otros elementos del lenguaje que sirven 
(entre otras cosas) para sustentar las consultas LINQ, tales como los métodos de 
extensión. Precisamente, la mayor parte de los métodos de extensión que vienen 
predefinidos en el Framework se apoyan sobre los Genéricos, demostrando así uno 
de los casos en los que se pone de manifiesto la utilidad de éstos. 
 
140 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Plataforma .NET y Lenguaje C# | 141 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Extensores, 
Lambdas Y LINQ 
En la versión 3.0 de C# se introdujeron las consultas integradas en el lenguaje 
(LINQ), que permiten escribir en el código fuente sentencias parecidas a las que se 
utilizan para acceder a bases de datos mediante SQL. Con el fin de dar soporte a 
estas consultas, el lenguaje incorporó además otros elementos sintácticos, como son 
los métodos de extensión, las expresiones Lambda, las declaraciones con var y los 
tipos anónimos. 
Algunos de estos elementos ya los hemos mencionado a lo largo del texto, mientras 
que en este capítulo veremos los que nos faltan. En cualquier caso, todos ellos se 
pueden usar directamente, con independencia de LINQ, pero es dentro del contexto 
de este tipo de consultas donde principalmente se les suele encontrar utilidad. 
Métodos de extensión 
Los métodos de extensión permiten añadir nuevos métodos a clases ya existentes 
sin necesidad de modificar su código fuente o de heredar de ellas. Gracias a esta 
técnica podemos, por ejemplo, añadir un nuevo método a la clase String, cuyo 
código fuente no podemos modificar (ya que forma parte del Framework) y además 
no podemos heredar de ella, ya que es una clase de tipo sealed. 
A la hora de definirlos, los métodos de extensión tienen el mismo aspecto que los 
métodos estáticos, con la diferencia de que su primer argumento lleva antepuesta la 
palabra clave this (que en este contexto no tiene nada que ver con el this que 
habitualmente utilizamos para tomar una referencia a la instancia actual). A la hora 
de llamar al método, ese argumento no se incluye, sino que automáticamente se 
142 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
toma en su lugar la instancia sobre la que estamos llamando al método. Para 
comprenderlo mejor, examinemos un ejemplo: 
public static class Extensores 
{ 
 public static int NumeroDePalabras(this string cadena) 
 { 
 return cadena.Split(' ').Length; 
 } 
} 
Podemos ver que se trata de un método estático definido dentro de una clase 
estática. Esto es obligatorio: para definir un método de extensión, la clase que lo 
contiene debe necesariamente estar marcada como static. 
La principal peculiaridad del método es que el argumento va precedido de la 
palabra this. Para llamar al método, lo invocamos desde cualquier otro sitio de la 
aplicación en el que exista un string: 
static void Main(string[] args) 
{ 
 String ejemplo = "Hola, ¿qué tal?"; 
 int nPalabras = ejemplo.NumeroDePalabras(); 
 Console.WriteLine(nPalabras); 
} 
La llamada a NumeroDePalabras se realiza sobre la cadena ejemplo de la misma 
manera que si fuera alguno de los métodos que vienen “de fábrica” con la clase 
String. Desde el punto de vista del código llamante, el método de extensión se 
utiliza igual que si fuera uno de los métodos nativos de la clase. Al teclear el código 
en Visual Studio, intellisense muestra el método de extensión en la misma lista que 
el resto de los métodos, pero se ilustra con un icono ligeramente distinto (con una 
pequeña flecha) para que podamos reconocer visualmente que se trata de un 
método de extensión. 
Plataforma .NET y Lenguaje C# | 143 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
Aprovechemos para señalar que en la anterior imagen aparece un número 
importante de métodos de extensión, además del método NumeroDePalabras que 
nosotros hemos escrito en una clase estática agregada al proyecto. ¿De dónde salen 
todos estos métodos? Si nos fijamos en el código fuente que Visual Studio ha 
generado al crear un nuevo proyecto, observamos que en la parte superior hay una 
sentencia “using System.Linq”. En este espacio de nombres están definidos 
todos esos extensores. Si quitamos el using, dejaremos de verlos en intellisense. 
Es lícito generar un método de extensión sobre una clase base o sobre una interfaz. 
De esta manera, todas las clases que hereden de la base, o que implementen la 
interfaz, adquirirán automáticamente el método de extensión. También, al igual que 
cualquier otro método, pueden ser Genéricos, y se aplicarán correctamente sobre 
cualquier clase que “encaje” en el tipo genérico. Por ejemplo, el siguiente método de 
extensión extiende la interfaz IEnumerable<T>: 
public static string Concatenar<T>( 
 this IEnumerable<T> lista) 
{ 
 StringBuilder sb = new StringBuilder(); 
 foreach (T item in lista) 
 { 
 sb.Append(item.ToString()); 
 } 
 return sb.ToString(); 
} 
 
//... 
 
int[] numeros = new int[] { 1, 2, 3 }; 
Console.WriteLine(numeros.Concatenar()); 
 
 
144 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Como vemos en el ejemplo de llamada, hemos creado un arreglo de int. El arreglo 
resulta ser un tipo que implementa IEnumerable<T>, y la T automáticamente se 
toma como int a partir del contexto en el que realizamos la llamada. Por lo tanto, 
se invoca correctamente el método Concatenar sobre el arreglo de números. 
Es legal añadir argumentos adicionales en los métodos de extensión, que luego se 
pasan con normalidad entre los paréntesis al hacer la llamada del método. También 
se pueden sobrecargar, creando más de un método con el mismo nombre pero 
distintos argumentos. 
public static string Concatenar<T>( 
 this IEnumerable<T> lista) 
{ 
 return lista.Concatenar(String.Empty); 
} 
public static string Concatenar<T>( 
 this IEnumerable<T> lista, string separador) 
{ 
 StringBuilder sb = new StringBuilder(); 
 foreach (T item in lista) 
 { 
 sb.Append(item.ToString() + separador); 
 } 
 sb.Remove(sb.Length - separador.Length, 
 separador.Length); 
 return sb.ToString(); 
} 
//... 
int[] numeros = new int[] { 1, 2, 3 }; 
Console.WriteLine(numeros.Concatenar(", ")); 
Aunque podemos añadir los métodos de extensión que deseemos en nuestro 
proyecto, se recomienda no abusar de este mecanismo puesto que puede llegar a 
causar confusión (dado que en las clases aparecen métodos que no figuran por 
ninguna parte dentro del código fuente de dichas clases). 
El uso más común de los métodos de extensión consiste en una serie de métodos 
estandarizados por LINQ, que permiten que internamente funcionen las consultas 
integradas en el lenguaje sobre cualquier clase que tenga definidos dichos métodos. 
Volveremos a mencionarlos en el apartado dedicado a LINQ. 
Plataforma .NET y Lenguaje C# | 145 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Expresiones Lambda 
Hemos visto ya en el capítulo dedicado a los delegados cómo se podía crear un 
método anónimo, devolviendo un delegado que apuntaba a la declaración del 
método. Las expresiones Lambda suponen un paso más en la construcción de este 
tipo de métodos, ya que permiten definirlos mediante una sintaxis funcional y 
concisa.Para definir una expresión Lambda, se usa el símbolo “=>”. A su izquierda se 
escriben los parámetros que normalmente irían como argumentos del método si 
usáramos la sintaxis convencional, y a la derecha se escriben los cálculos sobre esos 
parámetros, que normalmente irían dentro del cuerpo del método. Vamos a ver un 
ejemplo, en el que se definen dos delegados iguales, el primero mediante un método 
anónimo, y el segundo mediante una expresión Lambda. 
delegate string Convertidor(int numero); 
 
Convertidor delegado1 = 
 delegate(int x) { return x.ToString(); }; 
 
Convertidor delegado2 = x => x.ToString(); 
 
Nótese que la expresión Lambda no indica por ninguna parte que la variable x es de 
tipo int. El compilador lo deduce a partir de la definición del tipo de delegado que 
hay a la izquierda de la asignación (“Convertidor”). 
Dado que es muy frecuente escribir Lambdas que representan funciones que toman 
uno o más argumentos de distintos tipos, y devuelven un resultado de otro tipo, 
estos delegados vienen ya definidos en las librerías del Framework, para evitar que 
tengamos que definirlos cada vez que los usemos (como el Convertidor que 
hemos definido en el ejemplo anterior). Estos tipos de delegados son Genéricos, y 
llevan el nombre Func y una serie de parámetros genéricos indicando el tipo de los 
argumentos y el resultado. El ejemplo anterior se puede reescribir así: 
Func<int, string> delegado3 = x => x.ToString(); 
 
Veamos algunos ejemplos adicionales de Lambdas para hacernos una idea del tipo 
de variaciones que se permiten: 
Func<int, int> identidad = x => x; 
 
//Si hay más de un argumento, se utilizan paréntesis 
Func<int, int, int> producto = (x, y) => x * y; 
 
//Opcionalmente, se puede fijar el tipo de los argumentos 
146 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Func<int, int, int> sumar = (int x, int y) => x + y; 
//Si no hay ningún argumento, se pone (): 
Func<int> diez = () => 10; 
//Una Lambda puede llamar a otras 
Func<int, int> duplicar = x => producto(x, 2); 
Func<int, int> cuadrado = x => producto(x, x); 
//Si no se indica el tipo de los argumentos, es necesario 
// asignar el resultado a un delegado que describa el tipo. 
var fn = x => x; //Error al compilar 
Las lambdas no están limitadas a una única expresión en una línea. Es lícito escribir 
múltiples sentencias detrás del “=>” encerradas entre llaves: 
Action<string> imprimir = s => Console.WriteLine(s); 
Action<int> imprimirSiEsPar = i => 
{ 
 if (i % 2 == 0) 
 imprimir(i + " es par"); 
}; 
Este ejemplo introduce otro de los tipos de delegados que ya vienen predefinidos en 
el Framework. Action<T> sirve para apuntar a métodos que reciben un argumento 
de tipo T y no devuelven nada. 
Árboles de expresiones 
Los árboles de expresiones representan estructuras de datos que se ramifican en 
forma de árbol. Cada nodo del árbol es una expresión, por ejemplo, una llamada a 
un método o una operación tal como a+b. 
Se puede compilar y ejecutar el código representado por los árboles de expresiones, 
lo cual permite modificar dinámicamente el código que se ejecuta, por ejemplo para 
crear consultas dinámicas. 
Aunque se puede crear manualmente un árbol de expresiones, empleando las clases 
del espacio de nombres System.Linq.Expressions, una de las ventajas de las 
expresiones Lambda es que se pueden convertir directamente en árboles de 
expresiones. A continuación, el árbol de expresiones se puede ir recorriendo 
mediante bucles de código, examinando cada expresión y, por ejemplo, generando 
código en un lenguaje diferente para ejecutar las mismas operaciones que se 
definieron inicialmente en el árbol. Este es el mecanismo que utiliza internamente 
Plataforma .NET y Lenguaje C# | 147 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
LINQ-to-SQL para convertir en sentencias SQL las sentencias LINQ que nosotros 
escribimos en C#. 
Veamos un ejemplo sencillo, en el que se crea un árbol de expresiones a partir de 
una Lambda, se compila, y se ejecuta: 
Expression<Func<int, int>> cuadrado = x => x * x; 
 
Func<int, int> compilado = cuadrado.Compile(); 
 
Console.WriteLine(compilado(10)); //Escribe 100 
 
Por supuesto, esta no es la forma más típica de utilizarlas; para lograr este 
resultado, podríamos haber asignado la Lambda directamente a un delegado de tipo 
Func<int,int> (en lugar de Expression<...>), y habríamos podido ejecutarlo 
directamente sin necesidad de pasar por Compile. La potencia de los árboles de 
expresiones se pone de manifiesto cuando se recorren sus nodos mediante los 
métodos de la clase Expression, examinando uno por uno y realizando con ellos 
operaciones, como por ejemplo la que hemos mencionado antes de escribir lo 
mismo en otro lenguaje. 
No es este el lugar adecuado para analizar los métodos y el uso de la clase 
Expression. Normalmente no necesitaremos emplear estos métodos directamente 
en nuestro código, sino que los usaremos de forma indirecta al aplicar consultas del 
tipo de LINQ-to-SQL, en que las librerías del Framework hacen uso internamente 
de los árboles de expresiones. 
LINQ 
Con estas siglas se hace referencia a Languaje INtegrated Query, o “consulta 
integrada en el lenguaje”. LINQ se introdujo en la versión 3.0 de C# (al igual que los 
métodos de extensión y las Lambdas), y permite escribir dentro del código fuente 
sentencias análogas a las que componen el lenguaje SQL, pero con la ventaja de que 
el compilador de C# verifica su sintaxis en tiempo de compilación. Además se 
pueden aplicar no sólo sobre bases de datos, sino también sobre cualquier objeto 
que implemente una serie de métodos predefinidos tales como Select, Where, 
OrderBy, etc. Estos métodos pueden formar parte de la propia clase sobre la que se 
ejecuta la consulta, o agregarse a la misma como métodos de extensión. 
Tradicionalmente, las consultas sobre base de datos se expresan en forma de 
cadenas, como en este ejemplo: 
148 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
string cadena = 
 "Select Nombre from LaTabla where Codigo=1"; 
SqlCommand cmd = new SqlCommand(cadena, conexion); 
SqlDataReader rdr = cmd.ExecuteReader(); 
//... 
Desde el punto de vista del compilador de C#, la cadena es un simple texto entre 
comillas, y da lo mismo qué se haya escrito dentro; en todos los casos compilará 
correctamente. Por supuesto, si cometemos cualquier error en el texto que contiene, 
como por ejemplo escribir mal el nombre de un campo, se producirá un error en 
tiempo de ejecución. 
Además de este inconveniente, mientras escribimos el código (tanto la propia 
sentencia como posteriormente el acceso a los datos devueltos), no tenemos 
ninguna clase de asistencia por parte de intellisense para ayudarnos a teclear los 
datos correctos. 
Este tipo de inconvenientes vienen a ser resueltos por las consultas LINQ. Estas 
consultas se escriben en C# usando términos similares a los del lenguaje SQL, pero 
escritos en un orden ligeramente distinto. Por ejemplo: 
using (EjemploDataContext dc = new EjemploDataContext()) 
{ 
 var q = from prod in dc.Products 
 where prod.Color == "Black" 
 || prod.Color == null 
 orderby prod.ListPrice 
 select new { prod.ProductID, prod.Name }; 
 //... 
} 
La sentencia empieza con la palabra from (a diferencia del SQL tradicional que 
comienza por select y trae el from más adelante) con el único fin de que 
intellisense conozca desde el primer momento cuál es el objeto que estamos 
consultando y nos pueda ofrecer ayuda. 
Plataforma .NET y Lenguaje C# | 149 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Detrás del from viene el objeto sobre el que se aplica la consulta. En este ejemplo, 
resulta ser el objeto Products contenido dentro de la clase EjemploDataContext. 
Esta clase se puede escribir a mano, o se puede confiar en uno de los asistentes de 
Visual Studio para que la escriba. No es ahora mismo nuestro objetivo conocer 
cómo se genera esa clase, sino entender qué hacecon ella la sentencia LINQ. Y 
concretamente lo que hace el compilador es traducir la sentencia que hemos escrito 
en una secuencia de llamadas a los métodos Where(), OrderBy(), Select(), etc., 
que obligatoriamente deben existir dentro de la clase que estamos consultando, 
bien sea porque directamente contiene los métodos o porque los recibe como 
métodos de extensión. 
Este es el código que internamente genera el compilador, y que también podemos 
escribir a mano si lo deseamos: 
var q = dc.Products 
 .Where(p => p.Color == "Black" || p.Color == null) 
 .OrderBy(p => p.ListPrice) 
 .Select(p => new { p.ProductID, p.Name }); 
 
Como vemos, cada uno de estos métodos recibe como argumento una expresión 
Lambda que equivale al código que se escribió en la sentencia LINQ original. Esto 
permite internamente a la clase dc.Products extraer el árbol de expresiones y 
generar una consulta SQL equivalente, que se envía al servidor de base de datos. 
Por ejemplo, en el árbol de expresiones habrá una constante "Black", que al 
traducirla en SQL se convertirá en 'Black' (con comillas simples), de la misma 
manera que la comparación “==” se traducirá en un solo “=”, el “||” se convertirá 
en “OR”, y el “==null” se convertirá en “IS NULL”. Esto permite enviar al servidor 
una sentencia que se comporte de forma análoga a la escrita en C#. 
Nótese que la variable en la que almacenamos el resultado se declara como var. El 
resultado de esta consulta es de tipo IQueryable<tipo>, donde tipo es el tipo de 
datos que hemos escrito detrás del select. Como resulta que hemos utilizado un 
tipo anónimo, no podemos escribir el nombre del tipo como parámetro genérico del 
IQueryable. Por eso no tenemos más remedio que declararlo como var, 
demostrándose así la utilidad de esta palabra clave, como anticipábamos cuando la 
explicamos por primera vez dentro de este texto. 
Para obtener los resultados de la consulta, se enumera la misma dentro de un bucle 
foreach: 
foreach (var p in q) 
{ 
 Console.WriteLine(p.ProductID + " " + p.Name); 
} 
 
150 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
No vamos a entrar aquí en los detalles de todas las variaciones que se pueden 
introducir dentro de la consulta LINQ, ni la forma de generar clases que permitan 
trabajar contra bases de datos, ya que estos temas encuentran su cabida dentro del 
texto dedicado a acceso a bases de datos. 
Sí vamos a mencionar, en cambio, que LINQ no es sólo útil para acceder a bases de 
datos. El objeto sobre el que se aplica el from puede ser cualquier clase que 
contenga los métodos adecuados que ya hemos mencionado (Where, Select, etc.). 
Se pueden escribir estas clases a mano, o se pueden usar las que ya vienen previstas 
en el Framework para este fin. Dependiendo de las clases que se utilicen, se dice que 
estamos trabajando con LINQ-to-SQL, LINQ-to-XML, LINQ-to-Datasets, etc., 
dependiendo del tipo de objetos sobre los que opera la clase en cuestión. 
En particular, en el espacio de nombres System.Linq vienen una serie de métodos 
de extensión llamados Where, Select, etc. que se aplican a la interfaz 
IEnumerable<T>, y por tanto cualquier objeto que implemente dicha interfaz 
automáticamente recibe esos métodos. Y en consecuencia, podemos aplicar 
consultas LINQ sobre dichos objetos, que incluyen casi todas las colecciones y los 
arreglos. Basta para ello con que al principio de nuestro código fuente añadamos 
una directiva “using System.Linq”, que de forma predeterminada ya es añadida 
por las plantillas de Visual Studio. 
El resultado es que podemos escribir en cualquier parte de nuestro programa 
código como este: 
Int[] numeros = { 1, 3, 2, -4, 8, 99}; 
var q = from n in numeros 
 where n % 2 == 0 orderby n select n; 
foreach (int i in q) 
{ 
 Console.WriteLine(i); 
} 
Este ejemplo selecciona de la lista todos los números pares, los ordena por su valor 
numérico y los escribe. 
Nótese que en este caso no es imprescindible usar la palabra var porque aquí sí que 
conocemos el tipo del resultado (concretamente, IEnumerable<int>), pero es 
bastante común escribir var en este tipo de sentencias por simplicidad. 
En resumen, LINQ nos permite consultar datos de distintos orígenes, desde bases 
de datos hasta colecciones en memoria pasando por XML, permitiendo que la 
consulta sea validada en tiempo de compilación y ayudándonos a escribirla por 
medio de intellisense. 
Plataforma .NET y Lenguaje C# | 151 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
A continuación 
El capítulo que sigue trata una miscelánea de características de C# sin relación 
entre sí. Se trata de elementos del lenguaje que tienen su importancia, y sobre los 
que en algunos casos se han vertido ríos de tinta. Sin embargo, dado el carácter 
introductorio de este texto no tenemos espacio suficiente para tratar cada uno de 
ellos por separado en mayor extensión. 
 
152 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
Plataforma .NET y Lenguaje C# | 153 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Otras 
Características 
En este capítulo se cubren diversos elementos del lenguaje C# que no han tenido 
hasta ahora cabida en ningún otro capítulo. 
Atributos 
Los atributos constituyen un elemento del lenguaje que permite añadir metadatos a 
los ejecutables de .NET. Gracias a los atributos, se aporta información dentro del 
código fuente que al compilarse no produce código ejecutable, sino que queda 
embebida dentro del ensamblado para que pueda ser consultada desde 
programación. 
Existen numerosos atributos que vienen ya predefinidos en el Framework. También 
es posible, aunque poco usual, definir nuestros propios atributos por programación. 
Un atributo es una etiqueta declarativa que permite añadir información sobre los 
elementos de programa, tales como clases, métodos o ensamblados. Se puede 
pensar en ellos como anotaciones añadidas al programa. 
Para aplicar un atributo sobre un elemento de programa, se escribe entre corchetes 
el nombre del atributo, seguido opcionalmente de argumentos entre paréntesis. Por 
ejemplo: 
[WebMethod(Description = "Prueba")] 
public void MiMetodo() 
{ 
 //... 
} 
154 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
En este caso, WebMethod es el nombre del atributo, y lo hemos aplicado sobre el 
método MiMetodo, escribiéndolo por delante de éste. Este atributo en particular se 
utiliza cuando se define un Servicio Web XML para “decorar” aquellos métodos que 
deben exponerse al exterior como parte del servicio. La misma clase en la que se 
definen estos métodos podría también contener otros métodos que no tuvieran 
aplicado este atributo, en cuyo caso el “runtime” no los haría visibles al exterior. 
Se pueden aplicar múltiples atributos sobre un mismo elemento. Para ello, se 
pueden encerrar cada uno de ellos entre corchetes, o escribirlos todos separados por 
comas dentro de un único par de corchetes. En el siguiente ejemplo, el método 
SquareRoots tiene aplicados dos atributos: 
[ServiceContract] 
interface IMyService 
{ 
 [OperationContract] 
 [FaultContract(typeof(SquareRootError))] 
 double[] SquareRoots(double[] items); 
} 
Usualmente se escriben los atributos por delante del elemento al que se aplican (en 
el ejemplo anterior, una interfaz y un método). Sin embargo, en algunos casos no es 
posible. Por ejemplo, si queremos aplicar un atributo al resultado de un método, no 
podemos escribirlo por delante porque el compilador creería que se aplica al propio 
método. En este caso, se escribe dentro de los corchetes la palabra return separada 
por dos puntos, para indicar que el atributo corresponde al resultado. En el 
siguiente ejemplo aparecen dos atributos, el primero aplicado a un método y el 
segundo al valor devuelto por el mismo método. 
[DllImport("msvcrt.dll")] 
[return: MarshalAs(UnmanagedType.I4)] 
public static extern int puts(/*...*/); 
Otro caso en el que no se puede escribir un atributo por delantedel elemento al que 
se aplica es el de los atributos que se refieren a todo un ensamblado. Todos los 
archivos fuente que forman parte de nuestro proyecto se compilan juntos (en 
cualquier orden) para dar lugar al ensamblado. No hay ningún sitio que pueda 
considerarse “por delante de” el ensamblado. 
En este caso, se aplica la palabra assembly seguida de dos puntos dentro de los 
corchetes. Esto puede hacerse en cualquier parte de los fuentes que se compilan 
para dar lugar al ensamblado. Sin embargo, si repartimos atributos de este tipo por 
todos los fuentes del programa, será luego muy difícil encontrar todos los atributos 
aplicados a un ensamblado. Por este motivo, Visual Studio añade a nuestro proyecto 
Plataforma .NET y Lenguaje C# | 155 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
un archivo fuente denominado AssemblyInfo.cs, cuyo propósito es centralizar en 
su interior los distintos atributos del ensamblado. El siguiente bloque muestra un 
fragmento del contenido que se genera de forma predeterminada para este archivo: 
[assembly: AssemblyTitle("HolaMundo")] 
[assembly: AssemblyDescription("")] 
[assembly: AssemblyConfiguration("")] 
[assembly: AssemblyCompany("MiEmpresa")] 
[assembly: AssemblyProduct("HolaMundo")] 
[assembly: AssemblyCopyright("Copyright MiEmpresa 2011")] 
[assembly: AssemblyTrademark("")] 
[assembly: AssemblyCulture("")] 
 
[assembly: ComVisible(false)] 
 
El estudio detallado de los distintos atributos definidos en el Framework y la 
finalidad para la que se usan queda fuera del alcance de este texto. Señalemos 
simplemente, para el caso de que el lector desee buscarlos en la documentación, que 
por convención los nombres de los atributos suelen terminar en el sufijo Attribute. 
El compilador conoce esta convención, y automáticamente añade ese sufijo al 
nombre del atributo escrito entre corchetes, a no ser que se escriba explícitamente. 
Por ejemplo, el atributo ComVisible que aparece en el ejemplo anterior, en 
realidad se llama ComVisibleAttribute, y con este último nombre lo 
encontraremos en la documentación del Framework. A la hora de escribirlo en el 
fuente, da igual escribir ComVisibleAttribute que acortarlo a ComVisible. 
Clases Parciales 
Normalmente, cada vez que definimos una clase le dedicamos un archivo completo. 
Por ejemplo, si añadimos en Visual Studio la clase Prueba, creamos para ella un 
archivo Prueba.cs que contiene todo el texto de la clase. 
Visual Studio contiene diversos asistentes que escriben código automáticamente. 
Por ejemplo cuando se construye una aplicación de escritorio de tipo Windows 
Forms y se dibuja la pantalla con el editor de Visual Studio, se generan una serie de 
líneas de código que al ejecutarse dibujan esa misma pantalla que nosotros hemos 
preparado con la herramienta de diseño. En las primeras versiones de Visual 
Studio, este código autogenerado se insertaba dentro del mismo archivo fuente en el 
que nosotros añadíamos nuestro propio código. El resultado era que a veces ese 
archivo resultaba difícil de mantener, debido a la mezcla de códigos, e incluso a 
156 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
veces se corrompía cuando el desarrollador accidentalmente modificaba alguna de 
las partes generadas de forma automática. 
Las versiones modernas de Visual Studio generan el código en un archivo 
independiente, que al compilarse se “une” con otro archivo en el que el 
desarrollador escribe su parte del código. Para indicar que ambos archivos se deben 
unir en una sola clase, se aplica la palabra partial por delante de la definición de 
la clase: 
//En el archivo 1: 
partial class MiClase 
{ 
 // ... Miembros de la clase ... 
} 
//En el archivo 2: 
partial class MiClase 
{ 
 // ... Más miembros de la clase ... 
} 
Cuando Visual Studio hace esto, usualmente sigue la convención de nombrar el 
archivo autogenerado igual que el que contiene la clase original, pero añadiendo la 
extensión .designer.cs en lugar de solo .cs. No obstante, para el compilador 
resulta indiferente el nombre del archivo, permitiendo que este mecanismo se 
aplique sobre dos o más archivos cualesquiera. 
Además de la utilidad de este mecanismo para separar el código escrito por 
nosotros del código que se genera automáticamente, también puede resultar 
interesante cuando varias personas deban trabajar sobre una misma clase. 
Dividiéndola en varios archivos con partial class, se permite que las distintas 
partes sean desprotegidas por diferentes desarrolladores desde un sistema de 
control de código fuente. 
A la hora de trabajar con el código fuente en Visual Studio, intellisense muestra 
siempre todos los miembros de la clase, con independencia del fragmento parcial en 
el que estén definidos. De esta forma, refleja la semántica real del código, que se 
compila en una única clase sin tener en cuenta el fragmento parcial en el que se 
escribió en tiempo de desarrollo. 
Métodos parciales 
Mencionábamos antes que hay muchos casos en los que Visual Studio genera 
código de forma automática. Es frecuente que este código esté “salpicado” de 
Plataforma .NET y Lenguaje C# | 157 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
puntos de conexión en los que se puede introducir código creado por el diseñador. 
Por ejemplo, cuando se generan clases para trabajar con LINQ-to-SQL, las 
propiedades de la clase representan los campos de la tabla a la que se accede vía 
LINQ. Cada una de esas propiedades, en su accesor set, permite introducir código 
antes y después de cambiar el valor del campo. Típicamente ese código se inserta 
cuando el desarrollador desea introducir alguna validación, o cuando se desea 
actualizar en cascada otros datos al cambiar el valor del campo. 
Si se editase el código autogenerado, para introducir in situ el código manual, este 
código se perdería cada vez que Visual Studio volviese a generar el archivo. De 
hecho, en su cabecera aparece entre comentarios un mensaje de advertencia 
avisando que no se edite a mano. 
Por supuesto, existen mecanismos que ya conocemos para conectar código 
dinámicamente, tales como los delegados y los eventos. Por ejemplo, el set podría 
disparar sendos eventos antes y después de modificar el valor, y nuestro código 
podría implementar dichos eventos. El problema es que la mayor parte del tiempo, 
en la mayor parte de las propiedades, nunca se introduce ningún código. Sin 
embargo, si se utilizase el mecanismo de los eventos todas ellas ejecutarían 
(inútilmente) el código para disparar los eventos cada vez que fueran modificadas. 
En la versión 3.0 de C# se introdujo un mecanismo más eficiente para definir estas 
conexiones de código que en su mayor parte quedan sin utilizar. Es lo que se conoce 
como métodos parciales. En la clase que “publica” el sitio donde puede conectarse 
código de usuario, se introduce una llamada a un método. Y el método se decora 
con la palabra partial, y sólo contiene la declaración, no la implementación: 
public int ProductID 
{ 
 get { ... } 
 set 
 { 
 ... 
 this.OnProductIDChanging(value); 
 ... 
 this.OnProductIDChanged(); 
 } 
} 
... 
partial void OnProductIDChanging(int value); 
partial void OnProductIDChanged(); 
 
Para conectar código en estos puntos, se escribe dentro del mismo partial class 
(típicamente en otro archivo distinto) un método con el mismo nombre, pero esta 
vez sí que se le escribe la implementación: 
partial void OnProductIDChanging(int value) 
158 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
{ 
 Console.WriteLine("Nuevo valor:" + value); 
} 
Cuando existe este método parcial, se compila realmente dentro del set una 
llamada al mismo. Pero la mayor parte del tiempo, como ocurre en este ejemplo con 
OnProductIDChanged, no se implementa el método parcial. En este caso, el 
compilador elimina por completo del set la llamada al método parcial. De esta 
manera, el ejecutable queda completamente “limpio”y no se incurre en ninguna 
ineficiencia por el hecho de que el código fuente estuviese salpicado de cientos de 
llamadas a métodos parciales. 
En resumidas cuentas, los métodos parciales proporcionan un mecanismo eficiente 
para conectar código personalizado al código autogenerado. Este mecanismo se usa 
extensivamente en muchas de las clases generadas automáticamente por Visual 
Studio, como por ejemplo las de LINQ-to-SQL y las de Entity Framework. 
Inicializadores de colecciones 
Ya vimos en uno de los capítulos anteriores un mecanismo que nos permitía 
inicializar las propiedades de una instancia con una sola línea de código, poniendo 
entre llaves detrás de la llamada al constructor una lista de nombres y valores. 
Persona p = new Persona() { 
 Nombre = "Pepe", Apellido = "Perez" }; 
Existe una sintaxis muy parecida que se usa para inicializar los elementos de una 
colección. Para ello, basta con agregar detrás de la llamada al constructor la lista de 
valores encerrada entre llaves: 
List<int> miLista = new List<int>() { 1, 2, 3, 4, 5 }; 
Cuando se escribe una inicialización de este tipo, el compilador genera 
internamente una llamada al método Add de la colección por cada elemento que 
figura en la lista entre paréntesis. Por supuesto, esas llamadas podrían escribirse a 
mano, pero esta sintaxis resulta más compacta. 
Este mecanismo es válido para cualquier objeto de tipo ICollection<T>, y para 
cualquier IEnumerable que contenga un método Add. En el caso de que el Add 
requiera más de un parámetro, deben proporcionarse agrupados entre llaves: 
Plataforma .NET y Lenguaje C# | 159 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Dictionary<string, int> d = 
 new Dictionary<string, int> { {"A", 1}, {"B" , 2} }; 
 
Si se necesita inicializar un gran número de elementos, tiene mayor rendimiento el 
método AddRange, pero para pequeñas listas de valores, la sintaxis de los 
inicializadores de colecciones da lugar a un código claro y sencillo. 
Enumeradores 
Para poder recorrer el contenido de un objeto mediante un bucle foreach, el objeto 
en cuestión tiene que soportar una interfaz que permita al compilador hacer las 
llamadas necesarias para ir obteniendo elementos. La interfaz más típica empleada 
para este fin se llama IEnumerable, y define un único método llamado 
GetEnumerator. Este método devuelve un objeto que implementa la interfaz 
IEnumerator. Y esta interfaz, a su vez requiere que el objeto implemente los 
métodos Reset, MoveNext y Current, que sin entrar en detalles ya se ve cómo 
pueden servir para ir recorriendo los elementos de una colección. 
La cuestión es que si deseamos implementar un enumerador de este tipo en una 
clase escrita por nosotros, las cosas se complican más de lo que parece, porque hay 
que crear una instancia separada de un enumerador que lleve el control de hasta 
dónde se había enumerado previamente, con el fin de poder pasar al siguiente 
elemento cuando se llame al MoveNext. En las primeras versiones de C# no había 
más remedio que afrontar esta complejidad, pero a partir de C# 2.0 se introdujo un 
mecanismo para simplificar la escritura de enumeradores. Se basa en el uso de la 
palabra clave yield, y este es un ejemplo: 
public static IEnumerable Potencias(int queBase, int 
exponente) 
{ 
 int nVeces = 0; 
 int producto = 1; 
 while (nVeces++ < exponente) 
 { 
 producto *= queBase; 
 yield return producto; 
 } 
} 
 
Una vez escrito un método como este, se puede invocar así: 
foreach (int i in Potencias(2, 5)) 
{ 
160 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
 Console.WriteLine(i); // 2 4 6 8 16 32 
} 
La ejecución salta repetidamente de ida y vuelta entre ambos bloques de código: el 
foreach llama a Potencias, este método se ejecuta, y cada vez que encuentra el 
yield return dentro del while vuelve al foreach y le devuelve un valor; después 
la ejecución retorna de nuevo al punto que está detrás del yield. 
Si fuera necesario, existe también una instrucción yield break que permite 
interrumpir el bucle del enumerador y dar por terminada la enumeración. 
Gracias a esta construcción, se pueden escribir enumeradores empleando muchas 
menos líneas de código que las que se necesitaban en las primeras versiones de C#, 
antes de que existiera este mecanismo. 
Covariancia y contravariancia 
Sobre este tema se ha hablado largo y tendido y se han escrito centenares de 
artículos. Aunque aquí no tenemos espacio para tratarlo en profundidad, vamos a 
mencionar a grandes rasgos el significado de estos términos. 
En C#, desde las primeras versiones ha existido lo que se denominaba 
“compatibilidad en las asignaciones”, en referencia al hecho de que es posible 
asignar a una variable de un tipo cualquier clase hija del mismo tipo: 
string s = "Ejemplo"; 
// Clase hija asignada a clase madre 
object obj = s; 
Sin embargo, este mismo tipo de compatibilidad no tiene por qué extenderse a un 
objeto instanciado con argumentos de la clase hija que se asigna a un objeto 
instanciado con argumentos de la clase madre. Por ejemplo, aunque se puede 
asignar un string a un object, no tiene por qué ser lícito asignar una lista de 
strings a una lista de objects: 
IEnumerable<string> cadenas = new List<string>(); 
//Esto no tendría por qué funcionar, y de hecho produce 
// un error en versiones del Framework anteriores a la 4.0 
IEnumerable<object> objetos = cadenas; 
In C#, la covariancia y la contravariancia permiten que se realice la conversión 
implícita de referencias entre arreglos, delegados y argumentos de tipos genéricos. 
Plataforma .NET y Lenguaje C# | 161 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
La covariancia permite la compatibilidad de las asignaciones, y la contravariancia la 
invierte. Concretamente, el ejemplo anterior demuestra la covariancia entre 
argumentos genéricos. Este es un ejemplo de variancia en arreglos: 
string[] vector1 = new string[] { "a" }; 
object[] vector2 = vector1; 
 
La covariancia en los arreglos se soporta desde la versión 1.0 de C#, y la covariancia 
y contravariancia en delegados (que también se conoce como “variancia de grupos 
de métodos”) existe desde la 2.0. En la versión 4.0 se soporta además la variancia 
de parámetros de tipos genéricos en interfaces y delegados. 
Para habilitar variancia y covariancia sobre parámetros genéricos en C# 4.0, se 
utilizan las palabras reservadas in y out sobre dichos parámetros. La palabra clave 
out marca un parámetro como covariante, y la palabra in como contravariante. 
interface IVariante<out T, in V> 
{ 
 // ... 
} 
 
Entre las interfaces que vienen predefinidas en el Framework, hay varias que en la 
versión 4.0 han sido marcadas como covariantes o contravariantes. Por ejemplo la 
interfaz IEnumerable<T> es covariante en T, mientras que IComparable<T> es 
contravariante en T. 
Como ejemplo práctico, uno de los escenarios más frecuentes en la práctica es este: 
IEnumerable<object> objetos = new List<string>(); 
Aunque no parece particularmente interesante, el hecho de que sean lícitas las 
conversiones de este tipo nos permite reutilizar muchos métodos que acepten 
parámetros de tipo IEnumerable, que de otra forma habría que duplicar. Por 
ejemplo: 
class EjemploVariancia 
{ 
 public static void EscribirNombres( 
 IEnumerable<Persona> personas) 
 { 
 foreach (Persona p in personas) 
 Console.WriteLine(p.Nombre); 
 } 
 
 public void Prueba() 
 { 
 List<Empleado> empleados = new List<Empleado>(); 
 
 EscribirNombres(empleados); // <-- Covariancia 
162 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
 } 
} 
class Persona 
{ 
 public string Nombre { get; set; } 
} 
class Empleado: Persona 
{ 
 public string Departamento { get; set; } 
} 
En este ejemplo, hemos escrito un método EscribirNombres que recibe un 
argumento de tipo IEnumerable<Persona>. Sin embargo, más abajo lo hemos 
llamado pasándolecomo argumento un IEnumerable<Empleado>, siendo 
Empleado una clase hija de Persona. Esto compila correctamente en C# 4.0 
gracias a la covariancia. En versiones anteriores habría arrojado un error, y no 
habríamos podido reutilizar el método EscribirNombres como lo hacemos aquí. 
Plataforma .NET y Lenguaje C# | 163 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Conclusión 
A lo largo de los capítulos anteriores hemos examinado las principales 
características del lenguaje C#, señalando aquellas que son novedosas en las 
versiones más recientes. Previamente, hicimos una breve presentación del 
Framework de .NET, ya que aunque es independiente del lenguaje en sí mismo, 
proporciona la infraestructura sobre la que este se apoya. También hemos 
mencionado en algunos casos su interacción con Visual Studio y las facilidades que 
nos proporciona esta herramienta. Una vez más, la herramienta es independiente 
del lenguaje propiamente dicho, pero en la práctica nos encontraremos casi siempre 
trabajando con ella. 
El siguiente paso consiste en estudiar con cierto detenimiento las librerías que 
forman parte del Framework. Gracias a ellas dispondremos de una potente 
infraestructura que nos permite desarrollar aplicaciones con mucho menor esfuerzo 
del que sería necesario si no dispusiéramos de este soporte. Entre estas librerías se 
encuentran las de acceso a datos con ADO.NET y LINQ, así como las de creación de 
interfaces de usuario con Windows Forms y con Windows Presentation Foundation, 
que se estudian en otras secciones de este libro. 
164 | Plataforma .NET y Lenguaje C# 
Libro para José Mora 
 
 
 
 
 
Apartado II: 
ADO y Linq 
por Jorge L.Cangas 
 
166 | Windows Forms 
Libro para José Mora 
Windows Forms | 167 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
¡ConectADO! 
ADO y Linq son las librerías de .NET para acceder, consultar y manipular 
conjuntos de datos. El origen de los datos, la ‘fuente’, típicamente seria una 
base de datos relacional, o un fichero XML. Sin embargo, las posibles fuentes 
de datos soportadas son mas variadas: Linq puede trabajar sobre un simple 
array de nuestro código, ADO puede tomar una hoja Excel como fuente de 
datos o trabajar solo en memoria los datos de una aplicación, para 
posteriormente conectar a una base de datos y sincronizar cambios realizados. 
Ante ésta disparidad de situaciones, ambas librerías ofrecen una buena 
abstracción que permite reutilizar nuestro código para las diversas fuentes, 
con mínimos ajustes. El precio, claro está, es que bajo una relativa sencillez de 
uso, se esconden unas librerías de considerable tamaño y complejidad. 
Explicar en detalle cada interfaz, clase, método y propiedad, o ilustrar cada 
mínimo detalle de funcionalidad ocuparía seguramente varios volúmenes 
como éste. Sin ir mas lejos, DevGuru afirma que su "ADO Quick Reference" 
¡¡contiene 323 páginas!! 1 . 
Aquí no vamos ni siquiera a intentarlo. Además, el apoyo ofrecido por los 
entornos de desarrollo actual, es enorme: solemos encontrar ayuda sobre cada 
clase, método o propiedad, ejemplos, tutoriales, enlaces a la web etc. Creo, 
por todo lo anterior, que tratar de ser exhaustivo en un libro, esta condenado 
al fracaso. Más bien el problema que se encuentra quien trata de aprender 
ADO, es ¿por dónde empezar?. En mi opinión, un libro como éste, tiene más 
valor como herramienta para ganar una comprensión global razonable en un 
tiempo más corto. La batalla de profundizar en los detalles, se gana mucho 
mejor con la práctica y la inmediatez de los recursos en línea como la ayuda 
integrada en Visual Studio y la web. 
Por tanto, mí objetivo aquí será proporcionar, en un tiempo breve, una mapa 
conceptual de estas librerías: sus clases principales y su filosofía de uso. Para 
 
1 http://www.devguru.com/technologies/ado/quickref/ado_intro.html 
168 | Windows Forms 
Libro para José Mora 
ello usaré una serie de ejemplos, donde gradualmente aparecen los conceptos, 
ilustrando su uso, y describiré sus posibilidades. 
Para la comprensión de los ejemplos, supondré un conocimiento previo del 
lenguaje C# y su “entorno próximo” (manejo de Visual Studio, librerías 
básicas, etc), así como del manejo de bases de datos relacionales y el lenguaje 
SQL. 
Los ejemplos solo pretenden ilustrar de manera sencilla y directa el uso de 
ADO y por tanto dejan de lado aspectos como la organización del código o el 
diseño en una aplicación profesional de bases de datos. De esta forma 
podemos tener, en el menor tiempo posible, una comprensión suficiente de 
ADO y Linq para comenzar a utilizarlas en una base de datos. 
Vamos a ilustrar el uso de ADO trabajando sobre una base de datos relacional. 
Necesitarás disponer de una base de datos. Aquí, por facilidad, trabajaremos 
con bases de datos del motor MS SQL Server, ya que viene soportado por 
Visual Studio “de fábrica”. Puedes crear la base de datos con tu herramienta 
preferida que tenga soporte para este motor, pero veamos como crearla desde 
Visual Studio: 
1. Ir al Explorador de Bases de Datos pulsando Ctl + Alt + S, o través del
menú Herramientas.
2. Click derecho sobre el nodo Conexiones de Datos y escoger ‘Agregar
Conexión’.
3. En ‘nombre de archivo’, introduce (sin las “”) “<mi ruta en disco
local>\SERP.mdf”. Debes usar una ruta a una carpeta local, ya que los
motores relacionales no trabajan sobre carpetas de red.
4. Dado que el fichero no existe, VS te solicitará confirmación para crear
la base de datos. ¡Acepta y listo!, ya tenemos base de datos.
Ahora, lo que necesitamos es crear alguna tabla sencilla. Para usar algo que la 
mayoría conoce, nuestra base de datos representará un Sencillo ERP (por eso 
el nombre SERP.mdf). Como es un ejemplo familiar y sencillo, podremos 
concentrar las neuronas en entender el ADO y no el modelo de tablas. 
Vamos a crear una tabla simple de EMPRESAS, usando nuevamente el 
Explorador de Base de Datos de Visual Studio: 
Windows Forms | 169 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
1. Desplegar el nodo ‘SERP.mdf’ que representa nuestra base de datos. 
 
 
2. Click derecho sobre el subnodo Tablas, escoger ‘Agregar nueva Tabla’. 
 
3. Aparece una ventana en la que podemos definir los campos de nuestra 
tabla, tal como se muestra en la figura 3. Para establecer la clave 
primaria, debemos hacer click derecho sobre el campo ya definido. 
170 | Windows Forms 
Libro para José Mora 
Obtendremos de esta forma una tabla equivalente al resultado de la sentencia 
SQL siguiente: 
/*------------------------------------------------- 
Fichero: 001_create_empresas.sql 
-------------------------------------------------*/ 
CREATE TABLE [dbo].[EMPRESAS]( 
[ID] [bigint] NOT NULL, 
[CODIGO] [varchar](10) NOT NULL, 
[NOMBRE] [varchar](40) NOT NULL, 
[NIF] [varchar](16) NOT NULL, 
PRIMARY KEY CLUSTERED ( [ID] ASC) 
) ON [PRIMARY] 
Si tienes instalado SQL Server Management Studio, o tu herramienta favorita 
de base de datos, podemos usar el fichero 001_create_empresas.sql, o copiar 
y pegar su contenido, para definir la tabla ejecutando la sentencia. 
¡Ahora estamos listos para el primer programa! 
Windows Forms | 171 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
SqlConnection 
Antes de nada, crea un proyecto de consola y nómbralo “ConectarBD”. 
También puedes abrir el ejemplo ya hecho que se encuentra en la carpeta 
001_ConectarBD. 
Para conectar nuestra aplicación a un servidor de datos relacional, ADO 
necesita una clase capaz de establecer dicha conexión. Dicha clase es la que 
dialoga con las APIS cliente del motor (el ‘driver’). La clase necesaria para 
comunicar con MS SQL Server es: 
 
System.Data.SqlClient.SqlConnection 
 
Lo que necesitamos a continuación es configurar una instancia de 
SqlConnection con los detalles de nuestra conexión: ruta del fichero de la base 
de datos, usuario y clave,etc. Estos parámetros forman una colección de 
valores tipo <nombre de parámetro>=<valor de parámetro>, algo análogo a 
un típico fichero .INI. Concatenando cada pareja <nombre=valor> con un “;”, 
obtenemos lo que, en la jerga ADO, se llama una ‘cadena de conexión’. Un 
ejemplo para MS SQL Server, podría ser: 
 
string connectionStr = "Data 
Source=.\\SQLEXPRESS;Initial 
Catalog=c:\data\SERP.mdf;Integrated 
Security=True;Connect Timeout=30;User Instance=True"; 
 
Observa que los parámetros posibles son específicos del motor concreto al que 
conectas. Es lógico: la manera de configurar la conexión al motor, ¡depende 
de las capacidades de cada motor!. 
Para averiguar los parámetros y valores permitidos en cada caso, deberás 
consultar la documentación del driver ADO de tu motor concreto. Una 
búsqueda por Internet, también puede proporcionarte ejemplos concretos de 
cadenas de conexión que podrás adaptar. A este fin, un sitio interesante es: 
http://www.connectionstrings.com/ 
Ahí podrás encontrar ejemplos de cadenas para multitud de motores. Hay una 
cierta tendencia a que, parámetros equivalentes entre motores, tengan el 
mismo nombre de parámetro, como por ejemplo Integrated Security o 
Password, aunque lo mejor es no confiarse. 
172 | Windows Forms 
Libro para José Mora 
Volviendo a nuestro ejemplo en MS SQL Server, veamos como obtener la 
cadena que necesitamos: 
1. Ir al Explorador de Bases de Datos pulsando Ctl + Alt + S, o través del
menú Herramientas.
2. Click derecho sobre el nodo SERP.mdf y escoger ‘Propiedades’.
3. Se abrirá una ventana donde podemos ver la cadena de conexión.
Ahora, basta copiar y pegar en nuestro código. Con esto ya podemos escribir 
un sencillo programa que conecte a nuestra base de datos. Este podría ser el 
resultado: 
using System; 
using System.IO; 
using System.Collections.Generic; 
using System.Data; 
using System.Data.Common; 
using System.Data.SqlClient; 
using System.Linq; 
using System.Text; 
// Ejemplo en la carpeta 001_ConectarBD\ 
namespace ConectarBD 
{ 
 class Program 
 { 
 static void Main(string[] args) 
 { 
 SqlConnection connection; 
 string appPath = 
System.AppDomain.CurrentDomain.BaseDirectory; 
 string fullPathBD = 
Path.GetFullPath(String.Format(appPath + 
"..\\..\\..\\..\\..\\data\\SERP.mdf")); 
 string connectionStr = "Data 
Source=.\\SQLEXPRESS;Initial Catalog={0};Integrated 
Security=True;Connect Timeout=30;User Instance=True"; 
 try 
 { 
 connection = new 
SqlConnection(String.Format(connectionStr, 
fullPathBD)); 
Windows Forms | 173 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
 connection.Open(); 
 Console.WriteLine("Conectado!"); 
 } 
 catch(Exception ex) 
 { 
 Console.WriteLine("Error: Fallo 
al crear conexión a la base de datos. \n{0}", 
ex.Message); 
 } 
 Console.WriteLine("Pulse una tecla para 
salir..."); 
 Console.ReadKey(); 
 } 
 } 
} 
 
 
En el ejemplo se calcula la ruta a la base de datos, de forma relativa a la ruta 
del ejecutable, simplemente para que los ejemplos funcionen sin mas que 
copiarlos a una carpeta de tu disco local. 
En una aplicación de verdad, lo mas probable seria leer la cadena de conexión 
de algún medio externo, de manera que pudiera cambiarse sin compilar el 
programa. Posibilidades típicas serian: el registro de Windows, el fichero de 
configuración de la aplicación (“MiAplicacion.exe.econfig”), etc. 
En esta línea de razonamiento, tal vez quieras que tu programa pueda 
funcionar con mas de un motor determinado. En ese caso necesitaríamos 
crear en cada caso una instancia de la clase de conexión adecuada. 
Afortunadamente, ADO nos permite escribir nuestro código de forma que no 
dependa de la clase concreta de conexión. Parte del truco es que todas las 
clases de conexión deben implementar la misma interfaz IDbConnection. 
DbProviderFactory 
La interfaz IDbConnection nos permitiría declarar nuestra conexión sin 
referenciar la clase SqlConnection en la declaración: 
SqlConnection connection; 
 
También tenemos problemas en las líneas: 
174 | Windows Forms 
Libro para José Mora 
using System.Data.SqlClient; 
... 
connection = new SqlConnection … 
Naturalmente, el using es consecuencia de necesitar invocar el new. Para 
evitar usar new, lo que haremos es delegar la construcción de la instancia en 
un “objeto factoría”, es decir, uno cuya responsabilidad es crear instancias de 
otra clase. Estos objetos, en ADO, son instancias de las clase: 
System.Data.Common.DbProviderFactory 
En la jerga ADO, “data provider” es el termino usado para describir, lo que 
nosotros hemos venido llamando fuente de datos. Así que un DbProvider no 
es más que una fuente de datos del tipo “base de datos”. 
Para obtener nuestro DbProvider solo tenemos que pedirlo a la clase 
DbProviderFactories: 
DbProviderFactories.GetFactory("System.Data.SqlClient"
) 
La cadena usada para recuperar el DbProvider es única para cada uno. Los 
DbProviders deben registrarse en la plataforma .NET, para que la clase 
DbProviderFactories pueda encontrarlos. Normalmente esto sucede al 
instalar el driver o las herramientas cliente de tu motor. La información de 
registro aparece en el fichero ‘machine.config’ de tu instalación del 
framework .NET, concretamente en la sección DbProviderFactories.
 Un ejemplo del contenido de este fichero seria el siguiente fragmento: 
<system.data> 
 <DbProviderFactories> 
 <add name="SqlClient Data Provider" 
 invariant="System.Data.SqlClient" 
 description=".Net Framework Data Provider for 
SqlServer" 
 type="System.Data.SqlClient.SqlClientFactory, 
System.Data, 
 Version=2.0.0.0, Culture=neutral, 
PublicKeyToken=b77a5c561934e089" 
 /> 
Windows Forms | 175 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 </DbProviderFactories> 
</system.data> 
 
La cadena para pasar al método DbProviderFactories.GetFactory(), es el 
contenido del atributo “invariant” que vemos en el fragmento 
anterior. 
Ahora ya podemos reescribir nuestro ejemplo de conexión sin acoplar nuestro 
código con el provider especifico. El resultado podría ser así: 
 
// Ejemplo en la carpeta 002_ConectarBD2\ 
 
using System; 
using System.IO; 
using System.Collections.Generic; 
using System.Data; 
using System.Data.Common; 
using System.Data.SqlClient; 
using System.Linq; 
using System.Text; 
 
namespace ConectarBD 
{ 
 class Program 
 { 
 static void Main(string[] args) 
 { 
 string appPath = 
System.AppDomain.CurrentDomain.BaseDirectory; 
 string fullPathBD = 
Path.GetFullPath(String.Format(appPath + 
"..\\..\\..\\..\\..\\data\\SERP.mdf")); 
 
 string connectionStr = "Data 
Source=.\\SQLEXPRESS;Initial Catalog={0};Integrated 
Security=True;Connect Timeout=30;User Instance=True"; 
 
 IDbConnection connection; 
 
 try 
 { 
 connection = 
DbProviderFactories.GetFactory("System.Data.SqlClient").CreateConnec
tion(); 
 connection.ConnectionString = connectionStr; 
 
 Console.WriteLine("Conectado usando interfaz 
generica!"); 
 } 
 catch(Exception ex) 
 { 
 Console.WriteLine("Error: Fallo al crear conexión a 
la base de datos. \n{0}", ex.Message); 
 } 
 Console.WriteLine("Pulse una tecla para salir..."); 
 Console.ReadKey(); 
 } 
 } 
} 
 
176 | Windows Forms 
Libro para José Mora 
Recordatorio 
 ADO denomina “providers” a las clases que nos conectan con cada
fuente de datos.
 La interfaz IDbConnection representa una conexión a una base de
datos. Esta interfaz debe ser implementada por los “providers” de
.NET que acceden a una base de datos relacional.
 Para establecer la conexión con nuestra base de datos, necesitaremos
un objeto IDbConnection configurado mediante una cadena de
conexión.
 Unacadena de conexión establece, en formateo texto, un serie de
parejas <párametro=valor>. Los parámetros y valores posibles son
definidos por cada clase “provider”.
 Cada provider debe estar instalado en .NET, antes de poder usarse.
 Los providers pueden instanciarse de forma dinámica, a través de su
factoría. Las factorías aparecen registradas en la sección
DbProviderFactories del fichero ‘machine.config’ y su identificador es
el atributo ‘invariant’. Este atributo es lo que debemos pasar a
DbProviderFactories.GetFactory() para obtener la factoría.
 http://msdn.microsoft.com/es-es/library/h43ks021.aspx
 http://www.connectionstrings.com/
Windows Forms | 177 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
El Modo 
Conectado 
Los ejemplos anteriores eran realmente muy básicos y centrados totalmente 
en la tarea crítica de conectar con nuestra base de datos. Ni siquiera nos 
hemos ocupado de cerrar explícitamente la conexión a nuestra base de datos. 
Además, tampoco tenemos ningún dato en la base de datos. En este capítulo, 
vamos a resolver estos puntos haciendo uso de los servicios que ofrece ADO 
cuando trabajamos con lo que se conoce como “modo conectado”. 
Si sobre el código fuente del último ejemplo, nos colocamos en el identificador 
IDbConnection, y hacemos click derecho + ‘Ir a definición’, el editor nos 
mostrará el código fuente de la interfaz: 
 
#region Ensamblado System.Data.dll, v4.0.30319 
// C:\Archivos de programa\Reference 
Assemblies\Microsoft\Framework\.NETFramework\v4.0\Profile\Client\Sys
tem.Data.dll 
#endregion 
 
using System; 
 
namespace System.Data 
{ 
 // Resumen: 
 // Representa una conexión abierta a un origen de datos y la 
implementan los 
 // proveedores de datos de .NET Framework que tienen acceso 
a bases de datos 
 // relacionales. 
 public interface IDbConnection : IDisposable 
 { 
 // Resumen: 
 // Obtiene o establece la cadena que se utiliza para 
abrir una base de datos. 
 // 
 // Devuelve: 
 // Cadena que contiene la configuración de conexión. 
 string ConnectionString { get; set; } 
 // 
178 | Windows Forms 
Libro para José Mora 
 // Resumen: 
 // Obtiene el tiempo de espera para intentar establecer 
una conexión antes de 
 // detenerse y generar un error. 
 // 
 // Devuelve: 
 // Tiempo (en segundos) que se debe esperar para que se 
abra la conexión.El 
 // valor predeterminado es 15 segundos. 
 int ConnectionTimeout { get; } 
 // 
 // Resumen: 
 // Obtiene el nombre de la base de datos actual o de la 
que se va a utilizar 
 // una vez que se abre la conexión. 
 // 
 // Devuelve: 
 // Nombre de la base de datos actual o de la que se va a 
utilizar una vez que 
 // se abra la conexión.El valor predeterminado es una 
cadena vacía. 
 string Database { get; } 
 // 
 // Resumen: 
 // Obtiene el estado actual de la conexión. 
 // 
 // Devuelve: 
 // Uno de los valores de System.Data.ConnectionState. 
 ConnectionState State { get; } 
 // Resumen: 
 // Inicia una transacción de base de datos. 
 // 
 // Devuelve: 
 // Objeto que representa la nueva transacción. 
 IDbTransaction BeginTransaction(); 
 // 
 // Resumen: 
 // Inicia una transacción de base de datos con el valor 
de System.Data.IsolationLevel 
 // especificado. 
 // 
 // Parámetros: 
 // il: 
 // Uno de los valores de System.Data.IsolationLevel. 
 // 
 // Devuelve: 
 // Objeto que representa la nueva transacción. 
 IDbTransaction BeginTransaction(IsolationLevel il); 
 // 
 // Resumen: 
 // Cambia la base de datos actual para un objeto 
Connection abierto. 
 // 
 // Parámetros: 
 // databaseName: 
 // Nombre de la base de datos que se utiliza en lugar de 
la actual. 
 void ChangeDatabase(string databaseName); 
 // 
 // Resumen: 
 // Cierra la conexión con la base de datos. 
 void Close(); 
 // 
 // Resumen: 
 // Crea y devuelve un objeto Command asociado a una 
conexión. 
 // 
 // Devuelve: 
Windows Forms | 179 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 // Objeto Command asociado a una conexión. 
 IDbCommand CreateCommand(); 
 // 
 // Resumen: 
 // Abre una conexión de base de datos con la 
configuración indicada por la propiedad 
 // ConnectionString del objeto Connection específico del 
proveedor. 
 void Open(); 
 } 
} 
DbConnection 
Esta interfaz , además de abrir la conexión, permite crearla, consultar su 
estado, manejar transacciones y alguna cosa más. Aún así, parece algo 
limitada, y realmente lo es, las clases que implementan dicha interfaz como 
SqlClient, ofrecen más funcionalidad. Por ejemplo, disponemos de un evento 
para responder cuando la conexión se abre o cierra. 
No obstante, si consultamos la definición de DbProviderFactory, nos 
encontramos: 
 
public abstract class DbProviderFactory 
... 
public virtual DbConnection CreateConnection(); 
... 
 
Así que CreateConnection() no retorna la interfaz, si no la clase abstracta 
DbConnection. El uso de esta clase tampoco acopla nuestro código a un motor 
concreto, por lo que es una alternativa válida al uso de IDbConnection. 
Incluso podemos combinar ambas: 
static IDbConnection GetConnection() 
{ 
 DbConnection connection = 
DbProviderFactories.GetFactory("System.Data.SqlClient").CreateConnec
tion(); 
 connection.ConnectionString = GetConnectionString(); 
 connection.StateChange += new 
StateChangeEventHandler(connection_StateChange); 
 return connection; 
} 
 
Este método de nuestra aplicación crea la conexión y captura el evento 
StateChange para responder cuando la conexión se abre o cierra, pero 
devuelve sólo la interfaz al resto de la aplicación, que típicamente sólo 
ejecutará sentencias SQL y tal vez, manejo de transacciones. 
180 | Windows Forms 
Libro para José Mora 
Además de notificar el cambio de estado de la conexión, DbConnection te 
ofrece algunas otras cosas de interés, como la versión del servidor a la que 
estás conectado o recuperar información del esquema de la base de datos. 
Aunque esto último lo devuelve en un objeto del tipo DataTable, que aún no 
hemos visitado. 
DbCommand 
Sin duda la funcionalidad principal de DbConnection es la capacidad de crear 
objetos DbCommand. La clase DbCommand es la que nos permite enviar sentencias 
SQL a nuestro motor de base de datos relacional: 
static void Insert(IDbConnection connection) 
{ 
 string sentenciaSql = @" 
 INSERT INTO EMPRESAS 
 (ID, CODIGO, NOMBRE, NIF) 
 VALUES 
 ({0}, '{0}', 'EMPRESA_{0}', 'N-{0}') 
 "; 
 IDbCommand cmd = connection.CreateCommand(); 
 try 
 { 
 for (int idx = 0; idx < 100; idx++) 
{ 
 cmd.CommandText = String.Format(sentenciaSql, idx); 
 Console.WriteLine("Ejecutada sentencia {0}", idx); 
cmd.ExecuteNonQuery();
 } 
 } 
 catch (Exception e) 
 { 
 Console.WriteLine(e.Message); 
 } 
} 
Vemos que el uso es sencillo: basta asignar la propiedad commandText y usar 
una modalidad de ejecución de la sentencia. En nuestro caso escogemos 
ExecutaNonQuery(), ya que nuestra sentencia no es una query, es decir no 
es una sentencia tipo SELECT … que retorne un conjunto de filas, si no una 
sentencia INSERT... que creará nuevas filas en la tabla correspondiente. 
Otros métodos disponibles para ejecutar un IDbCommand pueden verse con ‘Ir 
a Definicion’ (F12 sobre el identificador) : 
... 
// Resumen: 
// Ejecuta el System.Data.IDbCommand.CommandText en 
System.Data.IDbCommand.Connection 
// y genera un System.Data.IDataReader. 
// 
Windows Forms | 181 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
// Devuelve: 
// Excepción System.Data.IDataReader. 
IDataReader ExecuteReader(); 
// 
// Resumen: 
// Ejecuta System.Data.IDbCommand.CommandText en 
System.Data.IDbCommand.Connection 
// y genera un System.Data.IDataReader mediante uno de losvalores de System.Data.CommandBehavior. 
// 
// Parámetros: 
// behavior: 
// Uno de los valores de System.Data.CommandBehavior. 
// 
// Devuelve: 
// Excepción System.Data.IDataReader. 
IDataReader ExecuteReader(CommandBehavior behavior); 
// 
// Resumen: 
// Ejecuta la consulta y devuelve la primera columna de la 
primera fila del 
// conjunto de resultados que devuelve la consulta.Las demás 
columnas o filas 
// no se tienen en cuenta. 
// 
// Devuelve: 
// Primera columna de la primera fila del conjunto de 
resultados. 
object ExecuteScalar(); 
... 
 
Ahora sólo queda juntar las piezas y tendrás un programa para rellenar 
nuestra tabla EMPRESAS: 
static void Main(string[] args) 
{ 
 try 
 { 
 using (IDbConnection connection = GetConnection()) 
 { 
 connection.Open(); 
 Console.WriteLine("Conectado!"); 
 Delete(connection); // vacia la tabla 
 Insert(connection); 
 Console.WriteLine("Completado!"); 
 } 
 } 
 catch (Exception ex) 
 { 
 Console.WriteLine("Error: \n{0}", ex.Message); 
 } 
 
 Console.WriteLine("Pulse una tecla para salir..."); 
 Console.ReadKey(); 
} 
 
 
Observa que ahora empleamos using para proteger el uso de la conexión, 
asegurando que se cierra y se libera cuando terminamos nuestra tarea, 
182 | Windows Forms 
Libro para José Mora 
efectivamente la conexión implementa IDispose el cual invoca el método 
Close de la conexión. Esto se ve durante la ejecución gracias al manejador del 
evento StateChange. Además hemos añadido un método que vacía la tabla 
mediante una sentencia DELETE..., para poder ejecutar el ejemplo 
repetidamente sin producir claves duplicadas. El ejemplo completo se 
encuentra en el proyecto 003_insertarDatos. ¡Ya tenemos datos en nuestra 
tabla!. Veamos ahora como leerlos. 
IDataReader 
La manera de proceder es muy similar: hemos de configurar una instancia de 
IDbCommand con una sentencia SELECT..., y, a continuación, invocar el 
método ExecuteReader(), el cual nos va a retornar un objeto IDataReader 
con el que podremos recorrer el conjunto de filas devueltas por nuestra 
consulta: 
using (IDataReader reader = cmd.ExecuteReader()) 
{ 
 while (reader.Read()) 
 { 
 Console.WriteLine("| {0,10} | {1,20} | {2,20} |", 
reader["CODIGO"], reader["NOMBRE"], reader["NIF"]); 
 }; 
}; 
Si consultamos la definición de IDbCommand, obtenemos:
// C:\Archivos de programa\Reference 
Assemblies\Microsoft\Framework\.NETFramework\v4.0\Profile\Client\Sys
tem.Data.dll 
#endregion 
using System; 
namespace System.Data 
{ 
 // Resumen: 
 // Proporciona un medio para leer una o más secuencias de 
sólo avance de conjuntos 
 // de resultados obtenidos mediante la ejecución de un 
comando en un origen 
 // de datos. La implementan los proveedores de datos de .NET 
Framework que tienen 
 // acceso a bases de datos relacionales. 
 public interface IDataReader : IDisposable, IDataRecord 
Windows Forms | 183 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 { 
 // Resumen: 
 // Obtiene un valor que indica la profundidad del 
anidamiento de la fila actual. 
 // 
 // Devuelve: 
 // Nivel de anidamiento. 
 int Depth { get; } 
 // 
 // Resumen: 
 // Obtiene un valor que indica si el lector de datos 
está cerrado. 
 // 
 // Devuelve: 
 // true si el lector de datos está cerrado; en caso 
contrario, false. 
 bool IsClosed { get; } 
 // 
 // Resumen: 
 // Obtiene el número de filas modificadas, insertadas o 
eliminadas mediante 
 // la ejecución de la instrucción SQL. 
 // 
 // Devuelve: 
 // Número de columnas modificadas, insertadas o 
eliminadas; 0 si no hay columnas 
 // afectadas o la instrucción produce un error; -1 para 
las instrucciones SELECT. 
 int RecordsAffected { get; } 
 
 // Resumen: 
 // Cierra el objeto System.Data.IDataReader. 
 void Close(); 
 // 
 // Resumen: 
 // Devuelve un System.Data.DataTable que describe los 
metadatos de columna del 
 // System.Data.IDataReader. 
 // 
 // Devuelve: 
 // System.Data.DataTable que describe los metadatos de 
columna. 
 // 
 // Excepciones: 
 // System.InvalidOperationException: 
 // La clase System.Data.IDataReader está cerrada. 
 DataTable GetSchemaTable(); 
 // 
 // Resumen: 
 // Desplaza el lector de datos al resultado siguiente al 
leer los resultados 
 // de instrucciones SQL por lotes. 
 // 
 // Devuelve: 
 // true si hay más filas; en caso contrario, false. 
 bool NextResult(); 
 // 
 // Resumen: 
 // Desplaza la interfaz System.Data.IDataReader al 
siguiente registro. 
 // 
 // Devuelve: 
 // true si hay más filas; en caso contrario, false. 
 bool Read(); 
 } 
} 
184 | Windows Forms 
Libro para José Mora 
Puedes ver que lo más relevante es el meétodo Read(), que retorna true si lee 
una nueva fila de datos y la interfaz heredada IDataRecord, que nos permite 
leer los valores de cada columna en la fila recuperada. Aquí tienes un listado, 
sin los comentarios de código: 
#region Ensamblado System.Data.dll, v4.0.30319 
// C:\Archivos de programa\Reference 
Assemblies\Microsoft\Framework\.NETFramework\v4.0\Profile\Client\Sys
tem.Data.dll 
#endregion 
using System; 
using System.Reflection; 
namespace System.Data 
{ 
 public interface IDataRecord 
 { 
 int FieldCount { get; } 
 object this[int i] { get; } 
 object this[string name] { get; } 
 bool GetBoolean(int i); 
 byte GetByte(int i); 
 long GetBytes(int i, long fieldOffset, byte[] buffer, int 
bufferoffset, int length); 
 char GetChar(int i); 
 long GetChars(int i, long fieldoffset, char[] buffer, int 
bufferoffset, int length); 
 IDataReader GetData(int i); 
 string GetDataTypeName(int i); 
 DateTime GetDateTime(int i); 
 decimal GetDecimal(int i); 
 double GetDouble(int i); 
 Type GetFieldType(int i); 
 float GetFloat(int i); 
 Guid GetGuid(int i); 
 short GetInt16(int i); 
 int GetInt32(int i); 
 long GetInt64(int i); 
 string GetName(int i); 
 int GetOrdinal(string name); 
 string GetString(int i); 
 object GetValue(int i); 
 int GetValues(object[] values); 
 bool IsDBNull(int i); 
 } 
} 
Como puedes ver, ésta interfaz, es también bastante sencilla. Asociando un 
índice a cada columna, según su posición, nos permite obtener su nombre 
Windows Forms | 185 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
tipo, si contiene null o su valor. El valor puede recuperarse con un método 
específico por tipo de dato, o de forma genérica como un object mediante 
GetValue. 
El argumento que debemos pasar, para usar estos métodos, es el índice de 
columna, es decir su posición en la consulta tomando como cero la primera. 
En cambio, con el selector de array, podemos usar el nombre de columna o el 
índice indistintamente. Aunque, lógicamente, obtenemos el valor siempre 
como object. Es fácil sospechar que el acceso por nombre es más lento, a 
partir del nombre de la columna se recupera el índice, después se lee el valor 
según su tipo nativo (que es como típicamente lo enviará el motor) y 
finalmente se convierte a object. Sin embargo, en el código de nuestra 
aplicación debemos usar el nombre y no el índice, ya que este último puede 
cambiar fácilmente, al tener que retocar nuestra consulta o modificar la 
estructura de la tabla. Imagina si tu código muestra la columna 3 en la caja de 
texto para el nombre y la columna 3 pasa, por ejemplo, a ser la edad, debido a 
una pequeña modificación en la base de datos. Este tipo de código es difícil de 
mantener. En cambio, sí podemos usar el índice cuandoel trabajo con cada 
columna es “anónimo”, es decir la columna concreta no juega ningún papel. 
Un ejemplo tipo sería una pequeña utilidad que compara el valor de todas las 
columnas de dos filas de una misma consulta. 
IDbDataParameter 
En una aplicación de base de datos típica, es frecuente que necesitemos usar 
consultas parametrizadas. Estas consultas son compiladas por el motor y 
pueden ser invocadas repetidamente, cambiando solo el valor del parámetro. 
Esto supone una ganancia de tiempo al compilar la consulta una sola vez en 
lugar de una vez por ejecución. Para lograr ésto, lo único que necesitamos 
hacer es invocar el método Prepare() de nuestro Command. Para manipular los 
parámetros de nuestra consulta, en cambio, necesitamos usar instancias de 
IDbDataParameter. Como siempre, un pequeño ejemplo nos ahorrará 
muchas palabras: 
static void Leer(IDbConnection connection, string filtro) 
{ 
 const string sqlSelect = @" 
 select * 
 from empresas 
 where nombre like @filtro; 
 "; 
 
 const string sqlCount = @" 
 select Count(*) 
186 | Windows Forms 
Libro para José Mora 
 from empresas 
 where nombre like '@filtro'; 
 "; 
 IDbCommand cmd = connection.CreateCommand(); 
 //Crear y configurar el parámetro 
 IDbDataParameter prm = cmd.CreateParameter(); 
 prm.ParameterName = "@filtro"; 
 prm.DbType = System.Data.DbType.String; 
 prm.Direction = ParameterDirection.Input; 
 prm.Size = 100; 
cmd.Parameters.Add(prm);
cmd.CommandText = sqlCount;
cmd.Prepare();
 prm.Value = filtro; 
 Console.WriteLine("Encontrados {0} registros:", 
cmd.ExecuteScalar());
cmd.CommandText = sqlSelect;
cmd.Prepare();
 using (IDataReader reader = cmd.ExecuteReader()) 
 { 
 while (reader.Read()) 
 { 
 Console.WriteLine("| {0,10} | {1,20} | {2,20} |", 
reader["CODIGO"], reader["NOMBRE"], reader["NIF"]); 
 }; 
 }; 
} 
Los objetos IDbDataParameter deben ser configurados y añadidos al 
comando, antes de que preparemos el comando. Después de prepararlo, ya 
podemos dar valores y ejecutar nuestro comando. Naturalmente si se 
modifica la propiedad commandText, el comando necesita prepararse de 
nuevo, pero podemos reutilizar los parámetros, como en el ejemplo. Aunque, 
si la nueva consulta no tiene nada que ver con la anterior, lo más sencillo es 
construir un nuevo comando. En el ejemplo hemos asignado explícitamente la 
dirección del parámetro para ilustrar el uso de la propiedad prm.Direction, 
pero Input es precisamente el valor por defecto, por lo que podríamos omitir 
ésta asignación. El código fuente completo de este ejemplo se encuentra en el 
proyecto 004_LeerDatos. 
DbCommand “reloaded” 
Con lo que acabamos de ver, podemos ya manipular y consultar nuestras 
tablas, enviando sentencias SQL a nuestro motor relacional. Sin embargo, las 
Windows Forms | 187 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
bases de datos relacionales suelen ofrecen otras capacidades, como 
disparadores, procedimientos almacenados o transacciones. Aunque la 
primera es una capacidad interna del motor, las dos últimas están disponibles 
para usarse desde el código de nuestra aplicación. 
Para ilustrar el uso de transacciones y procedimientos almacenados desde 
ADO, vamos primero a enriquecer nuestra base de datos. Primeramente 
creamos una tabla PAISES: 
 
/* Fichero sql\002_create_paises.sql */ 
 
CREATE TABLE [dbo].[PAISES]( 
 [ID] [bigint] NOT NULL, 
 [CODIGO] [varchar](10) NOT NULL, 
 [NOMBRE] [varchar](20) NOT NULL, 
 PRIMARY KEY CLUSTERED ( [ID] ASC) 
) ON [PRIMARY] 
 
 
Para ilustrar el uso de procedimientos almacenados, vamos a crear uno que 
usaremos para generar las claves primarias de nuestra tabla. La idea será 
tener unos contadores cuyo valor está guardado en otra tabla y utilizar un 
procedimiento que nos devuelve un nuevo valor y guarda de nuevo el último 
valor generado. Cada contador lo identificamos por un nombre. La tabla 
tendrá por tanto una columna para el identificador y otra para el valor, siendo 
el identificador clave primaria. Aquí tienes el listado de la tabla y el 
procedimiento: 
/* Fichero sql\003_create_IDVALUES.sql */ 
 
CREATE TABLE [dbo].[IDVALUES]( 
 [ID] [varchar](10) NOT NULL, 
 [VALUE] [bigint] NOT NULL 
 PRIMARY KEY CLUSTERED ( [ID] ASC) 
) ON [PRIMARY] 
 
GO 
 
 
CREATE PROCEDURE [dbo].[NEXTID] ( 
 @DELTA Integer, 
 @ID varchar(10), 
 @NEWVALUE bigint output) 
AS 
BEGIN 
 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ 
 BEGIN TRANSACTION 
188 | Windows Forms 
Libro para José Mora 
 UPDATE IDVALUES WITH (UPDLOCK) 
SET @NEWVALUE = VALUE = VALUE + @DELTA 
WHERE ID = UPPER(@ID) 
 COMMIT TRANSACTION 
END 
GO 
Para crear el procedimiento, abre el Explorador de Bases de Datos pulsando 
Ctl + Alt + S, o a través del menú Herramientas. Después, despliega el árbol 
de la base de datos SERP y, con click derecho sobre el nodo Procedimientos 
almacenados, escoge la opción “Agregar nuevo procedimiento almacenado”. 
Aparecerá un editor con un esqueleto de procedimiento, listo para completar. 
Basta copiar nuestro código y pegarlo en lugar del esqueleto mostrado, grabar 
y listo. 
Lo que hacemos con estos contadores y el procedimiento almacenado es 
imitar el comportamiento de lo que otros motores llaman “secuencias”. Sql 
Server no dispone de esta funcionalidad, pero con lo anterior podemos 
aproximar su funcionalidad. Lo interesante de las verdaderas secuencias es 
que son “trasparentes” a la transacción. Es decir aunque una transacción falle, 
el valor de la secuencia no retorna al estado anterior a la transacción. Por 
tanto no hay peligro de duplicar el valor obtenido al invocar la secuencia. Hay 
quien prefiere el uso de campos autoincremento para asignar la clave 
primaria, pero el inconveniente es que no conocemos el valor que tendrá la 
clave hasta que no insertamos la fila. Con la secuencia en cambio, podemos 
pedir el valor de antemano y usarlo en la aplicación para asignar la clave 
primaria, asignar otras claves foráneas que enlacen con ella y grabar todo en 
una sola transacción. En este sencillo ejemplo daría lo mismo, pero de esta 
forma tenemos una excusa para mostrar el uso de un procedimiento 
almacenado. 
Como ahora ya tenemos 3 tablas, vamos también a organizar mejor el código 
para evitar repeticiones. Como las tres tablas tienen mucho comportamiento 
común, lo abstraemos en la clase TableHelper. Este es el método de la clase 
TableHelper que usa nuestra tabla de contadores: 
public long NextID(long delta = 1) 
{ 
if (NextIDCmd == null) 
 { 
 NextIDCmd = DB.CreateCommand(); 
NextIDCmd.CommandType = CommandType.StoredProcedure; 
Windows Forms | 189 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 NextIDCmd.CommandText = "NEXTID"; 
 
 IDbDataParameter pID = NextIDCmd.CreateParameter(); 
 pID.DbType = DbType.String; 
 pID.Direction = ParameterDirection.Input; 
 pID.ParameterName = "@ID"; 
 pID.Value = GetTableName(); 
 NextIDCmd.Parameters.Add(pID); 
 
 IDbDataParameter pDELTA = 
NextIDCmd.CreateParameter(); 
 pDELTA.DbType = DbType.Int64; 
 pDELTA.Direction = ParameterDirection.Input; 
 pDELTA.ParameterName = "@DELTA"; 
 pDELTA.Value = delta; 
 NextIDCmd.Parameters.Add(pDELTA); 
 
 IDbDataParameter pNEWVALUE = 
NextIDCmd.CreateParameter(); 
 pNEWVALUE.DbType = DbType.Int64; 
 pNEWVALUE.Direction = ParameterDirection.Output; 
 pNEWVALUE.ParameterName = "@NEWVALUE"; 
 NextIDCmd.Parameters.Add(pNEWVALUE); 
 } 
 NextIDCmd.Transaction = DB.CurrentTransaction; 
 NextIDCmd.ExecuteNonQuery(); 
 
 return 
(long)((IDbDataParameter)NextIDCmd.Parameters["@NEWVALUE"]).Value; 
} 
 
 
Vemos que invocar un procedimiento es muy similar a ejecutar una sentencia 
SQL: 
En lugar de la sentencia usamos el nombre del procedimiento y los 
parámetros del comando representan los argumentos del procedimiento. Eso 
si, debemos indicar al comando que el tipo es StoredProcedure para que 
todo se interpreteadecuadamente. 
 
El método retorna el siguiente valor para el contador cuyo nombre coincide 
con el nombre de tabla, obtenido mediante el método GetTablename(): 
 
 public virtual string GetTableName() 
 { 
 return this.GetType().Name.ToUpper(); 
 } 
 
 
Ahora basta heredar de TableHelper, por ejemplo: 
 
190 | Windows Forms 
Libro para José Mora 
 public class Empresas : TableHelper 
 { 
 public Empresas(SerpDb DB) : base(DB) { } 
. 
. 
. 
 } 
DbTransaction 
Siguiendo con nuestra reorganización del código, creamos también una clase 
para simplificar el manejo de nuestra base de datos: 
public class SerpDb 
{ 
public SerpDb() 
{
connection =
DbProviderFactories.GetFactory("System.Data.SqlClient").CreateConnection(
); 
 connection.ConnectionString = GetConnectionString(); 
}
public DbTransaction CurrentTransaction 
{
get; private set; 
}
public void BeginTransaction() 
{
CurrentTransaction = connection.BeginTransaction();
}
public void CommitTransaction() 
{
CurrentTransaction.Commit();
CurrentTransaction = null; 
}
public void RollbackTransaction() 
{
CurrentTransaction.Rollback();
CurrentTransaction = null; 
}
public DbCommand CreateCommand() 
{
var result = connection.CreateCommand(); 
result.Transaction = CurrentTransaction;
return result; 
}
. 
. 
. 
Windows Forms | 191 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
 
La clase SerpDb encapsula la conexión a la base de datos y ofrece servicios 
relacionados como el manejo de transacciones. Ahora estamos listos para 
mostrar el uso de estas nuevas clases: 
 
 static void Main(string[] args) 
 { 
 try 
 { 
 
 var serpdb = new SerpDb(); 
 try 
 { 
 serpdb.StateChange += new 
StateChangeEventHandler(connection_StateChange); 
 serpdb.Open(); 
 
 var id_values = new IdValues(serpdb); 
 var paises = new Paises(serpdb); 
 var empresas = new Empresas(serpdb); 
 
 id_values.DeleteAll(); 
 id_values.Insert(Quote(paises.GetTableName()), 
0); 
 id_values.Insert(Quote(empresas.GetTableName()), 
0); 
 
 paises.DeleteAll(); 
 paises.Insert(paises.NextID(), Quote("ES"), 
Quote("España")); 
 paises.Insert(paises.NextID(), Quote("FR"), 
Quote("Francia")); 
 paises.Insert(paises.NextID(), Quote("PR"), 
Quote("Portugal")); 
 
 empresas.DeleteAll(); 
 
 serpdb.BeginTransaction(); 
 try 
 { 
 
 for (int idx = 0; idx < 100; idx++) 
 { 
 empresas.Insert(empresas.NextID(), 
Quote(idx), Quote("EMPRESA_" + idx), Quote("N-" + idx)); 
 if (idx % 10 == 0) 
 { 
 serpdb.CommitTransaction(); 
 serpdb.BeginTransaction(); 
 } 
 } 
 
 } 
 finally 
 { 
 serpdb.CommitTransaction(); 
 } 
 
192 | Windows Forms 
Libro para José Mora 
Fíjate que primero creamos los contadores a cero, mediante “inserts” en la 
tabla IDVALUES. También usamos transacciones para insertar las empresas de 
10 en 10. Cuando usamos transacciones, debemos tener presente que los 
comandos que usemos deben estar vinculados a la transacción que tengamos 
iniciada en cada momento. Por eso, nuestra clase SerpDb nos crea los 
comandos de forma conveniente: 
public DbCommand CreateCommand() 
{ 
var result = connection.CreateCommand(); 
result.Transaction = CurrentTransaction;
return result; 
} 
Si fallamos en esto, la ejecución del comando nos devolverá un error. 
Para que resulte completo, el ejemplo también ilustra como conectarse y 
como recuperar datos con una consulta. Todos los servicios ADO vistos hasta 
aquí forman los que se llama habitualmente el “modo conectado”, ya que su 
uso requiere tener una conexión abierta con nuestra base de datos. El código 
completo de este ejemplo se encuentra en la carpeta 005_ModoConectado.
Recordatorio 
 El modo conectado de ADO lo forman los servicios que requieren
disponer de una conexión abierta.
 En modo conectado hacemos uso de DbConnection, DbTransaction,
DbCommand, IDataReader, DbDataParameter.
 Mediante DbCommand podemos tanto enviar SQL a la base de datos
como invocar procedimientos.
 Usa Prepare() para optimizar consultas con parámetros.
Windows Forms | 193 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 Un DataReader nos permite navegar por los datos de una consulta, 
pero solo hacia adelante. 
 Si inicias una transacción debes vincular los comandos con ella antes 
de ejecutar el comando. 
 
194 | Windows Forms 
Libro para José Mora 
Windows Forms | 195 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
El Modo 
Desconectado 
Una vez que hemos recuperado los datos de una consulta, nuestra aplicación 
los procesará de alguna forma: los mostrará al usuario para editarlos, creará 
un informe, hará cálculos o lo que sea. Lo que es seguro es que mientras 
hacemos estas cosas, no necesitamos tener abierta la conexión a la base de 
datos. Es interesante que podamos cerrarla, para liberar recursos en nuestro 
motor de datos, de forma que pueda atender un mayor número de usuarios y 
peticiones. Eso es justo lo que nos va a permitir las clase ADO para el modo 
desconectado. 
DataTable 
La primera clase que vamos a visitar es DataTable. Sirve para representar 
una tabla directamente en memoria, sin estar conectado a una base de datos 
y, naturalmente, podemos dar un nombre a nuestra tabla: 
DataTable myTable = new DataTable("Empleados”); 
 
También podemos describir sus columnas usando la clase DataColumn: 
 
DataColumn column = new DataColumn("empID", 
Type.GetType("System.Int32")); 
column.Caption = "ID"; 
column.AllowDBNull = false; 
column.Unique = true; 
column.AutoIncrement = true; 
196 | Windows Forms 
Libro para José Mora 
column.AutoIncrementSeed = 100; 
column.AutoIncrementStep = 1; 
myTable.Columns.Add(column); 
Puedes ver que esta clase dispone de propiedades para definir diversos 
metadatos de la columna. Aquí los hemos usado para crear una columna 
autoincrementada, no nula y no duplicada, ya que la usaremos como clave 
primaria de nuestra tabla. La clave primaria no es más que un array de 
columnas, que debemos añadir a nuestra tabla: 
DataColumn[] PK = new DataColumn[1]; 
PK[0] = myTable.Columns["empID"]; 
myTable.PrimaryKey = PK; 
La clase DataTable también ofrece la propiedad Columns, que podemos 
consultar o modificar. Por ejemplo podemos, usarla para añadir nuevas 
columnas de una forma un poco más directa: 
column = myTable.Columns.Add("Apellido", 
Type.GetType("System.String")); 
column.Caption = "Apellido"; 
Ahora podemos añadir datos a la tabla mediante la clase DataRow.
Esta clase representa una fila de datos como si fuera un array de columnas. 
Podemos referirnos al valor de cada una de sus columnas por su nombre o 
índice. Sin embargo, hemos de tener en cuenta que la actualización de 
DataTable es un proceso en dos fases. Cuando modificamos una de su filas, 
ésta cambia de estado para indicar el cambio realizado (añadir, modificar o 
borrar) de manera que aún podríamos revertir los cambios realizados en el 
DataTable y devolverlo a su estado antes de la actualización. Para consolidar 
los cambios como definitivos, debemos invocar el método AcceptChanges. 
Veámoslo: 
DataRow myRow = myTable.NewRow(); 
myRow["FirstName"] = "First Name"; 
Program.Console.WriteLine("->Row state: {0}", 
myRow.RowState.ToString()); 
// Now add it to table.myTable.Rows.Add(myRow); 
Windows Forms | 197 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
El ejemplo completo se encuentra en 006_DataTableDemo. 
Para revertir los cambios usariamos el método RejectChanges. Esto puede 
parecer extraño a primera vista, pero en la práctica tiene mucho sentido. Lo 
frecuente es que nuestro DataTable contenga inicialmente los datos 
provenientes de una consulta, obtenidos mediante las clases de modo 
conectado. Posteriormente, modificamos los datos en la aplicación, mediante 
un formulario de usuario por ejemplo. Cuando estamos listos, damos los 
cambios por definitivos con AcceptChanges. De esta forma, reducimos al 
mínimo el acceso a la base de datos maximizando la disponibilidad de nuestro 
servidor de datos. 
DataSet 
La capacidad del modo desconectado va mucho mas allá de simplemente 
mantener los datos y modificar los datos de una tabla. ADO nos permite 
modelar relaciones entre tablas, aplicar restricciones a nuestros datos, 
responder a cambios de estado y sacar provecho de todo ello en nuestro 
desarrollo. Podemos agrupar un conjunto relevante de tablas para un proceso 
de nuestra aplicación (facturación por ejemplo), o todas las tablas si fuera el 
caso, usando la clase DataSet. Esta clase actúa como un contenedor de 
múltiples tablas relacionadas entre sí, como podrían ser LÍNEASFACTURA, 
FACTURAS y EMPRESAS. Naturalmente, las tablas que formarán el DataSet, 
son a nuestra libre elección. 
 
Para ilustar el uso del DataSet, añadiremos una tabla más a nuestra base de 
datos: 
 
/* Fichero sql\004_create_direcciones.sql */ 
 
CREATE TABLE [dbo].[DIRECCIONES]( 
 [ID] [bigint] NOT NULL, 
 [EMPRESA_ID] [bigint] NULL, 
 [NOMBRE] [varchar](30) NOT NULL, 
 [CALLE] [varchar](50) NULL, 
 [CODPOSTAL] [varchar](10) NULL, 
 [POBLACION] [varchar](50) NULL, 
 [PROVINCIA] [varchar](50) NULL, 
 [PAIS_ID] [bigint] NULL, 
198 | Windows Forms 
Libro para José Mora 
PRIMARY KEY CLUSTERED ( [ID] ASC) 
) ON [PRIMARY] 
Ya podemos crear nuestro DataSet: 
var ds = new DataSet("SerpDB"); 
Ahora solo necesitamos cargar los datos en algunos DataTables. ADO ofrece 
la interfaz IDbDataAdapter para hacer de puente entre el modo conectado y el 
modo desconectado. Es claro que cada DbProviderFactory debe ofrecer su 
propia implementación de esta interfaz, por eso debemos pedirle a 
DbProviderFactory que nos cree un adapter: 
public DbDataAdapter CreateAdapter(string command_text = "") 
{ 
var adapter = factory.CreateDataAdapter(); 
 adapter.SelectCommand = CreateCommand(command_text); 
 DbCommandBuilder builder = factory.CreateCommandBuilder(); 
 builder.DataAdapter = adapter; 
 adapter.InsertCommand = builder.GetInsertCommand(); 
 adapter.UpdateCommand = builder.GetUpdateCommand(); 
 adapter.DeleteCommand = builder.GetDeleteCommand(); 
 return adapter; 
} 
El adapter es de “ida y vuelta” a la base de datos, por eso necesita cuatro 
comandos para trabajar: SelectCommand lo usamos para especificar como 
obtener el conjunto de filas de nuestra base de datos, los otros tres para 
especificar como actualizar la base de datos según la modificación realizada 
en una fila cualquiera. Tal como vemos en el código anterior, podemos usar 
un DbComandBuilder para pedirle que nos construya un comando de cada 
tipo, completamente configurado. Por ejemplo, el InsertCommand que nos 
construye para la tabla PAISES tiene este CommandText: 
INSERT INTO [PAISES] ([ID], [CODIGO], [NOMBRE]) VALUES 
(@p1, @p2, @p3) 
Si la lógica de nuestra aplicación es más compleja, podemos usar nuestras 
propias sentencias, construyendo por código el correspondiente comando 
para cada situación. 
Windows Forms | 199 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Una vez disponemos del adapter, podemos usarlo para rellenar un DataTable 
en nuestro DataSet: 
 
adapter.Fill(ds, ”Empresas”); 
 
El efecto de esta línea es crear un nuevo DataTable, con nombre “Empresas”, 
añadirlo a la colección Tables[] del DataSet referenciado por la variable ds y, 
además, iniciarlo con los datos recuperados mediante el SelectCommand del 
adapter, lo cual abre la conexión a la base de datos si no estuviera ya abierta. 
Ahora podemos obtener este DataTable a través del DataSet: 
 
DataTable Empresas 
{ 
 get 
 { 
 return ds.Tables["EMPRESAS"]; 
 } 
} 
 
 
También podemos definir relaciones entre los DataTables contenidos en el 
DataSet. Cada relación se define mediante una instancia de la clase 
DataRelation y luego debemos añadirla a la colección Relations del 
DataSet. En nuestro caso, podemos definir la relación que existe entre una 
empresa y sus (múltiples) direcciones: 
 
DataColumn parentCol = Empresas.Columns["ID"]; 
DataColumn childCol = Direcciones.Columns["EMPRESA_ID"]; 
 
empresa_direcciones = new DataRelation("EMPRESA_DIRECCIONES", 
parentCol, childCol); 
 
ds.Relations.Add(empresa_direcciones); 
 
Incluso podemos especificar restricciones sobre lo que debe ocurrir en la tabla 
relacionada, al actualizar en la tabla padre: 
 
ForeignKeyConstraint foreignKey = 
empresa_direcciones.ChildKeyConstraint; 
foreignKey.DeleteRule = Rule.SetNull; 
foreignKey.UpdateRule = Rule.Cascade; 
foreignKey.AcceptRejectRule = AcceptRejectRule.Cascade; 
 
Lo que especificamos en este ejemplo es que la clave foránea debe ponerse a 
200 | Windows Forms 
Libro para José Mora 
null, si borramos el registro padre, y que debe actualizarse si cambiamos la 
clave primaria padre. La última línea especifica que debe suceder con los 
cambios en la tabla hija, si usamos AcceptChanges() o RejectChanges() 
sobre la tabla padre: en este caso, propagamos la misma petición, 
aceptando/rechazando los cambios en la tabla padre también 
aceptamos/rechazamos los de la tabla hija. 
Además de ForeignKeyConstraint, disponemos también de la clase 
UniqueConstraint para impedir la repetición de valores de una columna o 
combinación de columnas. De hecho, cuando añadimos un DataRelation al 
DataSet, se crea automáticamente una restricción de cada tipo: la 
UniqueConstraint se aplica a la columna padre y la ForeignKeyConstraint a 
la columna hija de la relación. Las restricciones se añaden a la colección 
Constrainst[] de cada DataTable. 
Cuando establecemos relaciones, podemos incluso definir columnas 
calculadas cuyo valor dependa de una relación: 
// Define una columna calculada llamada 'DIRECCIONES_COUNT' 
Empresas.Columns.Add("DIRECCIONES_COUNT", typeof(Int32), 
"Count(Child(EMPRESA_DIRECCIONES).ID)"); 
Las relaciones nos sirven también para facilitarnos navegar por los datos 
relacionados de una fila dada, usando GetChildRows(DataRelation). Es más, 
DataTable nos permite seleccionar un subconjunto de sus filas empleando 
una expresión de filtro en su método Select(). Un ejemplo que usa estas tres 
capacidades podrá ser este: 
foreach (DataRow empresa in Empresas.Select("ID > 96")) 
{ 
Console.WriteLine("| {0,10} | {1,20} | {2,20} | {3,4} |", 
empresa["CODIGO"], empresa["NOMBRE"], empresa["NIF"], 
empresa["DIRECCIONES_COUNT"]); 
foreach (DataRow direccion in 
empresa.GetChildRows(empresa_direcciones)) 
{
Console.WriteLine("| {0,10} | {1,20}", 
direccion["NOMBRE"], direccion["CALLE"]); 
}
} 
Observa que puedes obtener el valor de la columna calculada usando su 
nombre, como cualquier otra. 
El código de este ejemplo lo encontrarás en: 
007_DataSetDemo\DataSetDemo.sln 
Windows Forms | 201 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
DataView 
En una aplicación de base de datos, es frecuente que tengamos que filtrar y 
ordenar ciertos datos de una misma manera. Por ejemplo: las facturas suelen 
ordenarse por fecha, filtrarse para una empresa o periodo de tiempo, etc. 
También es frecuente que haya más de una forma típica de trabajar con el 
mismo grupo de datos: las facturas tal vez se muestran al usuario ordenadas 
de forma histórica en el tiempo, pero esas mismas facturas se agrupan por 
trimestre en un procesoque calcula los resultados de ventas. Las bases de 
datos relacionales permiten crear diferentes vistas de los mismos datos para 
ayudarnos en esa tarea y ADO ofrece una capacidad similar: la clase DataView. 
Esta clase nos permite crear una vista lógica de los datos contenidos en un 
DataTable. Esta vista puede tener una ordenación diferente o aplicar un filtro 
para obtener un subconjunto del DataTable original. Veamos como crear un 
DataView: 
 
var dv = new DataView(SerpDb.Instance.Empresas, "ID > 9","NOMBRE 
DESC", DataViewRowState.CurrentRows); 
 
Aquí creamos una vista del DataTable Empresas, filtrando las que tienen un 
ID mayor que 9 y ordenándo las filas por el campo NOMBRE en sentido 
descendente. El último argumento permite decidir que filas mostrar según su 
estado de modificación. 
Podemos ver los valores posibles consultando la definición del enumerado: 
 
public enum DataViewRowState 
{ 
// Resumen: 
// Ninguno. 
None = 0, 
// 
// Resumen: 
// Fila sin modificar. 
Unchanged = 2, 
// 
// Resumen: 
// Fila nueva. 
Added = 4, 
// 
// Resumen: 
// Fila eliminada. 
Deleted = 8, 
// 
// Resumen: 
// Versión actual de los datos originales modificados (vea 
ModifiedOriginal). 
202 | Windows Forms 
Libro para José Mora 
ModifiedCurrent = 16, 
// 
// Resumen: 
// Filas actuales, incluidas las filas sin modificar, nuevas y 
las modificadas. 
CurrentRows = 22, 
// 
// Resumen: 
// Versión original de los datos modificados.(Aunque se hayan 
modificado los 
// datos posteriormente, están disponibles como 
ModifiedCurrent). 
ModifiedOriginal = 32, 
// 
// Resumen: 
// Filas originales, incluidas las filas sin modificar y las 
eliminadas. 
OriginalRows = 42, 
} 
Hemos de tener presente que crear un DataView no duplica el conjunto de 
datos original. Debemos imaginarlo más bien como crear un índice que 
incluye las filas que cumplen los criterios de ordenación y filtrado, pero las 
filas que modificamos son las del DataSet origen del DataView. No obstante, 
podemos hacer la modificación directamente a través del DataView, ya que 
contiene los servicios necesarios. De hecho, su uso es similar al DataTable. 
Aunque podemos crear un DataView sin configurar, mediante el constructor 
sin parámetros, y configurar después, es más eficiente hacerlo en el 
constructor como en el ejemplo. De esta forma evitamos recrear 
innecesariamente el índice interno. 
Como puedes ver DataView tiene las ventajas de DataTable a la hora de 
añadir, modificar o eliminar filas, pero con la facilidad adicional de poder 
usar distintas formas de presentar los datos sin duplicarlos. Su uso natural es, 
de hecho, mostrar datos al usuario y permitirle editarlos, haciendo uso del 
“databinding” de los controles de usuario. 
DataBinding 
Podemos vincular controles de usuario, como una TextBox a una columna de 
un DataView, con lo que conseguimos que el TextBox muestre el valor de 
dicha columna, permitiendo además que el usuario cambie el valor de la 
columna, sin más que escribir en el TextBox.
Windows Forms | 203 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Vamos a mostrarlo con un ejemplo sencillo. Creamos una nueva solución de 
Visual Studio, escogiendo el tipo “Aplicación de Windows Forms”. En el 
formulario, pegamos un objeto DataGridView y debajo una TextBox. 
El resultado debe parecerse a esto: 
 
 
 
El código del formulario es también sencillo: 
 
namespace DataViewSample 
{ 
 public partial class Form1 : Form 
 { 
 public Form1() 
 { 
 InitializeComponent(); 
 } 
 
 private void Form1_Load(object sender, EventArgs e) 
 { 
 dataGridView1.DataSource = dv; 
 textBox1.DataBindings.Add("Text", dv, "NOMBRE"); 
 } 
 
 private void dataGridView1_SelectionChanged(object sender, 
EventArgs e) 
 { 
 BindingContext[dv].Position = 
dataGridView1.CurrentRow.Index; 
 } 
 
204 | Windows Forms 
Libro para José Mora 
 private DataView dv = new DataView(SerpDb.Instance.Empresas, 
"ID > 9", "NOMBRE DESC", DataViewRowState.CurrentRows); 
 } 
} 
En el evento Load del formulario asignamos la propiedad DataSource del 
dataGridView1 a nuestra DataView. Después creamos un DataBinding para 
enlazar el textBox1 con la columna NOMBRE del mismo DataView.
En el evento SelectionChanged del dataGridView1 sincronizamos la posición 
del DataBinding para lograr que el textBox1 muestre siempre el nombre de la 
empresa de la fila seleccionada en el dataGridView1. De esta forma la caja 
cambia si nos movemos por la grid. 
Puedes probar a editar en la caja y comprobarás que la grid se actualiza 
cuando completas la edición (al salir de la caja). 
El código completo para este ejemplo lo puedes encontrar en la ruta 
CH_03\008_DataViewSample\DataViewSample.sln 
TypedDataSet 
Lo visto no agota todas las posibilidades del modo desconectado. Tenemos la 
posibilidad de desactivar temporalmente las restricciones, habilitar y 
deshabilitar las actualizaciones y disponemos también de múltiples eventos 
Windows Forms | 205 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
para reaccionar a lo que sucede en cada momento con nuestros datos. Por 
ejemplo, tenemos eventos para antes y después de modificar una columna, 
antes y después de añadir, modificar o borrar filas, etc. Con ellos podemos 
implementar fácilmente validaciones y reglas de negocio complejas, sin más 
que atrapar el evento adecuado. 
Otra funcionalidad interesante es que estas clases disponen de métodos para 
leer y grabar XML, lo que nos permite usar la abstracción de DataTable y 
DataSet para manipular ficheros XML de la misma forma que nuestras tablas. 
Una vez comprendido el papel de cada una de estas clases en el modo 
desconectado, y con las facilidades para explorar código fuente y ayuda 
integrada que ofrece Visual Studio, estas posibilidades son sencillas de 
explorar: solo tienes que pulsar F12 sobre una clase para verlas. 
Con todo este arsenal, el DataSet nos permite construir realmente un potente 
modelo lógico de nuestros datos, independiente del modelo físico que 
tengamos en nuestras tablas. De esta forma, tu aplicación puede explotar y 
manipular los datos de una forma sofisticada y con un código bien 
organizado. 
Podemos hacer uso de todo ello codificando nuestra propia solución, si eso 
nos gusta o conviene, pero Visual Studio puede ayudarnos con buena parte del 
trabajo, además de forma visual, creando lo que se denominan “DataSets 
tipados” (TypedDataset). 
Veamos cómo crear uno para nuestra base de datos: 
1. Creamos primero una solución nueva del tipo “Aplicación de Windows 
Forms”. La llamaremos “TypedDataSetDemo”, para no pensar 
demasiado. 
2. Desde el explorador de soluciones, escogemos “Agregar” y “Nuevo 
elemento” 
206 | Windows Forms 
Libro para José Mora 
3. En la ventana que aparece, escogemos “Conjunto de datos” .
4. A continuación, Visual Studio nos mostrará una ventana de diseño
visual. Desde el explorador de bases de datos, arrastramos a esa
ventana nuestras tablas.
5. Ahora arrastramos la clave primaria EMPRESAS.ID sobre
DIRECCIONES.EMPRESA_ID. Nos aparece un diálogo para configurar la
relación. Escogemos relación y restricción foreignkey y “Cascade” en
Windows Forms | 207 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
las tres reglas. Después hacemos lo propio con PAISES.ID y 
DIRECCIONES.PAISES_ID. Aquí la regla adecuada sería “Cascade” para 
actualizar, “SetNull” para eliminar y “None” para aceptación o rechazo. 
6. Ahora pegaremos un DataGridView en nuestro formulario. 
 
 
7. Podemos observar que en la caja de herramientas tenemos un grupo 
“Componentes TypedDataSetDemo” donde aparece nuestro Dataset1 
y sus adapters. Arrastraremos el Dataset1 a nuestro formulario y al 
elemento que se creará lo llamamos “SerpDS”. 
 
8. Seleccionamos la grid. Asignamos su propiedadDataSource a 
“SerpDS” y su propiedad DataMember a “EMPRESAS”. 
 
 
 
 
 
208 | Windows Forms 
Libro para José Mora 
9. Finalmente, arrastraremos también el componente
EMPRESASTableAdapter al formulario.
10. En el evento Load del formulario pondremos este código:
empresasTableAdapter1.Fill(SerpDS.EMPRESAS); 
11. Si compilamos y ejecutamos, debemos ver los datos de la tabla
EMPRESAS en la pantalla.
Si observas los ficheros del proyecto, verás que Visual Studio ha generado un 
fichero app.config, con una entrada para la “connectionstring” de nuestra 
base de datos. Podemos mostrarla en el título de nuestro formulario: 
private void Form1_Load(object sender, EventArgs e) 
{ 
empresasTableAdapter1.Fill(SerpDS.EMPRESAS);
// Mostrar cadena de conexión en el título 
Text = Properties.Settings.Default.SERPConnectionString; 
} 
Windows Forms | 209 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Quizá has observado que nuestro código no usaba la cadena, ni abría 
explícitamente la conexión a la base de datos. Todo esto sucede gracias al 
código generado por Visual Studio en el fichero correspondiente al DataSet1. 
Tal y como explicamos anteriormente es empresasTableAdapter1.Fill() 
quien provoca que se abra la conexión. Pero para eso, el código generado en 
Dataset1, lo debe configurar correctamente. 
Tienes este ejemplo en 
CH_03\009_TypedDataSetDemo\TypedDataSetDemo.sln. Antes de usarlo, 
ajusta la ruta del fichero SERP.mdf en la cadena de conexión del fichero 
app.config. 
Conclusión 
Como has podido comprobar, ADO es una librería potente, flexible y rica en 
detalles. Hemos hecho un recorrido por cada una de sus funcionalidades 
principales, pero podríamos llenar una montaña de páginas, detallando los 
usos posibles y las formas de adaptarse a cada escenario posible. Tal como 
anuncie al comienzo, espero, al menos, que lo visto te haya servido para tener 
una visión global clara de su potencia y forma de trabajo y sobre todo, que eso 
te anime a usarla. 
 
210 | Windows Forms 
Libro para José Mora 
Windows Forms | 211 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Linq 
Cuando trabajamos con alguna fuente de datos, una parte importante de la 
tarea es hacer consultas de los datos. A fin de cuentas poco podremos hacer si 
no podemos acceder a los datos y escoger los que necesitemos para el trabajo 
entre manos. Una vez tenemos el subconjunto de datos necesarios, otro 
aspecto importante es su transformación en nuevas estructuras de datos para 
producir algún resultado o salida. El problema es que la forma de realizar 
ambas tareas, depende de la fuente de datos. Si tenemos un conjunto de 
pacientes en una tabla relacional, podemos, usar ADO y SQL para extraer los 
que necesitemos. Pero si el conjunto de pacientes, entra en nuestro proceso 
como una lista de objetos, entonces debemos usar bucles y condiciones para 
obtener una nueva lista con los que necesitamos. Sin embargo, gracias a Linq, 
podemos consultar ambas fuentes con las mismas armas. 
Lo que nos ofrece Linq, es precisamente una manera uniforme de realizar 
consultas sobre distintas fuentes de datos, usando un API potente, intuitivo y 
extensible. 
 Esto supone algunas ventajas importantes: 
 Nuestro código es más fácil de mantener si la fuente de datos que 
debemos consultar varía de formato (XML en vez de una Lista de objetos, 
por ejemplo), el impacto en nuestro código será minimo o incluso nulo. 
 No tenemos que aprender APIs diferentes, para explotar distintas fuentes 
de datos. 
Consultas con Linq 
Para sumergirnos en Linq, empecemos con un ejemplo sencillo, adaptado 
directamente de la ayuda de Visual Studio: 
 
212 | Windows Forms 
Libro para José Mora 
static void Main(string[] args) 
 { 
// Las tres partes de una consulta LINQ: 
// 1. origen/fuente de datos 
var numeros = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; 
// 2. creación de la consulta. 
// consulta_numeros es un IEnumerable<int> 
 var consulta_numeros = from x in numeros 
where (x % 2) == 0 
select x; 
// 3. Ejecución de la consulta_numeros. 
foreach (int n in consulta_numeros) 
{
Console.Write("{0} ", n); 
}
Console.WriteLine("\nPulse una tecla para salir ..."); 
Console.ReadKey(); 
} 
Como explica el ejemplo, una consulta Linq está formada por tres partes. 
Primero, necesitamos una fuente de datos. En el ejemplo, usamos un array 
con los enteros del 1 al 10, llamado numeros.
Después necesitamos crear la consulta. Vemos que Linq se inspira en SQL 
para definir la consulta: 
 from x in numeros 
where (x % 2) == 0 
 select x; 
Parece que el select este fuera de sitio, pero en Linq es así y espero que, más 
adelante, incluso te resulte más lógico. Otra cosa quizá te haya sorprendido es 
que Linq tiene un estilo declarativo en vez de procedimental. Se puede 
desglosar también en tres partes: 
1. ¿de dónde obtenemos los datos?
from x in números
2. ¿quién es aceptado en la consulta?
where (x % 2) == 0
3. ¿qué retorna la consulta?
select x;
Windows Forms | 213 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
Este último paso es trivial en este ejemplo, ya que devuelve el dato aceptado 
tal cual. Pero podemos imaginar algo un poco más interesante: 
 select x*x; 
 
El aspecto que presenta la consulta Linq, da la impresión que, más que una 
librería, forme parte del lenguaje de base, igual que otras construcciones 
como for, if o class. 
En realidad esto es cierto a medias. Lo que sucede en realidad, es que Linq 
funciona con un poco de ayuda del compilador, pero no tanta como parece. 
Esto es lo que sucede: 
Durante la compilación, la construcción: 
 from x in numeros 
 where (x % 2) == 0 
 select x; 
 
es transformada en esta otra: 
numeros.Where(x => (x % 3) == 0).Select(x=>x); 
 
Esto ya parece más normal ¿no? … ¿dices que no?. Entonce será porque no 
habías visto antes una “expresión lambda”. ¡Haberlo dicho, hombre! 
Expresiones Lambda 
Sin entrar en la historia de porque se llaman así, una expresión lambda es 
fácil de explicar. Empecemos con la más fácil: 
x=>x 
 
Esto es sólo una forma corta de: 
x=>{return x;} 
 
Esto es sólo una forma corta de: 
(int x)=>{return x;} 
 
Esto es sólo una forma corta de: 
214 | Windows Forms 
Libro para José Mora 
int (int x) 
{ 
return x;
} 
¡Voila! Es una función a la que falta el identificador. Por lo demás es una 
función en toda regla. Podemos verlo con otro ejemplo, también de la ayuda: 
delegate int del(int i); 
static void Main(string[] args) 
{ 
 del miDelegado = x => x * x; 
 int j = miDelegado(5); //j = 25 
} 
Podemos resumirlo diciendo que una expresión lambda es una función 
anónima (o procedimiento, si no retorna nada), que admite una sintaxis 
simplificada: 
 Podemos omitir {return } si queremos devolver directamente una
expresión.
 Podemos omitir los tipos cuando el compilador puede deducirlos del
contexto.
 Y podemos omitir los () de argumentos, si sólo tenemos un argumento.
Unos ejemplos más para hacernos una idea más exacta. 
(x, y) => x == y 
(int x, string s) => s.Length > x 
() => Sleep(10) // sin argumentos de entrada!, 
necesitamos los paréntesis 
Evaluación perezosa 
Un aspecto a tener en cuenta trabajando con Linq, es que las consultas no son 
evaluadas justo hasta que es necesario. Esta capacidad se denomina 
evaluación perezosa y permite hacer cosas sorprendentes, como trabajar con 
una colección infinita. He aquí la prueba: 
static IEnumerable<int> Naturals() 
{ 
for (var n = 0; ; n++) 
Windows Forms | 215 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 yield return n; 
} 
 
private static void Ejemplo2() 
{ 
 Console.WriteLine("\nPrimeros 10 cuadrados"); 
 
 // esta consultar contiene potencialmente infinitos valores 
 var cuadrados = from x in Naturals() select x*x; 
 
 // pero sólo cogemos 10. 
 foreach (var item in cuadrados.Take(10)) 
 Console.WriteLine(item); 
 
} 
 
En cada llamada, Naturals()retorna el siguiente valor del bucle infinito. La 
definición de cuadrados, simplemente almacena la expresión para poder 
evaluarla después. Pero ¿qué sucede en el foreach?. El método Take(10) es 
evaluado y se encarga de recuperar 1o valores de uno en uno. Es decir: no se 
obtiene la consulta y luego se hace Take(10). Si fuera así, el programa 
quedaría atrapado en el bucle infinito. 
Este ejemplo demuestra que Linq no evalúa más de lo necesario, a menos que 
lo forcemos: 
var cuadrados = from x in Naturals() 
 orderby x descending 
 select x * x; 
 
Haciendo ésto, el programa nunca llegará a imprimir el primer cuadrado. En 
cambio, si podríamos hacer: 
foreach (var item in cuadrados.Take(10).OrderByDescending(x=>x)) 
 Console.WriteLine(item); 
 
Operadores de consulta 
Hemos visto que la sintaxis de Linq se transforma en invocaciones a métodos 
que se pueden encadenar entre si. Incidentalmente, esta forma de diseñar un 
API, se conoce como “Fluent Interface” 
(http://en.wikipedia.org/wiki/Fluent_interface ). Este conjunto de métodos 
se denomina colectivamente “métodos operadores de consulta Linq” y, como 
ya he comentado, se inspiran en SQL. 
216 | Windows Forms 
Libro para José Mora 
Tras haber analizado estos ejemplos para comprender el funcionamiento 
general de Linq, seguramente te preguntes que otras operaciones permite 
efectuar. Esta es una lista bastante amplia, aunque incompleta, agrupadas por 
tipo de operación: 
Operadores de Métodos disponibles 
Restricción Where, OfType<T> 
Proyección Select, SelectMany 
Ordenación OrderBy, ThenBy 
Agrupación GroupBy 
Combinación Join, GroupJoin 
Cuantificadores Any, All 
Partición Take, Skip, TakeWhile, SkipWhile 
Conjuntos Distinct, Union, Intersect, Except 
Elemento First, Last, Single, ElementAt 
Agregados Count, Sum, Min, Max, Average 
Conversión ToArray, ToList, ToDictionary , OfType<T>, Cast<T> 
Su uso es intuitivo a partir del nombre del método, por lo que no voy a 
aburrirte con un ejemplo de cada cosa que podrás encontrar fácilmente en la 
ayuda, en Internet o directamente aquí http://code.msdn.microsoft.com/101-
LINQ-Samples-3fb9811b 
No obstante, si vamos a ver un ejemplo dónde podremos apreciar de nuevo las 
sutilezas de la evaluación perezosa. Aquí está: 
private static IEnumerable<int> MultiplesOf(int n) 
{ 
return from x in Naturals() where x % n == 0 select x; 
} 
Windows Forms | 217 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
private static void Ejemplo3() 
{ 
 var mulTres = MultiplesOf(3).TakeWhile(x => x < 100); 
 
 
 var mulCinco = MultiplesOf(5).TakeWhile(x => x <= 100); 
 
 
 var mulQuince = 
 from n in mulTres join m in mulCinco on n equals m select new { x 
= n, x2 = n * n }; 
 
 Console.WriteLine("Cuadrado de los multiplos de 15 menores 
que 100:\n"); 
 foreach (var item in mulQuince) 
 Console.WriteLine(item); 
 
 Console.WriteLine("\nSuma de suss cuadrados: {0}", 
 mulTres.Intersect(mulCinco).Select(x => x * 
x).Sum()); 
 
} 
 
Fíjate bien en el truco de usar TakeWhile en lugar de un simple where. 
Aunque a nivel lógico sean equivalentes, su implementación es diferente: 
TakeWhile actúa de forma perezosa, recogiendo elementos mientras se 
cumpla su condición. En cambio, where forzaría primero la evaluación y luego 
filtraría los elementos obtenidos. Con nuestra secuencia infinita de números, 
usando where el programa no terminaría nunca. 
Estos ejemplos los tienes en 
CH_04\010_NumerosConLinq\NumerosConLinq.sln. 
 
218 | Windows Forms 
Libro para José Mora 
Linq para DataSet 
En los ejemplos vistos hasta el momento, hemos usado como fuente de datos 
estructuras básicas del lenguaje, como arrays. La implementación del API de 
Linq para estos objetos (arrays, listas y similares) se denomina “Linq para 
objetos”. En realidad es simplemente Linq funcionado sobre una fuente que 
soporta IEnumerable, sin ningún proveedor de Linq intermedio. Puede 
usarse sobre arrays, List, Dictionary o directorios y archivos, por ejemplo. 
Sin embargo, tenemos disponibles otras para poder usar Linq con otras 
fuentes. 
La implementación de Linq que nos permite trabajar con DataSets de ADO, 
se llama, previsiblemente “Linq para DataSet”. Su uso es muy sencillo. Vamos 
a verlo usando el mismo ejemplo que usamos para presentar la clase DataSet 
con nuestra base de datos SERP.MDF. 
var query = from emp in Empresas.AsEnumerable() 
where (long)emp["ID"] > 96 
select new 
{
ID = emp["ID"], //el tipo es object 
Nombre = emp.Field<string>("NOMBRE") // tipado 
explicito a string 
};
foreach (var emp in query) 
Console.WriteLine(emp); 
Console.WriteLine("Pulse una tecla para salir..."); 
Console.ReadKey(); 
La idea es sencilla, por si no te acuerdas, Empresas es un DataTable sobre la 
tabla EMPRESAS. El método AsEnumerable() de DataTable, es una 
extensión para Linq y como su nombre indica, nos permite enumerar el 
DataTable.
Una vez tenemos el IEnumerable ya podemos usar Linq para objetos. Sencillo 
¿no?. ¡Pues no!. Piénsalo un poco. Si fuera así, significa que se debería cargar 
los datos del DataTable en una Lista, por ejemplo, y luego Linq para objects 
haría su trabajo. 
Ésto, además de ineficiente, violaría la definición de algunos operadores, 
nuestro viejo conocido TakeWhile, por ejemplo. Habíamos quedado que 
TakeWhile no evaluaba toda la colección, si no que se detenía al fallar su 
condición. Lo que sucede es que AsEnumerable() nos devuelve una 
Windows Forms | 219 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
implementación de IEnumerable con su propia implementación de los 
mismos operadores de Linq, que está optimizada para trabajar con 
DataTable. 
Otro punto a explicar es la extensiones a DataRow. Linq extiende esta clase 
con dos métodos: Field<> y SetField<>.Estos métodos permiten leer y 
asignar el valor de una columna, pero con la ventaja de manejar también el 
valor null. 
El ejemplo está en CH_04\011_LinqDataSetDemo\LinqDataSetDemo.sln. Te 
recomiendo que lo pruebes y trates de crear otras consultas Linq. Verás que 
procesos que antes resultaban laboriosos, se vuelven mucho más manejables 
gracias a Linq para DataSets. 
Linq para XML 
Otra implementación interesante es Linq para XML, ya que hoy día no es raro 
tener que trabajar con este formato. Si usamos Linq para esta tarea, nuestro 
trabajo se verá sin duda facilitado, ya que Linq para XML, no sólo facilita las 
consultas: también integra una funcionalidad similar a XPath y XQuery y 
facilidades para crear árboles XML, que permite trasformar árboles XML de 
manera sencilla. 
Veamos cómo crear un árbol: 
 
static XElement CrearContactos() 
{ 
 return new XElement("Contactos", 
 new XElement("Contacto", 
 new XElement("Nombre", "Jorge Cangas"), 
 new XElement("Teléfono", "666-
999-666", 
 new XAttribute("Tipo", 
"Móvil"))), 
 new XElement("Contacto", 
 new XElement("Nombre", "Pedro Pérez"), 
 new XElement("Teléfono", "999-
666-999", 
 new XAttribute("Tipo", 
"Fijo"))) 
 ); 
} 
 
Esto se conoce como “creación funcional” y resulta bastante natural. Las 
clases XElement y XAttribute incluyen servicios como Parent y 
Descendants() para navegar y filtrar los nodos del árbol. 
220 | Windows Forms 
Libro para José Mora 
Hagamos una consulta sobre el árbol que hemos construido: 
static void Main(string[] args) 
{ 
var contacts = CrearContactos(); 
Console.WriteLine("Contactos"); 
Console.WriteLine(contacts); 
var query = from el in contacts.Descendants("Teléfono") 
where (string)el.Attribute("Tipo") == "Fijo" 
select el; 
Console.WriteLine("\nEncontrados:"); 
foreach (XElement el in query) 
Console.WriteLine(el.Parent.Element("Nombre").Value); 
Console.WriteLine("Pulse una tecla para salir..."); 
Console.ReadKey(); 
} 
El ejemplo lo puedes encontrar en CH_04\012_LinqXMLDemo\LinqXMLDemo.sln. 
Ten en cuenta que Descendants(“Teléfono”)selecciona todos los nodos 
descendientes de tipo Télefono, mientras que Element(“nombre”) sólo 
escoge en los nodos hijos. 
La clase XElement contiene también métodos static para cargar el árbol desde 
un archivo ( Load() ) o desde un literal string en nuestro código fuente 
(Parse() ). También contiene algunos métodos adicionales de navegación en 
los nodos. Estas facilidades, combinadas con los operadores “tradicionales” de 
Linq, hacen que trabajar con XML sea mucho más sencillo e incluso divertido. 
Espero que te animes a probarlo en cuanto se presente la oportunidad. 
 
 
 
 
 
Apartado III: 
Windows Forms 
por José Vicente 
Sánchez 
 
 
 
 
 
 
 
 
 
 
 
 
Windows Forms | 223 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
Introducción 
Windows Forms es una tecnología de Microsoft para el desarrollo de lo que ha 
dado en llamarse “clientes inteligentes” (smart clients en el original en inglés). 
Bajo la nomenclatura cliente inteligente se engloban aplicaciones que son ricas 
desde el punto de vista gráfico, fáciles de desplegar, que pueden funcionar tanto 
conectadas como desconectadas de Internet y que pueden acceder a los recursos 
del ordenador local con mayor seguridad que las aplicaciones tradicionales para 
Windows. 
Los formularios de Windows Forms representan una tecnología de cliente 
inteligente para .NET Framework y constituyen un conjunto de librerías que 
simplifican las tareas comunes en cualquier entorno de desarrollo de software 
como puede ser el acceso a ficheros, comunicaciones, etc., facilitando la 
interacción con las APIs del Sistema Operativo desde un interfaz unificado de 
alto nivel. 
Un formulario de Windows Forms es una superficie visual que se utiliza para 
mostrar información al usuario. Sobre ella, se pueden situar elementos de 
interacción (cuadros de texto, etiquetas, botones, etc.) que permiten 
intercambiar información con el usuario de forma bidireccional. Dichos 
controles están orientados a eventos (lo que se conoce como “event driven 
controls”), lo cual significa que la aplicación reacciona ante un evento 
determinado (por ejemplo cuando el usuario hace clic con el ratón o cuando se 
recibe un paquete por la red) y ejecuta las acciones que han sido programadas 
para dicho evento. 
En cuanto a los controles que se utilizan en las aplicaciones basadas en 
Windows Forms, están estudiados y diseñados para simplificar las tareas más 
comunes de interacción entre el usuario y la aplicación por lo que pueden 
encontrarse controles especializados en tareas como entrada de datos por parte 
del usuario (cajas de texto, botones, etc.) y presentación de información en 
formatos estructurados (etiquetas, vistas de lista, vistas en árbol, etc.). Por otro 
lado, Windows Forms también ofrece mecanismos intuitivos y potentes tanto 
para la creación de controles con funcionalidades propias, como para la 
224 | Windows Forms 
Libro para José Mora 
extensión o modificación de los controles existentes, sacando provecho del 
concepto de herencia que proveen los lenguajes de programación en los que se 
basa .NET Framework. Como se verá más adelante, los controles de usuario se 
basan en la clase UserControl que provee un punto de partida para el desarrollo 
de este tipo de elementos. 
Existen multitud de libros que tratan, con mayor o menor profundidad, el 
desarrollo utilizando .NET Framework y, en especial, aplicaciones de interfaz de 
usuario basadas en Windows Forms. Esta tecnología es tan rica que, en sí 
misma, justificaría dedicarle un libro completo. En el caso que nos ocupa, sólo 
disponemos de una serie de capítulos por lo que, inevitablemente, tenemos que 
recortar por algún sitio, tarea siempre difícil. La intención del autor es que esta 
introducción al desarrollo de aplicaciones con Windows Forms no sea sólo eso, 
sino que aporte una serie de pinceladas o retazos sobre algunos puntos 
controvertidos o menos conocidos de estas tecnologías y que, por lo tanto, 
generar un mayor número de consultas en los foros. 
Visual Studio 
Las tareas implicadas en el desarrollo de aplicaciones basadas en Windows 
Forms como son el diseño, programación, depuración y despliegue se pueden 
llevar todas ellas a cabo mediante Visual Studio. 
Por un lado, Visual Studio facilita la creación rápida de formularios sin más que 
incorporar a los mismos controles de Windows Forms mediante el conocido 
mecanismo de “arrastrar y soltar”. De esta forma, y sin siquiera agregar ni una 
sola línea de código, es posible generar casi completamente el interfaz de 
usuario de nuestra aplicación, de forma visual e intuitiva. Así, podemos 
centrarnos en la implementación de la funcionalidad de nuestra aplicación 
sacando ventaja del concepto de controles orientados a eventos. Por otro lado, 
podemos agregar todo el código adicional como puede ser la inicialización de 
objetos y estructuras de datos, la gestión de errores, etc. Finalmente, 
agregaremos el código necesario para procesar aquellos eventos en los que 
estemos interesados. Una vez hemos finalizado el proceso de diseño y 
desarrollo, podemos depurar la aplicación desde el propio entorno así como 
prepararla para el despliegue. 
Windows Forms | 225 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
¿Por qué C#? 
Para realizar todos los ejemplos del libro se ha decidido utilizar el lenguaje C#. 
Tengo que reconocer que el único motivo de esta elección, es una cuestión de 
preferencia personal ya que el desarrollo de aplicaciones basadas en .NET 
Framework puede realizarse en cualquiera de los lenguajes de programación 
disponibles: C#, VB, F#, e incluso C++. 
 
De hecho, si pudiera elegir, utilizaría siempre C++, lenguaje con el que me 
siento mucho más cómodo. Lo que ocurre es que parece que la tendencia de 
Microsoft, en lo que respecta a los lenguajes de programación, es dejar C++ 
relegado para aplicaciones nativas Win32 o basadas en tecnologías que 
podríamos denominar “antiguas” como MFC, ATL, etc. 
 
La uniformidad de las clases que constituyen el .NET Framework garantiza que 
las sensaciones que experimenta el desarrollador son independientes del 
lenguaje elegido. De esta forma, puede concentrarse en los detalles de la 
implementación (que son los que de verdad importan) mientras trabaja con su 
lenguaje favorito. 
 
226 | Windows Forms 
Libro para José Mora 
Windows Forms | 227 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
Primeros Pasos 
Con Windows 
Forms 
La inmensa mayoría de los libros escritos sobre desarrollo de software, 
especialmente si describen una nueva tecnología o lenguaje de programación, 
comienzan siempre con el, podríamos decir, “entrañable” ejemplo Hello World! 
En este caso no vamos a ser menos así que a continuación describiremos los 
pasos necesarios para construir nuestra primera aplicación con Windows 
Forms. 
Para realizar los ejemplos que se describen en el libro, vamos a necesitar la 
herramienta Visual Studio 2010. Si no disponemos de una licencia, podemos ver 
diferentes promociones de Danysoft desde la URL: 
http://www.danysoft.com/?s=visual+studio 
 
o descargar la versión Express de forma totalmente gratuita desde la URL: 
http://www.microsoft.com/visualstudio/en-
us/products/2010-editions/express 
 
La versión Express tiene una serie de limitaciones pero eso no nos va a impedir 
sacar provecho de los ejemplos de este libro ya que dichas limitaciones sólo 
aplican a características avanzadas con las que no vamos a trabajar. 
228 | Windows Forms 
Libro para José Mora 
Para construir nuestra primera aplicación mediante Windows Forms, 
ejecutaremos el Visual Studio 2010. Podemos ver la pantalla inicial que se 
muestra en la siguiente figura: 
Windows Forms | 229 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
A continuación, seleccionaremos la opción de menú Archivo → Nuevo Proyecto. 
Visual Studio nos muestra el siguientediálogo: 
 
Seleccionamos el tipo “Aplicación de Windows Forms” y como nombre para el 
proyecto, utilizaremos “HelloWorld”. Visual Studio construye para nosotros el 
esqueleto de la aplicación y nos muestra la vista de la siguiente página: 
 
230 | Windows Forms 
Libro para José Mora 
En él podemos ver el Diseñador en la parte central-izquierda y el Explorador de 
Soluciones en el lateral derecho. Así mismo, en la parte más a la izquierda de la 
pantalla, podemos ver dos ventanas flotantes correspondientes al Cuadro de 
herramientas y los Orígenes de datos. 
Si, directamente, ejecutamos la aplicación (pulsando F5), Visual Studio la 
compila y la lanza en modo depuración. En la pantalla veremos un formulario 
como el siguiente: 
Windows Forms | 231 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
Como hemos visto, Visual Studio ha construido para nosotros una aplicación 
Windows Forms completa sin más que seleccionar unas pocas opciones. 
 
Aquellos que, como yo, desarrollaron aplicaciones en las primeras versiones de 
Windows (como por ejemplo Windows 3.0), utilizando el Microsoft Visual 
Studio 1.5, recordarán que para crear una aplicación que mostrase una ventana 
básica como la del ejemplo, hacía falta programar más de 70 líneas de código. 
 
Desde un punto de vista estrictamente riguroso, a nuestro formulario le falta 
algo para ser una auténtica aplicación tipo Hello World! así que vamos a 
agregarle unos cuantos ingredientes. 
En primer lugar, abriremos el Cuadro de Herramientas y veremos que éste nos 
ofrece una gran cantidad de controles especializados en las tareas más comunes 
en el diseño de aplicaciones como pueden ser Etiquetas, Cuadros de Texto, 
Botones, Cuadros de Lista, Cuadros de Árbol, Cuadros de Imagen, y muchos 
otros: 
232 | Windows Forms 
Libro para José Mora 
Seleccionamos una etiqueta (Label) y la arrastramos hasta el formulario. La 
seleccionamos con el ratón, y pulsamos botón derecho sobre ella y hacemos clic 
en “Propiedades”. 
En la siguiente imagen podemos ver como Visual Studio muestra una nueva 
ventana flotante denominada “Propiedades” en la que podemos ver las 
características del objeto seleccionado (en este caso la etiqueta). 
Nos situamos sobre la propiedad “Text” y cambiamos el valor actual (“label1”) 
por “Hello World”. Así mismo, editamos la propiedad “Font” y aumentamos el 
tamaño de la fuente a 18. 
A continuación, volveremos a lanzar nuestra 
aplicación pulsando F5. 
Windows Forms | 233 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
Para finalizar con este ejemplo básico, vamos a echar un vistazo al código 
generado por Visual Studio en nuestra aplicación. Si pulsamos el botón derecho 
del ratón en el “Explorador de soluciones” sobre el formulario “Form1.cs” y 
seleccionamos “Ver código”, Visual Studio nos muestra el código fuente de ficho 
formulario: 
using System; 
using System.Collections.Generic; 
using System.ComponentModel; 
using System.Data; 
using System.Drawing; 
using System.Linq; 
using System.Text; 
using System.Windows.Forms; 
 
namespace HelloWorld 
{ 
    public partial class Form1 : Form 
 { 
        public Form1() 
 { 
 InitializeComponent(); 
 } 
 } 
} 
 
 
Los elementos principales que podemos ver en el código anterior son: 
 Zona de directivas de inclusión: Mediante la palabra clave using, indica al 
compilador que debe incluir los namespaces2 indicados. 
using System; 
using System.Collections.Generic; 
using System.ComponentModel; 
using System.Data; 
using System.Drawing; 
using System.Linq; 
using System.Text; 
using System.Windows.Forms; 
 Definición de un nuevo espacio de nombres al que pertenecerán las clases 
que definamos en nuestra aplicación: 
namespace HelloWorld 
 
2 Los namespaces o espacios de nombres, no son más definiciones de ámbitos en 
los cuales están englobados los nombres de las clases, objetos, etc. 
234 | Windows Forms 
Libro para José Mora 
 Declaración de una clase para nuestro formulario que deriva de la clase base
“Form”:
public partial class Form1 : Form 
 Definición del constructor de nuestra clase que, por ahora, sólo invoca al
método InitializeComponent que es el encargado de configurar los valores
por defecto de todos y cada uno de los controles que forman nuestro
formulario (como por ejemplo el texto “Hello World” en la etiqueta
correspondiente.
public Form1() 
{ 
InitializeComponent();
} 
Si nos paramos un momento a pensar, nos daremos cuenta de que todo el 
código que hemos visto hasta ahora, no hace otra cosa que definir la clase 
correspondiente a nuestro formulario pero, ¿dónde se indica que se deba crear 
una instancia del formulario, mostrarlo en pantalla, etc.? La respuesta es 
sencilla: el proyecto contiene un fichero llamado “Program.cs” que, si lo 
editamos mediante botón derecho del ratón → “Ver código fuente”, nos 
encontramos con el siguiente código: 
using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Windows.Forms; 
namespace HelloWorld 
{ 
    static class Program 
 { 
/// <summary> 
/// Punto de entrada principal para la aplicación. 
/// </summary> 
[STAThread] 
static void Main() 
 { 
 Application.EnableVisualStyles(); 
 Application.SetCompatibleTextRenderingDefault(false); 
 Application.Run(new Form1()); 
 } 
 } 
} 
Windows Forms | 235 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
En el código anterior, vemos una serie de partes que nos son familiares (como 
las directivas de inclusión tipo “using” y la declaración de clase “Program”). 
Finalmente vemos que, la implementación del método Main, define una serie de 
opciones (activación de los estilos visuales como fuentes, colores, etc. y 
desactivación del renderizado de texto compatible, lo cual permite la 
representación de fuentes mediante GDI+) y, como último paso, invoca el 
método Run de la clase Program al que le pasa una nueva instancia del formulario 
Form1 que hemos definido en el fichero fuente descrito anteriormente. 
Un repaso a los controles básicos 
de Windows Forms 
En el ejemplo anterior hemos utilizado un sólo control de Windows Forms: la 
etiqueta o Label (además del propio formulario). A continuación vamos a dar un 
repaso a los controles básicos que Windows Forms pone a nuestra disposición 
para la construcción rápida de aplicaciones con interfaces de usuario ricos en 
usabilidad, uniformidad, etc. 
 
 
Botón: Permite ejecutar un evento cuando el usuario hace 
clic sobre el mismo. 
 
Cuadro de selección: Proporciona al usuario un mecanismo 
para seleccionar una o más opciones que no son 
mutuamente exclusivas. Un ejemplo de uso sería el de pedir 
al usuario que seleccione sus deportes favoritos de entre una 
lista de diez. 
236 | Windows Forms 
Libro para José Mora 
Cuadro de lista de selección: Tiene un uso similar al control 
anterior (CheckBox) con una diferencia fundamental: el 
CheckBox es un control independiente por lo que, para pedir 
al usuario que seleccione entre varias opciones, tenemos que 
crear un control por cada opción y posicionarlo sobre el 
formulario en el lugar que deseemos mientras que el 
CheckListBox muestra las diferentes opciones dentro de un 
rectángulo, manejando automáticamente las barras de scroll 
y similares, además de proveer acceso programático 
unificado a la consulta o modificación de las opciones 
seleccionadas. 
Cuadro de combinación: Permite mostrar al usuario una lista 
de elementos de entre los cuales deberá seleccionar una 
opción. Un ejemplo típico sería solicitar al usuario la 
selección de la provincia en la que vive a partir de la lista de 
provincias de España. 
Selector de fecha/hora: Es un control muy útil que, como su 
nombre indica, permite al usuario seleccionar una fecha de 
forma visual a partir de una representación de un calendario. 
Etiqueta: Muestra un texto al usuario que ésteno puede 
modificar aunque sí que puede ser cambiado 
programáticamente desde la lógica de ejecución. 
Etiqueta con enlace: Es un control de tipo etiqueta que 
permite mostrar un texto que no puede ser modificado por el 
usuario. La diferencia fundamental con el anterior es que 
puede realizar una acción cuando se hace clic sobre ella 
(desde hacer cualquier operación sobre un control de un 
formulario hasta abrir una página Web). 
Cuadro de lista: Muestra una lista de elementos al usuario 
para que este seleccione uno o más entre ellos. 
Vista de lista: Muestra una serie de elementos de forma 
similar a como lo hace una ventana del Explorador de 
Windows. Al igual que éste, admite los cuatro tipos de vista 
típicos (iconos grandes, iconos pequeños, lista, detalles y 
apilado de elementos). 
Windows Forms | 237 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
 
Cuadro de texto: Permite al usuario introducir un dato (por 
ejemplo sus apellidos). 
 
Cuadro de texto con máscara: Igual que el anterior pero 
permite definir una máscara de entrada de datos, muy útil 
para limitar qué información puede introducir el usuario, 
realizando una validación de facto de la misma. Un ejemplo 
típico de uso sería una fecha, un número de tarjeta de 
crédito, etc. 
 
Calendario mensual: Muestra un mes determinado, 
permitiendo selección de día, cambio de mes y año, etc. 
 
Icono de notificación: Agrega a la aplicación la función de 
mostrar un icono en el área de notificación de Windows 
(junto al reloj, en la esquina inferior derecha) así como 
facilitar el soporte para minimizarse a la zona de notificación 
en lugar de cerrarse, etc. 
 
Control numérico arriba-abajo: Facilita la selección de 
valores numéricos directamente haciendo clic con el ratón 
sin tener que utilizar el teclado. Un ejemplo de uso sería un 
selector de temperatura, pudiendo subir o bajar un grado 
con un simple clic. 
 
Cuadro de imagen: Muestra una imagen al usuario. 
Incorpora soporte para múltiples formatos (JPEG, PNG, 
etc.) así como adaptación del tamaño de la misma al control, 
zoom, cambio de tamaño, etc. 
 
Barra de progreso: Empleada habitualmente para mostrar al 
usuario el progreso de un determinado proceso que es 
típicamente largo en el tiempo (p.e. Mientras se copian 
cientos o miles de archivos desde una ubicación a otra). 
 
Botón de radio: Aunque similar al CheckBox, presenta la 
principal diferencia de que las diferentes opciones entre las 
que hay que seleccionar son mutuamente excluyentes (p.e. 
cuando se le pregunta al usuario su género, tiene que 
seleccionar entre hombre o mujer). 
238 | Windows Forms 
Libro para José Mora 
Cuadro de texto enriquecido: Similar a un TextBox aunque 
permite mostrar e introducir no sólo texto sino imágenes, 
además de soportar características avanzadas de edición 
como párrafos, fuentes, páginas, columnas, etc. 
Información de herramienta: Permite mostrar un cuadro 
emergente con información sobre un control cuando el 
puntero del ratón se situa sobre el mismo. 
A continuación, vamos a enumerar otros tipos de controles típicos de Windows 
Forms que quedarían englobados en el apartado de “Menús y barras de 
herramientas”: 
Tira de menú de contexto: Define un menú de opciones 
contextuales (por lo tanto relativas a un control o grupo de 
controles) que son mostradas de forma automática cuando el 
usuario hace clic con botón derecho sobre un control. 
También puede ser mostrado programáticamente según las 
condiciones que se definan. 
Tira de menú: Representa el menú clásico común a muchas 
aplicaciones de Windows. Típicamente suele contener 
opciones como “Archivo”, “Edición”, etc. 
Tira de estado: Se utiliza para mostrar al usuario el estado de 
determinadas condiciones de funcionamiento de la 
aplicación. Por ejemplo, se podría utilizar para mostrar si un 
cliente FTP está “Conectado” o “No conectado”. 
Tira de herramientas: También muy típica en aplicaciones 
Windows, está formada por una serie de botones que pueden 
llevar una imagen, texto, o ambos, y que constituyen atajos 
hacia funcionalidades de la aplicación que son de uso muy 
frecuente. 
Contenedor de tiras de herramientas: Representa un control 
sobre el que se pueden colocar y anclar tiras de herramientas 
mediante “arrastrar y soltar”. De esta forma, el usuario 
puede configurar las barras de herramientas en el lugar que 
le parezca más cómodo. Este mecanismo se usa en 
aplicaciones complejas que tienen cientos o miles de 
opciones como, por ejemplo, Adobe PhotoShop. 
Windows Forms | 239 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
 
Para finalizar con el repaso a los controles básicos de Windows Forms, vamos a 
describir los controles de tipo Contenedor que, como su nombre indica, se 
utilizan para agrupar de forma lógica otros tipos de controles. 
 
 
Panel de disposición de flujo: Permite disponer los controles 
que contiene, de forma automática, según una disposición 
izquierda-derecha y arriba-abajo aprovechando el espacio 
disponible. 
 
Caja de agrupación: Es un control clásico en Windows. 
Permite agrupar controles desde un punto de vista lógico 
(típicamente todos aquellos que tienen una función 
determinada). Por ejemplo, todos los RadioGroup dentro de 
un mismo GroupBox se comportan de forma mutuamente 
excluyente. 
 
Panel: Es el contenedor más básico que se puede utilizar 
para agrupar controles. 
 
Contenedor con separación: Permite dividir el contenedor 
padre (ya sea un formulario u otro contenedor a su vez) en 
dos espacios separados vertical y horizontalmente. Dicha 
separación puede modificarse en tiempo de ejecución 
arrastrando y soltando la barra de separación entre los 
correspondientes contenedores. 
 
Contenedor de pestañas: Utiliza un formato similar al que se 
utiliza por ejemplo en clasificadores y libros contables para 
marcar, con unas etiquetas superiores, el comienzo o final de 
una sección. Normalmente se utiliza cuando es necesario 
mostrar mucha información en pantalla distribuida en varios 
apartados lógicamente relacionados. 
 
Panel de disposición tipo tabla: Similar al FlowLayoutPanel, 
es mucho más flexibilible al permitir controlar mediante filas 
y columnas la disposición completa de los controles. 
240 | Windows Forms 
Libro para José Mora 
Windows Forms | 241 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
Un Ejemplo Más 
Completo: 
WordPad 
Con el fin de dar un repaso a algunos de los controles descritos en el capítulo 
anterior, vamos a construir una nueva aplicación de ejemplo algo más compleja 
que un simple HelloWorld! Concretamente, vamos a desarrollar una aplicación 
parecida al WordPad de Windows para mostrar cuánto de la funcionalidad 
básica de una aplicación como un procesador de textos ya lo implementa 
Windows Forms por nosotros y el poco código adicional que tenemos que 
agregar. 
242 | Windows Forms 
Libro para José Mora 
Aunque a estas alturas probablemente no habrá nadie que no conozca la 
aplicación WordPad, valga recordar que es un procesador de textos muy básico 
que viene de serie con el sistema operativo Windows desde versiones ancestrales. 
Para comenzar, crearemos un nuevo proyecto en Visual Studio, igual que 
hicimos con el ejemplo anterior. En este caso, vamos a llamar al proyecto 
“WordPad”. En cuanto al tipo de aplicación, seleccionaremos también “Windows 
Forms Application”. 
Como siempre, Visual Studio crea por nosotros la clase del formulario principal y 
la clase correspondiente al programa, de forma que éste cree una instancia de 
dicho formulario e inicie la aplicación. 
Windows Forms | 243 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
Lo primero que vamos a hacer es agregar una ToolBar al formulario principal, 
arrastrándola desde el cuadro de herramientas: 
A continuación, pulsamos botón derecho sobre ella y seleccionamos la opción: 
“Insertar elementos estándar”.Como podemos ver en la imagen siguiente, Visual Studio ha agregado por 
nosotros los típicos botones de las barras de herramientas con las opciones de 
Nuevo, Abrir, Guardar, etc.: 
244 | Windows Forms 
Libro para José Mora 
Lo primero que vamos a hacer es modificar los nombres del formulario, la barra 
de herramientas, etc., con el fin de que sea más intuitivo cuando hagamos 
referencia a los controles desde el código. 
Aunque este libro está escrito en Español, se utilizará el Inglés para los nombres 
de variables. Esta decisión (que no sólo se debe a la preferencia personal) está 
fundamentada en que, dado que tanto el propio C# como las clases de .NET 
Framework están escritas en Inglés, es más consistente mantener la 
uniformidad en el código del usuario. 
Windows Forms | 245 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
En primer lugar le asignaremos un nombre al formulario principal. Para ello, 
haremos clic con el botón derecho del ratón sobre el objeto Form1.cs en el 
Explorador de soluciones y seleccionaremos “Cambiar nombre”: 
 
Le indicamos como nuevo nombre “Main Form” y Visual Studio nos pregunta si 
queremos cambiar todas las referencias al formulario en el código y le decimos 
que sí: 
 
246 | Windows Forms 
Libro para José Mora 
A continuación, seleccionamos la barra de herramientas y, en la ventana de 
propiedades, cambiamos su nombre por “mainToolBar”: 
Seleccionamos en la barra de herramientas del formulario el botón de Abrir y en 
la ventana de Propiedades, le ponemos el nombre “openToolStripButton”: 
Windows Forms | 247 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
Repetiremos el proceso de renombrado de algunos botones, según la siguiente 
tabla: 
 
newToolStripButton 
saveToolStripButton 
copyToolStripButton 
pasteToolStripButton 
 
A continuación vamos a agregar un control de tipo RichTextBox al formulario 
principal. Para ello, desplegamos el Cuadro de herramientas y arrastramos el 
control hasta el formulario: 
 
 
Ahora cambiaremos el nombre del RichTextControl a “richTextBox”. 
248 | Windows Forms 
Libro para José Mora 
Hacemos clic con el botón derecho sobre la pestaña que contiene una flecha en 
la esquina superior derecha del control y seleccionamos “Acoplar en contenedor 
primario”. 
Como podemos ver, el RichTextBox pasa a adoptar un tamaño dinámico igual a 
la superficie libre del formulario (respetando lógicamente la barra de 
herramientas). 
Windows Forms | 249 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
De esta forma, cuando el formulario esté en ejecución, será el código propio de 
las clases de Windows Forms el que se encargue de redimensionar 
automáticamente el RichTextBox para que siempre ocupe la totalidad del 
formulario. Para verificar esto, simplemente lanzamos la aplicación con F5 y 
redimensionamos el formulario, comprobando que los controles se adaptan 
automáticamente al tamaño del mismo. 
Agregando algo de código 
Hasta ahora, no hemos hecho más que definir el esqueleto de nuestra aplicación 
pero no hemos implementado ninguna funcionalidad. 
Lo primero que haremos a continuación es agregar código para procesar un 
evento: concretamente, cuando el usuario hace clic en el botón “Abrir” (al que 
hemos denominado “openToolStripButton”). En la ventana de “Propiedades”, 
seleccionaremos “Eventos”: 
 
250 | Windows Forms 
Libro para José Mora 
A continuación nos situamos sobre el evento “Click” y hacemos doble-clic sobre 
el cuadro de selección. Visual Studio genera automáticamente para nosotros un 
nuevo método de la clase MainForm que será el encargado de procesar el evento 
de clic: 
private void openToolStripButton_Click(object sender, EventArgs 
e) 
{ 
} 
Como podemos ver, el convenio utilizado por Visual Studio es llamar al método 
de procesado de evento de la forma X_Y, donde X es el nombre del control e Y 
es el nombre del evento. De cualquier forma, siempre podemos renombrar el 
método utilizando las opciones de Refactorización. 
A continuación, lo que queremos hacer cuando el usuario pulse sobre el botón 
de apertura de archivo es solicitarle que introduzca qué fichero quiere abrir. En 
nuestro caso, como estamos implementado una aplicación tipo “WordPad”, 
queremos abrir archivos RTF3 para su edición. 
Para ello, utilizaremos un control de Windows Forms que se denomina 
OpenFileDialog y que encapsula la funcionalidad de interactuar con el usuario, 
3 Rich Text Format: Formato de archivo de texto enriquecido que puede contener 
texto, imágenes, formatos, paginación, párrafos, etc. 
Windows Forms | 251 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
mostrándole una ventana similar al Explorador de Windows, como la que puede 
verse en la siguiente imagen: 
 
Para solicitar al usuario el nombre del fichero a abrir mediante la clase 
OpenFileDialog, sólo tenemos que: 
 
 Instanciar la clase OpenFileDialog. 
 Configurarla. 
 Mostrar el diálogo al usuario. 
 Capturar el resultado de la operación (y los posibles errores). 
 Obtener el nombre del archivo. 
 
El código básico que realiza la funcionalidad descrita es el siguiente: 
private void openToolStripButton_Click(object sender, 
EventArgs e) 
{ 
  OpenFileDialog ofd = new OpenFileDialog(); 
  ofd.Filter = "Rich Text Format files|*.rtf"; 
  ofd.Multiselect = false; 
  DialogResult result = ofd.ShowDialog(); 
  if (result != DialogResult.OK) 
    return; 
252 | Windows Forms 
Libro para José Mora 
  MessageBox.Show("El usuario ha seleccionado el archivo ‐
> " + ofd.FileName);
} 
La explicación de cada una de las líneas del código anterior es: 
OpenFileDialog ofd = new OpenFileDialog(); 
Crea una instancia de la clase OpenFileDialog. 
ofd.Filter = "Rich Text Format files|*.rtf"; 
Configura el filtro de archivos del diálogo de forma que por defecto, se 
muestren sólo los archivos que tienen extensión RTF. 
ofd.Multiselect = false; 
Desactiva la selección múltiple de archivos. En este caso nos interesa 
abrir sólo un archivo cada vez. 
DialogResult result = ofd.ShowDialog(); 
Invoca el método ShowDialog de la clase creada para que muestre el 
diálogo al usuario. El resultado, se almacena en una nueva instancia de la 
clase DialogResult. Así, podemos capturar si, por ejemplo, el usuario 
cancela la operación en lugar de seleccionar un archivo. 
  if (result != DialogResult.OK) 
    return; 
Mediante este código, realizamos la comprobación de que el usuario ha 
seleccionado un archivo y pulsado OK. En cualquier otro caso, 
abandonamos la función sin hacer ninguna operación. 
Windows Forms | 253 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
  MessageBox.Show("El usuario ha seleccionado el archivo ‐
> " + ofd.FileName); 
Si hemos llegado hasta aquí es porque el usuario ha seleccionado un 
archivo. En este caso, simplemente mostramos un mensaje emergente 
con el nombre del archivo. 
 
Una nota importante de seguridad: Nunca te fíes de los usuarios. 
Probablemente habrás notado que hemos dado por supuesto que el usuario ha 
seleccionado un archivo de tipo RTF como le hemos pedido. Hay que tener en 
cuenta que el usuario podría haber modificado la configuración del diálogo de 
selección de forma que sea posible elegir un archivo de otro tipo. Por eso, cuando 
vayamos a realizar el procesado real del archivo, intentaremos protegernos 
contra este tipo de situaciones, ya sean causadas por errores involuntarios o por 
usuarios malintencionados. 
 
Bien, hasta ahora, y sin más que introducir unas pocas líneas de código, hemos 
implementado un proceso más o menos complejo que nos permite solicitar al 
usuario que seleccione un fichero RTF para su edición y nos muestra el nombre 
de dicho archivo en un diálogo emergente. Para dar por finalizado este proceso, 
sólo nos faltaría abrir el archivo como tal y cargar su contenido sobreel 
RichTextBox. Dicha operación es tan sencilla como comentar la línea que 
muestra el diálogo emergente y agregar el código resaltado en negrita: 
 
private void openToolStripButton_Click(object sender, 
EventArgs e) 
{ 
  OpenFileDialog ofd = new OpenFileDialog(); 
  ofd.Filter = "Rich Text Format files|*.rtf"; 
  ofd.Multiselect = false; 
  DialogResult result = ofd.ShowDialog(); 
  if (result != DialogResult.OK) 
    return; 
 
  //MessageBox.Show("El usuario ha seleccionado el archivo 
‐> " + ofd.FileName); 
254 | Windows Forms 
Libro para José Mora 
  try 
 { 
 richTextBox.LoadFile(ofd.FileName); 
 } 
  catch (Exception ex) 
 { 
    MessageBox.Show(String.Format("Descripción del error ‐
> {0}", ex.Message), "Error al cargar archivo",
MessageBoxButtons.OK, MessageBoxIcon.Error);
 } 
} 
Como podemos ver en el código anterior, la instrucción que carga el fichero en 
el RichTextBox no es más que la siguiente: 
 richTextBox.LoadFile(ofd.FileName); 
El resto del código que hemos agregado no es más que un control de 
excepciones (bloque try/catch) para evitar un fallo no controlado de la 
aplicación en el caso de que, como hemos comentado antes, el usuario 
especifique un fichero inválido. 
Para probar el código que hemos introducido, no tenemos más que ejecutar la 
aplicación (pulsando F5), hacer clic en el botón “Abrir” y seleccionar un fichero 
RTF. En este caso seleccionaremos el fichero “license.rtf” que en todas las 
versiones de Windows se encuentra en el directorio C:\Windows\System32. La 
aplicación carga el contenido del archivo en el RichTextBox como puede verse 
en la imagen de la siguiente página: 
Windows Forms | 255 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
 
Como puede verse en la imagen, el RichTextBox muestra el texto con el formato 
original, respetando fielmente las fuentes y tamaños de letra e incluso enlaces 
tipo URL. Ahora podemos modificar el texto directamente sobre el control 
RichTextBox. Para ello, nos situaremos con el ratón al comienzo del archivo y 
teclearemos “Hemos modificado este archivo”. 
 
Guardando los cambios del 
256 | Windows Forms 
Libro para José Mora 
archivo 
Hasta ahora, hemos construido una aplicación que permite cargar un fichero de 
tipo RTF respetando su formato, mostrarlo al usuario e incluso modificarlo. Y 
todo eso sin más que arrastrar unos cuantos controles y teclear unas diez líneas 
de código. La verdad es que, así contado, tenemos que reconocer que no está 
mal. El problema viene cuando nos preguntamos: “Y ahora, ¿cómo guardo los 
cambios que he hecho en mi documento?” 
Para responder a esta pregunta, no tenemos más que añadir algo más de código 
al botón “Guardar” (saveToolStripButton). El proceso es similar al que hemos 
seguido para el botón “Abrir” así que no lo vamos a describir con detalle (de 
hecho, consideraremos esta operación como un ejercicio para el lector). 
Una nota nostálgica del autor 
A lo largo de más de 16 años de carrera profesional en el mundo del Desarrollo 
de Software y Tecnología, tengo que reconocer que he aprendido mucho más 
cuando me he tenido que enfrentar a productos o sistemas cuya documentación 
era escasa o nula (o simplemente existía pero estaba plagada de errores). Esas 
noches sin dormir, tratando de deducir algo por el loable método de ensayo-
error dieron sus frutos. 
Además, aquellos eran otros tiempos porque ahora tenemos la inestimable 
ayuda de Danysoft, de buscadores como Google y toda la vasta comunidad de 
desarrolladores que comparten su conocimiento, experiencias, etc. a través de 
los foros. 
Windows Forms | 257 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
Si has seguido correctamente el procedimiento para definir el método de la clase 
MainForm que procesará el evento de clic sobre el botón “Guardar”, el código 
generado tiene que ser: 
private void saveToolStripButton_Click(object sender, EventArgs 
e) 
{ 
 
} 
 
Para guardar los cambios, tenemos que invocar al método SaveFile del control 
RichTextBox, pero para ello, necesitamos el nombre del archivo. Por eso, 
haremos primero una modificación simple al código para guardar, en una 
propiedad privada del formulario principal, el nombre del archivo que hemos 
abierto y así poder hacer referencia al mismo a la hora de guardar los cambios. 
Deberemos agregar el código resaltado en negrita que muestro a continuación: 
public partial class MainForm : Form 
{ 
  private string m_Filename = null; 
 
  public MainForm() 
 { 
 InitializeComponent(); 
 } 
 
De la misma forma, agregaremos una nueva línea en el método 
openToolStripButton_Click para almacenar el nombre del archivo cargado 
sobre la propiedad que acabamos de definir. La línea está resaltada con negrita 
en el siguiente fragmento de código: 
try 
{ 
 richTextBox.LoadFile(ofd.FileName); 
 m_Filename = ofd.FileName; 
} 
catch (Exception ex) 
{ 
  MessageBox.Show(String.Format("Descripción del error ‐> {0}", 
ex.Message), "Error al cargar archivo", MessageBoxButtons.OK, 
MessageBoxIcon.Error); 
} 
258 | Windows Forms 
Libro para José Mora 
Finalmente, el código del método saveToolStripButton_Click quedaría: 
private void saveToolStripButton_Click(object sender, EventArgs 
e) 
{ 
  if (m_Filename == null) 
    return; 
  try 
 { 
 richTextBox.SaveFile(m_Filename); 
 } 
  catch (Exception ex) 
 { 
     MessageBox.Show(String.Format("Descripción del error ‐> 
{0}", ex.Message), "Error al guardar el archivo", 
MessageBoxButtons.OK, MessageBoxIcon.Error); 
 } 
} 
Como puede comprobarse, hemos implementado de forma sencilla soporte para 
guardar los cambios sobre el archivo. 
Agregando un menú y una barra 
de estado 
Llegados a este punto, estamos empezando a echar de menos una serie de 
funcionalidades en nuestro editor de textos. La primera es poder guardar el 
archivo con un nombre distinto al original, la segunda es saber con qué fichero 
estamos trabajando y la tercera es cómo empezar a editar un archivo nuevo sin 
cerrar y volver a abrir la aplicación. 
Para guardar el fichero con otro nombre, necesitamos la conocida función 
“Guardar Como”. Vamos a aprovechar que queremos implementar esta 
funcionalidad para añadir a nuestra aplicación un menú. Esto implica realizar 
una operación sencilla: abrir el cuadro de herramientas y arrastrar un control de 
tipo MenuStrip a nuestro formulario. Como podemos ver en la siguiente imagen, 
Windows Forms | 259 
 
Guía práctica de desarrollo de aplicaciones Windows en .NET 
 
la barra de menú se coloca en la parte superior y nos muestra un rectángulo 
vacío en espera de que agreguemos ahí nuestra primera opción de menú: 
 
Haciendo clic en el rectángulo con el texto “Escriba aquí”, podemos agregar 
nuestra primera opción de menú a la que llamaremos “Archivo”. Debajo de ella, 
podremos agregar de la misma forma las típicas opciones “Nuevo”, “Abrir”, 
“Guardar”, “Guardar como” y “Salir”. El separador se define poniendo como 
texto de la opción un guión “-”. 
 
Los atajos de teclado se configuran anteponiendo un ampersand al texto (p.e. 
En lugar de “Guardar como”, escribiremos “Guardar &como”, de forma que 
habremos definido un atajo de teclado que relacionará la combinación de teclas 
Alt+C con la opción “Guardar como”). 
 
Si hemos seguido correctamente los pasos, deberíamos ver un menú similar al 
que se muestra en la siguiente imagen: 
260 | Windows Forms 
Libro para José Mora 
Para terminar con la configuración del menú, renombraremos las opciones 
respectivamente a: newMenuItem, openMenuItem, saveMenuItem, 
saveasMenuItem y exitMenuItem. 
A continuación implementaremos el código para el evento correspondiente a la 
acción de “Click” en la opción de menú “Nuevo”. Como siempre, nos situaremos 
sobre dicha opción, abriremos la ventana de Propiedades, Eventos y haremos 
doble-clic sobre el evento correspondiente. 
private void newToolStripMenuItem_Click(object