Aprende C# con Unity - Enums y Flags

Mencionamos brevemente los enums en nuestra última lección, por lo que sentí que ahora sería un buen momento para tratarla en mayor profundidad. Al mismo tiempo, podemos ampliar el tema e introducir Flags (máscaras de bits). Algo de esto se profundiza en el lado nerd de la programación, pero intentaré que todo sea fácil de entender.

GitHub Gitlab

El tipo “Enum”

Primero introduje la enumeración en la lección anterior con la configuración de “Difficulties”:


public enum Difficulties
{
  Easy,
  Medium,
  Hard,
  Count
}

Este fragmento de código se puede colocar fuera de una clase y se convierte en un tipo “global” que puede ser utilizado por cualquiera de sus scripts. También puede ubicarlo dentro de una clase, pero luego para acceder a él, necesita hacer referencia a la enumeración a través de puntos.


// Este ejemplo se puede usar cuando Difficulties no está contenido en una clase
// o cuando está definido en la clase actual
Difficulties d1 = Difficulties.Easy;
// Este ejemplo se usa cuando se define Difficulties en una clase
// que no sea la clase actual
Demo.Difficulties d2 = Demo.Difficulties.Easy;

El tipo enum es único para sí mismo, aunque tiene un tipo subyacente (por defecto, ‘int’). Es posible cambiar el tipo subyacente en la declaración, como especificar el “byte”, lo que podría hacer si sabe que no tendrá un gran rango de valores que deben representarse. El formulario se ve bastante similar a la herencia de clase, donde se hace referencia al tipo subyacente después de dos puntos, como en el siguiente ejemplo:


public enum Difficulties : byte
{
  Easy,
  Medium,
  Hard,
  Count
}

La instancia de una clase, puede asignarla a una referencia de tipo “base” sin conversión. Esto no es así con las enumeraciones; en su lugar, debe convertirlo o, de lo contrario, verá un error: “error CS0266: no se puede convertir implícitamente el tipo Demo.Difficulties ’en byte’. Existe una conversión explícita (¿falta using?) "


void Start ()
{
  // La conversión implícita de una instancia al tipo base está bien
  MonoBehaviour script = this;
  // La conversión implícita de una enumeración a un tipo subyacente no está bien...
  // byte value = Difficulties.Easy;
  // ... Usa esto en cambio
  byte value = (byte)Difficulties.Easy;
}

Los valores en nuestra enum comienzan con el valor predeterminado del tipo subyacente (‘0’) y cuentan hacia arriba, aunque puede especificar cualquier valor que el tipo subyacente pueda contener. Dejados solos, nuestros valores de Difficulties variarían de 0 a 3. Para especificar valores, use la siguiente forma:


public enum Multipliers
{
  Negative = -1,
  Nullify = 0,
  Positive = 1
}

Tenga en cuenta que debido a que los valores se autoincrementan, también podría haber especificado el valor “Negativo” y las otras entradas se habrían asignado correctamente.

Bits y cambio de bit

Uno de los mejores usos de las enumeraciones viene por medio de su uso como máscara de bits. Sin embargo, antes de comenzar a cubrir eso, puede ser útil tener una comprensión general de lo que está sucediendo bajo el capó.

Los bits se refieren a la secuencia binaria de 0 y 1 que, en conjunto, se utilizan para definir cosas más complejas. Los tipos de datos requieren una cierta cantidad de bits para estar completamente representados. Un ‘byte’, por ejemplo, requiere 8 bits como los siguientes:00000000

El valor numérico de la muestra anterior era cero, y al encender (establecer bits a 1 en varias ubicaciones) obtiene combinaciones que hacen los otros números. Aquí hay algunos números en orden con su patrón de bits asociado:


00000000 = 0
00000001 = 1
00000010 = 2
00000011 = 3
00000100 = 4

Es de esperar que pueda ver el patrón aquí, donde se incrementa a uno, luego mover un lugar y restablecer el otro bit a 0 para continuar incrementando el valor numérico.

Algunos programadores analizan esto y piensan: “Podría usar esta secuencia de bits como una matriz de bool”, y sin necesitar la cantidad equivalente de almacenamiento (tenga en cuenta que un “bool” requiere 8 bits). Básicamente, están mirando cada posición de bit y diciendo: ¿este bit es 0 (falso) o 1 (verdadero)?

Algunas matemáticas simples pueden revelar la posición de cada bit: 2 a la potencia del índice del bit que desea establecer, recuerde comenzar a contar desde cero:


00000001 = 2 ^ 0 = 1
00000010 = 2 ^ 1 = 2
00000100 = 2 ^ 2 = 4
00001000 = 2 ^ 3 = 8

Pero si no quiere recordar las matemáticas, puede usar algo llamado cambio de bit. Utilizará “«” para desplazarse un poco hacia la izquierda. Usaremos este método para declarar los valores de enum flags en un momento:


00000001 = 1 << 0
00000010 = 1 << 1
00000100 = 1 << 2
00001000 = 1 << 3

Flags (máscara de bits)

A veces verá una enumeración marcada con “Flags” como se muestra a continuación:


[System.Flags]
public enum Colors
{
  None = 0,
  Red = 1 << 0,
  Green = 1 << 1,
  Blue = 1 << 2
}

El marcador no tiene ningún efecto en la enumeración en sí o en los valores predeterminados que están asignados a sus elementos. Tenga en cuenta que aún tuve que asignar manualmente la potencia de 2 valores. Debajo del capó, este enum de ‘Colores’ todavía tiene un tipo subyacente de ‘int’ y puede contener cualquier valor que pueda tener ‘int’ y puede tener sus elementos asignados a cualquier valor dentro de ese rango, tal como lo podría haber hecho sin usar el marcador ‘Flags’. La razón por la que utiliza el marcador es habilitar otras funcionalidades, como la modificación de la salida, ToString() que pueden mostrar los nombres de los indicadores combinados en lugar del equivalente numérico del tipo subyacente.

Como definí cada elemento como una potencia de dos, esta enumeración puede tratarse como una “máscara de bits”, lo que significa que puede especificar cualquier combinación de los elementos (y activarlos o desactivarlos fácilmente en cualquier momento), en lugar de señalar solo a un solo elemento a la vez.

Varias características que deseará con frecuencia se muestran en el siguiente código:


void Start ()
{
  // Crea una variable para contener nuestra máscara de bits
  Colors c = Colors.None;
  // Activar
  c |= Colors.Red;
  // O asignar directamente una combinación de bits
  c = Colors.Blue | Colors.Green;  // Esto también se puede usar en la definición de nuevos elementos dentro del tipo enum mismo
  // Apagar
  c &= ~Colors.Blue;
  // Verifica si un bit está 'encendido'
  if ((c & Colors.Green) == Colors.Green)
  {
    // La flag está 'encendida'
  }
}

Algo de esto parece un poco difícil de leer, así que lo explicaré ahora. La línea vertical ‘|’ se lee ‘O’, lo que significa que está modificando la secuencia de bits para que esté ‘activada’ en cualquier ubicación en la que los otros bits de los números hayan estado ‘activados’.

Entonces, por ejemplo, esta línea c = Colors.Blue | Colors.Green; mira las dos secuencias de bits para hacer una secuencia de tercer bit donde cualquier bit que era un ‘1’ de cualquier valor será un ‘1’ en la salida final:


00000010 - Green
00000100 - Blue
-------- // Realice una OR para que se pase uno en cualquier columna
00000110 - Green, Blue

El ‘&’ se lee ‘AND’, pero es muy diferente de ‘OR’ porque requiere que se habilite el mismo bit (index-wise) en ambos números para que también se habilite el resultado final. Dado que Verde y Azul tienen índices diferentes, el uso de ‘AND’ en ellos daría como resultado cero, o ‘Colors.None’.

El ‘~’ se lee ‘NO’ y se refiere a una secuencia de bits que es opuesta a la secuencia actual (cada 0 se convierte en 1 y viceversa). Usamos una combinación de ‘Y’ y ‘NO’ para eliminar una flag, que funciona así:


00000110 - Verde, azul // Nuestro valor inicial tiene dos indicadores habilitados
11111011 - Esto es NO azul
-------- // Realizar un AND en estas dos secuencias deja solo los bits que están habilitados
00000010 - Volver a solo verde

BitMasks y Unity

Unity puede serializar su enumeración, desafortunadamente, no proporciona automáticamente un inspector de tipo selección de máscara por defecto. Obtendrá una buena lista desplegable con nombre para seleccionar una sola entrada a la vez, pero no la capacidad de seleccionar múltiples entradas como lo haría con una máscara de eliminación de la cámara como un ejemplo. Sin embargo, puede escribir un inspector personalizado para su script y exponer una propiedad usando EnumMaskField para obtener la funcionalidad que desea.

Por ejemplo, su script “EnumsFlags.cs”:


#region Librerias
using UnityEngine;
#endregion

namespace MoonAntonio
{
	[System.Flags]
	public enum Colors
	{
		None = 0,
		Red = 1 << 0,
		Green = 1 << 1,
		Blue = 1 << 2,
	}

	public class EnumsFlags : MonoBehaviour 
	{
		public Colors color;
	}
}

Se puede combinar con un script de inspector especial “EnumsFlagsInspector.cs” (Tenga en cuenta que este script debe estar ubicado dentro de una carpeta llamada “Editor” para funcionar):


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

namespace MoonAntonio
{
	[CustomEditor(typeof(EnumsFlags))]
	public class EnumsFlagsInspector : Editor
	{
		public override void OnInspectorGUI()
		{
			EnumsFlags e = (EnumsFlags)target;
			e.color = (Colors)EditorGUILayout.EnumFlagsField("Colors", e.color);
		}
	}
}

/img/codebackdoor/learncsharpunity/11.gif
.
Resumen
En esta lección, cubrí las enumeraciones y sus usos, incluido el uso como máscaras de bits. Tomamos una vista en profundidad de cómo se representan los bits y cubrimos escenarios de uso común, como encender y apagar bits o verificar el estado de un bit. Algunas de estas características no son tan “legibles” como otras opciones de codificación, pero generalmente son más eficientes, por lo que tiene sentido usarlas en ciertos escenarios. Finalmente, mostramos cómo Unity puede usar una máscara de bits en el editor escribiendo un script editor personalizado.
Siguiente - Structs
/img/ref.png
.