Обзор LINQ
Language-Integrated query (LINQ) предоставляет возможности запросов на уровне языка, а также функцию более высокого порядка API в C# и Visual Basic, которая позволяет писать экспрессивный декларативный код.
Синтаксис запросов на уровне языка
Это синтаксис запросов на уровне языка:
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)
Это тот же пример с помощью 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 выразителен
Представьте себе, что у вас есть список домашних животных, но хотите преобразовать его в словарь, где вы можете получить доступ к домашним животным непосредственно по его RFID
значению.
Это традиционный императивный код:
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
Цель кода не в том, чтобы создать новый Dictionary<int, Pet>
и добавить к нему через цикл, а в том, чтобы преобразовать существующий список в словарь! LINQ сохраняет намерение, тогда как императивный код этого не делает.
Это эквивалентное выражение LINQ:
var petLookup = pets.ToDictionary(pet => pet.RFID);
Dim petLookup = pets.ToDictionary(Function(pet) pet.RFID)
Код, использующий LINQ, ценен, так как он уравнивает позиции между намерением и кодом при размышлении с точки зрения программиста. Еще один бонус — это краткость кода. Представьте себе сокращение больших частей базы кода на 1/3, как показано выше. Сладкая сделка, да?
Поставщики LINQ упрощают доступ к данным
Для значительного фрагмента программного обеспечения в дикой природе все вращается вокруг работы с данными из некоторых источников (базы данных, JSON, XML и т. д.). Часто это включает в себя изучение нового API для каждого источника данных, что может раздражать. LINQ упрощает это путем абстрагирования общих элементов доступа к данным в синтаксис запроса, который выглядит одинаково независимо от выбранного источника данных.
Это находит все XML-элементы с определенным значением атрибута:
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
Написание кода для ручного обхода XML-документа для выполнения этой задачи будет гораздо более сложной задачей.
Взаимодействие с XML - это не единственная вещь, которую можно сделать с поставщиками LINQ. Linq to SQL — это довольно голые кости Object-Relational Mapper (ORM) для базы данных сервера MSSQL. Библиотека Json.NET предоставляет эффективный обход документов JSON через LINQ. Кроме того, если нет библиотеки, которая делает то, что вам нужно, вы также можете написать свой собственный поставщик LINQ!
Причины использования синтаксиса запроса
Почему используйте синтаксис запросов? Это вопрос, который часто возникает. В конце концов, следующий код:
var filteredItems = myItems.Where(item => item.Foo);
Dim filteredItems = myItems.Where(Function(item) item.Foo)
гораздо более кратким, чем это:
var filteredItems = from item in myItems
where item.Foo
select item;
Dim filteredItems = From item In myItems
Where item.Foo
Select item
Не является ли синтаксис API более кратким способом сделать синтаксис запроса?
Нет. Синтаксис запроса позволяет использовать конструкцию let, которая позволяет вводить и привязывать переменную в области выражения, используя её в последующих фрагментах выражения. Воспроизведение того же кода только синтаксисом API может быть выполнено, но, скорее всего, приведет к коду, который трудно читать.
Так возникает вопрос, следует ли просто использовать синтаксис запроса?
Ответ на этот вопрос да, если:
- Существующая база кода уже использует синтаксис запроса.
- Необходимо ограничивать область видимости переменных в ваших запросах из-за их сложности.
- Вы предпочитаете синтаксис запроса, и он не отвлекает вас от базы кода.
Ответ на этот вопрос - нет если...
- Существующая база кода уже использует синтаксис API
- Вам не нужно задавать область видимости для переменных в ваших запросах
- Вы предпочитаете синтаксис API, и он не отвлекает вас от базы кода.
Основные LINQ
Ниже приведены краткие демонстрации некоторых основных элементов LINQ. Это никак не комплексно, так как LINQ предоставляет больше функциональных возможностей, чем то, что демонстрируется здесь.
Хлеб и масло - Where
, Select
и 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)
Выравнивание списка списков
// 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)
Объединение между двумя множествами (с пользовательским компаратором)
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())
Пересечение двух множеств
// 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())
Заказ
// 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)
Равенство свойств экземпляра
Наконец, более сложный пример: определение, равны ли значения свойств двух экземпляров одного типа (заимствовано и изменено из этого поста на 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 или Parallel LINQ — это механизм параллельного выполнения для выражений LINQ. Другими словами, регулярное выражение LINQ можно тривиально параллелизировать в любом количестве потоков. Это достигается с помощью вызова AsParallel()
, который предшествует выражению.
Рассмотрим следующее:
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)
}
Этот код будет распределять facebookUsers
между системными потоками по мере необходимости, параллельно суммировать общее количество лайков для каждого потока, суммировать результаты, вычисляемые каждым потоком, и преобразует этот результат в удобочитаемую строку.
В форме схемы:
Параллельные задания, связанные с ЦП, которые можно легко выразить с помощью LINQ (иными словами, являются чистыми функциями и не имеют побочных эффектов) являются отличным кандидатом для PLINQ. Для заданий, которые