Przyspieszony kurs C# pod Unity3d dla leniwych i opornych. Dzisiaj: Czym są klasy abstrakcyjne i atrybut static
Poprzednie lekcje – Spis Treści
- Wstęp – Przygotowanie środowiska, zmienne i stałe, operatory arytmetyczne
- Funkcja warunkowa (If, Swith-Case), operatory logiczne
- Pętle (For, While, Do-While), tablice (Array) i kolekcje (ArrayList, HashMap)
- Klasa i funkcja
- Dziedziczenie i specyfikatory dostępu
- Klasy abstrakcyjne i zmienne statyczne
Klasy abstrakcyjne
Z poprzednich lekcji wiemy już czym jest klasa i czym jest dziedziczenie. Jeśli brakuje Ci tej wiedzy, wróć do poprzednich części kursu, bo bez tych informacji nie zrozumiesz zagadnień związanych z klasami abstrakcyjnymi.
Klasa abstrakcyjna jest w sumie klasą, która wygląda jak typowa klasa. Jest jednak pewna znacząca różnica. Klasy abstrakcyjne nie mogą mieć swojej instancji, służą jedynie jako wzorce. Czyli inne klasy dziedziczą po klasach abstrakcyjnych, ale klasy abstrakcyjne nie mogą istnieć same w sobie.
Po co nam to? Jak wiemy, jeśli różne klasy dziedziczą po jakiejś klasie, to ciągle są obiektem tej klasy nadrzędnej. Więc klasa abstrakcyjna staje się idealnym narzędziem, gdy chcemy przechować tablicę podobnych obiektów, na których można wykonać jakąś akcję, ale da ona różne efekty. Brzmi zawile, dlatego przykład growy.
Ekwipunek. W inwentarzu mamy np. miecz, jabłko, miksturę many, zwój kuli ognia. Wszystkie te obiekty chcemy trzymać w jednym plecaku (jedna tablica), ale gdy w ekwipunku klikniemy “użyj”, to każdy z tych przedmiotów zareaguje na to inaczej. Miecz pojawi się nam w ręce, jabłko doda nam życia, mikstura manę, a zwój sprawi że rzucimy kulę ognia. W takim przypadku klasa abstrakcyjna będzie idealna.
Aby móc się tym pobawić, potrzebujemy dwie klasy, dlatego utworzyłem sobie do testów dwa pliki C#: Lekcja_06a.cs i Lekcja_06b.cs
Skrypt Lekcja_06a:
using UnityEngine; public abstract class Lekcja_06a : MonoBehaviour{ public string txtToWrite = "Alive!"; public abstract void execute(string txt); public void writeSth() { Debug.Log(txtToWrite); } }
Co tutaj widzimy? Aby klasa była abstrakcyjna musimy ją poprzedzić słówkiem abstract. Widzimy też metodę poprzedzoną słówkiem abstract. Definiując w taki sposób metodę wymuszamy na obiektach dziedziczących posiadanie takiej metody, ale w klasie abstrakcyjnej nie definiujemy jej ciała.
Jak widać w klasie abstrakcyjnej mogą też istnieć metody i atrybuty nieabstrakcyjne. Wtedy dziedziczą się one w normalny sposób. Z racji, że zwykłe dziedziczenie było omawiane, nie będę do tego wracał.
Przejdźmy sobie do skryptu Lekcja_06b. Najpierw napiszemy go w bardzo podstawowej formie:
using UnityEngine; using System.Collections; public class Lekcja_06b : Lekcja_06a { }
Taki kod jednak zwróci nam błąd:
[stextbox id=”info” defcaption=”true”]Error CS0534 ‘Lekcja_06b’ does not implement inherited abstract member ‘Lekcja_06a.execute(string)’ KursCSharp.CSharp F:\Unity\QuickTip\KursCSharp\Assets\Lekcja_06b.cs 4 Active[/stextbox]
Wygląda strasznie. Ale ten błąd informuje nas o tym, że nasz skrypt Lekcja_06b dziedziczy po klasie abstrakcyjnej Lekcja_06a, która ma metodę abstrakcyjną execute(string), której my nie zaimplementowaliśmy w naszym kodzie. Jeśli kod zmienimy na taki:
using UnityEngine; using System.Collections; public class Lekcja_06b : Lekcja_06a { public override void execute(string txt) { } }
Wszystko działa. Metoda nie musi nawet nic robić. Ważne żeby była i miała deklarację ciała (czyli klamry). Druga ważne rzecz to słówko kluczowe override (z angielskiego nadpisz). Bez tego kod zwyczajnie nie zadziała.
Tutaj jest jeszcze jedna ważna uwaga. Jeśli chcemy w naszym skrypcie korzystać z MonoBehaviour to klasa abstrakcyjna (lub ta najbardziej nadrzędna) musi dziedziczyć po MonoBehaviour. Klasa nie może dziedziczyć jednocześnie po 2 klasach, więc nie możemy zmusić skryptu Lekcja_06b, aby jednocześnie dziedziczył po Lekcja_06a i MonoBehaviour.
Dokończmy teraz skrypt Lekcja_06b:
using UnityEngine; using System.Collections; public class Lekcja_06b : Lekcja_06a { public override void execute(string txt) { Debug.Log(txt); } void Start() { execute("Test"); writeSth(); } }
Po przypisaniu skryptu do jakiegoś obiektu (np. Main Camera) i uruchomieniu gry, zobaczymy w konsoli nasze oba napisy.
Tutaj warto przetestować jeszcze jedną rzecz i spróbować przypisać skrypt klasy abstrakcyjnej do jakiegoś obiektu. Oczywiście dostaniemy błąd zabraniający czegoś takiego, co nawiązuje do moich słów z samego początku. Nie da się tworzyć instancji klasy abstrakcyjnej.
Próba napisania kodu: Lekcja_06a klasaAbstrakcyjna = new Lekcja_06a(); również zawiedzie.
Static
Zmienne statyczne są bardzo powszechnie stosowane przez początkujących programistów, bo często ułatwiają pracę. Jednak jest to narzędzie zdradzieckie i zaleca się unikanie go jak ognia. Mówię tutaj bardzo poważnie. Nieumiejętne stosowanie zmiennych statycznych często przysparza więcej problemów niż korzyści.
Ale czym jest właściwie zmienna statyczna? Zmienne statyczne to takie, które zachowują swój stan przez cały czas działania programu i są dostępne z każdego miejsca. Aby utworzyć taką zmienną należy dodać sobie słówko static na początku nazwy zmiennej. O tak:
public static int sVariable = 0;
Żeby sobie to przetestować, dodałem dwa skrypty do kamery i wyglądają one tak:
using UnityEngine; using System.Collections; public class Lekcja_05 : MonoBehaviour { void Awake() { Lekcja_06b.sVariable++; } void Start() { Debug.Log ("L5: " + Lekcja_06b.sVariable); } }
Oraz:
using UnityEngine; using System.Collections; public class Lekcja_06b : Lekcja_06a { public static int sVariable = 0; public override void execute(string txt) { Debug.Log(txt); } void Awake() { sVariable++; } void Start() { sVariable++; Debug.Log("L6: " + Lekcja_06b.sVariable); } }
Co zwraca konsola po uruchomieniu czegoś takiego?
[stextbox id=”info” defcaption=”true”]L5: 2 i L6: 3[/stextbox]
Co z tego możemy wywnioskować? W skrypcie Lekcja_05 nie odwoływałem się do żadnej konkretnej instancji skryptu Lekcja_06b. Mimo to, zmienna została zmieniona i właśnie takie możliwości daje nam “statyczność”.
Najczęstszy błąd na jaki trafiałem przy wykorzystaniu zmiennych statycznych to przykład, gdzie ktoś stosował zmienną statyczną do przechowywania życia przeciwników. Dzięki temu miał do nich łatwy dostęp. Tyle że, trafienie dowolnego przeciwnika sprawiało, że wszyscy przeciwnicy tracili życie. Użycie statycznej zmiennej do liczenia ich HP sprowadzało się do tego, że wszyscy mieli wspólny licznik.
Więc kiedy stosować zmienne statyczne? Kiedy chcemy mieć łatwy dostęp do zmiennej, której stan jest jeden na całą grę i mamy 120% pewności, że nigdy nie będzie inaczej. Np. aktualny poziom na którym znajduje się gracz w trybie single player. Jednak tak jak pisałem na początku, starajmy się unikać zmiennych statycznych, bo łatwo sobie nimi zabałaganić kod.