Gå til indholdet

Tuples

C# understøtter som mange andre programmeringssprog en speciel datastruktur kaldet en tuple. Hvis du hører ordet ”tuple” udtalt, vil du sikkert høre trykket lagt på både TUple og tuPLE, men det helt korrekte er TUple (tuh·pl) .

Information til undervisere

Som minumum bør kursisterne vide, at en tuple er en datastruktur, der kan indeholde flere værdier af forskellige typer, og at det er en form for autogeneret klasse eller struktur. Den benyttes bla som argument eller returværdi i en metode.

Denne lidt specielle datastruktur giver en nem og effektiv mulighed for at opbevare værdier af forskellige typer, og du kan se det lidt som at slippe for at definere en klasse eller en struktur. De findes nemlig i to former – System.Tuple<> og System.ValueTuple<>. Førstnævnte er en referencebaseret type, og har været at finde i C# i flere versioner. Sidstnævnte er en værdibaseret type og blev tilføjet C# i version 7.

System.Tuple

Som eksempel på brug af den referencebaserede Tuple-klasse må du forestille dig en type til opbevaring af data relateret til en person – eksempelvis:

class Person 
{
    public string Navn { get; set; }
    public int Alder { get; set; }
    public bool ErDansk { get; set; }
}

Klassen er, som du kan se, en almindelig DTO (Data Transfer Object), og indeholder udelukkende data. Den kan ligge til grund for opbevaring af data relateret til en enkelt person:

Person p = new Person() 
{ 
    Navn = "Mathias", Alder = 14, ErDansk = true 
};

Eller en samling af personer:

List<Person> personer = new List<Person> {
    new Person() { Navn = "Mikkel", Alder = 17, ErDansk = true },
    new Person() { Navn = "Mathias", Alder = 14, ErDansk = true },
};

Eller som argument eller returværdi:

List<Person> personer = new List<Person> {
    new Person() { Navn = "Mikkel", Alder= 17, ErDansk = true },
    new Person() { Navn = "Mathias", Alder= 14, ErDansk = true },
};

Console.WriteLine(FindÆldstePerson(personer).Navn); // Mikkel

Person FindÆldstePerson(List<Person> personer)
{
    return personer.OrderBy(p => p.Alder).LastOrDefault();
}

Alle eksempler er helt korrekte og logiske, men kræver, at du opretter klassen Person. Ideen med tuples er, at samme kode kan skrives generisk uden definition af en klasse. Der kan angives op til otte forskellige typer i en tuple, og dermed kan vores person-klasse erstattes med følgende:

Tuple<string, int, bool> p;

Nu kan variablen p indeholde referencen til en (anonym) datastruktur bestående af en string, int og en bool:

Tuple<string, int, bool> p1;
p1 = new Tuple<string, int, bool>("Mathias", 14, true);
// eller
Tuple<string, int, bool> p2 = 
    new Tuple<string, int, bool>("Mikkel", 17, true);

Da det jo ikke er navngivne egenskaber, må værdier hentes ud på en anden måde:

Tuple<string, int, bool> p =
    new Tuple<string, int, bool>("Mikkel", 17, true);
Console.WriteLine(p.Item1); // Mikkel
Console.WriteLine(p.Item2); // 17
Console.WriteLine(p.Item3); // true

Egenskaber kan nu tilgås som itemX, hvor X er den rækkefølge, de er erklæret.

Førnævnte metode, der finder den ældste person, kan nu omskrives til:

List<Tuple<string, int, bool>> personer = 
  new List<Tuple<string, int, bool>>() {
    new Tuple<string, int, bool>("Mikkel", 17, true),
    new Tuple<string, int, bool>("Mathias", 14, true)
    };

Console.WriteLine(FindÆldstePerson(personer).Item1); // Mikkel

Tuple<string, int, bool> FindÆldstePerson
    (List<Tuple<string, int, bool>> personer)
{
    return personer.OrderBy(p => p.Item2).LastOrDefault();
}

Og den fungerer præcis på samme måde. Forskellen er, at du ikke behøver erklære og benytte en decideret klasse. Koden er blevet generisk og kan benyttes på eksempelvis både en person, hund eller maskine.

System.ValueTuple

Endnu mere smart er den værdibaserede type (bemærk – ikke referencebaseret) ValueTuple. Den kan du sammenligne med en struktur, der består af værdier. Førnævnte Person klasse kunne omskrives til:

struct Person
{
    public string Navn { get; set; }
    public int Alder { get; set; }
    public bool ErDansk { get; set; }
}

Det betyder, at værdier er placeret et andet sted i hukommelsen (stack) med (tit) tilhørende forbedring i performance, og eksempelvis kopiering af data resulterer i kopiering af værdier og ikke referencer:

Person p1 = new Person() 
  { Navn = "Mathias", Alder = 14, ErDansk = true };
Person p2 = p1;
p1.Navn = "Mikkel";
Console.WriteLine(p1.Navn); // Mikkel
Console.WriteLine(p2.Navn); // Mathias

Havde Person været en klasse ville p1 og p2 indeholde samme reference, og dermed udskrive 2 gange ”Mikkel”. Hvis du ønsker en værdibaseret tuple kan du benytte System.Value-Tuple<> med samme syntaks som System.Tuple<>:

ValueTuple<string, int, bool> p1 
    = new ValueTuple<string, int, bool>("Mikkel", 17, true);
ValueTuple<string, int, bool> p2
    = new ValueTuple<string, int, bool>("Mathias", 14, true);
Console.WriteLine(p1.Item1);    // Mikkel
Console.WriteLine(p2.Item1);    // Mathias

List<ValueTuple<string, int, bool>> personer = 
    new List<(string, int, bool)> 
    {
        p1, p2
    };

Du kan sågar direkte sammenligne værdier:

ValueTuple<string, int, bool> p1
    = new ValueTuple<string, int, bool>("Mikkel", 17, true);
ValueTuple<string, int, bool> p2
    = new ValueTuple<string, int, bool>("Mathias", 14, true);
ValueTuple<string, int, bool> p3
    = new ValueTuple<string, int, bool>("Mikkel", 17, true);

Console.WriteLine(p1 == p2);  // false
Console.WriteLine(p1 == p3);  // true

Men ValueTuple indeholder også en del syntakssukker. For det første kan værdier tildeles på en smart måde ved hjælp af parenteser:

ValueTuple<string, int, bool> p1
    = new ValueTuple<string, int, bool>("Mikkel", 17, true);
ValueTuple<string, int, bool> p2 = ("Mikkel", 17, true);
var p3  = ("Mikkel", 17, true);

Console.WriteLine(p1.Item1);    // Mikkel
Console.WriteLine(p2.Item1);    // Mikkel
Console.WriteLine(p3.Item1);    // Mikkel 

Alle tre tildelinger giver det samme, og især

var p3  = ("Mikkel", 17, true);

er jo ret fiks, fordi kompileren er kvik nok til at udlede typer.

Yderligere kan egenskaber navngives således, at du ikke behøver tilgå egenskaber gennem ItemX:

var p1 = (Navn: "Mathias", Alder: 14, ErDansk: true);

Console.WriteLine(p1.Navn);    // Mathias
Console.WriteLine(p1.Alder);   // 14
Console.WriteLine(p1.ErDansk); // true

Console.WriteLine(p1.Item1);   // Mathias
Console.WriteLine(p1.Item2);   // 14
Console.WriteLine(p1.Item3);   // true

Som du kan se, kan egenskaber nu både tilgås gennem et logisk navn, samt gennem ItemX.

Til slut er her førnævnte metode FindÆldstePerson – nu med Value-Tuple:

List<(string, int, bool)> personer = new List<(string, int, bool)>()
{
    ("Mikkel", 17, true),
    ("Mathias", 14, true)
};

Console.WriteLine(FindÆldstePerson(personer).Navn);   // Mikkel
Console.WriteLine(FindÆldstePerson(personer).Item1);  // Mikkel

(string Navn, int Alder, bool ErDansk) FindÆldstePerson
    (List<(string, int, bool)> personer)
{
    return personer.OrderBy(p => p.Item2).LastOrDefault();
}

Bemærk, at en tuple, der benyttes som returværdi, navngives, som var det argumenter til en metode.

Info

Du behøver ikke benytte tuples, men det kan gøre koden både generisk og nem at vedligeholde

Nedbrydning til variabler

En anden feature som tit er relateret til tuples er nedbrydning (deconstruction). Det gør det muligt nemt at nedbryde en tuple til enkelte variabler. Se eksempelvis denne tuple:

var a = ("Mikkel", 17);
Console.WriteLine(a.Item1); // Mikkel
Console.WriteLine(a.Item2); // 17

Den kan nedbrydes manuelt til variabler som følger:

string navn = a.Item1;
int alder = a.Item2;
Console.WriteLine(navn);    // Mikkel
Console.WriteLine(alder);   // 17

Men koden kan simplificeres en hel del – her på en enkelt linje:

(string navn, int alder) = a;
Console.WriteLine(navn);    // Mikkel
Console.WriteLine(alder);   // 17

og endnu nemmere:

(var navn, var alder) = a;
Console.WriteLine(navn);    // Mikkel
Console.WriteLine(alder);   // 17

Men nedbrydning er ikke bare relateret til tuples. Samme funktionalitet kan tilføjes dine egne klasser ved hjælp af en metode kaldet Deconstruct. Her er et eksempel med klassen Person:

class Person
{
    public string Navn { get; set; }
    public int Alder { get; set; }
    public bool ErDansk { get; set; }

    public void Deconstruct(out string navn, 
        out int alder, out bool erDansk)
    {
        navn = Navn;
        alder = Alder;
        erDansk = ErDansk;
    }
}

Bemærk, at metoden Deconstruct benytter såkaldte out-parametre, som blandt andet benyttes i avancerede metoder til at tildele værdier til variabler erklæret i den kaldende metode. Men her benyttes out- parametrene til automatisk nedbrydning:

Person p = new Person
{
    Navn = "Mathias",
    Alder = 14,
    ErDansk = true
};
Console.WriteLine(p.Navn);      // Mathias
Console.WriteLine(p.Alder);     // 14
Console.WriteLine(p.ErDansk);   // true

// Nedbrydning
(var navn, var alder, var erDansk) = p;
Console.WriteLine(navn);        // Mathias
Console.WriteLine(alder);       // 14
Console.WriteLine(erDansk);     // true 

Bemærk den nemme nedbrydning til enkelte variabler.

Metoden Deconstruct kan eventuelt overloades således, at nedbryd-ning kan ske med færre eller flere argumenter:

class Person
{
    public string Navn { get; set; }
    public int Alder { get; set; }
    public bool ErDansk { get; set; }

    public void Deconstruct(out string navn, out int alder, 
        out bool erDansk)
    {
        navn = Navn;
        alder = Alder;
        erDansk = ErDansk;
    }

    public void Deconstruct(out string navn, out int alder)
    {
        navn = Navn;
        alder = Alder;                
    }
}

Nu kan klassen nedbrydes til både en string, int og bool samt en string og en int.

Opgaver