224

人々の 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

私は何を間違っていますか?

4

16 に答える 16

216

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

追加された編集FullOuterGroupJoin( ideone )。私はGetOuter<>実装を再利用して、これを可能な限りパフォーマンスを低下させましたが、今のところ、最先端の最適化ではなく、「高レベル」のコードを目指しています。

http://ideone.com/O36nWcでライブをご覧ください

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

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

出力を印刷します。

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

デフォルトを指定することもできます: http://ideone.com/kG4kqO

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

印刷:

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

使用される用語の説明:

結合は、リレーショナル データベース設計から借用した用語です。

  • 結合は、対応するキーaを持つ要素が存在する回数だけ要素を繰り返します(つまり、空の場合は何もありません)。データベース用語ではこれを と呼びますb binner (equi)join
  • 外部結合には、対応する要素aがに存在しない要素が含まれます。(つまり、空の場合でも結果が得られます)。これは通常、 と呼ばれます。bbleft 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;
    }
}
于 2012-11-21T23:42:12.920 に答える
133

これがすべてのケースをカバーしているかどうかはわかりませんが、論理的には正しいようです。アイデアは、左外部結合と右外部結合を取得してから、結果の結合を取得することです。

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,
    };
于 2011-03-30T19:38:01.617 に答える
9

これを行う拡張メソッドを次に示します。

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));
}
于 2014-12-13T13:28:56.593 に答える
9

@seheのアプローチの方が強いと思いますが、それをよりよく理解するまで、@MichaelSanderの拡張機能から飛び降りていることに気づきます。here で説明されている組み込みの 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 つの理由があります。

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

ジェネリック、拡張機能、Func ステートメント、およびその他の機能についてはまだ初心者なので、フィードバックは大歓迎です。

編集:コードに問題があることに気付くのにそれほど時間はかかりませんでした。LINQPad で .Dump() を実行し、戻り値の型を調べていました。ちょうどIEnumerableだったので、合わせてみました。しかし、拡張機能で実際に .Where() または .Select() を実行すると、「'System Collections.IEnumerable' には 'Select' の定義が含まれていません ...」というエラーが表示されました。そのため、最終的に .Join() の入力構文を一致させることができましたが、戻り動作は一致しませんでした。

編集:関数の戻り値の型に「TResult」を追加しました。マイクロソフトの記事を読んだときにそれを見逃しましたが、もちろんそれは理にかなっています。この修正により、リターン動作が最終的に私の目標に沿ったものになったようです。

于 2015-01-24T00:02:36.373 に答える
5

お気づきのように、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)));
于 2011-03-30T17:58:22.237 に答える
-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ビューとして作成し、エンティティとしてインポートします。

もちろん、左結合と右結合の (別個の) 結合もそれを実現しますが、それはばかげています。

于 2015-05-04T15:53:00.653 に答える