Коментарий автора: Статья написана в 2012 году и пролежавшая 8 лет в песочнице.
LINQ — как много было придумано в C# лишь для того чтобы мы могли наслаждаться прелестями Language Integrated Query. А именно:
И это все чтобы мы могли написать нечто вроде такого:
Нельзя не согласиться — впечталяет.
И среди всего этого синтаксического сахара была ложка дегдя которая мне не давала нормально выспаться :)
Это тотальное отсутствие поддержки OUTER JOIN. Но как оказалось деготь с легкостью превращается… превращается… превращается…
… в еще один «синтаксический сахар».
Тем, кто пытался найти решение для LEFT OUTER JOIN в интернете, наверняка знакомо подобное решение:
Подобная конструкция явно на порядок запутывает понимание и усложняет и без того простую конструкцию. А это лишь замена INNER JOIN на LEFT OUTER JOIN. Чтобы не продолжать шокировать, пример с FULL OUTER JOIN приводить не буду.
Казалось бы как было бы просто если бы могли написать вот так:
или так
Ан нет. Авторы C# нам такого удовольствия не предоставили. Ну не беда. Все же они позволят нам это сделать самостоятельно, хоть и не таким красивым способом.
Начнем с того, что если кто то вам скажет, что LINQ и интерфейс System.Collections.Generic.IEnumerable имеют что то общее и не могут существовать по отдельности можете смело рассмеяться в лицо…
Конструкция
просто напросто транслируется компилятором в следующую последовательность символов:
и абсолютно не важно какого типа переменные listA и listB. Предположим что listA переменная типа TypeA, а пермеменная itemB типа TypeB. Так вот, если TypeA и TypeB содеражат свойство или поле с именем Key, TypeA содержит метод Join() с 4мя аргументами. Этот LINQ запрос свободно откомпилируется.
При использовании в LINQ переменных которые реализуют стандартный интерфейс IEnumerable используется метод расширения
Собственно этот метод и производит хорошо известный нам INNER JOIN. И вот теперь начинается уличная магия. Чтобы реализовать нам LEFT / RIGHT / FULL OUTER JOIN (или JOIN какой вашей душе будет угоден) необходимо подменить вызов стандартного метода на реализованый нами. Чтобы это сделать, надо переменную listA преобразовать каким то образом в тип который мы можем контролировать.
Реализовав два следующих класса:
мы запросто пишем следующий LINQ запрос
и теперь реализовав метод расширения Join для класса JoinedEnumerable нужным нам образом получаем все что нам нужно.
А вот собственно и методы расширения:
Вуаля…
Красивый LEFT OUTER JOIN:
Красивый RIGHT OUTER JOIN:
Красивый FULL OUTER JOIN:
Теперь при желании вы можете использовать и свой подход — так как поле для фантазии здесь громаднейшее. У меня в загашнике есть еще несколько инетерсных решений для реализации вкусностей. Будет время обязательно поделюсь ими.
Спасибо за внимание.
Да пребудет с вами СИЛА!
LINQ — как много было придумано в C# лишь для того чтобы мы могли наслаждаться прелестями Language Integrated Query. А именно:
- Generics
- Extension Methods
- Lamda expressions
- Expression trees
- Anonymous types
- Object initializers
- Type inferring
И это все чтобы мы могли написать нечто вроде такого:
var query = from itemA in listA join itemB in listB on itemA.Key equals itemB.Key select new {itemA, itemB};
Нельзя не согласиться — впечталяет.
И среди всего этого синтаксического сахара была ложка дегдя которая мне не давала нормально выспаться :)
Это тотальное отсутствие поддержки OUTER JOIN. Но как оказалось деготь с легкостью превращается… превращается… превращается…
… в еще один «синтаксический сахар».
Тем, кто пытался найти решение для LEFT OUTER JOIN в интернете, наверняка знакомо подобное решение:
var query = from itemA in listA join itemB in listB on itemA.Key equals itemB.Key into outer from itemO in outer.DefaultIfEmpty() select new {itemA, itemO};
Подобная конструкция явно на порядок запутывает понимание и усложняет и без того простую конструкцию. А это лишь замена INNER JOIN на LEFT OUTER JOIN. Чтобы не продолжать шокировать, пример с FULL OUTER JOIN приводить не буду.
Казалось бы как было бы просто если бы могли написать вот так:
var query = from itemA in listA left join itemB in listB on itemA.Key equals itemB.Key select new {itemA, itemB};
или так
var query = from itemA in listA full join itemB in listB on itemA.Key equals itemB.Key select new {itemA, itemB};
Ан нет. Авторы C# нам такого удовольствия не предоставили. Ну не беда. Все же они позволят нам это сделать самостоятельно, хоть и не таким красивым способом.
Начнем с того, что если кто то вам скажет, что LINQ и интерфейс System.Collections.Generic.IEnumerable имеют что то общее и не могут существовать по отдельности можете смело рассмеяться в лицо…
Конструкция
var query = from itemA in listA join itemB in listB on itemA.Key equals itemB.Key select new {itemA, itemB};
просто напросто транслируется компилятором в следующую последовательность символов:
var query = listA.Join(listB, itemA => itemA.Key, itemB => itemB.Key, (itemA, itemB) => new {itemA, itemB});
и абсолютно не важно какого типа переменные listA и listB. Предположим что listA переменная типа TypeA, а пермеменная itemB типа TypeB. Так вот, если TypeA и TypeB содеражат свойство или поле с именем Key, TypeA содержит метод Join() с 4мя аргументами. Этот LINQ запрос свободно откомпилируется.
При использовании в LINQ переменных которые реализуют стандартный интерфейс IEnumerable используется метод расширения
public class System.Linq.Enumerable { public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector) {...} }
Собственно этот метод и производит хорошо известный нам INNER JOIN. И вот теперь начинается уличная магия. Чтобы реализовать нам LEFT / RIGHT / FULL OUTER JOIN (или JOIN какой вашей душе будет угоден) необходимо подменить вызов стандартного метода на реализованый нами. Чтобы это сделать, надо переменную listA преобразовать каким то образом в тип который мы можем контролировать.
Реализовав два следующих класса:
public class JoinedEnumerable<T> : IEnumerable<T> { public readonly IEnumerable<T> Source; public bool IsOuter; public JoinedEnumerable(IEnumerable<T> source) { Source = source; } IEnumerator<T> IEnumerable<T>.GetEnumerator() { return Source.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return Source.GetEnumerator(); } } public static class JoinedEnumerable { public static JoinedEnumerable<TElement> Inner<TElement>(this IEnumerable<TElement> source) { return Wrap(source, false); } public static JoinedEnumerable<TElement> Outer<TElement>(this IEnumerable<TElement> source) { return Wrap(source, true); } public static JoinedEnumerable<TElement> Wrap<TElement>(IEnumerable<TElement> source, bool isOuter) { JoinedEnumerable<TElement> joinedSource = source as JoinedEnumerable<TElement> ?? new JoinedEnumerable<TElement>(source); joinedSource.IsOuter = isOuter; return joinedSource; } }
мы запросто пишем следующий LINQ запрос
var query = from itemA in listA.Outer() join itemB in listB on itemA.Key equals itemB.Key select new {itemA, itemB};
и теперь реализовав метод расширения Join для класса JoinedEnumerable нужным нам образом получаем все что нам нужно.
А вот собственно и методы расширения:
public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(this JoinedEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector, IEqualityComparer<TKey> comparer = null) { if (outer == null) throw new ArgumentNullException("outer"); if (inner == null) throw new ArgumentNullException("inner"); if (outerKeySelector == null) throw new ArgumentNullException("outerKeySelector"); if (innerKeySelector == null) throw new ArgumentNullException("innerKeySelector"); if (resultSelector == null) throw new ArgumentNullException("resultSelector"); bool leftOuter = outer.IsOuter; bool rightOuter = (inner is JoinedEnumerable<TInner>) && ((JoinedEnumerable<TInner>)inner).IsOuter; if (leftOuter && rightOuter) return FullOuterJoin(outer, inner, outerKeySelector, innerKeySelector, resultSelector, comparer); if (leftOuter) return LeftOuterJoin(outer, inner, outerKeySelector, innerKeySelector, resultSelector, comparer); if (rightOuter) return RightOuterJoin(outer, inner, outerKeySelector, innerKeySelector, resultSelector, comparer); return Enumerable.Join(outer, inner, outerKeySelector, innerKeySelector, resultSelector, comparer); } public static IEnumerable<TResult> LeftOuterJoin<TOuter, TInner, TKey, TResult>(this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector, IEqualityComparer<TKey> comparer = null) { var innerLookup = inner.ToLookup(innerKeySelector, comparer); foreach (var outerItem in outer) foreach (var innerItem in innerLookup[outerKeySelector(outerItem)].DefaultIfEmpty()) yield return resultSelector(outerItem, innerItem); } public static IEnumerable<TResult> RightOuterJoin<TOuter, TInner, TKey, TResult>(this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector, IEqualityComparer<TKey> comparer = null) { var outerLookup = outer.ToLookup(outerKeySelector, comparer); foreach (var innerItem in inner) foreach (var outerItem in outerLookup[innerKeySelector(innerItem)].DefaultIfEmpty()) yield return resultSelector(outerItem, innerItem); } public static IEnumerable<TResult> FullOuterJoin<TOuter, TInner, TKey, TResult>(this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector, IEqualityComparer<TKey> comparer = null) { var outerLookup = outer.ToLookup(outerKeySelector, comparer); var innerLookup = inner.ToLookup(innerKeySelector, comparer); foreach (var innerGrouping in innerLookup) if (!outerLookup.Contains(innerGrouping.Key)) foreach (TInner innerItem in innerGrouping) yield return resultSelector(default(TOuter), innerItem); foreach (var outerGrouping in outerLookup) foreach (var innerItem in innerLookup[outerGrouping.Key].DefaultIfEmpty()) foreach (var outerItem in outerGrouping) yield return resultSelector(outerItem, innerItem); }
Вуаля…
Красивый LEFT OUTER JOIN:
var query = from itemA in listA.Outer() join itemB in listB on itemA.Key equals itemB.Key select new {itemA, itemB};
Красивый RIGHT OUTER JOIN:
var query = from itemA in listA.Inner() join itemB in listB.Outer() on itemA.Key equals itemB.Key select new {itemA, itemB};
Красивый FULL OUTER JOIN:
var query = from itemA in listA.Outer() join itemB in listB.Outer() on itemA.Key equals itemB.Key select new {itemA, itemB};
Теперь при желании вы можете использовать и свой подход — так как поле для фантазии здесь громаднейшее. У меня в загашнике есть еще несколько инетерсных решений для реализации вкусностей. Будет время обязательно поделюсь ими.
Спасибо за внимание.
Да пребудет с вами СИЛА!
