Gå til indholdet

Lambda-udtryk

Nu har du lært om delegates og ved, at objekter af delegates kan indeholde referencer til metoder, og referencen til delegate objekterne kan sendes med som argumenter til andre metoder eller benyttes som returværdier. Dermed har du viden nok til at kunne forstå lambda-udtryk, som bliver benyttet meget i moderne C#.

Information til undervisere

Lambda-udtryk er en af de ting, der kan være svært at forstå for begyndere, men super vigtigt. Heldigvis er det også en af de ting, der er nemt at forklare – det er jo bare en anden måde at skrive en metode på. Man skal bare kende reglerne for forkortelse. Så det er super vigtigt at få skrevet kode med lambda-udtryk - fra en standard C# metode til et fuldt forkortet lambda-udtryk. Bed kursisterne i starten bruge Chat GPT eller Copilot til at “konvertere” metoder til lambda-udtryk og omvendt.

Lambda-udtryk har fået sit navn fra den amerikanske matematiker Alonzo Church, som blandt andet arbejdede med teorierne bag logiske lambda-kalkyler, og findes nu som en notation for anonyme funktioner i mange programmeringssprog – herunder C#.

Info

Lambda-udtryk er blot en nem måde at skrive en metode på.

I C# erklæres lambda-udtryk ved hjælp af operatoren => (fed pil eller fat arrow på engelsk). Så når du ser en => i koden, ved du, at du har fat i et lambda-udtryk/anonym metode. I virkeligheden er lambda-udtryk blot en nem og hurtig måde at skrive en funktion på, og når du lige lærer syntaksen, er det meget nemt. Det som kan forvirre en del, når du starter med at lære lambda-udtryk er, at et udtryk kan forkortes ned til næsten ingenting, men når du har lært reglerne for forkortelse, ser du hurtigt lyset. Lad os tage udgangspunkt i følgende kode:

using System;

namespace Demo
{
    internal class Program
    {
        private static void Main(string[] args)
        {

            Func<int, int> d = Dobbelt; 
            Console.WriteLine(d(5));    // 10
        }

        public static int Dobbelt(int a)
        {
            return a * 2;
        }
    }
}

I koden erklæres en variabel d, som kan indeholde referencen til en delegate, som igen kan indeholde referencer til metoder, der returnerer et heltal og tager et heltal som argument. På samme linje oprettes et delegate objekt med en reference til metoden. Herefter afvikles metoden indirekte gennem variablen d. Metoden Dobbelt er defineret som en helt almindelig metode, men kan ændres til et lambda-udtryk for at gøre koden nemmere at skrive og læse. Når du skriver lambda-udtryk, er operatoren => helt central. På venstre side af operatoren findes argumenterne, og på højre side selve metodekroppen. [argumenter] => [metodekrop]

Du kan også se det som argumenter, der bliver sendt ind i en metode. Så koden fra tidligere kan ændres til følgende:

using System;

namespace Demo
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            Func<int, int> d = (int a) => { return a * 2; };
            Console.WriteLine(d(5));    // 10
        }
    }
}

Allerede her kan du se, hvorfor et lambda-udtryk også kaldes en anonym metode. Vores Plus-metode har ikke længere noget navn, og selve udtrykket skaber blot en reference til et delegate objekt – præcis på samme måde som tidligere. Men styrken ved lambda-udtryk er muligheden for at forkorte koden, og det sker efter tre simple regler. Her er først det komplette udtryk uden forkortelser fra eksemplet tidligere:

Func<int, int> d = (int a) => { return a * 2; };

Første regel er, at du ikke behøver angive typen på argumenterne. Kompileren kan blot kigge på erklæringen af delegate objektet og ud fra dette udledes, at argumentet er et heltal. Så koden kan forkortes til:

Func<int, int> d = (a) => { return a * 2; };

Kompileren kan også ud af delegate erklæringen udlede, at retur-værdien er et heltal, så det behøver du slet ikke nævne. Anden regel er, at du ikke behøver omkranse argumenter i en parentes, hvis der kun er ét argument. Da vores metode kun har ét argument, kan koden forkortes yderligere til:

Func<int, int> d = a => { return a * 2; };

Hvis der er mere end et argument, skal du omkranse med parenteser. Hvis der ikke er nogen argumenter overhovedet, skal du angive tomme parenteser. Tredje regel er, at du ikke behøver angive tuborgklammer, hvis der kun er én instruktion. Yderligere hvis denne instruktion returnerer et resultat, skal du ikke benytte return-kodeordet. Så kan koden forkortes yderligere:

Func<int, int> d = a => a * 2;

Det giver præcis samme resultat som tidligere, men koden er forkortet kraftigt og er nem og hurtig at både læse og skrive (når du lige har luret syntaksen). Så tricket bag at læse et lambda-udtryk er at fjerne forkortelserne fra:

Func<int, int> d = a => a * 2;

til:

Func<int, int> d = (int a) => { return a * 2; };

Mere er der ikke i lambda-udtryk!

Det er blot en nem måde at skrive metoder på – især når metoden tager et enkelt argument og returnerer noget i første instruktion. Men alle metoder kan skrives som lambda-udtryk – du skal bare huske reglerne. Her er et par eksempler, du kan lege lidt med:

using System;

namespace Demo
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            Action d1 = Test1;
            Action<string, int> d2 = Test2;
            Func<bool> d3 = Test3;
            Func<int, bool, int> d4 = Test4;

            // ELLER
            Action d1a = () =>
            {
                Console.WriteLine("I Test1 på linie 1");
                Console.WriteLine("I Test1 på linie 2");
            };

            Action<string, int> d2a = (a, b) => 
                Console.WriteLine($"I Test2 med {a} og {b}");
            Func<bool> d3a = () => true;
            Func<int, bool, int> d4a = (a, b) =>
            {
                if (b)
                    return a;
                else
                    return a - 1;
            };
        }

        public static void Test1()
        {
            Console.WriteLine("I Test1 på linie 1");
            Console.WriteLine("I Test1 på linie 2");
        }

        public static void Test2(string a, int b)
        {
            Console.WriteLine($"I Test2 med {a} og {b}");
        }

        public static bool Test3()
        {
            return true;
        }

        public static int Test4(int a, bool b)
        {
            if (b)
                return a;
            else
                return a - 1;
        }
    }
}

Prøv at skrive koden selv og følg oversættelsen fra almindelige metoder til lambda-udtryk. Den eneste måde at blive god til at kode med lambda-udtryk er at skrive noget kode!

Valgfri parameter

I C# 12 du kan eventuelt tilføje en valgfri parameter til et lambda-udtryk. Dette gøres ved at tilføje en parameter med en standardværdi. Her er et eksempel:

var f = (int a = 1) => Console.WriteLine(a);
f();
f(2);

Discards

Discards i C# er et specielt syntaks, der gør det muligt at ignorere værdier, som du ikke har brug for. Discards er repræsenteret ved underscore-tegnet (_).

I konteksten af lambda udtryk, kan discards være særligt nyttige, når du har brug for at angive en parameter til en funktion, men du ikke har brug for selve parameterens værdi. Det er et signal til enhver, der læser koden, at parameteren bevidst ignoreres.

Her er et simpelt eksempel:

List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
numbers.ForEach(_ => Console.WriteLine("Hello, World!"));

I ovenstående kode, bruger vi List<T>.ForEach metoden, der kræver en Action<T> delegate. Denne delegate tager et parameter, men i vores tilfælde, har vi ikke brug for det, fordi vi bare vil skrive “Hello, World!” til konsollen for hver gang. Så vi bruger et discard (_) til at angive, at vi bevidst ignorerer dette parameter.

Uden brug af discards, skulle vi angive et parameter navn, selvom vi ikke bruger det:

List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
numbers.ForEach(number => Console.WriteLine("Hello, World!"));

I dette tilfælde ville number variablen være ubrugt, hvilket kunne forårsage forvirring for en person, der læser koden.

Brugen af discards gør det klart, at vi bevidst ignorerer parameteren, og gør koden nemmere at forstå.

Brug af var med lambda

Fra C# 10 er det muligt at lade kompileren finde returtypen. Alternativt kan den som noget nyt angives ved deklaration:

Func<int, int, int> f1 = (a, b) => a + b;
var f2 = (int a, int b) => a + b;
var f3 = int (int a, int b) => a + b;

Anden brug af lambda syntaks

Til orientering kan lambda syntaks også benyttes til egenskaber og metoder i typer - eksempelvis

using System;

namespace demo
{

    internal class Program
    {
        private static void Main(string[] args)
        {

            Person p = new Person { Alder = 30 };
            Console.WriteLine(p.BeregnetFødselsår);
            p.Skriv();
            Console.WriteLine(p.ErÆldre(31));
        }
    }

    public class Person {

        // Egenskaber (get/set)
        private int alder;
        public int Alder
        {
            get => alder;
            set => alder = value;
        }

        // Egenskab (get)
        public int BeregnetFødselsår => DateTime.Now.Year - Alder;

        // Metoder
        public void Skriv() => Console.WriteLine("Person er fra " + BeregnetFødselsår);
        public bool ErÆldre(int andenAlder) => alder > andenAlder;

    }
}

Opgaver