Статички полиморфизам, оператори¶
У овој лекцији:
статички полиморфизам - дефинисање више истоимених метода или оператора у једној класи,
како да дефинишемо значење израза облика
a+b
и сличних, за објекте наше класе,како да дозволимо компајлеру да сâм (имплицитно) конвертује целе бројеве у разломке,
како да обезбедимо удобно исписивање објеката наше класе.
Истоимени методи¶
У методу Razlomak.Parse
, којим смо се бавили у лекцији о методима класе (статичким методима),
појавиле су се ове наредбе:
int.Parse(s.Substring(0, k)),
int.Parse(s.Substring(k + 1)));
Као што знамо, метод Substring
враћа подстринг стринга из којег је позван. Овај метод смо већ
користили и навикли смо на то да можемо да га позивамо на два начина:
када наведемо само један параметар, нпр.
s.Substring(odakle)
, резултујући подстринг садржи знакове од наведене позиције до краја стрингаs
;када наведемо два параметра, нпр.
s.Substring(odakle, koliko)
, резултујући подстринг садржиkoliko
знакова почев од наведене позицијеodakle
.
Врло често се иза ових различитих начина позивања заправо налазе различити методи, тако да би често
било тачније да се каже да имамо два или више истоимених метода једне класе (мада и један метод
понекад може да се позива на различите начине, ако има подразумеване вредности за неке параметре).
Да бисмо могли да разликујемо истоимене методе, потребно је да се разликују њихове листе параметара,
било по дужини, било по типу параметра на одређеној позицији. На пример, у случају метода
Substring
, један метод има два параметра а други један, па се на месту позива лако препознаје
који од ова два метода је позван. Напоменимо да истоимени методи могу да се разликују и по типу
враћене вредности, али да та разлика не може да буде једина, тј. мора да постоји и разлика у листи
параметара (у противном компајлер не би могао да закључи који од метода се позива на датом месту).
Особина језика да допушта истоимене методе у једној класи позната је под више назива, а један од њих је статички полиморфизам. Сама реч полиморфизам значи „појављивање у више облика”, па је тај део назива ове особине јасан. Нешто касније ћемо објаснити и зашто се овај тип полиморфизма назива статички.
Многи програмски језици (а међу њима и C#) нам допуштају да и сами пишемо класе са више истоимених метода. У следећем примеру ћемо видети како изгледа једна таква класа.

Написати и тестирати класу, која омогућава брзо сабирање сегмената датог, непроменљивог низа. Обезбедити методе који сабирају:
све елементе низа
елементе почев од задате позиције до краја низа
задати број елемената почев од задате позиције
Тражену класу ћемо да назовемо Sabirac
. Да би сабирање сегмената датог низа било брзо,
израчунаћемо збир сваког префикса полазног низа (укључујући и празан префикс). Након тога, збир
било ког сегмента можемо брзо да израчунамо као разлику збирова два префикса.
Израчунавање збирова префикса можемо да обавимо у конструктору класе. Потребна су нам још три
јавна метода који израчунавају збирове према захтевима задатка. Пошто сва три метода сабирају
елементе неког сегмента полазног низа, природно је да сва три метода назовемо Saberi
.
Програм који следи можете да прекопирате и испробате у свом радном окружењу.
Статички полиморфизам нам омогућава да не морамо да смишљамо и памтимо слична имена метода за сличне поступке над сличним подацима. Такође, корисници наше класе не морају да памте разлике у именима сличних метода, па чак ни да буду свесни да је уопште реч о различитим методима. Према томе, статички полиморфизам у суштини није ништа више него флексибилност програмског језика, која нам пружа одређену удобност при именовању метода једне класе.
Истоимени оператори¶
Вратимо се нашој класи Razlomak
. За пуну функционалност класе потребно је још да унутар класе
подржимо рачунске операције са разломцима. Један начин да то урадимо је да напишемо методе Saberi
,
Oduzmi
, Pomnozi
и Podeli
. Сваки од ових метода би могао да буде статички, са два
параметра типа Razlomak
и резултатом истог типа.
Ипак, за програмера који користи нашу класу би било удобније да може да рачуна са разломцима на исти начин као и са целим и реалним бројевима. На пример:
Razlomak a = Razlomak.Parse(Console.ReadLine());
Razlomak b = Razlomak.Parse(Console.ReadLine());
Razlomak c = 2*a+b; // umesto: Razlomak c = Saberi(Pomnozi(2, a), b);
...
Знамо да је, на пример, за оператор + већ подржан полиморфизам, јер помоћу оператора који се исто
пише (+
) можемо да сабирамо целе бројеве, реалне бројеве и стрингове. Према типу аргумената,
компајлер може да одреди о којем од неколико оператора сабирања је реч.
Дефинисање оператора
Језик C# подржава могућност да дефинишемо оператор сабирања и за објекте било које наше класе.
Решење је веома једноставно – довољно је да напишемо метод који се зове operator +
:
public static Razlomak operator +(Razlomak r, Razlomak s)
{
int d = NZD(r.b, s.b);
int nzs = (r.b / d) * s.b;
return new Razlomak(r.a * (s.b / d) + s.a * (r.b / d), nzs);
}
Након овога, ако су a
, b
, c
објекти класе Razlomak
, можемо да пишемо, нпр:
c = a + b;
На сличан начин можемо да подржимо и остале рачунске операције:
public static Razlomak operator -(Razlomak r)
{
return new Razlomak(-r.a, r.b);
}
public static Razlomak operator -(Razlomak r, Razlomak s)
{
int d = NZD(r.b, s.b);
int nzs = (r.b / d) * s.b;
return new Razlomak(r.a * (s.b / d) - s.a * (r.b / d), nzs);
}
public static Razlomak operator *(Razlomak r, Razlomak s)
{
int ra = r.a, rb = r.b, sa = s.a, sb = s.b;
Skrati(ref ra, ref sb);
Skrati(ref sa, ref rb);
return new Razlomak(ra * sa, rb * sb);
}
public static Razlomak operator /(Razlomak r, Razlomak s)
{
int ra = r.a, rb = r.b, sa = s.a, sb = s.b;
Skrati(ref ra, ref sa);
Skrati(ref rb, ref sb);
return new Razlomak(ra * sb, rb * sa);
}
Приметимо да смо дефинисали два метода који се зову operator -
. Први од њих има само један
параметар и он нам омогућава да пишемо нпр. a = -b;
. Другим речима, метод operator -
са
једним параметром одговара унарном минусу у изразима, тј. дефинише како се извршава унарни минус
(промена знака). Други метод са истим именом, који има два параметра, омогућава писање наредби
попут c = a - b;
, тј. дефинише како се -
извршава као бинарни оператор (оператор одузимања).
Методи operator *
и operator /
функционишу на исти начин, тј. дефинишу како се извршавају
оператори *
и /
. Ови оператори су могли да буду имплементирани и једноставније, али смо их
написали на овај начин (користећи скраћивање) да бисмо смањили могућност да дође до прекорачења
опсега целобројне променљиве.
Да бисмо подржали и изразе попут a - 3
или 2 * a
, можемо да напишемо и ове методе:
public static Razlomak operator +(Razlomak r, int n)
{
return r + new Razlomak(n);
}
public static Razlomak operator +(int n, Razlomak r)
{
return r + new Razlomak(n);
}
public static Razlomak operator -(Razlomak r, int n)
{
return r - new Razlomak(n);
}
public static Razlomak operator -(int n, Razlomak r)
{
return r - new Razlomak(n);
}
public static Razlomak operator *(Razlomak r, int n)
{
return r * new Razlomak(n);
}
public static Razlomak operator *(int n, Razlomak r)
{
return r * new Razlomak(n);
}
public static Razlomak operator /(Razlomak r, int n)
{
return r / new Razlomak(n);
}
public static Razlomak operator /(int n, Razlomak r)
{
return r / new Razlomak(n);
}
У свих осам ових метода смо помоћу конструктора са једним параметром од целог броја добили разломак, а затим искористили претходно дефинисан оператор коме су оба параметра разломци. Мада је и ово прихватљиво решење, проблем можемо да решимо и елегантније. Да бисмо дошли до тог елегантнијег решења, подсетимо се како функционише имплицитна конверзија типа.
Цео број се имплицитно конвертује у реалан када се по синтакси на месту где је наведен цео,
очекује реалан број. На пример, када желимо да израчунамо \(\sqrt 2\) не морамо да пишемо
баш Math.Sqrt(2.0)
(са реалним параметром). Лако можемо да се уверимо да програм ради и
са Math.Sqrt(2)
, мада не постоји посебан метод Math.Sqrt
са целобројним параметром.
Када компајлер наиђе на позив метода (или оператора) са параметрима који не одговарају директно
ни једној од истоимених верзија метода, он покушава дозвољеним имплицитним конверзијама да
прилагоди типове параметара неком од постојећих метода са тим именом. Тако се целобројно 2
имплицитно конвертује у реално 2.0 и метод може да се позове.
Дефинисање имплицитне конверзије
У језику C# имамо могућност да дефинишемо имплицитну конверзију једног типа у други, коју ће компајлер да примени где је потребно да се тип прилагоди очекиваном, као што то ради са уграђеним основним типовима. Имплицитна конверзија целог броја у разломак може да се напише овако:
public static implicit operator Razlomak(int n)
{
return new Razlomak(n);
}
Овим методом смо дозволили компајлеру да имплицитно претвара целе бројеве у разломке када тиме може да избегне синтаксну грешку због неслагања типова. Захваљујући томе, претходних осам метода који дефинишу операције између целог броја и разломка постају непотребни. Додатно, у свакој будућој ситуацији где се у коду очекује разломак а наведен је цео број, компајлер ће моћи да разреши ситуацију помоћу имплицитне конверзије.
Подешавање приказа на екрану
Да би рад са разломцима био сасвим удобан, недостаје још само подршка да се вредност разломка
прикаже на екрану. Тренутно, за разломак r
би се наредбом
Console.WriteLine(r);
добио испис Razlomak
. То је зато што се и овде примењује нека врста имплицитне конверзије.
Наиме, метод WriteLine
класе Console
очекује стринг као параметар, па у оваквим сутуацијама
компајлер покушава да наведени параметар имплицитно претвори у стринг. То се ради применом метода
ToString
, који је дефинисан за све објекте било ког типа. Пошто компајлер не може да зна како
ми желимо да се приказује објекат класе коју смо сами писали (док му то не кажемо), он примењује
подразумевани метод ToString
, који дати објекат замењује именом његове класе. То је разлог
зашто смо у претходном случају добили испис Razlomak
.
Ово понашање можемо једноставно да променимо, тако што у нашој класи дефинишемо метод ToString
без параметара, који враћа стринг којим желимо да представимо објекат.
public override string ToString()
{
if (a == 0) { return "0"; }
if (b == 1) { return a.ToString(); }
return a.ToString() + "/" + b.ToString();
}
Приметимо да у дефиницији овог метода треба да се наведе реч override
, чиме наглашавамо да
желимо да прегазимо (редефинишемо, надјачамо) постојећи метод који такође нема параметара
(override на енглеском значи прегази). Редефинисање метода ToString
је такође неки вид
полиморфизма, али њиме ћемо се нешто детаљније бавити касније.
Следи пример са допуњеном класом Razlomak
, који омогућава учитавање разломака са тастатуре,
приказ на екрану, поређење и рачунање са разломцима (у примеру се решава линеарна једначина са
разломцима). Пример можете да копирате и испробате у свом радном окружењу.