C# nyelvben lehetőségünk van az osztályunkhoz rendelt operátorok felüldefiniálására. Azonban a C# nem olyan engedékeny ilyen téren, mint a C++.
C# esetén a következő alapvető műveletek átdefiniálhatóak bármelyik osztályunk esetén:
+ - * / % & | ^ << >> + - ++ -- ! ~
Az összehasonlító operátorok is átdefiniálhatóak, de csak párban és csak bool visszatérési értékük lehet. Ha átdefiniáljuk a kisebb, mint (<) operátort, akkor át kell definiálnunk a nagyobb, mint (>) operátor jelentését is. Az átdefiniálható logikai műveletek:
< > <= >= == !=
Az egyenlő és nem egyenlő operátorok átdefiniálásánál szükséges helyesen felüldefiniálni az Equals(object o) metódust és ezen operátorok definíciója ne legyen ellentmondó az Equals(object o) működésének. Az átdefiniálási szintaxis igen egyszerű:
public static <típus vissza> operator <műveletei jel>(<típus> paraméter1, <típus> paraméter2)
{
//kód logika
return <vissza érték>
}
Az operátor átdefiniálás lényegében nem más, mint egy speciális statikus metódus létrehozása. Értelemszerűen, ha a műveletünk egy operandusú, akkor úgy kell megírni a metódust, hogy csak egy paramétert várjon argumentumként. Az operátorok csak publikus elérésűek lehetnek.
Az operátorok közül kitüntetett szerepű az implicit konverziós operátor. Ez azt teszi lehetővé, hogy bizonyos típusokból, külön jelzés nélkül létrehozható legyen az objektumunk, amely fel van vértezve az implicit konverzió lehetőségével.
Explicit konverziós operátor is definiálható, amely segítségével csak külön jelzéssel konvertálható a típusunk egy másik típusra.
Az implicit és explicit konvertáló operátorok szintaxisa:
public static implicit operator <ki típus>(<be típus> változó)
{
//logika
return <vissza érték>
}
public static explicit operator <ki típus>(<be típus> változó)
{
//logika
return <vissza érték>
}
Az alábbi példaprogram az operátor átdefiniálást mutatja be:
namespace PeldaOpatedef
{
class Pont
{
public int X { get; private set; }
public int Y { get; private set; }
public Pont(int x, int y)
{
X = x;
Y = y;
}
public Pont(int x)
{
X = x;
Y = 0;
}
//toString implementáció
public override string ToString()
{
return string.Format("X: {0}, Y: {1}", X, Y);
}
//Az equals miatt ajánlatos, meg amúgy is
public override int GetHashCode()
{
unchecked
{
int hash = (int)2166136261;
hash = (hash * 16777619) ^ X.GetHashCode();
hash = (hash * 16777619) ^ Y.GetHashCode();
return hash;
}
}
//equals implementáció a hash miatt, meg amúgy is
public override bool Equals(object obj)
{
if (obj == null)
{
return false;
}
Pont masik = obj as Pont;
if (masik == null)
{
return false;
}
return X == masik.X && Y == masik.Y;
}
public static Pont operator +(Pont a, Pont b)
{
return new Pont(a.X + b.X, a.Y + b.Y);
}
//mindkét érték növelése egésszel.
//A + jel asszociativitása és a típusok eltérése miatt
//két variáns definiálása szükséges
public static Pont operator +(Pont a, int b)
{
return new Pont(a.X + b, a.Y + b);
}
//2. variáns az asszociativitás miatt.
public static Pont operator + (int a, Pont b)
{
return new Pont(b.X + a, b.Y + a);
}
public static Pont operator -(Pont a, Pont b)
{
return new Pont(a.X - b.X, a.Y - b.Y);
}
// egy változós művelet esetén csak egy argumentum kell
public static Pont operator ++(Pont a)
{
return new Pont(a.X + 1, a.Y + 1);
}
//ha a ++ operátort átdefiniáljuk, akkor konzisztencia miatt
//a -- operátort is érdemes
public static Pont operator --(Pont a)
{
return new Pont(a.X - 1, a.Y - 1);
}
public static bool operator == (Pont a, Pont b)
{
//equals implementációra támaszkodunk, mert már egyszer megvan
//és nem találjuk fel újra a kereket!
return a.Equals(b);
}
public static bool operator !=(Pont a, Pont b)
{
//szintén
return !a.Equals(b);
}
//implicit konvertálás
public static implicit operator Pont(int i)
{
return new Pont(i);
}
public static implicit operator int(Pont p)
{
return p.X;
}
//explicit konvertálás
public static explicit operator bool(Pont p)
{
return (p.X != 0 || p.Y != 0);
}
}
}
A tesztelő főprogram:
using System;
namespace PeldaOpatedef
{
class Program
{
static void Main(string[] args)
{
//implicit teszt
Pont pi = 22;
int i = pi;
//A ToString() itt kell, mert impicit int-re konvertálódna
Console.WriteLine(pi.ToString());
Console.WriteLine(i);
Console.WriteLine((bool)pi); //explicit
var p1 = new Pont(1, 3);
var p2 = new Pont(2, 3);
Console.WriteLine("p1 + p2: {0}", p1 + p2);
Console.WriteLine("p1 - p2: {0}", p1 - p2);
Console.WriteLine("p1 + 2: {0}", p1 + 2);
Console.WriteLine("2 + p2: {0}", 2 + p2);
Console.WriteLine("p2++ {0}", p2++);
Console.WriteLine("p2-- {0}", p2--);
Console.ReadKey();
}
}
}
A program kimenete:
X: 22, Y: 0
22
True
p1 + p2: X: 3, Y: 6
p1 - p2: X: -1, Y: 0
p1 + 2: X: 3, Y: 5
2 + p2: X: 4, Y: 5
p2++ X: 2, Y: 3
p2-- X: 3, Y: 4
A példaprogram egy pont osztályt definiál x és y adattagokkal, és ehhez a két alapműveletet, az összeadás és kivonás műveleteket. Továbbá az osztály definiál implicit konverziót int típusról és int típusra. Továbbá egy explicit konverziót bool típusra.
Logikai operátorokból az egyenlőség és nem egyenlőség műveletet definiálja felül, amely belső működésében az Equals(object o) metódusra támaszkodik. Ezen megközelítést érdemes alkalmazni minden esetben, mert máskülönben igen érdekes és nehezen felderíthető hibákba futunk bele.
Mivel az Equals(object o) felüldefiniált lett, ezért a példaprogram felüldefiniálja a pont típus esetén a GetHashCode() metódust is.
Operátor felüldefiniálásnál érdemes megjegyezni, hogy ha lehetőség van rá, akkor definiáljuk a műveletek inverz műveletét is, még ha a fordító nem is kényszeríti ezt ki. Például, ha definiáljuk az inkrementálást, akkor definiáljuk a dekrementálást is, mint a példában. Ezzel lényegében az osztályunk későbbi használatát könnyítjük meg más programozók számára.
Összetett értékadás felüldefiniálása
Az összetett értékadó (compound assignment) operátorok (+=, *=, /=, -=, stb…) felüldefiniálódnak, ha a megfelelő műveleti jelhez tartozó operátort felüldefiniáljuk. Tehát ha a típusunk definiál egy egyedi + operátort és van egy a += b kifejezésünk, akkor a fordító valójában az a = a + b kifejezést fogja behelyettesíteni a műveletvégzés helyére.
Ez érték típusoknál másolást eredményez, osztályok esetén pedig egy új példány allokációját, ami teljesítménykritikus kód esetén nem szerencsés. Éppen ezért, a C# 14 megengedi az összetett értékadó operátorok felüldefiniálását, hogy a felesleges másolások elkerülhetőek legyenek. Az előző pont példánál maradva a += operátor megvalósítása a következő lehetne:
public void operator +=(Pont b)
{
X += b.X;
Y += b.Y;
}
Mivel a jelenlegi példányt fogja módosítani az operátor, ezért az operátor nem lehet statikus és void visszatéréssel kell rendelkeznie.
Aktuális példányt módosító -- és ++ operátort is definiálhatunk, amely például egy a++ vagy a-- szituációban fut le. Ezen operátor is void visszatéréssel kell, hogy rendelkezzen és nem lehet statikus:
public void operator ++()
{
X++;
Y++;
}