LINQ — przegląd

Language-Integrated Query (LINQ) udostępnia funkcje zapytań na poziomie języka oraz interfejs API funkcji wyższego poziomu do języka 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 przy użyciu interfejsu IEnumerable<T> API:

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 ekspresyjny

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

Jest to tradycyjny kod imperatywne:

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> kodu i dodanie go za pomocą pętli. Ma to na celu przekonwertowanie istniejącej listy na słownik. LINQ zachowuje intencję, podczas gdy kod imperatywnego nie.

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ż równoczesne pole działania między intencją a kodem podczas rozumowania jako programista. Innym bonusem jest zwięzłość kodu. Imagine zmniejszenie dużej części bazy kodu o 1/3, jak pokazano powyżej. Słodka transakcja, prawda?

Dostawcy LINQ upraszczają dostęp do danych

W przypadku znacznego fragmentu oprogramowania w środowisku naturalnym wszystko obraca się wokół radzenia sobie z danymi ze źródła (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, abstrakując wspólne elementy dostępu do danych w składni zapytania, która wygląda tak samo niezależnie od wybranego źródła danych.

Spowoduje to znalezienie wszystkich elementów XML o określonej 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 kodem XML nie jest jedyną rzeczą, którą można zrobić z dostawcami LINQ. Linq to SQL to dość gołe kości Object-Relational Mapper (ORM) dla bazy danych serwera 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 korzystania ze składni zapytania

Dlaczego warto używać składni zapytania? 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 , która umożliwia wprowadzenie i powiązanie zmiennej w zakresie wyrażenia przy użyciu jej w kolejnych fragmentach wyrażenia. Odtworzenie tego samego kodu z tylko składnią interfejsu API można wykonać, ale najprawdopodobniej doprowadzi do kodu, który jest trudny do odczytania.

Więc to błaga pytanie, czy po prostu używasz składni zapytania?

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

  • Istniejąca baza kodu używa już składni zapytania.
  • Musisz ograniczyć zakres zmiennych w zapytaniach ze względu na złożoność.
  • Wolisz składnię zapytania i nie będzie rozpraszać uwagi od bazy kodu.

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

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

Podstawowe LINQ

Aby uzyskać naprawdę kompleksową listę przykładów LINQ, odwiedź stronę 101 LINQ Samples (Przykłady LINQ 101).

Poniższe przykłady to szybka demonstracja niektórych podstawowych elementów LINQ. Nie jest to w żaden sposób kompleksowe, 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, (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)

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 zestawami

// 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 wystąpienia

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 zrównane w dowolnej liczbie wątków. Jest to realizowane za pośrednictwem wywołania AsParallel() poprzedzającego wyrażenie.

Rozważ następujące źródła:

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 będzie partycjonować facebookUsers między wątkami systemowymi w razie potrzeby, sumując sumę polubień dla każdego wątku równolegle, sumując wyniki obliczone przez każdy wątek i projekt, które powodują miły ciąg.

W formularzu diagramu:

PLINQ diagram

Równoległe zadania związane z procesorem CPU, które można łatwo wyrazić za pośrednictwem LINQ (innymi słowy, są czystymi funkcjami i nie mają efektów ubocznych) są doskonałym kandydatem do PLINQ. W przypadku zadań, które mają efekt uboczny, rozważ użycie biblioteki równoległej zadań.

Więcej zasobów