Самый высокий рейтинг ответа на этот вопрос о принципе замещения Лискова изо всех сил старается различить термины подтип и подкласс . Это также указывает на то, что некоторые языки объединяют два, а другие нет.
Для объектно-ориентированных языков, с которыми я больше всего знаком (Python, C ++), «тип» и «класс» являются синонимами. С точки зрения C ++, что бы значило иметь различие между подтипом и подклассом? Скажем, например, что Foo
это подкласс, но не подтип FooBase
. Если foo
это экземпляр Foo
, будет ли эта строка:
FooBase* fbPoint = &foo;
больше не будет действительным?
Ответы:
Подтип - это форма полиморфизма типов, в которой подтип является типом данных, который связан с другим типом данных (супертипом) с помощью некоторого понятия замещаемости, то есть элементы программы, обычно подпрограммы или функции, написанные для работы с элементами супертипа, также могут оперировать элементами подтипа.
Если
S
это подтипT
, отношение подтипа часто пишетсяS <: T
, чтобы означать, что любой термин типаS
может безопасно использоваться в контексте, гдеT
ожидается термин типа . Точная семантика подтипирования в решающей степени зависит от того, что «безопасно используется в контексте, где» означает в данном языке программирования.Подклассы не следует путать с подтипами. В общем, подтип устанавливает отношения is-a, тогда как подклассы только повторно используют реализацию и устанавливают синтаксические отношения, а не обязательно семантические отношения (наследование не обеспечивает поведенческий подтип).
Чтобы различать эти понятия, подтипы также известны как наследование интерфейса , тогда как подклассы известны как наследование реализации или наследование кода.
Ссылки
Подтип
Наследование
источник
public
наследование вводит подтип, аprivate
наследование - подкласс.Типа , в контексте того, что мы говорим о здесь, по сути, набор поведенческих гарантий. Контракт , если вы будете. Или, заимствуя терминологию из Smalltalk, протокол .
Класс представляет собой комплект из методов. Это набор реализаций поведения .
Подтипирование является средством уточнения протокола. Подклассы являются средством повторного использования дифференциального кода, то есть повторного использования кода, только описывая разницу в поведении.
Если вы использовали Java или C♯, возможно, вы натолкнулись на совет, что все типы должны быть
interface
типами. На самом деле, если вы читаете книгу « Возвращение к абстракции данных» Уильяма Кука , то, возможно, знаете, что для выполнения ОО на этих языках вы должны использовать толькоinterface
s в качестве типов. (Кроме того, забавный факт: Java извлеклаinterface
s из протоколов Objective-C, которые, в свою очередь, взяты непосредственно из Smalltalk.)Теперь, если мы последуем этому совету по кодированию до логического завершения и представим версию Java, в которой только
interface
s являются типами, а классы и примитивы - нет, то одноinterface
наследование от другого создаст отношение подтипа, тогда как одноclass
наследование от другого будет быть просто для дифференциального повторного использования кода черезsuper
.Насколько я знаю, не существует основных статически типизированных языков, которые бы строго различали наследование кода (наследование реализации / создание подклассов) и наследование контрактов (подтипирование). В Java и C♯ наследование интерфейса - это чистый подтип (или, по крайней мере, так было до введения методов по умолчанию в Java 8 и, вероятно, C♯ 8), но наследование классов также является подтипом и наследованием реализации. Я помню, как читал об экспериментальном объектно-ориентированном объектно-ориентированном диалекте LISP со статической типизацией, в котором строго различаются миксины (которые содержат поведение), структуры (которые содержат состояние), интерфейсы (которые описываютповедение) и классы (которые составляют ноль или более структур с одним или несколькими миксинами и соответствуют одному или нескольким интерфейсам). Только классы могут быть созданы, и только интерфейсы могут быть использованы в качестве типов.
В динамически типизированном ОО-языке, таком как Python, Ruby, ECMAScript или Smalltalk, мы обычно думаем о типе (ах) объекта как о наборе протоколов, которому он соответствует. Обратите внимание на множественное число: объект может иметь несколько типов, и я не просто говорю о том факте, что каждый объект типа
String
также является объектом типаObject
. (Кстати: обратите внимание, как я использовал имена классов, чтобы говорить о типах? Как глупо с моей стороны!) Объект может реализовать несколько протоколов. Например, в RubyArrays
к ним можно добавлять, их можно индексировать, их можно повторять и сравнивать. Это четыре разных протокола, которые они реализуют!Теперь у Руби нет типов. Но у сообщества Ruby есть типы! Впрочем, они существуют только в головах программистов. И в документации. Например, любой объект, который отвечает на метод, вызываемый
each
путем выдачи его элементов один за другим, считается перечисляемым объектом. И есть миксин под названием,Enumerable
который зависит от этого протокола. Таким образом, если объект имеет правильный тип (который существует только в голове программиста), то допускается смешивать в (наследование от) вEnumerable
Mixin, и это хорошо получить все виды методов прохладных бесплатно, какmap
,reduce
,filter
и т.д. на.Точно так же, если объект отвечает
<=>
, то он считается реализовать сравнимый протокол, и он может смешиваться вComparable
Mixin и получить такие вещи , как<
,<=
,>
,<=
,==
,between?
, иclamp
бесплатно. Однако он также может реализовывать все эти методы сам по себе, и не наследоватьComparable
вообще, и он все равно будет считаться сопоставимым .Хорошим примером является
StringIO
библиотека, которая по существу подделывает потоки ввода / вывода со строками. Он реализует все те же методы, что иIO
класс, но между ними нет отношений наследования. Тем не менее,StringIO
можно использовать везде, гдеIO
можно. Это очень полезно в единичных тестов, где вы можете заменить файл илиstdin
сStringIO
без внесения каких - либо дополнительных изменений в программе. Поскольку ониStringIO
соответствуют одному и тому же протоколуIO
, они оба относятся к одному и тому же типу, даже если они являются разными классами, и не имеют общих отношений (кроме тривиального, что они оба расширяютсяObject
в некоторой точке).источник
Возможно, сначала полезно провести различие между типом и классом, а затем погрузиться в разницу между подтипами и подклассами.
В оставшейся части этого ответа я собираюсь предположить, что обсуждаемые типы являются статическими типами (поскольку подтип обычно возникает в статическом контексте).
Я собираюсь разработать игрушечный псевдокод, чтобы проиллюстрировать разницу между типом и классом, потому что большинство языков сопоставляют их хотя бы частично (по уважительной причине, которую я кратко коснусь).
Давайте начнем с типа. Тип - это метка для выражения в вашем коде. Значение этой метки и ее соответствие (для некоторого системного определения типа непротиворечивость) значению всех других меток может быть определено внешней программой (проверкой типов) без запуска вашей программы. Вот что делает эти этикетки особенными и заслуживающими своего имени.
На нашем игрушечном языке мы могли бы разрешить создание ярлыков следующим образом.
Тогда мы можем пометить различные значения как имеющие этот тип.
С помощью этих утверждений наш проверщик типов теперь может отклонять такие утверждения, как
если одним из требований нашей системы типов является то, что каждое выражение имеет уникальный тип.
Давайте пока оставим в стороне, насколько это неуклюже и как у вас будут проблемы с назначением бесконечного числа типов выражений. Мы можем вернуться к этому позже.
С другой стороны, класс - это набор методов и полей, которые сгруппированы вместе (возможно, с модификаторами доступа, такими как private или public).
Экземпляр этого класса получает возможность создавать или использовать ранее существующие определения этих методов и полей.
Мы могли бы связать класс с типом таким образом, чтобы каждый экземпляр класса автоматически помечался этим типом.
Но не каждый тип должен иметь связанный класс.
Также возможно, что в нашем игрушечном языке не каждый класс имеет тип, особенно если не все наши выражения имеют типы. Немного сложнее (но не невозможно) представить, как будут выглядеть правила согласованности системы типов, если у некоторых выражений есть типы, а у других - нет.
Более того, в нашем игрушечном языке эти ассоциации не должны быть уникальными. Мы могли бы связать два класса с одним и тем же типом.
Теперь имейте в виду, что для нашего средства проверки типов не требуется отслеживать значение выражения (и в большинстве случаев это не будет или невозможно сделать). Все, что он знает, - это ярлыки, которые вы ему сказали. Напомним, что ранее проверщик типов мог отклонять оператор только
0 is of type String
из-за нашего искусственно созданного правила типа, согласно которому выражения должны иметь уникальные типы, и мы уже пометили выражение как-0
то еще. У него не было специальных знаний о ценности0
.Так что насчет подтипов? Хорошо подтипирование - это название общего правила проверки типов, которое ослабляет другие правила, которые вы можете иметь. А именно, если
A is subtype of B
везде ваш типограф проверяет ярлыкB
, он также принимаетA
.Например, мы могли бы сделать следующее для наших чисел вместо того, что мы имели ранее.
Подклассы - это сокращение для объявления нового класса, которое позволяет вам повторно использовать ранее объявленные методы и поля.
Нам не нужно связывать экземпляры
ExtendedStringClass
с тем,String
что мы делали,StringClass
поскольку, в конце концов, это совершенно новый класс, нам просто не нужно было писать так много. Это позволило бы нам датьExtendedStringClass
тип, который несовместим сString
точки зрения типографа.Точно так же мы могли бы сделать целый новый класс
NewClass
и сделатьТеперь каждый экземпляр
StringClass
можно заменитьNewClass
на точку зрения типографа.Так что в теории подтипы и подклассы это совершенно разные вещи. Но ни один из известных мне языков, в которых есть типы и классы, на самом деле так не действует. Давайте начнем анализировать наш язык и объясним обоснование некоторых наших решений.
Во-первых, даже если теоретически совершенно разным классам может быть присвоен один и тот же тип или классу может быть присвоен тот же тип, что и значениям, которые не являются экземплярами какого-либо класса, это сильно затрудняет использование средства проверки типов. Проверка типов лишена возможности проверять, действительно ли метод или поле, которое вы вызываете в выражении, существует для этого значения, что, вероятно, является проверкой, которую вы хотели бы получить, если вам трудно играть вместе с проверки типов. В конце концов, кто знает, каково значение на самом деле под этим
String
ярлыком; это может быть что-то, чего нет, например,concatenate
метод вообще!Итак, давайте оговоримся, что каждый класс автоматически генерирует новый тип с тем же именем, что и у этого класса, и
associate
экземпляры с этим типом. Это позволяет нам избавиться отassociate
различных имен междуStringClass
иString
.По той же причине мы, вероятно, хотим автоматически установить отношение подтипа между типами двух классов, где один является подклассом другого. Ведь подкласс гарантированно имеет все методы и поля, которые есть у родительского класса, но обратное неверно. Поэтому, хотя подкласс может проходить в любое время, когда вам нужен тип родительского класса, тип родительского класса должен быть отклонен, если вам нужен тип подкласса.
Если вы объедините это с условием, что все пользовательские значения должны быть экземплярами класса, тогда вы можете использовать
is subclass of
двойную обязанность и избавляться от нееis subtype of
.И это подводит нас к характеристикам, которыми обладает большинство популярных статически типизированных ОО-языков. Существует набор «примитивных» типов (например
int
,float
и т. Д.), Которые не связаны ни с одним классом и не определены пользователем. Затем у вас есть все пользовательские классы, которые автоматически имеют типы с одинаковыми именами и идентифицируют подклассы с подтипами.Последнее замечание, которое я сделаю, касается грубости объявления типов отдельно от значений. Большинство языков связывают создание двух, так что объявление типа также является объявлением для генерации совершенно новых значений, которые автоматически помечаются этим типом. Например, объявление класса обычно создает как тип, так и способ создания экземпляров значений этого типа. Это избавляет от некоторых неудобств и, при наличии конструкторов, также позволяет создавать метки бесконечно много значений с типом в один штрих.
источник