LINQ - повне зовнішнє приєднання


202

У мене є список посвідчень людей та їх імені, а також список посвідчень людей та їх прізвища. Деякі не мають імені, а деякі не мають прізвища; Я хотів би зробити повне зовнішнє об'єднання у двох списках.

Отже, такі списки:

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

Відповіді:


122

Я не знаю, чи охоплює це всі випадки, логічно це здається правильним. Ідея полягає в тому, щоб взяти ліве зовнішнє з'єднання і праве зовнішнє з'єднання, а потім прийняти об'єднання результатів.

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 в 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
Union усуне дублікати. Якщо ви не очікуєте дублікатів або можете написати другий запит, щоб виключити все, що було включено в перший, замість цього скористайтеся Concat. Це різниця у SQL між UNION та UNION ALL
cadrell0

3
@ cadre110 дублікати будуть виникати, якщо у людини є ім’я та прізвище, тому союз є правильним вибором.
saus

1
@saus, але є стовпчик ідентифікатора, тож навіть якщо є дублікат імені та прізвища, ідентифікатор повинен бути іншим
cadrell0,

1
Ваше рішення працює для примітивних типів, але, здається, не працює для об'єктів. У моєму випадку FirstName - це доменний об’єкт, а LastName - ще один доменний об’єкт. Коли я об'єднав два результати, LINQ кинув NotSupportedException (типи в Union або Concat побудовані несумісно). Чи виникали у вас подібні проблеми?
Candy Chiu

1
@CandyChiu: Я фактично ніколи не стикався з такою справою. Я думаю, що це обмеження у вашого постачальника запитів. Ви, ймовірно, захочете використовувати LINQ для об'єктів у цьому випадку, зателефонувавши AsEnumerable()перед тим, як виконати об'єднання / конкатенацію. Спробуйте це і подивіться, як це відбувається. Якщо це не той маршрут, яким ви хочете пройти, я не впевнений, що я можу вам більше допомогти.
Джефф Меркадо

196

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

Редагувати додано FullOuterGroupJoin( ideone ). Я повторно використовував GetOuter<>реалізацію, роблячи цю частку менш ефективною, ніж могла бути, але я прагну до коду "highlevel", а не оптимізованого кровотоку.

Дивіться це в прямому ефірі на 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були порожніми)

Щось, що зазвичай не спостерігається в RDBMS, - це приєднання до групи [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 методу розширення при умови
sehe

Редаговано: Додано метод розширення FullOuterGroupJoin
вересня 1212

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

1
@RiskyMartin Дякую! Це, справді, робить всю справу більш елегантною. Я оновив відповідь і ideone-s. (Я припускаю, що продуктивність повинна бути збільшена, оскільки кількість об'єктів інстанціюється).
sehe

1
@Revable, що працює лише в тому випадку, якщо ви знаєте, що ключі унікальні. І це не звичайний випадок для / групування /. Крім цього, так, будь-якими способами. Якщо ви знаєте, що хеш не збирається перетягувати perf (контейнери на основі вузлів в принципі дорожчають, а хешування не безкоштовне, а ефективність залежить від хеш-функції / розповсюдження відра), це, безумовно, буде більш алгоритмічно ефективнішим. Таким чином, для малих навантажень я б очікувати , що вона не може бути швидше
sehe

27

Я думаю, що з більшості з них є проблеми, включаючи прийняту відповідь, оскільки вони не дуже добре працюють з Linq над IQueryable, або через занадто багато об’їзних серверів і надто багато повернень даних, або занадто багато виконання клієнтів.

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

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

Отже, ось мої розширення, які вирішують усі ці проблеми, генерують SQL, а також здійснюють приєднання LINQ до SQL безпосередньо, виконуючи на сервері, і це швидше і з меншою пам’яттю, ніж інші на перелічених номерах:

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.

Ручне кодування ExpressionExpression<Func<>>Ручне для обробки об'єднання в лямбда можна було б покращити за допомогою LinqKit, але було б добре, якби мова / компілятор додали для цього певну допомогу. Функції FullOuterJoinDistinctта RightOuterJoinфункції включені для повноти, але я її ще не реалізував FullOuterGroupJoin.

Я написав ще одну версію повного зовнішнього з'єднання для IEnumerableвипадків, коли ключ можна замовити, що приблизно на 50% швидше, ніж комбінування лівого зовнішнього з'єднання з правим антиполовинним з'єднанням, принаймні для невеликих колекцій. Він проходить кожну колекцію після сортування лише один раз.

Я також додав ще одну відповідь для версії, яка працює з EF, замінивши на Invokeспеціальне розширення.


З чим угода TP unusedP, TC unusedC? Вони буквально не використовуються?
Rudey

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

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

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 та інших функціях, тому зворотній зв'язок, безумовно, вітається.

EDIT: Не знадобилося мені багато часу, щоб зрозуміти, що проблема з моїм кодом. Я робив .Dump () в LINQPad і дивився на тип повернення. Це було просто НЕЧІЛЬКО, тому я спробував відповідати цьому. Але коли я насправді зробив .Where () або .Select () у своєму розширенні, я отримав помилку: "" System Collections.IEnumerable "не містить визначення для" Select "та ...". Отже, врешті-решт, я зміг зіставити вхідний синтаксис .Join (), але не поведінку повернення.

EDIT: До типу повернення для функції додано "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 , я придумав таке:

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.


Це не зберігає порядок введення послідовностей: пошук не гарантує цього, тому ці передбачення будуть перераховані в певному порядку лівої сторони, а потім деякого порядку правої сторони немає в лівій частині. Але реляційний порядок елементів не збереглося.
Іван Данилов

@IvanDanilov Ви праві, що насправді цього немає в договорі. Реалізація ToLookup, однак, використовує внутрішній клас пошуку в Enumerable.cs, який зберігає угруповання у впорядкованому вкладеному списку та використовує цей список для переробки через них. Отже, в поточній версії .NET замовлення гарантується, але оскільки MS, на жаль, не підтвердило це документацією, вони могли змінити його в наступних версіях.
Søren Boisen

Я спробував це на .NET 4.5.1 на Win 8.1, і він не зберігає порядок.
Іван Данилов

1
"..послідовності введення охоче перераховуються дзвінками до ToLookup". Але ваша реалізація робить точно так само. Врожайність тут не дає багато через витрати на машину з кінцевим станом.
pkuderov

4
Виклики пошуку здійснюються тоді, коли запитується перший елемент результату, а не тоді, коли створюється ітератор. Ось що означає відкладене виконання. Ви можете ще більше відкласти перерахування одного вхідного набору, повторивши лівий 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 до SQL, здається, він працює добре.
NetMage

@Troncho Чи можливо ви з'єднаєтесь між Guidключем та Guid?зовнішнім ключем?
NetMage

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

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буферизує всю послідовність з очевидних причин .
вересня 1616

@sehe Ви безперечно правильні для Linq до об'єктів. Якщо IEnumerable <T> IQueyable <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
На жаль, здається, функція в SelectManyне може бути перетворена в дерево вираження, гідне LINQ2SQL, здається.
АБО Mapper

edc65. Я знаю, що це може бути дурним питанням, якщо ви це вже зробили. Але про всяк випадок (як я помітив, деякі не знають), вам просто потрібно посилатися на простір імен joinext.
H7O

АБО Mapper, дайте мені знати, з яким типом колекції ви хотіли, щоб він працював. Це повинно чудово працювати з будь-якою колекцією IEnumerable
H7O

0

Я думаю, що пункт приєднання LINQ не є правильним рішенням цієї проблеми, оскільки мета пункту приєднання не є накопиченням даних таким чином, як це потрібно для цього рішення завдання. Код для об'єднання створених окремих колекцій стає занадто складним, можливо, це нормально для навчальних цілей, але не для реальних програм. Один із способів вирішити цю проблему - у наведеному нижче коді:

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 м: 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
Milan Švec

1
Я розумію, що це рішення "старої моди", але перед тим, як звернути увагу, порівняйте його складність з іншими рішеннями :) Крім прийнятого, це, звичайно, правильне.
Мілан Швець

Звичайно, це може бути база даних чи ні. Я шукаю рішення із зовнішнім з'єднанням списків у пам'яті
edc65
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.