Delegados en .NET - Parte I
Por Johann Granados
Contenido
1. Introducción (En el reino del desarrollo de software ...)
2. Delegados
3. Usos de los delegados
4. Definición e implementación de delegados
5. Ejemplos
1. Introducción (En el reino del desarrollo de software ...)
Antes del advenimiento de la plataforma .NET, Visual Basic era el rey (al menos en el reino del desarrollo de software para plataforma Microsoft). Cerca del 80% de las aplicaciones de negocios a nivel mundial estaban (y muchas siguen estando) escritas en ese lenguaje. Sin embargo, muchos programadores acostumbrados a la complejidad -y flexibilidad- de lenguajes de más bajo nivel (como C++) criticaban a Visual Basic por no permitir un mayor control sobre la forma en que se estructura y codifica el software. "Visual Basic es muy tieso", decían. Y tenían razón. Visual Basic está orientado a la funcionalidad del producto (no a su arquitectura). Lo anterior enfurecía a quienes deseaban un mayor control sobre los recursos de la máquina, las estructuras de programación, e incluso sobre la propia forma en la que el código fuente se traduce a código máquina. Con Visual Basic se podía crear aplicaciones mucho más rápido que en C++ (por ejemplo) pero dichas aplicaciones rara vez era más eficientes, escalables e incluso portables que las escritas en lenguajes de más bajo nivel. Con la llegada de la plataforma .NET, esta situación cambió.
.NET brinda al programador un mayor control sobre la forma en que su código accede a los recursos, utiliza las estructuras y técnicas de programación e, incluso, se traduce a lenguaje intermedio (el lenguaje de la plataforma .NET).
Este artículo se introduce en la discusión y aprendizaje de una serie de conceptos fundamentales en la programación de aplicaciones para la plataforma .NET, los cuales es importante dominar si se desea sacar verdadero provecho de las bondades de esta plataforma.
2. Delegados
Un delegado es una estructura de programación que nos permite invocar a uno o varios métodos a la vez. Estos métodos pueden encontrarse en la misma clase desde la que se invocan o en clases distintas asociadas a ésta. Hablando "a bajo nivel", un delegado es un puntero seguro a una función (pero sin la complejidad de la administración propia de dicho puntero).
3. Usos de los delegados
Gestión de eventos
Quizás éste sea el principal uso de los delegados. Notificar a uno o varios componentes el acontecimiento de un determinado evento con el fin de que dichos componentes tomen alguna acción al respecto. Este es el fundamento de la programación orientada a eventos (uno de los pilares de la programación visual). Utilizando eventos, los componentes de la interfaz avisan a la lógica de negocios que el usuario ha ejecutado alguna acción sobre los componentes de la misma (por ejemplo, presionar el botón del mouse o presionar una tecla).
Implementación de patrones de diseño
Existen muchos patrones de diseño basados en eventos (posiblemente el más conocido sea el patrón "Publicador-Suscriptor"). Utilizando eventos y delegados se puede establecer un mecanismo que permita que una clase interesada en los servicios de otra reciba una notificación instantánea en el momento en que acontezca un acontecimiento sobre el cual deba tomar alguna acción (por ejemplo, recibir una notificación cuando se realice una operación sobre una cuenta bancaria).
Llamadas asíncronas
La invocación a un componente relacionado puede tomar mucho tiempo en recibir respuesta. Esto sucede muchas veces cuando se manipulan grandes cantidades de datos o cuando la lógica del servicio que se invoca es extremadamente compleja. Lo más "triste" de esta situación es que muchas veces no requerimos la respuesta a dicha invocación para seguir trabajando, o bien, podemos realizar otras tareas mientras esperamos por ella. En estos casos podemos pasar el puntero a una función ("Perdón ... ¿Dijo un delegado?") para que en el momento en que el método invocado finalice su ejecución llame a la función que le fue pasada (utilizando su dirección o puntero) con el fin de indicar que ha terminado su trabajo.
A estos punteros a funciones que se pasan a otro método para que éste indique su finalización, se les conoce como "Funciones de retorno" (callback functions). Las funciones de retorno son el fundamento de la invocación asíncrona entre componentes (Web Services y .NET Remoting) en .NET.
Acceso al estado en otros hilos de ejecución
Debido a restricciones de seguridad no es posible acceder de forma directa a las propiedades o variables de estado de una clase localizada en otro hilo (Thread) de ejecución. Tampoco podemos invocar directamente un método de una clase localizado en otro hilo. Sin embargo, sí es posible "ejecutar" un puntero a una función ubicada en otro hilo (lo que equivale en términos sencillos a ejecutar la propia función). Esta particularidad -que puede parecer una contradicción- se debe al modelo de ejecución del sistema operativo (el cual abordaremos con más detalle en artículos posteriores).
Métodos anónimos
Los programadores aficionados al paradigma funcional amarán esta funcionalidad incorporada en la reciente versión del .NET Framework (la 2.0). ¿Porqué? Porque es la antesala de la llegada de las famosas funciones Lambda omnipresentes y todo poderosas en los lenguajes funcionales a la plataforma .NET.
Un método anónimo en .NET es un método que es definido en aquel lugar en que se le ocupa. Es decir, no contiene un encabezado, el código de dicho método se incorpora en el lugar en donde debería ir la invocación a la función. Para lograr lo anterior se utilizan delegados.
Para obtener más detalles sobre esta nueva técnica, puedes acceder a la siguiente página: https://msdn2.microsoft.com/en-us/vcsharp/default.aspx (al igual que el uso anterior, este tema será tratado en mayor detalle en artículos posteriores).
4. Definición e implementación de delegados
La sintaxis para la definición de delegados difiere según el lenguaje. No obstante, los conceptos fundamentales que se mostrarán a continuación son invariables independientemente del lenguaje que se utilice:
Lo primero que se debe hacer para definir un delegado es indicar cuál será la firma de las funciones a la que ese delegado podrá apuntar (por firma se entiende el tipo de datos de los parámetros que se reciben, y el tipo de datos que se retorna):
VB.NET
[Nivel de protección] Delegate [Sub|Function] NombreDelegado (Param1 As [Tipo], Param2 As [Tipo], ...) {As [Tipo de retorno]} C# [Nivel de protección] delegate [Tipo de retorno] NombreDelegado ([Tipo] Param1, Param2 [Tipo], ...)
El segundo paso consiste en definir las variables que almacenarán los punteros de las funciones que se invocarán a través del delegado. Para mayor claridad llamaremos a esta variable apuntador. Estas variables deben ser del tipo de datos del delegado (definido en el paso anterior):
VB.NET
[Nivel de protección] NombreApuntador As [TipoDelegado] C# [Nivel de protección] [Tipo Delegado] NombreApuntador
Una vez definida la firma del delegado y declarado el apuntador (variable del tipo de delegado) para invocar las funciones, el tercer paso consiste en indicar cuáles serán esas funciones. Para eso se utiliza el concepto de "sumar" métodos al apuntador (este concepto queda bastante evidente al observar la sintaxis para realizar dicha operación en C#):
VB.NET
NombreApuntador = [Delegate].Combine(NombreApuntador, New TipoDelegado(AddressOf FuncionAAgregar)) VB ofrece una sintaxis un poco más simple en el caso del uso de delegados para manejo de eventos: AddHandler NombreApuntador, AddressOf FuncionAAgregar C# NombreApuntador += new TipoDelegado(FuncionAAgregar)
- Ya se puede invocar al apuntador, lo cual provocará la ejecución de todas las funciones que le hayan sido agregadas. Para invocarlo simplemente basta con hacerlo por su nombre (pasándole los parámetros requeridos definidos en la firma del delegado).
5. Ejemplos
Para finalizar este artículo mostraremos dos ejemplos del uso de delegados para la gestión de eventos e implementación del patrón "Publicador-Suscriptor" (los otros 2 usos mencionados en este artículo se tratarán con mayor amplitud en posteriores artículos).
Gestión de eventos
En este ejemplo sólo el paso 3 es necesario. La razón de esto es que la variable apuntador para el evento Click del botón (que de hecho recibe ese mismo nombre) ya se encuentra definida dentro de la clase System.Windows.Forms.Button provista por el framework de .NET. El delegado al que corresponde dicho apuntador (que es el mismo para los eventos de todos los controles gráficos) es de tipo System.EventHandler. Es decir, para la gestión de eventos, lo único que se debe hacer es agregar al apuntador de cada evento las funciones que serán invocadas cuando éste suceda. (Nota: Algunas secciones del código han sido suprimidas para facilitar la comprensión del ejemplo):
VB .NET
Partial Class frmDelegados Inherits System.Windows.Forms.Form Friend WithEvents btnHolaMundo As System.Windows.Forms.Button '<System.Diagnostics.DebuggerStepThrough()> _ Private Sub InitializeComponent() Me.btnHolaMundo = New System.Windows.Forms.Button Me.SuspendLayout() ' 'btnHolaMundo ' Me.btnHolaMundo.Location = New System.Drawing.Point(25, 12) Me.btnHolaMundo.Name = "btnHolaMundo" Me.btnHolaMundo.Size = New System.Drawing.Size(75, 23) Me.btnHolaMundo.TabIndex = 0 Me.btnHolaMundo.Text = "Hola mundo" Me.btnHolaMundo.UseVisualStyleBackColor = True 'inicio paso 3 AddHandler btnHolaMundo.Click, AddressOf ClickBotonHolaMundo 'fin paso 3 End Sub Private Sub ClickBotonHolaMundo(ByVal sender As Object, ByVal e As EventArgs) MessageBox.Show(CType(sender, Button).Text) End Sub End Class
C#
partial class frmDelegados { private System.Windows.Forms.Button btnHolaMundo; private void InitializeComponent() { this.btnHolaMundo = new System.Windows.Forms.Button(); this.SuspendLayout(); // // btnHolaMundo // this.btnHolaMundo.Location = new System.Drawing.Point(36, 42); this.btnHolaMundo.Name = "btnHolaMundo"; this.btnHolaMundo.Size = new System.Drawing.Size(93, 28); this.btnHolaMundo.TabIndex = 0; this.btnHolaMundo.Text = "Hola mundo"; this.btnHolaMundo.UseVisualStyleBackColor = true; // inicio paso 3 this.btnHolaMundo.Click += new System.EventHandler(ClickBotonHolaMundo); // fin paso 3 } private void ClickBotonHolaMundo(object sender, EventArgs e) { MessageBox.Show(((Button)sender).Text); } }
Patrón Publicador-Suscriptor
VB .NET
Clase Publicador:
Public Class Publicador 'Paso 1: Definir la firma del evento a publicar Public Delegate Sub FirmaEventoAPublicar(ByVal texto As String) 'Paso 2: Definir el apuntador para guardar las funciones a invocar Public EventoAPublicar As FirmaEventoAPublicar Public Sub OcurrioEvento(ByVal texto As String) 'Paso 4: Invocar las funciones agregadas a través del invocador EventoAPublicar(texto) End Sub End Class
Clase Suscriptor:
Public Class Suscriptor Public Sub AvisemeAqui(ByVal texto As String) MessageBox.Show("Gracias por avisar. Este es el mensaje recibido:" & texto) End Sub End Class
Agregar e invocar los punteros a las funciones:
Private unPublicador As New Publicador Private unSuscriptor As New Suscriptor Private otroSuscriptor As New Suscriptor Private Sub ClickBotonHolaMundo(ByVal sender As Object, ByVal e As EventArgs) 'Paso 3: Agregar los punteros a las funciones a invocar unPublicador.EventoAPublicar = [Delegate].Combine(unPublicador.EventoAPublicar, New Publicador.FirmaEventoAPublicar(AddressOf unSuscriptor.AvisemeAqui)) unPublicador.EventoAPublicar = [Delegate].Combine(unPublicador.EventoAPublicar, New Publicador.FirmaEventoAPublicar(AddressOf otroSuscriptor.AvisemeAqui)) 'Llamar al método en el Publicador que invoca a las funciones agregadas al delegado unPublicador.OcurrioEvento(txtMensaje.Text) End Sub
C#
Clase Publicador:
class Publicador { //Paso 1: Definir la firma del evento a publicar public delegate void FirmaEventoAPublicar(string texto); //Paso 2: Definir el apuntador para guardar las direcciones de las funciones a invocar public FirmaEventoAPublicar EventoAPublicar; public void OcurrioEvento(string texto) { //Paso 4: Invocar las funciones EventoAPublicar(texto); } }
Clase Suscriptor:
class Suscriptor { public void AvisemeAqui(string texto) { MessageBox.Show("Gracias por avisar. Este es el mensaje recibido:" + texto); } }
Agregar e invocar los punteros a las funciones:
private Publicador unPublicador = new Publicador(); private Suscriptor unSuscriptor = new Suscriptor(); private Suscriptor otroSuscriptor = new Suscriptor(); private void ClickBotonHolaMundo(object sender, EventArgs e) { //Paso 3: Agregar los punteros a las funciones a invocar unPublicador.EventoAPublicar += new ublicador.FirmaEventoAPublicar(unSuscriptor.AvisemeAqui); unPublicador.EventoAPublicar += new ublicador.FirmaEventoAPublicar(otroSuscriptor.AvisemeAqui); //Llamar al método en el Publicador que invoca a las funciones agregadas al delegado unPublicador.OcurrioEvento(txtMensaje.Text); }
Johann Granados se desempeña como consultor y es MVP en .NET Compact Framework.