Gå til indholdet

Introduktion

Funktionsorienteret programmering er en tilgang, der lægger vægt på at bygge software ved at kombinere rene funktioner. Det adskiller sig fra den imperativ eller objektorienteret programmering ved at undgå delt tilstand, mutable data og sideeffekter. Dette paradigme stammer fra matematik og lambda-kalkulus, hvilket gør funktioner til førsteklasses borgere i programmeringssproget.

I funktionsorienteret programmering er konceptet om immutabilitet centralt. Det betyder, at dataobjekter ikke kan ændres efter de er oprettet. Dette kan forbedre pålideligheden og forståeligheden af kode, da det eliminerer potentielle fejlkilder fra ændringer i tilstanden under programmets udførelse. For eksempel, når en funktion tager input og producerer output uden at ændre input-værdierne eller andre eksterne tilstande, er funktionen ren. Dette gør det nemmere at forudse funktionens opførsel og teste den isoleret.

En anden grundlæggende egenskab i funktionsorienteret programmering er brugen af “high order” funktioner. Disse er funktioner, der kan tage andre funktioner som argumenter eller returnere dem som resultater. Dette koncept udvider fleksibiliteten og genanvendelsen af kode ved at tillade konstruktioner som funktionelle kompositioner og curry-funktioner, hvor en funktion med flere argumenter omdannes til en sekvens af funktioner, hver tager et enkelt argument.

Endelig benytter funktionsorienteret programmering ofte “lazy evaluation”, hvor udregning af udtryk udsættes, indtil deres resultater faktisk er nødvendige. Dette kan effektivisere ydeevnen ved at undgå unødvendig beregning, især i forbindelse med håndtering af store datasæt eller komplekse algoritmer.

Samlet set opmuntrer funktionsorienteret programmering til skrivning af kode, der er lettere at teste, lettere at forstå, og som er mere forudsigelig, hvilket alle er vigtige faktorer for at bygge robuste og vedligeholdelige software systemer.

Information til undervisere

Dette kapitel introducerer funktionsorienteret programmering og dækker de grundlæggende koncepter, der er nødvendige for at forstå funktionsorienteret programmering i C#. Der er ikke noget kode men derimod en forklaring på hvad funktionsorienteret programmering er og hvordan det bruges i C#. Brug eksemplet med terningen til at forklare, hvordan en delegate kan bruges til at afkoble funktionalitet.

I C# understøttes funktionsorienteret programmering gennem brug af funktioner, lambda-udtryk, og ikke mindst delegates. En delegate er en reference til en metode og giver mulighed for at kalde metoder dynamisk. Dette er især kraftfuldt i situationer, hvor du ønsker at programmere til interfaces snarere end implementeringer, da du kan sende metoder rundt som argumenter, gemme dem og udføre dem på et senere tidspunkt. Dette åbner op for mange avancerede programmeringsteknikker, såsom callbacks, event-håndtering og inversion of control (IoC).

Hvad er en delegate

Du har indtil nu lært om fire forskellige muligheder for skabe egne typer – klasse, struktur, enumeration og interface. I dette kapitel skal du lære om en af de mere avancerede typer kaldet en delegate.

En delegate, der, som navnet lidt antyder, giver mulighed for at delegere metodekald videre. I nogle programmeringssprog kaldes det en funktionspointer, men det er noget anderledes skruet sammen i C#, fordi sproget er så typestærkt og typesikkert.

I sin helt grundlæggende form er en delegate en type, du kan skabe et objekt af, og dette objekt indeholder en liste af referencer til metoder af samme signatur (returværdi og argumenter). Da delegate-typen er referencebaseret, betyder det, at du kan gemme eller videresende en reference til en eller flere metoder, og disse metoder kan så senere afvikles. Min erfaring med at lære studerende, hvordan delegates virker, er, at det ikke det er så svært rent syntaksmæssigt, men at det er svært at se det overordnede formål, så lad mig vise dig et par eksempler på brugen af delegates, inden vi kigger på syntaks.

En terning med log

Lad os antage, at du har fået til opgave at skabe en terning. Det har vi gjort mange gange i løbet af bogen, men denne terning skal kodes således, at hver gang der rystes en ny værdi, så skal den skrive info til en log om, at er der rystet samt angive den nye værdi.

I første version af terningen skal du blot logge til konsol, og det kan du kode som følger:

public class Terning
{
    public int Værdi { get; private set; }
    private Random rnd = new Random();
    public void Ryst()
    {
        this.Værdi = rnd.Next(1, 7);
        this.Log($"Rystet til en {this.Værdi}'er");
    }

    private void Log(string tekst)
    {
        Console.WriteLine(tekst);
    }
    public Terning()
    {
        this.Ryst();
    }
}

Bemærk, at når Ryst-metoden kaldes, så sørger den for at kalde den private Log-metode, som skriver til konsol. Prøv at kode det selv, så du kan se funktionaliteten. Det varer dog ikke længe, før der på listen af opgaver (issues hedder det typisk i et source control-system) kræves en rettelse af log-funktionaliteten. Flere kunder vil gerne have mulighed for at logge til en fil (c:\temp\terning.log), til konsol, til begge dele eller slet ikke logge. Du må tilbage til koden, og kommer måske frem til følgende simple løsning:

public class Terning
{
    public int Værdi { get; private set; }
    private Random rnd = new Random();
    public bool LogTilFil { get; 
            private set; }
    public bool LogTilKonsol { get; 
            private set; }
    public void Ryst()
    {
        this.Værdi = rnd.Next(1, 7);
        this.Log($"Terning er rystet til en {this.Værdi}'er");
    }

    private void Log(string tekst)
    {
        if (this.LogTilKonsol)
            Console.WriteLine(tekst);
        if (this.LogTilFil)
            System.IO.File.AppendAllText(@"c:\temp\log.txt", 
               tekst + "\r\n");
    }
    public Terning(bool logTilKonsol, bool logTilFil)
    {
        this.LogTilFil = logTilFil;
        this.LogTilKonsol = logTilKonsol;
        this.Ryst();
    }
}

Nu skal instanser af terninger skabes med angivelse af, hvordan man ønsker log:

// logger både til konsol og til fil
Terning t = new Terning(true, true);

Opgave løst – kompilér applikation og send til kunderne.

Der varer dog ikke længe, før nogle kunder også gerne vil kunne logge til forskellige databaser samt foretage http-kald. Det er jo skruen uden ende! Du skal gøre koden i klassen mere og mere kompleks for at imødekomme kundernes krav og vil blive nødt til at skuffe nogle kunder, fordi du ikke ønsker, at din Terning-applikation skal have reference til eksempelvis obskure databasedrivere.

Den endelige terning

Det er her, en delegate kan hjælpe dig!

En delegate er som nævnt en type, der kan skabes et objekt af, og i dette objekt er der en liste af reference til metoder med samme signatur. Hvis nu du aftaler med kunderne, at de selv kan kode deres egen log-metode med en konkret signatur (måske en void metode med et string-argument), så skal du nok sørge for at afvikle metoden eller metoderne, når der rystes. På den måde lægger du jo hele log-funktionaliteten over på kunderne selv. De kan kode præcis, hvad de har lyst til og blot tilføje reference til metoderne til et delegate-objekt. Terningen indeholder en reference til objektet og sørger for at kalde metoderne, når der rystes.

Her er et eksempel på en sådan løsning. Du skal ikke forstå syntaksen, men ideen bag brugen af et delegate-objekt:

public class Terning
{
    public int Værdi { get; private set; }
    private Random rnd = new Random();
    public Action<string> LogMetoder { get; 
        set; }

    public void Ryst()
    {
        this.Værdi = rnd.Next(1, 7);
        this.Log($"Terning er rystet til en {this.Værdi}'er");
    }

    private void Log(string tekst)
    {
        if (LogMetoder != null)
            LogMetoder.Invoke(tekst);
    }
    public Terning()
    {
        this.Ryst();
    }
}

Du skal altså se bort fra forståelse af syntaks lige nu, men i klassen er egenskaben LogMetoder en reference til et delegate objekt, der kan indeholde referencer til void-metoder med et enkelt string argument:

public Action<string> LogMetoder { get; set; }

og i Log-metode afvikles disse metoder (hvis der er nogen):

if (LogMetoder != null)
  LogMetoder.Invoke(tekst);

Så nu er al log-funktionalitet lagt i hænderne på brugerne af klassen (igen – se bort fra syntaks – det er forståelsen, det handler om):

// Ingen log
Terning t1 = new Terning();
t1.Ryst();

// Log til konsol
Terning t2 = new Terning();
t2.LogMetoder += tekst => Console.WriteLine(tekst);
t2.Ryst();

// Log til fil
Terning t3 = new Terning();
t3.LogMetoder += tekst => 
   File.AppendAllText(@"c:\temp\log.txt", tekst + "\r\n");
t3.Ryst();

// Log til fil og konsol
Terning t4 = new Terning();
t4.LogMetoder += tekst => Console.WriteLine(tekst);
t4.LogMetoder += tekst => 
   File.AppendAllText(@"c:\temp\log.txt", tekst + "\r\n");
t4.Ryst();

// Log til HTTP
Terning t5 = new Terning();
t5.LogMetoder += tekst => {
    using (System.Net.WebClient w = new System.Net.WebClient())
    {
        w.UploadString("http://www.minlog.dk/log/", "POST", tekst);
    }
};            
t5.Ryst();

I koden benyttes noget helt nyt i forhold til oprettelse af metode – en såkaldt lambda-syntaks – som i virkeligheden blot er en simpel måde at skabe en metode på. Mere om dette senere i dette kapitel, men eksempelvis vil koden:

t2.LogMetoder += tekst => Console.WriteLine(tekst);

oprette et delegate objekt og tildele en enkelt metode på listen som udskriver på konsol. Denne metode vil så blive kaldt af koden i Terning-klassen. At der benyttes en Lambda har ikke noget med delegate objektet at gøre. En Lambda er bare en anden måde at skrive en metode på.

I seneste kapitel lærte du om brugen af interface, og at det blandt andet blev brugt til afkobling af data (dependency injection). Brugen af en delegate til log-funktionalitet kan du også formulere som afkobling af funktionalitet.

Opgaver