Jest to znana pułapka dla osób, które moczyły stopy za pomocą 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;
}
}
}
Spowoduje to wydrukowanie „Fałsz”, ponieważ dla każdej nazwy podanej w celu utworzenia oryginalnej kolekcji funkcja select jest ciągle ponownie oceniana, a wynikowy Record
obiekt jest tworzony od nowa. Aby to naprawić, ToList
na końcu można dodać proste wezwanie do GenerateRecords
.
Jaką korzyść Microsoft miał nadzieję uzyskać, wdrażając go w ten sposób?
Dlaczego implementacja po prostu nie buforuje wyników w wewnętrznej tablicy? Jedną konkretną częścią tego, co się dzieje, może być odroczenie wykonania, ale można to nadal wdrożyć bez tego zachowania.
Po dokonaniu oceny danego członka kolekcji zwróconej przez LINQ, jaką korzyść zapewnia nie zachowanie wewnętrznego odwołania / kopii, ale ponowne obliczenie tego samego wyniku, jako zachowanie domyślne?
W sytuacjach, w których istnieje szczególna potrzeba logiki dla tego samego elementu kolekcji, który jest wielokrotnie przeliczany, wydaje się, że można to określić za pomocą opcjonalnego parametru, a domyślne zachowanie może zrobić inaczej. Ponadto przewaga prędkości uzyskana dzięki odroczonemu wykonaniu jest ostatecznie zmniejszana o czas potrzebny do ciągłego przeliczania tych samych wyników. Wreszcie jest to mylący blok dla tych, którzy są nowi w LINQ, i może prowadzić do subtelnych błędów w ostatecznie czyimś programie.
Jaka jest z tego korzyść i dlaczego Microsoft podjął tę pozornie bardzo świadomą decyzję?
źródło
return listOfNames.Select(x => new Record(Guid.NewGuid(), x)).ToList();
To daje „kopię w pamięci podręcznej”. Problem rozwiązany.Odpowiedzi:
Zapisywanie wyników w pamięci podręcznej po prostu nie zadziałałoby dla wszystkich. Tak długo, jak masz małe ilości danych, świetnie. Dobrze dla ciebie. Ale co, jeśli twoje dane są większe niż pamięć RAM?
Nie ma to nic wspólnego z LINQ, ale
IEnumerable<T>
ogólnie z interfejsem.Jest to różnica między File.ReadAllLines i File.ReadLines . Jeden wczyta cały plik do pamięci RAM, a drugi da ci go wiersz po wierszu, dzięki czemu możesz pracować z dużymi plikami (o ile mają one podział wiersza).
Możesz łatwo buforować wszystko, co chcesz buforować, zmaterializując sekwencję wywołując jedną z nich
.ToList()
lub jedną.ToArray()
z nich. Ale ci z nas, którzy nie chcą tego buforować, mamy szansę tego nie zrobić.I na powiązaną notatkę: jak buforujesz następujące elementy?
Nie możesz. Właśnie dlatego
IEnumerable<T>
istnieje.źródło
int i=1; while(true) { i++; yield fib(i); }
Enumerable.Range(1,int.MaxValue)
- bardzo łatwo jest ustalić dolną granicę, ile pamięci będzie zużywać.while (true) return ...
byłowhile (true) return _random.Next();
wygenerowanie nieskończonego strumienia liczb losowych.Poprawność Mam na myśli, że wyliczenie rdzenia może zmieniać się między połączeniami. Buforowanie spowoduje niepoprawne wyniki i otworzy całą „kiedy / jak unieważnić tę pamięć podręczną?” Puszka robaków.
A jeśli weźmiesz pod uwagę, że LINQ został pierwotnie zaprojektowany jako sposób wykonywania LINQ na źródłach danych (takich jak framework encji lub SQL bezpośrednio), to wyliczanie miało się zmienić, ponieważ to właśnie robią bazy danych .
Ponadto istnieją obawy dotyczące zasady pojedynczej odpowiedzialności. Znacznie łatwiej jest utworzyć kod zapytania, który działa i zbudować buforowanie na nim, niż zbudować kod, który wysyła zapytania i buforuje, ale następnie usuwa buforowanie.
źródło
ICollection
istnieje i prawdopodobnie zachowuje się tak, jak spodziewaIEnumerable
się OPPonieważ LINQ jest i od początku miał być ogólną implementacją wzorca Monady popularnego w funkcjonalnych językach programowania , a Monada nie jest ograniczona do tego, aby zawsze uzyskiwać te same wartości, biorąc pod uwagę tę samą sekwencję wywołań (w rzeczywistości jej użycie w programowaniu funkcjonalnym jest popularny właśnie ze względu na tę właściwość, która pozwala na uniknięcie deterministycznego zachowania funkcji czystych).
źródło
Innym powodem, o którym nie wspomniano, jest możliwość łączenia różnych filtrów i transformacji bez tworzenia śmieciowych wyników pośrednich.
Weźmy na przykład:
Jeśli metody LINQ obliczyłyby wyniki natychmiast, mielibyśmy 3 kolekcje:
Z których zależy nam tylko na ostatnim. Zapisywanie środkowych wyników nie ma sensu, ponieważ nie mamy do nich dostępu, a chcemy tylko wiedzieć o samochodach, które zostały już odfiltrowane i pogrupowane według roku.
Jeśli zachodzi potrzeba zapisania któregokolwiek z tych wyników, rozwiązanie jest proste: rozdzielić połączenia
.ToList()
na części i wywołać je i zapisać w zmiennej.Na marginesie, w JavaScript, metody Array faktycznie zwracają wyniki natychmiast, co może prowadzić do większego zużycia pamięci, jeśli nie jest się ostrożnym.
źródło
Zasadniczo ten kod - umieszczanie w
Guid.NewGuid ()
środkuSelect
instrukcji - jest wysoce podejrzany. To z pewnością jakiś zapach kodu!Teoretycznie niekoniecznie spodziewalibyśmy się, że
Select
instrukcja utworzy nowe dane, ale odzyska istniejące dane. Chociaż Select może łączyć dane z wielu źródeł w celu uzyskania połączonej zawartości o innym kształcie, a nawet obliczać dodatkowe kolumny, nadal możemy oczekiwać, że będzie funkcjonalny i czysty. UmieszczenieNewGuid ()
wnętrza sprawia, że jest on niefunkcjonalny i nieczysty.Tworzenie danych może być dokuczane poza selekcją i wprowadzane do pewnego rodzaju operacji tworzenia, dzięki czemu selekcja może pozostać czysta i nadawać się do ponownego użycia, w przeciwnym razie selekcja powinna zostać wykonana tylko raz i opakowana / zabezpieczona - to jest
.ToList ()
sugestią.Jednak, dla jasności, wydaje mi się, że problemem jest mieszanie twórczości wewnątrz selekcji, a nie brak buforowania. Umieszczenie w
NewGuid()
środku wybranego wydaje mi się niewłaściwym mieszaniem modeli programowania.źródło
Odroczone wykonywanie pozwala osobom zapisującym kod LINQ (a ściślej mówiąc, używającym
IEnumerable<T>
) jawnie wybrać, czy wynik zostanie natychmiast obliczony i zapisany w pamięci, czy nie. Innymi słowy, pozwala programistom wybrać czas obliczania w zależności od kompromisu przestrzeni dyskowej, który jest najbardziej odpowiedni dla ich zastosowania.Można argumentować, że większość aplikacji chce wyników natychmiast, więc powinno to być domyślne zachowanie LINQ. Ale istnieje wiele innych interfejsów API (np.
List<T>.ConvertAll
), Które oferują takie zachowanie i zrobiły to od czasu stworzenia Frameworka, podczas gdy do wprowadzenia LINQ nie było możliwości odroczenia wykonania. Co, jak pokazały inne odpowiedzi, jest warunkiem wstępnym włączenia niektórych rodzajów obliczeń, które w innym przypadku byłyby niemożliwe (poprzez wyczerpanie całej dostępnej pamięci) przy natychmiastowym wykonaniu.źródło