Яка різниця між підкласом і підтипом?


44

Відповідь на це запитання щодо принципу заміни Ліскова має найвищі оцінки для розмежування термінів підтип та підклас . Це також вказує на те, що деякі мови поєднують ці дві, тоді як інші - ні.

Для об'єктно-орієнтованих мов, які мені найбільше знайомі (Python, C ++), "type" і "class" є синонімічними поняттями. Що стосується C ++, що би означало розмежування між підтипом та підкласом? Скажімо, наприклад, що Fooце підклас, але не підтип FooBase. Якщо fooце примірник Foo, чи буде цей рядок:

FooBase* fbPoint = &foo;

більше не діє?


6
Власне, в Python "тип" і "клас" є різними поняттями. Насправді, Python бути динамічно типізованих, «типу» це не поняття взагалі в Python. На жаль, розробники Python цього не розуміють, і все ще поєднують це.
Йорг W Міттаг

11
"Тип" і "клас" також відрізняються в C ++. "Масив ints" - це тип; який клас це? "вказівник на змінну типу int" - це тип; який клас це? Ці речі не є будь-яким класом, але вони, безумовно, типи.
Ерік Ліпперт

2
Мені це було цікаво, прочитавши це питання та відповідь.
user369450

4
@JorgWMittag Якщо немає поняття «тип» в пітона , то хто - то повинен сказати , хто пише документацію: docs.python.org/3/library/stdtypes.html
Matt

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

Відповіді:


53

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

Якщо Sє підтипом T, часто пишеться співвідношення підтипів S <: T, що означає, що будь-який тип типу Sможе бути безпечно використаний у контексті, де Tочікується термін типу . Точна семантика субтипування вирішально залежить від конкретних даних про те, що "безпечно використовується в контексті, де" означає дану мову програмування.

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

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

Посилання
подтіпірованіі
Спадкування


1
Дуже добре сказано. У контексті питання, можливо, варто згадати, що програмісти на C ++ часто використовують чисті віртуальні базові класи для передачі відносин підтипу до системи типів. Звичайно, перевагу надають загальні підходи до програмування.
Алуан Хаддад

6
"Точна семантика субтипування вирішально залежить від деталей того, що" безпечно використовується в контексті, де "означає дану мову програмування". … І LSP визначає дещо розумне уявлення про те, що означає «безпечно», і розповідає, яким обмеженням повинні відповідати ці дані, щоб забезпечити цю конкретну форму «безпеки».
Йорг W Міттаг

Ще один зразок на купі: якщо я правильно зрозумів, у С ++ publicуспадкування вводить підтип, а при privateспадкуванні вводить підклас.
Квентін

Публічне успадкування @Quentin є і підтипом, і підкласом, але приватне все ж є лише підкласом, але не підтипом. Ви можете мати підтипизацію без підкласифікації з такими структурами, як інтерфейси Java
дорівнює

27

Типу , в контексті того, що ми говоримо про тут, по суті, набір поведінкових гарантій. Контракт , якщо ви будете. Або, запозичивши термінологію у Smalltalk, протокол .

Клас являє собою комплект з методів. Це набір реалізацій поведінки .

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

Якщо ви використовували Java або C♯, то, можливо, ви натрапили на поради, що всі типи мають бути interfaceтипами. Насправді, якщо ви читаєте « Урозуміння абстракції даних», переглянутого Вільямом Куком , то, можливо, ви знаєте, що для того, щоб робити ОО на цих мовах, ви повинні використовувати лише interfaces як типи. (Також цікавий факт: Java cribbed interfaces безпосередньо з протоколів Objective-C, які, в свою чергу, взяті безпосередньо з Smalltalk.)

Тепер, якщо ми дотримуємось цього поради щодо кодування до його логічного завершення і уявляємо версію Java, де тільки interface s - це типи, а класи та примітиви - ні, то один, interfaceуспадковуючи інший, створить співвідношення підтипів, тоді як один classуспадкує від іншого бути лише для диференціального повторного використання коду через super.

Наскільки я знаю, немає основних мов, що мають статичний тип, які б чітко розмежовували між спадковим кодом (успадкування реалізації / підкласифікація) та успадковуючими контрактами (субтипізація). У Java та C♯ успадкування інтерфейсу є чистим підтипом (або, принаймні, до введення методів за замовчуванням у Java 8 та, ймовірно, також C♯ 8), але успадкування класів також є підтипом, а також успадкуванням реалізації. Я пам'ятаю, як читав про експериментальний статично набраний об'єктно-орієнтований діалект LISP, який чітко розрізняв міксини (які містять поведінку), структури (які містять стан), інтерфейси (які описуютьповедінка) та класи (які складають нульову чи більше конструкцій з одним або декількома комбінаціями та відповідають одному або більше інтерфейсам). Тільки класи можуть бути ініційовані, і лише інтерфейси можуть використовуватися як типи.

У динамічно типізованій мові OO, такі як Python, Ruby, ECMAScript або Smalltalk, ми, як правило, думаємо про тип (типи) об'єкта як набір протоколів, яким він відповідає. Зверніть увагу на множину: об’єкт може мати декілька типів, і я не просто кажу про те, що кожен об’єкт типу Stringтакож є об'єктом типу Object. (BTW: зверніть увагу, як я використовував імена класів, щоб говорити про типи? Як я дурний!) Об'єкт може реалізувати декілька протоколів. Наприклад, в Ruby Arraysможна додати, їх можна проіндексувати, їх можна повторити і порівняти. Це чотири різних протоколи, які вони реалізують!

Тепер у Рубі немає типів. Але спільнота Ruby має типи! Однак вони існують лише в головах програмістів. І в документації. Наприклад, будь-який об'єкт, який відповідає на метод, який називається eachшляхом отримання його елементів один за одним, вважається об'єктом, що перелічується . І є міксин, Enumerableякий називається, що залежить від цього протоколу. Таким чином, якщо об'єкт має правильний тип (який існує тільки в голові програміста), то допускається змішувати в (успадкування від) в EnumerableMixin, і це добре отримати всі види методів прохолодних безкоштовно, як map, reduce, filterі т.д. на.

Точно так же, якщо об'єкт відповідає <=>, то він вважається реалізувати який можна порівняти протокол, і він може змішуватися в ComparableMixin і отримати такі речі , як <, <=, >, <=, ==, between?, і clampбезкоштовно. Однак він також може реалізовувати всі ці методи самостійно і зовсім не успадковувати Comparable, і це все одно вважатиметься порівнянним .

Хороший приклад - StringIOбібліотека, яка по суті підробляє потоки вводу / виводу рядками. Він реалізує всі ті ж методи, що і IOклас, але не існує спадкового зв'язку між ними. Тим не менш, StringIOможна використовувати скрізь, де IOможна використовувати. Це дуже корисно в одиничних тестах, де ви можете замінити файл або stdinз, StringIOне вносивши жодних додаткових змін у свою програму. Оскільки вони StringIOвідповідають одному і тому ж протоколу IO, вони обоє одного типу, навіть якщо вони різні класи, і не мають жодного зв'язку (крім тривіального, який вони обидва поширюють Objectу якийсь момент).


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

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

Чи є у вас посилання або кілька ключових слів, я б міг шукати додаткову інформацію про експериментальний діалект LISP, який ви згадали, що формально диференціює міксини, структури, інтерфейси та класи?
тел

@tel: Ні, вибач. Це було, напевно, близько 15-20 років тому, і в той час мої інтереси були всюди. Я не міг почати говорити тобі, що шукав, коли натрапив на це.
Йорг W Міттаг

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

2

Можливо, спочатку корисно розрізнити тип і клас, а потім зануритися в різницю між підтипом і підкласом.

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

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

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

Нашою мовою іграшок ми можемо дозволити створення таких етикеток.

declare type Int
declare type String

Тоді ми можемо позначити різні значення як такі, що є.

0 is of type Int
1 is of type Int
-1 is of type Int
...

"" is of type String
"a" is of type String
"b" is of type String
...

Завдяки цим твердженням наш інструмент перевірки типу може тепер відхиляти такі заяви, як

0 is of type String

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

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

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

class StringClass:
  defMethod concatenate(otherString): ...
  defField size: ...

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

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

associate StringClass with String

Але не кожному типу потрібно мати асоційований клас.

# Hmm... Doesn't look like there's a class for Int

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

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

associate MyCustomStringClass with String

Тепер майте на увазі, що для нашої перевірки типу даних немає вимоги відслідковувати значення виразу (а в більшості випадків це не робиться чи неможливо). Все, що він знає, - це етикетки, які ви їм сказали. Нагадуючи, раніше перевіряючий текст зміг відхилити заяву лише 0 is of type Stringчерез наше штучно створене правило типу, що вирази повинні мати унікальні типи, і ми вже позначили цей вираз 0чимось іншим. У неї не було особливих знань про значення 0.

То як щодо підтипу? Добре підтипізація - це назва загального правила перевірки набору тексту, яке розслаблює інші правила, які ви можете мати. А саме, якщо A is subtype of Bтоді скрізь ваш машинник вимагає етикетки B, він також приймає A.

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

declare type NaturalNum
declare type Int
NaturalNum is subtype of Int

0 is of type NaturalNum
1 is of type NaturalNum
-1 is of type Int
...

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

class ExtendedStringClass is subclass of StringClass:
  # We get concatenate and size for free!
  def addQuestionMark: ...

Нам не доводиться пов'язувати екземпляри з ExtendedStringClassтим, Stringяк ми робили, StringClassоскільки, нарешті, це зовсім новий клас, нам просто не довелося писати стільки. Це дозволило б нам надати ExtendedStringClassтип, несумісний з Stringточкою зору перевірки типу.

Так само ми могли вирішити зробити цілком новий клас NewClassі зробили це

associate NewClass with String

Тепер кожен примірник StringClassможна замінити з NewClassточки зору перевірки шрифтів.

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

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

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

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

Якщо ви поєднаєте це з умовою, що всі визначені користувачем значення повинні бути екземплярами класу, то ви можете is subclass ofвитягнути подвійний обов'язок і позбутися is subtype of.

І це приводить нас до характеристик, якими володіє більшість популярних статично типових мов ОО. Є набір "примітивних" типів (наприклад int, floatтощо), які не пов'язані з жодним класом і не визначені користувачем. Тоді у вас є всі визначені користувачем класи, які автоматично мають типи однойменних ідентифікують підкласи з підтипом.

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.