Ritorni covarianti
Nota
Questo articolo è una specifica di funzionalità. La specifica funge da documento di progettazione per la funzionalità. Include le modifiche specifiche proposte, insieme alle informazioni necessarie durante la progettazione e lo sviluppo della funzionalità. Questi articoli vengono pubblicati fino a quando le modifiche specifiche proposte non vengono completate e incorporate nella specifica ECMA corrente.
Potrebbero verificarsi alcune discrepanze tra la specifica di funzionalità e l'implementazione completata. Tali differenze vengono acquisite nelle pertinenti note della riunione di progettazione linguistica (LDM) .
Altre informazioni sul processo per l'adozione di speclet di funzionalità nello standard del linguaggio C# sono disponibili nell'articolo sulle specifiche di .
Sommario
Supporto tipi restituiti covarianti. In particolare, consentire l'override di un metodo per dichiarare un tipo di ritorno più derivato rispetto al metodo di cui esegue l'override e permettere allo stesso modo di dichiarare un tipo più derivato per l'override di una proprietà di sola lettura. Le dichiarazioni di override che appaiono in tipi più derivati sono tenute a fornire un tipo restituito almeno altrettanto specifico come quello che appare nelle sostituzioni nei relativi tipi di base. I chiamanti del metodo o della proprietà ricevono in modo statico il tipo restituito più raffinato da una chiamata.
Motivazione
È un modello comune nel codice che devono essere inventati nomi di metodi diversi per aggirare il vincolo della lingua secondo cui l'override deve restituire lo stesso tipo del metodo sovrascritto.
Questo sarebbe utile nel modello di fabbrica. Ad esempio, nella codebase roslyn avremmo
class Compilation ...
{
public virtual Compilation WithOptions(Options options)...
}
class CSharpCompilation : Compilation
{
public override CSharpCompilation WithOptions(Options options)...
}
Progettazione dettagliata
Si tratta di una specifica per tipi di ritorno covarianti in C#. Lo scopo è consentire di eseguire l'override di un metodo per restituire un tipo di ritorno più derivato rispetto al metodo sovrascritto e, così come, consentire di eseguire l'override di una proprietà di sola lettura per restituire un tipo di ritorno più derivato. I chiamanti del metodo o della proprietà riceveranno staticamente il tipo di ritorno più raffinato da una chiamata, e gli override che appaiono in tipi più derivati devono fornire un tipo di ritorno almeno specifico quanto quello che appare negli override nei relativi tipi base.
Override del metodo di classe
Vincolo esistente sui metodi per l'override della classe (§15.6.5)
- Il metodo di sovrascrittura ed il metodo di base sovrascritto hanno lo stesso tipo di ritorno.
viene modificato in
- Il metodo di override deve avere un tipo di restituzione convertibile tramite una conversione identità o (se il metodo ha una restituzione di un valore, non una restituzione di riferimento , vedere §13.1.0.5) una conversione implicita di riferimento al tipo di restituzione del metodo di base sovrascritto.
A tale elenco vengono aggiunti i requisiti aggiuntivi seguenti:
- Il metodo override deve avere un tipo di ritorno che sia convertibile tramite una conversione d'identità o (se il metodo restituisce un valore - non un ref return, §13.1.0.5) attraverso una conversione di riferimento implicita al tipo di ritorno di ogni override del metodo di base sovrascritto dichiarato in un tipo di base (diretto o indiretto) del metodo di override.
- Il tipo restituito dal metodo di override deve essere almeno altrettanto accessibile quanto il metodo di override (domini di accessibilità - §7.5.3).
Questo vincolo consente a un metodo di override in una classe private
di avere un tipo di ritorno private
. Tuttavia, richiede un metodo di override public
in un tipo public
per avere un tipo di restituzione public
.
Proprietà di classe e override dell'indicizzatore
Vincolo esistente per l'override della classe (proprietà §15.7.6)
Una dichiarazione di override di una proprietà deve specificare esattamente gli stessi modificatori di accessibilità e lo stesso nome della proprietà ereditata, e deve esistere una conversione di identità
tra il tipo della proprietà di override e quello della proprietà ereditata. Se la proprietà ereditata ha una sola funzione di accesso (ad esempio, se la proprietà ereditata è di sola lettura o di sola scrittura), la proprietà di override includerà solo tale funzione di accesso. Se la proprietà ereditata include entrambe le funzioni di accesso, ad esempio se la proprietà ereditata è di lettura/scrittura, la proprietà di override può includere una singola funzione di accesso o entrambe le funzioni di accesso.
viene modificato in
Una dichiarazione di proprietà di override specifica gli stessi modificatori di accessibilità e nome della proprietà ereditata e deve essere presente una conversione di identità o (se la proprietà ereditata è di sola lettura e ha un valore restituito, non un riferimento restituito§13.1.0.5) conversione implicita di riferimento dal tipo della proprietà di override al tipo della proprietà ereditata. Se la proprietà ereditata ha una sola funzione di accesso (ad esempio, se la proprietà ereditata è di sola lettura o di sola scrittura), la proprietà di override includerà solo tale funzione di accesso. Se la proprietà ereditata include entrambe le funzioni di accesso, ad esempio se la proprietà ereditata è di lettura/scrittura, la proprietà di override può includere una singola funzione di accesso o entrambe le funzioni di accesso. Il tipo della proprietà sovrascritta deve essere accessibile almeno quanto la proprietà sovrascrivente (domini di accessibilità - §7.5.3).
Il resto della bozza di specifica seguente propone un'ulteriore estensione per la restituzione covariante dei metodi di interfaccia da considerare successivamente.
Override del metodo interface, della proprietà e dell'indicizzatore
Con l'introduzione della funzionalità DIM in C# 8.0, oltre ai tipi di membri consentiti in un'interfaccia, aggiungiamo il supporto anche per i membri override
e i ritorni covarianti. Seguono le regole dei membri di override
come specificato per le classi, con le seguenti differenze:
Il testo seguente nelle classi:
Il metodo sovrascritto da una dichiarazione di override è noto come metodo base sovrascritto . Per un metodo sovrascritto
M
dichiarato in una classeC
, il metodo di base sovrascritto viene determinato esaminando ogni classe base diC
, a partire dalla classe base diretta diC
e continuando con ciascuna classe base diretta successiva, fino a trovare in un dato tipo di classe base almeno un metodo accessibile che abbia la stessa firma diM
dopo la sostituzione degli argomenti di tipo.
viene fornita la specifica corrispondente per le interfacce:
Il metodo sovrascritto da una dichiarazione di override è conosciuto come metodo base sovrascritto . Per un metodo di sovrascrittura
M
dichiarato in un'interfacciaI
, il metodo di base sottoposto a sovrascrittura viene determinato esaminando ogni interfaccia di base diretta o indiretta diI
, raccogliendo l'insieme delle interfacce che dichiarano un metodo accessibile con la stessa firma diM
dopo la sostituzione dei parametri di tipo. Se questo set di interfacce possiede un tipo più derivato , a cui si può effettuare una conversione di riferimento implicita o di identità da ogni tipo in questo set, e quel tipo contiene una dichiarazione univoca di quel metodo, allora si tratta del metodo di base sottoposto a override .
In modo analogo, permettiamo le proprietà e gli indicizzatori override
nelle interfacce come specificato per le classi in §15.7.6 accessor virtuali, sealed, override e astratti.
Ricerca nome
La ricerca del nome in presenza di dichiarazioni di classe override
modifica attualmente il risultato della ricerca del nome imponendo i dettagli del membro trovato dalla dichiarazione di override
più derivata nella gerarchia di classi a partire dal tipo del qualificatore dell'identificatore (o this
quando non è presente alcun qualificatore). Ad esempio, in §12.6.2.2 Parametri corrispondenti abbiamo
Per i metodi virtuali e gli indicizzatori definiti nelle classi, l'elenco di parametri viene selezionato dalla prima dichiarazione o override del membro della funzione trovato quando inizia con il tipo statico del ricevitore ed esegue la ricerca nelle relative classi di base.
a questo aggiungiamo
Per i metodi virtuali e gli indicizzatori definiti nelle interfacce, l'elenco di parametri viene selezionato dalla dichiarazione o dall'override del membro della funzione trovato nel tipo più derivato tra i tipi contenenti la dichiarazione di override del membro della funzione. Si tratta di un errore in fase di compilazione se non esiste alcun tipo univoco di questo tipo.
Per il tipo di risultato di un accesso a una proprietà o a un indicizzatore, il testo esistente
- Se
I
identifica una proprietà dell'istanza, il risultato è un accesso alle proprietà con un'espressione di istanza associata diE
e un tipo associato che corrisponde al tipo della proprietà. Nel caso in cuiT
sia un tipo di classe, il tipo associato viene selezionato dalla prima dichiarazione della proprietà o override trovata a partire daT
, cercando nelle sue classi di base.
è arricchito con
Se
T
è un tipo di interfaccia, il tipo associato viene selezionato dalla dichiarazione o dall'override della proprietà trovata nel tipo più derivato diT
o nelle sue interfacce di base dirette o indirette. Si tratta di un errore in fase di compilazione se non esiste alcun tipo univoco di questo tipo.
È necessario apportare una modifica simile in §12.8.12.3 Accesso all'indicizzatore
In §12.8.10 espressioni di invocazione aumentiamo il testo esistente
- In caso contrario, il risultato è un valore, con un tipo associato al tipo di ritorno del metodo o del delegato. Se la chiamata riguarda un metodo di istanza e il ricevitore è di un tipo di classe
T
, si seleziona il tipo associato dalla prima dichiarazione o override del metodo trovato a partire daT
e ricercando nelle relative classi base.
con
Se la chiamata è di un metodo di istanza e il ricevitore è di un tipo di interfaccia
T
, il tipo associato viene selezionato dalla dichiarazione o dall'override del metodo trovato nell'interfaccia più derivata traT
e le relative interfacce di base dirette e indirette. Si tratta di un errore in fase di compilazione se non esiste alcun tipo univoco di questo tipo.
Implementazioni di interfacce implicite
Questa sezione della specificazione
Ai fini del mapping dell'interfaccia, un membro della classe
A
corrisponde a un membro dell'interfacciaB
quando:
A
eB
sono metodi e i nomi, il tipo e gli elenchi di parametri formali diA
eB
sono identici.A
eB
sono proprietà, il nome e il tipo diA
eB
sono identici eA
hanno le stesse funzioni di accesso diB
(A
è consentito avere funzioni di accesso aggiuntive se non è un'implementazione esplicita del membro dell'interfaccia).A
eB
sono eventi e il nome e il tipo diA
eB
sono identici.A
eB
sono indicizzatori, gli elenchi di parametri formali e di tipo diA
eB
sono identici eA
hanno le stesse funzioni di accesso diB
(A
è consentito avere funzioni di accesso aggiuntive se non è un'implementazione esplicita del membro dell'interfaccia).
viene modificato come segue:
Ai fini del mapping dell'interfaccia, un membro della classe
A
corrisponde a un membro dell'interfacciaB
quando:
A
eB
sono metodi, e gli elenchi dei nomi e dei parametri formali diA
eB
sono identici, mentre il tipo di ritorno diA
è convertibile nel tipo di ritorno diB
tramite un'identità di conversione implicita nel tipo di ritorno diB
.A
eB
sono proprietà, il nome diA
eB
sono identici,A
ha le stesse funzioni di accesso diB
(A
è consentito avere funzioni di accesso aggiuntive se non è un'implementazione esplicita del membro dell'interfaccia) e il tipo diA
è convertibile nel tipo restituito diB
tramite una conversione di identità o, seA
è una proprietà readonly, una conversione di riferimento implicita.A
eB
sono eventi e il nome e il tipo diA
eB
sono identici.A
eB
sono indicizzatori, gli elenchi di parametri formali diA
eB
sono identici,A
hanno le stesse funzioni di accesso diB
(A
è consentito avere funzioni di accesso aggiuntive se non è un'implementazione esplicita del membro dell'interfaccia) e il tipo diA
è convertibile nel tipo restituito diB
tramite una conversione di identità o, seA
è un indicizzatore di sola lettura, una conversione di riferimento implicita.
Questo è tecnicamente un cambiamento che interrompe la compatibilità, poiché il programma seguente oggi stampa "C1.M", ma stamperebbe "C2.M" sotto la revisione proposta.
using System;
interface I1 { object M(); }
class C1 : I1 { public object M() { return "C1.M"; } }
class C2 : C1, I1 { public new string M() { return "C2.M"; } }
class Program
{
static void Main()
{
I1 i = new C2();
Console.WriteLine(i.M());
}
}
A causa di questa modifica, potremmo prendere in considerazione di non supportare i tipi restituiti covarianti nelle implementazioni implicite.
Vincoli per l'implementazione dell'interfaccia
Avremo bisogno di una regola che stabilisca che un'implementazione esplicita dell'interfaccia debba dichiarare un tipo di ritorno non meno derivato di quello dichiarato in qualsiasi override nelle sue interfacce di base.
Implicazioni per la compatibilità delle API
TBD
Problemi aperti
La specifica non indica come il chiamante ottiene il tipo restituito più specifico. Presumibilmente tale operazione verrebbe eseguita in modo simile al modo in cui i chiamanti ottengono le specifiche dei parametri di override più derivate.
Se sono disponibili le interfacce seguenti:
interface I1 { I1 M(); }
interface I2 { I2 M(); }
interface I3: I1, I2 { override I3 M(); }
Si noti che in I3
i metodi I1.M()
e I2.M()
sono stati "uniti". Quando si implementa I3
, è necessario implementarli entrambi insieme.
In genere, è necessaria un'implementazione esplicita per fare riferimento al metodo originale. La domanda è, in una classe
class C : I1, I2, I3
{
C IN.M();
}
Cosa significa qui? Cosa deve essere N?
Suggerisco di consentire l'implementazione di I1.M
o I2.M
(ma non entrambe) e di trattare tale implementazione come implementazione di entrambi.
Svantaggi
- [ ] Ogni modifica della lingua deve giustificare il proprio costo.
- [ ] È necessario assicurarsi che le prestazioni siano ragionevoli, anche nel caso di gerarchie di ereditarietà profonda
- [ ] È necessario assicurarsi che gli artefatti della strategia di traduzione non influiscano sulla semantica del linguaggio, anche quando si utilizza il nuovo IL dai compilatori precedenti.
Alternative
Potremmo rilassare leggermente le regole del linguaggio per consentire, nella fonte,
// Possible alternative. This was not implemented.
abstract class Cloneable
{
public abstract Cloneable Clone();
}
class Digit : Cloneable
{
public override Cloneable Clone()
{
return this.Clone();
}
public new Digit Clone() // Error: 'Digit' already defines a member called 'Clone' with the same parameter types
{
return this;
}
}
Domande non risolte
- [ ] Come funzionano le API compilate per usare questa funzionalità nelle versioni precedenti del linguaggio?
Riunioni di progettazione
- alcune discussioni presso https://github.com/dotnet/roslyn/issues/357.
- https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-01-08.md
- Discussione offline verso una decisione di supportare l'override dei metodi di classe solo in C# 9.0.
C# feature specifications