fbpx
devstyle.pl - Blog dla każdego programisty
devstyle.pl - Blog dla każdego programisty
6 minut

let – revisited


13.07.2008

W poprzednim poście zapoznaliśmy się ze słówkiem “let”. Dzisiaj do niego powrócimy i zobaczymy dlaczego należy korzystać z tej konstrukcji z uwagą. Jak wiadomo diabeł tkwi nie tylko w kobietach, ale i w szczegółach. A więc do rzeczy…

Przykład z ostatniego posta jest nadal aktualny – poszukujemy osób z wiekiem mniejszym niż średnia wieku całej kolekcji. Poniżej dwa sposoby:

1) Sposób pierwszy, znany od dawien dawna, w dzisiejszych czasach można by rzec “lamerski”:

 1:   double average = persons.Average(p => p.Age);
2: var young = from person in persons
3: where person.Age < average
4: select person;
5: foreach (var person in young)
6: {
7: Console.WriteLine(person.ToString());
8: Console.WriteLine(“——————-“);
9: }

2) Sposób drugi, modny, nowy w C#, “wykorzystujący najnowsze technologie”, szpanerski, itp itd

 1:   var young = from person in persons
2: let average = persons.Average(p => p.Age)
3: where person.Age < average
4: select person;
5: foreach (var person in young)
6: {
7: Console.WriteLine(person.ToString());
8: Console.WriteLine(“——————-“);
9: }

Czym się te rozwiązania różnią? Na pierwszy rzut oka – niczym. Na pierwszy rzut oka – w jednym i drugim przypadku deklarujemy zmienną przechowująca wartość średniej wieku, by później wykorzystać ją w poszukiwaniach pożądanych osób. Tyle tylko, że pierwsza wersja zapamiętuje i przechowuje – na pierwszy rzut oka – wartość średniej przed zapytaniem, a druga wersja zapamiętuje i przechowuje – na pierwszy rzut oka – wartość średniej w samym zapytaniu. Zapamiętuje i przechowuje. Przecież bez sensu byłoby dla KAŻDEJ osoby w kolekcji od nowa wyliczać średnią, która jest oczywiście niezmienna? Prawda? PRAWDA?

Oczywiście że prawda. Ale nie bez kozery tyle razy napisałem o pierwszym oka rzucie. Mam nadzieję, że ten wpis uświadomi nieuświadomionym, iż jakże głębokie stwierdzenie “with power comes responsibility” w procesie tworzenia oprogamowania nabiera jeszcze większej wartości.

Zróbmy mały eksperyment. Rozszerzmy funkcjonalność klasy Person tak, aby móc dokładnie zaobserwować zachowanie naszego programu. Szczególnie interesuje nas dostęp do właściwości Age, dzięki czemu niczym Adam Słodowy sprytnie zobaczymy kiedy dane tryby zaczynają się kręcić podczas poszukiwania konkretnych osób. Pełna implementacja poniżej (kluczowa jest linijka 12):

 1:   class Person
2: {
3: public string FirstName { get; set; }
4: public string LastName { get; set; }
5: private int _age;
6: public int Age
7: {
8: get
9: {
10: var color = Console.ForegroundColor;
11: Console.ForegroundColor = ConsoleColor.Red;
12: Console.WriteLine(“Returning age from person “ + this.ToString());
13: Console.ForegroundColor = color;
14: return _age;
15: }
16: set { _age = value; }
17: }
18:
19: public override string ToString()
20: {
21: return this.FirstName + ” “ + this.LastName;
22: }
23: }

Jak widać – przy każdym odczycie wartości Age na konsolę wypisany zostanie w czerwonym kolorze odpowiedni komunikat. Do wykonania eksperymentu potrzebne są jeszcze dane wejściowe. U mnie jest to następująca lista osób:

 1:   List<Person> persons = new List<Person>
2: {
3: new Person { FirstName = “FirstName1”, LastName = “LastName1”, Age = 12 },
4: new Person { FirstName = “FirstName2”, LastName = “LastName2”, Age = 14 },
5: new Person { FirstName = “FirstName3”, LastName = “LastName3”, Age = 18 },
6: new Person { FirstName = “FirstName4”, LastName = “LastName4”, Age = 19 }
7: };

Średnia ich wieku wynosi 15.75, zatem w wynikach powinny znaleźć się dwie pierwsze osoby.

Dobra, “let the shit hit the fan”! Oto efekt wykonania pierwszych instrukcji, z zapamiętaniem średniej “na zewnątrz” zapytania:

Bez niespodzianek. Najpierw pobierany jest wiek każdej z osób po kolei, który to krok do wyliczenia średniej jest konieczny. (no chyba że akurat znajdujemy się w kościele o 6 rano, gdy można ów wynik oszacować na 85-90, ale to tzw. “szczególny przypadek”) Następnie przy wykonaniu zapytania z każdego obiektu ponownie pobierany jest wiek w celu porównania go ze średnią. OK. A oto efekt drugiego zapytania:

Hmm… jakoś tego więcej. Przyjrzyjmy się bliżej. Możemy zaobserwować 4 serie po 5 odczytów wartości Age: 12341, 12342, 12343, 12344. Każdy element listy powoduje odczytanie tej wartości ze WSZYSTKICH jej elementów! Wniosek? Średnia wyliczana jest tutaj czterokrotnie! W tym przypadku nie niesie to za sobą żadnych konsekwencji, ale gdybyśmy mieli na liście więcej elementów? Oto wyniki czasowe dla 9999 osób:

Takiej różnicy po prostu nie można ot tak sobie zignorować. Skąd właściwie bierze się zaobserwowane zachowanie? Powodem jest efekt kompilacji tej konstrukcji, czyli tworzenie nowego obiektu dla każdego elementu kolekcji z wyrażeniem “let” jako doklejoną właściwością. W wyniku otrzymujemy nie jedną zmienną “average”, leczy tyle jej (każdorazowo obliczanych) kopii, ile mamy elementów w kolekcji. A o tym było ostatnim razem.

Jak widzimy – nowe konstrukcje są fajne, są przyjazne, są potrzebne i wypada je znać. Ale – zawsze zajrzyjmy trochę głębiej, aby zobaczyć co się kryje pod spodem. J. P. Boodhoo ma swoje motto: “Develop with passion!”. Ja bym dołożył do tego drugi człon: “Develop with rozwaga!”.

0 0 votes
Article Rating
1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
ucel
ucel
16 years ago

Efekt jest zwiazany z implementacja rozszerzen do Enumerable. W uproszczeniu mozna przyjac, ze wyrazenie LINQ jest filtrem enumeratora, wiec musi zostac uruchomione przy kazdym wywolaniu metody MoveNext().
Obejsc sie tego chyba nie da (nawet przez przepisanie do innej kolekcji, bo i tu enumerator musi zostac uruchomiony), wiec po prostu trzeba uwazac…

Kurs Gita

Zaawansowany frontend

Szkolenie z Testów

Szkolenie z baz danych

Książka

Zobacz również