¿Cuál es la diferencia entre una subclase y un subtipo?

44

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?

tel
fuente
66
En realidad, en Python "tipo" y "clase" son conceptos distintos . De hecho, Python se escribe dinámicamente, "tipo" no es un concepto en absoluto en Python. Desafortunadamente, los desarrolladores de Python no entienden eso, y aun así combinan los dos.
Jörg W Mittag
11
"Tipo" y "clase" también son distintos en C ++. "Array of ints" es un tipo; que clase es "puntero a una variable de tipo int" es un tipo; que clase es Estas cosas no son de ninguna clase, pero seguramente son tipos.
Eric Lippert
2
Me preguntaba esto después de leer esa pregunta y esa respuesta.
user369450
44
@JorgWMittag Si no hay un concepto de "tipo" en python, alguien debería decirle a quien escriba la documentación: docs.python.org/3/library/stdtypes.html
Matt
@Matt, para ser justos, los tipos llegaron en 3.5, que es bastante reciente, especialmente según los estándares de lo que se me permite usar en la producción.
Jared Smith

Respuestas:

53

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 de T, la relación de subtipo a menudo se escribe S <: T, lo que significa que cualquier término de tipo Spuede usarse de manera segura en un contexto donde Tse 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

Robert Harvey
fuente
1
Muy bien dicho. Vale la pena mencionar en el contexto de la pregunta que los programadores de C ++ a menudo emplean clases base virtuales puras para comunicar relaciones de subtipo al sistema de tipos. Los enfoques de programación genéricos a menudo son preferidos, por supuesto.
Aluan Haddad
66
"La semántica precisa del subtipo depende crucialmente de los detalles de lo que" se usa de manera segura en un contexto donde "significa en un lenguaje de programación dado". ... y el LSP define una idea algo razonable de lo que significa "con seguridad" y nos dice qué limitaciones tienen que cumplir esos detalles para habilitar esta forma particular de "seguridad".
Jörg W Mittag
Una muestra más en la pila: si entendí correctamente, en C ++, la publicherencia introduce un subtipo mientras que la privateherencia introduce una subclase.
Quentin
La herencia pública de @Quentin es a la vez un subtipo y una subclase, pero privada es solo una subclase, pero no un subtipo. Puede tener subtipos sin subclases con estructuras como interfaces Java
eques
27

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 usar interfaces como tipos. (También, dato curioso: Java se criba interfacedirectamente 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 interface s son tipos, y las clases y las primitivas no lo son, entonces uno interfaceheredando de otro creará una relación de subtipo, mientras que uno classheredará de otro ser simplemente para la reutilización de código diferencial a través de super.

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 tipo Object. (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 llamado Enumerableque 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) la Enumerablemixin, y así obtener todo tipo de métodos fresco de forma gratuita, como map, reduce, filteretc. en.

Del mismo modo, si un objeto responde a <=>, entonces se considera para implementar el comparables protocolo, y se puede mezclar en el Comparablemixin y obtener cosas por el estilo <, <=, >, <=, ==, between?, y clampde forma gratuita. Sin embargo, también puede implementar todos esos métodos en sí mismo, y no heredar Comparableen 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 la IOclase, pero no existe una relación de herencia entre los dos. Sin embargo, a StringIOse puede usar en todas partes y IOse puede usar. Esto es muy útil en pruebas unitarias, donde puede reemplazar un archivo o stdincon un StringIOsin tener que hacer más cambios en su programa. Dado que se StringIOajusta al mismo protocolo que IO, ambos son del mismo tipo, a pesar de que son clases diferentes y no comparten ninguna relación (aparte del trivial que ambos extienden Objecten algún momento).

Jörg W Mittag
fuente
Puede ser útil si los lenguajes permiten que los programas declaren simultáneamente un tipo de clase y una interfaz para la cual esa clase es una implementación, y también permiten que las implementaciones especifiquen "constructores" (que encadenarían a los constructores de clases especificadas por la interfaz). Para los tipos de objetos cuyas referencias se compartirían públicamente, el patrón preferido sería que el tipo de clase solo se usara al crear clases derivadas; La mayoría de las referencias deben ser del tipo de interfaz. Ser capaz de especificar constructores de interfaz sería útil en situaciones en las que ...
supercat
... por ejemplo, el código necesita una colección que permita que un determinado conjunto de valores se lea por índice, pero realmente no le importa de qué tipo sea. Si bien existen razones sólidas para reconocer las clases y las interfaces como tipos de tipos distintos, hay muchas situaciones en las que deberían poder trabajar más estrechamente de lo que actualmente permiten los idiomas.
supercat
¿Tiene una referencia o algunas palabras clave que podría buscar para obtener más información sobre el dialecto experimental de LISP que mencionó que diferencia formalmente mixins, estructuras, interfaces y clases?
tel
@tel: No, lo siento. Probablemente fue hace unos 15-20 años, y en ese momento mis intereses estaban por todas partes. No podría comenzar a decirte lo que estaba buscando cuando me topé con esto.
Jörg W Mittag
Awww. Ese fue el detalle más interesante en todas estas respuestas. El hecho de que una separación formal de esos conceptos sea realmente posible dentro de la implementación de un lenguaje realmente ayudó a cristalizar la distinción de clase / tipo para mí. Supongo que iré a buscar ese LISP, en cualquier caso. ¿Recuerdas si lo leíste en un artículo / libro de una revista, o si acabas de escucharlo en una conversación?
tel
2

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.

declare type Int
declare type String

Entonces podríamos etiquetar varios valores como de este 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
...

Con estas declaraciones, nuestro typechecker ahora puede rechazar declaraciones como

0 is of type String

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).

class StringClass:
  defMethod concatenate(otherString): ...
  defField size: ...

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.

associate StringClass with String

Pero no todos los tipos necesitan tener una clase asociada.

# Hmm... Doesn't look like there's a class for Int

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.

associate MyCustomStringClass with String

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 como 0algo diferente. No tenía ningún conocimiento especial del valor de 0.

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 etiqueta B, también aceptará un A.

Por ejemplo, podríamos hacer lo siguiente para nuestros números en lugar de lo que teníamos 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
...

La subclasificación es una forma abreviada de declarar una nueva clase que le permite reutilizar métodos y campos previamente declarados.

class ExtendedStringClass is subclass of StringClass:
  # We get concatenate and size for free!
  def addQuestionMark: ...

No tenemos que asociar instancias de ExtendedStringClasscon Stringcomo lo hicimos StringClassya que, después de todo, es una clase completamente nueva, simplemente no teníamos que escribir tanto. Esto nos permitiría dar ExtendedStringClassun tipo que es incompatible Stringdesde el punto de vista del typechecker.

Del mismo modo, podríamos haber decidido hacer una clase completamente nueva NewClassy hacer

associate NewClass with String

Ahora cada instancia de StringClasspuede ser sustituida NewClasspor 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, un concatenatemé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 de associatelos diferentes nombres entre StringClassy String.

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 él is 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.

badcook
fuente