Qual è la differenza tra una sottoclasse e un sottotipo?


44

La risposta più votata a questa domanda sul principio di sostituzione di Liskov fa fatica a distinguere tra i termini sottotipo e sottoclasse . Sottolinea inoltre che alcune lingue confondono le due, mentre altre no.

Per i linguaggi orientati agli oggetti con cui ho più familiarità (Python, C ++), "tipo" e "classe" sono concetti sinonimi. In termini di C ++, cosa significherebbe distinguere tra sottotipo e sottoclasse? Dire, ad esempio, che Fooè una sottoclasse, ma non un sottotipo, di FooBase. Se fooè un'istanza di Foo, questa riga:

FooBase* fbPoint = &foo;

non essere più valido?


6
In realtà, in Python "tipo" e "classe" sono concetti distinti . In realtà, Python viene digitato in modo dinamico, "tipo" non è un concetto del tutto in pitone. Sfortunatamente, gli sviluppatori di Python non lo capiscono e continuano a confondere i due.
Jörg W Mittag,

11
"Tipo" e "classe" sono distinti anche in C ++. "Array of ints" è un tipo; che classe è? "puntatore a una variabile di tipo int" è un tipo; che classe è? Queste cose non sono di classe, ma sono sicuramente tipi.
Eric Lippert,

2
Mi chiedevo proprio questa cosa dopo aver letto quella domanda e quella risposta.
user369450

4
@JorgWMittag Se non esiste il concetto di "tipo" in Python, qualcuno dovrebbe dire a chiunque scriva la documentazione: docs.python.org/3/library/stdtypes.html
Matt

@Matt per essere onesti, i tipi sono arrivati ​​in 3.5, che è abbastanza recente, specialmente per quanto mi è permesso usare nella produzione standard.
Jared Smith,

Risposte:


53

Il sottotipo è una forma di polimorfismo di tipo in cui un sottotipo è un tipo di dati correlato a un altro tipo di dati (il supertipo) da una nozione di sostituibilità, il che significa che gli elementi del programma, in genere subroutine o funzioni, scritti per operare su elementi del supertipo possono anche operare su elementi del sottotipo.

Se Sè un sottotipo di T, la relazione di sottotipo viene spesso scritta S <: T, per indicare che qualsiasi termine di tipo Spuò essere utilizzato in modo sicuro in un contesto in cui Tè previsto un termine di tipo . La semantica precisa del sottotipo dipende fondamentalmente dai dettagli di ciò che "usato in sicurezza in un contesto in cui" significa in un determinato linguaggio di programmazione.

La sottoclasse non deve essere confusa con il sottotipo. In generale, il sottotipo stabilisce una relazione is-a, mentre la sottoclasse riutilizza solo l'implementazione e stabilisce una relazione sintattica, non necessariamente una relazione semantica (l'ereditarietà non garantisce il sottotipo comportamentale).

Per distinguere questi concetti, il sottotipo è anche noto come eredità dell'interfaccia , mentre la sottoclasse è nota come eredità dell'implementazione o eredità del codice.

Riferimenti Ereditarietà dei
sottotipi


1
Davvero ben detto. Potrebbe essere utile menzionare nel contesto della domanda che i programmatori C ++ impiegano spesso classi di base virtuali pure per comunicare relazioni di sottotipi al sistema di tipi. Naturalmente sono spesso preferiti approcci di programmazione generici.
Aluan Haddad,

6
"La semantica precisa del sottotipo dipende fondamentalmente dai dettagli di ciò che" usato in sicurezza in un contesto in cui "significa in un determinato linguaggio di programmazione". ... e l'LSP definisce un'idea alquanto ragionevole di cosa significhi "in sicurezza" e ci dice quali vincoli devono soddisfare questi particolari per consentire questa particolare forma di "sicurezza".
Jörg W Mittag,

Un altro campione in pila: se ho capito bene, in C ++, l' publicereditarietà introduce un sottotipo mentre l' privateereditarietà introduce una sottoclasse.
Quentin,

L'eredità pubblica di @Quentin è sia un sottotipo che una sottoclasse, ma privata è solo una sottoclasse, ma non un sottotipo. Puoi avere un sottotipo senza sottoclasse con strutture come le interfacce Java
equivale al

27

Un tipo , nel contesto di cui stiamo parlando qui, è essenzialmente un insieme di garanzie comportamentali. Un contratto , se vuoi. Oppure, prendendo in prestito la terminologia da Smalltalk, un protocollo .

Una classe è un insieme di metodi. È un insieme di implementazioni comportamentali .

Il sottotipo è un mezzo per perfezionare il protocollo. La sottoclasse è un mezzo per riutilizzare il codice differenziale, ovvero riutilizzare il codice descrivendo solo la differenza di comportamento.

Se hai usato Java o C♯, allora potresti esserti imbattuto nel consiglio che tutti i tipi dovrebbero essere interfacetipi. In effetti, se leggi L'intesa sull'introduzione dei dati di William Cook , rivisitata , potresti sapere che per fare OO in quelle lingue, devi usare solo interfaces come tipi. (Inoltre, fatto divertente: Java ha paralizzato interfaces direttamente dai protocolli di Objective-C, che a loro volta sono presi direttamente da Smalltalk.)

Ora, se seguiamo quel consiglio di codifica fino alla sua conclusione logica e immaginiamo una versione di Java, dove solo i interface s sono tipi, e le classi e le primitive non lo sono, allora uno che interfaceeredita da un altro creerà una relazione di sottotipo, mentre uno che classeredita da un'altra volontà essere semplicemente per il riutilizzo differenziale del codice tramite super.

Per quanto ne so, non esistono linguaggi tipicamente statici tradizionali che distinguono rigorosamente tra ereditare il codice (eredità dell'implementazione / sottoclasse) ed ereditare i contratti (sottotipizzazione). In Java e C♯, l'ereditarietà dell'interfaccia è un sottotipo puro (o almeno lo era, fino all'introduzione di metodi predefiniti in Java 8 e probabilmente anche C♯ 8), ma anche l'ereditarietà delle classi è anche un sottotipo e l'ereditarietà dell'implementazione. Ricordo di aver letto un dialetto sperimentale LISP orientato agli oggetti tipicamente statico, che distingueva rigorosamente tra mixin (che contengono comportamento), strutture (che contengono stato), interfacce (che descrivonocomportamento) e classi (che compongono zero o più strutture con uno o più mixin e conformi a una o più interfacce). Solo le classi possono essere istanziate e solo le interfacce possono essere utilizzate come tipi.

In un linguaggio OO tipizzato in modo dinamico come Python, Ruby, ECMAScript o Smalltalk, generalmente pensiamo al tipo o ai tipi di un oggetto come all'insieme di protocolli a cui è conforme. Nota il plurale: un oggetto può avere più tipi e non sto solo parlando del fatto che ogni oggetto di tipo Stringè anche un oggetto di tipo Object. (A proposito: nota come ho usato i nomi delle classi per parlare di tipi? Quanto sono stupido da parte mia!) Un oggetto può implementare più protocolli. Ad esempio, in Ruby, Arrayspossono essere aggiunti, possono essere indicizzati, possono essere ripetuti e confrontati. Sono quattro protocolli diversi che implementano!

Ora, Ruby non ha tipi. Ma la comunità di Ruby ha tipi! Esistono solo nelle teste dei programmatori, però. E nella documentazione. Ad esempio, qualsiasi oggetto che risponde a un metodo chiamato eachrestituendo i suoi elementi uno alla volta viene considerato un oggetto enumerabile . E c'è un mixin chiamato Enumerableche dipende da questo protocollo. Quindi, se il vostro oggetto ha il corretto tipo (che esiste solo nella testa del programmatore), allora è permesso di mescolare in (ereditare dal) Enumerablemixin, e anche ottenere tutti i tipi di metodi interessanti per libero, come map, reduce, filtere così su.

Analogamente, se un oggetto risponde <=>, allora è considerato per implementare il comparabili protocollo, e può miscela nel Comparablemixin e ottenere cose simili <, <=, >, <=, ==, between?, e clampper libero. Tuttavia, può anche implementare tutti quei metodi stessi, e non ereditarli Comparableaffatto, e sarebbe comunque considerato comparabile .

Un buon esempio è la StringIOlibreria, che essenzialmente falsifica i flussi di I / O con le stringhe. Implementa tutti gli stessi metodi della IOclasse, ma non esiste alcuna relazione di ereditarietà tra i due. Tuttavia, un StringIOpuò essere usato ovunque un IOpuò essere usato. Questo è molto utile nei test unitari, in cui è possibile sostituire un file o stdincon un StringIOsenza dover apportare ulteriori modifiche al programma. Poiché è StringIOconforme allo stesso protocollo di IO, sono entrambi dello stesso tipo, anche se sono classi diverse e non condividono alcuna relazione (a parte il banale che entrambi estendono Objectad un certo punto).


Potrebbe essere utile se le lingue consentissero ai programmi di dichiarare contemporaneamente un tipo di classe e un'interfaccia per cui quella classe è un'implementazione, e consentire anche alle implementazioni di specificare "costruttori" (che sarebbero collegati ai costruttori di classi specificate dall'interfaccia). Per i tipi di oggetti per i quali i riferimenti sarebbero condivisi pubblicamente, il modello preferito sarebbe che il tipo di classe venga utilizzato solo durante la creazione di classi derivate; la maggior parte dei riferimenti dovrebbe essere del tipo di interfaccia. Essere in grado di specificare i costruttori di interfacce sarebbe utile nelle situazioni in cui ...
supercat

... ad es. il codice ha bisogno di una raccolta che consenta di leggere un certo set di valori per indice, ma non gli importa davvero di che tipo sia. Sebbene vi siano valide ragioni per riconoscere classi e interfacce come tipi distinti di tipi, esistono molte situazioni in cui dovrebbero essere in grado di lavorare più strettamente insieme di quanto le lingue attualmente consentano.
Supercat,

Hai un riferimento o alcune parole chiave che potrei cercare qualche informazione in più sul dialetto sperimentale LISP che hai citato che differenzia formalmente mixin, strutture, interfacce e classi?
tel

@tel: No, scusa. Fu probabilmente circa 15-20 anni fa, e in quel momento i miei interessi erano dappertutto. Non potrei iniziare a dirti cosa stavo cercando quando mi sono imbattuto in questo.
Jörg W Mittag,

Awww. Questo è stato il dettaglio più interessante in tutte queste risposte. Il fatto che una separazione formale di questi concetti sia effettivamente possibile nell'ambito dell'implementazione di un linguaggio ha davvero aiutato a cristallizzare la distinzione classe / tipo per me. Immagino che andrò a cercare quel LISP da solo, in ogni caso. Ti capita di ricordare se l'hai letto in un articolo / libro di giornale o se ne hai appena sentito parlare in una conversazione?
tel

2

È forse utile innanzitutto distinguere tra un tipo e una classe e quindi immergersi nella differenza tra sottotipo e sottoclasse.

Per il resto di questa risposta suppongo che i tipi in discussione siano tipi statici (dal momento che il sottotipo di solito si presenta in un contesto statico).

Svilupperò uno pseudocodice giocattolo per aiutare a illustrare la differenza tra un tipo e una classe perché la maggior parte delle lingue li confonde almeno in parte (per una buona ragione che toccherò brevemente).

Cominciamo con un tipo. Un tipo è un'etichetta per un'espressione nel tuo codice. Il valore di questa etichetta e se è coerente (per alcuni tipi di definizione specifica del sistema di coerenza) con il valore di tutte le altre etichette può essere determinato da un programma esterno (un typechecker) senza eseguire il programma. Questo è ciò che rende queste etichette speciali e meritevoli del proprio nome.

Nel nostro linguaggio dei giocattoli potremmo consentire la creazione di etichette in questo modo.

declare type Int
declare type String

Quindi potremmo etichettare vari valori come di questo tipo.

0 is of type Int
1 is of type Int
-1 is of type Int
...

"" is of type String
"a" is of type String
"b" is of type String
...

Con queste affermazioni il nostro typechecker può ora rifiutare dichiarazioni come

0 is of type String

se uno dei requisiti del nostro sistema di tipi è che ogni espressione ha un tipo unico.

Lasciamo da parte per ora quanto sia goffo e come avrete problemi ad assegnare un numero infinito di tipi di espressioni. Possiamo tornarci più tardi.

Una classe d'altra parte è una raccolta di metodi e campi che sono raggruppati insieme (potenzialmente con modificatori di accesso come privati ​​o pubblici).

class StringClass:
  defMethod concatenate(otherString): ...
  defField size: ...

Un'istanza di questa classe ha la possibilità di creare o utilizzare definizioni preesistenti di questi metodi e campi.

Potremmo scegliere di associare una classe a un tipo in modo tale che ogni istanza di una classe sia automaticamente etichettata con quel tipo.

associate StringClass with String

Ma non tutti i tipi devono avere una classe associata.

# Hmm... Doesn't look like there's a class for Int

È anche possibile che nel nostro linguaggio dei giocattoli non ogni classe abbia un tipo, specialmente se non tutte le nostre espressioni hanno tipi. È un po 'più complicato (ma non impossibile) immaginare che tipo di regole di coerenza del sistema apparirebbero se alcune espressioni avessero dei tipi e altre no.

Inoltre, nel nostro linguaggio dei giocattoli, queste associazioni non devono essere uniche. Potremmo associare due classi con lo stesso tipo.

associate MyCustomStringClass with String

Ora tieni presente che non è necessario che il nostro typechecker tenga traccia del valore di un'espressione (e nella maggior parte dei casi non sarà o è impossibile farlo). Tutto ciò che sa sono le etichette che gli hai detto. Come promemoria in precedenza, il typechecker era in grado di rifiutare l'affermazione solo a 0 is of type Stringcausa della nostra regola del tipo creata artificialmente che le espressioni devono avere tipi univoci e avevamo già etichettato l'espressione 0qualcos'altro. Non aveva alcuna conoscenza speciale del valore di 0.

E che dire del sottotipo? Bene il sottotipo è un nome per una regola comune nel controllo dei caratteri che rilassa le altre regole che potresti avere. Vale a dire se A is subtype of Bquindi ovunque il tuo typechecker richiede un'etichetta di B, accetterà anche un A.

Ad esempio, potremmo fare quanto segue per i nostri numeri invece di quello che avevamo in precedenza.

declare type NaturalNum
declare type Int
NaturalNum is subtype of Int

0 is of type NaturalNum
1 is of type NaturalNum
-1 is of type Int
...

La sottoclasse è una scorciatoia per dichiarare una nuova classe che consente di riutilizzare metodi e campi precedentemente dichiarati.

class ExtendedStringClass is subclass of StringClass:
  # We get concatenate and size for free!
  def addQuestionMark: ...

Non abbiamo alle istanze associati di ExtendedStringClasscon Stringcome abbiamo fatto con StringClasspoiché, dopo tutto è una classe del tutto nuova, abbiamo semplicemente non devono scrivere tanto. Ciò ci consentirebbe di fornire ExtendedStringClassun tipo incompatibile Stringdal punto di vista del tipechecker.

Allo stesso modo avremmo potuto decidere di creare una classe completamente nuova NewClasse fatto

associate NewClass with String

Ora ogni istanza di StringClasspuò essere sostituita NewClassdal punto di vista del typechecker.

Quindi in teoria il sottotipo e la sottoclasse sono cose completamente diverse. Ma nessuna lingua che conosco ha tipi e classi in realtà fa le cose in questo modo. Iniziamo a ridurre la nostra lingua e spieghiamo la logica alla base di alcune delle nostre decisioni.

Prima di tutto, anche se in teoria a classi completamente diverse potrebbe essere assegnato lo stesso tipo o a una classe potrebbe essere assegnato lo stesso tipo di valori che non sono istanze di alcuna classe, ciò ostacola gravemente l'utilità del typechecker. Il typechecker è effettivamente privato della possibilità di verificare se il metodo o il campo che stai chiamando all'interno di un'espressione esiste effettivamente su quel valore, che è probabilmente un controllo che ti piacerebbe se stai andando a giocare con un coontrollore dei tipo. Dopotutto, chissà quale sia il valore reale sotto Stringquell'etichetta; potrebbe essere qualcosa che non ha affatto, ad esempio, un concatenatemetodo!

Ok, quindi stipuliamo che ogni classe genera automaticamente un nuovo tipo con lo stesso nome della classe e delle associateistanze di quel tipo. Questo ci consente di sbarazzarci di associatecosì come i diversi nomi tra StringClasse String.

Per lo stesso motivo, probabilmente vogliamo stabilire automaticamente una relazione di sottotipo tra i tipi di due classi in cui una è una sottoclasse di un'altra. Dopo tutto, la sottoclasse è garantita per avere tutti i metodi e campi della classe genitore, ma non è vero il contrario. Pertanto, mentre la sottoclasse può passare ogni volta che è necessario un tipo di classe genitore, il tipo di classe genitore deve essere rifiutato se è necessario il tipo di sottoclasse.

Se si combina questo con la stipula secondo cui tutti i valori definiti dall'utente devono essere istanze di una classe, è possibile avere is subclass ofdoppio dovere e liberarsene is subtype of.

E questo ci porta alle caratteristiche che condividono la maggior parte delle lingue popolari OO tipicamente statiche. Esistono un insieme di tipi "primitivi" (ad es int. float, Ecc.) Che non sono associati a nessuna classe e non sono definiti dall'utente. Quindi hai tutte le classi definite dall'utente che hanno automaticamente tipi con lo stesso nome e identificano la sottoclasse con il sottotipo.

La nota finale che prenderò in considerazione è la confusione di dichiarare i tipi separatamente dai valori. La maggior parte delle lingue confonde la creazione delle due, in modo che una dichiarazione di tipo sia anche una dichiarazione per la generazione di valori completamente nuovi che vengono automaticamente etichettati con quel tipo. Ad esempio, una dichiarazione di classe in genere crea sia il tipo che un modo per creare un'istanza di valori di quel tipo. Questo elimina parte della confusione e, in presenza di costruttori, consente anche di creare un'etichetta infinita di valori con un tipo in un colpo.

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.