Возможно, сначала полезно провести различие между типом и классом, а затем погрузиться в разницу между подтипами и подклассами.
В оставшейся части этого ответа я собираюсь предположить, что обсуждаемые типы являются статическими типами (поскольку подтип обычно возникает в статическом контексте).
Я собираюсь разработать игрушечный псевдокод, чтобы проиллюстрировать разницу между типом и классом, потому что большинство языков сопоставляют их хотя бы частично (по уважительной причине, которую я кратко коснусь).
Давайте начнем с типа. Тип - это метка для выражения в вашем коде. Значение этой метки и ее соответствие (для некоторого системного определения типа непротиворечивость) значению всех других меток может быть определено внешней программой (проверкой типов) без запуска вашей программы. Вот что делает эти этикетки особенными и заслуживающими своего имени.
На нашем игрушечном языке мы могли бы разрешить создание ярлыков следующим образом.
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
и т. Д.), Которые не связаны ни с одним классом и не определены пользователем. Затем у вас есть все пользовательские классы, которые автоматически имеют типы с одинаковыми именами и идентифицируют подклассы с подтипами.
Последнее замечание, которое я сделаю, касается грубости объявления типов отдельно от значений. Большинство языков связывают создание двух, так что объявление типа также является объявлением для генерации совершенно новых значений, которые автоматически помечаются этим типом. Например, объявление класса обычно создает как тип, так и способ создания экземпляров значений этого типа. Это избавляет от некоторых неудобств и, при наличии конструкторов, также позволяет создавать метки бесконечно много значений с типом в один штрих.