Talvez seja útil primeiro distinguir entre um tipo e uma classe e depois mergulhar na diferença entre subtipagem e subclasse.
Para o restante desta resposta, assumirei que os tipos em discussão são estáticos (uma vez que a subtipagem geralmente surge em um contexto estático).
Vou desenvolver um pseudocódigo de brinquedo para ajudar a ilustrar a diferença entre um tipo e uma classe, porque a maioria dos idiomas os confunde pelo menos em parte (por uma boa razão que tocarei brevemente).
Vamos começar com um tipo. Um tipo é um rótulo para uma expressão no seu código. O valor desse rótulo e se ele é consistente (para algum tipo de definição de sistema consistente de consistente) com o valor de todos os outros rótulos pode ser determinado por um programa externo (um verificador de tipos) sem executar o programa. É isso que torna essas etiquetas especiais e merecedoras de seu próprio nome.
Em nossa linguagem de brinquedos, podemos permitir a criação de rótulos como esse.
declare type Int
declare type String
Então, podemos rotular vários valores como sendo desse tipo.
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
...
Com essas declarações, o nosso typechecker agora pode rejeitar declarações como
0 is of type String
se um dos requisitos do nosso sistema de tipos for que cada expressão tenha um tipo único.
Vamos deixar de lado, por enquanto, como isso é desajeitado e como você terá problemas para atribuir um número infinito de tipos de expressões. Podemos voltar a ele mais tarde.
Uma classe, por outro lado, é uma coleção de métodos e campos agrupados (potencialmente com modificadores de acesso, como privado ou público).
class StringClass:
defMethod concatenate(otherString): ...
defField size: ...
Uma instância desta classe tem a capacidade de criar ou usar definições pré-existentes desses métodos e campos.
Poderíamos optar por associar uma classe a um tipo, de modo que cada instância de uma classe seja automaticamente rotulada com esse tipo.
associate StringClass with String
Mas nem todo tipo precisa ter uma classe associada.
# Hmm... Doesn't look like there's a class for Int
Também é concebível que em nossa linguagem de brinquedos nem toda classe tenha um tipo, especialmente se nem todas as nossas expressões tiverem tipos. É um pouco mais complicado (mas não impossível) imaginar como seriam as regras de consistência do sistema de tipos se algumas expressões tivessem tipos e outras não.
Além disso, em nossa linguagem de brinquedos, essas associações não precisam ser únicas. Poderíamos associar duas classes do mesmo tipo.
associate MyCustomStringClass with String
Agora, lembre-se de que não é necessário que o nosso datilógrafo rastreie o valor de uma expressão (e, na maioria dos casos, não é ou é impossível fazê-lo). Tudo o que sabe são os rótulos que você contou. Como um lembrete anterior, o verificador de datilografia só conseguiu rejeitar a declaração 0 is of type String
por causa de nossa regra de tipo criada artificialmente de que expressões devem ter tipos únicos e que já tínhamos rotulado a expressão como 0
outra coisa. Não tinha nenhum conhecimento especial do valor de 0
.
E quanto a subtipagem? Subtipagem de poço é o nome de uma regra comum na digitação de textos que relaxa as outras regras que você possa ter. Ou seja, se A is subtype of B
em todo lugar o seu datilógrafo exigir um rótulo B
, ele também aceitará um A
.
Por exemplo, podemos fazer o seguinte para nossos números, em vez do que tínhamos anteriormente.
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
...
Subclassificação é uma abreviação para declarar uma nova classe que permite reutilizar métodos e campos declarados anteriormente.
class ExtendedStringClass is subclass of StringClass:
# We get concatenate and size for free!
def addQuestionMark: ...
Não precisamos associar instâncias de ExtendedStringClass
com, String
como fizemos com StringClass
, uma vez que, afinal, é uma classe totalmente nova, simplesmente não precisamos escrever tanto. Isso nos permitiria fornecer ExtendedStringClass
um tipo incompatível com String
o ponto de vista do datilógrafo.
Da mesma forma, poderíamos ter decidido fazer uma aula totalmente nova NewClass
e feito
associate NewClass with String
Agora, todas as instâncias de StringClass
podem ser substituídas NewClass
do ponto de vista do datilógrafo.
Então, em teoria, subtipagem e subclasse são coisas completamente diferentes. Mas nenhuma linguagem que eu conheça que tenha tipos e classes realmente faz as coisas dessa maneira. Vamos começar a reduzir nossa linguagem e explicar a lógica por trás de algumas de nossas decisões.
Primeiro, mesmo que, em teoria, classes completamente diferentes possam receber o mesmo tipo ou uma classe possa ter o mesmo tipo que valores que não são instâncias de nenhuma classe, isso dificulta gravemente a utilidade do verificador de letras. O typechecker é efetivamente roubado da capacidade de verificar se o método ou campo que você está chamando em uma expressão realmente existe nesse valor, o que provavelmente é uma verificação que você gostaria se estivesse com o problema de tocar junto com um typechecker. Afinal, quem sabe qual é o valor realmente embaixo desse String
rótulo; pode ser algo que não possui, por exemplo, um concatenate
método!
Ok, então vamos estipular que toda classe gera automaticamente um novo tipo com o mesmo nome que aquela classe e associate
instâncias com esse tipo. Isso nos permite livrar-nos dos associate
nomes diferentes entre StringClass
e String
.
Pelo mesmo motivo, provavelmente desejamos estabelecer automaticamente um relacionamento de subtipo entre os tipos de duas classes em que uma é uma subclasse da outra. Afinal, é garantido que a subclasse possui todos os métodos e campos da classe pai, mas o oposto não é verdadeiro. Portanto, embora a subclasse possa passar a qualquer momento que você precisar de um tipo da classe pai, o tipo da classe pai deve ser rejeitado se você precisar do tipo da subclasse.
Se você combinar isso com a estipulação de que todos os valores definidos pelo usuário devem ser instâncias de uma classe, você poderá is subclass of
obter o dobro do dever e se livrar is subtype of
.
E isso nos leva às características que a maioria das linguagens populares OO de tipo estatístico compartilha. Há um conjunto de tipos "primitivos" (por exemplo int
, float
etc.) que não estão associados a nenhuma classe e não são definidos pelo usuário. Então você tem todas as classes definidas pelo usuário que possuem automaticamente tipos com o mesmo nome e identificam subclassificação com subtipagem.
A nota final que farei é sobre o clunkiness de declarar tipos separadamente dos valores. A maioria dos idiomas confunde a criação dos dois, de modo que uma declaração de tipo também é uma declaração para gerar valores totalmente novos que são automaticamente rotulados com esse tipo. Por exemplo, uma declaração de classe geralmente cria o tipo e também uma maneira de instanciar valores desse tipo. Isso elimina algumas das imperfeições e, na presença de construtores, também permite criar infinitos rótulos de valores com um tipo em um só toque.