Quale vantaggio è stato ottenuto implementando LINQ in modo da non memorizzare nella cache i risultati?


20

Questa è una trappola nota per le persone che si bagnano i piedi con LINQ:

public class Program
{
    public static void Main()
    {
        IEnumerable<Record> originalCollection = GenerateRecords(new[] {"Jesse"});
        var newCollection = new List<Record>(originalCollection);

        Console.WriteLine(ContainTheSameSingleObject(originalCollection, newCollection));
    }

    private static IEnumerable<Record> GenerateRecords(string[] listOfNames)
    {
        return listOfNames.Select(x => new Record(Guid.NewGuid(), x));
    }

    private static bool ContainTheSameSingleObject(IEnumerable<Record>
            originalCollection, List<Record> newCollection)
    {
        return originalCollection.Count() == 1 && newCollection.Count() == 1 &&
                originalCollection.Single().Id == newCollection.Single().Id;
    }

    private class Record
    {
        public Guid Id { get; }
        public string SomeValue { get; }

        public Record(Guid id, string someValue)
        {
            Id = id;
            SomeValue = someValue;
        }
    }
}

Questo stamperà "Falso", perché per ogni nome fornito per creare la raccolta originale, la funzione di selezione continua a essere rivalutata e l' Recordoggetto risultante viene creato di nuovo. Per risolvere questo problema, è ToListpossibile aggiungere una semplice chiamata a alla fine di GenerateRecords.

Quale vantaggio sperava Microsoft di ottenere implementandolo in questo modo?

Perché l'implementazione non dovrebbe semplicemente memorizzare nella cache i risultati un array interno? Una parte specifica di ciò che sta accadendo potrebbe essere l'esecuzione differita, ma ciò potrebbe essere comunque implementato senza questo comportamento.

Una volta valutato un determinato membro di una raccolta restituita da LINQ, quale vantaggio viene offerto non mantenendo un riferimento / copia interno, ma invece ricalcolando lo stesso risultato, come comportamento predefinito?

In situazioni in cui vi è una particolare necessità nella logica per lo stesso membro di una raccolta ricalcolata più e più volte, sembra che potrebbe essere specificato tramite un parametro opzionale e che il comportamento predefinito potrebbe fare diversamente. Inoltre, il vantaggio di velocità ottenuto dall'esecuzione differita viene infine ridotto dal tempo necessario per ricalcolare continuamente gli stessi risultati. Infine, questo è un blocco confuso per coloro che sono nuovi a LINQ e potrebbe portare a bug sottili nel programma di chiunque.

Qual è il vantaggio di questo, e perché Microsoft ha preso questa decisione apparentemente molto deliberata?


1
Basta chiamare ToList () nel metodo GenerateRecords (). return listOfNames.Select(x => new Record(Guid.NewGuid(), x)).ToList(); Questo ti dà la tua "copia cache". Problema risolto.
Robert Harvey,

1
Lo so, ma mi chiedevo perché avrebbero reso questo necessario in primo luogo.
Panzercrisis,

11
Perché la valutazione pigra ha vantaggi significativi, non ultimo il fatto che "oh, a proposito, questo record è cambiato dall'ultima volta che lo hai richiesto; ecco la nuova versione", che è esattamente ciò che illustra il tuo esempio di codice.
Robert Harvey,

Potrei giurare di aver letto una domanda quasi identica qui negli ultimi 6 mesi, ma non la trovo ora. Il più vicino che posso trovare è stato dal 2016 su StackOverflow: stackoverflow.com/q/37437893/391656
Mr.Mindor

29
Abbiamo un nome per una cache senza un criterio di scadenza: "perdita di memoria". Abbiamo un nome per una cache senza una politica di invalidazione: "bug farm". Se non hai intenzione di proporre una politica di scadenza e invalidazione sempre corretta che funzioni per ogni possibile query LINQ, la tua domanda in genere risponde.
Eric Lippert,

Risposte:


51

Quale vantaggio è stato ottenuto implementando LINQ in modo da non memorizzare nella cache i risultati?

La memorizzazione nella cache dei risultati non funzionerebbe semplicemente per tutti. Finché hai minuscole quantità di dati, fantastico. Buon per te. E se i tuoi dati fossero più grandi della tua RAM?

Non ha nulla a che fare con LINQ, ma con l' IEnumerable<T>interfaccia in generale.

È la differenza tra File.ReadAllLines e File.ReadLines . Uno leggerà l'intero file nella RAM e l'altro te lo fornirà riga per riga, in modo da poter lavorare con file di grandi dimensioni (purché abbiano interruzioni di riga).

Puoi facilmente memorizzare nella cache tutto ciò che desideri memorizzare materializzando la tua sequenza chiamata .ToList()o .ToArray()su di essa. Ma quelli di noi che non vogliono memorizzarlo nella cache, abbiamo la possibilità di non farlo.

E su una nota correlata: come si memorizza nella cache quanto segue?

IEnumerable<int> AllTheZeroes()
{
    while(true) yield return 0;
}

Non puoi. Ecco perché IEnumerable<T>esiste così come è.


2
Il tuo ultimo esempio sarebbe più convincente se si trattasse di una serie infinita (come Fibonnaci), e non semplicemente una serie infinita di zero, il che non è particolarmente interessante.
Robert Harvey,

23
@RobertHarvey È vero, ho solo pensato che fosse più facile individuare che è un flusso infinito di zero quando non c'è alcuna logica da capire.
nvoigt

2
int i=1; while(true) { i++; yield fib(i); }
Robert Harvey,

2
L'esempio a cui stavo pensando era Enumerable.Range(1,int.MaxValue): è molto facile elaborare un limite inferiore per la quantità di memoria che verrà utilizzata.
Chris,

4
L'altra cosa che ho visto sulla falsariga di while (true) return ...era while (true) return _random.Next();generare un flusso infinito di numeri casuali.
Chris,

24

Quale vantaggio sperava Microsoft di ottenere implementandolo in questo modo?

Correttezza? Voglio dire, l'enumerabile core può cambiare tra le chiamate. La memorizzazione nella cache produrrebbe risultati errati e aprirà l'intero "quando / come posso invalidare quella cache?" Can dei worm.

E se si considera LINQ è stato originariamente concepito come un mezzo per fare LINQ to fonti di dati (come Entity Framework, SQL o direttamente), l'enumerabile è stato andando a cambiare in quanto questo è ciò che i database fanno .

Inoltre, vi sono preoccupazioni relative al principio di responsabilità unica. È molto più semplice creare un codice di query che funzioni e creare cache su di esso piuttosto che creare codice che interroga e memorizza nella cache, ma quindi rimuove la cache.


3
Potrebbe valere la pena ricordare che ICollectionesiste, e probabilmente si comporta nel modo in cui OP si aspetta IEnumerabledi comportarsi
Caleth,

Se si utilizza IEnumerable <T> per leggere un cursore di database aperto, i risultati non dovrebbero cambiare se si utilizza un database con transazioni ACID.
Doug

4

Poiché LINQ è, ed era inteso fin dall'inizio, un'implementazione generica del modello Monad popolare nei linguaggi di programmazione funzionale , e una Monad non è costretta a produrre sempre gli stessi valori data la stessa sequenza di chiamate (in effetti, il suo utilizzo nella programmazione funzionale è popolare proprio per questa proprietà, che consente di sfuggire al comportamento deterministico delle funzioni pure).


4

Un altro motivo che non è stato menzionato è la possibilità di concatenare diversi filtri e trasformazioni senza creare risultati intermedi.

Prendi questo per esempio:

cars.Where(c => c.Year > 2010)
.Select(c => new { c.Model, c.Year, c.Color })
.GroupBy(c => c.Year);

Se i metodi LINQ calcolassero immediatamente i risultati, avremmo 3 raccolte:

  • Dove risultato
  • Seleziona il risultato
  • Risultato GroupBy

Di cui ci preoccupiamo solo dell'ultimo. Non ha senso salvare i risultati intermedi perché non abbiamo accesso ad essi e vogliamo solo conoscere le auto già filtrate e raggruppate per anno.

Se fosse necessario salvare uno di questi risultati, la soluzione è semplice: separare le chiamate, chiamarle .ToList()e salvarle in una variabile.


Proprio come una nota a margine, in JavaScript, i metodi Array in realtà restituiscono immediatamente i risultati, il che può portare a un maggiore consumo di memoria se non si è attenti.


3

Fondamentalmente, questo codice - inserendo Guid.NewGuid ()una Selectdichiarazione - è altamente sospetto. Questo è sicuramente un odore di codice di qualche tipo!

In teoria, non ci aspetteremmo necessariamente che Selectun'istruzione crei nuovi dati ma recuperi i dati esistenti. Sebbene sia ragionevole per Select unire i dati da più fonti per produrre contenuti uniti di diversa forma o addirittura calcolare colonne aggiuntive, potremmo comunque aspettarci che sia funzionale e puro. Mettere l' NewGuid ()interno lo rende non funzionale e non puro.

La creazione dei dati potrebbe essere presa in giro a parte la selezione e messa in un'operazione di creazione di qualche tipo, in modo che la selezione possa rimanere pura e riutilizzabile, altrimenti la selezione dovrebbe essere fatta una sola volta e racchiusa / protetta - questo è il .ToList ()suggerimento.

Tuttavia, per essere chiari, il problema mi sembra il mescolamento della creazione all'interno della selezione piuttosto che la mancanza di memorizzazione nella cache. Mettere NewGuid()dentro la selezione mi sembra un mix inappropriato di modelli di programmazione.


0

L'esecuzione differita consente a chi scrive il codice LINQ (per essere precisi, utilizzando IEnumerable<T>) di scegliere esplicitamente se il risultato viene immediatamente calcolato e archiviato in memoria o meno. In altre parole, consente ai programmatori di scegliere il tempo di calcolo rispetto al compromesso dello spazio di archiviazione più appropriato per la loro applicazione.

Si potrebbe sostenere che la maggior parte delle applicazioni desidera immediatamente i risultati, quindi dovrebbe essere stato il comportamento predefinito di LINQ. Ma ci sono numerose altre API (ad es. List<T>.ConvertAll) Che offrono questo comportamento e lo hanno fatto da quando è stato creato il Framework, mentre fino all'introduzione di LINQ non c'era modo di ritardare l'esecuzione. Che, come hanno dimostrato altre risposte, è un prerequisito per abilitare determinati tipi di calcoli che altrimenti sarebbero impossibili (esaurendo tutta la memoria disponibile) quando si utilizza l'esecuzione immediata.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.