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

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

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

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

Съкратена версия

Типичните езици за програмиране обикновено се разделят на два големи лагера - въведени и нетипизирани (нетипизирани). Първият включва 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(required_element, list): for (index, element) in enumerate(list): if element == required_element: return index return (-1)

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

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

unsigned int find_int(int required_element, int array, unsigned int size) ( for (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, но защо са три? Загубено ли е статичното програмиране?

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

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

Шаблон unsigned int find(T required_element, std::vector масив) ( for (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). Искам да напиша само pi + 1!

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

В резултат на това, когато пишем проста функция sum x y , която умножава всички числа от 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

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

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

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

Има ли езици с явно въвеждане по подразбиране и имплицитно въвеждане по необходимост? Кон
със сигурност.

Неявно въвеждане по избор

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

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

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

// Ръчен тип std::vector vec = случаенВектор(30); for (std::vector::const_iterator it = vec.cbegin(); ...) ( ... ) // Автоматично извеждане на типа auto vec = randomVector (тридесет); за (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, ако имате различно мнение за някой език, пишете в коментарите.

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


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

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

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

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

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

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

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

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

Проблемът с писането за OO системи

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Реализъм

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

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

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

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

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

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

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

Песимизъм

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

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


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


ifn^2< 0 then n:= 3.67 end [E]

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

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

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

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

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

Предимства

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

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

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

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

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

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

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

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

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

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

Типизация: компоненти на успеха

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

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

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

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

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

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

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

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


клас SORTABLE_LIST ...

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

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

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

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

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

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

„Типизирано ли е бебето“?

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

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

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

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

Въвеждане и свързване

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

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

Въвеждане и свързване

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


моят_самолет.долен_колесник

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

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

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

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

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

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

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

ковариация

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

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

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

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

Резервирани са няколко стаи за настаняване на спортисти: само за мъже, само за момичета, само за жени победители. За да покажем това, използваме паралелна йерархия на класове: СТАЯ, МОМИЧЕСКА_СТАЯИ RANKED_GIRL_ROOM.

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


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

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


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

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


- Съквартирант.

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


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

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

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

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

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

[х].Елемент от единично свързан списък СВЪРЗВАЕМтрябва да бъде свързан с друг подобен елемент и инстанцията BI_LINKLE- с подобен. Covariant ще трябва да бъде заменен и аргументът да бъде включен put_right.

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

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

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


-- Задайте attrib на v.

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

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

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

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

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

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

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

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


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

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

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

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


създавам b; create g;-- Създаване на обекти МОМЧЕ и МОМИЧЕ.

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

Паралелната йерархична схема е също толкова проста: замяна СКИОРНа СКИОР1, предизвикателство дял- при поискване s.accommodate(gr), Където гр- тип субект МОМИЧЕСКА_СТАЯ. Резултатът е същият.

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

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

Скриване от потомък

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

Ориз. 17.8.Скриване от потомък

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


Пример за непрограмиране: класът "Щраус" скрива метода "Муха", получен от родителя "Птица".

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

Читателите могат да намерят подробно представяне на тези идеи в редица статии по тази тема, както и в публикации на Кардели (Cardelli), Кастаня (Castagna), Вебер (Weber) и др. Ние няма да се занимаваме с този проблем и ето защо.

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

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

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

Разчитане на фиксиране на типа

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

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


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

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

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

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


create b;create g;-- Създаване на обекти МОМЧЕ и МОМИЧЕ.
s:=b; -- Полиморфно присвояване.

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

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


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

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

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


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

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


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


Където Yкореспондираха х, сега предефинирайки компонента rще изглежда така:


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


създаване (TYPE1) t; s:=t; създаване (TYPE2) t

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Тип правило за Catcall

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

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

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

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

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

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

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

4 Е външна функция.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Степен

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

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

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

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

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

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

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

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

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

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

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


монтиран (друг: GENERAL): като други е
-- Текущият обект (Current), ако неговият тип съвпада с типа на обекта,
-- прикрепен към друго, в противен случай невалиден.
if other /= Void and then conforms_to (other) then

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

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

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


-- Изберете, ако е приложимо, друго като съседен номер.
-- gender_assertained - присвоен пол
gender_accertained_other: като Current
gender_assertained_other:= друго .fitted(Current)
if gender_accertained_other /= Невалиден тогава
споделяне (пол_установен_друг)
„Заключение: Колокация с други не е възможна“

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

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


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

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


occupant1.item.safe_share(occupant2.item)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • Динамичното типизиране е техника, широко използвана в езиците за програмиране и спецификационните езици, при които променливата е свързана с тип в момента на присвояване на стойността, а не в момента на деклариране на променливата. По този начин в различни части на програмата една и съща променлива може да приема стойности от различни типове. Примери за динамично въведени езици са Smalltalk, Python, Objective-C, Ruby, PHP, Perl, JavaScript, Lisp, xBase, Erlang, Visual Basic.

    Противоположната техника е статичното писане.

    В някои езици със слабо динамично въвеждане има проблем със сравняването на стойности, например PHP има операторите за сравнение "==", "!=" и "===", "!==", където втората двойка операции сравнява стойности и типове променливи. Операторът "===" се оценява като вярно само ако съвпада перфектно, за разлика от "==", който счита следния израз за верен: (1=="1"). Струва си да се отбележи, че това не е проблем на динамичното писане като цяло, а на конкретни езици за програмиране.

Свързани понятия

Езикът за програмиране е формален език за писане на компютърни програми. Езикът за програмиране дефинира набор от лексикални, синтактични и семантични правила, които определят външния вид на програмата и действията, които изпълнителят (обикновено компютър) ще изпълнява под негов контрол.

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

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

Разширената форма на Backus-Naur (EBNF) е формална система за дефиниране на синтаксис, в която някои синтактични категории се дефинират последователно чрез други. Използва се за описание на формални граматики без контекст. Предложено от Никлаус Вирт. Това е разширена преработка на формите на Backus-Naur, различава се от BNF в по-"обемни" конструкции, които със същата изразителна способност позволяват да се опрости...

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

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

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

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

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

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

Обектно-ориентираното програмиране (ООП) е методология за програмиране, базирана на представяне на програма като набор от обекти, всеки от които е екземпляр на определен клас, а класовете образуват йерархия на наследяване.

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

За да обясним две напълно различни технологии възможно най-просто, нека започнем от самото начало. Първото нещо, с което се сблъсква програмистът, когато пише код, е декларирането на променливи. Може да забележите, че например в езика за програмиране C++ трябва да посочите типа на променлива. Тоест, ако декларирате променлива x, тогава трябва да добавите int - за съхраняване на целочислени данни, float - за съхраняване на данни с плаваща запетая, char - за символни данни и други налични типове. Следователно C++ използва статично писане, точно както своя предшественик C.

Как работи статичното писане?

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

Нека разгледаме малък пример. Когато инициализираме променливата x (int x;), ние указваме идентификатора int - това е съкращение, за което се съхраняват само цели числа в диапазона от - 2 147 483 648 до 2 147 483 647. По този начин компилаторът разбира, че може да изпълни математически стойности на тази променлива - сума, разлика, умножение и деление. Но, например, функцията strcat(), която свързва две char стойности, не може да се приложи към x. В крайна сметка, ако премахнете ограниченията и се опитате да свържете две int стойности с помощта на символен метод, тогава ще възникне грешка.

Защо се нуждаем от езици с динамично писане?

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

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

Какъв тип писане е най-добър?

Дебатът, че динамичното въвеждане е по-добро от стриктното въвеждане, продължава и до днес. Обикновено те се срещат при високоспециализирани програмисти. Разбира се, уеб разработчиците използват всички предимства на динамичното писане всеки ден, за да създадат висококачествен код и крайния софтуерен продукт. В същото време системните програмисти, които разработват сложни алгоритми на езици за програмиране на ниско ниво, обикновено не се нуждаят от такива възможности, така че статичното въвеждане е достатъчно за тях. Има, разбира се, изключения от правилото. Например, динамичното въвеждане е напълно внедрено в Python.

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

Разделяне на "силно" и "слабо" типизиране

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

Динамична функция

Сигурно сте забелязали, че на етапа на писане на кода компилаторът анализира написаните конструкции и генерира грешка, ако типовете данни не съвпадат. Но не и JavaScript. Неговата уникалност се състои в това, че той ще извърши операцията във всеки случай. Ето лесен пример – искаме да добавим знак и число, което няма смисъл: „x“ + 1.

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

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

Възможни ли са съседни архитектури?

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

Но въпреки това на някои езици можете да промените въвеждането с помощта на допълнителни рамки.

  • В езика за програмиране Delphi подсистемата Variant.
  • В езика за програмиране AliceML - допълнителни пакети.
  • В езика за програмиране Haskell библиотеката Data.Dynamic.

Кога силното въвеждане е наистина по-добро от динамичното писане?

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

Предимства на динамичното писане

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

Научете повече за езиците за програмиране със статично въвеждане

  • C++ е най-широко използваният език за програмиране с общо предназначение. Днес той има няколко основни издания и голяма армия от потребители. Той стана популярен благодарение на своята гъвкавост, възможността за неограничено разширяване и поддръжката на различни програмни парадигми.

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

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

Научете повече за езиците за програмиране с динамично въвеждане

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

  • PHP е скриптов език. Широко използван в уеб разработката, осигуряващ взаимодействие с бази данни за създаване на интерактивни динамични уеб страници. Благодарение на динамичното писане, работата с бази данни е значително улеснена.

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

Динамичен тип писане - недостатъци

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

Обобщете

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

Когато изучавате езици за програмиране, често чувате фрази като „статично въведено“ или „динамично въведено“ в разговори. Тези понятия описват процеса на проверка на типа и както статичната проверка на типа, така и динамичната проверка на типа се отнасят до различни типове системи. Системата за типове е набор от правила, които присвояват свойство, наречено „тип“, на различни обекти в програма: променливи, изрази, функции или модули – с крайната цел да се намалят грешките, като се гарантира, че данните се показват правилно.

Не се притеснявайте, знам, че всичко това звучи объркващо, така че ще започнем с основите. Какво е „проверка на типа“ и какво е тип като цяло?

Тип

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

Често срещани погрешни схващания

Мит 1: Статично/динамично писане == силно/слабо писане

Често срещано погрешно схващане е, че всички статично типизирани езици са силно типизирани и всички динамично типизирани езици са слабо типизирани. Това е грешно и ето защо.

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

X = 1 + "2"

Често свързваме статично типизирани езици като Java и C# със строго типизирани езици (които те са), тъй като типът данни се задава изрично, когато променливата се инициализира - както в този пример на Java:

String foo = нов низ ("здравей свят");

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

Foo = "здравей свят"

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

Свободно типизираният език е език, в който променливите не са обвързани с определен тип данни; те все още имат тип, но ограниченията за безопасност на типа са много по-слаби. Разгледайте следния пример на PHP код:

$foo = "x"; $foo = $foo + 2; // не е грешка echo $foo; // 2

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

Митът е разбит.

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

Мит 2: Статично/динамично писане == компилирани/интерпретирани езици

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

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

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

Заключение

Знам, че в тази статия имаше много информация, но вярвам, че сте разбрали правилно. Бих искал да преместя информация за силно / слабо въвеждане в отделна статия, но това не е толкова важна тема; освен това трябваше да се покаже, че този вид писане няма нищо общо с проверката на типа.

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



Зареждане...
Връх