Gå til indholdet

Hukommelsesteori

Inden du begynder at kode og benytte dine egne klasser, er du nødt til at kende til grundlæggende hukommelsesteori. Ikke fordi du på dette niveau skal være superbevidst om performance, men fordi den mest klassiske begynderfejl i objektorienteret programmering er manglende forståelse for forskellen på værdibaserede og referencebaserede typer.

Information til undervisere

At forstå forskellen på reference- og værdibaserede typer er nok en af de vigtigste ting i det at lære objektorienteret programmering. Her er forskellige afsnit samt videoer som kan bruges til at forklare forskellen. Hent evt samtlige Powerpoint filer der ligger til grund for videoer - de kan være bedre at gennemgå manuelt i forbindelse med undervisning.

Som nævnt flere gange i bogen har du fem typer af vælge i mellem i C#:

  • Klasse
  • Struktur
  • Enumeration
  • Delegate
  • Interface

De tre første – klasser, poster og strukturer – bruger du som også nævnt til at skabe skabeloner for instanser, og disse instanser kan indeholde værdier, som placeres i felter. I frameworket har Microsoft også gjort brug af klasser og strukturer til de fleste af de skabeloner, der er til rådighed under System-namespacet. Af de mere kendte strukturer kan blandt andet nævnes System.Int32 (int) og System.DateTime. Af kendte klasser kan nævnes System.String (string), System.Random, System.Array og mange andre.

Du har frit valg og kan vælge at kode en Terning som en klasse:

internal class Terning
{
    public int Værdi;

    public void Ryst()
    {
        Random rnd = new Random();
        this.Værdi = rnd.Next(1, 7);
    }
}

som kan benyttes således:

Terning t = new Terning();
t.Ryst();
Console.WriteLine(t.Værdi);

Du kan også vælge at benytte en struktur:

internal struct Terning
{

    public int Værdi;

    public void Ryst()
    {
        Random rnd = new Random();
        this.Værdi = rnd.Next(1, 7);
    }
}

som benyttes på præcis samme måde:

Terning t = new Terning();
t.Ryst();
Console.WriteLine(t.Værdi);

Du kan ikke se forskel i brugen af typen, men på typeniveau er der forskel. Eksempelvis kan en struktur ikke indgå i et arvehierarki. Men den helt store forskel skal findes i, hvordan værdier opbevares i hukommelsen – og det er du nødt til at forstå, før du bliver god til objektorienteret programmering.

Diagram over hukommelsen

Når en applikation starter, bliver den tildelt et område af hukommelsen til at opbevare instruktioner og midlertidige data:

Hukommelse tildelt til instruktioner og data

Instruktioner (den kompilerede applikation) og statiske data kan du se bort fra lige nu, men stack og heap er vigtige begreber i mange programmeringssprog. I virkeligheden er brugen af stack og heap meget kompleks og en del af det, man lærer i teori relateret til udvikling af kompilere, men i grundlæggende C# behøver du kun forstå det overordnet.

Stack

Helt overordnet og konceptuelt består en stack af et område i hukommelsen, hvor de variabler, du har defineret i en applikation, er placeret. Området er igen opdelt i mindre enheder kaldet en stack-frame, og hver stack-frame er relateret til en metode, der bliver kaldt, når programmet eksekveres. Du har tidligere lært om virkefelter, og som du sikkert kan huske, så har en metode (eller andre medlemmer) sit eget virkefelt med helt isolerede variabler. Disse variabler er kun tilgængelige i denne metode. Hvis de skal benyttes i andre metoder, må de sendes med som argumenter.

Hvis du arbejder med en konsol-applikation, vil applikationen starte i Main-metoden, som runtime vil sørge for at afvikle. Applikationens stack har derfor en enkelt stack-frame, som vi kan relatere til Main-metoden. I denne stack-frame kan der angives de variabler, der er defineret og tildelt værdier eller referencer.

Forestil dig at du afvikler følgende applikation, og at du stopper afvikling ved diagram-kommentaren:

internal class Program
{
  private static void Main(string[] args)
  {
    int a = 0;
    // Diagram 1
  }  
}

Så vil du kunne skabe et diagram som følger:

Diagram 1

Navnet ”stack” kommer fra det faktum, at en metode kan kalde en anden metode, som også har sin egen stack-frame, og denne (konceptuelt) lægges oven på den forrige. Se følgende eksempel:

internal class Program
{
    private static void Main(string[] args)
    {
        int a = 0;  
        // Diagram 1
        Metode1();  
        // Diagram 4
    }
    private static void Metode1()
    {
           // Diagram 2
        int a = 0;
        // Diagram 3
    }
}

Diagrammerne ser således ud:

Diagram 1

Diagram 2

Diagram 3

Diagram 4

Som det fremgår, er hver metode indkapslet i sin egen lille sandkasse, og alt hvad der benyttes af variabler, lever kun her. Såfremt Metode1 kalder en anden metode, vil der blot dukke en ny stack-frame op som afvikles og forsvinder igen.

Værdibaserede variabler

Værdibaserede argumenter

Hvis en metode har argumenter, kan du se dem som variabler i selve metoden. Værdierne fra den kaldende metode kopieres ind i den kaldte metode:

internal class Program
{
    private static void Main(string[] args)
    {
        int a = 5;
        // Diagram 1
        Metode1(a);
    }
    private static void Metode1(int x)
    {
        int y = 8;
        // Diagram 2
    }
}

Koden resulterer i følgende:

Diagram 1

Diagram 2

Bemærk, at værdien i a kopieres ind i den kaldende metode og lever sit helt eget liv i sin helt egen lille verden. Når Metode1 er afviklet, forsvinder x og y og de andre variabler i Metode1. Og blot for en god ordens skyld – variablerne x og y kunne lige så godt være kaldt a og b. Det har ingen betydning for den kaldende metode (Main).

Stackframes

Værdibaserede og referencebaserede typer

Som du tidligere har lært, så er en struktur (struct) en type, hvor variabler indeholder værdier, medens en klasse (class) er en type, hvor variabler indeholder reference til et sted i hukommelsen.

Info

En struktur er en værdibaseret type, mens en klasse er en referencebaseret type!

Overordnet betyder det også, at variabler af strukturer opbevarer sine værdier på det område i hukommelsen, der kaldes en stack – ligesom de forrige diagrammer viser. Variabler af klasser er også placeret på den førnævnte stack, men indeholder reference til instanser, der er placeret et andet sted i hukommelsen kaldet en heap. Her vil runtime allokere (tildele) plads til de instanser, der ønskes, og sørge for at rydde op igen når instanserne ikke længere bliver brugt. Oprydningen sker med en såkaldt garbage collector. De diagrammer, du har set indtil nu, har udelukkende bestået af Int32 variabler, og da Int32 er en struktur, opbevares værdier direkte på den førnævnte stack. Men som du så i starten af kapitlet, kan du skabe skabeloner af både strukturer og klasser. Enten:

struct Terning1 {
    public int Værdi;
}

eller:

class Terning2 {
    public int Værdi;
}

Forskellen på brugen af de to typer kommer rigtig til syne, når du tegner diagrammer over, hvad der sker, når du skaber instanser. Med udgangspunkt i Terning1 og Terning2 kan der oprettes instanser som følger:

Terning1 t1 = new Terning1() { Værdi = 1 };
Terning2 t2 = new Terning2() { Værdi = 2 };
Console.WriteLine(t1.Værdi);    // 1
Console.WriteLine(t2.Værdi);    // 2

Et diagram, der viser, hvordan hukommelsen ser ud, når instanserne er oprettet, ser således ud:

Som det fremgår, indeholder t1 en værdi og t2 en reference, og det kan give en stor forskel i brugen af variablerne. Lad os se på det klassiske C# eksamensspørgsmål, der tager udgangs-punkt i følgende kode:

using System;
namespace Demo
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            Terning1 t1 = new Terning1() { Værdi = 1 };
            Terning1 t2 = t1;
            t1.Værdi = 6;
            // Hvad er værdien af t1.Værdi og t2.Værdi
        }
    }

    struct Terning1
    {
        public int Værdi;
    }

    class Terning2
    {
        public int Værdi;
    }
}

Hvad tror du, svaret er på spørgsmålet stillet som en kommentar i koden? Og inden du svarer – husk at t1 og t2 er af typen Terning1, som er en struktur. Du bør skrive koden selv og prøve det af – men et diagram afslører tydeligt svaret:

Værdier fra t1 er kopieret over i t2, og når der efterfølgende rettes i t1, har det ikke nogen konsekvens på t2. Så svaret er, at t1.Værdi = 6 og t2.Værdi = 1. Lad os så se på næste eksamensspørgsmål der tager udgangspunkt i følgende kode:

using System;
namespace Demo
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            Terning2 t1 = new Terning2() { Værdi = 1 };
            Terning2 t2 = t1;
            t1.Værdi = 6;
            // Hvad er værdien af t1.Værdi og t2.Værdi
        }
    }

    struct Terning1
    {
        public int Værdi;
    }

    class Terning2
    {
        public int Værdi;
    }
}

Hvad tror du, svaret er på spørgsmålet stillet som en kommentar i koden? Og igen, inden du svarer – husk at t1 og t2 er af typen Terning2, som er en klasse. Diagrammet afslører svaret meget tydeligt:

Variablerne t1 og t2 indeholder referencer til et sted i hukommelsen, så instruktionen t2 = t1 kopierer referencen fra t1 til t2. Da både t1 og t2 dermed peger på den samme instans i hukommelsen, er svaret, at t1.Værdi er lig med 6 og t2.Værdi også er lig med 6. Det er vigtigt, du forstår denne helt basale forskel på variabler af strukturer og variabler af klasser, så du bør prøve ovennævnte kode af selv og tegne et par diagrammer.

Her er et par videoer som også viser påvirkning af hukommelse ved værdi- og referencebaserede typer.

Værdibaserede variabler (structs)
Værdibaserede variabler (class)

Referencebaserede argumenter

Viden om forskellen på værdibaserede og referencebaserede typer vil også komme dig til gode, når du skal kalde metoder med argumenter. Her er endnu et klassisk eksamensspørgsmål, der tager udgangspunkt i følgende kode:

namespace Demo
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            Terning1 t1 = new Terning1() { Værdi = 1 };
            Terning2 t2 = new Terning2() { Værdi = 1 };
            Metode1(t1, t2);
            // Hvad er værdien af t1.Værdi og t2.Værdi
        }

        private static void Metode1(Terning1 ts, Terning2 tc) {
            ts.Værdi = 6;
            tc.Værdi = 6;
        }
    }

    struct Terning1
    {
        public int Værdi;
    }

    class Terning2
    {
        public int Værdi;
    }
}

Der bliver skabt en instans af Terning1 (struktur) og en instans af Terning2 (klasse), og variablerne benyttes som argumenter i en metode, hvor værdien sættes til 6. Hvad er værdien af terning1.Værdi og terning2.Værdi efter kaldet til metoden? Prøv at tegne diagrammet selv – det vil afsløre svaret:

Efter kaldet til Metode1 vil t1.Værdi have værdien 1 og t2.Værdi have værdien 6. Årsagen skal findes i forskellen på typerne – ved kaldet til Metode1 bliver værdien af t1 og referencen til t2 kopieret ind i metoden. Derfor vil en tilretning af t1 ikke have nogen konsekvens. Det er vigtigt, at du kan svare på de tre klassiske eksamensspørgsmål, så skriv gerne koden selv og brug debuggeren – og tegn nogle diagrammer.

Referencebaserede argumenter

Det er ikke svært, så længe du husker at:

  • struktur = værdi
  • klasse = reference

Opgaver