Posts Tagged microsoft

LPT мигает светодиодом

Эту статью я написал года четыре назад. А сейчас внезапно нашёл её, и решил выложить в блог. Довольно забавно :) Предупреждаю, я не несу ответственности за то, что может произойти с вашей техникой в результате применения данной информации.

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

Задача стоит весьма тривиальная: научиться управлять мерцанием светодиода, подключённого к ПК через LPT-порт. Почему именно LPT? Потому что он довольно прост и в меру интересен.
Поехали!

Подготовка

Итак, что нам нужно для воплощения этого ужаса в реальное существо:

  • ПК
  • Компилятор какого-нибудь языка программирования (Assembler, С, С++, Pascal, etc…).
  • Некоторый программный инструментарий
  • Светодиод на 5В
  • LPT-шнур

Шнур у меня был только разрезанный, но вы можете использовать любой, главное всё правильно соединять. Вот мой шнур, с уже выведенными контактами для подключения светодиода (об этом речь пойдёт далее):

image

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

image

Железо

Прежде чем приступить к практике, немного теории.

Как работаете LPT-порт? Об этом достаточно много написано, однако я всё-таки кратко расскажу как обстаят дела.

Параллельный порт ПК обычно используется для подключения принтеров, но на этом его возможности не ограничиваются. К нему можно подключать любое внешнее, самодельное устройство. LPT-порт имеет 25 пинов, но не все 25 необходимы. В нашем примере, например, нужно только 2. Рассматривать предназначение всех не будем.

image

Подключать светодиод будем плюсом к 2 пину, а минусом – к пину 18. Смотрите, не перепутайте, в противном случае светодиод может сгореть. Лучше предварительно проверить где у него плюс, где минус на маломощной батарейке.

Если у вас шнур папа-мама, то есть при подключении к компьютеру, на вашем конце шнура остаются входы, дело простое – просто воткнуть в нужные пины диод. Следует быть осторожным, неправильно подключив, можно повредить LPT-порт.

image

Лично я, раскурочив шнур, определил, какой относится к D0, какой к земле, и у меня всё выглядит примерно так, как показано на следующем рисунке. Я сидел с пробником, и вычислял, какая линия относится к D0. Очень занимательно! Вы можете найти более удочное решение.

image

В мерах предосторожности можно последовательно к диоду припаять сопротивление. Рекомендуют 470 Ом. На счёт заземления: кто-то заземляет не на пин GRD, а на корпус коннектора, но я предпочитаю пин.

На этом этапе вы должны представлять себе, как можно подключить светодиод к LPT-порту.

Программная часть

Подключить светодиод к компьютеру мы готовы, но вот, дальше-то что? А дальше самое интересное: управление им с бортового пульта. С одним светодиодом много не сделаешь: только включать/выключать его можно. Но зато можно менять частоту мигания, можно подключить ещё 7 светодиодов к остальным пинам D1-D8, и сделать светомузыку. Или приобрести яркие белые светодиоды, и сделать настольную лампу, питающуюся от LPT-порта, которая включается, когда вы начинаете печатать, чтобы подсветить вам клавиатуру. Можно вывести зелёненький светодиод куда-нибудь на видное место и написать программу, мигающую им, когда вам на E-mail приходит письмо. Сколько всего можно сделать только лишь с использованием светодиода! А подковав себя в электронике, можно собирать полноценные внешние устройства, но это, к сожалению, выходит за рамки данной скромной статьи.

Управление LPT-портом зависит от ОС. В былые времена, операционные системы DOS и Windows 95/98 разрешали пользовательским программам напрямую иметь доступ к железу. С появлением Windows NT/2000/XP всё изменилось, теперь общение с портами напрямую пользовательским программам запрещено, а разрешено только коду, выполняющемуся в режиме ядра. Всё это сделано в целях защиты и в других нужных и полезных целях, однако всё это одновременно и усложняет нашу задачу по подмигиванию светодиодом. Нам пришлось бы писать специальный драйвер устройства, разбираться в HAL (Hardware Abstraction Layer) и прочих премудростях. Вы можете заняться этим, почитав материалы по Microsoft DDK (Device Driver Kit). Но существует несколько обходных путей, позволяющих напрямую общаться с нашим LPT. Вообще, так делать не рекомендуется, но всё же, мы сделаем именно так, ибо так проще и нагляднее. Другими словами, напрямую получить доступ к регистрам LPT-порта просто так не удастся. Вы можете работать с драйвером устройства LPT как с обычным файлом при помощи функций CreateFile, ReadFile, WriteFile, а так же получать некоторую информацию о состоянии устройства функцией DeviceIoContol. Но работать с регистром, например, D0, который включает наш светодиод, не получится.

В ОС Linux всё обстоит по-иному. Там можно делать всё просто, имея на то специальные права. Необходимо только узнать по какому адресу находится ваш LPT. Обычно он находится по адресу 0378h. Узнать адрес в вашей системе можно, просмотрев файл /proc/ioports. Хотя, проще работать с файлом устройства /dev/lp0.

Разберём, как справиться с ОС Windows, потому что это немного сложнее.

Перед тем, как перейти непосредственно к программированию своими силами, проверим, работает ли всё это дело, собранное в прошлой части статьи. Для этого я использую чудесную программу мониторинга параллельного порта, так и названную: Parallel Port Monitor by Neil Fraser.

Как делаю я: запускаю программу, выключаю в ней все пины с 2 до 9, затем подключаю светодиод к LPT-порту (в нашем случае во второй пин, в D0). После этого в Parallel Port Monitor подаю единицу на D0. Светодиод должен загореться. Если ничего не произошло, возможно, вы неправильно его подключили или же в программе выбрали не тот LPT-порт. Попробуйте LPT1, LPT2, LPT3, если у вас их несколько.

Светодиод мигает? Отлично. Теперь можно побаловаться с ним своим программным кодом. Как было сказано ранее, это делать мы будем обходным путём. Если вы используете Windows 95/98/ME, можете сразу перейти к написанию программы, обходные пути вам не нужны, эти версии ОС Windows позволяют напрямую обращаться к портам.

1) Использование драйвера UserPort

Программа UserPort (автор Tomas Franzon), это системный драйвер режима ядра для Windows NT/2000/XP, который позволяет обращаться к портам ввода-вывода напрямую. Как раз то, что нам необходимо. Найти её в любом поисковике не составит труда. Скачав, настроив, согласно документации и запустив, мы получаем возможность мигать светодиодом из наших программ.

Сперва определим адреса портов в Device Manager. Видим, LPT1 по адресу 0378-037F. Именно туда мы и будем писать биты управления светодиодом. Пишем 1 – на контакты светодиода подаётся напряжение +5В, он загорается, пишем бит 0 – светодиод гаснет.

image

Напишем тестовую программу, мигающую светодиодом раз в секунду. Исходный текст приведён ниже. Здесь я использовал С++ со вставками ассемблерного кода и компилятор Visual C++. Вы можете использовать свой любимый язык программирования и компилятор, суть не меняется.

#include <iostream>
#include <windows.h>

void doLight(bool on)
{
	__asm
	{
		mov DX,0378h
		mov AL,on
		out DX,AL
	}
}

int main()
{
	while(1)
	{
		doLight(true);
		std::cout<<"Light On!"<<std::endl;
		Sleep(1000);
		doLight(false);
		std::cout<<"Light Off!"<<std::endl;
		Sleep(1000);
	}
	return 0;
}

Компилируем, запускаем, проверяем. Если светодиод стал мигать, значит всё ОК, всё работает, и можно издеваться дальше. Если же появилось сообщение об ошибке, например «First-chance exception at 0×00411524 in program.exe: 0xC0000096: Privileged instruction», значит, вы неправильно запустили или настроили UserPort. Обратитесь к документации по нему, там есть пример.

2) Использование библиотеки inpout32.dll.

Домашняя страница разработчиков библиотеки: http://www.logix4u.net. Там же можно найти много полезной информации. Перед тем как писать код, поместим в каталог с нашим проектом файлы inpout32.dll и inpout32.lib. В этих библиотеках имеются функции, позволяющие читать и записывать в порты.

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

#include <iostream>
#include <windows.h>

#pragma comment(lib, "inpout32.lib")

// прототип функции
void _stdcall Out32(short PortAddress, short data);

void doLight(bool on)
{  Out32(0x378, on);
}

int main()
{   while(1)   {   doLight(true);   std::cout<<"Light On!"<<std::endl;   Sleep(1000);   doLight(false);   std::cout<<"Light Off!"<<std::endl;   Sleep(1000);   }  return 0;
}

Вот так вот:

image

Вроде, всё. Идею можно развить, и сделать какие-нибудь более полезные или интересные программные решения.

Заключение

Как видно, LPT-порт один из наиболее простых в работе портов, и с ним можно довольно-таки интересно работать. Умельцы собирают и подключают роботов через LPT-порт, делают в некотором роде систему управления квартирой, включают/выключают свет в комнате с компьютера и ещё много интересных вещей.

P.S.: Я где-то слышал, что люди собирают LPT- и USB-фонарики, и решил себе сделать нечто подобное из старого микрофона от наушников. Вот что вышло:

image

.NET, cc, e-mail, Hardware

ParallelFX, или как я испытывал параллельности

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

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

Не так давно был анонсирован CTP библиотеки, находящийся на верхушке .NET Framework – Parallel FX Library. Наверняка, с её помощью можно будет быстрее писать многопоточные программы, которые к тому же будут менее подвержены ошибкам. Суть в том, что наш код автоматически распараллеливается на множестве имеющихся процессоров. Это чем-то похоже на распараллеливание запросов, которое делает СУБД, но здесь мы имеем это в нашем коде и с нашими объектами.

ParallelFX состоит из двух частей: PLINQ и TPL. PLINQ – это движок параллельного выполнения запросов для LINQ (Более подробно про LINQ в вы можете почитать, например, в моей статье), благодаря которому мы можем с лёгкостью распараллелить LINQ-запросы. TPL же вводит такие конструкции, как параллельные циклы; задачи (Task, маленькие части кода, которые могут быть выполнены независимо, они чем-то похожи на Thread, но легче синхронизируемые), и «будущие времена» (Future, специальная задача, которая возвращает результат). Стоит отметить, что информации по библиотеке совсем мало, а та, что есть, уже немного устарела, поскольку всё это ещё находится в разработке и меняется…

Что к чему

Для того, чтобы испытать PFX в действии вам потребуется .NET Framework 3.5, а так же PFX December 2007 CTP, который вы можете закачать отсюда. Если вы не поклонник сугубо текстовых редакторов и сборки из мейкфайлов, вам так же потребуется Visual Studio 2008. Так же было бы неплохо иметь доступ к многопроцессорному компьютеру, чтобы проверить всё собственноручно.

Добавьте ссылку на сборку System.Threading.dll, так, все необходимые нам классы находятся в пространстве имён System.Threading.

PLINQ

Допустим, у нас имеются какие-то сложные и большие LINQ-запросы. А машина, на которой исполняется наша программа – многоядерная. Так, мы хотим использовать каждое ядро по максимуму с минимумом затрат времени и сил. PLINQ – то, что нам поможет. Мы просто вызываем метод расширения AsParallel() для наших данных, и они «обвёртываются» чем-то, что знает, как всё распараллелить.

IEnumerable<T> data = ...;
var q = from a in data.AsParallel() where w(a) orderby o(a) select f(a);

Так же нам доступен «параллельный» аналог класса Enumerable – ParallelEnumerable с аналогичными статическими методами.

Таким образом, PLINQ помогает LINQ-запросам работать быстрее и обрабатывать большее количество данных, используя доступные процессоры.

TPL и Параллельные циклы

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

for (int i = 0; i < N; i++) {   a[i] = Math.Sqrt(a[i]);
}

Мы можем попросить нашу библиотеку распараллелить его. Для этого мы пользуемся параллельной версией цикла for – статической функцией из класса Parallel:

Parallel.For(0, N, i => {   a[i] = Math.Sqrt(a[i]);
});

Так, если у нас двуядерная машина, то половина итераций будет выполняться на одном ядре, тогда как другая половина – на другом. Библиотека адаптируется к конкретному компьютеру, и распараллеливает наш код на доступное число процессоров. На однопроцессорной машине настоящего распараллеливания не получится, и данный цикл выльется в простой последовательный.

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

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

Считаем Pi

Решив проверить параллельные циклы TPL в действии, я написал, возможно, не самую лучшую реализацию алгоритма вычисления числа Пи по формуле Лейбница. Но, тем не менее, при помощи этой программы можно наглядно увидеть кое-какие интересные результаты. Напомню, мы можем высчитать Пи следующим образом:

Pi/4 = 1/1 - 1/3 + 1/5 - 1/7 + 1/9 - ...

Последовательное вычисление можно организовать так:

public double CalculatePi()
{   double sum = 0;   int sign = 1;   for (int i = 1; i < ITER_COUNT; i += 2, sign = -sign;)   sum += (double)sign / i;   return 4 * sum ;
}

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

В следующем коде присутствуют некоторые причудливые синтаксические конструкции из новой версии С#, так что для понимания, что же здесь происходит рекомендую сперва ознакомиться с C# 3.0. Здесь так же используется перегрузка параллельного цикла For с шагом и состоянием потока.

const int ITER_COUNT = 100000000;
const int INNER_ITER_COUNT = 1000;     

public double CalculatePi_ParallelFor()
{   double sum = 0;   Parallel.For<double>(1, ITER_COUNT, INNER_ITER_COUNT, () => 0,   (i, state) =>   {   int sign = (i / 2) % 2 == 0 ? 1 : -1;   for (int j = i; j < i + INNER_ITER_COUNT; j += 2, sign=-sign)   state.ThreadLocalState += (double)sign / j;   }, partialSum => { lock (this) {sum += partialSum;} }   );   return 4 * sum;
}

Здесь в каждую параллельную итерацию внешнего цикла, помимо счётчика передаётся переменная состояния state, в её свойстве ThreadLocalState мы сохраняем «частичную сумму». Впоследствии все частичные суммы добавляются к общей сумме sum, формируя окончательный результат. Я запустил оба варианта на компьютере с четырёхядерным процессором, и вот что из этого вышло:

D:\>ParallelPi.exe
Calculating Pi in total 1000000000 iterations
And 1000 inner iterations in each process
OS: Microsoft Windows NT 5.2.3790 Service Pack 2
Processors count: 4     

Working Non-Parallel For...
> 3,14159265
00:00:07.3516085     

Working Parallel For...
> 3,14159265
00:00:03.9321267     

Time rating:
1. 00:00:03.9321267     Parallel For
2. 00:00:07.3516085     Non-Parallel For

Параллельная версия почти в двое быстрее последовательной:

image

Обратите внимания на график загрузки ядер: при параллельной обработке все четыре ядра используются на 100%, в то время как при последовательном исполнении процессор загружен на ~25%.

image

Запустив этот же расчёт на своей однопроцессорной машине, я получил следующий результат:

Calculating Pi in total 1000000000 iterations
And 1000 inner iterations in each process
OS: Microsoft Windows NT 5.1.2600 Service Pack 2
Processors count: 1    

Working Non-Parallel For...
> 3,14159265
00:00:04.4050481    

Working Parallel For...
> 3,14159265
00:00:09.2341356    

Time rating:
1. 00:00:04.4050481     Non-Parallel For
2. 00:00:09.2341356     Parallel For

Последовательная версия оказалась в два раза быстрее «параллельной». Наверняка так получилось из-за того, что затраты на организацию параллелизма (а как его получить на однопроцессорной машине?) не окупились из-за отсутствия реального распараллеливания.

image

Заключение

На мой взгляд, технология ParallelFX весьма интересна. В виду нынешнего расцвета многоядерных процессоров идея параллельности становится как нельзя актуальнее. С появлением технологии LINQ, и функциональных расширений для C# и других языков, что-то подобное обязательно должно было появиться. Ну что ж, посмотрим, что будет дальше…

Мне было бы очень интересно услышать о ваших экспериментах с PFX, если вы решите испытать её :).

.NET, CSharp, framework, LINQ

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 – это решение всех проблем, связанных с манипуляциями данными. Однако данная технология представляется достаточно элегантным «связующим звеном». Интересно будет понаблюдать за тем, как она приживётся, вытерпит ли проверку временем, и во что выльется в дальнейшем.

.NET, CSharp, framework, Functional Programming