fel
le

Operátorok

Az operátorok segítenek nekünk a kifejezések készítésében. A kifejezések minden programozási nyelven fontos építőkövei a programoknak.

Közismert, hogy az operátoroknak jeleik vannak. Jellemzően az operátorok egy karakteres jelekkel rendelkeznek (pl '+', '*', stb.), néha kétkarakteresek ('!=', '==', stb.). Ugyanakkor az operátor-jeleknek több jelentésük is van. Az összeadás jelnek más-más értelme van pl. két egész szám között, mint két string között.

Amikor OOP szemlélettel programozunk, gyakorlatilag típusokat (osztályokat) készítünk. A saját típusokhoz adatokat rendelünk (mezők), és műveleteket is kódolunk le (metódusok). A műveleteket azonban nem csak metódusokon keresztül kódolhatjuk le, hanem operátorokon keresztül is.

Vegyük például a string típus esetén az 'Equals' metódust. Ennek segítségével eldönthetjük (megvizsgálhatjuk), hogy két string egyenlő-e.

string s = Console.ReadLine();
if (s.Equals("alma")) Console.WriteLine("ez ALMA :)");
 

Ennél szebb, könnyebben olvasható megoldás, ha használjuk az '==' operátort:

if (s=="alma") Console.WriteLine("ez ALMA :)");
 

Operátorok kiterjeszthetősége

Az operátorok a C# alapvető típusai esetén eleve rendelkezésre állnak.A szám típusok esetén gyakorlatilag minden jellemző operátort megvan. A string esetén ugyan zavaróan hiányoznak az '<', '<=', '>', '>=' operátorok, de jellemzően azért a megszokott operátorok a megszokott környezetben rendelkezésre állnak.

De mi a helyzet a saját típusaink (osztályok) esetén? Lehetséges-e a saját osztályainkra valamely operátor jelentését kiterjeszteni?

Ebből a szempontból háromféle OOP programozási nyelvet ismerünk:

  • nem támogatja az operátorok jelentésének kiterjesztését
  • a nyelvben új operátort bevezetni nem lehet, de a meglévő operátorok jelentését ki lehet terjeszteni az új típusokra is
  • a nyelvben akár új operátorokat is lehet definiálni, és hozzárendelni az új típusokhoz

A C# a középső szintet támogatja, vagyis a nyelvben eleve léteznek operátorok, melyek operandusainak száma, prioritása nem megváltoztatható, de jelentésük kiterjeszthető az új típusok esetére is.

Példa egyoperandusú operátorra (Verem, --)

A C#-ban az alábbi egyoperandusú (unáris) operátorok vannak definiálva: '+', '-', '!', '~', '++', '--'.

A fent felsorolt operátorok jelentése kiterjeszthető saját típusok esetére is. Az operátorokat mint osztályszintű függvényt kell az adott osztályban megírni:

class Verem
{
    ...
    public static Verem operator --(Verem v)
    {
        if (v.veremMutato>0) v.veremMutato=v.veremMutato-1;
        return v;
    }
}
 

A fenti példa mutatja, hogy a saját 'Verem' osztály példányaira alkalmazható a '--' operátor. Ez jellemzően valamilyen formájú csökkentést jelent. Jelen esetben azt jelenti, hogy csökkentsük a veremmutató értékét, amely gyakorlatilag a verem legfelső elemének eltávolítását jelöli.

Verem v = new Verem();
v.Berak(10);
v.Berak('Hello');
while (!v.Teli)
  v--;
 

Egyoperandusú operátorok készítésének szabályai

Egyoperandusú operátor készítése esetén az alábbi szabályokat kell betartani:

  • a függvény visszatérési típusa kötelezően meg kell egyezzen az osztály típusával is
  • az operátor valamely egyoperandusú operátor kell legyen ('+', '-', '!', '~', '++', '--')
  • a függvénynek csak egy darab paramétere lehet
  • ezen egy darab paraméter típusa kötelezően meg kell egyezzen az osztály típusával

Mivel az operátor egy osztályszintű függvény alakban van megvalósítva, a példányt, amelyen dolgoznia kell, paraméterként kell megkapnia. Értelemszerűen a példány típusa határozza meg az operátor jelentését, vagyis egy 'Verem' típusú példányra alkalmazott '--' operátor kódja magában a 'Verem' osztály belsejében kell legyen. Ezért a paraméter típusa meg kell egyezzen a befoglaló osztály típusával. A fordító nem fogja végigszkennelni az osszes osztályt, hogy melyiknek a belsejében van definiálva olyan '--' operátor, amely 'Verem' típusra működik. Ilyet csakis maga a 'Verem' osztály tartalmazhat.

Másrészt, ha ez a megkötés nem lenne, akkor több problémával is szembe kellene nézni. A string típusra (vesszőparipám) nincs definiálva a '<=' operátor. Ezt az operátort utólag a nyelvbe bevezetni már sosem lehet, mivel nem lehet más osztály belsejébe ezt definiálni:

// !! HIBÁS PÉLDA !!
class sajatOsztaly
{
    public static string operator <=(string s)
    {
      ...
    }
}
 

Ez meggátolja a programozókat abban, hogy kifejleszthessék a hiányzó operátorokat, de meggátolja azt is, hogy a programozók akár több osztály belsejébe is belerakhassák azt, és a fordítóprogramot töprengésre késztessék, alkalmazása esetén melyik osztálybeli '<=' operátort is érti alatta a programozó.

A függvénynek jogában áll módosítást elvégezni az eredeti példányon, és a végén ezen eredeti (de már módosított tartalmú) példány referenciáját visszaadni, illetve új példányt készíteni, elvégezni a módosításokat, és ezen új példány referenciáját visszaadni. A fenti esetben az előbbit választottuk.

A fenti esetben az operátor használatát a fordítóprogram az alábbi kódra fordítja át:

// eredeti kódsor
v--;

// fordító a háttérben ezt érti alatta
v = Verem.--(v);
 

Példa egyoperandusú operátorra (String, ~)

Például string típus esetén ugyan nincs értelmezve a '~' operátor, de értelmezhetnék azt az alábbi módon (a '~' operátor általában valamilyen szintű tagadást jelképez): a string kisbetűit alakítsuk nagybetűre, a nagybetűket kisbetűre. Ezen függvényt nem lenne szerencsés úgy megoldani, hogy nem képezünk új string-példányt, mivel ekkor az egyéb lehetséges string típusú változók, akik szintén ugyanerre a stringre mutatnak - megváltoznának:

// !! EZ CSAK EGY ELMÉLETILEG LÉTEZŐ PÉLDA !!
string s = "Hello World";
string m = s;
~s;
Console.WriteLine("{0} és {1}",s,m);
 

Ha helyben alakítanánk át az 's' string-példány karaktereit, akkor az 'm' példány karakterei is átalakulnának. Annak, hogy az 'm' példány karakterei is megváltoznak - viszont nincs semmi jele a fenti kódban, így ez erősen értelemzavaró, illetve később nehezen kideríthető programhibát okozhat. Sok debuggolás előjele érezhető ezen megoldás során.

Ezért a fenti esetet javasolt új példány készítésével elvégezni:

// !! EZ CSAK EGY ELMÉLETILEG LÉTEZŐ OPERÁTOR !!
class string
{
 public static string operator ~(string s)
 {
    char[] ss = s.ToCharArray();
    for(int i=0;i<ss.Length;i++)
      if (Char.IsLower(ss[i])) ss[i]=Char.ToUpper(ss[i]);
      else if (Char.IsUpper(ss[i])) ss[i]=Char.ToLower(ss[i]);
    return new String(ss);
 }
}
 

Példa egyoperandusú operátorra (Teglalap, ++)

Nézzünk egy másik példát:

class Teglalap
{
public static Teglalap operator ++(Teglalap t)
{
  t.xKoord--;
  t.yKoord--;
  t.szelesseg += 2;
  t.magassag += 2;
  return t;
}
}
 

A 'Teglalap' osztály példányaira alkalmazható '++' operátor jelentése szerint a téglalap minden irányban megnő. Alkalmazása:

Teglalap t = new Teglalap(10,10,120,140);
t++;
Console.WriteLine("X={0}, Y={1}, Sz={2}, M={3}",t.xKoord,t.yKoord, t.szelesseg, t.magassag);
 

A fenti esetben az operátor alkalmazását a fordító az alábbi kódra fordítja át:

// eredeti kód
t++;

// a fordító az alábbit érti alakja
t = Teglalap.--(t);
 

Példa kétoperandusú operátorra (Verem, <=)

A C#-ban az alábbi kétoperandusú (bináris) operátorok vannak definiálva: '+', '-', '*', '/', '%', '&&', '||', '^', '<', '>', '==', '!=', '>=', '<=', '<', '>'.

Értelmezzük a verem és egy egész szám esetén a '<=' operátort oly módon, hogy legyen az operátor értéke logikai igaz érték, ha a veremben lévő minden egyes elem kisebb, vagy egyenlő a szóban forgó egész számnál. Ellenkező esetben az operátor értéke legyen logikai hamis érték:

class Verem
{
public static bool operator <= (Verem v, int x)
{
  foreach(Object o in veremPuffer)
  {
    if (o is Int32)
      if ((Int32)o)>x) return false;
  }
  return true;
}
}
 

A fenti esetben megvizsgáljuk, hogy a veremben lévő elemek közül melyek az int típusúak, azokra elvégezzük az ellenőrzést. Amennyiben találunk a szóban forgó 'x' elemnél nagyobbat - úgy nem minden elemkisebb vagy egyenlő, tehát a visszatérési érték legyen 'false'. Ha egy ilyen eset sem fordult elő, akkor a visszatérési értékünk legyen 'true'.

Használata például:

Verem v = new Verem();
// ... a 'v' példány feltöltése elemekkel
if (v <= 12) Console.WriteLine("Minden elem kisebb, vagy egyenlő mint 12");
else Console.WriteLine("Van olyan elem a veremben, amely nagyobb mint 12");
 

Kétoperandusú operátorok készítésének szabályai

A kétoperandusú operátorok is osztályszintű függvények formájában vannak megvalósítva.

  • a függvény visszatérési típusa tetszőleges típusú lehet
  • az operátor jel az alábbiak egyike kell legyen: '+', '-', '*', '/', '%', '&&', '||', '^', '<', '>', '==', '!=', '>=', '<=', '<', '>'.
  • a függvénynek két darab paramétere kell legyen (az operátor ugyanis kétoperandusú)
  • a két paraméter közül legalább az egyiknek a befoglaló típusnak kell lennie (meg kell egyezzen a tartalmazó osztály típusával)

Példa kétoperandusú operátorra (Halmaz, +, +)

Lehetséges másik példa, amikor két 'Halmaz' típusú példányra alkalmazzuk a '+' operátort. Jelentse ez a két halmaz közötti unió műveletet. Ennek megfelelően ennek eredménye egy újabb 'Halmaz' példány legyen:

class Halmaz
{
    public static Halmaz operator + (Halmaz a, Halmaz b)
    {
      ...
    }
}
 

Újabb lehetséges példa lehet a '+' operátor értelmezése egy 'Halmaz' és egy 'String' között jelentse az, hogy az elemet fel kell venni a 'Halmaz'-ba (amennyiben az még nem szerepelt volna benne). Ez utóbbi korlátozás értelemszerű egy 'Halmaz' típus esetén, hiszen egy halmazban egy elem vagy szerepel vagy nem, többször nem fordulhat elő benne.

class Halmaz
{
    public static Halmaz operator + (Halmaz a, String s)
    {
      ...
    }
}
 

Mindkét esetben meg kell fontolni, hogy új 'Halmaz' példányt hozzunk-e létre, vagy az eredeti példányon végezzük el a módosításokat. Mindkét esetre vannak érvek, és ellenérvek is.

Operátor overloading

Vegyük észre, hogy az előző példában a 'Halmaz' típusra a '+' operátor kétféle jelentését is megadtuk. Első esetben 'Halmaz' és 'Halmaz' között, a második esetben 'Halmaz' és 'string' között. Mindaddig ez rendben van, amíg az overloading szabály ezt lehetővé teszi, vagyis az 'operator +' nevű függvény-változatoknak más-más a paraméterezésük.

Mivel a két paraméternek eltérő típusa megengedett, így a második esetben ('Halmaz'+'String') a függvényt írhatjuk a 'Halmaz' osztály belsejébe, vagy írhatjuk a 'String' osztály belsejébe is.

Típuskonverziós operátorok

Az operátorok harmadik nagy csoportját képezik a típuskonverziós operátorok. Ennek lényege, hogy valamely változóban lévő érték típusát megváltoztassuk. Vagyis típuskonverzió során sosem a változó típusát változtatjuk meg, hanem a benne lévő értéket.

Fontos azt is tudni, hogy ez csak egy egyszeri művelet. Ha egy kifejezésben egy változó kétszer is szerepelne, akkor az egyik helyen alkalmazott típuskonverzió akár már ugyanezen kifejezés második helyén sem fejt ki semmilyen hatást.

Típuskonverziós operátortból két típusú van:

  • implicit típuskonverziós operátor: ezt a típuskonverziót a fordítóprogram "automatikusan" alkalmazza a kifejezés kiértékelése során, amennyiben ezt szükségesnek érzi. Ennek a programkódban tehát gyakorlatilag nincsen nyoma, csak azok tudják hogy a háttérben alkalmazva van ilyen operátor, akik alaposan ismerik a kifejezés kiértékelésének módját.
  • explicit típuskonverziós operátor: ezt a típuskonverziót a programozónak bele kell írni a program szövegébe, vagyis a kódban ennek nyoma van. Tipikusan két formája van: az 'AS' operátoros típuskényszerítés, és a hagyományos stílusú. Ez utóbbi esetben a változó elé kell a típust leírni zárójelben.

Ilyen típuskonverziós operátort tudunk fejleszteni saját típusainkhoz is - amennyiben szükségesnek érezzük.

Tegyük fel, hogy a 'Teglalap' osztályhoz szeretnénk olyan típuskonverziót készíteni, amelynek során 'Negyzet' típusra alakítjuk át a példányban lévő értéket oly módon, hogy olyan négyzetet készítünk, amelynek a területe megegyezik az eredeti téglalap példány területével:

class Teglalap
{
    public static explicit operator Negyzet(Teglalap t)
    {
      double terulet = t.aOldal * t.bOldal;
      double negyzet_oldal = Math.Sqrt( terulet );
      Negyzet n = new Negyzet(negyzet_oldal);
      return n;
    }
}
 

Az operátor itt is osztályszintű függvény alakjában létezik. Egyetlen paramétere van - az is értelemszerűen a típuskényszerítendő 'Teglalap' példány. A függvény visszatérési típusa valamely 'Negyzet' típus lesz. Ezen 'Negyzet' példány készítése előtt kiszámítjuk, mekkora legyen ezen négyzet oldalhosszúsága, hogy a terület értéke ne változzon. Aztán elkészítjük ezen 'Negyzet' példányt a kiszámított oldalhosszúság alapján, és visszaadjuk azt egy 'return' segítségével.

A fenti operátort az alábbi módon lehet alkalmazni pl:

Teglalap t = new Teglalap(10,20);
// hagyományos explicit típuskonverzió
Negyzet n1 = (Negyzet)t;
// AS operátoros típuskonverzió
Negyzet n2 = t as Negyzet;
 

Típuskonverziós operátorok írásának szabályai

A típuskonverziós operátorok készítése során az alábbi szabályokat kell betartani:

  • az operátor készítése során vagy az 'implicit' vagy az 'explicit' szót kell megadni, attól függően, milyen jellegű típuskonverziót szeretnénk készíteni
  • a függvény visszatérési típusa elvileg tetszőleges lehet
  • a függvénynek csak egy paramétere lehet
  • vagy a visszatérési típusnak, vagy a függvény paraméter-típusának meg kell egyeznie a tartalmazó osztály típusával

Típuskonverziók

  • automatikus: az OOP típuskompatibilitási szabályai miatt nincs értelme olyan típuskonverziós operátort írni, ahol egy gyermekosztály típusát valamely ős típusára konvertáljuk. Az ilyen jellegű típuskonverziót ugyanis maga az OOP szabályai biztosítják.

Ha saját típuskonverziót készítünk - mindig meg kell hozni azt a döntést, hogy implicit vagy explicit konverziót készítsünk.

  • implicit: konverziót készítünk általában akkor, amikor a konverziót valamilyen szempontból biztonságosnak érezzük. Például létezik implicit típuskonverzió az 'int' és a 'double' típusok között ebből az irányból. Ugyanis egy 'int' érték valamely 'double' típusú változóba történő elhelyezése minden további nélkül lehetséges, nem történhet veszteség.
  • explicit: konverziót készítünk, ha a konverziót lehetségesnek tartjuk, de vagy nem mindennapos a konverzió, vagy a konverzió során félő hogy az érték torzul, átalakul. Ezért szükségesnek érezzük, hogy a konverziót használó programozónak explicit módon bele kelljen írni azt a program kódjába, mintegy jelezvén, hogy tudomásul veszi a korlátokat, az értéktorzulást, és mégis szeretné a konverziót alkalmazni ("mosom kezeim" típusú konverzió). Erre példa a 'double' érték 'int' típusúvá alakítása, melynek során a tört számjegyek elvesznek, ezért a konverzió nem automatikus, de ha a programozó kéri - kivitelezhető.

int x =12;
// automatikus (implicit) típuskonverzió
double d = x;
// nem automatikus (explicit) típuskonverzió
int f = (int)d;
 
Hernyák Zoltán
2013-03-17 19:54:38