Személyes eszközök
Keresés

 

A InfoWiki wikiből


A probléma

Az alapvető típusok (int, double, char, bool, stb) a programok és a további típusok alapvető építőkövei. Mivel nyelvi alaptípusok - a program szövegébe épített értékeiket (literálok alakja) a fordítóprogram felismeri.

Gyakori azonban az az eset, amikor ezen típusok által lefedett értékek halmaza túlságosan bő. Tegyük fel például, hogy egy énekkari kórustagok hangjának jellegét kívánjuk (sok más adatuk mellett) programban tárolni. A hangzástípusok csakis a bariton, alt, tenor, mezoszoprán, szoprán értékek közül kerülhet ki. Nincs olyan típus a C#-ban (és más nyelven sem valószínű), amely csakis ilyen értékeket fogad el.

Ilyenkor a szokásos eljárás, hogy az egyes lehetséges értékeket kódoljuk. Pl. bariton lesz a 0, alt az 1, stb. A típus amire szükségünk lesz, valamely egész szám típus, hiszen ebben tudjuk majd tárolni a tényleges értékek kódjait.

Egyrészt vegyük észre, hogy máris kompromisszumot kötöttünk - nem a tényleges értékeket tároljuk, hanem azok kódjait. Ezen még tudunk segíteni valamelyest a konstansok használatával (bár ki fog derülni, hogy nem sokat):

const int bariton = 0;
const int alt = 1;
const int tenor = 2;
const int mezoszopran = 3;
const int szopran = 4;

Ez máris mutatja, hogy amennyiben egy konkrét énekes konkrét énekhangját akarnánk tárolni, akkor az int típust (vagy valamelyik egész szám típust) tudnánk használni:

int enrico_caruso_hangja = tenor;

Vegyük azonban észre, hogy a fenti kód az alábbi módon is írható:

int enrico_caruso_hangja = 2;

Valamint vegyük észre, hogy az alábbi kód sem jelez szintaktikai hibát:

int enrico_caruso_hangja = -42312;

A szóban forgó változó fizikailag képes ezen lehetetlen érték tárolására, a fordítóprogramnak meg nem adtunk semmilyen támpontot hogy ellenőrízhesse, és kizárhassa ezt a logikailag hibás értéket. Szűkíthetnénk a lehetséges értékek halmazát az által pl, hogy nem az int, hanem egy kisebb típust használunk:


byte enrico_caruso_hangja = -42312; // hibás

A byte típus nem enged meg negatív értékeket, ez okozza a fenti kód szintaktikai hibáját, és nem az, hogy nincs ilyen hangfekvés-kód. A fordítónak semmilyen ötlete nincs arról, hogy mik a lehetséges hangfekvés kódok, e miatt a byte esetén is működne az alábbi kód:

byte enrico_caruso_hangja = 178;

Tovább bonyolítja a helyzetet, hogy programunkban más jellegű konstansokat is felvehetnénk:

// termék minőségek
const int megfelelt = 0;
const int turheto = 1;
const int jo = 2;
const int kivalo = 3;

Mivel ezek mind-mind egy egész számot neveznek el, a hangzások kódolása is egész számok segítségével történik, ezért az alábbi kódok nem hibásak a fordító szerint:

byte enrico_caruso_hangja = megfelelt; // gyak. a bariton kódja
if (enrico_caruso_hangja==turheto) {...} // hangja alt ??
while (enrico_caruso_hangja>0) {...} // ebből mi akar lenni??

Tehát ezen kódolásos megoldás fizikailag megoldja a gondjainkat, a programokban ezen a módon végül is tudjuk tárolni az énekesek hanját (ha nem rontjuk el), de a programunkban ezt a dolgot csakis a mi jó szándékunk tartja életben. A fordítóprogram nem segít nekünk ellenőrízni a kódunk helyességét, nem szól, ha hangfekvés jellegű változóinkba illegális értéket akarunk helyezni, és a programunk szövegének utólagos (vagy mások által történő) olvasgatása során a típusinformáció nem ad támpontot a változók szerepének megértésében.

Felsorolt típus

Amire szükségünk lenne, hogy a fordítóprogramnak el tudjuk magyarázni, hogy mire is gondolunk mi. Erre van mód az enum kulcsszó segítségével:

enum hangfekvesek { bariton, alt, tenor, szopran, mezoszopran };

A fenti sorral megadhatjuk, hogy hangfekvesek alatt mi ezt az öt lehetséges értéket értjük a továbbiakban. A fenti kis kódrészben az enum szó foglalt szó, a többi azonban azonosítónak minősül, tehát mind a hangfekvesek szó, mind a bariton, alt stb szavak mindegyike az azonosító névképzési szabályainak megfelelően képzendő (nincs benne szóköz, számjeggyel nem kezdődhet, ékezetes betűk használata elvileg nem tilos, stb).

A fenti kis kódban a hangfekvesek azonosító a továbbiakban egy új típust azonosít, a kapcsos zárójelbeli felsorolás ezen típusba tartozó értékeket ad meg. Ez a konstrukció nagyon hasonlít a matematikai halmaz esetéhez, ahol is a halmazbeli elemeket (véletlenül) szintén a kapcsos zárójelbe szoktuk felsorolni.

Mivel a hangfekvesek itt egy új típus lesz, ezért szabad ilyen típusú változót deklarálni:

hangfekvesek enrico_caruso_hangja;

Ezen típusú változó (mint látni fogjuk) csakis a fenti felsorolásban megadott értékek valamelyikét képes (majd) fölvenni.

Tárolási jellemzők

Hány byte-ba kerül nekünk egy felsorolás alaptípusú változó? A szokásos 4 byte-os tipp megint bejön. Oka: a fenti változó valójában a háttérben int típusú lesz, és egy int mindíg 4 byte.

A háttérbeli kódolást ilyenkor a fordítóprogram végzi el helyettünk, ennek tényét maximálisan elrejtve előlünk. Némi kutakodás és gondolkodás után kideríthető, hogy a fenti kis enum felírás valójában majdnem teljesen egyenértékű a mi kódolási és konstansos kísérletünk eredményével.

Ennek megfelelően ami történik, az nem más, mint 5 darab konstans generálása a fordítóprogrammal, a fenti esetben a felsorolásbeli bariton kódja ténylegesen a nulla lesz, a alt kódja valójában egy, stb.

Kódolás befolyásolása

Néha szükség van arra, hogy ne az automatikus kódkiosztást használjuk, hanem fizikailag adhassuk meg az egyes értékek kódjait. Ezt a felsorolás belsejébe elhelyezett értékmegadásokkal tudjuk befolyásolni:

enum hangfekvesek 
{ bariton=10, 
  alt, 
  tenor=20, 
  mezoszopran,
  szopran=40
};

A fenti felírás eredményeként az alábbi kódolást írtuk elő:

bariton     = 10
alt         = 11
tenor       = 20
mezoszopran = 21
szopran     = 40

Vagyis ahol mi állítottuk be fixen a kódolási értéket, ott az kerül felhasználásra, egyébként pedig az automatikus kódkiosztás során a következő szabad kódot adja a rendszer az egyes lehetséges értékeknek.

A változó használata

Egy ilyen enum változóba tudunk értéket elhelyezni, de csak a lehetésges értékek közül. A lehetséges értékek neveivel tudunk rájuk hivatkozni, mintha ők konstansok lennének. De ezen konstansok minősített konstansok, vagyis nem csak a nevüket kell megadni, hanem azt is, melyik enumbeli melyik értékre gondolunk:

hangfekvesek enrico_caruso_hangja = hangfekvesek.tenor;

Egyes programozási nyelvekben ilyenkor nem kell már használni az enum típusnevét, csak a szóban forgó érték nevét. De ez félreértésekre is okot adhat. Például előfordulhat, hogy a szóban forgó azonosító más enumban is szerepel (és valószínűleg más értékkel):

enum hangfekvesek { bariton, alt, tenor, szopran, mezoszopran };
enum spec_billentyuk {alt, control, shift };

Mi történne ekkor, ha csak az azonosítót adnánk meg:

hangfekvesek kathleen_ferrier_hangja = alt;

Mivel az alt azonosító szerepel a hangfekvesek felsorolásban (1 értéket képviselve), és a spec_billentyuk felsorolásban is (nulla értéket képviselve) átérezhetjük a fordítóprogram zavarát. Ezt tudjuk fokozni, ha bosszantásképpen még definiálunk egy direkte ilyen nevű tényleges (egyszerű) konstanst is:

enum hangfekvesek { bariton, alt, tenor, szopran, mezoszopran };
enum spec_billentyuk {alt, control, shift };
const alt = 322;
...
hangfekvesek kathleen_ferrier_hangja = alt;

Ezért a felsorolással kapcsolatos műveletek esetén mindíg meg kell adni a felsorolás típusának nevét is:

hangfekvesek enrico_caruso_hangja = hangfekvesek.tenor;
hangfekvesek kathleen_ferrier_hangja = hangfekvesek.alt;

A fordítóprogram a típust mindíg nagyon komolyan veszi. Bár a háttérbeli működés szempontjából akár kivitelezhető is lenne, de logikailag ellentmondásos, ezért tiltott pl. az alábbi értékadás:

hangfekvesek tarja_turunen = spec_billentyuk.shift; // hibás

Nem csak értéket lehet benne elhelyezni ilyen direkt módon, az egyforma felsorolás típusú változók közötti értékadás is működik (az értékadás működéséhez a két oldal típusainak megfelelősége szükséges, ami jelen esetben teljesül):

hangfekvesek enrico_caruso_hangja = hangfekvesek.tenor;
hangfekvesek placido_domingo_hangja = enrico_caruso_hangja;

Valamint feltételes elágazásokban is meg lehet vizsgálni a bennük lévő értéket:

if (enrico_caruso_hangja == hangfekvesek.szopran)  
  Console.WriteLine("höhh???");

Sőt, a switch is kifejezetten szereti ezt az esetet:

switch(enrico_caruso_hangja)
{
  case hangfekvesek.tenor:
    Console.WriteLine("ez hihető");
	break;
  case hangfekvesek.szopran:
    Console.WriteLine("höhh???");
	break;
...
}

Egyéb lehetőségek

Nem csak ilyen típusú változót, de ilyen típusú függvényparamétert is lehet készíteni:

static List<hangfekvesek> korusTagok = new List<hangfekvesek>();
 
 static void korusKezel(hangfekvesek x)
 {
   korusTagok.Add(x);
 }

A fenti listát akár foreach segítségével is fel lehet dolgozni:

int db = 0;
 foreach(hangfekvesek p in korusTagok)
   if (p==hangfekvesek.szopran)
     db++;

Kiírás képernyőre

Felsorolás típusú változók kiírása a képernyőre kellemes meglepetésekkel teli. Ugyanis intelligens módon az értékének megfelelő azonosító íródik ki a képernyőre:

namespace enum_proba
{
    class Program
    {
        enum hangfekvesek { bariton, alt, tenor, szopran, mezoszopran };
        static void Main(string[] args)
        {
            hangfekvesek caruso = hangfekvesek.bariton;
            Console.WriteLine("Caruso hangfekvése {0}",caruso);
            Console.ReadLine();
        }
    }
}

Bekérés billentyűzetről

Amivel gond van például, az az adatbekérés billentyűzetről. Megszoktuk, hogy string-ek bekérése természetesen egyszerű, hiszen a Console.ReadLine() alapvetően egy string-et ad meg. Ha más típusú adatot kérünk be, akkor valamiféle Parse függvényre kell számítanunk.

Ezen a ponton máris gyanakodhatunk, hogy ilyen Parse() most nem lesz, hiszen a C# és a Visual Studio tervezői nyilván nem tudhatták előre, milyen enum-t álmodunk meg, így nem készíthettek előre hozzá Parse()-t. Később kiderül, hogy tévedtünk, de addig is nézzünk alternatív megoldások után:

hangfekvesek caruso;
Console.Write("Kérem mr. Caruso hangfekvését:");
string s = Console.ReadLine();
 
if (s=="bariton") caruso = hangfekvesek.bariton;
elseif (s=="alt") caruso = hangfekvesek.alt;
elseif (s=="tenor") caruso = hangfekvesek.tenor;
elseif (s=="szopran") caruso = hangfekvesek.mezoszopran;
elseif (s=="mezoszopran") caruso = hangfekvesek.szopran;
else Console.WriteLine("hiba");

Egy fokkal elegánsabb ezt switch-el írni:

hangfekvesek caruso;
Console.Write("Kérem mr. Caruso hangfekvését:");
string s = Console.ReadLine();
switch(s)
{
  case "bariton": caruso = hangfekvesek.bariton; break;
  case "alt": caruso = hangfekvesek.alt;break;
  case "tenor") caruso = hangfekvesek.tenor;break;
  case "szopran") caruso = hangfekvesek.mezoszopran;break;
  case "mezoszopran") caruso = hangfekvesek.szopran;break;
  default: Console.WriteLine("hiba");break;
}

A fenti két módszernek több hibája is van:

  • ha a felsorolás újabb elemmel bővül, bővíteni kell az elágazások számát is
  • ha a felsorolásból elemet veszünk ki, ki kell venni az elágazásból is
  • sok felsorolás esetén ez a megoldás sok sorból áll, lassú, nehézkes

Mégiscsak kellene az a Parse() ...


Parse

Az enum alapjaiból nagyon sok típus nőhet ki, mindenki egyedi felsorolás listával dolgozik. Mindegyikhez Parse()-t gyártani előre nyilván nem lehetséges, de van egy univerzális Parse(). Ez nyilván bonyolultabb lesz, mint a többi megszokott Parse, mivel ennek nem csak a parsolni kívánt stringet kell megismernie, de a lehetséges értékek listáját is meg kell kapnia. Az univerzális parse neve (nem túl meglepően) Enum.Parse(...).

Console.Write("Kérem mr. Caruso hangfekvését:");
string s = Console.ReadLine();
hangfekvesek caruse = (hangfekvesek)Enum.Parse(typeof(hangfekvesek),s);


Az Enum.Parse első paramétere maga az elfogadható elemek listája. Hogy ezt ne kelljen ezen a ponton újra felsorolni, ezért ravasz trükkel helyette inkább a szóban forgó típust adjuk oda (arra biztatván az Enum.Parse-t, hogy ennek járjon maga utána innentől kezdve). Egy típust paraméterként átadni alapvetően nem lehet (a paraméterek átadásánál mindíg valami értéket kell átadni, legyen az bármilyen nyakatekert. Ezért a typeof() operátort használjuk (bár alakjából ítélve ez inkább függvény, de gyökerei szerint őt typeof operátortnak hívjuk). A typeof operátort elkészíti a típus leírását, és a leírás memóriacímét adja oda az enum.parse-nak. Ez nyakatekert, nem is annyira érdekes ezen a ponton, de technikailag ez egy problémás rész, és így sikerült orvosolni.

A második paraméter a szóban forgó string, amelyet parsolni kell, ez már normális.

Az értékadás működéséhez szükséges, hogy a bal és jobb oldal típusa legyen egymással valamiféle barátságos viszonyban. Bal oldalon egy hangfekvesek típusú változó várakozik, a jobb oldal típusának is ehhez kell igazodnia. Az Enum.Parse azonban egyrészt univerzális parse, visszatérési típusát a legnagyobb jószándékkal sem tervezhették ilyen típusúra, hiszen a C# csapat nem is sejtette, mire lesz majd nálunk használva egy szép őszi délelőtt. Ezért a visszatérési típusát erős típuskényszerítéssel hangfekvesek típusra kell még kényszeríteni (módosítani), hogy az értékadó utasítás is megbékéljen.

Az Enum.Parse egyébként a string tényleges tartalmát, és a lehetséges értékek listáját aránylag szigorúan veti össze. Érzékeny a kisbetűkre, ékezetes betűkre, szóközökre, stb. A kisbetűs érzékenységet kikapcsoltathathuk egy plusz true paraméter beiktatásával:

hangfekvesek caruse = (hangfekvesek)Enum.Parse(typeof(hangfekvesek),s,true);


Konvertálások, típuskényszerítés

A bekérésnél kihasználhatjuk azt is, hogy az enum alapú változók valójában int-ek. Az intekből az enum irányába (és vissza) létezik típuskényszerítés lehetősége:

Console.Write("Kérem mr. Caruso hangfekvésének kódját");
int x = int.Parse( Console.ReadLine() );
hangfekvesek caruso = (hangfekvesek)x;

A fenti esetben a hangfekvés kódját kell beolvasni (reméljük a program kezelője tudja mit ír be), majd a számkódból előállítjuk annak felsorolásbeli párját. Feltételetzve, hogy a tenor kódja 20, ezen érték (20) beírása, int.Parse feldolgozása, és a hangfekvesek típusra kényszerítése után a caruso változóba be is kerül a hangfekvesek.tenor érték. Nyilván arra nagyon kell ügyelnünk, hogy olyan int értékek konvertálása, amelynek nincs felsorolásbeli párja - futási hibát fog okozni. Csodák nincsenek.


Hasonlóan, az int-re alakítás a másik irányban is működik:

hangfekvesek caruso = hangfekvesek.tenor;
int x = (int)caruso;
Console.WriteLine(x);

A fenti kód lefutása után a képernyőn a 20 érték jelenik meg (a hangfekvesek.tenor azonosító kódértéke).

A fenti int-es konvertálások akkor nagyon fontosak, ha az adatainkat valamiféle külső adattároló felé kívánjuk továbbítani, aki nem képes átvenni a mi típusrendszerünket. Ilyen lehet egy SQL szerver (elég gyakori eset), de egy bináris file, néha az xml is problémás tud lenni. Ez esetekben az int-é alakítás, és visszalakítás lehet az egyetlen megoldás.

Hernyák Zoltán
A lap eredeti címe: „http://wiki.ektf.hu/wiki/Mp1/page560
Nézetek
nincs sb_3.89.72.221 cikk