Perché i linguaggi OOP statici forti tradizionali impediscono di ereditare le primitive?


53

Perché è OK e principalmente previsto:

abstract type Shape
{
   abstract number Area();
}

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

... mentre questo non è OK e nessuno si lamenta:

concrete type Name : string
{
}

concrete type Index : int
{
}

concrete type Quantity : int
{
}

La mia motivazione è massimizzare l'uso del sistema di tipi per la verifica della correttezza in fase di compilazione.

PS: sì, ho letto questo e il wrapping è un bizzarro rimedio.


1
I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
maple_shaft

Ho avuto una motivazione simile in questa domanda , potresti trovarla interessante.
default.kramer,

Stavo per aggiungere una risposta confermando l'idea "non vuoi ereditare", e che il wrapping è molto potente, incluso darti il ​​cast implicito o esplicito (o fallimenti) che desideri, specialmente con le ottimizzazioni JIT che suggeriscono che ottieni comunque le stesse prestazioni, ma ti sei collegato a quella risposta :-) Aggiungerei, sarebbe bello se le lingue aggiungessero funzionalità per ridurre il codice del boilerplate necessario per inoltrare proprietà / metodi, specialmente se c'è un solo valore.
Mark Hurd,

Risposte:


82

Suppongo che stai pensando a linguaggi come Java e C #?

In quei linguaggi le primitive (come int) sono sostanzialmente un compromesso per le prestazioni. Non supportano tutte le funzionalità degli oggetti, ma sono più veloci e con meno spese generali.

Affinché gli oggetti supportino l'ereditarietà, ogni istanza deve "conoscere" in fase di esecuzione di quale classe è un'istanza. Altrimenti i metodi ignorati non possono essere risolti in fase di esecuzione. Per gli oggetti ciò significa che i dati di istanza sono archiviati in memoria insieme a un puntatore all'oggetto classe. Se tali informazioni dovessero anche essere archiviate insieme a valori primitivi, i requisiti di memoria aumenterebbero. Un valore intero a 16 bit richiederebbe i suoi 16 bit per il valore e inoltre la memoria a 32 o 64 bit per un puntatore alla sua classe.

Oltre al sovraccarico di memoria, ci si aspetterebbe anche di essere in grado di scavalcare le operazioni comuni su primitive come gli operatori aritmetici. Senza sottotipi, operatori come +possono essere compilati in una semplice istruzione di codice macchina. Se potrebbe essere sostituito, è necessario risolvere i metodi in fase di esecuzione, un'operazione molto più costosa. (È possibile che C # supporti il ​​sovraccarico dell'operatore, ma questo non è lo stesso. Il sovraccarico dell'operatore viene risolto al momento della compilazione, quindi non è prevista una penalità di runtime predefinita.)

Le stringhe non sono primitive ma sono ancora "speciali" nel modo in cui sono rappresentate nella memoria. Ad esempio, sono "internati", il che significa che due letterali di stringhe uguali possono essere ottimizzati allo stesso riferimento. Ciò non sarebbe possibile (o almeno molto meno efficace) se anche le istanze di stringa dovessero tenere traccia della classe.

Quello che descriveresti sarebbe sicuramente utile, ma supportarlo richiederebbe un sovraccarico prestazionale per ogni uso di primitive e stringhe, anche quando non sfruttano l'ereditarietà.

Il linguaggio Smalltalk fa (credo) permettono sottoclasse di interi. Ma quando è stato progettato Java, Smalltalk è stato considerato troppo lento e il sovraccarico di avere tutto come oggetto era considerato uno dei motivi principali. Java ha sacrificato l'eleganza e la purezza concettuale per ottenere prestazioni migliori.


12
@Den: stringè sigillato perché progettato per comportarsi immutabile. Se uno potesse ereditare dalla stringa, sarebbe possibile creare stringhe mutabili, il che renderebbe davvero soggetto a errori. Tonnellate di codice, incluso lo stesso framework .NET, si basano su stringhe che non hanno effetti collaterali. Vedi anche qui, ti dice lo stesso: quora.com/Why-String-class-in-C-is-a-sealed-class
Doc Brown

5
@DocBrown Questo è anche il motivo Stringè segnato anche finalin Java.
Dev

47
"quando è stato progettato Java, Smalltalk è stato considerato troppo lento [...]. Java ha sacrificato l'eleganza e la purezza concettuale per ottenere prestazioni migliori." - Ironia della sorte, ovviamente, Java non ha effettivamente ottenuto tale prestazione fino a quando Sun non ha acquistato una società Smalltalk per ottenere l'accesso alla tecnologia VM Smalltalk perché la JVM di Sun era lenta, e ha rilasciato HotSpot JVM, una VM Smalltalk leggermente modificata.
Jörg W Mittag,

3
@underscore_d: la risposta a cui hai collegato afferma esplicitamente che C♯ non ha tipi primitivi. Certo, alcune piattaforme per le quali esiste un'implementazione di C♯ possono o meno avere tipi primitivi, ma ciò non significa che C♯ abbia tipi primitivi. Ad esempio, esiste un'implementazione di Ruby per la CLI e la CLI ha tipi primitivi, ma ciò non significa che Ruby abbia tipi primitivi. L'implementazione può o meno scegliere di implementare tipi di valore mappandoli ai tipi primitivi della piattaforma, ma si tratta di un dettaglio dell'implementazione interna privata e non fa parte delle specifiche.
Jörg W Mittag,

10
Si tratta di astrazione. Dobbiamo mantenere la testa chiara, altrimenti finiremo con sciocchezze. Ad esempio: C♯ è implementato su .NET. .NET è implementato su Windows NT. Windows NT è implementato su x86. x86 è implementato su biossido di silicone. SiO₂ è solo sabbia. Quindi, a stringin C♯ è solo sabbia? No, certo che no, a stringin C♯ è ciò che dice la specifica C♯. Il modo in cui è implementato è irrilevante. Un'implementazione nativa di C♯ implementerebbe stringhe come array di byte, un'implementazione ECMAScript le mapperebbe su ECMAScript String, ecc.
Jörg W Mittag

20

Ciò che alcune lingue propongono non è la sottoclasse, ma il sottotipo . Ad esempio, Ada ti consente di creare tipi o sottotipi derivati . L' Ada Programming / Tipo di sistema sezione è la pena di leggere per capire tutti i dettagli. È possibile limitare l'intervallo di valori, che è quello che si desidera la maggior parte del tempo:

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

Puoi usare entrambi i tipi come numeri interi se li converti esplicitamente. Nota anche che non puoi usarne uno al posto di un altro, anche quando gli intervalli sono strutturalmente equivalenti (i tipi sono controllati dai nomi).

 type Reference is Integer;
 type Count is Integer;

I tipi sopra indicati sono incompatibili, anche se rappresentano lo stesso intervallo di valori.

(Ma puoi usare Unchecked_Conversion; non dire alla gente che te l'ho detto)


2
In realtà, penso che si tratti più di semantica. L'utilizzo di una quantità in cui è previsto un indice potrebbe quindi causare un errore di compilazione
Marjan Venema

@MarjanVenema Lo fa e questo viene fatto apposta per rilevare errori logici.
coredump,

Il mio punto era che non tutti i casi in cui si desidera la semantica, avresti bisogno degli intervalli. Avresti quindi type Index is -MAXINT..MAXINT;che in qualche modo non fa nulla per me dato che tutti gli interi sarebbero validi? Quindi che tipo di errore otterrei passando un angolo a un indice se tutto ciò che viene controllato sono gli intervalli?
Marjan Venema,

1
@MarjanVenema Nel suo secondo esempio entrambi i tipi sono sottotipi di Integer. Tuttavia, se si dichiara una funzione che accetta un conteggio, non è possibile passare un riferimento perché il controllo del tipo si basa sull'equivalenza del nome , che è il contrario di "tutto ciò che viene controllato sono gli intervalli". Questo non è limitato agli interi, è possibile utilizzare tipi o record enumerati. ( archive.adaic.com/standards/83rat/html/ratl-04-03.html )
coredump

1
@Marjan Un bell'esempio del perché i tipi di tag possono essere piuttosto potenti si può trovare nella serie di Eric Lippert sull'implementazione di Zorg in OCaml . In questo modo il compilatore può catturare molti bug - d'altra parte se si consente di convertire implicitamente tipi, questo sembra rendere inutile la funzione .. non ha senso semantico poter assegnare un tipo PersonAge a un tipo PersonId solo perché entrambi hanno lo stesso tipo di base.
Voo,

16

Penso che questa potrebbe benissimo essere una domanda X / Y. Punti salienti, dalla domanda ...

La mia motivazione è massimizzare l'uso del sistema di tipi per la verifica della correttezza in fase di compilazione.

... e dal tuo commento elaborando:

Non voglio essere in grado di sostituirne uno implicitamente un altro.

Mi scusi se mi manca qualcosa, ma ... Se questi sono i tuoi obiettivi, allora perché mai stai parlando di eredità? La sostituibilità implicita è ... come ... tutta la sua cosa. Sai, il principio di sostituzione di Liskov?

Ciò che sembra desiderare, in realtà, è il concetto di un "typedef forte", in base al quale qualcosa "è" ad es. intIn termini di portata e rappresentazione ma non può essere sostituito in contesti che si aspettano inte viceversa. Suggerirei di cercare informazioni su questo termine e su come la lingua o le lingue prescelte potrebbero chiamarlo. Ancora una volta, è praticamente letteralmente l'opposto dell'eredità.

E per coloro a cui potrebbe non piacere una risposta X / Y, penso che il titolo potrebbe essere ancora responsabile con riferimento a LSP. I tipi primitivi sono primitivi perché fanno qualcosa di molto semplice, ed è tutto ciò che fanno . Consentire loro di essere ereditati e rendere così infiniti i loro possibili effetti porterebbe a una grande sorpresa nella migliore delle ipotesi e una fatale violazione di LSP nel peggiore dei casi. Se posso ipotizzare ottimisticamente che a Thales Pereira non dispiacerà che citi questo commento fenomenale:

C'è il problema aggiunto che se qualcuno fosse in grado di ereditare da Int, avresti un codice innocente come "int x = y + 2" (dove Y è la classe derivata) che ora scrive un registro nel database, apre un URL e in qualche modo resuscitare Elvis. I tipi primitivi dovrebbero essere sicuri e con comportamenti più o meno garantiti e ben definiti.

Se qualcuno vede un tipo primitivo, in un linguaggio sano, giustamente presumono che farà sempre la sua piccola cosa, molto bene, senza sorprese. I tipi primitivi non hanno dichiarazioni di classe disponibili che segnalano se possono o meno essere ereditati e hanno i loro metodi sovrascritti. Se lo fossero, sarebbe davvero sorprendente (e romperebbe totalmente la compatibilità all'indietro, ma sono consapevole che è una risposta all'indietro al "perché X non è stato progettato con Y").

... anche se, come ha sottolineato Mooing Duck in risposta, i linguaggi che consentono il sovraccarico dell'operatore consentono all'utente di confondersi in misura simile o uguale se lo desiderano, quindi è dubbio che questo ultimo argomento valga. E smetterò di riassumere i commenti degli altri ora, eh.


4

Per consentire l'ereditarietà con l'invio virtuale 8 che è spesso considerato abbastanza desiderabile nella progettazione dell'applicazione), è necessario disporre di informazioni sul tipo di runtime. Per ogni oggetto, devono essere memorizzati alcuni dati relativi al tipo di oggetto. Un primitivo, per definizione, manca di queste informazioni.

Esistono due linguaggi OOP tradizionali (gestiti, eseguiti su una VM) che presentano primitive: C # e Java. Molte altre lingue non hanno primitivi, né usano ragionamenti simili per permetterli / usarli.

I primitivi sono un compromesso per le prestazioni. Per ogni oggetto, è necessario spazio per l'intestazione dell'oggetto (in Java, in genere 2 * 8 byte su macchine virtuali a 64 bit), oltre ai relativi campi e l'eventuale riempimento (in Hotspot, ogni oggetto occupa un numero di byte che è un multiplo di 8). Quindi un intoggetto as avrebbe bisogno di almeno 24 byte di memoria da conservare, invece di soli 4 byte (in Java).

Pertanto, sono stati aggiunti tipi primitivi per migliorare le prestazioni. Rendono molto più semplici. Cosa a + bsignifica se entrambi sono sottotipi di int? È necessario aggiungere un qualche tipo di dispathcing per scegliere l'aggiunta corretta. Questo significa spedizione virtuale. Avere la possibilità di utilizzare un codice operativo molto semplice per l'aggiunta è molto, molto più veloce e consente ottimizzazioni in fase di compilazione.

Stringè un altro caso. Sia in Java che in C #, Stringè un oggetto. Ma in C # è sigillato e in Java il suo finale. Ciò perché entrambe le librerie standard Java e C # richiedono che Strings sia immutabili e la loro sottoclasse li spezzerebbe l'immutabilità.

Nel caso di Java, la VM può (e fa) internare stringhe e "raggrupparle", consentendo prestazioni migliori. Funziona solo quando le stringhe sono veramente immutabili.

Inoltre, raramente è necessario sottoclassare i tipi primitivi. Finché i primitivi non possono essere suddivisi in sottoclassi, ci sono un sacco di cose ordinate che la matematica ci dice su di loro. Ad esempio, possiamo essere sicuri che l'addizione sia commutativa e associativa. Questo è qualcosa che ci dice la definizione matematica di numeri interi. Inoltre, in molti casi possiamo facilmente preimpostare gli invarianti su circuiti tramite induzione. Se consentiamo la sottoclasse di int, perdiamo quegli strumenti che la matematica ci fornisce, perché non possiamo più essere certi che determinate proprietà siano valide. Quindi, direi che la capacità di non essere in grado di sottoclassare i tipi primitivi è in realtà una buona cosa. Meno cose che qualcuno può rompere, oltre a un compilatore può spesso dimostrare di essere autorizzato a fare determinate ottimizzazioni.


1
Questa risposta è un abisso ... stretto. 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.Cosa, C ++ non è mainstream ora? Lo userò come confutazione come informazione sul tipo di runtime è un termine C ++. Non è assolutamente necessario a meno che non si usi dynamic_casto typeid. E anche se RTTI è attivo, l'ereditarietà consuma spazio solo se una classe ha virtualmetodi a cui deve essere indirizzata una tabella di metodi per classe per istanza
underscore_d

1
L'ereditarietà in C ++ funziona in modo molto diverso rispetto alle lingue eseguite su una macchina virtuale. la spedizione virtuale richiede RTTI, qualcosa che inizialmente non faceva parte del C ++. L'ereditarietà senza invio virtuale è molto limitata e non sono nemmeno sicuro che sia necessario confrontarla con l'ereditarietà con l'invio virtuale. Inoltre, la nozione di "oggetto" è molto diversa in C ++ rispetto a C # o Java. Hai ragione, ci sono alcune cose che potrei esprimere meglio, ma entrare in tutti i punti abbastanza implicati porta rapidamente a dover scrivere un libro sul design del linguaggio.
Poligono

3
Inoltre, non è vero che "la spedizione virtuale richiede RTTI" in C ++. Ancora una volta, solo dynamic_caste lo typeinforichiedono. Il dispacciamento virtuale viene praticamente implementato usando un puntatore alla vtable per la classe concreta dell'oggetto, permettendo così di chiamare le giuste funzioni, ma non richiede i dettagli di tipo e relazione inerenti a RTTI. Tutto ciò che il compilatore deve sapere è se la classe di un oggetto è polimorfica e, in tal caso, qual è il vptr dell'istanza. Si possono compilare banalmente classi praticamente inviate con -fno-rtti.
underscore_d

2
In realtà è il contrario, RTTI richiede l'invio virtuale. Letteralmente -C ++ non consente dynamic_castsulle classi senza invio virtuale. Il motivo dell'implementazione è che RTTI è generalmente implementato come membro nascosto di una vtable.
MSalters l'

1
@MilesRout C ++ ha tutto ciò di cui un linguaggio ha bisogno per OOP, almeno gli standard un po 'più recenti. Si potrebbe sostenere che i vecchi standard C ++ mancano di alcune cose necessarie per un linguaggio OOP, ma anche questo è un tratto. Il C ++ non è un linguaggio OOP di alto livello , in quanto consente un controllo più diretto e di basso livello su alcune cose, ma consente comunque OOP. (Alto livello / Basso livello qui in termini di astrazione , altri linguaggi come quelli gestiti sottraggono più sistema rispetto al C ++, quindi la loro astrazione è più alta).
Polygnome,

4

Nei linguaggi OOP statici forti tradizionali, la sottotipo è vista principalmente come un modo per estendere un tipo e sovrascrivere i metodi attuali del tipo.

Per fare ciò, gli 'oggetti' contengono un puntatore al loro tipo. Questo è un sovraccarico: il codice in un metodo che utilizza Shapeun'istanza deve prima accedere alle informazioni sul tipo di tale istanza, prima di conoscere il Area()metodo corretto da chiamare.

Una primitiva tende a consentire solo operazioni su di essa che possono tradursi in istruzioni in un singolo linguaggio macchina e che non portano alcuna informazione di tipo con esse. Rendere un numero intero più lento in modo che qualcuno potesse sottoclassarlo non era abbastanza attraente da impedire a qualsiasi lingua che lo facesse diventare mainstream.

Quindi la risposta a:

Perché i linguaggi OOP statici forti tradizionali impediscono di ereditare le primitive?

È:

  • C'era poca richiesta
  • E avrebbe reso la lingua troppo lenta
  • Il sottotipo era principalmente visto come un modo per estendere un tipo, piuttosto che un modo per ottenere un migliore controllo statico del tipo (definito dall'utente).

Tuttavia, stiamo iniziando a ottenere lingue che consentono il controllo statico basato su proprietà di variabili diverse da "type", ad esempio F # ha "dimensione" e "unità" in modo che non sia possibile, ad esempio, aggiungere una lunghezza a un'area .

Ci sono anche lingue che consentono "tipi definiti dall'utente" che non cambiano (o scambiano) ciò che fa un tipo, ma aiutano solo con il controllo statico del tipo; vedi la risposta di coredump.


Le unità di misura F # sono una caratteristica interessante, sebbene purtroppo erroneamente denominate. Inoltre è solo in fase di compilazione, quindi non molto utile, ad esempio quando si consuma un pacchetto NuGet compilato. Giusta direzione, però.
Den,

È forse interessante notare che "dimensione" non è "una proprietà diversa da" tipo "", è solo un tipo di tipo più ricco di quello a cui potresti essere abituato.
porglezomp,

3

Non sono sicuro se sto trascurando qualcosa qui, ma la risposta è piuttosto semplice:

  1. La definizione di primitive è: i valori primitivi non sono oggetti, i tipi primitivi non sono tipi di oggetti, le primitive non fanno parte del sistema di oggetti.
  2. L'ereditarietà è una caratteristica del sistema a oggetti.
  3. Ergo, i primitivi non possono prendere parte all'eredità.

Si noti che in realtà ci sono solo due potenti linguaggi OOP statici che hanno persino primitive, AFAIK: Java e C ++. (In realtà, non sono nemmeno sicuro di quest'ultimo, non so molto sul C ++ e ciò che ho trovato durante la ricerca era confuso.)

In C ++, le primitive sono fondamentalmente un retaggio ereditato (gioco di parole) da C. Quindi, non prendono parte al sistema a oggetti (e quindi all'ereditarietà) perché C non ha né un sistema a oggetti né eredità.

In Java, le primitive sono il risultato di un tentativo errato di migliorare le prestazioni. I primitivi sono anche gli unici tipi di valore nel sistema, infatti è impossibile scrivere tipi di valore in Java ed è impossibile che gli oggetti siano tipi di valore. Quindi, a parte il fatto che i primitivi non prendono parte al sistema degli oggetti e quindi l'idea di "eredità" non ha nemmeno senso, anche se tu potessi ereditare da loro, non saresti in grado di mantenere il " valore-ness". Questo è diverso da es C♯ quale ha avere tipi valore ( structs), che comunque sono oggetti.

Un'altra cosa è che non essere in grado di ereditare non è in realtà unico nemmeno per i primitivi. In C, structs eredita implicitamente System.Objecte può implementare interfaces, ma non può né ereditare né ereditare da classes o structs. Inoltre, sealed classnon è possibile ereditare es. In Java, final classnon è possibile ereditare es.

tl; dr :

Perché i linguaggi OOP statici forti tradizionali impediscono di ereditare le primitive?

  1. le primitive non fanno parte del sistema di oggetti (per definizione, se lo fossero, non sarebbero primitive), l'idea di eredità è legata al sistema di oggetti, l'eredità ergo primitiva è una contraddizione in termini
  2. le primitive non sono uniche, molti altri tipi non possono essere ereditati ( finalo sealedin Java o C♯, structs in C♯, case classes in Scala)

3
Ehm ... So che si pronuncia "C Sharp", ma, ehm
Mr Lister

Penso che ti sbagli piuttosto dal lato C ++. Non è affatto un linguaggio OO puro. I metodi di classe di default non lo sono virtual, il che significa che non obbediscono a LSP. Ad esempio, std::stringnon è un primitivo, ma si comporta come un altro valore. Tale semantica di valore è abbastanza comune, suppone l'intera parte STL di C ++.
Salterio,

2
"In Java, le primitive sono il risultato di un tentativo sbagliato di migliorare le prestazioni." Penso che tu non abbia idea dell'entità del successo prestazionale dell'implementazione delle primitive come tipi di oggetti espandibili dall'utente. Questa decisione in Java è sia deliberata che fondata. Immagina di dover allocare memoria per ogni intutilizzo. Ogni allocazione assume l'ordine di 100 ns più il sovraccarico della raccolta dei rifiuti. Confrontalo con il singolo ciclo della CPU consumato aggiungendo due primitive int. I tuoi codici java strisciano se i progettisti della lingua decidessero diversamente.
cmaster

1
@cmaster: Scala non ha primitivi e le sue prestazioni numeriche sono esattamente le stesse di Java. Perché, beh, compila numeri interi in primitive JVM int, quindi si comportano esattamente allo stesso modo. (I nativi di Scala li compila in registri primitivi della macchina, Scala.js li compila in primitivi ECMAScript Numbers.) Ruby non ha primitivi, ma YARV e Rubinius compongono gli interi in interi primitivi della macchina, JRuby li compila in JVM primitive longs. Praticamente ogni implementazione di Lisp, Smalltalk o Ruby utilizza primitive nella VM . Ecco dove le ottimizzazioni delle prestazioni ...
Jörg W Mittag l'

1
... appartengono: nel compilatore, non nella lingua.
Jörg W Mittag,

2

Joshua Bloch in "Effective Java" raccomanda di progettare esplicitamente per l'ereditarietà o vietarlo. Le classi primitive non sono progettate per l'ereditarietà perché sono progettate per essere immutabili e consentire l'ereditarietà potrebbe cambiare ciò nelle sottoclassi, infrangendo così il principio di Liskov e sarebbe fonte di molti bug.

Ad ogni modo, perché questa è una soluzione caotica? Dovresti davvero preferire la composizione rispetto all'eredità. Se il motivo è la prestazione di quanto tu abbia un punto e la risposta alla tua domanda è che non è possibile mettere tutte le funzionalità in Java perché ci vuole tempo per analizzare tutti i diversi aspetti dell'aggiunta di una funzione. Ad esempio Java non aveva Generics prima della 1.5.

Se hai molta pazienza, allora sei fortunato perché esiste un piano per aggiungere classi di valore a Java che ti permetterà di creare le tue classi di valore che ti aiuteranno ad aumentare le prestazioni e allo stesso tempo ti daranno maggiore flessibilità.


2

A livello astratto, puoi includere tutto ciò che desideri in una lingua che stai progettando.

A livello di implementazione, è inevitabile che alcune di queste cose siano più semplici da implementare, alcune saranno complicate, alcune possono essere rese veloci, altre sono destinate ad essere più lente, e così via. Per tenere conto di ciò, i progettisti spesso devono prendere decisioni e compromessi difficili.

A livello di implementazione, uno dei modi più veloci che abbiamo trovato per accedere a una variabile è scoprire il suo indirizzo e caricare il contenuto di tale indirizzo. Nella maggior parte delle CPU ci sono istruzioni specifiche per il caricamento dei dati dagli indirizzi e quelle istruzioni di solito devono sapere quanti byte devono caricare (uno, due, quattro, otto, ecc.) E dove inserire i dati che caricano (registro singolo, registro coppia, registro esteso, altra memoria, ecc.). Conoscendo la dimensione di una variabile, il compilatore può sapere esattamente quale istruzione emettere per gli usi di quella variabile. Non conoscendo la dimensione di una variabile, il compilatore dovrebbe ricorrere a qualcosa di più complicato e probabilmente più lento.

A livello astratto, il punto del sottotipo è di poter usare istanze di un tipo in cui è previsto un tipo uguale o più generale. In altre parole, può essere scritto un codice che prevede un oggetto di un determinato tipo o qualcosa di più derivato, senza sapere in anticipo cosa sarebbe esattamente questo. E chiaramente, poiché più tipi derivati ​​possono aggiungere più membri di dati, un tipo derivato non ha necessariamente gli stessi requisiti di memoria dei suoi tipi base.

A livello di implementazione, non esiste un modo semplice per una variabile di dimensioni predeterminate di contenere un'istanza di dimensioni sconosciute e di avere accesso in un modo che normalmente chiamereste efficiente. Ma c'è un modo per spostare un po 'le cose e usare una variabile non per immagazzinare l'oggetto, ma per identificare l'oggetto e lasciare che l'oggetto sia immagazzinato altrove. In questo modo è un riferimento (ad es. Un indirizzo di memoria) - un ulteriore livello di riferimento indiretto che garantisce che una variabile debba contenere solo un tipo di informazioni di dimensioni fisse, purché sia ​​possibile trovare l'oggetto attraverso tali informazioni. Per ottenere ciò, dobbiamo solo caricare l'indirizzo (dimensione fissa) e quindi possiamo lavorare come al solito usando quegli offset dell'oggetto che sappiamo essere validi, anche se quell'oggetto ha più dati negli offset che non conosciamo. Possiamo farlo perché non

A livello astratto, questo metodo consente di memorizzare un (riferimento a a) stringin una objectvariabile senza perdere le informazioni che lo rendono a string. Va bene per tutti i tipi di lavorare in questo modo e potresti anche dire che è elegante sotto molti aspetti.

Tuttavia, a livello di implementazione, il livello extra di indiretta comporta più istruzioni e sulla maggior parte delle architetture rende ogni accesso all'oggetto un po 'più lento. Puoi consentire al compilatore di ottenere più prestazioni da un programma se includi nella tua lingua alcuni tipi di uso comune che non hanno quel livello extra di riferimento indiretto (il riferimento). Ma rimuovendo quel livello di riferimento indiretto, il compilatore non può più permetterti di sottotipare in modo sicuro dalla memoria. Questo perché se aggiungi più membri di dati al tuo tipo e lo assegni a un tipo più generale, tutti i membri di dati extra che non rientrano nello spazio allocato per la variabile di destinazione verranno eliminati.


1

In generale

Se una classe è astratta (metafora: una scatola con buche), è OK (anche richiesto di avere qualcosa di utilizzabile!) Per "riempire le buche", ecco perché sottoclassiamo le classi astratte.

Se una classe è concreta (metafora: una casella piena), non è corretto modificare l'esistente perché se è piena, è piena. Non abbiamo spazio per aggiungere qualcosa di più all'interno della scatola, ecco perché non dovremmo sottoclassare classi concrete.

Con i primitivi

I primitivi sono classi concrete dal design. Rappresentano qualcosa che è ben noto, completamente definito (non ho mai visto un tipo primitivo con qualcosa di astratto, altrimenti non è più una primitiva) e ampiamente usato attraverso il sistema. Permettere di sottoclasse di un tipo primitivo e fornire la propria implementazione ad altri che si basano sul comportamento progettato dei primitivi può causare molti effetti collaterali e danni enormi!



Il link è un'interessante opinione progettuale. Ha bisogno di più pensieri per me.
Den

1

Di solito l'ereditarietà non è la semantica che desideri, perché non puoi sostituire il tuo tipo speciale ovunque ci si aspetta una primitiva. Prendendo in prestito dal tuo esempio, a Quantity + Indexnon ha senso semanticamente, quindi una relazione ereditaria è la relazione sbagliata.

Tuttavia, diverse lingue hanno il concetto di un tipo di valore che esprime il tipo di relazione che stai descrivendo. Scala è un esempio. Un tipo di valore utilizza una primitiva come rappresentazione sottostante, ma ha una diversa identità di classe e operazioni all'esterno. Ciò ha l'effetto di estendere un tipo primitivo, ma è più una composizione che una relazione di ereditarietà.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.