Posts tagged ‘delegates’

Co może udawać obiekt w C++

2010-05-06 14:56

Możliwości przeciążania operatorów w C++ dla własnych typów obiektów sprawiają, że mogą one (tj. te obiekty) zachowywać się w bardzo różny sposób. Mogą na przykład “udawać” pewne wbudowane konstrukcje językowe, nierzadko wykonując ich zadania lepiej i wygodniej. Przykładów na to można podać co najmniej kilka – oto one:

  • Obiekt może zachowywać się jak funkcja, czyli udostępniać możliwość “wywołania” siebie z określonymi parametrami. Takie twory często nazywa się funktorami i bywają używane podczas pracy ze standardową biblioteką STL.
    Działają one przy tym w bardzo prosty sposób, zwyczajnie przeciążając operator nawiasów okrągłych (). Jest on na tyle elastyczny, że może przyjmować dowolne parametry i zwracać dowolne rezultaty, co pozwala nawet na stworzenie więcej niż jednego sposobu “wywoływania” danego obiektu. Jednym z bardziej interesujących zastosowań dla tego operatora jest implementacja w C++ brakującego mechanizmu delegatów, czyli wskaźników na metody obiektów.
  • Na podobnej zasadzie obiekt może udawać tablicę – wie o tym każdy, kto choć raz używał klas STL typu vector. Wymagane jest tu przeciążenie operatora indeksowania []. Daje ono wtedy dostęp do elementów obiektu-pojemnika, który zresztą nie musi być wcale indeksowany liczbami, jak w przypadku tablic wbudowanych (dowodem jest choćby kontener map). Ograniczeniem jest jedynie to, że indeks może być (naraz) tylko jeden, bo chociaż konstrukcja typu:
    1. v[3,4] = 5;

    jest składniowo najzupełniej poprawna, to działa zupełnie inaczej niż można by było się spodziewać :)

  • Obiekty mogą też przypominać wskaźniki, które wtedy określa się mianem ‘sprytnych’ (smart pointers). Dzieje się tak głównie za sprawą przeciążenia operatorów * (w wersji jednoargumentowej) i ->. Normalnie te operatory nie mają zastosowania wobec obiektów, ale można nadać im znaczenie. Wtedy też mamy obiekt, który zachowuje się jak wskaźnik, czego przykładem jest choćby auto_ptr z STL-a czy shared_ptr z Boosta.
  • Wreszcie, obiekt może też działać jako flaga boolowska i być używany jako część warunków ifów i pętli. Sprytne wskaźniki zwykle to robią, a innymi wartymi wspomnienia obiektami, które też takie zachowanie wykazują, są strumienie z biblioteki standardowej. Jest to spowodowane przeciążeniem operatora logicznej negacji ! oraz konwersji na bool lub void*.

Reasumując, w C++ dzięki przeciążaniu operatorów możemy nie tylko implementować takie “oczywiste” typy danych jak struktury matematyczne (wektory, macierze, itp.), ale też tworzyć własne, nowe (i lepsze) wersje konstrukcji obecnych w języku. Szkoda tylko, że często jest to wręcz niezbędne, by w sensowny sposób w nim programować ;)

Wiązania zmiennych w anonimowych delegatach

2010-03-24 17:10

Możliwość używania delegatów w C# to fajna rzecz. Przyjemne jest zwłaszcza definiowanie ich “w locie”, czyli bez konieczności tworzenia zupełnie nowej funkcji. Takiego delegata nazywamy wówczas anonimowym:

  1. btnOK.Click += delegate (object sender, EventArgs e) { MessageBox.Show("Bu!"); }

Przydaje się to zwłaszcza to podawana różnego rodzaju predykatów do funkcji sortujących lub wyszukujących. Takie nienazwane funkcje są zwykle krótkie i bardzo proste.

A co jeśli jest inaczej?… W szczególności interesująca sytuacja jest wtedy, gdy nasz delegat odwołuje się do zmiennej zewnętrznej – czyli takiej, która nie została w nim zadeklarowana, ale której zasięg zawiera definicję delegata. Oto przykład:

  1. public MainForm_Load(object sender, EventArgs e)
  2. {
  3.     int x; // to jest zmienna zewnętrzna dla poniższego delegata
  4.     // ...
  5.     btnFoo.Click += delegate (object sender, EventArgs e)
  6.         {
  7.             if ((int)(sender as Control).Tag < x)
  8.                 MessageBox.Show ("Bu!");
  9.         });
  10. }&#91;/csharp]
  11. Pomyślmy teraz, że kliknięcie na przycisk <code>btnFoo</code> na pewno nastąpi już po zakończeniu funkcji <code>MainForm_Load</code>. Zmienna <code>x</code> jest w niej zmienną lokalną... Czy to oznacza, że stanie się wtedy coś złego, bo delegat spróbuje odwołać się do wartości zmiennej, która już nie istnieje?
  12. Otóż nie; na szczęście nic złego się nie zdarzy. Kompilator potrafi wykryć taką sytuację zawczasu i przenieść zmienną zewnętrzną na zarządzaną stertę, gdzie czas jej życia może zostać wydłużony. Nawet jeśli jest to zmienna lokalna, będzie ona dostępna tak długo, jak długo chociaż jedno odwołanie do wykorzystującego ją delegata nie jest możliwe do uprzątnięcia przez <em>garbage collector</em>. Wszystko będzie więc dobrze i - co więcej - zupełnie przezroczyście: w powyższym przykładzie nie widać przecież żadnych oznak tego, że zmienna <code>x</code> jest w rzeczywistości znacznie mniej lokalna niż się na pierwszy rzut oka wydaje ;)
  13.  
  14. Widać zatem, że anonimowe delegaty w C# wiążą się ściśle ze swoimi zmiennymi zewnętrznymi, odwołując się do nich w razie potrzeby. Naturalnym efektem ubocznym jest fakt, że wobec tego wszystkie zmiany wartości zmiennych zewnętrznych są widoczne w kolejnych wywołaniach delegatów, które z nich korzystają:
  15. [csharp]delegate void Procedure();
  16. //...
  17. int i = 0;
  18. Procedure p = delegate() { MessageBox.Show(i.ToString()); };
  19. p();
  20.  
  21. i = 1;
  22. p();

Powyższy kod (skompilowany pod .NET co najmniej 3.0) pokaże 0 i 1, bowiem anonimowy delegat wiązany jest z samą zmienną i, nie zaś jej wartością w momencie definicji funkcji (czyli 0). Że zachowanie to nie jest znowu tak oczywiste, można uzasadnić podając przykład biblioteki Boost.Lambda dla C++, gdzie jest odwrotnie. Tam domyślnie wiązane są same wartości zmiennych zewnętrznych (w momencie tworzenia anonimowej funkcji), ale można to zmienić niewielkim wysiłkiem (używając modyfikatora var, jeśli kogoś to interesuje).
W C# podobnej możliwości nie ma, a do anonimowych delegatów wiązane są zawsze same zmienne, a nie ich wartości. Jeśli jednak potrzebowalibyśmy czegoś takiego, to prostym wyjściem jest wprowadzanie zmiennej pomocniczej, zainicjowanie jej wartością zmiennej pierwotnej i używanie jej wewnątrz delegatu:

  1. Procedure p;
  2. int i = 0;
  3. {
  4.     int j = i;
  5.     p = delegate() { MessageBox.Show(j.ToString()); };
  6. }
  7. p(); // 0
  8.  
  9. i = 1;
  10. p(); // również 0

Zarówno delegata, jak i deklarację owej zmiennej dobrze jest też zamknąć w osobnym bloku kodu – tak jak powyżej. Dzięki temu eliminujemy możliwość przypadkowej jej modyfikacji, która oczywiście zostałaby “zauważona” przez delegata.

Tags: ,
Author: Xion, posted under Programming » 6 comments

Bolączki C++ #6 – Obsługa zdarzeń

2007-11-09 23:46

Dzisiaj w programowaniu aplikacji obowiązują dwie proste i fundamentalne zasady. Po pierwsze, programujemy obiektowo i kod zamykamy w klasy z polami i metodami. Po drugie, tworzymy programy działające w środowisku graficznym, z okienkami, przyciskami, polami tekstowymi i innymi klasycznymi elementami interfejsu.
Jednak jeden plus dwa równa się kłopoty, przynajmniej w C++. Już raz narzekałem zresztą na to, że w tym języku obiektowość i GUI to nie jest najlepsze połączenie. Zgrzyta tu mechanizm obsługi zdarzeń generowanych przez interfejs graficzny.

Wcześniej napisałem, że możliwym rozwiązaniem problemu jest zasymulowanie w jakiś sposób delegatów, czyli – z grubsza – wskaźników na metody obiektów. To jedna z dróg radzenia sobie z kwestią obsługi zdarzeń. Ale też inna, wykorzystująca mechanizm metod wirtualnych i polimorfizmu. Polega to na zdefiniowaniu jednolitej klasy bazowej dla obiektów, które mają odbierać zdarzenia. Zwie się je zwykle event handlers, co jak zwykle nie ma dobrego tłumaczenia. Taki handler wyglądać może na przykład tak:

  1. class IEventHandler
  2. {
  3.    public:
  4.       virtual void OnClick(IControl* pSender) = 0;
  5.       virtual void OnKeyDown(IControl* pSender) = 0;
  6.       // itd.
  7. };

Mając jakiś element interfejsu użytkownika, np. przycisk, przekazujemy mu nasz własny obiekt implementujący powyższy interfejs. Metody tego obiekty są następnie (polimorficznie) wywoływane w reakcji na odpowiednie zdarzenia.
Tak oczywiście można robić w C++, ale nie jest to zbyt wygodne. Tym czego znów brakuje, to niestatyczne klasy wewnętrzne, obecne choćby w Javie, których brak w C++ nie da się do końca zastąpić wielokrotnym dziedziczeniem.

Albo delegaty, albo sposób opisany przed chwilą – zapewne nie ma żadnej innej drogi obiektowej obsługi zdarzeń. Niestety, żaden z tych sposobów nie jest obecnie wspierany w C++ i nie zanosi się, by miało się to wkrótce zmienić.

Tags: , , ,
Author: Xion, posted under Programming » 2 comments
 


© 2023 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.