2007-12-21

C# delegate-ek


Év vége előtti vincseszter-gyomtalanításnál találtam egy fájlot, amibe a C# delegate-jairól véstem fel pár gondolatot valamikor. Egyébként kicsit bánom hogy nem mentem a Javapolisra. Legközelebb -főleg ha ilyen lehetőség lesz- az asztalra csapok, félreteszem az aktuális (sz*rból kihúzandó) projektet és megcsinálom amit akarok.



A delegate a C-ben és C++-ban megszokott függvénypointerekre hasonlít legjobban.
Magának a szónak a jelentése: kiküldött követ, meghatalmazott, képviselő.



A delegate egy olyan adatstruktúra ami egy vagy több metódusra mutat, példánymetódusok esetében egyben az azokhoz tartozó objektumokra is. Mondhatnánk azt is, hogy a delegate, mint deklaráció egy metódus szignatúráját írja le. Azokkal a metódusokkal kompatibilis, amelyeknek a paraméterlistája és a visszatérési értéke megegyezik a delegate paraméterlistájával és visszatérési értékével.


Delegate-t így kell deklarálni:


delegate int op(int a, int b);


Ha osztályon kívül deklaráljuk akkor a namespace-n belül lehet elérni, de ha osztályon belül deklaráljuk (static kulcszó nem megengedett), akkor az osztály nevén keresztül kell hivatkozni rá. Ilyenkor tehát a delegate nem a példányhoz hanem az osztályhoz tartozik.


A delegate a System.Delegate osztályból származik, viszont ezt az öröklődést csak a rendszer és a compiler-ek használják, mert maga az osztály sealed, tehát a programozáskor mi csak a delegate kulcsszóval találkozunk.


Delegate példányosítása a következőképpen történik:


op x = new op(add);


Ahol az add metódusnak kompatibilisnak kell lennie a delegate-tel, különben fordítási hiba jelentkezik.


Egy delegate-hez újabb metódusokat is hozzá lehet adni:


x += new op(sub);

x = new op(add) + new op(sub);


stb. Ha új értéket rendelünk a delegate-hez, a régi elveszik, mint a referenciáknál megszokott.



A delegate meghívása egyszerűen így történik:


x(1, 2);


Ilyenkor az összes metódus végrehajtódik szépen sorban szinkronban ugyanazokkal a paraméterekkel. Ha egyik metódus változtat valamit, azt a változást a sorban utána következo metódusok mind látni fogják. A legutolsó visszatérési érték fogja képezni a delegate visszatérési értékét. Ha exception következik be a hívások során és az nincs elkapva a híváslistában meghívott metódusban, akkor ez az exception a delegate-et hívó kódban fog jelentkezni, mintha a delegate hívása során keletkezett volna. Az exception keletkezése után a híváslistában szereplő többi metódus nem kerül meghívásra.



A delegate-bol el is lehet venni metódusokat a - és a -= operátorral. Ha olyan metódust próbálunk elvenni belőle ami előzőleg nem volt benne, nem történik változás.


Delegate-ek összehasonlítása



A delegate-ekre alkalmazható az egyenlőséget ill. az egyenlőtlenséget vizsgáló összehasonlító operátor (==,!=). Két delegate példány egyenlőnek tekinthető, ha mindkettő értéke null, vagy egyenlő híváslistával bírnak, tehát a híváslistákban szereplő metódusok ugyanazokra a statikus metódusokra és ugyanazon objektumpéldányok ugyanazon metódusaira mutatnak ugyanabban a sorrendben a két delegate esetében. A delegate-ek típusa nem számít, tehát két különböző típusú delegate (amik persze ugyanazt a szignatúrát képviselik) lehet egyenlő.


Event-ek



A delegate int op(int a, int b) tehát egy delegate-nek a deklarációja és már látható volt, hogyan lehet lokális változóként létrehozni. (Miért van külön event, ezt nem értem...)


public event Click EventHandler;


Így néz ki tehát egy event deklarációja. Az event kulcsszó, aztán az esemény neve (érdemes valami igeként elnevezni) aztán a típusa. Így egy delegate típus könnyen hozzárendelhető többféle eseményhez.


A delegate-ek és event-ek kiválóan használhatóak eseménykezelésre, ahogy ez meg is van valósítva a System.EventHandler esetében. Egy gombhoz például a következőképpen rendelhetünk egy eseménykezelőt:


button.Click += new EventHandler(this.buttonClick);


Ahol a Click egy event amihez EventHandler típusú metódusokat lehet kapcsolni. Az osztályon belül így történik az eseménykezelő hívása:


Click(this, null);


A megfelelő paraméterekkel természetesen.


Java vs. .net



A java 1.4-gyel szemben gyakorlatilag beépítették a nyelvbe az eseménykezelést. A jávás megoldásnak több hátránya is van a C#-hoz képest:


  • Meg kell írni vagy örökölni kell valahonnan a listenereket kezelő metódusokat. (add, remove, notify) Az örökléssel az a probléma hogy kizárja a máshonnan való öröklést, a logika megírása pedig a plusz hibalehetőségekkel járó ujjgyakorlat. Elvileg lehet írni egyedi implementációkat (pl. Chain of Responsibility), de az alkalmazott listener minta nem egy olyan dolog amit túlzottan meg lehetne fűszerezni.

  • C#-ban meg lehet csinálni, hogy az eseménykezelő metódusok nem ugyanabban az osztályban foglalnak helyet. A java-ban a Listener interfészek csokorba gyűjtik a metódusokat, amiket egy adott osztálynak implementálnia kell. (Billentyűeseményeknél lehet hogy csak a leütésre akarok figyelni, de az alkalmazandó interfész miatt a felengedésre is meg kell csinálnom az üres metódust.) Erre be is vezették az Adapter-osztályokat, amikben eleve benne van az összes metódus üres törzzsel, de ezek szintén öröklődést igényelnek.

  • Kissé antipattern az a megoldás, hogy egy meglévő -valamilyen célra már felhasznált osztályt- hirtelen kiegészítünk valami Listener logikával, mert éppen kell. Pl. public class MyForm extends JPanel implements ActionListener. Így az osztályban definiált public void actionPerformed(ActionEvent) metódust boldog boldogtalan hívogathatja. C#-ban egyszerűen lehet csinálni olyan eseménykezelő metódusokat amelyek az adott osztályon kívülről nem hozzáférhetőek. Ehhez jávában befoglalt osztályok kellenek, amik persze hosszabb távon egész jól megszokhatóak.

  • Már meglévő osztályok metódusait is fel lehet használni, anélkül hogy bele kellene nyúlnunk a kódjukba és implementálnunk kellene mégegy interfészt. Ezt java-ban wrapper osztállyal lehet megvalósítani, ami delegálja a metódushívást.


A fentiek ellenére nem hiányolom a delegate-eket a java-ból, főleg a hozzáadásos, összehasonlításos dolgait. Gyakorlatilag kis kerülőúttal mindent meg lehet oldani delegate-ek nélkül, ezenkívül több kérdést felvet, mint amennyit megold.



Mire jó még?



Kb. ugyanarra mint a C-s, függvénypointerek, tehát callback függvények megjelölésére, függvénytáblák létrehozására, stb. Egy példa függvénytábla létrehozására és használatára:


public op[] operations = new operations[10];

...

op[0] = new op(add);

...

operations[0](2, 3);

2007-12-06

Bp nt mUp, December

Csak röviden tegnap estéről:

Simon - Humanoid Robot, Pécskai Balázs (logintsys)


A srác egy humanoid roboton dolgozgat pár éve. Először ablaktörlő motorokkal építette, majd szervómotorokkal, amik még mindig nem elég erősek (pl. nem triviális hogy a robot fel tud-e állni). Delphi-ben programozza és egy külső PC-ről irányítja. Egyelőre csak vezérlése van, nem beszélünk szabályozásról. (Nincs visszacsatolás.) A statikus járás az amikor előre minden ki van számolva, a dinamikus pedig amikor mozgás közben derülnek ki dolgok. Vagy talán a statikus járás az, amikor a mozgás minden pillanatában a robot statikus egyensúlyi helyzetben van? Ott ült a robot statikusan mikulás sapkában. Elvileg az előadások után mozgott is, de addigra már eljöttem.

Google Android, Kis Gergely (EU Edge)

Főleg a Linux szemszögéből foglalkoztak a témával. Annyival lettem okosabb, hogy az SDK-ban található emulátor hardvert emulál (biztos azért ilyen lassú) és emiatt viszonylag jó közelítést ad a valós hardverekhez. Láthattunk képen egy eszközt, amin valóban fut az Android.

Keressük a peer-t a szénakazalban, Tóth Benedek

A P2P hálózatok lelkivilágát érintette az előadó, de ennyibe nagyon kevés fér bele. Főleg a fájlcserélőkről volt szó. Vannak "node"-ok vagy pedig a szomszédaikat ismerik a kliensek.

Milenia Grafter - a 64 kilobyteos flash media server, Tóth Milán (milgra.com)

Flash animációk kiszolgálására alkalmas szerver jávában, 64k-ban. Megkérdeztem volna, hogy obfuszkálva-e ennyi és NIO-t használ-e, de végülis nem kérdeztem. A tippem: nem, igen. Közben elgondolkodtam hogy mennyivel fizetnék többet, hogy ha egy szerver alkalmazás 6.4mega helyett 64k lenne. Valószínűleg semennyivel. Továbbá nagy különbség, hogy valamit nem kell, vagy nem lehet konfigurálni. Megcsodálhattuk a MacBook grafikai lehetőségeit.

Bölcsészettudomány Informatikai Önálló Program, Vaskó Péter

Kicsit messziről indulva a szemantikus webnél kötöttünk ki. Dolgoznak egy megvalósításon, amivel január környékén fognak jelentkezni élőben, erről láthattunk egy 1 percet demót. Jó kis sodró előadás volt. (iGlue a projekt neve ha jól értelmeztem.)

A hacker kép alakulása Nick Haflingertől Neo-ig, Szedlák Ádám

Mielőtt napi 8-12 órát ültem gép előtt, már akkor is próbáltam úgy válogatni az elolvasandó könyveimet, hogy minél kevesebb szó legyen benne számítógépről, kibertérről, ezért igencsak műveletlen vagyok a műfajban. A srác szakdolgozatának ez a témája, úgyhogy ha egyszer publikus lesz, biztos sokan elolvassák majd.

A buzzword bingót nem követtem, de szépen csendben elvitték az 5 pólót.
Ikszelgetés helyett inkább forraltboroztam.

2007-12-02

JUnit 4

Haladni kell a korral (illetve nem szabad lemaradni), úgyhogy átnézegettem a JUnit 4 dolgait. Íme két hasznos olvasmány a témában -egyik az IBM-nél, másik DevX-en. A JUnit 4.X alapvetően a JUnit 3.X továbbgondolása olyan formában, ami a JDK5-ös új feature-jeit jól kihasználja. Jelenleg a 4.4-es verziónál tart, a www.junit.org -ról pedig letölthető (158kbyte a jar forrás nélkül). Mi változott?

  • Legalább 5-ös Java kell hozzá (bármilyen meglepő).
  • A package megváltozott junit.framework-ről org.junit-ra.
  • @Test annotációk használata névkonvenciók helyett. Továbbra is publicnak ill. void-nak kell lennie a visszatérési értékének és nem lehetnek paraméterei. Ha ezt nem tartjuk be, az alábbi kivételekre számíthatunk:
    java.lang.Exception: Method xxx should have no parameters
    java.lang.Exception: Method xxx should be void

  • Statikus import használata: import static org.junit.Assert.assertEquals; Így a kódban Assert.assertEquals(...); helyett írhatunk assertEquals(...);-t. (Persze JUnit 3.8-nál is használhatjuk a statikus import funkciót, ha 5-ös Javank van.)
  • Nem kell kiterjeszteni a TestCase osztályt, így lehetőve válik a protected metódusok tesztelése azáltal, hogy a teszt-osztállyal a tesztelendő osztályt terjesztjük ki.
  • setUp() metódus helyett a @Before annotációt használjuk, akár többet is. Kérdés hogy ezek milyen sorrendben hajtódnak végre - erre a sorrendre nincs explicit szabály, ezért véletlenszerűnek vehető.
  • A tearDown() párja pedig az @After, amiből szintén lehet több.
  • Ősosztályokban nem kell explicite meghívni a setUp()==@Before illetve tearDown()==@After metódusokat, mert azok hívása automatikusan történik: Először az ősosztályban lévő @Before-k hívódnak, aztán a leszármazott osztályokon belüliek. @After-nél pont fordítva, először a leszármazottaké, majd az ősosztályban lévők.
  • A @Before és @After minden teszteset előtt és után hívódik, akárcsak a setUp() és tearDown(). Lehetőség van @BeforeClass és @AfterClass metódusok megadására, amelyek az osztályban lévő összes teszteset előtt illetve után hívódnak meg. Ilyen feature JUnit 3.X-ben nincs.
  • Ha egy osztályban nincs egyetlen @Test annotáció sem, hibát fogunk kapni.
  • JUnit 3.X-ben a kivételek ellenőrzése úgy történt, hogy a catch blokkba beírtunk egy assert-et. Itt az annotációba írhatjuk be, hogy milyen kivételt várunk: @Test(expected=ArithmeticException.class). Ha nem dobódik kivétel, vagy más kivétel dobódik, a teszt elbukik. Ha további ellenőrzésekre van szükség a kivétel paramétereivel és szövegével kapcsolatban, továbbra is a try-catch módszert kell használni.
  • Ha valami tesztet mégsem akarunk futtatni, @Ignore annotációval kiüthetjük. (A @Test annotáció megmaradhat, elé és mögé is beírhatjuk az @Ignore-et.) Megadható neki egy String típusú paraméter, hogy miért hagyjuk ki. A teszt nem fog lefutni, viszont jelezve lesz hogy ki lett hagyva. Az egész osztályra is modhatunk @Ignore-t, de vigyázat, az nem ugyanaz mintha a metódusokra mondanánk, mert az utóbbi esetben még a @AfterClass és @BeforeClass lefut.
  • Általam nagyra értékelt feature, hogy a teszteseteknek timeout adható így: @Test(timeout=500) Milliszekundummal.
  • Van új assert, ami objektum tömböket hasonlít össze, viszont sok (12) assert metódus eltűnt az autoboxing feature miatt. Mindegyik az assertXXX(Object, Object)-et használja. Illetve a DevX szerint így történt, de én furcsa módon elérem ezeket a mindenféle paraméterű assert metódusokat továbbra is a TestCase osztályban.
  • Lehet használni az 1.4-ben bevezetett assert kulcsszót is, de akkor a tesztek futtatásánál meg kell adni az -ea kapcsolót a JVM-nek, különben az assert-ek nem kerülnek kiértékelésre. Ekkor viszont a JUnit-os assertException helyett a nyelvi java.lang.AssertionError-t fogjuk megkapni adott esetben.
  • JUnit4-ben nincs suite()
    metódus. Ehelyett lehet csinálni egy üres osztályt, aminek annotációban adjuk meg, hogy milyen más osztályokat futtasson:
    @RunWith(Suite.class)
    @Suite.SuiteClasses({My1Test.class, My2Test.class, My2Test.class})
    public class AllTests {
    }

  • @RunWith osztály annotáció megadásával saját futtatót is megadhatunk a tesztesetek futtatásához. Pl. a org.junit.runners.Parameterized-et, ami általunk megadott paraméterekkel küldi meg a tesztet. Egy @Parameters annotációval felturbózott publikus statikus metódusra van még szükség, ami egy Collection-t ad vissza és egy publikus konstruktorra, ami a Collection-ben lévő elemeket tudja fogadni. Pl. ha a Collection integer párokat tartalmaz, akkor a konstruktornak is két integer-t kell fogadnia. A futtató végigyalogol a Collection-ön, minden egyes esetben meghívja a konstruktort, majd végig a @Test-tel ellátott metódusokat. A DevX-es cikk harmadik oldalán van erről egy példa. (Listing 2.)
  • Ha ezt adjuk meg: @RunWith(TestClassRunner.class) ez nem okoz semmi különbséget ahhoz képest hogy nem adunk meg semmit, mivel a TestClassRunner a default runner.
  • JUnit4 nem különbözteti meg az előre várt hibákat a rosszul megírt tesztesetekből származő nem várt hibáktól -ez visszalépés! Ergó egy tesztesetnek a JUnit3.8 (passed/error/failure) eseteihez képest csak (passed/failure/(ignored)) esetei vannak.
  • java –ea org.junit.runner.JUnitCore paranccsal lehet futtatni a teszteket. Tud futtatni 3.8-as teszteket is a DevX cikk szerint, de a gyakorlatban 3.8-as tesztekben kell egy kis módosítás:
    public static junit.framework.Test suite() {
    return new JUnit4TestAdapter(MyTestClass.class);

    4.0-ás tesztek természetesen nem mennek 3.8-on. Ha régi és új JUnit is van a classpath-on, akkor pedig lehet hogy összebalhéznak. Nekem legalábbis securityException-nel leállt.
  • Hallottam már egy úgynevezett assertThat metódusról, elvileg van ilyen 4.4 óta, de még a junit.org-on aktuálisan fenn lévő javadoc-ban nem láttam.
  • Eclipse 3.3-ban van JUnit4.4 és JUnit3.8 támogatás.
  • Olyan szinten el*ja ez a szerkesztő a betűtípusokat és a formázást, hogy öröm nézni. Szörnyű. Update 2007.12.04: Megjavítottam direkt html szerkesztéssel.
Update 2008.11.20: JUnit3-ban szépen le lehetett kérdezni az éppen végrehajtott teszt nevét az örökölt getName() metódussal. Ez a lehetőség JUnit4-ben szépen elveszett. A mostani pillanatban nem tudok rá beépített megoldást, de ezt találtam a témában. Csúnya hosszú kód, nem próbáltam ki.