Por que as principais linguagens OOP estáticas fortes impedem a herança de primitivas?

53

Por que isso está correto e principalmente esperado:

abstract type Shape
{
   abstract number Area();
}

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

... enquanto isso não está bom e ninguém reclama:

concrete type Name : string
{
}

concrete type Index : int
{
}

concrete type Quantity : int
{
}

Minha motivação é maximizar o uso do sistema de tipos para verificação da correção em tempo de compilação.

PS: sim, eu li isso e o empacotamento é uma solução alternativa.

Den
fonte
11
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
Maple_shaft
Eu tinha uma motivação semelhante nessa pergunta , você pode achar interessante.
precisa
Eu adicionaria uma resposta confirmando a idéia "você não quer herança" e que o empacotamento é muito poderoso, incluindo o que você deseja da conversão implícita ou explícita (ou falhas) que deseja, especialmente com as otimizações do JIT sugerindo que você obtenha quase o mesmo desempenho de qualquer maneira, mas você vinculou a essa resposta :-) Gostaria de acrescentar que seria bom que os idiomas adicionassem recursos para reduzir o código padrão necessário para o encaminhamento de propriedades / métodos, especialmente se houver apenas um valor.
Mark Hurd

Respostas:

82

Suponho que você esteja pensando em linguagens como Java e C #?

Nessas linguagens, as primitivas (como int) são basicamente um compromisso para o desempenho. Eles não suportam todos os recursos dos objetos, mas são mais rápidos e com menos sobrecarga.

Para que os objetos suportem herança, cada instância precisa "saber", em tempo de execução, de qual classe é uma instância. Caso contrário, os métodos substituídos não poderão ser resolvidos no tempo de execução. Para objetos, isso significa que os dados da instância são armazenados na memória junto com um ponteiro para o objeto de classe. Se essas informações também devem ser armazenadas juntamente com valores primitivos, os requisitos de memória aumentariam. Um valor inteiro de 16 bits exigiria 16 bits para o valor e adicionalmente 32 ou 64 bits de memória para um ponteiro para sua classe.

Além da sobrecarga de memória, você também esperaria substituir operações comuns em primitivas, como operadores aritméticos. Sem subtipagem, operadores como +podem ser compilados em uma simples instrução de código de máquina. Se pudesse ser substituído, você precisaria resolver métodos em tempo de execução, uma operação muito mais cara. (Você pode saber que o C # suporta sobrecarga do operador - mas isso não é o mesmo. A sobrecarga do operador é resolvida no tempo de compilação, portanto, não há penalidade de tempo de execução padrão.)

Strings não são primitivas, mas ainda são "especiais" na forma como são representadas na memória. Por exemplo, eles são "internados", o que significa que duas cadeias literais iguais podem ser otimizadas para a mesma referência. Isso não seria possível (ou pelo menos muito menos eficaz) se as instâncias de string também controlassem a classe.

O que você descreve seria certamente útil, mas suportá-lo exigiria uma sobrecarga de desempenho para todo uso de primitivas e cadeias, mesmo quando elas não tiram vantagem da herança.

A linguagem Smalltalk faz (eu acredito) permitem subclasse de inteiros. Porém, quando o Java foi projetado, o Smalltalk era considerado muito lento, e a sobrecarga de fazer com que tudo fosse um objeto era considerada uma das principais razões. Java sacrificou alguma elegância e pureza conceitual para obter melhor desempenho.

JacquesB
fonte
12
@ Den: stringé selado porque foi projetado para se comportar imutável. Se alguém pudesse herdar da string, seria possível criar strings mutáveis, o que tornaria muito propenso a erros. Toneladas de código, incluindo a própria estrutura .NET, dependem de seqüências de caracteres sem efeitos colaterais. Veja também aqui, diz o mesmo: quora.com/Why-String-class-in-C-is-a-sealed-class
Doc Brown
5
@DocBrown Este também é o motivo que Stringestá marcado finalem Java também.
Dev
47
"quando o Java foi projetado, o Smalltalk foi considerado muito lento [...]. O Java sacrificou alguma elegância e pureza conceitual para obter melhor desempenho". - Ironicamente, é claro, o Java não obteve esse desempenho até que a Sun comprou uma empresa Smalltalk para obter acesso à tecnologia Smalltalk VM, porque a JVM da Sun era muito lenta e lançou o HotSpot JVM, uma VM Smalltalk ligeiramente modificada.
Jörg W Mittag
3
@underscore_d: A resposta à qual você vinculou muito explicitamente afirma que C♯ não possui tipos primitivos. Certamente, alguma plataforma para a qual existe uma implementação de C♯ pode ou não ter tipos primitivos, mas isso não significa que C♯ tenha tipos primitivos. Por exemplo, existe uma implementação do Ruby para a CLI, e a CLI possui tipos primitivos, mas isso não significa que o Ruby tenha tipos primitivos. A implementação pode ou não optar por implementar tipos de valor, mapeando-os para os tipos primitivos da plataforma, mas esse é um detalhe interno privado da implementação e não faz parte da especificação.
Jörg W Mittag
10
É tudo sobre abstração. Temos que manter a cabeça limpa, caso contrário, acabamos com bobagens. Por exemplo: C♯ é implementado no .NET. .NET é implementado no Windows NT. Windows NT é implementado em x86. O x86 é implementado em dióxido de silicone. SiO₂ é apenas areia. Então, um stringem C♯ é apenas areia? Não, claro que não, a stringem C♯ é o que a especificação C♯ diz que é. Como é implementado é irrelevante. Uma implementação nativa de C♯ poderia implementar cordas como matrizes de bytes, uma implementação ECMAScript iria mapear os a ECMAScript Strings, etc
Jörg W Mittag
20

O que alguns idiomas propõem não é subclassificação, mas subtipagem . Por exemplo, o Ada permite criar tipos ou subtipos derivados . A seção Ada Programming / Type System vale a pena ler para entender todos os detalhes. Você pode restringir o intervalo de valores, que é o que você deseja na maioria das vezes:

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

Você pode usar os dois tipos como números inteiros se os converter explicitamente. Observe também que você não pode usar um no lugar do outro, mesmo quando os intervalos são estruturalmente equivalentes (os tipos são verificados por nomes).

 type Reference is Integer;
 type Count is Integer;

Os tipos acima são incompatíveis, mesmo que representem o mesmo intervalo de valores.

(Mas você pode usar Unchecked_Conversion; não conte às pessoas que eu contei isso)

coredump
fonte
2
Na verdade, acho que é mais sobre semântica. Usando uma quantidade onde se espera um índice, então, esperamos causar um erro de tempo de compilação
Marjan Venema
@MarjanVenema Sim, e isso é feito de propósito para detectar erros de lógica.
Coredump10
Meu argumento era que nem todos os casos em que você deseja a semântica, precisaria dos intervalos. Você teria o type Index is -MAXINT..MAXINT;que de alguma forma não faz nada por mim, pois todos os números inteiros seriam válidos? Então, que tipo de erro eu obteria ao passar um ângulo para um índice se tudo o que estiver marcado são os intervalos?
Marjan Venema
11
@MarjanVenema No segundo exemplo, ambos os tipos são subtipos de número inteiro. No entanto, se você declarar uma função que aceita uma Contagem, não poderá passar uma Referência porque a verificação de tipo se baseia na equivalência de nomes , que é o contrário de "tudo o que é verificado são os intervalos". Isso não está limitado a números inteiros, você pode usar tipos ou registros enumerados. ( archive.adaic.com/standards/83rat/html/ratl-04-03.html )
coredump
11
@Marjan Um bom exemplo de por que os tipos de tags podem ser bastante poderosos pode ser encontrado na série de Eric Lippert sobre a implementação do Zorg no OCaml . Isso permite ao compilador detectar muitos bugs - por outro lado, se você permitir converter tipos implicitamente, isso parece tornar o recurso inútil. Não faz sentido semântico poder atribuir um tipo PersonAge a um tipo PersonId. porque ambos têm o mesmo tipo subjacente.
Voo
16

Eu acho que isso pode muito bem ser uma pergunta X / Y. Pontos salientes, a partir da pergunta ...

Minha motivação é maximizar o uso do sistema de tipos para verificação da correção em tempo de compilação.

... e do seu comentário elaborando:

Não quero poder substituir um por outro implicitamente.

Com licença, se estou perdendo alguma coisa, mas ... Se esses são seus objetivos, por que diabos você está falando sobre herança? A substituibilidade implícita é ... como ... a coisa toda. Você sabe, o princípio da substituição de Liskov?

O que você parece querer, na realidade, é o conceito de 'forte typedef' - pelo qual algo 'é', por exemplo, um intem termos de alcance e representação, mas não pode ser substituído em contextos que esperam um inte vice-versa. Sugiro procurar informações sobre esse termo e como o idioma escolhido pode chamá-lo. Novamente, é praticamente o oposto de herança.

E para aqueles que podem não gostar de uma resposta X / Y, acho que o título ainda pode ser respondido com referência ao LSP. Tipos primitivos são primitivos porque fazem algo muito simples, e é tudo o que fazem . Permitir que eles sejam herdados e, assim, tornar infinitos seus possíveis efeitos, levaria a uma grande surpresa na melhor das hipóteses e a uma violação fatal do LSP na pior. Se eu puder assumir de maneira otimista, Thales Pereira não se importará de eu citar este comentário fenomenal:

Existe o problema adicional de que, se alguém pudesse herdar do Int, você teria um código inocente como "int x = y + 2" (onde Y é a classe derivada) que agora grava um log no banco de dados, abre uma URL e de alguma forma ressuscitar Elvis. Os tipos primitivos devem ser seguros e com comportamento mais ou menos garantido e bem definido.

Se alguém vê um tipo primitivo, em uma linguagem sã, com razão, presume que sempre fará apenas uma coisinha, muito bem, sem surpresas. Os tipos primitivos não possuem declarações de classe disponíveis que sinalizem se eles podem ou não ser herdados e se seus métodos foram substituídos. Se eles fossem, seria realmente muito surpreendente (e quebraria totalmente a compatibilidade com versões anteriores, mas sei que é uma resposta inversa ao 'por que o X não foi projetado com Y').

... embora, como o Mooing Duck apontou em resposta, as linguagens que permitem a sobrecarga do operador permitam que o usuário se confunda de maneira semelhante ou igual, se realmente quiserem, por isso é duvidoso que esse último argumento seja válido. E eu vou parar de resumir os comentários de outras pessoas agora, heh.

underscore_d
fonte
4

Para permitir a herança com o despacho virtual 8, que geralmente é considerado bastante desejável no design do aplicativo), é necessário obter informações sobre o tipo de tempo de execução. Para cada objeto, alguns dados sobre o tipo do objeto devem ser armazenados. Um primitivo, por definição, carece dessas informações.

Existem duas linguagens OOP mainstream (gerenciadas, executadas em uma VM) que apresentam primitivas: C # e Java. Muitos outros idiomas não têm primitivos em primeiro lugar ou usam raciocínio semelhante para permitir / usá-los.

As primitivas são um compromisso para o desempenho. Para cada objeto, você precisa de espaço para o cabeçalho do objeto (em Java, normalmente 2 * 8 bytes em VMs de 64 bits), além de seus campos, além de preenchimento eventual (no Hotspot, todo objeto ocupa um número de bytes que é múltiplo de 8) Portanto, um intobjeto as precisaria de pelo menos 24 bytes de memória, ao invés de apenas 4 bytes (em Java).

Assim, tipos primitivos foram adicionados para melhorar o desempenho. Eles facilitam muitas coisas. O que a + bsignifica se ambos são subtipos de int? É necessário adicionar algum tipo de desapego para escolher a adição correta. Isso significa expedição virtual. Ter a capacidade de usar um código de operação muito simples para a adição é muito, muito mais rápido e permite otimizações em tempo de compilação.

Stringé outro caso. Tanto em Java quanto em C #, Stringé um objeto. Mas em C # é selado e em Java é final. Isso porque as bibliotecas padrão Java e C # exigem que Strings sejam imutáveis ​​e a subclasse delas quebraria essa imutabilidade.

No caso de Java, a VM pode (e faz) internar Strings e "agrupá-las", permitindo um melhor desempenho. Isso só funciona quando Strings são realmente imutáveis.

Além disso, raramente é necessário subclassificar tipos primitivos. Contanto que os primitivos não possam ser subclassificados, há muitas coisas legais que a matemática nos diz sobre eles. Por exemplo, podemos ter certeza de que a adição é comutativa e associativa. Isso é algo que a definição matemática de números inteiros nos diz. Além disso, podemos facilmente fazer invariantes sobre loops por indução em muitos casos. Se permitirmos a subclassificação de int, perderemos as ferramentas que a matemática nos fornece, porque não podemos mais garantir que certas propriedades sejam mantidas. Assim, eu diria que a capacidade de não ser capaz de subclassificar tipos primitivos é realmente uma coisa boa. Menos coisas que alguém pode quebrar, além de um compilador, muitas vezes podem provar que ele tem permissão para fazer certas otimizações.

Polygnome
fonte
11
Esta resposta é abis ... estreita. 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.O que é C ++ não é mainstream agora? Vou usá-lo como refutação, pois as informações do tipo de tempo de execução são um termo em C ++. Não é absolutamente necessário, a menos que você use dynamic_castou typeid. E mesmo que está ligado, herança só consome espaço se uma classe tem RTTI virtualmétodos para que uma tabela por classe de métodos devem ser apontados por exemplo
underscore_d
11
A herança em C ++ funciona muito diferente das linguagens executadas em uma VM. o despacho virtual requer RTTI, algo que não fazia parte originalmente do C ++. A herança sem despacho virtual é muito limitada e nem tenho certeza se você deve compará-lo à herança com despacho virtual. Além disso, a noção de um "objeto" é muito diferente em C ++ e em C # ou Java. Você está certo, há algumas coisas que eu poderia dizer melhor, mas entrar em todos os pontos envolvidos rapidamente leva a ter que escrever um livro sobre design de linguagem.
Polygnome
3
Além disso, não é o caso de "despacho virtual requer RTTI" em C ++. Mais uma vez, apenas dynamic_caste typeinfoexigir isso. O despacho virtual é praticamente implementado usando um ponteiro para a vtable para a classe concreta do objeto, permitindo assim que as funções corretas sejam chamadas, mas não requer os detalhes do tipo e da relação inerentes ao RTTI. Tudo o que o compilador precisa saber é se a classe de um objeto é polimórfica e, em caso afirmativo, qual é o vptr da instância. Pode-se compilar trivialmente classes virtualmente despachadas com -fno-rtti.
underscore_d
2
Na verdade, é o contrário, o RTTI requer expedição virtual. Literalmente -C ++ não permite dynamic_castclasses sem despacho virtual. O motivo da implementação é que o RTTI geralmente é implementado como um membro oculto de uma vtable.
MSalters
11
O @MilesRout C ++ tem tudo o que uma linguagem precisa para OOP, pelo menos os padrões um pouco mais recentes. Pode-se argumentar que os padrões C ++ mais antigos carecem de algumas coisas necessárias para uma linguagem OOP, mas mesmo isso é exagerado. O C ++ não é uma linguagem OOP de alto nível , pois permite um controle de nível mais direto e baixo sobre algumas coisas, mas, no entanto, permite OOP. (Nível alto / nível baixo aqui em termos de abstração , outras linguagens como as gerenciadas abstraem mais o sistema do que o C ++, portanto a abstração é maior).
Polygnome
4

Nas principais linguagens OOP estáticas fortes, a sub-digitação é vista principalmente como uma maneira de estender um tipo e substituir os métodos atuais do tipo.

Para fazer isso, 'objetos' contêm um ponteiro para seu tipo. Isso é uma sobrecarga: o código em um método que usa uma Shapeinstância primeiro precisa acessar as informações de tipo dessa instância, antes de conhecer o Area()método correto a ser chamado.

Um primitivo tende a permitir apenas operações nele que podem ser traduzidas em instruções de linguagem de máquina única e não carregam nenhuma informação de tipo com eles. Tornar um número inteiro mais lento para que alguém pudesse subclassificar era desagradável o suficiente para impedir que qualquer idioma que o fizesse se tornasse mainstream.

Portanto, a resposta para:

Por que as principais linguagens OOP estáticas fortes impedem a herança de primitivas?

É:

  • Havia pouca demanda
  • E isso tornaria a linguagem muito lenta
  • A subtipagem foi vista principalmente como uma maneira de estender um tipo, em vez de uma maneira de obter uma melhor verificação de tipo estático (definido pelo usuário).

No entanto, estamos começando a obter idiomas que permitem a verificação estática com base em propriedades de variáveis ​​que não sejam 'type', por exemplo, o F # tem "dimension" e "unit" para que você não possa, por exemplo, adicionar um comprimento a uma área .

Também existem idiomas que permitem 'tipos definidos pelo usuário' que não alteram (nem trocam) o que um tipo faz, mas apenas ajudam na verificação estática de tipos; veja a resposta de coredump.

Ian
fonte
As unidades de medida do F # são um recurso interessante, embora, infelizmente, tenham um nome errado. Além disso, é apenas em tempo de compilação, portanto, não é super útil, por exemplo, ao consumir um pacote NuGet compilado. Direção certa, no entanto.
Den
Talvez seja interessante notar que "dimensão" não é "uma propriedade que não seja 'tipo'", é apenas um tipo de tipo mais rico do que você pode estar acostumado.
porglezomp
3

Não tenho certeza se estou ignorando algo aqui, mas a resposta é bastante simples:

  1. A definição de primitivas é: valores primitivos não são objetos, tipos primitivos não são tipos de objetos, primitivos não fazem parte do sistema de objetos.
  2. A herança é um recurso do sistema de objetos.
  3. Portanto, os primitivos não podem participar da herança.

Observe que realmente existem apenas duas linguagens OOP estáticas fortes que possuem até mesmo primitivas, AFAIK: Java e C ++. (Na verdade, nem tenho certeza sobre o último, não sei muito sobre C ++, e o que encontrei ao pesquisar foi confuso.)

No C ++, as primitivas são basicamente um legado herdado (trocadilhos) de C. Portanto, elas não participam do sistema de objetos (e, portanto, herança) porque C não possui um sistema de objetos nem herança.

Em Java, as primitivas são o resultado de uma tentativa equivocada de melhorar o desempenho. Primitivas também são os únicos tipos de valor no sistema, é, de fato, impossível escrever tipos de valor em Java e é impossível que objetos sejam tipos de valor. Portanto, além do fato de que os primitivos não participam do sistema de objetos e, portanto, a idéia de "herança" nem faz sentido, mesmo se você pudesse herdar deles, não seria capaz de manter o " valor ". Isto é diferente do exemplo C♯ que faz têm tipos de valor ( structes), o qual, no entanto, são objectos.

Outra coisa é que não poder herdar também não é exclusivo dos primitivos. Em C♯, structs herdam implicitamente System.Objecte podem implementar interfaces, mas eles não podem herdar nem herdar por classes ou structs. Além disso, sealed classes não podem ser herdados de. Em Java, final classes não podem ser herdados de.

tl; dr :

Por que as principais linguagens OOP estáticas fortes impedem a herança de primitivas?

  1. primitivas não fazem parte do sistema de objetos (por definição, se fossem, não seriam primitivas), a idéia de herança está ligada ao sistema de objetos; portanto, a herança primitiva é uma contradição em termos
  2. primitivas não são exclusivas, muitos outros tipos também não podem ser herdados ( finalou sealedem Java ou C♯, structs em C♯, case classes em Scala)
Jörg W Mittag
fonte
3
Ehm ... eu sei que é pronunciado "C Sharp", mas, ehm
Sr. Lister
Eu acho que você está muito enganado no lado do C ++. Não é uma linguagem OO pura. Os métodos de classe por padrão não são virtual, o que significa que eles não obedecem ao LSP. Por exemplo, std::stringnão é um primitivo, mas se comporta muito como apenas outro valor. Essa semântica de valores é bastante comum, toda a parte STL do C ++ assume isso.
MSalters 11/08/16
2
'Em Java, as primitivas são o resultado de uma tentativa equivocada de melhorar o desempenho.' Acho que você não tem idéia da magnitude do impacto no desempenho da implementação de primitivas como tipos de objetos expansíveis pelo usuário. Essa decisão em java é deliberada e bem fundamentada. Imagine ter que alocar memória para cada intuso. Cada alocação assume a ordem de 100ns mais a sobrecarga da coleta de lixo. Compare isso com o único ciclo de CPU consumido adicionando dois ints primitivos . Seus códigos java rastreariam se os designers da linguagem tivessem decidido o contrário.
precisa saber é
11
@ master: Scala não tem primitivas, e seu desempenho numérico é exatamente o mesmo que o de Java. Porque, bem, ele compila números inteiros em ints primitivos da JVM , para que eles executem exatamente o mesmo. (O Scala-native os compila em registros de máquinas primitivas, o Scala.js os compila em ECMAScript Numbers primitivos .) O Ruby não possui primitivas, mas YARV e Rubinius compilam números inteiros em números primários de máquinas, o JRuby os compila em longs primitivos da JVM . Praticamente todas as implementações de Lisp, Smalltalk ou Ruby usam primitivas na VM . É aí que as otimizações de desempenho ...
Jörg W Mittag
11
… Pertence: no compilador, não no idioma.
Jörg W Mittag
2

Joshua Bloch em “Java Efetivo” recomenda o design explícito para a herança ou a proibição. As classes primitivas não são projetadas para herança porque são projetadas para serem imutáveis ​​e permitir que a herança possa mudar isso nas subclasses, quebrando assim o princípio de Liskov e seria uma fonte de muitos bugs.

De qualquer forma, por que é esta uma solução hacky? Você realmente deve preferir composição a herança. Se o motivo é o desempenho, você tem razão e a resposta para sua pergunta é que não é possível colocar todos os recursos em Java, pois leva tempo para analisar todos os diferentes aspectos da adição de um recurso. Por exemplo, Java não tinha Generics antes da 1.5.

Se você tiver muita paciência, terá sorte, pois existe um plano para adicionar classes de valor ao Java, o que permitirá criar suas classes de valor, o que o ajudará a aumentar o desempenho e, ao mesmo tempo, oferecerá mais flexibilidade.

CodesInTheDark
fonte
2

No nível abstrato, você pode incluir o que quiser em um idioma que estiver criando.

No nível da implementação, é inevitável que algumas dessas coisas sejam mais simples de implementar, algumas serão complicadas, outras podem ser feitas rapidamente, outras são mais lentas e assim por diante. Para explicar isso, os designers geralmente precisam tomar decisões e compromissos difíceis.

No nível da implementação, uma das maneiras mais rápidas de acessar uma variável é descobrir o endereço e carregar o conteúdo desse endereço. Existem instruções específicas na maioria das CPUs para carregar dados de endereços e essas instruções geralmente precisam saber quantos bytes eles precisam carregar (um, dois, quatro, oito, etc.) e onde colocar os dados que eles carregam (registro único, registro par, registro estendido, outra memória etc.). Ao saber o tamanho de uma variável, o compilador pode saber exatamente qual instrução emitir para o uso dessa variável. Por não saber o tamanho de uma variável, o compilador precisaria recorrer a algo mais complicado e provavelmente mais lento.

No nível abstrato, o ponto de subtipagem é poder usar instâncias de um tipo em que é esperado um tipo igual ou mais geral. Em outras palavras, pode ser escrito código que espera um objeto de um tipo específico ou algo mais derivado, sem saber antecipadamente o que exatamente seria isso. E claramente, como mais tipos derivados podem adicionar mais membros de dados, um tipo derivado não necessariamente tem os mesmos requisitos de memória que seus tipos básicos.

No nível da implementação, não há uma maneira simples de uma variável de tamanho predeterminado manter uma instância de tamanho desconhecido e ser acessada da maneira que você normalmente chamaria de eficiente. Mas há uma maneira de mudar um pouco as coisas e usar uma variável não para armazenar o objeto, mas para identificá-lo e permitir que ele seja armazenado em outro lugar. Dessa maneira, é uma referência (por exemplo, um endereço de memória) - um nível extra de indireção que garante que uma variável precise armazenar apenas algum tipo de informação de tamanho fixo, desde que possamos encontrar o objeto através dessa informação. Para conseguir isso, precisamos carregar o endereço (tamanho fixo) e, em seguida, trabalhar como de costume, usando as compensações do objeto que sabemos que são válidas, mesmo que esse objeto tenha mais dados em compensações que não conhecemos. Podemos fazer isso porque não '

No nível abstrato, esse método permite armazenar a (referência a) stringem uma objectvariável sem perder as informações que a tornam uma string. É bom que todos os tipos funcionem assim e você também pode dizer que é elegante em muitos aspectos.

Ainda, no nível de implementação, o nível extra de indireção envolve mais instruções e, na maioria das arquiteturas, torna cada acesso ao objeto um pouco mais lento. Você pode permitir que o compilador reduza mais o desempenho de um programa se incluir no seu idioma alguns tipos comumente usados ​​que não possuem esse nível extra de indireção (a referência). Mas, ao remover esse nível de indireção, o compilador não pode mais permitir que você subtipo de forma segura a memória. Isso ocorre porque, se você adicionar mais membros de dados ao seu tipo e atribuir a um tipo mais geral, quaisquer membros de dados extras que não couberem no espaço alocado para a variável de destino serão cortados.

Theodoros Chatzigiannakis
fonte
1

Em geral

Se uma classe é abstrata (metáfora: uma caixa com furo (s)), não há problema (mesmo que seja necessário ter algo utilizável!) "Preencher o buraco (s)", é por isso que subclassificamos classes abstratas.

Se uma classe é concreta (metáfora: uma caixa cheia), não há problema em alterar a existente porque, se estiver cheia, está cheia. Não temos espaço para adicionar algo mais dentro da caixa, por isso não devemos subclassificar classes concretas.

Com primitivos

Primitivas são classes concretas por design. Eles representam algo que é bem conhecido, totalmente definido (nunca vi um tipo primitivo com algo abstrato, caso contrário, não é mais um primitivo) e amplamente utilizado através do sistema. Permitir subclassificar um tipo primitivo e fornecer sua própria implementação a outras pessoas que dependem do comportamento projetado das primitivas pode causar muitos efeitos colaterais e grandes danos!

Visto
fonte
O link é uma opinião interessante de design. Precisa de mais pensamento para mim.
Den
1

Geralmente, herança não é a semântica que você deseja, porque você não pode substituir seu tipo especial em qualquer lugar em que uma primitiva seja esperada. Para tomar emprestado do seu exemplo, a Quantity + Indexnão faz sentido semanticamente, portanto, um relacionamento de herança é o relacionamento errado.

No entanto, vários idiomas têm o conceito de um tipo de valor que expressa o tipo de relacionamento que você está descrevendo. Scala é um exemplo. Um tipo de valor usa uma primitiva como representação subjacente, mas tem uma identidade de classe e operações diferentes no exterior. Isso tem o efeito de estender um tipo primitivo, mas é mais uma composição do que uma relação de herança.

Karl Bielefeldt
fonte