Образователна програма за писане на езици за програмиране. Основни принципи на програмиране: статично и динамично писане Силно и слабо писане

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


означаваща операция енад обект, прикрепен към х, предавайки аргумента арг(възможни са множество аргументи или никакви). Програмистите на Smalltalk говорят в този случай за „преминаване към обект хсъобщения ес аргумент арг“, но това е просто разлика в терминологията и следователно е незначителна.

Това, че всичко се основава на тази основна конструкция, отчасти обяснява усещането за красотата на идеите на ОО.

Ненормални ситуации, които могат да възникнат по време на изпълнението, следват от Основната конструкция:

Определение: нарушение на типа

Нарушение на типа по време на изпълнение или просто нарушение на типа за кратко се случва в момента на повикването x.f (arg), където хприкрепен към обект OBJако едно от двете:

[х].няма компонент, съответстващ на еи приложимо за OBJ,

[х].има такъв компонент обаче, аргументът арге неприемливо за него.

Проблемът с въвеждането е избягването на ситуации като тази:

Проблемът с въвеждането на OO системи

Кога установяваме, че може да възникне нарушение на типа по време на изпълнението на OO система?

Ключовата дума е кога... Рано или късно ще разберете, че има нарушение на типа. Например, опит за изпълнение на компонента Torpedo Launcher за обект Employee няма да работи и ще се провали. Въпреки това, може да предпочетете да намерите грешки възможно най-рано, отколкото по-късно.

Статично срещу динамично писане

Докато са възможни междинни опции, тук са представени два основни подхода:

[х]. Динамично писане: изчакайте момента на приключване на всяко обаждане и след това вземете решение.

[х]. Статично писане: Въз основа на набор от правила, определете от изходния текст дали са възможни нарушения на типа по време на изпълнение. Системата се изпълнява, ако правилата гарантират, че няма грешки.

Тези термини са лесни за обяснение: при динамично въвеждане проверката на типа се извършва по време на изпълнение (динамично), докато при статично въвеждане проверката на типа се извършва върху текста статично (преди изпълнение).

Статичното писане предполага автоматична проверкаобикновено се присвоява на компилатора. В резултат на това имаме проста дефиниция:

Определение: статично въведен език

Езикът OO е статично въведен, ако идва с набор от последователни правила, проверени от компилатора, за да се гарантира, че изпълнението на системата не води до нарушение на типа.

В литературата терминът " силенпишете "( силен). Той отговаря на ултиматума на дефиницията, която не изисква никакво нарушение на типа. Възможно и слаб (слаб) форми на статично типизиране, при които правилата премахват определени нарушения, без да ги елиминират изцяло. В този смисъл някои OO езици са статично слабо типизирани. Ще се борим за най-силното писане.

Динамично въведените езици, известни като нетипизирани езици, нямат декларации за тип и всяка стойност може да бъде прикачена към обекти по време на изпълнение. В тях не е възможна статична проверка на типа.

Правила за писане

Нашата OO нотация е статично въведена. Неговите типови правила бяха въведени в предишни лекции и се свеждат до три прости изисквания.

[х].Когато се декларира всеки обект или функция, трябва да се посочи неговият тип, напр. acc: АКАУНТ... Всяка подпрограма има 0 или повече формални аргументи, чийто тип трябва да бъде посочен, например: сложи (x: G; i: ЦЯЛО ЧИСЛО).

[х].Във всяка задача x: = yи за всяко извикване на подпрограма, в което ге действителният аргумент за формалния аргумент х, Тип на източника гтрябва да е съвместим с целевия тип х... Определението за съвместимост се основава на наследяване: Бсъвместим с Аако е негов потомък, допълнен с правила за генерични параметри (вж. Лекция 14).

[х].Повикване x.f (arg)изисква това ебеше компонент на базов клас за целевия тип х, и етрябва да бъдат експортирани в класа, в който се появява повикването (вижте 14.3).

Реализъм

Въпреки че определението за статично въведен език е доста точно, то не е достатъчно – необходими са неформални критерии при създаване на правила за писане. Помислете за два крайни случая.

[х]. Напълно правилен език, в който всяка синтактично правилна система е правилна по отношение на типове. Не са необходими правила за деклариране на тип. Такива езици съществуват (представете си полската нотация за израз със събиране и изваждане на цели числа). За съжаление, нито един истински универсален език не отговаря на този критерий.

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

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

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

Да кажем, че езикът реалистиченако е подходящ за употреба и полезен на практика. За разлика от определението за статично писане, което дава категоричен отговор на въпроса: " X статично въведен ли е?“, определението за реализъм е отчасти субективно.

В тази лекция ще проверим дали нотацията, която предлагаме, е реалистична.

песимизъм

Статичното писане води по своята същност до "песимистична" политика. Опит да се гарантира това всички изчисления не водят до неуспехи, отхвърля изчисления, които биха могли да завършат без грешка.

Помислете за обикновен, необектен, подобен на Pascal език с различни типове ИСТИНСКИи ЦЯЛО ЧИСЛО... При описанието n: ЦЯЛО ЧИСЛО; r: Истинскиоператор n: = rще бъде отхвърлен като нарушение на правилата. По този начин компилаторът ще отхвърли всички от следните твърдения:


Ако ги активираме, ще видим, че [A] винаги ще работи, тъй като всяка бройна система има точно представяне на реалното число 0,0, което може да бъде недвусмислено преведено в 0 цели числа. [B] почти сигурно също ще работи. Резултатът от действието [C] не е очевиден (искаме ли да получим общата сума, като закръглим или отхвърлим дробната част?). [D] ще свърши своята работа, точно като оператора:


ако n ^ 2< 0 then n:= 3.67 end [E]

което включва недостижимото задание ( n ^ 2е квадратът на числото н). След подмяна n ^ 2на нсамо серия от стартирания ще даде правилния резултат. Възлагане нголяма стойност с плаваща запетая, която не може да бъде представена с цяло число, ще доведе до неуспех.

В въведените езици всички тези примери (работещи, неработещи, понякога работещи) се тълкуват безмилостно като нарушения на правилата за писане и се отхвърлят от всеки компилатор.

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

Статично писане: как и защо

Въпреки че ползите от статичното писане са очевидни, добре е да поговорим отново за тях.

Предимства

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

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

Ранното откриване на грешки също е важно, защото колкото по-дълго отлагаме откриването им, толкова повече ще се увеличат разходите за отстраняването им. Това свойство, интуитивно разбирано от всички професионални програмисти, е количествено потвърдено от добре познатите произведения на Бьом. Зависимостта на разходите за фиксиране от времето на намиране на грешки е показана на графиката, изградена според данните от редица големи индустриални проекти и експерименти, проведени с малък управляем проект:

Ориз. 17.1.Сравнителни разходи за корекции на грешки (публикувани с разрешение)

Четимостили Лекота за разбиране(четимост) има своите предимства. Във всички примери в тази книга появата на тип върху обект дава на читателя информация за неговата цел. Четимостта е изключително важна по време на фазата на поддръжка.

накрая, ефективностможе да определи успеха или неуспеха на обектната технология на практика. При липса на статично въвеждане, за изпълнение x.f (arg)може да отнеме колкото искате. Причината за това е, че по време на изпълнение не се намира ев основния клас на целта х, търсенето ще продължи сред нейните потомци, а това е сигурен път към неефективността. Можете да облекчите проблема, като подобрите търсенето на компонент в йерархията. Провеждат авторите на Self language добра работав опит да генерира по-добър код за динамично въведен език. Но именно статичното писане позволи на такъв OO продукт да се доближи или равен по ефективност на традиционния софтуер.

Ключът към статичното писане е вече изразената идея, че компилаторът генерира кода за конструкцията x.f (arg), познава вида х... Поради полиморфизма няма начин да се определи еднозначно подходящата версия на компонента е... Но декларацията стеснява многото възможни типове, позволявайки на компилатора да изгради таблица, която осигурява достъп до правилния ес минимални разходи, - ограничена константасложността на достъпа. Извършени допълнителни оптимизации статично свързванеи вграждане- също са улеснени от статично писане, като напълно се елиминират излишните разходи, където е приложимо.

Аргументи за динамично писане

Въпреки всичко това, динамичното писане не е загубило своите привърженици, особено сред програмистите на Smalltalk. Техните аргументи се основават предимно на реализма, разгледан по-горе. Те вярват, че статичното писане ги ограничава твърде много, пречи им да изразяват свободно творческите си идеи, понякога го наричат ​​„колан за целомъдрие“.

Може да се съгласим с това разсъждение, но само за статично въведени езици, които не поддържат редица функции. Струва си да се отбележи, че всички понятия, свързани с понятието тип и въведени в предишните лекции, са необходими - отхвърлянето на което и да е от тях е изпълнено със сериозни ограничения, а въвеждането им, напротив, дава на нашите действия гъвкавост и дава ни възможността да се насладим напълно на практичността на статичното писане.

Писане: условията за успех

Какви са механизмите за реалистично статично писане? Всички те бяха представени в предишните лекции и затова трябва само накратко да ги припомним. Изброяването им заедно показва последователността и силата на комбинирането им.

Нашата типова система се основава изцяло на концепцията клас... Дори основни типове като напр ЦЯЛО ЧИСЛО, и следователно не се нуждаем от специални правила за описание на предварително дефинирани типове. (Тук нашата нотация се различава от "хибридните" езици като Object Pascal, Java и C ++, където типовата система на по-старите езици се комбинира с базирана на клас технология за обекти.)

Разширени типовени дават повече гъвкавост, като позволяват типове, чиито стойности обозначават обекти, както и типове, чиито стойности означават препратки.

Решаващата дума при създаването на система от гъвкав тип принадлежи на наследствои свързана концепция съвместимост... Това преодолява основното ограничение на класическите въведени езици, например Pascal и Ada, в които операторът x: = yизисква вида хи гбеше същото. Това правило е твърде строго: то забранява използването на обекти, които могат да обозначават обекти от свързани типове ( СПЕСТОВНА СМЕТКАи CHECKING_ACCOUNT). При наследяване изискваме само съвместимост на типове гс тип х, Например, хе от тип СМЕТКА, y - СПЕСТОВНА СМЕТКА, а вторият клас е наследник на първия.

На практика статично въведен език се нуждае от поддръжка множествено наследяване... Има фундаментални обвинения за статичното писане, че не дава възможност за интерпретиране на обекти по различен начин. И така, обектът ДОКУМЕНТ(документ) може да се предава по мрежата и следователно се нуждае от компоненти, свързани с типа СЪОБЩЕНИЕ(съобщение). Но тази критика е вярна само за езици, ограничени до единично наследяване.

Ориз. 17.2.Множествено наследяване

Универсалносте необходимо, например, за описание на гъвкави, но безопасни контейнерни структури от данни (напр клас СПИСЪК [G] ...). Без този механизъм статичното въвеждане би изисквало деклариране на различни класове за списъци с различни типове елементи.

В някои случаи е необходима гъвкавост лимит, което ви позволява да използвате операции, които са приложими само за обекти от общ тип. Ако генеричният клас SORTABLE_LISTподдържа сортиране, изисква обекти от типа г, където г- общ параметър, наличието на операция за сравнение. Това се постига чрез свързване с гкласът, дефиниращ генеричното ограничение - СРАВНИМ:


клас SORTABLE_LIST ...

Всеки действителен общ параметър SORTABLE_LISTтрябва да е потомък на класа СРАВНИМкойто има необходимия компонент.

Друг задължителен механизъм е опит за възлагане- организира достъпа до тези обекти, чийто тип софтуерът не контролира. Ако ге обект на база данни или обект, получен през мрежата, след това операторът х? = уще възложи хсмисъл г, ако ге от съвместим тип, или, ако не е, ще даде хсмисъл Празно.

твърдениясвързани като част от идеята за проектиране по договор с класове и техните компоненти под формата на предпоставки, постусловия и инварианти на класове, правят възможно описването на семантични ограничения, които не са обхванати от спецификация на типа. В езици като Pascal и Ada има типове диапазони, които могат да ограничат стойностите на даден обект, например до интервала от 10 до 20, но, като ги използвате, няма да можете да постигнете стойността ибеше отрицателно, винаги двойно повече j... Инвариантите на класа идват на помощ, предназначени да отразяват точно наложените ограничения, без значение колко сложни са те.

Фиксирани рекламиса необходими, за да се избегне дублирането на лавинен код на практика. Обявяване y: като x, получавате гаранция, че гще се промени след всякакви повтарящи се декларации като хпотомъкът. Без този механизъм разработчиците биха били безмилостно заети с повторни декларации, опитвайки се да поддържат различните типове последователни.

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

При разработването на софтуерни системи всъщност се изисква още едно свойство, което е присъщо на самата среда за разработка - бързо инкрементално прекомпилиране... Когато пишете или модифицирате система, искате да видите ефекта от промяната възможно най-скоро. При статично въвеждане трябва да дадете време на компилатора да провери типа. Традиционните компилационни процедури изискват повторно компилиране на цялата система (и неговите събрания) и този процес може да бъде мъчително дълъг, особено с прехода към широкомащабни системи. Това явление се превърна в аргумент в полза на тълкуванесистеми, като ранните среди на Lisp или Smalltalk, които стартираха системата с малко или никаква обработка, без проверка на типа. Този аргумент сега е забравен. Добрият модерен компилатор открива как кодът се е променил след последната компилация и обработва само промените, които открива.

— Бебето написано ли е?

Нашата цел - строгстатично писане. Ето защо трябва да избягваме всякакви вратички в нашата „игра по правилата“ или поне да ги идентифицираме точно, ако съществуват.

Най-често срещаната вратичка в статично въведените езици е наличието на преобразувания, които променят типа на обект. В C и неговите производни те се наричат ​​"casting" или casting (cast). Записване (OTHER_TYPE) xпоказва, че стойността хсе възприема от компилатора като притежаващ типа OTHER_TYPE, предмет на някои ограничения за възможните типове.

Такива механизми заобикалят ограниченията на проверката на типа. Прехвърлянето е широко разпространено в програмирането на C, включително диалекта ANSI C. Дори в C ++ кастингът, макар и не толкова често, остава често срещан и може би необходим.

Придържането към статичните правила за писане не е лесно, ако те могат да бъдат заобиколени по всяко време чрез кастинг.

Писане и свързване

Въпреки че като читател на тази книга със сигурност ще правите разлика между статично и статично писане. обвързване, има хора, които не могат да направят това. Това може да се дължи отчасти на влиянието на Smalltalk, който се застъпва за динамичен подход към двата проблема и може да подведе хората, че имат едно и също решение. (Ние твърдим в нашата книга, че е желателно да се комбинира статично въвеждане и динамично свързване, за да се създават стабилни и гъвкави програми.)

Както въвеждането, така и свързването се занимават със семантиката на основната конструкция x.f (arg)но отговорете на два различни въпроса:

Писане и свързване

[х]. Въпросът за писане: когато трябва да знаем със сигурност, че по време на изпълнение ще има операция, съответстваща на еприложимо към обект, прикрепен към обект х(с параметър арг)?

[х]. Въпрос за свързване: кога трябва да знаем каква операция инициира дадено повикване?

Писането отговаря на въпроса за наличността поне единоперации, обвързването е отговорно за избора необходимо.

В рамките на обектния подход:

[х].проблемът с писането е с полиморфизъм: дотолкова доколкото хпо време на изпълнениеможе да обозначава обекти от няколко различни типа, трябва да сме сигурни, че операцията, представляваща е, на разположениевъв всеки един от тези случаи;

[х].проблемът с обвързването е причинен от повтарящи се съобщения: тъй като един клас може да променя наследените компоненти, може да има две или повече операции, които претендират да представляват ев това обаждане.

И двете задачи могат да се решават както динамично, така и статично. И четирите решения са представени на съществуващите езици.

[х].Редица необектни езици, като Pascal и Ada, прилагат както статично въвеждане, така и статично свързване. Всеки обект представлява обекти само от един тип, посочени статично. Това гарантира надеждността на решението, цената на което е неговата гъвкавост.

[х]. Smalltalk и други езици на ОО съдържат динамични връзки и възможности за динамично писане. В същото време се дава предпочитание на гъвкавостта за сметка на надеждността на езика.

[х].Някои необектни езици поддържат динамично въвеждане и статично свързване. Те включват асемблерни езици и редица скриптови езици.

[х].Идеите за статичното въвеждане и динамичното свързване са въплътени в нотацията, предоставена в тази книга.

Обърнете внимание на особеността на езика C ++, който поддържа статично въвеждане, макар и не строго поради наличието на прехвърляне на типове, статично свързване (по подразбиране), динамично свързване при изрично посочване на виртуални ( виртуален) реклами.

Причината за избора на статично въвеждане и динамично свързване е очевидна. Първият въпрос е: "Кога ще разберем за съществуването на компонентите?" - предлага статичен отговор: " Колкото по-скоро, толкова по-добре", което означава: по време на компилиране. Вторият въпрос," Кой компонент да използвам? "предлага динамичен отговор:" този, от който се нуждаете", - съответстващ на типа на динамичния обект, който се определя по време на изпълнение. Това е единственото приемливо решение, ако статичното и динамичното свързване дава различни резултати.

Следният пример за йерархия на наследяване ще помогне за изясняване на тези понятия:

Ориз. 17.3.Видове самолети

Помислете за обаждането:


my_aircraft.lower_landing_gear

Въпросът за въвеждане: кога да се уверите, че ще има компонент долно_шаси(„удължен колесник“), приложим за обекта (за КОПТЕРизобщо няма да бъде) Въпросът за обвързването: коя от няколко възможни версии да изберете.

Статичното свързване би означавало, че игнорираме типа на обекта, който се прикачва, и разчитаме на декларацията за обект. В резултат на това, когато работим с Boeing 747-400, бихме нарекли версията, разработена за конвенционалните самолети от серия 747, а не за тяхната модификация 747-400. Динамичното свързване прилага операцията, изисквана от обекта, и това е правилният подход.

При статично въвеждане компилаторът няма да отхвърли повикването, ако може да се гарантира, че при изпълнение на програмата към обекта моя_самолетобектът, снабден с компонента, съответстващ на долно_шаси... Основната техника за получаване на гаранции е проста: със задължителна декларация моя_самолетбазовият клас от неговия тип се изисква да включва такъв компонент. Така моя_самолетне може да се декларира като САМОЛЕТИтъй като последният няма долно_шасина това ниво; хеликоптерите, поне в нашия пример, не знаят как да освободят колесника. Ако декларираме субекта като САМОЛЕТ, - класът, съдържащ необходимия компонент - всичко ще бъде наред.

Динамичното въвеждане в стил Smalltalk изисква изчакване на обаждането и проверка за наличието на необходимия компонент в момента на неговото изпълнение. Това поведение е възможно за прототипи и експериментални проекти, но неприемливо за индустриални системи - в момента на полет е твърде късно да попитате дали имате колесник.

Ковариация и скриване на деца

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

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

Ковариация

Какво се случва с аргументите на компонент при отмяна на неговия тип? Това е основен проблем и вече сме виждали редица примери за неговото проявление: устройства и принтери, едно- и двусвързани списъци и т.н. (виж раздели 16.6, 16.7).

Ето още един пример, който да ви помогне да изясните естеството на проблема. И дори да е далеч от реалността и метафоричност, близостта му до програмните схеми е очевидна. Освен това, анализирайки го, често ще се връщаме към проблемите от практиката.

Представете си университетски отбор по ски, който се подготвя за шампионата. клас МОМИЧЕвключва жени скиори, МОМЧЕ- скиори. Класирани са редица участници от двата отбора, които са показали добри резултати в предишни състезания. Това е важно за тях, защото сега те ще бягат първи, спечелвайки предимство пред останалите. (Това правило, което дава привилегии на вече привилегированите, е може би това, което прави слалома и ски бягането толкова привлекателни в очите на много хора, което е добра метафора за самия живот.) Така че имаме два нови класа: RANKED_GIRLи RANKED_BOY.

Ориз. 17.4.Класификация на скиорите

Резервирани са редица стаи за настаняване на спортисти: само за мъже, само за момичета, само за призьори. За да покажем това, използваме йерархия на паралелни класове: СТАЯ, GIRL_ROOMи RANKED_GIRL_ROOM.

Ето скица на класа СКИОР:


- Съсед по номер.
... Други възможни компоненти, пропуснати в този и следващите класове ...

Интересуват ни два компонента: атрибутът съквартиранти процедура дял, "поставяйки" този скиор в същата стая като текущия скиор:


При деклариране на субект другиможете да откажете вида СКИОРв полза на фиксирания тип като съквартирант(или като Currentза съквартиранти другиедновременно). Но нека забравим за момент за закрепването на типовете (ще се върнем към тях по-късно) и да разгледаме проблема с ковариацията в първоначалния му вид.

Как да въведем отмяна на типа? Правилата изискват отделно настаняване за момчета и момичета, победители и други участници. За да решим този проблем, при отменяне ще променим типа на компонента съквартиранткакто е показано по-долу (по-нататък отменените елементи са подчертани).


- Съсед по номер.

Нека съответно предефинираме аргумента на процедурата дял... По-пълна версия на класа сега изглежда така:


- Съсед по номер.
- Изберете друг като съсед.

По същия начин, всички генерирани от СКИОРкласове (сега не използваме фиксиране на типа). В резултат на това имаме йерархия:

Ориз. 17.5.Йерархия на членовете и предефиниране

Тъй като наследяването е специализация, правилата за типове изискват при отмяна на резултата от компонент, в този случай съквартирант, новият тип беше потомък на оригинала. Същото важи и за отмяната на типа аргумент другиподпрограми дял... Тази стратегия, както знаем, се нарича ковариация, където префиксът "ko" показва съвместна промяна на типовете параметри и резултати. Обратната стратегия се нарича контравариантност.

Всички наши примери са убедително доказателство за практическата необходимост от ковариация.

[х].Едносвързан елемент от списъка СВЪРЗВАНЕтрябва да бъде свързан с друг елемент, подобен на себе си, и с екземпляра BI_LINKВЪЗМОЖНО- с някой като теб. Covariantly ще трябва да бъде отменен и аргументът да бъде включен сложи_правно.

[х].Всяка подпрограма в композицията LINKED_LISTс аргумент като СВЪРЗВАНЕкогато отиваш на TWO_WAY_LISTще изисква аргумент BI_LINKВЪЗМОЖНО.

[х].Процедура set_alternateвзема УСТРОЙСТВО-спор в клас УСТРОЙСТВОи ПРИНТЕР-аргумент – в класа ПРИНТЕР.

Ковариантните заменяния са особено популярни, тъй като скриването на информация води до създаване на процедури от формата


- Задайте атрибут на v.

да работя с атрибутТип SOME_TYPE... Такива процедури са естествено ковариантни, тъй като всеки клас, който променя типа на атрибут, трябва съответно да отмени аргумента. set_attrib... Въпреки че представените примери се вписват в една схема, ковариацията е много по-разпространена. Помислете например за процедура или функция, която извършва конкатенация на едносвързани списъци ( LINKED_LIST). Аргументът му трябва да бъде предефиниран като двусвързан списък ( TWO_ WAY_LIST). Универсална операция за събиране инфикс "+"взема ЦИФРОВО-спор в клас ЦИФРОВО, ИСТИНСКИ- в клас ИСТИНСКИи ЦЯЛО ЧИСЛО- в клас ЦЯЛО ЧИСЛО... Паралелни йерархии от телефонна услуга към процедура започнетев клас PHONE_SERVICEможе да се наложи аргумент АДРЕСпредставляващи адреса на абоната (за фактуриране), докато същата процедура в класа CORPORATE_SERVICEще има нужда от аргумент като CORPORATE_ADDRESS.

Ориз. 17.6.Комуникационни услуги

Какво ще кажете за контравариантно решение? В примера със скиори, това би означавало, че ако, отивайки на час RANKED_GIRL, тип резултат съквартирантпредефиниран като RANKED_GIRL, тогава, поради противоречие, типът на аргумента дялможе да бъде отменен за въвеждане МОМИЧЕили СКИОР... Единственият тип, който не е разрешен в контравариантно решение, е RANKED_GIRL! Достатъчно, за да събуди най-лошите подозрения у родителите на момичетата.

Паралелни йерархии

За да не оставите камък необърнат, разгледайте вариант на примера СКИОРс две паралелни йерархии. Това ще ни позволи да симулираме ситуация, която вече се среща на практика: TWO_ WAY_LIST> LINKED_LISTи BI_LINKABLE> ВЪЗМОЖНО; или йерархия с телефонна услуга PHONE_SERVICE.

Нека имаме йерархия с клас СТАЯчийто потомък е GIRL_ROOM(Клас МОМЧЕпропуснат):

Ориз. 17.7.Скиори и стаи

Нашите класове скиори са в тази паралелна йерархия вместо съквартиранти дялще има подобни компоненти настаняване (настаняване) и настани (място):


описание: "Нов вариант с паралелни йерархии"
accommodate (r: СТАЯ) е ... изисквам ... правя

Тук също са необходими ковариантни замени: в класа МОМИЧЕ 1как настаняванеи аргумента на подпрограмата настанитрябва да се замени с тип GIRL_ROOM, в клас МОМЧЕ 1- Тип BOY_ROOMи т.н. (Не забравяйте, че ние все още работим без закрепване.) Както в предишния пример, тук контравариантността е безполезна.

Своеволието на полиморфизма

Няма ли достатъчно примери, потвърждаващи практичността на ковариацията? Защо някой би помислил за противоречие, което противоречи на необходимото на практика (ако не се вземе предвид поведението на някои млади хора)? За да разберете това, разгледайте проблемите, които възникват при комбинирането на полиморфизма и ковариационната стратегия. Лесно е да измислите схема за саботаж и може би вече сте създали такава сами:


създаване b; създаване на g; - Създаване на обекти BOY и GIRL.

Резултатът от последното обаждане, доста вероятно приятен за младите мъже, е точно това, което се опитахме да избегнем със замяна на типа. Повикване дялводи до факта, че обектът МОМЧЕ, познат като би благодарение на полиморфизма, получения псевдоним сТип СКИОР, става съсед на обекта МОМИЧЕпознат като ж... Въпреки това обаждането, въпреки че противоречи на правилата на хостела, е съвсем правилно в текста на програмата, т.к дял-експортиран компонент като част от СКИОР, а МОМИЧЕ, тип аргумент ж, съвместим с СКИОР, типът на формалния параметър дял.

Схемата на паралелната йерархия е също толкова проста: заменете СКИОРна СКИОР1, повикване дял- да се обади s.accommodate (gr), където гр- тип обект GIRL_ROOM... Резултатът е същият.

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

Следователно, без да се опитваме да дърпаме контравариантно облекло върху ковариантно тяло, човек трябва да приеме ковариантната реалност и да търси начини за премахване нежелан ефект.

Скриване от дете

Преди да потърсим решение на проблема с ковариацията, нека разгледаме друг механизъм, който може да доведе до нарушения на типа в полиморфизма. Скриването на потомък е способността на клас да не експортира родителски компонент.

Ориз. 17.8.Скриване от дете

Типичен пример е компонентът add_verex(добавете връх), експортиран по клас ПОЛИГОНно скрит от неговия потомък ПРАВОЪГЪЛНИК(поради възможно нарушение на инварианта - класът иска да остане правоъгълник):


Пример за непрограмист: класът Ostrich скрива метода Fly, който е получил от родителя Bird.

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


създайте r; - Създаване на обект RECTANGLE.
p: = r; - Полиморфно присвояване.

Тъй като обектът rкриейки се под същността стрклас ПОЛИГОН, а add_verexекспортиран компонент ПОЛИГОН, след това неговото оспорване от субекта стрправилно. В резултат на изпълнение в правоъгълника ще се появи друг връх, което означава, че ще бъде създаден невалиден обект.

Коректност на системите и класовете

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

Системата се нарича валиден за систематаако няма нарушение на типа, когато се изпълнява.

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

Практически аспект

Простотата на проблема създава един вид парадокс: любознателен начинаещ ще изгради контрапример за броени минути; в реалната практика грешките в коректността на класа на системите се появяват всеки ден, но нарушенията на коректността на системата дори в големи, многогодишни проекти са изключително редки.

Това обаче не ни позволява да ги игнорираме и затова започваме да изучаваме три възможни начина за решаване на този проблем.

След това ще засегнем много фини и не толкова често усещащи се аспекти на обектния подход. Когато четете тази книга за първи път, можете да пропуснете останалите раздели от тази лекция. Ако наскоро сте се заели с OO технологията, тогава трябва по-добре да овладеете този материал, след като изучите лекции 1-11 от курса "Основи на обектно-ориентирания дизайн" по методологията на наследяването и особено лекция 6 от курса "Основи на Обектно-ориентирано проектиране" относно наследяването на методологията.

Коректност на системите: първо приближение

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

Контравариантност и невариантност

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

Оригиналността на езика C ++ е, че той използва стратегията новарианциябез да ви позволява да променяте типа на аргументите в отменените подпрограми! Ако C ++ беше силно въведен език, неговите системни типове биха били трудни за използване. Най-простото решение на проблема на този език, както и заобикалянето на други ограничения на C ++ (да речем, липсата на ограничена универсалност), е да се използва кастинг - тип кастинг, което ви позволява напълно да игнорирате съществуващия механизъм за въвеждане. Това решение не изглежда привлекателно. Забележете обаче, че редица предложения, разгледани по-долу, ще разчитат на дисперсията, чието значение ще бъде дадено от въвеждането на нови механизми за работа с типове вместо ковариантни отмяна.

Използване на общи параметри

Универсалността е в основата на една интересна идея, инициирана от Франц Вебер. Да обявим клас СКИОР1чрез ограничаване на универсализацията на общия параметър до класа СТАЯ:


характеристика на клас SKIER1
accommodate (r: G) е ... изисквам ... направете настаняване: = r край

След това класът МОМИЧЕ 1ще наследник СКИОР1и т. н. Същата техника, колкото и странна да изглежда на пръв поглед, може да се използва при липса на паралелна йерархия: клас СКИОР.

Този подход решава проблема с ковариацията. Всяко използване на класа изисква действителният общ параметър да бъде посочен СТАЯили GIRL_ROOM, така че грешната комбинация просто става невъзможна. Езикът става невариантен и системата напълно отговаря на нуждите от ковариация благодарение на общите параметри.

За съжаление тази техника е неприемлива като общо решение, тъй като води до разпространение на общи параметри, по един за всеки тип възможен ковариантен аргумент. По-лошото е, че добавянето на ковариантна подпрограма с аргумент от тип, който не е в списъка, ще изисква добавяне на общ параметър на класа и следователно ще промени интерфейса на класа, причинявайки промени на всички клиенти на класа, което е неприемливо.

Типични променливи

Редица автори, включително Ким Брус, Дейвид Шанг и Тони Саймънс, са предложили решение, базирано на тип променливи, чиито стойности са типове. Идеята им е проста:

[х].вместо ковариантни заменяния, позволете декларации на тип, използвайки променливи на типа;

[х].разширете правилата за съвместимост на типовете, за да контролирате такива променливи;

[х].предоставят възможност за присвояване на езикови типове като стойности на променливи на типа.

Читателите могат да намерят подробно представяне на тези идеи в редица статии по тази тема, както и в публикации на Cardelli, Castagna, Weber и др. Можете да започнете да изучавате въпроса от източниците, посочени в библиографските бележки към тази лекция . Няма да се занимаваме с този проблем и ето защо.

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

[х].Да предположим, че сте разработили общ променлив механизъм, който може да преодолее проблемите на комбинирането на ковариация и полиморфизъм (като все още игнорирате проблема със скриването на деца). Тогава от разработчика на класа ще се изисква да изключителна интуицияза да се реши предварително кой от компонентите ще бъде достъпен за отмяна на типове в производните класове и кой не. По-долу ще обсъдим този проблем, който възниква в практиката на създаване на програми и, уви, който поставя под съмнение приложимостта на много теоретични схеми.

Това ни принуждава да се върнем към вече обсъдените механизми: ограничена и неограничена универсалност, закрепване на типа и, разбира се, наследяване.

Разчитайки на закрепване на типа

Ще намерим почти готово решение на проблема с ковариацията, като разгледаме отблизо механизма на закрепените декларации, който познаваме.

При описание на класове СКИОРи СКИОР1не можеше да не посетите желанието, използвайки закрепените декларации, да се отървете от много замени. Закотвяването е типичен ковариантен механизъм. Ето как ще изглежда нашия пример (всички промени са подчертани):


споделяне (друго: като Current) е ... изисквам ... правя
accommodate (r: като настаняване) е ... изисквам ... правя

Сега потомците могат да напуснат класа СКИОРнепроменен и в СКИОР1те трябва само да заменят атрибута настаняване... Залепващи обекти: атрибут съквартиранти аргументи към подпрограми дяли настани- ще се промени автоматично. Това значително опростява работата и потвърждава факта, че при липса на обвързване (или друг подобен механизъм, например тип променливи), е невъзможно да се напише OO софтуерен продукт с реалистично писане.

Но успяхте ли да премахнете нарушенията на коректността на системата? Не! Както и преди, можем да надхитрим проверката на типа, като изпълняваме полиморфни присвоения, които нарушават коректността на системата.

Вярно е, че оригиналните версии на примерите ще бъдат отхвърлени. Позволявам:


създаване b; създаване g; - Създаване на обекти BOY и GIRL.
s: = b; - Полиморфно присвояване.

Аргумент жпредадени дял, сега е неправилно, тъй като изисква обект от тип като sи класа МОМИЧЕе несъвместим с този тип, тъй като според правилото за фиксираните типове нито един тип не е съвместим с като sосвен за себе си.

Въпреки това няма да се радваме дълго. В другата посока това правило казва това като sсъвместим с типа с... Така че, използвайки полиморфизъм не само на обект с, но и параметърът ж, можем отново да заобиколим системата за проверка на типа:


с: СКИОР; б: МОМЧЕ; g: като s; действителен_g: МОМИЧЕ;
създаване b; create current_g - Създава обекти BOY и GIRL.
s: = действителен_g; g: = s - Използвайте s, за да добавите g към МОМИЧЕТО.
s: = b - Полиморфно присвояване.

В резултат на това незаконното обаждане преминава.

Има изход. Ако сме сериозно готови да използваме закрепването на декларациите като единствен механизъм на ковариация, тогава можем да се отървем от нарушенията на коректността на системата, като напълно деактивираме полиморфизма на закрепените обекти. Това ще изисква промяна на езика: ще въведем нова ключова дума котва(тази хипотетична конструкция ни е необходима единствено, за да я използваме в тази дискусия):


Разрешаване на декларации за тип като sсамо когато сописано като котва... Нека променим правилата за съвместимост, за да гарантираме: си елементи като като sмогат да бъдат свързани само (в присвояване или чрез предаване на аргумент) един към друг.

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


докато класът ° С- родител дизглеждаше


където Йотговаряше хслед това сега отменя компонента rще изглежда така:


Остава само в клас дтип замяна your_anchor.

Това решение на проблема за ковариацията - полиморфизма ще се нарече подход Закотвяне... По-точно би било да се каже: "Ковариация само чрез обвързване". Свойствата на подхода са атрактивни:

[х].Закотвянето се основава на идеята за строго разделяне ковариантнаи потенциално полиморфни (или, накратко, полиморфни) елементи. Всички субекти, декларирани като котваили като some_anchorса ковариантни; други са полиморфни. Във всяка от двете категории са разрешени всякакви обединявания, но няма обект или израз, който да нарушава границата. Не можете например да присвоите полиморфен източник на ковариантна цел.

[х].Това просто и елегантно решение е лесно за обяснение, дори и за начинаещи.

[х].Той напълно елиминира възможността за нарушаване на коректността на системата в ковариантно конструирани системи.

[х].Той запазва концептуалната рамка, изложена по-горе, включително концепцията за ограничена и неограничена универсалност. (В резултат на това това решение според мен е за предпочитане пред типичните променливи, които заместват механизмите на ковариантност и универсалност, предназначени да решават различни практически проблеми.)

[х].Изисква малка промяна на езика — добавяне на една ключова дума, отразена в правилото за съвпадение — и не включва предполагаеми трудности при прилагането.

[х].Това е реалистично (поне на теория): всяка възможна по-рано система може да бъде пренаписана чрез замяна на ковариантните заменяния с фиксирани предекларации. Вярно е, че някои от присъединяванията ще бъдат невалидни в резултат на това, но те съответстват на случаи, които могат да доведат до нарушения на типа, и следователно трябва да бъдат заменени с опити за присвояване и ситуацията по време на изпълнение трябва да бъде подредена.

Изглежда, че дискусията може да приключи дотук. Така че защо подходът за закотвяне не е напълно удовлетворяващ? Първо, все още не сме засегнали проблема с укриването на деца. Освен това, основната причина за продължаване на дискусията е проблемът, който вече беше изразен при накратко споменаване на променливи от типа. Разделянето на сферите на влияние върху полиморфната и ковариантната част е донякъде подобно на резултата от конференцията в Ялта. Той приема, че разработчикът на класа има изключителна интуиция, която е в състояние да избере за всеки обект, който въвежда, по-специално за всеки аргумент, веднъж завинаги, да избере една от двете възможности:

[х].Един обект е потенциално полиморфен: сега или по-късно (чрез предаване на параметри или чрез присвояване) може да бъде прикачен към обект, чийто тип е различен от декларирания. Оригиналният тип обект не може да бъде променен от нито един потомък на класа.

[х].Един обект подлежи на отмяна на тип, тоест той е или закрепен, или сам по себе си е опорен.

Но как може един разработчик да предвиди всичко това? Цялата привлекателност на метода OO, изразена в много отношения в принципа Open-Closed, е свързана именно с възможността за промени, които имаме право да направим в извършената преди това работа, както и с факта, че разработчикът на универсални решения нетрябва да притежава безкрайна мъдрост, да разбира как неговият продукт може да бъде адаптиран към техните нужди от потомците.

С този подход отмяната и скриването е един вид "предпазен клапан", който ви позволява да използвате повторно съществуващ клас, почти подходящ за нашите цели:

[х].Прибягвайки до отмяна на тип, можем да променим декларациите в извлечения клас, без да засягаме оригинала. В този случай едно чисто ковариантно решение ще изисква коригиране на оригинала с помощта на описаните трансформации.

[х].Скриването от дете предпазва от много неуспехи при създаване на клас. Можете да критикувате проект, в който ПРАВОЪГЪЛНИК, използвайки факта, че тойе потомък ПОЛИГОН, се опитва да добави връх. Вместо това може да се предложи структура на наследяване, в която формите с фиксиран брой върхове са отделени от всички останали и проблемът няма да възникне. Въпреки това, когато се разработват структури на наследяване, винаги е за предпочитане да има такива, в които няма таксономични изключения... Но могат ли да бъдат напълно елиминирани? Докато обсъждаме ограниченията за износ в по-късна лекция, ще видим, че това не е възможно по две причини. Първият е наличието на конкуриращи се критерии за класификация. Второ, вероятността разработчикът да не намери идеалното решение, дори и да съществува.

Глобален анализ

Този раздел е посветен на описанието на междинния подход. Основните практически решения са изложени в Лекция 17.

Докато изучавахме опцията за закрепване, забелязахме, че основната й идея е да раздели ковариантните и полиморфните набори от обекти. Така че, ако вземем две инструкции от формата


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


проблемът започва с обединяването на два независими и напълно невинни оператора.

Погрешните повиквания водят до нарушение на типа. В първия пример, полиморфното присвояване прикрепя обект МОМЧЕкъм същността с, какво прави той жневалиден аргумент дялтъй като е свързан с обекта МОМИЧЕ... Във втория пример, към обекта rприкрепя обект ПРАВОЪГЪЛНИКкоето изключва add_verexот изнесените компоненти.

Ето идеята за ново решение: предварително - статично, при проверка на типове от компилатора или други инструменти - ние дефинираме наборна всеки обект, включително типовете обекти, с които обектът може да бъде свързан по време на изпълнение. След това отново статично се уверяваме, че всяко извикване е правилно за всеки елемент от набора целеви типове и аргументи.

В нашите примери операторът s: = bпоказва, че класът МОМЧЕпринадлежи към набора от типове за с(защото в резултат на изпълнението на оператора за създаване създаде бтой принадлежи към набора за б). МОМИЧЕ, с оглед наличието на указания създаде g, принадлежи към набора от типове за ж... Но след това обаждането дялще бъдат невалидни за целта сТип МОМЧЕи аргумент жТип МОМИЧЕ... По същия начин ПРАВОЪГЪЛНИКе в набора за стр, което се дължи на полиморфното присвояване, обаче, повикването add_verexза стрТип ПРАВОЪГЪЛНИКсе оказва невалидно.

Тези наблюдения ни водят до идеята за създаване глобаленподход, базиран на новото правило за писане:

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

Повикване x.f (arg)е системно правилно, ако и само ако е правилно за класа х, и аргот всякакъв тип от съответния набор от типове.

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

Системната коректност на повикване се свежда до коректност на класа, с изключение, че се проверява не за отделни елементи, а за всякакви двойки от набори от набори. Ето основните правила за създаване на набор от типове за всеки обект:

1 За всеки обект първоначалният набор от типове е празен.

2 След като се запознаете със следващата инструкция на формуляра създаване (SOME_TYPE) a, добавете SOME_TYPEв набор от типове за а... (За простота ще приемем, че всяка инструкция създавамще бъде заменен с инструкция създаване (ТИП) a, където ТИП- тип обект а.)

3 След като изпълните следващото задание на формуляра a: = b, добавете към набора от типове за а б.

4 Ако аима формален параметър на подпрограма, след това при среща на друго повикване с действителен параметър б, добавете към набора от типове за авсички елементи от набора от типове за б.

5 Ще повтаряме стъпки (3) и (4), докато наборите от типове спрат да се променят.

Тази формулировка не взема предвид механизма на универсалност, но е възможно да се разшири правилото, ако е необходимо, без никакви проблеми. Стъпка (5) е необходима поради възможността за присвояване и прехвърляне на вериги (от бДа се а, от ° СДа се би др.). Лесно е да се разбере, че след краен брой стъпки този процес ще спре.

Както може би сте забелязали, правилото игнорира последователността от инструкции. Кога


създаване (ТИП1) t; s: = t; създаване (ТИП2) t

в набор от типове за сще влезе като ТИП 1и ТИП 2, макар че с, като се има предвид последователността от инструкции, е в състояние да приема стойности само от първия тип. Отчитането на местоположението на инструкциите ще изисква от компилатора да анализира в дълбочина потока от инструкции, което ще доведе до прекомерно повишаване на нивото на сложност на алгоритъма. Вместо това се прилагат по-песимистични правила: последователността на операциите:


ще бъдат обявени за системно неправилни, въпреки че последователността на тяхното изпълнение не води до нарушение на типа.

Глобалният анализ на системата е представен (по-подробно) в 22-ра глава на монографията. Това реши както проблема с ковариацията, така и проблема с ограниченията за износ по време на наследяване. Този подход обаче има досаден практически недостатък, а именно: трябва да проверява система като цяло, а не всеки клас поотделно. Смъртоносно се оказва правило (4), което при извикване на библиотечна рутина ще вземе предвид всичките й възможни извиквания в други класове.

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

В резултат, доколкото знам, проверката за коректност на системата остана неизпълнена от никого. (Друга причина за този резултат може да е била сложността на самите правила за валидиране.)

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

Въпреки това, въпреки името си, всъщност е възможно да се провери коректността на системата, като се използва само инкрементална проверка на класа (докато се изпълнява нормален компилатор). Това ще бъде крайният принос към решаването на проблема.

Пазете се от полиморфни извиквания!

Правилото за коректност на системата е песимистично: за по-голяма простота то също така отхвърля напълно безопасни комбинации от инструкции. Парадоксално е, че ще изградим последното решение въз основа на още по-песимистично правило... Естествено, това ще повдигне въпроса доколко реалистичен ще бъде резултатът ни.

Обратно в Ялта

Същност на разтвора Catcall, - ще обясним значението на това понятие по-късно, - като се върнем към духа на Ялтинските споразумения, разделяйки света на полиморфен и ковариантен (а сателитът на ковариацията е укриването на потомци), но без да е необходимо да притежава безкрайна мъдрост.

Както и преди, ще стесним въпроса за ковариацията до две операции. В нашия основен пример това е полиморфно присвояване: s: = bи извикване на ковариантната подпрограма: с. дял (g)... Анализирайки кой е истинският виновник за нарушенията, изключваме аргумента жизмежду заподозрените. Всеки аргумент от тип СКИОРили генериран от него, той не ни подхожда поради полиморфизъм си ковариация дял... Следователно, ако статично опишете обекта другикак СКИОРи динамично се прикрепя към обекта СКИОРпосле се обади s.share (други)статично ще създаде впечатлението за идеален, но ще доведе до нарушение на типа, ако е полиморфно присвоено ссмисъл б.

Основният проблем е, че се опитваме да използваме спо два несъвместими начина: като полиморфна единица и като цел на извикване към ковариантна подпрограма. (В другия ни пример проблемът е използването стркато полиморфна единица и като цел за извикване на подпрограма на детето, криещо компонента add_verex.)

Решението на Catcall, подобно на Binding, е радикално по природа: то забранява използването на обект като полиморфен и ковариантен едновременно. Подобно на глобалния анализ, той статично определя кои обекти могат да бъдат полиморфни, но не се опитва да бъде твърде интелигентен в търсенето на набор от възможни типове за обекти. Вместо това всяко полиморфно образувание се възприема като достатъчно подозрително и е забранено влизането в съюз с кръг от уважавани личности, включително ковариация и укриване от потомството.

Едно правило и няколко определения

Правилото за типа за решението на Catcall е просто:

Правило за типа на Catcall

Полиморфните извиквания са неправилни.

Тя се основава на също толкова прости дефиниции. На първо място, полиморфна единица:

Определение: полиморфна единица

Същността хреферентният (неразширен) тип е полиморфен, ако има едно от следните свойства:

1 Среща се при назначение x: = yкъде е същността ге от различен тип или е полиморфен чрез рекурсия.

2 Намерено в инструкциите за създаване създаване (OTHER_TYPE) x, където OTHER_TYPEне е от типа, посочен в декларацията х.

3 Това е формален аргумент към подпрограма.

4 Това е външна функция.

Целта на тази дефиниция е да даде статут на полиморфен ("потенциално полиморфен") всеки обект, който може да бъде прикрепен към обекти от различен тип по време на изпълнение на програмата. Тази дефиниция се прилага само за референтни типове, тъй като разширените обекти не могат да бъдат полиморфни по природа.

В нашите примери скиорът си многоъгълник стр- са полиморфни според правилото (1). На първия е присвоен обект МОМЧЕ б, вторият - обектът ПРАВОЪГЪЛНИК r.

Ако сте запознати с формулирането на концепцията за набор от типове, ще забележите колко по-песимистично изглежда определението за полиморфен обект и колко по-лесно е да го тествате. Без да се опитваме да намерим всички видове динамични типове обекти, ние се задоволяваме с общия въпрос: може ли даден обект да бъде полиморфен или не? Най-изненадващо е правило (3), според което полиморфенброи всеки формален параметър(освен ако неговият тип не е разширен, какъвто е случаят с цели числа и т.н.). Дори не си правим труда да анализираме обажданията. Ако подпрограмата има аргумент, тогава тя е на пълно разположение на клиента, което означава, че не можете да разчитате на типа, посочен в декларацията. Това правило е тясно свързано с повторното използване - целта на обектната технология - където всеки клас може потенциално да бъде включен в библиотека и ще бъде извикан многократно от различни клиенти.

Характерното свойство на това правило е, че не изисква никакви глобални проверки. За да идентифицирате полиморфизма на даден обект, достатъчно е да погледнете текста на самия клас. Ако запазим информация за техния полиморфизъм за всички заявки (атрибути или функции), тогава дори не е нужно да изучаваме текстовете на предците. За разлика от търсенето на набори от типове, можете да откривате полиморфни обекти, като проверявате клас по клас в инкрементална компилация.

Обажданията, подобно на обектите, могат да бъдат полиморфни:

Определение: полиморфно извикване

Извикването е полиморфно, ако целта му е полиморфна.

И двете повиквания в нашите примери са полиморфни: с. дял (g)поради полиморфизъм с, p.add_ връх (...)поради полиморфизъм стр... По дефиниция само квалифицираните повиквания могат да бъдат полиморфни. (Чрез неквалифицирано обаждане е (...)вид квалифициран Current.f (...), ние не променяме същността на въпроса, тъй като Текущна който не може да се присвои нищо не е полиморфен обект.)

След това имаме нужда от концепция за Catcall, базирана на концепцията за CAT. (CAT означава промяна на наличността или типа). Подпрограмата е CAT подпрограма, ако някакво нейно предефиниране на дъщерни елементи води до един от двата вида промени, които, както видяхме, са потенциално опасни: промяна на типа на аргумента (ковариантно) или скриване на по-рано експортиран компонент.

Определение: CAT рутинни процедури

Подпрограма се нарича CAT подпрограма, ако някакво нейно предефиниране промени състоянието на експортиране или типа на някой от нейните аргументи.

Това свойство отново позволява постепенна валидация: всяко отмяна на типа на аргумента или състоянието на експортиране прави процедурата или функцията CAT подпрограма. Тук следва концепцията на Catcall: извикване на подпрограма CAT, което може да бъде погрешно.

Определение: Catcall

Извикването се нарича Catcall, ако някакво предефиниране на подпрограмата би я направила грешна поради промяна в статуса на експортиране или типа на аргумента.

Класификацията, която създадохме, ни позволява да разграничим специални групи от повиквания: полиморфни и catcalls. Полиморфните извиквания дават изразителна сила на обектния подход, извикванията ви позволяват да замените типове и да ограничите експортирането. Използвайки терминологията, въведена по-рано в тази лекция, можем да кажем, че полиморфните извиквания се разширяват полезност, catcolls - използваемост.

Предизвикателства дяли add_verexв нашите примери са котешки обаждания. Първият извършва ковариантно предефиниране на своя аргумент. Вторият се експортира от класа ПРАВОЪГЪЛНИКно скрит от класа ПОЛИГОН... И двете обаждания също са полиморфни, което ги прави перфектни примери за полиморфни извиквания. Те са грешни според правилото за типове Catcall.

Оценка

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

Видяхме три решения на проблема с ковариацията, две от които засягаха и ограниченията за износ. Кое е вярно?

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

Глобалният анализ изглежда непрактичен поради пълната проверка на цялата система. Той обаче ни помогна да разберем по-добре проблема.

Решението за свързване е изключително атрактивно. Той е прост, интуитивен и лесен за изпълнение. Още повече трябва да съжаляваме за невъзможността да се поддържат редица ключови изисквания на OO метода, отразени в принципа Open-Closed. Ако наистина имахме страхотна интуиция, тогава закрепването би било чудесно решение, но кой разработчик би се осмелил да твърди това или, още повече, да признае, че авторите на библиотечните класове, наследени в неговия проект, са имали такава интуиция?

Ако сме принудени да се откажем от фиксацията, тогава решението на Catcall изглежда най-подходящо, то е доста лесно обяснимо и приложимо на практика. Неговият песимизъм не трябва да изключва полезни комбинации от оператори. В случай, когато полиморфен catcall се генерира от "легитимен" оператор, винаги можете безопасно да го разрешите, като въведете опит за присвояване. По този начин редица проверки могат да бъдат пренесени към времето за изпълнение на програмата. Броят на подобни случаи обаче трябва да бъде изключително малък.

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

Пълно съответствие

За да завършим нашата дискусия за ковариацията, е полезно да разберем как общият метод може да се приложи към доста общ проблем. Методът се появи в резултат на теорията на Catcall, но може да се използва в рамките на основната версия на езика, без да се въвеждат нови правила.

Да предположим, че има два съвпадащи списъка, където първият е скиорите, а вторият е съквартирантът на скиора от първия списък. Искаме да извършим подходящата процедура за поставяне дял, само ако е позволено от правилата за описание на типове, които позволяват момичета с момичета, момичета-победители с момичета-победители и т.н. Проблеми от този вид са често срещани.

Вероятно просто решение, базирано на предишна дискусия и опит за възлагане. Помислете за генеричната функция монтирани(одобрява):


монтиран (други: ОБЩИ): като другите е
- Текущият обект (Current), ако неговият тип съответства на типа на обекта,
- прикрепен към други, иначе нищожен.
if other / = Void и след това conforms_to (other) тогава

Функция монтиранивръща текущия обект, но известен като обект на типа, прикачен към аргумента. Ако типът на текущия обект не съвпада с типа на обекта, прикачен към аргумента, той се връща Празно... Обърнете внимание на ролята на опита за присвояване. Функцията използва компонента съответства_наот класа ОБЩ, който установява съвместимостта на типовете на двойка обекти.

Замяна съответства_накъм друг компонент ОБЩС име същият_типни дава функция перфектно_монтиран (пълно съответствие), който се връща Празноако типовете и на двата обекта не са идентични.

Функция монтирани- ни дава просто решение на проблема със съпоставянето на скиори без нарушаване на правилата за описание на типове. И така, в кода на класа СКИОРможем да въведем нова процедура и да я използваме вместо нея дял, (последното може да се направи със скрита процедура).


- Изберете, ако е приложимо, друго като съсед по номер.
- gender_ascertained - установен пол
gender_ascertained_other: като Current
gender_ascertained_other: = други .монтирани (текущи)
ако gender_ascertained_other / = Невалиден тогава
споделяне (gender_ascertained_other)
"Заключение: съвместяването с други не е възможно"

За другипроизволен тип СКИОР(не само като Current) дефинирайте версията пол_установен_другот присвоения тип Текущ... Функцията ще ни помогне да гарантираме идентичността на типовете. перфектно_ монтиран.

Ако има два паралелни списъка със скиори, представляващи планираното настаняване:


обитател1, обитател2: СПИСЪК

можете да организирате цикъл, като извикате на всяка стъпка:


occupant1.item.safe_share (occupant2.item)

съвпадащи елементи от списъка, ако и само ако техните типове са напълно съвместими.

Ключови понятия

[х].Статичното писане е ключът към надеждността, четливостта и ефективността.

[х].За да бъде реалистично, статичното въвеждане изисква комбинация от механизми: твърдения, множествено наследяване, опит за присвояване, ограничена срещу неограничена гъвкавост, закрепени декларации. Типовата система не трябва да позволява капани (отмятания от типа).

[х].Основно правило за повторно деклариране е да се позволи ковариантно предефиниране. Типовете резултат и аргументи, когато са отменени, трябва да са съвместими с оригинала.

[х].Ковариантността, както и способността на детето да скрие компонент, експортиран от предшественик, в комбинация с полиморфизъм, създава рядък, но сериозен проблем с нарушение на типа.

[х].Тези нарушения могат да бъдат избегнати чрез използване на: глобален анализ (който е непрактичен), ограничаване на ковариацията до закрепени типове (което е в противоречие с принципа „Отворено-затворено“), решение на Catcall, което не позволява на полиморфна цел да извика подпрограма с ковариация или криейки се от дете.

Библиографски бележки

Редица материали от тази лекция са представени в доклади във форумите OOPSLA 95 и TOOLS PACIFIC 95, както и публикувани в. Редица рецензионни материали са заимствани от статията.

Понятието за автоматично приспадане на тип е въведено в, където е описан алгоритъмът за дедукция на типа на функционалния език ML. Връзката между полиморфизма и проверката на типа е изследвана в статията.

Техники за подобряване на ефективността на кода на динамично въведени езици в контекста на Self езика могат да бъдат намерени в.

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

Ръководството на ISE включва въведение в проблемите на едновременното използване на полиморфизъм, ковариация и скриване на деца. Липсата на подходящ анализ в първото издание на тази книга доведе до редица критични дискусии (първата от които беше коментарът на Филип Елинк в неговата бакалавърска работа "De la Conception-Programmation par Objets", Memoire de license, Universite Libre de Bruxelles (Белгия), 1988 г.), изразено в трудовете на И. Статията на Кук предоставя няколко примера, свързани с проблема с ковариацията и се опитва да го разреши. Решение, базирано на типични параметри за ковариантни обекти на TOOLS EUROPE 1992, беше предложено от Франц Вебер. Дадени са точните дефиниции на понятията за системна коректност, както и за коректност на класа, където се предлага решение с помощта на пълен системен анализ. Решението на Catcall е предложено за първи път в; Вижте също .

Решението за фиксиране беше представено в моята презентация на семинара TOOLS EUROPE 1994. Тогава обаче не виждах нужда от котва- декларации и свързани с тях ограничения за съвместимост. Пол Дюбоа и Амирам Йехудай побързаха да посочат, че при тези условия проблемът с ковариацията остава. Те, заедно с Reinhardt Budde, Karl-Heinz Sylla, Kim Walden и James McKim, направиха много критични точки в работата, която доведе до написването на тази лекция.

Голяма част от литературата е посветена на въпросите на ковариацията. В и ще намерите както обширна библиография, така и преглед на математическите аспекти на проблема. За списък с връзки към онлайн материали за теория на ООП и уеб страниците на техните автори вижте страницата на Лоран Дами. Понятията за ковариация и контравариантност са заимствани от теорията на категориите. Появата им в контекста на писането на програми дължим на Лука Кардели, който започва да ги използва в речите си от началото на 80-те, но не ги използва в печат до края на 1980-те.

Трикове, базирани на общи променливи, са описани в,,.

Контравариантността беше внедрена в езика Sather. Обясненията са дадени в.

Докато са възможни междинни опции, тук са представени два основни подхода:

  • Динамично писане: изчакайте момента на приключване на всяко обаждане и след това вземете решение.
  • Статично писане: Въз основа на набор от правила, определете от изходния текст дали са възможни нарушения на типа по време на изпълнение. Системата се изпълнява, ако правилата гарантират, че няма грешки.

Тези термини са лесни за обяснение: кога динамично писанеПроверката на типа се извършва, докато системата работи (динамично) и кога статично писанепроверката се извършва на текста статично (преди изпълнение).

Статично писанепредполага автоматична проверка, обикновено възложена на компилатора. В резултат на това имаме проста дефиниция:

Определение: статично въведен език

Езикът OO е статично въведен, ако идва с набор от последователни правила, проверени от компилатора, за да се гарантира, че изпълнението на системата не води до нарушение на типа.

В литературата терминът " силенпишете "( силен). Той отговаря на ултиматума на дефиницията, която не изисква никакво нарушение на типа. Възможно и слаб (слаб) форми статично писанепри които правилата премахват определени нарушения, без да ги отстраняват изцяло. В този смисъл някои OO езици са статично слабо типизирани. Ще се борим за най-силното писане.

В динамично въведени езициизвестен като нетипизиран, няма декларации за тип и всяка стойност може да бъде прикачена към обекти по време на изпълнение. В тях не е възможна статична проверка на типа.

Правила за писане

Нашата OO нотация е статично въведена. Неговите типови правила бяха въведени в предишни лекции и се свеждат до три прости изисквания.

  • Когато се декларира всеки обект или функция, трябва да се посочи неговият тип, напр. acc: АКАУНТ... Всяка подпрограма има 0 или повече формални аргумента, чийто тип трябва да бъде определен, например: put (x: G; i: INTEGER).
  • При всяко присвояване x: = y и във всяко извикване на подпрограма, в което y е действителният аргумент за формалния аргумент x, изходният тип y трябва да е съвместим с целевия тип x. Дефиницията за съвместимост се основава на наследяване: B е съвместим с A, ако е негов потомък, допълнен от правила за общи параметри (вижте "Въведение в наследяването").
  • Извикването към x.f (arg) изисква f да бъде компонент на базов клас за целевия тип x, а f трябва да бъде експортиран в класа, в който се появява извикването (вижте 14.3).

Реализъм

Въпреки че определението за статично въведен език е доста точно, то не е достатъчно – необходими са неформални критерии при създаване на правила за писане. Помислете за два крайни случая.

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

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

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

Да кажем, че езикът реалистиченако е подходящ за употреба и полезен на практика. За разлика от определението статично писанедавайки категоричен отговор на въпроса: " X е статично въведен?“, определението за реализъм е отчасти субективно.

В тази лекция ще проверим дали нотацията, която предлагаме, е реалистична.

песимизъм

Статично писаневоди по природа към "песимистична" политика. Опит да се гарантира това всички изчисления не водят до неуспехи, отхвърля изчисления, които биха могли да завършат без грешка.

Помислете за обикновен, необектен, подобен на Pascal език с различни типове REAL и INTEGER. При описание на n: INTEGER; r: Реален оператор n: = r ще бъде отхвърлен като в нарушение на правилата. По този начин компилаторът ще отхвърли всички от следните твърдения:

n: = 0,0 [A] n: = 1,0 [B] n: = -3,67 [C] n: = 3,67 - 3,67 [D]

Ако ги активираме, ще видим, че [A] винаги ще работи, тъй като всяка бройна система има точно представяне на реалното число 0,0, което може да бъде недвусмислено преведено в 0 цели числа. [B] почти сигурно също ще работи. Резултатът от действието [C] не е очевиден (искаме ли да получим общата сума, като закръглим или отхвърлим дробната част?). [D] ще свърши своята работа, точно като оператора:

ако n ^ 2< 0 then n:= 3.67 end [E]

където отива недостижимото присвояване (n ^ 2 е квадратът на n). След замяна на n ^ 2 с n, само серия от стартирания ще даде правилния резултат. Присвояването на голяма нецелочислена стойност с плаваща запетая на n няма да се справи.

V въведени езицивсички тези примери (работещи, неработещи, понякога работещи) се тълкуват безмилостно като нарушения на правилата за деклариране на типа и се отхвърлят от всеки компилатор.

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

Статично писане: как и защо

Въпреки че ползите статично писанеочевидно е добра идея да поговорим отново за тях.

Предимства

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

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

Ранното откриване на грешки също е важно, защото колкото по-дълго отлагаме откриването им, толкова повече ще се увеличат разходите за отстраняването им. Това свойство, интуитивно разбирано от всички професионални програмисти, е количествено потвърдено от добре познатите произведения на Бьом. Зависимостта на разходите за фиксиране от времето на намиране на грешки е показана на графиката, изградена според данните от редица големи индустриални проекти и експерименти, проведени с малък управляем проект:


Ориз. 17.1.

Четимостили Лекота за разбиране(четимост) има своите предимства. Във всички примери в тази книга появата на тип върху обект дава на читателя информация за неговата цел. Четимостта е изключително важна по време на фазата на поддръжка.

накрая, ефективностможе да определи успеха или неуспеха на обектната технология на практика. В отсъствието на статично писане x.f (arg) може да отнеме произволно време за изпълнение. Причината за това е, че по време на изпълнение, ако f не бъде намерен в базовия клас на целта x, търсенето ще продължи в неговите потомци, което е сигурен път към неефективност. Можете да облекчите проблема, като подобрите търсенето на компонент в йерархията. Авторите на Self са свършили страхотна работа, опитвайки се да генерират най-добрия код за динамично въведен език. Но е точно така статично писанепозволи на такъв OO продукт да се доближи или да се изравни с ефективността на традиционния софтуер.

Ключът към статично писанее вече изложената идея, че компилаторът, който генерира кода за конструкцията x.f (arg), знае типа на x. Поради полиморфизма няма начин да се определи еднозначно подходящата версия на f компонента. Но декларацията стеснява многото възможни типове, позволявайки на компилатора да изгради таблица, която осигурява достъп до правилния f с минимални разходи - ограничена константасложността на достъпа. Извършени допълнителни оптимизации статично свързванеи вграждане- също стана по-лесно благодарение на статично писаненапълно премахване на разходите, когато е приложимо.

Аргументи за динамично писане

Въпреки всичко това, динамично писанене губи своите привърженици, особено сред програмистите на Smalltalk. Техните аргументи се основават предимно на реализма, разгледан по-горе. Те са сигурни, че статично писанеги ограничава твърде много, пречи им свободно да изразяват творческите си идеи, наричайки го понякога „колан на целомъдрието“.

Може да се съгласим с това разсъждение, но само за статично въведени езици, които не поддържат редица функции. Струва си да се отбележи, че всички понятия, свързани с понятието тип и въведени в предишните лекции, са необходими - отхвърлянето на което и да е от тях е изпълнено със сериозни ограничения, а въвеждането им, напротив, дава на нашите действия гъвкавост и дава ни възможността да се насладим напълно на практичността. статично писане.

Писане: условията за успех

Какви са механизмите на реалист статично писане? Всички те бяха представени в предишните лекции и затова трябва само накратко да ги припомним. Изброяването им заедно показва последователността и силата на комбинирането им.

Нашата типова система се основава изцяло на концепцията клас... Дори основни типове като INTEGER са класове и затова не се нуждаем от специални правила за описание на предварително дефинирани типове. (Тук нашата нотация се различава от "хибридните" езици като Object Pascal, Java и C ++, където типовата система на по-старите езици се комбинира с базирана на клас технология за обекти.)

Разширени типовени дават повече гъвкавост, като позволяват типове, чиито стойности обозначават обекти, както и типове, чиито стойности означават препратки.

Решаващата дума при създаването на система от гъвкав тип принадлежи на наследствои свързана концепция съвместимост... Това преодолява основното ограничение на класическите типизирани езици, например Pascal и Ada, в които операторът x: = y изисква типовете x и y да бъдат еднакви. Това правило е твърде строго: то забранява използването на обекти, които могат да означават обекти от свързани типове (SAVINGS_ACCOUNT и CHECKING_ACCOUNT). При наследяване ние изискваме само съвместимост на типа y с тип x, например x е от тип ACCOUNT, y е SAVINGS_ACCOUNT, а вторият клас наследява от първия.

На практика статично въведен език се нуждае от поддръжка множествено наследяване... Основните обвинения са известни статично писанетъй като не дава възможност за интерпретиране на обекти по различен начин. Например, обектът DOCUMENT (документ) може да се предава по мрежата и следователно се нуждае от компоненти, свързани с типа MESSAGE (съобщение). Но тази критика е вярна само за ограничени езици единично наследство.


Ориз. 17.2.

Универсалносте необходимо, например, за описание на гъвкави, но безопасни контейнерни структури от данни (напр клас СПИСЪК [G] ...). Не бъдете този механизъм статично писанеще изисква деклариране на различни класове за списъци с различни типове елементи.

В някои случаи е необходима гъвкавост лимит, което ви позволява да използвате операции, които са приложими само за обекти от общ тип. Ако генеричният клас SORTABLE_LIST поддържа сортиране, той изисква обекти от тип G, където G е общ параметър, да имат операция за сравнение. Това се постига чрез свързване на общ клас ограничения към G, COMPARABLE:

клас SORTABLE_LIST ...

Всеки действителен общ SORTABLE_LIST трябва да бъде потомък на класа COMPARABLE, който има необходимия компонент.

Друг задължителен механизъм е опит за възлагане- организира достъпа до тези обекти, чийто тип софтуерът не контролира. Ако y е обект на база данни или обект, извлечен през мрежа, тогава x? = Y ще присвои x на y, ако y е съвместим тип, или ако не е, ще даде x на Void.

твърденияасоциирани като част от идеята Design by Contract с класове и техните компоненти под формата на предпоставки, постусловия и инварианти на класа, правят възможно описването на семантични ограничения, които не са обхванати спецификация на типа... Езици като Pascal и Ada имат типове диапазони, които могат да ограничат стойностите на даден обект, например до интервала от 10 до 20, но, като ги използвате, няма да можете да гарантирате, че стойността на i е отрицателно, винаги два пъти по-голямо от j. Инвариантите на класа идват на помощ, предназначени да отразяват точно наложените ограничения, без значение колко сложни са те.

Фиксирани рекламиса необходими, за да се избегне дублирането на лавинен код на практика. Обявяване y: като x, гарантирано е, че y ще се промени след всички повтарящи се декларации от тип x в детето. Без този механизъм разработчиците биха били безмилостно заети с повторни декларации, опитвайки се да поддържат различните типове последователни.

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

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

— Бебето написано ли е?

Нашата цел - строг статично писане... Ето защо трябва да избягваме всякакви вратички в нашата „игра по правилата“ или поне да ги идентифицираме точно, ако съществуват.

Най-често срещаната вратичка в статично въведени езицие наличието на трансформации, които променят типа на обекта. В C и неговите производни те се наричат ​​"casting" или casting (cast). Записът (OTHER_TYPE) x показва, че стойността x се интерпретира от компилатора като тип OTHER_TYPE, предмет на някои ограничения за възможни типове.

Такива механизми заобикалят ограниченията на проверката на типа. Прехвърлянето е широко разпространено в програмирането на C, включително диалекта ANSI C. Дори в C ++ кастингът, макар и не толкова често, остава често срещан и може би необходим.

Да спазват правилата статично писанене е толкова лесно, ако всеки момент могат да бъдат заобиколени чрез кастинг.

Писане и свързване

Въпреки че като читател на тази книга със сигурност ще правите разлика между статично и статично писане. обвързване, има хора, които не могат да направят това. Това може отчасти да се дължи на влиянието на Smalltalk, който се застъпва динамичен подходза двата проблема и е в състояние да създаде погрешно схващане, че имат едно и също решение. (Ние твърдим в нашата книга, че е желателно да се комбинира статично въвеждане и динамично свързване, за да се създават стабилни и гъвкави програми.)

Както въвеждането, така и обвързването се занимават със семантиката на Core Construct x.f (arg), но отговарят на два различни въпроса:

Писане и свързване

  • Въпросът за писане: кога трябва да знаем със сигурност, че по време на изпълнение ще има операция, съответстваща на f, приложима към обекта, прикрепен към обекта x (с параметъра arg)?
  • Въпрос за свързване: кога трябва да знаем каква операция инициира дадено повикване?

Писането отговаря на въпроса за наличността поне единоперации, обвързването е отговорно за избора необходимо.

В рамките на обектния подход:

  • проблемът с писането е с полиморфизъм: тъй като x по време на изпълнениеможе да обозначава обекти от няколко различни типа, трябва да сме сигурни, че операцията, представляваща f, на разположениевъв всеки един от тези случаи;
  • проблемът с обвързването е причинен от повтарящи се съобщения: тъй като един клас може да променя наследените компоненти, може да има две или повече операции, които претендират, че представляват f в дадено извикване.

И двете задачи могат да се решават както динамично, така и статично. И четирите решения са представени на съществуващите езици.

И динамичното свързване е въплътено в нотацията, предложена в тази книга.

Обърнете внимание на особеността на езика C ++, който поддържа статично писане, макар и не строго поради наличието на прехвърляне на типове, статично свързване(по подразбиране), динамично свързване при изрично посочване на виртуални ( виртуален) реклами.

Причина за избора статично писанеи динамичното свързване е очевидно. Първият въпрос е: "Кога ще разберем за съществуването на компонентите?" - предлага статичен отговор: " Колкото по-скоро, толкова по-добре", което означава: по време на компилиране. Вторият въпрос," Кой компонент да използвам? "предлага динамичен отговор:" този, от който се нуждаете“, – съответстващо динамичен типобект, който е дефиниран по време на изпълнение. Това е единственото жизнеспособно решение, ако статичното и динамичното свързване дават различни резултати.

В статично писанекомпилаторът няма да отхвърли повикването, ако може да се гарантира, че когато програмата се изпълни, обектът, снабден с компонента, съответстващ на low_landing_gear, ще бъде прикачен към обекта my_aircraft. Основната техника за получаване на гаранции е проста: задължителната декларация на my_aircraft изисква базовият клас от неговия тип да включва такъв компонент. Следователно my_aircraft не може да бъде деклариран като AIRCRAFT, тъй като последният няма по-ниско_спускателно_средство на това ниво; хеликоптерите, поне в нашия пример, не знаят как да освободят колесника. Ако декларираме субекта като САМОЛЕТ, - класът, съдържащ необходимия компонент - всичко ще бъде наред.

Динамично писанев стил Smalltalk изисква да изчакате обаждането и в момента на неговото изпълнение да проверите за наличието на необходимия компонент. Това поведение е възможно за прототипи и експериментални проекти, но неприемливо за индустриални системи - в момента на полет е твърде късно да попитате дали имате колесник.

Тази статия обсъжда разликата между статично въведени и динамично въведени езици, разглежда концепциите за „силно“ и „слабо“ писане и сравнява силата на системите за въвеждане на различни езици. Напоследък се наблюдава ясно движение към по-строги и по-мощни системи за писане в програмирането, така че е важно да се разбере какво се има предвид, когато се говори за типове и писане.



Типът е колекция от възможни стойности. Едно цяло число може да има стойности 0, 1, 2, 3 и т.н. Булевото може да бъде вярно или невярно. Можете да измислите свой собствен тип, например тип "GiveFive", в който са възможни стойностите "дай" и "5" и нищо друго. Това не е низ или число, това е нов, отделен тип.


Статично въведените езици ограничават типовете променливи: езикът за програмиране може да знае, например, че x е цяло число. В този случай на програмиста е забранено да прави x = true, това ще бъде неправилен код. Компилаторът ще откаже да го компилира, така че ние дори не можем да изпълним такъв код. Друг статично въведен език може да има различни изразителни способности и никоя от популярните типови системи не е в състояние да изрази нашия тип DayFive (но много от тях могат да изразяват други, по-сложни идеи).


Динамично въведените езици маркират стойности с типове: езикът знае, че 1 е цяло число, 2 е цяло число, но не може да знае, че променливата x винаги съдържа цяло число.


Средата за изпълнение на езика проверява тези етикети в различни моменти от времето. Ако се опитаме да добавим две стойности заедно, той може да провери дали те са числа, низове или масиви. След това ще добави тези стойности, ще ги залепи или ще даде грешка, в зависимост от типа.

Статично въведени езици

Статичните езици проверяват типовете в програмата по време на компилиране, дори преди програмата да се стартира. Всяка програма, в която типове нарушават правилата на езика, се счита за невалидна. Например, повечето статични езици ще отхвърлят израза "a" + 1 (C е изключение от това правило). Компилаторът знае, че "a" е низ и 1 е цяло число и че + работи само когато лявата и дясната страна са от един и същи тип. Така че няма нужда да стартира програмата, за да разбере, че има проблем. Всеки израз в статично въведен език е от специфичен тип, който можете да дефинирате, без да изпълнявате кода си.


Много статично въведени езици изискват обозначение на типа. Функцията на Java public int add (int x, int y) приема две цели числа и връща третото цяло число. Други статично въведени езици могат да определят типа автоматично. Същата функция за добавяне в Haskell изглежда така: добавете x y = x + y. Ние не казваме на езика за типове, но той може да ги разбере сам, защото знае, че + работи само върху числа, така че x и y трябва да са числа, така че add приема две числа като аргументи.


Това не намалява "статичния" характер на системата от типове. Типовата система на Haskell е известна с това, че е статична, строга и мощна, а Haskell изпреварва Java по всички тези фронтове.

Динамично въведени езици

Динамично въведените езици не изискват посочването на типа, но те не го дефинират сами. Типовете променливи са неизвестни, докато не имат конкретни стойности при стартиране. Например функция в Python


def f (x, y): връща x + y

можем да добавим две цели числа, да залепим низове, списъци и така нататък и не можем да разберем какво точно се случва, докато не стартираме програмата. Може би в един момент f ще бъде извикан с два низа и с две числа в друг момент. В този случай x и y ще съдържат стойности от различни типове в различно време. Следователно се казва, че стойностите в динамичните езици имат тип, но променливите и функциите не. Стойност от 1 определено е цяло число, но x и y могат да бъдат всичко.

Сравнение

Повечето динамични езици ще изведат грешка, ако типовете се използват неправилно (JavaScript е известно изключение; той се опитва да върне стойност за всеки израз, дори когато няма смисъл). Когато използвате динамично въведени езици, дори проста грешка като "a" + 1 може да възникне в производствената среда. Статичните езици предотвратяват подобни грешки, но разбира се степента на превенция зависи от мощността на системата от типове.


Статичните и динамичните езици са изградени върху фундаментално различни идеи за коректността на програмата. В динамичния език "a" + 1 това е правилна програма: кодът ще се изпълни и в средата на изпълнение ще се появи грешка. Въпреки това, в повечето статично въведени езици изразът "a" + 1 е не е програма: няма да бъде компилиран и няма да се изпълнява. Това не е валиден код, точно като куп произволни знаци! &% ^ @ * &% ^ @ * е невалиден код. Тази допълнителна концепция за коректност и некоректност няма еквивалент в динамичните езици.

Силно и слабо писане

Понятията "силен" и "слаб" са много двусмислени. Ето няколко примера за тяхното използване:

    Понякога "силен" означава "статичен".
    Това е просто, но е по-добре да използвате термина "статичен", защото повечето хора го използват и разбират.

    Понякога "силен" означава "не извършва имплицитно преобразуване на тип".
    Например JavaScript ви позволява да пишете "a" + 1, което може да се нарече "слабо писане". Но почти всички езици осигуряват някакво ниво на имплицитно преобразуване, което ви позволява автоматично да превключвате от цели числа към числа с плаваща запетая като 1 + 1.1. В действителност повечето хора използват думата „силен“, за да определят границата между приемлива и неприемлива трансформация. Няма общоприета граница, всички те са неточни и зависят от мнението на конкретен човек.

    Понякога „силен“ означава, че няма начин да заобиколите силните правила за писане в езика.

  • Понякога "силен" означава безопасен за памет.
    C е пример за несигурен в паметта език. Ако xs е масив от четири числа, тогава C с радост ще изпълни xs или xs кода, връщайки някаква стойност от паметта непосредствено зад xs.

Да спрем. Ето как някои езици отговарят на тези дефиниции. Както можете да видите, само Haskell е постоянно силен във всички отношения. Повечето езици не са толкова ясни.



(„Кога как“ в колоната Неявни преобразувания означава, че разделението между силни и слаби зависи от това кои преобразувания смятаме за приемливи).


Често термините "силен" и "слаб" се отнасят до неясна комбинация от различни дефиниции по-горе и други определения, които не са показани тук. Цялото това объркване прави думите "силен" и "слаб" практически безсмислени. Когато искате да използвате тези термини, по-добре е да опишете какво точно се има предвид. Например, може да кажете, че „JavaScript се връща, когато се добави низ с число, но Python връща грешка.“ В този случай няма да губим енергията си, опитвайки се да постигнем съгласие относно многото значения на думата „силен“. Или, още по-лошо: в крайна сметка имаме неразрешено недоразумение поради терминология.


В повечето случаи термините „силен“ и „слаб“ в интернет са неясни и недобре дефинирани мнения на конкретни хора. Използват се да наричат ​​език „лош” или „добър”, а това мнение се превежда на технически жаргон.



Силно писане: Типова система, която обичам и се чувствам комфортно.

Слабо писане: Типовата система, която ме притеснява или не ми е удобна.

Постепенно писане

Могат ли да се добавят статични типове към динамични езици? В някои случаи да. В други е трудно или невъзможно. Най-очевидният проблем е eval и други подобни възможности на динамичните езици. Извършването на 1 + eval ("2") в Python дава 3. Но какво дава 1 + eval (read_from_the_network ())? Зависи какво е в мрежата в момента на изпълнение. Ако получим число, значи изразът е правилен. Ако низ, тогава не. Не е възможно да се знае преди стартирането, така че не е възможно да се анализира статично типа.


Незадоволително решение на практика е да се даде на eval () тип Any, който напомня на Object в някои обектно-ориентирани езици за програмиране или интерфейс () в Go: това е тип, който удовлетворява всяка стойност.


Стойностите на тип Any не са ограничени от нищо, така че способността на системата от типове да ни помага в кода с eval изчезва. Езиците, които имат както eval, така и система от типове, трябва да отхвърлят безопасността на типа всеки път, когато се използва eval.


Някои езици имат незадължително или постепенно въвеждане: те са динамични по подразбиране, но позволяват добавяне на някои статични пояснения. Python наскоро добави незадължителни типове; TypeScript е добавка за JavaScript, която има незадължителни типове; Flow извършва статичен анализ на добрия стар JavaScript код.


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

Компилиране на статично въведен код

Когато компилирате статично въведен код, първо се проверява синтаксисът, както всеки компилатор. След това се проверяват видовете. Това означава, че статичен език може първоначално да докладва една синтактична грешка и след като я поправи, да се оплаква от 100 грешки при въвеждане. Поправката на синтактична грешка не генерира 100-те грешки при въвеждане. Компилаторът просто нямаше начин да открие грешки в типа, докато синтаксисът не беше коригиран.


Компилаторите за статични езици обикновено могат да генерират по-бърз код от компилаторите за динамични. Например, ако компилаторът знае, че функцията за добавяне приема цели числа, тогава той може да използва собствената инструкция за CPU ADD. Динамичният език ще провери типа по време на изпълнение, като избере една от многото функции за добавяне в зависимост от типовете (добавяне на цели числа или плаващи числа, или залепване на низове или може би списъци?) Или трябва да решите, че е имало грешка и типовете не са. съвпада. Всички тези проверки отнемат време. Динамичните езици използват различни трикове за оптимизация, като компилация точно навреме, при която кодът се прекомпилира по време на изпълнение, след като получи цялата необходима информация за типовете. Въпреки това, нито един динамичен език не може да се сравни със скоростта на спретнато написан статичен код на език като Rust.

Аргументи за статични срещу динамични типове

Привържениците на система от статичен тип посочват, че без система от типове простите грешки могат да доведат до проблеми в производството. Това, разбира се, е вярно. Всеки, който е използвал динамичен език, е изпитал това лично.


Привържениците на динамичните езици посочват, че такива езици изглежда са по-лесни за кодиране. Това определено е вярно за някои видове код, който пишем от време на време, като този код с eval. Това е противоречиво решение за редовна работа и има смисъл да запомните неясната дума „лесно“ тук. Рич Хики говори отлично за думата „лесно“ и връзката й с думата „просто“. След като гледате тази беседа, ще разберете, че не е лесно да използвате правилно думата „лесно“. Пазете се от "лекотата".


Плюсовете и минусите на статичните и динамичните системи за писане все още са слабо разбрани, но определено са специфични за езика и специфични за съответната задача.


JavaScript се опитва да продължи, дори ако това означава безсмислено преобразуване (като "a" + 1 дава "a1"). Python, от друга страна, се опитва да бъде консервативен и често връща грешки, какъвто е случаят с "a" + 1.


Има различни подходи с различни нивасигурност, но Python и JavaScript са динамично въведени езици.



Haskell, от друга страна, няма да ви позволи да добавите цяло число и float без изрично преобразуване, преди да го направите. C и Haskell са статично въведени, въпреки толкова големи разлики.


Има много вариации на динамични и статични езици. Всяко безусловно твърдение като „статичните езици са по-добри от динамичните езици, когато става въпрос за X“ е почти гарантирана глупост. Това може да е вярно за конкретни езици, но тогава е по-добре да кажете „Haskell е по-добър от Python, когато става въпрос за X“.

Разнообразие от статични системи за писане

Нека да разгледаме два известни примера за статично въведени езици: Go и Haskell. Системата за писане на Go няма общи типове, типове с "параметри" от други типове. Например, можете да създадете свой собствен тип за списъци MyList, който може да съхранява всички данни, от които се нуждаем. Искаме да можем да създадем MyList от цели числа, MyList от низове и така нататък, без да променяме оригиналния код на MyList. Компилаторът трябва да внимава за въвеждане: ако има MyList от цели числа и ние случайно добавим низ там, тогава компилаторът трябва да отхвърли програмата.


Go е умишлено проектиран така, че да не можете да дефинирате типове като MyList. Най-доброто, което можете да направите, е да създадете MyList от "празни интерфейси": MyList може да съдържа обекти, но компилаторът просто не знае техния тип. Когато получим обекти от MyList, трябва да кажем на компилатора техния тип. Ако кажем „Получавам низ“, но в действителност стойността е число, тогава ще има грешка при изпълнение, какъвто е случаят с динамичните езици.


В Go също липсват много от другите функции, намиращи се в съвременните статично въведени езици (или дори някои системи от 70-те години на миналия век). Създателите на Go имаха свои собствени причини да вземат тези решения, но мненията на външни лица по този въпрос понякога могат да звучат остро.


Сега нека сравним с Haskell, който има много мощна система от типове. Ако е зададен да тип MyList, типът "списък с числа" е просто MyList Integer. Haskell ни предпазва от случайно добавяне на низ към списъка и гарантира, че не поставяме елемент от списъка в низова променлива.


Haskell може да изразява много по-сложни идеи директно с типове. Например Num a => MyList a означава „Моят списък със стойности, които са от същия тип числа“. Това може да бъде списък с цели числа, плаващи числа или десетични числа с фиксирана точност, но определено никога няма да бъде списък с низове, който се проверява по време на компилиране.


Можете да напишете функция за добавяне, която работи на всеки числов тип. Тази функция ще има тип Num a => (a -> a -> a). Това означава:

  • a може да бъде произволен числов тип (Num a =>).
  • Функцията приема два аргумента от тип a и връща тип a (a -> a -> a).

Последният пример. Ако типът на функцията е String -> String, тогава тя приема низ и връща низ. Но ако е String -> IO String, тогава той също прави някои I/O. Това може да бъде достъп до диск, до мрежата, четене от терминал и т.н.


Ако функцията в типа Не IO, тогава знаем, че той не извършва никакви I/O операции. В уеб приложение, например, можете да разберете дали дадена функция модифицира база данни, като просто погледнете нейния тип. Никакви динамични и почти никакви статични езици не са в състояние да направят това. Това е характеристика на езиците с най-мощната система за писане.


В повечето езици ще трябва да се справим с функцията и всички функции, които се извикват оттам, и така нататък, опитвайки се да намерим нещо, което модифицира базата данни. Това е досаден процес и е лесно да се правят грешки. И системата тип Haskell може да отговори на този въпрос просто и с гаранция.


Сравнете тази мощност с Go, която не е в състояние да изрази простата идея на MyList, да не говорим за „функция, която приема два аргумента, и двата са числови и от един и същи тип и прави вход/изход“.


Подходът на Go улеснява писането на инструменти за програмиране в Go (по-специално, реализацията на компилатора може да бъде проста). Освен това има по-малко концепции за учене. Как тези ползи се сравняват със значителни ограничения е субективен въпрос. Въпреки това, не може да се твърди, че Haskell е по-труден за научаване от Go и че системата от типове на Haskell е много по-мощна и че Haskell може да предотврати много повече видове грешки при компилация.


Go и Haskell са толкова различни езици, че групирането им в един клас „статични езици“ може да бъде подвеждащо, въпреки че терминът се използва правилно. По отношение на практическите ползи за сигурността, Go е по-близо до динамичните езици, отколкото до Haskell.


От друга страна, някои динамични езици са по-безопасни от някои статични езици. (Общо взето Python се счита за много по-безопасен от C). Когато искате да обобщите за статичните или динамичните езици като групи, не забравяйте за голямо количестворазлики между езиците.

Конкретни примери за разлики във възможностите на системите за въвеждане

В по-мощните системи за писане можете да зададете ограничения на по-малки нива. Ето някои примери, но не се спирайте на тях, ако синтаксисът не е ясен.


В Go можете да кажете „функцията за добавяне взема две цели числа“ и връща цяло число „:


func add (x int, y int) int (връщане x + y)

В Haskell можете да кажете „функция отнема всякаквичислов тип и връща число от същия тип ":


f :: Num a => a -> a -> a добавете x y = x + y

В Idris можете да кажете "функцията взема две цели числа" и връща цяло число, но първият аргумент трябва да е по-малък от втория аргумент ":


добави: (x: Nat) -> (y: Nat) -> (автоматично по-малък: LT x y) -> Nat добави x y = x + y

Ако се опитате да извикате функцията add 2 1, където първият аргумент е по-голям от втория, компилаторът ще отхвърли програмата. по време на компилиране... Невъзможно е да се напише програма, където първият аргумент е по-голям от втория. Рядък език притежава тази способност. В повечето езици тази проверка се случва по време на изпълнение: бихме написали нещо като if x> = y: raise SomeError ().


Няма еквивалент на Haskell на примера с Idris по-горе, а Go няма еквивалент нито на примера на Haskell, нито на примера на Idris. В резултат на това Idris може да предотврати много грешки, които Haskell не може, а Haskell може да предотврати много грешки, които Go няма да забележи. И в двата случая са необходими допълнителни функции на системата за въвеждане, за да направят езика по-сложен.

Системите за писане на някои статични езици

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

  • C (1972), Go (2009): Тези системи изобщо не са мощни, без поддръжка на общ тип. Няма начин да зададете типа MyList да означава "списък с цели числа", "списък на низове" и т.н. Вместо това трябва да направите „списък с неприсвоени стойности“. Програмистът трябва ръчно да докладва „това е списък с низове“ всеки път, когато низ бъде извлечен от списъка и това може да доведе до грешка по време на изпълнение.
  • Java (1995), C # (2000): И двата езика поддържат общи типове, така че можете да кажете MyList и да получите списък с низове, за които компилаторът знае и може да наложи правилата за тип. Елементите от списъка ще бъдат от тип String, компилаторът ще наложи правилата при компилиране както обикновено, така че грешките по време на изпълнение са по-малко вероятни.
  • Haskell (1990), Rust (2010), Swift (2014): Всички тези езици имат няколко разширени функции, включително общи типове, алгебрични типове данни (ADT) и типове класове или нещо подобно (съответно типове класове, черти и протоколи). Rust и Swift са по-популярни от Haskell и се популяризират от големи организации (съответно Mozilla и Apple).
  • Агда (2007), Идрис (2011): Тези езици поддържат зависими типове, което ви позволява да създавате типове като "функция, която приема две цели числа x и y, където y е по-голямо от x". Дори ограничението "y е по-голямо от x" се прилага по време на компилиране. Когато приключите, y никога няма да бъде по-малко или равно на x, независимо какво се случва. Много фини, но важни свойства на системата могат да бъдат проверени статично на тези езици. Много малко програмисти ги изучават, но са много ентусиазирани от тези езици.

Има ясно движение към по-мощни системи за писане, особено ако се съди по популярността на езиците, а не по простото съществуване на езици. Забележително изключение е Go, което обяснява защо много привърженици на статичен език го смятат за стъпка назад.


Втора група (Java и C #) са масови езици, зрели и широко използвани.


Трета група е на прага да навлезе в мейнстрийма, с много подкрепа от Mozilla (Rust) и Apple (Swift).


Четвърта група (Идрис и Агда) са далеч от мейнстрийма, но това може да се промени с времето. Езиците на третата група бяха далеч от мейнстрийма преди десетилетие.

Тази статия съдържа необходимия минимум от онези неща, които просто трябва да знаете за писането, за да не наречете динамичното писане зло, Lisp – език без тип, а C – силно въведен език.

Пълната версия съдържа Подробно описаниеот всички видове писане, подправени с примери за кодове, връзки към популярни езици за програмиране и илюстративни снимки.

Препоръчвам първо да прочетете кратката версия на статията, а след това, ако желаете, пълната версия.

Кратка версия

Чрез въвеждане, езиците за програмиране обикновено се разделят на два големи лагера - напечатани и ненапечатани (безтипни). Първият включва например C, Python, Scala, PHP и Lua, докато вторият включва асемблер, Forth и Brainfuck.

Тъй като „безтипното писане“ е по своята същност толкова просто като тапа, то не може да бъде допълнително разделено на други типове. Но въведените езици са разделени на няколко припокриващи се категории:

  • Статично/динамично писане. Статичното се определя от факта, че крайните типове променливи и функции се задават по време на компилиране. Тези. вече компилаторът е 100% сигурен кой тип е къде. При динамично писане всички типове се откриват по време на изпълнение.

    Примери:
    Статично: C, Java, C #;
    Динамичен: Python, JavaScript, Ruby.

  • Силно / слабо писане (също понякога се казва, че е силно / слабо). Силното въвеждане се отличава с факта, че езикът не позволява смесване на различни типове в изрази и не извършва автоматични имплицитни преобразувания, например, не можете да извадите набор от низ. Слабо въведените езици извършват много имплицитни преобразувания автоматично, дори ако може да възникне загуба на прецизност или неяснота.

    Примери:
    Силни: Java, Python, Haskell, Lisp;
    Слаб: C, JavaScript, Visual Basic, PHP.

  • Изрично / неявно въвеждане. Изрично въведените езици се различават по това, че типът на новите променливи / функции / техните аргументи трябва да бъдат посочени изрично. Съответно, езиците с имплицитно въвеждане прехвърлят тази задача към компилатора / интерпретатора.

    Примери:
    Изрично: C ++, D, C #
    Неявно: PHP, Lua, JavaScript

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

Въпреки това, няма езици със статично и динамично писане едновременно. Въпреки че изпреварвам, ще кажа, че лежа тук - те наистина съществуват, но повече за това по-късно.

Подробна версия

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

Безтипно писане

В безтиповите езици за програмиране всички обекти се считат за просто поредици от битове с различна дължина.

Въвеждането без тип обикновено е присъщо на ниско ниво (език на асембли, Forth) и езотерични (Brainfuck, HQ9, Piet) езици. Въпреки това, наред със своите недостатъци, той има и някои предимства.

Предимства
  • Позволява ви да пишете на изключително ниско ниво и компилаторът / интерпретаторът няма да пречи на проверката на типа. Вие сте свободни да извършвате всякакви операции с всякакъв вид данни.
  • Полученият код обикновено е по-ефективен.
  • Прозрачност на инструкциите. При познаване на езика обикновено няма съмнение, че този или онзи код е такъв.
Недостатъци
  • Сложност. Често е необходимо да се представят сложни стойности като списъци, низове или структури. Това може да бъде неудобно.
  • Липса на проверки. Всички безсмислени действия, като изваждане на указател към масив от символ, ще се считат за напълно нормални, което е изпълнено с фини грешки.
  • Ниско ниво на абстракция. Работата с всеки сложен тип данни не се различава от работата с числа, което разбира се ще създаде много трудности.
Силно безтипно писане?

Да, съществува. Например, в асемблерния език (за архитектура x86 / x86-64, не познавам други), не можете да сглобите програма, ако се опитате да заредите данни от регистъра rax (64 бита) в регистъра cx (16 бита) .

mov cx, eax; грешка във времето за сглобяване

Значи излиза, че все още има писане в асемблера? Смятам, че тези проверки не са достатъчни. И вашето мнение, разбира се, зависи само от вас.

Статично срещу динамично писане

Основното нещо, което отличава статичното въвеждане от динамичното, е, че всички проверки на типовете се извършват по време на компилиране, а не по време на изпълнение.

Някои хора може да смятат, че статичното писане е твърде ограничено (всъщност е така, но отдавна се отърва от него с помощта на някои техники). За някои динамично въведените езици са игра с огън, но какви са характеристиките, които ги отличават? Имат ли шанс за съществуване и двата вида? Ако не, защо има много както статично, така и динамично въведени езици?

Нека го разберем.

Предимства на статичното писане
  • Проверките на типа се извършват само веднъж, по време на компилиране. Това означава, че няма да е необходимо постоянно да установяваме дали се опитваме да разделим число на низ (и или да изведем грешка, или да извършим преобразуване).
  • Скорост на изпълнение. От предишната точка е ясно, че статично въведените езици почти винаги са по-бързи от динамично въведените.
  • При някои допълнителни условия ви позволява да откривате потенциални грешки още на етапа на компилация.
Предимства на динамичното писане
  • Лесно създаване на универсални колекции - куп от всичко и всичко (такава нужда рядко възниква, но когато възникне динамично писане, ще помогне).
  • Удобство за описване на обобщени алгоритми (например сортиране на масив, който ще работи не само върху списък с цели числа, но и върху списък с реални числа и дори върху списък с низове).
  • Лесно учене - Динамично въведените езици обикновено са много добри в началото на програмирането.

Обобщено програмиране

Добре, най-важният аргумент за динамично писане е удобството да се описват общи алгоритми. Нека си представим проблем - имаме нужда от функция за търсене в множество масиви (или списъци) - масив от цели числа, масив от реални числа и масив от знаци.

Как ще го решим? Нека го решим на 3 различни езика: един с динамично писане и два със статично писане.

Алгоритъмът за търсене, който ще взема е един от най-простите - груба сила. Функцията ще получи необходимия елемент, самия масив (или списък) и ще върне индекса на елемента или, ако елементът не е намерен, (-1).

Динамично решение (Python):

Def find (задължителен_елемент, списък): за (индекс, елемент) в изброяване (списък): ако елемент == необходим_елемент: връщане на индекс (-1)

Както виждате, всичко е просто и няма проблеми с факта, че списъкът може да съдържа поне числа, дори списъци, въпреки че няма други масиви. Много добре. Нека продължим и да решим същия проблем в C!

Статично решение (C):

Unsigned int find_int (int required_element, int масив, unsigned int size) (за (unsigned int i = 0; i< size; ++i) if (required_element == array[i]) return i; return (-1); } unsigned int find_float(float required_element, float array, unsigned int size) { for (unsigned int i = 0; i < size; ++i) if (required_element == array[i]) return i; return (-1); } unsigned int find_char(char required_element, char array, unsigned int size) { for (unsigned int i = 0; i < size; ++i) if (required_element == array[i]) return i; return (-1); }

Е, всяка функция е индивидуално подобна на версията на Python, но защо има три? Наистина ли статичното програмиране се е провалило?

Да и не. Има няколко техники за програмиране, една от които ще разгледаме сега. Това се нарича общо програмиране и езикът C ++ го поддържа доста добре. Нека да разгледаме новата версия:

Статично решение (общо програмиране, C ++):

Шаблон unsigned int find (T required_element, std :: vector масив) (за (unsigned int i = 0; i< array.size(); ++i) if (required_element == array[i]) return i; return (-1); }

Добре! Не изглежда много по-сложно от версията на Python и не трябва да пише много. Освен това получихме реализация за всички масиви, а не само за 3, необходими за решаване на проблема!

Тази версия изглежда е точно това, от което се нуждаете - получаваме както предимствата на статичното въвеждане, така и някои от предимствата на динамичното.

Чудесно е, че дори е възможно, но може да е още по-добре. Първо, общото програмиране може да бъде по-удобно и красиво (например на езика Haskell). Второ, в допълнение към общото програмиране, можете да използвате и полиморфизъм (резултатът ще бъде по-лош), претоварване на функции (по подобен начин) или макроси.

Статична динамика

Трябва също да се спомене, че много статични езици позволяват динамично писане, например:

  • C # поддържа динамичния псевдотип.
  • F # поддържа синтактична захар под формата на? Оператор, въз основа на който могат да се реализират динамични симулации на въвеждане.
  • Haskell - динамичното въвеждане се осигурява от модула Data.Dynamic.
  • Delphi - чрез специалния тип Variant.

Освен това някои динамично въведени езици ви позволяват да се възползвате от статичното въвеждане:

  • Common Lisp - декларации за тип.
  • Perl - от версия 5.6, доста ограничен.

Силно и слабо писане

Силно въведените езици не позволяват смесване на обекти от различни типове в изрази и не извършват никакви автоматични преобразувания. Наричат ​​се още „силно типизирани езици“. Английският термин за това е силно въвеждане.

Слабо въведените езици, напротив, правят всичко възможно програмистът да смесва различни типове в един израз, а самият компилатор ще доведе всичко до един тип. Наричат ​​се още „свободно написани езици“. Английският термин за това е слабо писане.

Слабото писане често се бърка с динамичното, което е напълно погрешно. Динамично въведен език може да бъде както слабо, така и силно въведен.

Малко хора обаче придават значение на строгостта при въвеждане. Често се твърди, че ако езикът е статично въведен, можете да хванете много потенциални грешки при компилация. Лъжат те!

Езикът също трябва да има силен тип. Всъщност, ако компилаторът, вместо да докладва грешка, просто добави низ към число или дори по-лошо, извади друг от един масив, каква полза за нас, че всички „проверки на типа“ са по време на компилиране? Точно така – слабото статично писане е дори по-лошо от силното динамично писане! (Ами това е моето мнение)

Така че слабото писане няма никакви предимства? Може да изглежда така, но въпреки че съм твърд привърженик на силното писане, трябва да се съглася, че слабото писане също има предимства.

Искате ли да знаете кои?

Предимствата на силното писане
  • Надеждност - Ще получите изключение или грешка при компилация вместо неправилно поведение.
  • Скорост - вместо скрити преобразувания, които могат да бъдат доста скъпи, при силно въвеждане, трябва да ги напишете изрично, което кара програмиста поне да знае, че тази част от кода може да бъде бавен.
  • Разбиране на работата на програмата - отново вместо имплицитно прехвърляне на тип, програмистът пише всичко сам, което означава, че грубо разбира, че сравнението на низ и число не се случва от само себе си и не по магия.
  • Сигурност – когато пишете преобразувания на ръка, вие знаете точно към какво конвертирате и какво. Също така винаги ще разбирате, че подобни трансформации могат да доведат до загуба на прецизност и до неправилни резултати.
Предимства на слабото писане
  • Лесно използване на смесени изрази (например от цели и реални числа).
  • Абстракция от писане и фокусиране върху задачата.
  • Кратостта на записа.

Добре, разбрахме, оказва се, че слабото писане също има предимства! Има ли начини за прехвърляне на ползите от слабото писане към силното?

Оказва се, че са дори две.

Неявно преобразуване на тип, в недвусмислени ситуации и без загуба на данни

Ъъъ... Доста дълга точка. Позволете ми да го съкратя допълнително до "ограничено имплицитно преобразуване" И така, какво е значението на недвусмислена ситуация и загуба на данни?

Недвусмислена ситуация е трансформация или операция, при която обектът е веднага разбираем. Например добавянето на две числа е недвусмислена ситуация. И преобразуването на число в масив не е (може би ще бъде създаден масив от един елемент, може би масив с такава дължина, изпълнен с елементи по подразбиране, и може би числото ще бъде преобразувано в низ и след това в масив на знаци).

Загубата на данни е още по-лесна. Ако преобразуваме реалното число 3,5 в цяло число, ще загубим част от данните (всъщност тази операция също е двусмислена – как ще се извърши закръгляването? Нагоре? Надолу? Отхвърляне на дробната част?).

Двусмислените трансформации и трансформациите със загуба са много, много лоши. Няма нищо по-лошо от това в програмирането.

Ако не ми вярвате, научете PL / I или дори просто потърсете спецификацията му. Има правила за конвертиране между ВСИЧКИ типове данни! Това е просто ад!

Добре, нека си спомним ограниченото имплицитно преобразуване. Има ли такива езици? Да, например в Pascal можете да преобразувате цяло число в реално число, но не и обратното. Подобни механизми има и в C #, Groovy и Common Lisp.

Добре, казах, че все още има начин да получите няколко предимства на слабото въвеждане на силен език. И да, това е и се нарича полиморфизъм на конструктора.

Ще го обясня с помощта на прекрасния език Haskell като пример.

Полиморфните конструктори са резултат от наблюдението, че най-често са необходими безопасни неявни преобразувания при използване на числови литерали.

Например, в израза pi + 1 не искате да пишете pi + 1.0 или pi + float (1). Бих искал да напиша само пи + 1!

И това се прави в Haskell, благодарение на факта, че literal 1 няма специфичен тип... То не е нито цялостно, нито реално, нито сложно. Това е просто число!

В резултат на това, когато пишем проста функция sum xy, която умножава всички числа от x до y (с увеличение от 1), получаваме няколко версии наведнъж - сума за цели числа, сума за реално, сума за рационални, сума за комплексни числа , и дори сума за всички онези числови типове, които сами сте дефинирали.

Разбира се, тази техника спестява само при използване на смесени изрази с числови литерали и това е само върхът на айсберга.

По този начин можем да кажем, че най-доброто решение би било да балансираме на ръба, между силно и слабо писане. Но досега нито един език няма перфектен баланс, така че съм склонен да клоня повече към силно въведени езици (като Haskell, Java, C #, Python), а не към слабо въведени (като C, JavaScript, Lua, PHP) .

Явно срещу неявно въвеждане

Изрично въведен език предполага, че програмистът трябва да посочи типовете на всички променливи и функции, които декларира. Английският термин за това е изрично въвеждане.

Неявно въведен език, от друга страна, ви приканва да забравите за типовете и да оставите задачата за извеждане на тип на компилатора или интерпретатора. Английският термин за това е имплицитно въвеждане.

Отначало можете да решите, че имплицитното въвеждане е еквивалентно на динамично, а явното – на статично, но по-късно ще видим, че това не е така.

Има ли предимства за всеки вид и отново, има ли комбинации от тях и има ли езици, които поддържат и двата метода?

Предимства на изричното въвеждане
  • Наличието на подпис за всяка функция (например int add (int, int)) ви позволява лесно да определите какво прави функцията.
  • Програмистът веднага записва какъв тип стойност може да бъде съхранена в определена променлива, което премахва необходимостта да се помни това.
Предимства на имплицитното въвеждане
  • Съкращението за def add (x, y) е очевидно по-кратко от int add (int x, int y).
  • Устойчивост към промяна. Например, ако временна променлива във функция е от същия тип като входния аргумент, тогава в изрично въведен език, когато типът на входния аргумент се промени, типът на временната променлива също ще трябва да бъде променен.

Добре, виждате, че и двата подхода имат плюсове и минуси (кой е очаквал нещо друго?), Така че нека да потърсим начини да комбинираме двата!

Изрично въвеждане по избор

Има езици с имплицитно въвеждане по подразбиране и възможност за определяне на типа стойности, ако е необходимо. Преводачът автоматично ще изведе реалния тип израз. Един от тези езици е Haskell, позволете ми да ви дам прост пример за яснота:

Без изрично указание на типа add (x, y) = x + y - Изрично указание за тип add :: (Integer, Integer) -> Integer add (x, y) = x + y

Забележка: Умишлено използвах функция, която не се крие, и също така умишлено написах частен подпис вместо по-общото добавяне :: (Num a) -> a -> a -> a, тъй като исках да покаже идеята, без да обяснява синтаксиса на Haskell "a.

ХМ Както виждаме, той е много хубав и кратък. Функционалният запис отнема само 18 знака на един ред, включително интервали!

Автоматичното извеждане на типове обаче е сложно нещо и дори на готин език като Haskell понякога се проваля. (като пример може да се посочи ограничението на мономорфизма)

Има ли езици, които са въведени изрично по подразбиране и имплицитно въведени по необходимост? Con
завинаги.

Незадължително имплицитно въвеждане

Новият езиков стандарт C ++, наречен C ++ 11 (преди наричан C ++ 0x), въведе ключовата дума auto, която позволява на компилатора да изведе типа въз основа на контекста:

Нека сравним: // Ръчна спецификация на неподписан тип int a = 5; unsigned int b = a + 3; // Автоматично извеждане на неподписан тип int a = 5; авто b = a + 3;

Не е зле. Но рекордът не се сви много. Нека да видим пример с итератори (ако не разбирате, не се страхувайте, основното е да отбележим, че записът е значително намален поради автоматичното извеждане):

// Ръчна спецификация на векторния тип std :: vec = произволен вектор (30); for (std :: vector :: const_iterator it = vec.cbegin (); ...) (...) // Автоматичен извод на типа auto vec = randomVector (тридесет); for (auto it = vec.cbegin (); ...) (...)

Еха! Това е съкращението. Добре, но можете ли да направите нещо в духа на Haskell, където връщаният тип ще зависи от типовете на аргументите?

Отново отговорът е да, благодарение на ключовата дума decltype в комбинация с auto:

// Ръчно уточняване на типа int divide (int x, int y) (...) // Автоматично приспадане на типа auto divide (int x, int y) -> decltype (x / y) (...)

Може да изглежда, че тази нотация е много добра, но когато се комбинира с генерично програмиране (шаблони/генерични), имплицитното въвеждане или автоматичното извеждане на тип прави чудеса.

Някои езици за програмиране според тази класификация

Ще дам кратък списък с популярни езици и ще напиша как са класифицирани във всяка категория „набиране“.

JavaScript - Динамичен / Слаб / Неявен Ruby - Динамичен / Силен / Неявен Python - Динамичен / Силен / Неявен Java - Статичен / Силен / Явен PHP - Динамичен / Слаб / Неявен C - Статичен / Слаб / Явен C ++ - Статичен / Полу- силен / Явен Perl - Динамичен / Слаб / Имплицитен Objective-C - Статичен / Слаб / Изричен C # - Статичен / Силен / Явен Haskell - Статичен / Силен / Неявен Common Lisp - Динамичен / Силен / Неявен

Може би сбърках някъде, особено с CL, PHP и Obj-C, ако имате различно мнение за някой език - пишете в коментарите.

Въвеждане - присвояване на тип на информационните обекти.

Най-често срещаните примитивни типове данни са:

  • Числова
  • характер
  • Логично

Основните функции на системата за тип данни:

  • Сигурност
    Всяка операция се проверява за аргументи точно от типовете, за които е предназначена;
  • Оптимизация
    Въз основа на вида се избира метод за ефективно съхранение и алгоритми за неговата обработка;
  • Документация
    Изтъкват се намеренията на програмиста;
  • Абстракция
    Използването на типове данни от високо ниво позволява на програмиста да мисли за стойностите като обекти от високо ниво, а не като колекция от битове.

Класификация

Има много класификации на езиците за програмиране, но основните са само 3:

Статично/динамично писане

Статично - проверката на присвояването и съгласуваността на типа се извършва по време на компилиране. Типовете данни са свързани с променливи, а не с конкретни стойности. Статичното писане ви позволява да намерите грешки при въвеждане, направени в рядко използвани клонове на логиката на програмата по време на компилиране.

Динамичното писане е обратното на статичното. При динамично писане всички типове се определят по време на изпълнение.

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

Динамично писане

Var luckyNumber = 777; var siteName = "Tyapk"; // имаме предвид число, пишем низ var wrongNumber = "999";

Статично писане

Нека luckyNumber: номер = 777; нека siteName: string = "Tyapk"; // ще изведе грешка let wrongNumber: number = "999";

  • Статично: Java, C #, TypeScript.
  • Динамичен: Python, Ruby, JavaScript.

Изрично / неявно въвеждане.

Изрично въведените езици се различават по това, че типът на новите променливи / функции / техните аргументи трябва да бъдат посочени изрично. Съответно, езиците с имплицитно въвеждане прехвърлят тази задача към компилатора / интерпретатора. Изричното въвеждане е обратното на имплицитното въвеждане.

Изричното въвеждане изисква изрична декларация на тип за всяка използвана променлива. Този тип въвеждане е специален случай на статично писане, т.к типът на всяка променлива се определя по време на компилиране.

Неявно въвеждане

Нека stringVar = "777" + 99; // получи "77799"

Изрично въвеждане (измислен език, подобен на JS)

Нека wrongStringVar = "777" + 99; // хвърля грешка let stringVar = "777" + String (99); // получи "77799"

Силно / свободно писане

Нарича се още силно/слабо писане. При силно писане типовете се задават "веднъж завинаги", при слабо писане те могат да се променят по време на изпълнение на програмата.

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

Силно въвеждане (измислен език, подобен на JS)

Нека грешно число = 777; грешенНомер = грешенНомер + "99"; // получаваме грешка, че низ е добавен към числовата променлива wrongNumber let trueNumber = 777 + Number ("99"); // получи 876

Свободно въвеждане (както е в js)

Нека грешно число = 777; грешенНомер = грешенНомер + "99"; // получи низа "77799"

  • Строги: Java, Python, Haskell, Lisp.
  • Lax: C, JavaScript, Visual Basic, PHP.
Споделя това