воскресенье, 4 октября 2009 г.

LINQ. First vs. Single. Часть вторая.

Неожиданно предыдущий пост про LINQ породил несколько вопросов, которые в свою очередь породили еще несколько :) О них-то мы сегодня и поговорим.

Основная мысль того поста была настолько очевидной, что я даже сомневался, писать или нет. Когда же пост уже был написан, решил сделать его хоть немного менее унылым, снабдив исходниками реализаций методов First и Single LINQ To Objects. При этом реализация метода Single мне показалась неоптимальной. За комментариями по этому поводу я обратился на форумы MSDN. К сожалению, я не сразу понял мысль коллег про "exceptional cases"... но в целом дискуссия получилась довольно конструктивной.

Итак, напомню, как выглядел код, полученный при помощи Reflector'a:

TSource local = default(TSource);
long num = 0L;
foreach (TSource local2 in source)
{
if (predicate(local2))
{
local = local2;
num += 1L;
}
}
long num2 = num;
if ((num2 <= 1L) && (num2 >= 0L))
{
switch (((int) num2))
{
case 0:
throw Error.NoMatch();

case 1:
return local;
}
}
throw Error.MoreThanOneMatch();

А почему бы не добавить в тело условия if (predicate(local2)) проверку на равенство num двум:

TSource local = default(TSource);
long num = 0L;
foreach (TSource local2 in source)
{
if (predicate(local2))
{
local = local2;
num += 1L;
if (num == 2)
throw Error.MoreThanOneMatch();
}
}
long num2 = num;
if ((num2 <= 1L) && (num2 >= 0L))
{
switch (((int) num2))
{
case 0:
throw Error.NoMatch();

case 1:
return local;
}
}

С одной стороны, этот код привносит условие, которое тоже требует машинного времени для своего выполнения. Но, во-первых, условие if (num == 2) - элементарное, а во-вторых, при любом раскладе оно выполнится не более двух раз. Что же мы при этом экономим? Экономим мы k проходов Enumenator'а (который лежит в основе foreach) и столько же выполнений предиката (только в случае наличия дубликатов, ибо если элемент уникальный, нам все равно придется пройти коллекцию полностью). Очевидно, что стандартная реализация проигрывает.

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

В целом, конечно, данный вопрос представляет собой, в большей степени, академический интерес (кстати, неплохая иллюстрация закона дырявых абстракций): на практике в 99.9% случаев проблем из-за текущей реализации метода Single не будет. Если же для Вас метод Single стал проблемой, замените его на собственный extension method.

На этом с LINQ To Objects на сегодня закончим - поговорим о LINQ To Entities и его реализации метода Single. Напомню, что в EF v1 метод Single не поддерживался: приходилось либо довольствоваться First, либо использовать разные костыли, например, вот этот. Проблема приведенного по ссылке workaround'а в том, что при вызове query.Count из базы будут возвращены все удовлетворяющие запросу записи (в том числе и дублирующиеся). В случае, если мы обращаемся к "тяжелым" данным, результат может быть плачевным.

Так как же с Single обстоят дела в EF 4? Добравшись до VS 2010, я был приятно удивлен - работает. Но маленький параноик в моей голове убедил меня (спасибо ему за это) открыть Profiler и посмотреть на генерируемый SQL-код. А вот и он:

SELECT TOP (2)
[Extent1].[AddressId] AS [AddressId],
[Extent1].[City] AS [City],
[Extent1].[Street] AS [Street],
[Extent1].[PostalCode] AS [PostalCode],
[Extent1].[Apartment] AS [Apartment]
FROM [dbo].[Addresses] AS [Extent1]
WHERE 1 = [Extent1].[AddressId]

Данный код при наличии дубликатов возвращает две записи. Это, конечно, чуть лучше, чем решение, обсуждавшееся выше (там возвращалось неограниченное число дубликатов), но хотелось бы большего: хотелось бы, чтобы вопрос наличия дубликатов решался на стороне СУБД (например, при помощи подзапросов), а возвращалась бы либо одна (уникальная) запись, либо вообще ничего (если есть дубликаты).

За комментариями я в очередной раз обратился на форумы MSDN (прекрасное место :) ). Прежде всего, меня интересовало, финальное ли это решение для EF 4. Как Вы можете судить по ответу, шансов на то, что это поведение изменится до релиза .NET 4, мало.

На этой печальной ноте и закончим сегодняшний разговор о LINQ.

суббота, 3 октября 2009 г.

Знакомимся с IKVM.NET

В статье "Сравнительный анализ фреймворков для работы с онтологиями под .NET и Java" я уже упоминал проект IKVM.NET. Cегодня я хотел бы поговорить о нем подробнее.

IKVM.NET предоставляет поддержку Java для Mono и .NET, включает в себя JVM, рализованную на .NET, а также interoperability-средства между Java и .NET (с акцентом на использование Java в .NET, а не наоборот).

3 основных варианта использования IKVM.NET:

1. Drop-in JVM (ikvm.exe): Позволяет динамически запускать Java приложения под .NET.
2. Use Java libraries in your .NET applications (ikvmc.exe): Статически компилирует jar'ы в .NET-сборки.
3. Develop .NET applications in Java (ikvmstub.exe): Позволяет использовать .NET-классы при разработке на Java (при условии, что затем Java-код будет скомпилирован в .NET при помощи ikvmc.exe).

Несмотря на то, что текущая версия проекта "всего лишь" 0.40, он уже очень многое умеет. Самое узкое место - графический интерфейс: конвертация AWT и Swing в WinForms (ни о Java FX, ни о WPF пока ни слова) - очень трудоемкая задача. К тому же, по заявлению разработчиков, приоритет этой задачи низкий (хотя, насколько я могу судить по их блогу, в последнее время деятельность в этом направлении существенно активизировалась). Полностью разделяю позицию разработчиков относительно GUI и считаю гораздо более ценной миссию "портирования" бесчисленного множества Java-библиотек в .NET. В конце концов, имея backend, всегда можно прикрутить какой-никакой интерфейс, а вот наоборот - вряд ли.

Быстродействие и потребление памяти IKVM.NET можно измерить, как минимум, двумя способоами: сравнить показатели 1. "чистой" Java и конвертированной под .NET 2. конвертированного под .NET кода и "чистого" .NET кода. В первом варианте используются разные платформы, поэтому многое зависит от деталей реализации (чего только различия в сборщиках мусора стоят). А вот второй вариант более объективен (при как можно близких реализациях тестовых программ под Java и .NET). По заверениям разработчиков, быстродействие (вероятно, использовался второй подход) может падать в 1.5-2 раза... хотя на форумах я встречал и свидетельства об ускорении работы приложений (видимо, здесь для сравнения использовался первый подход). Вообще при перекомпиляции происходит много интересных метаморфоз: так, например, мне приходилось сталкиваться с тем, что не работающий при некоторых условиях Java-код отлично работал после компиляции в .NET.

А не костыль ли это? В определенной степени - да. Первое, что бросается в глаза при начале использования конвертированных Java-классов, - несоответствие code agreement'ов в Java и .NET. Из-за этого код становится немного "грязнее". Но это мелочь по сравнению, например, с тем фактом, что в любой момент Вы можете натолкнуться на критичную для Вас возможность, которая еще не реализована. Но, с другой стороны, а какие есть альтернативы? Использовать другие interoperability-средства (возможно, еще менее надежные) или переписывать необходимый Java-фреймворк под .NET? Сложный выбор. Если Вам потребуется функциональность IKVM.NET, тщательно взвесьте все за и против, ибо последствия принятого решения могут оказаться очень серьезными. Я свой выбор сделал (по крайней мере для текущего проекта) и пока не разочаровался.

С огромным уважением отношусь к разработчикам такого технического сложного проекта. It is really rocket science! Заставить две разные платформы (которые похожи лишь с высоты орлиного полета) работать вместе - это дорогого стоит.

Я хотел было материализовать свою благодарность разработчикам в виде "пожертвования"... но не нашел традиционной кнопки Donate на официальном сайте. Списавшись с разработчиками, получил ответ:

Hi Alexander,

A donation is not necessary, but your mail is appreciated!

Regards,
Jeroen

Вот так.

На этом знакомство с IKVM.NET будем считать оконченным. Если возникнет интерес, в следующий раз рассмотрим применение IKVM.NET на практике.

пятница, 2 октября 2009 г.

LINQ. First vs. Single

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

Для получения первого элемента, удовлетворяющего предикату, в LINQ используются два основных метода: First и Single. В различных реализациях LINQ детали могут меняться, поэтому для определенности рассмотрим логику этих методов на примере LINQ To Objects: First просто возвращает первый элемент (а если его нет - генерирует исключение), а Single возвращает единственный элемент (а исключение генерирует не только если элементов нет, но и если их больше одного). Однако несмотря на то, что названия методов вполне отражают различия между ними, порой программисты относятся к этим двум методам как к равноценным и взаимозаменяемым.

Давайте немного углубимся в алгоритмы работы методов: First находит первый элемент - и возвращает его, Single же на этом не останавливается и продолжает поиск второго элемента, дабы либо убедиться, что его нет, либо сгенерировать исключение, если он все же есть. Как следствие, First всегда быстрее Single. Таким образом, для получения первого элемента всегда нужно использовать First, Single же пригодится лишь в тех случаях, когда необходимо быть уверенным, что полученное значение является единственным (как бы банально это ни звучало, порой об этом забывают).

Вооружимся Reflector'ом и от теории перейдем к практике.

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

foreach (TSource local in source)
{
if (predicate(local))
{
return local;
}
}
throw Error.NoMatch();

и Single:

TSource local = default(TSource);
long num = 0L;
foreach (TSource local2 in source)
{
if (predicate(local2))
{
local = local2;
num += 1L;
}
}
long num2 = num;
if ((num2 <= 1L) && (num2 >= 0L))
{
switch (((int) num2))
{
case 0:
throw Error.NoMatch();

case 1:
return local;
}
}
throw Error.MoreThanOneMatch();

Если с методом First все ясно, то к реализации метода Single у меня есть вопросы. В ходе цикла num не проверяется на равенство двум, поэтому выполнение цикла в любом случае продолжается до конца коллекции. Сразу представляется параноидальная ситуация, когда Single'ом с "тяжелым" предикатом обрабатывается огромная коллекция с объектами с "тяжелой" перегрузкой Equals, и все ужасно тормозит... Если в комментариях не последует оперативного разъяснения (может, я чего проглядел, а может, это известная "фича"), обращусь за оным в MSDN'овские форумы.

В заключение скажу пару слов об Entity Framework, а точнее о LINQ To Entities.

В EF v. 1 LINQ To Entities не поддерживал метод Single. Особенно неожиданным это открытие становилось для тех, кто переходил с, казалось бы, менее функционального LINQ To SQL: EF оказался заложником своей универсальности (по сравнению с заточенным под одну СУБД LINQ To SQL). Хотя на практике горевать по этому поводу приходилось редко (особенно на фоне более "выдающихся" ограничений EF v. 1), потому что, как правило, такие запросы выполняются по первичному ключу, уникальность которого гарантируется на уровне БД. Сейчас нет под рукой VS 2010... но как доберусь до нее - проверю, реализовали ли эту возможность (соответствующие планы у команды EF были) в текущем public-build'е EF4 и обновлю пост.

Update. Продолжение в этом посте.