¿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.
type-systems
static-typing
strong-typing
Guarida
fuente
fuente
Respuestas:
¿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.
fuente
string
está 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-classString
está marcadafinal
en Java también.string
en C♯ es solo arena? No, por supuesto que no, astring
en 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 ECMAScriptString
los asignaría a ECMAScript , etc.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:
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).
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)
fuente
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?Creo que esta podría ser una pregunta X / Y. Puntos sobresalientes, de la pregunta ...
... y de tu comentario elaborando:
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
int
en términos de rango y representación, pero no puede ser sustituido en contextos que esperen anint
y 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:
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.
fuente
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
int
objeto 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 + b
significa si ambos son subtipos deint
? 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.String
Es otro caso. Tanto en Java como en C #,String
es 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 queString
s 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.fuente
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 usedynamic_cast
otypeid
. Y aun cuando está encendido, la herencia sólo consume espacio si una clase tiene RTTIvirtual
métodos a los que una tabla por cada clase de métodos hay que señalar por ejemplodynamic_cast
ytypeinfo
requiere 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
.dynamic_cast
clases sin envío virtual. La razón de implementación es que RTTI generalmente se implementa como un miembro oculto de una vtable.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
Shape
instancia primero tiene que acceder a la información de tipo de esa instancia, antes de conocer elArea()
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:
Es:
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.
fuente
No estoy seguro si estoy pasando por alto algo aquí, pero la respuesta es bastante simple:
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
struct
s), que sin embargo son objetos.Otra cosa es que no poder heredar tampoco es exclusivo de las primitivas. En C♯,
struct
s hereda implícitamenteSystem.Object
y puede implementarinterface
s, pero no pueden heredar ni heredar declass
es ostruct
s. Además,sealed
class
es no se puede heredar de. En Java,final
class
es no se puede heredar de.tl; dr :
final
osealed
en Java o C♯,struct
s en C♯,case class
es en Scala)fuente
virtual
, lo que significa que no obedecen a LSP. Por ejemplo,std::string
no 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.int
uso. 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 dosint
s primitivas . Sus códigos java se arrastrarían si los diseñadores del lenguaje hubieran decidido lo contrario.int
, por lo que realizan exactamente lo mismo. (Scala-native los compila en registros de máquina primitivos, Scala.js los compila en primitivos ECMAScriptNumber
s.) Ruby no tiene primitivos, pero YARV y Rubinius compilan enteros en enteros de máquinas primitivas, JRuby los compila en primitivos de JVMlong
. Casi todas las implementaciones de Lisp, Smalltalk o Ruby usan primitivas en la VM . Ahí es donde las optimizaciones de rendimiento ...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.
fuente
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)
string
en unaobject
variable sin perder la información que la convierte en astring
. 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.
fuente
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!
fuente
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 + Index
no 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.
fuente