Aprende C# con Unity - Structs

Las estructuras son como Clases, pero completamente diferentes. Si no sabe qué es una estructura o cuándo la usaría, o si no conoce la diferencia entre pasar por referencia y pasar por valor, esta lección es para usted.

GitHub Gitlab

Structs en Unidad

Dado que esta serie se basa en aprender C# para Unity, comencemos por señalar algunos lugares donde es posible que ya haya estado utilizando Structs:

  • Vector2, Vector3 and Vector4
  • Rect
  • Color y Color32
  • Bounds
  • Touch

En particular, las diversas formas de Vector (2-4) se utilizan en todas partes. Verá que se utilizan para almacenar todo, desde la posición, la rotación y la escala de una transformación hasta la velocidad de un cuerpo rígido, o la ubicación de un toque o un clic del mouse en la pantalla.

¿Qué es una Struct?

Una estructura es algo así como un tipo de datos compuestos. Se ve muy similar a una clase, porque puede definir campos y métodos de la misma manera. El siguiente ejemplo define una estructura y clase que son casi idénticas:


public struct PointA
{
  public int x;
  public int y;
}

public class PointB
{
  public int x;
  public int y;
}

En este ejemplo, la única diferencia notable se encuentra en las palabras clave: “struct” en lugar de “class”. Algunas otras diferencias entre los dos incluyen:

  • Una estructura no puede heredar de un tipo base como una clase
  • Las estructuras no pueden tener constructores sin parámetros
  • Todos los campos de una estructura se deben asignar antes de dejar un constructor
  • Las estructuras se pasan por valor, mientras que una instancia de una clase se pasa por referencia

El último punto, para mí, es el más importante. Existen muchas diferencias significativas entre los tipos de “valor” y los tipos de “referencia” que impactan cuándo y cómo debe usarlos.

Tipos de referencia

Cuando decimos que una instancia de una clase se pasa por referencia, lo que realmente está sucediendo es que obtenemos un “puntero” a la dirección en la memoria del objeto y luego pasamos ese “valor”. Esto es importante porque una instancia de una clase en realidad puede ser muy grande, que contiene muchos campos e incluso otros objetos. En ese tipo de escenario, copiar y transmitir todo puede afectar negativamente el rendimiento, y es por eso que solo pasa la dirección.

Los tipos de referencia se asignan en el “montón” y se limpian mediante algo llamado “recolección de basura”. La recolección de basura es un proceso que ocurre de manera automática pero que es lento y, en general, da cuenta de los contratiempos en la velocidad de fotogramas de tu juego. Por este motivo, no desea crear objetos con frecuencia y permitir que salgan del alcance. El siguiente ejemplo es un gran no-no :


// NO DEBERÍAS HACER ESTO
void Update ()
{
  // Crear una instancia de una clase con ámbito local en el ciclo de actualización (llamado cada frame)
  List<GameObject> objects = new List<GameObject>();
  // Imagina que las cosas se hacen con esta lista de objetos (se completa e itera etc.)
  for (int i = 0; i < objects.Count; i++) 
  {
  }
  // Cuando el método finaliza, la lista de objetos sale del alcance y en algún momento necesitará
  // ser basura recogida
}

Tipos de valor

Cuando decimos que algo se pasa por valor, lo que realmente está sucediendo es que la variable se clona/copia por completo, y la copia se transfiere mientras el original se deja intacto. Las estructuras son tipos de valores y se pasan por valor. Esto significa que las estructuras son idealmente pequeñas estructuras de datos.

Los tipos de valores se asignan en la “pila”, lo que significa que su memoria es fácil de recuperar y no tienen ningún efecto en “recolección de basura”. A diferencia del ejemplo de ciclo de actualización con tipos de referencia, es totalmente aceptable crear tipos de valores y permitir que salgan del alcance sin temor a una desaceleración inminente o un problema de memoria. Por ejemplo, lo siguiente es totalmente aceptable:


// Esto esta bien
void Update ()
{
  // Crea una variable local de un tipo de valor - struct 
  Vector3 offset = new Vector3 (UnityEngine.Random.Range (-1, 1), 0, 0);
  // Hacer cosas con eso
  Vector3 pos = transform.localPosition;
  pos += offset * Time.deltaTime;
  transform.localPosition = pos;
  // Tu memoria de estructuras será recuperada fácilmente ya que sale del alcance
}

Gotchas

Es tentador tratar de usar una estructura como una instancia de una clase, pero dado que se pasa por valor, hay varios errores que se encuentran a menudo. Considere el siguiente ejemplo:


#region Librerias
using UnityEngine;
#endregion

namespace MoonAntonio
{
	public class GotchasA : MonoBehaviour 
	{
		public Vector3 v1;
		public Vector3 v2 { get; private set; }

		void Start()
		{
			v1.Set(1, 2, 3);
			v1.x = 4;
			v2.Set(1, 2, 3);      // ** (Nota 2)
			//v2.x = 4;           // * (Nota 1)

			Debug.Log(v1.ToString());
			Debug.Log(v2.ToString());
		}
	}
}

  • (Nota 1) Esta muestra no se compilará debido a esta línea. Obtendrá el error “error CS1612: no se puede modificar un valor de retorno del tipo de valor de v2 . Considere almacenar el valor en una variable temporal”. El compilador intenta protegerte de un error lógico (que explicaré en un minuto), y sugiere que primero obtengas una nueva estructura, modifiques esa nueva estructura y la vuelvas a asignar.
  • (Nota 2) Esta línea es mucho más peligrosa porque se compilará y ejecutará pero no parece haber funcionado.

Si este código se compilara y ejecutara, vería el siguiente resultado:


( 4.0 , 2.0 , 3.0 )
( 0.0 , 0.0 , 0.0 )

Esto NO es lo que podrías haber esperado. Entonces, ¿qué está pasando? C# crea automáticamente una propiedad de respaldo oculto para ‘v2’. Cuando utiliza el getter (simplemente haciendo referencia a ‘v2’) C# proporciona una copia del respaldo, no del respaldo real: recuerde que esto se debe a que las estructuras se pasan por valor no por referencia. En la línea marcada por la Nota 2, lo que sucede es que obtienes una copia del respaldo, modificas la copia en su lugar, y luego la información se pierde de inmediato porque no está asignada a nada.

El siguiente ejemplo es similar: ilustra cómo a menudo se pasa por alto el concepto de un tipo de referencia frente a un tipo de valor y causa problemas. Aquí tenemos una referencia a una lista, que contiene referencias a Vector3:


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

namespace MoonAntonio
{
	public class GotchasB : MonoBehaviour 
	{
		void Start()
		{
			List<Vector3> coords = new List<Vector3>();
			coords.Add(new Vector3(0, 0, 0));
			coords[0].Set(1, 2, 3);
			//coords[0].x = 4; // error CS1612 (consulte el ejemplo anterior y comente esta línea para compilar)
			Debug.Log(coords[0].ToString());  // Será (0.0, 0.0, 0.0) - ¡No es lo que esperaba!
		}
	}
}

Por el contrario, el siguiente ejemplo funcionará como usted espera (o al menos como esperaría antes de que comenzara a asustarlo/confundirlo con los ejemplos anteriores)


#region Librerias
using UnityEngine;
#endregion

namespace MoonAntonio
{
	public class Foo
	{
		public Vector3 pos;
	}

	public class GotchasC : MonoBehaviour 
	{
		void Start()
		{
			Foo myFoo = new Foo();
			myFoo.pos.Set(1, 2, 3);
			myFoo.pos.x = 4; // Sin error de compilación
			Debug.Log(myFoo.pos.ToString()); //Será (4.0, 2.0, 3.0) - ¡Como esperabas!
		}
	}
}

¿Por qué este ejemplo funciona cuando los otros no? La respuesta es porque tenemos una referencia al objeto ‘myFoo’, no una referencia al campo del objeto. El objeto mantiene los valores de la estructura directamente (como un campo) y lo modifica directamente sin ningún problema.

Si el Moo hubiera implementado su Vector3 como una propiedad en lugar de como un campo (incluso con un campo de respaldo específico), habría sido un problema - vea el siguiente ejemplo:


#region Librerias
using UnityEngine;
#endregion

namespace MoonAntonio
{
	public class Moo
	{
		public Vector3 pos { get { return _pos; } set { _pos = value; } }
		private Vector3 _pos;
	}

	public class GotchasD : MonoBehaviour 
	{
		void Start()
		{
			Moo myMoo = new Moo();
			myMoo.pos.Set(1, 2, 3);
			// myMoo.pos.x = 4; // error CS1612 (consulte el ejemplo anterior y comente esta línea para compilar)
			Debug.Log(myMoo.pos.ToString()); // // Será (0.0, 0.0, 0.0) - ¡No es lo que esperaba!
		}
	}
}

Muchos de estos problemas se alivian si puede entrar en la mentalidad de tratar sus estructuras como “inmutables” (esto significa que nunca cambia los valores de ningún campo), o hacerlos inmutables (si es su estructura).

Resumen
En esta lección, presentamos la estructura y comparamos cuándo, dónde y por qué usaría uno sobre una clase. Mostramos algunas de las limitaciones y errores de las estructuras, pero también sus beneficios. Usadas correctamente, las estructuras son una herramienta muy valiosa y eficiente para agregar a tu arsenal de programación.
Siguiente - Scriptable Objects
/img/ref.png
.