Aprende C# con Unity - Clases

Ya sea que lo recuerdes o no, has estado creando clases todo el tiempo en las lecciones anteriores. He comentado brevemente varias características de clases como herencia y constructores, pero hay mucho más por cubrir. En esta lección, nos enfocaremos en esos temas con mayor profundidad e introduciremos algunos temas más avanzados como polimorfismo, constructores estáticos, clases abstractas y clases estáticas.

GitHub Gitlab

Herencia

La herencia es el concepto de que una clase toma todas las funcionalidades de una clase “padre” o “base” al mismo tiempo que brinda la oportunidad de ampliar o modificar la lógica previamente disponible. El código de plantilla proporcionado por Unity para todos sus scripts hará que su script herede de “MonoBehaviour” de forma predeterminada.

Puedes decir qué la clase hereda de tu script al observar los dos puntos en la declaración de clase:


public class HerenciaMono : MonoBehaviour
{
  // Yo heredo de MonoBehaviour
}

No tiene que heredar de MonoBehaviour. Puede especificar cualquier otra clase en su lugar como “Estadisticas” (tenga en cuenta que tendría que haber creado una clase llamada Estadisticas para que compile):


public class HerenciaEsta : Estadisticas
{
  // Yo heredo de Estadisticas
}

Si no especifica una clase para heredar, todavía hereda de algo: System.Object:


public class ClaseNormal
{
  // Yo heredo de System.Object aunque no lo menciono
}

En C#, una clase solo puede tener una clase base. Si desea algo así como herencia múltiple, considere usar interfaces o arquitecturas basadas en componentes en su lugar.

Polimorfismo

MonoBehaviour hereda de Behavior, que a su vez hereda de Component. El componente hereda de UnityEngine.Object que finalmente hereda de System.Object. Puedo decir que un MonoBehaviour es cualquiera de esas clases base (pero no siempre puedo decir lo contrario). Considere el siguiente ejemplo:


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

namespace MoonAntonio
{
	public class Polimorfismo : MonoBehaviour 
	{
		void Start()
		{
			List<System.Object> myList = new List<System.Object>();
			myList.Add("Hola Mundo!");
			myList.Add(42);
			myList.Add(Camera.main);
			for (int i = 0; i < myList.Count; ++i)
				Debug.Log(i + ": " + myList[i].ToString());
		}
	}
}

En este fragmento, declaro una lista genérica de objetos. Una lista genérica está restringida a contener un solo tipo de datos. Sin embargo, agrego tres cosas muy diferentes: una cadena de texto, un valor entero y un componente de cámara, todo sin problema. Cada uno de los tipos “concretos” aún comparte una “base” de “Objeto” y por lo tanto son adiciones válidas a la lista.

Debido al polimorfismo, puedo tratar estos tres elementos muy diferentes exactamente de la misma manera. Para demostrarlo, recorro todos los elementos de la lista y llamo a un método, todos comparten “ToString” e imprimen el resultado en la consola. El resultado es el siguiente:

0: Hola Mundo! 1: 42 2: Cámara principal (UnityEngine.Camera)

Cada elemento sabía cómo convertirse en una cadena a pesar de que todos pueden manejar el proceso de manera diferente. Por ejemplo, el componente de la cámara imprimió un mensaje informativo que incluía el nombre del GameObject al que estaba adjuntado. El código “cliente” (nuestro script de demostración que imprimió los mensajes en la consola) no necesita saber que los objetos son diferentes o cómo responderán a la llamada al método. Solo necesita saber que tienen el método.

Siempre es seguro tratar un objeto como una versión de clase base de sí mismo. Sin embargo, no siempre es seguro tratar una referencia de una clase base como una clase que hereda de ella. Considere la siguiente línea de código:


Camera c = myList[2];

Aunque sé que el objeto en el índice 2 de myList es una cámara, no puedo agregar esta línea al final del método. Si lo intenta, obtendrá un error, “error CS0266: no se puede convertir implícitamente el tipo objeto ’en UnityEngine.Camera’. Existe una conversión explícita (¿falta algo?) "

El motivo por el que se produce este error es porque, aunque todos los componentes de la cámara son Objetos, no todos los objetos son cámaras. Esto es evidente incluso en la demostración, porque los otros dos “objetos” en la lista no son cámaras.

Para trabajar con el objeto en su verdadera forma, ahora debe “echar” el objeto hacia atrás. También puedes verificar primero el “tipo” de un objeto para asegurarte de que funcionará:


// Verifica el tipo de una referencia de objeto con la palabra clave "es"
if (myList[2] is Camera)
{
  	// Emitir la referencia usando el nombre de la clase entre paréntesis
  	Camera c = (Camera)myList[2];
}

Alternativamente, puede usar una palabra clave “as” para intentar tratar un objeto como un tipo específico:


Camera c = myList[1] as Camera;
Debug.Log("success: " + (c != null));

Tenga en cuenta que en el ejemplo anterior, hice referencia al objeto en el índice 1 (un entero) que no se puede convertir a una cámara. Al usar la palabra clave “as”, a nuestra variable local se le asignará una referencia “nula” en el caso en que el lanzamiento falle.

Cómo modificar la funcionalidad de la clase base

Vimos que la implementación ToString de la cámara que es diferente de la implementación ToString de un entero. Supongamos que queremos que nuestro script crea una implementación diferente. Ponga el cursor fuera de sus otros métodos y comience a escribir “override” las características de autocompletar de su editor de scripts con la esperanza de comenzar y sugerir métodos que puede modificar. Visual studio me muestra: Equals, GetHashCode y ToString como opciones válidas para sobrescribir. Al seleccionar ToString, mi editor creó el siguiente fragmento:


public override string ToString ()
{
  return string.Format ("[Demo]");
}

La firma de este método debe resultarle familiar, con la excepción de la palabra clave “override”, que es lo que nos informa que podemos reemplazar, modificar o extender la funcionalidad de la clase base.

Con la implementación tal como está, llamar a ToString en instancias de nuestra clase informará a los usuarios que están haciendo referencia a una instancia de la clase Demo. Si tuviéramos información adicional que sería conveniente saber, podríamos agregarla a la cadena. Por ejemplo, imagine que teníamos dos campos llamados ‘x’ e ‘y’ que contienen información de coordenadas. Entonces podría usar la siguiente línea que nos dirá que tenemos una instancia de demostración así como también los valores que tiene actualmente:


return string.Format ("[{0} x:{1} y:{2}]", base.ToString(), x, y);

Cómo preparar una clase para la herencia

Imagina que quieres tener un montón de objetos de juego que sean “seleccionables”, pero cuando los seleccionas pasan cosas diferentes. Por ejemplo, seleccionar una silla puede hacer que el material de la silla destelle, lo que indica que lo tocó. Seleccionar un personaje puede causar una animación especial y un efecto de sonido para jugar. La mayoría de la lógica de selección en sí misma será la misma, pero los detalles de lo que sucede después de haber sido seleccionado son diferentes. En este caso, querrá volver a utilizar todo el código posible y luego extenderlo a la variación.


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

namespace MoonAntonio
{
	public abstract class Selectable : MonoBehaviour
	{
		public bool isSelected { get; protected set; }
		public virtual void SetSelected(bool value)
		{
			if (isSelected == value)
				return;
			isSelected = value;
			if (isSelected)
				Select();
			else
				Deselect();
		}
		protected abstract void Select();
		protected abstract void Deselect();
	}
}

Esta muestra presenta varias palabras clave nuevas. En la definición de clase, incluí la palabra clave “abstract”. Una clase base abstracta es una clase que no puede ser instanciada; si lo intenta, obtendrá un error, “error CS0144: No se puede crear una instancia de la clase abstracta o interfaz ‘Selectable’”

Lo mismo cuando trates de adjuntar un componente abstracto a un objeto en Unity. Verá un cuadro de diálogo de error si lo intenta, “No se puede agregar el comportamiento del script Selectable. ¡La clase de guion no puede ser abstracta!

La razón por la que desea marcar un resumen de clase es porque cuando usa esta palabra clave, puede declarar métodos sin implementarlos. Puede dejar detalles específicos de implementación hasta subclases.

Con respecto a la visibilidad, ya he hablado de “public” y “private”, pero la propiedad de esta clase usa una nueva palabra clave: un “protected”. Esta palabra clave es similar a “private”, excepto que las subclases también tienen acceso permitido. Entonces, básicamente, la propiedad “isSelected” puede ser leída por otras clases, pero solo puede ser escrita por ella misma o por subclases de ella misma.

Tenemos un método público, llamado “SetSelected” que acepta un parámetro bool que indica si el objeto será o no seleccionado. Marqué este método como “virtual”, lo que indica que las subclases pueden “override” si desean modificar, reemplazar o ampliar la funcionalidad, pero no es necesario. Proporcioné una implementación predeterminada que funciona. El método en sí mismo verifica si estamos tratando de establecer el objeto en un estado de selección en el que ya se encuentra, y si es así, regresa. Luego marca la propiedad isSelected y llama a un método que refleja su nuevo estado de selección.

Finalmente tengo dos métodos “protegidos”, “Select” y “Deselect”. Protegido implica el mismo significado aquí, otras clases no pueden ver o invocar estos métodos, a menos que sean una subclase de “Selectable”. También marqué los dos métodos como “abstract” que, a diferencia del uso de “virtual”, significa que la subclase debe “override” el método (a menos que también sean clases abstractas).

Tenga en cuenta que solo puede marcar un método como abstracto si la clase también es abstracta o si obtiene un error, “error CS0513: Selectable.Select() ’es abstracto pero se declara en la clase no abstracta Selectable’”

Si crea una subclase no abstracta y no implementa los métodos abstractos, obtendrá otro error, “error CS0534: SelectableDecoration ’no implementa el miembro abstracto heredado Selectable.Select()’”

Constructores y Destructores

Los constructores le permiten controlar lo que sucede cuando se crea una instancia de un objeto o cómo se crea una instancia. Los destruidores le permiten controlar lo que sucede cuando se destruye un objeto y proporcionan una forma de limpiar los recursos, etc.


#region Librerias
using UnityEngine;
#endregion

namespace MoonAntonio
{
	public class Point
	{
		public int x;
		public int y;

		public Point()
		{
			Debug.Log("¡Estoy vivo! ... pero estoy a cero.");
		}

		public Point(int x, int y)
		{
			this.x = x;
			this.y = y;
			Debug.Log("Whoooa, debo ser valioso!");
		}

		~Point()
		{
			Debug.Log("Me estoy derritiendo, FUNDICIÓN !!!");
		}
	}

	public class Constructores : MonoBehaviour 
	{
		public void Start()
		{
			Point p1 = new Point();
			Point p2 = new Point(2, 5);
		}
	}
}

Esta muestra definió una clase llamada “Point” que muestra dos constructores diferentes: un constructor “predeterminado” (uno sin parámetros) y un constructor con parámetros. Se puede decir que un método es un constructor porque no tiene un tipo de retorno y porque el identificador tiene el mismo nombre que el nombre de la clase.

La clase Constructores crea dos instancias de la clase Point, una para cada tipo de constructor. Tenga en cuenta que debe usar la palabra clave “new” junto con el constructor o obtendrá un error, “error CS0119: expresión denota un ’tipo’, donde se esperaba una variable, valor o grupo de métodos”

También tenga en cuenta que no debe usar constructores con scripts basados ​​en MonoBehaviour. Si lo intenta, verá una advertencia en la consola, “Está tratando de crear un comportamiento mono con la palabra clave ’new’. Esto no esta permitido. MonoBehaviours solo se puede agregar usando AddComponent(). Alternativamente, su script puede heredar de ScriptableObject o no tener ninguna clase base”. Unity requiere que cree objetos y que una los componentes de manera especial para que pueda configurar y rastrear todo correctamente.

Nuestra clase Point también define un “Destructor” que puedes identificar con la tilde que procede con un identificador que coincide con el nombre de la clase. A diferencia de un constructor, no invocas directamente un destructor. Se llamará automáticamente cuando un objeto sale “fuera del alcance” o se destruye. En este ejemplo, las instancias de dos puntos son locales al método “Start”, por lo que tan pronto como el método finalice ya estarán fuera del alcance.

Si adjunta este script a un GameObject y ejecuta su escena, verá el siguiente resultado en la ventana de la consola:

¡Estoy vivo! … pero estoy a cero. Whoooa, debo ser valioso! Me estoy derritiendo, FUNDICIÓN !!! Me estoy derritiendo, FUNDICIÓN !!!

Constructores estáticos

De la misma manera que puede especificar la lógica para el inicio de una instancia de una clase, también puede especificar la lógica para el inicio de la clase. Un constructor estático no se invoca directamente, sino que se llamará inmediatamente (y solo una vez) tan pronto como cualquier clase haga referencia a la clase. Un constructor estático parece un constructor normal, pero va precedido de la palabra clave “static”.


static Point ()
{
  Debug.Log("Hola Mundo!");
}

Si agregó esto a la clase Point en la muestra anterior, verá “Hola Mundo!” Impreso en la consola antes de que se haya instanciado el primer punto.

Si su clase hace uso de variables estáticas, el constructor estático es un buen lugar para inicializarlas.

Clases estáticas

Una clase que está marcada como estática no puede ser instanciada. Todas las variables y métodos que contiene también se deben marcar como estáticos.


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

namespace MoonAntonio
{
	public static class Global
	{
	  public static int count;

	  public static void DoStuff ()
	  {
	    count++;
	    Debug.Log("Tengo " + count);
	  }
	}
}

Si intenta crear una instancia de una clase estática, obtendrá un error, “error CS0723: prueba: no ​​puede declarar variables de tipos estáticos”

Si intentas subclasificar una clase estática, también obtendrás un error, “error CS0709: Test : no ​​puede derivar de la clase estática Global”

Trabaja con una clase estática haciendo referencia al nombre de la clase y usa la notación de puntos, como Global.DoStuff();

Las clases estáticas pueden ser formas convenientes de almacenar información a la que cualquier cosa puede acceder desde cualquier punto, porque no necesita hacer un seguimiento u obtener una instancia de la clase. Tenga en cuenta que los campos estáticos vivirán tanto como lo haga la clase, que es la duración del programa.

/img/codebackdoor/learncsharpunity/09.gif
.
Resumen
En esta lección, cubrimos una gran cantidad de características de las clases que incluyen constructores normales y estáticos, destructores, herencia y polimorfismo. Introduje nuevas palabras clave como abstract, virtual, override y protected. También mostré cómo una clase en sí misma puede ser abstracta o estática. Aunque muchas de estas características son fáciles de mostrar, se pueden considerar material avanzado. La arquitectura que aprovecha estas características puede ser difícil de entender por completo.
Siguiente - Guardando datos
/img/ref.png
.