Costruttori primari
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 riportate nelle note pertinenti del language design meeting (LDM) .
Ulteriori informazioni sul processo per l'adozione degli speclet delle funzionalità nello standard del linguaggio C# si trovano nell'articolo sulle specifiche .
Problema del campione: https://github.com/dotnet/csharplang/issues/2691
Sommario
Le classi e gli struct possono avere un elenco di parametri e la specifica della classe di base può avere un elenco di argomenti. I parametri del costruttore primario sono inclusi nell'ambito di tutta la dichiarazione di classe o struct e, se vengono acquisiti da un membro di funzione o da una funzione anonima, vengono archiviati in modo appropriato ,ad esempio come campi privati indicibili della classe o dello struct dichiarati.
La proposta rivede i costruttori primari già disponibili nei record in relazione a questa funzionalità più generale, con alcuni membri aggiuntivi generati automaticamente.
Motivazione
La capacità di una classe o di una struct in C# di avere più costruttori offre generalità, ma a scapito di un po' di noia nella sintassi della dichiarazione, perché l'input del costruttore e lo stato della classe devono essere separati in modo pulito.
I costruttori primari inseriscono i parametri di un costruttore nell'ambito dell'intera classe o della struttura da usare per l'inizializzazione o direttamente come stato dell'oggetto. Il compromesso è che qualsiasi altro costruttore deve chiamare tramite il costruttore primario.
public class B(bool b) { } // base class
public class C(bool b, int i, string s) : B(b) // b passed to base constructor
{
public int I { get; set; } = i; // i used for initialization
public string S // s used directly in function members
{
get => s;
set => s = value ?? throw new ArgumentNullException(nameof(S));
}
public C(string s) : this(true, 0, s) { } // must call this(...)
}
Progettazione dettagliata
In questo modo viene descritta la progettazione generalizzata tra record e non record, quindi viene descritto in dettaglio il modo in cui vengono specificati i costruttori primari esistenti per i record aggiungendo un set di membri sintetizzati in presenza di un costruttore primario.
Sintassi
Le dichiarazioni di classe e struct vengono aumentate per consentire un elenco di parametri nel nome del tipo, un elenco di argomenti nella classe di base e un corpo costituito solo da un ;
:
class_declaration
: attributes? class_modifier* 'partial'? class_designator identifier type_parameter_list?
parameter_list? class_base? type_parameter_constraints_clause* class_body
;
class_designator
: 'record' 'class'?
| 'class'
class_base
: ':' class_type argument_list?
| ':' interface_type_list
| ':' class_type argument_list? ',' interface_type_list
;
class_body
: '{' class_member_declaration* '}' ';'?
| ';'
;
struct_declaration
: attributes? struct_modifier* 'partial'? 'record'? 'struct' identifier type_parameter_list?
parameter_list? struct_interfaces? type_parameter_constraints_clause* struct_body
;
struct_body
: '{' struct_member_declaration* '}' ';'?
| ';'
;
interface_declaration
: attributes? interface_modifier* 'partial'? 'interface'
identifier variant_type_parameter_list? interface_base?
type_parameter_constraints_clause* interface_body
;
interface_body
: '{' interface_member_declaration* '}' ';'?
| ';'
;
enum_declaration
: attributes? enum_modifier* 'enum' identifier enum_base? enum_body
;
enum_body
: '{' enum_member_declarations? '}' ';'?
| '{' enum_member_declarations ',' '}' ';'?
| ';'
;
Nota: Queste produzioni sostituiscono record_declaration
in record e record_struct_declaration
negli struct record, che entrambi diventano obsoleti.
È un errore che un class_base
abbia un argument_list
se il class_declaration
che racchiude non contiene un parameter_list
. Al massimo una dichiarazione di tipo parziale di una classe o uno struct parziale può fornire un parameter_list
. I parametri nella parameter_list
di una dichiarazione di record
devono essere tutti parametri di valore.
Si noti che in base a questa proposta class_body
, struct_body
, interface_body
e enum_body
possono essere costituiti solo da un ;
.
Una classe o uno struct con un parameter_list
ha un costruttore pubblico implicito la cui firma corrisponde ai parametri valore della dichiarazione di tipo. Questo metodo viene chiamato costruttore primario per il tipo e fa sì che il costruttore senza parametri dichiarato in modo implicito, se presente, venga eliminato. È un errore avere un costruttore primario e un costruttore con la stessa firma già presente nella dichiarazione di tipo.
Ricerca
La ricerca di nomi semplici è stata estesa per gestire i parametri del costruttore primario. Le modifiche sono evidenziate in grassetto nell'estratto seguente:
- In caso contrario, per ogni tipo di istanza
T
(§15.3.2), a partire dal tipo di istanza della dichiarazione di tipo immediatamente racchiudente e continuando con il tipo di istanza di ciascuna dichiarazione di classe o struct involgente (se ce ne sono):
- Se la dichiarazione di
T
include un parametro del costruttore primarioI
e il riferimento si verifica all'interno delargument_list
delclass_base
diT
o all'interno di un inizializzatore di un campo, di una proprietà o di un evento diT
, il risultato è il parametro del costruttore primarioI
- In caso contrario, se
e
è zero e la dichiarazione diT
include un parametro di tipo con nomeI
, il simple_name fa riferimento a tale parametro di tipo.- In caso contrario, se una ricerca di membro (§12.5) di
I
inT
con argomenti di tipoe
produce una corrispondenza:
- Se
T
è il tipo di istanza della classe o del tipo struct che lo racchiude immediatamente e la ricerca identifica uno o più metodi, il risultato è un gruppo di metodi con un'espressione di istanza associata athis
. Se è stato specificato un elenco di argomenti di tipo, viene usato per chiamare un metodo generico (§12.8.10.2).- In caso contrario, se
T
è il tipo di istanza del tipo di classe o struct immediatamente circostante, se la ricerca identifica un membro dell'istanza e se il riferimento si verifica all'interno del blocco di un costruttore d'istanza, di un metodo d'istanza o di un accessore d'istanza (§12.2.1), il risultato corrisponde a un accesso membro (§12.8.7) del tipothis.I
. Questo problema può verificarsi solo quandoe
è zero.- Altrimenti, il risultato è equivalente a un accesso a un membro (§12.8.7) del tipo
T.I
oT.I<A₁, ..., Aₑ>
.- In caso contrario, se la dichiarazione di
T
include un parametro del costruttore primarioI
, il risultato è il parametro del costruttore primarioI
.
La prima aggiunta corrisponde alla modifica subita dai costruttori primari sui recorde garantisce che i parametri del costruttore primario siano trovati prima dei campi corrispondenti all'interno degli inizializzatori e degli argomenti della classe di base. Estende questa regola anche agli inizializzatori statici. Tuttavia, poiché i record hanno sempre un membro dell'istanza con lo stesso nome del parametro, l'estensione può causare solo una modifica in un messaggio di errore. Accesso non valido a un parametro e accesso non valido a un membro dell'istanza.
La seconda aggiunta consente di trovare i parametri del costruttore primario in altre parti del corpo del tipo, ma solo se non oscurati dai membri.
Si tratta di un errore per fare riferimento a un parametro del costruttore primario se il riferimento non si verifica all'interno di uno dei seguenti elementi:
- argomento
nameof
- inizializzatore di un campo dell'istanza, di una proprietà o di un evento del tipo dichiarante (tipo che dichiara il costruttore primario con il parametro ).
-
argument_list
diclass_base
del tipo dichiarante. - corpo di un metodo di istanza (si noti che i costruttori di istanza sono esclusi) del tipo dichiarato.
- corpo di una funzione di accesso dell'istanza del tipo dichiarante.
In altre parole, i parametri del costruttore primario sono inclusi nell'ambito in tutto il corpo del tipo dichiarante. Sono membri ombra del tipo dichiarante all'interno di un inizializzatore di un campo, di una proprietà o di un evento del tipo dichiarante, oppure all'interno del argument_list
di class_base
del tipo dichiarante. Sono oscurate dai membri del tipo dichiarativo altrove.
Pertanto, nella dichiarazione seguente:
class C(int i)
{
protected int i = i; // references parameter
public int I => i; // references field
}
L'inizializzatore per il campo i
fa riferimento al parametro i
, mentre il corpo della proprietà I
fa riferimento al campo i
.
Avvisa di sovrascrittura da parte di un membro della classe base
Il compilatore genererà un avviso sull'utilizzo di un identificatore quando un membro di base oscura un parametro del costruttore primario, se tale parametro del costruttore primario non è stato passato al tipo di base tramite il relativo costruttore.
Un parametro del costruttore primario viene considerato passato al tipo di base tramite il relativo costruttore quando tutte le condizioni seguenti sono vere per un argomento in class_base:
- L'argomento rappresenta una conversione di identità, implicita o esplicita, di un parametro del costruttore principale.
- L'argomento non fa parte di un argomento
params
espanso;
Semantica
Un costruttore primario conduce alla creazione di un costruttore di istanza nel tipo contenitore con i parametri specificati. Se l'class_base
ha un elenco di argomenti, il costruttore dell'istanza generata avrà un inizializzatore base
con lo stesso elenco di argomenti.
I parametri del costruttore primario nelle dichiarazioni di classe/struct possono essere dichiarati ref
, in
o out
. La dichiarazione dei parametri ref
o out
rimane illegale nei costruttori principali delle dichiarazioni di record.
Tutti gli inizializzatori dei membri di istanza nel corpo della classe si trasformeranno in assegnazioni nel costruttore generato.
Se viene fatto riferimento a un parametro del costruttore primario dall'interno di un membro dell'istanza e il riferimento non si trova all'interno di un argomento nameof
, viene acquisito nello stato del tipo di inclusione, in modo che rimanga accessibile dopo la terminazione del costruttore. Una strategia plausibile di implementazione consiste nell'usare un campo privato con un nome offuscato. In uno struct readonly i campi di acquisizione saranno di sola lettura. Pertanto, l'accesso ai parametri acquisiti di uno struct readonly avrà restrizioni simili all'accesso ai campi readonly. L'accesso ai parametri acquisiti all'interno di un membro readonly avrà restrizioni simili all'accesso ai campi dell'istanza nello stesso contesto.
L'acquisizione non è consentita per i parametri con tipo ref e l'acquisizione non è consentita per i parametri ref
, in
o out
. È simile a una limitazione nell'acquisizione nelle lambda.
Se viene fatto riferimento a un parametro del costruttore primario solo dagli inizializzatori dei membri dell'istanza, tali parametri possono fare riferimento direttamente al parametro del costruttore generato, perché vengono eseguiti come parte di esso.
Il costruttore primario eseguirà la sequenza di operazioni seguente:
- I valori dei parametri vengono archiviati nei campi di acquisizione, se presenti.
- Gli inizializzatori di istanza vengono eseguiti
- L'inizializzatore del costruttore di base viene chiamato
I riferimenti ai parametri in qualsiasi codice utente vengono sostituiti con i riferimenti ai campi di acquisizione corrispondenti.
Ad esempio, questa dichiarazione:
public class C(bool b, int i, string s) : B(b) // b passed to base constructor
{
public int I { get; set; } = i; // i used for initialization
public string S // s used directly in function members
{
get => s;
set => s = value ?? throw new ArgumentNullException(nameof(value));
}
public C(string s) : this(true, 0, s) { } // must call this(...)
}
Genera codice simile al seguente:
public class C : B
{
public int I { get; set; }
public string S
{
get => __s;
set => __s = value ?? throw new ArgumentNullException(nameof(value));
}
public C(string s) : this(0, s) { ... } // must call this(...)
// generated members
private string __s; // for capture of s
public C(bool b, int i, string s)
{
__s = s; // capture s
I = i; // run I's initializer
B(b) // run B's constructor
}
}
È un errore che una dichiarazione di costruttore non primario abbia la stessa lista di parametri del costruttore primario. Tutte le dichiarazioni del costruttore non primario devono usare un inizializzatore this
, in modo che venga chiamato il costruttore primario.
I record generano un avviso se un parametro del costruttore primario non viene letto all'interno degli inizializzatori di istanza (eventualmente generati) o dell'inizializzatore di base. Gli avvisi simili verranno segnalati per i parametri del costruttore primario nelle classi e nelle strutture:
- per un parametro passato per valore, se il parametro non viene acquisito e non viene letto in nessun inizializzatore di istanza o di base.
- per un parametro
in
, se il parametro non viene letto in alcun inizializzatore di istanza o inizializzatore di base. - per un parametro
ref
, se il parametro non viene letto o scritto in nessun inizializzatore di istanza o inizializzatore di base.
Nomi semplici identici e nomi di tipo
È disponibile una regola di linguaggio speciale per gli scenari spesso definiti scenari "Colore colore", nomi semplici identici e nomi dei tipi.
In un membro di accesso al modulo
E.I
, seE
è un singolo identificatore e se il significato diE
come simple_name (§12.8.4) è un parametro costante, campo, proprietà, variabile locale o parametro con lo stesso tipo del significato diE
come type_name (§7.8.1), quindi sono consentiti entrambi i significati possibili diE
. La ricerca del membro diE.I
non è mai ambigua, poichéI
deve necessariamente essere membro del tipoE
in entrambi i casi. In altre parole, la regola consente semplicemente l'accesso ai membri statici e ai tipi annidati diE
in cui si sarebbe verificato un errore in fase di compilazione.
Per quanto riguarda i costruttori primari, la regola influisce sul fatto che un identificatore all'interno di un membro di istanza debba essere considerato un riferimento al tipo o come un riferimento al parametro del costruttore primario, che cattura il parametro nello stato del tipo circostante. Anche se "la ricerca del membro di E.I
non è mai ambigua", quando la ricerca restituisce un gruppo di membri, in alcuni casi è impossibile determinare se un accesso membro si riferisce a un membro statico o a un membro dell'istanza senza completare la risoluzione (binding) dell'accesso membro. Allo stesso tempo, l'acquisizione di un parametro del costruttore primario modifica le proprietà del tipo di inclusione in modo che influisca sull'analisi semantica. Ad esempio, il tipo potrebbe diventare non gestito e non soddisfare determinati vincoli per questo motivo.
Esistono anche scenari per cui l'associazione può avere esito positivo in entrambi i casi, a seconda che il parametro venga considerato acquisito o meno. Per esempio:
struct S1(Color Color)
{
public void Test()
{
Color.M1(this); // Error: ambiguity between parameter and typename
}
}
class Color
{
public void M1<T>(T x, int y = 0)
{
System.Console.WriteLine("instance");
}
public static void M1<T>(T x) where T : unmanaged
{
System.Console.WriteLine("static");
}
}
Se si considera il ricevitore Color
come valore, il parametro viene acquisito e "S1" viene gestito. Il metodo statico diventa quindi inapplicabile a causa del vincolo e si chiama il metodo dell'istanza. Tuttavia, se si considera il ricevitore come tipo, non si acquisisce il parametro e "S1" rimane non gestito, entrambi i metodi sono applicabili, ma il metodo statico è "migliore" perché non ha un parametro facoltativo. Nessuna delle due scelte genera un errore, ma ognuna comporta un comportamento distinto.
Dato questo, il compilatore genererà un errore di ambiguità per l'accesso a un membro E.I
quando vengono soddisfatte tutte le condizioni seguenti:
- La ricerca dei membri di
E.I
restituisce un gruppo di membri contenente contemporaneamente membri statici e di istanza. I metodi di estensione applicabili al tipo di ricevitore vengono considerati come metodi di istanza allo scopo di questo controllo. - Se
E
fosse considerato come un nome semplice, anziché un nome di tipo, farebbe riferimento a un parametro del costruttore primario e incorporerebbe il parametro nello stato dell'ambiente circostante.
Avvisi sul doppio utilizzo dello spazio di archiviazione
Se un parametro del costruttore primario viene passato alla base e anche acquisito, esiste un rischio elevato che venga archiviato inavvertitamente due volte nell'oggetto.
Il compilatore genererà un avviso per in
o per un argomento passato per valore in un class_base
argument_list
quando tutte le seguenti condizioni sono vere:
- L'argomento rappresenta una conversione implicita o esplicita dell'identità di un parametro del costruttore primario;
- L'argomento non fa parte di un argomento
params
espanso; - Il parametro del costruttore primario viene acquisito nello stato del tipo di inclusione.
Il compilatore genererà un avviso per un variable_initializer
quando vengono soddisfatte tutte le condizioni seguenti:
- L'inizializzatore di variabile rappresenta una conversione implicita o esplicita dell'identità di un parametro del costruttore primario;
- Il parametro del costruttore primario viene acquisito nello stato del tipo circostante.
Per esempio:
public class Person(string name)
{
public string Name { get; set; } = name; // warning: initialization
public override string ToString() => name; // capture
}
Attributi destinati ai costruttori primari
A https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-03-13.md abbiamo deciso di adottare la proposta di https://github.com/dotnet/csharplang/issues/7047.
La destinazione dell'attributo "method" è consentita su un class_declaration/struct_declaration con parameter_list e fa sì che il costruttore primario corrispondente possieda tale attributo.
Gli attributi con la destinazione method
in un Class_Declaration/Struct_Declaration senza Parameter_List sono ignorati con avviso.
[method: FooAttr] // Good
public partial record Rec(
[property: Foo] int X,
[field: NonSerialized] int Y
);
[method: BarAttr] // warning CS0657: 'method' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'type'. All attributes in this block will be ignored.
public partial record Rec
{
public void Frobnicate()
{
...
}
}
[method: Attr] // Good
public record MyUnit1();
[method: Attr] // warning CS0657: 'method' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'type'. All attributes in this block will be ignored.
public record MyUnit2;
Costruttori primari nei record
Con questa proposta, i record non devono più specificare separatamente un meccanismo di costruttore primario. Al contrario, le dichiarazioni di record (classe e struct) con costruttori primari seguiranno le regole generali, con queste semplici aggiunte:
- Per ogni parametro del costruttore primario, se esiste già un membro con lo stesso nome, deve essere una proprietà o un campo dell'istanza. In caso contrario, una proprietà automatica init-only pubblica con lo stesso nome viene generata con un inizializzatore di proprietà che assegna un valore dal parametro.
- Un decompositore viene sintetizzato con parametri out in modo che corrispondano ai parametri del costruttore primario.
- Se una dichiarazione di costruttore esplicita è un "costruttore di copia", cioè un costruttore che accetta un singolo parametro del tipo contenitore, non è necessario chiamare un inizializzatore
this
e non eseguirà gli inizializzatori dei membri presenti nella dichiarazione del record.
Svantaggi
- La dimensione di allocazione degli oggetti costruiti è meno ovvia, poiché il compilatore determina se allocare un campo per un parametro del costruttore primario in base al testo completo della classe. Questo rischio è simile all'acquisizione implicita delle variabili da espressioni lambda.
- Una tentazione comune (o un modello accidentale) potrebbe essere quella di acquisire lo stesso parametro a più livelli di ereditarietà mentre viene passato lungo la catena del costruttore, invece di assegnargli esplicitamente un campo protetto nella classe base, portando ad allocazioni duplicate per gli stessi dati all'interno degli oggetti. Questo è molto simile al rischio attuale di sovrascrivere le proprietà automatiche con le proprietà automatiche.
- Come proposto qui, non c'è spazio per una logica aggiuntiva che in genere può essere espressa nel corpo del costruttore. L'estensione "corpi del costruttore primario" di seguito risolve il problema.
- Come proposto, la semantica dell'ordine di esecuzione è leggermente diversa rispetto a quella dei costruttori ordinari, ritardando l'inizializzazione dei membri fino a dopo le chiamate alla base. Questo potrebbe essere probabilmente risolto, ma a costo di alcune proposte di estensione (in particolare "corpi del costruttore primario").
- La proposta funziona solo per scenari in cui un singolo costruttore può essere designato come primario.
- Non è possibile esprimere l'accessibilità separata della classe e del costruttore primario. Un esempio è quando tutti i costruttori pubblici delegano a un costruttore privato "build-it-all". Se necessario, la sintassi potrebbe essere proposta in un secondo momento.
Alternative
Nessuna acquisizione
Una versione molto più semplice della funzionalità vieterebbe ai parametri del costruttore primario di essere presenti nei corpi dei membri. Fare riferimento ad essi sarebbe un errore. I campi devono essere dichiarati in modo esplicito se lo spazio di archiviazione è desiderato oltre il codice di inizializzazione.
public class C(string s)
{
public string S1 => s; // Nope!
public string S2 { get; } = s; // Still allowed
}
Questo potrebbe ancora essere sviluppato nella proposta completa in un secondo momento, ed eviterebbe una serie di decisioni e complessità, al costo di rimuovere meno testo predefinito inizialmente, e probabilmente anche sembrando non intuitivo.
Campi generati esplicitamente
Un approccio alternativo prevede che i parametri del costruttore primario generino sempre e visibilmente un campo con lo stesso nome. Invece di chiudere i parametri nello stesso modo delle funzioni locali e anonime, esisterebbe in modo esplicito una dichiarazione membro generata, simile alle proprietà pubbliche generate per i parametri construcor primari nei record. Proprio come per i record, se esiste già un membro appropriato, non ne verrebbe generato uno.
Se il campo generato è privato, potrebbe comunque essere eliminato quando non viene utilizzato come campo all'interno dei corpi collegiali. Nelle classi, tuttavia, un campo privato spesso non è la scelta giusta, a causa della duplicazione dello stato che potrebbe causare nelle classi derivate. Un'opzione consiste nel generare invece un campo protetto nelle classi, favorendo il riutilizzo dello spazio di archiviazione tra i livelli di ereditarietà. Tuttavia, non sarebbe possibile elidere la dichiarazione e incorrere in costi di allocazione per ogni parametro del costruttore primario.
In questo modo, i costruttori primari non di tipo record vengono allineati più strettamente con quelli dei record, in quanto i membri vengono sempre generati (almeno concettualmente), anche se si tratta di tipi diversi di membri con differenti livelli di accessibilità. Ma porterebbe anche a differenze sorprendenti dal modo in cui i parametri e le variabili locali vengono acquisiti altrove in C#. Se fosse stato possibile consentire classi locali, ad esempio, acquisirebbero in modo implicito parametri e variabili locali. La generazione visibile di campi di ombreggiatura per loro non sembra un comportamento ragionevole.
Un altro problema spesso generato con questo approccio è che molti sviluppatori hanno convenzioni di denominazione diverse per parametri e campi. Quale deve essere usato per il parametro del costruttore primario? Entrambe le scelte porterebbero a incoerenze con il resto del codice.
Infine, generare visibilmente le dichiarazioni dei membri è ciò che conta per i record, ma è molto più sorprendente e atipico per classi e strutture non-record. Tutti questi sono i motivi per cui la proposta principale opta per l'acquisizione implicita, con un comportamento sensibile (coerente con i record) per le dichiarazioni esplicite dei membri quando sono desiderate.
Rimuovere i membri dell'istanza dall'ambito dell'inizializzatore
Le regole di ricerca precedenti sono destinate a consentire il comportamento corrente dei parametri del costruttore primario nei record quando un membro corrispondente viene dichiarato manualmente e per spiegare il comportamento del membro generato quando non lo è. Ciò richiede che la ricerca sia diversa tra "ambito di inizializzazione" (inizializzatori di base, inizializzatori di membri) e "ambito del corpo" (corpi membro), che la proposta precedente ottiene modificando quando vengono cercati i parametri del costruttore primario, a seconda della posizione in cui si verifica il riferimento.
Un'osservazione è che il riferimento a un membro dell'istanza con un nome semplice nell'ambito dell'inizializzatore comporta sempre un errore. Invece di nascondere semplicemente i membri dell'istanza in tali posizioni, è sufficiente rimuoverli dall'ambito? In questo modo, non ci sarebbe questo strano ordinamento condizionale di ambiti.
Questa alternativa è probabilmente possibile, ma avrebbe alcune conseguenze che sono piuttosto vaste e potenzialmente indesiderate. Prima di tutto, se si rimuovono i membri dell'istanza dall'ambito dell'inizializzatore, un nome semplice che non corrisponde a un membro dell'istanza ma non a un parametro del costruttore primario potrebbe accidentalmente legarsi a qualcosa al di fuori della dichiarazione del tipo. Questo sembra che raramente sarebbe intenzionale e un errore sarebbe meglio.
Inoltre, membri statici possono fare riferimento all'ambito di inizializzazione. È quindi necessario distinguere tra membri statici e dell'istanza nella ricerca, qualcosa che non facciamo oggi. (Facciamo una distinzione nella risoluzione dei sovraccarichi, ma ciò non è rilevante qui). Quindi sarebbe necessario cambiare anche questo, portando a ulteriori situazioni in cui, ad esempio, nei contesti statici qualcosa si assocerebbe a un contesto più esterno piuttosto che generare un errore per aver trovato un membro dell'istanza.
Tutto sommato questa "semplificazione" porterebbe a una complicazione piuttosto complicata a posteriori di cui nessuno ha chiesto.
Possibili estensioni
Si tratta di variazioni o aggiunte alla proposta di base che può essere considerata in combinazione con essa, o in una fase successiva, se ritenuta utile.
Accesso ai parametri del costruttore primario all'interno dei costruttori
Le regole precedenti fanno sì che sia un errore fare riferimento a un parametro del costruttore primario all'interno di un altro costruttore. Ciò potrebbe essere consentito all'interno del corpo di altri costruttori, anche se, poiché il costruttore primario viene eseguito per primo. Tuttavia, sarebbe necessario rimanere vietato nell'elenco degli argomenti dell'inizializzatore this
.
public class C(bool b, int i, string s) : B(b)
{
public C(string s) : this(b, s) // b still disallowed
{
i++; // could be allowed
}
}
Tale accesso potrebbe comunque comportare l'acquisizione, poiché è l'unico modo in cui il corpo del costruttore potrebbe accedere alla variabile dopo che il costruttore primario sia già stato eseguito.
Il divieto sui parametri del costruttore primario nei parametri di inizializzazione potrebbe essere indebolito per consentirli, ma senza assegnazione certa, ma ciò non sembra utile.
Permetti costruttori senza inizializzatore this
I costruttori senza un inizializzatore this
(ad esempio, con un inizializzatore implicito o esplicito base
) potrebbero essere consentiti. Un costruttore di questo tipo non campo dell'istanza di esecuzione, proprietà e inizializzatori di eventi, in quanto tali valori verranno considerati solo come parte del costruttore primario.
In presenza di tali costruttori di chiamata di base, sono disponibili due opzioni per la gestione dell'acquisizione dei parametri del costruttore primario. Il più semplice consiste nel non consentire completamente l'acquisizione in questa situazione. I parametri del costruttore primario sono per l'inizializzazione solo quando tali costruttori esistono.
In alternativa, se combinato con l'opzione descritta in precedenza per consentire l'accesso ai parametri del costruttore primario all'interno dei costruttori, i parametri potrebbero essere considerati come non assegnati in modo definitivo nel corpo del costruttore, e quelli acquisiti devono essere sicuramente assegnati entro la fine del corpo del costruttore. Si tratta essenzialmente di parametri out impliciti. In questo modo, i parametri del costruttore primario acquisiti avranno sempre un valore sensibile (ovvero assegnato in modo esplicito) al momento in cui vengono utilizzati da altri membri della funzione.
Un vantaggio di questa estensione (in entrambe le forme) è che generalizza pienamente l'attuale esenzione per i "costruttori di copia" nei record, evitando situazioni in cui si osservano parametri del costruttore primario non inizializzati. Essenzialmente, i costruttori che inizializzano l'oggetto in modi alternativi sono efficaci. Le restrizioni correlate all'acquisizione non sono una modifica di rilievo per i costruttori di copia definiti manualmente esistenti nei record, perché i record non acquisiscono mai i parametri del costruttore primario (generano invece campi).
public class C(bool b, int i, string s) : B(b)
{
public int I { get; set; } = i; // i used for initialization
public string S // s used directly in function members
{
get => s;
set => s = value ?? throw new ArgumentNullException(nameof(value));
}
public C(string s2) : base(true) // cannot use `string s` because it would shadow
{
s = s2; // must initialize s because it is captured by S
}
protected C(C original) : base(original) // copy constructor
{
this.s = original.s; // assignment to b and i not required because not captured
}
}
Corpi del costruttore primario
I costruttori stessi spesso contengono logica di convalida dei parametri o altro codice di inizializzazione nontriviale che non può essere espresso come inizializzatori.
È possibile estendere i costruttori primari per consentire la visualizzazione dei blocchi di istruzioni direttamente nel corpo della classe. Tali istruzioni vengono inserite nel costruttore generato nel punto in cui appaiono tra le assegnazioni di inizializzazione e quindi vengono eseguite intersperse con gli inizializzatori. Per esempio:
public class C(int i, string s) : B(s)
{
{
if (i < 0) throw new ArgumentOutOfRangeException(nameof(i));
}
int[] a = new int[i];
public int S => s;
}
Una parte considerevole di questo scenario potrebbe essere adeguatamente trattata se introducessimo "inizializzatori finali" che si eseguono una volta che i costruttori e e tutti gli inizializzatori di oggetti/raccolte siano stati completati. Tuttavia, la convalida degli argomenti è una cosa che idealmente accadrebbe il prima possibile.
I corpi del costruttore primario possono anche offrire una posizione in cui consentire l'uso di un modificatore di accesso per il costruttore primario, permettendogli di deviare dall'accessibilità del tipo contenitore.
Dichiarazioni combinate di parametri e membri
Un'aggiunta possibile e spesso menzionata potrebbe essere quella di consentire l'annotazione dei parametri del costruttore primario in modo che anche dichiarare un membro nel tipo. In genere viene proposto di consentire a un identificatore di accesso sui parametri di attivare la generazione del membro:
public class C(bool b, protected int i, string s) : B(b) // i is a field as well as a parameter
{
void M()
{
... i ... // refers to the field i
... s ... // closes over the parameter s
}
}
Esistono alcuni problemi:
- Cosa accade se si desidera una proprietà, non un campo? La sintassi
{ get; set; }
inline in un elenco di parametri non sembra appetitosa. - Cosa accade se vengono usate convenzioni di denominazione diverse per parametri e campi? Quindi questa funzionalità sarebbe inutile.
Si tratta di una potenziale aggiunta futura che può essere adottata o meno. La proposta attuale lascia aperta la possibilità.
Domande aperte
Ordine di ricerca per i parametri di tipo
La sezione https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/primary-constructors.md#lookup specifica che i parametri di tipo del tipo dichiarante devono venire prima dei parametri del costruttore primario del tipo in ogni contesto in cui tali parametri si trovano nell'ambito. Tuttavia, si dispone già di un comportamento esistente con i record: i parametri del costruttore primario vengono prima dei parametri di tipo negli inizializzatori di base e negli inizializzatori di campo.
Cosa dobbiamo fare su questa discrepanza?
- Modificare le regole in modo che corrispondano al comportamento.
- Modificare il comportamento (modifica dirompente possibile).
- Non consentire a un parametro del costruttore primario di utilizzare il nome del parametro di tipo (una possibile modifica di rilievo).
- Non eseguire alcuna operazione, accettare l'incoerenza tra la specifica e l'implementazione.
Conclusione:
Modificare le regole in modo che corrispondano al comportamento (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-09-25.md#primary-constructors).
Attributi di destinazione dei campi per i parametri del costruttore primario acquisiti
È consigliabile consentire gli attributi di destinazione dei campi per i parametri del costruttore primario acquisiti?
class C1([field: Test] int x) // Parameter is captured, the attribute goes to the capture field
{
public int X => x;
}
class C2([field: Test] int x) // Parameter is not captured, the attribute is ignored with a warning CS0657: 'field' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'param'. All attributes in this block will be ignored.
{
public int X = x;
}
Al momento gli attributi vengono ignorati con un avviso, indipendentemente dalla cattura del parametro.
Si noti che per i record, gli attributi di destinazione dei campi sono consentiti quando una proprietà viene sintetizzata per essa. Gli attributi passano al campo di supporto, quindi.
record R1([field: Test]int X); // Ok, the attribute goes on the backing field
record R2([field: Test]int X) // warning CS0657: 'field' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'param'. All attributes in this block will be ignored.
{
public int X = X;
}
Conclusione:
Non consentito (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-03.md#attributes-on-captured-parameters).
Avvisare sul mascheramento da un membro della classe base
È consigliabile segnalare un avviso quando un membro della base nasconde un parametro del costruttore primario nel membro (vedere https://github.com/dotnet/csharplang/discussions/7109#discussioncomment-5666621)?
Conclusione:
È stata approvata una progettazione alternativa - https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-08.md#primary-constructors
Acquisizione dell'istanza del tipo di inclusione in una chiusura
Quando si fa riferimento a un parametro acquisito nello stato del tipo di inclusione anche in un'espressione lambda all'interno di un inizializzatore di istanza o in un inizializzatore di base, l'espressione lambda e lo stato del tipo di inclusione devono fare riferimento alla stessa posizione per il parametro . Per esempio:
partial class C1
{
public System.Func<int> F1 = Execute1(() => p1++);
}
partial class C1 (int p1)
{
public int M1() { return p1++; }
static System.Func<int> Execute1(System.Func<int> f)
{
_ = f();
return f;
}
}
Poiché una semplice implementazione per catturare un parametro nello stato del tipo si limita a inserire il parametro in un campo di istanza privata, l'espressione lambda deve fare riferimento allo stesso campo. Di conseguenza, deve essere in grado di accedere all'istanza del tipo. Ciò richiede l'acquisizione di this
in una chiusura prima che venga richiamato il costruttore di base. Ciò, a sua volta, comporta un IL sicuro ma non verificabile. È accettabile?
In alternativa, è possibile:
- Non consentire lambda così;
- Oppure, catturare parametri in questo modo all'interno di un'istanza di una classe separata (ancora un'altra chiusura) e condividere tale istanza tra la chiusura e l'istanza del tipo che lo include. Eliminando così la necessità di catturare
this
in una closure.
Conclusione:
È possibile acquisire this
in una chiusura prima che venga richiamato il costruttore di base (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md).
Anche il team di runtime non ha trovato il modello IL problematico.
Assegnazione a this
all'interno di una struct
C# consente di assegnare a this
all'interno di uno struct. Se lo struct acquisisce un parametro del costruttore primario, l'assegnazione sovrascriverà il valore, che potrebbe non essere ovvio per l'utente. Vogliamo segnalare un avviso per assegnazioni di questo tipo?
struct S(int x)
{
int X => x;
void M(S s)
{
this = s; // 'x' is overwritten
}
}
Conclusione:
Consentito, nessun avviso (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md).
Avviso di doppia memoria per l'inizializzazione e acquisizione
Viene visualizzato un avviso se un parametro del costruttore primario viene passato alla base e anche acquisito, perché esiste un rischio elevato che sia archiviato inavvertitamente due volte nell'oggetto.
Sembra che esista un rischio simile se un parametro viene utilizzato per inizializzare un membro ed è anche catturato. Ecco un piccolo esempio:
public class Person(string name)
{
public string Name { get; set; } = name; // initialization
public override string ToString() => name; // capture
}
Per una determinata istanza di Person
, le modifiche apportate a Name
non si rifletteranno nell'output di ToString
, che probabilmente non è voluta dallo sviluppatore.
È necessario introdurre un doppio avviso di archiviazione per questa situazione?
Ecco come funziona:
Il compilatore genererà un avviso per un variable_initializer
quando tutte le condizioni seguenti sono vere:
- L'inizializzatore di variabile rappresenta una conversione implicita o esplicita dell'identità di un parametro del costruttore primario;
- Il parametro del costruttore primario viene memorizzato nello stato del tipo contenitore.
Conclusione:
Approvato, vedere https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-15.md#primary-constructors
Riunioni LDM
- https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-10-17.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-01-18.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-22.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-03-13.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-03.md#primary-constructors
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-08.md#primary-constructors
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-15.md#primary-constructors
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-09-25.md#primary-constructors
C# feature specifications