Videre med LINQ
LINQ (Language Integrated Query) er en kraftfuld og fleksibel del af C#, som giver dig mulighed for at arbejde med data på en intuitiv og effektiv måde. Efter at have fået et grundlæggende kendskab til LINQ, er det nu tid til at dykke lidt dybere ned i nogle mere avancerede aspekter.
Information til undervisere
Dette kapitel er en viderebygning på LINQ kapitlet og forudsætter at læseren har et grundlæggende kendskab til LINQ. Kapitlet kan bruges som en opfølgning på det første kapitel, eller som en del af en længere undervisningsforløb om LINQ.
IQueryable og IEnumerable i C#
I C# er IQueryable
og IEnumerable
to vigtige interfaces, der anvendes i LINQ for at arbejde med samlinger af data. Du vil høre om dem hele tiden så det er vigtigt at forstå forskellene mellem dem.
Den korte version
Valget mellem IEnumerable
og IQueryable
afhænger af din applikations behov:
- Brug
IEnumerable
, når du arbejder med in-memory samlinger. - Brug
IQueryable
, når du arbejder med komplekse forespørgsler mod en eksempelvis en database, hvor du ønsker at udnytte database serverens kapacitet til at behandle data.
IEnumerable
IEnumerable
er et interface, der er en del af System.Collections-namespace. Det bruges til at iterere over en samling af objekter i hukommelsen. Når du bruger IEnumerable
, udføres alle operationer, såsom filtrering og sortering, i applikationens hukommelse. Det betyder, at hele datasættet skal være indlæst i hukommelsen, før du kan arbejde med det.
- Bruges primært med in-memory samlinger som arrays, lister osv.
- LINQ til objekter anvender
IEnumerable
til at udføre forespørgsler. - Når du anvender LINQ-forespørgsler på
IEnumerable
, returneres der et nytIEnumerable
objekt, der indeholder resultatet. - Hvert kald til en LINQ-metode som
Where
ellerSelect
påIEnumerable
returnerer et nytIEnumerable
objekt, og forespørgslen evalueres “lazy”, hvilket betyder, at den kun evalueres, når du itererer over resultatsættet.
Eksempel på brug af IEnumerable
:
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
// Opretter en liste af personer
List<Person> personer = GetPersonList();
// Filtrer personer ældre end 30 år
IEnumerable<Person> filtreredePersoner = personer.Where(p => p.Alder > 30);
// Udskriver de filtrerede personer
foreach (var person in filtreredePersoner)
{
Console.WriteLine($"{person.Navn} er {person.Alder} år gammel");
}
}
static List<Person> GetPersonList()
{
return new List<Person>
{
new Person { Navn = "Hans", Alder = 25 },
new Person { Navn = "Grethe", Alder = 35 },
new Person { Navn = "Jens", Alder = 42 },
new Person { Navn = "Lise", Alder = 28 }
};
}
}
class Person
{
public string Navn { get; set; }
public int Alder { get; set; }
}
Info
Se eventuelt aloisdg/awesome-linq for yderligere ressourcer.
IQueryable
IQueryable
er en del af System.Linq-namespace og er designet til at arbejde med datakilder, der ligger uden for applikationen, såsom en database. Når du bruger IQueryable
, konstrueres forespørgslen i form af en Expression Tree, som så oversættes til en passende forespørgselsformat for datakilden, for eksempel SQL for en SQL Server database.
- Bruges med datakilder som databases via ORM-frameworks som Entity Framework.
- Tillader udførelse af forespørgsler på serveren, hvilket kan være mere effektivt, især for store datasæt.
- LINQ til Entities (eller lignende teknologier) anvender
IQueryable
til at bygge og udføre forespørgsler. - Når en LINQ-forespørgsel anvendes på
IQueryable
, konstrueres en Expression Tree, som senere oversættes til den specifikke forespørgselssprog for datakilden.
Eksempel på brug af IQueryable
:
// EF DbContext
IQueryable<Person> personer = dbContext.Personer; // Personer er en database tabel
var filtreredePersoner = personer.Where(p => p.Alder > 30);
Lige nu skal du bare kende forskellen mellem IEnumerable
og IQueryable
.
Avancerede metoder
LINQ tilbyder en rig syntaks, der kan håndtere komplekse dataforespørgsler. En dybere forståelse af denne syntaks åbner for mere avancerede forespørgsler.
Join
Join
bruges i LINQ til at kombinere to sekvenser baseret på nøgleværdier og returnere flade par af matchende elementer.
Her er den grundlæggende syntaks:
var result = outerSequence.Join(
innerSequence,
outerKeySelector,
innerKeySelector,
resultSelector);
- outerSequence: Den ydre sekvens, som vi vil sammenkæde.
- innerSequence: Den indre sekvens, der skal sammenkædes med den ydre.
- outerKeySelector: En funktion til at udtrække nøglen fra den ydre sekvens.
- innerKeySelector: En funktion til at udtrække nøglen fra den indre sekvens.
- resultSelector: En funktion, der definerer hvordan de sammenkædede objekter skal projekteres i resultatet.
Her er et eksempel:
var customers = new[]
{
new { CustomerId = 1, Name = "John" },
new { CustomerId = 2, Name = "Jane" }
};
var orders = new[]
{
new { OrderId = 1, CustomerId = 1, Amount = 100 },
new { OrderId = 2, CustomerId = 2, Amount = 150 },
new { OrderId = 3, CustomerId = 1, Amount = 200 }
};
var customerOrders = customers.Join(
orders,
customer => customer.CustomerId,
order => order.CustomerId,
(customer, order) => new
{
CustomerName = customer.Name,
OrderAmount = order.Amount
});
foreach (var item in customerOrders)
{
Console.WriteLine($"Customer: {item.CustomerName}, Order Amount: {item.OrderAmount}");
}
Dette eksempel kombinerer kunder og deres ordrer ved hjælp af CustomerId
og viser resultaterne som flade par af kundernavne og ordrebeløb.
GroupJoin
GroupJoin
bruges i LINQ til at kombinere to sekvenser baseret på nøgleværdier og returnere grupperede resultater. Den giver mulighed for at lave en gruppering af relaterede elementer fra to datakilder.
Her er den grundlæggende syntaks:
var result = outerSequence.GroupJoin(
innerSequence,
outerKeySelector,
innerKeySelector,
resultSelector);
- outerSequence: Den ydre sekvens, som vi vil sammenkæde.
- innerSequence: Den indre sekvens, der skal sammenkædes med den ydre.
- outerKeySelector: En funktion til at udtrække nøglen fra den ydre sekvens.
- innerKeySelector: En funktion til at udtrække nøglen fra den indre sekvens.
- resultSelector: En funktion, der definerer hvordan de sammenkædede objekter skal projekteres i resultatet.
Her er et eksempel:
var customers = new[]
{
new { CustomerId = 1, Name = "John" },
new { CustomerId = 2, Name = "Jane" }
};
var orders = new[]
{
new { OrderId = 1, CustomerId = 1, Amount = 100 },
new { OrderId = 2, CustomerId = 2, Amount = 150 },
new { OrderId = 3, CustomerId = 1, Amount = 200 }
};
var customerOrders = customers.GroupJoin(
orders,
customer => customer.CustomerId,
order => order.CustomerId,
(customer, customerOrders) => new
{
CustomerName = customer.Name,
Orders = customerOrders
});
foreach (var customer in customerOrders)
{
Console.WriteLine($"Customer: {customer.CustomerName}");
foreach (var order in customer.Orders)
{
Console.WriteLine($" Order Amount: {order.Amount}");
}
}
Dette eksempel kombinerer kunder og deres ordrer ved hjælp af CustomerId
og viser resultaterne i en struktureret form.
SelectMany
SelectMany
er en metode i LINQ (Language Integrated Query) i C#, der flader en samling af samlinger ud. Det er ofte brugt, når du har en sekvens af sekvenser, og du vil behandle det som en enkelt sekvens.
Her er et eksempel på, hvordan du kan bruge SelectMany
:
List<List<int>> listOfLists = new List<List<int>>
{
new List<int> { 1, 2, 3 },
new List<int> { 4, 5, 6 },
new List<int> { 7, 8, 9 }
};
IEnumerable<int> flattened = listOfLists.SelectMany(subList => subList);
foreach(int number in flattened)
{
Console.WriteLine(number);
}
I dette eksempel har vi en liste af lister af integers. Vi bruger SelectMany
til at flade listen af lister ud til en enkelt sekvens af integers. Outputtet vil være:
Her er et andet eksempel. Forestil dig, at vi har en Faktura
klasse, som indeholder en liste af FakturaLinje
objekter. Vi vil gerne hente en fladet liste af alle fakturalinjer fra en liste af fakturaer:
using System;
using System.Collections.Generic;
using System.Linq;
namespace App
{
class Program
{
static void Main(string[] args)
{
// Opretter en liste af fakturaer
List<Faktura> fakturaer = new List<Faktura>
{
new Faktura
{
FakturaId = 1,
FakturaLinjer = new List<FakturaLinje>
{
new FakturaLinje { Produkt = "Blyant", Antal = 10, PrisPrEnhed = 2 },
new FakturaLinje { Produkt = "Kuglepen", Antal = 5, PrisPrEnhed = 5 }
}
},
new Faktura
{
FakturaId = 2,
FakturaLinjer = new List<FakturaLinje>
{
new FakturaLinje { Produkt = "Notesbog", Antal = 3, PrisPrEnhed = 15 },
new FakturaLinje { Produkt = "Viskelæder", Antal = 7, PrisPrEnhed = 3 }
}
}
};
// Anvender SelectMany til at flade listen af fakturaer ud til en liste af fakturalinjer
var alleFakturaLinjer = fakturaer.SelectMany(f => f.FakturaLinjer);
foreach (var linje in alleFakturaLinjer)
{
Console.WriteLine($"Produkt: {linje.Produkt}, Antal: {linje.Antal}, Pris pr. enhed: {linje.PrisPrEnhed}");
}
}
}
public class Faktura
{
public int FakturaId { get; set; }
public List<FakturaLinje> FakturaLinjer { get; set; }
}
public class FakturaLinje
{
public string Produkt { get; set; }
public int Antal { get; set; }
public decimal PrisPrEnhed { get; set; }
}
}
I dette eksempel har vi to Faktura
objekter, hver med sin egen liste af FakturaLinje
objekter. Ved at anvende SelectMany
, kombinerer vi disse lister af fakturalinjer til en enkelt liste, hvilket gør det lettere at arbejde med dem i en samlet kontekst. Det er en meget effektiv måde at arbejde med samlinger af samlinger på.
SelectMany
er meget nyttigt, når du arbejder med komplekse datastrukturer, hvor du har brug for at flade en samling af samlinger ud til en enkelt sekvens.
Brugerdefinerede LINQ Udvidelser
At oprette en brugerdefineret LINQ-udvidelse er en kraftfuld måde at tilføje specifik funktionalitet til LINQ-forespørgsler, der muligvis ikke er dækket af de indbyggede metoder. En brugerdefineret LINQ-udvidelse implementeres som en extensionmetode.
Info
Læs om Extension metoder inden du fortsætter.
Eksempel: Even og Odd
At tilføje Even
og Odd
som brugerdefinerede LINQ-udvidelser kan være en god idé, da det giver en intuitiv og lettilgængelig måde at filtrere samlinger af tal baseret på, om de er lige eller ulige.
Først defineres udvidelsesmetoderne Even
og Odd
i en statisk klasse:
public static class LinqExtensions
{
public static IEnumerable<int> Even(this IEnumerable<int> source)
{
return source.Where(n => n % 2 == 0);
}
public static IEnumerable<int> Odd(this IEnumerable<int> source)
{
return source.Where(n => n % 2 != 0);
}
}
I disse metoder anvender vi Where
-metoden fra LINQ. For Even
-metoden tjekker vi, om et tal n
er lige ved at se, om resten er 0, når n
divideres med 2. For Odd
-metoden gør vi det modsatte.
List<int> tal = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var ligeTal = tal.Even();
var uligeTal = tal.Odd();
Console.WriteLine("Lige tal:");
foreach (var n in ligeTal)
{
Console.WriteLine(n);
}
Console.WriteLine("\nUlige tal:");
foreach (var n in uligeTal)
{
Console.WriteLine(n);
}
Outputtet for denne kode vil være en liste af lige tal efterfulgt af en liste af ulige tal fra den oprindelige liste tal
:
Eksempel: TakeEvery
At tilføje en brugerdefineret LINQ-udvidelse, som TakeEvery
, der tillader at tage hvert andet, tredje, fjerde element osv., kan være særligt nyttig, når du arbejder med store datasæt og kun er interesseret i en delmængde af dataene baseret på en bestemt intervalsekvens. Her er et eksempel på, hvordan du kan implementere en sådan udvidelse:
Først definerer vi TakeEvery
-metoden i en statisk klasse:
public static class LinqExtensions
{
public static IEnumerable<T> TakeEvery<T>(this IEnumerable<T> source, int interval)
{
if (interval <= 0)
{
throw new ArgumentException("Interval skal være større end 0", nameof(interval));
}
int index = 0;
foreach (var item in source)
{
if (index++ % interval == 0)
{
yield return item;
}
}
}
}
I denne metode bruger vi yield return
til at returnere hvert interval
‘te element fra den givne source
samling. Hvis interval
er sat til 2, vil metoden returnere hvert andet element, og hvis det er sat til 3, hvert tredje element, og så videre.
Her er et eksempel på, hvordan du kan bruge TakeEvery
-metoden:
List<int> tal = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var hvertAndetTal = tal.TakeEvery(2);
var hvertTredjeTal = tal.TakeEvery(3);
Console.WriteLine("Hvert andet tal:");
foreach (var n in hvertAndetTal)
{
Console.WriteLine(n);
}
Console.WriteLine("\nHvert tredje tal:");
foreach (var n in hvertTredjeTal)
{
Console.WriteLine(n);
}
Outputtet for denne kode vil være en liste af hvert andet tal efterfulgt af en liste af hvert tredje tal fra den oprindelige liste tal
.
LINQ og performance
LINQ (Language Integrated Query) er en kraftfuld feature i C# og .NET, der tillader udviklere at skrive komplekse data queries direkte i C#. Men som med enhver anden teknologi, kan brugen af LINQ have potentielle performanceproblemer, hvis det ikke bruges korrekt. Her er nogle af de mest almindelige områder, hvor LINQ kan påvirke performance:
-
Unødvendig gentagelse af queries: LINQ queries er “lazy” i det, de kun udføres, når resultatet er nødvendigt. Dette betyder, at hver gang du refererer til en LINQ query, bliver den udført igen. Hvis du bruger det samme resultat flere gange, skal du gemme resultatet i en variabel og genbruge den i stedet for at udføre den samme query flere gange. Du kan også bruge
.ToList()
eller.ToArray()
for at tvinge udførelsen af en query med det samme. -
For tidlig udførelse af queries: På den anden side kan det at tvinge en query til at blive udført for tidligt (for eksempel ved at bruge
.ToList()
eller.ToArray()
) også være et problem, hvis du kun har brug for en del af dataene. Hvis du kun har brug for det første element, der opfylder en betingelse, kan det være mere effektivt at bruge.FirstOrDefault()
i stedet for at udføre hele queryen og derefter hente det første element. -
Brug af ineffektive operators: Nogle LINQ operators kan være meget ineffektive, hvis de ikke bruges korrekt. For eksempel,
.Contains()
,.Any()
,.All()
,.First()
,.Last()
kan alle potentielt gennemgå hele samlingen. Hvis du arbejder med store samlinger, skal du være opmærksom på, hvilke operators du bruger. -
Brug af LINQ i hot paths: LINQ er generelt langsommere end traditionelle loops, fordi det medfører ekstra overhead for at tilbyde sin fleksible syntaks og features. Hvis du har en del af din kode, der bliver kaldt meget ofte (en “hot path”), kan det være mere effektivt at bruge traditionelle loops i stedet for LINQ.
-
Brug af LINQ med Entity Framework eller andre ORMs: Når du bruger LINQ med Entity Framework eller andre Object-Relational Mappers (ORMs), skal du være opmærksom på, at dine LINQ queries bliver oversat til SQL. Nogle LINQ queries kan resultere i meget ineffektiv SQL, hvilket kan medføre performanceproblemer. Det er altid en god ide at teste dine LINQ queries og overvåge den genererede SQL for at sikre, at den er effektiv.
Overordnet set, selvom LINQ kan være meget kraftfuldt og bekvemt, er det vigtigt at være opmærksom på, hvordan det kan påvirke performance. Som med enhver anden teknologi, er den bedste måde at sikre god performance at profilere din kode, identificere flaskehalse og
Opgaver
Eksterne biblioteker
Der findes en del 3. parts biblioteker der udvider LINQ - se eksempelvis