В чем разница между подклассом и подтипом?

44

Самый высокий рейтинг ответа на этот вопрос о принципе замещения Лискова изо всех сил старается различить термины подтип и подкласс . Это также указывает на то, что некоторые языки объединяют два, а другие нет.

Для объектно-ориентированных языков, с которыми я больше всего знаком (Python, C ++), «тип» и «класс» являются синонимами. С точки зрения C ++, что бы значило иметь различие между подтипом и подклассом? Скажем, например, что Fooэто подкласс, но не подтип FooBase. Если fooэто экземпляр Foo, будет ли эта строка:

FooBase* fbPoint = &foo;

больше не будет действительным?

телефон
источник
6
На самом деле, в Python «тип» и «класс» - это разные понятия. На самом деле, Python быть динамически типизированных, «типа» это не понятие вообще в Python. К сожалению, разработчики Python не понимают этого, и все же объединяют их.
Йорг Миттаг
11
«Тип» и «класс» также различны в C ++. «Массив целых» - это тип; какой класс это? «указатель на переменную типа int» является типом; какой класс это? Эти вещи не какого-то класса, но они, безусловно, типы.
Эрик Липперт
2
Мне было интересно именно это после прочтения этого вопроса и этого ответа.
user369450
4
@JorgWMittag Если в python нет понятия «тип», то кто-то должен сказать, кто пишет документацию: docs.python.org/3/library/stdtypes.html
Мэтт
@ Справедливости ради, типы сделали это в версии 3.5, что довольно недавно, особенно в соответствии со стандартами «что мне разрешено использовать в производстве».
Джаред Смит

Ответы:

53

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

Если Sэто подтип T, отношение подтипа часто пишется S <: T, чтобы означать, что любой термин типа Sможет безопасно использоваться в контексте, где Tожидается термин типа . Точная семантика подтипирования в решающей степени зависит от того, что «безопасно используется в контексте, где» означает в данном языке программирования.

Подклассы не следует путать с подтипами. В общем, подтип устанавливает отношения is-a, тогда как подклассы только повторно используют реализацию и устанавливают синтаксические отношения, а не обязательно семантические отношения (наследование не обеспечивает поведенческий подтип).

Чтобы различать эти понятия, подтипы также известны как наследование интерфейса , тогда как подклассы известны как наследование реализации или наследование кода.

Ссылки
Подтип
Наследование

Роберт Харви
источник
1
Очень хорошо сказано. Возможно, стоит упомянуть в контексте вопроса, что программисты на C ++ часто используют чисто виртуальные базовые классы для передачи отношений подтипов в систему типов. Конечно, подходы к общему программированию часто предпочтительнее.
Алуан Хаддад
6
«Точная семантика подтипирования в решающей степени зависит от того, что« безопасно используется в контексте, где »означает в данном языке программирования». … И LSP определяет довольно разумное представление о том, что означает «безопасно», и говорит нам, каким ограничениям должны удовлетворять эти данные, чтобы обеспечить эту конкретную форму «безопасности».
Йорг Миттаг
Еще один пример: если я правильно понял, в C ++ publicнаследование вводит подтип, а privateнаследование - подкласс.
Квентин
Публичное наследование @Quentin является одновременно подтипом и подклассом, но частное - это только подкласс, но не подтип. Вы можете иметь подтипы без
eques
27

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

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

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

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

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

Насколько я знаю, не существует основных статически типизированных языков, которые бы строго различали наследование кода (наследование реализации / создание подклассов) и наследование контрактов (подтипирование). В Java и C♯ наследование интерфейса - это чистый подтип (или, по крайней мере, так было до введения методов по умолчанию в Java 8 и, вероятно, C♯ 8), но наследование классов также является подтипом и наследованием реализации. Я помню, как читал об экспериментальном объектно-ориентированном объектно-ориентированном диалекте LISP со статической типизацией, в котором строго различаются миксины (которые содержат поведение), структуры (которые содержат состояние), интерфейсы (которые описываютповедение) и классы (которые составляют ноль или более структур с одним или несколькими миксинами и соответствуют одному или нескольким интерфейсам). Только классы могут быть созданы, и только интерфейсы могут быть использованы в качестве типов.

В динамически типизированном ОО-языке, таком как Python, Ruby, ECMAScript или Smalltalk, мы обычно думаем о типе (ах) объекта как о наборе протоколов, которому он соответствует. Обратите внимание на множественное число: объект может иметь несколько типов, и я не просто говорю о том факте, что каждый объект типа Stringтакже является объектом типа Object. (Кстати: обратите внимание, как я использовал имена классов, чтобы говорить о типах? Как глупо с моей стороны!) Объект может реализовать несколько протоколов. Например, в Ruby Arraysк ним можно добавлять, их можно индексировать, их можно повторять и сравнивать. Это четыре разных протокола, которые они реализуют!

Теперь у Руби нет типов. Но у сообщества Ruby есть типы! Впрочем, они существуют только в головах программистов. И в документации. Например, любой объект, который отвечает на метод, вызываемый eachпутем выдачи его элементов один за другим, считается перечисляемым объектом. И есть миксин под названием, Enumerableкоторый зависит от этого протокола. Таким образом, если объект имеет правильный тип (который существует только в голове программиста), то допускается смешивать в (наследование от) в EnumerableMixin, и это хорошо получить все виды методов прохладных бесплатно, как map, reduce, filterи т.д. на.

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

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

Йорг Миттаг
источник
Возможно, было бы полезно, если бы языки позволяли программам одновременно объявлять тип класса и интерфейс, для которого этот класс является реализацией, а также позволять реализациям указывать «конструкторы» (которые будут связываться с конструкторами классов, указанных интерфейсом). Для типов объектов, ссылки на которые будут общедоступными, предпочтительным шаблоном будет то, что тип класса будет использоваться только при создании производных классов; большинство ссылок должно иметь тип интерфейса. Возможность указывать конструкторы интерфейса была бы полезна в ситуациях, когда ...
суперкат
... например, для кода нужна коллекция, которая позволит определенному набору значений считываться по индексу, но на самом деле не имеет значения, какой это тип. Хотя существуют веские причины для распознавания классов и интерфейсов как отдельных типов типов, существует много ситуаций, когда они должны иметь возможность работать более тесно друг с другом, чем позволяют языки в настоящее время.
суперкат
У вас есть ссылка или несколько ключевых слов, которые я мог бы найти для получения дополнительной информации об упомянутом вами экспериментальном диалекте LISP, в котором формально проводится различие между миксинами, структурами, интерфейсами и классами?
тел
@tel: Нет, извините. Это было лет 15-20 назад, и тогда мои интересы были повсюду. Я не мог начать говорить вам, что я искал, когда наткнулся на это.
Йорг Миттаг
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

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

Давайте пока оставим в стороне, насколько это неуклюже и как у вас будут проблемы с назначением бесконечного числа типов выражений. Мы можем вернуться к этому позже.

С другой стороны, класс - это набор методов и полей, которые сгруппированы вместе (возможно, с модификаторами доступа, такими как private или public).

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и т. Д.), Которые не связаны ни с одним классом и не определены пользователем. Затем у вас есть все пользовательские классы, которые автоматически имеют типы с одинаковыми именами и идентифицируют подклассы с подтипами.

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

badcook
источник