Skip to content

Instantly share code, notes, and snippets.

@cprieto
Created November 4, 2012 12:54
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save cprieto/4011791 to your computer and use it in GitHub Desktop.
Save cprieto/4011791 to your computer and use it in GitHub Desktop.
Capitulo 2 - Delegados, Eventos, Heap and Stack, Tipos por Valor y Referencia

Capitulo 2

Lenguaje - Delegados

Es sumamente dificil que uno vea la importancia de los delegados inmediatamente cuando se comienza a aprender C#, recuerdo que la primera vez que los vi y lei sobre ellos pense que habia comprendido como funcionaban y no podia ver el porque eran tan interesantes o importantes, o bueno, ver la razon de su existencia.

Como menciona el capitulo, un delegado no es nada mas que un nivel de abstracion hacia una operacion algo que es similar a un puntero hacia una funcion en C , sin embargo van un poco mas alla, tomare en cuenta o por sentado que conoces las implicaciones y usos de un puntero a una funcion en C, si no, quizas refrescar el concepto te ayude Pointer to functions y nunca puede faltar el link de Wikipedia Function pointer.

Sin embargo, el concepto de un puntero a funcion se queda corto comparado con un delegado en C# por varias razones:

  1. No es un cocepto del lenguaje, los delegados son un concepto de la CIL (recuerdas? el lenguaje intermedio que habla la CLR), viendolo de esta manera tanto C# como VB.NET implementan su concepto de delegado, y para ser honesto, los delegados en C# son mucho mas limpios
  2. Un delegado en realidad es un tipo, un tipo por referencia
  3. Son, a mi criterio, la mejor estructura de lenguaje que refleja la evolucion de C# desde 1.0 hasta C# 5.0, cada version de C# ha implementado cambios en los delegados convitiendolo en algo sumamente versatil y poderoso. Entender delegados es clave para entender la evolucion de C#

Los delegados de igual manera son la mejor expresion de diferencia entre C# y Java o entre la CLR y la JVM. Java (hasta la version futura 1.8) no posee delegados. A simple vista parece algo no importante, pero cuando veamos como han evolucionado los delegados atraves del tiempo en C# te daras cuenta de lo que Java se ha estado perdiendo...

Imaginemos un metodo que necesita buscar datos en una listado de informacion, una forma simple y elegante de hacerlo en C# es usando un delegado que tome el objeto que necesitas "filtrar" y retornar bool si el objeto llena el criterio, de esta manera desacoplas la logica de filtrado de la implementacion del filtrado:

^^^csharp

class RecordStorage { public delegate bool FilterRecords(Record item); // Este es el delegado para filtrado public static IList FindRecords(FilterRecords filter) { // Aqui hacemos el filtrado, mas de esto en el futuro... } }

// La forma de buscar es simple

FilterRecords filterByAge = new FilterRecords(FilterByAge);

IList records = RecordStorage.FindRecords(filterByAge);

// Y la definicion del metodo FilterByAge que contiene la 'implementacion' del filtro

// ... public bool FilterByAge(Record item) { return item.Age > 18; } ^^^

Sin embargo, con todo y el poder de los delegados en la version 1.0, hay un problema significativo... Debido a que son un tipo te ves obligado a declarar la implementacion concreta del delegado para luego usarla, eso trae varios problemas inmediatos, especialmente en el tiempo en que C# 1.0 salio al aire (recuerda, eran tiempos de usar cosas como Winforms con una fuerte orientacion a eventos), aunque pueda parecer algo no muy relevante escribir mas codigo (tenias que declarar un delegado y asignarlo aunque el uso de este fuera trivial y solo sucederia una vez) tenia otras complicaciones, como el caso donde un delegado en una clase de vida corta fuera referenciado por otra clase de vida larga, debido a que la clase de vida larga mantenia una referencia al delegado la clase de vida corta seguiria viviendo y no podia ser recolectada por el garbage collector... Tenias un memory leak que seria sumamente dificil de encontrar.

Los delegados sufrieron una transformacion en C# 2.0 pero hablaremos de ello mas tarde o en otro post ;)

Lenguaje - Eventos

Bien, los eventos, estos estan fuertemente ligados a los delegados y es otra caracteristica que separar C# 1.0 de Java, en Java (y en muchos otros lenguajes como PHP por ejemplo), no existe el concepto de un evento como un ciudadano del lenguaje si, tienes eventos implementados como clases separadas usando la Observer Pattern pero al igual que los delegados forman parte de la definicion de la CLI en la CLR y los lenguajes como C# o VB.NET simplemente proyectan el uso en una forma natural para el lenguaje (nuevamente aqui vemos la separacion entre el lenguaje y el runtime, donde cada lenguaje tiene la libertad de proyectar los conceptos del runtime a como sea mas natural para el lenguaje).

Un evento indica algo que 'sucede' y debe apuntar a un delegado, esto es debido a que cuando algo 'sucede' hay una accion que debe 'ejecutarse', asi pues, si no apunta un evento a un delegado es porque nadie esta interesado en que eso pase. Un ejemplo de evento y delegado podria ser este:

^^^csharp public class WordProcessor { public delegate void WordEntryAction(string word); public event WordEntryAction WhenWordEntered;

public void EnterWord(string word){
	// Process word here
	if (WhenWordEntered != null)
		WhenWordEntered(word);
}

} ^^^

Como ves el evento necesita apuntar a un tipo delegado, una limitacion del diseno de C# es que necesitamos revisar que el evento tiene delegados suscritos (la linea que lee diferente a null) y luego disparar los delegados apuntados al evento, esta limitacion no sucede en lenguajes como VB.NET pero es simplemente una desicion de diseno del lenguaje (disenar un lenguaje no es una tarea sencilla no importa quien lo diga).

Un articulo que puede ayudarte a profundizar en delegados y eventos es este en CodeProject solo ten cuidado porque incluye un par de cosas de C# 2.0

Stack y Heap

Explicar Stack y Heap es algo curioso y que en lo personal me llama mucho la atencion (quizas porque soy un freak en cuanto a compiladores y lenguajes y esas cosas marcianas que a medio mundo no le interesan), al mismo tiempo es complicado ya que el concepto de Stack esta prostuido en Computer Sciences (es como el concepto de Servicios).

Stack puede referirse a varias cosas:

  1. A la estructura de datos conocida como Stack (pila)
  2. A la forma en que la maquina o el interprete colecciona y llama las rutinas, ejemplo, Stack Virtual Machine comparado con Register Virtual Machine.
  3. A la forma en que el layout de memoria es manejado en el ordenador.

En este momento nos referiremos a la Stack usando el ultimo inciso (aunque los primeros dos son igual de interesantes pero no quiero confundirte).

El stack no vive solo, tiene un hermano menor, en realidad es ese tipo de relaciones de hermanos que no comprendes porque ambos son bien diferentes. El Stack es alguien metodico, ordenado, estricto, rapido y delgado; el Heap es el hermano desordenado, lento, olvidadizo, menos estricto y gordo.

Porque existen? bueno, por la misma razon que Adan y Eva, en un principio no habia nada y la unica forma en que habia para programar era metiendo tarjetas perforadas en maquinas que ejecutaban las ordenes. Esas tarjetas perforadas no contenian en realidad codigo, sino memoria. En un inicio todo era procedimental (y en realidad en este mundo lleno de objetos aun lo es, pero no quiero confundirte -aun-) y luego de que los "programas" se volvieran mas complejos se encontro una solucion, separar complejos problemas en pequenas soluciones, a esas pequenas soluciones se les llamaba funciones. Como el nombre lo dice, toda la idea viene de el mundo de las matematicas. En matematicas cuando metes algo entre parentesis eso se ejecuta primero y se crea un mundo 'encapsulado', bien, la forma de modelar ese comportamiento en los primeros ordenadores fue crear una estructura de pila, en la cual se apilaban instrucciones y conforme estas se iban ejecutando y terminando se pasaba a la siguiente instruccion. De esta manera al llamar una funcion uno realmente esta 'pushing' valores encima en la stack y cuando esta termina de ejecutar ese nivel de la stack es 'popped'.

Una forma linda de ilustrarlo es viendo las llamadas de tres funciones, observa este diagrama de secuencia: Function call order

En este caso la stack estaria en el siguiente orden (Siendo 1 el tope)

  1. C
  2. B
  3. A

Y como ves el proceso es simple, el ordenador (o la maquina virtual) primero haria "pop" a C, luego le toca a B y de ultimo a A... Cuando A llama a B realmente esta "pushing" B a la stack.

Que tiene que ver todo esto con la memoria? bueno, la Stack realmente es una estructura en memoria. El CPU necesita recordar donde quedo ejecutando y a quien le toca luego de que ejecute las instrucciones que le pediste, es asi como la "call stack" contiene informacion de memoria importante:

  1. Un puntero de direccion (o mejor dicho, la direccion) de retorno, esto es, la direccion a donde la funcion debe retornar una vez que termine
  2. El valor de retorno de la funcion una vez que esta termine, o sea, si retornas un valor este debe ser guardado, de otra manera cuando la funcion o metodo sea 'popped' se perderia el retorno.
  3. Las variables locales de esa funcion o metodo

El ultimo valor mencionado quizas es el mas dificil de comprender o razonar de el porque existe. La razon es simple pero quizas dificil de explicar, intentare hacerlo para el final de este "post".

Como recordaras te mencionaba que Stack es solo un miembro de la familia, el otro hermano "Heap" es diferente. Para entenderlo basta con decir que Heap es todo lo que Stack no es. Explicar la diferencia en lenguajes como C/C++ es sumamente sencillo, la Stack la usas cada vez que declaras implicitamente una variable mientras la Heap la usas cada vez que llamas malloc o una funcion familiar. La stack es limitada y es declarada por el compilador en el momento de compilacion, de esta manera el 'tamanio' deseado de la stack es conocida en ese momento y 'empaquetada' en una estructura ordenada y compacta en el momento de compilacion. La Heap es dinamica y asignada en el momento de ejecucion. Es por eso que en C/C++ la stack es usada cada vez que declaras una variable y el compilador lo sabe en momento de compilacion mientras que el uso de la heap es delegada a la funcion malloc o similar en momento de ejecucion, el compilador no sabe de antemano si vas a tener espacio o no para guardar esa info mientras te puede decir en momento de compile time si vas a tener espacio para guardarlo en la stack.

Esto lo aprendi muchisimos anios atras cuando programaba (o aprendia a programar) en C y Pascal, uno de los programas que queria hacer era una hoja de calculo, y en mi pequena mente en ese entonces queria guardar todo en un arreglo, asi que declare un arreglo gigante de muchos miembros y adivina que, mi programa no compilaba... La razon era simple, la Stack es una estructura estatica y estricta, el compilador me avisaba que la estructura que queria crear localmente era muy grande para caber en la stack de la maquina. Recuerda, hay una diferencia entre la memoria de la maquina y la memoria del CPU la primera puedes meterle cuanto quieras y agrandarla, la segunda es incluida en el procesador y es mucho mas escaza. La primera es la heap, la segunda la stack. La primera es variable, la segunda viene con los registros del procesador.

La razon por la que existen es bien logica e historica. Vos creciste ya en un mundo con Gigas de RAM, pero yo lo hice (al igual que el mundo de los compiladores) en un mundo donde 64KB era la norma y todo lo que tenias a disposicion, claro, esto no te limitaba en creatividad. Un buen ejemplo es el Z80 que viene con el Gameboy original y Gameboy Color ;) 64KB de pura diversion.

Como ves, la Stack es delgada y estricta, la Heap es gorda y flacida.

Hay un buen par de links acerca de esto, te recomiendo Wikipedia y este en C Stack and memory allocation

Tipos por Referencia y por Valor

Esto implica que la stack y heap no existen en lenguajes como PHP? claro que no, como vez es algo de la arquitectura del procesador y no depende de la maquina, la razon es que el lenguaje lo maneja por ti y en cierta manera lo mismo sucede en C#, la CLR hace Garbage Collection y por lo tanto la 'alocacion' de valores es automatica y lo hace el compilador por ti (es por eso que a diferencia de C/C++ no ves un malloc o similar en .NET), sin embargo hay dos grandes dilemas aqui.

Hay valores que son 'nativamente' manejados por los registros del procesador, un buen ejemplo son los valores numericos (enteros y puntos flotantes, no es en vano que son llamados 'ordenadores'), sin embargo el procesador no sabe nada acerca de 'objetos' y como manejarlos. Los primeros tienen razon de ser y son optimizados para manejarse en ese momento por el CPU y viven localmente en la stack, estos son los tipos por valor. Cada vez que tienes un tipo por valor estos son copiados directamente a un registro o un set de registros en la estructura inmediata del stack mientras que los tipos por referencia siempre van a vivir en la heap. En lenguajes como Java no existe la posibilidad de declarar un tipo por valor y solo existen los llamados 'tipos primitivos' que vienen con el lenguaje. Como el valor cada vez que es usado realmente usas una 'copia' lo que hagas con el se pierde una vez que la stack es 'popped' (a menos que hagas ciertas cosas requeridas por el compilador). En la CLR se permite crear tipos de usuario por valor, algunos tipos como structs son por valor, mientras que todos los demas son por referencias.

En realidad tipos como int (que en realidad son Int32) aunque son hijos de object son hijos 'indirectos' y son tratados por valor por el CLR, asi que son copiados a la Stack en cuanto son declarados, mientras que todos los demas son por referencia. Esto es un tema un poco mas avanzado, pero si en el futuro quieres ahondar el libro de Richter tiene un excelente capitulo sobre tipos por valor y referencia en la CLR.

Esto tiene una seria implicacion en cuanto a performance. Debido a que los tipos por valor viven en la stack (que en realidad son registros fijos del procesador o la maquina virtual) y los de referencia viven en la heap, en realidad estan bien 'lejos' uno del otro, al mismo tiempo debido a que todos los objetos en la CLR heredan de object que en realidad es un tipo por referencia puedo hacer cosas como estas:

^^^csharp int a = 1; object b = a; // Luego en otro metodo mas adelante quizas donde pasas b int c = (int) b; ^^^

Es totalmente posible y no tiene nada de malo, pero, cuando declaras a lo haces en la stack local, luego declaras algo en la heap y le asignas el valor de a, luego cuando usas b (que esta en la heap) tienes que correr e ir a traer el valor de a que en realidad esta en la stack de otro metodo y copiarlo a la stack local, o sea, caminas y haces un movimiento que "cuesta" instrucciones de CPU, a simple vista pareciera no mucha carga pero imaginate que lo haces con un millon de enteros... Es cuando notas la diferencia en performance.

Al movimiento de la stack a la heap se le llama boxing y a la de la heap a la stack se le llama unboxing, esto solo sucede cuando casteas un tipo por valor a uno por referencia y uno de referencia luego a uno por valor. Esa palabrita boxing/unboxing la escucharas cuando veas generics.

Si, un tipo por referencia puede vivir en el stack, y de hecho en C/C++ lo haces, lo que pasas es el puntero o referencia a ese lugar en memoria. Recuerda que la stack es una estructura en tiempo de compilacion y la heap en tiempo de ejecucion, al poner algo en la stack necesitas saber de antemano como vas a 'empaquetar' en memoria de los registros del procesador la estructura.

Para terminar, este link hasta tiene figuras que quizas expliquen mejor que yo las diferencias entre los dos ;) C# and CLR Memory - Part 1 (en realidad son 4 partes, dale una leida).

Saludos Mario!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment