Книга е още в много ранна фаза на написване


Обекти и техните времена на живот



страница12/73
Дата25.07.2016
Размер13.53 Mb.
#6732
1   ...   8   9   10   11   12   13   14   15   ...   73

Обекти и техните времена на живот


Технически погледнато, ООП е точно за абстрактните типове дании, на­сле­дя­ва­не­то и полиморфизма, но и други неща са също поне толкова важни. Оста­на­ла­та част от тази секция ще се занимае с тях.

Един от най-важните фактори е начинът по който се създават и разрушават обек­ти. Къде са данните на обекта и как се управлява времето на живот на обек­та? Има различни философии на въпроса, които работят. C++ има за най-важни въпросите за управление на ефективността, така че дава на програмиста из­бор. За максимална скорост на изпълнение още по време на писане на про­гра­ма­та обектите се слагат на стека (понякога са наричани автоматични или с об­хват променливи) или в поле от статична памет. Това слага приоритета на въпросите за алокиране и освобождаване на памет и този контрол може да бъде из­вънредно ценен в някои ситуации. По този начин, обаче, се убива гъв­ка­вост­та, понеже трябва да се знае всичко за обектите по време на писането на про­гра­мата. Ако се опитвате да решавате по-общ проблем като CAD например, скла­дово стопанство или контрол на въздушния транспорт, това е твърде огра­ни­чаващо.

Другият подход е да се създават обектите в област, наречена heap. При този под­ход не се знае точно колко обекти ще има по време на изпълнение, колко е тях­ното време на живот и точният им тип. Тези неща се определят по време на из­пълнение, както се случи в програмата. Ако трябва нов обект, той просто се съз­дава на хийпа щом стане нужен. Понеже паметта се управлява динамично, зна­чително повече време трябва да се алокира или освободи памет, отколкото в слу­чая, когато тя е върху стека. (Често алокирането на памет върху стека е асемблерска инструкция за запис в стековия указател, респективно осво­бож­да­ва­нето - запис в указателя променящ го в противната посока.) Динамичният под­ход има тенденция да са усложнени, така че допълнителното време за от­де­ля­не и освобождаване на памет ще е малко в сравнение с общото време на из­пъл­нение. В добавка голямата гъвкавост е основна предпоставка за решаването на общия програмен проблем.

C++ позволява на програмиста да определи дали обектите ще се създават по вре­ме на написването на програмата (т.е. по време на компилацията - б.пр.) или по време на изпълнение, позволявайки по този начин управление на ефек­тив­ност­та. Би могло да се помисли, че, понеже е по-гъвкаво, обектите трябва ви­на­ги да се създават в хийпа, а не на стека. Има обаче и друго нещо и то е времето на живот на обекта. Ако се създаде обект на стека или в статичната памет, ком­пи­латорът може да определи времето на живот на този обект и да определи ко­га да го разруши. Ако обаче се създаде обект в хийпа, компилаторът не знае то­ва. Програмистът ема две възможности за разрушаване на обекта: може про­грам­но да се определи кога да се разруши обекта или програмната среда може да доставя услугата събиране на боклука която автоматично открива кога обек­тът е вече ненужен и го разрушава. Разбира се, събирачът на боклук е много по-удобен, но се изисква всички програми да са съгласувани с него и има до­пъл­нителен разход на ресурси за неговата работа. Това не отговаря на изиск­ва­ния­та към езика C++ и затова не е включено, но Java има събирач на боклук (как­то и Smalltalk; Delphi няма, но може да се добави такъв. Съществуват събирачи за C++ произведени от "странични" доставчици).

Останалата част от тази секция разглежда допълнителни фактори, засягащи обек­тите и времето им на живот.

Колекции и итератори


Ако не знаете колко обекта ще ви трябвата за решаването на даден проблем или кол­ко време те ще просъществуват вие също не знаете къде да ги сложите. Как да знаете колко място да отделите? Това не може да стане, понеже инфор­ма­ция­та ще е достъпна чак по време на изпълнение.

Решението на повечето проблеми в ООП изглежда лекомислено: създавате друг тип обект. Новият обект който решава конкретния проблем съдържа ма­ни­пу­ла­то­ри на други обекти. Разбира се, може да се направи нещо подобно и с масив, което е достъпно в повечето езици. Но наличното тук е повече. Този нов обект, на­речен изобщо колекция (също наричан контейнер, но Swing GUI библиотеката използва този термин в друг смисъл, така че в тази книга ще се из­ползва“колекция”), ще се разширява от самосебе си за да поеме всичко, което ще решите да сложите в нея. Така че не е необходимо да се знае колко обекта ще се слагат в колекцията. Само се създава обекта колекция и се оставя да се гри­жи за детайлите.

За щастие един добър ООП език идва с набор добри колекции. В C++ това е Standard Template Library (STL). Object Pascal има колекции в неговата Visual Com­ponent Library (VCL). Smalltalk има много завършено множество от ко­лек­ции. Java също има колекции в стандартната си библиотека. В някои би­блио­те­ки общата колекция се счита за добра за всички нужди, в други (C++ в част­ност) има различни типове колекции за различните нужди: виктор за смислен до­стъп до всеки елемент, свързан лист за смислена итерация по елементите, на­при­мер да можете да изберете подходящ тип за своите нужди. Може да включ­ват мрежи, опашки, хеш таблици, дървета, стекове и т.н.

Всички колекции имат начин да слагат неща вътре и да ги вадят навън. Начинът да се сложи нещо во колекцията е очевиден. Има функция наречена “push” или “add” или нещо подобно. Извличането на неща от колекцията не е винаги тол­ко­ва непосредствено; ако е нещо подобно на масив, като вектор например, мо­же да е възможно да се използува индексиращ оператор или функция. Но в мно­го ситуации това няма значение. Също, функция за избиране само на един еле­мент е много ограничаваща. Ако искате да работите с или да сравнявате ня­колко неща в колекцията?

Решението е итератор, чието предназначение е да избира елементи от колекцията и да ги представя на потребителя на итератора. Като клас ите­ра­то­рът също дава някакво ниво на абстракция. Тази абстракция може да се из­пол­зу­ва за разделяне на детайлите на колекцията от кода, който извлича еле­мен­ти­те. Колекцията, чрез итератора, се свежда просто до последователност (от еле­мен­ти -б.пр.). Итераторът позволява да се работи с тази последователност без да се познават детайлите на истинската структура – тоест дали е вектор, свър­зан списък, стек или нещо друго. Това дава гъвкавостта лесно да се променя под­лежащата структура без да се проминя кода на приложната програма. Java за­почна (във версии 1.0 и 1.1) със стандартен итератор, наречен Enumeration, за всичките си класове-колекции. Java 2 добави Iterator който прави много по­ве­че от стария Enumeration.

От гледна точка на проектирането всичко, което е нужно, е последователност, коя­то решава конкретния пробем. Ако една единствена последователност ре­ша­ва проблема, няма нужда от повече. Има две причини за необходимост от из­бор на колекция. Първо, колекциите дават различни интерфейси и поведение. Стекът има различен интерфейс и поведение от опашката, която е различна от мно­жеството или списъка. Един от тези типове би могло да дава по-добро ре­шение на вашия проблем от другите. Второ, различните колекции имат раз­лич­на ефективност в различните ситуации. Най-добрият пример са векторът и спи­съ­кът. Двете са прости последователности, които могат да имат еднакви интер­фейси и поведение. Но някои операции могат да имат много различна цена. До­стъпът до случайни елементи от масива е за едно и също време винаги. За свър­за­ния списък (лист -б.пр.) такава операция би отнела много време, ако еле­ментът е дълбоко в списъка. От друга страна, вмъкването на елемент някъде по сре­дата е лесно в списъка и отнемащо много време в масива. Тези и други опе­ра­ции имат различна ефективност в зависимост от подлежащата структура. Във фа­зата на проектирането може да се започне със списък и после, когато се на­строй­ва производителността, да се премине към масив. Поради абстракцията чрез итераторите това може да стане чрез минимална промяна на кода.

Накрая, запомнете, че колекцията е просто шкаф от памет за слагане на обекти вътре. Ако шкафът удовлетворява всичките нужди, няма значение как е на­пра­вен (основна концепция с повечето типове обекти). Ако работите в програмна сре­да, която има допълнителни разходи на ресурси, сължащи се на други фак­то­ри (работата под Windows, например, или цената на събирача на боклук), то­га­ва разликата в ефективността на свързания списък и масива може да няма зна­че­ние. Може да се нуждаете само от един тип последователност. Може даже да си въобразите “перфектна” абстракция на колекция, която може автоматично да сменя типа в зависимост от използваната подлежаща система.

Йерархия с един корен


Едно от нещата в ООП, което стана доста забележително след въвеждането на C++ е въпросът дали всичко в края на краищата ще е наследник на един един­ствен клас . В Java (както и в практически всички останали ООП езици) от­го­во­рът е "да" и прародителят на всички класове е просто Object. Това показва, че ползите от йерархията с един корен са много.

Всички обекти в такава йерархия имат общ интерфейс, така че те в края на краищата са от един тип. Алтернативата (като в C++) е, че не може да се каже, че всичко е от някакъв фундаментален тип. От гледна точка на обратната съв­ме­стимост това повече подхожда за преход от C и може да изглежда по-малко огра­ничаващо, но когато се иска да се направи напълно ООП се налага самостаятелно да се направи същото, което вече го има в другите ООП езици. И каквато и нова библиотека да се вземе, тя ще бъде с някакъв нов несъвместим ин­терфейс. Това изисква усилието (и може би многократното наследяване) да се вмести новия интерфейс. Заслужава ли си това допълнителната “гъвкавост” на C++ ? Ако се нуждаете от нея – ако имате голяма инвестиция в C – много да­же си заслужава. Ако започвате от нулата, други варианти като Java често мо­гат да бъдат по-перспективни.

All objects in a singly-rooted hierarchy (such as Java provides) can be guaranteed to have certain functionality. You know you can perform certain basic operations on every object in your system. A singly-rooted hierarchy, along with creating all objects on the heap, greatly simplifies argument passing (one of the more complex topics in C++).

Йерархията с един корен прави много по-лесно вграждането на събирач на бо­клука. Необходимата поддръжка може да се постави в базовия клас и тогава тряб­ва само събирачът да изпрати съответните съобщения до всички обекти в си­стемата. Без йерархията с един корен и система, която управляма обектите чрез манипулатори е много трудно да се направи събирач на боклука.

Доколкото информацията по време на изпълнение е във всички обекти, никога про­грамата не може да завърши с обект, чийто тип не може да се определи. То­ва е важно специално при системните операции като обработката на изклю­че­ния и за правене на по-гъвкави програми.

Може да се чудите защо, като е толкова полезна, йерархията с един корен не е пред­ставена в C++. Това е старата надпревара между ефективността и управ­ле­нието. Йерархията с един корен налага ограничения върху проектирането и в част­ност на съществуващия C код. Тези ограничения представляват проблем са­мо в някои случаи, но като цяло не е наложена йерархията с един корен в C++. В Java, който започна от нулата и нямаше изисквания за обратна съв­ме­сти­мост с никой език, логично бе представена йерархията с един корен, както и в повечето други ООП езици.


Библиотеки от колекции и поддръжка за лесно използване


Тъй като колекциите са нещо, което се използва често, полезно е да има биб­лио­теки от повторно използваеми колекции, от където се взема каквото е под­хо­дящо и се използува. Java има такава колекция, въпреки че едоста ограничена в Java 1.0 и 1.1 (Би­блио­теката от колекции на Java 2 обаче удовлетворява повечето нужди).

Downcasting и templates/generics


За да бъдат колекциите повторно използваеми те съдържат фундаменталния тип в Java който беше споменат по-рано: Object. Йерархията с един корен зна­чи, че всичко е Object, така че колекция която държи Object-и може да държи ка­квото и да е. Това я прави лесна за повторно използване.

За да се използва такава колекция просто се добавят манипулатори към нея, а след това се изискват обратно. Но, доколкото колекцията съдържа само Object-и, когато се добавя манипулатор става upcasting към Object, като по то­зи начин се губи идентичността. Когато се взема обратно се полечава ма­ни­пу­ла­тор на Object , а не този, който е бил добавен. Как се връщаме към нещото с по­лезен интерфейс, което сме сложили в колекцията?

Пак се използва casting, но този път не е нагоре към по-общ тип, а надолу по йе­рар­хията към по-специфичен тип. Това се нарича downcasting. С upcasting, на­при­мер, се знае, че Circle е от типа на Shape така че може безопасно да се на­пра­ви upcast, но не се знае дали Object е непременно Circle или Shape така че ма­нипулаторът не може сигурно да се преобразува, ако не знаете точно с какъв тип имате работа.

Това не е чак толкова опасно, понеже ако се направи опит за неправилно прео­бра­зуване, по време на изпълнение ще се получи изключение, което ще опишем на­кратко. Когато извличате манипулатори от колекцията трябва да има начин да се помни типът им, за да се правят подходящи преобразувания.

Downcasting-ът и проверките по време на изпълнение довеждат до изразходване на допълнително време и допълнителни усилия на програмиста. Би ли имало смисъл да се създаде колекция, която да помни типовете и т.н., та да не се налага всеки път да се прави? Решението е параметризирани типове, кои­то са класове, които компилаторът може автоматично да приспособява за кон­кретния случай. Например, с параметризирана колекция, компилаторът мо­же да я направи да работи само с Shape-ове и за извлича само Shape-ове.

Параметризираните типове са важна част от C++, частично защото C++ няма йерархия с един корен. В C++ ключовата дума за прилагане на пара­ме­три­зи­ра­ни­те типове е template. Java в момента няма параметризирани типове, но те мо­гат да се направят – тромаво, обаче – използвайки йерархията с един корен. От ед­на страна думата generic (ключовата дума използвана в Ada за неговите templates) беше в списъка на думите “резервирани за бъдещо приложение.” Ня­кои от тези думи мистериозно се плъзнаха в “Бермудския триъгълник” на клю­чо­вите думи и е трудно да се каже какво би могло да се случи.


Домакинската дилема:
кой ще чисти?


Всеки обект се нуждае от ресурси за да съществува, от които първа е паметта. Когато обектът вече не е необходим тези ресурси трябва да се освободят за пов­торно използване. В прости програмни ситуации въпросът колко съ­ще­ству­ва обекта не изглежда голямо предизвикателство: създавате обекта, той съ­ще­ству­ва колкото е необходимо и после трябва да се разруши. Не е много трудно, оба­че, да се намерят ситуации в които отговорът е много по-сложен.

Да предположим, например, че се проектира система за управление на трафика за летище. (Същият модел е за управление на палетите в складово стопанство, за видеоленти под наем и за боксовете за пансион на домашни любимци.) От­на­ча­ло изглежда просто: прави се колекция да сложим аеропланите, после се пра­ви обект и се вкарва в колекцията за всеки самолет, който влиза в зоната на управ­ление на трафика. За почистване просто се изтрива обекта за всеки аеро­план, който напуска зоната.

Но може да има и друга система, която записва данни за самолетите; може би тя не изисква такава бърза намеса като прякото управление на полетите. Може да е запис на маршрутите на всички малки самолети, които напускат летището. Та­ка че имаме втора колекция от малки самолети и винаги, когато се създава обект свързан с аероплан, той се вкарва и във втората колекция, ако самолетът е малък. После някакъв фонов процес върши необходимото в моменти на без­дей­ствие на инсталацията.

Се­га проблемът е по-тежък: как бихте могли да знаете кога да се разрушат обек­тите? Когато главният процес е приключил с даден обект, някаква друга част от системата може да не е. Същия проблем може да възникне в множество си­туа­ции и в програмни системи (като C++) в които обектът трябва явно да се уни­щожи, когато не е необходим, може да стане много сложен.6

В Java събирачът на боклук е проектиран да се грижи за освобождаването на па­метта (въпреки че това не включва други аспекти от унищожаването на обек­ти­те). Събирачът “знае” когато един обект вече не се използва и автоматично осво­бождава паметта на обекта. Това комбинирано с факта, че всички обекти са наследници на единствен клас Object и че може да се създават обекти по единствен начин: на хийпа, прави процеса на програмиране на Java много по-прост от този на C++. Имате много по-малко решения за вземане и препятствия за преодоляване.

Събирачите на боклук срещу
ефективността и гъвкавостта


Ако всичко това е толкова добра идея, защо не са направили и C++ така? Разбира се, има цена за всичкото това програмистко удобство и тази цена е до­пъл­нителния разход на ресурси по време на изпълнение. Както се спомена пре­ди, в C++ и в този случай те автоматично се почистват (но нямате гъвкавостта да създавате колкото ви трябват по време на изпълнение). Създаването на обек­ти на стека е най-ефективния начин за заемане и освобождаване на паметта. Създаването на обекти в хййпа може да бъде много по-скъпо. Наследяването ви­наги на базовия клас и правенето на всички извиквания на функции поли­морф­ни също събира своя малък данък. Но събирачът на боклук е отделен про­блем, защото никога не се знае кога ще се включи и колко ще работи. Това значи, че има неопределено време на реакцията на Java програма, така че не мо­же да я използвате в някои ситуации, където времето на реакция е критично. (Та­кива ситуации се наричат изобщо програми в реално време, макар и не всич­ки изисквания на програмирането в реално време да са толкова строги.)7

Проектантите на езика C++, опитвайки се да ухажват C програмистите (и най-успешно - докато го правеха), не искаха да добавят черти, които могат да за­сег­нат скоростта или използването на C++ в каквито и да са ситуации, където C би мо­гъл да бъде използван. Тази цел беше постигната, но на цената на по-голама сложност на програмирането на C++. Java е по-прост от C++, но недостатъкът е в ефективността и понякога в приложимостта. За значителна част от про­грам­ните задачи, обаче, Java често е най-добрият избор.





Сподели с приятели:
1   ...   8   9   10   11   12   13   14   15   ...   73




©obuch.info 2024
отнасят до администрацията

    Начална страница