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 for post “Wiązania zmiennych w anonimowych delegatach”.
  1. Kos:
    March 25th, 2010 o 14:50

    Nie spotkałem się z terminem “wiązanie”. Po angielsku mówi się zwykle na to “closure”, po polsku słyszałem “domknięcie”. Ta druga nazwa też obowiązuje?

    Strasznie przyjemny mechanizm, BTW. Bardzo popularny w językach skryptowych, choćby Pythonie, a najczęściej się go stosuje w Javascript – “emuluje się” tam w ten sposób klasom pola prywatne. :)

  2. Xion:
    March 25th, 2010 o 15:31

    Nazwa “domknięcie” jest czasem spotykana, ale niezbyt częsta. Jeśli chcemy się spierać o terminologię, to formalnie closure oznacza samą anonimową funkcję, która ma jakieś zmienne zewnętrzne. Ja tu użyłem bardziej ogólnego terminu ‘wiązanie’, bo chodziło mi o relację takiej funkcji zarówno z zewnętrznymi zmiennymi (domyślny przypadek w C#), jak i wartościami tych zmiennych (to, co w C# można niejako wymusić trikiem opisanym pod koniec, lub co jest domyślne w Boost.Lambda).

    Co do podobnego mechanizmu w JavaScripcie… Domyślam się jak to może pomagać w symulowaniu prywatnych pól, ale nie obraziłbym się na jakiś przykładowy kawałek kodu :) (Jeju co ja mówię, z własnej woli chcę oglądać kod JavaScript o.0).

  3. nilphilus:
    March 25th, 2010 o 21:04

    button1.Click += (a, b) => MessageBox.Show("T");

    Tak ładniej ;->

  4. Kos:
    March 25th, 2010 o 22:23

    Xion: Niech kod posłuży za obrazek :) Ofc to jest jedyne jedno podejście z możliwych. Javascript jest sam w sobie dość surowy, ale pozwala ‘stworzyć sobie’ model obiektowy wedle uznania.

    function Klasa(x) { // funkcja-konstruktor

    var a = arg;

    function jakasPrywatnaFunkcja() {
    alert("nikt o mnie nie wie!");
    }

    // tworzymy funkcję (funkcja w js = first-class value)

    function getValue() {
    return a;
    }

    return { // zwracamy obiekt, czyli w sumie tablicę asosjacyjną

    // albo pośrednio: tak:
    getValue: getValue,

    // albo od razu:
    setValue: function(x) {
    a = x;
    }
    }
    // zmienna 'a' jest prywatna, bo jest związana z obiektem jedynie poprzez closures jego funkcji
    }

  5. Kos:
    March 25th, 2010 o 22:27

    Długi komentarz mi się nie chciał pojawić (mimo że przy drugiej próbie wklejenia usłyszałem, że duplikat), dobrze działa Twój blog?

    Wrzucę zatem kod na nopaste:
    http://www.nopaste.pl/o53-javascript

    Przypomnę że jest to tylko jedno możliwe podejście – JS to zabawny luźny język pozwalający sobie “pokombinować” z robieniem swojego OOP-u. :)

  6. Xion:
    March 26th, 2010 o 0:48

    Naciąłeś się na przewrażliwiony filtr antyspamowy. Rzadko ma fałszywe pozytywy, ale czasem się zdarzają :) A co do tego kodu, to wygląda mi to na gorszą wersję obiektowości z Pythona… a przypominam, że tam hermetyzacji nie ma w ogóle ;-)

Comments are disabled.
 


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