Qual é a diferença entre uma subclasse e um subtipo?

44

A resposta com a classificação mais alta para essa pergunta sobre o Princípio da Substituição de Liskov faz um esforço para distinguir entre os termos subtipo e subclasse . Também afirma que algumas línguas conflitam as duas, enquanto outras não.

Para as linguagens orientadas a objetos com as quais eu estou mais familiarizado (Python, C ++), "type" e "class" são conceitos sinônimos. Em termos de C ++, o que significaria ter uma distinção entre subtipo e subclasse? Digamos, por exemplo, que Fooseja uma subclasse, mas não um subtipo, de FooBase. Se foofor uma instância de Foo, esta linha:

FooBase* fbPoint = &foo;

não é mais válido?

tel
fonte
6
Na verdade, no Python, "tipo" e "classe" são conceitos distintos . De fato, sendo digitado dinamicamente o Python, "type" não é um conceito no Python. Infelizmente, os desenvolvedores do Python não entendem isso e ainda confundem os dois.
Jörg W Mittag
11
"Tipo" e "classe" também são distintos em C ++. "Matriz de entradas" é um tipo; que classe é essa? "ponteiro para uma variável do tipo int" é um tipo; que classe é essa? Essas coisas não são de classe, mas certamente são do tipo.
Eric Lippert
2
Fiquei me perguntando exatamente isso depois de ler essa pergunta e essa resposta.
user369450
4
@JorgWMittag Se não existe o conceito de um "tipo" em python então alguém deveria dizer a quem escreve a documentação: docs.python.org/3/library/stdtypes.html
Matt
@Matt para ser justo, os tipos chegaram no 3.5, o que é bastante recente, especialmente pelos padrões do que eu sou permitido usar na produção.
Jared Smith

Respostas:

53

Subtipagem é uma forma de polimorfismo de tipo em que um subtipo é um tipo de dados que está relacionado a outro tipo de dados (o supertipo) por alguma noção de substituibilidade, o que significa que os elementos do programa, geralmente sub-rotinas ou funções, escritas para operar em elementos do supertipo também podem operar em elementos do subtipo.

Se Sfor um subtipo de T, a relação de subtipo é frequentemente escrita S <: T, para significar que qualquer termo do tipo Spode ser usado com segurança em um contexto em que um termo do tipo Té esperado. A semântica precisa da subtipagem depende crucialmente dos detalhes do que "usado com segurança em um contexto em que" significa em uma determinada linguagem de programação.

Subclassificação não deve ser confundida com subtipagem. Em geral, a subtipagem estabelece um relacionamento é-a, enquanto a subclasse apenas reutiliza a implementação e estabelece uma relação sintática, não necessariamente uma relação semântica (a herança não garante a subtipagem comportamental).

Para distinguir esses conceitos, a subtipagem também é conhecida como herança de interface , enquanto a subclasse é conhecida como herança de implementação ou herança de código.

Referências
Subtipagem
Herança

Robert Harvey
fonte
1
Muito bem dito. Vale a pena mencionar no contexto da pergunta que os programadores de C ++ geralmente empregam classes básicas virtuais puras para comunicar relacionamentos de subtipagem com o sistema de tipos. Abordagens de programação genérica são frequentemente preferidas, é claro.
Aluan Haddad
6
"A semântica precisa da subtipagem depende crucialmente dos detalhes do que" usado com segurança em um contexto em que "significa" em uma determinada linguagem de programação ". ... e o LSP define uma idéia razoavelmente razoável do que "segurança" significa e nos diz quais restrições essas particularidades devem satisfazer para permitir essa forma específica de "segurança".
Jörg W Mittag
Mais uma amostra na pilha: se eu entendi corretamente, em C ++, a publicherança introduz um subtipo enquanto a privateherança introduz uma subclasse.
Quentin
A herança pública do @Quentin é um subtipo e uma subclasse, mas privada ainda é apenas uma subclasse, mas não um subtipo. Você pode ter subtipagem sem subclassificar com estruturas como interfaces Java
igual a
27

Um tipo , no contexto sobre o qual estamos falando aqui, é essencialmente um conjunto de garantias comportamentais. Um contrato , se você quiser. Ou, terminologia de empréstimo do Smalltalk, um protocolo .

Uma classe é um pacote de métodos. É um conjunto de implementações de comportamento .

A subtipagem é um meio de refinar o protocolo. A subclassificação é um meio de reutilização de código diferencial, ou seja, reutilização de código apenas descrevendo a diferença de comportamento.

Se você usou Java ou C♯, pode ter encontrado o conselho de que todos os tipos devem ser interfacetipos. De fato, se você ler On Understanding Data Abstraction, Revisited , de William Cook , talvez saiba que, para fazer OO nesses idiomas, você deve usar apenas interfaces como tipos. (Além disso, o engraçado é que o Java utilizou interfaces diretamente dos protocolos da Objective-C, que por sua vez são extraídos diretamente do Smalltalk.)

Agora, se seguirmos esse conselho de codificação para sua conclusão lógica e imaginarmos uma versão do Java, onde apenas interface s são tipos, e classes e primitivas não, então uma interfaceherança de outra criará um relacionamento de subtipagem, enquanto uma classherança de outra será seja apenas para reutilização de código diferencial via super.

Até onde eu sei, não há linguagens estaticamente tipificadas mainstream que distinguem estritamente entre herdar código (herança de implementação / subclasse) e herdar contratos (subtipo). Em Java e C♯, a herança de interface é pura subtipagem (ou pelo menos era, até a introdução de métodos padrão no Java 8 e provavelmente C♯ 8 também), mas a herança de classe também está subdividida, bem como a herança de implementação. Lembro-me de ler sobre um dialeto experimental LISP orientado a objeto, estaticamente tipado, que distinguia estritamente entre mixins (que contêm comportamento), estruturas (que contêm estado), interfaces (que descrevemcomportamento) e classes (que compõem zero ou mais estruturas com um ou mais mixins e estão em conformidade com uma ou mais interfaces). Somente classes podem ser instanciadas e apenas interfaces podem ser usadas como tipos.

Em uma linguagem OO de tipo dinâmico, como Python, Ruby, ECMAScript ou Smalltalk, geralmente pensamos no (s) tipo (s) de um objeto como o conjunto de protocolos aos quais ele está em conformidade. Observe o plural: um objeto pode ter vários tipos, e não estou falando apenas do fato de que cada objeto do tipo Stringtambém é um objeto do tipo Object. (BTW: observe como eu usei nomes de classe para falar sobre tipos? Que estúpido da minha parte!) Um objeto pode implementar vários protocolos. Por exemplo, no Ruby, Arrayspodem ser anexados, eles podem ser indexados, podem ser iterados e comparados. São quatro protocolos diferentes que eles implementam!

Agora, Ruby não tem tipos. Mas a comunidade Ruby tem tipos! Eles existem apenas na cabeça dos programadores. E na documentação. Por exemplo, qualquer objeto que responda a um método chamado eachproduzindo seus elementos um por um é considerado um objeto enumerável . E existe um mixin chamado Enumerableque depende desse protocolo. Assim, se seu objeto tem o correto tipo (que só existe na cabeça do programador), então é permitido misturar (herdam) o Enumerablemixin, e assim obter todos os tipos de métodos legais para livre, como map, reduce, filtere assim em.

Da mesma forma, se um objeto responde a <=>, então ele é considerado para implementar o comparável protocolo, e pode misturar na Comparablemixin e obter coisas como <, <=, >, <=, ==, between?, e clampde graça. No entanto, ele também pode implementar todos esses métodos em si, e não herdar Comparablenada, e ainda assim seria considerado comparável .

Um bom exemplo é a StringIObiblioteca, que essencialmente falsifica os fluxos de E / S com cadeias. Ele implementa todos os mesmos métodos da IOclasse, mas não há relação de herança entre os dois. No entanto, a StringIOpode ser usado em qualquer lugar que IOpossa ser usado. Isso é muito útil em testes de unidade, onde você pode substituir um arquivo ou stdinpor um StringIOsem precisar fazer mais alterações no seu programa. Como StringIOestá em conformidade com o mesmo protocolo que IO, ambos são do mesmo tipo, embora sejam classes diferentes e não compartilhem nenhum relacionamento (exceto o trivial que ambos estendem Objectem algum momento).

Jörg W Mittag
fonte
Pode ser útil se as linguagens permitissem aos programas declarar simultaneamente um tipo de classe e uma interface para a qual essa classe é uma implementação e também permitir que as implementações especificassem "construtores" (que seriam encadeados para construtores de classes especificadas pela interface). Para tipos de objetos para os quais as referências seriam compartilhadas publicamente, o padrão preferido seria que o tipo de classe fosse usado apenas ao criar classes derivadas; a maioria das referências deve ser do tipo de interface. Ser capaz de especificar os construtores de interface seria útil em situações em que ... #
487
... por exemplo, o código precisa de uma coleção que permita que um determinado conjunto de valores seja lido pelo índice, mas realmente não se importa com o tipo. Embora existam razões sólidas para reconhecer classes e interfaces como tipos distintos de tipo, há muitas situações em que eles devem poder trabalhar mais próximos do que os idiomas atualmente permitem.
Supercat
Você tem uma referência ou algumas palavras-chave? Eu poderia procurar mais informações sobre o dialeto LISP experimental que você mencionou que diferencia formalmente mixins, estruturas, interfaces e classes?
tel
@tel: Não, desculpe. Provavelmente foi há cerca de 15 a 20 anos, e naquela época meus interesses estavam por todo o lugar. Não pude começar a dizer o que estava procurando quando me deparei com isso.
Jörg W Mittag
Awww. Esse foi o detalhe mais interessante em todas essas respostas. O fato de uma separação formal desses conceitos ser realmente possível na implementação de uma linguagem realmente ajudou a cristalizar a distinção de classe / tipo para mim. Eu acho que vou procurar por esse LISP, em qualquer caso. Você se lembra se leu sobre isso em um artigo / livro de revista ou se acabou de ouvir sobre isso em conversas?
tel
2

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 Stringpor 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 0outra 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 Bem 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 ExtendedStringClasscom, Stringcomo fizemos com StringClass, uma vez que, afinal, é uma classe totalmente nova, simplesmente não precisamos escrever tanto. Isso nos permitiria fornecer ExtendedStringClassum tipo incompatível com Stringo ponto de vista do datilógrafo.

Da mesma forma, poderíamos ter decidido fazer uma aula totalmente nova NewClasse feito

associate NewClass with String

Agora, todas as instâncias de StringClasspodem ser substituídas NewClassdo 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 Stringrótulo; pode ser algo que não possui, por exemplo, um concatenatemétodo!

Ok, então vamos estipular que toda classe gera automaticamente um novo tipo com o mesmo nome que aquela classe e associateinstâncias com esse tipo. Isso nos permite livrar-nos dos associatenomes diferentes entre StringClasse 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 ofobter 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, floatetc.) 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.

badcook
fonte