A LINQ áttekintése

A Language-Integrated Query (LINQ) nyelvszintű lekérdezési képességeket, valamint egy magasabb rendű függvény API-t biztosít a C# és a Visual Basic számára, amelyekkel kifejező deklaratív kódot írhat.

Nyelvszintű lekérdezési szintaxis

Ez a nyelvszintű lekérdezési szintaxis:

var linqExperts = from p in programmers
                  where p.IsNewToLINQ
                  select new LINQExpert(p);
Dim linqExperts = From p in programmers
                  Where p.IsNewToLINQ
                  Select New LINQExpert(p)

Ez ugyanaz a példa az IEnumerable<T> API használatával:

var linqExperts = programmers.Where(p => p.IsNewToLINQ)
                             .Select(p => new LINQExpert(p));
Dim linqExperts = programmers.Where(Function(p) p.IsNewToLINQ).
                             Select(Function(p) New LINQExpert(p))

A LINQ kifejező

Imagine van egy háziállatok listája, de szeretné átalakítani egy szótárba, ahol közvetlenül elérheti a kisállatot az RFID értéke alapján.

Ez a hagyományos imperatív kód:

var petLookup = new Dictionary<int, Pet>();

foreach (var pet in pets)
{
    petLookup.Add(pet.RFID, pet);
}
Dim petLookup = New Dictionary(Of Integer, Pet)()

For Each pet in pets
    petLookup.Add(pet.RFID, pet)
Next

A kód mögött nem az a szándék, hogy újat Dictionary<int, Pet> hozzon létre, és hurkon keresztül adja hozzá, hanem egy meglévő listát konvertál szótárba! A LINQ megőrzi a szándékot, míg az imperatív kód nem.

Ez az egyenértékű LINQ-kifejezés:

var petLookup = pets.ToDictionary(pet => pet.RFID);
Dim petLookup = pets.ToDictionary(Function(pet) pet.RFID)

A LINQ-t használó kód azért hasznos, mert a programozóként végzett érvelés során kiegyenlíti a játékteret a szándék és a kód között. Egy másik bónusz a kód rövidsége. Imagine a kódbázis nagy részeinek 1/3-tal való csökkentése a fentieknek megfelelően. Édes üzlet, ugye?

A LINQ-szolgáltatók leegyszerűsítik az adathozzáférést

A vadonban jelentős mennyiségű szoftver esetében minden az egyes forrásból (adatbázisokból, JSON-ból, XML-ből stb.) származó adatok kezelésével foglalkozik. Ez gyakran egy új API elsajátítása minden adatforráshoz, ami bosszantó lehet. A LINQ leegyszerűsíti ezt, ha az adathozzáférés gyakori elemeit egy olyan lekérdezési szintaxisba absztraháljuk, amely ugyanúgy néz ki, függetlenül attól, hogy melyik adatforrást választja.

Ez megkeresi az adott attribútumértékkel rendelkező összes XML-elemet:

public static IEnumerable<XElement> FindAllElementsWithAttribute(XElement documentRoot, string elementName,
                                           string attributeName, string value)
{
    return from el in documentRoot.Elements(elementName)
           where (string)el.Element(attributeName) == value
           select el;
}
Public Shared Function FindAllElementsWithAttribute(documentRoot As XElement, elementName As String,
                                           attributeName As String, value As String) As IEnumerable(Of XElement)
    Return From el In documentRoot.Elements(elementName)
           Where el.Element(attributeName).ToString() = value
           Select el
End Function

A feladat elvégzéséhez az XML-dokumentum manuális bejárására vonatkozó kód írása sokkal nagyobb kihívást jelent.

A LINQ-szolgáltatókkal nem csak az XML-műveletek használhatók. A Linq to SQL meglehetősen csupasz Object-Relational Mapper (ORM) egy MSSQL Server-adatbázishoz. A Json.NET kódtár hatékony JSON-dokumentumbejárást biztosít a LINQ-n keresztül. Továbbá, ha nincs olyan kódtár, amely azt teszi, amire szüksége van, saját LINQ-szolgáltatót is írhat!

A lekérdezési szintaxis használatának indokai

Miért érdemes lekérdezési szintaxist használni? Ez egy olyan kérdés, amely gyakran felmerül. Végül is a következő kód:

var filteredItems = myItems.Where(item => item.Foo);
Dim filteredItems = myItems.Where(Function(item) item.Foo)

sokkal tömörebb, mint ez:

var filteredItems = from item in myItems
                    where item.Foo
                    select item;
Dim filteredItems = From item In myItems
                    Where item.Foo
                    Select item

Az API szintaxisa nem csak tömörebb módja a lekérdezési szintaxisnak?

Nem. A lekérdezési szintaxis lehetővé teszi a let záradék használatát, amely lehetővé teszi egy változó bevezetését és kötését a kifejezés hatókörén belül, a kifejezés későbbi részeiben használva. Ugyanazt a kódot csak az API-szintaxissal reprodukálni lehet, de valószínűleg nehezen olvasható kódhoz vezet.

Tehát ez felveti a kérdést, csak a lekérdezési szintaxist kell használnia?

A válasz erre a kérdésre igen , ha:

  • A meglévő kódbázis már használja a lekérdezési szintaxist.
  • A változókat összetettség miatt a lekérdezéseken belül kell hatókörbe helyeznie.
  • A lekérdezési szintaxist részesíti előnyben, és ez nem vonja el a figyelmet a kódbázistól.

A válasz erre a kérdésre nem , ha...

  • A meglévő kódbázis már használja az API-szintaxist
  • A lekérdezéseken belül nincs szükség változók hatókörének meghatározására
  • Az API-szintaxist részesíti előnyben, és ez nem vonja el a figyelmet a kódbázistól

Alapvető LINQ

A LINQ-minták igazán átfogó listájáért látogasson el a 101 LINQ-mintára.

Az alábbi példák a LINQ néhány alapvető elemének gyors bemutatását szemléltetik. Ez semmiképpen sem átfogó, mivel a LINQ több funkciót biztosít, mint amit itt bemutatunk.

A kenyér és a vaj - Where, Select, és Aggregate

// Filtering a list.
var germanShepherds = dogs.Where(dog => dog.Breed == DogBreed.GermanShepherd);

// Using the query syntax.
var queryGermanShepherds = from dog in dogs
                          where dog.Breed == DogBreed.GermanShepherd
                          select dog;

// Mapping a list from type A to type B.
var cats = dogs.Select(dog => dog.TurnIntoACat());

// Using the query syntax.
var queryCats = from dog in dogs
                select dog.TurnIntoACat();

// Summing the lengths of a set of strings.
int seed = 0;
int sumOfStrings = strings.Aggregate(seed, (s1, s2) => s1.Length + s2.Length);
' Filtering a list.
Dim germanShepherds = dogs.Where(Function(dog) dog.Breed = DogBreed.GermanShepherd)

' Using the query syntax.
Dim queryGermanShepherds = From dog In dogs
                          Where dog.Breed = DogBreed.GermanShepherd
                          Select dog

' Mapping a list from type A to type B.
Dim cats = dogs.Select(Function(dog) dog.TurnIntoACat())

' Using the query syntax.
Dim queryCats = From dog In dogs
                Select dog.TurnIntoACat()

' Summing the lengths of a set of strings.
Dim seed As Integer = 0
Dim sumOfStrings As Integer = strings.Aggregate(seed, Function(s1, s2) s1.Length + s2.Length)

Listalista egybesimítása

// Transforms the list of kennels into a list of all their dogs.
var allDogsFromKennels = kennels.SelectMany(kennel => kennel.Dogs);
' Transforms the list of kennels into a list of all their dogs.
Dim allDogsFromKennels = kennels.SelectMany(Function(kennel) kennel.Dogs)

Egyesítés két halmaz között (egyéni összehasonlítóval)

public class DogHairLengthComparer : IEqualityComparer<Dog>
{
    public bool Equals(Dog a, Dog b)
    {
        if (a == null && b == null)
        {
            return true;
        }
        else if ((a == null && b != null) ||
                 (a != null && b == null))
        {
            return false;
        }
        else
        {
            return a.HairLengthType == b.HairLengthType;
        }
    }

    public int GetHashCode(Dog d)
    {
        // Default hashcode is enough here, as these are simple objects.
        return d.GetHashCode();
    }
}
...

// Gets all the short-haired dogs between two different kennels.
var allShortHairedDogs = kennel1.Dogs.Union(kennel2.Dogs, new DogHairLengthComparer());

Public Class DogHairLengthComparer
  Inherits IEqualityComparer(Of Dog)

  Public Function Equals(a As Dog,b As Dog) As Boolean
      If a Is Nothing AndAlso b Is Nothing Then
          Return True
      ElseIf (a Is Nothing AndAlso b IsNot Nothing) OrElse (a IsNot Nothing AndAlso b Is Nothing) Then
          Return False
      Else
          Return a.HairLengthType = b.HairLengthType
      End If
  End Function

  Public Function GetHashCode(d As Dog) As Integer
      ' Default hashcode is enough here, as these are simple objects.
      Return d.GetHashCode()
  End Function
End Class

...

' Gets all the short-haired dogs between two different kennels.
Dim allShortHairedDogs = kennel1.Dogs.Union(kennel2.Dogs, New DogHairLengthComparer())

Metszet két halmaz között

// Gets the volunteers who spend share time with two humane societies.
var volunteers = humaneSociety1.Volunteers.Intersect(humaneSociety2.Volunteers,
                                                     new VolunteerTimeComparer());
' Gets the volunteers who spend share time with two humane societies.
Dim volunteers = humaneSociety1.Volunteers.Intersect(humaneSociety2.Volunteers,
                                                     New VolunteerTimeComparer())

Rendezés

// Get driving directions, ordering by if it's toll-free before estimated driving time.
var results = DirectionsProcessor.GetDirections(start, end)
              .OrderBy(direction => direction.HasNoTolls)
              .ThenBy(direction => direction.EstimatedTime);
' Get driving directions, ordering by if it's toll-free before estimated driving time.
Dim results = DirectionsProcessor.GetDirections(start, end).
                OrderBy(Function(direction) direction.HasNoTolls).
                ThenBy(Function(direction) direction.EstimatedTime)

Példánytulajdonságok egyenlősége

Végül egy fejlettebb minta: annak meghatározása, hogy két azonos típusú példány tulajdonságainak értékei egyenlőek-e ( ebből a StackOverflow-bejegyzésből kölcsönzött és módosított):

public static bool PublicInstancePropertiesEqual<T>(this T self, T to, params string[] ignore) where T : class
{
    if (self == null || to == null)
    {
        return self == to;
    }

    // Selects the properties which have unequal values into a sequence of those properties.
    var unequalProperties = from property in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)
                            where !ignore.Contains(property.Name)
                            let selfValue = property.GetValue(self, null)
                            let toValue = property.GetValue(to, null)
                            where !Equals(selfValue, toValue)
                            select property;
    return !unequalProperties.Any();
}
<System.Runtime.CompilerServices.Extension()>
Public Function PublicInstancePropertiesEqual(Of T As Class)(self As T, [to] As T, ParamArray ignore As String()) As Boolean
    If self Is Nothing OrElse [to] Is Nothing Then
        Return self Is [to]
    End If

    ' Selects the properties which have unequal values into a sequence of those properties.
    Dim unequalProperties = From [property] In GetType(T).GetProperties(BindingFlags.Public Or BindingFlags.Instance)
                            Where Not ignore.Contains([property].Name)
                            Let selfValue = [property].GetValue(self, Nothing)
                            Let toValue = [property].GetValue([to], Nothing)
                            Where Not Equals(selfValue, toValue) Select [property]
    Return Not unequalProperties.Any()
End Function

PLINQ

A PLINQ vagy parallel LINQ a LINQ-kifejezések párhuzamos végrehajtási motorja. Más szóval egy reguláris LINQ-kifejezés triviálisan párhuzamosítható tetszőleges számú szálon. Ez a kifejezés előtti hívással AsParallel() érhető el.

A következőket ajánljuk figyelmébe:

public static string GetAllFacebookUserLikesMessage(IEnumerable<FacebookUser> facebookUsers)
{
    var seed = default(UInt64);

    Func<UInt64, UInt64, UInt64> threadAccumulator = (t1, t2) => t1 + t2;
    Func<UInt64, UInt64, UInt64> threadResultAccumulator = (t1, t2) => t1 + t2;
    Func<Uint64, string> resultSelector = total => $"Facebook has {total} likes!";

    return facebookUsers.AsParallel()
                        .Aggregate(seed, threadAccumulator, threadResultAccumulator, resultSelector);
}
Public Shared GetAllFacebookUserLikesMessage(facebookUsers As IEnumerable(Of FacebookUser)) As String
{
    Dim seed As UInt64 = 0

    Dim threadAccumulator As Func(Of UInt64, UInt64, UInt64) = Function(t1, t2) t1 + t2
    Dim threadResultAccumulator As Func(Of UInt64, UInt64, UInt64) = Function(t1, t2) t1 + t2
    Dim resultSelector As Func(Of Uint64, string) = Function(total) $"Facebook has {total} likes!"

    Return facebookUsers.AsParallel().
                        Aggregate(seed, threadAccumulator, threadResultAccumulator, resultSelector)
}

Ez a kód szükség szerint particionálja facebookUsers a rendszerszálakat, párhuzamosan összegzi az egyes szálak összes tetszését, összegzi az egyes szálak által kiszámított eredményeket, és egy szép sztringet eredményező projektet hoz létre.

Diagram formájában:

PLINQ diagram

A LINQ-n keresztül könnyen kifejezhető párhuzamos cpu-kötésű feladatok (más szóval tiszta funkciók, és nincsenek mellékhatásaik) kiválóan alkalmasak a PLINQ-ra. A mellékhatással rendelkező feladatok esetében fontolja meg a párhuzamos feladattár használatát.

További erőforrások

  • 101 LINQ-minta
  • Linqpad, egy játszótéri környezet és adatbázis-lekérdezési motor C#/F#/Visual Basic
  • EduLinq, e-könyv a LINQ-objektumok implementálásának megismeréséhez