LINQ - Tam Dış Bağlantı


204

İnsanların kimliğini ve adlarını, insanların kimliğini ve soyadlarını içeren bir listem var. Bazı insanların bir adı ve bazılarının soyadı yoktur; İki listede tam bir dış birleşim yapmak istiyorum.

Yani aşağıdaki listeler:

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

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

Üretmek gerekir:

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

LINQ'da yeniyim (topallıysam beni affet) ve 'LINQ Outer Joins' için oldukça benzer, ancak gerçekten dış birleşimler gibi görünen birkaç çözüm buldum.

Şu ana kadar yaptığım girişimler şöyle:

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

Ama bu geri dönüyor:

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

Neyi yanlış yapıyorum?


2
Yalnızca bellek içi listelerde veya Linq2Sql'de çalışmak için buna ihtiyacınız var mı?
JamesFaix

Yanıtlar:


123

Bunun tüm vakaları kapsayıp kapsamadığını bilmiyorum, mantıksal olarak doğru görünüyor. Fikir, sol dış birleştirme ve sağ dış birleştirme almak ve sonuçların birleşimini almaktır.

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

Bu, LINQ to Objects olduğu için yazıldığı gibi çalışır. SQL veya başka bir LINQ ise, sorgu işlemcisi güvenli gezinme veya diğer işlemleri desteklemeyebilir. Değerleri koşullu olarak almak için koşullu işleci kullanmanız gerekir.

yani

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
Sendika yinelemeleri ortadan kaldıracak. Yinelenenleri beklemiyorsanız veya ilkine dahil olan herhangi bir şeyi hariç tutmak için ikinci sorguyu yazabiliyorsanız bunun yerine Concat kullanın. Bu, UNION ve UNION ALL arasındaki SQL farkıdır
cadrell0 20

3
Bir kişinin adı ve soyadı varsa @ cadre110 kopyaları oluşacaktır, bu nedenle birleşim geçerli bir seçimdir.
saus

1
@saus ancak bir kimlik sütunu var, bu yüzden yinelenen bir ad ve soyadı olsa bile, kimlik farklı olmalı
cadrell0

1
Çözümünüz ilkel türler için çalışıyor, ancak nesneler için çalışmıyor gibi görünüyor. Benim durumumda, FirstName bir etki alanı nesnesidir, LastName ise başka bir etki alanı nesnesidir. İki sonucu birleştirdiğimde, LINQ bir NotSupportedException oluşturdu (Birlik veya Concat Türleri uyumsuz olarak oluşturuldu). Benzer problemler yaşadın mı?
Candy Chiu

1
@CandyChiu: Aslında hiç böyle bir durumla karşılaşmadım. Sanırım bu sorgu sağlayıcınızla ilgili bir sınırlama. AsEnumerable()Birleştirme / birleştirme işlemini gerçekleştirmeden önce büyük olasılıkla bu durumda Nesneler için LINQ kullanmak isteyeceksiniz . Bunu deneyin ve bunun nasıl gittiğini görün. Bu gitmek istediğiniz rota değilse, bundan daha fazla yardım alabileceğimden emin değilim.
Jeff Mercado

196

Güncelleme 1: gerçekten genelleştirilmiş bir uzatma yöntemi sağlama FullOuterJoin
Güncelleme 2: isteğe bağlı IEqualityComparerolarak anahtar türü için bir özel kabul
Güncelleme 3 : bu uygulama son zamanlarda bir parçası haline geldiMoreLinq - Teşekkürler çocuklar!

Düzenle Eklendi FullOuterGroupJoin( ideone ). GetOuter<>Uygulamayı yeniden kullandım, bunu olabileceğinden daha az performanslı hale getirdim, ancak şu anda kanama kenarı optimize edilmemiş 'yüksek seviye' kodunu hedefliyorum.

Http://ideone.com/O36nWc adresinde canlı olarak izleyebilirsiniz.

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

Çıktıyı yazdırır:

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

Varsayılanları da sağlayabilirsiniz: 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)" }
        )

Baskı:

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

Kullanılan terimlerin açıklaması:

Birleştirme, ilişkisel veritabanı tasarımından ödünç alınan bir terimdir:

  • Bir katılmak elemanları tekrarlayacak aiçinde unsur vardır kadar çok kez b anahtar karşılık gelen (yani: hiçbir şey eğer bboş). Veritabanı lingo bunu çağırırinner (equi)join .
  • Bir dış birleştirme elemanları içeren aolan bir tekabül eden eleman bulunmaktadır b. (yani: bboş olsaydı bile sonuçlar ). Bu genellikleleft join .
  • Bir tam dış birleştirme gelen kayıtları içeren a hem deb eğer hiçbir karşılık gelen elemanın diğer bulunmaktadır. (örneğin a, boşsa bile sonuçlar )

RDBMS'de genellikle görülmeyen bir şey bir gruba katılmadır [1] :

  • Bir grup katılması , yukarıda tarif edildiği gibi aynı yapar , ancak bunun yerine elemanları tekrar eden akarşılık gelen çoklu için b, bu gruplar, ilgili tuşlar ile kayıtları. Bu, ortak bir anahtara göre 'birleştirilmiş' kayıtlar aracılığıyla numaralandırmak istediğinizde genellikle daha kullanışlıdır.

Ayrıca bazı genel arka plan açıklamalarını da içeren GroupJoin'e bakınız .


[1] (Oracle ve MSSQL'in bunun için özel uzantıları olduğuna inanıyorum)

Tam kod

Bunun için genelleştirilmiş bir 'bırakma' Uzantı sınıfı

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

FullOuterJoinSağlanan uzatma yönteminin kullanımını göstermek için düzenlendi
sehe

Düzenlendi: FullOuterGroupJoin uzatma yöntemi eklendi
sehe

4
Bir Sözlük kullanmak yerine, yardımcı uzantı yöntemlerinizde ifade edilen işlevselliği içeren bir Arama kullanabilirsiniz . Örneğin, yazabilirsiniz a.GroupBy(selectKeyA).ToDictionary();olarak a.ToLookup(selectKeyA)ve adict.OuterGet(key)olduğu gibi alookup[key]. Anahtarlarını teslim alma olsa biraz yanıltıcıdır vardır: alookup.Select(x => x.Keys).
Riskli Martin

1
@RiskyMartin Teşekkürler! Bu gerçekten de her şeyi daha zarif kılıyor. Cevabı ve ideojenleri güncelledim . (Daha az nesne başlatıldığından performansın artırılması gerektiğini düşünüyorum).
12'de

1
@Sadece anahtarların benzersiz olduğunu biliyorsanız işe yarar. Ve / grouping / için ortak durum bu değildir. Bunun dışında, evet, elbette. Hash'in sürüklenmeyeceğini biliyorsanız (düğüm bazlı kaplar prensipte daha fazla maliyete sahiptir ve hashlama ücretsiz değildir ve verimlilik karma fonksiyonuna / kova yayılmasına bağlıdır), kesinlikle daha algoritmik olarak verimli olacaktır. Yani, küçük yükler için ben daha hızlı olmayabilir beklediğiniz
sehe

27

Ben, kabul edilen cevap da dahil olmak üzere, bunların çoğu ile ilgili sorunlar olduğunu düşünüyorum, çünkü çok fazla sunucu gidiş-dönüş ve çok fazla veri iadesi yapmak ya da çok fazla istemci yürütme nedeniyle IQueryable üzerinde Linq ile iyi çalışmıyor.

IEnumerable için Sehe'nin cevabını ya da benzerini sevmiyorum çünkü aşırı bellek kullanımı var (basit bir 10000000 iki liste testi 32GB makinemde Linqpad'i bellekte çalıştırdı).

Ayrıca, diğerlerinin çoğu aslında uygun bir Tam Dış Birleştirme uygulamıyor çünkü bir Sağ Anti Yarı Birleşim ile Concat yerine Sağ Birleşme ile bir Birlik kullanıyorlar, bu da sadece yinelenen iç birleşim satırlarını sonuçtan ortadan kaldırmakla kalmıyor, aynı zamanda orijinal olarak sol veya sağ verilerde var olan uygun kopyalar.

İşte burada tüm bu sorunları ele, SQL oluşturmak yanı sıra doğrudan SQL üzerinde LINQ birleştirme uygulamak, sunucuda yürütme ve Enumerables diğerlerine göre daha hızlı ve daha az bellek ile uzantıları:

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

Sağ Anti-Yarı-Birleştirme arasındaki fark çoğunlukla Linq to Objects veya kaynakta tartışmalıdır, ancak son yanıtta sunucu (SQL) tarafında bir fark yaratarak gereksiz olanı kaldırır JOIN.

Bir lambda ile Expressionbirleştirmek için el kodlaması Expression<Func<>>LinqKit ile geliştirilebilir, ancak dil / derleyici bunun için biraz yardım ekleseydi iyi olurdu. FullOuterJoinDistinctVe RightOuterJoinişlevleri tamamlama amacıyla dahil edilmişlerdir ama-uygulamak yeniden vermedi FullOuterGroupJoinhenüz.

İçin tam dış birleşimin başka bir sürümünü yazdımIEnumerableEn azından küçük koleksiyonlarda, sol dış birleştirmeyi sağ anti-yarı birleştirmeyle birleştirmekten yaklaşık% 50 daha hızlı olan durumlarda anahtarın düzenlenebilir olduğu durumlar . Sadece bir kez sıraladıktan sonra her koleksiyondan geçer.

Ayrıca , özel bir genişletme ile değiştirerek EF ile çalışan bir sürüm için başka bir yanıt ekledim Invoke.


Anlaşma nedir TP unusedP, TC unusedC? Kelimenin tam anlamıyla kullanılmamış mı?
Rudey

Evet, onlar içinde türlerini yakalamak için sadece mevcut TP, TC, TResultdüzgün oluşturmak için Expression<Func<>>. Ben bunların yerine olabilir gerekiyordu _, __, ___C # yerine kullanılacak uygun bir parametre joker olana kadar yerine, ama bu daha açık görünmüyor.
NetMage

1
@MarcL. 'Yorucu' hakkında pek emin değilim - ama bu cevabın bu bağlamda çok yararlı olduğuna katılıyorum. Etkileyici şeyler (her ne kadar benim için Linq-to-SQL eksikliklerini
doğrulasa da

3
Anlıyorum The LINQ expression node type 'Invoke' is not supported in LINQ to Entities.. Bu kodla ilgili herhangi bir kısıtlama var mı? IQueryables üzerinden FULL JOIN gerçekleştirmek istiyorum
Öğrenci

1
EF ile çalışması için satır içi Invokebir geleneğin yerine yeni bir yanıt ekledim . Deneyebilir misin? ExpressionVisitorInvoke
NetMage

7

İşte bunu yapan bir uzantı yöntemi:

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), yani tam bir dış birleşim = sol dış birleşim birleşimi tüm sağ dış birleşim! Bu yaklaşımın sadeliğini takdir ediyorum.
TamusJRoyce

1
@TamusJRoyce UnionYinelenenleri kaldırır, bu nedenle orijinal verilerde yinelenen satırlar varsa sonuçta olmazlar.
NetMage

Harika bir nokta! yinelenenlerin kaldırılmasını önlemeniz gerekiyorsa benzersiz bir kimlik ekleyin. Evet. Benzersiz bir kimlik olduğunu ve sendikanın hepsini sendikaya çevirdiğini (dahili sezgisel tarama / optimizasyonlar aracılığıyla) ima etmedikçe sendika biraz savurgan. Ama işe yarayacak.
TamusJRoyce


7

Sanırım @ sehe'nin yaklaşımı daha güçlü, ama daha iyi anlayana kadar kendimi @ MichaelSander'ın uzantısından atlayarak buluyorum. Burada açıklanan yerleşik Enumerable.Join () yönteminin sözdizimi ve dönüş türüyle eşleşecek şekilde değiştirdim . @ Cadrell0'ın @ JeffMercado'nun çözümü altındaki yorumu ile ilgili "farklı" soneki ekledim.

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

}

Örnekte, bunu şu şekilde kullanırsınız:

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

Gelecekte, daha fazla bilgi edindikçe, popülerliği göz önüne alındığında @ sehe'nin mantığına geçeceğimi hissediyorum. Ama o zaman bile dikkatli olmalıyım, çünkü mümkünse mevcut ".Join ()" yönteminin sözdizimiyle eşleşen en az bir aşırı yükün iki nedenden dolayı önemli olduğunu hissediyorum:

  1. Yöntemlerdeki tutarlılık zaman kazanmanıza, hatalardan kaçınmanıza ve istenmeyen davranışlardan kaçınmanıza yardımcı olur.
  2. Gelecekte bir kutudan çıkma ".FullJoin ()" yöntemi varsa, şu anda varolan ".Join ()" yönteminin sözdizimine uymaya çalışacağını düşünürdüm. Eğer öyleyse, o zaman taşımak istiyorsanız, parametreleri değiştirmeden veya kodunuzu kıran farklı dönüş türleri hakkında endişelenmeden işlevlerinizi yeniden adlandırabilirsiniz.

Jenerikler, uzantılar, Func ifadeleri ve diğer özelliklerle hala yeniyim, bu yüzden geri bildirim kesinlikle hoş geldiniz.

EDIT: Kodumda bir sorun olduğunu fark etmek beni uzun sürmedi. LINQPad'de bir .Dump () yapıyordum ve dönüş tipine bakıyordum. Sadece IEnumerable idi, bu yüzden eşleştirmeye çalıştım. Ancak uzantımda aslında bir .Where () veya .Select () işlemi yaptığımda bir hata aldım: "'Sistem Koleksiyonları. Gereksinim', 'Seç' ve ..." için bir tanım içermiyor. Sonunda .Join () giriş sözdizimini eşleştirebildim, ancak dönüş davranışıyla eşleşmedi.

EDIT: İşlevin dönüş türüne "TResult" eklendi. Microsoft makalesini okurken kaçırdım ve elbette mantıklı. Bu düzeltmeyle birlikte, geri dönüş davranışı sonuçta hedeflerime uygun görünüyor.


Michael Sanders'ın yanı sıra bu cevap için +2. Bunu yanlışlıkla tıkladım ve oylar kilitlendi. Lütfen iki tane ekleyin.
TamusJRoyce

@TamusJRoyce, az önce kod formatlarını düzenlemek için gittim. Bir düzenleme yapıldıktan sonra oyunuzu yeniden seçme seçeneğiniz olduğuna inanıyorum. İsterseniz bir şans verin.
pwilcox

Çok teşekkür ederim!
Roshna Omer

6

Bildiğiniz gibi Linq'in bir "dış birleşim" yapısı yok. Alabileceğiniz en yakın şey, belirttiğiniz sorguyu kullanarak sol dış birleşmedir. Buna, soyad listesinin birleştirme işleminde temsil edilmeyen tüm öğelerini ekleyebilirsiniz:

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'nin cevabını seviyorum, ancak ertelenmiş yürütme kullanmıyor (giriş dizileri ToLookup'a yapılan çağrılarla hevesle numaralandırılıyor). Yani LINQ-to-nesneler için .NET kaynaklarına baktıktan sonra , ben bu ile geldi:

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

Bu uygulama aşağıdaki önemli özelliklere sahiptir:

  • Ertelenmiş yürütme, giriş dizisi, çıkış dizisi numaralandırılmadan önce numaralandırılmaz.
  • Giriş dizilerini yalnızca bir kez numaralandırır.
  • Sol sekans ve sonra sağa doğru tuples vereceği anlamında giriş dizilerinin sırasını korur (sol dizide bulunmayan tuşlar için).

Bu özellikler önemlidir, çünkü FullOuterJoin'de yeni olan ancak LINQ ile deneyimli birinin beklediği şeydir.


Giriş dizilerinin sırasını korumaz: Arama bunu garanti etmez, bu nedenle bu foreach'lerin bir miktar sol tarafa göre sıralanması, daha sonra sağ tarafın sol tarafında bulunmayan bazı sıralar. Ancak öğelerin ilişkisel sırası korunmaz.
Ivan Danilov

@IvanDanilov Bunun aslında sözleşmede olmadığını doğru söylüyorsunuz. Bununla birlikte, ToLookup uygulaması, Enumerable.cs dosyasında gruplamaları ekleme emriyle bağlantılı bir bağlantı listesinde tutan dahili bir Arama sınıfı kullanır ve bu listeyi yinelemek için kullanır. Bu nedenle, geçerli .NET sürümünde sipariş garanti edilir, ancak MS maalesef bunu belgelemediğinden, sonraki sürümlerde değiştirebilirler.
Søren Boisen

Win 8.1 üzerinde .NET 4.5.1 üzerinde denedim ve siparişi korumaz.
Ivan Danilov

1
".. giriş dizileri ToLookup'a yapılan çağrılarla hevesle sıralanır". Ama sizin uygulamanız da aynı şeyi yapıyor .. Sonlu-devlet makinesindeki masraflar nedeniyle, üretim burada fazla bir şey vermiyor.
pkuderov

4
Arama çağrıları, yineleyici oluşturulduğunda değil, sonucun ilk öğesi istendiğinde yapılır. Ertelenmiş yürütmenin anlamı budur. Bir girdi kümesinin numaralandırılmasını daha da ileriye götürebilirsiniz, sol Numaralandırmayı bir Aramaya dönüştürmek yerine doğrudan yineleyerek, sol kümenin sırasının korunduğu ekstra fayda sağlayarak.
Rolf

2

Yeterince test edilmediğimden emin olmadığım için bunu ayrı bir cevap olarak eklemeye karar verdim. Bu, Entity Framework'ü çalışması için FullOuterJoinyöntemin basitleştirilmiş, özelleştirilmiş bir LINQKit Invoke/ Expandfor sürümünü kullanarak yeniden uygulanmasıdır Expression. Önceki cevabımla hemen hemen aynı olduğu için çok fazla açıklama yok.

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, etkileyici kodlama! Basit bir örnekle çalıştırdığımda ve [NullVisitor.Visit (..) [base.Visit (Node)] içinde çağrıldığında bir [System.ArgumentException: Bağımsız Değişken Türleri eşleşmiyor] atar. Bir [Guid] TKey kullanıyorum ve bir noktada null ziyaretçi bir [Guid?] Tipi bekliyor. Belki bir şey eksik olabilirim. EF 6.4.4 için kodlanmış kısa bir örneğim var. Lütfen bu kodu sizinle nasıl paylaşabilirim. Teşekkürler!
Troncho

@Troncho Test için normalde LINQPad kullanıyorum, bu yüzden EF 6 kolayca yapılmıyor. base.Visit(node)bir istisna atmamalıydı, çünkü sadece ağaçtan aşağı iner. Hemen hemen tüm kod paylaşım hizmetlerine erişebilirim, ancak bir test veritabanı kurmuyorum. Gerçi benim LINQ to SQL testine karşı çalıştırmak iyi çalışıyor gibi görünüyor.
NetMage

@Troncho Bir Guidanahtar ile Guid?yabancı anahtar arasında katılmanız mümkün mü ?
NetMage

Test için de LinqPad kullanıyorum. Sorgum ArgumentException attı, bu yüzden [.Net Framework 4.7.1] ve en son EF 6 VS2019 hata ayıklama karar verdi. Orada gerçek sorunu izlemek lazım. Kodunuzu test etmek için, aynı [Kişiler] tablosundan kaynaklanan 2 ayrı veri kümesi oluşturuyorum. Her iki seti de filtreleyeceğim, böylece bazı kayıtlar her set için benzersiz ve bazıları her iki sette de var olacak. [PersonId], [Birincil Anahtar] Kılavuzudur (c #) / Uniqueidentifier (SqlServer) ve her iki küme de boş [PersonId] değeri üretmez. Paylaşılan kod: github.com/Troncho/EF_FullOuterJoin
Troncho

1

Her iki giriş üzerinde bir bellek içi akış numaralandırması gerçekleştirir ve her satır için seçiciyi çağırır. Geçerli yinelemede korelasyon yoksa, seçici bağımsız değişkenlerden biri boş olacaktır .

Misal:

   var result = left.FullOuterJoin(
         right, 
         x=>left.Key, 
         x=>right.Key, 
         (l,r) => new { LeftKey = l?.Key, RightKey=r?.Key });
  • Korelasyon türü için bir IComparer gerektirir, sağlanmamışsa Compareer.Default öğesini kullanır.

  • Girilen numaralandırmalara 'OrderBy' uygulanmasını gerektirir

    /// <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
Bu, işleri “akış” haline getirmek için kahramanca bir çaba. Ne yazık ki, tüm kazançlar OrderByher iki önemli projeksiyonda da yaptığınız ilk adımda kayboluyor . OrderBybariz nedenlerle tüm diziyi tamponlar .
16:51 2151 sehe

@sehe Linq to Objects için kesinlikle doğru. IEnumerable <T> IQueryable <T> ise, kaynak sıralanmalıdır - yine de test etmek için zaman yoktur. Bu konuda yanılıyorsam, IEnumerable <T> girdisini IQueryable <T> ile değiştirmek, kaynak / veritabanında sıralanmalıdır.
James Caradoc-Davies

1

Anahtarın her iki numarada da benzersiz olduğu durumlar için temiz çözümüm:

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

yani

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

çıktılar:

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

0

İki veya daha fazla tablo için tam dış birleştirme: İlk olarak, birleştirmek istediğiniz sütunu çıkarın.

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

Sonra çıkarılan sütun ve ana tablolar arasında sol dış birleşim kullanın.

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

Bu uzantı sınıfını belki 6 yıl önce bir uygulama için yazdım ve o zamandan beri sorunsuz bir şekilde birçok çözümde kullanıyorum. Umarım yardımcı olur.

edit: Bazıları bir uzantı sınıfı kullanmayı bilmiyor olabilir fark ettim.

Bu uzantı sınıfını kullanmak için joinext kullanarak aşağıdaki satırı ekleyerek sınıfınızdaki ad alanına başvurmanız yeterlidir;

^ Bu, kullandığınız IEnumerable nesne koleksiyonundaki uzantı işlevlerinin akıllılığını görmenizi sağlayacaktır.

Bu yardımcı olur umarım. Hala net olup olmadığını bana bildirin, umarım nasıl kullanılacağına dair örnek bir örnek yazacağım.

İşte sınıf:

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
Ne yazık ki, işlev SelectManyLINQ2SQL-değerli bir ifade ağacına dönüştürülemez gibi görünüyor, öyle görünüyor.
VEYA Haritacı

edc65. Bunu zaten yapsaydın aptalca bir soru olabileceğini biliyorum. Ancak her ihtimale karşı (bazılarının bilmediğini fark ettiğim gibi), sadece joinext ad alanına başvurmanız gerekir.
H7O

VEYA Mapper, ne tür bir koleksiyonun çalışmasını istediğinizi bana bildirin. Herhangi bir IEnumerable koleksiyonuyla iyi çalışmalıdır
H7O

0

Bence LINQ birleştirme yan tümcesi bu soruna doğru çözüm değildir, çünkü bir yan tümce amacı bu görev çözümü için gereken şekilde veri biriktirmek değildir. Oluşturulan ayrı koleksiyonları birleştirmek için kod çok karmaşık hale gelir, belki de öğrenme amaçları için sorun olmaz, ancak gerçek uygulamalar için uygun değildir. Bu sorunu çözmenin yollarından biri aşağıdaki koddadır:

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 oluşumu için gerçek koleksiyonlar büyükse, foreach döngüleri aşağıdaki kodu kullanabilir:

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

İlginç gönderiler için herkese teşekkürler!

Kodu değiştirdim çünkü benim durumumda ihtiyacım vardı

  • Bir yüklem katılmak kişiselleştirilmiş
  • bir kişiselleştirilmiş Sendika ayrı karşılaştırıcısı

İlgilenenler için bu benim değiştirilmiş kodum (VB'de, üzgünüm)

    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

Yine başka bir tam dış birleşim

Diğer önermelerin basitliğinden ve okunabilirliğinden o kadar mutlu olmadığından, bununla sonuçlandım:

Hızlı olma iddiası yok (2020m CPU'da 1000 * 1000'e katılmak için yaklaşık 800 ms: 2.4 ghz / 2 çekirdek). Bana göre, sadece kompakt ve rahat bir tam dış birleşim.

SQL FULL OUTER JOIN ile aynı şekilde çalışır (koruma kopyaları)

Şerefe ;-)

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

Fikir

  1. Sağlanan temel işlev oluşturuculara dayalı olarak ID'ler oluşturma
  2. Yalnızca soldaki öğeleri işle
  3. Proses iç birleşimi
  4. Yalnızca doğru öğeleri işleme

İşte onunla birlikte kısa ve öz bir test:

Manuel olarak beklendiği gibi davrandığını doğrulamak için bir kesme noktası yerleştirin

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

Bu linq ifadelerinden gerçekten nefret ediyorum, bu yüzden SQL var:

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

Bunu veritabanında sql görünümü olarak oluşturun ve varlık olarak içe aktarın.

Tabii ki, sol ve sağ birleşimlerin (belirgin) birleşimi de bunu yapacak, ama aptalca.


11
Neden sadece mümkün olduğunca fazla soyutlama yapmıyor ve bunu makine kodunda yapmıyorsunuz? (İpucu: yüksek dereceli soyutlamalar programcı için hayatı kolaylaştırır). Bu soruya cevap vermiyor ve bana LINQ'ya karşı bir rant gibi görünüyor.
harcama

8
Verilerin bir veritabanından geldiğini kim söyledi?
user247702

1
Tabii ki, veritabanı, sözde "dış katılmak" sözleri var :) google.cz/search?q=outer+join
Milan Švec

1
Bunun "eski moda" çözümü olduğunu anlıyorum, ancak indirmeden önce karmaşıklığını diğer çözümlerle karşılaştırın :) Kabul edilenin dışında, elbette doğru olanı.
Milan Švec

Tabii ki bir veritabanı olabilir ya da olmayabilir. Hafızadaki listeler arasında dış birleşim olan bir çözüm
arıyorum
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.