La respuesta mejor calificada a esta pregunta sobre el Principio de sustitución de Liskov se esfuerza por distinguir entre los términos subtipo y subclase . También señala que algunos idiomas combinan los dos, mientras que otros no.
Para los lenguajes orientados a objetos con los que estoy más familiarizado (Python, C ++), "tipo" y "clase" son conceptos sinónimos. En términos de C ++, ¿qué significaría tener una distinción entre subtipo y subclase? Digamos, por ejemplo, que Fooes una subclase, pero no un subtipo, de FooBase. Si fooes una instancia de Foo, ¿sería esta línea:
FooBase* fbPoint = &foo;
ya no es valido?

Respuestas:
El subtipo es una forma de polimorfismo de tipo en el que un subtipo es un tipo de datos que está relacionado con otro tipo de datos (el supertipo) por alguna noción de sustituibilidad, lo que significa que los elementos del programa, generalmente subrutinas o funciones, escritas para operar en elementos del supertipo también pueden operar en elementos del subtipo.
Si
Ses un subtipo deT, la relación de subtipo a menudo se escribeS <: T, lo que significa que cualquier término de tipoSpuede usarse de manera segura en un contexto dondeTse espera un término de tipo . La semántica precisa del subtipo depende crucialmente de los detalles de lo que "se usa con seguridad en un contexto donde" significa en un lenguaje de programación dado.La subclasificación no debe confundirse con la subtipificación. En general, el subtipo establece una relación is-a, mientras que la subclasificación solo reutiliza la implementación y establece una relación sintáctica, no necesariamente una relación semántica (la herencia no garantiza el subtipo de comportamiento).
Para distinguir estos conceptos, el subtipo también se conoce como herencia de interfaz , mientras que la subclasificación se conoce como herencia de implementación o herencia de código.
Referencias
Subtipo de
herencia
fuente
publicherencia introduce un subtipo mientras que laprivateherencia introduce una subclase.Un tipo , en el contexto del que estamos hablando aquí, es esencialmente un conjunto de garantías de comportamiento. Un contrato , si quieres. O, tomando prestada la terminología de Smalltalk, un protocolo .
Una clase es un conjunto de métodos. Es un conjunto de implementaciones de comportamiento .
Subtipar es un medio de refinar el protocolo. La subclasificación es un medio de reutilización del código diferencial, es decir, la reutilización del código al describir solo la diferencia de comportamiento.
Si ha utilizado Java o C♯, es posible que haya encontrado el consejo de que todos los tipos deberían ser
interfacetipos. De hecho, si lee William Cook's On Understanding Data Abstraction, Revisited , entonces puede saber que para hacer OO en esos idiomas, solo debe usarinterfaces como tipos. (También, dato curioso: Java se cribainterfacedirectamente desde los protocolos de Objective-C, que a su vez se toman directamente de Smalltalk).Ahora, si seguimos ese consejo de codificación hasta su conclusión lógica e imaginamos una versión de Java, donde solo los
interfaces son tipos, y las clases y las primitivas no lo son, entonces unointerfaceheredando de otro creará una relación de subtipo, mientras que unoclassheredará de otro ser simplemente para la reutilización de código diferencial a través desuper.Hasta donde sé, no hay lenguajes estáticos convencionales que distingan estrictamente entre el código heredado (herencia de implementación / subclasificación) y los contratos heredados (subtipo). En Java y C♯, la herencia de interfaz es subtipo puro (o al menos lo era, hasta la introducción de métodos predeterminados en Java 8 y probablemente también C♯ 8), pero la herencia de clase también es subtipo, así como la herencia de implementación. Recuerdo haber leído sobre un dialecto experimental LISP orientado a objetos estáticamente tipado, que distinguía estrictamente entre mixins (que contienen comportamiento), estructuras (que contienen estado), interfaces (que describencomportamiento) y clases (que componen cero o más estructuras con uno o más mixins y se ajustan a una o más interfaces). Solo se pueden crear instancias de clases y solo se pueden usar interfaces como tipos.
En un lenguaje OO de tipo dinámico como Python, Ruby, ECMAScript o Smalltalk, generalmente pensamos en los tipos de un objeto como el conjunto de protocolos a los que se ajusta. Tenga en cuenta el plural: un objeto puede tener múltiples tipos, y no solo estoy hablando del hecho de que cada objeto de tipo
Stringtambién es un objeto de tipoObject. (Por cierto: observe cómo usé los nombres de clase para hablar sobre los tipos. ¡Qué estúpido de mi parte!) Un objeto puede implementar múltiples protocolos. Por ejemplo, en Ruby,Arraysse pueden agregar, se pueden indexar, se pueden iterar y se pueden comparar. ¡Son cuatro protocolos diferentes que implementan!Ahora, Ruby no tiene tipos. ¡Pero la comunidad Ruby tiene tipos! Sin embargo, solo existen en la cabeza de los programadores. Y en la documentación. Por ejemplo, cualquier objeto que responda a un método llamado
eachal entregar sus elementos uno por uno se considera un objeto enumerable . Y hay un mixin llamadoEnumerableque depende de este protocolo. Por lo tanto, si el objeto tiene el correcto tipo (que sólo existe en la mente del programador), entonces se dejó mezclar en (hereda de) laEnumerablemixin, y así obtener todo tipo de métodos fresco de forma gratuita, comomap,reduce,filteretc. en.Del mismo modo, si un objeto responde a
<=>, entonces se considera para implementar el comparables protocolo, y se puede mezclar en elComparablemixin y obtener cosas por el estilo<,<=,>,<=,==,between?, yclampde forma gratuita. Sin embargo, también puede implementar todos esos métodos en sí mismo, y no heredarComparableen absoluto, y aún se consideraría comparable .Un buen ejemplo es la
StringIObiblioteca, que esencialmente falsifica las secuencias de E / S con cadenas. Implementa los mismos métodos que laIOclase, pero no existe una relación de herencia entre los dos. Sin embargo, aStringIOse puede usar en todas partes yIOse puede usar. Esto es muy útil en pruebas unitarias, donde puede reemplazar un archivo ostdincon unStringIOsin tener que hacer más cambios en su programa. Dado que seStringIOajusta al mismo protocolo queIO, ambos son del mismo tipo, a pesar de que son clases diferentes y no comparten ninguna relación (aparte del trivial que ambos extiendenObjecten algún momento).fuente
Quizás sea primero útil distinguir entre un tipo y una clase y luego sumergirse en la diferencia entre subtipo y subclasificación.
Para el resto de esta respuesta, voy a suponer que los tipos en discusión son tipos estáticos (ya que el subtipo generalmente aparece en un contexto estático).
Voy a desarrollar un pseudocódigo de juguete para ayudar a ilustrar la diferencia entre un tipo y una clase porque la mayoría de los idiomas los combinan al menos en parte (por una buena razón que mencionaré brevemente).
Comencemos con un tipo. Un tipo es una etiqueta para una expresión en su código. El valor de esta etiqueta y si es consistente (para algún tipo de definición específica de sistema de consistente) con el valor de todas las otras etiquetas puede ser determinado por un programa externo (un typechecker) sin ejecutar su programa. Eso es lo que hace que estas etiquetas sean especiales y merezcan su propio nombre.
En nuestro lenguaje de juguetes, podríamos permitir la creación de etiquetas como esta.
Entonces podríamos etiquetar varios valores como de este tipo.
Con estas declaraciones, nuestro typechecker ahora puede rechazar declaraciones como
Si uno de los requisitos de nuestro sistema de tipos es que cada expresión tiene un tipo único.
Dejemos de lado por ahora lo torpe que es esto y cómo va a tener problemas para asignar un número infinito de tipos de expresiones. Podemos volver a eso más tarde.
Una clase, por otro lado, es una colección de métodos y campos que se agrupan (potencialmente con modificadores de acceso como privado o público).
Una instancia de esta clase tiene la capacidad de crear o usar definiciones preexistentes de estos métodos y campos.
Podríamos elegir asociar una clase con un tipo de modo que cada instancia de una clase se etiquete automáticamente con ese tipo.
Pero no todos los tipos necesitan tener una clase asociada.
También es concebible que en nuestro lenguaje de juguete no todas las clases tengan un tipo, especialmente si no todas nuestras expresiones tienen tipos. Es un poco más complicado (pero no imposible) imaginar cómo se verían las reglas de coherencia del sistema de tipos si algunas expresiones tuvieran tipos y otras no.
Además, en nuestro lenguaje de juguete, estas asociaciones no tienen que ser únicas. Podríamos asociar dos clases con el mismo tipo.
Ahora tenga en cuenta que no hay ningún requisito para que nuestro typechecker rastree el valor de una expresión (y en la mayoría de los casos no lo hará o es imposible hacerlo). Todo lo que sabe son las etiquetas que le has dicho. Como recordatorio anterior, el comprobador de tipos solo podía rechazar la declaración
0 is of type Stringdebido a nuestra regla de tipo creada artificialmente de que las expresiones deben tener tipos únicos y ya habíamos etiquetado la expresión como0algo diferente. No tenía ningún conocimiento especial del valor de0.Entonces, ¿qué pasa con el subtipo? Subtipo de pozo es un nombre para una regla común en la verificación de tipos que relaja las otras reglas que pueda tener. Es decir, si en
A is subtype of Btodas partes su typechecker exige una etiquetaB, también aceptará unA.Por ejemplo, podríamos hacer lo siguiente para nuestros números en lugar de lo que teníamos anteriormente.
La subclasificación es una forma abreviada de declarar una nueva clase que le permite reutilizar métodos y campos previamente declarados.
No tenemos que asociar instancias de
ExtendedStringClassconStringcomo lo hicimosStringClassya que, después de todo, es una clase completamente nueva, simplemente no teníamos que escribir tanto. Esto nos permitiría darExtendedStringClassun tipo que es incompatibleStringdesde el punto de vista del typechecker.Del mismo modo, podríamos haber decidido hacer una clase completamente nueva
NewClassy hacerAhora cada instancia de
StringClasspuede ser sustituidaNewClasspor el punto de vista del typechecker.Entonces, en teoría, el subtipo y la subclasificación son cosas completamente diferentes. Pero ningún lenguaje que conozca que tenga tipos y clases realmente hace las cosas de esta manera. Comencemos por reducir nuestro lenguaje y expliquemos los fundamentos de algunas de nuestras decisiones.
En primer lugar, a pesar de que, en teoría, las clases completamente diferentes podrían recibir el mismo tipo o una clase podría recibir el mismo tipo que los valores que no son instancias de ninguna clase, esto obstaculiza gravemente la utilidad del comprobador de tipos. El typechecker es efectivamente despojado de la capacidad de verificar si el método o campo que está llamando dentro de una expresión realmente existe en ese valor, lo que probablemente sea una comprobación que le gustaría si tiene la molestia de jugar junto con un máquina de escribir Después de todo, quién sabe cuál es el valor realmente debajo de esa
Stringetiqueta; ¡podría ser algo que no tiene, por ejemplo, unconcatenatemétodo en absoluto!Bien, entonces estipulemos que cada clase genera automáticamente un nuevo tipo del mismo nombre que esa clase y
associatelas instancias de ese tipo. Eso nos permite deshacernos deassociatelos diferentes nombres entreStringClassyString.Por la misma razón, probablemente queremos establecer automáticamente una relación de subtipo entre los tipos de dos clases donde una es una subclase de otra. Después de toda la subclase se garantiza que tiene todos los métodos y campos que tiene la clase padre, pero lo contrario no es cierto. Por lo tanto, aunque la subclase puede pasar en cualquier momento que necesite un tipo de la clase principal, el tipo de la clase principal debe rechazarse si necesita el tipo de la subclase.
Si combina esto con la estipulación de que todos los valores definidos por el usuario deben ser instancias de una clase, entonces puede
is subclass ofhacer doble trabajo y deshacerse de élis subtype of.Y esto nos lleva a las características que comparten la mayoría de los lenguajes OO de tipo estático populares. Hay un conjunto de tipos "primitivos" (por ejemplo
int,floatetc.) que no están asociados con ninguna clase y no están definidos por el usuario. Luego tiene todas las clases definidas por el usuario que automáticamente tienen tipos del mismo nombre e identifican subclases con subtipos.La nota final que haré es en torno a la complejidad de declarar tipos por separado de los valores. La mayoría de los idiomas combinan la creación de los dos, de modo que una declaración de tipo también es una declaración para generar valores completamente nuevos que se etiquetan automáticamente con ese tipo. Por ejemplo, una declaración de clase generalmente crea el tipo, así como una forma de instanciar valores de ese tipo. Esto elimina parte de la fragilidad y, en presencia de constructores, también le permite crear etiquetas de infinitos valores con un tipo de un solo trazo.
fuente