Welchen Vorteil hat die Implementierung von LINQ erzielt, bei der die Ergebnisse nicht zwischengespeichert werden?

20

Dies ist eine bekannte Gefahr für Menschen, die sich mit LINQ die Füße nass machen:

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

Dies gibt "False" aus, da für jeden Namen, der zum Erstellen der ursprünglichen Sammlung angegeben wird, die Auswahlfunktion immer wieder neu bewertet wird und das resultierende RecordObjekt neu erstellt wird. Um dies zu beheben, ToListkönnte am Ende von ein einfacher Aufruf an hinzugefügt werden GenerateRecords.

Welchen Vorteil erhoffte sich Microsoft von dieser Implementierung?

Warum speichert die Implementierung die Ergebnisse nicht einfach in einem internen Array? Ein spezifischer Teil dessen, was passiert, ist möglicherweise eine verzögerte Ausführung, die jedoch auch ohne dieses Verhalten implementiert werden kann.

Welchen Vorteil hat es, wenn ein bestimmtes Mitglied einer von LINQ zurückgegebenen Sammlung nicht intern referenziert oder kopiert wird, sondern dasselbe Ergebnis als Standardverhalten neu berechnet wird?

In Situationen, in denen in der Logik ein bestimmtes Erfordernis für dasselbe Mitglied einer Sammlung besteht, das immer wieder neu berechnet wird, könnte dies anscheinend über einen optionalen Parameter angegeben werden, und das Standardverhalten könnte sich auch anders verhalten. Darüber hinaus verringert sich der Geschwindigkeitsvorteil, der durch die verzögerte Ausführung erzielt wird, letztendlich um die Zeit, die für die kontinuierliche Neuberechnung derselben Ergebnisse erforderlich ist. Letztendlich ist dies ein verwirrender Block für diejenigen, die neu in LINQ sind, und es könnte letztendlich zu subtilen Fehlern in jedem Programm kommen.

Welchen Vorteil hat dies, und warum hat Microsoft diese scheinbar sehr absichtliche Entscheidung getroffen?

Panzerkrise
quelle
1
Rufen Sie einfach ToList () in Ihrer GenerateRecords () -Methode auf. return listOfNames.Select(x => new Record(Guid.NewGuid(), x)).ToList(); Das gibt Ihnen Ihre "zwischengespeicherte Kopie". Problem gelöst.
Robert Harvey
1
Ich weiß, aber ich habe mich gefragt, warum sie das überhaupt nötig gemacht hätten.
Panzercrisis
11
Da eine verzögerte Evaluierung erhebliche Vorteile hat, darunter nicht zuletzt "Oh, übrigens, dieser Datensatz hat sich seit Ihrer letzten Anfrage geändert. Hier ist die neue Version". Genau das zeigt Ihr Codebeispiel.
Robert Harvey
Ich könnte schwören, dass ich hier in den letzten 6 Monaten eine fast identisch formulierte Frage gelesen habe, aber ich finde sie jetzt nicht. Die nächstgelegene, die ich finden kann, war ab 2016 auf stackoverflow: stackoverflow.com/q/37437893/391656
Mr.Mindor
29
Wir haben einen Namen für einen Cache ohne Ablaufrichtlinie: "Memory Leak". Wir haben einen Namen für einen Cache ohne Ungültigkeitsrichtlinie: "Bugfarm". Wenn Sie keine immer korrekte Ablauf- und Ungültigkeitsrichtlinie vorschlagen, die für jede mögliche LINQ-Abfrage geeignet ist, beantwortet sich Ihre Frage von selbst.
Eric Lippert

Antworten:

51

Welchen Vorteil hat die Implementierung von LINQ erzielt, bei der die Ergebnisse nicht zwischengespeichert werden?

Das Cachen der Ergebnisse würde einfach nicht für alle funktionieren. Solange Sie winzige Datenmengen haben, ist das großartig. Schön für dich. Aber was ist, wenn Ihre Daten größer als Ihr RAM sind?

Es hat nichts mit LINQ zu tun, sondern mit dem IEnumerable<T> Oberfläche im Allgemeinen.

Dies ist der Unterschied zwischen File.ReadAllLines und File.ReadLines . Man wird die gesamte Datei in den RAM gelesen, und der andere wird es dir geben Zeile für Zeile, so dass Sie mit großen Dateien arbeiten können (solange sie haben Zeilenumbrüche).

Sie können einfach alles zwischenspeichern, was Sie zwischenspeichern möchten, indem Sie Ihren Sequenzaufruf entweder materialisieren .ToList()oder .ToArray()darauf ablegen. Aber diejenigen von uns, die es nicht zwischenspeichern wollen, haben die Chance, dies nicht zu tun.

Und zu einem verwandten Thema: Wie wird Folgendes zwischengespeichert?

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

Du kannst nicht. Deshalb IEnumerable<T>existiert es so wie es ist.

nvoigt
quelle
2
Ihr letztes Beispiel wäre überzeugender, wenn es eine tatsächliche unendliche Reihe (wie Fibonnaci) wäre und nicht nur eine endlose Folge von Nullen, was nicht besonders interessant ist.
Robert Harvey
23
@RobertHarvey Das stimmt, ich dachte nur, es ist einfacher zu erkennen, dass es sich um einen endlosen Strom von Nullen handelt, wenn überhaupt keine Logik zu verstehen ist.
Nvoigt
2
int i=1; while(true) { i++; yield fib(i); }
Robert Harvey
2
Das Beispiel, an das ich gedacht habe, war Enumerable.Range(1,int.MaxValue)- es ist sehr einfach, eine Untergrenze für den zu verwendenden Speicher zu ermitteln.
Chris
4
Das andere, was ich gesehen habe, while (true) return ...war while (true) return _random.Next();, einen unendlichen Strom von Zufallszahlen zu erzeugen.
Chris
24

Welchen Vorteil erhoffte sich Microsoft von dieser Implementierung?

Richtigkeit? Ich meine, die Kernaufzählung kann sich zwischen den Aufrufen ändern. Das Zwischenspeichern würde zu falschen Ergebnissen führen und das gesamte Fenster "Wann / Wie kann ich diesen Cache ungültig machen?" Von Würmern öffnen.

Und wenn Sie LINQ betrachten ursprünglich als Mittel entwickelt wurde LINQ auf Datenquellen (wie Entity Framework oder SQL direkt) zu tun, die zählbare wurde denn das ist , zu ändern gehen , welche Datenbanken tun .

Darüber hinaus gibt es Bedenken hinsichtlich des Grundsatzes der einheitlichen Verantwortung. Es ist weitaus einfacher, einen funktionierenden Abfragecode zu erstellen und darauf einen Zwischenspeicher zu erstellen, als einen Code zu erstellen, der Abfragen und Zwischenspeicherungen durchführt, die Zwischenspeicherung dann jedoch entfernt.

Telastyn
quelle
3
Es kann erwähnenswert sein, ICollectionund wahrscheinlich verhält sich die Art und Weise OP besteht erwartet IEnumerablezu verhalten
Caleth
Wenn Sie IEnumerable <T> zum Lesen eines offenen Datenbankcursors verwenden, sollten sich Ihre Ergebnisse nicht ändern, wenn Sie eine Datenbank mit ACID-Transaktionen verwenden.
Doug
4

Da LINQ eine generische Implementierung des in funktionalen Programmiersprachen beliebten Monad-Musters ist und von Anfang an sein sollte, ist eine Monad nicht darauf beschränkt, bei gleicher Reihenfolge der Aufrufe immer dieselben Werte zu liefern (in der Tat ihre Verwendung) gerade wegen dieser eigenschaft, die es erlaubt, dem deterministischen verhalten reiner funktionen zu entgehen, ist die funktionale programmierung beliebt.

Jules
quelle
4

Ein weiterer Grund, der nicht erwähnt wurde, ist die Möglichkeit, verschiedene Filter und Transformationen zu verketten, ohne müllmittlere Ergebnisse zu erzielen.

Nimm das zum Beispiel:

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

Wenn die LINQ-Methoden die Ergebnisse sofort berechnen würden, hätten wir 3 Sammlungen:

  • Wo das Ergebnis ist
  • Ergebnis auswählen
  • GroupBy Ergebnis

Davon kümmern wir uns nur um den letzten. Es macht keinen Sinn, die mittleren Ergebnisse zu speichern, da wir keinen Zugriff darauf haben und nur Informationen zu den Autos benötigen, die bereits nach Jahren gefiltert und gruppiert wurden.

Wenn eines dieser Ergebnisse gespeichert werden musste, ist die Lösung einfach: Teilen Sie die Aufrufe auf, rufen Sie .ToList()sie auf und speichern Sie sie in einer Variablen.


Nur als Randnotiz: In JavaScript geben die Array-Methoden die Ergebnisse sofort zurück, was zu einem höheren Speicherverbrauch führen kann, wenn man nicht aufpasst.

Arturo Torres Sánchez
quelle
3

Grundsätzlich ist dieser Code, der Guid.NewGuid ()eine SelectAussage enthält, sehr verdächtig. Dies ist sicherlich eine Art Code-Geruch!

Theoretisch würden wir nicht unbedingt erwarten, dass eine SelectAnweisung neue Daten erstellt, sondern vorhandene Daten abruft. Obwohl es für Select sinnvoll ist, Daten aus mehreren Quellen zu verknüpfen, um zusammengefügte Inhalte unterschiedlicher Form zu erstellen oder sogar zusätzliche Spalten zu berechnen, können wir dennoch davon ausgehen, dass diese funktional und rein sind. Wenn Sie das NewGuid ()Innere einsetzen, ist es nicht mehr funktionsfähig und nicht mehr rein.

Die Erstellung der Daten könnte getrennt von der Auswahl in eine Art Erstellungsoperation versetzt werden, damit die Auswahl rein und wiederverwendbar bleibt, oder die Auswahl sollte nur einmal durchgeführt und verpackt / geschützt werden ist der .ToList () Vorschlag.

Um jedoch klar zu sein, scheint mir das Problem eher die Vermischung der Schöpfung innerhalb der Auswahl zu sein, als das Fehlen von Caching. Das Setzen des NewGuid()Inneren der Auswahl scheint mir eine unangemessene Mischung von Programmiermodellen zu sein.

Erik Eidt
quelle
0

Die verzögerte Ausführung ermöglicht denjenigen, die LINQ - Code schreiben (um genau zu sein mit IEnumerable<T> ), explizit auswählen, ob das Ergebnis sofort berechnet und im Speicher abgelegt wird oder nicht. Mit anderen Worten, es ermöglicht Programmierern, den für ihre Anwendung am besten geeigneten Kompromiss zwischen Rechenzeit und Speicherplatz zu wählen.

Es könnte argumentiert werden, dass die Mehrheit der Anwendungen die Ergebnisse sofort haben möchte, so dass dies das Standardverhalten von LINQ gewesen sein sollte. Es gibt jedoch zahlreiche andere APIs (z. B. List<T>.ConvertAll), die dieses Verhalten bieten und dies seit der Erstellung des Frameworks getan haben, während es bis zur Einführung von LINQ keine Möglichkeit gab, die Ausführung zu verschieben. Wie andere Antworten gezeigt haben, ist dies eine Grundvoraussetzung, um bestimmte Arten von Berechnungen zu ermöglichen, die andernfalls bei sofortiger Ausführung unmöglich (durch Ausschöpfung des gesamten verfügbaren Speichers) wären.

Ian Kemp
quelle