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

20

Questa è una trappola nota per le persone che si stanno bagnando i piedi usando 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à "False", perché per ogni nome fornito per creare la collezione originale, la funzione di selezione continua a essere rivalutata e l'oggetto Record risultante viene creato di nuovo. Per risolvere il problema, è possibile aggiungere una semplice chiamata a ToList alla fine di GenerateRecords .

Quale vantaggio sperava di ottenere Microsoft implementandolo in questo modo?

Perché l'implementazione non memorizza semplicemente i risultati in un array interno? Una parte specifica di ciò che sta accadendo potrebbe essere un'esecuzione posticipata, ma potrebbe comunque essere implementata senza questo comportamento.

Una volta valutato un membro di una raccolta restituito da LINQ, quale vantaggio viene fornito dal fatto di non mantenere un riferimento / una copia interna, ma invece di ricalcolare lo stesso risultato, come comportamento predefinito?

In situazioni in cui c'è una particolare necessità nella logica per lo stesso membro di una raccolta ricalcolata ripetutamente, sembra che possa essere specificato attraverso un parametro opzionale e che il comportamento predefinito potrebbe fare diversamente. Inoltre, il vantaggio di velocità ottenuto dall'esecuzione posticipata è in definitiva ridotto rispetto al 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 in definitiva nel programma di chiunque.

Che vantaggio c'è in questo, e perché Microsoft ha preso questa decisione apparentemente molto deliberata?

    
posta Panzercrisis 23.03.2018 - 15:11
fonte

6 risposte

52

What advantage was gained by implementing LINQ in a way that does not cache the results?

La memorizzazione nella cache dei risultati semplicemente non funzionerebbe per tutti. Finché hai una piccola quantità di dati, ottimo. Buon per te. Ma cosa succede se i tuoi dati sono più grandi della tua RAM?

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

È la differenza tra File.ReadAllLines e < a href="https://msdn.microsoft.com/en-us/library/system.io.file.readlines(v=vs.110).aspx"> File.ReadLines . Uno leggerà l'intero file nella RAM e l'altro lo fornirà linea 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 vuoi memorizzare, materializzando la tua sequenza chiamando .ToList() o .ToArray() su di essa. Ma quelli di noi che non vogliono nasconderlo, abbiamo la possibilità di non di farlo.

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

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

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

    
risposta data 23.03.2018 - 16:42
fonte
24

What advantage did Microsoft hope to gain by implementing it this way?

Correttezza? Voglio dire, il nucleo numerabile può cambiare tra una chiamata e l'altra. La memorizzazione nella cache produrrebbe risultati errati e aprirà l'intero "quando / come posso invalidare quella cache?" Can di worm.

E se pensate che LINQ sia stato originariamente progettato come mezzo per eseguire LINQ su fonti di dati (come framework di entità, o SQL direttamente), l'enumerabile era sta per cambiare poiché è ciò che i database fare .

Inoltre, vi sono i timori relativi al principio di responsabilità singola. È molto più semplice creare un codice di query che funzioni e costruisca il caching su di esso piuttosto che creare codice che richieda e memorizzi le cache, ma poi rimuove la cache.

    
risposta data 23.03.2018 - 15:20
fonte
4

Perché LINQ è, e doveva essere fin dall'inizio, un'implementazione generica del modello Monad popolare nei linguaggi di programmazione funzionale , e una Monade non è costretta a fornire sempre gli stessi valori data la stessa sequenza di chiamate (in effetti, il suo uso nella programmazione funzionale è popolare proprio a causa di questa proprietà, che consente di sfuggire al comportamento deterministico delle funzioni pure).

    
risposta data 23.03.2018 - 18:23
fonte
4

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

Prendi questo ad 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 risulta
  • Seleziona risultato
  • GroupBy result

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

Se è necessario salvare uno di questi risultati, la soluzione è semplice: suddividere le chiamate e chiamare .ToList() su di esse e salvarle in una variabile.

Come nota a margine, in JavaScript, i metodi Array restituiscono immediatamente i risultati, il che può comportare un maggiore consumo di memoria se non si presta attenzione.

    
risposta data 23.03.2018 - 19:39
fonte
3

Fondamentalmente, questo codice - mettendo un Guid.NewGuid () all'interno di un'istruzione Select - è altamente sospetto. Questo è sicuramente un odore di codice di qualche tipo!

In teoria, non ci aspetteremmo necessariamente un'istruzione Select per creare nuovi dati, ma per recuperare i dati esistenti. Sebbene sia ragionevole per Seleziona unire i dati da più fonti per produrre contenuti uniti di forma diversa o anche calcolare colonne aggiuntive, potremmo comunque aspettarci che sia funzionale e amp; puro. Mettere il NewGuid () all'interno lo rende non funzionale & non pura.

La creazione dei dati potrebbe essere presa in considerazione separatamente dalla selezione e inserita in un'operazione di creazione di qualche tipo, in modo che la selezione possa rimanere pura e riutilizzabile, altrimenti la selezione dovrebbe essere eseguita una sola volta e avvolta / protetto - questo è il suggerimento .ToList () .

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

    
risposta data 23.03.2018 - 18:16
fonte
0

L'esecuzione differita consente a coloro che scrivono il codice LINQ (per essere precisi, utilizzando IEnumerable<T> ) di scegliere esplicitamente se il risultato viene immediatamente calcolato e memorizzato, oppure no. 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 richiede immediatamente i risultati, quindi dovrebbe essere stato il comportamento predefinito di LINQ. Ma ci sono numerose altre API (ad esempio List<T>.ConvertAll ) che offrono questo comportamento e hanno fatto da quando è stato creato il Framework, mentre fino a quando non è stato introdotto LINQ, non c'era modo di avere un'esecuzione posticipata. Che, come hanno dimostrato altre risposte, è un prerequisito per abilitare certi tipi di calcoli che altrimenti sarebbero impossibili (esaurendo tutta la memoria disponibile) quando si utilizza l'esecuzione immediata.

    
risposta data 24.03.2018 - 17:04
fonte

Leggi altre domande sui tag