Zine.net online

Witaj na Zine.net online Zaloguj się | Rejestracja | Pomoc
w Szukaj

mgrzeg.net - Admin on Rails :)

Funkcje anonimowe a pętle - czy coś jeszcze może zaskoczyć?

Na początek prosty przykład

 

using System;

 

class Test

{

  static void Main()

  {

    LoopIn();

    Console.WriteLine();

    LoopOut();

  }

 

  static void LoopIn()

  {

    for (int i = 0; i < 3; i++)

    {

      int x = i + 1;

      Console.WriteLine(x);

    }

  }

 

  static void LoopOut()

  {

    int x;

    for (int i = 0; i < 3; i++)

    {

      x = i + 1;

      Console.WriteLine(x);

    }

  }

}

 

Output:

1

2

3

 

1

2

3

 

Oczywiście wynik działania obu metod nie jest dla nikogo żadnym zaskoczeniem. Bo niby czemuż miałby być? Różnica między obiema metodami polega wyłącznie na wyprowadzeniu zmiennej lokalnej x poza pętlę, co dla wielu jest oczywiste i traktowane jako dobra praktyka programistyczna. Ja również należę do tej grupy i uważam, że nadmiarowe tworzenie bytów nie jest dobrą praktyką. Przejdźmy jednak do kolejnego przykładu, a zobaczymy, czy nasze ‘dobre praktyki’ mają sens:

 

using System;

delegate void D();

 

class Test

{

  static void Main()

  {

    foreach (D d in LoopInWithAnonymous()) d();

    Console.WriteLine();

    foreach (D d in LoopOutWithAnonymous()) d();

  }

 

  static D[] LoopInWithAnonymous()

  {

    D[] result = new D[3];

    for (int i = 0; i < 3; i++)

    {

      int x = i + 1;

      result[i] = delegate { Console.WriteLine(x); };

    }

    return result;

  }

 

  static D[] LoopOutWithAnonymous()

  {

    D[] result = new D[3];

    int x;

    for (int i = 0; i < 3; i++)

    {

      x = i + 1;

      result[i] = delegate { Console.WriteLine(x); };

    }

    return result;

  }

}

 

Różnica w stosunku do poprzedniego przykładu polega na wprowadzeniu metod anonimowych służących do definicji tablicy delegatów i wykorzystaniu ich do wypisania na ekran wartości zmiennej x.

A oto wynik działania:

1

2

3

 

3

3

3

 

A zatem - wyprowadzenie zmiennej x poza pętlę w przypadku użycia metod anonimowych skończyło się całkowicie odmiennym wynikiem! Nie będę ukrywał, że ten przykład łamie moją intuicję co do ‘bezkarności i konieczności’ wykonania tej operacji, ale być może należę do wyjątków i dla innych jest to oczywiste.

 

Zauważmy, że ten przykład można przepisać z wykorzystaniem wyrażeń lambda:

 

using System;

delegate void D();

 

class Test

{

  static void Main()

  {

    foreach (D d in LoopInWithLambda()) d();

    Console.WriteLine();

    foreach (D d in LoopOutWithLambda()) d();

  }

 

  static D[] LoopInWithLambda()

  {

    D[] result = new D[3];

    for (int i = 0; i < 3; i++)

    {

      int x = i + 1;

      result[i] = () => { Console.WriteLine(x); };

    }

    return result;

  }

 

  static D[] LoopOutWithLambda()

  {

    D[] result = new D[3];

    int x;

    for (int i = 0; i < 3; i++)

    {

      x = i + 1;

      result[i] = () => { Console.WriteLine(x); };

    }

    return result;

  }

}

 

I wynik w tym przypadku jest taki sam jak w poprzednim przykładzie.

 

Pozostaje jeszcze wyjaśnienie dlaczego tak się dzieje, bo odpowiedź na pytanie o cel jak dla mnie pozostaje zagadką, choć wydaje mi się, że prawdopodobnie jest to efekt uboczny stosowania funkcji anonimowych, niezamierzony przez twórców języka.

Wyjaśnienie spróbuję opisać w najbliższym czasie, ale może do tego czasu znajdzie się jakiś śmiałek, który opisze w komentarzach co też takiego się wydarzyło i dlaczego wynik jest taki, a nie inny??? Udało mi się nawet zdobyć nagrodę dla tegoż osobnika (osobniczki) - Arek zgodził się zasponsorować ‘uścisk dłoni prawicy prezia’ przy pierwszej nadarzającej się okazji, czyli np. podczas najbliższego spotkania WG.NET :D, a może dorzuci coś jeszcze :)

 

Przy przygotowywaniu przykładów posiłkowałem się specyfikacją języka C#, a dokładniej rozdziałem 7.14.4.2 tegoż dokumentu.

Opublikowane 1 lipca 2008 19:16 przez mgrzeg
Filed under:

Powiadamianie o komentarzach

Jeżeli chciałbyś otrzymywać email gdy ta wypowiedź zostanie zaktualizowana, to zarejestruj się tutaj

Subskrybuj komentarze za pomocą RSS

Komentarze:

 

Marcin Hoppe said:

Oto moje wyjaśnienie:

1. Anonimowa metoda może "złapać" zmienną x, której zakres zawiera definicję anonimowej metody.

2. Jedna instancja anonimowej metody "łapie" jedną instancję zmiennej x. W tym celu kompilator tworzy ukryty typ, który ma metodę z ciałem anonimowej metody i pole x.

3. Pierwszy przypadek: w każdym przebiegu pętli tworzona jest nowa instancja zmiennej x, a więc każda instancja anonimowej metody odwołuje się do innej instancji zmiennej x. Trzy utworzone instancje x mają wartości: 1, 2 i 3, co tłumaczy wynik programu.

4. Drugi przypadek: tworzona jest tylko jedna instancja zmiennej x i każda z utworzonych instancji anonimowej metody odwołuje się do tej samej instancji x. Po trzech przebiegach pętli wartość instancji x wynosi 3, a więc wykonanie każdej z instancji anonimowej metody wypisuje tę samą wartość.

Ufff :).

Od siebie dodam, że można to jeszcze bardziej zakręcić:

delegate void Kod();

public void Zakrecone() {

   List<Kod> kod = new List<Kod>();

   int zewnetrzna = 0;

   for(int i = 0; i != 3; ++i) {

       int wewnetrzna = 0;

       kod.Add(delegate

       {

           Console.WriteLine("{0},{1}", zewnetrzna, wewnetrzna);

           zewnetrzna++;

           wewnetrzna++;

       });

   }

   foreach(var k in kod)

       k();

}

Proponuję, żeby pierwsza osoba, która odpowie na moje pytania, również otrzymała uścisk dłoni jakiegoś prezesa ;).

PS. Zagadnienie to jest dobrze opisane w książce Jona Skeeta "C# in Depth".

lipca 1, 2008 21:03
 

Marcin Hoppe said:

Zdałem sobie po chwili sprawę (jednak trzeba robić proof reading nawet komentarzy na cudzych blogach), że nie sformułowałem pytań :). Oto one:

1. Co wypisze powyższa metoda?

2. Co wygeneruje kompilator? Podpowiedź: ile klas zostanie wygenerowanych i jak będą ze sobą powiązane obiekty tych klas?

Pozdrawiam!

Marcin

lipca 1, 2008 21:10
 

saku said:

Istny ekspres ;) Nie zdążyłem zabrać się do piania a już pojawiło się wyczerpujące wytłumaczenie.

lipca 1, 2008 21:33
 

Marcin Hoppe said:

Starałem się nie popsuć zabawy, ale odbić piłeczkę w inną stronę :).

lipca 1, 2008 22:42
 

mgrzeg said:

@Marcin: nie pozostaje Ci nic innego jak przyjechac kiedys do wawy i odebrac nagrode :)  Arek juz myje reke ;)

Przy okazji - nie czytalem tej ksiazki! Ze spisu tresci wyglada naprawde ciekawie...

@saku: wariacja Marcina to ciekawe rozwiniecie - odpowiedz na pytania, a jestem przekonany, ze jak przyjedziesz kiedys do kraju nad Wisłą, to i Arek i Marcin i ja rzecz jasna bardzo chetnie nagrodzimy Cie wielokrotnym hand-shakiem ;)

@Arek, co nic nie piszesz? Obiecywales gory zlote, a tu co? Rozumiem, ze czekasz na kogos bardziej namacalnego z wawy :D

A moze ktos inny odbierze odbita pileczke? :)

No i ciekawi mnie dalej to, czy inni rowniez uwazaja te implementacje jako 'lekko nieintuicyjna'...?

lipca 2, 2008 02:12
 

Procent said:

Miałem ten temat na swojej liście "to blog about..." :). Jako uzupełnienie polecam lekturę postów http://blogs.msdn.com/abhinaba/archive/2005/10/18/482180.aspx i http://blogs.msdn.com/brada/archive/2004/08/03/207164.aspx .

lipca 2, 2008 10:44
 

mgrzeg said:

@Maciek - temat wcale nie zostal wyczerpany! Ja tu widze studnie bez dna ;)

lipca 2, 2008 11:30
 

saku said:

Trochę się nagłowiłem, nad problemem Marcina ale myślę, że kompilator sobie to rozpisze jakoś tak:

List<Kod> kod = new List<Kod>();

Wygenerowany_Typ1 a = new Wygenerowany_Typ1();

a.zewnetrzna = 0;

for (int i = 0; i != 3; ++i)

{

   Wygenerowany_Typ2 b = new Wygenerowany_Typ2();

   b.a = a; // typ b zawiera pole typu a

   b.wewnetrzna = 0;

   kod.Add(b.Wykonaj());

}

foreach (var k in kod)

   k();

Przy czym definicja Wykonaj to nasza 'delegacja':

public void Wykonaj()

{

   Console.WriteLine("{0},{1}", zewnetrzna, wewnetrzna);

   this.a.zewnetrzna++;

   this.wewnetrzna++;

}

Teraz określenie wartości to pestka (no chyba, że coś pokręciłem), muszę przyznać, że patrząc na kod z metodą anonimową nie miałem pojęcia jaka będzie wartość zmiennej 'zewnetrzna'.

lipca 2, 2008 16:25
 

Marcin Hoppe said:

Mnie się wydaje, że powinna być za to jakaś nagroda :). Moim zdaniem też się coś takiego wygeneruje. Można sobie nawet podpatrzyć Reflectorem, że tak się dzieje.

Gratulacje!

lipca 2, 2008 17:41
 

jacek hoppe said:

marcin ogromna prosba napisz do mnie jacekhop@wp.pl, jhoppe@wi.ps.pl sprawa pilna

lipca 10, 2008 00:17
 

Marcin Hoppe said:

Jacek, odpisałem na oba podane przez Ciebie adresy e-mail. Prostszy sposób na kontakt przez portal to rejestracja i wysłanie prywatnej wiadomości. Ciężko jest śledzić komentarze przez dłuższy czas, natomiast wiadomość prywatna trafia prosto do skrzynki e-mail.

To tak na przyszłość :).

lipca 10, 2008 16:27

Co o tym myślisz?

(wymagane) 
(opcjonalne)
(wymagane) 

  
Wprowadź kod: (wymagane)
Wyślij
W oparciu o Community Server (Personal Edition), Telligent Systems