¿Por qué los lenguajes de POO estáticos fuertes convencionales evitan heredar primitivas?

53

¿Por qué está bien y es más esperado?

abstract type Shape
{
   abstract number Area();
}

concrete type Triangle : Shape
{
   concrete number Area()
   {
      //...
   }
}

... mientras esto no está bien y nadie se queja:

concrete type Name : string
{
}

concrete type Index : int
{
}

concrete type Quantity : int
{
}

Mi motivación es maximizar el uso del sistema de tipos para la verificación de corrección en tiempo de compilación.

PD: sí, he leído esto y envolverlo es una solución alternativa.

Guarida
fuente
1
Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
maple_shaft
Tenía una motivación similar en esta pregunta , puede que le resulte interesante.
default.kramer
Iba a agregar una respuesta que confirmara la idea de "no quieres herencia", y que la envoltura es muy poderosa, incluida la entrega de la conversión implícita o explícita (o fallas) que desees, especialmente con optimizaciones JIT que sugieren que de todos modos, obtengo casi el mismo rendimiento, pero se ha vinculado a esa respuesta :-) Solo agregaría, sería bueno que los idiomas agregaran características para reducir el código repetitivo necesario para reenviar propiedades / métodos, especialmente si solo hay un valor único.
Mark Hurd

Respuestas:

82

¿Asumo que estás pensando en lenguajes como Java y C #?

En esos idiomas, las primitivas (como int) son básicamente un compromiso para el rendimiento. No son compatibles con todas las características de los objetos, pero son más rápidos y con menos sobrecarga.

Para que los objetos admitan la herencia, cada instancia debe "saber" en tiempo de ejecución de qué clase es una instancia. De lo contrario, los métodos anulados no se pueden resolver en tiempo de ejecución. Para los objetos, esto significa que los datos de la instancia se almacenan en la memoria junto con un puntero al objeto de la clase. Si dicha información también se almacenara junto con valores primitivos, los requisitos de memoria se dispararían. Un valor entero de 16 bits requeriría sus 16 bits para el valor y, adicionalmente, memoria de 32 o 64 bits para un puntero a su clase.

Además de la sobrecarga de memoria, también esperaría poder anular operaciones comunes en primitivas como operadores aritméticos. Sin subtipar, los operadores como +se pueden compilar a una simple instrucción de código de máquina. Si pudiera anularse, necesitaría resolver métodos en tiempo de ejecución, una operación mucho más costosa. (Es posible que sepa que C # admite la sobrecarga del operador, pero esto no es lo mismo. La sobrecarga del operador se resuelve en el momento de la compilación, por lo que no existe una penalización de tiempo de ejecución predeterminada).

Las cadenas no son primitivas, pero siguen siendo "especiales" en cómo se representan en la memoria. Por ejemplo, están "internados", lo que significa que dos cadenas literales que son iguales se pueden optimizar con la misma referencia. Esto no sería posible (o al menos mucho menos efectivo) si las instancias de cadena también hicieran un seguimiento de la clase.

Lo que describa sin duda sería útil, pero su soporte requeriría una sobrecarga de rendimiento para cada uso de primitivas y cadenas, incluso cuando no aprovechan la herencia.

El lenguaje que Smalltalk permite (creo) permite la subclasificación de enteros. Pero cuando se diseñó Java, Smalltalk se consideró demasiado lento, y la sobrecarga de tener todo como objeto se consideró una de las principales razones. Java sacrificó algo de elegancia y pureza conceptual para obtener un mejor rendimiento.

JacquesB
fuente
12
@Den: stringestá sellado porque está diseñado para comportarse inmutable. Si uno pudiera heredar de una cadena, sería posible crear cadenas mutables, lo que lo haría realmente propenso a errores. Toneladas de código, incluido el propio framework .NET, se basan en cadenas que no tienen efectos secundarios. Vea también aquí, le dice lo mismo: quora.com/Why-String-class-in-C-is-a-sealed-class
Doc Brown
55
@DocBrown Esta también es la razón por la que Stringestá marcada finalen Java también.
Dev
47
"Cuando se diseñó Java, Smalltalk se consideraba demasiado lento [...]. Java sacrificó algo de elegancia y pureza conceptual para obtener un mejor rendimiento". - Irónicamente, por supuesto, Java en realidad no obtuvo ese rendimiento hasta que Sun compró una compañía Smalltalk para obtener acceso a la tecnología Smalltalk VM porque la propia JVM de Sun era lenta y lanzó el HotSpot JVM, una VM Smalltalk ligeramente modificada.
Jörg W Mittag
3
@underscore_d: La respuesta a la que se vinculó explícitamente indica que C♯ no tiene tipos primitivos. Claro, alguna plataforma para la cual existe una implementación de C♯ puede o no tener tipos primitivos, pero eso no significa que C♯ tenga tipos primitivos. Por ejemplo, hay una implementación de Ruby para la CLI, y la CLI tiene tipos primitivos, pero eso no significa que Ruby tenga tipos primitivos. La implementación puede o no elegir implementar tipos de valor asignándolos a los tipos primitivos de la plataforma, pero eso es un detalle de implementación interna privado y no parte de la especificación.
Jörg W Mittag
10
Se trata de abstracción. Tenemos que mantener la cabeza despejada, de lo contrario terminamos sin sentido. Por ejemplo: C♯ se implementa en .NET. .NET se implementa en Windows NT. Windows NT se implementa en x86. x86 se implementa en dióxido de silicona. SiO₂ es solo arena. Entonces, ¿una stringen C♯ es solo arena? No, por supuesto que no, a stringen C♯ es lo que dice la especificación C♯. Cómo se implementa es irrelevante. Una implementación nativa de C♯ implementaría cadenas como conjuntos de bytes, una implementación de ECMAScript Stringlos asignaría a ECMAScript , etc.
Jörg W Mittag
20

Lo que propone un lenguaje no es la subclase, sino el subtipo . Por ejemplo, Ada le permite crear tipos o subtipos derivados . El Ada Programación del sistema / Tipo sección vale la pena leer para entender todos los detalles. Puede restringir el rango de valores, que es lo que desea la mayor parte del tiempo:

 type Angle is range -10 .. 10;
 type Hours is range 0 .. 23; 

Puede usar ambos tipos como enteros si los convierte explícitamente. Tenga en cuenta también que no puede usar uno en lugar de otro, incluso cuando los rangos son estructuralmente equivalentes (los tipos se verifican por nombres).

 type Reference is Integer;
 type Count is Integer;

Los tipos anteriores son incompatibles, aunque representan el mismo rango de valores.

(Pero puedes usar Unchecked_Conversion; no le digas a la gente que te dije eso)

volcado de memoria
fuente
2
En realidad, creo que se trata más de semántica. Usar una cantidad donde se espera un índice podría causar un error de tiempo de compilación
Marjan Venema
@MarjanVenema Sí, y esto se hace a propósito para detectar errores lógicos.
coredump
Mi punto era que no todos los casos en los que desea la semántica, necesitaría los rangos. ¿Entonces tendrías type Index is -MAXINT..MAXINT;que de alguna manera no hace nada por mí ya que todos los enteros serían válidos? Entonces, ¿qué tipo de error obtendría al pasar un ángulo a un índice si todo lo que está marcado son los rangos?
Marjan Venema
1
@MarjanVenema En su segundo ejemplo, ambos tipos son subtipos de Integer. Sin embargo, si declara una función que acepta un Conteo, no puede pasar una Referencia porque la verificación de tipo se basa en la equivalencia de nombre , que es lo contrario de "todo lo que se verifica son los rangos". Esto no se limita a los enteros, puede usar tipos o registros enumerados. ( archive.adaic.com/standards/83rat/html/ratl-04-03.html )
coredump
1
@Marjan Un buen ejemplo de por qué los tipos de etiquetado pueden ser bastante potentes se puede encontrar en la serie de Eric Lippert sobre la implementación de Zorg en OCaml . Hacer esto permite que el compilador detecte muchos errores; por otro lado, si permite convertir implícitamente los tipos, esto parece hacer que la función sea inútil ... no tiene sentido semántico poder asignar un tipo PersonAge a un tipo PersonId solo porque ambos tienen el mismo tipo subyacente.
Voo
16

Creo que esta podría ser una pregunta X / Y. Puntos sobresalientes, de la pregunta ...

Mi motivación es maximizar el uso del sistema de tipos para la verificación de corrección en tiempo de compilación.

... y de tu comentario elaborando:

No quiero poder sustituir uno por otro implícitamente.

Disculpe si me falta algo, pero ... Si estos son sus objetivos, ¿por qué demonios habla de herencia? La sustituibilidad implícita es ... como ... todo. ¿Sabes, el principio de sustitución de Liskov?

Lo que parece querer, en realidad, es el concepto de una "definición de tipo fuerte", en la que algo "es", por ejemplo, un inten términos de rango y representación, pero no puede ser sustituido en contextos que esperen an inty viceversa. Sugeriría buscar información sobre este término y cualquiera que sea el idioma o idiomas elegidos. De nuevo, es prácticamente lo opuesto a la herencia.

Y para aquellos a quienes no les guste una respuesta X / Y, creo que el título aún podría responder con referencia al LSP. Los tipos primitivos son primitivos porque hacen algo muy simple, y eso es todo lo que hacen . Permitir que sean heredados y así hacer infinitos sus posibles efectos conduciría a una gran sorpresa en el mejor de los casos y una violación fatal de LSP en el peor. Si puedo suponer optimistamente que a Thales Pereira no le importará citar este fenomenal comentario:

Existe el problema adicional de que si alguien pudiera heredar de Int, tendría un código inocente como "int x = y + 2" (donde Y es la clase derivada) que ahora escribe un registro en la Base de datos, abre una URL y de alguna manera resucitar a Elvis. Se supone que los tipos primitivos son seguros y con un comportamiento más o menos garantizado y bien definido.

Si alguien ve un tipo primitivo, en un lenguaje sensato, presume con razón que siempre hará su pequeña cosa, muy bien, sin sorpresas. Los tipos primitivos no tienen declaraciones de clase disponibles que indiquen si pueden o no heredarse y si se anulan sus métodos. Si lo fueran, sería realmente sorprendente (y rompería totalmente la compatibilidad con versiones anteriores, pero sé que esa es una respuesta inversa a 'por qué X no se diseñó con Y').

... aunque, como señaló Mooing Duck en respuesta, los lenguajes que permiten la sobrecarga del operador permiten al usuario confundirse de manera similar o igual si realmente lo desean, por lo que es dudoso si este último argumento es válido. Y dejaré de resumir los comentarios de otras personas ahora, je.

subrayado_d
fuente
4

Para permitir la herencia con el despacho virtual 8, que a menudo se considera bastante deseable en el diseño de aplicaciones, se necesita información del tipo de tiempo de ejecución. Para cada objeto, se deben almacenar algunos datos sobre el tipo de objeto. Un primitivo, por definición, carece de esta información.

Hay dos lenguajes OOP principales (administrados, ejecutados en una VM) que presentan primitivas: C # y Java. Muchos otros idiomas no tienen primitivas en primer lugar, o usan un razonamiento similar para permitirlos / usarlos.

Las primitivas son un compromiso para el rendimiento. Para cada objeto, necesita espacio para su encabezado de objeto (en Java, generalmente 2 * 8 bytes en máquinas virtuales de 64 bits), más sus campos, más relleno eventual (en el punto de acceso, cada objeto ocupa un número de bytes que es un múltiplo de 8) Por lo tanto, un intobjeto como necesitaría al menos 24 bytes de memoria para mantener, en lugar de solo 4 bytes (en Java).

Por lo tanto, se agregaron tipos primitivos para mejorar el rendimiento. Hacen muchas cosas más fáciles. ¿Qué a + bsignifica si ambos son subtipos de int? Se debe agregar algún tipo de desprecio para elegir la adición correcta. Esto significa despacho virtual. Tener la capacidad de usar un código de operación muy simple para la adición es mucho, mucho más rápido y permite optimizaciones en tiempo de compilación.

StringEs otro caso. Tanto en Java como en C #, Stringes un objeto. Pero en C # está sellado, y en Java es final. Esto se debe a que las bibliotecas estándar Java y C # requieren que Strings sea inmutable, y su subclasificación rompería esta inmutabilidad.

En el caso de Java, la VM puede (y lo hace) internar cadenas y "agruparlas", lo que permite un mejor rendimiento. Esto solo funciona cuando las cadenas son realmente inmutables.

Además, uno rara vez necesita subclasificar los tipos primitivos. Mientras las primitivas no puedan subclasificarse, hay muchas cosas buenas que las matemáticas nos dicen sobre ellas. Por ejemplo, podemos estar seguros de que la suma es conmutativa y asociativa. Eso es algo que la definición matemática de enteros nos dice. Además, en muchos casos, podemos realizar fácilmente invariantes profilácticos sobre bucles mediante inducción. Si permitimos la subclasificación de int, perdemos las herramientas que las matemáticas nos dan, porque ya no podemos garantizar que ciertas propiedades se mantengan. Por lo tanto, diría que la capacidad de no poder subclasificar los tipos primitivos es realmente algo bueno. Menos cosas que alguien puede romper, además de un compilador a menudo puede demostrar que se le permite hacer ciertas optimizaciones.

Poligoma
fuente
1
Esta respuesta es abys ... estrecha. to allow inheritance, one needs runtime type information.Falso. For every object, some data regarding the type of the object has to be stored.Falso. There are two mainstream OOP languages that feature primitives: C# and Java.¿Qué, C ++ no es corriente ahora? Lo usaré como mi refutación ya que la información de tipo de tiempo de ejecución es un término de C ++. No es absolutamente necesario a menos que use dynamic_casto typeid. Y aun cuando está encendido, la herencia sólo consume espacio si una clase tiene RTTI virtualmétodos a los que una tabla por cada clase de métodos hay que señalar por ejemplo
underscore_d
1
La herencia en C ++ funciona de manera muy diferente que en los lenguajes que se ejecutan en una VM. el despacho virtual requiere RTTI, algo que originalmente no era parte de C ++. La herencia sin envío virtual es muy limitada y ni siquiera estoy seguro de si debe compararlo con la herencia con envío virtual. Además, la noción de un "objeto" es muy diferente en C ++ que en C # o Java. Tienes razón, hay algunas cosas que podría decir mejor, pero entrar en todos los puntos bastante involucrados rápidamente lleva a tener que escribir un libro sobre diseño de idiomas.
Polygnome
3
Además, no es el caso que "el despacho virtual requiera RTTI" en C ++. De nuevo, solo dynamic_casty typeinforequiere eso. El despacho virtual se implementa prácticamente usando un puntero a la tabla vtable para la clase concreta del objeto, permitiendo así que se invoquen las funciones correctas, pero no requiere el detalle de tipo y relación inherente en RTTI. Todo lo que el compilador necesita saber es si la clase de un objeto es polimórfica y, de ser así, cuál es el vptr de la instancia. Uno puede compilar trivialmente clases virtualmente despachadas con -fno-rtti.
underscore_d
2
De hecho, es al revés, RTTI requiere un despacho virtual. Literalmente -C ++ no permite dynamic_castclases sin envío virtual. La razón de implementación es que RTTI generalmente se implementa como un miembro oculto de una vtable.
MSalters
1
@MilesRout C ++ tiene todo lo que un lenguaje necesita para OOP, al menos los estándares algo más nuevos. Se podría argumentar que los estándares más antiguos de C ++ carecen de algunas cosas que se necesitan para un lenguaje OOP, pero incluso eso es una exageración. C ++ no es un lenguaje OOP de alto nivel , ya que permite un control más directo y de bajo nivel sobre algunas cosas, pero de todos modos permite OOP. (Nivel alto / Nivel bajo aquí en términos de abstracción , otro lenguaje como los gestionados abstraen más del sistema que C ++, por lo tanto, su abstracción es mayor).
Polygnome
4

En los principales lenguajes estáticos fuertes de OOP, el subtipado se ve principalmente como una forma de extender un tipo y anular los métodos actuales del tipo.

Para hacerlo, los 'objetos' contienen un puntero a su tipo. Esto es una sobrecarga: el código en un método que usa una Shapeinstancia primero tiene que acceder a la información de tipo de esa instancia, antes de conocer el Area()método correcto para llamar.

Una primitiva tiende a permitir solo operaciones que pueden traducirse en instrucciones de lenguaje de máquina único y no llevar ningún tipo de información con ellas. Hacer que un número entero sea más lento para que alguien pueda subclasificarlo no fue lo suficientemente atractivo como para evitar que los idiomas que se convirtieron en la corriente principal.

Entonces la respuesta a:

¿Por qué los lenguajes de POO estáticos fuertes convencionales evitan heredar primitivas?

Es:

  • Hubo poca demanda
  • Y habría hecho el lenguaje demasiado lento
  • El subtipo se vio principalmente como una forma de extender un tipo, en lugar de una forma de mejorar la verificación de tipos estáticos (definidos por el usuario).

Sin embargo, estamos comenzando a obtener lenguajes que permiten la comprobación estática basada en propiedades de variables distintas de 'tipo', por ejemplo, F # tiene "dimensión" y "unidad" para que no pueda, por ejemplo, agregar una longitud a un área .

También hay idiomas que permiten 'tipos definidos por el usuario' que no cambian (o intercambian) lo que hace un tipo, sino que solo ayudan con la verificación de tipos estáticos; Ver la respuesta de Coredump.

Ian
fuente
Las unidades de medida F # son una buena característica, aunque desafortunadamente están mal nombradas. También es solo en tiempo de compilación, por lo que no es súper útil, por ejemplo, cuando se consume un paquete NuGet compilado. Sin embargo, la dirección correcta.
Den
Quizás sea interesante notar que "dimensión" no es "una propiedad que no sea 'tipo'", es simplemente un tipo de tipo más rico de lo que usted está acostumbrado.
porglezomp
3

No estoy seguro si estoy pasando por alto algo aquí, pero la respuesta es bastante simple:

  1. La definición de primitivas es: los valores primitivos no son objetos, los tipos primitivos no son tipos de objetos, las primitivas no son parte del sistema de objetos.
  2. La herencia es una característica del sistema de objetos.
  3. Ergo, los primitivos no pueden participar en la herencia.

Tenga en cuenta que en realidad solo hay dos lenguajes estáticos fuertes de OOP que incluso tienen primitivos, AFAIK: Java y C ++. (En realidad, ni siquiera estoy seguro de esto último, no sé mucho acerca de C ++, y lo que encontré al buscar fue confuso).

En C ++, las primitivas son básicamente un legado heredado (juego de palabras) de C. Por lo tanto, no participan en el sistema de objetos (y, por lo tanto, la herencia) porque C no tiene ni un sistema de objetos ni una herencia.

En Java, las primitivas son el resultado de un intento equivocado de mejorar el rendimiento. Las primitivas también son los únicos tipos de valores en el sistema, de hecho, es imposible escribir tipos de valores en Java, y es imposible que los objetos sean tipos de valores. Entonces, aparte del hecho de que los primitivos no participan en el sistema de objetos y, por lo tanto, la idea de "herencia" ni siquiera tiene sentido, incluso si pudieras heredar de ellos, no serías capaz de mantener el " valor ". Esto es diferente de por ejemplo C♯ que hace tener tipos (valor de structs), que sin embargo son objetos.

Otra cosa es que no poder heredar tampoco es exclusivo de las primitivas. En C♯, structs hereda implícitamente System.Objecty puede implementar interfaces, pero no pueden heredar ni heredar de classes o structs. Además, sealed classes no se puede heredar de. En Java, final classes no se puede heredar de.

tl; dr :

¿Por qué los lenguajes de POO estáticos fuertes convencionales evitan heredar primitivas?

  1. las primitivas no son parte del sistema de objetos (por definición, si lo fueran, no serían primitivas), la idea de herencia está ligada al sistema de objetos, la herencia primitiva ergo es una contradicción en términos
  2. las primitivas no son únicas, muchos otros tipos no pueden heredarse también ( finalo sealeden Java o C♯, structs en C♯, case classes en Scala)
Jörg W Mittag
fuente
3
Ehm ... Sé que se pronuncia "C Sharp", pero, ehm
Sr. Lister
Creo que estás bastante equivocado en el lado de C ++. No es un lenguaje OO puro en absoluto. Los métodos de clase por defecto no lo son virtual, lo que significa que no obedecen a LSP. Por ejemplo, std::stringno es primitivo, pero se comporta como un valor más. Tal semántica de valores es bastante común, toda la parte STL de C ++ lo asume.
MSalters
2
"En Java, las primitivas son el resultado de un intento equivocado de mejorar el rendimiento". Creo que no tiene idea de la magnitud del impacto en el rendimiento de implementar primitivas como tipos de objetos expandibles por el usuario. Esa decisión en Java es deliberada y bien fundada. Solo imagine tener que asignar memoria para cada intuso. Cada asignación toma el orden de 100ns más la sobrecarga de recolección de basura. Compare eso con el ciclo de CPU único consumido al agregar dos ints primitivas . Sus códigos java se arrastrarían si los diseñadores del lenguaje hubieran decidido lo contrario.
cmaster
1
@cmaster: Scala no tiene primitivas, y su rendimiento numérico es exactamente el mismo que el de Java. Porque, bueno, compila enteros en primitivos JVM int, por lo que realizan exactamente lo mismo. (Scala-native los compila en registros de máquina primitivos, Scala.js los compila en primitivos ECMAScript Numbers.) Ruby no tiene primitivos, pero YARV y Rubinius compilan enteros en enteros de máquinas primitivas, JRuby los compila en primitivos de JVM long. Casi todas las implementaciones de Lisp, Smalltalk o Ruby usan primitivas en la VM . Ahí es donde las optimizaciones de rendimiento ...
Jörg W Mittag
1
… Pertenecer: en el compilador, no en el idioma.
Jörg W Mittag
2

Joshua Bloch en "Java eficaz" recomienda diseñar explícitamente para la herencia o prohibirla. Las clases primitivas no están diseñadas para la herencia porque están diseñadas para ser inmutables y permitir la herencia podría cambiar eso en las subclases, por lo tanto, romper el principio de Liskov y sería una fuente de muchos errores.

De todos modos, ¿por qué es esta una solución alternativa? Realmente deberías preferir la composición sobre la herencia. Si la razón es el rendimiento, entonces tiene un punto y la respuesta a su pregunta es que no es posible poner todas las características en Java porque lleva tiempo analizar todos los diferentes aspectos de agregar una característica. Por ejemplo, Java no tenía genéricos antes de 1.5.

Si tiene mucha paciencia, entonces tiene suerte porque hay un plan para agregar clases de valor a Java que le permitirá crear sus clases de valor que lo ayudarán a aumentar el rendimiento y al mismo tiempo le dará más flexibilidad.

CodesInTheDark
fuente
2

En el nivel abstracto, puede incluir lo que quiera en un idioma que esté diseñando.

En el nivel de implementación, es inevitable que algunas de esas cosas sean más simples de implementar, algunas serán complicadas, algunas se pueden hacer rápido, algunas serán más lentas, etc. Para dar cuenta de esto, los diseñadores a menudo tienen que tomar decisiones difíciles y compromisos.

A nivel de implementación, una de las formas más rápidas que hemos encontrado para acceder a una variable es encontrar su dirección y cargar el contenido de esa dirección. Hay instrucciones específicas en la mayoría de las CPU para cargar datos desde direcciones y esas instrucciones generalmente necesitan saber cuántos bytes necesitan cargar (uno, dos, cuatro, ocho, etc.) y dónde colocar los datos que cargan (registro único, registro par, registro extendido, otra memoria, etc.). Al conocer el tamaño de una variable, el compilador puede saber exactamente qué instrucción emitir para los usos de esa variable. Al no conocer el tamaño de una variable, el compilador necesitaría recurrir a algo más complicado y probablemente más lento.

En el nivel abstracto, el punto de subtipo es poder usar instancias de un tipo donde se espera un tipo igual o más general. En otras palabras, se puede escribir un código que espere un objeto de un tipo particular o algo más derivado, sin saber de antemano qué sería exactamente esto. Y claramente, como más tipos derivados pueden agregar más miembros de datos, un tipo derivado no necesariamente tiene los mismos requisitos de memoria que sus tipos base.

En el nivel de implementación, no hay una manera simple para que una variable de un tamaño predeterminado contenga una instancia de tamaño desconocido y se acceda de una manera que normalmente llamaría eficiente. Pero hay una manera de mover un poco las cosas y usar una variable no para almacenar el objeto, sino para identificar el objeto y dejar que ese objeto se almacene en otro lugar. Esa es una referencia (por ejemplo, una dirección de memoria): un nivel adicional de indirección que asegura que una variable solo necesita contener algún tipo de información de tamaño fijo, siempre que podamos encontrar el objeto a través de esa información. Para lograr eso, solo tenemos que cargar la dirección (tamaño fijo) y luego podemos trabajar como de costumbre usando los desplazamientos del objeto que sabemos que son válidos, incluso si ese objeto tiene más datos en los desplazamientos que no conocemos. Podemos hacer eso porque no lo hacemos

En el nivel abstracto, este método le permite almacenar una (referencia a) stringen una objectvariable sin perder la información que la convierte en a string. Está bien que todos los tipos trabajen así y también se podría decir que es elegante en muchos aspectos.

Aún así, en el nivel de implementación, el nivel adicional de indirección implica más instrucciones y en la mayoría de las arquitecturas hace que cada acceso al objeto sea algo más lento. Puede permitir que el compilador exprima más rendimiento de un programa si incluye en su idioma algunos tipos comúnmente utilizados que no tienen ese nivel adicional de indirección (la referencia). Pero al eliminar ese nivel de indirección, el compilador ya no puede permitirle subtipear de forma segura en la memoria. Esto se debe a que si agrega más miembros de datos a su tipo y lo asigna a un tipo más general, todos los miembros de datos adicionales que no quepan en el espacio asignado para la variable de destino se cortarán.

Theodoros Chatzigiannakis
fuente
1

En general

Si una clase es abstracta (metáfora: una caja con agujeros), está bien (¡incluso se requiere tener algo utilizable!) Para "llenar los agujeros", por eso subclasificamos las clases abstractas.

Si una clase es concreta (metáfora: una caja llena), no está bien alterar la existente porque si está llena, está llena. No tenemos espacio para agregar algo más dentro de la caja, por eso no deberíamos subclasificar las clases concretas.

Con primitivas

Las primitivas son clases concretas por diseño. Representan algo que es bien conocido, completamente definido (nunca he visto un tipo primitivo con algo abstracto, de lo contrario ya no es un primitivo) y ampliamente utilizado a través del sistema. ¡Permitir subclasificar un tipo primitivo y proporcionar su propia implementación a otros que confían en el comportamiento diseñado de los primitivos puede causar muchos efectos secundarios y daños enormes!

Manchado
fuente
El enlace es una opinión de diseño interesante. Necesita más pensar para mí.
Den
1

Por lo general, la herencia no es la semántica que desea, porque no puede sustituir su tipo especial en cualquier lugar donde se espere una primitiva. Para tomar prestado de su ejemplo, a Quantity + Indexno tiene sentido semánticamente, por lo que una relación de herencia es la relación incorrecta.

Sin embargo, varios idiomas tienen el concepto de un tipo de valor que expresa el tipo de relación que está describiendo. Scala es un ejemplo. Un tipo de valor utiliza una primitiva como representación subyacente, pero tiene una identidad de clase y operaciones diferentes en el exterior. Eso tiene el efecto de extender un tipo primitivo, pero es más una composición que una relación de herencia.

Karl Bielefeldt
fuente