Tutoriale C#

Tutoriale C# – Delegări, evenimente, generice

În tutorialele precedente am discutat despre particularitățile limbajului C#, raportate la paradigma programării orientate pe obiecte precum și la trăsăturile native care îl definesc. Acesta este ultimul articol pe care îl dedic în mod exclusiv prezentării limbajului la nivel de sintaxă. Mai departe, vom învăța cum să lucrăm în mediul de dezvoltare Visual Studio, cum să aplicăm noțiunile învățate anterior și implicit, cum să dezvoltăm aplicații funcționale, care fac ceva anume. Fiind ultimul articol orientat pe sintaxă, îl vom dedica unor concepte deosebit de importante, care au o aplicabilitate foarte însemnată în practică.

1. Delegări

Delegările sunt tipuri referință care au rolul de a menține o listă de metode cu aceeași semnătură și apoi de a le apela pe toate simultan. Se declară în afara claselor (la nivel global) cu ajutorul cuvântului cheie delegate,  și cel mai important despre delegări este că ele nu au corp! Ele nu sunt ceva gen clase sau interfețe! Prin declararea unei delegări, se specifică semnătura metodelor pe care ea poate să le înglobeze. Exemplu:

delegate int Delegare(int x);

Aici avem declarată o delegare, cu numele Delegare, care acceptă metode cu tip de retur int și un parametru tot de tip int. Acum am declarat delegarea, va trebui să îi spunem ce metode să rețină (evident, metode care respectă formatul ei). O să implementăm un program complet, pentru a ilustra întreaga funcționalitate a delegărilor:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace code_it.ro
{
    public delegate int Delegare(int x);
    
    class Numar
    {
        public int numar;
        public int Incrementeaza(int y)
        {
            return (++y);
        }

        public int Decrementeaza(int y)
        {
            return (--y);
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Numar obiect = new Numar();
            Delegare obiectDelegat;

            obiectDelegat = obiect.Incrementeaza;
            obiectDelegat += obiect.Decrementeaza;

            int valoare = obiectDelegat(5);

            Console.WriteLine("Rezultatul este: {0}", valoare);
            
            Console.ReadKey();
        }
    }
}

Avem o clasă denumită Numar care are un câmp întreg, numar, și două metode care respectă formatul delegării. În Main(), instanțiem clasa Numar dar și delegarea declarată mai sus. Am spus că delegările sunt tipuri referință, deci ele trebuie să fie instanțiate, trebuie să declarăm un obiect și apoi să alocăm memorie pentru el. În cazul delegărilor, se poate aloca memorie și cu operatorul new dar varianta mult mai utilizată și mai rapidă este aceea în care se adaugă direct metode în lista de invocare a delegării.

obiectDelegat = obiect.Incrementeaza;
obiectDelegat += obiect.Decrementeaza;

Aici practic se adaugă metode în delegare. Totalitatea metodelor conținute de o delegare formează lista de invocare a delegării.

int valoare = obiectDelegat(5);

Apelarea delegării este similară unui apel normal de funcție, numai că apelarea unei delegări presupune apelarea simultană a tuturor metodelor aflate în lista de invocare.

Evident, delegarea se apelează cu sau fără parametrii, în funcție de formatul ei. În linia de mai sus se întâmplă două lucruri: se declară o variabilă de tip întreg denumită valoare și i se atribuie valoarea rezultată în urma apelului delegării.

De reținut este că apelarea unei delegări care conține mai multe metode (care returnează ceva, deci care nu sunt void) în lista de invocare, dă ca rezultat valoarea returnată de ultima metodă apelată. În cazul exemplului complet de mai sus, se apelează delegarea cu valoarea 5, se incrementează, deci devine 6 (valoare care se pierde), apoi se apelează metoda de decrementare tot cu parametrul 5, deci se va afișa în final 4.

Dacă metodele au tip de retur void, ele se apelează în ordine, fără restricții. De remarcat faptul că o delegare care are în lista de invocare o singură metodă este echivalentul unui pointer la funcții în limbajul C.

2. Evenimente

Aplicațiile pe care vom învăța să le dezvoltăm în tutorialele viitoare vor fi bazate în mare parte pe interfață grafică (Windows Forms). Ce înseamnă asta?  Înseamnă că programul pe care îl vom concepe va consta dintr-o fereastră (sau mai multe) similară (sau nu) celor din Windows, care poate să facă tot felul de chestii, pe care tu le gestionezi, și anume: să răspundă în diverse forme la click pe butoane, pe care tu le pui, le aranjezi, le programezi; să apară diverse meniuri, toolbar-uri, combo-box-uri, etc.; să execute o acțiune după trecerea unui anumit interval de timp și multe alte chestii interesante.

Ei bine, evenimentele au un rol aparte în acest context al dezvoltării aplicațiilor cu interfață grafică. Spre exemplu, avem o fereastră cu un buton pe ea. Atunci când dăm click pe buton, un eveniment se declanșează! Și anume evenimentul ce corespunde click-ului pe acel buton. Dacă trecem cu săgeata de la mouse (cursorul) pe deasupra butonului sau pe deasupra ferestrei, se declanșează un alt eveniment! Dacă apăsăm pe orice tastă atunci când aplicația este activă, se declanșează un alt eveniment, și tot așa. Evenimentele, în cadrul unei aplicații de tip Windows Forms, au o mare diversitate și servesc dezvoltării unei game foarte variate de programe. De aceea este necesar să cunoaștem mecanismele din spatele lor.

Rememorăm exemplul de mai sus, cu acea fereastră cu buton pe ea. Atunci când utilizatorul dă click pe buton, o clasă specială din B.C.L., denumită clasă Publisher, semnalează alte clase despre producerea acestui eveniment de click pe buton. Aceste alte clase se numesc clase subscriber. Rolul acestor clase din urmă este de a executa efectiv codul (scris de programator) care trebuie să se execute atunci când respectivul eveniment a fost invocat. De regulă, noi o să lucrăm cu controalele .NET (și implicit cu evenimentele aferente lor), deci nu o să avem contact cu clasele Publisher, întrucât ele au fost deja create de cei de la Microsoft. Noi o să scriem doar codul executabil în clasele Subscriber.

Dacă, în schimb, se dorește crearea unor controale noi, lucru destul de complex, de altfel, trebuie să se creeze și niște evenimente care să se asocieze acelor controale, deci implicit se va avea în vedere și clasele Publisher.

În programul de mai jos vom emula funcționalitatea evenimentelor, cu adaptare la ceea ce ne permite o aplicație în consolă:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace code_it.ro
{
    public delegate void iteratie(int i);     // declarare tip delegat atasat evenimentului

    class Publisher
    {
        public event iteratie eveniment;

        public void DeclansareEveniment() 
        {
            for(int i=0; i<=20; i = i + 2) 
            {
                if (eveniment != null)     // verificam existenta metodelor subscriber
                    eveniment(i);          // declansam evenimentul
            }
        }
    }

    class SubscriberA             // exemplu de clasa subscriber. Pot fi oricat de multe astfel de clase
    {
        public void metodaClasaSubscriber(int x)
        {
            Console.WriteLine("Ne aflam în corpul metodei clasei subscriberA la iteratia: {0}", x);
        }
    }

    class SubscriberB
    {
        public void metodaClasaSubscriber(int x)
        {
            Console.WriteLine("Ne aflam în corpul metodei clasei subscriberB la iteratia: {0}", x);
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Publisher Pb = new Publisher();

            SubscriberA SBa = new SubscriberA();
            SubscriberB SBb = new SubscriberB();

            Pb.eveniment += SBa.metodaClasaSubscriber;
            Pb.eveniment += SBb.metodaClasaSubscriber;

            Pb.DeclansareEveniment();

            Console.ReadKey();
        }
    }
}

Să analizăm ce am făcut aici: în prima parte a codului am creat un tip delegat, denumit iteratie care va reține lista de metode ce vor fi executate în urma declanșării evenimentului. Clasa Publisher (poate avea orice alt nume, am pus ”Publisher” doar pentru a sublinia specificul ei), conține un eveniment, individualizat prin cuvântul cheie event care îl precede, un eveniment de tip iteratie care are numele eveniment. Deci mare atenție: un eveniment nu este un tip delegat! este un tip care conține un tip delegat în componența sa. Un eveniment poate fi considerat un tip de gestiune, în sensul că are rolul de a coordona legăturile stabilite între tipul delegat și metodele care îl declanșează.

Clasa Publisher are, de asemenea, o metodă prin care se declanșează evenimentul. Aici trebuie să înțelegem următorul lucru: evenimentul propriu-zis pe care noi îl emulăm în acest program în consolă este iterația. Noi aici facem să se întâmple acest eveniment (îl inducem). În aplicațiile funcționale, evenimentul nu este indus, ci apare în urma unei acțiuni (externe, de cele mai multe ori), cum ar fi: click pe buton, apăsare de tastă, mișcarea cursorului, trecerea unui interval de timp, mișcarea rotiței de la mouse, acțiunea unui program extern (virus, etc.), acțiuni ale sistemului de operare, semnalări de avarii la componentele hardware, etc.

Revenim la program. Am spus că noi practic facem să se întâmple acest eveniment. Cum facem asta? Păi ne folosim de instrucțiunea repetitivă for pentru a itera din 2 în 2 de la 0 la 20. Verificăm dacă evenimentul are metode care să le poată executa, după care declanșăm evenimentul, prin transmiterea parametrului pe care îl acceptă (și anume un parametru de tip întreg – îi transmitem direct contorul for-ului). Am terminat cu clasa Publisher.

Mai departe, clasele Subscriber pot fi oricât de multe și oricât de variate. Singura condiție care impune funcționalitatea lor este ca ele să conțină metode cu aceeași semnătură pe care o are tipul delegat asociat evenimentului. Deci dacă delegarea returnează void și acceptă un parametru de tip întreg, exact așa trebuie să fie și metoda al cărei cod se dorește a fi executat din clasele Subscriber. Am zis mai simplu, SubscriberA și SubscriberB, fiecare cu câte o metodă adecvată.

În Main() instanțiem toate cele trei clase, după care inițializăm lista de invocare a evenimentului (de fapt a tipului delegat asociat evenimentului), prin atașarea celor două metode ale celor două clase Subscriber, după care declanșăm evenimentul.

Vă las pe voi să vedeți ce anume afișază programul!

Metodele care execută codul pe care utilizatorul dorește să îl asocieze evenimentului se numesc metode manipulatoare de eveniment sau event-handlers. 

Toate aceste elemente sunt practic lucruri de bază atunci când vine vorba de evenimente. În practică, nu lucrăm, așa cum am spus mai sus, cu astfel de programe explicite, în materie de evenimente. Atunci când avem o fereastră cu un buton pe ea, dacă dăm click-dreapta pe buton și mergem la Properties, vom observa o fereastră în partea dreaptă a ecranului în care putem seta diverse proprietăți asociate butonului, precum: culoare, grosime margini, culoare margini, imagine de fundal, text, locație, mărime, aliniere, etc. Dacă dăm click pe iconița cu fulger tot din partea de sus a ferestrei de proprietăți, vom vedea o listă mare cu…evenimente! Ce fel de evenimente? Păi evident, legate de buton, că despre el vorbim! Spre exemplu: Click, MouseLeave, MouseHover, MouseEnter, Move, etc. Dacă dăm dublu-click pe oricare din aceste evenimente, vom fi trimiși la hander-ul de eveniment, deci direct la cod. Mediul de dezvoltare creează automat metoda subscriber, noi trebuie doar să scriem codul executabil.

buttonProperties

3. Generice

Am discutat în tutorialul destinat prezentării limbajului C# despre faptul că acesta admite programarea generică, adică îi permite programatorului să creeze tipuri de date precum și funcții care să se execute independent de tipul datelor. Spre exemplu dacă avem o clasă care ilustrează funcționalitatea unei cozi (sau a unei stive – să îi spunem container), atunci noi putem stoca în acel container doar date de un singur tip: spre exemplu, dacă avem un container de întregi, nu putem stoca valori de tip float sau double sau string, ci doar de întregi. Dacă avem nevoie în proiectul nostru de clase pentru int/float/double/string, atunci va trebui să scriem o clasă pentru fiecare tip de date, ceea ce crește semnificativ volumul de cod și scade drastic înțelegerea acestuia!  Programarea generică ne permite să scriem o singură clasă, care să aibă un singur tip general (nu e nici int, nici float, nici double, nici string, este T (în general), dar poate fi orice altceva). În momentul instanțierii clasei, se va preciza tipul utilizat. Dacă vrem să lucrăm cu un container de int, la instanțiere precizăm acest lucru și clasa noastră generică (denumită clasă șablon) se transformă într-o clasă care ilustrează funcținalitatea unui container de întregi. Analog pentru celelalte tipuri. Funcțiile generice au exact aceeași funcționalitate. Execută codul scris în funcție de tipul general transmis ca argument.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace code_it.ro
{
    class numberClass<tipGeneral>
    {
        public void method(tipGeneral param)
        {
            Console.WriteLine("Parametrul transmis este: {0}", param);
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            numberClass<double> obiect = new numberClass<double>();

            obiect.method(30.44);
            
            
            Console.ReadKey();
        }
    }
}

Când vine vorba de generice, în mod indispensabil o să găsiți și parantezele unghiulare < >. Practic, clasa generică numberClass admite un tip general denumit tipGeneral. Clasa nu știe nimic despre acest tip. Adică nu știe care e: int, float double, etc. Ea știe doar că poate să fie oricare dintre astea, sau poate fi unul creat de programator, nu neapărat built-in. De regulă se pune T, și nu tipGeneral, eu am pus așa doar ca să vedeți că nu e o regulă strictă să se pună T. Clasa are o singură metodă void care acceptă un parametru de acest tip general. Ce face cu el? Îl afișază…ce altceva ar putea să facă cu el? :-)) Evident, poate să facă multe chestii…bine, trebuie mai muți parametrii, sau unul singur, dar să fie un tablou, sau ceva, care să aibă o utilitate.

Mare atenție:

numberClass<double> obiect = new numberClass<double>();

Această linie instanțiază obiectul denumit obiect. În acest pas este musai să se precizeze tipul de date asociat obiectului propriuzis. Aici am pus double, dar puteți pune orice alt tip, chiar și user-defined, așa cum am spus mai sus. E la fel ca o instanțiere normală, numai că aici apare în plus tipul de date între acele paranteze unghiulare.

Următorul program ilustrează funcționalitatea metodelor generice:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace code_it.ro
{
    class genericMethod<T>
    {
        T field;
        public T method<U, V>(U param1, V param2)
        {
            Console.WriteLine("{0}, {1}", param1.ToString(), param2.ToString());

            return field;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            genericMethod<int> obiect = new genericMethod<int>();

            obiect.method<int, double>(39, 38.2);
            
            
            Console.ReadKey();
        }
    }
}

Deci metoda din clasa genericMethod are doi parametrii generici, care trebuie specificați la apelare, așa cum observăm în Main(), independenți de parametrul generic al clasei, și anume T. Codul nu face mare lucru pentru că, de regulă, atunci când trebuie să implementezi algoritmi generici, trebuie să ții cont de următorul aspect: este deosebit de important ca toate instrucțiunile pe care le scrii să fie independente de tipul datelor, adică să nu scrii instrucțiuni specifice unui anumit tip: spre exemplu, nu poți crea o metodă generică cu două tipuri care să utilizeze operatorul + sau – sau <, >, !=, etc. asupra unuia sau a ambelor tipuri, pentru că nu s-a definit o metodă de supraîncărcare a operatorului respectiv pentru acel tip. Dacă se face acest lucru, atunci totul e în regulă. Este absolut necesar ca atunci când scrii cod generic să ai în vedere independența de tip a instrucțiunilor pe care le scrii.

În momentul compilării, clasa/metoda generică este transformată într-o metodă dependentă de tip (cel precizat de programator la instanțiere), ceea ce crește semnificativ performanța algoritmului, întrucât la rularea propriu-zisă a programului, nu se mai pierde timp pentru instanțierea șablonului de clasă/metodă, ci se execută direct clasa/metoda transformată. De asemenea, un cod generic poate fi reutilizat în alte aplicații care necesită alte tipuri de date decât cele pentru care a fost creat.

B.C.L.-ul are integrate câteva containere generice realizate în mod eficient de cei care au creat limbajul, precum: Stack<T> , List<T> , Dictionary<Tkey, Tvalue>, etc. Sunt foarte intuitiv de utilizat. Merită să încercați.

Pentru cei interesați:

  • Tratarea excepțiilor în C#
  • Clasa string – proprietăți, metode. Lucrul cu șiruri de caractere.
  • Operații de intrare de la tastatură (cum se citește un șir de caractere/numere).

Respectfully, Ioniță Cosmin

Write A Comment