Udostępnij za pośrednictwem


Omówienie LINQ

Language-Integrated Query (LINQ) zapewnia możliwości wykonywania zapytań na poziomie języka, a funkcja wyższego rzędu interfejsu API do języków C# i Visual Basic, które umożliwiają pisanie ekspresowego kodu deklaratywnego.

Składnia zapytań na poziomie języka

Jest to składnia zapytań na poziomie języka:

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)

Jest to ten sam przykład użycia interfejsu API IEnumerable<T>:

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))

LINQ jest ekspresyjna

Wyobraź sobie, że masz listę zwierząt domowych, ale chcesz przekonwertować ją na słownik, w którym możesz uzyskać dostęp do zwierzaka bezpośrednio według jego wartości RFID.

Jest to tradycyjny kod imperatywny:

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

Intencją kodu nie jest utworzenie nowego Dictionary<int, Pet> i dodanie do niego za pomocą pętli, lecz przekonwertowanie istniejącej listy na słownik! LINQ zachowuje intencję, podczas gdy kod imperatywny tego nie robi.

Jest to równoważne wyrażenie LINQ:

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

Kod korzystający z LINQ jest cenny, ponieważ wyrównuje szanse między intencją a kodem, co ułatwia zrozumienie dla programisty. Innym bonusem jest zwięzłość kodu. Wyobraź sobie zmniejszenie dużych części bazy kodu o 1/3 zgodnie z powyższymi czynnościami. Słodka transakcja, prawda?

Dostawcy LINQ upraszczają dostęp do danych

W przypadku znacznego fragmentu oprogramowania w środowisku dzikim wszystko koncentruje się na radzeniu sobie z danymi z niektórych źródeł (Bazy danych, JSON, XML itd.). Często wiąże się to z uczeniem nowego interfejsu API dla każdego źródła danych, co może być irytujące. LinQ upraszcza to przez abstrakcja typowych elementów dostępu do danych do składni zapytania, która wygląda tak samo niezależnie od wybranego źródła danych.

Spowoduje to znalezienie wszystkich elementów XML z określoną wartością atrybutu:

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

Pisanie kodu w celu ręcznego przechodzenia przez dokument XML w celu wykonania tego zadania byłoby znacznie trudniejsze.

Interakcja z językiem XML nie jest jedyną rzeczą, którą można zrobić z dostawcami LINQ. Linq to SQL to dość podstawowy Object-Relational mapper (ORM) dla bazy danych na serwerze MSSQL. Biblioteka Json.NET zapewnia wydajne przechodzenie dokumentów JSON za pośrednictwem LINQ. Ponadto, jeśli nie ma biblioteki, która robi to, czego potrzebujesz, możesz również napisać własnego dostawcę LINQ!

Powody używania składni zapytania

Dlaczego warto używać składni zapytań? Jest to pytanie, które często pojawia się. W końcu następujący kod:

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

jest o wiele bardziej zwięzłe niż to:

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

Czy składnia interfejsu API nie jest tylko bardziej zwięzłym sposobem wykonywania składni zapytania?

Nie. Składnia zapytania umożliwia użycie klauzuli let, co pozwala na wprowadzenie i powiązanie zmiennej w zakresie wyrażenia, używając jej w kolejnych częściach wyrażenia. Odtworzenie tego samego kodu przy użyciu tylko składni interfejsu API może być wykonane, ale najprawdopodobniej doprowadzi do kodu, który jest trudny do odczytania.

To rodzi pytanie, czy po prostu powinieneś użyć składni zapytania?

Odpowiedź na to pytanie jest tak, jeśli:

  • Istniejąca baza kodu używa już składni zapytania.
  • Ze względu na złożoność należy ograniczyć zakres zmiennych w zapytaniach.
  • Preferujesz składnię zapytań i nie będzie odwracać Twojej uwagi od bazy kodu.

Odpowiedź na to pytanie jest nie, jeśli...

  • Istniejąca baza kodu używa już składni interfejsu API
  • Nie musisz określać zakresu zmiennych w swoich zapytaniach
  • Wolisz składnię interfejsu API i nie będzie odciągać uwagi od twojej bazy kodu

Podstawy LINQ

Poniższe przykłady to szybki pokaz niektórych podstawowych elementów LINQ. Nie jest to w żaden sposób kompleksowy, ponieważ LINQ zapewnia więcej funkcji niż to, co zostało tutaj zaprezentowane.

Chleb i masło - Where, Selecti 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, (partialSum, nextString) => partialSum + nextString.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(partialSum, nextString) partialSum + nextString.Length)

Spłaszczanie listy list

// 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)

Łączenie między dwoma zestawami (z niestandardowym komparatorem)

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())

Przecięcie między dwoma zbiorami

// 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())

Zamawianie

// 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)

Równość właściwości instancji

Na koniec bardziej zaawansowany przykład: określanie, czy wartości właściwości dwóch wystąpień tego samego typu są równe (pożyczone i zmodyfikowane z tego wpisu StackOverflow):

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

PLINQ lub Parallel LINQ to aparat wykonywania równoległego dla wyrażeń LINQ. Innymi słowy, regularne wyrażenie LINQ może być trywialnie równoległe w dowolnej liczbie wątków. Jest to realizowane za pośrednictwem wywołania AsParallel() poprzedzającego wyrażenie.

Rozważ następujące kwestie:

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)
}

Ten kod podzieli facebookUsers między wątki systemowe, zsumuje polubienia dla każdego wątku równolegle, zsumuje wyniki obliczone przez każdy wątek i przekształci ten wynik w ładny ciąg znaków.

W formie diagramu:

diagramu PLINQ

Zadania obciążające CPU, które można łatwo opisać za pomocą LINQ (innymi słowy, które są czystymi funkcjami i nie mają efektów ubocznych), są doskonałym kandydatem do podlegania zrównolegleniu w PLINQ. W przypadku zadań, które mają efekt uboczny, rozważ użycie biblioteki równoległej zadań .

Więcej zasobów

  • Linqpad, środowisko testowe i silnik zapytań do baz danych dla języka C#/F#/Visual Basic
  • EduLinq, książka elektroniczna do nauki implementacji LINQ-to-objects