LINQ-完全外部結合


202

人のIDと名前のリストと、人のIDと姓のリストがあります。名を持たない人もいれば、姓を持たない人もいます。2つのリストで完全外部結合を実行したいと思います。

したがって、以下のリスト:

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

回答:


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 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
Unionは重複を排除します。重複が予想されない場合、または2番目のクエリを記述して最初のクエリに含まれていたものをすべて除外できる場合は、代わりにConcatを使用してください。これはUNIONとUNION ALLのSQLの違いです
cadrell0

3
@ cadre110の重複は、個人に姓名がある場合に発生するため、和集合が有効な選択肢です。
saus

1
@sausですが、ID列があるため、姓と名が重複していても、IDは異なるはずです
cadrell0

1
あなたの解決策はプリミティブ型に対しては機能しますが、オブジェクトに対しては機能しないようです。私の場合、FirstNameはドメインオブジェクトですが、LastNameは別のドメインオブジェクトです。2つの結果を結合すると、LINQはNotSupportedExceptionをスローしました(UnionまたはConcatの型は互換性なく構築されます)。同様の問題が発生しましたか?
Candy Chiu

1
@CandyChiu:私は実際にそのような事件に遭遇したことはありません。それはあなたのクエリプロバイダーの制限だと思います。その場合AsEnumerable()は、ユニオン/連結を実行する前にを呼び出して、LINQ to Objectsを使用することをお勧めします。それを試して、それがどうなるか見てください。これがあなたが行きたいルートでない場合、私はそれ以上の助けになることができるとは思いません。
Jeff Mercado

196

更新1:真に一般化された拡張メソッドの提供FullOuterJoin
更新2:オプションでIEqualityComparerキータイプのカスタムを受け入れる
更新3:この実装は最近一部になりましたMoreLinq -皆さんありがとう!

編集を追加FullOuterGroupJoinideone)。私は再利用しました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空の場合でも結果)

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拡張メソッドが追加されました
sehe

4
辞書を使用する代わりに、ヘルパー拡張メソッドで表現された機能を含むLookupを使用できます。たとえば、a.GroupBy(selectKeyA).ToDictionary();as a.ToLookup(selectKeyA)およびadict.OuterGet(key)as と書くことができますalookup[key]。ただし、キーのコレクションを取得するには少し注意が必要ですalookup.Select(x => x.Keys)
Risky Martin、

1
@RiskyMartinありがとう!それは確かに、全体をよりエレガントにします。答え ideone-s を更新しました。(インスタンス化されるオブジェクトが少ないため、パフォーマンスを向上させる必要があると思います)。
sehe

1
@Reviousは、キーが一意であることを知っている場合にのみ機能します。そして、それは/ grouping /の一般的なケースではありません。それ以外は、はい、ぜひ。ハッシュがパフォーマンスをドラッグしないことがわかっている場合(ノードベースのコンテナーは原則としてコストが高く、ハッシュは無料ではなく、効率はハッシュ関数/バケットの広がりに依存します)、それは確かによりアルゴリズム的に効率的です。したがって、小さな負荷の場合は、速くはないかもしれません
sehe

27

受け入れられた回答を含め、これらのほとんどに問題があると思います。サーバーのラウンドトリップが多すぎてデータが返されすぎたり、クライアントの実行が多すぎたりするため、IQueryableではLinqとうまく連動しないためです。

IEnumerableの場合、過度のメモリ使用があるため、Seheの回答などは好きではありません(単純な10000000の2つのリストのテストでは、Linqpadが32GBマシンのメモリ不足で実行されました)。

また、他のほとんどは、適切な完全外部結合を実際には実装していません。これは、右反セミ結合による連結ではなく、右結合を持つユニオンを使用しているため、結果から重複する内部結合行が削除されるだけでなく、左側または右側のデータに元々存在していた適切な重複。

したがって、これらのすべての問題を処理し、SQLを生成し、LINQ to SQLに直接結合してサーバー上で実行する拡張機能を以下に示します。これらはEnumerableの他のものよりも高速で、メモリが少ないです。

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ます。

ラムダへのExpressionマージを処理するためのの手動コーディングはExpression<Func<>>、LinqKitで改善できますが、言語/コンパイラがそのためのヘルプを追加していると便利です。機能は完全を期すために含まれていますが、私は再実装されていませんでしたまだ。FullOuterJoinDistinctRightOuterJoinFullOuterGroupJoin

私は完全外部結合の別のバージョンIEnumerable、キーが順序付け可能な場合の。これは、少なくとも小さなコレクションで、左の外部結合を右の反セミ結合と組み合わせるよりも約50%高速です。一度だけソートした後、各コレクションを通過します。

また、をカスタム拡張に置き換えることにより、EFで動作するバージョンに別の回答を追加しましたInvoke


どうしたのTP unusedP, TC unusedC?それらは文字通り未使用ですか?
Rudey 2017

はい、彼らはただの型をキャプチャするために提示されているTPTCTResult適切に作成しますExpression<Func<>>。私は私がそれらを置き換えることができなって______代わりに、それC#の代わりに使用するための適切なパラメータワイルドカードを持つまでは、明確には見えません。
NetMage 2017

1
@MarcL。私は「面倒」についてはよくわかりませんが、この答えはこの文脈で非常に役立つことに同意します。印象的なもの(私にはLinq-to-SQLの欠点が確認されています)
sehe

3
私は得ていThe LINQ expression node type 'Invoke' is not supported in LINQ to Entities.ます。このコードに制限はありますか?IQueryablesでFULL JOINを実行したい
学習者

1
EFで動作するようにインライン化InvokeするカスタムExpressionVisitorに置き換える新しい答えを追加しましたInvoke。試して頂けますか?
NetMage 2018年

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 2017

1
@TamusJRoyce Except Unionは重複を削除するため、元のデータに重複行がある場合、結果には含まれません。
NetMage

素晴らしい点!重複が削除されないようにする必要がある場合は、一意のIDを追加します。はい。ユニオンは、一意のIDがあり、ユニオンがユニオンオールに切り替わる(内部のヒューリスティックス/最適化によって)ことを示唆できない限り、少し無駄です。しかし、うまくいきます。
TamusJRoyce


7

@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のロジックに人気が出てきたら、それに移行する気がします。しかし、それでも、2つの理由から、可能であれば、既存の ".Join()"メソッドの構文に一致するオーバーロードを少なくとも1つ持つことが重要であるため、注意が必要です。

  1. メソッドの一貫性は、時間の節約、エラーの回避、意図しない動作の回避に役立ちます。
  2. 将来、すぐに使える「.FullJoin()」メソッドが存在する場合、可能であれば、現在存在する「.Join()」メソッドの構文を維持しようとするでしょう。その場合、移行したい場合は、パラメーターを変更したり、さまざまな戻り値の型がコードを壊すことを心配することなく、関数の名前を変更できます。

ジェネリック、拡張機能、Funcステートメント、その他の機能はまだ新しいので、フィードバックは間違いなく歓迎されます。

編集:コードに問題があったことに気づくのに長い時間はかかりませんでした。私はLINQPadで.Dump()を実行し、戻り値の型を調べていました。IEnumerableだけだったので、一致させようとしました。しかし、拡張機能で実際に.Where()または.Select()を実行したときに、「 'System Collections.IEnumerable'には 'Select'および...の定義が含まれていません」というエラーが発生しました。したがって、最終的には.Join()の入力構文を一致させることができましたが、戻り動作は一致しませんでした。

編集:関数の戻り値の型に「TResult」を追加しました。マイクロソフトの記事を読んでいるときにそれを逃した、そしてもちろんそれは理にかなっている。今回の修正により、結局のところ、返品行動は私の目標に沿っているようです。


+2この回答とマイケルサンダース。誤ってクリックして投票をロックしました。2つ追加してください。
TamusJRoyce 2017

@TamusJRoyce、コード形式を少し編集したところです。編集後は、投票をやり直すことができると思います。必要に応じて試してください。
pwilcox 2017年

どうもありがとうございます!
Roshna Omer 2017

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の呼び出しによって熱心に列挙されています)。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);
    }
}

この実装には、次の重要なプロパティがあります。

  • 遅延実行。出力シーケンスが列挙される前に入力シーケンスは列挙されません。
  • 入力シーケンスをそれぞれ1回だけ列挙します。
  • 入力シーケンスの順序を保持します。つまり、左のシーケンス、次に右の順序でタプルを生成します(左のシーケンスに存在しないキーの場合)。

これらのプロパティは、FullOuterJoinを初めて使用する人でもLINQの経験がある人が期待するものであるため、重要です。


入力シーケンスの順序は保持されません。ルックアップはそのことを保証しません。したがって、これらのforeachは左側のいくつかの順序で列挙し、次に左側に存在しない右側のいくつかの順序を列挙します。ただし、要素の関係順序は保持されません。
Ivan Danilov

@IvanDanilovこれは実際には契約に含まれていないことは間違いない。ただし、ToLookupの実装では、Enumerable.csの内部Lookupクラスを使用して、挿入順のリンクリストにグループを保持し、このリストを使用してグループを反復します。したがって、現在の.NETバージョンでは順序が保証されていますが、残念ながらMSはこれを文書化していないため、新しいバージョンで変更される可能性があります。
セーレンBoisen

Win 8.1の.NET 4.5.1で試してみましたが、順序が保持されません。
Ivan Danilov

1
"..入力シーケンスは、ToLookupの呼び出しによって熱心に列挙されます。" しかし、実装はまったく同じです。有限状態マシンの費用のため、ここでは降伏はあまり役に立ちません。
pkuderov

4
Lookup呼び出しは、イテレーターが作成されたときではなく、結果の最初の要素が要求されたときに行われます。これが遅延実行の意味です。左のEnumerableをLookupに変換する代わりに直接反復することにより、1つの入力セットの列挙をさらに延期できます。その結果、左のセットの順序が保持されるという追加の利点が得られます。
Rolf

2

十分にテストされていることを確信していないため、これを別の回答として追加することにしました。これは、再実装であるFullOuterJoin本質的に簡略化し、カスタマイズされたバージョンの使用方法LINQKit Invoke/ ExpandのためExpression、それはエンティティフレームワークを動作するはずようにします。それは私の以前の答えとほとんど同じなので、あまり説明はありません。

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を使用していて、ある時点でnullのビジターが[Guid?]タイプを期待しているため、どちらが正しいですか。何かが足りないのかもしれません。EF 6.4.4用にコード化された短い例があります。このコードをどのように共有できるか教えてください。ありがとう!
トロンチョ

@Troncho私は通常、テストにLINQPadを使用しているため、EF 6は簡単には実行できません。base.Visit(node)ツリーを再帰するだけなので、例外をスローするべきではありません。ほとんどすべてのコード共有サービスにアクセスできますが、テストデータベースはセットアップできません。ただし、LINQ to SQLテストに対して実行すると、正常に動作するようです。
NetMage

@Troncho GuidキーとGuid?外部キーの間に参加している可能性はありますか?
NetMage

テストにもLinqPadを使用しています。クエリでArgumentExceptionがスローされたため、[。Net Framework 4.7.1]のVS2019と最新のEF 6でデバッグすることにしました。そこで、実際の問題を追跡しました。コードをテストするために、同じ[Persons]テーブルから作成された2つの個別のデータセットを生成しています。一部のレコードが各セットに固有であり、一部が両方のセットに存在するように、両方のセットをフィルタリングします。[PersonId]は[Primary Key] Guid(c#)/ Uniqueidentifier(SqlServer)であり、どちらのセットもnull [PersonId]値を生成しません。共有コード:github.com/Troncho/EF_FullOuterJoin
Troncho

1

両方の入力でメモリ内ストリーミング列挙を実行し、各行のセレクターを呼び出します。現在の反復で相関関係がない場合、セレクター引数の1つはnullになります

例:

   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明らかな理由により、シーケンス全体をバッファリングします
2016年

@seheあなたは間違いなくLinq to Objectsに適しています。IEnumerable <T>がIQueryable <T>の場合、ソースは並べ替える必要がありますが、テストする時間はありません。これが間違っている場合は、入力IEnumerable <T>をIQueryable <T>に置き換えるだけで、ソース/データベースで並べ替えが行われます。
James Caradoc-Davies

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

2つ以上のテーブルの完全外部結合:最初に、結合する列を抽出します。

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
残念ながら、関数はSelectManyLINQ2SQLに値する式ツリーに変換できないようです。
またはMapper

edc65。あなたがすでにそれをしたなら、私はそれがばかげた質問であるかもしれないことを知っています。ただし、念のため(知らない人もいるようですが)、名前空間joinextを参照するだけで済みます。
H7O

またはマッパー、あなたがそれを機能させたいコレクションの種類を教えてください。IEnumerableコレクションで問題なく動作するはずです
H7O

0

結合句の目的はこのタスクソリューションに必要な方法でデータを蓄積することではないため、LINQ結合句はこの問題の正しい解決策ではないと思います。作成された個別のコレクションをマージするコードは非常に複雑になり、おそらく学習目的には適していますが、実際のアプリケーションには適していません。この問題を解決する方法の1つは、以下のコードにあります。

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

興味深い投稿をありがとうございました!

私の場合は必要だったので、コードを変更しました

  • パーソナライズされた述語に参加
  • パーソナライズされた労働組合明確な比較子

興味のある人のために、これは私の変更されたコードです(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

さらに別の完全外部結合

他の命題のシンプルさと読みやすさに満足していなかったので、私はこれで終わりました:

高速であるというプリテンションはありません(2020m CPUで1000 * 1000に参加するには約800 ms:2.4 GHz / 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. 提供された主要な関数ビルダーに基づいてIDを作成する
  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に対する怒りのように思えます。
16

8
データはデータベースからのものだと誰が言ったのですか?
user247702

1
もちろん、データベースです。問題の「外部結合」という単語があります:) google.cz/search?q=outer+join
ミラノŠvec18年

1
私はこれが「旧式」のソリューションであることを理解していますが、反対票を投じる前に、その複雑さを他のソリューションと比較してください。
ミラノŠvec18年

もちろん、データベースであってもなくてもかまいません。メモリ内のリスト間の外部結合を使用したソリューションを探しています
edc65
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.