Aprende C# con Unity - Genéricos

Los genéricos proporcionan una forma de hacer una especie de “plantilla” de su código que funciona de la misma manera en una variedad de tipos de datos diferentes. Si bien podría considerarse un tema más avanzado, existen algunos beneficios importantes al usarlos desde el principio. En esta lección, presentaré listas genéricas y diccionarios, y mostraré cómo se usan los genéricos para funcionalidades específicas de Unity, como obtener componentes y cargar recursos. Si te sientes aventurero, no dudes en echar un vistazo a algunos ejemplos rápidos de clases y métodos genéricos personalizados al final.

GitHub Gitlab

Lista genérica

Si has hecho alguna prueba con matrices, es posible que hayas topado con algunas características de la lista de deseos. Por ejemplo, es posible que se haya sentido decepcionado al ver que son “inmutables”, lo que significa que no puede cambiar el tamaño añadiendo o eliminando objetos de forma dinámica. Muchas de las funciones que está esperando se excluyen por el bien del rendimiento. Si está escribiendo un código altamente optimizado, como una inteligencia artificial compleja para jugar al Ajedrez, entonces querrá esa velocidad extra. En la mayoría de los otros casos, los beneficios de opciones son un poco menos eficaces, pero más robustos.

La lista genérica es muy parecida a una matriz. Gran parte de la sintaxis para leer y escribir se verá igual también. Sin embargo, una lista puede agregar o eliminar elementos dinámicamente, indicarle el índice de un elemento que contiene, ordenar, etc. Veamos cómo se ve en el código:


#region Librerias
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
#endregion

namespace MoonAntonio
{
	public class ListExamples : MonoBehaviour 
	{
		public List<int> indices = new List<int>();

		void Start()
		{
			for (int i = 0; i < 10; ++i)
			{
				int index = UnityEngine.Random.Range(0, 10);
				Debug.Log("Agregada entrada: " + index);
				indices.Add(index);
			}

			if (indices.Contains(3))
			{
				Debug.Log("Eliminada entrada.");
				indices.Remove(3);
			}

			indices.Sort();
			Debug.Log("Entradas ordenadas ... Ahora:");

			for (int i = 0; i < indices.Count; ++i)
			{
				int index = indices[i];
				Debug.Log(string.Format("i: " + index));
			}
		}
	}
}

En primer lugar, tenga en cuenta que en la línea 3, he especificado que estoy usando un nuevo espacio de nombres, “System.Collections.Generic”. Esto nos permite declarar y usar genéricos sin especificarlos completamente. Por ejemplo, sin la instrucción using, la línea 7 se vería así en su lugar:


public System.Collections.Generic.List<int> indices = new System.Collections.Generic.List<int>();

Declaramos nuestra lista en la línea 7 y la inicializamos como una lista vacía. Esto es genial para mostrar que no necesita saber qué tan grande será su lista, o qué elementos específicos contendrá. La parte “genérica” ​​de la línea se ve entre " <" y " >" donde especificamos un tipo de datos; en este ejemplo usamos un “int”. Esto significa que esta lista en particular solo puede contener valores int. Debido a que todos los elementos de la lista están obligados a ser del mismo tipo de datos, el código se ejecutará de forma más segura y rápida. Lo llamamos genérico porque podemos pasar cualquier tipo de DataType (a veces hay excepciones a esta regla) en la declaración. Una lista de float, string o Transform se declararía de manera similar:


List<float> list1 = new List<float>();
List<string> list2 = new List<string>();
List<Transform> list3 = new List<Transform>();

En el método de inicio, ejecuté una variedad de tareas en nuestra lista e imprimí mensajes a la consola explicando lo que estaba sucediendo. Primero, uso un bucle for para generar y agregar 10 números aleatorios a nuestra lista. Los hice al azar para mostrar que la lista puede contener valores duplicados, y para ayudar a ilustrar el hecho de que puedo agregar valores fuera de orden y ordenarlos más tarde.

Luego, en la línea 21, verifico si agregamos alguna entrada del número 3. Si lo hicimos, eliminamos ese valor. (Tenga en cuenta que en este ejemplo solo elimino la primera ocurrencia que encuentro, pero podría haber más).

En la línea 27, le digo a la lista que se ordene a sí misma: de manera predeterminada, ordenará sus elementos en orden ascendente. Hay otras formas de ordenar los elementos que puede investigar más adelante.

Finalmente, en la línea 30 hago otro ciclo para imprimir los elementos en orden tal como aparecen después de haberlos ordenado. Con suerte, debería reconocer la sintaxis del paréntesis para leer un elemento en un índice, porque es lo mismo que trabajar con una matriz.

Copie el código de ejemplo y luego adjunte su script a un objeto en escena. Ejecute la escena y revise la salida en la ventana de la consola. En una ejecución de muestra, vi que agregué los siguientes índices: (1, 1, 7, 8, 3, 6, 1, 5, 0, 2), luego vi que se eliminó una entrada y luego vi las entradas ordenados como (0, 1, 1, 1, 2, 5, 6, 7, 8).

Diccionario genérico

Un diccionario es otro tipo de recopilación de datos utilizada por los programadores. A diferencia de una matriz o lista, se considera desordenada, y se accede mediante una “clave” en lugar de un “índice”. Ciertos escenarios pueden hacer que esto se sienta más natural, porque hacer referencia a un objeto por índice no significa necesariamente nada. Por ejemplo, si tengo una lista de GameObject, entonces intuitivamente no sé que GameObject en el índice 0 representa un subordinado y que GameObject en el índice 1 representa un jefe. Sin embargo, al asociar un valor a una clave, la relación se vuelve más obvia y legible en su código.

Imagina que estás haciendo un juego de rol con una gran variedad de artículos de la tienda. Algunas tiendas solo pueden mostrar ciertos artículos en un momento dado, y los objetos que muestran pueden no estar basados ​​en ningún orden en particular. Si administraba sus artículos como una lista o como una matriz, cada vez que quisiera obtener una referencia a un artículo, tendría que “recorrer” cada elemento de la lista hasta que encuentre el que coincida. Con un diccionario, básicamente puedes decir, “dame el elemento llamado ‘Daga’” e inmediatamente obtén su referencia. Veamos cómo se ve en el código:


#region Librerias
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
#endregion

namespace MoonAntonio
{
	public class DictionaryExamples : MonoBehaviour 
	{
		public Dictionary<string, int> stats = new Dictionary<string, int>();

		void Start()
		{
			stats.Add("HP", 10);
			stats.Add("MP", 3);
			stats.Remove("MP");
			if (stats.ContainsKey("HP")) Debug.Log(string.Format("Tienes {0} puntos de golpe restantes", stats["HP"]));
		}
	}
}

En la línea 7, declaramos nuestro diccionario. Tenga en cuenta que declaramos que estamos utilizando el espacio de nombres “System.Collections.Generic” para usar la especificación más corta. Los diccionarios tienen una “Clave” y un “Valor”, cada uno de los cuales puede ser un tipo de dato diferente. Lo ilustro aquí haciendo que la “clave” sea un tipo de cadena, y el “valor” un tipo de “int”.

En el método de inicio, uso algunos de los métodos disponibles para los diccionarios. Primero agrego una clave y un valor para una estadística llamada “HP”. Luego agrego una estadística diferente llamada “MP”. Además de agregar pares clave-valor, puede eliminarlos, aunque en ese caso solo especifica la clave. Elimino la estadística “MP” en la línea 13. Cuando trabaje con diccionarios, es una buena idea verificar que un diccionario tenga una clave antes de intentar leerlo. Lo hago en la línea 14 antes de leer el valor con la sintaxis del paréntesis (de nuevo, similar a leer una matriz) al final de la línea 15.

Hay algunos inconvenientes con los que puede encontrarse al usar diccionarios que debe tener en cuenta:

  1. Unity no “reconoce” los diccionarios, por lo que incluso si lo marca como público, no se serializará ni aparecerá en el inspector. Tenga en cuenta que esto no le impide usarlos en sus scripts, pero sí crea un problema en la inicialización basada en el editor.
  2. Si intenta utilizar la sintaxis Agregar (.Add (clave, valor)) a un diccionario que ya contiene esa clave, obtendrá un error: “ArgumentException: ya existe un elemento con la misma clave en el diccionario”. Puede cambiar un valor por el indexador como (diccionario [clave] = valor).
  3. Si intenta escribir un valor de diccionario por indexador, y la clave no existe, agregará la clave y asignará el valor automáticamente.
  4. Si intenta leer un valor de diccionario por indexador, y la clave no existe, obtendrá un error: “KeyNotFoundException: la clave dada no estaba presente en el diccionario”.
  5. Si intenta establecer un valor de diccionario para el tipo de dato incorrecto, el compilador se quejará: “error CS1502: la mejor coincidencia de método sobrecargado para ‘System.Collections.Generic.Dictionary.Add (string, int)’ tiene algunos inválidos argumentos "

Genéricos y Unity

Aunque los ejemplos de genéricos que he dado hasta ahora se aplicaron específicamente a instancias de una clase (nuestra lista y variables de diccionario), debe saber que también se puede aplicar a declaraciones de métodos. Debido a los beneficios de los genéricos, los encontrarás diseminados a lo largo de la funcionalidad de Unity. El primer lugar que puede observar es con la capacidad de “Agregar” o “Obtener” componentes en un GameObject:


Foo foo = gameObject.AddComponent<Foo>();
Bar bar = gameObject.GetComponent<Bar>();

Estas dos líneas muestran cómo se usan los marcadores genéricos en los métodos AddComponent y GetComponent para especificar qué tipo de componente se agregará o recuperará. Tenga en cuenta que en el ejemplo, “Foo” y “Bar” son ejemplos de nombres de clases para los que habría necesitado crear scripts.

Al usar la primera línea, puede agregar una instancia de un script a GameObject mientras su juego se está ejecutando. Por ejemplo, puede agregar un script de Muerte a un objeto siempre que sus puntos de golpe se reduzcan a cero. También puede compilar objetos complejos en tiempo de ejecución en lugar de en tiempo de edición para asegurarse de que el orden en que se agregan le permita configurar correctamente todas las dependencias de un objeto. Por ejemplo, si Foo necesita una referencia a Bar, entonces debe asegurarse de que Bar se haya agregado primero al objeto.

La segunda línea devuelve una instancia de un script en un GameObject (o null si no puede encontrar uno). Puede usar esto para probar si una referencia a un objeto tiene un componente que está buscando y, de ser así, realice algún tipo de acción sobre él (consulte sus propiedades públicas o utilice sus métodos públicos).

También verá una sintaxis similar al cargar recursos:


TextAsset data = Resources.Load<TextAsset>("Level_0");

Tenga en cuenta que para que esa línea funcione, necesitará un asset de texto con el nombre correcto en su proyecto dentro de una carpeta llamada “Resources”. Es posible tener múltiples tipos diferentes de assets que comparten el mismo nombre, como un prefabricado y un elemento de imagen, pero cuando especifica el tipo de objeto que se cargará como argumento genérico, devolverá el elemento correcto.

Métodos genéricos personalizados

A estas alturas ya se estará preguntando cómo crear su propio método genérico. Considere los siguientes ejemplos:


void ToggleComponent<T> () where T : MonoBehaviour {
    T t = gameObject.GetComponent<T>();
    if (t)
        t.enabled = !t.enabled;
}
 
T FindOrAdd<T> () where T : MonoBehaviour {
    T t = gameObject.GetComponent<T>();
    if (t == null)
        t = gameObject.AddComponent<T>();
    return t;
}

Una declaración de método genérico comienza como cualquier otro método que haya declarado, sin embargo, el marcador de tipo genérico “<“T”>” se inserta justo antes del paréntesis. El valor que coloca dentro del marcador genérico es similar al nombre de un parámetro, pero se usa para referirse al “Tipo de datos” que se usará dentro del método. Tenga en cuenta que puede volver a utilizar el mismo identificador como el tipo de datos para el tipo de devolución del método o como un tipo de datos en un parámetro que acepta el método.

Después del paréntesis hay algo llamado “restricción”, un bit de código opcional que se aplica a los tipos de datos sobre los que permitimos que funcione este método. En este ejemplo, especificamos que los métodos solo pueden funcionar con tipos de datos que son, o derivan de, la clase base MonoBehaviour. Debido a nuestra restricción, podemos escribir cualquier código dentro del método que se aplique a cualquier comportamiento en mono, como la capacidad de alternar si está habilitado o no. Sin la restricción, el compilador no sabría ningún detalle del objeto y no podría escribir una implementación tan específica.

Clases genéricas personalizadas

También puedes hacer una clase Genérica. De hecho, así es como se crearían la Lista y el Diccionario que utilizamos anteriormente:


using System;
 
public class InfoEventArgs<T> : EventArgs {
    public T info;
 
    public InfoEventArgs() {
        info = default(T);
    }
 
    public InfoEventArgs (T info) {
        this.info = info;
    }
}

Esta clase se convierte en una especie de plantilla para una subclase de EventArgs que contiene una única propiedad, pero esa propiedad puede ser de cualquier tipo. Este ejemplo muestra dos “constructores” que son un tipo especial de método que crea una instancia de una clase. Cuando inicializamos nuestro diccionario usando “nuevo diccionario”, estábamos llamando a un constructor. Se puede decir que un método es un constructor porque no tiene un tipo de retorno en su firma y porque el nombre del método es el mismo nombre que el nombre de la clase.

El primer constructor es un constructor predeterminado, no toma un parámetro. Como no utilicé ninguna restricción en esta clase, el tipo de datos podría ser un tipo de valor (como un int) o un tipo de referencia (como un GameObject), cada uno de los cuales tiene valores predeterminados diferentes e incompatibles: no puedo asignar un “GameObject” un valor de “0” y no puedo asignarle un valor “null” a “int”. La palabra clave “default” me resuelve este problema e inicia automáticamente un tipo genérico para mí.

En el segundo constructor, inicio la variable “info” a cualquier valor que pase el usuario. Utilicé una palabra clave especial “this”, que es una forma de que un script se refiera a sí mismo. El uso de la notación de puntos en la palabra clave “this” me permite diferenciar entre la variable “info” de la instancia y el parámetro “info” del constructor, ya que comparten el mismo nombre. Tenga en cuenta que también podría haberlos diferenciado dando al parámetro un nombre diferente.

/img/codebackdoor/learncsharpunity/07.gif
.
Resumen
En esta lección, presentamos una nueva función de lenguaje llamada genéricos. Los genéricos se pueden aplicar a métodos y clases como una forma de ayudar a que el código se pueda volver a usar en una variedad de DataTypes de una manera segura y rápida. Introduje la lista genérica y el diccionario, mostré cómo Unity usa los genéricos en sus propias bibliotecas y, finalmente, mostré cómo crear clases y métodos genéricos personalizados, con o sin restricciones.
Siguiente - Corrutinas
/img/ref.png
.