LINQ - Tham gia đầy đủ bên ngoài


202

Tôi có một danh sách ID người và tên của họ, và danh sách ID người và họ của họ. Một số người không có tên và một số không có họ; Tôi muốn tham gia đầy đủ bên ngoài vào hai danh sách.

Vì vậy, các danh sách sau đây:

ID  FirstName
--  ---------
 1  John
 2  Sue

ID  LastName
--  --------
 1  Doe
 3  Smith

Nên sản xuất:

ID  FirstName  LastName
--  ---------  --------
 1  John       Doe
 2  Sue
 3             Smith

Tôi chưa quen với LINQ (vì vậy hãy tha thứ cho tôi nếu tôi bị khập khiễng) và đã tìm thấy khá nhiều giải pháp cho 'LINQ Outer Joins', tất cả đều trông khá giống nhau, nhưng thực sự dường như bị bỏ lại bên ngoài.

Những nỗ lực của tôi cho đến nay đi một cái gì đó như thế này:

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

Nhưng điều này trả về:

ID  FirstName  LastName
--  ---------  --------
 1  John       Doe
 2  Sue

Tôi đang làm gì sai?


2
Bạn có cần điều này để chỉ làm việc cho danh sách trong bộ nhớ hoặc cho Linq2Sql không?
JamesFaix

Câu trả lời:


122

Tôi không biết nếu điều này bao gồm tất cả các trường hợp, về mặt logic nó có vẻ đúng. Ý tưởng là tham gia bên ngoài bên trái và tham gia bên ngoài bên phải sau đó lấy kết quả của kết quả.

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

Điều này hoạt động như được viết vì nó nằm trong LINQ to Object. Nếu LINQ to SQL hoặc khác, bộ xử lý truy vấn có thể không hỗ trợ điều hướng an toàn hoặc các hoạt động khác. Bạn sẽ phải sử dụng toán tử có điều kiện để có được các giá trị một cách có điều kiện.

I E,

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
Liên minh sẽ loại bỏ trùng lặp. Nếu bạn không mong đợi trùng lặp hoặc có thể viết truy vấn thứ hai để loại trừ bất cứ điều gì được bao gồm trong lần đầu tiên, thay vào đó hãy sử dụng Concat. Đây là điểm khác biệt của SQL giữa UNION và UNION ALL
cadrell0

3
@ cadre110 trùng lặp sẽ xảy ra nếu một người có tên và họ, vì vậy liên minh là một lựa chọn hợp lệ.
xích

1
@saus nhưng có một cột ID, vì vậy ngay cả khi có tên và họ trùng lặp, ID phải khác nhau
cadrell0

1
Giải pháp của bạn hoạt động cho các kiểu nguyên thủy, nhưng dường như không hoạt động cho các đối tượng. Trong trường hợp của tôi, FirstName là một đối tượng miền, trong khi LastName là một đối tượng miền khác. Khi tôi kết hợp hai kết quả, LINQ đã ném NotSupportedException (Các loại trong Union hoặc Concat được xây dựng không tương thích). Bạn đã trải qua vấn đề tương tự?
Kẹo Chiu

1
@CandyChiu: Tôi thực sự không bao giờ gặp phải trường hợp như vậy. Tôi đoán đó là một hạn chế với nhà cung cấp truy vấn của bạn. Bạn có thể muốn sử dụng LINQ cho các Đối tượng trong trường hợp đó bằng cách gọi AsEnumerable()trước khi bạn thực hiện kết hợp / ghép. Hãy thử điều đó và xem làm thế nào đi. Nếu đây không phải là con đường bạn muốn đi, tôi không chắc mình có thể giúp được gì nhiều hơn thế không.
Jeff Mercado

196

Cập nhật 1: cung cấp phương thức mở rộng thực sự khái quát FullOuterJoin
Cập nhật 2: tùy chọn chấp nhận tùy chỉnh IEqualityComparercho loại khóa
Cập nhật 3 : việc triển khai này gần đây đã trở thành một phần củaMoreLinq - Cảm ơn các bạn!

Chỉnh sửa Đã thêm FullOuterGroupJoin( ideone ). Tôi đã sử dụng lạiGetOuter<> triển khai, làm cho điều này trở nên ít hiệu quả hơn so với khả năng của nó, nhưng tôi đang nhắm đến mã 'highlevel', không được tối ưu hóa ngay bây giờ.

Xem trực tiếp trên 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);
}

In đầu ra:

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

Bạn cũng có thể cung cấp mặc định: 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)" }
        )

In ấn:

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

Giải thích các thuật ngữ được sử dụng:

Tham gia là một thuật ngữ mượn từ thiết kế cơ sở dữ liệu quan hệ:

  • Một phép nối sẽ lặp lại các phần tử từ anhiều lần như có các phần tử b với khóa tương ứng (nghĩa là: không có gì nếu btrống). Cơ sở dữ liệu lingo gọi đâyinner (equi)join .
  • Một phép nối ngoài bao gồm các phần tử akhông có phần tử tương ứng nào tồn tại b. (tức là: thậm chí kết quả nếu btrống). Điều này thường được gọi làleft join .
  • Một kết nối bên ngoài đầy đủ bao gồm các bản ghi từ a cũng nhưb nếu không có phần tử tương ứng tồn tại trong phần khác. (tức là kết quả thậm chí nếu atrống)

Một cái gì đó không thường thấy trong RDBMS là tham gia nhóm [1] :

  • Một nhóm tham gia , thực hiện giống như được mô tả ở trên, nhưng thay vì lặp lại các phần tử từ acho nhiều tương ứng b, nó nhóm các bản ghi với các khóa tương ứng. Điều này thường thuận tiện hơn khi bạn muốn liệt kê thông qua các bản ghi 'đã tham gia', dựa trên một khóa chung.

Xem thêm GroupJoin cũng chứa một số giải thích nền chung.


[1] (Tôi tin rằng Oracle và MSSQL có phần mở rộng độc quyền cho việc này)

Mã đầy đủ

Một lớp mở rộng 'thả xuống' tổng quát cho việc này

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

Đã chỉnh sửa để hiển thị việc sử dụng FullOuterJoinphương thức mở rộng được cung cấp
sehe

Đã chỉnh sửa: Đã thêm phương thức mở rộng FullOutergroupJoin
sehe

4
Thay vì sử dụng Từ điển, bạn có thể sử dụng Tra cứu , chứa chức năng được thể hiện trong các phương thức mở rộng của trình trợ giúp. Ví dụ, bạn có thể viết a.GroupBy(selectKeyA).ToDictionary();như a.ToLookup(selectKeyA)adict.OuterGet(key)như alookup[key]. Mặc dù vậy, việc thu thập các khóa là khó khăn hơn một chút : alookup.Select(x => x.Keys).
Rủi ro Martin

1
@RiskyMartin Cảm ơn! Điều đó, thực sự, làm cho toàn bộ điều thanh lịch hơn. Tôi đã cập nhật câu trả lời ideone-s. (Tôi cho rằng hiệu suất nên được tăng lên vì có ít đối tượng được khởi tạo).
sehe

1
@Revious chỉ hoạt động nếu bạn biết các phím là duy nhất. Và đó không phải là trường hợp phổ biến cho / nhóm /. Ngoài ra, có, bằng mọi cách. Nếu bạn biết hàm băm sẽ không kéo theo perf (các thùng chứa dựa trên nút có chi phí cao hơn về nguyên tắc và băm không miễn phí và hiệu quả phụ thuộc vào hàm băm / lây lan xô), chắc chắn nó sẽ hiệu quả hơn về mặt thuật toán. Vì vậy, đối với các tải nhỏ, tôi hy vọng nó có thể không nhanh hơn
sehe

27

Tôi nghĩ rằng có nhiều vấn đề với hầu hết trong số này, bao gồm cả câu trả lời được chấp nhận, vì chúng không hoạt động tốt với Linq qua IQueryable do thực hiện quá nhiều chuyến đi vòng quanh máy chủ và trả lại quá nhiều dữ liệu hoặc thực hiện quá nhiều ứng dụng khách.

Đối với IEnountable tôi không thích câu trả lời của Sehe hoặc tương tự vì nó có sử dụng bộ nhớ quá mức (một bài kiểm tra 10000000 hai danh sách đơn giản đã chạy Linqpad ra khỏi bộ nhớ trên máy 32GB của tôi).

Ngoài ra, hầu hết những người khác không thực sự thực hiện Tham gia đầy đủ bên ngoài thích hợp vì họ đang sử dụng Liên kết với quyền tham gia thay vì kết hợp với kết nối chống bán quyền, điều này không chỉ loại bỏ các hàng tham gia bên trong trùng lặp khỏi kết quả, nhưng kết quả bất kỳ bản sao thích hợp nào tồn tại ban đầu trong dữ liệu bên trái hoặc bên phải.

Vì vậy, đây là các tiện ích mở rộng của tôi xử lý tất cả các vấn đề này, tạo SQL cũng như triển khai trực tiếp tham gia LINQ to SQL, thực thi trên máy chủ và nhanh hơn và có ít bộ nhớ hơn các vấn đề khác trên Enumerables:

public static class Ext {
    public static IEnumerable<TResult> LeftOuterJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector) {

        return from left in leftItems
               join right in rightItems on leftKeySelector(left) equals rightKeySelector(right) into temp
               from right in temp.DefaultIfEmpty()
               select resultSelector(left, right);
    }

    public static IEnumerable<TResult> RightOuterJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector) {

        return from right in rightItems
               join left in leftItems on rightKeySelector(right) equals leftKeySelector(left) into temp
               from left in temp.DefaultIfEmpty()
               select resultSelector(left, right);
    }

    public static IEnumerable<TResult> FullOuterJoinDistinct<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector) {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Union(leftItems.RightOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }

    public static IEnumerable<TResult> RightAntiSemiJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector) {

        var hashLK = new HashSet<TKey>(from l in leftItems select leftKeySelector(l));
        return rightItems.Where(r => !hashLK.Contains(rightKeySelector(r))).Select(r => resultSelector(default(TLeft),r));
    }

    public static IEnumerable<TResult> FullOuterJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector)  where TLeft : class {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Concat(leftItems.RightAntiSemiJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }

    private static Expression<Func<TP, TC, TResult>> CastSMBody<TP, TC, TResult>(LambdaExpression ex, TP unusedP, TC unusedC, TResult unusedRes) => (Expression<Func<TP, TC, TResult>>)ex;

    public static IQueryable<TResult> LeftOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        var sampleAnonLR = new { left = default(TLeft), rightg = default(IEnumerable<TRight>) };
        var parmP = Expression.Parameter(sampleAnonLR.GetType(), "p");
        var parmC = Expression.Parameter(typeof(TRight), "c");
        var argLeft = Expression.PropertyOrField(parmP, "left");
        var newleftrs = CastSMBody(Expression.Lambda(Expression.Invoke(resultSelector, argLeft, parmC), parmP, parmC), sampleAnonLR, default(TRight), default(TResult));

        return leftItems.AsQueryable().GroupJoin(rightItems, leftKeySelector, rightKeySelector, (left, rightg) => new { left, rightg }).SelectMany(r => r.rightg.DefaultIfEmpty(), newleftrs);
    }

    public static IQueryable<TResult> RightOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        var sampleAnonLR = new { leftg = default(IEnumerable<TLeft>), right = default(TRight) };
        var parmP = Expression.Parameter(sampleAnonLR.GetType(), "p");
        var parmC = Expression.Parameter(typeof(TLeft), "c");
        var argRight = Expression.PropertyOrField(parmP, "right");
        var newrightrs = CastSMBody(Expression.Lambda(Expression.Invoke(resultSelector, parmC, argRight), parmP, parmC), sampleAnonLR, default(TLeft), default(TResult));

        return rightItems.GroupJoin(leftItems, rightKeySelector, leftKeySelector, (right, leftg) => new { leftg, right }).SelectMany(l => l.leftg.DefaultIfEmpty(), newrightrs);
    }

    public static IQueryable<TResult> FullOuterJoinDistinct<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Union(leftItems.RightOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }

    private static Expression<Func<TP, TResult>> CastSBody<TP, TResult>(LambdaExpression ex, TP unusedP, TResult unusedRes) => (Expression<Func<TP, TResult>>)ex;

    public static IQueryable<TResult> RightAntiSemiJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

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

        return rightItems.GroupJoin(leftItems, rightKeySelector, leftKeySelector, (right, leftg) => new { leftg, right }).Where(lgr => !lgr.leftg.Any()).Select(newrightrs);
    }

    public static IQueryable<TResult> FullOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Concat(leftItems.RightAntiSemiJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }
}

Sự khác biệt giữa Quyền chống bán tham gia chủ yếu là tranh luận với Linq to Object hoặc trong nguồn, nhưng tạo ra sự khác biệt về phía máy chủ (SQL) trong câu trả lời cuối cùng, loại bỏ một thứ không cần thiết JOIN.

Mã hóa tay Expressionđể xử lý việc hợp nhất Expression<Func<>>thành lambda có thể được cải thiện với LinqKit, nhưng thật tuyệt nếu ngôn ngữ / trình biên dịch đã thêm một số trợ giúp cho việc đó. Các chức năng FullOuterJoinDistinctRightOuterJoinđược bao gồm để hoàn thiện, nhưng tôi chưa thực hiện FullOuterGroupJoinlại.

Tôi đã viết một phiên bản khác của một tham gia bên ngoài đầy đủ choIEnumerable các trường hợp khóa có thể sắp xếp theo thứ tự, nhanh hơn khoảng 50% so với kết hợp nối ngoài bên trái với liên kết chống bán phải, ít nhất là trên các bộ sưu tập nhỏ. Nó đi qua mỗi bộ sưu tập sau khi sắp xếp chỉ một lần.

Tôi cũng đã thêm một câu trả lời khác cho phiên bản hoạt động với EF bằng cách thay thế Invokebằng một bản mở rộng tùy chỉnh.


Thỏa thuận với TP unusedP, TC unusedCcái gì? Họ có nghĩa đen không được sử dụng?
Rudey

Vâng, họ chỉ là hiện tại để nắm bắt các loại trong TP, TC, TResultđể tạo ra thích hợp Expression<Func<>>. Tôi nghĩ tôi có thể thay thế chúng với _, __, ___thay vào đó, nhưng điều đó dường như không bất cứ rõ ràng cho đến khi C # có một ký tự đại diện tham số thích hợp để sử dụng thay thế.
NetMage

1
@MarcL. Tôi không chắc lắm về 'mệt mỏi' - nhưng tôi đồng ý câu trả lời này rất hữu ích trong bối cảnh này. Những thứ ấn tượng (mặc dù với tôi nó xác nhận những thiếu sót của Linq-to-SQL)
sehe

3
Tôi đang nhận được The LINQ expression node type 'Invoke' is not supported in LINQ to Entities.. Có bất kỳ hạn chế với mã này? Tôi muốn thực hiện THAM GIA ĐẦY ĐỦ trên IQueryables
Người học

1
Tôi đã thêm một câu trả lời mới thay thế Invokebằng một tùy chỉnh ExpressionVisitorđể nội tuyến Invokeđể nó sẽ hoạt động với EF. Bạn có thể thử nó không?
NetMage

7

Đây là một phương pháp mở rộng làm điều đó:

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), có nghĩa là tham gia bên ngoài đầy đủ = liên kết bên ngoài bên trái tất cả tham gia bên ngoài bên phải! Tôi đánh giá cao sự đơn giản của phương pháp này.
TamusJRoyce

1
@TamusJRoyce Ngoại trừ Unionloại bỏ trùng lặp, vì vậy nếu có các hàng trùng lặp trong dữ liệu gốc, chúng sẽ không có kết quả.
NetMage

Điểm tuyệt vời! thêm một id duy nhất nếu bạn cần ngăn chặn các bản sao bị xóa. Đúng. Liên minh là một chút lãng phí trừ khi bạn có thể gợi ý rằng có một id duy nhất và liên minh chuyển sang liên minh tất cả (thông qua các heuristic / tối ưu hóa nội bộ). Nhưng nó sẽ hoạt động.
TamusJRoyce


7

Tôi đoán cách tiếp cận của @ sehe mạnh mẽ hơn, nhưng cho đến khi tôi hiểu rõ hơn về nó, tôi thấy mình nhảy vọt ra khỏi phần mở rộng của @ MichaelSander. Tôi đã sửa đổi nó để phù hợp với cú pháp và kiểu trả về của phương thức Enumerable.Join () tích hợp được mô tả ở đây . Tôi đã thêm hậu tố "khác biệt" vào bình luận của @ cadrell0 theo giải pháp của @ 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);
    }

}

Trong ví dụ này, bạn sẽ sử dụng nó như thế này:

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

Trong tương lai, khi tôi tìm hiểu thêm, tôi có cảm giác tôi sẽ chuyển sang logic của @ sehe vì sự phổ biến của nó. Nhưng ngay cả sau đó tôi sẽ phải cẩn thận, bởi vì tôi cảm thấy điều quan trọng là phải có ít nhất một tình trạng quá tải phù hợp với cú pháp của phương thức ".Join ()" hiện có nếu khả thi, vì hai lý do:

  1. Sự nhất quán trong các phương pháp giúp tiết kiệm thời gian, tránh sai sót và tránh hành vi ngoài ý muốn.
  2. Nếu trong tương lai sẽ có một phương thức ".FullJoin () "bên ngoài, tôi sẽ tưởng tượng nó sẽ cố gắng giữ đúng cú pháp của phương thức" .Join () "hiện có nếu có thể. Nếu vậy, nếu bạn muốn di chuyển sang nó, bạn chỉ cần đổi tên các hàm của mình mà không thay đổi các tham số hoặc lo lắng về các kiểu trả về khác nhau phá vỡ mã của bạn.

Tôi vẫn còn mới với tính tổng quát, tiện ích mở rộng, báo cáo Func và các tính năng khác, vì vậy phản hồi chắc chắn được hoan nghênh.

EDIT: Tôi không mất nhiều thời gian để nhận ra có vấn đề với mã của tôi. Tôi đã thực hiện một .Dump () trong LINQPad và xem loại trả về. Nó chỉ là IEnumerable, vì vậy tôi đã cố gắng để phù hợp với nó. Nhưng khi tôi thực sự đã thực hiện .Where () hoặc .Select () trên tiện ích mở rộng của mình, tôi đã gặp lỗi: "'Bộ sưu tập hệ thống.IEnumerable' không chứa định nghĩa cho 'Chọn' và ...". Vì vậy, cuối cùng tôi đã có thể khớp với cú pháp đầu vào của .Join (), nhưng không phải là hành vi trả về.

EDIT: Đã thêm "TResult" vào kiểu trả về cho hàm. Bỏ lỡ điều đó khi đọc bài viết của Microsoft, và tất nhiên nó có ý nghĩa. Với bản sửa lỗi này, có vẻ như hành vi trả lại phù hợp với mục tiêu của tôi.


+2 cho câu trả lời này cũng như Michael Sanders. Tôi vô tình bấm vào đây và bỏ phiếu. Vui lòng thêm hai.
TamusJRoyce

@TamusJRoyce, tôi vừa vào để chỉnh sửa các định dạng mã một chút. Tôi tin rằng sau khi chỉnh sửa được thực hiện, bạn có tùy chọn để lấy lại phiếu bầu của mình. Cho nó một shot nếu bạn thích.
pwilcox

Cảm ơn bạn rất nhiều!
Roshna Omer

6

Như bạn đã tìm thấy, Linq không có cấu trúc "tham gia ngoài". Gần nhất bạn có thể nhận được là một tham gia bên ngoài bên trái bằng cách sử dụng truy vấn bạn đã nêu. Để làm điều này, bạn có thể thêm bất kỳ yếu tố nào trong danh sách họ không được thể hiện trong phép nối:

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

Tôi thích câu trả lời của sehe, nhưng nó không sử dụng thực thi bị trì hoãn (các chuỗi đầu vào được liệt kê một cách háo hức bằng các cuộc gọi đến ToLookup). Vì vậy, sau khi xem các nguồn .NET cho LINQ-to-object , tôi đã nghĩ ra điều này:

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

Việc thực hiện này có các thuộc tính quan trọng sau:

  • Thực hiện hoãn lại, trình tự đầu vào sẽ không được liệt kê trước khi trình tự đầu ra được liệt kê.
  • Chỉ liệt kê các chuỗi đầu vào một lần mỗi.
  • Giữ nguyên thứ tự của các chuỗi đầu vào, theo nghĩa là nó sẽ tạo ra các bộ dữ liệu theo thứ tự của chuỗi bên trái và sau đó bên phải (đối với các phím không có trong chuỗi bên trái).

Các thuộc tính này rất quan trọng, vì chúng là những gì một người mới sử dụng FullOuterJoin nhưng có kinh nghiệm với LINQ sẽ mong đợi.


Nó không bảo toàn thứ tự của các chuỗi đầu vào: Tra cứu không đảm bảo rằng, vì vậy các lệnh này sẽ liệt kê theo một số thứ tự bên trái, sau đó một số thứ tự của bên phải không xuất hiện ở phía bên trái. Nhưng thứ tự quan hệ của các yếu tố không được bảo tồn.
Ivan Danilov

@IvanDanilov Bạn đúng là điều này không thực sự có trong hợp đồng. Tuy nhiên, việc triển khai ToLookup sử dụng lớp Tra cứu nội bộ trong Enumerable.cs giữ các nhóm trong danh sách được liên kết theo thứ tự chèn và sử dụng danh sách này để lặp qua chúng. Vì vậy, trong phiên bản .NET hiện tại, thứ tự được đảm bảo, nhưng vì MS không may không có tài liệu này, nên họ có thể thay đổi nó trong các phiên bản sau.
Søren Boisen

Tôi đã thử nó trên .NET 4.5.1 trên Win 8.1 và nó không giữ được thứ tự.
Ivan Danilov

1
".. các chuỗi đầu vào được liệt kê một cách háo hức bởi các cuộc gọi đến ToLookup". Nhưng việc thực hiện của bạn thực hiện giống hệt nhau .. Năng suất không mang lại nhiều ở đây vì chi phí cho máy trạng thái hữu hạn.
pkuderov

4
Các cuộc gọi Tra cứu được thực hiện khi phần tử đầu tiên của kết quả được yêu cầu, chứ không phải khi trình lặp được tạo. Đó là ý nghĩa của việc thực hiện hoãn lại. Bạn có thể trì hoãn việc liệt kê một bộ đầu vào hơn nữa, bằng cách lặp lại trực tiếp Số lượng bên trái thay vì chuyển đổi nó thành Tra cứu, dẫn đến lợi ích bổ sung là thứ tự của bộ bên trái được giữ nguyên.
Rolf

2

Tôi quyết định thêm câu này dưới dạng câu trả lời riêng vì tôi không tích cực, nó đã được kiểm tra đủ. Đây là một thực hiện lại củaFullOuterJoin phương thức bằng cách sử dụng một phiên bản LINQKit Invoke/ Expandcho đơn giản hóa, tùy chỉnh Expressionđể nó hoạt động với Entity Framework. Không có nhiều lời giải thích vì nó khá giống với câu trả lời trước đây của tôi.

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, mã hóa ấn tượng! Khi tôi chạy nó với một ví dụ đơn giản và khi [NullVisitor.Visit (..) được gọi trong [base.Visit (Node)], nó sẽ ném [System.ArgumentException: Loại đối số không khớp]. Điều này đúng, vì tôi đang sử dụng [Hướng dẫn] TKey và tại một số điểm, khách truy cập null mong đợi Loại [Hướng dẫn?]. Có thể tôi đang thiếu một cái gì đó. Tôi có một ví dụ ngắn được mã hóa cho EF 6.4.4. Xin vui lòng cho tôi biết làm thế nào tôi có thể chia sẻ mã này với bạn. Cảm ơn!
Troncho

@Troncho Tôi thường sử dụng LINQPad để thử nghiệm, vì vậy, EF 6 không dễ thực hiện. base.Visit(node)không nên ném một ngoại lệ vì nó chỉ tái phát xuống cây. Tôi có thể truy cập khá nhiều dịch vụ chia sẻ mã, nhưng không thiết lập cơ sở dữ liệu thử nghiệm. Mặc dù vậy, việc chạy nó chống lại thử nghiệm LINQ to SQL của tôi dường như vẫn hoạt động tốt.
NetMage

@Troncho Có thể bạn đang tham gia giữa một Guidkhóa và một Guid?khóa ngoại?
NetMage

Tôi cũng đang sử dụng LinqPad để thử nghiệm. Truy vấn của tôi đã ném ArgumentException vì vậy tôi quyết định gỡ lỗi nó trên VS2019 trên [.Net Framework 4.7.1] và phiên bản mới nhất của EF 6. Tôi phải theo dõi vấn đề thực sự. Để kiểm tra mã của bạn, tôi đang tạo 2 bộ dữ liệu riêng biệt có nguồn gốc từ cùng một bảng [Người]. Tôi lọc cả hai bộ để một số bản ghi là duy nhất cho mỗi bộ và một số tồn tại trên cả hai bộ. [PersonId] là Hướng dẫn [Khóa chính] (c #) / Uniqueidentifier (SqlServer) và không được đặt tạo bất kỳ giá trị null [PersonId] nào. Mã được chia sẻ: github.com/Troncho/EF_FullOuterJoin
Troncho

1

Thực hiện phép liệt kê luồng trong bộ nhớ trên cả hai đầu vào và gọi bộ chọn cho mỗi hàng. Nếu không có mối tương quan nào ở lần lặp hiện tại, một trong các đối số của bộ chọn sẽ là null .

Thí dụ:

   var result = left.FullOuterJoin(
         right, 
         x=>left.Key, 
         x=>right.Key, 
         (l,r) => new { LeftKey = l?.Key, RightKey=r?.Key });
  • Yêu cầu IComparer cho loại tương quan, sử dụng so sánh.Default nếu không được cung cấp.

  • Yêu cầu 'OrderBy' được áp dụng cho các liệt kê đầu vào

    /// <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
Đó là một nỗ lực anh hùng để làm cho mọi thứ "phát trực tuyến". Đáng buồn thay, tất cả lợi ích bị mất ở bước đầu tiên, nơi bạn thực hiện OrderBytrên cả hai phép chiếu chính. OrderByđệm toàn bộ chuỗi, vì những lý do rõ ràng .
sehe

@sehe Bạn chắc chắn đúng cho Linq to Object. Nếu IEnumerable <T> là IQueryable <T> thì nguồn sẽ được sắp xếp - mặc dù không có thời gian để kiểm tra. Nếu tôi sai về điều này, chỉ cần thay thế đầu vào IEnumerable <T> bằng IQueryable <T> sẽ sắp xếp trong nguồn / cơ sở dữ liệu.
James Caradoc-Davies

1

Giải pháp sạch của tôi cho tình huống đó là duy nhất trong cả hai bảng liệt kê:

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

vì thế

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

đầu ra:

f: ali l: rezaei
f: mohammad l:
f:  l: kazemi

0

Tham gia ngoài đầy đủ cho hai hoặc nhiều bảng: Đầu tiên trích xuất cột mà bạn muốn tham gia.

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

Sau đó sử dụng nối ngoài bên trái giữa cột được trích xuất và các bảng chính.

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

Tôi đã viết lớp tiện ích mở rộng này cho một ứng dụng có lẽ 6 năm trước và đã sử dụng nó từ nhiều giải pháp mà không gặp vấn đề gì. Hy vọng nó giúp.

chỉnh sửa: Tôi nhận thấy một số có thể không biết cách sử dụng một lớp mở rộng.

Để sử dụng lớp mở rộng này, chỉ cần tham chiếu không gian tên của nó trong lớp của bạn bằng cách thêm dòng sau bằng joinext;

^ điều này sẽ cho phép bạn thấy sự xuất hiện của các hàm mở rộng trên bất kỳ bộ sưu tập đối tượng IEnumerable nào bạn sử dụng.

Hi vọng điêu nay co ich. Hãy cho tôi biết nếu nó vẫn chưa rõ ràng và tôi hy vọng sẽ viết một ví dụ mẫu về cách sử dụng nó.

Bây giờ đây là lớp học:

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
Thật không may, có vẻ như hàm trong SelectManykhông thể được chuyển đổi thành cây biểu thức xứng đáng với LINQ2Query.
HOẶC Mapper

edc65. Tôi biết đó có thể là một câu hỏi ngớ ngẩn nếu bạn đã làm điều đó rồi. Nhưng chỉ trong trường hợp (như tôi đã nhận thấy một số người không biết), bạn chỉ cần tham chiếu không gian tên joinext.
H7O

HOẶC Mapper, hãy cho tôi biết loại bộ sưu tập bạn muốn nó hoạt động. Nó sẽ hoạt động tốt với mọi bộ sưu tập IEnumerable
H7O

0

Tôi nghĩ rằng mệnh đề tham gia LINQ không phải là giải pháp chính xác cho vấn đề này, vì mục đích của mệnh đề tham gia không phải là tích lũy dữ liệu theo cách cần thiết cho giải pháp nhiệm vụ này. Mã để hợp nhất các bộ sưu tập riêng biệt được tạo ra trở nên quá phức tạp, có thể nó ổn cho mục đích học tập, nhưng không phải cho các ứng dụng thực tế. Một trong những cách giải quyết vấn đề này là trong đoạn mã dưới đây:

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

Nếu các bộ sưu tập thực sự lớn cho sự hình thành Hashset thay vì các vòng lặp foreach có thể được sử dụng mã dưới đây:

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

Cảm ơn tất cả mọi người vì những bài viết thú vị!

Tôi đã sửa đổi mã bởi vì trong trường hợp của tôi, tôi cần

  • một vị ngữ tham gia cá nhân
  • một so sánh liên minh cá nhân

Đối với những người quan tâm đây là mã sửa đổi của tôi (trong VB, xin lỗi)

    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

Một tham gia bên ngoài đầy đủ khác

Vì không hài lòng với sự đơn giản và dễ đọc của các đề xuất khác, tôi đã kết thúc với điều này:

Nó không có giả vờ là nhanh (khoảng 800 ms để tham gia 1000 * 1000 trên CPU 2020m: 2.4ghz / 2 lõi). Đối với tôi, nó chỉ là một tham gia bên ngoài đầy đủ nhỏ gọn và giản dị.

Nó hoạt động tương tự như SQL FULL OUTER THAM GIA (bảo tồn trùng lặp)

Chúc mừng ;-)

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

Ý tưởng là để

  1. Xây dựng Id dựa trên các trình xây dựng chức năng chính được cung cấp
  2. Quá trình chỉ còn lại các mục
  3. Quá trình tham gia bên trong
  4. Xử lý đúng mục duy nhất

Đây là một bài kiểm tra ngắn gọn đi kèm với nó:

Đặt điểm dừng ở cuối để xác minh thủ công rằng nó hoạt động như mong đợi

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

Tôi thực sự ghét các biểu thức linq này, đây là lý do tại sao SQL tồn tại:

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

Tạo cái này dưới dạng sql view trong cơ sở dữ liệu và nhập nó dưới dạng thực thể.

Tất nhiên, sự kết hợp (khác biệt) của các phép nối trái và phải sẽ làm cho nó quá, nhưng nó là ngu ngốc.


11
Tại sao không bỏ càng nhiều trừu tượng càng tốt và làm điều này trong mã máy? (Gợi ý: bởi vì sự trừu tượng bậc cao hơn giúp cuộc sống của lập trình viên dễ dàng hơn). Điều này không trả lời câu hỏi và đối với tôi giống như một lời ca ngợi chống lại LINQ.
tiêu

8
Ai nói dữ liệu đến từ cơ sở dữ liệu?
user247702

1
Tất nhiên, đó là cơ sở dữ liệu, có từ "tham gia bên ngoài" trong câu hỏi :) google.cz/search?q=outer+join
Milan vec

1
Tôi hiểu rằng đây là giải pháp "lỗi thời", nhưng trước khi hạ cấp, hãy so sánh độ phức tạp của nó với các giải pháp khác :) Ngoại trừ giải pháp được chấp nhận, tất nhiên đó là giải pháp chính xác.
Milan Švec

Tất nhiên nó có thể là một cơ sở dữ liệu hoặc không. Tôi đang tìm kiếm một giải pháp với sự kết hợp bên ngoài giữa các danh sách trong bộ nhớ
edc65
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.