In che modo la varianza del sito di utilizzo di Java si confronta con la varianza del sito di dichiarazione di C #?

La mia comprensione è che la specificazione della varianza per i generici in C # avviene a livello di dichiarazione del tipo: quando si crea il tipo generico, si specifica la varianza per gli argomenti di tipo. In Java, d’altra parte, viene specificata la varianza in cui viene utilizzato un generico: quando si crea una variabile di un tipo generico, si specifica in che modo gli argomenti del tipo possono variare.

Quali sono i pro e i contro per ciascuna opzione?

Risponderò semplicemente alle differenze tra la varianza tra sito di dichiarazione e sito d’uso, poiché, mentre i generici di C # e Java differiscono in molti altri modi, queste differenze sono per lo più ortogonali rispetto alla varianza.

Prima di tutto, se ricordo bene la varianza del sito-uso è strettamente più potente della varianza del sito di dichiarazione (anche se al costo della concisione), o almeno i jolly di Java sono (che sono in realtà più potenti della varianza del sito d’uso). Questo aumento di potenza è particolarmente utile per le lingue in cui i costrutti di stato sono usati pesantemente, come C # e Java (ma Scala molto meno, soprattutto dal momento che i suoi elenchi standard sono immutabili). Considera List (o IList ). Poiché ha metodi sia per aggiungere E sia per ottenere E, è invariante rispetto a E, quindi la varianza del sito di dichiarazione non può essere utilizzata. Tuttavia, con la varianza del sito di utilizzo si può semplicemente dire List<+Number> per ottenere il sottoinsieme covariante di List ed List<-Number> per ottenere il sottoinsieme controverso di List . In un linguaggio di dichiarazione del sito, il progettista della libreria dovrebbe creare interfacce separate (o classi se si consente l’ereditarietà multipla delle classi) per ogni sottoinsieme e avere l’estensione di tali interfacce. Se il progettista della biblioteca non lo fa (nota che IEnumerable C # fa solo un piccolo sottoinsieme della porzione covariante di IList ), allora sei sfortunato e devi ricorrere alle stesse seccature che devi fare in una lingua senza qualsiasi tipo di varianza.

Ecco i vantaggi dell’ereditarietà del sito di utilizzo rispetto all’ereditarietà dei siti di dichiarazione. Il vantaggio dell’ereditarietà del sito di dichiarazione sull’ereditarietà del sito di utilizzo è fondamentalmente una concisione per l’utente (a condizione che il progettista abbia affrontato lo sforzo di separare ogni class / interfaccia nelle sue porzioni covarianti e controvarianti). Per qualcosa come IEnumerable o Iterator , è bello non dover specificare la covarianza ogni volta che si utilizza l’interfaccia. Java ha reso questo particolarmente fastidioso usando una lunga syntax (eccetto per la bivarianza per la quale la soluzione Java è sostanzialmente ideale).

Naturalmente, queste due funzionalità linguistiche possono coesistere. Per i parametri di tipo che sono naturalmente covarianti o controvarianti (come in IEnumerable / Iterator ), dichiararlo nella dichiarazione. Per i parametri di tipo che sono naturalmente invarianti (come in (I)List ), dichiarare quale tipo di varianza si desidera ogni volta che lo si utilizza. Basta non specificare una varianza del sito d’uso per gli argomenti con una varianza del sito di dichiarazione in quanto ciò rende le cose semplicemente confuse.

Ci sono altri problemi più dettagliati che non ho approfondito (come ad esempio il modo in cui i caratteri jolly sono in realtà più potenti della varianza del sito d’uso), ma spero che questo risponda alla tua domanda al tuo contenuto. Ammetto che sono prevenuto riguardo alla varianza del sito d’uso, ma ho cercato di illustrare i principali vantaggi di entrambi che sono emersi nelle mie discussioni con i programmatori e con i ricercatori di lingue.

La maggior parte delle persone preferisce la varianza del sito di dichiarazione, perché rende più semplice per gli utenti della libreria (rendendola un po ‘più difficile per lo sviluppatore della libreria, anche se direi che lo sviluppatore della biblioteca deve pensare alla varianza indipendentemente da dove si trova la varianza è in realtà scritto.)

Ma tieni presente che né Java né C # sono esempi di buon design del linguaggio.

Mentre Java ha ottenuto la varianza giusta e funziona indipendentemente dalla JVM a causa di miglioramenti compatibili della VM in Java 5 e cancellazione del tipo, la varianza del sito di utilizzo rende l’uso un po ‘ingombrante e la particolare implementazione della cancellazione dei tipi ha attirato critiche meritate.

Il modello di varianza del sito di dichiarazione di C # toglie il peso all’utente della biblioteca, ma durante la sua introduzione di generici reificati ha fondamentalmente costruito le regole di varianza nella sua VM. Ancora oggi non possono sostenere pienamente la co / contravarianza a causa di questo errore (e l’introduzione non retrocompatibile delle classi di raccolta reificate ha diviso i programmatori in due campi).

Ciò pone una restrizione difficile a tutte le lingue che si rivolgono al CLR ed è una delle ragioni per cui i linguaggi di programmazione alternativi sono molto più vivaci sulla JVM anche se sembra che il CLR abbia “caratteristiche molto più piacevoli”.

Diamo un’occhiata a Scala : Scala è un ibrido funzionale completamente orientato agli oggetti che gira su JVM. Usano la cancellazione di tipo come Java, ma sia l’implementazione di Generics che la varianza (dichiarazione-sito) sono più facili da capire, più semplici e potenti di Java (o C #), perché la VM non impone regole su come deve variare la varianza lavoro. Il compilatore Scala controlla le notazioni della varianza e può rifiutare il codice sorgente non corretto in fase di compilazione invece di generare eccezioni in fase di runtime, mentre i file .class risultanti possono essere utilizzati senza problemi da Java.

Uno svantaggio della varianza del sito di dichiarazione è che sembra rendere più difficile l’inferenza di tipo in alcuni casi.

Allo stesso tempo Scala può usare tipi primitivi senza inscatolarli in raccolte come in C # usando l’annotazione @specialized che dice al compilatore Scala di generare una o più implementazioni aggiuntive di una class o di un metodo specializzato per il tipo primitivo richiesto.

Scala può anche “quasi” reificare i generici usando Manifests che consente loro di recuperare i tipi generici in fase di esecuzione come in C #.

Svantaggi dei generici in stile Java

Una conseguenza è che la versione java funziona solo con tipi di riferimento (o tipi di valore in box) e non con tipi di valore. IMO è il più grande svantaggio dal momento che impedisce i generici ad alte prestazioni in molti scenari e richiede la scrittura manuale di tipi specializzati.

Non garantisce una invarianza come “Questo elenco contiene solo oggetti di tipo x” e richiede controlli di runtime a ogni getter. Il tipo generico esiste davvero.

Quando si utilizza la riflessione non è ansible chiedere un’istanza di un object generico che ha parametri generici.

Vantaggi dei generici in stile Java

Ottieni varianza / può lanciare tra diversi parametri generici.

Java: Generica della varianza del sito di utilizzo da Java 5. Array covarianti interrotti con una syntax diversa a partire da 1.0. Nessuna informazione sul tipo di runtime dei generici.

C #: Generici della varianza del sito d’uso da C # 2.0. Aggiunta la varianza del sito di dichiarazione in C # 4.0. Array covarianti spezzati con una syntax diversa a partire dalla 1.0 (problema identico a Java). generici “reificati” che indicano che le informazioni sul tipo non vengono perse al momento della compilazione.

Scala: Sia la variazione del sito di utilizzo / dichiarazione-sito dalle prime versioni della lingua (almeno dal 2008). Le matrici non sono una funzione linguistica separata, quindi si utilizzano la stessa syntax generica e le regole di varianza del tipo. Alcune raccolte vengono implementate a livello di VM con gli array JVM in modo da ottenere prestazioni di runtime uguali o migliori rispetto al codice Java.

Per elaborare il problema di sicurezza del tipo di array C # / Java: è ansible eseguire il cast di un cane [] su un animale domestico [] e aggiungere un gatto e triggersre un errore di runtime che non viene rilevato durante la compilazione. Scala lo ha implementato correttamente.