Senior .Net Developer
4,8
рейтинг
31 января 2013 в 22:32

Разработка → Библиотека morelinq: то, чего не хватает в LINQ to Objects из коробки

C#*, .NET*
Я думаю многим читателям блога .Net знакомо имя Jon Skeet. Особенно после вчерашнего прошлогоднего поста юзера SergeyT. Поэтому я не буду повторять про сравнение с Чаком Норрисом и первое место по карме на StackOverflow.com. А вот упомянуть лишний раз про его замечательную книгу “C# In Depth” точно лишним не будет. Центральное место в ней занимает LINQ вообще и LINQ to Objects в частности. Джон очень обстоятельно описывает все возможности языка C# и платформы .Net, которые сделали возможным появление LINQ в его нынешнем виде, а также подробности его реализации. Именно после прочтения этой книги я стал активно использовать LINQ to Objects в своих проектах. Однако в стандартной библиотеке не хватает нескольких крайне нужных операторов. К счастью, Джон Скит исправил это недоразумение. Так появилась небольшая, но очень полезная библиотка morelinq. А с конца прошлого года она доступна в виде NuGet-пакета.

Операторы библиотеки morelinq


Batch Превращает одну последовательность в несколько последовательностей по n элементов.
Concat Присоединяет элемент к коллекции либо коллекцию к элементу.
Consume «Поглощает» коллекцию, не производя никаких действий над элементами.
DistinctBy Возвращает только уникальные элементы (по заданному критерию).
EquiZip Создает новую последовательность, где каждый элемент создается на основе соответствующих элементов исходных последовательностей. Если последовательности имеют различное количество элементов, будет брошено исключение InvalidOperationException.
ExceptBy Возвращает элементы первой последовательности, которые не содержатся во второй (по заданному критерию).
ForEach Выполняет действие над каждым элементом последовательности.
Generate Генерирует последовательности по начальному элементу и функции-генератору.
GenerateByIndex Генерирует последовательность по индексам элементов.
GroupAdjacent Подобен GroupBy, но в группу попадают только идущие подряд элементы.
Index Возвращает последовательность пар индекс-значение.
MaxBy Возвращает максимальный элемент последовательности по заданному критерию.
MinBy Возвращает минимальный элемент последовательности по заданному критерию.
Pad Если количество элементов последовательности меньше заданного, дополняет последовательность значениями по умолчанию до заданного количества.
Pairwise Возвращает последовательность результатов функции текущего и предыдущего элемента (не применяется к первому элементу).
Pipe Возвращает исходную последовательность, выполняя Action над каждым элементом.
Prepend Дополняет начало коллекции заданным элементом.
PreScan Возвращает последовательность исходной длины, в которой N-й элемент определяется применением заданного преобразования к N-1 элементов.
Scan Возвращает последовательность исходной длины, в которой N-й элемент определяется применением заданного преобразования к N элементов.
SingleOrFallback Возвращает единственный элемент последовательности либо результат заданного делегата, если последовательность пуста.
SkipUntil Пропускает элементы исходной последовательности, пока заданное условие не станет истинным. Текущий элемент будет последним пропущенным.
Split Разделяет последовательность заданным разделителем (возвращает последовательность последовательностей).
TakeEvery Возвращает каждый N-й элемент исходной последовательности.
TakeLast Возвращает последние N элементов исходной последовательности.
TakeUntil Возвращает элементы исходной последовательности, пока заданное условие не станет истинным. Текущий элемент будет последним возвращенным.
ToDataTable Позволяет преобразовать последовательность в новую DataTable или заполнить имеющуюся. Есть возможность задать лямдами получение из исходного элемента значений для полей таблицы.
ToDelimitedString Преобразует последовательность в строку с разделителями (то что обычно приходится делать через нудный Aggregate).
ToHashSet Возвращает HashSet〈T&кang; от исходных элементов.
Zip То же, что EquiZip, но длина результирующей последовательности будет равна длине наименьшей из исходных.
ZipLongest То же, что EquiZip, но длина результирующей последовательности будет равна длине наибольшей из исходных (в качестве недостающих значений будет использовано значение по умолчанию).

Большинство операторов перегружены для большей гибкости использования (например, можно задать свой IComparer и т.п.). Кроме перечисленных операторов, имеются еще два отладочных – AssertCount (проверяют количество элементов последовательности) и Trace (выводит все элементы в debug-консоль).

Вместе с исходниками библиотеки идет прекрасная документация в стиле MSDN, в которой подробно описаны все операторы, их параметры и имеются примеры использования. Также есть комментарии в исходном коде.

В заключение хочу обратить внимание читателей на две другие библиотеки авторства Джона Скита – MiscUtils и NodaTime (NuGet Package). Особенно интересна последняя – библиотека предназначена для работы с датой/временем. Джон занимался ей последние несколько лет и в ноябре прошлого года наконец выпустил версию 1.0. В его блоге можно почитать много интересного на тему того, чем плохи для этих целей стандартные классы .Net и какие подводные грабли поджидают разработчика, серьезно работающего со временем.

UPD: Перевод статьи про NodaTime.

UPD2: Я не стал использовать в статье слово «проекция», так как по-моему оно не употребляется в русскоязычном сообществе. Хотелось бы увидеть ваше мнение в опросе и комментариях.
Понятно ли было бы слово «проекция» в описании операторов morelinq? (например, DistinctBy в документации описывается как «Returns all distinct elements of the given source, where 'distinctness' is determined via a projection and the default eqaulity comparer for the projected type.»

Проголосовало 74 человека. Воздержалось 34 человека.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Алексей Мерсон @m_a_d
карма
65,0
рейтинг 4,8
Senior .Net Developer
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (42)

  • +1
    Спасибо за статью, 2 момента:
    1. "… после вчерашнего поста юзера..." вчерашнего?
    2. Статья о Noda Time не так давно переводилась здесь: Что же всё-таки не так со структурой DateTime?
    • 0
      Так пост действительно вчерашний, просто он был опубликован поздно вечером, что сложилось ощущение, что он сегодняшний.
      • +1
        Прошу прощения, у меня все еще 2012 год закешировался))
    • 0
      1. Конец рабочего дня, начало года, ну вы понимаете…
      2. Спасибо, добавил ссылку в пост.
  • +4
    Преобразует последовательность в строку с разделителями (то что обычно приходится делать через нудный Aggregate).

    Нет, я все понимаю, но чем string.Join не угодил?
    • 0
      string.Join работает только коллекцией строк. Его реализация, судя по коду, работает с чем угодно (т.к. ToString() есть у любого объекта)
      • 0
        Эм, а зачем? А в том маловероятном случае, когда нужно — select(s => s.ToString()), и все.
        • +1
          «Для общности», я так думаю.

          Вообще, как выяснилось, соответствующий метод уже добавлен в Framework начиная с версии 4.0.
    • 0
      На мой взгляд и string.Join, и Aggregate по-своему некрасивы (я использовал Aggregate в LINQ-выражениях для единообразия). Вариант Скита — самый лаконичный.
  • 0
    Ооо, класс!
    Особенно радуют Split, TakeEvery, Prepend, DistinctBy, Batch.
    А вот с ForEach не очень понял, в чем отличие от стандартного линковского?
    • +3
      А вот с ForEach не очень понял, в чем отличие от стандартного линковского?

      «Стандартного линковского» не существует.
      • +1
        А я-то думал, что ForEach — метод расширения и всем коллекциям доступен. А оказывается только List'у. Эх. Еще одно грустное открытие. :-)
        • 0
          Почему грустное то? Раз вы про это не знали, то значит и проблем с этим не было, а если возникнут — вы теперь в курсе, как это легко решить )
          • 0
            Да я привык обычным foreach проходиться, но знал, что есть и такой вот вариант — альтернативный через метод. А теперь мой мир порушен. :-)
        • +5
          Мне кажется, наоборот — хорошо. «Когда в руке молоток — всё кажется гвоздём» — часто вижу когда в подобные ФорИчи втыкают несколько-строчные лямбды — Ну почему не воспользоваться нормальным человеческим foreach?
          • 0
            в LINQ короче запись получается. И если глаз натренирован, код компактнее получается, незахламленный «правильной структурой языка».
            • 0
              Также он нарушает purity присущую всем LINQ методам расширениям, и гораздо хуже выражает intention изменения данных.
              НУ и очевидно дублирует классический foreach.

              Лучше бы и у листа не было.
              • 0
                Плюс еще в лямбдах не должно быть сайд эффектов
                • 0
                  Совершенно верно. Это всё идёт из фундаментальных принципов ФП. А ребята пекутся о выравнивании буковок в IDE.
          • +1
            Нужно отталкиваться от удобочитаемости. Если логика обработки каждой записи достаточно сложная, то конечно же лучше использовать классический foreach. Если же нужно сделать что-то простое вроде emails.ForEach(m => SendMail(m)) (а еще лучше — emails.ForEach(SendMail)) — то LINQ гораздо лаконичней.
            • 0
              Лучше исходить из того что side-effect-less код можно совать в лямбды extension-методов LINQ, а то что вы написали — нет.
              • 0
                Если воспринимать linq только по его последней букве q от query, то да, код, который исходную последовательность может изменить туда лучше не ставить, и да, ForEach тогда не нужен, ибо это практически его предназначение. А если этот метод воспринимать не как один из запросов, а как инкапсулированный обычный foreach, и использовать его для того, чтобы код был более читаемым и менее громоздким — почему бы и нет. В сочетании с другими операторами линка, записанными одной последовательностью через точку это выглядит гармоничнее, чем обычный foreach.
  • +1
    >Особенно после вчерашнего поста юзера SergeyT

    Пост прошлогодний, глобально же вы мыслите :)

    Что касается библиотек на .NET от Джона Скита, ещё вполне интересны protobuf-csharp-port и unconstrained-melody.

    Первая понятно о чём (кстати, есть более активно развивающаяся альтернатива — protobuf-net), а вторая позволяет задавать нетипичные ограничения для generics.
    Правда сложилось так, что ни одним из них сам не пользовался, но имею в виду, если попадётся подходящая задача.
    • 0
      >Особенно после вчерашнего поста юзера SergeyT

      Пост прошлогодний, глобально же вы мыслите :)

      Спасибо, поправил =)
  • +2
    >>Index Возвращает последовательность пар индекс-значение.
    Select из коробки так умеет (одна из его перегрузок)
  • 0
    Очень полезно, спасибо. Например, ЗИПа нет под WP, пришлось брать реализацию из Скитовского же Reimplementing LINQ to Objects, а тут сразу такая библиотечка. Странно, что раньше ее не заметил.
  • 0
    Вот этим, наверное, вдохновлялся
    www.haskell.org/ghc/docs/latest/html/libraries/base/Data-List.html
    :)
  • +1
    Я думаю многим читателям блога .Net знакомо имя John Skeet.

    Jon Skeet пишется без «h», фишка у него такая.
    • +1
      Могу ошибаться, но полагаю, что Jon без «h» потому, что он не Джон, а Джонатан (jonathan). По крайней мере здесь.
    • 0
      Fixed.
  • +2
    У команды Rx есть Ix (Interactive extensions), доступный в NuGet. Там есть EnumerableEx, который всё то, что есть в Rx для IQueryable повторяет для IEnumerable. Ну так вот там списочек расширений подлиннее немного.
    А morelinq посмотрим, спасибо.
  • 0
    Полезная библиотека. Не придется теперь создавать свои велосипеды.
  • 0
    Не совсем понятен смысл DistinctBy().
    Пример:
    List<int> list = new List<int>() { 1, 3, 4, 5, 7, 8, 23 };
    List<int> numbers = list.DistinctBy(x => x > 5).ToList();
    В результате numbers содержит две цифры: 1 и 7, хотя должен содержать только цифру 7.
    • 0
      Нет, просто перевод неточен.
    • 0
      Потому что с переводом налажали. В реальности там для каждого элемента последовательности применяется описанное преобразование и берутся те элементы (я так понимаю, что первые), у которых различаются результаты преобразования.

      В вашем случае это преобразование, которое возвращает true для всех элементов больше 5 и false для всех остальных. Результат понятен.
      • 0
        Тогда в чем отличие от обычного Where?

        UPD — все, понял.
    • 0
      Нет. Уникальные по значению предиката.
      1 — false, берем
      3..5 — false пропускаются
      7 — true, пропускаем
      8..29 — true, пропускаем.
      • 0
        Спасибо. Прояснили. Только в вашем комментарии ошибка 7- truе, берем.
      • 0
        Это не предикат, а проекция.
        • 0
          К слову о слове «проекция»: я специально погуглил, когда писал статью, и не увидел употребления этого слова в русскоязычном сообществе. Я не там искал?
          • 0
            Не задумывался об этом, просто перевел термин с английского.
  • 0
    Хорошая библиотека, надо брать. Сразу навскидку — оператора ForEach сильно не хватало. Приходилось делать Select, возвращать что-нибудь ненужное и результат выполнения никуда не класть. Не покидало гадливое чувство кривости получившегося кода. Все хотел свой extension метод писать, но как то руки не доходили.

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