Podczas przygotowywania kolejnego posta z serii “Samples” zaimplementowało mi się coś bardzo fajnego, co chyba zasługuje na osobną notkę. Oto zatem kolejna krótka demonstracja potęgi połączonych mechanizmów C# (v. 2 i 3).
Scenariusz: mamy formatkę wypełnioną panelami, groupboxami, layoutami i wszystkimi innymi kontenerami jakie tam jeszcze Bozia z Redmond na toolbox wrzuciła. Nachodzi nas chętka na wykonanie pewnej operacji na WSZYSTKICH kontrolkach zawartych w oknie, niezależnie od tego gdzie są zagnieżdżone. Jak się do nich dobrać? Here comes the beauty of YIELD:
1: public static IEnumerable<Control> AllChildControls(this Control instance) 2: { 3: foreach (Control control in instance.Controls) 4: { 5: yield return control; 6: foreach (Control childControl in control.AllChildControls()) 7: yield return childControl; 8: } 9: }
Wykorzystajmy to w jakimś interesującym przypadku… na przykład pod każdy TextBox podepnijmy ToolTipa pokazującego aktualną wartość właściwości Tag. Warunki z tego wynikające są dwa: kontrolka-dziecko musi być TextBoxem i jej Tag nie może być null. Na potęgę posępnego czerepu, LINQ przybywaj!!!
1: this.AllChildControls().OfType<TextBox>() 2: .Where(tb => tb.Tag != null).ToList() 3: .ForEach(tb => toolTip.SetToolTip(tb, tb.Tag.ToString()));
Może to jakieś zaćmienie, może za dużo kodowania bez przerwy, może nie widzę innego równie wyśmienitego rozwiązania, ale… jestem pod wrażeniem. Jak wyglądałby kod robiący to samo jeszcze kilkanaście miesięcy temu? Pewnie jakoś tak. Nie uzależniajmy się od technologii, ale wykorzystujmy w pełni to co nam oferuje!
Byłaby ładna rekurencja i chyba nawet czytelniejsza :P
“AllChildControls” znajdziemy i w linii 1 (deklaracja) i w linii 6 (wywołanie samej siebie), więc rekurencję jak najbardziej mamy:)
Da sie to zrobic oszczedniej, ale wymaga implementacji ForEach dla IEnumerable. Zauwaz ze wywolujesz metode ToList(), co w efekcie podwoi wymagania pamieciowe kolekcji. Implementacja extension method ForEach() dla IEnumerable zlikwidowalaby ten problem . Z drugiej strony nie rozumiem dlaczego nie ma tego natywnie w System.Core :/.
Tak jest, też się nad tym zastanawiałem. “Przeszukałem nawet pół internetu” ale odpowiedzi nie znalazłem.
A co do podwojenia wymagań pamięciowych… czy na pewno? W obecnej postaci utworzona zostanie lista ze wszystkimi textboxami a dopiero potem niepotrzebne się odfiltrują. Z ForEach w IEnumerable filtrowanie odbędzie się na etapie tworzenia listy. Tak czy siak – wszystkie kontrolki już istnieją, więc gdzie może wystąpić dodatkowe zuzycie pamięci? ToList() wymusza po prostu wykonanie instrukcji o jeden krok wcześniej niż gdyby nastąpiło to po Where(). W efekcie od razu uzyskalibyśmy mniejszą listę.
Chyba że się mylę, wtedy z radością powitam wszelkie wyjaśnienia:)
@ucel: Z koncepcyjnego punktu widzenia zapytanie LINQ nie powinno wywoływać efektów ubocznych na elementach kolekcji, stąd brak metod takich jak ForEach, które ośmielałyby programistów do adopcji takiego modelu programowania. Jeśli spojrzeć z tej strony, to podejście Procenta jest słuszne: zapytanie jest formułowane i ewaluowane, a następnie elementy, które znalazły się w zbiorze wynikowym są przetwarzane. Inna sprawa, że możnaby zakończyć zapytanie zaraz po Where, a następnie użyć zwykłej pętli foreach do iteracji po wynikach.()(this IEnumerable items, Action action)()
Rolę takiego deklaratywnego ForEacha w świecie LINQ może z powodzeniem pełnić operator Select:
this.AllChildControls()
.OfType
.Where(tb => tb.Tag != null)
.Select(tb => { toolTip.SetToolTip(tb, tb.Tag.ToString()); return tb; });
Problemem jest tylko to, że musimy jeszcze takie wyrażenie jakoś ewaluować, żeby miało sens.
@Procent: ucel może trochę przeszacował zużycie pamięci, jednak słusznie zwrócił uwagę, że etapu, na którym tworzymy nowy obiekt listy można uniknąć. Do szczęścia wystarczy nam referencja do iteratora zwracana przez metodę Where:
public static void ForEach
{
foreach (T item in items) {
action(item);
}
}
Teraz możemy zapisać wyrażenie w ten sposób:
this.AllChildControls()
.OfType
.Where(tb => tb.Tag != null)
.ForEach(tb => toolTip.SetToolTip(tb, tb.Tag.ToString()));
Na żadnym etapie nie są tworzone tymczasowe kolekcje – wszystko oparte jest o iteratory.
Dzięki Olek za wyjaśnienia, szczególnie kwestii “dlaczego w System.Core nie zaimplementowano IEnumerable.ForEach”. “Koncepcyjny punkt widzenia” ma sens, nigdy w ten sposób na to nie spojrzałem.
No przeciez wlasnie cos takiego mialem na mysli ;)