LINQ - полное внешнее соединение - PullRequest
172 голосов
/ 30 марта 2011

У меня есть список удостоверений личности и их имени, а также список удостоверений личности и их фамилии.У некоторых людей нет имени, а у некоторых нет фамилии;Я хотел бы сделать полное внешнее объединение в двух списках.

Итак, следующие списки:

ID  FirstName
--  ---------
 1  John
 2  Sue

ID  LastName
--  --------
 1  Doe
 3  Smith

Должны произвести:

ID  FirstName  LastName
--  ---------  --------
 1  John       Doe
 2  Sue
 3             Smith

Яновичок в LINQ (так что извините меня, если я хромаю) и нашел немало решений для 'LINQ Outer Joins', которые выглядят очень похоже, но на самом деле кажутся оставленными внешними объединениями.

Мои попыткипока идем примерно так:

private void OuterJoinTest()
{
    List<FirstName> firstNames = new List<FirstName>();
    firstNames.Add(new FirstName { ID = 1, Name = "John" });
    firstNames.Add(new FirstName { ID = 2, Name = "Sue" });

    List<LastName> lastNames = new List<LastName>();
    lastNames.Add(new LastName { ID = 1, Name = "Doe" });
    lastNames.Add(new LastName { ID = 3, Name = "Smith" });

    var outerJoin = from first in firstNames
        join last in lastNames
        on first.ID equals last.ID
        into temp
        from last in temp.DefaultIfEmpty()
        select new
        {
            id = first != null ? first.ID : last.ID,
            firstname = first != null ? first.Name : string.Empty,
            surname = last != null ? last.Name : string.Empty
        };
    }
}

public class FirstName
{
    public int ID;

    public string Name;
}

public class LastName
{
    public int ID;

    public string Name;
}

Но это возвращает:

ID  FirstName  LastName
--  ---------  --------
 1  John       Doe
 2  Sue

Что я делаю не так?

Ответы [ 13 ]

176 голосов
/ 22 ноября 2012

Обновление 1: предоставление действительно обобщенного метода расширения FullOuterJoin
Обновление 2: возможно принятие пользовательского IEqualityComparer для типа ключа
Обновление 3 : эта реализация недавно стала частью MoreLinq - Спасибо, ребята!

Редактировать Добавлено FullOuterGroupJoin ( Идеон ). Я повторно использовал реализацию GetOuter<>, сделав ее менее производительной, чем могла бы быть, но сейчас я стремлюсь к «высокоуровневому» коду, а не оптимизированному переднему краю, прямо сейчас.

Посмотреть это в прямом эфире на http://ideone.com/O36nWc

static void Main(string[] args)
{
    var ax = new[] { 
        new { id = 1, name = "John" },
        new { id = 2, name = "Sue" } };
    var bx = new[] { 
        new { id = 1, surname = "Doe" },
        new { id = 3, surname = "Smith" } };

    ax.FullOuterJoin(bx, a => a.id, b => b.id, (a, b, id) => new {a, b})
        .ToList().ForEach(Console.WriteLine);
}

Печатает вывод:

{ a = { id = 1, name = John }, b = { id = 1, surname = Doe } }
{ a = { id = 2, name = Sue }, b =  }
{ a = , b = { id = 3, surname = Smith } }

Вы также можете указать значения по умолчанию: http://ideone.com/kG4kqO

    ax.FullOuterJoin(
            bx, a => a.id, b => b.id, 
            (a, b, id) => new { a.name, b.surname },
            new { id = -1, name    = "(no firstname)" },
            new { id = -2, surname = "(no surname)" }
        )

Печать:

{ name = John, surname = Doe }
{ name = Sue, surname = (no surname) }
{ name = (no firstname), surname = Smith }

Объяснение используемых терминов:

Присоединение - это термин, заимствованный из реляционной базы данных:

  • A join будет повторять элементы с a столько раз, сколько есть элементов в b с соответствующим ключом (т. Е. Ничего, если b были пусты). База данных жаргон называет это inner (equi)join.
  • Внешнее соединение включает элементы из a, для которых не соответствует элемент существует в b. (т.е. даже результаты, если b были пустыми). Обычно его называют left join.
  • A полное внешнее объединение включает записи из a , а также b, если нет соответствующего элемента в другом. (т. е. даже результаты, если a были пусты)

Что-то не обычно , замеченное в СУБД, является объединением группы [1] :

  • A присоединение к группе , делает то же, что описано выше, , но вместо повторяющихся элементов из a для нескольких соответствующих b, это групп записи с соответствующими ключами. Это часто бывает удобнее, когда вы хотите перечислять через «объединенные» записи на основе общего ключа.

См. Также GroupJoin , который также содержит некоторые общие справочные объяснения.


[1] (я считаю, что Oracle и MSSQL имеют собственные расширения для этого)

Полный код

Обобщенный класс раскрывающихся расширений для этого

internal static class MyExtensions
{
    internal static IEnumerable<TResult> FullOuterGroupJoin<TA, TB, TKey, TResult>(
        this IEnumerable<TA> a,
        IEnumerable<TB> b,
        Func<TA, TKey> selectKeyA, 
        Func<TB, TKey> selectKeyB,
        Func<IEnumerable<TA>, IEnumerable<TB>, TKey, TResult> projection,
        IEqualityComparer<TKey> cmp = null)
    {
        cmp = cmp?? EqualityComparer<TKey>.Default;
        var alookup = a.ToLookup(selectKeyA, cmp);
        var blookup = b.ToLookup(selectKeyB, cmp);

        var keys = new HashSet<TKey>(alookup.Select(p => p.Key), cmp);
        keys.UnionWith(blookup.Select(p => p.Key));

        var join = from key in keys
                   let xa = alookup[key]
                   let xb = blookup[key]
                   select projection(xa, xb, key);

        return join;
    }

    internal static IEnumerable<TResult> FullOuterJoin<TA, TB, TKey, TResult>(
        this IEnumerable<TA> a,
        IEnumerable<TB> b,
        Func<TA, TKey> selectKeyA, 
        Func<TB, TKey> selectKeyB,
        Func<TA, TB, TKey, TResult> projection,
        TA defaultA = default(TA), 
        TB defaultB = default(TB),
        IEqualityComparer<TKey> cmp = null)
    {
        cmp = cmp?? EqualityComparer<TKey>.Default;
        var alookup = a.ToLookup(selectKeyA, cmp);
        var blookup = b.ToLookup(selectKeyB, cmp);

        var keys = new HashSet<TKey>(alookup.Select(p => p.Key), cmp);
        keys.UnionWith(blookup.Select(p => p.Key));

        var join = from key in keys
                   from xa in alookup[key].DefaultIfEmpty(defaultA)
                   from xb in blookup[key].DefaultIfEmpty(defaultB)
                   select projection(xa, xb, key);

        return join;
    }
}
111 голосов
/ 30 марта 2011

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

var firstNames = new[]
{
    new { ID = 1, Name = "John" },
    new { ID = 2, Name = "Sue" },
};
var lastNames = new[]
{
    new { ID = 1, Name = "Doe" },
    new { ID = 3, Name = "Smith" },
};
var leftOuterJoin =
    from first in firstNames
    join last in lastNames on first.ID equals last.ID into temp
    from last in temp.DefaultIfEmpty()
    select new
    {
        first.ID,
        FirstName = first.Name,
        LastName = last?.Name,
    };
var rightOuterJoin =
    from last in lastNames
    join first in firstNames on last.ID equals first.ID into temp
    from first in temp.DefaultIfEmpty()
    select new
    {
        last.ID,
        FirstName = first?.Name,
        LastName = last.Name,
    };
var fullOuterJoin = leftOuterJoin.Union(rightOuterJoin);

Это работает так, как написано, поскольку находится в LINQ to Objects. Если LINQ to SQL или другое, обработчик запросов может не поддерживать безопасную навигацию или другие операции. Вы должны использовать условный оператор для условного получения значений.

т.е.,

var leftOuterJoin =
    from first in firstNames
    join last in lastNames on first.ID equals last.ID into temp
    from last in temp.DefaultIfEmpty()
    select new
    {
        first.ID,
        FirstName = first.Name,
        LastName = last != null ? last.Name : default,
    };
18 голосов
/ 28 апреля 2017

Я думаю, что есть проблемы с большинством из них, включая принятый ответ, потому что они плохо работают с Linq по сравнению с IQueryable либо из-за слишком большого числа обращений к серверу и слишком большого количества возвратов данных, либо из-за слишком большого количества выполнений клиента.

Для IEnumerable мне не нравится ответ Sehe или аналогичный, потому что он чрезмерно использует память (простой тест 10000000 с двумя списками вывел Linqpad из памяти на моей машине на 32 ГБ).

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

Итак, вот мои расширения, которые обрабатывают все эти проблемы, генерируют SQL так же хорошо, как непосредственное объединение в Linq, выполнение на сервере, и это быстрее и эффективнее.с меньшим объемом памяти, чем у других на Enumerables:

public static class Ext {
    public static IEnumerable<TResult> LeftOuterJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector) {

        return from left in leftItems
               join right in rightItems on leftKeySelector(left) equals rightKeySelector(right) into temp
               from right in temp.DefaultIfEmpty()
               select resultSelector(left, right);
    }

    public static IEnumerable<TResult> RightOuterJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector) {

        return from right in rightItems
               join left in leftItems on rightKeySelector(right) equals leftKeySelector(left) into temp
               from left in temp.DefaultIfEmpty()
               select resultSelector(left, right);
    }

    public static IEnumerable<TResult> FullOuterJoinDistinct<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector) {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Union(leftItems.RightOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }

    public static IEnumerable<TResult> RightAntiSemiJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector) where TLeft : class {

        var hashLK = new HashSet<TKey>(from l in leftItems select leftKeySelector(l));
        return rightItems.Where(r => !hashLK.Contains(rightKeySelector(r))).Select(r => resultSelector((TLeft)null,r));
    }

    public static IEnumerable<TResult> FullOuterJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector)  where TLeft : class {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Concat(leftItems.RightAntiSemiJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }

    private static Expression<Func<TP, TC, TResult>> CastSMBody<TP, TC, TResult>(LambdaExpression ex, TP unusedP, TC unusedC, TResult unusedRes) => (Expression<Func<TP, TC, TResult>>)ex;

    public static IQueryable<TResult> LeftOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) where TLeft : class where TRight : class where TResult : class {

        var sampleAnonLR = new { left = (TLeft)null, rightg = (IEnumerable<TRight>)null };
        var parmP = Expression.Parameter(sampleAnonLR.GetType(), "p");
        var parmC = Expression.Parameter(typeof(TRight), "c");
        var argLeft = Expression.PropertyOrField(parmP, "left");
        var newleftrs = CastSMBody(Expression.Lambda(Expression.Invoke(resultSelector, argLeft, parmC), parmP, parmC), sampleAnonLR, (TRight)null, (TResult)null);

        return leftItems.AsQueryable().GroupJoin(rightItems, leftKeySelector, rightKeySelector, (left, rightg) => new { left, rightg }).SelectMany(r => r.rightg.DefaultIfEmpty(), newleftrs);
    }

    public static IQueryable<TResult> RightOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) where TLeft : class where TRight : class where TResult : class {

        var sampleAnonLR = new { leftg = (IEnumerable<TLeft>)null, right = (TRight)null };
        var parmP = Expression.Parameter(sampleAnonLR.GetType(), "p");
        var parmC = Expression.Parameter(typeof(TLeft), "c");
        var argRight = Expression.PropertyOrField(parmP, "right");
        var newrightrs = CastSMBody(Expression.Lambda(Expression.Invoke(resultSelector, parmC, argRight), parmP, parmC), sampleAnonLR, (TLeft)null, (TResult)null);

        return rightItems.GroupJoin(leftItems, rightKeySelector, leftKeySelector, (right, leftg) => new { leftg, right }).SelectMany(l => l.leftg.DefaultIfEmpty(), newrightrs);
    }

    public static IQueryable<TResult> FullOuterJoinDistinct<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) where TLeft : class where TRight : class where TResult : class {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Union(leftItems.RightOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }

    private static Expression<Func<TP, TResult>> CastSBody<TP, TResult>(LambdaExpression ex, TP unusedP, TResult unusedRes) => (Expression<Func<TP, TResult>>)ex;

    public static IQueryable<TResult> RightAntiSemiJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) where TLeft : class where TRight : class where TResult : class {

        var sampleAnonLgR = new { leftg = (IEnumerable<TLeft>)null, right = (TRight)null };
        var parmLgR = Expression.Parameter(sampleAnonLgR.GetType(), "lgr");
        var argLeft = Expression.Constant(null, typeof(TLeft));
        var argRight = Expression.PropertyOrField(parmLgR, "right");
        var newrightrs = CastSBody(Expression.Lambda(Expression.Invoke(resultSelector, argLeft, argRight), parmLgR), sampleAnonLgR, (TResult)null);

        return rightItems.GroupJoin(leftItems, rightKeySelector, leftKeySelector, (right, leftg) => new { leftg, right }).Where(lgr => !lgr.leftg.Any()).Select(newrightrs);
    }

    public static IQueryable<TResult> FullOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) where TLeft : class where TRight : class where TResult : class {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Concat(leftItems.RightAntiSemiJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }
}

Разница между правильным анти-полусоединением в основном спорна с Linq to Objects или в источнике, но имеет значение на стороне сервера (SQL) в окончательном ответе, удаляя ненужные JOIN.

Ручное кодирование Expression для обработки слияния Expression<Func<>> в лямбду может быть улучшено с помощью LinqKit, но было бы неплохо, если бы язык / компилятор добавил некоторую помощь для этого.Функции FullOuterJoinDistinct и RightOuterJoin включены для полноты, но я еще не реализовал FullOuterGroupJoin.

Я написал другую версию полного внешнего соединения для IEnumerable для случаев, когда ключ можно заказать, что примерно на 50% быстрее, чем объединение левого внешнего соединения с правым анти-полусоединением, по крайней мере, для небольших коллекций.Он проходит каждую коллекцию после сортировки только один раз.

7 голосов
/ 13 декабря 2014

Вот метод расширения, который делает это:

public static IEnumerable<KeyValuePair<TLeft, TRight>> FullOuterJoin<TLeft, TRight>(this IEnumerable<TLeft> leftItems, Func<TLeft, object> leftIdSelector, IEnumerable<TRight> rightItems, Func<TRight, object> rightIdSelector)
{
    var leftOuterJoin = from left in leftItems
        join right in rightItems on leftIdSelector(left) equals rightIdSelector(right) into temp
        from right in temp.DefaultIfEmpty()
        select new { left, right };

    var rightOuterJoin = from right in rightItems
        join left in leftItems on rightIdSelector(right) equals leftIdSelector(left) into temp
        from left in temp.DefaultIfEmpty()
        select new { left, right };

    var fullOuterJoin = leftOuterJoin.Union(rightOuterJoin);

    return fullOuterJoin.Select(x => new KeyValuePair<TLeft, TRight>(x.left, x.right));
}
6 голосов
/ 30 марта 2011

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

outerJoin = outerJoin.Concat(lastNames.Select(l=>new
                            {
                                id = l.ID,
                                firstname = String.Empty,
                                surname = l.Name
                            }).Where(l=>!outerJoin.Any(o=>o.id == l.id)));
5 голосов
/ 24 января 2015

Я предполагаю, что подход @ sehe сильнее, но пока я не понимаю его лучше, я обнаружил, что прыгаю от расширения @ MichaelSander.Я изменил его так, чтобы он соответствовал синтаксису и типу возврата встроенного метода Enumerable.Join (), описанного здесь .Я добавил «отличный» суффикс в отношении комментария @ cadrell0 к решению @ JeffMercado.

public static class MyExtensions {

    public static IEnumerable<TResult> FullJoinDistinct<TLeft, TRight, TKey, TResult> (
        this IEnumerable<TLeft> leftItems, 
        IEnumerable<TRight> rightItems, 
        Func<TLeft, TKey> leftKeySelector, 
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector
    ) {

        var leftJoin = 
            from left in leftItems
            join right in rightItems 
              on leftKeySelector(left) equals rightKeySelector(right) into temp
            from right in temp.DefaultIfEmpty()
            select resultSelector(left, right);

        var rightJoin = 
            from right in rightItems
            join left in leftItems 
              on rightKeySelector(right) equals leftKeySelector(left) into temp
            from left in temp.DefaultIfEmpty()
            select resultSelector(left, right);

        return leftJoin.Union(rightJoin);
    }

}

В этом примере вы будете использовать его следующим образом:

var test = 
    firstNames
    .FullJoinDistinct(
        lastNames,
        f=> f.ID,
        j=> j.ID,
        (f,j)=> new {
            ID = f == null ? j.ID : f.ID, 
            leftName = f == null ? null : f.Name,
            rightName = j == null ? null : j.Name
        }
    );

В будущемПо мере того, как я узнаю больше, у меня есть ощущение, что я перейду к логике @ sehe, учитывая ее популярность.Но даже тогда мне придется быть осторожным, потому что я считаю, что важно иметь хотя бы одну перегрузку, которая соответствует синтаксису существующего метода ".Join ()", если это возможно, по двум причинам:

  1. Согласованность методов помогает экономить время, избегать ошибок и избегать непреднамеренного поведения.
  2. Если в будущем когда-нибудь появится метод «.FullJoin ()», я бы вообразилон попытается придерживаться синтаксиса существующего в настоящее время метода .Join (), если сможет.Если это так, то если вы хотите перейти на него, вы можете просто переименовать свои функции, не меняя параметров и не беспокоясь о различных типах возвращаемых данных, нарушающих ваш код.

Я все еще новичок в обобщениях, расширениях, операторах Func и других функциях, поэтому отзывы, безусловно, приветствуются.

РЕДАКТИРОВАТЬ: Не взялЯ долго не мог понять, что возникла проблема с моим кодом.Я делал .Dump () в LINQPad и смотрел тип возвращаемого значения.Это было просто IEnumerable, поэтому я попытался сопоставить его.Но когда я на самом деле сделал .Where () или .Select () для своего расширения, я получил ошибку: «System Collections.IEnumerable» не содержит определения для «Select» и ... ».Таким образом, в итоге я смог сопоставить входной синтаксис .Join (), но не поведение возврата.

EDIT: Добавил «TResult» к типу возврата для функции.Пропустил это при прочтении статьи Microsoft, и это конечно имеет смысл.С этим исправлением теперь кажется, что возвращаемое поведение соответствует моим целям.

1 голос
/ 14 октября 2018

Моё чистое решение для ситуации, когда ключ уникален в обоих перечислимых значениях:

 private static IEnumerable<TResult> FullOuterJoin<Ta, Tb, TKey, TResult>(
            IEnumerable<Ta> a, IEnumerable<Tb> b,
            Func<Ta, TKey> key_a, Func<Tb, TKey> key_b,
            Func<Ta, Tb, TResult> selector)
        {
            var alookup = a.ToLookup(key_a);
            var blookup = b.ToLookup(key_b);
            var keys = new HashSet<TKey>(alookup.Select(p => p.Key));
            keys.UnionWith(blookup.Select(p => p.Key));
            return keys.Select(key => selector(alookup[key].FirstOrDefault(), blookup[key].FirstOrDefault()));
        }

, поэтому

    var ax = new[] {
        new { id = 1, first_name = "ali" },
        new { id = 2, first_name = "mohammad" } };
    var bx = new[] {
        new { id = 1, last_name = "rezaei" },
        new { id = 3, last_name = "kazemi" } };

    var list = FullOuterJoin(ax, bx, a => a.id, b => b.id, (a, b) => "f: " + a?.first_name + " l: " + b?.last_name).ToArray();

вывод:

f: ali l: rezaei
f: mohammad l:
f:  l: kazemi
1 голос
/ 22 марта 2018

Я решил добавить это как отдельный ответ, так как я не уверен, что он достаточно проверен.Это повторная реализация метода FullOuterJoin с использованием, по сути, упрощенной, настроенной версии LINQKit Invoke / Expand для Expression, чтобы он работал на Entity Framework.Там не так много объяснений, как и мой предыдущий ответ.

public static class Ext {
    private static Expression<Func<TP, TC, TResult>> CastSMBody<TP, TC, TResult>(LambdaExpression ex, TP unusedP, TC unusedC, TResult unusedRes) => (Expression<Func<TP, TC, TResult>>)ex;

    public static IQueryable<TResult> LeftOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) where TLeft : class where TRight : class where TResult : class {

        // (lrg,r) => resultSelector(lrg.left, r)
        var sampleAnonLR = new { left = (TLeft)null, rightg = (IEnumerable<TRight>)null };
        var parmP = Expression.Parameter(sampleAnonLR.GetType(), "lrg");
        var parmC = Expression.Parameter(typeof(TRight), "r");
        var argLeft = Expression.PropertyOrField(parmP, "left");
        var newleftrs = CastSMBody(Expression.Lambda(resultSelector.Apply(argLeft, parmC), parmP, parmC), sampleAnonLR, (TRight)null, (TResult)null);

        return leftItems.GroupJoin(rightItems, leftKeySelector, rightKeySelector, (left, rightg) => new { left, rightg }).SelectMany(r => r.rightg.DefaultIfEmpty(), newleftrs);
    }

    public static IQueryable<TResult> RightOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) where TLeft : class where TRight : class where TResult : class {

        // (lgr,l) => resultSelector(l, lgr.right)
        var sampleAnonLR = new { leftg = (IEnumerable<TLeft>)null, right = (TRight)null };
        var parmP = Expression.Parameter(sampleAnonLR.GetType(), "lgr");
        var parmC = Expression.Parameter(typeof(TLeft), "l");
        var argRight = Expression.PropertyOrField(parmP, "right");
        var newrightrs = CastSMBody(Expression.Lambda(resultSelector.Apply(parmC, argRight), parmP, parmC), sampleAnonLR, (TLeft)null, (TResult)null);

        return rightItems.GroupJoin(leftItems, rightKeySelector, leftKeySelector, (right, leftg) => new { leftg, right })
                         .SelectMany(l => l.leftg.DefaultIfEmpty(), newrightrs);
    }

    private static Expression<Func<TParm, TResult>> CastSBody<TParm, TResult>(LambdaExpression ex, TParm unusedP, TResult unusedRes) => (Expression<Func<TParm, TResult>>)ex;

    public static IQueryable<TResult> RightAntiSemiJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) where TLeft : class where TRight : class where TResult : class {

        // newrightrs = lgr => resultSelector((TLeft)null, lgr.right)
        var sampleAnonLgR = new { leftg = (IEnumerable<TLeft>)null, right = (TRight)null };
        var parmLgR = Expression.Parameter(sampleAnonLgR.GetType(), "lgr");
        var argLeft = Expression.Constant(null, typeof(TLeft));
        var argRight = Expression.PropertyOrField(parmLgR, "right");
        var newrightrs = CastSBody(Expression.Lambda(resultSelector.Apply(argLeft, argRight), parmLgR), sampleAnonLgR, (TResult)null);

        return rightItems.GroupJoin(leftItems, rightKeySelector, leftKeySelector, (right, leftg) => new { leftg, right }).Where(lgr => !lgr.leftg.Any()).Select(newrightrs);
    }

    public static IQueryable<TResult> FullOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) where TLeft : class where TRight : class where TResult : class {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Concat(leftItems.RightAntiSemiJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }

    public static Expression Apply(this LambdaExpression e, params Expression[] args) {
        var b = e.Body;

        foreach (var pa in e.Parameters.Cast<ParameterExpression>().Zip(args, (p, a) => (p, a))) {
            b = b.Replace(pa.p, pa.a);
        }

        return b.PropagateNull();
    }

    public static Expression Replace(this Expression orig, Expression from, Expression to) => new ReplaceVisitor(from, to).Visit(orig);
    public class ReplaceVisitor : System.Linq.Expressions.ExpressionVisitor {
        public readonly Expression from;
        public readonly Expression to;

        public ReplaceVisitor(Expression _from, Expression _to) {
            from = _from;
            to = _to;
        }

        public override Expression Visit(Expression node) => node == from ? to : base.Visit(node);
    }

    public static Expression PropagateNull(this Expression orig) => new NullVisitor().Visit(orig);
    public class NullVisitor : System.Linq.Expressions.ExpressionVisitor {
        public override Expression Visit(Expression node) {
            if (node is MemberExpression nme && nme.Expression is ConstantExpression nce && nce.Value == null)
                return Expression.Constant(null, nce.Type.GetMember(nme.Member.Name).Single().GetMemberType());
            else
                return base.Visit(node);
        }
    }

    public static Type GetMemberType(this MemberInfo member) {
        switch (member) {
            case FieldInfo mfi:
                return mfi.FieldType;
            case PropertyInfo mpi:
                return mpi.PropertyType;
            case EventInfo mei:
                return mei.EventHandlerType;
            default:
                throw new ArgumentException("MemberInfo must be if type FieldInfo, PropertyInfo or EventInfo", nameof(member));
        }
    }
}
1 голос
/ 28 ноября 2016

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

Пример:

   var result = left.FullOuterJoin(
         right, 
         x=>left.Key, 
         x=>right.Key, 
         (l,r) => new { LeftKey = l?.Key, RightKey=r?.Key });
  • Требуется IComparer для типа корреляции, используется значение Comparer.Default, если не указано.

  • Требуется, чтобы 'OrderBy' применялся к входным перечислимым значениям

    /// <summary>
    /// Performs a full outer join on two <see cref="IEnumerable{T}" />.
    /// </summary>
    /// <typeparam name="TLeft"></typeparam>
    /// <typeparam name="TValue"></typeparam>
    /// <typeparam name="TRight"></typeparam>
    /// <typeparam name="TResult"></typeparam>
    /// <param name="left"></param>
    /// <param name="right"></param>
    /// <param name="leftKeySelector"></param>
    /// <param name="rightKeySelector"></param>
    /// <param name="selector">Expression defining result type</param>
    /// <param name="keyComparer">A comparer if there is no default for the type</param>
    /// <returns></returns>
    [System.Diagnostics.DebuggerStepThrough]
    public static IEnumerable<TResult> FullOuterJoin<TLeft, TRight, TValue, TResult>(
        this IEnumerable<TLeft> left,
        IEnumerable<TRight> right,
        Func<TLeft, TValue> leftKeySelector,
        Func<TRight, TValue> rightKeySelector,
        Func<TLeft, TRight, TResult> selector,
        IComparer<TValue> keyComparer = null)
        where TLeft: class
        where TRight: class
        where TValue : IComparable
    {
    
        keyComparer = keyComparer ?? Comparer<TValue>.Default;
    
        using (var enumLeft = left.OrderBy(leftKeySelector).GetEnumerator())
        using (var enumRight = right.OrderBy(rightKeySelector).GetEnumerator())
        {
    
            var hasLeft = enumLeft.MoveNext();
            var hasRight = enumRight.MoveNext();
            while (hasLeft || hasRight)
            {
    
                var currentLeft = enumLeft.Current;
                var valueLeft = hasLeft ? leftKeySelector(currentLeft) : default(TValue);
    
                var currentRight = enumRight.Current;
                var valueRight = hasRight ? rightKeySelector(currentRight) : default(TValue);
    
                int compare =
                    !hasLeft ? 1
                    : !hasRight ? -1
                    : keyComparer.Compare(valueLeft, valueRight);
    
                switch (compare)
                {
                    case 0:
                        // The selector matches. An inner join is achieved
                        yield return selector(currentLeft, currentRight);
                        hasLeft = enumLeft.MoveNext();
                        hasRight = enumRight.MoveNext();
                        break;
                    case -1:
                        yield return selector(currentLeft, default(TRight));
                        hasLeft = enumLeft.MoveNext();
                        break;
                    case 1:
                        yield return selector(default(TLeft), currentRight);
                        hasRight = enumRight.MoveNext();
                        break;
                }
            }
    
        }
    
    }
    
1 голос
/ 17 мая 2015

Мне нравится ответ sehe, но он не использует отложенное выполнение (входные последовательности охотно перечисляются при вызовах ToLookup). Поэтому, посмотрев источники .NET для LINQ-to-objects , я пришел к следующему:

public static class LinqExtensions
{
    public static IEnumerable<TResult> FullOuterJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> left,
        IEnumerable<TRight> right,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TKey, TResult> resultSelector,
        IEqualityComparer<TKey> comparator = null,
        TLeft defaultLeft = default(TLeft),
        TRight defaultRight = default(TRight))
    {
        if (left == null) throw new ArgumentNullException("left");
        if (right == null) throw new ArgumentNullException("right");
        if (leftKeySelector == null) throw new ArgumentNullException("leftKeySelector");
        if (rightKeySelector == null) throw new ArgumentNullException("rightKeySelector");
        if (resultSelector == null) throw new ArgumentNullException("resultSelector");

        comparator = comparator ?? EqualityComparer<TKey>.Default;
        return FullOuterJoinIterator(left, right, leftKeySelector, rightKeySelector, resultSelector, comparator, defaultLeft, defaultRight);
    }

    internal static IEnumerable<TResult> FullOuterJoinIterator<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> left,
        IEnumerable<TRight> right,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TKey, TResult> resultSelector,
        IEqualityComparer<TKey> comparator,
        TLeft defaultLeft,
        TRight defaultRight)
    {
        var leftLookup = left.ToLookup(leftKeySelector, comparator);
        var rightLookup = right.ToLookup(rightKeySelector, comparator);
        var keys = leftLookup.Select(g => g.Key).Union(rightLookup.Select(g => g.Key), comparator);

        foreach (var key in keys)
            foreach (var leftValue in leftLookup[key].DefaultIfEmpty(defaultLeft))
                foreach (var rightValue in rightLookup[key].DefaultIfEmpty(defaultRight))
                    yield return resultSelector(leftValue, rightValue, key);
    }
}

Эта реализация имеет следующие важные свойства:

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

Эти свойства важны, потому что они ожидают того, кто новичок в FullOuterJoin, но имеет опыт работы с LINQ.

...