Operatori su funkcije koje imaju drugačiju sintaksu poziva od klasičnih funkcija.
Sintaksa poziva funkcije Saberi
:
int s = Saberi(x, y);
Sintaksa poziva operatora +
:
int s = x + y;
Po broju operanada operatori u C# mogu biti: unarni (-x
, x++
), binarni (x + y
, x < y
) i ternarni (z ? x : y
).
U klasama koje kreiramo možemo definisati ponašanje operatora koji se poziva nad objektom te klase.
Operatore definišemo po sledećoj sintaksi:
public static TypeToReturn operator +(OperandTypeA a, OperandTypeA b) {
// povratna vrednost mora biti tipa TypeToReturn
}
Ovde je prikazan binarni operator +
. Za unarne operatore sintaksa je ista, razlikuju se jedino po broju parametara, unarni bi imao samo jedan parametar.
Primer za unarni operator negacije (-
):
public static TypeToReturn operator -(OperandType a) {
// povratna vrednost mora biti tipa TypeToReturn
}
Ovde moramo ispoštovati par pravila:
Imamo klasu koja predstavlja tačku 2D prostoru:
public class Point2D
{
public double x;
public double y;
public Point2D(double x, double y)
{
this.x = x;
this.y = y;
}
}
Prvo, devinisaćemo operator sabiranja koji će sabirati dve tačke:
public static Point2D operator +(Point2D a, Point2D b)
{
double x = a.x + b.x;
double y = a.y + b.y;
return new Point2D(x, y);
}
Kao povratnu vrednost dobijamo referencu na novokreirani Point2D objekat čiji su atributi x
i y
dobijeni sabiranjem vrednosti atributa druge dve tačke.
Isti želimo da uradimo i za operator oduzimanja. Mogli bismo da samo kopiramo prethodni kod i kod sabiranja vrednosti atrivuta zamenimo +
za -
. Ali, imamo i drugi način.
Možemo definisati unarni operator negacije, i iskoristi njega i prethodno definisani operator sabiranja, i preko njih definišemo operator oduzimanja.
Unarni operator negacije:
public static Point2D operator -(Point2D a)
{
a.x = -a.x;
a.y = -a.y;
return a;
}
Povratna vrednost operatora će biti referenca na isti Point2D
objekat koji je i učestvovao u operaciji (nije se kreirao novi objekat).
Sada, operator oduzimanja možemo definisati vrlo jednostavno na sledeći način:
public static Point2D operator -(Point2D a, Point2D b)
{
return a + (-b);
}
U Main
metodi možemo istestirati ove operatore:
Point2D a = new Point2D(1, 2);
Point2D b = new Point2D(10, 20);
Point2D c = new Point2D(17, 17);
var p = c - (b + a); // p je Point2D
Console.WriteLine("Tačka p: " + p.x + ", " + p.y);
Dobijamo ispis:
Tacka p: 6, -5
Operacija konverzije tipa (cast) je unarni operator.
Za tipove koje kreiramo (klase) možemo definisati ponašanje operatora konverzije. Pri definisanju navodimo da li se to odnosi na implicitnu ili eksplicitnu konverziju.
Implicitna konverzija se dešava automatski, u slučajevima kada konverija neće dovesti do gubljenja podataka, kao na primer konverzija int
u float
. Skup vrednosti koje int
može uzeti je manji od onog kod float
-a, pa je moguća implicitna konverzija, koja se dešava u ovakvim situacijama: float x = 123;
Eksplicitna konverzija se dešava na zahtev, kada eksplicitno navedemo da želimo da se konverzija dogodi. Ona je neophodna u slučajevima kada bi zbog konverzije došlo do nekakvog gubitka podataka, kao u slučaju konverzije float
u int
. Primer: int x = (int)123.45f
Ponašanje operatora konverzije definišemo na sledeći način:
public static explicit operator MyClass(OtherType x) {
// Povratna vrednost mora biti MyClass
}
public static explicit operator OtherType(MyClass x) {
// Povratna vrednost mora biti OtherType
}
Ako je reč o konverziji koja bi trebalo da se desi implicitno, umesto ključne reči explicit
pišemo implicit
.
Operator konverzije koji definišemo u nekoj klasi mora vršiti konverziju ili iz tipa te klase, ili u tip te klase.
Svaki operator konverzije koji definišemo mora biti public static
.
U sledećem primeru definisaćemo cast operator za knverziju iz Point2D
u double
niz, i obratno.
Prvo, kako bi smo sebi olakšali posao, kreiraćemo konstruktor koji prima bouble
niz, i ukoliko postoje prva 2 elementa, njih uzima za vrednosti x
i y
atributa, redom, a ukoliko ne postoje ovi atributi će ostati 0
.
public class Point2D
{
public double x = 0;
public double y = 0;
public Point2D(double[] arr)
{
if (arr.Length > 0)
x = arr[0];
if (arr.Length > 1)
y = arr[1];
}
}
U istoj klasi (Point2D
) definisaćemo 2 operatora konverzije:
Point2D
u double[]
double[]
u Point2D
public static implicit operator double[](Point2D p)
{
// Konstrukcija novog double niza sa potsavljenim članovima
return new double[] { p.x, p.y };
}
public static explicit operator Point2D(double[] arr)
{
// Konstruktor prima niz i već poseduje logiku za konverziju u Point2D
return new Point2D(arr);
}
U Main
metodi možemo isprobati ove konverzije:
Point2D p1 = new Point2D(1.0, 2.0);
double[] arr = point; // <- Implicitna konverzija
Point2D p2 = (Point2D)arr; // <- Eksplicitna konverzija
TODO: Objasniti kako se var uklapa u ovu priču.
TODO: urediti
Indekser je posebna vrsta svojstva (property).
Definicija indeksera:
public ValueType this[IndexType index]
{
get
{
// Povratna vrednost je tipa ValueType
}
set
{
// "value" je tipa ValueType
}
}
Napomene:
get
i set
moraju da imaju teloindex
je dostupan u telu get
i set
, predstavlja vrednost prosledjenu u objekat[indeks]
this[Type p1, Type p2]
moze stajati proizvoljan broj parametara sa proizvoljnim tipovimaImamo klasu Vector
, koja predstavlja n-dimenzioni vektor. Koordinate se cuvaju u atributu data
, koji je double
niz.
Definisali smo dva konstruktora. Jedan prima duzinu vektora i instancira novi niz te tuzine. Drugi prima vec pripremljen niz i kopira referencu na njega u data
.
class Vector
{
private double[] data;
public Vector(int len)
{
data = new double[len]; // Instanciranje niza (na heap-u)
}
public Vector(double[] arr)
{
data = arr; // Kopiranje reference na prosledjeni `arr` niz
}
}
Dodajemo indekser koji omogucava citanje niza data
, ali izmenu niza ogranicava sa private
:
public double this[int i]
{
get
{
return data[i];
}
private set
{
data[i] = value;
}
}
Dodajemo indekser koji uzima podniz:
public double[] this[int first, int last]
{
get
{
if (last >= data.Length)
return new double[0];
int len = last - (first - 1);
double[] sub = new double[len];
for (int i = 0; i < len; i++)
{
sub[i] = data[first + i];
}
return sub;
}
}
U Main
metodi cemo kreirati vektor:
var data = new double[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var vec = new Vector(data);
i iskoristiti prvi indekser:
Console.WriteLine(vec[3]); // Ispis: 3
vec[5] = 17.0; // Error: Indekser ima privatni setter, ovde nije dostupan
Koriscenje drugog indeksera:
var sub = vec[3, 8]; // <- Koriscenje indeksera. Tip "sub"-a ce biti "double[]"
foreach (var item in sub)
{
Console.Write(item + " ");
}
Console.WriteLine();
ispis: 3 4 5 6 7 8
Kao sto mozemo imati vise metoda sa istim imenom, a koje se razlikuju po broju i tipu parametara, tako mozemo imati i vise indeksera. Ono sto je vazno jeste da se oni razlikuju po broju i tipu parametara, tj da im potpis bude razlicit. U tom slucaju prilikom koriscenja indeksera moze se zakljuciti koji od njih ce biti pozvan.
TODO: urediti
public object Current
{
get {
// Vraca foreach petlji trenutni element
}
}
public bool MoveNext()
{
// Poziva se na pocetku svake foreach iteracije
// Sluzi da azurira nesto sto ce Current getter da koristi kako bi vratio trenutni element
// return true => foreach nastavlja
// return false => foreach staje
}
public void Reset()
{
// Nije potrebno implementirati
}
Implementiramo GetEnumerator
:
class Vector : IEnumerable
{
// Sav kod je isti kao i pre. Dodajemo novo.
// Koristicemo u implementaciji enumeratora
public int Length
{
get { return data.Length; }
}
// Potrebno implementirati zbog IEnumerable
public IEnumerator GetEnumerator()
{
return new VectorEnumerator(this);
}
}
Implementiramo Current
, MoveNext
i Rest
(koji moze ostati prazan):
class VectorEnumerator : IEnumerator
{
Vector vector;
private int index = -1;
public VectorEnumerator(Vector vector)
{
this.vector = vector;
}
public object Current
{
get { return vector[index]; } // Koriscenje indeksera
}
public bool MoveNext()
{
return ++index < vector.Length;
}
public void Reset() { }
}
U vector
cuvamo referencu na Vector
objekat kroz koji iterisemo.
Atribut index
na pocetku treba biti -1
jer se MoveNext
poziva pre svake iteracije, pre nego sto se pozove Current
. Zato, da bi prilikom prve iteracije, kada se Current
poziva, index
bio 0
, prilikom konstrukcije objekta mora biti postavljen na -1
, jer ce ga povecati MoveNext
. Dakle, mozemo reci da MoveNext
vrsi pripreme za trenutnu iteraciju, ne za narednu.
TODO: Da li ovo ostaviti za nakon generic tipova?
static class Program()
{
public static void Main()
{
foreach(int item in Iterate())
{
Console.WriteLine(item);
}
}
}
class MyClass
{
public IEnumerable<int> Iterate()
{
yield return 1;
yield return 2;
yield return 3;
yield return 4;
}
}
Naredba yield return
vraća vrednost foreach
petlji. Prilikom svake iteracije, tj. svakog poziva metode Iterate
u foreach
-u, metoda će nastaviti od dela koji sledi nakon poslednje izvršene yield return
naredbe, tj. odande gde je stala, i tako sve dok ne završi sa radom.
Ispis:
1
2
3
4
«< 1. Termin | 3. Termin »> |