Estos 4 tipos de datos de C# sirven para facilitar el trabajo con métodos (funciones). La idea también es abrir más la perspectiva a nuevos tipos de datos y formas de trabajar. Básicamente, son tipos de datos que guardan funciones dentro.
ÍNDICE DE CONTENIDOS
CREACIÓN DE DELEGATEs
Delegate
Es un tipo de dato que representa un formato específico de función. Sólo se pueden guardar las funciones que cumplan con lo especificado en la declaración. La ventaja de usar esto es que podemos “ejecutar” una variable (ya que tiene una función adentro), pasarle parámetros y que haga cosas diferente, dependiendo de lo que queramos que haga. Es un principio muy fundamental para la Inteligencia Artificial y una mejor forma de estructuración.
Siempre que tengamos la necesidad de guardar una función en una variable, los delegados son una muy buena opción para ello. Su uso es bastante sencillo y su declaración es la parte en la cual hay que dejar en claro qué características tienen que tener las funciones que podemos guardar dentro.
DECLARACIÓN:
public delegate void MyFirstDelegate(int param1, int param2);
Observen que la declaración es muy similar a la declaración de una función cualquiera, solamente que se pone la palabra “delegate” entre el “public” y el “void“.
Con esto básicamente estamos creando un “tipo” de dato, que guarda SOLAMENTE funciones que reciban dos parámetros enteros y que no devuelvan nada. Por ejemplo, cualquiera de las siguientes funciones:
public void Subtrac(int a, int b) { print(a - b); } public void Add(int a, int b) { print(a + b); } public void SpawnEnemies(int count, int life) { }
Una vez creado dicho tipo, podemos crear una variable y guardar dentro cualquier función que cumpla con esas características:
public delegate void MyFirstDelegate(int param1, int param2); public MyFirstDelegate callback; //Se ejecuta al inicializar... void Start () { //Así lo guardamos. callback = Subtrac; //Así lo invocamos. callback.Invoke(5, 5); } public void Subtrac(int a, int b) { print(a - b); } public void Add(int a, int b) { print(a + b); } public void SpawnEnemies(int count, int life) { }
La ventaja de hacer esto, es que podemos llamar al delegado que llamamos “callback“, pasarle dos parámetros, y que haga cosas diferentes dependiendo de lo que esté guardando adentro. En el campo de la inteligencia artificial, esto es muy importante, ya que un personaje específico podría llamar siempre al mismo callback para hacer determinadas acciones, y lo único que podríamos cambiar es el contenido, o sea, la función que guarda dentro, y el personaje haría cosas diferentes (esto lo veremos en otro post más en profundidad).
Un detalle importante es que si la variable “callback” no tiene nada dentro, por lógica hay que preguntar, como con la mayoría de las variables, si es distinto de “null” antes de usarla.
USO DE EVENT
Event
Es un tipo especial de dato, que se usa para declarar eventos, como dice su nombre. Un evento es un suceso que se produce en un momento determinado. Por ejemplo, la función “OnCollisionEnter()” de Unity3D se ejecuta cuando ocurre una colisión entre dos objetos. Podemos hacer lo mismo para cada cosa que ocurra en un juego. La ventaja de esto es que podemos dejar que determinados objetos de nuestro juego “escuchen” determinadas cosas que ocurran para desencadenar sucesos que mantienen el flujo. Por ejemplo, podemos hacer que una puerta al final de un nivel dispare un evento, y que el objeto encargado de cargar el siguiente nivel lo escuche. De esta manera, no hace falta que estemos todo el tiempo chequeando si pasaron determinadas cosas, si el jugador tiene tal puntaje, si llegó a tal lugar, etc. Sino que solamente esperamos a que se dispare determinado acontecimiento y el programa ejecutará automáticamente lo que corresponda. Por supuesto detrás de esto también habrá algún que otro “if“, pero con esto queda todo más estructurado.
Los eventos son básicamente delegados. Imaginemos una variable en la que guardamos un array de funciones. Todas ellas se van a ejecutar simultáneamente al momento en el que lo indiquemos. Por ejemplo, cuando un personaje mata a otro, cuando agarra un item, cuando termina un nivel, etc.
En primer lugar, pensemos cuál es el suceso que vamos a querer notificar. Si lo que queremos hacer es avisar que un personaje agarró un item, podemos seguir la siguiente estructura:
public delegate void MyCustomEventDelegate(Character c); public event MyCustomEventDelegate OnCharacterTakeItem;
Expliquemos un poco esta parte. La primer línea es la declaración de un delegado. Esto es lo que vimos en la parte de arriba. La siguiente es la creación de un evento. Noten que luego de “public event” pondríamos el tipo de DELEGADO que es. Acá no puede ir cualquier tipo de dato, como por ejemplo “int” o “float“. Únicamente podemos poner un tipo de delegado que hayamos creado. En nuestro caso “MyCustomEventDelegate” que lo creamos justo una línea arriba (trato de usar nombres largos para que sea más explicativo).
Luego de haber creado nuestro evento, debemos “agregar subscriptores“. Esto es como cuando ustedes se subscriben a una página web o a un canal de YouTube. Le dan click al botón, y luego les llega un mail cada vez que el tipo al que se subscribieron sube un video nuevo. Esto es lo mismo. En un lugar X, ustedes se subscriben a un evento, y el objeto dueño de dicho evento lo dispara en el momento que pasa algo. Entonces el script que está “escuchando” hace algo que ustedes le pidan. Por ejemplo, subscribimos un objeto a un evento del personaje, para que este nos avise cuándo agarra un item. ¿Por qué? Porque, por ejemplo, podríamos hacer que la GUI se subscriba a nuestro evento que al dispararse se actualice el puntaje y además se refleje el valor en el juego.
Bien, vamos a nuestro script “Character.as“:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Character : MonoBehaviour { public delegate void EventCallback(Character c); public event EventCallback OnCharacterTakeItem; //Se ejecuta al inicializar... void Start () { } //Se llama al pintar la pantalla... void Update () { } }
Acá es donde creamos el delegado y el evento. Ahora veamos el script “GUI.as“:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class GUI : MonoBehaviour { public Character theCharacter; //Se ejecuta al inicializar... void Start () { theCharacter = GameObject.Find("Hero").GetComponent(); theCharacter.OnCharacterTakeItem += TheCharacter_OnCharacterTakeItem; } private void TheCharacter_OnCharacterTakeItem(Character c) { throw new System.NotImplementedException(); } //Se llama al pintar la pantalla... void Update () { } }
Bien… Vamos a explicar parte por parte para que no les agarre una embolia…
En la línea 12 básicamente obtenemos al Character. Esto es suponiendo que está en la escena. Puede estar donde ustedes quieran, en sus juegos sólo deberían obtenerlo desde el lugar en el que sea que esté. Así que acá pondrían lo que corresponda al juego de ustedes. Lo importante es que esta es la línea en donde obtienen la referencia.
La línea 13 lo que hace es crear una “suscripción“. Noten que accedemos al evento creado, y ponemos “ += “. Esta es la forma de “Agregar” una suscripción. Van a notar que automáticamente después de escribir esto, el compilador les va a dar la opción de autocompletar:
Esto es por si ustedes no tienen la función creada, el compilador puede hacerlo por ustedes automáticamente. Y la línea de código que crea dentro de la función es sólo para tirar un error, en caso de que se olviden de escribir algo dentro. Pueden borrarla tranquilamente.
Bien. Ahora sólo queda el momento de la ejecución. Una vez que el evento ocurra, debemos avisarle a todos nuestros suscriptores, o sea, ejecutar TODAS las funciones que están suscritas. Lo mejor de esto, es que no es necesario recorrer ningún array, hacer algún for, ni nada. Sólo se hace de la siguiente manera: Imaginemos que el evento debemos dispararlo al momento en el que el personaje agarre el item:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Character : MonoBehaviour { public delegate void EventCallback(Character c); public event EventCallback OnCharacterTakeItem; //Se ejecuta al inicializar... void Start () { } void OnCollisionEnter(Collision collision) { if(collision.gameObject.name != null) { OnCharacterTakeItem(this); } } //Se llama al pintar la pantalla... void Update () { } }
La línea del “if” básicamente es para preguntar: “SI (el objeto con el que colisionamos es un item) ENTONCES… Avisamos a los suscriptores“. El “this” es porque la función recibe un character. Es muy común en los eventos pasar por parámetro al objeto que lo dispara. De esta manera el suscriptor sabe quién fue el que disparó el evento. Es como “OnCollisionEnter()“. Se pasa como parámetro el objeto que disparó el evento para saber con quién colisiono, su nombre, etc.
Un último punto a tener en cuenta es lo mismo que dijimos en los delegados: Hay que chequear si es distinto de null. En este caso, eso verifica si hay un suscriptor. O sea que quedaría de la siguiente manera:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Character : MonoBehaviour { public delegate void EventCallback(Character c); public event EventCallback OnCharacterTakeItem; //Se ejecuta al inicializar... void Start () { } void OnCollisionEnter(Collision collision) { if(collision.gameObject.GetComponent() != null && OnCharacterTakeItem != null) { OnCharacterTakeItem(this); } } //Se llama al pintar la pantalla... void Update () { } }
USO DE ACTIONs
Action
Encapsula un método que recibe un parámetro y no devuelve nada.
Si entendieron todo lo explicado hasta acá, esto es mucho más fácil de entender (si no es así, deberían releerlo). Action es básicamente un delegado. Sirve para guardar dentro funciones que reciben un sólo parámetro y no devuelven ningún valor.
La declaración de los Actions es similar a los “List<>“:
public Action testingAction;
El parámetro que se pone entre los paréntesis angulares es el tipo de dato del parámetro que recibirá la función guardada en ese lugar. Esto es útil también si vamos a crear un evento que sólo recibe un parámetro y no devuelve nada (de hecho, los eventos no deberían devolver ningún tipo de dato ya que se los llama para “notificar” algo nada más).
COMPLEMENTAR UTILIZANDO FUNC
CB
Guarda una función que recibe un parámetro y devuelve un valor.
Func es similar a Action, con la diferencia de que SÍ devuelve un valor. La declaración es muy similar:
public Func<string, int> testingFunc;
El primer parámetro es igual al de Func, o sea, es el tipo de parámetro que recibe. El segundo es el valor que devuelve. De más está decir que los tipos de datos especificados dentro de los paréntesis angulares puede ser el que nosotros queramos. Incluso, de una manera más compleja, cualquiera de ellos podría incluso ser un “Func“, un “Action“, o un “Delegate“.
RECOMENDACIONES FINALES
Si bien en algunas partes les dije que deberían chequear si el evento/delegado es distinto de null antes de usarlo o invocarlo, es una muy mala práctica hacerlo. En general, deberían SIEMPRE QUE SE PUEDA omitir el null-check. Es por una cuestión de legibilidad, pero por sobre todo por una cuestión de estructura. Hagan de cuenta que el null-check es un tipo que les dice: “hiciste algo mal, hiciste algo mal, hiciste algo mal…”. Cada vez que se sientan en la necesidad de tipearlo, piensen una nueva forma de reestructurar las cosas como para no tener que hacerlo.
Por ejemplo, si se encuentran en esta situación de este post, en la cual tienen que chequear si el evento es null, en lugar de hacer eso podrían crear una función vacía y agregarla al evento para que nunca sea null.
Ojo… No es algo que sea la muerte si se hace. Pero siempre que se pueda deberían ahorrarlo. Esto lo pueden usar como para darse una idea que tan bien (o mal), están estructurando las cosas. Si se ven muchas veces en la situación de tener que preguntar si algo es null entonces están en problemas.
Ojo 2… Hay veces en las que podrían incluso necesitar que se devuelva null como parte de un acuerdo. Es como decir: “no tengo ningún valor válido para devolver”. En algunos casos incluso devolver la instancia de un objeto vacío o un valor por defecto podría resulta en conflictos con otra cosa. Chequer por null en esos casos “obliga” de alguna manera que preguntes: “¿Es válido el valor o es fruta?”. La “obligación” de preguntar se puede imponer a propósito para que el valor no se use en caso de no ser válido.
Y eso es todo… POR AHORA…