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