![]() |
Технология Клиент-Сервер 2007'2 |
||||||
|
ПРИМЕЧАНИЕ Статья была изменена, чтобы учесть изменения появившиеся в LINQ в Beta1 Orcas. Оригинальная статья основывалась на майском CTP от 2006 года. |
.NET 3.0 уже выпущена, так что нам следует уже разобраться в ней, не так ли? Здорово, это не похоже на то, как в старые времена появился .NET 2.0. Хорошо, если вы еще не поняли, .NET 3.0 содержит довольно много нового:
Как видите, здесь есть, что изучать. Я сейчас изучаю WPF/WCF, но я также интересуюсь небольшим алмазиком по имени LINQ, который, я уверен, станет частью .NET 3.5 и Visual Studio "Orcas" (как ее сейчас называют). LINQ добавит новые возможности к C# и VB.NET. Есть три разновидности LINQ:
LINQ довольно крут, и я занялся его изучением, рассчитывая написать статью о том, что я узнал о LINQ/DLINQ/XLINQ, и надеясь, что это пойдет на пользу некоторым из вас. Эта статья, первая из трех, будет сфокусирована на LINQ.
Предполагаемая серия статей состоит из:
Стандартный LINQ – это нововведение в .NET (реально – несколько новых DLL), позволяющее программисту запрашивать данные в памяти примерно так же, как это делается с помощью привычного SQL-синтаксиса.
То есть там, где выполнялся SQL-запрос подобный:
SELECT * from Books WHERE QuatityInStock > 50 AND Price > 50.00 |
теперь можно написать такой LINQ-запрос (если в памяти есть структура, поддерживающая такие запросы):
var result = from b Books where b.QuatityInStock > 50 AND Price > 50.00 select b; |
Как видите, очень похоже. Это крайне мощная возможность. В общем, это именно то, что позволяет делать LINQ. И, как нетрудно понять, DLINQ занимается тем же самым с БД, а XLINQ занят запросами и созданием XML-документов.
LINQ также вводит немало концепций, пришедших из таких функциональных языков программирования, как Haskell и LISP. Вот некоторые из них:
Возможно, это станет понятнее вам по ходу изложения.
Для исполнения прилагаемого к статье кода потребуется установить новый CTP, вышедший в марте 2007 года и доступный по адресу http://www.microsoft.com/downloads/details.aspx?familyid=281FCB3D-5E79-4126-B4C0-8DB6332DE26E&displaylang=en. Размер его полного инсталлятора – около 4 GB (поскольку это не просто LINQ, а целая новая Visual Studio "Orcas"), однако старого, майского CTP 2006 года, будет недостаточно для целей этой статьи, поскольку за год в LINQ был сделан ряд принципиальных изменений.
if (UserResponse == Yes) { SELECT RestOfArticleContent } |
Прежде чем перейти к будням LINQ, я скажу несколько слов о демонстрационном приложении. Оно выглядит так:
Как видите, оно состоит из панели слева и области справа. Слева пользователь видит PropertyGrid и элемент управления Numeric Up / Down для каждого из списков.
Там, где пользователь может использовать Numeric Up / Down для исследования индивидуальных элементов данных источника данных List, PropertyGrid всегда будет показывать текущий элемент списка по требованию пользователя. List не всегда будет одним и тем же, он зависит от типа исполняемого запроса. Но PropertyGrid всегда позволит пользователю просматривать List описанным способом.
Основными источниками данных для большинства запросов будут простые типизированные списки List<T>, объекты, которые содержат простые экземпляры классов. Ниже приведен список тестовых данных:
_itemList = new List<Item> { new Item { ItemID = 1, ItemName = "Enclopedia", Category="Knowledge", UnitPrice = 55.99M, UnitsInStock = 39 }, new Item { ItemID = 2, ItemName = "Trainers", Category="Sports", UnitPrice = 75.00M, UnitsInStock = 17 }, new Item { ItemID = 3, ItemName = "Box of CDs", Category="Storage", UnitPrice = 4.99M, UnitsInStock = 13 }, new Item { ItemID = 4, ItemName = "Tomatoe ketchup", Category="Food", UnitPrice = 0.56M, UnitsInStock = 53 }, new Item { ItemID = 5, ItemName = "IPod", Category="Entertainment", UnitPrice = 220.99M, UnitsInStock = 0 }, new Item { ItemID = 6, ItemName = "Rammstein CD", Category="Entertainment", UnitPrice = 7.99M, UnitsInStock = 120 }, new Item { ItemID = 7, ItemName = "War of the worlds DVD", Category="Entertainment", UnitPrice = 6.99M, UnitsInStock = 15 }, new Item { ItemID = 8, ItemName = "Cranberry Sauce", Category="Food", UnitPrice = 0.89M, UnitsInStock = 6 }, new Item { ItemID = 9, ItemName = "Rice steamer", Category="Food", UnitPrice = 13.00M, UnitsInStock = 29 }, new Item { ItemID = 10, ItemName = "Bunch of grapes", Category="Food", UnitPrice = 1.19M, UnitsInStock = 4 } }; _orderList = new List<Order> { new Order { OrderID = 1, OrderName = "John Smith", OrderDate = DateTime.Now }, new Order { OrderID = 2, OrderName = "Professor X", OrderDate = DateTime.Now }, new Order { OrderID = 3, OrderName = "Naomi Campbell", OrderDate = DateTime.Now }, new Order { OrderID = 4, OrderName = "The Hulk", OrderDate = DateTime.Now }, new Order { OrderID = 5, OrderName = "Malcolm X", OrderDate = DateTime.Now } }; |
Как видите, первый список содержит 10 объектов Item, а второй список содержит 10 объектов Order. Но что представляют собой эти объекты Item и Order? Как я уже говорил, это очень простые объекты, приведенные здесь только для того, чтобы продемонстрировать таланты LINQ:
using System; using System.Collections.Generic; using System.Text; namespace LinqApp { public class Item { #region Instance fields private int itemID; private string itemName; private string category; private decimal unitPrice; private int unitsInStock; #endregion #region Public Properties public int ItemID { get { return itemID; } set { itemID = value; } } public string ItemName { get { return itemName; } set { itemName = value; } } public string Category { get { return category; } set { category = value; } } public decimal UnitPrice { get { return unitPrice; } set { unitPrice = value; } } public int UnitsInStock { get { return unitsInStock; } set { unitsInStock = value; } } public override string ToString() { return string.Format("ItemID : {0} \r\nItemName: {1} \r\n " + "Category: {2} \r\nUnitPrice: {3} \r\nUnitsInStock: {4}", ItemID, ItemName, Category, UnitPrice, UnitsInStock); } #endregion } } using System; using System.Collections.Generic; using System.Text; namespace LinqApp { public class Order { #region Instance fields private int orderID; private string orderName; private DateTime orderDate; #endregion #region Ctor /// <summary> /// ctor /// </summary> //public Order() { } #endregion #region Public Properties public int OrderID { get { return orderID; } set { orderID = value; } } public string OrderName { get { return orderName; } set { orderName = value; } } public DateTime OrderDate { get { return orderDate; } set { orderDate = value; } } public override string ToString() { return string.Format("OrderID:{0} \r\nOrderName: {1} OrderDate: {2}", OrderID, OrderName, OrderDate); } #endregion } } |
Правая область демонстрационного приложения показывает текущий запрос (реальный LINQ-синтаксис) и полученные результаты.
В значительной степени все исходные данные для демонстрационного приложения готовы, за некоторыми исключениями, где вместо объектов List<T> используются простые массивы значений, но я упомяну о них, когда мы к ним подойдем.
Считайте демонстрационное приложение небольшой площадкой для игры в LINQ.
Я думаю, что это все, что вам нужно знать о демонстрационном приложении на текущий момент. Продолжим?
Как я уже говорил, стандартный LINQ (не DLINQ/XLINQ) работает со структурами данных в памяти, то есть массивами, коллекциями и т.д., и т.д. Фактически LINQ делает это с помощью методов, известных как Standard Query Operators (стандартные операторы запросов).
Новый статический класс из .NET 3.5 System.Linq.Enumerable объявляет набор методов, реализующих эти самые операторы.
Большая часть стандартных операторов запросов – это методы, расширяющие IEnumerable<T>.
Стоит объяснить, что означает "расширять". В C# 3.0 введен новый тип методов – методы расширения (Extencion Methods). Это обычные публичные статические методы, первый параметр которых помечен ключевым словмо this. Компилятор распознает такие методы и позволяет применять их к экземлярам типов, совместимых с типом первого (помеченного) параметра. При этом запись выглядит так, как будто метод реально был описан внутри этого типа. Например, следующий код демонструет метод-расширение Last (расширяющий интферфейс IList<T>), возвращающий последний элемент списка, и его применение для массива и List<T>:
using System; using System.Collections.Generic; public static class Ext { public static T Last<T>(this IList<T> seq) { return seq[seq.Count - 1]; } } class Program { static void Main(string[] args) { int[] ary = { 1, 2, 3, 4 }; Console.WriteLine(ary.Last()); // вызов того же метода, но как обычного статического метода Console.WriteLine(Ext.Last(ary)); List<int> list = new List<int>(new int[] { 1, 2, 3, 4 }); Console.WriteLine(list.Last()); Console.WriteLine(Ext.Last(list)); } } |
Я думаю, что лучше всего будет представить вам стандартные операторы LINQ и дать формальное определение и пример использования каждого из них.
В спецификации LINQ оговариваются следующие операторы:
Стандартные операторы работают с последовательностями. Любой объект, реализующий интерфейс IEnumerable<T> для некоторого типа Т считается последовательностью этого типа.
Я постараюсь дать формальное определение и привести один пример использования для каждого оператора. Дальнейшее изучение (поскольку я покажу только один пример, а вариантов использования каждого оператора много) остается в качестве упражнения для читателя.
Время привести примеры.
Оператор WHERE фильтрует последовательность, основываясь на предикате.
public static IEnumerable<T> Where<T>( this IEnumerable<T> source, Func<T, bool> predicate); public static IEnumerable<T> Where<T>( this IEnumerable<T> source, Func<T, int, bool> predicate); |
Вы спросите, кто такие предикаты?
Предикаты – это выражения (в случае LINQ делегаты), получающие некоторый набор параметров и возвращающие булево значение.
LINQ использует распространенный в мире функционального программирования подход, называющийся применением функций высшего порядка, т.е. функций, получающих в качестве параметра другие функции, изменяющие поведение данной функции. Это позоляет, с одной стороны, производить декомпозицию функций, вводя набор универсальных функций высшего порядка и уточняя их конкретными функциями (делегатами) для получения необходимого результата.
Так, функция высшего порядка Where производит фильтрацию списка (IEnumerable<T> source), отбрасывая все элементы исходного списка, для которых делегат predicate вернет false. Учитывая, что делегаты можно инициализировать анонимными функциями (лямбдами), которые допускают замыкание (т.е. позволяют использовать переменные, объявленные в текущем контексте), мы получаем возможность очень гибко задавать условия фильтрации, используя для этого все данные, которые мы имеем в текущем контексте.
Ниже перечислены примеры использования функции Where:
Func<Customer, bool> predicate = c => c.City == "London"; IEnumerable<Customer> customersInLondon = customers.Where(predicate); |
Или, тоже самое но без промежуточной переменной:
IEnumerable<Customer> customersInLondon =
customers.Where(c => c.City == "London");
|
что можно записать и более привычным образом (он, возможно, понравится знатокам SQL):
IEnumerable<Customer> customersInLondon = from c in customers where c.City == "London" select c; |
Мы можем быть еще ленивее, и написать так:
we could write:
var customersInLondon = from c in customers where c.City == "London" select c; |
Использование ключевого слова var – это еще одна особенность C# 3.0. Она позволяет не указывать тип переменной явно. Вместо этого компилятор выведет тип переменной из инициализирующего ее выражения. При этом переменная останется статически типизированной (т.е. C# ни в коей мере не превратится в скрипт).
Вот так LINQ использует предикаты. Самый простой путь понять, что такое предикаты – это думать о них как о фильтрах, которые приравниваются к True или False, и фильтруют источник данных IEnumerable<T> так, чтобы примененное к нему Expression содержало только элементы, соответствующие фильтру (предикату).
Однако мы отвлеклись. Это нужно было сделать, чтобы объяснить, что такое предикаты. Но теперь, когда все ясно, давайте вернемся к первому стандартному оператору.
Как вы помните, ограничивающий оператор Where был определен так:
public static IEnumerable<T> Where<T>( this IEnumerable<T> source, Func<T, bool> predicate); public static IEnumerable<T> Where<T>( this IEnumerable<T> source, Func<T, int, bool> predicate); |
Оператор Where может иметь одну из форм, приведенных выше, когда первый аргумент функции предиката представляет проверяемый элемент. Второй аргумент, если он есть, представляет начинающийся с нуля индекс элемента в исходной последовательности.
Посмотрим на реальный пример (используя демонстрационное приложение и локальный источник данных_itemList List).
var iSmall =
from it in _itemList
where it.UnitPrice < 50.00M
select it;
|
Оператор проекции выполняет проекцию последовательности.
public static IEnumerable<TResult> Select<TSource, TResult>( this IEnumerable<TSource> source, Func<TSource, int, TResult> selector); public static IEnumerable<TResult> Select<TSource, TResult>( this IEnumerable<TSource> source, Func<TSource, TResult> selector); |
Оператор Select может иметь одну из форм, показанных выше, где первый аргумент функции выборки представляет обрабатываемый элемент. Второй аргумент, если он есть, представляет начинающийся с нуля индекс элемента в исходной последовательности.
Посмотрим на реальный пример (используя демонстрационное приложение и локальный источник данных_itemList List).
var iNames = from i in _itemList select i.ItemName;
|
И на еще один пример:
var namesAndPrices =
_itemList.Where(i => i.UnitPrice >= 10).Select(i => new { i.ItemName,
i.UnitPrice }).ToList();
|
Это интересное выражение. Первая его часть – это предикат i => i.UnitPrice >= 10, так что выбираются только элементы с UnitPrice > 10. Затем из выбранного генерируется новый список (с помощью метода ToList()) включающий только ItemName и UnitPrice.
Есть также оператор SelectMany, который я не стал рассматривать здесь Но после установки LINQ вы можете поиграть с ним сами.
Оператор разбиения состоит из четырех частей.
public static IEnumerable<TSource> Take<TSource>( this IEnumerable<TSource> source, int count); |
Посмотрим на реальный пример (используя демонстрационное приложение и локальный источник данных_itemList List).
var MostExpensive2 = _itemList.OrderByDescending(i => i.UnitPrice).Take(2); |
Здесь выбираются две самые дорогие Items.
public static IEnumerable<TSource> Skip<TSource>( this IEnumerable<TSource> source, int count); |
Посмотрим на реальный пример (используя демонстрационное приложение и локальный источник данных_itemList List).
var AllButMostExpensive2 = _itemList.OrderByDescending(i => i.UnitPrice).Skip(2); |
Здесь выбирается все, кроме двух самых дорогих Items.
Оператор TakeWhile выбирает элементы последовательности пока проверка выдает true, а затем пропускает оставшуюся часть последовательности. Оставлю его вам в качестве упражения.
Оператор TakeWhile пропускает элементы последовательности, пока проверка выдает true, а затем выдает оставшуюся часть последовательности. Оставлю его вам в качестве упражения.
Оператор Join состоит из двух частей:
public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>( this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector); public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>( this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector, IEqualityComparer<TKey> comparer); |
Связывает элементы двух последовательностей, основываясь на соответствии ключей. Эта операция аналогична Join в SQL.
Посмотрим на реальный пример (используя демонстрационное приложение и локальные источники данных _itemList List и _orderList).
var itemOrders = from i in _itemList join o in _orderList on i.ItemID equals o.OrderID select new { i.ItemName, o.OrderName }; |
Этот пример выбирает все объекты Order, имеющие то же значение OrderID, что и текущий Item.ItemID.
Оператор GroupJoin выполняет групповое соединение двух последовательностей на основе соответствия ключей, извлеченных из элементов и группирует результаты.
public static IEnumerable<TResult> GroupJoin<TOuter, TInner, TKey, TResult>( this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, IEnumerable<TInner>, TResult> resultSelector); public static IEnumerable<TResult> GroupJoin<TOuter, TInner, TKey, TResult>( this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, IEnumerable<TInner>, TResult> resultSelector, IEqualityComparer<TKey> comparer); |
Оператор Concat конкатенирует две последовательности.
public static IEnumerable<TSource> Concat<TSource>( this IEnumerable<TSource> first, IEnumerable<TSource> second); |
Посмотрим на пример (используя демонстрационное приложение и локальный источник данных _itemList List).
var items = (from itEnt in _itemList where itEnt.Category.Equals("Entertainment") select itEnt.ItemName ).Concat( from it2 in _itemList where it2.Category.Equals("Food") select it2.ItemName ).Distinct(); |
Этот пример выбирает все объекты Item с Category "Entertainment" и конкатенирует результат с результатом выборки всех объектов Item с Category "Food", гарантируя отсутствие дубликатов благодаря использованию метода Distinct().
Семейство операторов OrderBy / ThenBy упорядочивает последовательность по одному или нескольким ключам.
public static IOrderedSequence<TSource> OrderBy<TSource, TKey>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector); public static IOrderedSequence<TSource> OrderBy<TSource, TKey>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IComparer<TKey> comparer); public static IOrderedSequence<TSource> OrderByDescending<TSource, TKey>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector); public static IOrderedSequence<TSource> OrderByDescending<TSource, TKey>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IComparer<TKey> comparer); public static IOrderedSequence<TSource> ThenBy<TSource, TKey>( this IOrderedSequence<TSource> source, Func<TSource, TKey> keySelector); public static IOrderedSequence<TSource> ThenBy<TSource, TKey>( this IOrderedSequence<TSource> source, Func<TSource, TKey> keySelector, IComparer<TKey> comparer); public static IOrderedSequence<TSource> ThenByDescending<TSource, TKey>( this IOrderedSequence<TSource> source, Func<TSource, TKey> keySelector); public static IOrderedSequence<TSource> ThenByDescending<TSource, TKey>( this IOrderedSequence<TSource> source, Func<TSource, TKey> keySelector, IComparer<TKey> comparer); |
Посмотрим на реальный пример (используя демонстрационное приложение и локальный источник данных _itemList List).
var orderItems = _itemList.OrderBy(i => i.Category).ThenByDescending(i => i.UnitPrice); |
Здесь пример выбирает все объекты Items и упорядочивает их сперва по Category, а затем по UnitPrice.
Оператор GroupBy группирует элементы последовательности.
public static IEnumerable<IGrouping<TKey, TSource>> GroupBy<TSource, TKey>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector); public static IEnumerable<IGrouping<TKey, TElement>> GroupBy<TSource, TKey, TElement>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector); public static IEnumerable<IGrouping<TKey, TSource>> GroupBy<TSource, TKey>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer); public static IEnumerable<IGrouping<TKey, TElement>> GroupBy<TSource, TKey, TElement>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector, IEqualityComparer<TKey> comparer); public interface IGrouping<TKey, TElement> : IEnumerable<TElement>, IEnumerable { TKey Key { get; } } |
Пример:
var itemNamesByCategory = from i in _itemList group i by i.Category into g select new { Category = g.Key, Items = g }; StringBuilder res = new StringBuilder(); foreach (var category in itemNamesByCategory) { res.AppendLine("Category: " + category.Category); foreach (var group in category.Items) { res.AppendLine(group.ItemName); res.AppendLine(); } } |
Данный пример выбирает все объекты Item и группирует их по Category. Ключ, по которому группируются значения, помещается в поле Category анонимного типа, а сама группа в поле Item (того же анонимного типа). Давайте посмотрим на результаты:
Category : Knowledge Enclopedia Category : Sports Trainers Category : Storage Box of CDs Category : Food Tomatoe ketchup Cranberry Sauce Rice steamer Bunch of grapes |
Category : Entertainment IPod Rammstein CD War of the worlds DVD |
Можно заметить, что здесь присутствуют все объекты Item из исходного списка, но теперь они сгруппированы. Это не так удобно, как стандартный SQL-синтаксис, но это работает.
Оператор Distinct удаляет из последовательности дубликаты элементов.
IEnumerable<TSource> Distinct<TSource>(
this IEnumerable<TSource> source);
|
Посмотрим на пример (используя демонстрационное приложение и локальный источник данных _itemList List).
var itemCategory = (from i in _itemList select i.Category).Distinct()
|
Этот пример получает уникальный список всех категорий для списка элементов.
Оператор Union производит объединение двух последовательностей.
public static IEnumerable<T> Union<T>( this IEnumerable<T> first, IEnumerable<T> second); |
Пример:
var un = (from i in _itemList select i.ItemName).Distinct() .Union((from o in _orderList select o.OrderName).Distinct()); |
Этот пример создает уникальный список всех ItemName и затем объединяет этот результат с уникальным списком всех OrderName.
Оператор Intersect создает новую последовательность, являющуюся пересечением двух других последовательностей.
public static IEnumerable<TSource> Intersect<TSource>( this IEnumerable<TSource> first, IEnumerable<TSource> second); public static IEnumerable<TSource> Intersect<TSource>( this IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer); |
Посмотрим на пример (используя демонстрационное приложение и локальные источники данных _itemList List и _orderList List).
var inter = (from i in _itemList select i.ItemID).Distinct() .Intersect((from o in _orderList select o.OrderID).Distinct()); |
Здесь пример создает уникальный список из значений ItemID, а затем создает пересечение этого результата с уникальным списком всех значений OrderID. Результатом является список целых чисел, общих для списков ItemID и OrderID.
Не ищите логики в этих действиях, здесь просто соединяется теплое с мягким – прим.ред.
Оператор Except порождает множество различий между двумя последовательностями.
The Except operator produces the set difference between two sequences.
public static IEnumerable<TSource> Except<TSource>( this IEnumerable<TSource> first, IEnumerable<TSource> second); public static IEnumerable<TSource> Except<TSource>( this IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer); |
Посмотрим на пример:
var inter = (from i in _itemList select i.ItemID).Distinct() .Except((from o in _orderList select o.OrderID).Distinct()); |
ToArray преобразует последовательность (то есть все, что может быть приведено к IEnumerable<T>) в массив.
public static TSource[] ToArray<TSource>( this IEnumerable<TSource> source); |
Посмотрим на пример.
var query = _itemList.Where(i => i.Category.Equals("Food")).ToArray();
|
Этот пример фильтрует элементы списка с помощью Where и конвертирует результат в массив. Это может быть удобно, если вам требуется передать результат в функцию, принимающую массив, или осуществлять индексированный доступ к результатам запроса.
ToList полностью аналогичен ToArray за тем исключением, что преобразует последовательность в список System.Collections.Generic.List<T>.
public static List<TSource> ToList<TSource>( this IEnumerable<TSource> source); |
Пример:
var list = _itemList.Where(i => i.ItemID > 5).ToList(); list.Add(new Item { ItemName = "Some Item" }); |
Здесь результат запроса преобразуется в список, после чего к этому списку добавляется элемнт.
Оператор ToDictionary создает из последовательности Dictionary<K,E>.
The ToDictionary operator creates a Dictionary<K,E> from a sequence.
public static Dictionary<TKey, TSource> ToDictionary<TSource, TKey>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector); public static Dictionary<TKey, TElement> ToDictionary<TSource, TKey, TElement>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector); public static Dictionary<TKey, TSource> ToDictionary<TSource, TKey>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer); public static Dictionary<TKey, TElement> ToDictionary<TSource, TKey, TElement>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector, IEqualityComparer<TKey> comparer); |
Рассмотрим пример.
var scoreRecords = new [] { new {Name = "Alice", Score = 50}, new {Name = "Bob" , Score = 40}, new {Name = "Cathy", Score = 45} }; var scoreRecordsDict = scoreRecords.ToDictionary(sr => sr.Name); |
Здесь создается новый массив анонимных типов (состоящих из двух полей), который конвертируется в объект Dictionary<K, V>.
Оператор ToLookup создает из последовательности Lookup<K, T>. Я оставлю это вам для самостоятельного исследования.
public static Lookup<TKey, TSource> ToLookup<TSource, TKey>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector); public static Lookup<TKey, TElement> ToLookup<TSource, TKey, TElement>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector); public static Lookup<TKey, TSource> ToLookup<TSource, TKey>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer); public static Lookup<TKey, TElement> ToLookup<TSource, TKey, TElement>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector, IEqualityComparer<TKey> comparer); |
Функция OfType производит фильтрацию элементов последовательности, выделяя элементы определенного типу. Очевидно, что такую функцию имеет смысл применять только к последовательностям, содержащим элементы разного типа.
public static IEnumerable<TResult> OfType<TResult>( this IEnumerable source); |
Пример, использующий простой массив:
object[] numbers = { null, 1.0, "two", 3, 4.0f, 5, "six", 7.0 }; var doubles = numbers.OfType<double>(); |
Здесь создается новый массив из различных объектов, а затем применяется OfType для выборки только тех из них, чей тип Double.
Оператор Cast преобразует элементы к заданному типу.
public static IEnumerable<TResult> Cast<TResult>(this IEnumerable source); |
Оператор SequenceEqual производит поэлементное сравнение последовательностей и возвращает True, если они эквивалентны.
public static bool SequenceEqual<TSource>( this IEnumerable<TSource> first, IEnumerable<TSource> second); public static bool SequenceEqual<TSource>( this IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer); |
Посмотрим на пример.
var result = (from i in _itemList select i.ItemID).SequenceEqual( from o in _orderList select o.OrderID); System.Diagnostics.Trace.Assert(!result); |
Результат выполнения этого запроса – false, так как последовательности не эквивалентны.
Следующий пример вернет true:
This is an Equal Example:
int[] scoreRecords1 = { 10, 20 }; int[] scoreRecords2 = { 10, 20 }; var result = scoreRecords1.SequenceEqual(scoreRecords2); System.Diagnostics.Trace.Assert(result); |
Этот набор операторов состоит из 9 частей:
The Set operators are made up of nine parts:
Оператор First возвращает первый элемент последовательности.
public static TSource First<TSource>( this IEnumerable<TSource> source); public static TSource First<TSource>( this IEnumerable<TSource> source, Func<TSource, bool> predicate); |
Оператор First перебирает исходную последовательность и возвращает первый элемент, для которого predicate возвращает true. Если predicate не указан, оператор First просто возвращает первый элемент последовательности.
Рассмотрим пример.
string itemName = "War of the worlds DVD"; Item itm = _itemList.First(i => i.ItemName == itemName); |
Этот пример выдает первый элемент списка, соответствующий i.ItemName == itemName, где itemName = "War of the worlds DVD".
FirstOrDefault возвращает первый элемент последовательности или значение, принятое для типа элемента по умолчанию, если элемент не найден. Например, для ссылочных типов это будет null, а для числовых – 0. Имеется две разновидности функции, отличающиеся тем, что одна из них получает предикат, позволяющий отфильтровать значения.
public static TSource FirstOrDefault<TSource>( this IEnumerable<TSource> source); public static TSource FirstOrDefault<TSource>( this IEnumerable<TSource> source, Func<TSource, bool> predicate); |
Пример:
string itemName = "A Non existence Element"; Item itm = _itemList.FirstOrDefault(i => i.ItemName == itemName); |
Здесь производится поиск первого элемента, ItemName которого равен "A Non existence Element". В данном случае такого нет, и мы получаем null.
Last и LastOrDefault аналогичны First и FirstOrDefault, за тем исключением, что вместо первого элемента возвращают последний.
public static TSource Last<TSource>( this IEnumerable<TSource> source); public static TSource Last<TSource>( this IEnumerable<TSource> source, Func<TSource, bool> predicate); public static TSource LastOrDefault<TSource>( this IEnumerable<TSource> source); public static TSource LastOrDefault<TSource>( this IEnumerable<TSource> source, Func<TSource, bool> predicate); |
Оператор Single возвращает единственный элемент последовательности.
public static T Single<T>( this IEnumerable<T> source); public static T Single<T>( this IEnumerable<T> source, Func<T, bool> predicate); |
Оператор Single перебирает исходную последовательность и возвращает единственный элемент, для которого функция-предикат возвращает true.
Он работает так же, как First. Возможно, разнца в работе этих функций проявится при выборке значений из БД, так как для семантики First важна последовательность выборки, а Single может потенциально взять произвольный элемент последовательности.
Оператор SingleOrDefault возвращает единственный элемент последовательности или значение по умолчанию, если элемент не найден.
Оператор ElementAt возвращает элемент последовательности с указанным индексом. Обратите внимание, что эта функция может применяться к любым последовательностям, а не только к тем, которые поддерживают индексированный доступ.
public static TSource ElementAt<TSource>( this IEnumerable<TSource> source, int index); public static TSource ElementAtOrDefault<TSource>( this IEnumerable<TSource> source, int index); |
Эти функции оптимизируют доступ к коллекциям, реализующим IList<T>. Они пытаются привести тип к IList<T>, и если это удается, просто пользуются идексатором для получения элемента. В противном случае нужный элемент получается простым перебором.
Рассмотрим пример:
Item thirdMostExpensive = _itemList.OrderByDescending(i => i.UnitPrice).ElementAt(2); |
Здесь список упорядочивается по UnitPrice, а затем выбирается третий (с индексом два) элемент списка.
DefaultIfEmpty проверяет, не пуста ли последовательность, и если она пуста, добавляет в нее элемент, значение которого совпадает со значением по умолчанию для типа или значением, переданным в качестве параметра defaultValue.
public static IEnumerable<TSource> DefaultIfEmpty<TSource>( this IEnumerable<TSource> source); public static IEnumerable<TSource> DefaultIfEmpty<TSource>( this IEnumerable<TSource> source, TSource defaultValue); |
Пример:
System.Diagnostics.Trace.Assert(new int[0].DefaultIfEmpty().ToArray()[0] == 0); |
В данном примере создается пустой массив, для него вызывается функция DefaultIfEmpty, которая преобразует его в последовательность, содержащую один элемент со значением "ноль" (значением по умолчанию для типа int).
Оператор Range генерирует последовательность целых чисел.
public static IEnumerable<int> Range(int start, int count); |
Рассмотрим пример:
var squares = Sequence.Range(1, 10).Select(x => x * x); |
Этот пример создает последовательность квадратов чисел от одного до десяти включительно.
Оператор Repeat генерирует последовательность, повторяя значение заданное число раз.
public static IEnumerable<TResult> Repeat<TResult>( TResult element, int count); |
Пример:
int[] numbers = Sequence.Repeat(5, 5).ToArray();
|
Здесь создается новый массив из 5 повторений числа 5.
Оператор Empty возвращает пустую последовательность заданного типа.
public static IEnumerable<TResult> Empty<TResult>(); |
Я не знаю, зачем это может вам понадобиться.
Есть два варианта функции Any:
public static bool Any<TSource>(this IEnumerable<TSource> source); public static bool Any<TSource>( this IEnumerable<TSource> source, Func<TSource, bool> predicate); |
Первый определяет, есть ли хоть какие-то элементы в заданной последовательности, для чего он получает итератор и пытается выбрать первый элемент. По уму, такая функция должна бы называться IsEmpty и работать зеркальным образом.
Второй проверяет, удовлетворяет ли заданному условию хотя бы один элемент последовательности.
Рассмотрим пример:
bool b = _itemList.Any(i => i.UnitPrice >= 400);
|
Этот пример просто возвращает true, если существует Item с UnitPrice > 400.
Оператор All проверяет, удовлетворяют ли условию все элементы последовательности.
public static bool All<TSource>( this IEnumerable<TSource> source, Func<TSource, bool> predicate); |
Пример:
System.Diagnostics.Trace.Assert( new int[] { 2, 4, 6 }.All(x => x % 2 == 0)); System.Diagnostics.Trace.Assert(!new int[] { 2, 4, 1 }.All(x => x % 2 == 0)); |
В этом примере элементы массивов проверяются на четность. В первой строке примера функция All вернет true, так как все элементы массива четные. Во второй строке функция All вернет false, так как последний элемент массива не четный.
Оператор Contains проверяет, содержит ли последовательность заданный элемент.
public static bool Contains<TSource>( this IEnumerable<TSource> source, TSource value); public static bool Contains<TSource>( this IEnumerable<TSource> source, TSource value, IEqualityComparer<TSource> comparer); |
Оператор Contains сперва пытаетс привести коллекцию к типу ICollection<T>. Если это удается, то для получения результата используется метод ICollection<T>.Contains(). В противном случае поиск элемента производится перебором. Первый вариант функции использует для сравнения элементов компаратор, используемый по умолчанию. Второй вариант позволяет задать компаратор явно.
Посмотрим на пример:
System.Diagnostics.Trace.Assert(new int[] { 2, 1, 6 }.Contains(1)); |
Этот пример проверяет, содержит ли массив значение 1.
Таких операторов семь.
Count подсчитывает число элементов в последовательности.
public static int Count<TSource>( this IEnumerable<TSource> source); public static int Count<TSource>( this IEnumerable<TSource> source, Func<TSource, bool> predicate); |
Оператор Count без предиката сперва проверяет, реализует ли исходная последовательность ICollection. Если да, для получения числа элементов используется реализация свойство ICollection<T>.Count. В противном случае количество элементов вычисляется перебором.
Оператор Count с предикатом всегда производит перебор последовательности.
Рассмотрим пример:
var foodCat = (from i in _itemList where i.Category.Equals("Food") select i).Count(); |
Здесь подсчитывается число всех элементов Item, категория которых – Food.
LongCount тоже подсчитывает число элементов последовательности, но возвращает long, а не int, что в некоторых случаях предотвращает переполнение при подсчетах.
public static long LongCount<TSource>( this IEnumerable<TSource> source); public static long LongCount<TSource>( this IEnumerable<TSource> source, Func<TSource, bool> predicate); |
Оператор Sum подсчитывает сумму элементов последовательности числового типа.
public static decimal? Sum(this IEnumerable<decimal?> source); public static decimal Sum(this IEnumerable<decimal> source); public static double? Sum(this IEnumerable<double?> source); public static double Sum(this IEnumerable<double> source); public static float? Sum(this IEnumerable<float?> source); public static float Sum(this IEnumerable<float> source); public static int? Sum(this IEnumerable<int?> source); public static int Sum(this IEnumerable<int> source); public static long? Sum(this IEnumerable<long?> source); public static long Sum(this IEnumerable<long> source); public static decimal? Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, decimal?> selector); public static decimal Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, decimal> selector); public static double? Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, double?> selector); public static double Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, double> selector); public static float? Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, float?> selector); public static float Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, float> selector); public static int? Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, int?> selector); public static int Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, int> selector); public static long? Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, long?> selector); public static long Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, long> selector); |
Оператор Sum перебирает исходную последовательность, вызывает функцию-селектор для каждого элемента и вычисляет сумму результирующих значений. Если функция-селектор не указана, рассчитывается сумма самих элементов.
Функции, принимающие последовательности, содержащие nullable-элементы, в отличие от своего SQL-аналога, просто игнорируют элементы, содержащие null. Что интересно, эти функции возвращают также nullable-значения, но их реализация в LINQ to objects такова, сто она никогда не возвращает null. Даже если последовательность состоит только из null-элементов, или вовсе пуста, все равно будет возвращен 0, хотя и запакованный в nullable-тип. Скорее всего, в LINQ to SQL поведение функции будет зависеть от настроек и/или реализации СУБД.
Рассмотрим пример:
var totals = (from i in _itemList select i.UnitPrice).Sum();
|
Этот пример просто суммирует значения UnitPrice всех Item.
Оператор Min ищет минимальное (а Max – максимальное) значение в последовательности.
public static decimal? Min(this IEnumerable<decimal?> source); public static decimal Min(this IEnumerable<decimal> source); public static double? Min(this IEnumerable<double?> source); public static double Min(this IEnumerable<double> source); public static float? Min(this IEnumerable<float?> source); public static float Min(this IEnumerable<float> source); public static int? Min(this IEnumerable<int?> source); public static int Min(this IEnumerable<int> source); public static long? Min(this IEnumerable<long?> source); public static long Min(this IEnumerable<long> source); public static TSource Min<TSource>(this IEnumerable<TSource> source); public static decimal? Min<TSource>(this IEnumerable<TSource> source, Func<TSource, decimal?> selector); public static decimal Min<TSource>(this IEnumerable<TSource> source, Func<TSource, decimal> selector); public static double? Min<TSource>(this IEnumerable<TSource> source, Func<TSource, double?> selector); public static double Min<TSource>(this IEnumerable<TSource> source, Func<TSource, double> selector); public static float? Min<TSource>(this IEnumerable<TSource> source, Func<TSource, float?> selector); public static float Min<TSource>(this IEnumerable<TSource> source, Func<TSource, float> selector); public static int? Min<TSource>(this IEnumerable<TSource> source, Func<TSource, int?> selector); public static int Min<TSource>(this IEnumerable<TSource> source, Func<TSource, int> selector); public static long? Min<TSource>(this IEnumerable<TSource> source, Func<TSource, long?> selector); public static long Min<TSource>(this IEnumerable<TSource> source, Func<TSource, long> selector); public static TResult Min<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector); |
Фактически все, сказанное о Sum, справедливо и для Min/Max.
Значения сравниваются, используя их реализацию интерфейса IComparable<T>, или, если значения не реализуют этот интерфейс, не-generic интерфейс IComparable.
Пример:
var minimum = (from i in _itemList select i.UnitPrice).Min();
|
Этот пример просто выбирает минимальное значение UnitPrice.
Average полностью аналогичен Sum, за тем исключением, что вычислет не сумму, а среднее арифметическое для последовательности.
Aggregate применяет к последовательности функцию.
public static TSource Aggregate<TSource>( this IEnumerable<TSource> source, Func<TSource, TSource, TSource> func); public static TAccumulate Aggregate<TSource, TAccumulate>( this IEnumerable<TSource> source, TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> func); public static TResult Aggregate<TSource, TAccumulate, TResult>( this IEnumerable<TSource> source, TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> func, Func<TAccumulate, TResult> resultSelector); |
Оператор Aggregate с начальным значением начинает работу с присвоения начального значения внутреннему аккумулятору. Затем он перебирает исходную последовательность, последовательно вычисляя следующее значение аккумулятора с помощью вызова указанной функции с текущим значением аккумулятора в качестве первого аргумента и текущего элемента последовательности в качестве второго аргумента. Конечное значение аккумулятора возвращается в виде результата.
Эта функция аналогична функциям Fold, FoldRight или Reduсe в функциональных языках программирования. С их помощью можно избавиться от многих циклов, значительно сократив объем кода.
Посмотрите на пример:
int[] ints = { 2, 3 };
var res2 = ints.Aggregate(0, (acc, x) => acc + x * x);
|
Этот пример подсчитывает сумму квадратов значений из массива целых чисел. Чтобы было понятнее, я приведу этот пример с коментариями.
int[] ints = { 2, 3 }; var res2 = ints.Aggregate(0, // 0 – это "seed", начальное значение акомулятора. (acc, x) => // acc – это аккумулятор, значение которого равно значению, возвращенному // этой функцией на предыщей итерации, или значение seed, если это // первая итерация. // x – по очереди сопоставляется с каждым элементом // коллекции (массива в данном случае). // Возводим значение элемента в квадрат и складываем с сумой квадратов // предыдущих элементов. Результат вычисления возвращается из функции // и помещается в аккумулятор. acc + x * x); |
Этот пример можно переписать в императивном стиле следующим образом:
int[] ints = { 2, 3 }; int acc = 0; foreach (var x in ints) acc += x * x; |
DLINQ позволяет разработчикам обращаться с запросами к данным, хранящимся в БД SQL Server. DLINQ поддерживает только эту СУБД. Для работы DLINQ необходимо создать несколько сущностей. Эти сущности должны быть размещены в файле с кодом и соответствовать схеме БД. Затем эти сущности аннотируются некими новыми атрибутами DLINQ. Эти атрибуты позволяют DLINQ корректно идентифицировать сущности, а также создавать SQL-команды, отсылаемые СУБД. Например, имеются атрибуты Table, Column и Association, отражающие реальные объекты БД: Table, Field (или Column, если вам больше нравится) и Relationship.
Судя по тому, что я об этом читал, все это очень схоже с технологией объектно-реляционного отображения (O/R Mapping, Object Relational Mapping).
O/R mapping (оно же O/RM, оно же ORM) – технология программирования, предназначенная для преобразования данных между несовместимыми системами типов баз данных и объектно-ориентированных языков программирования. По сути, при этом создается "виртуальная объектная БД", которую можно использовать из языка программирования. Существуют как коммерческие, так и свободно распространяемые пакеты для ORM, хотя некоторые программисты предпочитают создавать свои собственные ORM-средства.
Именно это и обещает DLINQ; он предлагает слой персистентности, где можно хранить изменения, ожидающие записи в нижележащую БД. Можете рассматривать его как версию БД в памяти.
Очевидно, LINQ должен каким-то образом узнать о структуре БД (чтобы уметь генерировать корректные SQL-команды, возвращающие корректные результаты). Для этого применяются файлы Entity. Используя новые DLINQ-атрибуты Table, Column и Association, можно корректно разметить достаточно стандартный (за исключением нескольких новых DLINQ-классов наподобие EntitySet) .NET-код для отображения структуры нижележащей БД.
После создания эти сущностей с помощью DLINQ можно запрашивать, удалять, изменять или создавать новые сущности. Так что если попросить DLINQ удалить сущность, он создаст (скрыто) соответствующую SQL-команду для удаления указанной записи из таблицы БД.
Насколько я знаю, DLINQ и Entity-файлы идут рука об руку; для использования DLINQ вам нужны Entity-файлы, и, с другой стороны, Entity-файлы используются только при работе с DLINQ. Я полагаю, что можно сказать, что если есть DLINQ, то где-то рядом есть и Entity-код, позволяющий DLINQ воссоздать в памяти структуру нижележащей БД. Иначе DLINQ просто не будет знать, как сгенерировать корректные SQL-команды.
Возможно, среди читателей найдутся такие, кто скажет, что O/R Mapping существует уже давно, а некоторые даже уже использовали его. Я не собираюсь спорить, поскольку лично я не использовал никакого O/R Mapping под .NET. Я знаю, что подход DLINQ очень похож на J2EE Enterprise Java Beans в Java (которые я использовал в довольно большом проекте), за тем исключением, что Java поддерживает много разных СУБД, а не один SQL Server.
Остальная часть статьи посвящена тому, как начать работу с DLINQ, какие шаги нужно выполнить, чтобы сгенерировать Entity-файлы, а также собственно DLINQ-запросам.
На самом деле, чтобы начать работать с DLINQ, нужно выполнить всего несколько шагов:
Вот и все, что нужно. В остальной части статьи я буду показывать, как это делается.
Я просто душой отдыхал, творя эту статью, и был весьма тронут простотой использования DLINQ. Хотя мне по прежнему нравятся хранимые процедуры, и я думаю, что они просто добавляют еще один уровень кода к уровню доступа к данным. Я до сих пор не на 100% верю в DLINQ. Его просто использовать. Но вам придется поддерживать все эти entity-классы, что существенно ограничит вас в дизайне БД, несмотря даже на то, что с использованием SQLMetail легко сгенерировать эти классы заново. Мне нравится, как легко стало обновлять БД, я думал, что DataAdaptor-ы были круты, но dataContext намного лучше.
Есть ряд интересных материалов по LINQ и концепциям функционального программирования. Это, конечно же, сайт LINQ, некоторые замечательные примеры в Web, а также несколько статей на Code Project. Я перечислю некоторые из них для самых любопытных:
Copyright © 1994-2016 ООО "К-Пресс"