Основни појмови о класама и објектима¶
У овој лекцији:
шта је објекат, а шта класа; који је њихов однос,
од чега се састоји класа; шта су поља, методи и својства,
приватни и јавни чланови класе.
Из самог назива објектно оријентисано програмирање је јасно да је централни појам ове парадигме – објекат. Зато, пре него што почнемо да се бавимо концептима ООП, потребно је да се укратко упознамо са његовим основним појмовима.
Објекти и класе¶
Однос између класа и објеката је исти као однос између типова и променљивих. Програмски језици најчешће подржавају више целобројних и реалних типова, логички тип и знаковни тип. Ове типове сматрамо основним типовима и они су део самог језика. Поред основних типова, можемо да користимо и типове које сами дефинишемо. Тако можемо да дефинишемо разне набројиве типове (Enum) и структуре.
У објектно оријентисаним језицима, класа је у суштини још један од типова које сами дефинишемо када пишемо програм. Након што дефинишемо класу, можемо у програму да користимо примерке (инстанце) те класе, као што користимо променљиве неког целобројног или другог типа. Примерци класе се зову објекти. За креирање објекта одређене класе често се користи израз инстанцирање, што значи стварање инстанце, односно примерка класе.
На основу дефиниције класе компајлер израчунава колико меморије ће бити потребно за сваки појединачан објекат те класе, где ће који од делова објекта у тој меморији да се налази, на који начин меморија иницијално треба да се попуни и како се касније користи. Посматрајући класе на тај начин, можемо да кажемо да је класа прецизно упутство за касније креирање објеката те класе и извршавање поступака над њима.
Класе су веома сличне структурама. Суштинска разлика између њих је то, што су структуре вредносни типови, а класе су референцирани типови. Подсетимо се шта то значи:
када су параметри метода, структуре се (као и променљиве простог типа) преносе по вредности, тј. копирају се, док се објекти класа (као и низови и матрице) преносе по референци, тј. методу се прослеђује информација о месту у меморији на коме се налази објекат;
за структуре се простор алоцира у статичкој меморији (на стеку позива функција), осим ако су део неког низа или објекта. За објекте се простор увек алоцира у динамичкој меморији, тј. на хипу.
Из ове главне разлике између класа и структура проистичу још неке разлике, којима се нећемо бавити у овом курсу. За сада можемо да сматрамо да се класе у свему понашају исто као и структуре (изузев када су параметри метода).
Чланови класе¶
Дефиниција класе садржи прецизан опис свих њених делова. Делови од којих се класа састоји називају се чланови класе. Ти делови (као код структуре) могу да буду поља, својства, методи, операторски методи, индексери и догађаји. У курсу за први разред (у лекцији о структурама) било је речи о пољима и својствима структура. Све што је тамо речено, важи и за поља и својства класа.
Поља¶
Поља су чланови класе у којима се налазе подаци неког једноставнијег типа (или референце на објекте
исте или друге класе, или низове). Поља обично чине неку логичку (смисаону) целину, тј. заједно
представљају некакав крупнији ентитет. На пример, класа Prozor
која представља положај прозора
на екрану, може да изгледа овако:
public class Prozor
{
public int prviRed;
public int prvaKolona;
public int visina;
public int sirina;
}
У овом примеру сваки објекат класе Prozor
можемо да схватимо као правоугаоник у матрици пиксела
екрана. Поља класе говоре у ком реду и колони матрице пиксела почиње тај прозор (prviRed
,
prvaKolona
), као и које су димензије тог прозора (visina
, sirina
).
Реч class
у првом реду значи да у наставку наводимо дефиницију класе. Реч public
испред
речи class
значи да је класа доступна у сваком делу пројекта који садржи ову дефиницију. Реч
Prozor
је име класе. Између заграда {
и }
налази се тело класе. У телу класе дефинишемо
од чега се класа састоји. У конкретном случају, класа Prozor
се састоји од четири поља prviRed
,
prvaKolona
, visina
, sirina
, сва четири типа int
.
Реч public
испред декларације поља значи да је то поље јавно, тј. да је доступно ван класе, да
вредност тог поља може да се чита и мења. Вредности појединих поља објекта се у програму користе
навођењем имена објекта, тачке и имена поља, овако: imeObjekta.imePolja
.
На пример, за претходно дефинисану класу Prozor
, објекте можемо да стварамо и користимо тако да
вредности поља постављамо, мењамо и очитавамо ван класе.
// постављање вредности поља
Prozor w = new Prozor() {prviRed = 5, prvaKolona = 6, visina = 9, sirina = 50};
// мењање вредности поља
w.visina += 5;
// очитавање вредности поља
Console.WriteLine("Visina prozora je {0}", w.visina);
Методи¶
Дефиниција класе може да садржи и функције, које зовемо методи. Методи дефинишу шта можемо да
урадимо са објектима дате класе. На пример, у тело класе Prozor
можемо да додамо метод
Povrsina
на следећи начин:
public class Prozor
{
public int prviRed;
public int prvaKolona;
public int visina;
public int sirina;
public double Povrsina()
{
return visina * sirina;
}
}
Овај метод израчунава површину прозора, тј. број пиксела у том прозору. Метод користимо на уобичајени начин, као и методе класа из библиотеке:
Prozor w = new Prozor() { prviRed = 5, prvaKolona = 6, visina = 20, sirina = 50 };
...
Console.WriteLine("Povrsina prozora je {0} piksela.", w.Povrsina());
Својства¶
Претпоставимо да поред уведених поља prviRed
, prvaKolona
, visina
и sirina
, желимо да
омогућимо кориснику класе да поставља и очитава вредности последњег реда и последње колоне прозора.
Један начин да то урадимо је да уведемо поља poslednjiRed
и poslednjaKolona
. Међутим, када
постоји веза између неких величина које описују објекте класе, таква да једна величина може да се
израчуна на основу осталих, као што је то случај са класом Prozor
и величинама prviRed
,
visina
и poslednjiRed
, није добро да за сваку од тих величина користимо по једно поље. Наиме,
у случају да за сваку од ове три величине користимо по једно поље, могло би се догодити да током
рада програма грешком буде нарушена једнакост prviRed + visina == poslednjiRed
, односно да
та три поља добију међусобно контрадикторне вредности и да објекат постане неконзистентан, тј.
нејасног смисла. Бољи начин да кориснику класе омогућимо да поред постојећих вредности очитава и
поставља и вредности последњег реда и колоне је употреба својстава.
Својство је члан класе који има извесне особине и метода и поља. Увођење својстава омогућава кориснику класе да о њима размишља као о пољима и да их тако и користи, а аутору класе да свако очитавање и постављање вредности неког својства „пресретне” и контролише, тако да се уместо простог очитавања или уписивања вредности изврши неки унапред припремљен кôд који обезбеђује конзистентност објекта.
Објаснимо синтаксу и семантику својстава детаљније. У коду ван дате класе, на месту где се својство
користи, синтакса својства је иста као за поље. Другим речима, гледајући само део кôда ван класе у коме
се користе поља и својства класе, не можемо да разликујемо својства од поља. Захваљујући томе, корисник
класе не мора ни да зна да ли стварно користи поље или својство (тј. може да замишља да користи поље).
Међутим, унутар класе се јасно види разлика између својства и поља. Својство се у класи записује, тј.
дефинише као пар специјалних метода, који се зову приступници (енгл. accessors). Један од та два
метода се зове get
, а други set
. Приступник get
је у суштини метод истог типа као и својство,
а његовим извршавањем се израчунава и враћа неки резултат, који корисник класе види као вредност својства.
Приступник set
је у суштини метод типа void
, тј. метод који не враћа вредност. Он као параметар
добија вредност коју корисник жели да додели својству, а у коду приступника set
тај параметар се увек
зове value
и не наводи се експлицитно као параметар. Тиме је аутору класе омогућено да на једноставан
начин одржава конзистентност објеката. Претпоставимо, на пример, да мењање последњег реда (колоне) желимо
да тумачимо као промену висине (ширине) прозора. Тада би претходна дефиниција класе могла да се прошири
додавањем својстава poslednjiRed
и poslednjaKolona
на следећи начин.
using System;
public class Prozor
{
public int prviRed;
public int prvaKolona;
public int visina;
public int sirina;
public int Povrsina()
{
return visina * sirina;
}
public int poslednjiRed
{
get { return prviRed + visina - 1; }
set { visina = value + 1 - prviRed; }
}
public int poslednjaKolona
{
get { return prvaKolona + sirina - 1; }
set { sirina = value + 1 - prvaKolona; }
}
};
После овакве дефиниције класе, у методу Main
бисмо могли да пишемо овакве наредбе:
class Program
{
static void Main(string[] args)
{
Prozor a = new Prozor {
prviRed = 20, prvaKolona = 10,
visina = 100, sirina = 200
};
Console.WriteLine(a.poslednjiRed);
a.poslednjiRed = 200;
Console.WriteLine(a.visina);
}
}
Наредбом Console.WriteLine(a.poslednjiRed);
извршава се приступник get
својства poslednjiRed
објекта a
, док се наредбом a.poslednjiRed = 200;
извршава приступник set
истог својства.
Резултат извршавања целог програма је
119
181
Уколико кориснику није потребно да и очитава и мења вредности неког својства, приликом писања тог
својства један од приступника get
, set
може и да се изостави (али могу не оба). На пример,
ако се аутор и корисник класе договоре да кориснику није потребно да експлицитно поставља вредност
последњег реда и колоне, класа може да се напише и овако:
public class Prozor
{
public int prviRed;
public int prvaKolona;
public int visina;
public int sirina;
public int Povrsina()
{
return visina * sirina;
}
public int poslednjiRed { get { return prviRed + visina - 1; } }
public int poslednjaKolona { get { return prvaKolona + sirina - 1; } }
};
У том случају, својства poslednjiRed
и poslednjaKolona
имају само приступник get
, што
значи да могу да се користе само за читање вредности. На пример, можемо да пишемо:
Prozor a = new Prozor {
prviRed = 20, prvaKolona = 10,
visina = 100, sirina = 200
};
Console.WriteLine(a.poslednjiRed);
…али не и:
a.poslednjiRed = 200;
…или:
a.poslednjiRed++;
…јер својство poslednjiRed
нема приступник set
. Обрнуто, ако би био изостављен приступник
get
, а задржан приступник set
, својство би могло да се користи само за додељивање вредности.
Приметимо да се за вредност својства не одваја простор у меморији коју заузима објекат, као што се то ради за поља. Када је кориснику класе потребна вредност својства, она се том приликом израчунава и за то се користе вредности поља. Слично томе, када корисник жели да зада вредност својства, та вредност може да се запамти у неком од поља објекта, али може и да послужи за сложеније провере и рачунања, на основу којих се мења вредност једног или више поља на неки сложенији начин.
Јавни и приватни чланови класе¶
У уводном делу је поменуто да је један од разлога за стварање класе била потреба да се доступност
неких података и неких функција ограничи. На пример, помоћу класа може једноставно да се постигне
да одређени чланови (поља и методи) класе не могу да се користе ван класе којој припадају. За то
је довољно да се изостави реч public
испред имена поља или метода.
Погледајмо шта би се догодило ако изоставимо реч public
испред имена метода Povrsina
:
public class Prozor
{
public int prviRed;
public int prvaKolona;
public int visina;
public int sirina;
int Povrsina()
{
return visina * sirina;
}
...
}
...
Console.WriteLine("Povrsina prozora je {0} piksela.", w.Povrsina());
Приликом покушаја да покренемо програм, добијамо следећу поруку о грешци (подразумева се окружење Visual Studio):
Ово значи да је метод Povrsina
недоступан због нивоа заштићености тог метода.
Грешка се односи на линију кода којом желимо да прикажемо површину прозора w
. Позивање метода
Povrsina
у тој линији кода је синтаксна грешка, зато што је та линија ван дефиниције класе
Prozor
. Наиме, пошто метод Povrsina
није означен као јаван, он аутоматски постаје приватан
за класу Prozor
и ван класе не може да се користи (није доступан).
Потпуно исто важи и за поља класе: изостављањем речи public
испред дефиниције тих поља, она
постају приватна за класу.
public class Prozor
{
int prviRed;
int prvaKolona;
int visina;
int sirina;
...
}
...
Prozor w = new Prozor() { prviRed = 5, prvaKolona = 6, visina = 20, sirina = 50 };
w.visina += 5;
Console.WriteLine("Visina prozora je {0}", w.visina);
Овога пута добијамо више синтаксних грешака, које се све односе на последње три линије кода у
примеру. У тим линијама се приступа вредностима поља w.prviRed
, w.prvaKolona
, w.visina
и w.sirina
прозора w
, а то због приватности ових поља није дозвољено ван тела класе
Prozor
.
У случају да програмерима који користе нашу класу желимо да омогућимо да читају вредност поља али
не и да је мењају, једноставно и често примењивано решење је да поље оставимо као приватно и да му
придружимо својство које има само приступник get
. На пример:
public class Prozor
{
int prviRed;
int prvaKolona;
int visina;
int sirina;
public int PrviRed { get { return prviRed; } }
public int PrvaKolona { get { return prvaKolona; } }
public int Visina { get { return visina; } }
public int Sirina { get { return sirina; } }
...
}
Поменимо и да члан класе може и експлицитно да буде проглашен за приватан, писањем речи private
уместо речи public
у дефиницији тог члана.
public class Prozor
{
private int prviRed;
private int prvaKolona;
...
Тиме се постиже исти ефекат као када само изоставимо реч public
у дефиницији метода. Ми ћемо
у примерима ипак експлицитно да означавамо приватне делове класе као такве, да бисмо истакли одлуку
да ти делови буду приватни. У пракси, тим програмера обично усвоји конвенцију о томе да ли ће код
приватних чланова (поља и метода) класе писати реч private
или неће. Мада ова одлука не утиче
на понашање програма, корисно је да се усвојено правило доследно примењује, јер доприноси
разумевању кода са мање напора (због створене навике).