2016-03-10 12 views
10

ha un modo per "memorizzare" i risultati della query precedente durante l'interrogazione di?LINQ con query "Memoria"

Si consideri il seguente caso:

public class Foo { 
    public int Id { get; set; } 
    public ICollection<Bar> Bars { get; set; } 
} 

public class Bar { 
    public int Id { get; set; } 
} 

Ora, se due o più Foo hanno lo stesso insieme di Bar (non importa quale sia l'ordine è), essi sono considerati come simileFoo.

Esempio:

foo1.Bars = new List<Bar>() { bar1, bar2 }; 
foo2.Bars = new List<Bar>() { bar2, bar1 }; 
foo3.Bars = new List<Bar>() { bar3, bar1, bar2 }; 

Nel caso precedente, foo1 è simile a foo2 ma entrambi foo1 e foo2 sono nonsimile a foo3

Dato che abbiamo un risultato query consistente IEnumerable o IOrderedEnumerable di Foo. Da query, dobbiamo trovare il primo Nfoo che sono nonsimile.

Questa attività sembra richiedere una memoria della raccolta di bars che è stata scelta in precedenza.

Con parzialeLINQ potremmo fare in questo modo:

private bool areBarsSimilar(ICollection<Bar> bars1, ICollection<Bar> bars2) { 
    return bars1.Count == bars2.Count && //have the same amount of bars 
     !bars1.Select(x => x.Id) 
     .Except(bars2.Select(y => y.Id)) 
     .Any(); //and when excepted does not return any element mean similar bar 
} 

public void somewhereWithQueryResult(){ 
    . 
    . 
    List<Foo> topNFoos = new List<Foo>(); //this serves as a memory for the previous query 
    int N = 50; //can be any number 
    foreach (var q in query) { //query is IOrderedEnumerable or IEnumerable 
     if (topNFoos.Count == 0 || !topNFoos.Any(foo => areBarsSimilar(foo.Bars, q.Bars))) 
      topNFoos.Add(q); 
     if (topNFoos.Count >= N) //We have had enough Foo 
      break; 
    } 
} 

Il topNFoosList servirà come memoria della query precedente e possiamo saltare la Foo q nella foreach ciclo che già hanno identica Bars con Any dello Foo nello topNFoos.

La mia domanda è: esiste un modo per farlo in LINQ (completamenteLINQ)?

var topNFoos = from q in query 
       //put something 
       select q; 

Se la "memoria" richiesto è da un particolare elemento di query q o una variabile al di fuori della query, allora potremmo usare let variabile per memorizzare nella cache è:

int index = 0; 
var topNFoos = from q in query 
       let qc = index++ + q.Id //depends on q or variable outside like index, then it is OK 
       select q; 

Ma se deve venire dal interrogazione precedente della query stessa quindi le cose cominciano a diventare più fastidiose.

C'è un modo per farlo?


Edit:

(Io attualmente sono creating a test case (link GitHub) per le risposte.Ancora capire come posso testare tutte le risposte equamente)

(La maggior parte delle risposte di seguito hanno lo scopo di risolvere il mio particolare domanda e sono di per sé buono (Rob, Spender di, e le risposte di David B che utilizzano IEqualityComparer sono particolarmente impressionante). Tuttavia, se c'è qualcuno che può dare risposta alla mia domanda più generale "non LINQ ha un modo per 'memorizzare' le sue precedenti risultati delle query durante l'interrogazione", vorrei anche essere felice)

(parte dalla significativa differenza di prestazioni per il caso particolare che ho presentato sopra quando si utilizza LINQ completo/parziale, una risposta che mira a rispondere alla mia domanda generale sulla memoria LINQ è di Ivan Stoev. Un altro con una buona combinazione è Rob. Per essere più chiaro, cerco una soluzione generale ed efficiente, se ce n'è una, usando LINQ)

+0

Il tuo caso "Senza LINQ" sembra utilizzare principalmente LINQ. – spender

+0

Questo è ciò 'Ora, se due o più Foo hanno la stessa collezione di barre (indipendentemente dall'ordine), sono considerate come Foo simili. Considerate valide per l'intera applicazione o solo in questo caso? – Rob

+0

@spender hai ragione, quello che intendo in realtà è * parziale * LINQ, dovrei aggiornarlo ... – Ian

risposta

3

Quindi, è ... possibile. Ma questo è lontano dal codice performante.

var res = query.Select(q => new { 
    original = q, 
    matches = query.Where(innerQ => areBarsSimilar(q.Bars, innerQ.Bars)) 
}).Select(g => new { original = g, joinKey = string.Join(",", g.matches.Select(m => m.Id)) }) 
.GroupBy (g => g.joinKey) 
.Select(g => g.First().original.original) 
.Take(N); 

Questo presuppone che il Id s sono uniche per ogni Foo (si può anche usare la loro GetHashCode(), suppongo).

Una soluzione molto migliore è quello di mantenere o quello che hai fatto, o implementare un operatore di confronto personalizzato, come segue:


Nota: Come sottolineato nei commenti dal @spender, il sotto Equals e GetHashCode non funzionerà per le raccolte con duplicati.Fare riferimento alla loro risposta per una migliore attuazione - tuttavia, il codice d'uso rimarrebbe la stessa


class MyComparer : IEqualityComparer<Foo> 
{ 
    public bool Equals(Foo left, Foo right) 
    { 
     return left.Bars.Count() == right.Bars.Count() && //have the same amount of bars 
      left.Bars.Select(x => x.Id) 
      .Except(right.Bars.Select(y => y.Id)) 
      .ToList().Count == 0; //and when excepted returns 0, mean similar bar 
    } 

    public int GetHashCode(Foo foo) 
    { 
     unchecked { 
      int hc = 0; 
      if (foo.Bars != null) 
       foreach (var p in foo.Bars) 
       hc ^= p.GetHashCode(); 
      return hc; 
     } 
    } 
} 

E poi la query diventa semplicemente:

var res = query 
    .GroupBy (q => q, new MyComparer()) 
    .Select(g => g.First()) 
    .Take(N); 
+0

Grazie per la risposta, per favore dammi un po 'di tempo per guardare attraverso questo e l'altra risposta, hai ottenuto il mio uptote per lo sforzo significativo però ... – Ian

+0

Hah! Ho seguito lo stesso approccio e stavo solo cercando la sintassi 'GroupBy'. Fai attenzione al caso in cui ci sono numeri ineguagliabili di duplicati nella raccolta 'Bars', perché l'implementazione' GetHashCode' non è coerente con le operazioni basate sull'implementazione di 'Equals'. Ho scelto di fare un 'SequenceEqual' invece per assicurarmi che le implementazioni hashcode ed equals siano in completo accordo. – spender

+1

@spender Sì, hai ragione, ero un po 'pigro e ho appena usato l'implementazione originale di "Equals" (principalmente un esempio di "IEqualityComparer"). Sicuramente darei risposta alla tua risposta, un'implementazione molto più pulita di 'Equals' ed è coerente – Rob

1

Idea. Potreste essere in grado di incidere qualcosa elaborando la propria interfaccia fluente di mutators su una cache che ti sentiresti di catturare in "Sia x = ..." clausole, lungo le linee di,

from q in query 
let qc = ... // your cache mechanism here 
select ... 

ma ho il sospetto Dovremo stare attenti a limitare gli aggiornamenti alla cache solo a quelli "let ...", poiché dubito che l'implementazione degli operatori standard di Linq e dei metodi di estensione sarà felice se si consente che tali effetti collaterali si verifichino nella loro schiena attraverso i predicati applicati nelle clausole "where", "join", "group by", ecc.

'HTH,

+0

ah sì, mi chiedo cosa potrebbe essere in 'qc = ...' in realtà. :) se è una variabile risultante da un particolare oggetto di query 'q' o una variabile al di fuori della query, allora dovrebbe essere OK. Ma se deve venire * dall'interrogazione precedente della query stessa * allora le cose cominciano a diventare più fastidiose. E questo è il punto della domanda in realtà. :) – Ian

6

io non ho intenzione di rispondere direttamente alla tua domanda, ma piuttosto, proporre un metodo che sarà abbastanza efficace in modo ottimale per filtrare i primi N elementi non simili.

Innanzitutto, è consigliabile scrivere un IEqualityComparer<Foo> che utilizza la raccolta Bars per misurare l'uguaglianza. Qui, sto assumendo che le liste possono contenere le voci duplicate, in modo da avere un bel definizione rigorosa di somiglianza:

public class FooSimilarityComparer:IEqualityComparer<Foo> 
{ 
    public bool Equals(Foo a, Foo b) 
    { 
     //called infrequently 
     return a.Bars.OrderBy(bar => bar.Id).SequenceEqual(b.Bars.OrderBy(bar => bar.Id)); 
    } 
    public int GetHashCode(Foo foo) 
    { 
     //called frequently 
     unchecked 
     { 
      return foo.Bars.Sum(b => b.GetHashCode()); 
     } 
    } 
} 

Si può davvero efficiente ottenere i migliori N poste non simili utilizzando un HashSet con l'IEqualityComparer sopra :

IEnumerable<Foo> someFoos; //= some list of Foo 
var hs = new HashSet<Foo>(new FooSimilarityComparer()); 
foreach(var f in someFoos) 
{ 
    hs.Add(f); //hashsets don't add duplicates, as measured by the FooSimilarityComparer 
    if(hs.Count >= 50) 
    { 
     break; 
    } 
} 

@ Rob s approccio di cui sopra è molto simile, e mostra come è possibile utilizzare l'operatore di confronto direttamente in LINQ, ma prestare attenzione ai commenti che ho fatto per la sua risposta.

+1

Grazie per la risposta, per favore dammi un po 'di tempo per guardare attraverso questa e l'altra risposta, hai ottenuto il mio uptote per lo sforzo significativo però ... – Ian

+0

La somma degli hash è un buon hash? Uno XOR bitwise piegato/aggregato produrrà risultati migliori? – Mephy

+1

@Mephy aggregando xor nel caso di duplicati sarebbe disastroso per un hash affidabile – spender

1

Credo che da "full LINQ" si media degli operatori LINQ standard/metodi di estensione Enumerable.

Non penso che questo possa essere fatto con la sintassi della query LINQ. Dalla metodi standard l'unico che supporta mutevole stato di elaborazione è Enumerable.Aggregate, ma ti dà niente di più di un sapore LINQ sulla pianura foreach:

var result = query.Aggregate(new List<Foo>(), (list, next) => 
{ 
    if (list.Count < 50 && !list.Any(item => areBarsSimilar(item.Bars, next.Bars))) 
     list.Add(next); 
    return list; 
}); 

Dal momento sembra che siamo autorizzati a usare metodi ausiliari (come areBarsSimilar) , il meglio che possiamo fare è quello di rendere almeno sembrare più LINQ-ish definendo e utilizzando un metodo di estensione personalizzata

var result = query.Aggregate(new List<Foo>(), (list, next) => list.Count < 50 && 
    !list.Any(item => areBarsSimilar(item.Bars, next.Bars)) ? list.Concat(next) : list); 

in cui il metodo personalizzato è

public static class Utils 
{ 
    public static List<T> Concat<T>(this List<T> list, T item) { list.Add(item); return list; } 
} 

Ma si noti che rispetto alla vaniglia foreach, Aggregate ha un ulteriore svantaggio di non essere in grado di uscire prima, quindi consumerà l'intera sequenza di input (che oltre alle prestazioni significa anche che non funziona con sequenze infinite).

Conclusione: Mentre questo dovrebbe rispondere alla tua domanda iniziale, vale a dire che è tecnicamente possibile fare quello che stai chiedendo, LINQ (come lo SQL standard) non è adatto per questo tipo di trattamento.

+0

Questo mi dà ancora una risposta da valutare! per favore dammi un po 'di tempo per guardare attraverso questa e le altre risposte, hai il mio' upvote 'per lo sforzo significativo però. :) (e, è * tu * di nuovo!) – Ian

+0

@Ian Sì, sono di nuovo io :) Ma seriamente, penso che l'argomento della domanda sia cambiato in modo significativo. La maggior parte delle risposte sta cercando di risolvere il problema concreto in un modo molto efficace, e ho visto che stai preparando un test delle prestazioni. Non è giusto perché sta confrontando le mele con le arance. Ci sono soluzioni piuttosto buone, ma per una domanda diversa, molto probabilmente con i tag 'algorithm' e' performance'. –

+0

È bello vederti - vivo * e * attivo. :) Hai ragione nel dire che la mia domanda iniziale era trovare la soluzione in generale, piuttosto che la soluzione in particolare. Metto semplicemente la soluzione particolare perché può dare un'immagine alla domanda generale. Tuttavia, è anche vero che il motivo per cui sto cercando di trovare una soluzione generale è perché penso che potrebbe essere più * efficiente *, che è anche parte della mia preoccupazione originale. Ecco perché penso che la tua soluzione risponda ancora alla parte essenziale della mia domanda. Ma dall'altra parte, potrebbe essere richiesto un test. Questo è il motivo per cui creo un test. – Ian

2
IEnumerable<Foo> dissimilarFoos = 
    from foo in query 
    let key = string.Join('|', 
    from bar in foo.Bars 
    order by bar.Id 
    select bar.Id.ToString()) 
    group foo by key into g 
    select g.First(); 

IEnumerable<Foo> firstDissimilarFoos = 
    dissimilarFoos.Take(50); 

A volte, non è possibile, come il comportamento dei GroupBy nelle query di cui sopra. Nel momento in cui la query viene enumerata, groupby enumera l'intera fonte. Se si desidera solo l'enumerazione parziale, allora si dovrebbe passare ad distinto e un operatore di confronto:

class FooComparer : IEqualityComparer<Foo> 
{ 
    private string keyGen(Foo foo) 
    { 
    return string.Join('|', 
     from bar in foo.Bars 
     order by bar.Id 
     select bar.Id.ToString()); 
    } 
    public bool Equals(Foo left, Foo right) 
    { 
    if (left == null || right == null) return false; 
    return keyGen(left) == keyGen(right); 
    } 
    public bool GetHashCode(Foo foo) 
    { 
    return keyGen(foo).GetHashCode(); 
    } 
} 

quindi scrivere:

IEnumerable<Foo> dissimilarFoos = query.Distinct(new FooComparer()); 
IEnumerable<Foo> firstDissimilarFoos = dissimilarFoos.Take(50); 
+0

Grazie per la risposta, attualmente sto scrivendo un test case per valutare tutte le risposte. Per favore dammi un po 'di tempo per guardare attraverso questo e le altre risposte, hai ottenuto il mio uptote per lo sforzo significativo però ... – Ian