Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
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 note
Ulteriori dettagli sul processo di adozione di speclet di funzionalità nello standard del linguaggio C# sono disponibili nell'articolo sulle specifiche di .
Problema del campione: https://github.com/dotnet/csharplang/issues/39
Sommario
Questa proposta aggiunge il concetto di init solo proprietà e indicizzatori a C#.
Queste proprietà e indicizzatori possono essere impostate al momento della creazione dell'oggetto, ma diventano effettivamente get solo una volta completata la creazione di oggetti.
Ciò consente un modello molto più flessibile non modificabile in C#.
Motivazione
I meccanismi sottostanti per la creazione di dati non modificabili in C# non sono stati modificati dalla versione 1.0. Rimangono:
- Dichiarare i campi come
readonly. - Dichiarazione di proprietà che contengono solo un accessorio
get.
Questi meccanismi sono efficaci nel consentire la costruzione di dati non modificabili, ma lo fanno incrementando i costi del codice boilerplate dei tipi ed escludendo tali tipi da funzionalità come gli inizializzatori di oggetti e raccolte. Ciò significa che gli sviluppatori devono scegliere tra facilità d'uso e immutabilità.
Un semplice oggetto immutabile come Point richiede il doppio del codice boilerplate per supportare la costruzione rispetto a dichiarare il tipo. Più grande è il tipo più grande è il costo di questa piastra caldaia:
struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
this.X = x;
this.Y = y;
}
}
La funzione di accesso init rende gli oggetti non modificabili più flessibili consentendo al chiamante di modificare i membri durante l'atto di costruzione. Ciò significa che le proprietà non modificabili dell'oggetto possono partecipare agli inizializzatori di oggetti e quindi rimuove la necessità di tutto il codice ripetitivo del costruttore nel tipo. Il tipo Point è ora semplicemente:
struct Point
{
public int X { get; init; }
public int Y { get; init; }
}
Il consumatore può quindi usare gli inizializzatori dell'oggetto per creare l'oggetto
var p = new Point() { X = 42, Y = 13 };
Progettazione dettagliata
funzioni di accesso init
Una proprietà init (o indicizzatore) viene dichiarata usando l'accessor init al posto dell'accessor set:
class Student
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
Una proprietà di istanza contenente un accessor init è considerata impostabile nelle seguenti circostanze, tranne quando si trova in una funzione locale o in un'espressione lambda:
- Durante un inizializzatore di oggetto
- Durante un inizializzatore di espressione
with - All'interno di un costruttore di istanza del tipo contenitore o del tipo derivato, in
thisobase - All'interno dell'accessorio
initdi qualsiasi proprietà, suthisobase - All'interno degli utilizzi degli attributi con parametri denominati
I tempi sopra elencati in cui le funzioni di accesso init sono impostabili vengono indicati in questo documento come la fase di costruzione dell'oggetto.
Ciò significa che la classe Student può essere usata nei modi seguenti:
var s = new Student()
{
FirstName = "Jared",
LastName = "Parosns",
};
s.LastName = "Parsons"; // Error: LastName is not settable
Le regole su quando gli accessori init possono essere impostati si estendono attraverso le gerarchie di tipi. Se il membro è accessibile e l'oggetto è noto per essere nella fase di costruzione, il membro è configurabile. Ciò consente in particolare quanto segue:
class Base
{
public bool Value { get; init; }
}
class Derived : Base
{
public Derived()
{
// Not allowed with get only properties but allowed with init
Value = true;
}
}
class Consumption
{
void Example()
{
var d = new Derived() { Value = true };
}
}
Nel momento in cui viene richiamata una funzione di accesso init, l'istanza è nota per trovarsi nella fase di costruzione aperta. Di conseguenza, una funzione di accesso init può eseguire le azioni seguenti oltre a ciò che una normale funzione di accesso set può eseguire:
- Chiamare altre funzioni di accesso
initdisponibili tramitethisobase - Assegna i campi
readonlydichiarati nello stesso tipo tramitethis
class Complex
{
readonly int Field1;
int Field2;
int Prop1 { get; init; }
int Prop2
{
get => 42;
init
{
Field1 = 13; // okay
Field2 = 13; // okay
Prop1 = 13; // okay
}
}
}
La possibilità di assegnare i campi readonly da un accessor init è limitata ai campi dichiarati allo stesso tipo dell'accessor. Non può essere usato per assegnare campi readonly in un tipo di base. Questa regola garantisce che gli autori di tipi rimangano in controllo sul comportamento di mutabilità del tipo. Gli sviluppatori che non desiderano utilizzare init non possono essere interessati da altri tipi che scelgono di farlo:
class Base
{
internal readonly int Field;
internal int Property
{
get => Field;
init => Field = value; // Okay
}
internal int OtherProperty { get; init; }
}
class Derived : Base
{
internal readonly int DerivedField;
internal int DerivedProperty
{
get => DerivedField;
init
{
DerivedField = 42; // Okay
Property = 0; // Okay
Field = 13; // Error Field is readonly
}
}
public Derived()
{
Property = 42; // Okay
Field = 13; // Error Field is readonly
}
}
Quando init viene usato in una proprietà virtuale, anche tutti gli override devono essere contrassegnati come init. Analogamente, non è possibile sostituire una semplice set con init.
class Base
{
public virtual int Property { get; init; }
}
class C1 : Base
{
public override int Property { get; init; }
}
class C2 : Base
{
// Error: Property must have init to override Base.Property
public override int Property { get; set; }
}
Una dichiarazione di interface può anche partecipare all'inizializzazione dello stile init tramite il modello seguente:
interface IPerson
{
string Name { get; init; }
}
class Init
{
void M<T>() where T : IPerson, new()
{
var local = new T()
{
Name = "Jared"
};
local.Name = "Jraed"; // Error
}
}
Restrizioni di questa funzionalità:
- L'accessor
initpuò essere usato solo nelle proprietà di istanza - Una proprietà non può contenere contemporaneamente un accessore
inite un accessoreset. - Tutte le sovrascritture di una proprietà devono avere
initse la base avevainit. Questa regola si applica anche all'implementazione dell'interfaccia.
Structs readonly
init funzioni di accesso (entrambe le funzioni di accesso implementate automaticamente e le funzioni di accesso implementate manualmente) sono consentite sulle proprietà di readonly structe sulle proprietà readonly.
init le funzioni di accesso non possono essere contrassegnate esse stesse come readonly, né nei tipi readonly, né nei tipi nonreadonlystruct.
readonly struct ReadonlyStruct1
{
public int Prop1 { get; init; } // Allowed
}
struct ReadonlyStruct2
{
public readonly int Prop2 { get; init; } // Allowed
public int Prop3 { get; readonly init; } // Error
}
Codifica dei metadati
Gli accessori della proprietà init saranno emessi come un accessorio standard set con il tipo di ritorno contrassegnato con un modreq di IsExternalInit. Si tratta di un nuovo tipo che avrà la definizione seguente:
namespace System.Runtime.CompilerServices
{
public sealed class IsExternalInit
{
}
}
Il compilatore corrisponderà al tipo in base al nome completo. Non è necessario che venga visualizzato nella libreria principale. Se ci sono più tipi con questo nome, il compilatore risolverà i conflitti nell'ordine seguente:
- Quello definito nel progetto in fase di compilazione
- Quello definito in corelib
Se nessuno di questi elementi esiste, verrà generato un errore di ambiguità del tipo.
La progettazione per IsExternalInit è ulteriormente descritta in in questa edizione
Domande
Modifiche di rilievo
Uno dei punti principali della codifica di questa funzionalità è la domanda seguente:
Si tratta di una modifica che causa un'interruzione binaria per sostituire
initconset?
La sostituzione di init con set e rendendo così una proprietà completamente scrivibile, non rappresenta mai un cambiamento che rompe il codice sorgente in una proprietà non virtuale. Espande semplicemente il set di scenari in cui la proprietà può essere scritta. L'unico comportamento in questione è se questo rimanga un cambiamento che interrompe la compatibilità binaria.
Se vogliamo rendere la modifica da init a set una modifica compatibile sia con il codice sorgente che a livello binario, ciò ci obbliga a prendere una decisione nella scelta tra modreq e attributi sottostante, poiché escluderà i modreq come possibile soluzione. Se d'altra parte questo è visto come non interessante, questo renderà la decisione tra modreq e attributo meno significativa.
Risoluzione Questo scenario non è considerato convincente da LDM.
Modreqs vs. attributi
La strategia di emissione per gli accessor di proprietà init deve scegliere tra l'uso di attributi o di modreqs durante l'emissione dei metadati. Questi hanno compromessi diversi che devono essere presi in considerazione.
L'annotazione di un set accessor con una dichiarazione modreq indica che i compilatori conformi al CLI ignoreranno il set accessor a meno che non supportino il modreq. Ciò significa che solo i compilatori che sono a conoscenza di init leggeranno il membro. I compilatori che non sanno di init ignoreranno la funzione di accesso set e pertanto non considereranno accidentalmente la proprietà come lettura/scrittura.
Lo svantaggio di modreq è che init diventa parte della firma binaria della funzione di accesso set. L'aggiunta o la rimozione di init interromperà la compatibilità binaria dell'applicazione.
L'uso degli attributi per annotare la funzione di accesso set significa che solo i compilatori che conoscono l'attributo sapranno limitare l'accesso. Un compilatore non a conoscenza di init lo vedrà come una semplice proprietà di lettura/scrittura e consentirà l'accesso.
Ciò significa apparentemente che questa decisione è una scelta tra maggiore sicurezza a scapito della compatibilità binaria. Approfondendo un po', la sicurezza aggiuntiva non è esattamente ciò che sembra. Non proteggerà dalle seguenti circostanze:
- Riflessione sui membri di
public - Uso di
dynamic - Compilatori che non riconoscono modreqs
È anche opportuno considerare che, quando si completano le regole di verifica IL per .NET 5, init sarà una di queste regole. Ciò significa che un controllo aggiuntivo verrà ottenuto semplicemente verificando i compilatori che generano IL verificabile.
I linguaggi principali per .NET (C#, F# e VB) verranno tutti aggiornati per riconoscere queste funzioni di accesso init. Di conseguenza, l'unico scenario realistico è quando un compilatore C# 9 genera proprietà init e sono gestite da un set di strumenti precedente, ad esempio C# 8, VB 15 e così via. Questo è il compromesso da considerare e bilanciare con la compatibilità binaria.
Nota Questa discussione si applica principalmente ai membri, non ai campi. Anche se i campi init sono stati rifiutati da LDM, sono ancora interessanti da considerare per la discussione su modreq vs. attributo. La funzionalità init per i campi è un allentamento della restrizione esistente di readonly. Ciò significa che se si emettono i campi come readonly + un attributo non esiste alcun rischio di errori nell'uso del campo da parte dei compilatori meno recenti perché riconoscerebbero già readonly. Di conseguenza, l'uso di un modreq qui non aggiunge alcuna protezione aggiuntiva.
Risoluzione La funzionalità userà un modreq per codificare la proprietà init setter. I fattori interessanti erano (in nessun ordine particolare):
- Desiderio di scoraggiare i compilatori meno recenti dal violare le semantiche
init - Intenzione di far sì che l'aggiunta o la rimozione di
initin una dichiarazione divirtualointerfacesiano entrambe una modifica del codice sorgente e una modifica binaria.
Dato che non c'era un supporto significativo per la rimozione di init come modifica compatibile a livello binario, la scelta di usare modreq è stata semplice.
init vs. initonly
Durante la riunione LDM sono state prese in considerazione tre forme di sintassi:
// 1. Use init
int Option1 { get; init; }
// 2. Use init set
int Option2 { get; init set; }
// 3. Use initonly
int Option3 { get; initonly; }
risoluzione Non c'era una sintassi che fosse particolarmente favorita in LDM.
Un punto che ha ottenuto un'attenzione significativa è stato come la scelta della sintassi potrebbe influenzare la nostra capacità di implementare i membri init come una funzionalità generale in futuro.
Scegliere l'opzione 1 significherebbe che sarebbe difficile definire una proprietà che abbia un metodo di stile initget in futuro. Alla fine è stato deciso che se si decidesse di andare avanti con i membri generali init in futuro, potremmo consentire a init di essere un modificatore nell'elenco delle funzioni di accesso alle proprietà, nonché una scorciatoia per init set. Essenzialmente le due dichiarazioni seguenti sarebbero identiche.
int Property1 { get; init; }
int Property1 { get; init set; }
La decisione è stata presa di procedere con init come funzione di accesso autonoma nell'elenco delle funzioni di accesso alle proprietà.
Avvisa in caso di inizializzazione fallita
Si consideri lo scenario seguente. Un tipo dichiara un init come unico membro che non è impostato nel costruttore. Il codice che costruisce l'oggetto riceve un avviso se non è stato possibile inizializzare il valore?
A questo punto è chiaro che il campo non verrà mai impostato e quindi presenta molte analogie con l'avviso relativo al mancato inizializzazione dei dati private.
Di conseguenza, un avviso sembra avere un valore qui?
L'avviso presenta alcuni svantaggi significativi:
- Complica la storia di compatibilità della modifica di
readonlyininit. - Richiede il trasporto di metadati aggiuntivi per indicare i membri che devono essere inizializzati dal chiamante.
Inoltre, se si ritiene che sia presente un valore nello scenario generale di forzare gli autori di oggetti a ricevere un avviso o un errore su campi specifici, questo probabilmente ha senso come funzionalità generale. Non c'è motivo per cui deve essere limitato solo ai membri init.
Risoluzione Non ci sarà alcun avviso sull'utilizzo dei campi e delle proprietà di init.
LDM vuole avere una discussione più ampia sull'idea di campi e proprietà obbligatori. Questo potrebbe farci tornare e riconsiderare la nostra posizione sui membri del init e sulla convalida.
Consenti init come modificatore di campo
Allo stesso modo init può fungere da accessore di proprietà, può essere usato anche come designazione sui campi per offrire comportamenti simili alle proprietà init.
Ciò consentirebbe di assegnare il campo prima del completamento della costruzione da parte del tipo, dei tipi derivati o degli inizializzatori di oggetti.
class Student
{
public init string FirstName;
public init string LastName;
}
var s = new Student()
{
FirstName = "Jarde",
LastName = "Parsons",
}
s.FirstName = "Jared"; // Error FirstName is readonly
Nei metadati questi campi vengono contrassegnati nello stesso modo dei campi readonly, ma con un attributo aggiuntivo o modreq per indicare che sono campi di stile init.
Risoluzione LDM concorda sul fatto che la proposta sia solida, ma nel complesso lo scenario appariva scollegato dalle proprietà. La decisione è di procedere solo con le proprietà init per il momento.
Questo ha un livello di flessibilità appropriato perché una proprietà init può modificare un campo readonly del tipo dichiarativo della proprietà. Questo verrà riconsiderato se è presente un feedback significativo dei clienti che giustifica lo scenario.
Consenti init come modificatore di tipo
Allo stesso modo, il modificatore readonly può essere applicato a un struct per dichiarare automaticamente tutti i campi come readonly, il modificatore init solo può essere dichiarato in un struct o class per contrassegnare automaticamente tutti i campi come init.
Ciò significa che le due dichiarazioni di tipo seguenti sono equivalenti:
struct Point
{
public init int X;
public init int Y;
}
// vs.
init struct Point
{
public int X;
public int Y;
}
risoluzione Questa funzionalità è troppo carina qui ed è in conflitto con la funzionalità readonly struct su cui si basa. La funzionalità readonly struct è semplice in quanto applica readonly a tutti i membri: campi, metodi e così via... La funzionalità init struct si applica solo alle proprietà. Questo in realtà finisce per creare confusione per gli utenti.
Dato che init è valido solo per determinati aspetti di un tipo, abbiamo rifiutato l'idea di averlo come modificatore di tipo.
Considerazioni
Compatibilità
La funzionalità init è progettata per essere compatibile solo con le proprietà esistenti get. In particolare, è destinato a essere un cambiamento completamente incrementale per una proprietà che attualmente è solo get, ma che desidera modalità di creazione di oggetti più flessibili.
Si consideri ad esempio il tipo seguente:
class Name
{
public string First { get; }
public string Last { get; }
public Name(string first, string last)
{
First = first;
Last = last;
}
}
L'aggiunta di init a queste proprietà è una modifica non di rilievo:
class Name
{
public string First { get; init; }
public string Last { get; init; }
public Name(string first, string last)
{
First = first;
Last = last;
}
}
Verifica IL
Quando .NET Core decide di implementare nuovamente la verifica IL, le regole dovranno essere modificate per tenere conto dei membri init. Questa operazione deve essere inclusa nelle modifiche alle regole per l'accesso non modificabile ai dati readonly.
Le regole di verifica IL dovranno essere suddivise in due parti:
- Consentire ai membri di
initdi definire un camporeadonly. - Determinare quando un membro
initpuò essere chiamato legalmente.
La prima è una semplice regolazione delle regole esistenti. Il verificatore IL può essere configurato per riconoscere i membri di init e a quel punto deve solo considerare come impostare un campo readonly su this in tale membro.
La seconda regola è più complicata. Nel semplice caso di inizializzatori di oggetto la regola è semplice. Dovrebbe essere legale chiamare i membri di init quando il risultato di un'espressione new è ancora nello stack. Ovvero fino a quando il valore non è stato archiviato in un campo locale, di matrice o passato come argomento a un altro metodo, sarà comunque legale chiamare init membri. In questo modo, una volta che il risultato dell'espressione new viene pubblicato su un identificatore denominato (diverso da this), non sarà più consentito richiamare i membri di init.
Il caso più complesso è tuttavia quando si combinano membri init, inizializzatori di oggetti e await. Ciò può causare l'innalzamento temporaneo dell'oggetto appena creato in una macchina a stati e quindi il suo posizionamento in un campo.
var student = new Student()
{
Name = await SomeMethod()
};
Qui il risultato di new Student() verrà trasferito in una macchina a stati come campo prima che si verifichi il processo di Name. Il compilatore dovrà contrassegnare tali campi sollevati in modo che il verificatore IL comprenda che non sono accessibili dall'utente e quindi non violi la semantica di initprevista.
membri init
Il modificatore init potrebbe essere esteso per applicarsi a tutti i membri di istanza. In questo modo si generalizzerebbe il concetto di init nel corso della costruzione di oggetti e consentire ai tipi di dichiarare metodi helper che potrebbero partecipare al processo di costruzione per inizializzare i campi e le proprietà init.
Tali membri avrebbero tutte le restrizioni che un accessor init ha in questo design. La necessità è discutibile e può essere aggiunta in modo sicuro in una versione futura del linguaggio in modo compatibile.
Generare tre metodi di accesso
Un'implementazione potenziale delle proprietà di init consiste nel rendere init completamente separato da set. Ciò significa che una proprietà può avere tre funzioni di accesso diverse: get, sete init.
Questo ha il potenziale vantaggio di consentire l'uso di modreq per imporre la correttezza, mantenendo la compatibilità binaria. L'implementazione sarebbe approssimativamente la seguente:
- Una funzione di accesso
initè sempre generata se è presente unset. Se non definito dallo sviluppatore, è semplicemente un riferimento aset. - L'assegnazione di una proprietà in un inizializzatore di oggetto utilizzerà sempre
initse presente, ma passerà asetse manca.
Ciò significa che uno sviluppatore può sempre eliminare in modo sicuro init da una proprietà.
Il lato negativo di questo design è che è utile solo se init è sempre emesso quando c'è un set. Il linguaggio non può sapere se init è stato eliminato in passato; deve presupporre che lo sia stato e, quindi, init deve essere sempre generato. Ciò causerebbe un'espansione significativa dei metadati e non vale semplicemente il costo della compatibilità qui.
C# feature specifications