Апстрактни методи и класе¶
У овој лекцији:
шта доносе апстрактни методи и апстрактне класе,
пример употребе апстрактне класе и апстрактних метода,
апстрактне класе као облик динамичког полиморфизма.
Да бисмо најлакше разумели у којој ситуацији апстрактни методи и класе могу да нам буду од користи и шта тачно помоћу њих добијамо, овај пут полазимо од конкретног задатка, који ћемо да искористимо као студијски пример. Задатак ћемо прво да решимо на начин који нам је познат, да сагледамо недостатке таквог решења и дођемо до мотивације за увођење апстрактних метода и класа. После објашњења потребних детаља у вези са апстрактним методима и класама, излажемо и решење засновано на њима, уз поређење свих датих решења.
Студијски пример – обими и површине кругова и многоуглова¶

Дато је неколико кругова и правилних многоуглова, који су различито обојени. За сваки круг позната је боја и полупречник, а за сваки многоугао боја, број страница и дужина странице.
Потребно је да се за сваку фигуру израчуна обим и површина те фигуре, а за сваку боју укупна површина те боје.
Задатак је довољно једноставан да би му решење без класа и објеката било сасвим примерено. Међутим, овде желимо управо да на малом примеру уведемо један нови концепт, који ће се показати као врло користан у већим задацима.
Прво решење¶
За укупне површине по бојама можемо да користимо речник који пресликава боје у површине. Поред
тога, за сваку фигуру треба да израчунамо обим и површину. Природно је да у овом тренутку замишљамо
класе Krug
и Ntougao
са методима Obim
и Povrsina
. Прво решење би могло да користи
ове две класе као независне.
Програм исписује
Figura: Boja=Color [Red], Obim=31.42, Povrsina=78.54
Figura: Boja=Color [Green], Obim=43.98, Povrsina=153.94
Figura: Boja=Color [Red], Obim=24.00, Povrsina=36.00
Figura: Boja=Color [Blue], Obim=12.00, Povrsina=10.39
Ukupna povrsina boje Color [Red] je 114.54
Ukupna povrsina boje Color [Green] je 153.94
Ukupna povrsina boje Color [Blue] je 10.39
У делу програма где се рачунају обими и површине (линије 41-58) видимо две скоро исте петље.
Првом од њих се пролази кроз листу кругова, а другом кроз листу многоуглова. У сложенијем примеру
можемо да замислимо да такве петље буду знатно дуже, па би било добро да се оне некако сведу на
једну петљу. Да бисмо то постигли, треба да имамо заједничку листу за кругове и многоуглове, а то
значи да нам је потребна заједничка базна класа, коју можемо да назовемо Figura2D
(дводимензиона
фигура). У овој класи би се нашло поље boja
и својство Boja
, али за сада није јасно како бисмо
позивали методе Obim
и Povrsina
.
Друго решење¶
Комплетности ради, напомињемо да језик C# (као и већина објектно оријентисаних програмских језика)
нуди могућност да за дату референцу на базну класу проверимо да ли указује на објекат одређене
изведене класе. Поред других начина, у језику C# у ту сврху могу да се употребе оператори is
и
as
, као што показује следећи исечак кода:
foreach (Figura2D f in figure)
{
double obim = 0, povrsina = 0;
if (f is Krug)
{
Krug k = f as Krug;
obim = k.Obim();
povrsina = k.Povrsina();
}
else if (f is Ntougao)
{
Ntougao p = f as Ntougao;
obim = p.Obim();
povrsina = p.Povrsina();
}
Console.WriteLine("Figura: Boja={0}, Obim={1:0.00}, Povrsina={2:0.00}",
f.Boja, obim, povrsina);
double prethPov = 0;
povBoje.TryGetValue(f.Boja, out prethPov);
povBoje[f.Boja] = prethPov + povrsina;
}
Логички услов (f is Krug)
у овом случају има вредност true
ако је објекат на који указује
референца f
инстанца класе Krug
, а вредност false
у супротном. У општем случају,
вредност true
би се добила и да је објекат инстанца неке класе компатибилне са класом Krug
(нпр. класе изведене из класе Krug
).
Наредбом Krug k = f as Krug;
креирамо референцу k
класе Krug
, која указује на исти
објекат као и референца f
, ако је тај објекат инстанца класе Krug
(или неке компатибилне
класе).
У противном, референца k
добија вредност null
.
На овај начин претходне две петље се заиста своде на једну, али поента је некако промашена. Овакво решење није нарочито удобно и елегантно јер морамо да водимо рачуна о типу фигуре, а нисмо се у потпуности ослободили ни понављања кода (позиви метода за израчунавање обима и површине се и даље јављају на два места). Зато овај приступ не препоручујемо за решавање постављеног проблема. Комплетан програм можете да видите ако кликнете на дугме испод.
Комплетан програм са провером типа у време извршавања
Резултат рада програма је исти као у првом решењу.
Треће решење¶
Удобно решење би требало да нам омогући да завршни део програма изгледа овако:
foreach (Figura2D f in figure)
{
Console.WriteLine("Figura: Boja={0}, Obim={1:0.00}, Povrsina={2:0.00}",
f.Boja, f.Obim(), f.Povrsina());
double prethPov = 0;
povBoje.TryGetValue(f.Boja, out prethPov);
povBoje[f.Boja] = prethPov + f.Povrsina();
}
Уколико бисмо пробали да убацимо позиве f.Obim()
и f.Povrsina()
у заједничку петљу у
другом решењу (у коме су класе Krug
и Ntougao
изведене из класе Figura2D
), добили
бисмо синтаксне грешке, јер у класи Figura2D
нема ових метода. Са друге стране, методе
Obim
и Povrsina
не можемо да дефинишемо у базној класи, јер обим и површина се за круг и
многоугао израчунавају на различите начине. Према томе, потребно нам је да базна класа зна за ове
методе да не бисмо добијали синтаксне грешке, а да се дефиниције метода ипак налазе у изведеним
класама. Управо ту могућност нам доносе апстрактни методи.
Апстрактан метод је метод који у базној класи није дефинисан, већ је само декларисан (најављен) својим нивоом приступа, именом, типом повратне вредности и типовима параметара.
Испред апстрактног метода се пише реч abstract
. Она компајлеру (и другим програмерима) говори
да метод није дефинисан и да се очекује да буде дефинисан у класама наследницама.
Када нека класа има бар један апстрактан, недефинисан метод, она није довршена и објекти те класе не
могу да буду креирани (класа не може да се инстанцира). Таква класа се назива апстрактна класа и
такође се означава речју abstract
на свом почетку. Да би класа која непосредно или посредно (кроз
хијерархију) наслеђује апстрактну класу могла да има инстанце, неопходно је да у њој (или у класама
које јој претходе у хијерархији) буду дефинисани сви апстрактни методи базне класе.
Свако накнадно дефинисање (као и редефинисање) у некој од класа наследница означава се речју
override
. Реч override
значи да се метод редефинише у односу на базну класу, а може (и не
мора) да се даље редефинише у класама наследницама.
У трећем решењу базну класу проглашавамо за апстрактну и додајемо јој декларације апстрактних метода
Obim
и Povrsina
. Обратите пажњу на то да се реч abstract
појављује на три места у базној
класи.
public abstract class Figura2D
{
protected Color boja; // boja figure
public Figura2D(Color b) { boja = b; }
public Color Boja { get { return boja; } }
public abstract double Povrsina();
public abstract double Obim();
}
У дефиницијама метода у изведеним класама треба да назначимо да се ради о дефиницији управо апстрактног
метода декларисаног у базној класи. Обратите пажњу на реч override
у изведеним класама.
public class Krug : Figura2D
{
private double r;
public Krug(Color b, double r0) : base(b) { r = r0; }
public override double Povrsina() { return r * r * Math.PI; }
public override double Obim() { return 2 * r * Math.PI; }
}
public class Ntougao : Figura2D
{
private double a;
private int n;
public Ntougao(Color b, double a0, int n0) : base(b) { a = a0; n = n0; }
public override double Povrsina()
{
double rUpisanogKruga = 0.5 * a / Math.Tan(Math.PI / n);
return n * 0.5 * a * rUpisanogKruga;
}
public override double Obim() { return n * a; }
}
Ево како изгледа цео програм, који користи апстрактну базну класу:
Резултат рада програма је исти као у прва два решења.
Видимо да се инстанциране фигуре користе помоћу референци на базну класу на униформан начин,
исти за све објекте.
Приликом употребе (петља foreach (Figura2D f in figure)
) не морамо да водимо рачуна о томе која
фигура је круг, а која многоугао, али се ипак обим и површина сваке од њих израчунавају правилно, тј.
у складу са стварном природом фигуре.
Различито понашање једнако третираних објеката, тј. понашање објеката у складу са њиховим стварним типом је појава коју називамо динамички полиморфизам.
Подсетимо се, раније смо помињали статички полиморфизам и тада смо реч полиморфизам објаснили као појављивање у више облика. Статички полиморфизам подразумева употребу више истоимених метода у оквиру једне класе. Ти методи се разликују по броју и типу параметара, па већ у време превођења (компајлирања) постоји довољно информација да се сваки од позива тих метода разреши, тј. да се закључи о којем од неколико истоимених метода се ради. Зато тај вид полиморфизма називамо статички.
За разлику од тога, облик полиморфизма који смо управо упознали не може да се разреши у време превођења (статички). То значи да се позив метода разрешава тек у време извршавања програма, па се зато овај вид полиморфизма назива динамички.
Следеће наредбе појашњавају зашто позив метода преко референце на базну класу не може да се разреши у време превођења програма.
Figura2D f;
string s = Console.ReadLine();
if (s == "krug")
f = new Krug(Color.Red, 5);
else
f = new Ntougao(Color.Red, 6, 4);
Console.WriteLine(f.Povrsina());
У оваквом случају врста објекта на који указује f
зависи од уноса корисника, па је јасно да
компајлер нема информацију на какав објекат указује референца f
у последњем реду. То нам
говори да у време превођења не може да се закључи о којем од два истоимена метода Povrsina
се ради (по један метод се налази у свакој од изведених класа).
Ово је први пример динамичког полиморфизма у овом курсу. Он је овде остварен помоћу апстрактне базне класе, што је најважнији, али не и једини начин остваривања динамичког полиморфизма.
Динамичким полиморфизмом ћемо се бавити у већем делу овог поглавља, а у наредним поглављима ћемо видети и неке његове примене које доприносе мањој сложености кода тамо где је то заиста потребно.
Сложеност у случају динамичког полиморфизма није на једном месту, већ је разложена и распоређена по класама наследницама.