Основні засади програмування: статична та динамічна типізація. Типізація мов програмування Види типізації

Ця стаття містить необхідний мінімум тих речей, які просто необхідно знати про типізацію, щоб не називати динамічну типізацію злом, Lisp – безтиповою мовою, а C – мовою зі строгою типізацією.

У повній версії міститься докладний опис всіх видів типізації, приправлений прикладами коду, посиланнями на популярні мови програмування та показовими картинками.

Рекомендую прочитати спочатку коротку версію статті, а потім за бажанням і повну.

Коротка версія

Мови програмування по типізації прийнято ділити на два великі табори - типізовані та нетипізовані (бестипові). До першого наприклад відносяться C, Python, Scala, PHP і Lua, а до другого - мова асемблера, Forth та Brainfuck.

Так як "бестипова типізація" за своєю суттю проста як пробка, далі вона ні на які інші види не ділиться. А ось типізовані мови поділяються ще на кілька категорій, що перетинаються:

  • Статична/динамічна типізація. Статична визначається тим, що кінцеві типи змінних та функцій встановлюються на етапі компіляції. Тобто. вже компілятор на 100% упевнений, який тип де знаходиться. У динамічній типізації всі типи з'ясовуються вже під час виконання програми.

    Приклади:
    Статична: C, Java, C#;
    Динамічний: Python, JavaScript, Ruby.

  • Сильна/слабка типізація (також іноді говорять строга/нестрога). Сильна типізація виділяється тим, що мова не дозволяє змішувати у виразах різні типи і не виконує автоматичні неявні перетворення, наприклад не можна відняти безліч. Мови зі слабкою типізацією виконують безліч неявних змін автоматично, навіть якщо може статися втрата точності або перетворення неоднозначно.

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

  • Явна/неявна типізація. Явно-типізовані мови відрізняються тим, що тип нових змінних/функцій/їх аргументів потрібно ставити явно. Відповідно мови з неявною типізацією перекладають це завдання на компілятор/інтерпретатор.

    Приклади:
    Явна: C++, D, C#
    Неявна: PHP, Lua, JavaScript

Також слід зазначити, що всі ці категорії перетинаються, наприклад, мова C має статичну слабку явну типізацію, а мова Python - динамічну сильну неявну.

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

Детальна версія

Якщо короткій версії вам здалося недостатньо, добре. Чи не дарма я писав докладну? Головне, що в короткій версії просто неможливо було вмістити всю корисну та цікаву інформацію, а докладна буде можливо надто довгою, щоб кожен зміг її прочитати, не напружуючись.

Безтипова типізація

У безтипових мовах програмування - всі сутності є просто послідовностями біт, різної довжини.

Безтипова типізація зазвичай притаманна низькорівневим (мова асемблера, Forth) та езотеричним (Brainfuck, HQ9, Piet) мовам. Однак і в неї, поряд із вадами, є деякі переваги.

Переваги
  • Дозволяє писати на гранично низькому рівні, причому компілятор/інтерпретатор не заважатиме будь-якими перевірками типів. Ви вільні робити будь-які операції над будь-якими видами даних.
  • Отримуваний код зазвичай ефективніший.
  • Прозорість вказівок. При знанні мови зазвичай немає сумнівів, що собою являє той чи інший код.
Недоліки
  • Складність. Часто виникає потреба у поданні комплексних значень, таких як списки, рядки чи структури. Із цим можуть виникнути незручності.
  • Відсутність перевірок. Будь-які безглузді дії, наприклад віднімання вказівника на масив із символу будуть вважатися цілком нормальними, що може призвести до трудноуловимыми помилками.
  • Низький рівень абстракції. Робота з будь-яким складним типом даних нічим не відрізняється від роботи з числами, що звичайно створюватиме багато труднощів.
Сильна безтипова типізація?

Так, таке є. Наприклад, у мові асемблера (для архітектури х86/х86-64, інших не знаю) не можна асемблювати програму, якщо ви спробуєте завантажити в регістр cx (16 біт) дані з регістру rax (64 біта).

mov cx, eax; помилка часу асемблювання

Тож виходить, що в ассемлері все-таки є типізація? Я вважаю, що цих перевірок замало. А Ваша думка, звісно, ​​залежить лише від Вас.

Статична та динамічна типізації

Головне, що відрізняє статичну (static) типізацію від динамічної (dynamic) те, що всі перевірки типів виконуються на етапі компіляції, а не на етапі виконання.

Деяким людям може здатися, що статична типізація занадто обмежена (насправді так і є, але цього давно позбулися за допомогою деяких методик). Деяким, що динамічно типізовані мови - це гра з вогнем, але які ж риси їх виділяють? Невже обидва види мають шанси на існування? Якщо ні, чому багато як статично, і динамічно типизированных мов?

Давайте розберемося.

Переваги статичної типізації
  • Перевірки типів відбуваються лише один раз – на етапі компіляції. А це означає, що нам не потрібно буде постійно з'ясовувати, чи не намагаємося ми поділити число на рядок (і видати помилку, або здійснити перетворення).
  • Швидкість виконання. З попереднього пункту ясно, що статично типізовані мови практично завжди швидше динамічно типізовані.
  • За деяких додаткових умов дозволяє виявляти потенційні помилки вже на етапі компіляції.
Переваги динамічної типізації
  • Простота створення універсальних колекцій - купа всього і вся (рідко виникає така необхідність, але коли виникає динамічна типізація виручить).
  • Зручність опису узагальнених алгоритмів (наприклад, сортування масиву, яке працюватиме не тільки на списку цілих чисел, але й на списку речових і навіть на списку рядків).
  • Легкість у освоєнні - мови з динамічною типізацією зазвичай дуже хороші у тому, щоб почати програмувати.

Узагальнене програмування

Добре, найважливіший аргумент за динамічну типізацію – зручність опису узагальнених алгоритмів. Давайте уявімо собі проблему - нам потрібна функція пошуку за кількома масивами (або списками) - за масивом цілих чисел, за масивом речових та масивом символів.

Як же ми її вирішуватимемо? Вирішимо її трьома різними мовами: однією з динамічною типізацією і двома зі статичною.

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

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

Def find(required_element, list): для (index, element) in enumerate(list): if element == required_element: return index return (-1)

Як бачите, все просто і жодних проблем з тим, що список може містити хоч числа, хоч списки, хоч інші масиви немає. Дуже добре. Давайте підемо далі - вирішимо це завдання на Сі!

Статичне рішення (Сі):

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, але чому їх три? Невже статичне програмування програло?

І так і ні. Є кілька методик програмування, одну з яких ми зараз розглянемо. Вона називається узагальнене програмування та мова C++ її непогано підтримує. Давайте подивимося на нову версію:

Статичне рішення (узагальнене програмування, C++):

Template unsigned int find(T required_element, std::vector array) ( for (unsigned int i = 0; i< array.size(); ++i) if (required_element == array[i]) return i; return (-1); }

Добре! Це виглядає не дуже складно, ніж версія на Python і при цьому не довелося багато писати. До того ж ми отримали реалізацію для всіх масивів, а не тільки для трьох, необхідних для вирішення задачі!

Ця версія схоже саме те, що потрібно – ми отримуємо одночасно плюси статичної типізації та деякі плюси динамічної.

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

Статика у динаміці

Також треба згадати, що багато статичних мов дозволяють використовувати динамічну типізацію, наприклад:

  • C# підтримує псевдо тип dynamic.
  • F# підтримує синтаксичний цукор як оператора?, з урахуванням чого то, можливо реалізована імітація динамічної типізації.
  • Haskell – динамічна типізація забезпечується модулем Data.Dynamic.
  • Delphi – за допомогою спеціального типу Variant.

Також деякі динамічно типізовані мови дозволяють скористатися перевагами статичної типізації:

  • Common Lisp – декларації типів.
  • Perl – з версії 5.6, досить обмежено.

Сильна та слабка типізації

Мови з сильною типізацією неможливо змішувати сутності різних типів у виразах і виконують жодних автоматичних перетворень. Також їх називають "мови із суворою типізацією". Англійський термін для цього – strong typing.

Слабо типізовані мови, навпаки, всіляко сприяють, щоб програміст змішував різні типи в одному вираженні, причому компілятор сам приведе все до єдиного типу. Також їх називають "мови з несуворою типізацією". Англійський термін для цього – weak typing.

Слабу типізацію часто плутають з динамічною, що зовсім не так. Динамічно типізована мова може бути слабко і сильно типізована.

Однак мало хто надає значення строгості типізації. Часто заявляють, що якщо мова статично типізована, то Ви зможете відловити безліч потенційних помилок під час компіляції. Вони Вам брешуть!

Мова повинна мати ще й сильну типізацію. І справді, якщо компілятор замість повідомлення про помилку буде просто додавати рядок до числа, або що ще гірше, відніме з одного масиву інший, який нам толк, що всі "перевірки" типів будуть на етапі компіляції? Правильно – слабка статична типізація ще гірша, ніж сильна динамічна! (Ну, це моя думка)

Так що ж у слабкої типізації взагалі немає плюсів? Можливо так виглядає, проте незважаючи на те, що я затятий прихильник сильної типізації, повинен погодитися, що у слабкої теж є переваги.

Бажаєте дізнатися які?

Переваги сильної типізації
  • Надійність - Ви отримаєте виняток або помилку компіляції замість неправильної поведінки.
  • Швидкість - замість прихованих перетворень, які можуть бути досить витратними, із сильною типізацією необхідно писати їх явно, що змушує програміста як мінімум знати, що ця ділянка коду може бути повільною.
  • Розуміння роботи програми - знову ж таки, замість неявного приведення типів, програміст пише все сам, а значить приблизно розуміє, що порівняння рядка і числа відбувається не само-собою і не по-чарівному.
  • Визначеність - коли ви пишете перетворення вручну ви точно знаєте, що ви перетворюєте і на що. Також ви завжди будете розуміти, що такі перетворення можуть призвести до втрати точності та невірних результатів.
Переваги слабкої типізації
  • Зручність використання змішаних виразів (наприклад з цілих чи речових чисел).
  • Абстрагування від типізації та зосередження на задачі.
  • Короткість запису.

Гаразд, ми розібралися, виявляється у слабкої типізації теж є переваги! А чи є способи перенести плюси слабкої типізації на сильну?

Виявляється, є і навіть два.

Неявне приведення типів у однозначних ситуаціях і без втрат даних

Ух… Досить довгий пункт. Давайте я далі скорочуватиму його до "обмежене неявне перетворення" Так що ж означає однозначна ситуація і втрати даних?

Однозначна ситуація, це перетворення чи операція у якій сутність відразу зрозуміла. Ось наприклад додавання двох чисел - однозначна ситуація. А перетворення числа в масив - ні (можливо створиться масив з одного елемента, можливо масив, з такою довгою, заповнений елементами за замовчуванням, а можливо число перетворюється на рядок, а потім на масив символів).

Втрата даних ще простіше. Якщо ми перетворимо речове число 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), ми отримуємо відразу кілька версій - sum для цілих, sum для речових, sum для раціональних, sum для комплексних чисел і навіть sum для всіх тих числових типів, що Ви самі визначили.

Звичайно рятує цей прийом тільки при використанні змішаних виразів із числовими літералами, а це лише верхівка айсбергу.

Таким чином можна сказати, що найкращим виходом буде балансування на межі між сильною і слабкою типізацією. Але поки що ідеальний баланс не тримає жодної мови, тому я більше схиляюся до сильно типізованих мов (таких як Haskell, Java, C#, Python), а не до слабо типізованих (таких як C, JavaScript, Lua, PHP).

Явна та неявна типізації

Мова з явною типізацією передбачає, що програміст повинен вказувати типи всіх змінних та функцій, які оголошує. Англійський термін для цього – explicit typing.

Мова з неявною типізацією, навпаки, пропонує забути про типи і перекласти завдання виведення типів на компілятор або інтерпретатор. Англійський термін для цього – implicit typing.

Спочатку можна вирішити, що неявна типізація рівносильна динамічною, а явна - статичною, але далі ми побачимо, що це не так.

Чи є плюси кожного виду, і знову ж таки, чи є їх комбінації і чи є мови з підтримкою обох методів?

Переваги явної типізації
  • Наявність кожної функції сигнатури (наприклад 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; auto b = a +3;

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

// Ручна вказівка ​​типу std::vector vec = randomVector(30); for (std::vector::const_iterator it = vec.cbegin(); ...) ( ... ) // Автоматичний висновок типу auto vec = randomVector (30); for (auto it = vec.cbegin(); ...) ( ... )

Ух ти! Ось це скорочення. Гаразд, але чи можна зробити щось у дусі Haskell, де тип значення, що повертається, буде залежати від типів аргументів?

І знову відповідь так, завдяки ключовому слову decltype в комбінації з auto:

// Ручна вказівка ​​типу int divide(int x, int y) (...) // Автоматичний висновок типу auto divide(int x, int y) -> decltype(x / y) (...)

Може здатися, що ця форма запису не дуже хороша, але в комбінації з узагальненим програмуванням (templates/generics) неявна типізація або автоматичне виведення типів творять дива.

Деякі мови програмування за даною класифікацією

Я наведу невеликий список з популярних мов і напишу як вони поділяються на кожну категорію "типізацій".

JavaScript - Динамічна / Слаба / Неявна Ruby - Динамічна / Сильна / Неявна Python - Динамічна / Сильна / Неявна Java - Статична / Сильна / Явна PHP - Динамічна / Слаба / Неявна C - Статична / Слаба / Явна C++ - Статична / Напівсильна / Явна - Динамічна / Слаба / Неявна Objective-C - Статична / Слаба / Явна C# - Статична / Сильна / Явна Haskell - Статична / Сильна / Неявна Common Lisp - Динамічна / Сильна / Неявна

Можливо я десь помилився, особливо з CL, PHP та Obj-C, якщо з якоїсь мови у Вас інша думка – напишіть у коментарях.

Простота типізації в ГО-підході є наслідком простоти об'єктної обчислювальної моделі. Опускаючи деталі, можна сказати, що при виконанні ГО-системи відбуваються події лише одного роду – виклик компонента (feature call):


що означає виконання операції fнад об'єктом, приєднаним до x, з передачею аргументу arg(Можливо кілька аргументів або жодного взагалі). Програмісти Smalltalk говорять у цьому випадку про "передачу об'єкту xповідомлення fз аргументом arg", але це - лише відмінність у термінології, тому вона несуттєво.

Те, що все засноване на цій Базовій Конструкції (Basic Construct), частково пояснює відчуття краси ГО-ідей.

З Базисної Конструкції випливають і ті ненормальні ситуації, які можуть виникнути у процесі виконання:

Визначення: порушення типу

Порушення типу в період виконання або, для стислості, просто порушення типу (type violation) виникає у момент виклику x.f (arg), де xприєднано до об'єкту OBJ, якщо:

[x].не існує компонента, що відповідає fі застосовного до OBJ,

[x].такий компонент є, однак, аргумент argйому неприпустимо.

Проблема типізації – уникати таких ситуацій:

Проблема типізації ГО-систем

Коли ми виявляємо, що з виконанні ГО-системи може статися порушення типу?

Ключовим є слово коли. Рано чи пізно ви зрозумієте, що має місце порушення типу. Наприклад, спроба виконати компонент "Пуск торпеди" для об'єкта "Служаючий" не буде працювати і при виконанні відмовиться. Однак можливо ви вважаєте за краще знаходити помилки якомога раніше, а не пізніше.

Статична та динамічна типізація

Хоча можливі і проміжні варіанти, тут представлено два основні підходи:

[x]. Динамічна типізація: чекати на момент виконання кожного виклику і тоді приймати рішення.

[x]. Статична типізація: з урахуванням набору правил визначити за вихідним текстом, чи можливі порушення типів під час виконання. Система виконується, якщо правила гарантують відсутність помилок.

Ці терміни легко можна пояснити: при динамічній типізації перевірка типів відбувається під час роботи системи (динамічно), а при статичній типізації перевірка виконується над текстом статично (до виконання).

Статична типізація передбачає автоматичну перевірку, яка, як правило, покладається на компілятор. У результаті маємо просте визначення:

Визначення: статично типізована мова

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

У літературі зустрічається термін " сильнатипізація" ( strong). Він відповідає ультимативній природі визначення, що потребує повної відсутності порушення типів. Можливі і слабкі (weak) форми статичної типізації, у яких правила усувають певні порушення, не ліквідовуючи їх цілком. У цьому сенсі деякі ГО-мови статично слабо типизированными. Ми боротимемося за найсильнішу типізацію.

У динамічно типизованих мовах, відомих як нетипізовані, відсутні оголошення типів, а до сутностей під час виконання можуть приєднуватися будь-які значення. Статична перевірка типів у них неможлива.

Правила типізації

Наша ОО-нотація є статично типізованою. Її правила типів були запроваджені у попередніх лекціях і зводяться до трьох простих вимог.

[x].При оголошенні кожної сутності або функції повинен задаватися її тип, наприклад, acc: ACCOUNT. Кожна підпрограма має 0 або більше формальних аргументів, тип яких має бути заданий, наприклад: put (x: G; i: INTEGER).

[x].У будь-якому привласненні x:= yі за будь-якого виклику підпрограми, в якому y- це фактичний аргумент для формального аргументу x, тип джерела yмає бути сумісним з типом мети x. Визначення сумісності засноване на спадкування: Bсумісний з A, якщо його нащадком, - доповнене правилами для родових параметрів (див. лекцію 14).

[x].Виклик x.f (arg)вимагає, щоб fбув компонентом базового класу для типу мети x, і fмає бути експортований класу, у якому з'являється виклик (див. 14.3).

Реалізм

Хоча визначення статично типизированного мови дано цілком точно, його недостатньо, - необхідні неформальні критерії під час створення правил типизации. Розглянемо два крайні випадки.

[x]. Абсолютно коректна мова, у якому кожна синтаксично правильна система коректна щодо типів. Правила опису типів непотрібні. Такі мови існують (уявіть собі польський запис висловлювання зі складанням та відніманням цілих чисел). На жаль, жодна реальна універсальна мова не відповідає цьому критерію.

[x]. Абсолютно некоректна мова, який легко створити, взявши будь-яку існуючу мову і додавши правило типізації, що робить будь-якусистему некоректною. За визначенням, ця мова типізована: оскільки немає систем, що відповідають правилам, то жодна система не викличе порушення типів.

Можна сказати, що мови першого типу придатні, але марні, другі, можливо, корисні, але не придатні.

Насправді необхідна система типів, придатна і корисна одночасно: досить потужна реалізації потреб обчислень і досить зручна, яка змушує нас на ускладнення задоволення правил типизации.

Говоритимемо, що мова реалістичнийякщо він придатний до застосування і корисний на практиці. На відміну від визначення статичної типізації, що дає безапеляційну відповідь на запитання: " Чи типизована мова X статично?", Визначення реалізму частково суб'єктивне.

У цій лекції ми переконаємося, що запропонована нами нотація є реалістичною.

Песимізм

Статична типізація приводить за своєю природою до "песимістичної" політики. Спроба дати гарантію, що всі обчислення не призводять до відмови, відкидає обчислення, які могли б закінчитися без помилок.

Розглянемо звичайну, необ'єктну, Pascal-подібну мову з різними типами REALі INTEGER. При описі n: INTEGER; r: Realоператор n:= rбуде відхилено, як порушує правила. Так, компілятор відкине всі наступні оператори:


Якщо ми дозволимо їх виконання, то побачимо, що [A] буде працювати завжди, тому що будь-яка система числення має точне уявлення речового числа 0,0, що недвозначно перекладається в 0 цілих. [B] майже напевно також працюватиме. Результат дії [C] не очевидний (чи хочемо отримати результат округленням або відкиданням дробової частини?). [D] впорається зі своїм завданням, як і оператор:


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

куди входить недосяжне привласнення ( n^2- це квадрат числа n). Після заміни n^2на nправильний результат дасть лише ряд запусків. Привласнення nвеликого речового значення, не представимого цілим, призведе до відмови.

У типизованих мовах всі ці приклади (працюючі, непрацюючі, іноді працюють) безжально трактуються як порушення правил опису типів і відхиляються будь-яким компілятором.

Питання не в тому, будемоми песимістами, а в тому, наскількипесимістичні ми можемо дозволити собі бути. Повернемося до вимоги реалізму: якщо правила типів настільки песимістичні, що перешкоджають простоті запису обчислень, ми їх відкинемо. Але якщо досягнення безпеки типів досягається невеликою втратою виразної сили, ми приймемо їх. Наприклад, серед розробки, що надає функції округлення і виділення цілої частини - roundі truncate, оператор n:= rвважається некоректним справедливо, оскільки змушує вас явно записати перетворення речовинного числа на ціле, замість використання двозначних перетворень за умовчанням.

Статична типізація: як і чому

Хоча переваги статичної типізації є очевидними, непогано поговорити про них ще раз.

Переваги

Причини застосування статичної типізації в об'єктній технології ми перерахували на початку лекції. Це надійність, простота розуміння та ефективність.

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

Раннє виявлення помилок важливо ще й тому, що чим довше ми відкладатимемо їх пошук, тим сильніше зростуть витрати на виправлення. Ця властивість, інтуїтивно зрозуміла всім програмістам-професіоналам, кількісно підтверджують широко відомі роботи Бема (Boehm). Залежність витрат за виправлення від часу відшукання помилок наведено на графіці, побудованому за даними низки великих промислових проектів і проведених експериментів з невеликим керованим проектом:

Рис. 17.1.Порівняльні витрати на виправлення помилок (, публікується з дозволу)

Читабельністьабо Простота розуміння(Readability) має свої переваги. У всіх прикладах цієї книги поява типу власне дає читачеві інформацію про її призначення. Читабельність дуже важлива на етапі супроводу.

Зрештою, ефективністьможе визначати успіх чи відмовитися від об'єктної технології практично. За відсутності статичної типізації виконання x.f (arg)може піти скільки завгодно часу. Причина цього в тому, що на етапі виконання, не знайшовши fв базовому класі цілі x, Пошук буде продовжено у її нащадків, а це вірна дорога до неефективності. Зняти гостроту проблеми можна, покращивши пошук компонента з ієрархії. Автори мови Self провели велику роботу, прагнучи генерувати найкращий код для мови з динамічною типізацією. Але саме статична типізація дозволила такому ГО-продукту наблизитися або зрівнятися з ефективності з традиційним ПЗ.

Ключем до статичної типізації є вже висловлена ​​ідея, що компілятор, що генерує код для конструкції x.f (arg), знає тип x. Через поліморфізм немає можливості однозначно визначити відповідну версію компонента f. Але оголошення звужує безліч можливих типів, дозволяючи компілятор побудувати таблицю, що забезпечує доступ до правильного fз мінімальними витратами, - з обмеженою константоюскладністю доступу. Додатково виконувані оптимізації статичного зв'язування (static binding)і підстановки (inlining)- також полегшуються завдяки статичної типізації, повністю усуваючи витрати у випадках, що вони застосовні.

Аргументи на користь динамічної типізації

Незважаючи на все це, динамічна типізація не втрачає своїх прихильників, зокрема серед Smalltalk-програмістів. Їхні аргументи засновані насамперед на реалізмі, про який йшлося вище. Вони впевнені, що статична типізація надто обмежує їх, не даючи їм вільно висловлювати свої творчі ідеї, іноді називаючи її "поясом цнотливості".

З такою аргументацією можна погодитись, але лише для статично типізованих мов, які не підтримують низку можливостей. Варто відзначити, що всі концепції, пов'язані з поняттям типу і введені в попередніх лекціях, необхідні - відмова від будь-якої з них загрожує серйозними обмеженнями, а їх введення, навпаки, надає нашим діям гнучкості, а нам самим дає можливість повною мірою насолодитися практичністю статичної типізації.

Типізація: доданки успіху

Які механізми реалістичної статичної типізації? Всі вони введені в попередніх лекціях, а тому нам лишається коротко про них нагадати. Їхнє спільне перерахування показує узгодженість і потужність їх об'єднання.

Наша система типів повністю заснована на понятті класу. Класами є навіть такі основні типи, як INTEGER, отже, нам не потрібні спеціальні правила опису визначених типів. (У цьому наша нотація відрізняється від "гібридних" мов на кшталт Object Pascal, Java і C++, де система типів старих мов поєднується з об'єктною технологією, що базується на класах.)

Розгорнуті типидають нам більше гнучкості, допускаючи типи, чиї значення позначають об'єкти, поруч із типами, чиї значення позначають посилання.

Вирішальне слово у створенні гнучкої системи типів належить успадкуванняі пов'язане з ним поняття сумісності. Тим самим долається головне обмеження класичних типізованих мов, наприклад, Pascal та Ada, у яких оператор x:= yвимагає, щоб тип xі yбув однаковим. Це правило дуже суворо: воно забороняє використовувати сутності, які можуть означати об'єкти взаємопов'язаних типів ( SAVINGS_ACCOUNTі CHECKING_ACCOUNT). При наслідуванні ми вимагаємо лише сумісності типу yз типом x, наприклад, xмає тип ACCOUNT, y - SAVINGS_ACCOUNT, і другий клас - спадкоємець першого.

На практиці статично типізована мова потребує підтримки множинного успадкування. Відомі важливі звинувачення статичної типізації у цьому, що вона дає можливість по-різному інтерпретувати об'єкти. Так, об'єкт DOCUMENT(документ) може передаватися через мережу, а тому потребує наявності компонентів, пов'язаних з типом MESSAGE(повідомлення). Але ця критика правильна лише для мов, обмежених поодиноким успадкуванням.

Рис. 17.2.Множинне успадкування

Універсальністьнеобхідна, наприклад, для опису гнучких, але безпечних контейнерних структур даних (наприклад class LIST [G] ...). Якби не було цього механізму, статична типізація зажадала б оголошення різних класів для списків, що відрізняються типом елементів.

У ряді випадків універсальність потрібна обмежитищо дозволяє використовувати операції, застосовні лише до сутностей родового типу. Якщо родовий клас SORTABLE_LISTпідтримує сортування, він вимагає від сутностей типу G, де G- Пологовий параметр, наявність операції порівняння. Це досягається зв'язуванням з Gкласу, що задає родове обмеження, - COMPARABLE:


class SORTABLE_LIST ...

Будь-який фактичний родовий параметр SORTABLE_LISTмає бути нащадком класу COMPARABLEмає необхідний компонент.

Ще один обов'язковий механізм – спроба привласнення- організує доступ до тих об'єктів, типом яких не керує. Якщо y- це об'єкт бази даних або об'єкт, отриманий через мережу, оператор x ?= yприсвоїть xзначення y, якщо yмає сумісний тип, або якщо це не так, дасть xзначення Void.

Затвердження, пов'язані як частина ідеї Проектування за Контрактом, з класами та їх компонентами у формі передумов, постумов та інваріантів класу, дають можливість описувати семантичні обмеження, які не охоплюються специфікацією типу. У таких мовах, як Pascal і Ada, є типи-діапазони, здатні обмежити значення сутності, наприклад, інтервалом від 10 до 20, однак, застосовуючи їх, вам не вдасться досягти того, щоб значення iбуло негативним, завжди вдвічі перевищуючи j. На допомогу приходять інваріанти класів, покликані точно відображати обмеження, якими б складними вони не були.

Закріплені оголошенняпотрібні у тому, щоб практично уникати лавинного дублювання коду. Оголошуючи y: like x, ви отримуєте гарантію того, що yбуде змінюватися за будь-якими повторними оголошеннями типу xу нащадка. Без цього механізму розробники постійно займалися б повторними оголошеннями, прагнучи зберегти відповідність різних типів.

Закріплені оголошення - це особливий випадок останнього необхідного мовного механізму - підступності, докладне обговорення якого ми маємо пізніше.

При розробці програмних систем на ділі необхідна ще одна властивість, властива самому середовищу розробки - швидка, зростаюча (fast incremental) перекомпіляція. Коли ви пишите або модифікуєте систему, хотілося б якнайшвидше побачити ефект змін. При статичній типізації ви повинні дати компілятору час на повторну перевірку типів. Традиційні підпрограми компіляції вимагають повторної трансляції усієї системи (і її складання), і цей процес може бути дуже тривалим, особливо з переходом до систем великого масштабу. Це стало аргументом на користь інтерпретуютьсистем, таких як ранні середовища Lisp або Smalltalk, що запускали систему майже без обробки, не виконуючи перевірку типів. Наразі цей аргумент забутий. Хороший сучасний компілятор визначає, як змінився код з останньої компіляції, і обробляє лише знайдені зміни.

"Типізована чи малюк"?

Наша мета - строгастатична типізація. Саме тому ми і повинні уникати будь-яких лазівок у нашій "грі за правилами", принаймні точно їх ідентифікувати, якщо вони існують.

Найпоширенішою лазівкою у статично типізованих мовах є наявність перетворень, що змінюють тип сутності. У C та похідних від нього мовами їх називають "наведенням типу" або кастингом (cast). Запис (OTHER_TYPE) xвказує на те, що значення xсприймається компілятором як має тип OTHER_TYPE, за дотримання деяких обмежень на можливі типи.

Подібні механізми оминають обмеження перевірки типів. Приведення широко поширене при програмуванні мовою C, включаючи діалект ANSI C. Навіть у мові C++ приведення типів, хоч і настільки часто, залишається звичним і, можливо, необхідною справою.

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

Типізація та зв'язування

Хоча як читач цієї книги ви напевно відрізняєте статичну типізацію від статичного зв'язування, є люди, яким подібне не під силу. Частково це може бути пов'язане з впливом мови Smalltalk, що відстоює динамічний підхід до обох завдань і здатного сформувати неправильне уявлення, що вони мають однакове рішення. (Ми ж у своїй книзі стверджуємо, що для створення надійних та гнучких програм бажано поєднати статичну типізацію та динамічне зв'язування.)

Як типізація, і зв'язування мають справу з семантикою Базисної Конструкції x.f (arg), але відповідають на два різні питання:

Типізація та зв'язування

[x]. Питання про типізацію: коли ми повинні точно знати, що під час виконання з'явиться операція, що відповідає f, застосовна до об'єкта, приєднаного до сутності x(З параметром arg)?

[x]. Питання про зв'язування: коли ми повинні знати, яку операцію ініціює цей виклик?

Типізація відповідає на запитання про наявність як мінімум однієїоперації, зв'язування відповідає за вибір потрібної.

В рамках об'єктного підходу:

[x].проблема, що виникає при типізації, пов'язана з поліморфізмом: оскільки xпід час виконанняможе позначати об'єкти декількох різних типів, ми повинні бути впевнені, що операція, що представляє f, доступнау кожному з цих випадків;

[x].проблема зв'язування викликана повторними оголошеннями: оскільки клас може змінювати успадковані компоненти, то можуть знайтися дві або більше операції, які претендують на те, щоб представляти fу цьому виклику.

Обидві завдання можуть бути вирішені динамічно, так і статично. У існуючих мовах представлені чотири варіанти рішення.

[x].Ряд необ'єктних мов, скажімо, Pascal та Ada, реалізують як статичну типізацію, так і статичне зв'язування. Кожна сутність є об'єктами лише одного типу, заданого статично. Тим самим забезпечується надійність рішення, платою яку є його гнучкість.

[x]. Smalltalk та інші ГО-мови містять засоби динамічного зв'язування та динамічної типізації. При цьому перевага віддається гнучкості на шкоду надійності мови.

[x].Окремі необ'єктні мови підтримують динамічну типізацію та статичне зв'язування. Серед них - мови асемблера та ряд мов сценаріїв (scripting languages).

[x].Ідеї ​​статичної типізації та динамічного зв'язування втілені у нотації, запропонованій у цій книзі.

Зазначимо своєрідність мови C++, що підтримує статичну типізацію, хоча і не строгу з огляду на наявність типів, статичне зв'язування (за замовчуванням), динамічне зв'язування при явній вказівці віртуальних ( virtual) оголошень.

Причина вибору статичної типізації та динамічного зв'язування очевидна. Перше питання: "Коли ми знатимемо про існування компонентів?" - передбачає статичну відповідь: " Чим раніше, тим краще", що означає: під час компіляції. Друге питання: "Який компонент використовувати?" передбачає динамічний відповідь: " той, який потрібен", - відповідний динамічному типу об'єкта, що визначається під час виконання. Це єдино прийнятне рішення, якщо статичне та динамічне зв'язування дає різні результати.

Наступний приклад ієрархії наслідування допоможе прояснити ці поняття:

Рис. 17.3.Види літальних апаратів

Розглянемо виклик:


my_aircraft.lower_landing_gear

Питання про типізацію: коли переконатись, що тут буде компонент lower_landing_gear("випустити шасі"), що застосовується до об'єкта (для COPTERйого не буде зовсім) Питання про зв'язування: яку з кількох можливих версій вибрати.

Статичне зв'язування означало б, що ми ігноруємо тип об'єкта, що приєднується, і покладаємося на оголошення сутності. У результаті, маючи справу з Boeing 747-400, ми викликали б версію, розроблену для звичайних лайнерів серії 747, а не їх модифікації 747-400. Динамічне зв'язування застосовує операцію, потрібну об'єктом, і це правильний підхід.

При статичній типізації компілятор не відхиляє виклик, якщо можна гарантувати, що під час виконання програми до сутності my_aircraftбуде приєднано об'єкт, що поставляється з компонентом, що відповідає lower_landing_gear. Базисна техніка отримання гарантій проста: за обов'язкового оголошення my_aircraftпотрібно, щоб базовий клас типу включав такий компонент. Тому my_aircraftне може бути оголошений як AIRCRAFT, оскільки останній не має lower_landing_gearна цьому рівні; вертольоти, принаймні у нашому прикладі, випускати шасі не вміють. Якщо ж ми оголосимо сутність як PLANE, - клас, що містить необхідний компонент, - все буде гаразд.

Динамічна типізація в стилі Smalltalk вимагає дочекатися виклику, і в момент виконання перевірити наявність потрібного компонента. Така поведінка можлива для прототипів та експериментальних розробок, але неприпустимо для промислових систем – у момент польоту пізно запитувати, чи є у вас шасі.

Коваріантність та приховування нащадком

Якби світ був простий, то розмову про типізацію можна було б закінчити. Ми визначили цілі та переваги статичної типізації, вивчили обмеження, яким повинні відповідати реалістичні системи типів, та переконалися у тому, що запропоновані методи типізації відповідають нашим критеріям.

Але світ непростий. Об'єднання статичної типізації з деякими вимогами програмної інженерії створює проблеми складніші, ніж це здається на перший погляд. Проблеми викликають два механізми: коваріантність (covariance)- Зміна типів параметрів при перевизначенні, приховування нащадком (descendant hiding)- Здатність класу нащадка обмежувати статус експорту успадкованих компонентів.

Коваріантність

Що відбувається з аргументами компонента під час перевизначення його типу? Це найважливіша проблема, і ми вже бачили ряд прикладів її прояву: пристрої та принтери, одно- та двозв'язні списки тощо (див. розділи 16.6, 16.7).

Ось ще один приклад, який допомагає усвідомити природу проблеми. І нехай він далекий від реальності і метафоричний, але його близькість до програмних схем очевидна. До того ж, розбираючи його, ми часто повертатимемося до завдань із практики.

Уявімо собі лижну команду університету, що готується до чемпіонату. Клас GIRLвключає лижниць, що виступають у складі жіночої збірної, BOY- Лижників. Низка учасників обох команд ранжована, показавши хороші результати на попередніх змаганнях. Це важливо для них, оскільки тепер вони втечуть першими, отримавши перевагу перед рештою. (Це правило, що дає привілеї вже привілейованим, можливо і робить слалом і лижні гонки настільки привабливими в очах багатьох людей, будучи гарною метафорою самого життя.) Отже, ми маємо два нові класи: RANKED_GIRLі RANKED_BOY.

Рис. 17.4.Класифікація лижників

Для проживання спортсменів заброньовано ряд номерів: тільки для чоловіків, лише для дівчат, тільки для дівчат-призерів. Для відображення цього використовуємо паралельну ієрархію класів: ROOM, GIRL_ROOMі RANKED_GIRL_ROOM.

Ось малюнок класу SKIER:


- Сусід за номером.
... Інші можливі компоненти, опущені в цьому та наступних класах...

Нас цікавлять два компоненти: атрибут roommateта процедура share, "що розміщує" даного лижника в одному номері з поточним лижником:


При оголошенні сутності otherможна відмовитися від типу SKIERна користь закріпленого типу like roommate(або like Currentдля roommateі otherодночасно). Але забудемо на час про закріплення типів (ми до них ще повернемося) і подивимося на проблему коваріантності в її первісному вигляді.

Як ввести перевизначення типів? Правила вимагають роздільного проживання юнаків та дівчат, призерів та інших учасників. Для вирішення цього завдання при перевизначенні змінимо тип компонента roommate, як показано нижче (тут і надалі перевизначені елементи підкреслені).


- Сусід за номером.

Перевизначимо, відповідно, і аргумент процедури share. Більш повний варіант класу тепер виглядає так:


- Сусід за номером.
- Вибрати сусіда іншого.

Аналогічно слід змінити всі породжені від SKIERкласи (закріплення типів ми не використовуємо). У результаті маємо ієрархію:

Рис. 17.5.Ієрархія учасників та повторні визначення

Оскільки успадкування є спеціалізацією, правила типів вимагають, щоб при перевизначенні результату компонента, в даному випадку roommate, новий тип був нащадком вихідного. Те саме стосується і перевизначення типу аргументу otherпідпрограми share. Ця стратегія називається коваріантністю, де приставка "ко" вказує на спільну зміну типів параметра і результату. Протилежна стратегія називається контраваріантністю.

Усі наші приклади переконливо свідчать про практичну необхідність підступності.

[x].Елемент однозв'язкового списку LINKABLEповинен бути пов'язаний з іншим подібним до себе елементом, а екземпляр BI_LINKABLE- З подібним до себе. Коваріантно потрібно перевизначається і аргумент у put_right.

[x].Будь-яка підпрограма у складі LINKED_LISTз аргументом типу LINKABLEпри переході до TWO_WAY_LISTвимагатиме аргументу BI_LINKABLE.

[x].Процедура set_alternateприймає DEVICE-аргумент у класі DEVICEі PRINTER-аргумент – у класі PRINTER.

Коваріантне перевизначення набуло особливого поширення тому, що приховування інформації веде до створення процедур виду


-- Встановити attrib у v.

для роботи з attribтипу SOME_TYPE. Подібні процедури, природно, підступні, оскільки будь-який клас, який змінює тип атрибуту, повинен відповідно перевизначати і аргумент set_attrib. Хоча представлені приклади укладаються в одну схему, але коваріантність поширена значно ширше. Подумайте, наприклад, про процедуру чи функцію, що виконує конкатенацію однозв'язних списків ( LINKED_LIST). Її аргумент має бути перевизначено як двозв'язний список ( TWO_ WAY_LIST). Універсальна операція додавання infix "+"приймає NUMERIC-аргумент у класі NUMERIC, REAL- в класі REALі INTEGER- в класі INTEGER. У паралельних ієрархіях телефонної служби процедури startв класі PHONE_SERVICEможе бути потрібний аргумент ADDRESS, що представляє адресу абонента, (для виписки рахунку), в той час як в цій же процедурі в класі CORPORATE_SERVICEпотрібен аргумент типу CORPORATE_ADDRESS.

Рис. 17.6.Служби зв'язку

Що можна сказати про контраваріантне рішення? У прикладі з лижниками воно означало б, що якщо, переходячи до класу RANKED_GIRL, тип результату roommateперевизначили як RANKED_GIRL, то через контраваріантність тип аргументу shareможна перевизначити на тип GIRLабо SKIER. Єдиний тип, який не допустимо при контраваріантному рішенні, - це RANKED_GIRL! Достатньо, щоб порушити найгірші підозри у батьків дівчат.

Паралельні ієрархії

Щоб не залишити каменю на камені, розглянемо варіант прикладу SKIERіз двома паралельними ієрархіями. Це дозволить нам змоделювати ситуацію, яка вже зустрічалася на практиці: TWO_ WAY_LIST > LINKED_LISTі BI_LINKABLE > LINKABLE; або ієрархію з телефонною службою PHONE_SERVICE.

Нехай є ієрархія із класом ROOM, нащадком якого є GIRL_ROOM(клас BOYопущений):

Рис. 17.7.Лижники та кімнати

Наші класи лижників у цій паралельній ієрархії замість roommateі shareматимуть аналогічні компоненти accommodation (розміщення) та accommodate (розмістити):


description: "Новий варіант із паралельними ієрархіями"
accommodate (r: ROOM) is ... require ... do

Тут також потрібні коваріантні перевизначення: у класі GIRL1як accommodation, так і аргумент підпрограми accommodateмають бути замінені типом GIRL_ROOM, в класі BOY1- типом BOY_ROOMі т.д. (Не забудьте: ми, як і раніше, працюємо без закріплення типів.) Як і в попередньому варіанті прикладу, контраваріантність тут марна.

Своєрідність поліморфізму

Чи не вистачає прикладів, що підтверджують практичність коваріації? Чому ж хтось розглядає контраваріантність, яка суперечить тому, що необхідно на практиці (якщо не брати до уваги поведінки деяких молодих людей)? Щоб зрозуміти це, розглянемо проблеми, що виникають при поєднанні поліморфізму та стратегії коваріантності. Вигадати шкідницьку схему нескладно, і, можливо, ви вже створили її самі:


create b; create g;-- Створення об'єктів BOY та GIRL.

Результат останнього виклику, цілком можливо, приємний для юнаків, - це саме те, що ми намагалися не допустити за допомогою перевизначення типів. Виклик shareведе до того, що об'єкт BOY, відомий як bі завдяки поліморфізму, який отримав псевдонім sтипу SKIER, стає сусідом об'єкту GIRL, відомого під ім'ям g. Проте виклик, хоч і суперечить правилам гуртожитку, є цілком коректним у програмному тексті, оскільки share-експортований компонент у складі SKIER, а GIRL, тип аргументу g, сумісний з SKIER, типом формального параметра share.

Схема з паралельною ієрархією така ж проста: замінимо SKIERна SKIER1, виклик share- на виклик s.accommodate (gr), де gr- сутність типу GIRL_ROOM. Результат – той самий.

При контраваріантному вирішенні цих проблем не виникало б: спеціалізація мети виклику (у прикладі s) вимагала б узагальнення аргументу. Контраваріантність у результаті веде до більш простої математичної моделі механізму: успадкування – перевизначення – поліморфізм. Цей факт описаний у ряді теоретичних статей, що пропонують цю стратегію. Аргументація не надто переконлива, оскільки, як показують наші приклади та інші публікації, контраваріантність немає практичного використання.

Тому, не намагаючись натягнути контраваріантний одяг на ковріантне тіло, слід прийняти дійсність і шукати шляхи усунення небажаного ефекту.

Приховування нащадком

Перш ніж шукати вирішення проблеми коваріантності, розглянемо ще один механізм, здатний в умовах поліморфізму призвести до порушень типу. Приховування нащадком (descendant hiding) – це здатність класу не експортувати компонент, отриманий від батьків.

Рис. 17.8.Приховування нащадком

Типовим прикладом є компонент add_vertex(додати вершину), що експортується класом POLYGON, але приховується його нащадком RECTANGLE(через можливе порушення інваріанту - клас хоче залишатися прямокутником):


Чи не програмістський приклад: клас "Страус" приховує метод "Літати", отриманий від батька "Птиця".

Давайте на хвилину приймемо цю схему такою, як вона є, і поставимо питання, чи буде легітимним поєднання успадкування та приховування. Моделююча роль приховування, подібно до коваріантності, порушується через трюки, можливі через поліморфізм. І тут не важко побудувати шкідливий приклад, що дозволяє, незважаючи на приховування компонента, викликати його та додати прямокутнику вершину:


create r; -- Створення об'єкта RECTANGLE.
p:= r; - Поліморфне привласнення.

Оскільки об'єкт rховається під сутністю pкласу POLYGON, а add_vertexекспортований компонент POLYGON, то його виклик сутністю pкоректний. В результаті виконання у прямокутнику з'явиться ще одна вершина, а отже, буде створено неприпустимий об'єкт.

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

Для обговорення проблем коваріантності та приховування нащадком нам знадобиться кілька нових термінів. Будемо називати класово-коректною (class-valid)систему, що відповідає трьом правилам опису типів, наведеним на початку лекції. Нагадаємо їх: - кожна сутність має свій тип; тип фактичного аргументу має бути сумісним із типом формального, аналогічна ситуація із привласненням; компонент, що викликається, повинен бути оголошений у своєму класі і експортований класу, що містить виклик.

Система називається системно-коректною (system-valid), якщо за її виконанні немає порушення типів.

В ідеалі обидва поняття мають співпадати. Однак ми вже бачили, що класово-коректна система в умовах успадкування, підступності та приховування нащадком може не бути системно-коректною. Назвемо таку помилку порушенням системної коректності (system validity error).

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

Простота проблеми створює своєрідний парадокс: допитливий новачок побудує контрприклад за лічені хвилини, у реальній практиці день у день виникають помилки класової коректності систем, але порушення системної коректності навіть у великих багаторічних проектах виникають виключно рідко.

Однак це не дозволяє ігнорувати їх, тому ми приступаємо до вивчення трьох можливих шляхів вирішення даної проблеми.

Далі ми зачіпатимемо дуже тонкі і не так часто дають про себе знати аспекти об'єктного підходу. Читаючи книгу вперше, ви можете пропустити розділи цієї лекції, що залишилися. Якщо ви лише недавно зайнялися питаннями ГО-технології, то краще засвоїте цей матеріал після вивчення лекцій 1-11 курсу "Основи об'єктно-орієнтованого проектування", присвяченої методології успадкування, та особливо лекції 6 курсу "Основи об'єктно-орієнтованого проектування", присвяченої методології успадкування.

Коректність систем: перше наближення

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

Контраваріантність та безваріантність

Контраваріантність усуває теоретичні проблеми, пов'язані з порушенням системної коректності. Однак при цьому втрачається реалістичність системи типів, тому розглядати цей підхід надалі немає жодної необхідності.

Оригінальність мови C++ у тому, що вона використовує стратегію безваріантності (novariance), не дозволяючи змінювати тип аргументів у підпрограмах, що перевизначаються! Якби мова C++ була строго типізованою мовою, її системної типів було б важко користуватися. Найпростіше вирішення проблеми у цій мові, як і обхід інших обмежень C++ (скажімо, відсутності обмеженої універсальності), полягає у використанні кастингу – приведення типу, що дозволяє повністю ігнорувати наявний механізм типізації. Це рішення не видається привабливим. Зауважимо, однак, що ряд пропозицій, що обговорюються нижче, спиратиметься на безваріантність, сенс якої додасть запровадження нових механізмів роботи з типами натомість коваріантного перевизначення.

Використання родових параметрів

Універсальність є основою цікавої ідеї, вперше висловленої Францем Вебером (Franz Weber). Оголосимо клас SKIER1обмеживши універсалізацію родового параметра класом ROOM:


class SKIER1 feature
accommodate (r: G) is ... require ... do accommodation:= r end

Тоді клас GIRL1буде спадкоємцем SKIER1і т. д. Тим самим прийомом, яким би дивним він не здавався на перший погляд, можна скористатися і за відсутності паралельної ієрархії: class SKIER.

Цей підхід дозволяє вирішити проблему підступності. За будь-якого використання класу необхідно задати фактичний родовий параметр ROOMабо GIRL_ROOMтак що неправильна комбінація просто стає неможливою. Мова стає безваріантною, а система повністю відповідає потребам коваріантності завдяки родовим параметрам.

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

Типові змінні

Ряд авторів, серед яких Кім Брюс (Kim Bruce), Девід Шенг (David Shang) та Тоні Саймонс (Tony Simons), запропонували рішення на основі типових змінних (type variables), значення яких є типи. Їхня ідея проста:

[x].замість коваріантних перевизначень дозволити оголошення типів, що використовує типові змінні;

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

[x].забезпечити можливість привласнення типовим змінним як значень типів мови.

Докладний виклад цих ідей читачі можуть знайти у ряді статей з цієї тематики, соціальній та публікаціях Карделлі (Cardelli), Кастаньи (Castagna), Вебера (Weber) та інших. Почати вивчення питання можна з джерел, зазначених у бібліографічних нотатках до цієї лекції. Ми ж не займатимемося цією проблемою, і ось чому.

[x].Належно реалізований механізм типових змінних відноситься до категорії, що дозволяє використовувати тип без повної специфікації. Ця ж категорія включає універсальність та закріплення оголошень. Цей механізм міг би замінити інші механізми цієї категорії. Спочатку це можна витлумачити на користь типових змінних, але результат може виявитися жалюгідним, тому що не ясно, чи зможе цей всеосяжний механізм впоратися з усіма завданнями з тією легкістю та простотою, яка притаманна універсальності та закріпленню типів.

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

Це змушує нас повернутися до вже розглянутих механізмів: обмеженої та необмеженої універсальності, закріплення типів та, звичайно, успадкування.

Покладаючись на закріплення типів

Майже готове вирішення проблеми підступності ми знайдемо, придивившись до відомого нам механізму закріплених оголошень.

При описі класів SKIERі SKIER1вас не могло не відвідати бажання, скориставшись закріпленими оголошеннями, позбавитися багатьох перевизначень. Закріплення – це типовий коваріантний механізм. Ось як виглядатиме наш приклад (всі зміни підкреслено):


share (other: like Current) is ... require ... do
accommodate (r: like accommodation) is ... require ... do

Тепер нащадки можуть залишити клас SKIERбез змін, а в SKIER1їм знадобиться перевизначити лише атрибут accommodation. Закріплені сутності: атрибут roommateта аргументи підпрограм shareі accommodate- Змінюватимуться автоматично. Це значно спрощує роботу та підтверджує той факт, що за відсутності закріплення (або іншого подібного механізму, наприклад, типових змінних) написати ГО-програмний продукт із реалістичною типізацією неможливо.

Але чи вдалося усунути порушення коректності системи? Ні! Ми, як і раніше, можемо перехитрити перевірку типів, виконавши поліморфні привласнення, що спричиняють порушення системної коректності.

Щоправда, вихідні варіанти прикладів буде відхилено. Нехай:


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

Аргумент g, переданий share, тепер невірний, тому що тут потрібний об'єкт типу like s, а клас GIRLне сумісний з цим типом, оскільки за правилом закріплених типів жоден тип не сумісний з like sкрім нього самого.

Втім, радіти нам не довго. В інший бік це правило свідчить, що like sсумісний з типом s. Отже, використовуючи поліморфізм не тільки об'єкта s, але і параметра g, ми можемо знову обійти систему перевірки типів:


s: SKIER; b: BOY; g: like s; actual_g: GIRL;
create b; create actual_g -- Створення об'єктів BOY та GIRL.
s:= actual_g; g:= s -- Через s приєднати g до GIRL.
s:= b - Поліморфне присвоєння.

Внаслідок цього незаконний виклик проходить.

Вихід із становища є. Якщо ми всерйоз готові використовувати закріплення оголошень як єдиний механізм коваріантності, то позбутися порушень системної коректності можна повністю заборонивши поліморфізм закріплених сутностей. Це вимагатиме зміни у мові: введемо нове ключове слово anchor(ця гіпотетична конструкція потрібна нам виключно для того, щоб використовувати її в даному обговоренні):


Дозволимо оголошення виду like sлише тоді, коли sописано як anchor. Змінимо правила сумісності так, щоб гарантувати: sта елементи типу like sможуть приєднуватися (у привласнення або передачі аргументу) тільки один до одного.

За такого підходу ми усуваємо з мови можливість перевизначення типу будь-яких аргументів підпрограми. Крім цього, ми могли заборонити перевизначати тип результату, але цього немає необхідності. Можливість перевизначення типу атрибутів, звісно, ​​зберігається. Усеперевизначення типів аргументів тепер виконуватимуться неявно через механізм закріплення, який ініціює коваріантність. Там, де за колишнього підходу клас Dперевизначав успадкований компонент як:


тоді як у класу C- Батька Dце виглядало


де Yвідповідало X, то тепер перевизначення компонента rбуде виглядати так:


Залишається тільки у класі Dперевизначити тип your_anchor.

Це вирішення проблеми коваріантності - поліморфізму називатимемо підходом Закріплення (Anchoring). Акуратніше варто було б говорити: "Коваріація тільки через Закріплення". Властивості підходу привабливі:

[x].Закріплення ґрунтується на ідеї строгого поділу підступнихта потенційно поліморфних (або, для стислості, поліморфних) елементів. Усі сутності, оголошені як anchorабо like some_anchorпідступні; інші-поліморфні. У кожній із двох категорій допустимі будь-які приєднання, але немає сутності чи виразу, що порушують кордон. Не можна, наприклад, присвоїти поліморфне джерело підступної мети.

[x].Це просте і елегантне рішення легко пояснити навіть початківцям.

[x].Воно повністю усуває можливість порушення системної коректності у коваріантно побудованих системах.

[x].Воно зберігає закладену вище концептуальну основу, у тому числі поняття обмеженої та необмеженої універсальності. (У результаті це рішення, на мою думку, краще типових змінних, що підміняють собою механізми коваріантності та універсальності, призначених для вирішення різних практичних завдань.)

[x].Воно вимагає незначного зміни мови, - додаючи одне ключове слово, відбите у правилі відповідності, - і пов'язані з відчутними труднощами у реалізації.

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

Здавалося б, дискусію можна закінчити. То чому ж підхід закріплення не повністю нас влаштовує? Насамперед, ми ще не стосувалися проблеми приховування нащадком. Крім цього, основною причиною продовження дискусії є проблема, що вже висловлена ​​при короткій згадці типових змінних. Розділ сфер впливу на поліморфну ​​та підступну частину, чимось схожий на результат Ялтинської конференції. Він припускає, що розробник класу має неабияку інтуїцію, що він може для кожної введеної ним сутності, зокрема для кожного аргументу раз і назавжди вибрати одну з двох можливостей:

[x].Сутність є потенційно поліморфною: зараз чи пізніше вона (за допомогою передачі параметрів або шляхом привласнення) може бути приєднана до об'єкта, тип якого відрізняється від оголошеного. Вихідний тип сутності зможе змінити жоден нащадок класу.

[x].Сутність є суб'єктом перевизначення типів, тобто або закріплена, або сама є опорним елементом.

Але як розробник може це передбачити? Вся привабливість ГО-методу багато в чому виражена в принципі Відкрито-Закрито якраз і пов'язана з можливістю змін, які ми маємо право внести в раніше зроблену роботу, а також з тим, що розробник універсальних рішень неповинен мати нескінченну мудрість, розуміючи, як його продукт зможуть адаптувати до своїх потреб нащадки.

При такому підході перевизначення типів і приховування нащадком - свого роду "запобіжний клапан", що дозволяє повторно використовувати існуючий клас, майже придатний для досягнення наших цілей:

[x].Вдавшись до перевизначення типів, ми можемо змінювати оголошення в породженому класі, не торкаючись оригіналу. При цьому чисто підступне рішення вимагатиме виправлення оригіналу шляхом описаних перетворень.

[x].Приховування нащадком захист від багатьох невдач під час створення класу. Можна критикувати проект, у якому RECTANGLE, використовуючи той факт, що вінє нащадком POLYGONнамагається додати вершину. Натомість можна було б запропонувати структуру успадкування, в якій фігури з фіксованою кількістю вершин відокремлені від інших, і проблеми не виникало б. Однак при розробці структур успадкування переважно завжди ті, в яких немає таксономічних винятків. Але чи можна їх повністю усунути? Обговорюючи обмеження експорту в одній із наступних лекцій, ми побачимо, що подібне неможливе з двох причин. По-перше, це наявність конкуруючих критеріїв класифікації. По-друге, ймовірність того, що розробник не знайде ідеального рішення, навіть якщо воно існує.

Глобальний аналіз

Цей розділ присвячений опису проміжного підходу. Основні практичні рішення викладені у лекції 17 .

Вивчаючи варіант із закріпленням, ми помітили, що його основною ідеєю було поділ коваріантного та поліморфного наборів сутностей. Так, якщо взяти дві інструкції виду


кожна їх служить прикладом правильного застосування важливих ГО-механізмів: перша - поліморфізму, друга - перевизначення типів. Проблеми починаються при об'єднанні їх для однієї і тієї ж сутності s. Аналогічно:


проблеми починаються з об'єднання двох незалежних та абсолютно безневинних операторів.

Помилкові виклики призводять до порушення типів. У першому прикладі поліморфне привласнення приєднує об'єкт BOYдо суті s, що робить gнеприпустимим аргументом share, оскільки вона пов'язана з об'єктом GIRL. У другому прикладі до сутності rприєднується об'єкт RECTANGLE, що виключає add_vertexу складі експортованих компонентів.

Ось і ідея нового рішення: заздалегідь – статично, під час перевірки типів компілятором чи іншими інструментальними засобами – визначимо набір типів (typeset)кожної сутності, що включає типи об'єктів, з якими сутність може бути пов'язана під час виконання. Потім, знову ж таки статично, ми переконаємося в тому, що кожен виклик є правильним для кожного елемента з наборів типів цілей та аргументів.

У наших прикладах оператор s:= bвказує на те, що клас BOYналежить набору типів для s(оскільки в результаті виконання інструкції створення create bвін належить до набору типів для b). GIRL, через наявність інструкції create g, належить набору типів для g. Але тоді виклик shareбуде неприпустимим для мети sтипу BOYта аргументу gтипу GIRL. Аналогічно RECTANGLEзнаходиться в наборі типів для p, що обумовлено поліморфним привласненням, однак, виклик add_vertexдля pтипу RECTANGLEвиявиться неприпустимим.

Ці спостереження наводять нас на думку про створення глобальногопідходу на основі нового правила типізації:

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

Виклик x.f (arg)є системно-коректним, якщо і тільки якщо він класово-коректний для x, і arg, що мають будь-які типи з відповідних наборів типів.

У цьому визначенні виклик вважається класово-коректним, якщо він не порушує правила виклику Компонентів, яке свідчить: якщо Cє базовий клас типу x, компонент fповинен експортуватися C, а тип argмає бути сумісним з типом формального параметра f. (Згадайте: для простоти ми вважаємо, що кожен підпрограма має тільки один параметр, однак, не важко розширити дію правила на довільне число аргументів.)

Системна коректність виклику зводиться до класової коректності у тому винятком, що вона перевіряється задля окремих елементів, а будь-яких пар з наборів множин. Ось основні правила створення набору типів для кожної сутності:

1 Для кожної сутності початковий набір типів порожній.

2 Зустрівши чергову інструкцію виду create (SOME_TYPE) a, додамо SOME_TYPEу набір типів для a. (Для простоти будемо вважати, що будь-яка інструкція create aбуде замінено інструкцією create (ATYPE) a, де ATYPE- Тип сутності a.)

3 Зустрівши чергове присвоєння виду a:= b, додамо в набір типів для a b.

4 Якщо aє формальний параметр підпрограми, то зустрівши черговий виклик з фактичним параметром b, додамо в набір типів для aвсі елементи набору типів для b.

5 Повторюватимемо кроки (3) і (4), поки набори типів не перестануть змінюватися.

Це формулювання не враховує механізму універсальності, проте розширити правило потрібним чином можна без особливих проблем. Крок (5) необхідний через можливість ланцюжків привласнення та передач (від bдо a, від cдо bі т.д.). Неважко зрозуміти, що через кілька кроків цей процес припиниться.

Як ви, можливо, помітили, правило не враховує послідовність інструкцій. В разі


create (TYPE1) t; s:= t; create (TYPE2) t

у набір типів для sувійде як TYPE1, так і TYPE2, хоча s, враховуючи послідовність інструкцій, здатний набувати значення лише першого типу. Облік розташування інструкцій вимагатиме від компілятора глибокого аналізу потоку команд, що призведе до надмірного підвищення рівня складності алгоритму. Натомість застосовуються більш песимістичні правила: послідовність операцій:


буде оголошено системно-некоректною, незважаючи на те, що послідовність їх виконання не призводить до порушення типу.

Глобальний аналіз системи був (детальніше) представлений у 22-му розділі монографії. При цьому було вирішено як проблему коваріантності, так і проблему обмежень експорту при успадкування. Однак у цьому підході є прикрий практичний недолік, а саме: передбачається перевірка системи в цілому, а чи не кожного класу окремо. Вбивчим виявляється правило (4), яке при виклику бібліотечної підпрограми враховуватиме всі її можливі виклики в інших класах.

Хоча потім було запропоновано алгоритми роботи з окремими класами в , їх практичну цінність встановити не вдалося. Це означало, що серед програмування, підтримує зростаючу компіляцію, необхідно буде організувати перевірку всієї системи. Бажано перевірку вводити як елемент (швидкої) локальної обробки змін, внесених користувачем деякі класи. Хоча приклади застосування глобального підходу відомі, - так, програмісти мовою C використовують інструмент lintдля пошуку невідповідностей у системі, що не виявляються компілятором, - все це виглядає не надто привабливо.

У результаті, як мені відомо, перевірка системної коректності залишилася ніким не реалізованою. (Іншою причиною такого результату, можливо, стала складність самих правил перевірки.)

Класова коректність передбачає перевірку, обмежену класом, і, отже, можлива при зростаючій компіляції. Системна коректність передбачає глобальну перевірку всієї системи, що входить у суперечність із зростаючою компіляцією.

Проте, попри своє ім'я, можна перевірити системну коректність, використовуючи лише зростаючу перевірку класів (у процесі роботи звичайного компілятора). Це і буде фінальним внеском у вирішення проблеми.

Стережіться поліморфних кетколлів!

Правило Системної Коректності песимістично: для спрощення воно відкидає і цілком безпечні комбінації інструкцій. Як не парадоксально, але останній варіант рішення ми збудуємо на основі ще більш песимістичного правила. Звичайно, це порушить питання про те, наскільки реалістичним буде наш результат.

Назад до Ялти

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

Як і раніше, зсуваємо питання про підступність до двох операцій. У нашому головному прикладі це поліморфне присвоєння: s:= b, та виклик коваріантної підпрограми: s.share (g). Аналізуючи, хто є істинним винуватцем порушень, виключимо аргумент gу складі підозрюваних. Будь-який аргумент, що має тип SKIERабо породжений від нього, нам не підходить через поліморфізм sта підступності share. А тому якщо статично описати суть otherяк SKIERта динамічно приєднати до об'єкту SKIER, то виклик s.share (other)статично створить враження ідеального варіанту, але призведе до порушення типів, якщо поліморфно привласнити sзначення b.

Фундаментальна проблема в тому, що ми намагаємось використати sдвома несумісними способами: як поліморфну ​​сутність та як мета виклику коваріантної підпрограми. (В іншому нашому прикладі проблема полягає у використанні pяк поліморфної сутності та як мети виклику підпрограми нащадка, що приховує компонент add_vertex.)

Рішення Кетколл, як і Закріплення, має радикальний характер: воно забороняє використовувати сутність як поліморфну ​​та підступну одночасно. Подібно до глобального аналізу, воно статично визначає, які сутності можуть бути поліморфними, проте, не намагається бути надто розумним, відшукуючи для сутностей набори можливих типів. Натомість будь-яка поліморфна сутність сприймається як досить підозріла, і їй забороняється вступати в союз з колом поважних осіб, що включають підступність і приховування нащадком.

Одне правило та кілька визначень

Правило типів для вирішення Кетколл має просте формулювання:

Правило типів для Кетколл

Поліморфні кетколи некоректні.

У його основі - такі ж прості визначення. Насамперед, поліморфна сутність:

Визначення: поліморфна сутність

Сутність xпосилального (не розгорнутого) типу поліморфна, якщо вона має одну з наступних властивостей:

1 Зустрічається у привласненні x:= yде сутність yмає інший тип або за рекурсією поліморфною.

2 Зустрічається в інструкціях створення create (OTHER_TYPE) x, де OTHER_TYPEне є типом, зазначеним у оголошенні x.

3 Є формальним аргументом підпрограми.

4 Є зовнішньою функцією.

Мета цього визначення - надати статусу поліморфної ("потенційно поліморфної") будь-якої сутності, яку при виконанні програми можна приєднати до об'єктів різних типів. Це визначення застосовується лише до посилальних типів, оскільки розгорнуті сутності за природою неможливо знайти поліморфними.

У наших прикладах лижник sта багатокутник p- Поліморфні за правилом (1). Першому з них надається об'єкт BOY b, другому - об'єкт RECTANGLE r.

Якщо ви познайомилися з формулюванням поняття набору типів, то помітили, наскільки більш песимістично виглядає визначення поліморфної сутності, і наскільки простіше його перевірити. Не намагаючись відшукати всілякі динамічні типи сутності, ми задовольняємося загальним питанням: чи може ця сутність бути поліморфною чи ні? Найбільш дивним виглядає правило (3), за яким поліморфнимвважається кожен формальний параметр(якщо його тип не розширений, як у випадку з цілими тощо). Ми навіть не турбуємо себе аналізом викликів. Якщо підпрограми є аргумент, він знаходиться у повному розпорядженні клієнта, отже, і покладатися на зазначений в оголошенні тип не можна. Це тісно пов'язане з повторним використанням - метою об'єктної технології, - де будь-який клас потенційно може бути включений до складу бібліотеки, і буде багаторазово викликатися різними клієнтами.

Характерною властивістю цього правила є те, що воно не потребує жодних глобальних перевірок. Для виявлення поліморфності сутності достатньо переглянути текст класу. Якщо для всіх запитів (атрибутів або функцій) зберігати інформацію про статус поліморфності, то не доводиться вивчати навіть тексти предків. На відміну від пошуку наборів типів, можна виявити поліморфні сутності, перевіряючи клас за класом у процесі зростаючої компіляції.

Виклики, як і сутності, можуть бути поліморфними:

Визначення: поліморфний виклик

Виклик є поліморфним, якщо його мета є поліморфною.

Обидва виклики у наших прикладах поліморфні: s.share (g)через поліморфізм s, p.add_ vertex (...)через поліморфізм p. Згідно з визначенням, лише кваліфіковані виклики можуть бути поліморфними. (Надавши некваліфікованому виклику f (...)вид кваліфікованого Current.f (...), ми не змінюємо суть справи, оскільки Current, присвоїти якому нічого не можна, не є поліморфним об'єктом.)

Далі нам потрібно поняття Кетколла, засноване на понятті CAT. (CAT – це абревіатура Changing Availability or Type – зміна доступності або типу). Підпрограма є CAT підпрограмою, якщо деяке її перевизначення нащадком призводить до змін одного з двох видів, які, як ми бачили, є потенційно небезпечними: змінює тип аргументу (коваріантно) або приховує компонент, що раніше експортувався.

Визначення: CAT-підпрограми

Підпрограма називається підпрограмою CAT, якщо деяке її перевизначення змінює статус експорту або тип будь-якого з її аргументів.

Ця властивість знов-таки допускає зростаючу перевірку: будь-яке перевизначення типу аргументу чи статусу експорту роблять процедуру чи функцію CAT-підпрограмою. Звідси випливає поняття Кетколла: виклику CAT-підпрограми, який може бути помилковим.

Визначення: Кетколл

Виклик називається Кетколлом, якщо деяке перевизначення підпрограми зробило його помилковим через зміни статусу експорту чи типу аргументу.

Створена нами класифікація дозволяє виділяти спеціальні групи викликів: поліморфні та кетколли. Поліморфні виклики надають виразну міць об'єктному підходу, кетколли дозволяють перевизначати типи та обмежувати експорт. Використовуючи термінологію, запроваджену раніше у цій лекції, можна сказати, що поліморфні виклики розширюють корисність (usefulness), кетколли - використовуваність(usability).

Виклики shareі add_vertex, розглянуті в прикладах, є кет-колами. Перший здійснює підступне перевизначення свого аргументу. Другий експортується класом RECTANGLE, але прихований класом POLYGON. Обидва виклики також і поліморфні, а тому вони є чудовим прикладом поліморфних кетколлів. Вони є помилковими згідно з правилами типів Кетколл.

Оцінка

Перш ніж ми зведемо докупи все, що дізналися про підступність і приховування нащадком, згадаємо ще раз про те, що порушення коректності систем виникають дійсно рідко. Найбільш важливі властивості статичної ГО-типізації були узагальнені на початку лекції. Цей вражаючий ряд механізмів роботи з типами спільно з перевіркою класової коректності відкриває дорогу до безпечного та гнучкого методу конструювання ПЗ.

Ми бачили три вирішення проблеми коваріантності, два з яких торкнулися і питання обмеження експорту. Яке ж із них правильне?

На це питання немає остаточної відповіді. Наслідки підступної взаємодії ГО-типізації та поліморфізму вивчені не так добре, як питання, викладені у попередніх лекціях. В останні роки з'явилися численні публікації на цю тему, посилання на які наведені в бібліографії наприкінці лекції. Крім того, я сподіваюся, що в цій лекції мені вдалося представити елементи остаточного рішення або хоча б наблизитися до нього.

Глобальний аналіз здається непрактичним через повну перевірку всієї системи. Проте він допоміг нам краще зрозуміти проблему.

Рішення на основі закріплення надзвичайно привабливе. Воно просте, інтуїтивно зрозуміле, зручне у реалізації. Тим більше ми маємо шкодувати про неможливість підтримки в ньому низки ключових вимог ГО-методу, відображених у принципі «Відкрито-Закрито». Якби ми й справді мали чудову інтуїцію, то закріплення стало б чудовим рішенням, але який розробник наважиться стверджувати це, або, тим більше, визнати, що таку інтуїцію мали автори бібліотечних класів, що успадковуються в його проекті?

Якщо від закріплення ми змушені відмовитися, то найбільш підходящим здається Кетколл-рішення, що досить легко пояснити і застосовується на практиці. Його песимізм не повинен унеможливлювати корисні комбінації операторів. У випадку, коли поліморфний кетколл породжений "легітимним" оператором, можна безпечно допустити його, введенням спроби привласнення. Тим самим, ряд перевірок можна перенести на час виконання програми. Проте кількість таких випадків має бути гранично малою.

Як пояснення я повинен зауважити, що на момент написання книги рішення Кетколл не було реалізовано. Доки компілятор не буде адаптований до перевірки правила типів Кетколл і не буде успішно застосований до репрезентативних систем - великих і малих, - рано говорити, що в проблемі примирення статичної типізації з поліморфізмом, що поєднується з коваріантністю і прихованням нащадком, сказано останнє слово .

Повна відповідність

Завершуючи обговорення коваріантності, корисно зрозуміти, як загальний метод можна застосувати до вирішення загальної проблеми. Метод виник як результат Кетколл-теорії, але може використовуватися в рамках базового варіанта мови без введення нових правил.

Нехай є два узгоджені списки, де перший задає лижників, а другий - сусіда по кімнаті для лижника з першого списку. Ми хочемо виконувати відповідну процедуру розміщення share, тільки якщо вона дозволена правилами опису типів, які дозволяють поселяти дівчат із дівчатами, дівчат-призерів із дівчатами-призерами тощо. Проблеми такого виду трапляються часто.

Можливе просте рішення, засноване на попередньому обговоренні та спробі привласнення. Розглянемо універсальну функцію fitted(погодити):


fitted (other: GENERAL): like other is
-- Поточний об'єкт (Current), якщо його тип відповідає типу об'єкта,
-- приєднаного до іншого, інакше void.
if other /= Void and then conforms_to (other) then

Функція fittedповертає поточний об'єкт, але відомий як суть типу, приєднаного до аргументу. Якщо тип поточного об'єкта відповідає типу об'єкта, приєднаного до аргументу, то повертається Void. Зверніть увагу на роль спроби привласнення. Функція використовує компонент conforms_toз класу GENERAL, що з'ясовує сумісність типів пари об'єктів.

Заміна conforms_toна інший компонент GENERALз ім'ям same_typeдає нам функцію perfect_fitted (повна відповідність), яка повертає Voidякщо типи обох об'єктів не ідентичні.

Функція fitted- дає нам просте вирішення проблеми відповідності лижників без порушення правил опису типів. Так, у код класу SKIERми можемо ввести нову процедуру та використовувати її замість share, (Останню можна зробити прихованою процедурою).


- Вибрати, якщо припустимо, інші як сусіди за номером.
-- gender_ascertained - встановлена ​​підлога
gender_ascertained_other: like Current
gender_ascertained_other:= other .fitted (Current)
if gender_ascertained_other /= Void then
share (gender_ascertained_other)
"Висновок: спільне розміщення з іншим неможливо"

Для otherдовільного типу SKIER(а не тільки like Current) визначимо версію gender_ascertained_other, що має тип, закріплений за Current. Гарантувати ідентичність типів нам допоможе функція perfect_ fitted.

За наявності двох паралельних списків лижників, що представляють заплановане розміщення:


occupant1, occupant2: LIST

можна організувати цикл, виконуючи на кожному кроці виклик:


occupant1.item.safe_share (occupant2.item)

зіставляє елементи списків, якщо тільки якщо їх типи повністю сумісні.

Ключові концепції

[x].Статична типізація - запорука надійності, читабельності та ефективності.

[x].Щоб бути реалістичною, статичної типізації потрібне спільне застосування механізмів: тверджень, множинного успадкування, спроби привласнення, обмеженої та необмеженої універсальності, закріплених оголошень. Система типів повинна допускати пасток (наведень типу).

[x].Практичні правила повторного оголошення повинні допускати підступне перевизначення. Типи результатів та аргументів при перевизначенні мають бути сумісними з вихідними.

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

[x].Цих порушень можна уникнути, використовуючи: глобальний аналіз (що непрактично), обмежуючи коваріантність закріпленими типами (що суперечить принципу "Відкритий-Закритий"), рішення Кетколл, що перешкоджає виклику поліморфною метою підпрограми з підступністю або прихованням нащадком.

Бібліографічні зауваження

Ряд матеріалів цієї лекції представлений у доповідях на форумах OOPSLA 95 та TOOLS PACIFIC 95, а також опубліковано у . Ряд оглядових матеріалів запозичений із статті.

Поняття автоматичного виведення типів введено у , де описаний алгоритм виведення типів функціональної мови ML. Зв'язок між поліморфізмом та перевіркою типів був досліджений у роботі.

Прийоми підвищення ефективності коду динамічно типізованих мов у контексті мови Self можна знайти у .

Теоретичну статтю, присвячену типам у мовах програмування і що справила великий вплив на фахівців, написали Лука Карделлі (Luca Cardelli) та Петер Вегнер (Peter Wegner). Ця робота, побудована з урахуванням лямбда-числения (див. ), послужила основою багатьох подальших пошуків. Їй передувала інша фундаментальна стаття Карделлі.

Посібник з ISE включає введення у проблеми спільного застосування поліморфізму, підступності та приховування нащадком. Відсутність належного аналізу в першому виданні цієї книги спричинила ряд критичних дискусій (першими з яких стали коментарі Філіпа Елінка (Philippe Elinck) у бакалаврській роботі "De la Conception-Programmation par Objets", Memoire de licenci, Universite Libre de Bruxelles (Belgium), 1988), висловлених у роботах та . У статті Кука наведено кілька прикладів, пов'язаних із проблемою коваріантності, та зроблено спробу її вирішення. Рішення на основі типових параметрів для підступних сутностей на TOOLS EUROPE 1992 запропонував Франц Вебер. Точні визначення понять системної коректності, і навіть класової коректності, дані в , там запропоновано рішення із застосуванням повного аналізу системи. Рішення Кетколл вперше запропоновано в ; Див. також .

Рішення Закріплення було представлено у моїй доповіді на семінарі TOOLS EUROPE 1994. Тоді я, однак, не побачив потреби в anchor-Оголошення та пов'язані з цим обмеження сумісності. Поль Дюбуа (Paul Dubois) та Амірам Йехудай (Amiram Yehudai) не забули зауважити, що в цих умовах проблема коваріантності залишається. Вони, а також Рейнхардт Будде (Reinhardt Budde), Карл-Хайнц Зілла (Karl-Heinz Sylla), Кім Вальден (Kim Walden) і Джеймс Мак-Кім (James McKim) висловили безліч зауважень, що мали принципове значення у роботі, яка привела до написання цієї лекції.

Питанням підступності присвячено велику кількість літератури. Ви знайдете як велику бібліографію, так і огляд математичних аспектів проблеми. Перелік посилань на онлайнові матеріали з теорії типів в ООП та Web-сторінки їх авторів див. на сторінці Лорана Дамі (Laurent Dami). Поняття коваріантності та контраваріантності запозичені з теорії категорій. Їхньою появою в контексті програмної типізації ми зобов'язані Луке Карделлі, який почав використовувати їх у своїх виступах з початку 80-х рр., але до кінця 80-х не вдавався до них у пресі.

Прийоми на основі типових змінних описані в , , .

Контраваріантність була реалізована у мові Sather. Пояснення наведено в .

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

    Протилежний прийом – статична типізація.

    У деяких мовах зі слабкою динамічною типізацією стоїть проблема порівняння величин, так, наприклад, PHP має операції порівняння «==», «!=» і «===», «!==», де друга пара операцій порівнює значення, та типи змінних. Операція "===" дає true тільки при повному збігу, на відміну від "==", який вважає вірним такий вираз: (1=="1"). Слід зазначити, що це проблема динамічної типізації загалом, а конкретних мов програмування.

Пов'язані поняття

Мова програмування - формальна мова, призначена для запису комп'ютерних програм. Мова програмування визначає набір лексичних, синтаксичних і семантичних правил, визначальних зовнішній вигляд програми та події, які виконає виконавець (зазвичай - ЕОМ) під керівництвом.

Синтаксичний цукор (англ. syntactic sugar) у мові програмування - це синтаксичні можливості, застосування яких впливає поведінка програми, але робить використання мови зручнішим для людини.

Властивість - спосіб доступу до внутрішнього стану об'єкта, що імітує змінну певного типу. Звернення до якості об'єкта виглядає так само, як і звернення до структурного поля (у структурному програмуванні), але насправді реалізовано через виклик функції. При спробі встановити значення даної властивості викликається один метод, а при спробі отримати значення даної властивості - інший.

Розширена форма Бекуса - Наура (розширена Бекус - Наурова форма (РБНФ)) - формальна система визначення синтаксису, в якій одні синтаксичні категорії послідовно визначаються через інші. Використовується для опису контекстно-вільних формальних граматик. Запропонована Ніклаусом Віртом. Є розширеною переробкою форм Бекуса - Наура, відрізняється від БНФ більш «ємними» конструкціями, що дозволяють при тій самій виразній здатності спростити...

Аплікативне програмування - один із видів декларативного програмування, в якому написання програми полягає у систематичному здійсненні застосування одного об'єкта до іншого. Результатом такого застосування знову є об'єкт, який може брати участь у застосування як у ролі функції, так і в ролі аргументу і так далі. Це робить запис програми математично ясною. Той факт, що функція позначається виразом, свідчить про можливість використання значень-функцій - функціональних...

Конкатенативна мова програмування - це мова програмування, заснована на тому, що конкатенація двох фрагментів коду виражає їхню композицію. У мові широко використовується неявне вказівку аргументів функцій (див. безточкове програмування), нові функції визначаються як композиція функцій, а замість аплікації застосовується конкатенація. Цьому підходу протиставляється аплікативне програмування.

Змінна - атрибут фізичної або абстрактної системи, який може змінювати своє, як правило, чисельне значення. Поняття змінної широко використовується у таких галузях як математика, природничі науки, техніка та програмування. Прикладами змінних можуть бути: температура повітря, параметр функції та багато іншого.

Синтаксичний аналіз (або розбір, жарг. парсинг ← англ. parsing) у лінгвістиці та інформатиці - процес зіставлення лінійної послідовності лексем (слів, токенів) природної або формальної мови з її формальною граматикою. Результатом зазвичай є дерево розбору (синтаксичне дерево). Зазвичай застосовується разом із лексичним аналізом.

Узагальнений тип алгебри даних (англ. generalized algebraic data type, GADT) - один з видів алгебраїчних типів даних, який характеризується тим, що його конструктори можуть повертати значення не свого типу, пов'язаного з ним. Сконструйовані під впливом робіт про індуктивні сімейства серед дослідників залежних типів.

Семантика у програмуванні - дисципліна, що вивчає формалізації значень конструкцій мов програмування за допомогою побудови їх формальних математичних моделей. Як інструменти побудови таких моделей можуть використовуватися різні засоби, наприклад, математична логіка, λ-обчислення, теорія множин, теорія категорій, теорія моделей, універсальна алгебра. Формалізація семантики мови програмування може використовуватися як для опису мови, визначення властивостей мови.

Об'єктно-орієнтоване програмування (ООП) - методологія програмування, заснована на представленні програми у вигляді сукупності об'єктів, кожен з яких є екземпляром певного класу, а класи утворюють ієрархію спадкування.

Динамічна змінна - змінна в програмі, місце в оперативній пам'яті, під яку виділяється під час виконання програми. По суті вона є ділянкою пам'яті, виділеною системою програмі для конкретних цілей під час роботи програми. Цим вона відрізняється від глобальної статичної змінної – ділянки пам'яті, виділеної системою програмі для конкретних цілей перед початком роботи програми. Динамічна змінна – один із класів пам'яті змінної.

Щоб максимально просто пояснити дві абсолютно різні технології, почнемо спочатку. Перше, із чим стикається програміст при написанні коду – оголошення змінних. Можна помітити, що, наприклад, у мові програмування C++ необхідно вказувати тип змінної. Тобто якщо ви оголошуєте змінну x, то обов'язково потрібно додати int – для зберігання цілих даних, float – для зберігання даних з плаваючою точкою, char – для символьних даних, та інші доступні типи. Отже, C++ використовується статична типізація, так само як і в його попереднику C.

Як працює статична типізація?

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

Розглянемо маленький приклад. При ініціалізації змінної x (int x;) ми вказуємо ідентифікатор int - це скорочення від якого зберігає тільки цілі числа в діапазоні від - 2147483648 до 2147483647. Таким чином, компілятор розуміє, що може виконувати над цією змінною математичні значення - суму, різницю, множення та розподіл. А ось, наприклад, функцію strcat(), яка поєднує два значення типу char, застосувати до x не можна. Адже якщо зняти обмеження та спробувати поєднати два значення int символьним методом, тоді станеться помилка.

Навіщо знадобились мови із динамічною типізацією?

Незважаючи на деякі обмеження, статична типізація має ряд переваг і не вносить великого дискомфорту в написання алгоритмів. Тим не менш, для різних цілей можуть знадобитися і більш «вільні правила» щодо типів даних.

Вдалий приклад, який можна навести - JavaScript. Ця мова програмування зазвичай використовують для вбудовування у фреймворк з метою отримання функціонального доступу до об'єктів. Через таку особливість він набув великої популярності у web-технологіях, де ідеально почувається динамічна типізація. У рази спрощується написання маленьких скриптів і макросів. А також з'являється перевага у повторному використанні змінних. Але таку можливість використовують досить рідко, через можливі плутанини та помилки.

Який вид типізації кращий?

Суперечки у тому, що динамічна типізація краще, ніж сувора, не припиняються і по сьогодні. Зазвичай вони виникають у вузькоспеціалізованих програмістів. Безумовно, веб-розробники повсякденно використовують усі переваги динамічної типізації для створення якісного коду та підсумкового програмного продукту. У той же час системні програмісти, які розробляють найскладніші алгоритми низькорівневими мовами програмування, зазвичай не потребують таких можливостей, тому їм цілком вистачає статичної типізації. Бувають, звичайно, винятки із правил. Наприклад, повністю реалізована динамічна типізація в 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 = new String("hello world");

Тим не менш, Ruby, Python і JavaScript (всі вони мають динамічну типізацію) також є сильно типізованими, хоча розробнику і не потрібно вказувати тип змінної при оголошенні. Розглянемо такий самий приклад, але написаний на Ruby:

Foo = "hello world"

Обидві мови є дуже типизованими, але використовують різні методи перевірки типів. Такі мови, як Ruby, Python і JavaScript не вимагають явного визначення типів через виведення типів - здатність програмно виводити потрібний тип змінної залежно від її значення. Висновок типів - це окреме властивість мови, і належить до системам типів.

Слабо типізована мова - це мова, у якій змінні не прив'язані до конкретного типу даних; у них все ще є тип, але обмеження типобезпеки набагато слабші. Розглянемо наступний приклад коду на PHP:

$ foo = "x"; $ foo = $ foo + 2; // not an error echo $foo; // 2

Оскільки PHP має слабку типізацію, помилки в цьому коді немає. Аналогічно попередньому припущенню, в повному обсязі слабко типізовані мови є динамічно типизированными: PHP - це динамічно типізований мову, але ось C - теж мова зі слабкою типізацією - воістину статично типизирован.

Міф зруйновано.

Хоча статична/динамічна і сильна/слабка системи типів і є різними, вони обидві пов'язані з типобезпекою. Найпростіше це висловити так: перша система говорить про те, коли перевіряється типобезпека, а друга – як.

Міф 2: статична / динамічна типізація == компілювані / інтерпретовані мови

Буде вірним сказати, більшість статично типизированных мов зазвичай компилируются, а динамічно типизированных - інтерпретуються, але узагальнити це твердження не можна, тому є простий приклад.

Коли ми говоримо про типізацію мови, ми говоримо про мову як ціле. Наприклад, неважливо, яку версію Java ви використовуєте - вона завжди буде статично типизована. Це відрізняється від того випадку, коли мова є компілюваною або інтерпретованою, оскільки в цьому випадку ми говоримо про конкретну реалізацію мови. Теоретично, будь-яка мова може бути як компилируемым, і інтерпретованим. Найпопулярніша реалізація мови Java використовує компіляцію в байткод, який інтерпретує JVM - але є інші реалізації цієї мови, які компілюються безпосередньо в машинний код або інтерпретуються як є.

Якщо це все ще незрозуміло, раджу прочитати цей цикл.

Висновок

Я знаю, що в цій статті було багато інформації – але я вірю, що ви впоралися. Я хотів би винести інформацію про сильну / слабку типізацію в окрему статтю, але це не така важлива тема; до того ж, треба було показати, що цей вид типізації не має відношення до перевірки типів.

Немає однозначної відповіді на питання "яка типізація краща?" - у кожної є свої переваги та недоліки. Деякі мови – такі як Perl та C# – навіть дозволяють вам самостійно вибирати між статичною та динамічною системами перевірки типів. Розуміння цих систем дозволить вам краще зрозуміти природу помилок, що виникають, а також спростить боротьбу з ними.



Завантаження...
Top