LINQ - полное внешнее соединение


204

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

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

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

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


2
Вам нужно, чтобы это работало только для списков в памяти или для Linq2Sql?
JamesFaix

Попробуйте .GroupJoin () stackoverflow.com/questions/15595289/…
jdev.ninja

Ответы:


123

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

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,
    };

2
Союз устранит дубликаты. Если вы не ожидаете дубликатов или можете написать второй запрос, чтобы исключить все, что было включено в первый, используйте вместо этого Concat. В этом разница SQL между UNION и UNION ALL
cadrell0

3
Дубликаты @ cadre110 будут иметь место, если у человека есть имя и фамилия, поэтому союз является правильным выбором.
Сос

1
@saus, но есть столбец идентификатора, поэтому, даже если есть повторяющиеся имя и фамилия, идентификатор должен отличаться
cadrell0

1
Ваше решение работает для примитивных типов, но, похоже, не работает для объектов. В моем случае FirstName - это объект домена, а LastName - другой объект домена. Когда я объединяю два результата, LINQ генерирует NotSupportedException (типы в Union или Concat создаются несовместимо). Были ли у вас похожие проблемы?
Candy Chiu

1
@CandyChiu: Я никогда не сталкивался с таким делом. Я полагаю, это ограничение вашего поставщика запросов. Возможно, вы захотите использовать LINQ to Objects в этом случае, позвонив AsEnumerable()перед выполнением объединения / объединения. Попробуйте и посмотрите, как это происходит. Если это не тот путь, по которому ты хочешь идти, я не уверен, что смогу помочь тебе больше.
Джефф Меркадо

196

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

Редактировать Добавлено FullOuterGroupJoin( ideone ). Я повторно использовал 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стольких раз , сколько есть элементы b с соответствующим ключом (то есть: ничего , если бы bне было пусто). База данных жаргон называет этоinner (equi)join .
  • Внешнее соединение включает в себя элементы из , aдля которых нет соответствующего элемента не существует в b. (то есть: даже результаты, если bбыли пусты). Обычно это называетсяleft join .
  • Полное внешнее соединение включает в себя записи из a , а такжеb , если нет соответствующего элемента не существует в другом. (т.е. даже результаты, если aбыли пусты)

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

  • Группа присоединиться , делает то же самое , как описано выше, но вместо того , чтобы повторять элементы 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;
    }
}

Отредактировано, чтобы показать использование предоставленного FullOuterJoinметода расширения
сехе

Отредактировано: добавлен метод расширения FullOuterGroupJoin
сех

4
Вместо использования словаря вы можете использовать поиск , который содержит функциональность, выраженную в ваших методах расширения помощника. Например, вы можете написать a.GroupBy(selectKeyA).ToDictionary();как a.ToLookup(selectKeyA)и adict.OuterGet(key)как alookup[key]. Получение коллекции ключей немного сложнее, хотя: alookup.Select(x => x.Keys).
Рискованный Мартин

1
@RiskyMartin Спасибо! Это, действительно, делает все это более элегантным. Я обновил ответ и ideone-ы. (Я полагаю, производительность должна быть увеличена, поскольку создается меньше объектов).
Сех

1
@ Очевидно, это работает, только если вы знаете, что ключи уникальны. И это не частый случай для / группировки /. Кроме этого, да, конечно. Если вы знаете, что хеш не собирается перетаскивать (у контейнеров на основе узлов в принципе больше затрат, а хеширование не является бесплатным, а эффективность зависит от хеш-функции / разброса сегментов), это, безусловно, будет более алгоритмически эффективным. Таким образом, для небольших нагрузок я бы ожидал, что это не будет быстрее
се

27

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

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

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

Итак, вот мои расширения, которые решают все эти проблемы, генерируют SQL, а также реализуют соединение в LINQ to SQL напрямую, выполняются на сервере и работают быстрее и с меньшим объемом памяти, чем другие в 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) {

        var hashLK = new HashSet<TKey>(from l in leftItems select leftKeySelector(l));
        return rightItems.Where(r => !hashLK.Contains(rightKeySelector(r))).Select(r => resultSelector(default(TLeft),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) {

        var sampleAnonLR = new { left = default(TLeft), rightg = default(IEnumerable<TRight>) };
        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, default(TRight), default(TResult));

        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) {

        var sampleAnonLR = new { leftg = default(IEnumerable<TLeft>), right = default(TRight) };
        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, default(TLeft), default(TResult));

        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) {

        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) {

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

        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) {

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

Я также добавил еще один ответ для версии, которая работает с EF, заменив Invokeпользовательское расширение.


С чем дело TP unusedP, TC unusedC? Они буквально не используются?
Рудей

Да, они просто присутствуют , чтобы захватить типы в TP, TC, TResultчтобы создать правильный Expression<Func<>>. Я должен я мог заменить их _, __, ___вместо этого, но это не кажется более ясным , пока C # не имеет надлежащего подстановочные параметр , чтобы использовать вместо.
NetMage

1
@MarcL. Я не очень уверен насчет «утомительного» - но я согласен, что этот ответ очень полезен в этом контексте. Впечатляющие вещи (хотя для меня это подтверждает недостатки Linq-to-SQL)
17

3
Я получаю The LINQ expression node type 'Invoke' is not supported in LINQ to Entities.. Есть ли ограничения с этим кодом? Я хочу , чтобы выполнить полный РЕГИСТРИРУЙТЕСЬ над IQueryables
Learner

1
Я добавил новый ответ, который заменяет Invokeпользовательский, ExpressionVisitorчтобы встроить, Invokeчтобы он работал с EF. Можешь попробовать?
NetMage

7

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

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));
}

3
+1. R ⟗ S = (R ⟕ S) ∪ (R ⟖ S), что означает полное внешнее соединение = левое внешнее объединение все правое внешнее объединение! Я ценю простоту этого подхода.
TamusJRoyce

1
@TamusJRoyce Кроме того, Unionудаляет дубликаты, поэтому, если в исходных данных есть повторяющиеся строки, они не будут в результате.
NetMage

Отличный момент! добавьте уникальный идентификатор, если вам нужно предотвратить удаление дубликатов. Да. Объединение является немного расточительным, если вы не можете намекнуть, что существует уникальный идентификатор, и объединение переключается на объединение всех (с помощью внутренней эвристики / оптимизации). Но это будет работать.
TamusJRoyce


7

Я предполагаю, что подход @ 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 (), но не поведение возврата.

РЕДАКТИРОВАТЬ: Добавлено «TResult» в тип возвращаемого значения для функции. Пропустил это при прочтении статьи Microsoft, и это конечно имеет смысл. С этим исправлением теперь кажется, что возвращаемое поведение соответствует моим целям.


+2 за этот ответ, а также Майкл Сандерс. Я случайно нажал на это, и голосование заблокировано. Пожалуйста, добавьте два.
TamusJRoyce

@TamusJRoyce, я просто немного отредактировал форматы кода. Я полагаю, что после внесения изменений у вас есть возможность изменить свой голос. Дайте ему шанс, если хотите.
pwilcox

Спасибо вам большое!
Рошна Омер

6

Как вы обнаружили, у 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)));

2

Мне нравится ответ 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.


Он не сохраняет порядок входных последовательностей: Lookup не гарантирует этого, поэтому эти foreaches будут перечислять в некотором порядке левой стороны, тогда некоторый порядок правой стороны не будет присутствовать в левой стороне. Но реляционный порядок элементов не сохраняется.
Иван Данилов

@IvanDanilov Вы правы, что это на самом деле не в контракте. Однако реализация ToLookup использует внутренний класс Lookup в Enumerable.cs, который хранит группировки в связанном списке с упорядочением вставок и использует этот список для их перебора. Таким образом, в текущей версии .NET порядок гарантирован, но поскольку MS, к сожалению, не задокументировала это, они могут изменить его в более поздних версиях.
Сорен Бойсен

Я пробовал это на .NET 4.5.1 на Win 8.1, и это не сохраняет порядок.
Иван Данилов

1
msgstr "входные последовательности охотно перечисляются при вызовах ToLookup". Но ваша реализация делает то же самое. Уступка здесь не дает много из-за затрат на конечный автомат.
пкудеров

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

2

Я решил добавить это как отдельный ответ, поскольку я не уверен, что это достаточно проверено. Это повторная реализация 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) {

        // (lrg,r) => resultSelector(lrg.left, r)
        var sampleAnonLR = new { left = default(TLeft), rightg = default(IEnumerable<TRight>) };
        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, default(TRight), default(TResult));

        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) {

        // (lgr,l) => resultSelector(l, lgr.right)
        var sampleAnonLR = new { leftg = default(IEnumerable<TLeft>), right = default(TRight) };
        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, default(TLeft), default(TResult));

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

        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));
        }
    }
}

NetMage, впечатляющее кодирование! Когда я запускаю его на простом примере и когда [NullVisitor.Visit (..) вызывается в [base.Visit (Node)], он выдает [System.ArgumentException: типы аргументов не совпадают]. Что верно, поскольку я использую [Guid] TKey и в какой-то момент нулевой посетитель ожидает тип [Guid?]. Может быть, я что-то упустил. У меня есть короткий пример для EF 6.4.4. Пожалуйста, дайте мне знать, как я могу поделиться этим кодом с вами. Спасибо!
Трончо

@ Troncho Я обычно использую LINQPad для тестирования, поэтому EF 6 сделать нелегко. base.Visit(node)не должно выбрасывать исключение, поскольку это просто повторяется вниз по дереву. Я могу получить доступ практически к любой службе совместного использования кода, но не могу настроить тестовую базу данных. Тем не менее, выполнение этого теста на моем LINQ to SQL работает нормально.
NetMage

@ Troncho Возможно ли, что вы соединяете между Guidключом и Guid?внешним ключом?
NetMage

Я использую LinqPad для тестирования тоже. Мой запрос вызвал ArgumentException, поэтому я решил отладить его на VS2019 на [.Net Framework 4.7.1] и на последнем EF 6. Там я должен отследить реальную проблему. Чтобы протестировать ваш код, я генерирую 2 отдельных набора данных из одной таблицы [Персоны]. Я фильтрую оба набора так, что некоторые записи уникальны для каждого набора, а некоторые существуют в обоих наборах. [PersonId] является [первичным ключом] Guid (c #) / Uniqueidentifier (SqlServer), и ни один из них не генерирует нулевое значение [PersonId]. Общий код: github.com/Troncho/EF_FullOuterJoin
Трончо

1

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

Пример:

   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
Это героическое усилие, чтобы сделать вещи "потоковыми". К сожалению, весь выигрыш теряется на первом этапе, когда вы выполняете OrderByоба ключевых прогноза. OrderByбуферизирует всю последовательность по очевидным причинам .
Сехе

@sehe Вы определенно подходите для Linq to Objects. Если IEnumerable <T> равен IQueryable <T>, источник должен отсортироваться - хотя времени для тестирования нет. Если я ошибаюсь по этому поводу, простая замена ввода IEnumerable <T> на IQueryable <T> должна отсортировать в источнике / базе данных.
Джеймс Карадок-Дэвис

1

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

 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

0

Полное внешнее объединение для двух или более таблиц. Сначала извлеките столбец, к которому хотите присоединиться.

var DatesA = from A in db.T1 select A.Date; 
var DatesB = from B in db.T2 select B.Date; 
var DatesC = from C in db.T3 select C.Date;            

var Dates = DatesA.Union(DatesB).Union(DatesC); 

Затем используйте левое внешнее соединение между извлеченным столбцом и основными таблицами.

var Full_Outer_Join =

(from A in Dates
join B in db.T1
on A equals B.Date into AB 

from ab in AB.DefaultIfEmpty()
join C in db.T2
on A equals C.Date into ABC 

from abc in ABC.DefaultIfEmpty()
join D in db.T3
on A equals D.Date into ABCD

from abcd in ABCD.DefaultIfEmpty() 
select new { A, ab, abc, abcd })
.AsEnumerable();

0

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

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

Чтобы использовать этот класс расширения, просто обратитесь к его пространству имен в своем классе, добавив следующую строку, используя joinext;

^ это должно позволить вам видеть смысл функций расширения в любом наборе объектов IEnumerable, который вы случайно используете.

Надеюсь это поможет. Дайте мне знать, если это все еще не ясно, и я надеюсь, напишу пример того, как его использовать.

Теперь вот класс:

namespace joinext
{    
public static class JoinExtensions
    {
        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)
            where TInner : class
            where TOuter : class
        {
            var innerLookup = inner.ToLookup(innerKeySelector);
            var outerLookup = outer.ToLookup(outerKeySelector);

            var innerJoinItems = inner
                .Where(innerItem => !outerLookup.Contains(innerKeySelector(innerItem)))
                .Select(innerItem => resultSelector(null, innerItem));

            return outer
                .SelectMany(outerItem =>
                {
                    var innerItems = innerLookup[outerKeySelector(outerItem)];

                    return innerItems.Any() ? innerItems : new TInner[] { null };
                }, resultSelector)
                .Concat(innerJoinItems);
        }


        public static IEnumerable<TResult> LeftJoin<TOuter, TInner, TKey, TResult>(
            this IEnumerable<TOuter> outer,
            IEnumerable<TInner> inner,
            Func<TOuter, TKey> outerKeySelector,
            Func<TInner, TKey> innerKeySelector,
            Func<TOuter, TInner, TResult> resultSelector)
        {
            return outer.GroupJoin(
                inner,
                outerKeySelector,
                innerKeySelector,
                (o, i) =>
                    new { o = o, i = i.DefaultIfEmpty() })
                    .SelectMany(m => m.i.Select(inn =>
                        resultSelector(m.o, inn)
                        ));

        }



        public static IEnumerable<TResult> RightJoin<TOuter, TInner, TKey, TResult>(
            this IEnumerable<TOuter> outer,
            IEnumerable<TInner> inner,
            Func<TOuter, TKey> outerKeySelector,
            Func<TInner, TKey> innerKeySelector,
            Func<TOuter, TInner, TResult> resultSelector)
        {
            return inner.GroupJoin(
                outer,
                innerKeySelector,
                outerKeySelector,
                (i, o) =>
                    new { i = i, o = o.DefaultIfEmpty() })
                    .SelectMany(m => m.o.Select(outt =>
                        resultSelector(outt, m.i)
                        ));

        }

    }
}

1
К сожалению, кажется, что функция in SelectManyне может быть преобразована в дерево выражений, достойное LINQ2SQL.
ИЛИ Mapper

edc65. Я знаю, что это может быть глупый вопрос, если вы уже сделали это. Но на всякий случай (как я заметил, некоторые не знают), вам просто нужно сослаться на пространство имен joinext.
H7O

ИЛИ Mapper, дайте мне знать, с каким типом коллекции вы хотите, чтобы она работала. Он должен хорошо работать с любой коллекцией IEnumerable
H7O

0

Я думаю, что предложение LINQ join не является правильным решением этой проблемы, поскольку цель предложения join состоит не в том, чтобы накапливать данные таким образом, как это требуется для решения этой задачи. Код для объединения созданных отдельных коллекций становится слишком сложным, может быть, это нормально для целей обучения, но не для реальных приложений. Одним из способов решения этой проблемы является приведенный ниже код:

class Program
{
    static void Main(string[] args)
    {
        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" });

        HashSet<int> ids = new HashSet<int>();
        foreach (var name in firstNames)
        {
            ids.Add(name.ID);
        }
        foreach (var name in lastNames)
        {
            ids.Add(name.ID);
        }
        List<FullName> fullNames = new List<FullName>();
        foreach (int id in ids)
        {
            FullName fullName = new FullName();
            fullName.ID = id;
            FirstName firstName = firstNames.Find(f => f.ID == id);
            fullName.FirstName = firstName != null ? firstName.Name : string.Empty;
            LastName lastName = lastNames.Find(l => l.ID == id);
            fullName.LastName = lastName != null ? lastName.Name : string.Empty;
            fullNames.Add(fullName);
        }
    }
}
public class FirstName
{
    public int ID;

    public string Name;
}

public class LastName
{
    public int ID;

    public string Name;
}
class FullName
{
    public int ID;

    public string FirstName;

    public string LastName;
}

Если реальные коллекции велики для формирования HashSet, то вместо циклов foreach можно использовать код ниже:

List<int> firstIds = firstNames.Select(f => f.ID).ToList();
List<int> LastIds = lastNames.Select(l => l.ID).ToList();
HashSet<int> ids = new HashSet<int>(firstIds.Union(LastIds));//Only unique IDs will be included in HashSet

0

Спасибо всем за интересные посты!

Я изменил код, потому что в моем случае мне нужно было

  • персонализированный предикат
  • персонализированный союз отчетливый Comparer

Для интересующихся это мой модифицированный код (в VB, извините)

    Module MyExtensions
        <Extension()>
        Friend Function FullOuterJoin(Of TA, TB, TResult)(ByVal a As IEnumerable(Of TA), ByVal b As IEnumerable(Of TB), ByVal joinPredicate As Func(Of TA, TB, Boolean), ByVal projection As Func(Of TA, TB, TResult), ByVal comparer As IEqualityComparer(Of TResult)) As IEnumerable(Of TResult)
            Dim joinL =
                From xa In a
                From xb In b.Where(Function(x) joinPredicate(xa, x)).DefaultIfEmpty()
                Select projection(xa, xb)
            Dim joinR =
                From xb In b
                From xa In a.Where(Function(x) joinPredicate(x, xb)).DefaultIfEmpty()
                Select projection(xa, xb)
            Return joinL.Union(joinR, comparer)
        End Function
    End Module

    Dim fullOuterJoin = lefts.FullOuterJoin(
        rights,
        Function(left, right) left.Code = right.Code And (left.Amount [...] Or left.Description.Contains [...]),
        Function(left, right) New CompareResult(left, right),
        New MyEqualityComparer
    )

    Public Class MyEqualityComparer
        Implements IEqualityComparer(Of CompareResult)

        Private Function GetMsg(obj As CompareResult) As String
            Dim msg As String = ""
            msg &= obj.Code & "_"
            [...]
            Return msg
        End Function

        Public Overloads Function Equals(x As CompareResult, y As CompareResult) As Boolean Implements IEqualityComparer(Of CompareResult).Equals
            Return Me.GetMsg(x) = Me.GetMsg(y)
        End Function

        Public Overloads Function GetHashCode(obj As CompareResult) As Integer Implements IEqualityComparer(Of CompareResult).GetHashCode
            Return Me.GetMsg(obj).GetHashCode
        End Function
    End Class

0

Еще одно полное внешнее соединение

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

Он не имеет предварительного напряжения, чтобы быть быстрым (около 800 мс, чтобы присоединиться 1000 * 1000 на 2020 м CPU: 2,4 ГГц / 2 ядра). Для меня это просто компактное и повседневное полное внешнее соединение.

Он работает так же, как SQL FULL OUTER JOIN (сохранение дубликатов)

Ура ;-)

using System;
using System.Collections.Generic;
using System.Linq;
namespace NS
{
public static class DataReunion
{
    public static List<Tuple<T1, T2>> FullJoin<T1, T2, TKey>(List<T1> List1, Func<T1, TKey> KeyFunc1, List<T2> List2, Func<T2, TKey> KeyFunc2)
    {
        List<Tuple<T1, T2>> result = new List<Tuple<T1, T2>>();

        Tuple<TKey, T1>[] identifiedList1 = List1.Select(_ => Tuple.Create(KeyFunc1(_), _)).OrderBy(_ => _.Item1).ToArray();
        Tuple<TKey, T2>[] identifiedList2 = List2.Select(_ => Tuple.Create(KeyFunc2(_), _)).OrderBy(_ => _.Item1).ToArray();

        identifiedList1.Where(_ => !identifiedList2.Select(__ => __.Item1).Contains(_.Item1)).ToList().ForEach(_ => {
            result.Add(Tuple.Create<T1, T2>(_.Item2, default(T2)));
        });

        result.AddRange(
            identifiedList1.Join(identifiedList2, left => left.Item1, right => right.Item1, (left, right) => Tuple.Create<T1, T2>(left.Item2, right.Item2)).ToList()
        );

        identifiedList2.Where(_ => !identifiedList1.Select(__ => __.Item1).Contains(_.Item1)).ToList().ForEach(_ => {
            result.Add(Tuple.Create<T1, T2>(default(T1), _.Item2));
        });

        return result;
    }
}
}

Идея состоит в том, чтобы

  1. Идентификаторы сборки на основе предоставленных ключевых конструкторов функций
  2. Обработка оставленных только предметов
  3. Процесс внутреннего соединения
  4. Обрабатывать только правильные предметы

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

Установите точку останова в конце, чтобы вручную убедиться, что она ведет себя так, как ожидается

using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NS;

namespace Tests
{
[TestClass]
public class DataReunionTest
{
    [TestMethod]
    public void Test()
    {
        List<Tuple<Int32, Int32, String>> A = new List<Tuple<Int32, Int32, String>>();
        List<Tuple<Int32, Int32, String>> B = new List<Tuple<Int32, Int32, String>>();

        Random rnd = new Random();

        /* Comment the testing block you do not want to run
        /* Solution to test a wide range of keys*/

        for (int i = 0; i < 500; i += 1)
        {
            A.Add(Tuple.Create(rnd.Next(1, 101), rnd.Next(1, 101), "A"));
            B.Add(Tuple.Create(rnd.Next(1, 101), rnd.Next(1, 101), "B"));
        }

        /* Solution for essential testing*/

        A.Add(Tuple.Create(1, 2, "B11"));
        A.Add(Tuple.Create(1, 2, "B12"));
        A.Add(Tuple.Create(1, 3, "C11"));
        A.Add(Tuple.Create(1, 3, "C12"));
        A.Add(Tuple.Create(1, 3, "C13"));
        A.Add(Tuple.Create(1, 4, "D1"));

        B.Add(Tuple.Create(1, 1, "A21"));
        B.Add(Tuple.Create(1, 1, "A22"));
        B.Add(Tuple.Create(1, 1, "A23"));
        B.Add(Tuple.Create(1, 2, "B21"));
        B.Add(Tuple.Create(1, 2, "B22"));
        B.Add(Tuple.Create(1, 2, "B23"));
        B.Add(Tuple.Create(1, 3, "C2"));
        B.Add(Tuple.Create(1, 5, "E2"));

        Func<Tuple<Int32, Int32, String>, Tuple<Int32, Int32>> key = (_) => Tuple.Create(_.Item1, _.Item2);

        var watch = System.Diagnostics.Stopwatch.StartNew();
        var res = DataReunion.FullJoin(A, key, B, key);
        watch.Stop();
        var elapsedMs = watch.ElapsedMilliseconds;
        String aser = JToken.FromObject(res).ToString(Formatting.Indented);
        Console.Write(elapsedMs);
    }
}

}


-4

Я действительно ненавижу эти выражения linq, вот почему SQL существует:

select isnull(fn.id, ln.id) as id, fn.firstname, ln.lastname
   from firstnames fn
   full join lastnames ln on ln.id=fn.id

Создайте это как представление sql в базе данных и импортируйте как сущность.

Конечно, (различное) объединение левого и правого объединений также сделает это, но это глупо.


11
Почему бы просто не отбросить как можно больше абстракций и сделать это в машинном коде? (Подсказка: потому что абстракции высшего порядка облегчают жизнь программисту). Это не отвечает на вопрос и выглядит для меня как разглагольствование против LINQ.
спонсор

8
Кто сказал, что данные поступают из базы данных?
user247702

1
Конечно, это база данных, в вопросе есть слова «внешнее соединение» :) google.cz/search?q=outer+join
Милан Швец

1
Я понимаю, что это «старомодное» решение, но перед тем, как понизить голосование, сравните его сложность с другими решениями :) Кроме принятого, оно, конечно, правильное.
Милан Швец

Конечно, это может быть база данных или нет. Я ищу решение с внешним соединением между списками в памяти
edc65
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.