Static

Static

Je hebt het keyword static al een paar keer zien staan.  Wat is het nu?

Bij klassen en objecten duidt static aan dat een methode of variabele “gedeeld” wordt over alle objecten van die klasse.

static kan op 2 manieren gebruikt worden:

  1. Bij variabelen om een gedeelde variabele aan te maken, over de objecten heen.
  2. Bij methoden om zogenaamde methoden-bibliotheken of hulpmethoden aan te maken.


Variabelen en het static keyword

Wanneer gebruiken we nu static en wanneer niet? Als vuistregel: We gebruiken static enkel als het echt nodig is, anders niet.

Het is belangrijk om een klasse te bekijken als een blauwdruk van een object dat gemaakt kan worden. Bijvoorbeeld een klasse ‘Auto’. Een blauwdruk van een wagen kan plaats voor informatie voorzien zoals het merk, het model, kleur, het jaar van assemblage etc.

Het voorziet plaats voor al deze elementen, maar uiteindelijk kunnen we met een blauwdruk niet rondrijden, een nieuwe kleur geven of vragen hoeveel kilometers er op de teller staan. Dat is informatie die bijgehouden wordt in het object zelf, in dit geval een wagen, en niet op de blauwdruk. Om die informatie en methodes te kunnen gebruiken moeten we eerst een nieuw object aanmaken met het new keyword. Zie onderstaand voorbeeld.

    public class Car
    {
        public string Manufacturer { get; set; }

        public string Model { get; set; }

        public CarColor CarColor { get; set; }

        public Car(string manufacturer, string model, CarColor carColor)
        {
            Manufacturer = manufacturer;
            Model = model;
            CarColor = carColor;
        }

        public void ResprayCar(CarColor carColor)
        {
            CarColor = carColor;
        }

        public override string ToString()
        {
            return $"Car Details: nManufacturer: {Manufacturer}nModel:{Model}nColor: {CarColor}";
        }
    }

    public enum CarColor
    {
        Silver, Red, Blue, Green, BlueGreen, Yellow
    }
}

De klasse Car bevat de Properties in Manufacturer, Model en CarColor. Bij het aanmaken van het object wordt deze informatie voorzien en rolt er een nieuwe wagen met desbetreffende attributen van de band. Om alle informatie in dit object makkelijk weer te geven overriden we de ingebouwde ToString() methode.

var audi = new Car("Audi", "A5", CarColor.Red);
var ford = new Car("Ford", "Fiesta", CarColor.Blue);

Console.WriteLine(audi.ToString());
Console.WriteLine();
Console.WriteLine(ford.ToString());

Een klasse kan en zal vaak methodes bevatten die een bepaalde State van een object kunnen wijzigen. Deze klasse heeft bijvoorbeeld een methode ResprayCar die de kleur van de wagen aanpast. Als we de kleur van het Audi object wijzigen, heeft dat enkel invloed op de Audi instantie. De andere instanties en de blauwdruk (class) blijven ongewijzigd.

Kortgezegd: Elk Car object slaagt zijn eigen informatie/ State op in zichzelf. Als we de informatie van 1 object wijzigen heeft dat geen effect op de State van een ander Car object. Elk object is een afgesloten eilandje.

var audi = new Car("Audi", "A5", CarColor.Red);
var ford = new Car("Ford", "Fiesta", CarColor.Blue);

Console.WriteLine(audi.ToString());
Console.WriteLine();

audi.ResprayCar(CarColor.Silver);
Console.WriteLine(audi.ToString());
Console.WriteLine();

Console.WriteLine(ford.ToString());
Een methode die interne data aanpast heeft geen effect op andere instanties. Alles is geencapsuleerd.

Zonder het static keyword heeft ieder object z’n eigen variabelen en aanpassingen binnen het object heeft geen invloed op andere objecten.


Variabelen MET static

Maar wat is dan een static variabele? Een static variabele is een variabele die informatie bevat op de blauwdruk zelf, en niet op instanties van die blauwdruk. Stel dat ik bijvoorbeeld wil weten hoeveel Car objecten er zijn aangemaakt, kan ik dat niet opvragen aan 1 van de instanties. Zij bevatten enkel hun eigen informatie en weten niets af van andere instanties. In een geval zoals dit kunnen we een static variabele aanmaken.

Een static variabele is steeds beschikbaar in het geheugen. Hierdoor hoeven we geen instantie aan te maken van de klasse maar kunnen we rechtstreeks statische methodes, properties en variabele op klasseniveau aanspreken. We kunnen dus bijvoorbeeld een nieuwe private static int variabele toevoegen ‘carsProduced’ en deze incrementeren iedere keer een nieuw Car object aangemaakt wordt. We kunnen daarna een statische methode aanmaken die dit aantal retourneert.

public class Car
    {
        public string Manufacturer { get; set; }

        public string Model { get; set; }

        public CarColor CarColor { get; set; }

        // A static variable describes the class rather than an instance.
        private static int carsProduced;

        public Car(string manufacturer, string model, CarColor carColor)
        {
            Manufacturer = "Mercedes";
            Model = model;
            CarColor = carColor;

            carsProduced++;
        }

        public void ResprayCar(CarColor carColor = CarColor.Silver)
        {
            CarColor = carColor;
        }

        public override string ToString()
        {
            string result = $"Car Details: nManufacturer: {Manufacturer}nModel:{Model}nColor: {CarColor}";
            return result;
        }
        
        // A static method can only call other static methods and variables.
        public static int GetCarsProduced()
        {
            return carsProduced;
        }
    }

    public enum CarColor
    {
        Silver, Red, Blue, Green, BlueGreen, Yellow
    }
var audi = new Car("Audi", "A5", CarColor.Red);
var ford = new Car("Ford", "Fiesta", CarColor.Blue);

Console.WriteLine(audi.ToString());
Console.WriteLine();

audi.ResprayCar(CarColor.Red);
Console.WriteLine(audi.ToString());
Console.WriteLine();

Console.WriteLine(ford.ToString());
Console.WriteLine();

// A static methode is called on the Class itself rather than on an instance
Console.WriteLine($"Amount of cars produced: {Car.GetCarsProduced()}");

Console.ReadLine();
Met een statische variabele kunnen we algemene data bijhouden die de klasse beschrijft i.p.v. een instantie, zoals het aantal instanties dat aangemaakt is via deze klasse.

Belangrijk om te weten is dat een statische methode enkel statische variabelen kan gebruiken. Dit is ergens logisch aangezien statische en niet-statische methodes en variabelen verschillende onderdelen beschrijven. Niet-statische elementen beschrijven een object zoals een auto of computer. Een statisch element beschrijft de blauwdruk die gebruikt wordt om dergelijk object aan te maken.

Static laat je dus toe om informatie over de objecten heen te delen. Gebruik static niet te pas en te onpas: vaak druist het in tegen de concepten van OO en wordt het vooral misbruikt! Ga je dit vaak nodig hebben? Niet zo vaak. Het volgende concept wel.


Methoden met static

Heb je er al bij stil gestaan waarom je dit kan doen:

Math.Pow(3,2);

Zonder dat we objecten moeten aanmaken in de trend van:

Math myMath= new Math(); //dit mag niet!
myMath.Pow(3,2)

De reden dat je de math-bibliotheken kan aanroepen rechtsreeks op de klasse en niet op objecten van die klasse is omdat de methoden in die klasse als static gedefineerd staan.


Voorbeeld van static methoden

Stel dat we enkele veelgebruikte methoden willen groeperen en deze gebruiken zonder telkens een object te moeten aanmaken dan doen we dit als volgt:

class EpicLibray
{
    static public void ToonInfo()
    {
        Console.WriteLine("Ik ben ik");
    }

    static public int TelOp(int a, int b)
    {
        return a+b;
    }
}

We kunnen deze methoden nu als volgt aanroepen:

EpicLibrary.ToonInfo();

int opgeteld= EpicLibrary.TelOp(3,5);

Mooi toch.


Nog een voorbeeld

In het volgende voorbeeld gebruiken we een static variabele om bij te houden hoeveel objecten (via de constructor) er van de klasse reeds zijn aangemaakt:

class Fiets
{
    private static int aantalFietsen = 0;
    public Fiets()
    {
        aantalFietsen++;
        Console.WriteLine($"Er zijn nu {aantalFietsen} gemaakt");
    }

    public static void VerminderFiets()
    {
        aantalFietsen--;
    }
}

Merk op dat we de methoden VerminderFiets enkel via de klasse kunnen aanroepen:

Fiets.VerminderFiets();

Nota: Op het eerste zicht lijkt het gebruik van static een goede zaak. Minder instanties = minder code = gemakkelijker werken. Niets is echter minder waar. In praktijk willen we zoveel mogelijk met instanties van klassen werken en zo weinig mogelijk op klasse niveau zelf. Probeer static zo veel mogelijk te vermijden, zeker als je niet zeker weet waar je mee bezig bent.

Als vuistregel gebruik je dezelfde mentaliteit als voor private en public. Als je een variabele of methode public wilt maken, moet je hier een goede reden voor hebben. Als je die niet hebt, gebruik je private. Net hetzelfde met static. Als je geen goede reden hebt om iets static te maken, maak je het niet static. (Dit maakt het mij iets makkelijker is geen goede reden)

Static vs non-static

Van zodra je een methode hebt die static is dan zal deze methode enkel andere `static methoden en variabelen kunnen aanspreken. Dat is logisch: een static methode heeft geen toegang tot de gewone niet-statische variabelen van een individueel object, want welk object zou hij dan moeten aanpassen?

Volgende code zal dus een error geven:

class Mens
{
    private int gewicht=50;

    private static void VerminderGewicht()
    {
        gewicht--;
    }

De error die verschijnt An object reference is required for the non-static field, method, or property ‘Program.Fiets.gewicht’ zal bij de lijn gewicht-- staan.

Een eenvoudige regel is te onthouden dat van zodra je in een static omgeving (meestal een methode) bent je niet meer naar de niet-static delen van je code zal geraken.


Static en main

Dit verklaart ook waarom je bij console applicaties in Program.cs steeds alle methoden static moet maken. Een console-applicatie is als volgt beschreven wanneer je het aanmaakt:

public class Program
{
        public static void Main()
        {

        }
}

Zoals je ziet is de Main methode als static gedefinieerd. Willen we dus vanuit deze methode andere methoden aanroepen dan moeten deze als static aangeduid zijn.

Nota: Vanaf .Net 6 is het niet langer nodig om een static Main te gebruiken in een Console applicatie. In .Net 6 kan code rechtstreeks in de Program klasse uitgevoerd worden, zonder een methode te moeten gebruiken. Dit is vergelijkbaar met Python, wat mede omwille dezd reden over het algemeen wordt beschouwd als een goede taal voor beginners.

Zoals gezegd kan een statische methodes enkel andere statische methodes en variabelen aanspreken. Hierdoor kregen veel beginners de foutieve indruk dat werken met static goed was/verwacht werd. Dankzij .Net 6 en hoger hoeven we niet langer static te gebruiken in een console app. Zo wordt het foutieve gebruik van static nog verder ontmoedigd.

Is het absoluut nodig om static te werken als we informatie willen bijhouden dat de blauwdruk i.p.v. het object beschrijft? Nee, er bestaat een bepaald patroon dat het werken met static verder overbodig maakt, namelijk het Factory pattern.

Factory Method (refactoring.guru)

Abstract Factory (refactoring.guru)

In dit patroon wordt een bepaalde klasse gebruikt die als enige verantwoordelijk het aanmaken van andere objecten heeft. Denk bijvoorbeeld aan een Car Factory. Deze klasse zou dan met bijvoorbeeld een methode CreateCar() verantwoordelijk kunnen zijn voor het aanmaken van nieuwe Car objecten. Als we het totaal aantal gemaakte Car objecten willen bijhouden kan dit in een variabele in een instantie van een Factory klasse.

Dat vermijdt het gebruik van new, wat zoals we later zullen zien onvoorziene problemen met zich mee kan brengen. Lees zeker eens dit artikel:


Een use-case met static

Beeld je in dat je (weer) een pong-variant moet maken waarbij meerdere balletjes over het scherm moeten botsen. Je wilt echter niet dat de balletjes zelf allemaal apart moeten weten wat de grenzen van het scherm zijn. Mogelijk wil bijvoorbeeld dat je code ook werkt als het speelveld kleiner is dan het eigenlijke Console-scherm.

We gaan dit oplossen met een static property waarin we de grenzen voor alle balletjes bijhouden. Onze basis-klasse wordt dan al vast:

class Mover
{
    static public int Width { get; set; }
    static public int Height { get; set; }

    public void Update()
    {
        //Soon
    }

    public void Draw()
    {
        //Soon
    }
}

Elders kunnen we nu dit doen:

Mover.Height = Console.WindowHeight;
Mover.Width = Console.WindowWidth;

Mover m1 = new Mover();
Mover m2= new Mover();

Maar dat hoeft dus niet, even goed maken we de grenzen voor alle balletjes kleiner:

Mover.Height = 20;
Mover.Width = 10;

Mover m1 = new Mover();
Mover m2= new Mover();

De interne werking van de balletjes hoeft dus geen rekening meer te houden met de grenzen van het scherm. De klasse Mover bereiden we nu uit naar de standaard “beweeg” en “teken jezelf” code:

class Mover
{
    public Mover(int xi, int yi, int dxi, int dyi)
    {
        x = xi;
        y = yi;
        dx = dxi;
        dy = dyi;
    }

    static public int Width { get; set; }
    static public int Height { get; set; }

    private int dx=1;
    private int dy=0;
    private int x=0;
    private int y=0;

    public void Update()
    {
        x += dx;
        if(x>=Mover.Width|| x<0)  //hier gebruiken we de static Width
        {
            dx *= -1;
            x += dx;
        }

        y += dy;
        if (y >= Mover.Height || y<0)
        {
            dy *= -1;
            y += dy;
        }
    }

    public void Draw()
    {
        Console.SetCursorPosition(x, y);
        Console.Write("O");
    }
}

En nu kunnen we vlot balletjes laten rondbewegen op het scherm:

static void Main(string[] args)
{
    Console.CursorVisible = false; //handig dit hoor
    Mover.Height = Console.WindowHeight;
    Mover.Width = Console.WindowWidth;

    Mover m1 = new Mover(1,1,1,1);
    Mover m2 = new Mover(6,7,-2,1);

    while (true)
    {
        m1.Update();
        m1.Draw();

        m2.Update();
        m2.Draw();


        System.Threading.Thread.Sleep(50);
        Console.Clear();
    }
}

Stel dat we nu elke seconden het speelveld met 1 willen vergroten, dan hoeven we hiervoor enkel een extra variabele int count=0 voor de loop te zetten en dan in de loop het volgende te doen:

 if(count%20==0) //iedere seconde (daar we telkens 50ms slapen (1seconde =1000 ms => 1000ms/50ms == 20))
{
    Mover.Width++;
    Mover.Height++;
}


Maximum grootte

Als je voorgaande code zou runnen zal je zien dat je redelijk snel een error krijgt. Dit komt omdat de hoogte en breedte van een Console maar tot bepaalde waardes kunnen verhogen.

We kunnen dit opvangen door in de klasse Mover volgende twee autoproperties:

    static public int Width { get; set; }
    static public int Height { get; set; }

Te vervangen door fullproperties die controleren of er niet over de grenzen wordt gegaan mbv Console.LargestWindowWidth en Console.LargestWindowHeight. Voor `Widthkrijgen we dan:

private static int width;

public static int Width
{
    get { return width; }
    set
    {
        if (value > 0 && value <  Console.LargestWindowWidth)
            width = value;
    }
}