Az objektum-elvű programozással kapcsolatos lehetőségek lényegében megegyeznek a Java megközelítésével. Néhány kérdést azonban mégis érdemes kiemelni:
Az osztály konstans változói. Tetszőleges típust tudunk konstansként definiálni. Ha egy konstanst akarunk megadni, akkor kötelező kezdeti értéket adni neki. Az is megengedett, hogy a fordítási egységen belül tetszőlegesen hivatkozunk más konstansokra. Amennyiben nincs körkörös hivatkozás, úgy a fordító a helyes sorrendet fogja kialakítani. Például:
class A
{
public const int X = B.Z + 1;
public const int Y = 10;
}
class B
{
public const int Z = A.Y + 1;
}
Az osztály változói lehetnek osztály( static) ill. példányszintűek. Osztályszintű változók csak egyszer tárolódnak el a memóriában, míg a példányszintű változók külön-külön példányonként. Új dolog a readonly hozzáférési kategória. Az ilyen változóknak csak két esetben lehet értéket adni: vagy a deklaráláskor vagy a megfelelő konstruktorokban. Akkor érdemes használni static readonly hozzáférést a const helyett, ha a változót konstansként akarjuk használni, de nem biztos, hogy fordítási időben meg tudjuk adni az értékét. További hozzáférési kategóriák:
Az osztályok metódusai függvények, ill. void visszatérési értéket használva eljárások. Négyféle paraméterátadási lehetőségünk van:
Metódusok is lehetnek osztályszintűek( static). Ilyenkor nem lehet a this kulcsszót, sem példányváltozókat használni.
Statikus vagy nem virtuális metódusok esetén a new módosító kulcsszót kell használni, ha egy származtatott osztály egy a bázisosztályéval azonos szignatúrával vezet be egy metódust. Ha a new kulcsszót mellőzzük, a fordítóprogram figyelmeztetést ad, nehogy véletlenül definiáljunk felül egy metódust.
Virtuális metódusok esetén az override kulcsszó segítségével kell jeleznünk, hogy az ősosztály virtuális metódusát szeretnénk specializálni.
class A { public void F() { Console.WriteLine("A.F"); } public virtual void G() { Console.WriteLine("A.G"); } }
class B: A { new public void F() { Console.WriteLine("B.F"); } public override void G() { Console.WriteLine("B.G"); } }
class Test { static void Main() { B b = new B(); A a = b; a.F(); b.F(); a.G(); b.G(); } }
//A.F B.F B.G B.G
A közvetlen ősosztályra a base kulcsszóval hivatkozhatunk.
A sealed kulcsszó használatával megakadályozhatjuk, hogy egy osztályból származtassanak vagy egy metódust felüldefiniáljon a származtatott osztály. Ha egy metódusnál a sealed override hozzáférést használjuk, akkor ezzel meggátoljuk, hogy egy származtatott osztályban felülírjuk ezt a metódust.
Az external módosítóval rendelkező metódusok valamilyen más nyelven vannak implementálva. Éppen ezért a metódus törzsében csak egy pontosvessző áll. Az ilyen metódus nem lehet abstract.
Az osztály egy példányának létrehozásánál hívódik meg. A konstruktor neve megegyezik az osztály nevével. Négyféle hozzáférési módja lehet egy konstruktornak: public, protected, internal, private. Természetesen definiálhatunk attribútumokat is a konstruktorokhoz.
Közvetlenül a konstruktor törzsének végrehajtása előtt automatikusan történik egy másik konstruktor hívás is. Ezt nevezik konstruktor inicializálásnak is. Kétfajta lehetőségünk van: vagy meghívjuk az ős valamelyik példány konstruktorát, vagy az adott osztály egy másik konstruktorát hívjuk meg először. Ha nem használjuk egyiket sem, akkor az ős alapértelmezett konstruktora hívódik meg. Azaz a következő két deklaráció ekvivalens egymással:
C( ...) {...}
C( ...): base() {...}
Példák konstruktor inicializátorra:
class A { public A(int x, int y) {} } class B: A { public B(int x, int y): base(x + y, x - y) {} } class Text { public Text(): this(0, 0, null) {} public Text(int x, int y): this(x, y, null) {} public Text(int x, int y, string s) {// valami kód} }
Ha egy osztálynak nem deklarálunk semmilyen példány konstruktort, akkor egy olyan default konstruktor jön létre, ami az alapértelmezett konstruktor inicializátorral fog rendelkezni.
Készíthetünk private konstruktorokat is, de ebben az esetben az osztály nem példányosítható és nem lehet örököltetni sem belőle. Akkor célszerű ezt használni, ha pl. csak statikus elemeket tartalmaz az osztályunk.
A konstruktor neve előtt a static kulcsszót kell használni. A statikus konstruktorok az osztály inicializálásakor futnak le. Ez pontosan akkor van, amikor az ősosztály betöltődik. Ezekre a konstruktorokra nem lehet hivatkozni és természetesen nem is öröklődnek.
Az objektumok megsemmisülésekor hívódnak meg. Mivel a C#-ban is megvalósították az automatikus szemétgyűjtést, ezért a destruktorok automatikusan hívódnak meg, közvetlenül nem lehet őket hívni. A destruktorok az öröklődési láncon végighaladva egymás után hívódnak meg.
Példa rekurzióra egy bináris fa, ahol a bal és a jobb gyerek típusa ugyanaz, mint a gyökéré:
class BinFa
{
public BinFa balGyerek = null, jobbGyerek = null; // a saját osztályát használjuk
public int adat = 0;
public BinFa(BinFa bal, BinFa jobb)
{
balGyerek = bal;
jobbGyerek = jobb;
}
public void Inorder(BinFa gyökér)
{
if (null != gyökér)
{
Inorder(gyökér.balGyerek);
//Feldolgozás: gyökér.adat
Inorder(gyökér.jobbGyerek);
}
}
}
Az abstract kulcsszó használatával azt jelezhetjük, hogy az adott osztály még nincs teljesen megvalósítva. Absztrakt osztály emiatt nem példányosítható, csak ősosztály lehet. Az absztraktként definiált metódusait a származtatott osztályban meg kell valósítani. Természetesen egy absztrakt osztály nem lehet sealed (véglegesített). Annak ellenére, hogy egy absztrakt metódus tulajdonképpen virtuális, nem használhatjuk a virtual megjelölést. Természetesen static -ként sem lehet definiálni az absztrakt metódusokat. Ezeket a metódusokat a származtatott osztályokban implementálhatjuk úgy, hogy az adott metódushoz megírjuk a törzs részt is.
public abstract class Shape { public abstract void Paint(Graphics g, Rectangle r); } public class Ellipse: Shape { public override void Paint(Graphics g, Rectangle r) { g.drawEllipse(r); } }
public class Box: Shape { public override void Paint(Graphics g, Rectangle r) { g.drawRect(r); } }
Egy metódus törzsében nem hivatkozhatunk az ős absztrakt metódusára:
class { public abstract void F(); }
class B: A
{
public override void F()
{
base.F();
// Hiba, mert a base.F() metódus absztrakt
}
}
DE, ha nem a metódus, hanem az osztály absztrakt akkor lehet a függvénynek törzse. Például a közös részeket az absztrakt osztályban meg lehet írni és csak a base kulcsszóval hivatkozunk rá. Így könnyebben karbantartható és átlátható lesz a kód is.
//absztrakt
abstract class A0
{
public virtual void F() { System.Console.WriteLine("HELLO! "); }
}
class B0: A0
{
public override void F()
{
base.F();
// Nem Hiba, hiába absztrakt az osztály
}
}
Az öröklődés Class Osztály: ŐsOsztály {..}
Utód osztályban nem lehet bővebb hozzáférés, mint ős osztályban, a base kulcsszóval a közvetlen ősosztály metódusa elérhető. A virtuális függvényeknél a new kulcsszó lehagyásával warning-ot kapunk.
CB: felüldefiniálja új függvénnyel (override)
CC: elrejti, nem virtuális függvénnyel
CD: új virtuális függvény
CE: sealed – lezárt osztály, nem lehet belőle származtatni (csak felhasználni)
//virtual
public class CA
{
public virtual void f() { }
}
public class CB : CA
{
public override void f() { }
}
public class CC : CA
{
private new void f() { }
}
public class CD : CA
{
public new virtual void f() { }
}
public class CE : CA
{
public sealed override void f() { }
}
Hasonlóan az absztrakt osztályoknál az öröklés:
//abstract
public abstract class A
{
public abstract void f();
}
public class B:A
{
public override void f() { }
}
public class C1 : A
{
public sealed override void f() { }
}
public sealed class C2 : A
{
public sealed override void f() { }
}
Nincs többszörös öröklődés C#-ban, helyette interfacek vannak.
Az interfész egy szerződés. Amelyik struktúra vagy osztály megvalósítja, annak meg kell felelnie ennek a szerződésnek. Az interfész definiálhat metódusokat, indexelőket, property-ket, eseményeket, de azokat nem valósítja meg. Egy interfésznek több más interfész is lehet őse, egy osztály ill. struktúra pedig több interfészt is megvalósíthat. Lényegében az "elfelejtett" többszörös öröklődést pótolja ez valamennyire. Azokat az interfészeket, amelyeknek már nincsen őse explit ősöknek is szokták nevezni. Az interfész jellegéből logikusan következik, hogy nem szabad használni az interfész tagjaira az abstract, virtual, static, override módosítókat.
A .NET Remoting szolgáltatásának köszönhetően egy tetszőleges szoftvercsatorna felhasználásával kapcsolatot teremthetünk alkalmazásaink között. Ez Interfaceok segítségével valósítható meg. A távoli Interfészt kell helyben implementálni.
Még egy fontos dolog, hogy öröklődéskor származtathatunk úgy is, hogy egy nem interface osztályból és utána akármennyi interfészből.
Két példát olvashatunk alant.
interface I2
{
void MyFunction();
}
class Test : I1,I2
{
public void MyFunction()
{
Console.WriteLine("Guess which interface I represent???!");
}
}
class AppClass
{
public static void Main(string[] args)
{
Test t=new Test();
I1 i1=(I1)t;
i1.MyFunction();
I2 i2=(I2)t;
i2.MyFunction();
}
}
}
//EXAMPLE 2
using System;
namespace PavelTsekov
{
interface I1
{
void MyFunction();
}
interface I2
{
void MyFunction();
}
class Test : I1,I2
{
void I1.MyFunction()
{
Console.WriteLine("Now I can say this here is I1 implemented!");
}
void I2.MyFunction()
{
Console.WriteLine("Now I can say this here is I2 implemented!");
}
}
class AppClass
{
public static void Main(string[] args)
{
Test t=new Test();
I1 i1=(I1)t;
i1.MyFunction();
I2 i2=(I2)t;
i2.MyFunction();
}
}
}
Lehetőség van az operátorok átdefiniálására, ide értve az explicit és implicit típuskonverziós operátorokat is. Az operátorok deklarásánál figyelni kell, hogy csak, és kizárólag csak a public static módosítókat lehet használni. Operátorok paraméterei érték típusúak, nem lehetnek ref ill. out paraméterek. Kétféle típusú operátort lehet megadni:
public struct Digit { byte value; public Digit(byte value) { if (value < 0 || value > 9) throw new ArgumentException(); this.value = value; } public static implicit operator byte(Digit d) { return d.value; } public static explicit operator Digit(byte b) { return new Digit(b); } public static Digit operator+(Digit a, Digit b) { return new Digit(a.value + b.value); } public static bool operator==(Digit a, Digit b) { return a.value == b.value; } }
Definiálhatók elérők (accessor), melyek tulajdonságként viselkednek, de az írás (set) illetve olvasás (get) számára külön eljárások írhatók. Például, ha egy elérőre csak a get eljárást definiáljuk, akkor az elérő egy csak olvasható attribútumként fog viselkedni. Megjegyzés: lehetséges, de nem javasolt olyan eljárásokat írni, melyeknek mellékhatása van. Például: az elérő kétszeri egymás utáni olvasása különböző eredményt ad.
public class Button: Control
{
private string caption;
public string Caption
{
get { return caption; }
set {
if (caption != value)
{ caption = value; Repaint(); }
}
}
}
Definiálhatók indexelők (indexer) is melyekkel, szögletes zárójellel indexelhető osztályok hozhatók létre. Fontos, hogy az indexelő paramétere bármilyen típus lehet. Így bármilyen object úgy indexelhető, mint egy tömb. Az indexelő deklarálásakor meg kell adni az elemek típusát, majd a this-t követi a formális paraméterlista szögletes zárójelben. Ez a paraméterlista határozza meg az indexelés szintaxisát, benne nem szerepelhet ref ill. out paraméter.
class Grid
{
const int NumRows = 26;
const int NumCols = 10;
int[,] cells = new int[NumRows, NumCols];
public int this[char c,int colm]
{
get
{
c = Char.ToUpper(c); if (c < 'A' || c > 'Z') throw new ArgumentException();
if (colm < 0 || colm > = NumCols) throw new IndexOutOfRangeException();
return cells[c - 'A', colm];
}
set
{
c = Char.ToUpper(c);
if (c < 'A' || c > 'Z') throw new ArgumentException();
if (colm < 0 || colm > = NumCols) throw new IndexOutOfRangeException();
cells[c - 'A', colm] = value;
}
}
}
Az (event) az Új nyelvi elemek fejezetben lesz szó.