[C#] LINQ-전체 외부 조인

나는 사람들의 신분증과 그들의 이름과 사람들의 신분증과 성의 목록을 가지고 있습니다. 어떤 사람들은 이름이없고 어떤 사람들은 성이 없습니다. 두 목록에서 전체 외부 조인을 수행하고 싶습니다.

따라서 다음 목록이 있습니다.

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

내가 뭘 잘못하고 있죠?



답변

이것이 모든 경우에 적용되는지는 모르겠지만 논리적으로는 정확합니다. 아이디어는 왼쪽 외부 조인과 오른쪽 외부 조인을 취한 다음 결과를 결합하는 것입니다.

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


답변

업데이트 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는 가입 에서 요소를 반복 할 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;
    }
}


답변

너무 많은 서버 왕복과 너무 많은 데이터 반환 또는 너무 많은 클라이언트 실행으로 인해 IQueryable을 통해 Linq와 잘 작동하지 않기 때문에 허용 된 답변을 포함하여 대부분의 문제에 문제가 있다고 생각합니다.

IEnumerable의 경우 과도한 메모리 사용 (32GB 시스템의 Linqpad에서 간단한 10000000 2 목록 테스트를 실행했습니다) 때문에 Sehe의 대답이나 비슷한 것을 좋아하지 않습니다.

또한 다른 대부분은 실제로 올바른 반 외부 조인을 사용하는 Concat 대신 오른쪽 조인이있는 Union을 사용하므로 결과에서 중복 된 내부 조인 행을 제거 할뿐 아니라 적절한 전체 외부 조인을 실제로 구현하지 않습니다. 원래 왼쪽 또는 오른쪽 데이터에 존재하는 적절한 복제본

다음은 이러한 모든 문제를 처리하고, SQL을 생성하고, LINQ to 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));
    }
}

Right Anti-Semi-Join의 차이점은 Linq to Objects 또는 소스와 거의 관련이 없지만 최종 답변에서 서버 (SQL) 측면에서 차이를 만들어 불필요한 것을 제거합니다. JOIN .

LinqKit을 사용하여을 람다로 Expression병합하는 데 필요한 핸드 코딩을 Expression<Func<>>향상시킬 수는 있지만 언어 / 컴파일러가 도움을 주면 좋을 것입니다. FullOuterJoinDistinctRightOuterJoin기능을 완전하게하기 위해서 포함되어 있습니다,하지만 난 구현을 다시하지 않았다FullOuterGroupJoin 아직.

에 대한 전체 외부 조인의 다른 버전 을 썼습니다IEnumerable 키가 빠르게 왼쪽 외부 결합 50 % 이상에 관한 권리 반 반이 적어도 작은 컬렉션에 가입과 함께 참여하는 주문할 수 있습니다 경우에. 한 번만 정렬 한 후 각 컬렉션을 통과합니다.

또한 사용자 지정 확장 으로 대체하여 EF와 호환되는 버전에 대한 또 다른 답변 을 추가했습니다 Invoke.


답변

이를 수행하는 확장 방법은 다음과 같습니다.

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


답변

@sehe의 접근 방식이 더 강력하다고 생각하지만 더 잘 이해할 때까지 @MichaelSander의 확장 기능을 뛰어 넘습니다. 여기에 설명 된 내장 Enumerable.Join () 메소드의 구문 및 반환 유형과 일치하도록 수정했습니다 . @JeffMercado의 솔루션에서 @ cadrell0의 의견과 관련하여 “고유 한”접미사를 추가했습니다.

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 문 및 기타 기능을 처음 사용하므로 피드백을 환영합니다.

편집하다: 내 코드에 문제가 있음을 깨닫는 데 오래 걸리지 않았습니다. LINQPad에서 .Dump ()를 수행하고 반환 유형을보고있었습니다. IEnumerable이되었으므로 일치 시키려고했습니다. 그러나 실제로 내 확장에서 .Where () 또는 .Select ()를 수행하면 ” ‘System Collections.IEnumerable’에 ‘Select’및 …”에 대한 정의가 포함되어 있지 않습니다. 결국 나는 .Join ()의 입력 구문을 일치시킬 수 있었지만 반환 동작은 일치하지 않았습니다.

편집 : 함수의 반환 유형에 “TResult”가 추가되었습니다. Microsoft 기사를 읽을 때 그 사실을 잊어 버렸습니다. 이 수정으로, 이제 리턴 동작이 결국 내 목표와 일치하는 것 같습니다.


답변

아시다시피 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)));


답변

나는 sehe의 대답을 좋아하지만 지연 된 실행을 사용하지 않습니다 (입력 시퀀스는 ToLookup에 대한 호출에 의해 열심히 열거됩니다). 따라서 LINQ-to-objects 의 .NET 소스를 살펴본 후 다음 을 수행했습니다.

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를 경험 한 사람이 기대하는 것이므로 중요합니다.