Автор: Нодыр Туракулов

Содержание

·        Базовые нововведения

·        Implicitly typed variables

·        Object and collection initialization

·        Anonymous types

·        Auto-implemented properties

·        Extension methods

·        Lambda expressions

·        Language Integrated Query

·        Понятие последовательности

·        Операторы над последовательностями

·        Типизация последовательностей

·        Создание своих операторов

·        Как это работает

·        О языке запросов

·        Альтернативное выполнение запроса

·        Подробнее о LambdaExpression

·        Приложения

·        Спецификация языка запросов

·        Справочник операторов

·        Инструменты разработчика

·        Linq To Xml

Введение

Не прошло и года с момента появления Microsoft .NET 3.0, как уже вышла Visual Studio 2008 Beta 2 и через полгода, надеюсь, мы увидим релиз новой студии, а с ней и релиз новой версии языка C#. На этот раз компания Microsoft больше всего озабочена вопросом обработки данных, и, как результат,  .NET Framework 3.5 включает в себя две новые технологии работы с базами данных, новую технологию обработки XML , наконец, ключевое нововведение в самом языке C# – это улучшенная обработка данных.

 

В нашей компании NetDec применяют C# 3.0 в производстве, и эта статья излагает весь опыт работы с новой версией языка.

 

Все нововведения языка C# 3.0 можно поделить на «базовые» и Linq. Linq (Language Integrated Query) – это новая технология обработки данных на уровне языка. Базовые новшества являются основой для Linq, и некоторые из них практически не находят применения без Linq. В отличие от принципиальных изменений, сделанных во C# 2.0 - обобщения (generics) - затронувших CLR,  в C# 3.0 обновляется только компилятор и библиотека классов – никаких изменений в CLR нет. Ниже приведена карта нововведений:

 

«Базовые» новшества изображены синим цветом,  Linq - зеленым, черным  – элементы, не относящиеся непосредственно к новой версии языка, но служащие основой технологии Linq. Курсивом выделены элементы, которые находятся на уровне классов, а не языка.

Далее, по порядку снизу вверх, слева направо по карте будут описаны все нововведения.

 

Базовые новшества

Кроме того, что «базовые» новшества являются основой для Linq, они значительно уменьшают количество кода и повышают его читабельность. Ниже будут детально рассмотрены “базовые” изменения  в C# 3.0:

1.     Implicitly typed variables (неявно типизированные переменные)

В C# 3.0 появилась возможность объявлять переменные, явно не указывая ее тип:

 

var x = 3;

 

Тип переменной определяется из устанавливаемого значения. Фактически этот код преобразуется компилятором в

 

int x = 3;

 

Важно понять, что в данном случае переменная x имеет тип не object, а именно int. В этой таблице показаны эквивалентные строки кода, написанные на C# 2.0 и C# 3.0.

C# 2.0

C# 3.0

List<int> lst = new List<int>();

var lst = new List<int>();

MyClass b = (MyClass)a;

var b = (MyClass)a;

Dictionary<string, string> dic = new

Dictionary<string, string>();

var dic = new Dictionary<string, string>();

foreach (KeyValuePair<string, string> p in dic)…

foreach (var p in dic)…

Как видно из таблицы, это простое нововведение делает код короче. Есть и ограничения: если вы объявили переменную с помощью var, то она обязательно должна быть инициализирована значением, из которого можно определить тип. Такой код вызовет ошибку компиляции:

 

var x = null; // тип не может быть определен

 

var x; // инициализация обязательна

x = 3;

 

Также есть возможность создавать неявно типизированные массивы:

C# 2.0

C# 3.0

int[] arr = new int[] { 1, 2, 3 };

 

var arr = new[] { 1, 2, 3 };

 

double[] arr = new double[] { 1, 2, 3.0 };

var arr = new[] { 1, 2, 3.0 };

object[] arr = new object [] { 1, new object() };

var arr = new[] { 1, new object() };

 

Тип массива определяется по типам элементов. Если типы элементов разные и между ними нет неявных преобразований (как например из Int32 в Double), то компилятор выдаст ошибку:

 

class Class1{}

class Class2{}

var arr = new[] { new Class1(), new Class2() };

 

2.     Object and collection initialization (инициализация объектов и коллекций)

Представьте, что вам нужно создать объект и тут же инициализировать его. Обычно для этого создается конструктор, в который мы передаем значения. Если инициализация объекта не обязательна, то приходится создавать два конструктора – один без параметров.

В C# 3.0 появилась возможность инициализировать объекты таким образом:

 

var e = new Employee()

           {

               FirstName="Иван",

               LastName="Иванов"

           };

 

Напоминает установку значений свойств у атрибута. Этот код преобразуется компилятором в следующий:

 

Employee e = new Employee();

e.FirstName = "Иван";

e.LastName= "Иванов";

 

Устанавливать можно как поля, так и свойства. Особенно это полезно, когда нам нужно создать объект только для того, чтобы передать его какому-нибудь методу в качестве аргумента. В C# 2.0 для этого приходилось создавать лишнюю переменную, но в C# 3.0 мы можем написать так:

 


 

SomeMethod(new Employee()

           {

               FirstName="Иван",

               LastName="Иванов"

           });

 

Компилятор создаст временную переменную сам, впрочем, он создает ее всегда, даже в предыдущем примере. Было бы здорово, если бы компилятор в таком случае после вызова метода устанавливал значение временной переменной в null. Если далее в этом методе вызываются долго работающие методы, то эта переменная будет считаться корнем при сборке мусора, а значит, попадет во второе поколение объектов, хотя, может быть, и не должна была.

 

Инициализация коллекций, позволяет писать следующее:

 

var coll = new List<int>() { 1, 2, 3 };

 

Этот код преобразуется в:

 

List<int> coll = new List<int>();

coll.Add(1);

coll.Add(2);

coll.Add(3);

 

Причем, это нововведение можно использовать, только с классами, которые реализуют интерфейс IEnumerable<T> и имеют открытый метод Add(T item).

 

Теперь представьте себе, что вам необходимо создать и заполнить список определенными инициализированными объектами. Снова сравнение:

 

C# 2.0

C# 3.0

List<Employee> ee = new List<Employee>()

Employee e = new Employee();

e.FirstName = "Grady";

e.LastName = "Booch";

ee.Add(e);

e = new Employee();

e.FirstName = "Ivar";

e.LastName = "Jacobson";

ee.Add(e);

e = new Employee();

e.FirstName = "James";

e.LastName = "Rumbaugh";

ee.Add(e);

 

 

var ee = new List<Employee>()

{

    new Employee()

    {

        FirstName = "Grady",

        LastName = "Booch"

    },

    new Employee()

    {

        FirstName="Ivar",

        LastName="Jacobson"

    },

    new Employee()

    {

        FirstName = "James",

        LastName = "Rumbaugh"

    }

};

Как видите, не приходится объявлять временных переменных, а самое главное, код стал гораздо читабельнее.

3.     Anonymous types (анонимные типы)

На основе предыдущих нововведений появилась возможность создавать экземпляры анонимных классов таким образом:

 

var x = new { Text = "abc", Num = 10, Now= DateTime.Now };

Console.WriteLine(x.Text);

 

Компилятор сгенерирует класс с соответствующими свойствами:

 

        // упрощенное описание

        class AnonymousClass

        {

            readonly string _Text;

            public string Text

            {

                get { return _Text; }

            }

 

            readonly int _Num;

            public int Num

            {

                get { return _Num; }

            }

 

            readonly DateTime _Date;

            public DateTime Date

            {

                get { return _Date; }

            }

 

            public AnonymusClass(string Text, int Num, DateTime Date)

            {

                _Text = Text;

                _Num = Num;

                _Date = Date;

            }

        }

 

И ваш код преобразуется в

 

AnonymusClass x = new AnonymusClass("abc", 10, DateTime.Now);

Console.WriteLine(x.Text);

 

Как видите свойства только для чтения, значения можно установить только при создании.

 

Если вы хотите назвать свойство так же, как свойство, значение которого вы получаете, то название анонимного свойства писать необязательно:

 

var x = new { DateTime.Now };

 

Свойство будет названо Now. Типы свойств также определяются из устанавливаемых значений, поэтому такой код не скомпилируется:

 

var x = new { Text = null }; //невозможно определить тип из null

Строгая типизация анонимного класса актуальна только в пределах данного метода, а анонимных

полей не бывает. Действительная польза от анонимных классов будет очевидна далее в запросах.

 

Детали генерации анонимных типов

Кроме свойств с полями, генерируется следующие члены класса:

·        Конструктор с параметрами для значений свойств

·        Метод Equals  - сравнение происходит по значению полей

·        Метод GetHashCode – хэш-код определяется по значению полей

·        Метод ToString – возвращается строка следующего шаблона:

{Prop1 = Prop1Value, Prop2 = Prop2Value}

 

Компилятор пытается кэшировать анонимные классы – анонимные классы генерируются обобщенными – для каждого свойства создается тип-параметр. Таким образом, для кода

 

            var i = new { A = 1, B = 2 }; // A и B типа Int32

            var f = new { A = 1f, B = "adasd"};// A типа Single, а B – String

 

компилятор сгенерирует только один класс:

 

        // упрощенное описание

        public class AnonymousClass2<TA, TB>

        {

            readonly TA _A;

            public TA A

            {

                get { return _A; }

            }

 

            readonly TB _B;

            public TB B

            {

                get { return _B; }

            }

 

            public AnonymouseClass2(TA A, TB B)

            {

                _A = A;

                _B = B;

            }

        }

 

А ваш код преообразуется в:

 

            var i = new AnonymousClass2<int,int>(1, 2);

            var f = new AnonymousClass2<float,string>(1f, "adasd");


 

4.     Auto-implemented properties (автореализованные свойства)

Свойства теперь можно объявлять так:

 

class Employee

{

    public string FirstName { get; set; }

    public string LastName { get; set; }

}

 

 В этом случае, скрытое поле и аксессоры свойства генерируются автоматически, и Ваш код преобразуется примерно в следующий:

 

class Employee

{

    private string _FirstName;

    public string FirstName

    {

        get { return _FirstName; }

        set { _FirstName = value; }

    }

 

    private string _LastName;

    public string LastName

    {

        get { return _LastName; }

        set { _LastName = value; }

    }

}

 

Аксессор set должен быть обязательно. Как и раньше, можно указывать модификаторы доступа:

 

class Employee

{

    public decimal SomeData{ get; private set; }

}

 

В таком случае, аксессор get будет public, а set – private.

5.     Extension methods (методы-расширения)

Очень часто бывают такие ситуации, когда мы пользуемся чужим готовым классом, у которого не хватает нужных нам специфических методов. В этом случае  существуют  два решения:

 

1.      Можно наследовать от исходного класса новый класс и писать нужные методы в дочернем классе, но у этого подхода есть явный минус: мы не можем использовать эти методы для экземпляров базового класса.

2.      Можно создать статический метод, принимающий этот объект, над которым необходимо сделать какие-то операции. Плюс этого подхода в том, что мы можем использовать наш метод для экземпляров базового класса, но минус в том, что код начинает напоминать процедурный подход.

 

В C# 3.0 появляется возможность писать статические методы, но вызывать их как методы,  принадлежащие исходному классу:

 

static class MyDateTimeExtensions

 {

     public static DateTime EndOfDay(this DateTime date)

     {

         return date.AddDays(1).Date.AddMilliseconds(-1);

     }

 }

DateTime dt=DateTime.Now.EndOfDay();

 

Ключевое слово this перед параметром указывает, что этот метод является расширением для типа параметра. Теперь у всех объектов типа DateTime, «появляется» метод EndOfDay  без параметров.

 

Как видно из примера, этот подход совмещает лучшие стороны обоих решений. Методы расширения должны быть в статическом классе, а чтобы их использовать, необходимо включить пространство имен, в котором находится класс-контейнер методов. Особенно приятен тот факт, что аргумент может быть интерфейсного типа. Таким образом, этот метод расширяет все классы, которые реализует этот интерфейс. Ниже приведено несколько полезных примеров:

 

public static bool EnsureOpened(this IDbConnection connection)

{

if (connection.State == ConnectionState.Closed || connection.State==ConnectionState.Broken)

{

        connection.Open();

        return true;

    }

    return false;

}

public static void InsertRange<T>(this IList<T> lst, int index, IEnumerable<T> items)

{

    foreach (var it in items)

        lst.Insert(index++, it);

}

myList.InsertRange(4, myArray);

 

Как видите, в методе-расширении может быть несколько параметров. Метод InsertRange обобщенный и при вызове тип-параметр определяется автоматически.

 

Кроме того, методы-расширения удобны тем, что программист  может безопасно переносить метод-расширение из одного класса в другой – если метод использовался, как расширение, то такое перемещение не отразится на остальных частях проекта. Так же бывают ситуации, когда было бы удобно в определенный класс добавить метод, но это бы нарушило независимость слоев (layers) приложения. Методы-расширения решают эту проблему: например, у нас была ситуация, когда в класс бизнес-логики хотелось добавить метод для проверки значений элементов управления. Но добавить этот функционал в класс логики не верно, потому что бизнес логика ничего не должна знать об интерфейсе пользователя.  Создав метод-расширение для класса бизнес-логики в библиотеке интерфейса пользователя, мы решаем эту проблему.

 

Свойств и операторов-расширений не существует. Ключевым словом this можно пометить только первый параметр.

6.     Lambda expressions (лямбда выражения)

В библиотеке System.Core объявлены следующие универсальные делегаты:

 

public delegate TResult Func<T1, T2, T3, T4, TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);

public delegate TResult Func<T1, T2, T3, TResult>(T1 arg1, T2 arg2, T3 arg3);

public delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2);

public delegate TResult Func<T, TResult>(T arg1);

public delegate TResult Func<TResult>();

 

public delegate void Action<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);

public delegate void Action<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3);

public delegate void Action<T1, T2 >(T1 arg1, T2 arg2);

public delegate void Action<T1>(T1 arg1); // был еще в .NET 2.0

public delegate void Action();

 

Они подходят практически для любых функций и процедур, и я буду использовать их для описания. Итак, в C# 2.0 появилась возможность писать анонимные методы таким образом:

 

Func<int, bool> predicate=delegate(int x)

{

    return x > 3;

};

 

В C# 3.0 этот же метод можно переписать так:

 

Func<int, bool> predicate = x => x > 3;

 

В данном случае x до знака “=>” - это объявление параметра метода, а x+3 это то, что вставится после return. В данном случае типы параметров определяется по типу делегата. Если ваш метод должен принимать два параметра, то запись будет такая:

 

Func<string, int, int> f = (s, x) => s.Length + x;

 

Если метод ничего не принимает, то вместо параметров можно написать пустые скобки:

 

Func<int> three = () => 3;

 

Метод может ничего не возвращать  - компилятор просто не добавит return:

 

Action p = () => Console.WriteLine("Hello lambda!");

Action<string> p = s => Console.WriteLine(s);

 

Можно явно указывать типы параметров. Это полезно, когда вы вызываете обобщенный метод, принимающий функцию в качестве аргумента:

 

 

public void SomeMethod<T>(Func<T, bool> p)

SomeMethod((int x) => x > 3);


 

К сожалению, такой код не скомпилируется:

var f = (int x) => x < 3;

 

Нужно писать полностью:

 

Func<int, bool> f = (int x) => x < 3;

 

По-прежнему есть возможность использовать переменные, объявленные за пределами анонимного метода:

 

bool b= ... ;

Func<int, bool> f = (int x) => x < 3 || b;

 

Компилятор создаст класс, имеющий открытые поля для хранения «внешних» переменных и ваш код преобразуется в следующее:

 

private sealed class GeneratedClass

{

    public bool b;

 

    public bool Method(int x)

    {

        return x < 3 || this.b;

    }

}

GeneratedClass c = new GeneratedClass();

c.b= ...;

Func<int, bool> f = c.Method;

 

 

Если в лямбда-выражении требуется написать больше, чем одну строку кода в, то это делается так:

 

Func<double, double> m = (v) =>

{

    double cached=SlowMethod(v);

    return cached * cached;

};

 

Как видите, return уже надо писать самому. Если существует не обобщенный метод, который принимает функцию в качестве аргумента, то тип параметров указывать необязательно:

 

public void SomeMethod(Func<int, bool> f)

SomeMethod(z => z % 2 == 0);

 

Рекурсивное лямбда-выражение, как и любой анонимный метод, написать не возможно. Из базовых нововведений видно, что внедряется тенденция писать несколько операндов одним целым, и лямбда-выражения, как раз, особенно приятны именно тогда, когда у нас один операнд.

Language Integrated Query

Большинство сегодняшних приложений служат для того, чтобы обрабатывать некую информацию. Как было сказано во вступлении, важнейшим нововведением языка является улучшенная обработка данных (Language Integrated Query, Linq). Linq представляет собой универсальную модель запросов, подходящую как для запросов в локальные последовательности объектов (например структуры данных), так и в удаленные источники данных, например СУБД. Об удаленных запросах будет рассказано далее в разделе «Альтернативное выполнение запроса».

 

Что же представляет собой запрос? Будем рассматривать запросы, как преобразования одного или нескольких входных потоков данных в новый поток данных. Преобразования могут включать фильтрацию, проекцию, соединение, группировку, агрегацию и прочее.

Понятие последовательности

В случае .NET потоком данных является последовательность объектов, а последовательностью является экземпляр любого типа, который реализует интерфейс IEnumerable (здесь и далее будет иметься в виду generic IEnumerable<T>). Вспомним, что собой представляют интерфейс IEnumerable и цикл foreach:

 

// упрощенное описание

public interface IEnumerable<T>

{

    IEnumerator<T> GetEnumerator();

}

public interface IEnumerator<T> : IDisposable

{

    T Current { get; }

    bool MoveNext();

    void Reset();

}

 

 

 

IEnumerable<int> nn = new int[] { 1, 2, 3, 45, 23 };

foreach (int x in nn)

{

    Console.WriteLine(x);

}

 

преобразуется в

 

IEnumerable<int> nn = new int[] { 1, 2, 3, 45, 23 };

using (IEnumerator<int> en = nn.GetEnumerator())

    while (en.MoveNext())

    {

        int x = en.Current;

        Console.WriteLine(x);

    }

 

Последовательность объектов – это не обязательно структура данных. Как видно из последнего примера, цикл foreach просто получает один за другим элементы из последовательности. Фактически можно создать бесконечную последовательность, что мы и сделаем в следующем примере.

В C# 2.0 появился замечательный оператор yield:

 

        IEnumerable<int> YieldPrimes()

        {

            for (int i = 1; ; i++)

                if (IsPrime(i)) // является ли число простым

                    yield return i;

        }

 

Этот метод возвращает бесконечную последовательность простых чисел. А вот более интересный пример:

 

        IEnumerable<Employee> GetArchitects(IEnumerable<Employee> input)

        {

            foreach (var e in input)

                if (e.Position == "Architect")

                    yield return e;

        }

 

Таким образом, функция возвращает последовательность. Каждый раз, когда мы пытаемся получить следующий элемент этой последовательности (вызов метода MoveNext), продолжается работа функции после последнего yield return. Этот функция не начнет работать, пока не начнется цикл foreach.

Операторы над последовательностями

Linq предоставляет программисту готовые, удобные, универсальные операторы для любых последовательностей. Например, предыдущий метод GetArchitects – частный случай оператора фильтрации.

 

Linq реализован на двух уровнях:

1.      Уровень классов: в новой библиотеке System.Core пространство имен System.Linq содержит классы, инкапсулирующие функционал запросов. Основной класс  – Enumerable содержит обобщенные (generic) методы-расширения, представляющие собой операторы над последовательностями.

2.      Уровень компилятора (новые ключевые слова): чтобы код  был более читабельным и коротким, был разработан язык запросов. Компилятор преобразует запрос в вызовы методов, т.е. Linq на уровне компилятора  - это лишь более короткая запись использования Linq на уровне классов.

 

Обозначения

·        Для краткости вместо слова последовательность, я буду использовать слово ряд.

·        Операторами я буду называть разнообразные методы-расширения, оперирующие рядами

·        Входным (In) рядом – ряд-параметр метода

·        Выходным (Out) рядом – ряд-результат метода

·        Функциями – делегаты Func

·        Предикатами – функции, возвращающие Boolean

·        Ключами – функции, принимающие каждый элемент входного ряда и возвращающие его ключевое значение

 

Примеры запросов будут показаны сначала в виде вызовов методов, а потом на языке запросов. Перед запросом будут схематически показаны начальные данные, и после запроса, его результат; фигурными скобками будут показаны ряды, а квадратными – объекты со свойствами.

 

Основные операторы Linq:

1.      Фильтрация

2.      Проекция

3.      Сортировка

4.      Горизонтальное соединение (join)

5.      Группировка и агрегация

Фильтрация

Оператор where принимает предикат и возвращает отфильтрованный ряд:

 

// Customers: {["Alfreds","Berlin"], ["Antonio","Mexico"], ["Victoria","London"],

// ["Elizabeth","London"]}           

 

List<Customer> customers = GetCustomerList();

IEnumerable<Customer> q = customers

                .Where(c => c.City == "London");

 

            foreach (var c in q)

                Console.WriteLine(c.Name);

 

// Result: {["Victoria", "London"], ["Elizabeth", "London"]}

 

 

В переменной q находится лишь запрос, но не результат запроса. Запрос начнет выполняться (замечу: не выполнится, а лишь начнет выполняться) только тогда, когда по ряду начнут проходить циклом. Таким образом, запросы можно достраивать динамически:

 

q = q.Where(c => c.Name.StartsWith("A"));

 

Вот аналог первого запроса на встроенном языке запросов:

 

            var q = from с in customers

                where c.City == "London"

                select с;

 

Фактически этот код идентичен приведенному выше: компилятор преобразует любой запрос в вызовы методов. Этим и обусловлен порядок ключевых слов from, where и select. Любой запрос должен начинаться с from, в котором мы указываем источник данных и «объявляем» переменную, с которой далее будем работать. Естественно, никакой переменной не объявляется - это лишь обозначение, для последующего использования только в пределах запроса. О select будет сказано далее.

 

            var bigMoscowCustomers = from c in customers

                     where c.Orders.Count>10 && c.City=="Moscow"

                     select c;

 

преобразуется в

 

            IEnumerable<Customer> bigMoscowCustomers = customers

                .Where(c => c.Orders.Count > 10 && c.City == "Moscow");

 

Проекция

Операторы проекции предполагают, что каждый элемент из входного ряда проецируется новый элемент. На основе проекций формируется выходной ряд. Существует два оператора проекции.

 

Оператор Select для каждого элемента входного ряда вызывает функцию, и ее результаты формируют выходной ряд:

 

// Customers: {["Alfreds","Berlin"], ["Antonio","Mexico"], ["Victoria","London"], ["Elizabeth","London"]}

 

            var customerNames = customers.Select(c => c.Name);

 

            // Result: {"Alfreds", "Antonio", "Victoria", "Elizabeth"}

 

Таким образом, из ряда клиентов мы получаем ряд их имен. Аналог на языке запросов выглядит так:

 

            var customerNames = from c in customers

                select c.Name;

 

Если необходимо получить несколько значений, то имеет смысл задействовать анонимные классы:

 

            // Customers: {["Alfreds","Berlin", "Obere Str. 57"], ["Antonio","Mexico", "Mataderos  2312"]}

 

            var customerAddresses = customers

                .Select(c => new

                    {

                        c.Name,

                        FullAddress=string.Format("{0}, {1}", c.Address, c.City)

                    });

 

            var customerAddresses = from c in customers

                     select new

                      {

                          c.Name,

                          FullAddress = string.Format("{0}, {1}", c.Address, c.City)

                      };

 

            // Result: {["Alfreds","Berlin, Obere Str. 57"], ["Antonio","Mexico, Mataderos  2312"]}

 

Результатом становится ряд экземпляров анонимного типа. Далее по ней можно проходить циклом или сделать другой запрос:

 

            foreach (var e in customerAddresses)

            {

                Console.WriteLine(e.Name);

                Console.WriteLine(e.FullAddress);

                Console.WriteLine();

            }

 

 

Оператор SelectMany предполагает, что аргумент-функция для каждого элемента входного ряда возвращает другой ряд, объединение которых формирует выходной ряд:

 

            var custOrders = customers.SelectMany(c => c.Orders);

            var custOrders = from c in customers

                     from o in c.Orders

                     select o;

 

Результатом становится ряд заказов, принадлежащих данным клиентам.

Сортировка

Операторы OrderBy, OrderByDescending сортируют ряд по заданному ключу:

 

// Customers: {["Antonio"], ["Alfreds"], ["Elizabeth"], ["Victoria"]}

 

var custsByName = customers.OrderBy(c => c.Name);

var custsByName = from c in customers

                    orderby c.Name

                    select c;

 

            // Result: {["Alfreds"], ["Antonio"], ["Victoria"], ["Elizabeth"]}

 

Для того чтобы отсортировать ряд по нескольким ключам, сначала ее нужно отсортировать с помощью OrderBy(Descending), а далее с помощью ThenBy(Descending). Оператор ThenBy(Descending) учитывает предыдущую сортировку, в то время как OrderBy(Descending) просто сортирует ряд. В отличие от OrderBy(Descending), оператор ThenBy(Descending) в качестве ряда принимает IOrderedEnumerable:

 

// Customers: {["Antonio", 02.02.1965], ["Antonio", 02.02.1960], ["Alfreds", 02.06.1960]}

 

            var q = customers.OrderByDescending (c => c.Name)

                .ThenBy (c => c.BirthDate);

            var q = from c in customers

                     orderby c.Name descending, c.BirthDate

                     select c;

 

            // Result: {["Eugene", 02.06.1960], ["Antonio", 02.02.1960], ["Antonio", 02.02.1965] }

 

В качестве алгоритма сортировки используется QuickSort.

Операторы горизонтального соединения

Под горизонтальным соединением понимается такое преобразование двух рядов, что для каждого элемента одного ряда подбирается соответствие из другого ряда по определенному критерию, а результатом становится данные, полученные из найденных соответствий.

Join

Аналог Inner Join в SQL, за тем исключением, что соединение происходит только по равенству ключей, а не по любому условию. Для каждого найденного соответствия вызывается функция, и ее результаты формируют выходной ряд.

Допустим, нам нужно соединить ряды клиентов и заказов по идентификатору клиента, а результатом должен быть ряд имен клиентов и дат соответствующих заказов:

 

            // Customers: {["ALKFI", "Alfred"], ["GOURL", "Gourmet"]}

            // Orders: {["ALKFI", 12.02.2002],["ALKFI", 12.12.2002], ["GOURL", 12.12.2003]}

 

            var q = customers.Join(

                orders,

                c => c.ID,

                o => o.CustomerID,

                (c, o) => new { c.Name, c.Date });

 

            var q = from c in customers

                    join o in orders on c.ID equals o.CustomerID

                    select new { c.Name, c.Date };

 

            // Result: {["Alfred", 12.02.2002], ["Alfred", 12.12.2002], ["Gourmet", 12.12.2003]}

 

Явно указать условие соединения невозможно, это обусловлено реализацией - для поиска соответствий используется Dictionary. Для того чтобы произвести соединение по нескольким ключам, можно использовать анонимные классы:

 

            IEnumerable<SomeClass1> seq1 =

            IEnumerable<SomeClass2> seq2 =

 

            var q2 = seq1.Join(

                    seq2,

                    e1 => new { e1.Key1, e1.Key2 },

                    e2 => new { e2.Key1, e2.Key2 },

                    (e1, e2) => new { Data1 = e1.Data, Data2 = e2.Data });

 

 

            var q2 = from e1 in seq1

                 join e2 in seq2

                 on new { e1.Key1, e1.Key2 } equals new { e2.Key1, e2.Key2 }

                 select new { Data1 = e1.Data, Data2 = e2.Data };

 

Как я уже писал ранее, экземпляры анонимных классов сравниваются по значению свойств.

GroupJoin

Для этого оператора аналога в SQL нет. Работу этого оператора лучше показать графически. Это исходные данные:

 

OrderID

CustomerID

1

1

2

1

3

2

CustomerID

Name

1

A

2

B

 

 

 

 

Это результат простого Join:

 

Left

Right

Customer(1,A)

Order(1,1)

Customer(1,A)

Order(2,1)

Customer(2, B)

Order(3,2)

 

А это результат GroupJoin:

 

Left

Right

Customer(1,A)

Order(1,1)

Order(2,1)

Customer(2, B)

Order(3,2)

 

Т.е. каждый элемент выходного ряда содержит элемент из первой последовательности и ряд соответствий. Пример использования:

 

            // Customers: {["ALKFI", "Alfred"], ["GOURL", "Gourmet"], ["EASTC", "Ann"]}

            // Orders: {["ALKFI", 12.02.2002],["ALKFI", 17.12.2002], ["GOURL", ", 12.12.2003]}

 

            var custOrders = customers.GroupJoin(orders,

                c => c.ID,

                o => o.CustomerID,

                (c, os) => new { c, os });

 

            foreach (var e in custOrders)

                e.c.Orders.AddRange(e.os);

 

            // Result: {

            //  [["ALKFI", "Alfred"], {

            //      ["ALKFI", 12.02.2002],

            //      ["ALKFI", 12.12.2007]}]

            //  [["GOURL", "Gourmet"], { ["GOURL", ", 12.12.2003] }],

            //  [["EASTC", "Ann"], {}]

            // }

 

Синтаксис GroupJoin на языке запросов отличается от “обычного” Join лишь тем, что добавляется слово into:

 

            var custOrders = from c in customers

                 join o in orders

                   on c.ID equals o.CustomerID into os

                 select new { c, os };

 

Здесь «переменная» os содержит ряд соответствий. Далее мы не можем использовать «переменную» o, нам доступны только «переменные» os и c.

Группировка и агрегация

GroupBy

Группировка – это выделение групп элементов по определенному ключу, а результатом является ряд групп:

 

// Customers: {["Alfreds","Berlin"], ["Antonio","Mexico"], ["Victoria","London"], ["Elizabeth","London"]}

 

var custsByCity = customers.GroupBy(c => c.City);

// q is IEnumerable<IGrouping<string, Customer>>

            var custsByCity = from c in customers

                    group c by c.City;

           

            foreach (var g in custsByCity)

            {

                Console.WriteLine("In {0} city:", g.Key);

                // g.Key в данном случае – город клиента

 

                foreach (var cust in g)

                    Console.WriteLine(cust.Name);

            }

 

            // Result: {

            // ["Berlin", {["Alfred"]}],

            // ["Mexico", {["Antonio"]}]

            // ["London", {["Victoria"], ["Elizabeth"]}]

            //}

 

 

Определение интерфейса IGrouping выглядит примерно так:

 

    public interface IGrouping<TKey, TElement> : IEnumerable<TElement>

    {

        TKey Key { get; }

    }

 

Таким образом, группа тоже является последовательностью рядом, но еще обладает ключом.

 

Оператор группировки предоставляет возможность сделать проекцию внутри групп:

 

            var namesByCity = customers.GroupBy(c => c.City, c => c.Name);

            var namesByCity = from c in customers

                    group c.Name by c.City;

 

Так мы получим ряд имен клиентов, сгруппированных по городу. Для группировки по нескольким ключам используются анонимные типы:

 

            // Customers: {

            // ["Ann  ","London", 02.02.1954],

            // ["Antonio","Mexico", 05.06.1985],

            // ["Victoria","London", 03.02.1975],

            // ["Elizabeth","London", 25.02.1954]

            // }

 

            var q = customers

                .GroupBy(c => new { c.City, c.BirthDate.Year });

            var q = from c in customers

                     group c by new { c.City, c.BirthDate.Year };

 

            // Result: {

            // [["Mexico", 1985], {["Alfred"]}],

            // [["London", 1975], {["Victoria"]}]

            // [["London", 1954], {["Ann"], ["Elizabeth"]}]

            //}

 

 

Count, LongCount

Если ряд реализует интерфейс ICollection, то возвращается его свойство Count, иначе приходится перечислять всю последовательность. LongCount отличается от Count лишь тем, что он возвращает Int64.

 

            // Orders: {["ALFKI"], ["FISSA"], ["ERNSH"], ["ERNSH"], ["FISSA"], ["FRANK"], ["ERNSH"]}

 

            var q = orders.GroupBy(o => o.CustomerID)

                .Select(g => new

                {

                    g.Key, // Key is CustomerID

                    Cnt = g.Count()

                });

            var q = from o in orders

                group o by o.CustomerID into g

                select new

             {

                 g.Key, // Key is CustomerID

                 Cnt = g.Count()

             };

 

            // Orders: {["ALFKI", 1], ["FISSA", 2], ["ERNSH", 3], ["FRANK", 1]}

 

О ключевом слове into будет сказано в разделе «язык запросов».

Существуют перегрузки операторов, принимающие предикат:

 

            var londonCustCount = customers.Count(c => c.City == "London");

 

Будет получено количество клиентов, проживающих в Лондоне.

Min, Max, Sum, Average

Как ни странно, из всех операторов больше всего перегрузок приходится именно на эти операторы. Все перегрузки можно поделить на несколько групп:

1.      Принимающие числовой ряд. В случае пустого ряда для Min/Max/Average вызывается исключение.

2.      Принимающие ряд nullable-чисел. В случае пустого ряда для Min/Max/Average возвращается null.

3.      Принимающие любой ряд и функцию, возвращающую число (можно nullable). В этом случае сначала производится проекция (оператор Select), а затем вызывается соответствующий оператор Min/Max/Sum/Average.

4.      Принимающие любой ряд. Создается Comparer по умолчанию, и с помощью него производится сравнение. Эта перегрузка касается только операторов Min и Max.

 

            // Customers: {

            // ["Ann  ","London", 02.02.1954],

            // ["Antonio","Mexico", 05.06.1985],

            // ["Victoria","London", 03.02.1975],

            // ["Elizabeth","London", 25.02.1954]

            // }

 

var q = customers.GroupBy(c => c.City)

    .Select(g => new

    {

        g.Key, // Key is CustomerID

        Youngest=g.Min(c=>c.BirthDate),

        Oldest=g.Max(c=>c.BirthDate)

    });

var q = from c in customers

         group c by c.City into g

         select new

      {

          g.Key, // Key is CustomerID

          Youngest = g.Min(c => c.BirthDate),

          Oldest = g.Max(c => c.BirthDate)

   };

 

// Result: {["London", 02.02.1954, 03.02.1975], ["Mexico", 05.06.1985", 05.06.1985]}

 

А вот более сложный пример:

 

// Customers: {["ALKFI","Berlin"], ["ANTON","Mexico"], ["SEVES","London"], ["BOTTM","London"]}

            // Orders: {["ALKFI", 300],["SEVES", 1000], ["BOTTM", ", 100]}

 

            var q=from c in customers

                   from o in orders

                   group o by c.City into g

                   select new

                   {

                       City=g.Key,

                       Amount=g.Sum(o=>o.Amount)

                   };

 

            // Result: {["Berlin", 300], ["London", 1100"], ["Mexico", 0]}

Типизация последовательностей

Тот факт, что методы-операторы - обобщенные (generic), требует, чтобы ряды были типизированными (IEnumerable<T>). Особенно часто такая проблема возникает, когда мы работаем со специфическими коллекциями, которые появились еще в .NET  1.x: ControlCollection, TreeNodeCollection, ListViewItemCollection и т.п. Для решения этой проблемы существует два оператора:

OfType<T>

Этот оператор просто отфильтровывает элементы неподходящего типа:

 

            Control c = …

            var qVisibleTxt = c.Controls.OfType<TextBox>()

                .Where(t => t.Visible);

 

В результате получаем типизированный ряд. Этот оператор напоминает безопасный оператор преобразования as.

Сast<T>

Сигнатура этого метода точно такая же, как у метода OfType, но поведение другое: этот оператор приводит все элементы ряда к указанному типу, и если хотя бы один элемент не может быть приведен, то вызывается исключение. Этот оператор напоминает привидение типа (Type)expression.

 

            ListView lv = …

            object someValue =

            var myItems = lv.Items.Cast<ListViewItem>()

                .Where(lv => someValue.Equals(lv.Tag));

 

В отличие от оператора OfType, для этого оператора есть поддержка со стороны языка запросов:

 

            var myItems = from ListViewItem lvi in lv.Items

                          where someValue.Equals(lvi.Tag)

                          select lvi;

 

Создание своих операторов

Создавать свои операторы очень просто. Вот пример оператора, конвертирующего ряд иерархических элементов в плоский ряд:

 

public static IEnumerable<T> Flatten<T>(this IEnumerable<T> input,

Func<T, IEnumerable<T>> expander)

{

    foreach (T item in input)

    {

        yield return item;

        foreach (T sub in Flatten(expander(item), expander))

            yield return sub;

    }

}

 

Данный оператор дает возможность делать иерархические запросы. Этим я хочу показать, что Linq легко расширяем.

Как это работает

Как вы уже поняли, запрос представляет собой цепочку итераторов в виде объектов IEnumerable, каждый из которых запрашивает свой источник для получения следующего элемента, причем, только тогда, когда это потребуется. Давайте разберем следующий запрос:

 

var nums = new List<int> { 4, 3, 2, 1 };

var odd = nums

    .Where(x => x % 2 == 1)

    .OrderBy(x => x);

 

Примерно так выглядит статическая структура данного запроса:

 

Итератор List`а является источником данных для итератора Where, а итератор Where, в свою очередь, является источником данных для итератора OrderBy.

 

Теперь, немного усложним наш запрос:

var q = nums

    .Where(x => x % 2 == 1)

    .Take(2)

    .OrderBy(x => x);

 

Оператор Take (все операторы описаны в  справочнике по операторам в приложении) – это аналог TOP в SQL, т.е. он берет первые n элементов.

Рассмотрим динамику выполнения. На следующем рисунке показан ход выполнения запроса, читать его надо начиная с нижнего левого угла. Стрелками изображены запросы итератору следующего элемента (вызовы метода MoveNext):

 

Когда вы начинаете запрашивать элементы у итератора, он, в свою очередь требует их у своего источника. Когда запрос доходит до итератора List`а, возвращается первый элемент. Оператор where получает его, отфильтровывает и требует следующий. Следующий элемент удовлетворяет условию и передается обратно итератору Take. Итератор Take получив элемент, возвращает его итератору OrderBy и увеличивает свой счетчик. Когда у итератора Take потребуют 3й элемент, он ничего не вернет, тем самым сообщая, что ряд кончился, и запрос на этом кончается – остальные элемента списка не подвергаются никаким лишним проверкам.

Язык запросов

Поскольку первый уровень реализации Linq – на уровне классов – мог бы быть сделан и обычным программистом не из Microsoft, это не имело бы такой ценности, как со вторым уровнем – уровнем языка. В действительности серьезные запросы написать без поддержки компилятора очень сложно и код был бы совершенно не читабельный - вспомнить только оператор Join. Во время обработки компилятором Linq-запроса, трансляция в вызовы методов происходит в несколько этапов. Допустим, мы написали такой запрос:

 

from c in customers
from o in c.Orders
orderby o.Total descending
select new { c.Name, o.Total }

 

Обратите внимание на то, что станет результатом запроса: здесь мы используем оба объекта – и клиента и заказ. Последовательность в один момент времени может вернуть только один объект, следовательно, для реализации такого запроса нам нужен объект, который содержит и клиента и заказ. Поэтому запрос преобразуется в следующий:

 

from x in
            from c in customers
            from o in c.Orders
            select new { c, o }
orderby x.o.Total descending

select new { x.c.Name, x.o.Total }

 

Так как любой запрос – это вызовы методов, запрос преобразуется в следующее:

 

Customers.SelectMany(c => c.Orders.Select(o => new { c, o }))

.OrderByDescending(x => x.o.Total)

.Select(x => new { x.c.Name, x.o.Total })

 

Вот более сложный пример:

1

 

from c in customers
join o in orders on c.CustomerID equals o.CustomerID
join d in details on o.OrderID equals d.OrderID
join p in products on d.ProductID equals p.ProductID
select new { c.Name, o.OrderDate, p.ProductName }

 

2

 

from z in
          
from y in
                      
from x in
                                  
from c in customers
                                  
join o in orders on c.CustomerID equals o.CustomerID
                                  
select new { c, o }
                      
join d in details on x.o.OrderID equals d.OrderID
                      
select new { x, d }
          
join p in products on y.d.ProductID equals p.ProductID
          
select new { y, p }
select new { z.y.x.c.Name, z.y.x.o.OrderDate, z.p.ProductName }

 

3

 

Customers

.Join(orders, c => c.CustomerID, o => o.CustomerID, (c, o) => new { c, o })

.Join(details, x => x.o.OrderID, d => d.OrderID, (x, d) => new { x, d })

.Join(products, y => y.d.ProductID, p => p.ProductID, (y, p) => new { y, p })

.Select(z => new { z.y.x.c.Name, z.y.x.o.OrderDate, z.p.ProductName })

 

 

Ключевое слово into

Подзапросы можно писать так:

 

            var q = from g in

                        from c in customers

                        group c by c.City

                    select new

                    {

                        City=g.Key,

                        Yongest=g.Min(c=>c.BirthDate)

                    };

 

Но есть более удобная запись:

 

var q = from c in customers

                group c by c.City into g

                select new

                {

                    City = g.Key,

                    Yongest = g.Min(c => c.BirthDate)

                };

 

Эти записи эквивалентны. Сначала пишется подзапрос, далее into с новой «переменной» и затем внешний запрос. Далее показан код из вышеописанной таблицы преобразований запроса, но с использованием into:

 

            var withInto = from c in customers

                       join o in order on c.CustomerID equals o.CustomerID

                       select new { c, o } into x

                       join d in details on x.o.OrderID equals d.OrderID

                       select new { x, d } into y

                       join p in products on y.d.ProductID equals p.ProductID

                       select new { y, p } into z

                       select new { z.y.x.c.Name, z.y.x.o.OrderDate, z.p.ProductName };

 

Ключевое слово let

Ключевое слово let позволяет кэшировать значения в запросах:

 

            var q = from c in customers

                    let x = SomeAlgorithm(c)

                    where x>1000

                    select x;

 

что преобразуется в

 

            var q = from c in customers

                select new { c, x = SomeAlgorithm(c) } into a

                where a.x > 1000

                select a.x;

Порядок операторов

Помните, что операторы в языке запросов преобразуются в вызовы методов в таком же порядке. В отличие от SQL, где все операторы находятся в предназначенных для них секциях (clauses), в Linq операторы следует писать в той последовательности, в которой они будут исполняться. Присмотритесь к следующему запросу:

 

            var q = from c in customers

                    from o in c.Orders

                    where c.City == "London" && o.Date.Year==2007

                    select o;

 

Следующий, кажущийся таким же, фрагмент кода будет работать быстрее:

 

            var q = from c in customers

                where c.City == "London"

                from o in c.Orders

                where o.Date.Year == 2007

                select o;

 

В первом запросе оба условия проверяются для всех заказов каждого клиента, хотя, возможно, этот клиент не из Лондона, и вовсе не нужно делать проверку на город для всех его заказов – для всех заказов этого клиента результат будет один и тот же.

Альтернативное выполнение запроса

Помимо добавления новых операторов, Linq предоставляет возможность полностью заменить способ выполнения запроса. Допустим, что вы хотите некое альтернативное выполнение запроса – например, на сервере базы данных, а не на клиенте, но в тоже время хотелось бы использовать стиль запросов Linq. Например, ваш запрос

 

            var q = from c in customers

                    where c.City == "London"

                    select c;

 

преобразовался бы в

 

SELECT c.* FROM Customers c

WHERE c.City='London'

 

и выполнился на сервере базы данных, а клиенту вернулся только результат запроса. Это я буду называть альтернативным выполнением запроса (АВЗ, термин не официальный), а поставщиком запроса (Query provider) я буду называть компонент, который занимается самим выполнением запроса.

До этого мы рассматривали обычные запросы, которые представляют собой цепочку итераторов, в которых в качестве функций используются делегаты. Для АВЗ поставщику необходимо анализировать тело запроса, следовательно, оно должно храниться в иной форме, нежели цепочка итераторов с делегатами, хотя бы, потому что анализировать тело делегата практически невозможно. Поэтому тело запроса записывается в виде объектов - так называемых Linq-выражений. Вернемся к нашему запросу с клиентами из Лондона. Для обычных последовательностей этот запрос транслировался бы компилятором в

 

            var q = customers.Where(c => c.City == "London");

 

В случае АВЗ для каждой операции, которая присутствует в запросе, создается Linq-выражение и составляется дерево Linq-выражений:

 

 

 

Классы выражений находятся в пространстве имен System.Linq.Expressions. Основные классы:

·        BinaryExpression – бинарная операция (*, +, -, /, %, |, ||, &, &&, ==, !=, >, <, >=, <=, ^, <<, >>)

·        ConstantExpression – значения, которые константны по отношению к запросу. Например, внешние локальные переменные, использованные в запросе, являются константами.

·        Expression<TDelegate>, наследован от LambdaExpression – лямбда выражение. В основном используется с делегатами Func, например, Expression<Func<Customer, bool>>

·        MethodCallExpression – вызов метода, например вызов метода Where, Select  и т.д.

·        NewExpression – создание нового объекта

·        ParameterExpression – параметр в лямбда выражении

·        UnaryExpression – унарная операция (-, +, !, ~)

                              

С их помощью можно записать любой запрос. Вот еще один пример запроса и как он выглядит в виде Linq-выражений.

            var qa = from c in customers

                 group c by c.City into g

                 select new

                 {

                     g.Key, // Key is City

                     Youngest = g.Min(c => c.BirthDate)

                 };

Для запросов с альтернативным выполнением существуют интерфейсы IQueryable, IQueryProvider (тот самый поставщик запросов) и класс Queryable – зеркало класса Enumerable с тем лишь отличием, что вместо IEnumerable он принимает IQueryable, а вместо делегатов – Linq-выражения.

 

Далее описаны упрощенные определения интерфейсов IQueryable и IQueryProvider:

 

public interface IQueryable<T> : IEnumerable<T>

{

    // Тело запроса в виде Linq-выражения

    Expression Expression { get; }

    // Поставщик выполнения запроса

    IQueryProvider Provider { get; }

}

public interface IQueryProvider

{

    // Создать новый запрос по телу запроса

    IQueryable<TElement> CreateQuery<TElement>(Expression expression);

    // Выполнить запрос и вернуть скалярное значение

    TResult Execute<TResult>(Expression expression);

}

Наш первый запрос с клиентами из Лондона транслируется в дерево Linq-выражений:

 

            ParameterExpression p = Expression.Parameter(typeof(Customer), "c");

            IQueryable<Customer> q = this.customers

                .Where<Customer>(

                    Expression.Lambda<Func<Customer, bool>>(

                        Expression.Equal(

                            Expression.Property(

                                p,

                                (MethodInfo)methodof(Customer.get_City)),

                            Expression.Constant("London", typeof(string)),

                            false,

                            (MethodInfo)methodof(string.op_Equality)),

                        new ParameterExpression[] { p }));

 

В данном примере, вызывается метод Where класса Queryable и в качестве аргумента передается Linq-выражение предиката. Такая трансляция происходит в том случае, если источник данных реализует интерфейс IQueryable.

IQueryable-запрос можно выполнить двумя путями:

1.      Как обычно, начать перечислять (цикл foreach)

2.      Вызвать метод IQueryProvider.Execute – для получения скалярного значения

 

На данный момент мне известно только две технологии, в которых реализован эти интерфейсы: Linq to Sql и Entity Framework. Когда вы пытаетесь выполнить запрос, поставщику запроса приходится анализировать тело запроса и на основе него действовать. Это сложная задача, если заглянуть Reflector`ом в библиотеку System.Data.Linq, то вы увидите, что большинство кода приходится именно на генерацию SQL-кода по Linq-запросу.

 

Linq To Sql

Теперь, когда мы прошли теорию, перейдем к практике. Самая простая известная мне реализация АВЗ – это технология «Linq To Sql». Linq To Sql представляет собой Object-Relation Mapping. Эта тема заслуживает отдельной статьи, поэтому я не буду описывать ее полностью, а буду только использовать для примеров.

Для настройки отображения (mapping) мы будем использовать атрибуты:

 

    [Table(Name="Customers")]

    public class Customer

    {

        [Column(Name="CustomerID", IsPrimaryKey=true)]

        public string ID { get; set; }

        [Column(Name="ContactName")]

        public string Name { get; set; }

        [Column]

        public string Address { get; set; }

        [Column]

        public string PostalCode { get; set; }

        [Column]

        public string City { get; set; }

 

        EntitySet<Order> _orders=new EntitySet<Order>();

        [Association(Storage="_orders", OtherKey="CustomerID")]

        public EntitySet<Order> Orders

        {

            get { return _orders; }

            set {_orders.Assign(value); }

        }

    }

 

Теперь, создав объект DataContext, мы можем получить экземпляр класса Table<Customer> (реализует IQueryable<Customer>) и делать запросы:

 

using (DataContext ctx = new DataContext("Server=.;Database=Northwind;Integrated Security=True"))    {

                Table<Customer> custs = ctx.GetTable<Customer>();

                Table<Order> orders= ctx.GetTable<Order>();

                var q = from c in custs

                        where c.City == "London"

                        select new {c.Name, c.City};

                // на данном этапе запрос сформирован

                // q is IQueryable<AnonymousType>

 

                // трансляция Linq-запроса в SQL-запрос и выполнение

                foreach (var c in q)

                    Console.WriteLine("{0} is from {1}", c.Name, c.City);

            }

 

Во время цикла по очереди работает ваш код (тело цикла) и код Linq To Sql: получение следующей порции данных с сервера и создание объектов  анонимного класса. В этом примере Linq To Sql будет получать только то, что вы запросили:

 

SELECT c.Name, c.City

FROM Customers c

WHERE c.City='London'

Если Вы напишите такой запрос:

 

var q = from c in custs

        where c.City == "London"

        select c;

 

 

то Linq To Sql запросит все данные:

 

SELECT c.*

FROM Customers c

WHERE c.City='London'

 

 

Далее приведено несколько примеров трансляций запросов на языке запросов в SQL:

 

var q = from c in custs

        where c.City == "London"

            && c.Orders.Any(o=>o.Amount>1000)

        select new { c.Name, c.City };

 

SELECT [t0].[ContactName], [t0].[City]

FROM [Customers] AS [t0]

WHERE ([t0].[City] = @p0) AND (EXISTS(

    SELECT NULL AS [EMPTY]

    FROM [Orders] AS [t1]

    WHERE ([t1].[Amount] > @p1)

AND ([t1].[CustomerID] = [t0].[CustomerID])

    ))

 

 


 

var q = from c in custs

             where c.Orders.Any(o => o.Date == orders.Max(o2 => o2.Date))

             select c;

 

SELECT [t0].[CustomerID] AS [ID], [t0].[ContactName] AS [Name], [t0].[Address], [t0].[PostalCode], [t0].[City]

FROM [Customers] AS [t0]

WHERE EXISTS(

    SELECT NULL AS [EMPTY]

    FROM [Orders] AS [t1]

    WHERE ([t1].[OrderDate] = ((

        SELECT MAX([t2].[OrderDate])

        FROM [Orders] AS [t2]

        ))) AND ([t1].[CustomerID] = [t0].[CustomerID])

    )

 

var q4=from o in orders

       where o.Customer.City=="London"

       select o;

 

SELECT [t0].[OrderID] AS [ID], [t0].[OrderDate] AS [Date], [t0].[CustomerID], [t0].[Amount]

FROM [Orders] AS [t0]

LEFT OUTER JOIN [Customers] AS [t1] ON [t1].[CustomerID] = [t0].[CustomerID]

WHERE [t1].[City] = @p0

 

Подробнее о LambdaExpression

Как уже стало понятно из предыдущей части, в виде Linq-выражений можно хранить любое

.NET выражение. Этой части статьи не было бы, если бы не следующая возможность:

 

            // этот код скомпилируется!

            Expression<Func<int, bool>> l = x => x > 3;    

 

Напомню, что Expression<TDelegate> наследован от LambdaExpression. Когда компилятор видит, что в Expression<TDelegate> вы пытаетесь присвоить лямбда-выражение, он генерирует создание соответствующего Linq-выражения, и мы получаем:

 

            ParameterExpression p = Expression.Parameter(typeof(int), "x");

            Expression<Func<int, bool>> l = Expression.Lambda<Func<int, bool>>(

                Expression.GreaterThan(

                    p,

                    Expression.Constant(3, typeof(int))),

                p);

 

Любое лямбда-выражение может быть преобразовано в Linq-выражение. Сначала это может показаться не такой уж интересной особенностью, но на самом деле это дает возможность динамически формировать сложные запросы. Разработчики могут создавать разнообразные построители запросов, параметрами которых будут такие вот Linq-выражения, и главное указывать их будет удобно. Далее один из примеров использования этой замечательной возможности.


 

 

Динамические условия

Очень часто перед разработчиками встает задача динамического формирования условий для запросов, особенно в отчетах. Возможность несколько раз вызывать оператор Where решает проблему, но не полностью - условия соединяются логическим оператором «И». Автор статьи разработал классы решающие эту проблему:

Класс Condition представляет собой динамическое условие. У него перегружены операторы !, & и |. Condition может быть приобразован в Linq-выражение (метод Compile).

Класс ConditionBuilder служит для формирования динамического условия. Пока сделано только два метода для формирования условий:

1.      Lambda – условие по лямбда-выражению

2.      OneOf – значение ключа объекта равно одному из указанных значений

 

Попробуем ими воспользоваться:

 

IQueryable<Customer> FilterCustomers(IQueryable<Customer> custs, string name,

       DateTime? birthday, string[] cities)

{

    var cb = new ConditionBuider<Customer>(); // строитель условия

    var cond = cb.True;   // cond: Condition<Customer> - тело условия

    // cb.True - условие, которое всегда удовлетворяется

 

    if (!string.IsNullOrEmpty(name))

        cond &= cb.Lambda(c => c.Name.StartsWith(name)); // лямбда-условие на имя клиента

 

    if (birthday.HasValue)

        cond &= cb.Lambda(c => c.BirthDate == birthday.Value);

 

    if (cities!=null && cities.Length>0)

        cond &= cb.OneOf(c => c.City, cities); // город клиента должен присутствовать в списке

 

   

    // extension-method Where для IQueryable<T> принимающий Condition<T>

    return customers.Where(cond);

    // может получиться такой результат:

    // c => c.Name.StartsWith("A") && c.BirthDate=(02.02.2002)

    // && (c.City=="London" || c.City=="Moscow" || c.City=="Tashkent")

}

 

Если кому-то удобно, создавать лямбда-условие можно и оператором «+» у ConditionBuilder:

 

            var cond = cb + (c => c.Name.StartsWith(name));

 

Также можно писать вложенные условия:

 

            Condition<Customer> c1 = …, c2 = …, c3 = …;

            var compositeCond = !(c1 | c2) & c3;

 

И наконец, если вы собираете условие по частям, вы можете воспользоваться методом Condition<T>.Translate:

 

            Condition<Customer> customerCond = …;

            Condition<Order> orderCond = customerCond.Translate((Order o) => o.Customer);

 

У объекта Order есть свойство Customer и мы применили к нему условие customerCond.

Таким образом, динамические условия строить гораздо приятнее. Мы используем эти классы для формирования более сложных запросов, ими указываются пользовательские условия. Эти классы и некоторые другие утилиты для работы с Linq-выражениями, вы можете скачать здесьблога.

Заключение

Поработав на C# 3.0, наша команда вряд ли захочет писать на C# 2.0. Сильно изменился стиль программирования – сместился в сторону функционального. Количество циклов резко уменьшилось, появилась сильная тенденция писать методы-расширения к часто используемым классам. Теперь куда не плюнь, повсюду запросы. Уже сложно отвыкнуть от ключевого слова var. К хорошему действительно быстро привыкаешь.


 

Приложения

Спецификация языка запросов:

Linq-запрос:

источник  тело-запроса

 

источник:

from тип? идентификатор in выражение

 

тело-запроса:

(источник|соединение|кэширование|фильтр)*

сортировка?

(select выражение|group выражение by выражение)

продолжение-запроса?

 

соединение:

join тип? идентификатор in выражение

on выражение equals выражение (into идентификатор)?

 

кэширование:

let идентификатор  =  выражение

 

фильтр:

where булево-выражение

 

сортировка:

orderby ключ-сортировки (, ключ-сортировки)*

 

ключ-сортировки:

выражение (ascending|descending)?

 

продолжение-запроса:

into идентификатор тело-запроса

 

Квалификаторы:

* – 0 или несколько раз

? – 0 или 1 раз (необязательно)

 

Справочник по операторам

Операторы фильтрации

Where – фильтрация по предикату.

OfType<T> – фильтрация по типу. Меняет тип ряда.

Distinct – выборка только уникальных элементов.

Take, Skip выборка/пропуск первых n элементов.

TakeWhile, SkipWhile – выборка/пропуск первых элементов удовлетворяющих предикату.

Операторы  проекции

Select – проекция каждого элемента в новый элемент.  Результатом является ряд новых элементов.

SelectMany – проекция каждого элемента в новый ряд. Результатом является объединение этих новых рядов.

Операторы изменения порядка

OrderBy, OrderByDescending – сортировка по заданному ключу

ThenBy, ThenByDescending – сортировка по заданному ключу, с учетом предыдущей сортировки.

Reverse – разворот ряда в обратную сторону.

Операторы горизонтального соединения

Операторы горизонтального соединения производят поиск совпадений в двух рядах по заданным ключам для каждого из ряда.

Join – Для каждого совпадения вызывается делегат, результаты которого формируют выходной ряд.

GroupJoin – Для каждого элемента из первого ряда сопоставляется ряд соответствующих элементов из второго ряда, и эти пары формируют выходной ряд.

Операторы группировки и агрегации

GroupBy – выделение групп элементов по заданному ключу, для каждой из которых определен ключ группировки.

Count, LongCount – определение общего количества элементов. Оператор Count возвращает Int32, а LongCountInt64. Если входной ряд реализует интерфейс ICollection, то возвращается значение свойства Count, иначе перечисляется весь ряд.

Min, Max, Average, Sumопределение минимального/максимального/среднего элемента или суммы элементов соответственно. Все перегрузки этих операторов можно поделить на несколько групп:

1.      Принимающие числовой ряд. В случае пустого ряда для Min/Max/Average вызывается исключение.

2.      Принимающие ряд  nullable-чисел. В случае пустого ряда для Min/Max/Average возвращается null.

3.      Принимающий любой ряд и функцию, возвращающую число (можно nullable). В этом случае сначала производится проекция (оператор Select), а затем снова вызывается соответствующий оператор.

4.      Принимающий любой ряд. Создается Comparer по умолчанию, и с помощью него производится сравнение. Эта перегрузка касается только операторов Min и Max.

 

Aggregate – агрегация собственной функцией агрегации. Сигнатура функции агрегации:

 

TAccumulate MyAggregate<TAccumulate, TSource>(TAccumulate aggregated, TSource nextValue);

 

Операторы конвертации последовательности в структуру данных (кэшировние результата запроса)

ToArray - создание массива и заполнение элементами ряда. Примерный размер массива указать невозможно. В самом начале создается массив из четырех элементов и далее при нехватке размера увеличивается в два раза.

ToList - создается список List<T> и ряд подается в качестве аргумента конструктора.

ToDictionary, ToLookup – кэширование ряда в хеш-таблицу. Если Dictionary – это отображение один-к-одному, то Lookup – это отображение один-ко-многим. В качестве аргументов необходимо подать функцию для получения ключа. Так же можно подать функцию получения элемента. К сожалению Lookup – только для чтения, и создать его можно только этим оператором.

Операторы получения одного элемента

Эти операторы сначала определяют, реализует ли входной ряд интерфейс ICollection: если да, то IEnumerator не создается. Исключением из этого правила составляют те перегрузки методов, которые дополнительно принимают предикат.

 

First, Single – получение первого элемента. Дополнительно можно подать предикат. Оператор Single отличается от First лишь тем, что дополнительно проверяет, что в ряду больше нет элементов, которые можно было бы вернуть.

Last – получение последнего элемента. Дополнительно можно подать предикат.

ElementAt – получение элемента по индексу.

FirstOrDefault, SingleOrDefault, LastOrDefault , ElementAtOrDefaultэти операторы отличаются от своих родственников тем, что если они не найдут подходящий элемент, то вместо вызова исключения, они вернут значение по умолчанию.

Операторы вертикального соединения

Все операторы вертикального соединения имеют такую сигнатуру:

 

TSource Intersect<TSource>(this IEnumerable<TSource> first,

IEnumerable<TSource> second);

 

Intersect – пересечение рядов. Оператор возвращает ряд элементов, которые присутствуют в обеих входных рядах. Сначала он кэширует первый ряд, затем проходится по второму.

Except – разность рядов. Оператор возвращает все элементы первого ряда, которые отсутствуют во втором ряду. Сначала оператор кэширует второй ряд, затем проходится по первой.

Union, Concat – объединение рядов. Union отличается от Concat тем, что возвращает только уникальные элементы.

Прочие операторы

Cast<T> - типизация ряда. Каждый элемент входного ряда приводится к заданному типу; в случае неудачи вызывается исключение. Меняет тип ряда.

DefaultIfEmpty - если ряд пуст, возвращает ряд, состоящий из одного элемента – значения по умолчанию. С помощью этого оператора можно реализовать Left Join:

 

var  q=from c in custs

        select new {c, MaxAmount=c.Orders.DefaultIfEmpty().Max(o=>o.Amount)};

В этом случае сгенерируется SQL c Left Join, а не Inner Join:

 

Методы, генерирующие последовательность

Empty - возвращает пустой ряд:

 

var em = Enumerable.Empty<int>();

 

Range - возвращает ряд целых чисел в указанном пределе:

 

var e1234 = Enumerable.Range(1, 4);

 

Repeat - повторяет определенный элемент n раз:

var e222 = Enumerable.Repeat(2, 3);

Инструменты разработчика

Visual Studio 2008 Beta 2

IntelliSense прекрасно работает: при подаче лямбда-выражения в качестве аргумента, студия определяет типы параметров. При инициализации объекта IntelliSense показывает только названия полей и свойств создаваемого объекта. Методы-расширения прекрасно отображаются в коде, а в Object Browser видны только методы-расширения, находящиеся в той же сборке. Refactor Rename для «переменных» в запросах работает (в отличии от Beta 1). Что касается времени выполнения, то со времен Beta 1 пока ни одного бага в Linq не найдено.

Reflector

Reflector уже умеет декомпилировать лямбда-выражения, методы-расширения, инициализацию объектов (благодаря лишней временной переменной) и самое главное - Linq-запросы.

Linq To Xml

С появлением Linq, разработчики Microsoft решили дать возможность делать запросы в Xml. Для этого была разработана новая объектная модель xml –документа. В этой статье я лишь покажу примерами новые возможности, но не буду их подробно описывать.

 

Создание xml-документа:

 

            var doc = new XDocument(

                new XElement("customers",

                        new XElement("customer",

                        new XAttribute("id", "ALFKI"),

                        new XAttribute("name", "Alfreds Futterkiste"),

                        new XAttribute("city", "Berlin"),

                        new XElement("orders",

                            new XElement("order",

                                    new XAttribute("id", 10643),

                                    new XAttribute("orderdate", new DateTime(1997, 8, 25))))),

                        new XElement("customer",

                            new XAttribute("id", "ANATR"),

                            new XAttribute("name", "Ana Trujillo Emparedados y helados"),

                            new XAttribute("city", "México D.F."),

                        new XElement("orders"))));

 

 Как видите, снова присутствует тенденция все написать одним операндом. Именно это и дает нам возможность писать такие запросы:

 

            var q = from c in customers

                    where c.City == "London"

                    select new XElement("customer",

                        new XAttribute("id", c.ID),

                        new XAttribute("name", c.Name),

                        new XAttribute("city", c.City),

                        new XElement("orders",

                              from o in c.Orders

                              select new XElement("order",

                                   new XAttribute("id", o.ID),

                                   new XAttribute("orderdate", o.Date))));

 

А так же Linq to Xml может заменить XPath. Сравните интерпретируемый запрос xpath //customers/customer/ с этим:

 

var q = doc.Element("customers").Elements("customer");

// q is IEnumerable<XElement>

 

Во-первых, это компилируется, во-вторых, нам доступен весь .NET Framework в запросах - в xpath сложного условия не построишь:

 

            string[] cities = …;

            var q = from xc in doc.Element("customers").Elements("customers")

                    where Array.IndexOf(cities, (string)xc.Attribute("city")) != -1

                        || xc.Element("orders").Elements("order").Count()>10

                    select new

                    {

                        ID = (int)xc.Attribute("id"),

                        Name = (string)xc.Attribute("name")

                    };

 

Класс XAttribute имеет множество операторов преобразований, особенно полезны nullable версии операторов при анализе xml:

 

        Customer XmlToCustomer(XElement x)

        {

            return new Customer

            {

                ID=(string)x.Attribute("id"),

     // если атрибута не окажится, возвращается null

                BirthDate = (DateTime?)x.Attribute("birthdate") ?? DateTime.MinValue,

                City = (string)x.Attribute("city") ?? "SomeDefaultCity"

            };

        }

 

Ко всему прочему эта объектная модель гораздо удобнее, чем DOM из пространства имен System.Xml.

Hosted by uCoz