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.
Respostas:
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.
fonte
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-classString
está marcadofinal
em Java também.string
em C♯ é apenas areia? Não, claro que não, astring
em 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 ECMAScriptString
s, etcO 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:
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).
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)
fonte
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?Eu acho que isso pode muito bem ser uma pergunta X / Y. Pontos salientes, a partir da pergunta ...
... e do seu comentário elaborando:
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
int
em termos de alcance e representação, mas não pode ser substituído em contextos que esperam umint
e 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:
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.
fonte
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
int
objeto 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 + b
significa se ambos são subtipos deint
? É 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 queString
s 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.fonte
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ê usedynamic_cast
outypeid
. E mesmo que está ligado, herança só consome espaço se uma classe tem RTTIvirtual
métodos para que uma tabela por classe de métodos devem ser apontados por exemplodynamic_cast
etypeinfo
exigir 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
.dynamic_cast
classes sem despacho virtual. O motivo da implementação é que o RTTI geralmente é implementado como um membro oculto de uma vtable.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
Shape
instância primeiro precisa acessar as informações de tipo dessa instância, antes de conhecer oArea()
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:
É:
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.
fonte
Não tenho certeza se estou ignorando algo aqui, mas a resposta é bastante simples:
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 (
struct
es), o qual, no entanto, são objectos.Outra coisa é que não poder herdar também não é exclusivo dos primitivos. Em C♯,
struct
s herdam implicitamenteSystem.Object
e podem implementarinterface
s, mas eles não podem herdar nem herdar porclass
es oustruct
s. Além disso,sealed
class
es não podem ser herdados de. Em Java,final
class
es não podem ser herdados de.tl; dr :
final
ousealed
em Java ou C♯,struct
s em C♯,case class
es em Scala)fonte
virtual
, o que significa que eles não obedecem ao LSP. Por exemplo,std::string
nã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.int
uso. 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 doisint
s primitivos . Seus códigos java rastreariam se os designers da linguagem tivessem decidido o contrário.int
s 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 ECMAScriptNumber
s 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 emlong
s primitivos da JVM . Praticamente todas as implementações de Lisp, Smalltalk ou Ruby usam primitivas na VM . É aí que as otimizações de desempenho ...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.
fonte
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)
string
em umaobject
variável sem perder as informações que a tornam umastring
. É 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.
fonte
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!
fonte
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 + Index
nã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.
fonte