Dizajniranje Softvera

Vežbe | 2. termin

Sadržaj

Operator overloading

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.

Definisanje operatora

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:

Primer

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

Konverzija tipova

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:

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.

Primer

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:

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.

Indekseri

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:

Primer

Imamo 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.

Podrška za foreach petlju

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.

Yield

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 »>