C# 3.0 и LINQ: для тех, кто ещё не в курсе

Что было бы, если проснувшись утром, вы обнаружили в языке, на котором говорите, возможность декларативно описывать те вещи, которые вы хотите получить? На самом деле, в русском языке существует такая возможность, и мы пользуемся ей ежедневно. Но на практике она не всегда работает: мы приходим в магазин и говорим: «Дайте мне книги Дональда Кнута, вышедшие позже 1970 года, в заголовке которых встречается слово программирование». Наверняка, продавец косо на нас посмотрит (хотя, всё зависит от магазина), подведёт к книжной полке и предложит самим найти то, что нам требуется :).

Хотя не исключено и то, что он запишет наши требования и удалится куда-то к белому ящику с монитором, и, вернувшись, вручит стопку нужных нам книг. Этот случай будет означать то, что продавец понял тот запрос, что мы сформулировали ему на родном (native) языке, затем он транслировал его в какой-то свой ментальный формат и воспользовался каким-либо средством для поиска и выборки наших данных. Нам не пришлось самим искать эти книги на полке, а так же не пришлось брать листик бумаги и расписывать на нём SQL-запрос, или же разучивать специальные термины, используемые в культуре поиска книг. Мы воспользовались расширением своего родного языка, чтобы получить необходимую выборку.

Тут может возникнуть проблема «чистоты»: нужно ли загромождать чистый и простой язык новыми сомнительными конструкциями, вместо того, чтобы использовать для этих целей другой, специально для этого предназначенный? Разработчики .NET всё-таки решили добавить в C# и другие языки платформы .NET проблемно-ориентированное расширение LINQ, интегрированный язык запросов.

И теперь мы можем писать так:

var books = from b in all_books
            where b.Author == "Donald Knuth"
&& b.Title.ToLower().Contains("programming")
&& b.DatePublished.Year > 1970
            select new { b.Title, b.ISDN };

Причём такие запросы можно выполнять к любым объектам, реализующим IEnumerable. Для незнакомого с LINQ человека это может выглядеть непонятно, но я думаю, к концу статьи всё станет на свои места. Конечно, данная конструкция кажется неуместной в контексте обычных конструкций языка C#. Однако, использовать такой синтаксис или нет – дело конкретного случая. Аналогичные действия можно выполнить и с помощью «родного» синтаксиса С#, но они не будут выглядеть так впечатляюще :). На самом деле всё это транслируется в стандартные конструкции перед выполнением, так что кто-то может заметить, что перед нами синтаксический сахар в чистом виде.

Немного про LINQ

Идеи, появившиеся в новой версии C#, позаимствованы из экспериментального языка , который специально был создан для улучшения обработки XML и реляционных данных в языке C#. Так же некоторое сходство можно проследить с функциональным языком F# (потомок OCaml).

Расширения LINQ представлены в С# 3.0. Всё необходимое для работы с основными возможностями LINQ находится в пространстве имён System.Linq. Стоит отметить, что Microsoft представила целый набор новых технологий на базе LINQ для использования с базами данных, XML, объектами, реализующими IEnumerable. Технологии LINQ to Databases, LINQ to XML и LINQ to Entities выходят за рамки данной статьи.

Нам понадобится Visual Studio 2008, либо Visual Studio 2005 с установленным .NET Framework 3.5 и расширениями LINQ. Либо же, вы можете совершенно бесплатно загрузить Visual C# 2008 Express Edition отсюда.

Итак, начнём.

Что за декларативность и функциональность?

Работая с .NET, мы работаем с объектно-ориентированной средой, и ООП-языками: C#, VB.NET, и другими. В них мы оперируем классами, объектами, пространствами имён и так далее. ООП отлично справляется с отдельными объектами, однако, когда дело доходит, например, до коллекций объектов, всё усложняется. Коллекции гораздно тяжелее обрабатывать. Для этого было написано много вспомогательных классов. Но когда нам требуется сотворить нетривиальную операцию над коллекциями, мы заходим в лабиринт загадочных нагромождений излишнего кода.

LINQ пытается решить проблему сложных манипуляций, привлекая сильные стороны функционального программирования. Мы говорим, что функция – это объект, позволяем передавать её как параметр в другие функции, возвращать в качестве значения, хранить массивы из функций. Такие функции назвали λ-функциями. Для них предусмотрен свой синтаксис, однако, на самом деле они являются чем-то похожим на анонимные делегаты. Более того, мы можем передавать λ-функциям в качестве параметра другие функции, таким образом организовывая функции высших порядков. Берём нашу коллекцию (или несколько коллекций), которая реализует IEnumerable<T>, пропускаем через цепочку λ-функций, и на выходе получаем то, что нам необходимо.

LINQ пытается решить проблемы манипуляций с данными вообще. Посмотрите, бизнес-объекты в ООП, и данные в реляционной БД (или XML) имеют разную природу, но им нужно как-то согласовываться. Этим «мостиком» и может служить LINQ. Мы строим декларативные запросы в своём языке, которые хранятся в виде «деревьев выражений», и исполняем их, когда нам это потребуется.

Для того, чтобы получить всё это в нефункциональном языке, требуется добавить в него множество расширений. А так же некоторые синтаксические «штуки», которые облегчат кодирование. Всё это будет рассмотрено далее. Это именно то, что появилось в C# 3.0.

Вывод типа

Вывод типа (type-inference) – возможность, широко используемая в динамически-типизируемых языках. Идея состоит в том, что мы можем не указывать тип переменной, ссылающейся на объект, если этот тип может быть однозначно определён из значения (или выражения). Мы можем писать так:

var s = "here i am!";
var i = 123;
var varlist = new List<double>();

Конечно помимо того, что этот синтаксис сокращает длину кода, он обладает и другими преимуществами, о которых будет рассказано позже. Согласитесь, удобнее вместо

MySuperLongClass<MyLongClass<int, string>> obj =
    new MySuperLongClass<MyLongClass<int, string>>()

Писать

var obj = MySuperLongClass<MyLongClass<int, string>>()

И тип переменной будет автоматически выведен из выражения. Однако при всём этом, С# остаётся строгим статически-типизируемым языком, и мы не можем позже присвоить нашей переменной объект другого типа.

Проверить, какой тип нам вывел компилятор, мы можем так:

Console.WriteLine(s.GetType());
Console.WriteLine(i.GetType());
Console.WriteLine(varlist.GetType());

Как видно, мы получили именно то, чего и ожидали:

System.String
System.Int32
System.Collections.Generic.List`1[System.Double]

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

var obj = null

Анонимные типы

Анонимные типы позволяют создавать экземпляры классов, которые мы не определяли ранее. Если нам «здесь и сейчас» нужен экземпляр какого-то класса, имя которого нам не важно, а важен только сам объект (и его свойства), мы воспользуемся следующей конструкцией:

var anonim = new { Name = "Inkognito", Age = 150 };

Вот что мы получим в итоге:

<>f__AnonymousType0`2[System.String,System.Int32]

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

Console.WriteLine("My name is {0}, and i am {1} years old!",
                  anonim.Name, anonim.Age);

Методы расширения

Идея возможности расширять классы новыми методами без использования наследования не нова. Например, такую же возможность можно встретить в Ruby в виде примесей (mixins). Смысл в том, что мы пишем метод вне зоны определения класса, который мы собираемся расширять, а потом можем использовать этот метод как «родной» для этого класса. При этом мы можем расширять классы, для которых у нас нет исходного текста, нам он не требуется, поскольку методы расширения помещаются в так называемых статических классах, которые предназначены специально для хранения таких методов.

Допустим, нам очень не хватает в стандартном классе String метода, который бы преобразовывал нашу строку в строку, написанную ЗаБоРоМ. Мы определяем статический класс, в котором будет содержаться необходимый нам метод:

public static class Util
{
    public static string Zaborom(this string s)
    {
          StringBuilder builder = new StringBuilder();
          bool upper = true;
          foreach (char c in s)
          {
              if (upper)
                 builder.Append(c.ToString().ToUpper());
             else
                 builder.Append(c.ToString().ToLower());
             upper = !upper;
          }
          return builder.ToString();
    }
}

Обратите внимание на одну особенность – аргумент метода: this string s. Ключевое слово this в этом контексте как раз и указывает на то, что данный метод является методом расширения для класса string. Мы можем вызывать его следующими способами.

// Обычным, как будто мы вызываем статический метод:
s = "he-he-he! hello world!";
Util.Zaborom(s)                 

// Либо как метод-расширение:
s = "he-he-he! hello world!";
s.Zaborom()                 

// Конечно, так тоже можно :)
"he-he-he! hello world!".Zaborom();

Результат во всех случаях будет аналогичный:

He-hE-He! HeLlO WoRlD!

Хотя методы расширения используются по полной в LINQ, они и сами по себе не менее полезны.

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

Методы расширений позволяют выполнять довольно интересные трюки, например такие:

static class MyUtils
{
    public static int Kilobytes(this int bytes)
    {
        return bytes * 1024;
    }
}                 

// ...                 

5.Kilobytes(); // -> 5120
2.Kilobytes() + 10; // -> 2058

Применительно же к контейнерам, мы можем написать единый вариант функции Count(), подсчитывающий количество элементов в контейнере, следующим образом:

public static class EnumerableUtil
{
       public static int MyCount(this IEnumerable enumerable)
       {
           int count = 0;
           foreach (var e in enumerable)
               count++;
           return count;
       }
}

Автоматические свойства

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

class Person
{
    public Person(string name, string lastname)
    {
        Name = name;
        Lastname = lastname;
    }                 

    public string Name { get; private set; }
    public string Lastname { get; private set; }
    public int Age { get; set; }
    public bool IsProgrammer { get; set;}
}

Get-теры и Set-теры свойств могут объявляться с модификатором private, что означает запрет на использование из вне класса. Теперь можно использовать их, как обычные свойства:

Person me = new Person("Vasya", "Pupkin");
me.Age = 200; // good
me.Name = "Inkognito"; // error! private setter

Инициализация свойств

Ещё одна вещь, которая часто встречается при работе с нашими объектами – это их инстанцирование. И зачастую, именно при создании нового экземпляра класса нам необходимо задать состояние нашего объекта перечнем его свойств. Для этого приходится делать широкие конструкторы со многими параметрами. Или отказаться от этой идеи, и установить некоторые свойства нашего объекта уже после создания (в смысле записи кода).

Инициализация свойств позволяет задавать значения свойств объекта прямо при его создании (имеется в виду запись кода, на самом же деле всё происходит немного по-другому). Вспомнив про класс Person из предыдущего раздела, мы может создать массив людей так:

var persons = new Person[]
{
    new Person("Harry", "Hacker") { Age = 30, IsProgrammer = true },
    new Person("Vinnie", "Pooch") { IsProgrammer = false, Age = 45},
    new Person("Kolo", "Bok") { IsProgrammer = true}
};

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

λ-выражения

λ-выражения (лямбда-выражения) – интересный механизм, который уже долгое время используется во многих языках программирования. Говоря неформально, λ-выражение – это функция, которую можно выполнять, передавать в качестве параметра, возвращать из метода и так далее. На самом деле λ-выражения в некотором виде уже присутствуют в C# 2.0 в виде замыканий анонимных делегатов:

int[] array = new[] { 1, 321, 234, 54, 34 };
var a = array.Where(
      delegate(int elem) { return elem % 2 == 0; }
);                 

foreach (var elem in a)
    Console.Write(elem + " ");

Получим:

234 54 34

В этом примере используется стандартный метод расширения Where, который выбирает из IEnumerable элементы, соответствующие некоторому условию. Здесь мы использовали анонимный делегат для представления предиката. Однако такая запись достаточно громоздка, и в LINQ был введён дополнительный синтаксис, так что аналогичное можно записать так:

var a = array.Where(elem => elem % 2 == 0);

На самом деле эта конструкция есть экземпляр Func<int,bool>. Func инкапсулирует метод, его аргументы и возвращаемое значение. Так, например, метод расширения Where мог быть реализован так:

public static IEnumerable Where<T>(
            this IEnumerable<T> enumerable,
            Func<T, bool> condition )
{
    foreach (T e in enumerable)
        if (condition(e))
            yield return e;
}

Func же может представляет из себя следующее:

delegate R MyFunc<T, R>(T arg);

С помощью λ-выражений в купе с методами расширения тоже можно развлечься следующим образом:

static class MyUtils
{
        public delegate void TimesDelegate(int i);
        public static void Times (this int n, TimesDelegate func)
        {
            for (int i = 0; i < n; i++)
                func(i);
        }
}                 

// ...                 

5.Times(
    i => Console.WriteLine(i)
);

Ленивые вычисления

Идея ленивых (lazy evaluation) или отложенных вычислений тоже родом из функциональных языков, а особенно из «чистых» функциональных языков. Чистые языки – это такие, в которых отсутствуют побочные эффекты вроде тех, что, при вызове одного и того же метода с одними и теми же параметрами несколько раз, он может возвращать разные значения. Большинство языков являются не чистыми. Даже более того, мы настолько привыкли к этому явлению, что и не замечаем его. Однако данная «нечистота» негативно сказывается на многих вещах: программы сложнее писать, сложнее отлаживать, поскольку наши методы могут вести себя непредвиденно. Однако рассмотрение отложенных вычислений и их отношение к чистоте языков программирования выходят за рамки данной статьи. C# не чистый, и даже в полной мере не функциональный язык, так что ленивые вычисления в нём могут использоваться лишь в ограниченном круге задач.

Не стоит думать, что раз вычисления ленивые, то их нужно уговаривать выполнить их работу. Согласно идее ленивых вычислений, вычисления следует откладывать до того момента, пока их результат действительно не понадобится. В императивных языках часто бывает так, что мы вычисляем некоторое значение, а потом оно никогда не используется – время тратится зря. На самом деле какая-то часть «ленивости» присутствует в некоторых языках, например, C++, при проверке условий вроде

If (a == true && b == 123)

Если a принимает значение false, то условия для b не проверяется, поскольку не влияет на результат.

Вернёмся к C#. На самом деле мы уже использовали ленивость в предыдущих разделах, когда рассматривали λ-выражения и методы расширения. Вспомним пример с нахождением чётных чисел:

var a = array.Where(elem => elem % 2 == 0);

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

foreach (var elem in a)
       Console.Write("{0}, ", elem);

Увидеть это можно, внеся изменения в Where (Не забудьте поместить его в какой-нибудь статический класс):

public static IEnumerable MyWhere<T>(
            this IEnumerable<T> enumerable,
            Func<T, bool> condition )
{
    foreach (T e in enumerable)
        if (condition(e))
        {
            Console.Write("(Мы в фильтре)");
            yield return e;
        }
}

Теперь следующий код

int[] array = new[] { 1, 321, 234, 54, 34 };           

Console.WriteLine("Фильтруем");
var lazy = array.MyWhere(elem => elem % 2 == 0);            

Console.WriteLine("Распечатываем");
foreach (var elem in lazy)
       Console.Write("{0}, ", elem);

Даст такой вывод:

Фильтруем
Распечатываем
(Мы в фильтре)234, (Мы в фильтре)54, (Мы в фильтре)34,

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

Запросы

Вооружившись знанием из предыдущих разделов, напишем LINQ-запрос. Возьмём знакомый нам массив людей:

var persons = new Person[]
{
    new Person("Harry", "Hacker") { Age = 30, IsProgrammer = true },
    new Person("Vinnie", "Pooch") { IsProgrammer = false, Age = 45},
    new Person("Kolo", "Bok") { IsProgrammer = true}
};

И найдём полные имена и возраст тех, кто является программистом, отсортируем по возрасту (не стоит забывать, что аналогичный запрос работал бы и с любой коллекцией из Person, которая реализует IEnumerable<Person>):

var programmers = persons.Where(p => p.IsProgrammer)
        .Select(p => new { Fullname = p.Name + " " + p.Lastname, p.Age })
        .OrderByDescending(p=>p.Age);

Видно, здесь использован и вывод типа, и анонимный класс, и λ-функции, и отложенные вычисления, поскольку на самом деле выборка будет производится при непосредственном доступе к полученной коллекции программистов:

Console.WriteLine("Programmers: ");
foreach (var p in programmers)
    Console.WriteLine("{0}, {1} years old", p.Fullname, p.Age);

Примечательно, мы тут мы можем использовать специальный SQL-подобный синтаксис, так, предыдущий запрос перепишем так:

var programmers = from p in persons
                  where p.IsProgrammer
                  orderby p.Age descending
                  select new { Fullname = p.Name + " " + p.Lastname,
                               p.Age };

Чего мы и добивались. Однако это не всё, нам доступен весь спектр нужных вещей, например, таких как объединения (join). Допустим, у нас есть следующие классы:

public class Customer
{
      public int Key;
      public string Name;
}                 

public class Order
{
      public int CustomerKey;
      public string What;
}

И массивы (Обратите внимание на запятую после последнего элемента массива, синтаксической ошибки не возникает. Это сделано для того, чтобы было легче добавлять новые элементы в конец):

var customers = new Customer[]
{
    new Customer { Key=1, Name="Vasya"},
    new Customer { Key=2, Name="Petya"},
    new Customer { Key=12, Name="Vova"},
};                 

var orders = new Order[]
{
    new Order { CustomerKey=1, What="Book" },
    new Order { CustomerKey=1, What="Clock" },
    new Order { CustomerKey=2, What="Pen"},
    new Order { CustomerKey=12, What="Phone"},
};

Вот простой Join-запрос:

var q = from c in customers
        join o in orders on c.Key equals o.CustomerKey
        select new { c.Name, o.What };

Даст нам следующее:

{ Name = Vasya, What = Book }
{ Name = Vasya, What = Clock }
{ Name = Petya, What = Pen }
{ Name = Vova,  What = Phone }

Мы так же можем использовать группировку (group by), аггрегирование (например, Sum(), Count()), и другие функции, полный список которых вы можете найти в документации по LINQ.

Деревья выражений

Как было сказано, в LINQ λ-функции представляються в виде деревьев выражений. Мы можем создавать λ-выражения динамически. Класс Expression<T> (который находится в System.Linq.Expressions) представляет собой такое дерево выражений. Взгляните на следующий пример:

Expression<Func<int, int>> square = p => p * p;

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

var square = square_expr.Compile();
square(5); // -> 25

А вот как мы можем построить дерево выражений вручную, используя статические методы из Expression:

ParameterExpression pe = Expression.Parameter(typeof(int), "num");
Expression<Func<int, int>> mysquare =
        Expression.Lambda<Func<int, int>>(
            Expression.Multiply(pe, pe),
            new ParameterExpression[]{ pe}
        );
mysquare.Compile()(5); // -> 25

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

Заключение

С каждой версией C# впитывает всё больше новых расширений. В данной статье было рассмотрено относительно проблемно-ориентированное расширение LINQ, которое помогает легче взаимодействовать с миром данных. Да и сами по себе новые возможности C# 3.0 достаточно полезны. Чтобы идти в ногу со временем, языки эволюционируют, впитывая в себя современные идеи. Наверное, не стоит думать, что то, что принесёт нам LINQ – это решение всех проблем, связанных с манипуляциями данными. Однако данная технология представляется достаточно элегантным «связующим звеном». Интересно будет понаблюдать за тем, как она приживётся, вытерпит ли проверку временем, и во что выльется в дальнейшем.

Реклама

16 ответов на “C# 3.0 и LINQ: для тех, кто ещё не в курсе

  1. Уведомление: Хороший обзор по нововведениям в C# из 2008-й студии « SilverCloud’s Weblog

  2. Уведомление: ParallelFX, или как я испытывал параллельности « Bits of Mind

  3. Ребята.Чем глубже изучаю джаву то понимаю, что Майкрософт все содрала у джавы и плюсов. Хоть бы рас придумали что-то свое. А то нет, надо содрать

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

  5. 2AlexDev
    баран ты, парниша. Это не тот случай где нужно бороться за оригинальность. То, что твоя джава содрана с крестов тебя не смущает?

  6. Была вычислительная задача с большим количеством данных. Делал на C#.

    Пока хранил данные в коллекциях, поддерживающих IEnumerable — скорость каждого цикла вычислений исчислялась минутами. Это не устраивало, так как таких циклов предполагалось делать несколько десятков. После того, как поместил данные в обычные массивы — скорость возрасла в ДЕСЯТКИ раз.

    Мораль сей басни такова: все эти красивые оберточки вроде LINQ хороши для нересурсоемких задач. Как только надо будет сделать что-то серьезное — придется от этих рюшечек отказываться и вспоминать основы.

  7. Уведомление: C# 3.0 и LINQ: для тех, кто ещё не в курсе – Korobchinskiy's blog

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s