Садржај
2 Класе и објекти
2.1 Основни појмови о класама и објектима
3 Генеричке класе
4 Наслеђивање и полиморфизам
5 Примери пројеката са решењима
5.1 Различита кретања
5.2 Квиз
5.4 Приказ рада алгоритама сортирања

Вишеструко наслеђивање и појам интерфејса

У овој лекцији:

  • шта је вишеструко наслеђивање,

  • шта је интерфејс и како се разликује од апстрактне класе,

  • примери имплементирања интерфејса из .Net библиотеке,

  • креирање и имплементирање сопствених интерфејса.

Појам интерфејса

У одређеним ситуацијама се јавља потреба да једна класа има више базних класа, тј. да наследи више различитих класа. Овакав облик наслеђивања се назива вишеструко наслеђивање. Око овог концепта било је много полемике још од његовог увођења, а мишљења нису у потпуности усаглашена ни данас. Наиме, многи научници рачунарства мисле да вишеструко наслеђивање уводи сувише проблема (компликује језик и његову имплементацију, повећава шансе појављивања багова због погрешне употребе вишеструког наслеђивања) и да због тога не треба да буде подржано у програмским језицима, док неки и даље мисле да програмски језици треба да га омогуће, а да је на програмерима да савладају његову правилну употребу и тако заобиђу могуће проблеме. Због тога данас неки језици подржавају вишеструко наслеђивање, а неки не. Типично, новији језици су ти који не подржавају вишеструко наслеђивање, а уместо њега уводе неке сличне концепте, који нуде приближно исте могућности уз мање ризика од погрешне употребе. Постојећи концепти који служе као замене за вишеструко наслеђивање и даље се развијају и мењају. Са друге стране, разни језици који дозвољавају вишеструко наслеђивање имају различите додатне захтеве за писање кода (у смислу синтаксе), како би се смањила могућност грешке.

Темом вишеструког наслеђивања и могућим проблемима који са њим долазе нећемо се овде даље бавити, а заинтересовани читаоци већ могу да нађу више информација на страници Википедије о вишеструком наслеђивању.

У језику C#, наслеђивање више класа није могуће. Сродан концепт који у великој мери надокнађује потребу за вишеструким наслеђивањем се у језику C# назива интерфејс. До сада смо реч интерфејс користили мање формално, да означимо јавни (изложени) део класе. Сада се срећемо са новим, друкчијим значењем ове речи.

У језику C#, интерфејс је посебан језички конструкт који пре свега служи да зада декларације (потписе) једног или више метода. Поред метода, интерфејс може да садржи декларације својстава, индексера и догађаја. За класу која садржи дефиниције метода и других чланова декларисаних у интерфејсу, каже се да имплементира дати интерфејс.

У неким програмским језицима уместо речи „интерфејс” користи се реч „протокол”, а уместо да се каже да класа имплементира интерфејс, каже се да класа усваја (енгл. adopts) протокол. Реч је о суштински истом концепту.

Појам интерфејса је врло близак појму апстрактне класе, јер оба садрже „најављене”, тј. декларисане методе (и друге чланове) без дефиниције. Главна разлика је у томе што интерфејс не може да садржи нестатичка поља са подацима. На пример, раније уведена апстрактна класа Figura3D не би (таква каква је) могла да се напише као интерфејс, јер садржи референцу osnova на класу Figura2D, као и реалан податак visina. У том смислу, интерфејс можемо да схватимо као апстрактну класу без (нестатичких) података. Из ове разлике између интерфејса и апстрактних класа проистиче и разлика у употреби, а то је да остале класе могу да имплементирају више интерфејса, али независно од броја интерфејса које имплементирају, могу да наследе само једну класу, било апстрактну или не. То и јесте основни разлог за увођење интерфејса.

У новијим верзијама језика C# разлике између интерфејса и апстрактних класа се све више смањују, са идејом да интерфејси дају што више могућности које би донело вишеструко наслеђивање класа, али тако да не уведу и исте проблеме.

Употреба интерфејса

Погледајмо следећи метод за исписивање свих елемената листе:

static void Ispis(List<int> a)
{
    Console.Write("[");
    foreach (int x in a)
        Console.Write(" {0}", x);
    Console.WriteLine(" ]");
}

Као што је познато, дати метод употребљавамо тако што га позовемо са листом као аргументом.

List<int> a = new List<int>() { 1, 2, 3 };
Ispis(a);

Уколико нам је потребно да на исти начин исписујемо и елементе низова и других објеката који садрже колекције, могли бисмо да за сваки тип колекције напишемо по један такав метод. Међутим, испоставило би се да сви ти методи изгледају потпуно исто, осим што се разликују у декларацији параметра. Зато, уместо да пишемо посебан метод за сваки тип објекта, можемо да употребимо интерфејс IEnumerable као општи тип за све објекте који садрже колекције, а који би се прослеђивали методу као параметар. У том случају, метод би изгледао овако:

static void Ispis(IEnumerable<int> e)
{
    Console.Write("[");
    foreach (int x in e)
        Console.Write(" {0}", x);
    Console.WriteLine(" ]");
}

Овако написан метод нам допушта много разноврснију употребу. На пример, сада можемо да пишемо:

List<int> a = new List<int>() { 1, 2, 3 };
Ispis(a);

int[] b = new int[] { 1, 2, 3 };
Ispis(b);

SortedSet<int> ss = new SortedSet<int>() { 1, 2, 3 };
Ispis(ss);

Stack<int> st = new Stack<int>();
st.Push(1); st.Push(2); st.Push(3);
Ispis(st);

Queue<int> q = new Queue<int>();
q.Enqueue(1); q.Enqueue(2); q.Enqueue(3);
Ispis(q);

Оваква уштеда у писању метода је могућа јер наредба foreach прихвата било који објекат, чија класа имплементира интерфејс IEnumerable. Како и низ и свака од класа List, SortedSet, Stack, Queue имплементирају овај интерфејс, сваки од ових објеката може да се проследи методу као параметар. Више од тога, оваквим методом смо подржали исписивање и свих објеката који још не постоје, а који ће бити написани тако да имплементирају интерфејс IEnumerable. Такође, ако на неком месту у коду користимо листу, па се касније предомислимо и желимо да користимо низ или неку другу колекцију, ради тога нећемо морати да преправљамо и метод за исписивање елемената колекције.

Употреба интерфејса уместо конкретних типова повећава употребљивост кода који пишемо, а тиме и смањује потребу за његовим каснијим копирањем и преправљањем.

Предност коју нам доноси употреба интерфејса је иста она коју смо описали у делу о апстрактним класама, а то је да једанпут написаним кодом обрађујемо објекте различитог типа, не водећи при томе рачуна о стварном типу објеката. Зато је пожељно да, где год нам то природа алгоритма допушта, користимо интерфејсе уместо конкретних типова. Ово се једнако односи на готове интерфејсе из .Net библиотеке и на интерфејсе које сами креирамо, с тим да уместо сопствених интерфејса можемо да користимо и апстрактне класе (ако нам није потребно да објекти задовољавају више интерфејса истовремено).

Имплементирање интерфејса из .Net библиотеке

Језик C# је веома богат већ дефинисаним интерфејсима у библиотеци .Net, као и наредбама и библиотечким методима који користе те интерфејсе. Да бисмо објекте наших класа могли да користимо у таквим наредбама и методима, довољно је да те наше класе имплементирају одговарајуће интерфејсе. Следећих пар примера илуструје неке од користи које можемо да имамо када класом коју пишемо имплементирамо интерфејс из библиотеке.

Пример – сортирање низова и листи

(имплементирање интерфејса IComparable)

Подсетимо се примера са разломцима са почетка овог курса. Написали смо једну релативно богату класу, која омогућава читање, исписивање и рачунање са разломцима на исти начин као што се то ради са целим или реалним бројевима. Ипак, тиме нисмо достигли пуну функционалност, коју би неко могао да очекује од класе Razlomak. На пример, са постојећом имплементацијом класе није могуће сортирати низ разломака методом Array.Sort. Ако бисмо писали:

int n = int.Parse(Console.ReadLine());
Razlomak[] r = new Razlomak[n];
for (int i = 0; i < n; i++)
    r[i] = Razlomak.Parse(Console.ReadLine());

Array.Sort(r);

…програм би могао да се покрене, али би пукао током извршавања наредбе Array.Sort(r);. Тачније, програм би „бацио” изузетак и добили бисмо следећу поруку:

System.InvalidOperationException: ’Failed to compare two elements in the array.’
Inner Exception
ArgumentException: At least one object must implement IComparable.

Ово значи да статички метод Sort класе Array тек приликом покушаја да упореди два елемента низа r открива да му није доступан метод помоћу кога би могао да изврши поређење. Мада смо у класи Razlomak дефинисали метод CompareTo, нигде нисмо саопштили да је то метод који друге класе (нпр. оне из библиотеке) треба да користе за поређење инстанци класе Razlomak. Управо у ту сврху је у библиотеци дефинисан интерфејс IComparable.

Када нека класа имплементира интерфејс IComparable, друге класе је виде као класу чије инстанце могу да се пореде методом CompareTo.

Према томе, да бисмо могли да сортирамо бројеве на наведени начин, потребно је да мало преправимо класу Razlomak. Конкретно, уместо:

public class Razlomak
{
    // ...

    public int CompareTo(Razlomak r)
    {
        return a * r.b - r.a * b;
    }

    // ...
}

…треба да пишемо:

public class Razlomak : IComparable
{
    // ...

    public int CompareTo(Object obj)
    {
        Razlomak r = obj as Razlomak;
        return a * r.b - r.a * b;
    }

    // ...
}

Сада се претходне наредбе за сортирање низа разломака извршавају баш онако како смо и очекивали.

Из овог примера видимо да се интерфејс који дата класа имплементира наводи после имена класе и двотачке, потпуно исто као и базна класа коју дата класа наслеђује. Друга измена је измена типа параметра метода CompareTo. Она је потребна зато што је таква декларација овог метода у библиотечком интерфејсу.

Пример – аутоматско ослобађање ресурса

(имплементирање интерфејса IDisposable)

У Петљином курсу за први разред објашњено је да постоји више начина да упишемо податке у текстуални фајл, користећи објекат StreamWriter. Један начин подразумева експлицитно затварање фајла помоћу метода Close.

StreamWriter sw = new StreamWriter(putanja);  // Отвори фајл за писање
sw.Write("...");                              // Пиши у фајл
sw.WriteLine("...");                          // Пиши у фајл
// ...
sw.Close();                                   // Затвори фајл

Други, новији начин подразумева употребу наредбе using, која аутоматски води рачуна о затварању фајла.

using (StreamWriter sw = new StreamWriter(putanja))
{
    sw.Write("...");
    sw.WriteLine("...");
    // ...
}

Подсетимо се укратко по чему је други начин бољи од првог, тј. зашто је језик проширен наредбом using. Употреба наредбе using гарантује да ће фајл бити затворен чак и у случају да током употребе објекта sw наступи изузетак. Самим тим гарантује се и да ће пре затварања фајла сви подаци бити преписани из бафера на диск, као и да ће бити ослобођени ресурси оперативног система (file handle), који омогућавају да се фајл држи отвореним. У случају незатварања фајла (први начин писања података у фајл), може да дође до губитка података заосталих у баферу фајла, као и до продуженог заузећа ресурса, а тиме и до успореног рада система и ометања других програма у њиховом раду.

Сличне проблеме можемо да имамо и када објекти наше класе држе неке ресурсе отворене. То може да буде интернет конекција, велика количина меморије (нпр. велики Bitmap објекат) или било који други ресурс. Истина је да ће меморија свакако бити ослобођена посредством сакупљача отпада (енгл. garbage collector), али до тада може да прође извесно време, а да у међувремену систем буде успорен због недостатка оперативне меморије. Без обзира на врсту ресурса о коме је реч, било би добро да предности наредбе using можемо да користимо и за инстанце наше класе. Конкретно, било би корисно да уместо:

MojaKlasa a = new MojaKlasa();
// koristi objekat a
// ...

…можемо да пишемо:

using (MojaKlasa a = new MojaKlasa())
{
    // koristi objekat a
    // ...
}

Ако ово покушамо, добијамо следећу синтаксну грешку приликом компајлирања програма:

Error CS1674 ’MojaKlasa’: type used in a using statement must be implicitly convertible to ’System.IDisposable’.

Ово значи да класа MojaKlasa треба да имплементира интерфејс IDisposable, односно његов једини метод void Dispose(), да би могла да буде употребљена у наредби using. Следећи мали пример показује како то може да се уради.

public class MojaKlasa : IDisposable
{
    Bitmap bmp;
    public MojaKlasa()
    {
        bmp = new Bitmap(10000, 10000);
    }

    public void Dispose()
    {
        Console.WriteLine("Pozvan metod A.Dispose");
        bmp.Dispose();
    }
}


// ...

using (MojaKlasa a = new MojaKlasa())
{
    Console.WriteLine("Upotreba objekta a");
}
Console.WriteLine("kraj programa");

Извршавањем последња три реда исписује се:

Upotreba objekta a
Pozvan metod A.Dispose
kraj programa

Поред синтаксе потребне за имплементацију интерфејса IDisposable, пример показује да се по изласку из тела наредбе using позива метод Dispose() објекта a, што је и поента целог примера. Наравно, исписивање је додато само да би могао да се испрати редослед извршавања. У реалној употреби у методу Dispose() би се нашле све потребне наредбе за ослобађање ресурса које је објекат a заузимао. У овом примеру, то је само наредба bmp.Dispose(), која ослобађа све ресурсе које је заузимао објекат bmp.

(Created using Swinx, RunestoneComponents and PetljaDoc)
© 2022 Petlja
A- A+