15 classi
15.1 Generale
Una classe è una struttura di dati che può contenere membri dati (costanti e campi), membri di funzione (metodi, proprietà, eventi, indicizzatori, operatori, costruttori di istanza, finalizzatori e costruttori statici) e tipi annidati. I tipi di classe supportano l'ereditarietà, un meccanismo in cui una classe derivata può estendere ed specializzare una classe di base.
15.2 Dichiarazioni di classe
15.2.1 Generale
Un class_declaration è un type_declaration (§14.7) che dichiara una nuova classe.
class_declaration
: attributes? class_modifier* 'partial'? 'class' identifier
type_parameter_list? class_base? type_parameter_constraints_clause*
class_body ';'?
;
Un class_declaration è costituito da un set facoltativo di attributi (§22), seguito da un set facoltativo di class_modifiers (§15.2.2), seguito da un modificatore facoltativo (partial
), seguito dalla parola chiave e da un class
che denomina la classe, seguito da un type_parameter_list facoltativo (§15.2.3), seguito da una specifica facoltativa di class_base (§15.2.4)), seguito da un set facoltativo di type_parameter_constraints_clause (§15.2.5), seguito da un class_body (§15.2.6), seguito facoltativamente da un punto e virgola.
Una dichiarazione di classe non deve fornire un type_parameter_constraints_clausea meno che non fornisca anche un type_parameter_list.
Una dichiarazione di classe che fornisce un type_parameter_list è una dichiarazione di classe generica. Inoltre, qualsiasi classe annidata all'interno di una dichiarazione di classe generica o una dichiarazione di struct generica è una dichiarazione di classe generica, poiché gli argomenti di tipo per il tipo contenitore devono essere forniti per creare un tipo costruito (§8.4).
15.2.2 Modificatori classe
15.2.2.1 Generale
Un class_declaration può includere facoltativamente una sequenza di modificatori di classe:
class_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'abstract'
| 'sealed'
| 'static'
| unsafe_modifier // unsafe code support
;
unsafe_modifier (§23.2) è disponibile solo nel codice non sicuro (§23).
Si tratta di un errore in fase di compilazione per visualizzare più volte lo stesso modificatore in una dichiarazione di classe.
Il new
modificatore è consentito nelle classi annidate. Specifica che la classe nasconde un membro ereditato con lo stesso nome, come descritto in §15.3.5. Si tratta di un errore in fase di compilazione per il new
modificatore da visualizzare in una dichiarazione di classe che non è una dichiarazione di classe nidificata.
I public
modificatori , protected
internal
, e private
controllano l'accessibilità della classe . A seconda del contesto in cui si verifica la dichiarazione di classe, alcuni di questi modificatori potrebbero non essere consentiti (§7.5.2).
Quando una dichiarazione di tipo parziale (§15.2.7) include una specifica di accessibilità (tramite i public
modificatori , protected
internal
, e private
), tale specifica deve accettare tutte le altre parti che includono una specifica di accessibilità. Se nessuna parte di un tipo parziale include una specifica di accessibilità, al tipo viene assegnata l'accessibilità predefinita appropriata (§7.5.2).
I abstract
modificatori , sealed
e static
sono descritti nelle sottoclause seguenti.
15.2.2.2 Classi astratte
Il abstract
modificatore viene usato per indicare che una classe è incompleta e che deve essere usata solo come classe di base. Una classe astratta è diversa da una classe non astratta nei modi seguenti:
- Non è possibile creare direttamente un'istanza di una classe astratta ed è un errore in fase di compilazione per usare l'operatore
new
in una classe astratta. Sebbene sia possibile avere variabili e valori i cui tipi in fase di compilazione sono astratti, tali variabili e valori sarannonull
necessariamente o contengono riferimenti a istanze di classi non astratte derivate dai tipi astratti. - Una classe astratta è consentita (ma non necessaria) per contenere membri astratti.
- Una classe astratta non può essere sealed.
Quando una classe non astratta è derivata da una classe astratta, la classe non astratta include le implementazioni effettive di tutti i membri astratti ereditati, ignorando così tali membri astratti.
Esempio: nel codice seguente
abstract class A { public abstract void F(); } abstract class B : A { public void G() {} } class C : B { public override void F() { // Actual implementation of F } }
La classe
A
astratta introduce un metodoF
astratto . La classeB
introduce un metodoG
aggiuntivo , ma poiché non fornisce un'implementazione diF
,B
deve anche essere dichiarata astratta. La classeC
esegue l'overrideF
e fornisce un'implementazione effettiva. Poiché non sono presenti membri astratti inC
,C
è consentito (ma non obbligatorio) essere non astratto.esempio finale
Se una o più parti di una dichiarazione di tipo parziale (§15.2.7) di una classe includono il abstract
modificatore, la classe è astratta. In caso contrario, la classe non è astratta.
15.2.2.3 Classi sealed
Il sealed
modificatore viene usato per impedire la derivazione da una classe . Si verifica un errore in fase di compilazione se una classe sealed viene specificata come classe base di un'altra classe.
Una classe sealed non può essere anche una classe astratta.
Nota: il
sealed
modificatore viene usato principalmente per impedire la derivazione imprevista, ma abilita anche determinate ottimizzazioni di runtime. In particolare, poiché una classe sealed non ha mai classi derivate, è possibile trasformare le chiamate dei membri della funzione virtuale in istanze di classe sealed in chiamate non virtuali. nota finale
Se una o più parti di una dichiarazione di tipo parziale (§15.2.7) di una classe includono il sealed
modificatore, la classe è sealed. In caso contrario, la classe non è bloccato.
15.2.2.4 Classi statiche
15.2.2.4.1 Generale
Il static
modificatore viene usato per contrassegnare la classe dichiarata come classe statica. Una classe statica non deve essere creata un'istanza, non deve essere utilizzata come tipo e deve contenere solo membri statici. Solo una classe statica può contenere dichiarazioni di metodi di estensione (§15.6.10).
Una dichiarazione di classe statica è soggetta alle restrizioni seguenti:
- Una classe statica non include un
sealed
modificatore oabstract
. Tuttavia, poiché non è possibile creare un'istanza o derivare da una classe statica, si comporta come se fosse sia sealed che astratta. - Una classe statica non include una specifica class_base (§15.2.4) e non può specificare in modo esplicito una classe di base o un elenco di interfacce implementate. Una classe statica eredita in modo implicito dal tipo
object
. - Una classe statica deve contenere solo membri statici (§15.3.8).
Nota: tutte le costanti e i tipi annidati vengono classificati come membri statici. nota finale
- Una classe statica non dispone di membri con
protected
,private protected
oprotected internal
l'accessibilità dichiarata.
Si tratta di un errore in fase di compilazione per violare una di queste restrizioni.
Una classe statica non dispone di costruttori di istanza. Non è possibile dichiarare un costruttore di istanza in una classe statica e non viene fornito alcun costruttore di istanza predefinito (§15.11.5) per una classe statica.
I membri di una classe statica non sono statici automaticamente e le dichiarazioni dei membri includono in modo esplicito un static
modificatore (ad eccezione delle costanti e dei tipi annidati). Quando una classe è annidata all'interno di una classe esterna statica, la classe annidata non è una classe statica, a meno che non includa in modo esplicito un static
modificatore.
Se una o più parti di una dichiarazione di tipo parziale (§15.2.7) di una classe includono il static
modificatore, la classe è statica. In caso contrario, la classe non è statica.
15.2.2.4.2 Riferimento ai tipi di classi statiche
Un namespace_or_type_name (§7.8) può fare riferimento a una classe statica se
- Il namespace_or_type_name è in
T
un namespace_or_type_name del formatoT.I
, o - Il nome namespace_or_type è in
T
un typeof_expression (§12.8.18) del formatotypeof(T)
.
Un primary_expression (§12.8) può fare riferimento a una classe statica se
- Il primary_expression è in
E
un member_access (§12.8.7) del formatoE.I
.
In qualsiasi altro contesto, si tratta di un errore in fase di compilazione per fare riferimento a una classe statica.
Nota: ad esempio, si tratta di un errore per una classe statica da utilizzare come classe di base, un tipo costitutivo (§15.3.7) di un membro, un argomento di tipo generico o un vincolo di parametro di tipo. Analogamente, non è possibile usare una classe statica in un tipo di matrice, una nuova espressione, un'espressione cast, un'espressione is, un'espressione come espressione, un'espressione
sizeof
o un'espressione di valore predefinita. nota finale
15.2.3 Parametri di tipo
Un parametro di tipo è un identificatore semplice che indica un segnaposto per un argomento di tipo fornito per creare un tipo costruito. Per constrast, un argomento di tipo (§8.4.2) è il tipo sostituito per il parametro di tipo quando viene creato un tipo costruito.
type_parameter_list
: '<' type_parameters '>'
;
type_parameters
: attributes? type_parameter
| type_parameters ',' attributes? type_parameter
;
type_parameter è definito in §8.5.
Ogni parametro di tipo in una dichiarazione di classe definisce un nome nello spazio di dichiarazione (§7.3) di tale classe. Pertanto, non può avere lo stesso nome di un altro parametro di tipo della classe o di un membro dichiarato in tale classe. Un parametro di tipo non può avere lo stesso nome del tipo stesso.
Due dichiarazioni di tipo generico parziale (nello stesso programma) contribuiscono allo stesso tipo generico non associato se hanno lo stesso nome completo (che include un generic_dimension_specifier (§12.8.18) per il numero di parametri di tipo) (§7.8.3). Due dichiarazioni di tipo parziale specificano lo stesso nome per ogni parametro di tipo, in ordine.
15.2.4 Specifica di base della classe
15.2.4.1 Generale
Una dichiarazione di classe può includere una specifica class_base , che definisce la classe base diretta della classe e le interfacce (§18) implementate direttamente dalla classe .
class_base
: ':' class_type
| ':' interface_type_list
| ':' class_type ',' interface_type_list
;
interface_type_list
: interface_type (',' interface_type)*
;
15.2.4.2 Classi base
Quando un class_type viene incluso nella class_base, specifica la classe base diretta della classe dichiarata. Se una dichiarazione di classe non parziale non ha class_base o se il class_base elenca solo i tipi di interfaccia, si presuppone che la classe base diretta sia object
. Quando una dichiarazione di classe parziale include una specifica della classe base, tale specifica della classe base fa riferimento allo stesso tipo di tutte le altre parti del tipo parziale che includono una specifica della classe base. Se nessuna parte di una classe parziale include una specifica della classe base, la classe base è object
. Una classe eredita i membri dalla classe base diretta, come descritto in §15.3.4.
Esempio: nel codice seguente
class A {} class B : A {}
La classe
A
è detta classe base diretta diB
eB
si dice che sia derivata daA
. PoichéA
non specifica in modo esplicito una classe base diretta, la relativa classe base diretta è implicitamenteobject
.esempio finale
Per un tipo di classe costruito, incluso un tipo annidato dichiarato all'interno di una dichiarazione di tipo generico (§15.3.9.7), se nella dichiarazione di classe generica viene specificata una classe base, la classe base del tipo costruito viene ottenuta sostituendo, per ogni type_parameter nella dichiarazione di classe base, il type_argument corrispondente del tipo costruito.
Esempio: Data delle dichiarazioni di classe generica
class B<U,V> {...} class G<T> : B<string,T[]> {...}
la classe base del tipo
G<int>
costruito sarebbeB<string,int[]>
.esempio finale
La classe base specificata in una dichiarazione di classe può essere un tipo di classe costruito (§8.4). Una classe base non può essere un parametro di tipo autonomo (§8.5), anche se può coinvolgere i parametri di tipo inclusi nell'ambito.
Esempio:
class Base<T> {} // Valid, non-constructed class with constructed base class class Extend1 : Base<int> {} // Error, type parameter used as base class class Extend2<V> : V {} // Valid, type parameter used as type argument for base class class Extend3<V> : Base<V> {}
esempio finale
La classe base diretta di un tipo di classe deve essere accessibile almeno quanto il tipo di classe stesso (§7.5.5). Ad esempio, si tratta di un errore in fase di compilazione per una classe pubblica da derivare da una classe privata o interna.
La classe base diretta di un tipo di classe non deve essere uno dei tipi seguenti: System.Array
, System.Delegate
System.Enum
, System.ValueType
o il dynamic
tipo . Inoltre, una dichiarazione di classe generica non deve essere utilizzata System.Attribute
come classe base diretta o indiretta (§22.2.1).
Per determinare il significato della specifica A
della classe base diretta di una classe B
, si presuppone temporaneamente che la classe base diretta di B
sia object
, che garantisce che il significato di una specifica di classe base non possa dipendere in modo ricorsivo da se stesso.
Esempio: il codice seguente
class X<T> { public class Y{} } class Z : X<Z.Y> {}
è in errore poiché nella specifica
X<Z.Y>
della classe di base la classe base diretta diZ
è considerataobject
, e di conseguenza (dalle regole di §7.8)Z
non è considerato un membroY
.esempio finale
Le classi base di una classe sono la classe base diretta e le relative classi di base. In altre parole, il set di classi base è la chiusura transitiva della relazione di classe base diretta.
Esempio: nell'esempio seguente:
class A {...} class B<T> : A {...} class C<T> : B<IComparable<T>> {...} class D<T> : C<T[]> {...}
Le classi di base di
D<int>
sonoC<int[]>
,B<IComparable<int[]>>
,A
eobject
.esempio finale
Ad eccezione della classe , ogni classe object
ha esattamente una classe base diretta. La object
classe non ha una classe base diretta ed è la classe base finale di tutte le altre classi.
Si tratta di un errore in fase di compilazione per una classe da dipendere da se stesso. Ai fini di questa regola, una classe dipende direttamente dalla relativa classe base diretta (se presente) e dipende direttamente dalla classe di inclusione più vicina all'interno della quale è annidata (se presente). Data questa definizione, il set completo di classi su cui dipende una classe è la chiusura transitiva dell'oggetto direttamente dipende dalla relazione.
Esempio: esempio
class A : A {}
è errato perché la classe dipende da se stessa. Analogamente, l'esempio
class A : B {} class B : C {} class C : A {}
è in errore perché le classi dipendono in modo circolare da se stessi. Infine, l'esempio
class A : B.C {} class B : A { public class C {} }
restituisce un errore in fase di compilazione perché A dipende dalla
B.C
classe di base diretta, che dipende daB
(la relativa classe di inclusione immediata), che dipende circolarmente daA
.esempio finale
Una classe non dipende dalle classi annidate al suo interno.
Esempio: nel codice seguente
class A { class B : A {} }
B
dipende daA
(poichéA
è sia la relativa classe base diretta che la relativa classe immediatamente racchiusa), maA
non dipendeB
(poichéB
non è né una classe base né una classe di inclusione diA
). Pertanto, l'esempio è valido.esempio finale
Non è possibile derivare da una classe sealed.
Esempio: nel codice seguente
sealed class A {} class B : A {} // Error, cannot derive from a sealed class
La classe
B
è in errore perché tenta di derivare dalla classeA
sealed .esempio finale
Implementazioni dell'interfaccia 15.2.4.3
Una specifica class_base può includere un elenco di tipi di interfaccia, nel qual caso si dice che la classe implementi i tipi di interfaccia specificati. Per un tipo di classe costruito, incluso un tipo annidato dichiarato all'interno di una dichiarazione di tipo generico (§15.3.9.7), ogni tipo di interfaccia implementato viene ottenuto sostituendo, per ogni type_parameter nell'interfaccia specificata, il type_argument corrispondente del tipo costruito.
Il set di interfacce per un tipo dichiarato in più parti (§15.2.7) è l'unione delle interfacce specificate in ogni parte. Una particolare interfaccia può essere denominata una sola volta in ogni parte, ma più parti possono denominare le stesse interfacce di base. Vi sarà solo un'implementazione di ogni membro di una determinata interfaccia.
Esempio: nell'esempio seguente:
partial class C : IA, IB {...} partial class C : IC {...} partial class C : IA, IB {...}
il set di interfacce di base per la classe
C
èIA
,IB
eIC
.esempio finale
In genere, ogni parte fornisce un'implementazione delle interfacce dichiarate in tale parte; tuttavia, questo non è un requisito. Una parte può fornire l'implementazione di un'interfaccia dichiarata in una parte diversa.
Esempio:
partial class X { int IComparable.CompareTo(object o) {...} } partial class X : IComparable { ... }
esempio finale
Le interfacce di base specificate in una dichiarazione di classe possono essere costruiti tipi di interfaccia (§8.4, §18.2). Un'interfaccia di base non può essere un parametro di tipo autonomamente, anche se può coinvolgere i parametri di tipo inclusi nell'ambito.
Esempio: il codice seguente illustra come una classe può implementare ed estendere i tipi costruiti:
class C<U, V> {} interface I1<V> {} class D : C<string, int>, I1<string> {} class E<T> : C<int, T>, I1<T> {}
esempio finale
Le implementazioni dell'interfaccia sono illustrate più avanti in §18.6.
15.2.5 Vincoli dei parametri di tipo
Le dichiarazioni di tipo e metodo generico possono facoltativamente specificare vincoli di parametro di tipo includendo type_parameter_constraints_clauses.
type_parameter_constraints_clauses
: type_parameter_constraints_clause
| type_parameter_constraints_clauses type_parameter_constraints_clause
;
type_parameter_constraints_clause
: 'where' type_parameter ':' type_parameter_constraints
;
type_parameter_constraints
: primary_constraint (',' secondary_constraints)? (',' constructor_constraint)?
| secondary_constraints (',' constructor_constraint)?
| constructor_constraint
;
primary_constraint
: class_type nullable_type_annotation?
| 'class' nullable_type_annotation?
| 'struct'
| 'notnull'
| 'unmanaged'
;
secondary_constraint
: interface_type nullable_type_annotation?
| type_parameter nullable_type_annotation?
;
secondary_constraints
: secondary_constraint (',' secondary_constraint)*
;
constructor_constraint
: 'new' '(' ')'
;
Ogni type_parameter_constraints_clause è costituito dal token where
, seguito dal nome di un parametro di tipo, seguito da due punti e dall'elenco di vincoli per tale parametro di tipo. Può essere presente al massimo una where
clausola per ogni parametro di tipo e le where
clausole possono essere elencate in qualsiasi ordine. Analogamente ai get
token e set
in una funzione di accesso alle proprietà, il where
token non è una parola chiave.
L'elenco dei vincoli specificati in una where
clausola può includere uno dei componenti seguenti, in questo ordine: un singolo vincolo primario, uno o più vincoli secondari e il vincolo del costruttore, new()
.
Un vincolo primario può essere un tipo di classe, il vincoloclass
di tipo riferimento , il vincolostruct
di tipo valore , il vincolonotnull
non Null o il vincolounmanaged
di tipo non gestito . Il tipo di classe e il vincolo del tipo di riferimento possono includere il nullable_type_annotation.
Un vincolo secondario può essere un interface_type o un type_parameter, seguito facoltativamente da un nullable_type_annotation. La presenza del nullable_type_annotatione* indica che l'argomento di tipo può essere il tipo riferimento nullable che corrisponde a un tipo riferimento non nullable che soddisfa il vincolo.
Il vincolo di tipo riferimento specifica che un argomento di tipo utilizzato per il parametro di tipo deve essere un tipo riferimento. Tutti i tipi di classe, i tipi di interfaccia, i tipi delegati, i tipi di matrice e i parametri di tipo noti come tipo riferimento (come definito di seguito) soddisfano questo vincolo.
Il tipo di classe, il vincolo di tipo riferimento e i vincoli secondari possono includere l'annotazione del tipo nullable. La presenza o l'assenza di questa annotazione nel parametro di tipo indica le aspettative di nullità per l'argomento di tipo:
- Se il vincolo non include l'annotazione del tipo nullable, l'argomento di tipo deve essere un tipo riferimento non nullable. Un compilatore può generare un avviso se l'argomento di tipo è un tipo riferimento nullable.
- Se il vincolo include l'annotazione del tipo nullable, il vincolo viene soddisfatto sia da un tipo riferimento non nullable che da un tipo riferimento nullable.
Il supporto dei valori Null dell'argomento di tipo non deve corrispondere al valore Nullbility del parametro di tipo. Un compilatore può generare un avviso se la nullabilità del parametro di tipo non corrisponde alla nullabilità dell'argomento di tipo.
Nota: per specificare che un argomento di tipo è un tipo riferimento nullable, non aggiungere l'annotazione del tipo nullable come vincolo (usare
T : class
oT : BaseClass
), ma usareT?
in tutta la dichiarazione generica per indicare il tipo di riferimento nullable corrispondente per l'argomento di tipo type. nota finale
L'annotazione del tipo nullable, ?
, non può essere usata in un argomento di tipo non vincolato.
Per un parametro T
di tipo quando l'argomento di tipo è un tipo C?
riferimento nullable , le istanze di T?
vengono interpretate come C?
, non C??
.
Esempio: negli esempi seguenti viene illustrato come il supporto dei valori Null di un argomento di tipo influisca sul valore Nullbility di una dichiarazione del relativo parametro di tipo:
public class C { } public static class Extensions { public static void M<T>(this T? arg) where T : notnull { } } public class Test { public void M() { C? mightBeNull = new C(); C notNull = new C(); int number = 5; int? missing = null; mightBeNull.M(); // arg is C? notNull.M(); // arg is C? number.M(); // arg is int? missing.M(); // arg is int? } }
Quando l'argomento di tipo è un tipo non nullable, l'annotazione
?
del tipo indica che il parametro è il tipo nullable corrispondente. Quando l'argomento di tipo è già un tipo riferimento nullable, il parametro è lo stesso tipo nullable.esempio finale
Il vincolo non Null specifica che un argomento di tipo utilizzato per il parametro di tipo deve essere un tipo valore non nullable o un tipo riferimento non nullable. È consentito un argomento di tipo che non è un tipo valore non annullabile o un tipo riferimento non annullabile, ma un compilatore potrebbe generare un avviso diagnostico.
Il vincolo di tipo valore specifica che un argomento di tipo utilizzato per il parametro di tipo deve essere un tipo valore non nullable. Tutti i tipi di struct non nullable, i tipi enum e i parametri di tipo con il vincolo di tipo valore soddisfano questo vincolo. Si noti che, anche se classificato come tipo valore, un tipo valore nullable (§8.3.12) non soddisfa il vincolo di tipo valore. Un parametro di tipo con il vincolo di tipo valore non deve avere anche il constructor_constraint, anche se può essere usato come argomento di tipo per un altro parametro di tipo con un constructor_constraint.
Nota: il
System.Nullable<T>
tipo specifica il vincolo di tipo valore non nullable perT
. Pertanto, i tipi costruiti in modo ricorsivo delle formeT??
eNullable<Nullable<T>>
sono vietati. nota finale
Poiché unmanaged
non è una parola chiave, in primary_constraint il vincolo non gestito è sempre sintatticamente ambiguo con class_type. Per motivi di compatibilità, se una ricerca del nome (§12.8.4) del nome unmanaged
ha esito positivo, viene considerata come .class_type
In caso contrario, viene considerato come vincolo non gestito.
Il vincolo di tipo non gestito specifica che un argomento di tipo utilizzato per il parametro di tipo deve essere un tipo non nullable non gestito (§8.8).
I tipi puntatore non possono mai essere argomenti di tipo e non soddisfano alcun vincolo di tipo, anche non gestito, nonostante siano tipi non gestiti.
Se un vincolo è un tipo di classe, un tipo di interfaccia o un parametro di tipo, tale tipo specifica un "tipo base" minimo supportato da ogni argomento di tipo utilizzato per tale parametro di tipo. Ogni volta che viene usato un tipo costruito o un metodo generico, l'argomento di tipo viene controllato in base ai vincoli del parametro di tipo in fase di compilazione. L'argomento di tipo fornito soddisfa le condizioni descritte in §8.4.5.
Un vincolo class_type soddisfa le regole seguenti:
- Il tipo deve essere un tipo di classe.
- Il tipo non deve essere
sealed
. - Il tipo non deve essere uno dei tipi seguenti:
System.Array
oSystem.ValueType
. - Il tipo non deve essere
object
. - Al massimo un vincolo per un determinato parametro di tipo può essere un tipo di classe.
Un tipo specificato come vincolo interface_type soddisfa le regole seguenti:
- Il tipo deve essere un tipo di interfaccia.
- Un tipo non deve essere specificato più di una volta in una determinata
where
clausola.
In entrambi i casi, il vincolo può includere uno qualsiasi dei parametri di tipo della dichiarazione del tipo o del metodo associato come parte di un tipo costruito e può comportare la dichiarazione del tipo.
Qualsiasi tipo di classe o interfaccia specificato come vincolo di parametro di tipo deve essere accessibile almeno quanto accessibile (§7.5.5) come il tipo o il metodo generico dichiarato.
Un tipo specificato come vincolo type_parameter soddisfa le regole seguenti:
- Il tipo deve essere un parametro di tipo.
- Un tipo non deve essere specificato più di una volta in una determinata
where
clausola.
Inoltre, non esistono cicli nel grafico delle dipendenze dei parametri di tipo, in cui la dipendenza è una relazione transitiva definita da:
- Se un parametro
T
di tipo viene usato come vincolo per il parametroS
di tipo,S
dipende da .T
- Se un parametro
S
di tipo dipende da un parametroT
di tipo eT
dipende da un parametroU
S
di tipo, dipende da .U
Data questa relazione, si tratta di un errore in fase di compilazione per un parametro di tipo da dipendere direttamente o indirettamente.
Tutti i vincoli devono essere coerenti tra i parametri di tipo dipendente. Se il parametro S
di tipo dipende dal parametro T
di tipo, allora:
-
T
non deve avere il vincolo di tipo valore. In caso contrario,T
è bloccato in modo efficace, quindiS
verrebbe forzato a essere lo stesso tipo diT
, eliminando la necessità di due parametri di tipo. - Se
S
ha il vincolo di tipo valore,T
non avrà un vincolo class_type . - Se
S
ha un vincolo class_type eA
ha unB
, è necessario eseguire una conversione di riferimento identity o una conversione implicita di riferimento da aA
o una conversione di riferimento implicita daB
B
aA
. - Se
S
dipende anche dal parametroU
di tipo eU
ha unA
eT
ha unB
, sarà presente una conversione di identità o una conversione implicita di riferimento da aA
o una conversione di riferimento implicita daB
B
aA
.
È valido per S
avere il vincolo di tipo valore e T
avere il vincolo del tipo di riferimento. In effetti, questi limiti T
ai tipi System.Object
, System.ValueType
, System.Enum
e a qualsiasi tipo di interfaccia.
Se la where
clausola per un parametro di tipo include un vincolo di costruttore (che ha il formato new()
), è possibile usare l'operatore new
per creare istanze del tipo (§12.8.17.2). Qualsiasi argomento di tipo utilizzato per un parametro di tipo con un vincolo di costruttore deve essere un tipo valore, una classe non astratta con un costruttore pubblico senza parametri o un parametro di tipo con vincolo di tipo valore o costruttore.
Si tratta di un errore in fase di compilazione per type_parameter_constraints con un primary_constraint di struct
o unmanaged
per avere anche un constructor_constraint.
Esempio: di seguito sono riportati esempi di vincoli:
interface IPrintable { void Print(); } interface IComparable<T> { int CompareTo(T value); } interface IKeyProvider<T> { T GetKey(); } class Printer<T> where T : IPrintable {...} class SortedList<T> where T : IComparable<T> {...} class Dictionary<K,V> where K : IComparable<K> where V : IPrintable, IKeyProvider<K>, new() { ... }
L'esempio seguente è in errore perché causa una circolarità nel grafico delle dipendenze dei parametri di tipo:
class Circular<S,T> where S: T where T: S // Error, circularity in dependency graph { ... }
Gli esempi seguenti illustrano altre situazioni non valide:
class Sealed<S,T> where S : T where T : struct // Error, `T` is sealed { ... } class A {...} class B {...} class Incompat<S,T> where S : A, T where T : B // Error, incompatible class-type constraints { ... } class StructWithClass<S,T,U> where S : struct, T where T : U where U : A // Error, A incompatible with struct { ... }
esempio finale
La cancellazione dinamica di un tipo C
Cₓ
è costruita nel modo seguente:
- Se
C
è un tipoOuter.Inner
annidato,Cₓ
è un tipoOuterₓ.Innerₓ
annidato . - Se
C
Cₓ
è un tipo costruito con argomentiG<A¹, ..., Aⁿ>
di tipoA¹, ..., Aⁿ
,Cₓ
è il tipoG<A¹ₓ, ..., Aⁿₓ>
costruito . - Se
C
è un tipo diE[]
matrice,Cₓ
è il tipo diEₓ[]
matrice . - Se
C
è dinamico,Cₓ
èobject
. - In caso contrario,
Cₓ
saràC
.
La classe base efficace di un parametro T
di tipo è definita come segue:
Si supponga R
di essere un set di tipi in modo che:
- Per ogni vincolo di che è un parametro di
T
tipo,R
contiene la relativa classe base efficace. - Per ogni vincolo di
T
che è un tipo di struct,R
contieneSystem.ValueType
. - Per ogni vincolo di che è un tipo di
T
enumerazione,R
contieneSystem.Enum
. - Per ogni vincolo di
T
che è un tipo delegato,R
contiene la cancellazione dinamica. - Per ogni vincolo di
T
che è un tipo di matrice,R
contieneSystem.Array
. - Per ogni vincolo di che è un tipo di
T
classe,R
contiene la cancellazione dinamica.
Risultato
- Se
T
ha il vincolo di tipo valore, la classe base effettiva èSystem.ValueType
. - In caso contrario, se
R
è vuoto, la classe base effettiva èobject
. - In caso contrario, la classe base efficace di
T
è il tipo più incluso (§10.5.3) di setR
. Se il set non include alcun tipo, la classe base effettiva diT
èobject
. Le regole di coerenza assicurano che esista il tipo più incluso.
Se il parametro di tipo è un parametro di tipo del metodo i cui vincoli vengono ereditati dal metodo di base, la classe base effettiva viene calcolata dopo la sostituzione del tipo.
Queste regole assicurano che la classe base efficace sia sempre un class_type.
Il set di interfacce effettivo di un parametro T
di tipo è definito come segue:
- Se
T
non ha secondary_constraints, il set di interfacce effettivo è vuoto. - Se
T
dispone di vincoli interface_type ma non di vincoli type_parameter , il set di interfacce effettivo è il set di eliminazioni dinamiche dei vincoli interface_type . - Se
T
non ha vincoli interface_type ma ha vincoli type_parameter , il set di interfacce effettivo è l'unione dei set di interfacce effettivi dei vincoli type_parameter . - Se
T
dispone sia di vincoli interface_type che di vincoli type_parameter , il set di interfacce effettivo è l'unione del set di cancellazione dinamica dei vincoli interface_type e dei set di interfacce effettivi dei relativi vincoli type_parameter .
Un parametro di tipo è noto come tipo riferimento se ha il vincolo di tipo riferimento o la relativa classe base effettiva non object
è o System.ValueType
. Un parametro di tipo è noto come tipo riferimento non nullable se è noto come tipo riferimento e ha il vincolo di tipo riferimento non nullable.
I valori di un tipo di parametro di tipo vincolato possono essere usati per accedere ai membri dell'istanza impliciti dai vincoli.
Esempio: nell'esempio seguente:
interface IPrintable { void Print(); } class Printer<T> where T : IPrintable { void PrintOne(T x) => x.Print(); }
I metodi di
IPrintable
possono essere richiamati direttamente sux
perchéT
è vincolato a implementareIPrintable
sempre .esempio finale
Quando una dichiarazione di tipo generico parziale include vincoli, i vincoli sono d'accordo con tutte le altre parti che includono vincoli. In particolare, ogni parte che include vincoli deve avere vincoli per lo stesso set di parametri di tipo e per ogni parametro di tipo, i set di vincoli primario, secondario e costruttore devono essere equivalenti. Due set di vincoli sono equivalenti se contengono gli stessi membri. Se nessuna parte di un tipo generico parziale specifica vincoli di parametro di tipo, i parametri di tipo vengono considerati non vincolati.
Esempio:
partial class Map<K,V> where K : IComparable<K> where V : IKeyProvider<K>, new() { ... } partial class Map<K,V> where V : IKeyProvider<K>, new() where K : IComparable<K> { ... } partial class Map<K,V> { ... }
è corretto perché tali parti che includono vincoli (i primi due) specificano in modo efficace lo stesso set di vincoli primario, secondario e costruttore per lo stesso set di parametri di tipo, rispettivamente.
esempio finale
15.2.6 Corpo della classe
Il class_body di una classe definisce i membri di tale classe.
class_body
: '{' class_member_declaration* '}'
;
15.2.7 Dichiarazioni parziali
Il modificatore partial
viene usato per definire una classe, uno struct o un tipo di interfaccia in più parti. Il partial
modificatore è una parola chiave contestuale (§6.4.4) e ha un significato speciale solo prima di una delle parole chiave class
, struct
o interface
.
Ogni parte di una dichiarazione di tipo parziale include un partial
modificatore e deve essere dichiarata nello stesso spazio dei nomi o contenente il tipo delle altre parti. Il partial
modificatore indica che altre parti della dichiarazione di tipo potrebbero esistere altrove, ma l'esistenza di tali parti aggiuntive non è un requisito. È valida per l'unica dichiarazione di un tipo per includere il partial
modificatore. È valido per una sola dichiarazione di un tipo parziale per includere la classe base o le interfacce implementate. Tuttavia, tutte le dichiarazioni di una classe di base o le interfacce implementate devono corrispondere, inclusi i valori Null di qualsiasi argomento di tipo specificato.
Tutte le parti di un tipo parziale devono essere compilate insieme in modo che le parti possano essere unite in fase di compilazione. I tipi parziali in particolare non consentono l'estensione dei tipi già compilati.
I tipi annidati possono essere dichiarati in più parti usando il partial
modificatore . In genere, il tipo contenitore viene dichiarato anche usando partial
e ogni parte del tipo annidato viene dichiarata in una parte diversa del tipo contenitore.
Esempio: la classe parziale seguente viene implementata in due parti, che risiedono in unità di compilazione diverse. La prima parte viene generata da uno strumento di mapping del database mentre la seconda parte viene creata manualmente:
public partial class Customer { private int id; private string name; private string address; private List<Order> orders; public Customer() { ... } } // File: Customer2.cs public partial class Customer { public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted); public bool HasOutstandingOrders() => orders.Count > 0; }
Quando le due parti precedenti vengono compilate insieme, il codice risultante si comporta come se la classe fosse stata scritta come una singola unità, come indicato di seguito:
public class Customer { private int id; private string name; private string address; private List<Order> orders; public Customer() { ... } public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted); public bool HasOutstandingOrders() => orders.Count > 0; }
esempio finale
La gestione degli attributi specificati nei parametri di tipo o di tipo di parti diverse di una dichiarazione parziale è descritta in §22.3.
15.3 Membri della classe
15.3.1 Generale
I membri di una classe sono costituiti dai membri introdotti dai relativi class_member_declaratione dai membri ereditati dalla classe base diretta.
class_member_declaration
: constant_declaration
| field_declaration
| method_declaration
| property_declaration
| event_declaration
| indexer_declaration
| operator_declaration
| constructor_declaration
| finalizer_declaration
| static_constructor_declaration
| type_declaration
;
I membri di una classe sono suddivisi nelle categorie seguenti:
- Costanti, che rappresentano valori costanti associati alla classe (§15.4).
- Campi, che sono le variabili della classe (§15.5).
- Metodi, che implementano i calcoli e le azioni che possono essere eseguiti dalla classe (§15.6).
- Proprietà, che definiscono le caratteristiche denominate e le azioni associate alla lettura e alla scrittura di tali caratteristiche (§15.7).
- Eventi, che definiscono le notifiche che possono essere generate dalla classe (§15.8).
- Indicizzatori, che consentono l'indicizzazione delle istanze della classe nello stesso modo (sintatticamente) delle matrici (§15.9).
- Operatori che definiscono gli operatori di espressione che possono essere applicati alle istanze della classe (§15.10).
- Costruttori di istanze, che implementano le azioni necessarie per inizializzare le istanze della classe (§15.11)
- Finalizzatori, che implementano le azioni da eseguire prima che le istanze della classe vengano eliminate definitivamente (§15.13).
- Costruttori statici, che implementano le azioni necessarie per inizializzare la classe stessa (§15.12).
- Tipi, che rappresentano i tipi locali per la classe (§14.7).
Un class_declaration crea un nuovo spazio di dichiarazione (§7.3) e i type_parameter e i class_member_declarationimmediatamente contenuti nel class_declaration introducono nuovi membri in questo spazio di dichiarazione. Le regole seguenti si applicano a class_member_declarations:
I costruttori di istanza, i finalizzatori e i costruttori statici hanno lo stesso nome della classe che lo racchiude immediatamente. Tutti gli altri membri devono avere nomi diversi dal nome della classe che lo racchiude immediatamente.
Il nome di un parametro di tipo nella type_parameter_list di una dichiarazione di classe deve differire dai nomi di tutti gli altri parametri di tipo nella stessa type_parameter_list e deve differire dal nome della classe e dai nomi di tutti i membri della classe.
Il nome di un tipo deve essere diverso dai nomi di tutti i membri non di tipo dichiarati nella stessa classe. Se due o più dichiarazioni di tipo condividono lo stesso nome completo, le dichiarazioni devono avere il
partial
modificatore (§15.2.7) e queste dichiarazioni si combinano per definire un singolo tipo.
Nota: poiché il nome completo di una dichiarazione di tipo codifica il numero di parametri di tipo, due tipi distinti possono condividere lo stesso nome purché abbiano un numero diverso di parametri di tipo. nota finale
Il nome di una costante, un campo, una proprietà o un evento deve essere diverso dai nomi di tutti gli altri membri dichiarati nella stessa classe.
Il nome di un metodo deve essere diverso dai nomi di tutti gli altri metodi non dichiarati nella stessa classe. Inoltre, la firma (§7.6) di un metodo differisce dalle firme di tutti gli altri metodi dichiarati nella stessa classe e due metodi dichiarati nella stessa classe non devono avere firme che differiscono esclusivamente per
in
,out
eref
.La firma di un costruttore di istanza deve essere diversa dalle firme di tutti gli altri costruttori di istanza dichiarati nella stessa classe e due costruttori dichiarati nella stessa classe non avranno firme che differiscono esclusivamente per
ref
eout
.La firma di un indicizzatore deve essere diversa dalle firme di tutti gli altri indicizzatori dichiarati nella stessa classe.
La firma di un operatore deve essere diversa dalle firme di tutti gli altri operatori dichiarati nella stessa classe.
I membri ereditati di una classe (§15.3.4) non fanno parte dello spazio di dichiarazione di una classe.
Nota: pertanto, una classe derivata può dichiarare un membro con lo stesso nome o firma di un membro ereditato (che in effetti nasconde il membro ereditato). nota finale
Il set di membri di un tipo dichiarato in più parti (§15.2.7) è l'unione dei membri dichiarati in ogni parte. I corpi di tutte le parti della dichiarazione di tipo condividono lo stesso spazio di dichiarazione (§7.3) e l'ambito di ogni membro (§7.7) si estende ai corpi di tutte le parti. Il dominio di accessibilità di qualsiasi membro include sempre tutte le parti del tipo di inclusione; un membro privato dichiarato in una parte è accessibile liberamente da un'altra parte. Si tratta di un errore in fase di compilazione per dichiarare lo stesso membro in più di una parte del tipo, a meno che tale membro non abbia il partial
modificatore.
Esempio:
partial class A { int x; // Error, cannot declare x more than once partial class Inner // Ok, Inner is a partial type { int y; } } partial class A { int x; // Error, cannot declare x more than once partial class Inner // Ok, Inner is a partial type { int z; } }
esempio finale
L'ordine di inizializzazione dei campi può essere significativo all'interno del codice C# e alcune garanzie vengono fornite, come definito in §15.5.6.1. In caso contrario, l'ordinamento dei membri all'interno di un tipo è raramente significativo, ma può essere significativo quando si interagisce con altri linguaggi e ambienti. In questi casi, l'ordinamento dei membri all'interno di un tipo dichiarato in più parti non è definito.
15.3.2 Tipo di istanza
Ogni dichiarazione di classe ha un tipo di istanza associato. Per una dichiarazione di classe generica, il tipo di istanza viene formato creando un tipo costruito (§8.4) dalla dichiarazione di tipo, con ognuno degli argomenti di tipo forniti come parametro di tipo corrispondente. Poiché il tipo di istanza usa i parametri di tipo, può essere usato solo in cui i parametri di tipo si trovano nell'ambito; ovvero all'interno della dichiarazione di classe. Il tipo di istanza è il tipo di this
per il codice scritto all'interno della dichiarazione di classe. Per le classi non generiche, il tipo di istanza è semplicemente la classe dichiarata.
Esempio: di seguito sono illustrate diverse dichiarazioni di classe insieme ai relativi tipi di istanza:
class A<T> // instance type: A<T> { class B {} // instance type: A<T>.B class C<U> {} // instance type: A<T>.C<U> } class D {} // instance type: D
esempio finale
15.3.3 Membri di tipi costruiti
I membri non ereditati di un tipo costruito vengono ottenuti sostituendo, per ogni type_parameter nella dichiarazione membro, il type_argument corrispondente del tipo costruito. Il processo di sostituzione si basa sul significato semantico delle dichiarazioni di tipo e non è semplicemente una sostituzione testuale.
Esempio: data la dichiarazione di classe generica
class Gen<T,U> { public T[,] a; public void G(int i, T t, Gen<U,T> gt) {...} public U Prop { get {...} set {...} } public int H(double d) {...} }
Il tipo
Gen<int[],IComparable<string>>
costruito ha i membri seguenti:public int[,][] a; public void G(int i, int[] t, Gen<IComparable<string>,int[]> gt) {...} public IComparable<string> Prop { get {...} set {...} } public int H(double d) {...}
Il tipo del membro nella dichiarazione
a
di classe generica è "matrice bidimensionale diGen
", quindi il tipo del membroT
nel tipo costruito precedente è "matrice bidimensionale di matrice unidimensionale dia
" oint
.int[,][]
esempio finale
All'interno dei membri della funzione dell'istanza, il tipo di è il tipo di this
istanza (§15.3.2) della dichiarazione contenitore.
Tutti i membri di una classe generica possono usare parametri di tipo di qualsiasi classe di inclusione, direttamente o come parte di un tipo costruito. Quando un particolare tipo costruito chiuso (§8.4.3) viene utilizzato in fase di esecuzione, ogni utilizzo di un parametro di tipo viene sostituito con l'argomento di tipo fornito al tipo costruito.
Esempio:
class C<V> { public V f1; public C<V> f2; public C(V x) { this.f1 = x; this.f2 = this; } } class Application { static void Main() { C<int> x1 = new C<int>(1); Console.WriteLine(x1.f1); // Prints 1 C<double> x2 = new C<double>(3.1415); Console.WriteLine(x2.f1); // Prints 3.1415 } }
esempio finale
15.3.4 Ereditarietà
Una classe eredita i membri della relativa classe base diretta. L'ereditarietà significa che una classe contiene in modo implicito tutti i membri della relativa classe base diretta, ad eccezione dei costruttori di istanza, dei finalizzatori e dei costruttori statici della classe base. Alcuni aspetti importanti dell'ereditarietà sono:
L'ereditarietà è transitiva. Se
C
è derivato daB
eB
viene derivato daA
,C
eredita i membri dichiarati inB
e i membri dichiarati inA
.Una classe derivata estende la relativa classe base diretta. Una classe derivata può aggiungere nuovi membri a quelli ereditati, ma non può rimuovere la definizione di un membro ereditato.
I costruttori di istanza, i finalizzatori e i costruttori statici non vengono ereditati, ma tutti gli altri membri sono, indipendentemente dall'accessibilità dichiarata (§7.5). Tuttavia, a seconda dell'accessibilità dichiarata, i membri ereditati potrebbero non essere accessibili in una classe derivata.
Una classe derivata può nascondere i membri ereditati (§7.7.2.3) dichiarando nuovi membri con lo stesso nome o firma. Tuttavia, nascondendo un membro ereditato non viene rimosso, il membro diventa semplicemente inaccessibile direttamente tramite la classe derivata.
Un'istanza di una classe contiene un set di tutti i campi dell'istanza dichiarati nella classe e le relative classi di base e una conversione implicita (§10.2.8) esiste da un tipo di classe derivata a uno dei relativi tipi di classe base. Pertanto, un riferimento a un'istanza di una classe derivata può essere considerato come riferimento a un'istanza di una delle relative classi di base.
Una classe può dichiarare metodi virtuali, proprietà, indicizzatori ed eventi e classi derivate può eseguire l'override dell'implementazione di questi membri della funzione. Ciò consente alle classi di presentare un comportamento polimorfico in cui le azioni eseguite da una chiamata a un membro della funzione variano a seconda del tipo di runtime dell'istanza tramite cui viene richiamato il membro della funzione.
I membri ereditati di un tipo di classe costruito sono i membri del tipo di classe base immediato (§15.2.4.2), che viene trovato sostituendo gli argomenti di tipo del tipo costruito per ogni occorrenza dei parametri di tipo corrispondenti nel base_class_specification. Questi membri, a loro volta, vengono trasformati sostituendo, per ogni type_parameter nella dichiarazione membro, il type_argument corrispondente del base_class_specification.
Esempio:
class B<U> { public U F(long index) {...} } class D<T> : B<T[]> { public T G(string s) {...} }
Nel codice precedente, il tipo
D<int>
costruito ha un membro pubblicoint
G(string s)
non ereditato ottenuto sostituendo l'argomentoint
di tipo per il parametroT
di tipo .D<int>
ha anche un membro ereditato dalla dichiarazioneB
di classe . Questo membro ereditato è determinato per prima cosa determinando il tipoB<int[]>
di classe base diD<int>
sostituendoint
perT
nella specificaB<T[]>
della classe base . Quindi, come argomento di tipo aB
,int[]
viene sostituito daU
inpublic U F(long index)
, producendo il membropublic int[] F(long index)
ereditato .esempio finale
15.3.5 Il nuovo modificatore
Un class_member_declaration è autorizzato a dichiarare un membro con lo stesso nome o firma di un membro ereditato. In questo caso, il membro della classe derivata viene detto nascondere il membro della classe di base. Per una specifica precisa di quando un membro nasconde un membro ereditato, vedere §7.7.2.3 .
Un membro M
ereditato viene considerato disponibile se M
è accessibile e non esiste un altro membro accessibile ereditato N che nasconde M
già . Nascondere in modo implicito un membro ereditato non è considerato un errore, ma un compilatore genera un avviso a meno che la dichiarazione del membro della classe derivata non includa un modificatore new
per indicare in modo esplicito che il membro derivato deve nascondere il membro di base. Se una o più parti di una dichiarazione parziale (§15.2.7) di un tipo annidato includono il new
modificatore, non viene generato alcun avviso se il tipo annidato nasconde un membro ereditato disponibile.
Se un new
modificatore viene incluso in una dichiarazione che non nasconde un membro ereditato disponibile, viene generato un avviso per tale effetto.
15.3.6 Modificatori di accesso
Un class_member_declaration può avere uno dei tipi consentiti di accessibilità dichiarata (§7.5.2): public
, protected internal
, protected
, private protected
, internal
, o private
. Ad eccezione delle protected internal
combinazioni e private protected
, si tratta di un errore in fase di compilazione per specificare più di un modificatore di accesso. Quando si presuppone che un class_member_declaration non includa modificatori di accesso. private
15.3.7 Tipi costitutivi
I tipi utilizzati nella dichiarazione di un membro vengono chiamati tipi costitutivi di tale membro. I tipi costitutivi possibili sono il tipo di una costante, un campo, una proprietà, un evento o un indicizzatore, il tipo restituito di un metodo o un operatore e i tipi di parametro di un metodo, un indicizzatore, un operatore o un costruttore di istanza. I tipi costitutivi di un membro devono essere accessibili almeno quanto il membro stesso (§7.5.5).
15.3.8 Membri statici e dell'istanza
I membri di una classe sono membri statici o membri dell'istanza.
Nota: in genere, è utile considerare i membri statici come appartenenti a classi e membri dell'istanza come appartenenti a oggetti (istanze di classi). nota finale
Quando una dichiarazione di campo, metodo, proprietà, evento, operatore o costruttore include un static
modificatore, dichiara un membro statico. Inoltre, una dichiarazione costante o di tipo dichiara in modo implicito un membro statico. I membri statici hanno le caratteristiche seguenti:
- Quando si fa riferimento a un membro
M
statico in un member_access (§12.8.7) del formatoE.M
,E
denota un tipo con un membroM
. Si tratta di un errore in fase di compilazione perE
indicare un'istanza di . - Un campo statico in una classe non generica identifica esattamente una posizione di archiviazione. Indipendentemente dal numero di istanze di una classe non generica, è presente una sola copia di un campo statico. Ogni tipo costruito chiuso distinto (§8.4.3) ha un proprio set di campi statici, indipendentemente dal numero di istanze del tipo costruito chiuso.
- Un membro della funzione statica (metodo, proprietà, evento, operatore o costruttore) non opera su un'istanza specifica e si tratta di un errore in fase di compilazione per fare riferimento a questo oggetto in un membro di tale funzione.
Quando una dichiarazione di campo, metodo, proprietà, evento, indicizzatore, costruttore o finalizzatore non include un modificatore statico, dichiara un membro dell'istanza. Un membro dell'istanza viene talvolta chiamato membro non statico. I membri dell'istanza hanno le caratteristiche seguenti:
- Quando si fa riferimento a un membro
M
di istanza in un member_access (§12.8.7) del formatoE.M
,E
denota un'istanza di un tipo con un membroM
. Si tratta di un errore di binding per E per indicare un tipo. - Ogni istanza di una classe contiene un set separato di tutti i campi dell'istanza della classe .
- Un membro della funzione dell'istanza (metodo, proprietà, indicizzatore, costruttore di istanza o finalizzatore) opera su una determinata istanza della classe ed è possibile accedere a questa istanza come
this
(§12.8.14).
Esempio: l'esempio seguente illustra le regole per l'accesso ai membri statici e dell'istanza:
class Test { int x; static int y; void F() { x = 1; // Ok, same as this.x = 1 y = 1; // Ok, same as Test.y = 1 } static void G() { x = 1; // Error, cannot access this.x y = 1; // Ok, same as Test.y = 1 } static void Main() { Test t = new Test(); t.x = 1; // Ok t.y = 1; // Error, cannot access static member through instance Test.x = 1; // Error, cannot access instance member through type Test.y = 1; // Ok } }
Il
F
metodo mostra che in un membro della funzione dell'istanza è possibile usare un simple_name (§12.8.4) per accedere sia ai membri dell'istanza che ai membri statici. IlG
metodo mostra che in un membro di funzione statica si tratta di un errore in fase di compilazione per accedere a un membro dell'istanza tramite un simple_name. IlMain
metodo indica che in un member_access (§12.8.7), i membri dell'istanza devono essere accessibili tramite istanze e i membri statici devono essere accessibili tramite tipi.esempio finale
15.3.9 Tipi annidati
15.3.9.1 Generale
Un tipo dichiarato all'interno di una classe o uno struct è denominato tipo annidato. Un tipo dichiarato all'interno di un'unità di compilazione o di uno spazio dei nomi è denominato tipo non annidato.
Esempio: nell'esempio seguente:
class A { class B { static void F() { Console.WriteLine("A.B.F"); } } }
la classe
B
è un tipo annidato perché è dichiarato all'interno della classeA
e la classeA
è un tipo non annidato perché viene dichiarato all'interno di un'unità di compilazione.esempio finale
15.3.9.2 Nome completo
Il nome completo (§7.8.3) per una dichiarazione S.N
S
di tipo annidata è il nome completo della dichiarazione di tipo in cui il tipo N
viene dichiarato e N
è il nome non qualificato (§7.8.2) della dichiarazione di tipo annidata (incluso qualsiasi generic_dimension_specifier (§12.8.18)).
15.3.9.3 Accessibilità dichiarata
I tipi non annidati possono avere public
o internal
dichiarare l'accessibilità e hanno internal
dichiarato l'accessibilità per impostazione predefinita. I tipi annidati possono avere anche queste forme di accessibilità dichiarate, oltre a una o più forme aggiuntive di accessibilità dichiarata, a seconda che il tipo contenitore sia una classe o uno struct:
- Un tipo annidato dichiarato in una classe può avere uno qualsiasi dei tipi consentiti di accessibilità dichiarata e, come altri membri della classe, per impostazione predefinita viene
private
dichiarata accessibilità. - Un tipo annidato dichiarato in uno struct può avere una qualsiasi delle tre forme di accessibilità dichiarata (
public
,internal
oprivate
) e, come altri membri struct, per impostazione predefinita vieneprivate
dichiarata accessibilità.
Esempio: esempio
public class List { // Private data structure private class Node { public object Data; public Node? Next; public Node(object data, Node? next) { this.Data = data; this.Next = next; } } private Node? first = null; private Node? last = null; // Public interface public void AddToFront(object o) {...} public void AddToBack(object o) {...} public object RemoveFromFront() {...} public object RemoveFromBack() {...} public int Count { get {...} } }
dichiara una classe
Node
annidata privata.esempio finale
15.3.9.4 Nascondere
Un tipo annidato può nascondere (§7.7.2.2) un membro di base. Il new
modificatore (§15.3.5) è consentito nelle dichiarazioni di tipo annidate in modo da poter essere espresso in modo esplicito.
Esempio: esempio
class Base { public static void M() { Console.WriteLine("Base.M"); } } class Derived: Base { public new class M { public static void F() { Console.WriteLine("Derived.M.F"); } } } class Test { static void Main() { Derived.M.F(); } }
mostra una classe
M
nidificata che nasconde il metodoM
definito inBase
.esempio finale
15.3.9.5 accesso
Un tipo annidato e il relativo tipo contenitore non hanno una relazione speciale per quanto riguarda this_access (§12.8.14). In particolare, this
all'interno di un tipo annidato non può essere usato per fare riferimento ai membri dell'istanza del tipo contenitore. Nei casi in cui un tipo annidato deve accedere ai membri dell'istanza del tipo contenitore, è possibile fornire l'accesso this
per l'istanza del tipo contenitore come argomento del costruttore per il tipo annidato.
Esempio: l'esempio seguente
class C { int i = 123; public void F() { Nested n = new Nested(this); n.G(); } public class Nested { C this_c; public Nested(C c) { this_c = c; } public void G() { Console.WriteLine(this_c.i); } } } class Test { static void Main() { C c = new C(); c.F(); } }
mostra questa tecnica. Un'istanza di
C
crea un'istanza diNested
e passa il proprio oggetto alNested
costruttore per fornire l'accesso successivo aiC
membri dell'istanza di .esempio finale
15.3.9.6 Accesso a membri privati e protetti del tipo contenitore
Un tipo annidato ha accesso a tutti i membri accessibili al tipo contenitore, inclusi i membri del tipo contenitore che hanno private
e protected
dichiarato l'accessibilità.
Esempio: esempio
class C { private static void F() => Console.WriteLine("C.F"); public class Nested { public static void G() => F(); } } class Test { static void Main() => C.Nested.G(); }
mostra una classe che contiene una classe
C
Nested
annidata. All'internoNested
di , il metodoG
chiama il metodoF
statico definito inC
eF
ha l'accessibilità dichiarata privata.esempio finale
Un tipo annidato può anche accedere ai membri protetti definiti in un tipo di base del tipo contenitore.
Esempio: nel codice seguente
class Base { protected void F() => Console.WriteLine("Base.F"); } class Derived: Base { public class Nested { public void G() { Derived d = new Derived(); d.F(); // ok } } } class Test { static void Main() { Derived.Nested n = new Derived.Nested(); n.G(); } }
La classe
Derived.Nested
nidificata accede al metodoF
protetto definito nellaDerived
classe di base,Base
, chiamando tramite un'istanza diDerived
.esempio finale
15.3.9.7 Tipi annidati in classi generiche
Una dichiarazione di classe generica può contenere dichiarazioni di tipo annidate. I parametri di tipo della classe contenitore possono essere usati all'interno dei tipi annidati. Una dichiarazione di tipo annidata può contenere parametri di tipo aggiuntivi che si applicano solo al tipo annidato.
Ogni dichiarazione di tipo contenuta all'interno di una dichiarazione di classe generica è implicitamente una dichiarazione di tipo generico. Quando si scrive un riferimento a un tipo annidato all'interno di un tipo generico, il tipo costruito contenente, inclusi gli argomenti di tipo, deve essere denominato. Tuttavia, dall'interno della classe esterna, il tipo annidato può essere utilizzato senza qualifica; Il tipo di istanza della classe esterna può essere usato in modo implicito durante la costruzione del tipo annidato.
Esempio: di seguito sono illustrati tre diversi modi corretti per fare riferimento a un tipo costruito creato da
Inner
. I primi due sono equivalenti:class Outer<T> { class Inner<U> { public static void F(T t, U u) {...} } static void F(T t) { Outer<T>.Inner<string>.F(t, "abc"); // These two statements have Inner<string>.F(t, "abc"); // the same effect Outer<int>.Inner<string>.F(3, "abc"); // This type is different Outer.Inner<string>.F(t, "abc"); // Error, Outer needs type arg } }
esempio finale
Anche se è uno stile di programmazione non valido, un parametro di tipo in un tipo annidato può nascondere un membro o un parametro di tipo dichiarato nel tipo esterno.
Esempio:
class Outer<T> { class Inner<T> // Valid, hides Outer's T { public T t; // Refers to Inner's T } }
esempio finale
15.3.10 Nomi membri riservati
15.3.10.1 Generale
Per facilitare l'implementazione di runtime C# sottostante, per ogni dichiarazione di membro di origine che è una proprietà, un evento o un indicizzatore, l'implementazione riserva due firme di metodo in base al tipo di dichiarazione del membro, al relativo nome e al relativo tipo (§15.3.10.2, §15.3.10.3, §15.10.4). Si tratta di un errore in fase di compilazione per un programma per dichiarare un membro la cui firma corrisponde a una firma riservata da un membro dichiarato nello stesso ambito, anche se l'implementazione di runtime sottostante non usa queste prenotazioni.
I nomi riservati non introducono dichiarazioni, pertanto non partecipano alla ricerca dei membri. Tuttavia, le firme dei metodi riservati associati a una dichiarazione partecipano all'ereditarietà (§15.3.4) e possono essere nascoste con il new
modificatore (§15.3.5).
Nota: la prenotazione di questi nomi ha tre scopi:
- Per consentire all'implementazione sottostante di usare un identificatore ordinario come nome di metodo per ottenere o impostare l'accesso alla funzionalità del linguaggio C#.
- Per consentire ad altri linguaggi di interagire usando un identificatore comune come nome di metodo per ottenere o impostare l'accesso alla funzionalità del linguaggio C#.
- Per garantire che l'origine accettata da un compilatore conforme venga accettata da un altro, rendendo coerenti le specifiche dei nomi dei membri riservati in tutte le implementazioni C#.
nota finale
Anche la dichiarazione di un finalizzatore (§15.13) fa sì che una firma sia riservata (§15.3.10.5).
Alcuni nomi sono riservati per l'uso come nomi di metodi di operatore (§15.3.10.6).
15.3.10.2 Nomi dei membri riservati alle proprietà
Per una proprietà P
(§15.7) di tipo T
, le firme seguenti sono riservate:
T get_P();
void set_P(T value);
Entrambe le firme sono riservate, anche se la proprietà è di sola lettura o di sola scrittura.
Esempio: nel codice seguente
class A { public int P { get => 123; } } class B : A { public new int get_P() => 456; public new void set_P(int value) { } } class Test { static void Main() { B b = new B(); A a = b; Console.WriteLine(a.P); Console.WriteLine(b.P); Console.WriteLine(b.get_P()); } }
Una classe
A
definisce una proprietàP
di sola lettura, riservando quindi le firme perget_P
i metodi eset_P
.A
La classeB
deriva daA
e nasconde entrambe queste firme riservate. L'esempio produce l'output:123 123 456
esempio finale
15.3.10.3 Nomi membri riservati per gli eventi
Per un evento E
(§15.8) di tipo T
delegato , le firme seguenti sono riservate:
void add_E(T handler);
void remove_E(T handler);
15.3.10.4 Nomi membri riservati agli indicizzatori
Per un indicizzatore (§15.9) di tipo T
con parameter-list L
, le firme seguenti sono riservate:
T get_Item(L);
void set_Item(L, T value);
Entrambe le firme sono riservate, anche se l'indicizzatore è di sola lettura o di sola scrittura.
Inoltre, il nome Item
del membro è riservato.
15.3.10.5 Nomi dei membri riservati per i finalizzatori
Per una classe contenente un finalizzatore (§15.13), la firma seguente è riservata:
void Finalize();
15.3.10.6 Nomi dei metodi riservati per gli operatori
I nomi dei metodi seguenti sono riservati. Anche se molti hanno operatori corrispondenti in questa specifica, alcuni sono riservati per l'uso da versioni future, mentre alcuni sono riservati per l'interoperabilità con altre lingue.
Nome metodo | Operatore C# |
---|---|
op_Addition |
+ (binario) |
op_AdditionAssignment |
(riservato) |
op_AddressOf |
(riservato) |
op_Assign |
(riservato) |
op_BitwiseAnd |
& (binario) |
op_BitwiseAndAssignment |
(riservato) |
op_BitwiseOr |
\| |
op_BitwiseOrAssignment |
(riservato) |
op_CheckedAddition |
(riservato per uso futuro) |
op_CheckedDecrement |
(riservato per uso futuro) |
op_CheckedDivision |
(riservato per uso futuro) |
op_CheckedExplicit |
(riservato per uso futuro) |
op_CheckedIncrement |
(riservato per uso futuro) |
op_CheckedMultiply |
(riservato per uso futuro) |
op_CheckedSubtraction |
(riservato per uso futuro) |
op_CheckedUnaryNegation |
(riservato per uso futuro) |
op_Comma |
(riservato) |
op_Decrement |
-- (prefisso e prefisso) |
op_Division |
/ |
op_DivisionAssignment |
(riservato) |
op_Equality |
== |
op_ExclusiveOr |
^ |
op_ExclusiveOrAssignment |
(riservato) |
op_Explicit |
coercizione esplicita (narrowing) |
op_False |
false |
op_GreaterThan |
> |
op_GreaterThanOrEqual |
>= |
op_Implicit |
coercizione implicita (ampliamento) |
op_Increment |
++ (prefisso e prefisso) |
op_Inequality |
!= |
op_LeftShift |
<< |
op_LeftShiftAssignment |
(riservato) |
op_LessThan |
< |
op_LessThanOrEqual |
<= |
op_LogicalAnd |
(riservato) |
op_LogicalNot |
! |
op_LogicalOr |
(riservato) |
op_MemberSelection |
(riservato) |
op_Modulus |
% |
op_ModulusAssignment |
(riservato) |
op_MultiplicationAssignment |
(riservato) |
op_Multiply |
* (binario) |
op_OnesComplement |
~ |
op_PointerDereference |
(riservato) |
op_PointerToMemberSelection |
(riservato) |
op_RightShift |
>> |
op_RightShiftAssignment |
(riservato) |
op_SignedRightShift |
(riservato) |
op_Subtraction |
- (binario) |
op_SubtractionAssignment |
(riservato) |
op_True |
true |
op_UnaryNegation |
- (unario) |
op_UnaryPlus |
+ (unario) |
op_UnsignedRightShift |
(riservato per uso futuro) |
op_UnsignedRightShiftAssignment |
(riservato) |
15.4 Costanti
Una costante è un membro della classe che rappresenta un valore costante: un valore che può essere calcolato in fase di compilazione. Un constant_declaration introduce una o più costanti di un determinato tipo.
constant_declaration
: attributes? constant_modifier* 'const' type constant_declarators ';'
;
constant_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
;
Un constant_declaration può includere un set di attributi (§22), un new
modificatore (§15.3.5) e uno dei tipi consentiti di accessibilità dichiarata (§15.3.6). Gli attributi e i modificatori si applicano a tutti i membri dichiarati dal constant_declaration. Anche se le costanti sono considerate membri statici, un constant_declaration non richiede né consente un static
modificatore. Si tratta di un errore che lo stesso modificatore viene visualizzato più volte in una dichiarazione costante.
Il tipo di un constant_declaration specifica il tipo dei membri introdotti dalla dichiarazione. Il tipo è seguito da un elenco di constant_declarators (§13.6.3), ognuno dei quali introduce un nuovo membro. Un constant_declarator è costituito da un identificatore che denomina il membro, seguito da un token "=
", seguito da un constant_expression (§12.23) che assegna il valore del membro.
Il tipo specificato in una dichiarazione costante deve essere sbyte
, byte
, short
ushort
int
uint
, long
ulong
char
float
double
decimal
, , bool
, , string
, un enum_type o un reference_type. Ogni constant_expression restituisce un valore del tipo di destinazione o di un tipo che può essere convertito nel tipo di destinazione da una conversione implicita (§10.2).
Il tipo di una costante deve essere accessibile almeno quanto la costante stessa (§7.5.5).
Il valore di una costante viene ottenuto in un'espressione utilizzando un simple_name (§12.8.4) o un member_access (§12.8.7).
Una costante può partecipare a un constant_expression. Pertanto, una costante può essere usata in qualsiasi costrutto che richiede un constant_expression.
Nota: esempi di tali costrutti includono
case
etichette,goto case
istruzioni, dichiarazioni di membri,enum
attributi e altre dichiarazioni costanti. nota finale
Nota: come descritto in §12.23, un constant_expression è un'espressione che può essere valutata completamente in fase di compilazione. Poiché l'unico modo per creare un valore non Null di un reference_type diverso da consiste nell'applicare l'operatore e poiché l'operatore
string
non è consentito in unnew
, l'unico valore possibile per le costanti dinew
diverse da è .string
null
nota finale
Quando si desidera un nome simbolico per un valore costante, ma quando il tipo di tale valore non è consentito in una dichiarazione costante o quando il valore non può essere calcolato in fase di compilazione da un constant_expression, è possibile utilizzare un campo readonly (§15.5.3).
Nota: la semantica del controllo delle versioni di
const
ereadonly
differisce (§15.5.3.3). nota finale
Una dichiarazione costante che dichiara più costanti equivale a più dichiarazioni di singole costanti con gli stessi attributi, modificatori e tipo.
Esempio:
class A { public const double X = 1.0, Y = 2.0, Z = 3.0; }
equivale a
class A { public const double X = 1.0; public const double Y = 2.0; public const double Z = 3.0; }
esempio finale
Le costanti possono dipendere da altre costanti all'interno dello stesso programma, purché le dipendenze non siano di natura circolare.
Esempio: nel codice seguente
class A { public const int X = B.Z + 1; public const int Y = 10; } class B { public const int Z = A.Y + 1; }
Un compilatore deve prima valutare
A.Y
, quindi valutareB.Z
e infine valutareA.X
, producendo i valori10
,11
e12
.esempio finale
Le dichiarazioni costanti possono dipendere da costanti di altri programmi, ma tali dipendenze sono possibili solo in una direzione.
Esempio: facendo riferimento all'esempio precedente, se
A
eB
sono stati dichiarati in programmi separati, sarebbe possibileA.X
dipendere daB.Z
, maB.Z
non poteva quindi dipendere simultaneamente daA.Y
. esempio finale
15.5 Campi
15.5.1 Generale
Un campo è un membro che rappresenta una variabile associata a un oggetto o a una classe. Un field_declaration introduce uno o più campi di un determinato tipo.
field_declaration
: attributes? field_modifier* type variable_declarators ';'
;
field_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'readonly'
| 'volatile'
| unsafe_modifier // unsafe code support
;
variable_declarators
: variable_declarator (',' variable_declarator)*
;
variable_declarator
: identifier ('=' variable_initializer)?
;
unsafe_modifier (§23.2) è disponibile solo nel codice non sicuro (§23).
Un field_declaration può includere un set di attributi (§22), un new
modificatore (§15.3.5), una combinazione valida dei quattro modificatori di accesso (§15.3.6) e un static
modificatore (§15.5.2). Inoltre, un field_declaration può includere un readonly
modificatore (§15.5.3) o un volatile
modificatore (§15.5.4), ma non entrambi. Gli attributi e i modificatori si applicano a tutti i membri dichiarati dal field_declaration. Si tratta di un errore che indica che lo stesso modificatore viene visualizzato più volte in un field_declaration.
Il tipo di un field_declaration specifica il tipo dei membri introdotti dalla dichiarazione. Il tipo è seguito da un elenco di variable_declarators, ognuno dei quali introduce un nuovo membro. Un variable_declarator è costituito da un identificatore che assegna un nome al membro, seguito facoltativamente da un token "=
" e da un variable_initializer (§15.5.6) che assegna il valore iniziale di tale membro.
Il tipo di un campo deve essere accessibile almeno quanto il campo stesso (§7.5.5).
Il valore di un campo viene ottenuto in un'espressione utilizzando un simple_name (§12.8.4), un member_access (§12.8.7) o un base_access (§12.8.15). Il valore di un campo non di sola lettura viene modificato utilizzando un'assegnazione (§12.21). Il valore di un campo non di sola lettura può essere ottenuto e modificato utilizzando operatori di incremento e decremento postfissi (§12.8.16) e operatori di incremento e decremento del prefisso (§12.9.6).
Una dichiarazione di campo che dichiara più campi equivale a più dichiarazioni di singoli campi con gli stessi attributi, modificatori e tipo.
Esempio:
class A { public static int X = 1, Y, Z = 100; }
equivale a
class A { public static int X = 1; public static int Y; public static int Z = 100; }
esempio finale
15.5.2 Campi statici e di istanza
Quando una dichiarazione di campo include un static
modificatore, i campi introdotti dalla dichiarazione sono campi statici. Quando non è presente alcun static
modificatore, i campi introdotti dalla dichiarazione sono campi di istanza. I campi statici e i campi dell'istanza sono due dei diversi tipi di variabili (§9) supportati da C#, e a volte vengono definiti rispettivamente variabili statiche e variabili di istanza.
Come spiegato in §15.3.8, ogni istanza di una classe contiene un set completo dei campi dell'istanza della classe, mentre è presente un solo set di campi statici per ogni classe non generica o tipo costruito chiuso, indipendentemente dal numero di istanze della classe o del tipo costruito chiuso.
15.5.3 Campi di sola lettura
15.5.3.1 Generale
Quando un field_declaration include un modificatore, i campi introdotti dalla dichiarazione sono readonly
di sola lettura. Le assegnazioni dirette ai campi di sola lettura possono essere eseguite solo come parte di tale dichiarazione o in un costruttore di istanza o in un costruttore statico nella stessa classe. Un campo di sola lettura può essere assegnato più volte in questi contesti. In particolare, le assegnazioni dirette a un campo di sola lettura sono consentite solo nei contesti seguenti:
- Nella variable_declarator che introduce il campo (includendo un variable_initializer nella dichiarazione).
- Per un campo di istanza, nei costruttori di istanza della classe che contiene la dichiarazione di campo; per un campo statico, nel costruttore statico della classe che contiene la dichiarazione di campo. Si tratta anche degli unici contesti in cui è valido passare un campo readonly come parametro di output o riferimento.
Il tentativo di assegnare a un campo readonly o di passarlo come parametro di output o riferimento in qualsiasi altro contesto è un errore in fase di compilazione.
15.5.3.2 Uso di campi di sola lettura statici per le costanti
Un campo di sola lettura statico è utile quando si desidera un nome simbolico per un valore costante, ma quando il tipo del valore non è consentito in una dichiarazione const o quando il valore non può essere calcolato in fase di compilazione.
Esempio: nel codice seguente
public class Color { public static readonly Color Black = new Color(0, 0, 0); public static readonly Color White = new Color(255, 255, 255); public static readonly Color Red = new Color(255, 0, 0); public static readonly Color Green = new Color(0, 255, 0); public static readonly Color Blue = new Color(0, 0, 255); private byte red, green, blue; public Color(byte r, byte g, byte b) { red = r; green = g; blue = b; } }
I
Black
membri ,White
Red
Green
, , eBlue
non possono essere dichiarati come membri const perché i relativi valori non possono essere calcolati in fase di compilazione. Tuttavia, dichiararlestatic readonly
ha invece molto lo stesso effetto.esempio finale
15.5.3.3 Controllo delle versioni delle costanti e dei campi statici di sola lettura
Le costanti e i campi di sola lettura hanno una semantica di controllo delle versioni binaria diversa. Quando un'espressione fa riferimento a una costante, il valore della costante viene ottenuto in fase di compilazione, ma quando un'espressione fa riferimento a un campo di sola lettura, il valore del campo non viene ottenuto fino alla fase di esecuzione.
Esempio: si consideri un'applicazione costituita da due programmi distinti:
namespace Program1 { public class Utils { public static readonly int x = 1; } }
e
namespace Program2 { class Test { static void Main() { Console.WriteLine(Program1.Utils.X); } } }
Gli
Program1
spazi dei nomi eProgram2
indicano due programmi compilati separatamente. PoichéProgram1.Utils.X
è dichiarato comestatic readonly
campo, l'output del valore dell'istruzioneConsole.WriteLine
non è noto in fase di compilazione, ma viene ottenuto in fase di esecuzione. Pertanto, se il valore diX
viene modificato eProgram1
viene ricompilato, l'istruzioneConsole.WriteLine
restituirà il nuovo valore anche seProgram2
non viene ricompilato. Tuttavia, se fosseX
stata una costante, il valore diX
sarebbe stato ottenuto al momentoProgram2
della compilazione e rimarrà invariato dalle modifiche apportateProgram1
fino aProgram2
quando non viene ricompilato.esempio finale
15.5.4 Campi volatili
Quando un field_declaration include un volatile
modificatore, i campi introdotti da tale dichiarazione sono campi volatili. Per i campi non volatili, le tecniche di ottimizzazione che riordinano le istruzioni possono causare risultati imprevisti e imprevedibili nei programmi multithread che accedono ai campi senza sincronizzazione, ad esempio quelli forniti dal lock_statement (§13.13). Queste ottimizzazioni possono essere eseguite dal compilatore, dal sistema di runtime o dall'hardware. Per i campi volatili, tali ottimizzazioni di ordinamento sono limitate:
- Una lettura di un campo volatile è detta lettura volatile. Una lettura volatile ha una "semantica di acquisizione"; ovvero, è garantito che si verifichi prima di tutti i riferimenti alla memoria che si verificano dopo di esso nella sequenza di istruzioni.
- Una scrittura di un campo volatile è detta scrittura volatile. Una scrittura volatile ha "semantica di rilascio"; ovvero, è garantito che si verifichi dopo tutti i riferimenti di memoria prima dell'istruzione di scrittura nella sequenza di istruzioni.
Queste limitazioni garantiscono che tutti i thread considereranno scritture di tipo volatile eseguite da altri thread nell'ordine in cui sono stati eseguiti. Un'implementazione conforme non è necessaria per fornire un singolo ordinamento totale di scritture volatili, come illustrato da tutti i thread di esecuzione. Il tipo di un campo volatile deve essere uno dei seguenti:
- Un reference_type.
- Un type_parameter noto come tipo riferimento (§15.2.5).
- Tipo
byte
,sbyte
short
,ushort
,int
,uint
,char
,float
,bool
,System.IntPtr
, oSystem.UIntPtr
. - Un enum_type con un tipo di enum_base di
byte
, ,sbyte
short
,ushort
int
, ouint
.
Esempio: esempio
class Test { public static int result; public static volatile bool finished; static void Thread2() { result = 143; finished = true; } static void Main() { finished = false; // Run Thread2() in a new thread new Thread(new ThreadStart(Thread2)).Start(); // Wait for Thread2() to signal that it has a result // by setting finished to true. for (;;) { if (finished) { Console.WriteLine($"result = {result}"); return; } } } }
produce l'output:
result = 143
In questo esempio il metodo
Main
avvia un nuovo thread che esegue il metodoThread2
. Questo metodo archivia un valore in un campo non volatile denominatoresult
, quindi archiviatrue
nel campofinished
volatile . Il thread principale attende che il campofinished
venga impostato sutrue
, quindi legge il camporesult
. Poichéfinished
è stato dichiaratovolatile
, il thread principale leggerà il valore143
dal camporesult
. Se il campofinished
non fosse stato dichiaratovolatile
, sarebbe consentito che l'archivioresult
sia visibile al thread principale dopo l'archivio infinished
e quindi per il thread principale leggere il valore 0 dal camporesult
. La dichiarazionefinished
comevolatile
campo impedisce una tale incoerenza.esempio finale
15.5.5 Inizializzazione dei campi
Il valore iniziale di un campo, che si tratti di un campo statico o di un campo di istanza, è il valore predefinito (§9.3) del tipo del campo. Non è possibile osservare il valore di un campo prima che si sia verificata l'inizializzazione predefinita e un campo non sia mai "inizializzato".
Esempio: esempio
class Test { static bool b; int i; static void Main() { Test t = new Test(); Console.WriteLine($"b = {b}, i = {t.i}"); } }
produce l'output
b = False, i = 0
perché
b
ei
vengono inizializzati automaticamente in valori predefiniti.esempio finale
15.5.6 Inizializzatori variabili
15.5.6.1 Generale
Le dichiarazioni di campo possono includere variable_initializers. Per i campi statici, gli inizializzatori di variabili corrispondono alle istruzioni di assegnazione eseguite durante l'inizializzazione della classe. Per i campi dell'istanza, gli inizializzatori di variabili corrispondono alle istruzioni di assegnazione eseguite quando viene creata un'istanza della classe .
Esempio: esempio
class Test { static double x = Math.Sqrt(2.0); int i = 100; string s = "Hello"; static void Main() { Test a = new Test(); Console.WriteLine($"x = {x}, i = {a.i}, s = {a.s}"); } }
produce l'output
x = 1.4142135623730951, i = 100, s = Hello
poiché un'assegnazione a
x
si verifica quando gli inizializzatori di campo statici eseguono e si verificano assegnazioni ai
es
si verificano quando vengono eseguiti gli inizializzatori di campi dell'istanza.esempio finale
L'inizializzazione predefinita dei valori descritta in §15.5.5 si verifica per tutti i campi, inclusi i campi con inizializzatori variabili. Pertanto, quando viene inizializzata una classe, tutti i campi statici in tale classe vengono inizializzati per primi sui valori predefiniti e quindi gli inizializzatori di campo statici vengono eseguiti in ordine testuale. Analogamente, quando viene creata un'istanza di una classe, tutti i campi dell'istanza in tale istanza vengono inizializzati per primi sui valori predefiniti e quindi gli inizializzatori dei campi dell'istanza vengono eseguiti in ordine testuale. Quando sono presenti dichiarazioni di campo in più dichiarazioni di tipo parziale per lo stesso tipo, l'ordine delle parti non viene specificato. Tuttavia, all'interno di ogni parte gli inizializzatori di campo vengono eseguiti in ordine.
È possibile osservare i campi statici con inizializzatori variabili nello stato del valore predefinito.
Esempio: tuttavia, questo è fortemente sconsigliato in materia di stile. L'esempio:
class Test { static int a = b + 1; static int b = a + 1; static void Main() { Console.WriteLine($"a = {a}, b = {b}"); } }
presenta questo comportamento. Nonostante le definizioni circolari di
a
eb
, il programma è valido. Restituisce l'outputa = 1, b = 2
poiché i campi
a
statici eb
vengono inizializzati in0
(il valore predefinito perint
) prima dell'esecuzione dei relativi inizializzatori. Quando l'inizializzatore pera
l'esecuzione, il valore dib
è zero e quindia
viene inizializzato in1
. Quando viene eseguito l'inizializzatore perb
l'esecuzione, il valore di è già1
, quindib
viene inizializzato in2
.esempio finale
15.5.6.2 Inizializzazione dei campi statici
Gli inizializzatori di variabili di campo statici di una classe corrispondono a una sequenza di assegnazioni eseguite nell'ordine testuale in cui appaiono nella dichiarazione di classe (§15.5.6.1). All'interno di una classe parziale, il significato di "ordine testuale" è specificato da §15.5.6.1. Se esiste un costruttore statico (§15.12) nella classe , l'esecuzione degli inizializzatori di campo statici viene eseguita immediatamente prima di eseguire tale costruttore statico. In caso contrario, gli inizializzatori di campo statici vengono eseguiti in un momento dipendente dall'implementazione prima del primo utilizzo di un campo statico di tale classe.
Esempio: esempio
class Test { static void Main() { Console.WriteLine($"{B.Y} {A.X}"); } public static int F(string s) { Console.WriteLine(s); return 1; } } class A { public static int X = Test.F("Init A"); } class B { public static int Y = Test.F("Init B"); }
potrebbe produrre uno dei due output:
Init A Init B 1 1
o l'output:
Init B Init A 1 1
poiché l'esecuzione dell'inizializzatore
X
e dell'inizializzatoreY
di può verificarsi in entrambi gli ordini; sono vincolati solo a verificarsi prima dei riferimenti a tali campi. Tuttavia, nell'esempio:class Test { static void Main() { Console.WriteLine($"{B.Y} {A.X}"); } public static int F(string s) { Console.WriteLine(s); return 1; } } class A { static A() {} public static int X = Test.F("Init A"); } class B { static B() {} public static int Y = Test.F("Init B"); }
l'output sarà:
Init B Init A 1 1
poiché le regole per quando i costruttori statici vengono eseguiti (come definito in §15.12) forniscono che
B
il costruttore statico (e quindiB
gli inizializzatori di campo statici) deve essere eseguito primaA
del costruttore statico e degli inizializzatori di campo.esempio finale
15.5.6.3 Inizializzazione dei campi dell'istanza
Gli inizializzatori di variabili di campo dell'istanza di una classe corrispondono a una sequenza di assegnazioni eseguite immediatamente dopo l'ingresso a uno dei costruttori di istanza (§15.11.3) di tale classe. All'interno di una classe parziale, il significato di "ordine testuale" è specificato da §15.5.6.1. Gli inizializzatori di variabile vengono eseguiti nell'ordine testuale in cui vengono visualizzati nella dichiarazione di classe (§15.5.6.1). Il processo di creazione e inizializzazione dell'istanza di classe è descritto più avanti in §15.11.
Un inizializzatore di variabile per un campo dell'istanza non può fare riferimento all'istanza creata. Pertanto, si tratta di un errore in fase di compilazione a cui fare riferimento this
in un inizializzatore di variabile, poiché si tratta di un errore in fase di compilazione per un inizializzatore di variabile per fare riferimento a qualsiasi membro dell'istanza tramite un simple_name.
Esempio: nel codice seguente
class A { int x = 1; int y = x + 1; // Error, reference to instance member of this }
L'inizializzatore di variabile per
y
genera un errore in fase di compilazione perché fa riferimento a un membro dell'istanza creata.esempio finale
15.6 Metodi
15.6.1 Generale
Un metodo è un membro che implementa un calcolo o un'azione che può essere eseguita da un oggetto o una classe. I metodi vengono dichiarati usando method_declarations:
method_declaration
: attributes? method_modifiers return_type method_header method_body
| attributes? ref_method_modifiers ref_kind ref_return_type method_header
ref_method_body
;
method_modifiers
: method_modifier* 'partial'?
;
ref_kind
: 'ref'
| 'ref' 'readonly'
;
ref_method_modifiers
: ref_method_modifier*
;
method_header
: member_name '(' parameter_list? ')'
| member_name type_parameter_list '(' parameter_list? ')'
type_parameter_constraints_clause*
;
method_modifier
: ref_method_modifier
| 'async'
;
ref_method_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| unsafe_modifier // unsafe code support
;
return_type
: ref_return_type
| 'void'
;
ref_return_type
: type
;
member_name
: identifier
| interface_type '.' identifier
;
method_body
: block
| '=>' null_conditional_invocation_expression ';'
| '=>' expression ';'
| ';'
;
ref_method_body
: block
| '=>' 'ref' variable_reference ';'
| ';'
;
Note grammaticali:
- unsafe_modifier (§23.2) è disponibile solo nel codice non sicuro (§23).
- quando si riconosce un method_body se entrambe le alternative di null_conditional_invocation_expression ed espressione sono applicabili, il primo deve essere scelto.
Nota: la sovrapposizione di e la priorità tra le alternative qui è esclusivamente per praticità descrittiva. Le regole grammaticali potrebbero essere elaborate per rimuovere la sovrapposizione. ANTLR e altri sistemi grammaticali adottano la stessa praticità e quindi method_body ha automaticamente la semantica specificata. nota finale
Un method_declaration può includere un set di attributi (§22) e uno dei tipi consentiti di accessibilità dichiarata (§15.3.6), il new
(§15.3.5), static
(§15.6.3), (§15.15.3) virtual
6.4), override
(§15.6.5), sealed
(§15.6.6), abstract
(§15.6.7), extern
(§15.6.8) e async
(§15.15) modificatori.
Una dichiarazione ha una combinazione valida di modificatori se sono soddisfatte tutte le condizioni seguenti:
- La dichiarazione include una combinazione valida di modificatori di accesso (§15.3.6).
- La dichiarazione non include più volte lo stesso modificatore.
- La dichiarazione include al massimo uno dei modificatori seguenti:
static
,virtual
eoverride
. - La dichiarazione include al massimo uno dei modificatori seguenti:
new
eoverride
. - Se la dichiarazione include il
abstract
modificatore, la dichiarazione non include i modificatori seguenti:static
,virtual
,sealed
oextern
. - Se la dichiarazione include il
private
modificatore, la dichiarazione non include i modificatori seguenti:virtual
,override
oabstract
. - Se la dichiarazione include il
sealed
modificatore, la dichiarazione include anche iloverride
modificatore. - Se la dichiarazione include il
partial
modificatore, non include alcuno dei modificatori seguenti:new
,public
protected
internal
private
virtual
sealed
override
,abstract
, o .extern
I metodi vengono classificati in base a ciò che, se qualcosa, restituiscono:
- Se
ref
è presente, il metodo viene restituito per riferimento e restituisce un riferimento a una variabile, facoltativamente di sola lettura; - In caso contrario, se return_type è
void
, il metodo è returns-no-value e non restituisce un valore; - In caso contrario, il metodo restituisce per valore e restituisce un valore.
Il return_type di una dichiarazione di metodo returns-by-value o returns-no-value specifica il tipo del risultato, se presente, restituito dal metodo . Solo un metodo returns-no-value può includere il partial
modificatore (§15.6.9). Se la dichiarazione include il async
modificatore , return_type deve essere void
o il metodo restituisce per valore e il tipo restituito è un tipo di attività (§15.15.1).
Il ref_return_type di una dichiarazione di metodo returns-by-ref specifica il tipo della variabile a cui fa riferimento il variable_reference restituito dal metodo .
Un metodo generico è un metodo la cui dichiarazione include un type_parameter_list. Specifica i parametri di tipo per il metodo . Il type_parameter_constraints_clausefacoltativo specifica i vincoli per i parametri di tipo.
Un method_declaration generico per un'implementazione esplicita del membro dell'interfaccia non deve avere alcun type_parameter_constraints_clauses. La dichiarazione eredita eventuali vincoli dai vincoli nel metodo di interfaccia.
Analogamente, una dichiarazione di metodo con il override
modificatore non avrà alcun type_parameter_constraints_clausee i vincoli dei parametri di tipo del metodo vengono ereditati dal metodo virtuale sottoposto a override.
Il member_name specifica il nome del metodo. A meno che il metodo non sia un'implementazione esplicita del membro dell'interfaccia (§18.6.2), il member_name è semplicemente un identificatore.
Per un'implementazione esplicita del membro dell'interfaccia, il member_name è costituito da un interface_type seguito da un ".
" e da un identificatore. In questo caso, la dichiarazione non include modificatori diversi (possibilmente) extern
o async
.
Il parameter_list facoltativo specifica i parametri del metodo (§15.6.2).
Il return_type o ref_return_type e ognuno dei tipi a cui si fa riferimento nel parameter_list di un metodo deve essere accessibile almeno quanto il metodo stesso (§7.5.5).
Il method_body di un metodo returns-by-value o returns-no-value è un punto e virgola, un corpo del blocco o un corpo dell'espressione. Un corpo del blocco è costituito da un blocco, che specifica le istruzioni da eseguire quando viene richiamato il metodo. Un corpo dell'espressione =>
è costituito da , seguito da un null_conditional_invocation_expression o da un'espressione e un punto e virgola e indica una singola espressione da eseguire quando viene richiamato il metodo.
Per i metodi astratti ed extern, il method_body è costituito semplicemente da un punto e virgola. Per i metodi parziali, il method_body può essere costituito da un punto e virgola, un corpo del blocco o un corpo dell'espressione. Per tutti gli altri metodi, il method_body è un corpo del blocco o un corpo dell'espressione.
Se il method_body è costituito da un punto e virgola, la dichiarazione non includerà il async
modificatore.
Il ref_method_body di un metodo returns-by-ref è un punto e virgola, un corpo del blocco o un corpo dell'espressione. Un corpo del blocco è costituito da un blocco, che specifica le istruzioni da eseguire quando viene richiamato il metodo. Un corpo dell'espressione =>
è costituito da , seguito da ref
, da un variable_reference e da un punto e virgola e indica un singolo variable_reference da valutare quando viene richiamato il metodo.
Per i metodi astratti ed extern, il ref_method_body è costituito semplicemente da un punto e virgola. Per tutti gli altri metodi, il ref_method_body è un corpo di blocco o un corpo dell'espressione.
Il nome, il numero di parametri di tipo e l'elenco di parametri di un metodo definiscono la firma (§7.6) del metodo. In particolare, la firma di un metodo è costituita dal nome, dal numero dei parametri di tipo e dal numero, parameter_mode_modifier s (§15.6.2.1) e dai tipidei relativi parametri. Il tipo restituito non fa parte della firma di un metodo, né sono i nomi dei parametri, i nomi dei parametri di tipo o i vincoli. Quando un tipo di parametro fa riferimento a un parametro di tipo del metodo, la posizione ordinale del parametro di tipo (non il nome del parametro di tipo) viene utilizzata per l'equivalenza del tipo.
Il nome di un metodo deve essere diverso dai nomi di tutti gli altri metodi non dichiarati nella stessa classe. Inoltre, la firma di un metodo deve essere diversa dalle firme di tutti gli altri metodi dichiarati nella stessa classe e due metodi dichiarati nella stessa classe non avranno firme che differiscono esclusivamente per in
, out
e ref
.
I type_parameter del metodo sono inclusi nell'ambito di tutto il method_declaration e possono essere usati per formare i tipi in tale ambito in return_type o ref_return_type, method_body o ref_method_body e type_parameter_constraints_clausema non negli attributi.
Tutti i parametri e i parametri di tipo devono avere nomi diversi.
15.6.2 Parametri del metodo
15.6.2.1 Generale
I parametri di un metodo, se presenti, vengono dichiarati dal parameter_list del metodo.
parameter_list
: fixed_parameters
| fixed_parameters ',' parameter_array
| parameter_array
;
fixed_parameters
: fixed_parameter (',' fixed_parameter)*
;
fixed_parameter
: attributes? parameter_modifier? type identifier default_argument?
;
default_argument
: '=' expression
;
parameter_modifier
: parameter_mode_modifier
| 'this'
;
parameter_mode_modifier
: 'ref'
| 'out'
| 'in'
;
parameter_array
: attributes? 'params' array_type identifier
;
L'elenco di parametri è costituito da uno o più parametri delimitati da virgole di cui solo l'ultimo può essere un parameter_array.
Un fixed_parameter è costituito da un set facoltativo di attributi (§22); un modificatore facoltativo in
, out
, ref
o , this
un tipo, un identificatore e un default_argument facoltativo. Ogni fixed_parameter dichiara un parametro del tipo specificato con il nome specificato. Il this
modificatore definisce il metodo come metodo di estensione ed è consentito solo sul primo parametro di un metodo statico in una classe statica non generica non annidata. Se il parametro è un struct
tipo o un parametro di tipo vincolato a un struct
oggetto , il this
modificatore può essere combinato con il ref
modificatore o in
, ma non il out
modificatore. I metodi di estensione sono descritti ulteriormente in §15.6.10. Un fixed_parameter con un default_argument è noto come parametro facoltativo, mentre un fixed_parameter senza un default_argument è un parametro obbligatorio. Un parametro obbligatorio non verrà visualizzato dopo un parametro facoltativo in un parameter_list.
Un parametro con un ref
modificatore o out
this
non può avere un default_argument. Un parametro di input può avere un default_argument. L'espressione in un default_argument deve essere una delle seguenti:
- a constant_expression
- espressione del form
new S()
in cuiS
è un tipo valore - espressione del form
default(S)
in cuiS
è un tipo valore
L'espressione deve essere convertibile in modo implicito da una conversione identity o nullable nel tipo del parametro .
Se i parametri facoltativi si verificano in una dichiarazione di metodo parziale (§15.6.9), un'implementazione esplicita del membro dell'interfaccia (§18.6.2), una dichiarazione dell'indicizzatore a parametro singolo (§15.9) o in una dichiarazione di operatore (§15.10.1) un compilatore deve fornire un avviso, perché questi membri non possono mai essere richiamati in modo da consentire l'omissione degli argomenti.
Un parameter_array è costituito da un set facoltativo di attributi (§22), un modificatore, un params
array_type e un identificatore. Una matrice di parametri dichiara un singolo parametro del tipo di matrice specificato con il nome specificato. Il array_type di una matrice di parametri deve essere un tipo di matrice unidimensionale (§17.2). In una chiamata al metodo, una matrice di parametri consente di specificare un singolo argomento del tipo di matrice specificato oppure consente di specificare zero o più argomenti del tipo di elemento della matrice. Le matrici di parametri sono descritte più avanti in §15.6.2.4.
Un parameter_array può verificarsi dopo un parametro facoltativo, ma non può avere un valore predefinito. L'omissione di argomenti per un parameter_array comporta invece la creazione di una matrice vuota.
Esempio: di seguito vengono illustrati diversi tipi di parametri:
void M<T>( ref int i, decimal d, bool b = false, bool? n = false, string s = "Hello", object o = null, T t = default(T), params int[] a ) { }
Nella parameter_list per
M
i
è un parametro obbligatorioref
,d
è un parametro di valore obbligatorio,b
,s
o
e sono parametri di valore facoltativi edt
a
è una matrice di parametri.esempio finale
Una dichiarazione di metodo crea uno spazio di dichiarazione separato (§7.3) per parametri e parametri di tipo. I nomi vengono introdotti in questo spazio di dichiarazione dall'elenco dei parametri di tipo e dall'elenco di parametri del metodo . Il corpo del metodo, se presente, viene considerato annidato all'interno di questo spazio di dichiarazione. Si tratta di un errore per due membri dello spazio di dichiarazione di un metodo con lo stesso nome.
Una chiamata al metodo (§12.8.10.2) crea una copia, specifica di tale chiamata, dei parametri e delle variabili locali del metodo e l'elenco di argomenti della chiamata assegna valori o riferimenti variabili ai parametri appena creati. All'interno del blocco di un metodo è possibile fare riferimento ai parametri in simple_name espressioni (§12.8.4).
Esistono i tipi di parametri seguenti:
- Parametri valore (§15.6.2.2).
- Parametri di input (§15.6.2.3.2).
- Parametri di output (§15.6.2.3.4).
- Parametri di riferimento (§15.6.2.3.3).
- Matrici di parametri (§15.6.2.4).
Nota: come descritto in §7.6, i
in
modificatori ,out
eref
fanno parte della firma di un metodo, ma ilparams
modificatore non è. nota finale
15.6.2.2 Parametri valore
Un parametro dichiarato senza modificatori è un parametro value. Un parametro value è una variabile locale che ottiene il valore iniziale dall'argomento corrispondente fornito nella chiamata al metodo.
Per le regole di assegnazione definite, vedere §9.2.5.
L'argomento corrispondente in una chiamata al metodo deve essere un'espressione convertibile in modo implicito (§10.2) nel tipo di parametro.
È consentito assegnare nuovi valori a un parametro di valore. Tali assegnazioni influiscono solo sulla posizione di archiviazione locale rappresentata dal parametro value, che non hanno alcun effetto sull'argomento effettivo specificato nella chiamata al metodo.
15.6.2.3 Parametri per riferimento
15.6.2.3.1 Generale
I parametri di input, output e riferimento sono parametridi riferimento s. Un parametro per riferimento è una variabile di riferimento locale (§9.7); il referenziale iniziale viene ottenuto dall'argomento corrispondente fornito nella chiamata al metodo.
Nota: il referenziale di un parametro per riferimento può essere modificato usando l'operatore ref assignment (
= ref
).
Quando un parametro è un parametro per riferimento, l'argomento corrispondente in una chiamata al metodo deve essere costituito dalla parola chiave corrispondente, in
, ref
o out
, seguita da un variable_reference (§9.5) dello stesso tipo del parametro. Tuttavia, quando il parametro è un in
parametro, l'argomento può essere un'espressioneper la quale esiste una conversione implicita (§10.2) da tale espressione di argomento al tipo del parametro corrispondente.
I parametri per riferimento non sono consentiti nelle funzioni dichiarate come iteratore (§15.14) o funzione asincrona (§15.15).
In un metodo che accetta più parametri per riferimento, è possibile che più nomi rappresentino la stessa posizione di archiviazione.
15.6.2.3.2 Parametri di input
Un parametro dichiarato con un in
modificatore è un parametro di input. L'argomento corrispondente a un parametro di input è una variabile esistente al punto della chiamata al metodo o una creata dall'implementazione (§12.6.2.3) nella chiamata al metodo. Per le regole di assegnazione definite, vedere §9.2.8.
Si tratta di un errore in fase di compilazione per modificare il valore di un parametro di input.
Nota: lo scopo principale dei parametri di input è l'efficienza. Quando il tipo di un parametro di metodo è uno struct di grandi dimensioni (in termini di requisiti di memoria), è utile evitare di copiare l'intero valore dell'argomento quando si chiama il metodo . I parametri di input consentono ai metodi di fare riferimento ai valori esistenti in memoria, fornendo al tempo stesso protezione da modifiche indesiderate a tali valori. nota finale
15.6.2.3.3 Parametri di riferimento
Un parametro dichiarato con un ref
modificatore è un parametro di riferimento. Per le regole di assegnazione definite, vedere §9.2.6.
Esempio: esempio
class Test { static void Swap(ref int x, ref int y) { int temp = x; x = y; y = temp; } static void Main() { int i = 1, j = 2; Swap(ref i, ref j); Console.WriteLine($"i = {i}, j = {j}"); } }
produce l'output
i = 2, j = 1
Per la chiamata di
Swap
inMain
,x
rappresentai
ey
rappresentaj
. Pertanto, la chiamata ha l'effetto di scambiare i valori dii
ej
.esempio finale
Esempio: nel codice seguente
class A { string s; void F(ref string a, ref string b) { s = "One"; a = "Two"; b = "Three"; } void G() { F(ref s, ref s); } }
la chiamata di
F
inG
passa un riferimento as
per entrambia
eb
. Pertanto, per tale chiamata, i nomis
,a
eb
tutti fanno riferimento alla stessa posizione di archiviazione e le tre assegnazioni modificano tutti il campos
dell'istanza .esempio finale
Per un tipo, all'interno di un struct
metodo di istanza, la funzione di accesso all'istanza (§12.2.1) o il costruttore di istanza con un inizializzatore del costruttore, la this
parola chiave si comporta esattamente come parametro di riferimento del tipo struct (§12.8.14).
15.6.2.3.4 Parametri di output
Un parametro dichiarato con un out
modificatore è un parametro di output. Per le regole di assegnazione definite, vedere §9.2.7.
Un metodo dichiarato come metodo parziale (§15.6.9) non avrà parametri di output.
Nota: i parametri di output vengono in genere usati nei metodi che producono più valori restituiti. nota finale
Esempio:
class Test { static void SplitPath(string path, out string dir, out string name) { int i = path.Length; while (i > 0) { char ch = path[i - 1]; if (ch == '\\' || ch == '/' || ch == ':') { break; } i--; } dir = path.Substring(0, i); name = path.Substring(i); } static void Main() { string dir, name; SplitPath(@"c:\Windows\System\hello.txt", out dir, out name); Console.WriteLine(dir); Console.WriteLine(name); } }
L'esempio produce l'output:
c:\Windows\System\ hello.txt
Si noti che le
dir
variabili ename
possono essere annullate prima di essere passate aSplitPath
e che vengono considerate assegnate definitivamente dopo la chiamata.esempio finale
15.6.2.4 Matrici di parametri
Un parametro dichiarato con un params
modificatore è una matrice di parametri. Se un elenco di parametri include una matrice di parametri, deve essere l'ultimo parametro nell'elenco e deve essere di un tipo di matrice unidimensionale.
Esempio: i tipi
string[]
estring[][]
possono essere usati come tipo di una matrice di parametri, ma il tipostring[,]
non può. esempio finale
Nota: non è possibile combinare il
params
modificatore con i modificatoriin
,out
oref
. nota finale
Una matrice di parametri consente di specificare gli argomenti in uno dei due modi in una chiamata al metodo:
- L'argomento specificato per una matrice di parametri può essere una singola espressione convertibile in modo implicito (§10.2) nel tipo di matrice di parametri. In questo caso, la matrice di parametri agisce esattamente come un parametro di valore.
- In alternativa, la chiamata può specificare zero o più argomenti per la matrice di parametri, dove ogni argomento è un'espressione convertibile in modo implicito (§10.2) nel tipo di elemento della matrice di parametri. In questo caso, la chiamata crea un'istanza del tipo di matrice di parametri con una lunghezza corrispondente al numero di argomenti, inizializza gli elementi dell'istanza della matrice con i valori dell'argomento specificati e usa l'istanza della matrice appena creata come argomento effettivo.
Ad eccezione di un numero variabile di argomenti in una chiamata, una matrice di parametri equivale esattamente a un parametro value (§15.6.2.2) dello stesso tipo.
Esempio: esempio
class Test { static void F(params int[] args) { Console.Write($"Array contains {args.Length} elements:"); foreach (int i in args) { Console.Write($" {i}"); } Console.WriteLine(); } static void Main() { int[] arr = {1, 2, 3}; F(arr); F(10, 20, 30, 40); F(); } }
produce l'output
Array contains 3 elements: 1 2 3 Array contains 4 elements: 10 20 30 40 Array contains 0 elements:
La prima chiamata di
F
passa semplicemente la matricearr
come parametro di valore. La seconda chiamata di F crea automaticamente un oggetto a quattro elementiint[]
con i valori dell'elemento specificati e passa tale istanza di matrice come parametro di valore. Analogamente, la terza chiamata diF
crea un elementoint[]
zero e passa tale istanza come parametro di valore. La seconda e la terza chiamata sono esattamente equivalenti alla scrittura:F(new int[] {10, 20, 30, 40}); F(new int[] {});
esempio finale
Quando si esegue la risoluzione dell'overload, potrebbe essere applicabile un metodo con una matrice di parametri, nel formato normale o nel formato espanso (§12.6.4.2). La forma espansa di un metodo è disponibile solo se la forma normale del metodo non è applicabile e solo se un metodo applicabile con la stessa firma del modulo espanso non è già dichiarato nello stesso tipo.
Esempio: esempio
class Test { static void F(params object[] a) => Console.WriteLine("F(object[])"); static void F() => Console.WriteLine("F()"); static void F(object a0, object a1) => Console.WriteLine("F(object,object)"); static void Main() { F(); F(1); F(1, 2); F(1, 2, 3); F(1, 2, 3, 4); } }
produce l'output
F() F(object[]) F(object,object) F(object[]) F(object[])
Nell'esempio, due delle possibili forme espanse del metodo con una matrice di parametri sono già incluse nella classe come metodi regolari. Questi moduli espansi non vengono pertanto considerati durante l'esecuzione della risoluzione dell'overload e le chiamate al primo e al terzo metodo selezionano quindi i metodi regolari. Quando una classe dichiara un metodo con una matrice di parametri, non è insolito includere anche alcuni moduli espansi come metodi regolari. In questo modo, è possibile evitare l'allocazione di un'istanza di matrice che si verifica quando viene richiamata una forma espansa di un metodo con una matrice di parametri.
esempio finale
Una matrice è un tipo riferimento, quindi il valore passato per una matrice di parametri può essere
null
.Esempio: Esempio:
class Test { static void F(params string[] array) => Console.WriteLine(array == null); static void Main() { F(null); F((string) null); } }
produce l'output:
True False
La seconda chiamata produce
False
come equivale aF(new string[] { null })
e passa una matrice contenente un singolo riferimento Null.esempio finale
Quando il tipo di una matrice di parametri è object[]
, si verifica una potenziale ambiguità tra la forma normale del metodo e la forma espansa per un singolo object
parametro. Il motivo dell'ambiguità è che un oggetto object[]
è convertibile in modo implicito nel tipo object
. L'ambiguità non presenta tuttavia alcun problema, poiché può essere risolto inserendo un cast, se necessario.
Esempio: esempio
class Test { static void F(params object[] args) { foreach (object o in args) { Console.Write(o.GetType().FullName); Console.Write(" "); } Console.WriteLine(); } static void Main() { object[] a = {1, "Hello", 123.456}; object o = a; F(a); F((object)a); F(o); F((object[])o); } }
produce l'output
System.Int32 System.String System.Double System.Object[] System.Object[] System.Int32 System.String System.Double
Nella prima e nell'ultima chiamata di
F
, la forma normale diF
è applicabile perché esiste una conversione implicita dal tipo di argomento al tipo di parametro (entrambi sono di tipoobject[]
). Pertanto, la risoluzione dell'overload seleziona la forma normale diF
e l'argomento viene passato come parametro di valore regolare. Nella seconda e nella terza chiamata, la forma normale diF
non è applicabile perché non esiste alcuna conversione implicita dal tipo di argomento al tipo di parametro (il tipoobject
non può essere convertito in modo implicito nel tipoobject[]
). Tuttavia, la forma espansa di è applicabile, quindi viene selezionata dalla risoluzione dell'overloadF
. Di conseguenza, un elementoobject[]
viene creato dalla chiamata e il singolo elemento della matrice viene inizializzato con il valore dell'argomento specificato ( che è un riferimento a un oggettoobject[]
).esempio finale
15.6.3 Metodi statici e di istanza
Quando una dichiarazione di metodo include un static
modificatore, si dice che tale metodo sia un metodo statico. Quando non è presente alcun static
modificatore, si dice che il metodo sia un metodo di istanza.
Un metodo statico non opera su un'istanza specifica ed è un errore in fase di compilazione a cui fare riferimento this
in un metodo statico.
Un metodo di istanza opera su una determinata istanza di una classe e tale istanza può essere accessibile come this
(§12.8.14).
Le differenze tra i membri statici e dell'istanza sono descritte più avanti in §15.3.8.
15.6.4 Metodi virtuali
Quando una dichiarazione del metodo di istanza include un modificatore virtuale, tale metodo viene detto come metodo virtuale. Quando non è presente alcun modificatore virtuale, il metodo viene detto come metodo non virtuale.
L'implementazione di un metodo non virtuale è invariante: l'implementazione è la stessa se il metodo viene richiamato su un'istanza della classe in cui viene dichiarata o un'istanza di una classe derivata. Al contrario, l'implementazione di un metodo virtuale può essere sostituita da classi derivate. Il processo di sostituzione dell'implementazione di un metodo virtuale ereditato è noto come override di tale metodo (§15.6.5).
In una chiamata al metodo virtuale, il tipo di runtime dell'istanza per cui viene eseguita la chiamata determina l'implementazione effettiva del metodo da richiamare. In una chiamata al metodo non virtuale, il tipo in fase di compilazione dell'istanza è il fattore determinante. In termini precisi, quando viene richiamato un metodo denominato N
con un elenco A
di argomenti in un'istanza con un tipo in fase di compilazione e un tipo C
R
di runtime (dove R
è C
o una classe derivata da C
), la chiamata viene elaborata come segue:
- In fase di associazione, la risoluzione dell'overload viene applicata a , e per selezionare un metodo
C
specifico dal set di metodi dichiarati in e ereditati daN
.A
M
C
Questo articolo è descritto in §12.8.10.2. - Quindi in fase di esecuzione:
- Se
M
è un metodo non virtuale,M
viene richiamato. - In caso contrario,
M
è un metodo virtuale e viene richiamata l'implementazione più derivata diM
rispetto aR
.
- Se
Per ogni metodo virtuale dichiarato in o ereditato da una classe, esiste un'implementazione più derivata del metodo rispetto a tale classe. L'implementazione più derivata di un metodo M
virtuale rispetto a una classe R
è determinata nel modo seguente:
- Se
R
contiene la dichiarazione virtuale introduttiva diM
, si tratta dell'implementazione più derivata diM
rispetto aR
. - In caso contrario, se
R
contiene un override diM
, si tratta dell'implementazione più derivata diM
rispetto aR
. - In caso contrario, l'implementazione più derivata di
M
rispetto aR
è uguale all'implementazione più derivata diM
rispetto alla classe base diretta diR
.
Esempio: l'esempio seguente illustra le differenze tra metodi virtuali e non virtuali:
class A { public void F() => Console.WriteLine("A.F"); public virtual void G() => Console.WriteLine("A.G"); } class B : A { public new void F() => Console.WriteLine("B.F"); public override void G() => Console.WriteLine("B.G"); } class Test { static void Main() { B b = new B(); A a = b; a.F(); b.F(); a.G(); b.G(); } }
Nell'esempio vengono introdotti
A
un metodoF
non virtuale e un metodoG
virtuale . La classeB
introduce un nuovo metodoF
non virtuale, nascondendo così l'oggetto ereditatoF
ed esegue anche l'override del metodoG
ereditato . L'esempio produce l'output:A.F B.F B.G B.G
Si noti che l'istruzione
a.G()
richiamaB.G
, nonA.G
. Questo perché il tipo di runtime dell'istanza (ovveroB
), non il tipo in fase di compilazione dell'istanza (ovveroA
), determina l'implementazione effettiva del metodo da richiamare.esempio finale
Poiché i metodi possono nascondere i metodi ereditati, è possibile che una classe contenga diversi metodi virtuali con la stessa firma. Questo non presenta un problema di ambiguità, poiché tutto ma il metodo più derivato è nascosto.
Esempio: nel codice seguente
class A { public virtual void F() => Console.WriteLine("A.F"); } class B : A { public override void F() => Console.WriteLine("B.F"); } class C : B { public new virtual void F() => Console.WriteLine("C.F"); } class D : C { public override void F() => Console.WriteLine("D.F"); } class Test { static void Main() { D d = new D(); A a = d; B b = d; C c = d; a.F(); b.F(); c.F(); d.F(); } }
le
C
classi eD
contengono due metodi virtuali con la stessa firma: quello introdotto daA
e quello introdotto daC
. Il metodo introdotto daC
nasconde il metodo ereditato daA
. Pertanto, la dichiarazione di override inD
esegue l'override del metodo introdotto daC
e non è possibileD
eseguire l'override del metodo introdotto daA
. L'esempio produce l'output:B.F B.F D.F D.F
Si noti che è possibile richiamare il metodo virtuale nascosto accedendo a un'istanza di
D
tramite un tipo meno derivato in cui il metodo non è nascosto.esempio finale
15.6.5 Eseguire l'override dei metodi
Quando una dichiarazione del metodo di istanza include un override
modificatore, si dice che il metodo sia un metodo di override. Un metodo di override esegue l'override di un metodo virtuale ereditato con la stessa firma. Mentre una dichiarazione di metodo virtuale introduce un nuovo metodo, una dichiarazione di metodo di override è specializzata in un metodo virtuale ereditato esistente fornendo una nuova implementazione di tale metodo.
Il metodo sottoposto a override da una dichiarazione di override è noto come metodo base sottoposto a override Per un metodoM
di override dichiarato in una classe C
, il metodo base sottoposto a override viene determinato esaminando ogni classe base di C
, a partire dalla classe base diretta di C
e continuando con ogni classe base diretta successiva, fino a quando in un determinato tipo di classe base si trova almeno un metodo accessibile che ha la stessa firma di M
dopo la sostituzione degli argomenti di tipo. Ai fini dell'individuazione del metodo di base sottoposto a override, un metodo viene considerato accessibile se è public
, se è protected
, se è protected internal
o se è internal
o private protected
e dichiarato nello stesso programma di C
.
Si verifica un errore in fase di compilazione, a meno che non siano soddisfatte tutte le condizioni seguenti per una dichiarazione di override:
- Un metodo di base sottoposto a override può essere individuato come descritto in precedenza.
- Esiste esattamente un metodo di base sottoposto a override. Questa restrizione ha effetto solo se il tipo di classe base è un tipo costruito in cui la sostituzione degli argomenti di tipo rende la firma di due metodi uguali.
- Il metodo base sottoposto a override è un metodo virtuale, astratto o di override. In altre parole, il metodo base sottoposto a override non può essere statico o non virtuale.
- Il metodo base sottoposto a override non è un metodo sealed.
- Esiste una conversione di identità tra il tipo restituito del metodo di base sottoposto a override e il metodo di override.
- La dichiarazione di override e il metodo di base sottoposto a override hanno la stessa accessibilità dichiarata. In altre parole, una dichiarazione di override non può modificare l'accessibilità del metodo virtuale. Tuttavia, se il metodo di base sottoposto a override è protetto interno e viene dichiarato in un assembly diverso rispetto all'assembly contenente la dichiarazione di override, l'accessibilità dichiarata della dichiarazione di override deve essere protetta.
- La dichiarazione di override non specifica alcun type_parameter_constraints_clauses. I vincoli vengono invece ereditati dal metodo di base sottoposto a override. I vincoli che sono parametri di tipo nel metodo sottoposto a override possono essere sostituiti da argomenti di tipo nel vincolo ereditato. Ciò può causare vincoli non validi quando vengono specificati in modo esplicito, ad esempio tipi valore o tipi sealed.
Esempio: di seguito viene illustrato come funzionano le regole di override per le classi generiche:
abstract class C<T> { public virtual T F() {...} public virtual C<T> G() {...} public virtual void H(C<T> x) {...} } class D : C<string> { public override string F() {...} // Ok public override C<string> G() {...} // Ok public override void H(C<T> x) {...} // Error, should be C<string> } class E<T,U> : C<U> { public override U F() {...} // Ok public override C<U> G() {...} // Ok public override void H(C<T> x) {...} // Error, should be C<U> }
esempio finale
Una dichiarazione di override può accedere al metodo di base sottoposto a override usando un base_access (§12.8.15).
Esempio: nel codice seguente
class A { int x; public virtual void PrintFields() => Console.WriteLine($"x = {x}"); } class B : A { int y; public override void PrintFields() { base.PrintFields(); Console.WriteLine($"y = {y}"); } }
la
base.PrintFields()
chiamata inB
richiama il metodo PrintFields dichiarato inA
. Un base_access disabilita il meccanismo di chiamata virtuale e considera semplicemente il metodo di base come metodo nonvirtual
. Se la chiamata inB
è stata scritta((A)this).PrintFields()
, richiama in modo ricorsivo ilPrintFields
metodo dichiarato inB
, non quello dichiarato inA
, poichéPrintFields
è virtuale e il tipo di runtime di((A)this)
èB
.esempio finale
Solo includendo un override
modificatore, un metodo può eseguire l'override di un altro metodo. In tutti gli altri casi, un metodo con la stessa firma di un metodo ereditato nasconde semplicemente il metodo ereditato.
Esempio: nel codice seguente
class A { public virtual void F() {} } class B : A { public virtual void F() {} // Warning, hiding inherited F() }
il
F
metodo inB
non include unoverride
modificatore e pertanto non esegue l'override delF
metodo inA
. Invece, ilF
metodo inB
nasconde il metodo inA
e viene segnalato un avviso perché la dichiarazione non include un nuovo modificatore.esempio finale
Esempio: nel codice seguente
class A { public virtual void F() {} } class B : A { private new void F() {} // Hides A.F within body of B } class C : B { public override void F() {} // Ok, overrides A.F }
il
F
metodo inB
nasconde il metodo virtualeF
ereditato daA
. Poiché il nuovoF
inB
ha accesso privato, l'ambito include solo il corpo della classe diB
e non si estende aC
. Pertanto, la dichiarazione diF
inC
è consentita per eseguire l'override dell'oggettoF
ereditato daA
.esempio finale
15.6.6 Metodi sealed
Quando una dichiarazione di metodo di istanza include un sealed
modificatore, si dice che tale metodo sia un metodo sealed. Un metodo sealed esegue l'override di un metodo virtuale ereditato con la stessa firma. Un metodo sealed deve anche essere contrassegnato con il override
modificatore. L'uso del sealed
modificatore impedisce a una classe derivata di eseguire ulteriormente l'override del metodo.
Esempio: esempio
class A { public virtual void F() => Console.WriteLine("A.F"); public virtual void G() => Console.WriteLine("A.G"); } class B : A { public sealed override void F() => Console.WriteLine("B.F"); public override void G() => Console.WriteLine("B.G"); } class C : B { public override void G() => Console.WriteLine("C.G"); }
la classe
B
fornisce due metodi di override: unF
metodo con ilsealed
modificatore e unG
metodo che non lo fa.B
L'uso del modificatore impediscesealed
l'overrideC
F
di .esempio finale
15.6.7 Metodi astratti
Quando una dichiarazione del metodo di istanza include un abstract
modificatore, tale metodo viene detto come metodo astratto. Anche se un metodo astratto è implicito anche un metodo virtuale, non può avere il modificatore virtual
.
Una dichiarazione di metodo astratto introduce un nuovo metodo virtuale, ma non fornisce un'implementazione di tale metodo. Al contrario, le classi derivate non astratte sono necessarie per fornire la propria implementazione eseguendo l'override di tale metodo. Poiché un metodo astratto non fornisce alcuna implementazione effettiva, il corpo del metodo di un metodo astratto è costituito semplicemente da un punto e virgola.
Le dichiarazioni di metodi astratti sono consentite solo nelle classi astratte (§15.2.2.2).
Esempio: nel codice seguente
public abstract class Shape { public abstract void Paint(Graphics g, Rectangle r); } public class Ellipse : Shape { public override void Paint(Graphics g, Rectangle r) => g.DrawEllipse(r); } public class Box : Shape { public override void Paint(Graphics g, Rectangle r) => g.DrawRect(r); }
la
Shape
classe definisce la nozione astratta di un oggetto forma geometrica che può disegnare se stesso. IlPaint
metodo è astratto perché non esiste un'implementazione predefinita significativa. LeEllipse
classi eBox
sono implementazioni concreteShape
. Poiché queste classi non sono astratte, sono necessarie per eseguire l'override delPaint
metodo e fornire un'implementazione effettiva.esempio finale
Si tratta di un errore in fase di compilazione per un base_access (§12.8.15) per fare riferimento a un metodo astratto.
Esempio: nel codice seguente
abstract class A { public abstract void F(); } class B : A { // Error, base.F is abstract public override void F() => base.F(); }
Viene segnalato un errore in fase di compilazione per la
base.F()
chiamata perché fa riferimento a un metodo astratto.esempio finale
Una dichiarazione di metodo astratta può eseguire l'override di un metodo virtuale. Ciò consente a una classe astratta di forzare la ri-implementazione del metodo nelle classi derivate e rende l'implementazione originale del metodo non disponibile.
Esempio: nel codice seguente
class A { public virtual void F() => Console.WriteLine("A.F"); } abstract class B: A { public abstract override void F(); } class C : B { public override void F() => Console.WriteLine("C.F"); }
La classe
A
dichiara un metodo virtuale, la classeB
esegue l'override di questo metodo con un metodo astratto e la classeC
esegue l'override del metodo astratto per fornire la propria implementazione.esempio finale
15.6.8 Metodi esterni
Quando una dichiarazione di metodo include un extern
modificatore, si dice che il metodo sia un metodo esterno. I metodi esterni vengono implementati esternamente, in genere usando un linguaggio diverso da C#. Poiché una dichiarazione di metodo esterno non fornisce alcuna implementazione effettiva, il corpo del metodo di un metodo esterno è semplicemente costituito da un punto e virgola. Un metodo esterno non deve essere generico.
Il meccanismo tramite il quale viene ottenuto il collegamento a un metodo esterno è definito dall'implementazione.
Esempio: l'esempio seguente illustra l'uso del
extern
modificatore e dell'attributoDllImport
:class Path { [DllImport("kernel32", SetLastError=true)] static extern bool CreateDirectory(string name, SecurityAttribute sa); [DllImport("kernel32", SetLastError=true)] static extern bool RemoveDirectory(string name); [DllImport("kernel32", SetLastError=true)] static extern int GetCurrentDirectory(int bufSize, StringBuilder buf); [DllImport("kernel32", SetLastError=true)] static extern bool SetCurrentDirectory(string name); }
esempio finale
15.6.9 Metodi parziali
Quando una dichiarazione di metodo include un partial
modificatore, tale metodo viene detto come metodo parziale. I metodi parziali possono essere dichiarati solo come membri di tipi parziali (§15.2.7) e sono soggetti a una serie di restrizioni.
I metodi parziali possono essere definiti in una parte di una dichiarazione di tipo e implementati in un altro. L'implementazione è facoltativa; se nessuna parte implementa il metodo parziale, la dichiarazione del metodo parziale e tutte le chiamate a esso vengono rimosse dalla dichiarazione di tipo risultante dalla combinazione delle parti.
I metodi parziali non definiscono modificatori di accesso; sono implicitamente privati. Il tipo restituito deve essere void
e i relativi parametri non devono essere parametri di output. L'identificatore parziale viene riconosciuto come parola chiave contestuale (§6.4.4) in una dichiarazione di metodo solo se viene visualizzato immediatamente prima della void
parola chiave. Un metodo parziale non può implementare in modo esplicito metodi di interfaccia.
Esistono due tipi di dichiarazioni di metodo parziali: se il corpo della dichiarazione del metodo è un punto e virgola, la dichiarazione viene definita dichiarazione di metodo parziale. Se il corpo è diverso da un punto e virgola, si dice che la dichiarazione sia una dichiarazione di metodo parziale che implementa. Nelle parti di una dichiarazione di tipo può essere presente una sola dichiarazione di metodo parziale con una determinata firma e potrebbe essere presente una sola dichiarazione di metodo parziale con una determinata firma. Se viene specificata una dichiarazione di metodo parziale di implementazione, esiste una dichiarazione di metodo parziale che definisce corrispondente e le dichiarazioni corrispondono a quanto specificato nel codice seguente:
- Le dichiarazioni devono avere gli stessi modificatori (anche se non necessariamente nello stesso ordine), il nome del metodo, il numero di parametri di tipo e il numero di parametri.
- I parametri corrispondenti nelle dichiarazioni devono avere gli stessi modificatori (anche se non necessariamente nello stesso ordine) e gli stessi tipi o tipi convertibili identity (differenze modulo nei nomi dei parametri di tipo).
- I parametri di tipo corrispondenti nelle dichiarazioni devono avere gli stessi vincoli (differenze di modulo nei nomi dei parametri di tipo).
Una dichiarazione di metodo parziale di implementazione può essere visualizzata nella stessa parte della dichiarazione di metodo parziale che definisce la corrispondente.
Solo un metodo parziale che definisce partecipa alla risoluzione dell'overload. Pertanto, indipendentemente dal fatto che venga fornita o meno una dichiarazione di implementazione, le espressioni di chiamata possono risolvere le chiamate del metodo parziale. Poiché un metodo parziale restituisce void
sempre , tali espressioni di chiamata saranno sempre istruzioni di espressione. Inoltre, poiché un metodo parziale è implicitamente private
, tali istruzioni si verificheranno sempre all'interno di una delle parti della dichiarazione di tipo all'interno della quale viene dichiarato il metodo parziale.
Nota: la definizione della corrispondenza che definisce e implementa dichiarazioni di metodi parziali non richiede la corrispondenza dei nomi dei parametri. Questo può produrre comportamenti sorprendenti, sebbene ben definiti, quando vengono utilizzati argomenti denominati (§12.6.2.1). Ad esempio, data la definizione della dichiarazione di metodo parziale per
M
in un file e la dichiarazione del metodo parziale di implementazione in un altro file:// File P1.cs: partial class P { static partial void M(int x); } // File P2.cs: partial class P { static void Caller() => M(y: 0); static partial void M(int y) {} }
non è valido perché la chiamata usa il nome dell'argomento dall'implementazione e non la dichiarazione di metodo parziale di definizione.
nota finale
Se nessuna parte di una dichiarazione di tipo parziale contiene una dichiarazione di implementazione per un determinato metodo parziale, qualsiasi istruzione di espressione che richiama viene semplicemente rimossa dalla dichiarazione di tipo combinato. Pertanto, l'espressione di chiamata, inclusa qualsiasi sottoespressione, non ha alcun effetto in fase di esecuzione. Anche il metodo parziale viene rimosso e non sarà un membro della dichiarazione di tipo combinato.
Se esiste una dichiarazione di implementazione per un determinato metodo parziale, le chiamate dei metodi parziali vengono mantenute. Il metodo parziale dà origine a una dichiarazione di metodo simile alla dichiarazione del metodo parziale di implementazione, ad eccezione dei seguenti:
Il
partial
modificatore non è incluso.Gli attributi nella dichiarazione del metodo risultante sono gli attributi combinati della definizione e la dichiarazione del metodo parziale di implementazione in ordine non specificato. I duplicati non vengono rimossi.
Gli attributi nei parametri della dichiarazione del metodo risultante sono gli attributi combinati dei parametri corrispondenti della definizione e l'implementazione della dichiarazione parziale del metodo in ordine non specificato. I duplicati non vengono rimossi.
Se per un metodo M
parziale viene specificata una dichiarazione di definizione ma non una dichiarazione di implementazione, si applicano le restrizioni seguenti:
Si tratta di un errore in fase di compilazione per creare un delegato da
M
(§12.8.17.6).Si tratta di un errore in fase di compilazione a cui fare riferimento
M
all'interno di una funzione anonima convertita in un tipo di albero delle espressioni (§8.6).Le espressioni che si verificano come parte di una chiamata di non influiscono sullo stato di assegnazione definito (
M
), che può potenzialmente causare errori in fase di compilazione.M
non può essere il punto di ingresso per un'applicazione (§7.1).
I metodi parziali sono utili per consentire a una parte di una dichiarazione di tipo di personalizzare il comportamento di un'altra parte, ad esempio una generata da uno strumento. Si consideri la dichiarazione di classe parziale seguente:
partial class Customer
{
string name;
public string Name
{
get => name;
set
{
OnNameChanging(value);
name = value;
OnNameChanged();
}
}
partial void OnNameChanging(string newName);
partial void OnNameChanged();
}
Se questa classe viene compilata senza altre parti, le dichiarazioni di metodo parziali di definizione e le relative chiamate verranno rimosse e la dichiarazione di classe combinata risultante sarà equivalente alla seguente:
class Customer
{
string name;
public string Name
{
get => name;
set => name = value;
}
}
Si supponga tuttavia che venga fornita un'altra parte che fornisce dichiarazioni di implementazione dei metodi parziali:
partial class Customer
{
partial void OnNameChanging(string newName) =>
Console.WriteLine($"Changing {name} to {newName}");
partial void OnNameChanged() =>
Console.WriteLine($"Changed to {name}");
}
La dichiarazione di classe combinata risultante sarà quindi equivalente alla seguente:
class Customer
{
string name;
public string Name
{
get => name;
set
{
OnNameChanging(value);
name = value;
OnNameChanged();
}
}
void OnNameChanging(string newName) =>
Console.WriteLine($"Changing {name} to {newName}");
void OnNameChanged() =>
Console.WriteLine($"Changed to {name}");
}
Metodi di estensione 15.6.10
Quando il primo parametro di un metodo include il this
modificatore, si dice che tale metodo sia un metodo di estensione. I metodi di estensione devono essere dichiarati solo in classi statiche non generiche non annidate. Il primo parametro di un metodo di estensione è limitato, come indicato di seguito:
- Può essere un parametro di input solo se ha un tipo valore
- Può essere un parametro di riferimento solo se ha un tipo valore o ha un tipo generico vincolato allo struct
- Non deve essere un tipo di puntatore.
Esempio: di seguito è riportato un esempio di una classe statica che dichiara due metodi di estensione:
public static class Extensions { public static int ToInt32(this string s) => Int32.Parse(s); public static T[] Slice<T>(this T[] source, int index, int count) { if (index < 0 || count < 0 || source.Length - index < count) { throw new ArgumentException(); } T[] result = new T[count]; Array.Copy(source, index, result, 0, count); return result; } }
esempio finale
Un metodo di estensione è un metodo statico normale. Inoltre, se la classe statica di inclusione è nell'ambito, è possibile richiamare un metodo di estensione usando la sintassi di chiamata al metodo di istanza (§12.8.10.3), usando l'espressione ricevitore come primo argomento.
Esempio: il programma seguente usa i metodi di estensione dichiarati in precedenza:
static class Program { static void Main() { string[] strings = { "1", "22", "333", "4444" }; foreach (string s in strings.Slice(1, 2)) { Console.WriteLine(s.ToInt32()); } } }
Il
Slice
metodo è disponibile instring[]
e ilToInt32
metodo è disponibile instring
, perché sono stati dichiarati come metodi di estensione. Il significato del programma è uguale al seguente, usando chiamate di metodo statiche normali:static class Program { static void Main() { string[] strings = { "1", "22", "333", "4444" }; foreach (string s in Extensions.Slice(strings, 1, 2)) { Console.WriteLine(Extensions.ToInt32(s)); } } }
esempio finale
Corpo del metodo 15.6.11
Il corpo del metodo di una dichiarazione di metodo è costituito da un corpo del blocco, un corpo dell'espressione o un punto e virgola.
Le dichiarazioni di metodo astratte ed esterne non forniscono un'implementazione del metodo, quindi i corpi dei metodi sono semplicemente costituiti da un punto e virgola. Per qualsiasi altro metodo, il corpo del metodo è un blocco (§13.3) che contiene le istruzioni da eseguire quando tale metodo viene richiamato.
Il tipo restituito effettivo di un metodo è void
se il tipo restituito è void
o se il metodo è asincrono e il tipo restituito è «TaskType»
(§15.15.1). In caso contrario, il tipo restituito effettivo di un metodo non asincrono è il tipo restituito e il tipo restituito effettivo di un metodo asincrono con tipo «TaskType»<T>
restituito (§15.15.1) è T
.
Quando il tipo restituito effettivo di un metodo è void
e il metodo ha un corpo del blocco, return
le istruzioni (§13.10.5) nel blocco non specificano un'espressione. Se l'esecuzione del blocco di un metodo void viene completata normalmente (ovvero il controllo scorre fuori dalla fine del corpo del metodo), il metodo torna semplicemente al chiamante.
Quando il tipo restituito effettivo di un metodo è void
e il metodo ha un corpo dell'espressione, l'espressione E
deve essere un statement_expression e il corpo è esattamente equivalente a un corpo a blocchi della maschera { E; }
.
Per un metodo returns-by-value (§15.6.1), ogni istruzione return nel corpo del metodo specifica un'espressione che è implicitamente convertibile nel tipo restituito effettivo.
Per un metodo returns-by-ref (§15.6.1), ogni istruzione restituita nel corpo del metodo specifica un'espressione il cui tipo è quello del tipo restituito effettivo e ha un contesto di contesto di riferimento di contesto chiamante (§9.7.2).
Per i metodi returns-by-value e returns-by-ref, l'endpoint del corpo del metodo non può essere raggiungibile. In altre parole, il controllo non è autorizzato a fluire dalla fine del corpo del metodo.
Esempio: nel codice seguente
class A { public int F() {} // Error, return value required public int G() { return 1; } public int H(bool b) { if (b) { return 1; } else { return 0; } } public int I(bool b) => b ? 1 : 0; }
Il metodo che restituisce
F
valore genera un errore in fase di compilazione perché il controllo può passare dalla fine del corpo del metodo. IG
metodi eH
sono corretti perché tutti i possibili percorsi di esecuzione terminano in un'istruzione return che specifica un valore restituito. IlI
metodo è corretto, perché il corpo è equivalente a un blocco con una sola istruzione return.esempio finale
15.7 Proprietà
15.7.1 Generale
Una proprietà è un membro che fornisce l'accesso a una caratteristica di un oggetto o di una classe. Esempi di proprietà includono la lunghezza di una stringa, le dimensioni di un tipo di carattere, la didascalia di una finestra e il nome di un cliente. Le proprietà sono un'estensione naturale dei campi, entrambi sono membri denominati con tipi associati e la sintassi per l'accesso a campi e proprietà è la stessa. A differenza dei campi, tuttavia, le proprietà non denotano posizioni di memoria, ma hanno funzioni di accesso che specificano le istruzioni da eseguire nel momento in cui ne vengono letti o scritti i valori. Le proprietà forniscono quindi un meccanismo per associare le azioni alla lettura e alla scrittura delle caratteristiche di un oggetto o di una classe; inoltre, consentono di calcolare tali caratteristiche.
Le proprietà vengono dichiarate usando property_declarations:
property_declaration
: attributes? property_modifier* type member_name property_body
| attributes? property_modifier* ref_kind type member_name ref_property_body
;
property_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| unsafe_modifier // unsafe code support
;
property_body
: '{' accessor_declarations '}' property_initializer?
| '=>' expression ';'
;
property_initializer
: '=' variable_initializer ';'
;
ref_property_body
: '{' ref_get_accessor_declaration '}'
| '=>' 'ref' variable_reference ';'
;
unsafe_modifier (§23.2) è disponibile solo nel codice non sicuro (§23).
Esistono due tipi di property_declaration:
- La prima dichiara una proprietà non con valori di riferimento. Il valore ha un tipo. Questo tipo di proprietà può essere leggibile e/o scrivibile.
- Il secondo dichiara una proprietà con valori di riferimento. Il valore è un variable_reference (§9.5
readonly
Questo tipo di proprietà è leggibile solo.
Un property_declaration può includere un set di attributi (§22) e uno dei tipi consentiti di accessibilità dichiarata (§15.3.6), il new
(§15.3.5), static
(§15.7.2), virtual
(§15.6.4, §15.7.6), override
(§15.6.5, §15.7.6), sealed
(§15.6.6), abstract
(§15.6.7, §15.7.6) e extern
(§15.6.8) modificatori.
Le dichiarazioni di proprietà sono soggette alle stesse regole delle dichiarazioni di metodo (§15.6) relative a combinazioni valide di modificatori.
Il member_name (§15.6.1) specifica il nome della proprietà. A meno che la proprietà non sia un'implementazione esplicita del membro dell'interfaccia, il member_name è semplicemente un identificatore. Per un'implementazione esplicita del membro dell'interfaccia (§18.6.2), il member_name è costituito da un interface_type seguito da un ".
" e da un identificatore.
Il tipo di una proprietà deve essere accessibile almeno quanto la proprietà stessa (§7.5.5).
Un property_body può essere costituito da un corpo dell'istruzione o da un corpo dell'espressione. In un corpo dell'istruzione, accessor_declarations, che deve essere racchiuso in token "{
" e "}
", dichiara le funzioni di accesso (§15.7.3) della proprietà . Le funzioni di accesso specificano le istruzioni eseguibili associate alla lettura e alla scrittura della proprietà.
In un property_body un corpo di espressione costituito da un'espressione=>
e un punto e virgola è esattamente equivalente al corpo E
dell'istruzione e può quindi essere usato solo per specificare proprietà di { get { return E; } }
sola lettura in cui il risultato della funzione di accesso get viene fornito da una singola espressione.
Un property_initializer può essere fornito solo per una proprietà implementata automaticamente (§15.7.4) e provoca l'inizializzazione del campo sottostante di tali proprietà con il valore specificato dall'espressione.
Un ref_property_body può essere costituito da un corpo dell'istruzione o da un corpo dell'espressione. In un corpo di un'istruzione un get_accessor_declaration dichiara la funzione di accesso get (§15.7.3) della proprietà . La funzione di accesso specifica le istruzioni eseguibili associate alla lettura della proprietà.
In un ref_property_body un corpo di =>
espressione costituito da seguito da ref
, un variable_referenceV
e un punto e virgola equivale esattamente al corpo { get { return ref V; } }
dell'istruzione .
Nota: anche se la sintassi per l'accesso a una proprietà è identica a quella di un campo, una proprietà non viene classificata come variabile. Pertanto, non è possibile passare una proprietà come
in
argomento ,out
oref
a meno che la proprietà non sia con valori di riferimento e restituisce quindi un riferimento a una variabile (§9.7). nota finale
Quando una dichiarazione di proprietà include un extern
modificatore, la proprietà viene considerata una proprietà esterna. Poiché una dichiarazione di proprietà esterna non fornisce alcuna implementazione effettiva, ognuno dei accessor_body nel relativo accessor_declarations deve essere un punto e virgola.
15.7.2 Proprietà statiche e dell'istanza
Quando una dichiarazione di proprietà include un static
modificatore, si dice che la proprietà sia una proprietà statica. Quando non è presente alcun static
modificatore, la proprietà viene considerata una proprietà dell'istanza.
Una proprietà statica non è associata a un'istanza specifica ed è un errore in fase di compilazione a cui fare riferimento this
nelle funzioni di accesso di una proprietà statica.
Una proprietà dell'istanza è associata a una determinata istanza di una classe e tale istanza può essere accessibile come this
(§12.8.14) nelle funzioni di accesso di tale proprietà.
Le differenze tra i membri statici e dell'istanza sono descritte più avanti in §15.3.8.
15.7.3 Funzioni di accesso
Nota: questa clausola si applica a entrambe le proprietà (§15.7) e agli indicizzatori (§15.9). La clausola viene scritta in termini di proprietà, quando la lettura per gli indicizzatori sostituisce indicizzatore/indicizzatori per proprietà/proprietà e consulta l'elenco delle differenze tra le proprietà e gli indicizzatori indicati in §15.9.2. nota finale
Il accessor_declarations di una proprietà specifica le istruzioni eseguibili associate alla scrittura e/o alla lettura di tale proprietà.
accessor_declarations
: get_accessor_declaration set_accessor_declaration?
| set_accessor_declaration get_accessor_declaration?
;
get_accessor_declaration
: attributes? accessor_modifier? 'get' accessor_body
;
set_accessor_declaration
: attributes? accessor_modifier? 'set' accessor_body
;
accessor_modifier
: 'protected'
| 'internal'
| 'private'
| 'protected' 'internal'
| 'internal' 'protected'
| 'protected' 'private'
| 'private' 'protected'
;
accessor_body
: block
| '=>' expression ';'
| ';'
;
ref_get_accessor_declaration
: attributes? accessor_modifier? 'get' ref_accessor_body
;
ref_accessor_body
: block
| '=>' 'ref' variable_reference ';'
| ';'
;
Il accessor_declarations è costituito da un get_accessor_declaration, un set_accessor_declaration o entrambi. Ogni dichiarazione della funzione di accesso è costituita da attributi facoltativi, da un accessor_modifier facoltativo, dal token o get
da set
.
Per una proprietà con valori di riferimento, il ref_get_accessor_declaration è costituito da attributi facoltativi, un accessor_modifier facoltativo, il token get
, seguito da un ref_accessor_body.
L'uso di accessor_modifiers è disciplinato dalle restrizioni seguenti:
- Un accessor_modifier non deve essere usato in un'interfaccia o in un'implementazione esplicita del membro dell'interfaccia.
- Per una proprietà o un indicizzatore senza
override
modificatore, è consentito un accessor_modifier solo se la proprietà o l'indicizzatore dispone di una funzione di accesso get e set e quindi è consentita solo in una di queste funzioni di accesso. - Per una proprietà o un indicizzatore che include un
override
modificatore, una funzione di accesso deve corrispondere alla accessor_modifier, se presente, della funzione di accesso sottoposta a override. - Il accessor_modifier dichiara un'accessibilità rigorosamente più restrittiva rispetto all'accessibilità dichiarata della proprietà o dell'indicizzatore stesso. Per essere precisi:
- Se la proprietà o l'indicizzatore ha un'accessibilità dichiarata di
public
, l'accessibilità dichiarata da accessor_modifier può essereprivate protected
,protected internal
internal
,protected
, oprivate
. - Se la proprietà o l'indicizzatore ha un'accessibilità dichiarata di
protected internal
, l'accessibilità dichiarata da accessor_modifier può essereprivate protected
,protected private
internal
,protected
, oprivate
. - Se la proprietà o l'indicizzatore ha un'accessibilità dichiarata di
internal
oprotected
, l'accessibilità dichiarata da accessor_modifier deve essereprivate protected
oprivate
. - Se la proprietà o l'indicizzatore dispone di un'accessibilità dichiarata di
private protected
, l'accessibilità dichiarata da accessor_modifier deve essereprivate
. - Se la proprietà o l'indicizzatore dispone di un'accessibilità dichiarata di
private
, non è possibile usare accessor_modifier .
- Se la proprietà o l'indicizzatore ha un'accessibilità dichiarata di
Per abstract
e le proprietà non con valori di riferimento, qualsiasi extern
per ogni funzione di accesso specificata è semplicemente un punto e virgola. Una proprietà non astratta, non extern, ma non un indicizzatore, può avere anche il accessor_body per tutte le funzioni di accesso specificate come punto e virgola, nel qual caso è una proprietà implementata automaticamente (§15.7.4). Una proprietà implementata automaticamente deve avere almeno una funzione di accesso get. Per le funzioni di accesso di qualsiasi altra proprietà non astratta, non extern, il accessor_body è:
- blocco che specifica le istruzioni da eseguire quando viene richiamata la funzione di accesso corrispondente; o
- un corpo dell'espressione
=>
, costituito da seguito da un'espressione e da un punto e virgola, e indica una singola espressione da eseguire quando viene richiamata la funzione di accesso corrispondente.
Per abstract
le proprietà con valori di riferimento e il extern
è semplicemente un punto e virgola. Per la funzione di accesso di qualsiasi altra proprietà non astratta, non extern, il ref_accessor_body è:
- blocco che specifica le istruzioni da eseguire quando viene richiamata la funzione di accesso get; o
- corpo dell'espressione
=>
, costituito da seguito daref
, un variable_reference e un punto e virgola. Il riferimento alla variabile viene valutato quando viene richiamata la funzione di accesso get.
Una funzione di accesso get per una proprietà senza valori di riferimento corrisponde a un metodo senza parametri con un valore restituito del tipo di proprietà. Ad eccezione della destinazione di un'assegnazione, quando viene fatto riferimento a tale proprietà in un'espressione, viene richiamata la funzione di accesso get per calcolare il valore della proprietà (§12.2.2).
Il corpo di una funzione di accesso get per una proprietà non con valori di riferimento deve essere conforme alle regole per i metodi di restituzione dei valori descritti in §15.6.11. In particolare, tutte le return
istruzioni nel corpo di una funzione di accesso get specificano un'espressione convertibile in modo implicito nel tipo di proprietà. Inoltre, l'endpoint di una funzione di accesso get non può essere raggiungibile.
Una funzione di accesso get per una proprietà con valori di riferimento corrisponde a un metodo senza parametri con un valore restituito di un variable_reference a una variabile del tipo di proprietà. Quando viene fatto riferimento a tale proprietà in un'espressione, viene richiamata la funzione di accesso get per calcolare il valore variable_reference della proprietà. Tale riferimento alla variabile, come qualsiasi altro, viene quindi usato per leggere o, per variable_referencenon in sola lettura, scrivere la variabile a cui si fa riferimento come richiesto dal contesto.
Esempio: l'esempio seguente illustra una proprietà con valori di riferimento come destinazione di un'assegnazione:
class Program { static int field; static ref int Property => ref field; static void Main() { field = 10; Console.WriteLine(Property); // Prints 10 Property = 20; // This invokes the get accessor, then assigns // via the resulting variable reference Console.WriteLine(field); // Prints 20 } }
esempio finale
Il corpo di una funzione di accesso get per una proprietà con valori di riferimento deve essere conforme alle regole per i metodi con valori di riferimento descritti in §15.6.11.
Una funzione di accesso set corrisponde a un metodo con un singolo parametro valore del tipo di proprietà e un void
tipo restituito. Il parametro implicito di una funzione di accesso set è sempre denominato value
. Quando si fa riferimento a una proprietà come destinazione di un'assegnazione (§12.21) o come operando di o ++
(–-
, §12.9.6), la funzione di accesso set viene richiamata con un argomento che fornisce il nuovo valore (§12.21.2). Il corpo di una funzione di accesso set deve essere conforme alle regole per void
i metodi descritti in §15.6.11. In particolare, le istruzioni return nel corpo della funzione di accesso set non sono autorizzate a specificare un'espressione. Poiché una funzione di accesso set ha implicitamente un parametro denominato value
, si tratta di un errore in fase di compilazione per una variabile locale o una dichiarazione costante in una funzione di accesso set per avere tale nome.
In base alla presenza o all'assenza delle funzioni di accesso get e set, una proprietà viene classificata come segue:
- Una proprietà che include sia una funzione di accesso get che una funzione di accesso set viene considerata una proprietà di lettura/scrittura.
- Una proprietà che ha solo una funzione di accesso get è detta proprietà di sola lettura. Si tratta di un errore in fase di compilazione per una proprietà di sola lettura come destinazione di un'assegnazione.
- Una proprietà con solo una funzione di accesso impostata è detta proprietà di sola scrittura. Ad eccezione della destinazione di un'assegnazione, si tratta di un errore in fase di compilazione per fare riferimento a una proprietà di sola scrittura in un'espressione.
Nota: gli operatori pre-e postfissi
++
e--
di assegnazione composta non possono essere applicati alle proprietà di sola scrittura, poiché questi operatori leggono il valore precedente dell'operando prima di scrivere quello nuovo. nota finale
Esempio: nel codice seguente
public class Button : Control { private string caption; public string Caption { get => caption; set { if (caption != value) { caption = value; Repaint(); } } } public override void Paint(Graphics g, Rectangle r) { // Painting code goes here } }
il
Button
controllo dichiara una proprietà pubblicaCaption
. La funzione di accesso get della proprietà Caption restituisce l'oggettostring
archiviato nel campo privatocaption
. La funzione di accesso set controlla se il nuovo valore è diverso dal valore corrente e, in tal caso, archivia il nuovo valore e aggiorna il controllo. Le proprietà spesso seguono il modello illustrato in precedenza: la funzione di accesso get restituisce semplicemente un valore archiviato in unprivate
campo e la funzione di accesso set modifica taleprivate
campo e quindi esegue eventuali azioni aggiuntive necessarie per aggiornare completamente lo stato dell'oggetto. Data laButton
classe precedente, di seguito è riportato un esempio di utilizzo dellaCaption
proprietà :Button okButton = new Button(); okButton.Caption = "OK"; // Invokes set accessor string s = okButton.Caption; // Invokes get accessor
In questo caso, la funzione di accesso set viene richiamata assegnando un valore alla proprietà e la funzione di accesso get viene richiamata facendo riferimento alla proprietà in un'espressione.
esempio finale
Le funzioni di accesso get e set di una proprietà non sono membri distinti e non è possibile dichiarare separatamente le funzioni di accesso di una proprietà.
Esempio: esempio
class A { private string name; // Error, duplicate member name public string Name { get => name; } // Error, duplicate member name public string Name { set => name = value; } }
non dichiara una singola proprietà di lettura/scrittura. Dichiara invece due proprietà con lo stesso nome, una sola lettura e una sola scrittura. Poiché due membri dichiarati nella stessa classe non possono avere lo stesso nome, nell'esempio si verifica un errore in fase di compilazione.
esempio finale
Quando una classe derivata dichiara una proprietà con lo stesso nome di una proprietà ereditata, la proprietà derivata nasconde la proprietà ereditata rispetto alla lettura e alla scrittura.
Esempio: nel codice seguente
class A { public int P { set {...} } } class B : A { public new int P { get {...} } }
la
P
proprietà inB
nasconde laP
proprietà inA
relazione sia alla lettura che alla scrittura. Pertanto, nelle istruzioniB b = new B(); b.P = 1; // Error, B.P is read-only ((A)b).P = 1; // Ok, reference to A.P
l'assegnazione a
b.P
causa di un errore in fase di compilazione da segnalare, poiché la proprietà di solaP
lettura inB
nasconde la proprietà di solaP
scrittura inA
. Si noti, tuttavia, che un cast può essere usato per accedere alla proprietà nascostaP
.esempio finale
A differenza dei campi pubblici, le proprietà forniscono una separazione tra lo stato interno di un oggetto e la relativa interfaccia pubblica.
Esempio: considerare il codice seguente, che usa uno
Point
struct per rappresentare una posizione:class Label { private int x, y; private string caption; public Label(int x, int y, string caption) { this.x = x; this.y = y; this.caption = caption; } public int X => x; public int Y => y; public Point Location => new Point(x, y); public string Caption => caption; }
In questo caso, la
Label
classe usa dueint
campi ex
y
, per archiviarne la posizione. La posizione viene esposta pubblicamente sia comeX
Y
proprietà che comeLocation
proprietà di tipoPoint
. Se, in una versione futura diLabel
, diventa più comodo archiviare la posizione comePoint
internamente, la modifica può essere apportata senza influire sull'interfaccia pubblica della classe:class Label { private Point location; private string caption; public Label(int x, int y, string caption) { this.location = new Point(x, y); this.caption = caption; } public int X => location.X; public int Y => location.Y; public Point Location => location; public string Caption => caption; }
Se invece
x
y
fosseropublic readonly
campi, sarebbe stato impossibile apportare tale modifica allaLabel
classe.esempio finale
Nota: l'esposizione dello stato tramite le proprietà non è necessariamente meno efficiente rispetto all'esposizione diretta dei campi. In particolare, quando una proprietà non è virtuale e contiene solo una piccola quantità di codice, l'ambiente di esecuzione potrebbe sostituire le chiamate alle funzioni di accesso con il codice effettivo delle funzioni di accesso. Questo processo è noto come inlining e rende efficiente l'accesso alle proprietà come l'accesso ai campi, ma mantiene la maggiore flessibilità delle proprietà. nota finale
Esempio: poiché richiamare una funzione di accesso get è concettualmente equivalente alla lettura del valore di un campo, è considerato uno stile di programmazione non valido per le funzioni di accesso get per avere effetti collaterali osservabili. Nell'esempio
class Counter { private int next; public int Next => next++; }
Il valore della
Next
proprietà dipende dal numero di volte in cui è stato eseguito l'accesso alla proprietà in precedenza. Pertanto, l'accesso alla proprietà produce un effetto collaterale osservabile e la proprietà deve essere implementata come metodo.La convenzione "nessun effetto collaterale" per le funzioni di accesso get non significa che le funzioni di accesso get devono sempre essere scritte semplicemente per restituire valori archiviati nei campi. Le funzioni di accesso get spesso calcolano il valore di una proprietà accedendo a più campi o richiamando metodi. Tuttavia, una funzione di accesso get progettata correttamente non esegue azioni che causano modifiche osservabili nello stato dell'oggetto.
esempio finale
Le proprietà possono essere usate per ritardare l'inizializzazione di una risorsa fino al momento in cui viene fatto riferimento per la prima volta.
Esempio:
public class Console { private static TextReader reader; private static TextWriter writer; private static TextWriter error; public static TextReader In { get { if (reader == null) { reader = new StreamReader(Console.OpenStandardInput()); } return reader; } } public static TextWriter Out { get { if (writer == null) { writer = new StreamWriter(Console.OpenStandardOutput()); } return writer; } } public static TextWriter Error { get { if (error == null) { error = new StreamWriter(Console.OpenStandardError()); } return error; } } ... }
La
Console
classe contiene tre proprietà,In
,Out
eError
, che rappresentano rispettivamente i dispositivi di input, output ed errore standard. Esponendo questi membri come proprietà, la classe può ritardare l'inizializzazioneConsole
fino a quando non vengono effettivamente usate. Ad esempio, al primo riferimento allaOut
proprietà , come inConsole.Out.WriteLine("hello, world");
viene creato il sottostante
TextWriter
per il dispositivo di output. Tuttavia, se l'applicazione non fa riferimento alleIn
proprietà eError
, non vengono creati oggetti per tali dispositivi.esempio finale
15.7.4 Proprietà implementate automaticamente
Una proprietà implementata automaticamente (o proprietà automatica per breve), è una proprietà non astratta, non extern, non ref-valued con punto e virgola accessor_bodys. Le proprietà automatiche hanno una funzione di accesso get e facoltativamente possono avere una funzione di accesso set.
Quando una proprietà viene specificata come proprietà implementata automaticamente, un campo sottostante nascosto viene automaticamente disponibile per la proprietà e le funzioni di accesso vengono implementate per leggere e scrivere in tale campo sottostante. Il campo sottostante nascosto non è accessibile, può essere letto e scritto solo tramite le funzioni di accesso alle proprietà implementate automaticamente, anche all'interno del tipo contenitore. Se la proprietà automatica non ha alcuna funzione di accesso impostata, il campo sottostante viene considerato readonly
(§15.5.3). Analogamente a un readonly
campo, è anche possibile assegnare una proprietà automatica di sola lettura a nel corpo di un costruttore della classe contenitore. Tale assegnazione viene assegnata direttamente al campo sottostante di sola lettura della proprietà.
Una proprietà automatica può facoltativamente avere un property_initializer, che viene applicato direttamente al campo sottostante come variable_initializer (§17.7).
Esempio:
public class Point { public int X { get; set; } // Automatically implemented public int Y { get; set; } // Automatically implemented }
equivale alla dichiarazione seguente:
public class Point { private int x; private int y; public int X { get { return x; } set { x = value; } } public int Y { get { return y; } set { y = value; } } }
esempio finale
Esempio: nell'esempio seguente
public class ReadOnlyPoint { public int X { get; } public int Y { get; } public ReadOnlyPoint(int x, int y) { X = x; Y = y; } }
equivale alla dichiarazione seguente:
public class ReadOnlyPoint { private readonly int __x; private readonly int __y; public int X { get { return __x; } } public int Y { get { return __y; } } public ReadOnlyPoint(int x, int y) { __x = x; __y = y; } }
Le assegnazioni al campo di sola lettura sono valide perché si verificano all'interno del costruttore.
esempio finale
Anche se il campo sottostante è nascosto, tale campo può avere attributi di destinazione del campo applicati direttamente al campo tramite il property_declaration della proprietà implementata automaticamente (§15.7.1).
Esempio: il codice seguente
[Serializable] public class Foo { [field: NonSerialized] public string MySecret { get; set; } }
restituisce l'attributo
NonSerialized
di destinazione del campo applicato al campo sottostante generato dal compilatore, come se il codice fosse stato scritto come segue:[Serializable] public class Foo { [NonSerialized] private string _mySecretBackingField; public string MySecret { get { return _mySecretBackingField; } set { _mySecretBackingField = value; } } }
esempio finale
15.7.5 Accessibilità
Se una funzione di accesso ha un accessor_modifier, il dominio di accessibilità (§7.5.3) della funzione di accesso viene determinato usando l'accessibilità dichiarata del accessor_modifier. Se una funzione di accesso non dispone di un accessor_modifier, il dominio di accessibilità della funzione di accesso viene determinato dall'accessibilità dichiarata della proprietà o dell'indicizzatore.
La presenza di un accessor_modifier non influisce mai sulla ricerca dei membri (§12.5) o sulla risoluzione dell'overload (§12.6.4). I modificatori nella proprietà o nell'indicizzatore determinano sempre la proprietà o l'indicizzatore a cui è associato, indipendentemente dal contesto dell'accesso.
Dopo aver selezionato una particolare proprietà non con valori di riferimento o indicizzatore non con valori di riferimento, vengono usati i domini di accessibilità delle funzioni di accesso specifiche coinvolte per determinare se tale utilizzo è valido:
- Se l'utilizzo è come valore (§12.2.2), la funzione di accesso get esiste e sarà accessibile.
- Se l'utilizzo è come destinazione di un'assegnazione semplice (§12.21.2), la funzione di accesso set esiste e sarà accessibile.
- Se l'utilizzo è come destinazione dell'assegnazione composta (§12.21.4) o come destinazione degli
++
operatori or--
(§12.8.16, §12.9.6), entrambe le funzioni di accesso get e la funzione di accesso set sono disponibili ed essere accessibili.
Esempio: nell'esempio seguente la proprietà
A.Text
viene nascosta dalla proprietàB.Text
, anche nei contesti in cui viene chiamata solo la funzione di accesso set. Al contrario, la proprietàB.Count
non è accessibile alla classeM
, pertanto viene usata la proprietàA.Count
accessibile.class A { public string Text { get => "hello"; set { } } public int Count { get => 5; set { } } } class B : A { private string text = "goodbye"; private int count = 0; public new string Text { get => text; protected set => text = value; } protected new int Count { get => count; set => count = value; } } class M { static void Main() { B b = new B(); b.Count = 12; // Calls A.Count set accessor int i = b.Count; // Calls A.Count get accessor b.Text = "howdy"; // Error, B.Text set accessor not accessible string s = b.Text; // Calls B.Text get accessor } }
esempio finale
Una volta selezionata una particolare proprietà con valori di riferimento o indicizzatore con valori di riferimento; se l'utilizzo è un valore, la destinazione di un'assegnazione semplice o la destinazione di un'assegnazione composta; Il dominio di accessibilità della funzione di accesso get viene usato per determinare se tale utilizzo è valido.
Una funzione di accesso usata per implementare un'interfaccia non deve avere un accessor_modifier. Se viene usata una sola funzione di accesso per implementare un'interfaccia, l'altra funzione di accesso può essere dichiarata con un accessor_modifier:
Esempio:
public interface I { string Prop { get; } } public class C : I { public string Prop { get => "April"; // Must not have a modifier here internal set {...} // Ok, because I.Prop has no set accessor } }
esempio finale
15.7.6 Funzioni di accesso virtuali, sealed, override e astratte
Nota: questa clausola si applica a entrambe le proprietà (§15.7) e agli indicizzatori (§15.9). La clausola viene scritta in termini di proprietà, quando la lettura per gli indicizzatori sostituisce indicizzatore/indicizzatori per proprietà/proprietà e consulta l'elenco delle differenze tra le proprietà e gli indicizzatori indicati in §15.9.2. nota finale
Una dichiarazione di proprietà virtuale specifica che le funzioni di accesso della proprietà sono virtuali. Il virtual
modificatore si applica a tutte le funzioni di accesso non private di una proprietà. Quando una funzione di accesso di una proprietà virtuale ha il private
accessor_modifier, la funzione di accesso privata non è implicitamente virtuale.
Una dichiarazione di proprietà astratta specifica che le funzioni di accesso della proprietà sono virtuali, ma non forniscono un'implementazione effettiva delle funzioni di accesso. Le classi derivate non astratte sono invece necessarie per fornire la propria implementazione per le funzioni di accesso eseguendo l'override della proprietà . Poiché una funzione di accesso per una dichiarazione di proprietà astratta non fornisce alcuna implementazione effettiva, il relativo accessor_body è semplicemente costituito da un punto e virgola. Una proprietà astratta non dispone di una private
funzione di accesso.
Una dichiarazione di proprietà che include i abstract
modificatori e override
specifica che la proprietà è astratta ed esegue l'override di una proprietà di base. Anche le funzioni di accesso di questa proprietà sono astratte.
Le dichiarazioni di proprietà astratte sono consentite solo nelle classi astratte (§15.2.2.2). Le funzioni di accesso di una proprietà virtuale ereditata possono essere sottoposte a override in una classe derivata includendo una dichiarazione di proprietà che specifica una override
direttiva. Questa operazione è nota come dichiarazione di proprietà di override. Una dichiarazione di proprietà di override non dichiara una nuova proprietà. Ma è semplicemente specializzata nelle implementazioni delle funzioni di accesso di una proprietà virtuale esistente.
La dichiarazione di override e la proprietà di base sottoposta a override devono avere la stessa accessibilità dichiarata. In altre parole, una dichiarazione di override non modifica l'accessibilità della proprietà di base. Tuttavia, se la proprietà di base sottoposta a override è protetta interna e viene dichiarata in un assembly diverso rispetto all'assembly contenente la dichiarazione di override, l'accessibilità dichiarata della dichiarazione di override deve essere protetta. 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. È prevista una conversione di identità tra il tipo dell'override e la proprietà ereditata.
Una dichiarazione di proprietà di override può includere il sealed
modificatore. L'uso di questo modificatore impedisce a una classe derivata di eseguire ulteriormente l'override della proprietà. Anche le funzioni di accesso di una proprietà sealed sono sealed.
Ad eccezione delle differenze nella sintassi di dichiarazione e chiamata, le funzioni di accesso virtuali, sealed, override e astratte si comportano esattamente come metodi virtuali, sealed, override e astratti. In particolare, le regole descritte in §15.6.4, §15.6.5, §15.6.6 e §15.6.7 si applicano come se le funzioni di accesso fossero metodi di un modulo corrispondente:
- Una funzione di accesso get corrisponde a un metodo senza parametri con un valore restituito del tipo di proprietà e gli stessi modificatori della proprietà contenitore.
- Una funzione di accesso set corrisponde a un metodo con un singolo parametro value del tipo di proprietà, un tipo restituito void e gli stessi modificatori della proprietà contenitore.
Esempio: nel codice seguente
abstract class A { int y; public virtual int X { get => 0; } public virtual int Y { get => y; set => y = value; } public abstract int Z { get; set; } }
X
è una proprietà di sola lettura virtuale,Y
è una proprietà di lettura/scrittura virtuale edZ
è una proprietà astratta di lettura/scrittura. PoichéZ
è astratta, anche la classe contenitore A deve essere dichiarata astratta.Di seguito è illustrata una classe che deriva da
A
:class B : A { int z; public override int X { get => base.X + 1; } public override int Y { set => base.Y = value < 0 ? 0: value; } public override int Z { get => z; set => z = value; } }
In questo caso, le dichiarazioni di ,
X
eY
sostituiscono le dichiarazioni diZ
proprietà. Ogni dichiarazione di proprietà corrisponde esattamente ai modificatori di accessibilità, al tipo e al nome della proprietà ereditata corrispondente. La funzione di accesso get di e la funzione diX
accesso set dell'uso dellaY
parola chiave base per accedere alle funzioni di accesso ereditate. La dichiarazione di esegue l'override diZ
entrambe le funzioni di accesso astratte, pertanto non esistono membri di funzione in sospesoabstract
inB
eB
può essere una classe non astratta.esempio finale
Quando una proprietà viene dichiarata come override, tutte le funzioni di accesso sottoposte a override saranno accessibili al codice di override. Inoltre, l'accessibilità dichiarata della proprietà o dell'indicizzatore stesso e delle funzioni di accesso deve corrispondere a quella del membro e delle funzioni di accesso sottoposte a override.
Esempio:
public class B { public virtual int P { get {...} protected set {...} } } public class D: B { public override int P { get {...} // Must not have a modifier here protected set {...} // Must specify protected here } }
esempio finale
15.8 Eventi
15.8.1 Generale
Un evento è un membro che consente a un oggetto o a una classe di inviare notifiche. Clients can attach executable code for events by supplying gestori eventi.
Gli eventi vengono dichiarati usando event_declarations:
event_declaration
: attributes? event_modifier* 'event' type variable_declarators ';'
| attributes? event_modifier* 'event' type member_name
'{' event_accessor_declarations '}'
;
event_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| unsafe_modifier // unsafe code support
;
event_accessor_declarations
: add_accessor_declaration remove_accessor_declaration
| remove_accessor_declaration add_accessor_declaration
;
add_accessor_declaration
: attributes? 'add' block
;
remove_accessor_declaration
: attributes? 'remove' block
;
unsafe_modifier (§23.2) è disponibile solo nel codice non sicuro (§23).
Un event_declaration può includere un set di attributi (§22) e uno dei tipi consentiti di accessibilità dichiarata (§15.3.6), il new
(§15.3.5), static
(§15.6.3, §15.8.4), virtual
(§15.6.4, §15.8.5), override
(§15.6.5, §15.8.5), sealed
(§15.6.6), abstract
(§15.6.7, §15.8.5) e extern
(§15.6.8) modificatori.
Le dichiarazioni di eventi sono soggette alle stesse regole delle dichiarazioni di metodo (§15.6) per quanto riguarda le combinazioni valide di modificatori.
Il tipo di dichiarazione di evento deve essere un delegate_type (§8.2.8) e tale delegate_type deve essere accessibile almeno quanto l'evento stesso (§7.5.5).
Una dichiarazione di evento può includere event_accessor_declarations. Tuttavia, se non lo fa, per gli eventi non extern e non astratti, un compilatore fornirà automaticamente gli eventi (§15.8.2); per gli eventi extern
, gli accessori vengono forniti esternamente.
Una dichiarazione di evento che omette event_accessor_declarations definisce uno o più eventi, uno per ognuno dei variable_declarator. Gli attributi e i modificatori si applicano a tutti i membri dichiarati da tale event_declaration.
Si tratta di un errore in fase di compilazione per un event_declaration includere sia il abstract
modificatore che event_accessor_declarations.
Quando una dichiarazione di evento include un extern
modificatore, l'evento viene detto come un evento esterno. Poiché una dichiarazione di evento esterno non fornisce alcuna implementazione effettiva, è un errore per includere sia il extern
modificatore che event_accessor_declarations.
Si tratta di un errore in fase di compilazione per un variable_declarator di una dichiarazione di evento con un abstract
modificatore o external
per includere un variable_initializer.
Un evento può essere usato come operando sinistro degli +=
operatori e -=
. Questi operatori vengono usati, rispettivamente, per associare gestori eventi a o per rimuovere i gestori eventi da un evento e i modificatori di accesso del controllo dell'evento nei contesti in cui tali operazioni sono consentite.
Le uniche operazioni consentite per un evento da codice esterno al tipo in cui viene dichiarato l'evento sono +=
e -=
. Pertanto, anche se tale codice può aggiungere e rimuovere gestori per un evento, non può ottenere o modificare direttamente l'elenco sottostante di gestori eventi.
In un'operazione della maschera x += y
o x –= y
, quando x
è un evento il risultato dell'operazione ha tipo void
(§12.21.5) (anziché avere il tipo di , con il valore di x
x
dopo l'assegnazione, come per gli altri +=
operatori e -=
definiti in tipi non di evento). Ciò impedisce al codice esterno di esaminare indirettamente il delegato sottostante di un evento.
Esempio: l'esempio seguente mostra come vengono collegati i gestori eventi alle istanze della
Button
classe :public delegate void EventHandler(object sender, EventArgs e); public class Button : Control { public event EventHandler Click; } public class LoginDialog : Form { Button okButton; Button cancelButton; public LoginDialog() { okButton = new Button(...); okButton.Click += new EventHandler(OkButtonClick); cancelButton = new Button(...); cancelButton.Click += new EventHandler(CancelButtonClick); } void OkButtonClick(object sender, EventArgs e) { // Handle okButton.Click event } void CancelButtonClick(object sender, EventArgs e) { // Handle cancelButton.Click event } }
In questo caso, il costruttore dell'istanza
LoginDialog
crea dueButton
istanze e collega i gestori eventi agliClick
eventi.esempio finale
15.8.2 Eventi simili ai campi
All'interno del testo del programma della classe o dello struct che contiene la dichiarazione di un evento, alcuni eventi possono essere usati come campi. Per essere utilizzato in questo modo, un evento non deve essere astratto o extern e non includerà in modo esplicito event_accessor_declarations. Tale evento può essere usato in qualsiasi contesto che consenta un campo. Il campo contiene un delegato (§20), che fa riferimento all'elenco di gestori eventi aggiunti all'evento. Se non sono stati aggiunti gestori eventi, il campo contiene null
.
Esempio: nel codice seguente
public delegate void EventHandler(object sender, EventArgs e); public class Button : Control { public event EventHandler Click; protected void OnClick(EventArgs e) { EventHandler handler = Click; if (handler != null) { handler(this, e); } } public void Reset() => Click = null; }
Click
viene usato come campo all'interno dellaButton
classe . Come illustrato nell'esempio, il campo può essere esaminato, modificato e usato nelle espressioni di chiamata del delegato. IlOnClick
metodo nellaButton
classe "genera" l'eventoClick
. Generare un evento equivale a richiamare il delegato rappresentato dall'evento. Non sono quindi necessari speciali costrutti di linguaggio per generare eventi. Si noti che la chiamata al delegato è preceduta da un controllo che garantisce che il delegato sia diverso da Null e che il controllo venga eseguito in una copia locale per garantire la thread safety.All'esterno della dichiarazione della
Button
classe, ilClick
membro può essere usato solo sul lato sinistro degli+=
operatori e–=
, come inb.Click += new EventHandler(...);
che aggiunge un delegato all'elenco chiamate dell'evento
Click
eClick –= new EventHandler(...);
che rimuove un delegato dall'elenco chiamate dell'evento
Click
.esempio finale
Quando si compila un evento simile a un campo, un compilatore creerà automaticamente uno spazio di archiviazione per mantenere il delegato e creerà metodi di accesso per l'evento che aggiungono o rimuovono gestori di eventi al campo delegato. Le operazioni di addizione e rimozione sono thread-safe e possono (ma non è necessario) essere eseguite tenendo premuto il blocco (§13.13) sull'oggetto contenitore per un evento di istanza o l'oggetto System.Type
(§12.8.18) per un evento statico.
Nota: di conseguenza, una dichiarazione di evento di istanza del formato:
class X { public event D Ev; }
deve essere compilato in un elemento equivalente a:
class X { private D __Ev; // field to hold the delegate public event D Ev { add { /* Add the delegate in a thread safe way */ } remove { /* Remove the delegate in a thread safe way */ } } }
All'interno della classe
X
, i riferimenti aEv
sul lato sinistro degli+=
operatori e–=
determinano la chiamata delle funzioni di accesso add and remove. Tutti gli altri riferimenti aEv
vengono compilati per fare riferimento al campo__Ev
nascosto (§12.8.7). Il nome "__Ev
" è arbitrario. Il campo nascosto potrebbe avere un nome o nessun nome.nota finale
15.8.3 Funzioni di accesso agli eventi
Nota: le dichiarazioni di evento in genere omettono event_accessor_declarations, come nell'esempio
Button
precedente. Ad esempio, potrebbero essere inclusi se il costo di archiviazione di un campo per evento non è accettabile. In questi casi, una classe può includere event_accessor_declaratione usare un meccanismo privato per archiviare l'elenco di gestori eventi. nota finale
Il event_accessor_declarations di un evento specifica le istruzioni eseguibili associate all'aggiunta e alla rimozione di gestori eventi.
Le dichiarazioni della funzione di accesso sono costituite da un add_accessor_declaration e da un remove_accessor_declaration. Ogni dichiarazione della funzione di accesso è costituita dall'aggiunta o dalla rimozione del token seguita da un blocco. Il blocco associato a un add_accessor_declaration specifica le istruzioni da eseguire quando viene aggiunto un gestore eventi e il blocco associato a un remove_accessor_declaration specifica le istruzioni da eseguire quando viene rimosso un gestore eventi.
Ogni add_accessor_declaration e remove_accessor_declaration corrisponde a un metodo con un singolo parametro valore del tipo di evento e un void
tipo restituito. Il parametro implicito di una funzione di accesso agli eventi è denominato value
. Quando un evento viene usato in un'assegnazione di eventi, viene usata la funzione di accesso eventi appropriata. In particolare, se l'operatore di assegnazione è , viene +=
usata la funzione di accesso add e, se l'operatore di assegnazione è –=
, viene usata la funzione di accesso remove. In entrambi i casi, l'operando destro dell'operatore di assegnazione viene usato come argomento per la funzione di accesso all'evento. Il blocco di un add_accessor_declaration o un remove_accessor_declaration è conforme alle regole per void
i metodi descritti in §15.6.9. In particolare, return
le istruzioni in un blocco di questo tipo non sono autorizzate a specificare un'espressione.
Poiché una funzione di accesso eventi ha in modo implicito un parametro denominato value
, si tratta di un errore in fase di compilazione per una variabile locale o una costante dichiarata in una funzione di accesso all'evento per avere tale nome.
Esempio: nel codice seguente
class Control : Component { // Unique keys for events static readonly object mouseDownEventKey = new object(); static readonly object mouseUpEventKey = new object(); // Return event handler associated with key protected Delegate GetEventHandler(object key) {...} // Add event handler associated with key protected void AddEventHandler(object key, Delegate handler) {...} // Remove event handler associated with key protected void RemoveEventHandler(object key, Delegate handler) {...} // MouseDown event public event MouseEventHandler MouseDown { add { AddEventHandler(mouseDownEventKey, value); } remove { RemoveEventHandler(mouseDownEventKey, value); } } // MouseUp event public event MouseEventHandler MouseUp { add { AddEventHandler(mouseUpEventKey, value); } remove { RemoveEventHandler(mouseUpEventKey, value); } } // Invoke the MouseUp event protected void OnMouseUp(MouseEventArgs args) { MouseEventHandler handler; handler = (MouseEventHandler)GetEventHandler(mouseUpEventKey); if (handler != null) { handler(this, args); } } }
la
Control
classe implementa un meccanismo di archiviazione interno per gli eventi. IlAddEventHandler
metodo associa un valore delegato a una chiave, ilGetEventHandler
metodo restituisce il delegato attualmente associato a una chiave e ilRemoveEventHandler
metodo rimuove un delegato come gestore eventi per l'evento specificato. Presumibilmente, il meccanismo di archiviazione sottostante è progettato in modo che non vi sia alcun costo per l'associazione di un valore delegato Null a una chiave e quindi gli eventi non gestiti non utilizzano alcuna risorsa di archiviazione.esempio finale
15.8.4 Eventi statici e di istanza
Quando una dichiarazione di evento include un static
modificatore, si dice che l'evento sia un evento statico. Quando non è presente alcun static
modificatore, si dice che l'evento sia un evento di istanza.
Un evento statico non è associato a un'istanza specifica ed è un errore in fase di compilazione a cui fare riferimento this
nelle funzioni di accesso di un evento statico.
Un evento di istanza è associato a una determinata istanza di una classe ed è possibile accedere a questa istanza come this
(§12.8.14) nelle funzioni di accesso di tale evento.
Le differenze tra i membri statici e dell'istanza sono descritte più avanti in §15.3.8.
15.8.5 Funzioni di accesso virtuali, sealed, override e astratte
Una dichiarazione di evento virtuale specifica che le funzioni di accesso di tale evento sono virtuali. Il virtual
modificatore si applica a entrambe le funzioni di accesso di un evento.
Una dichiarazione di evento astratta specifica che le funzioni di accesso dell'evento sono virtuali, ma non forniscono un'implementazione effettiva delle funzioni di accesso. Al contrario, le classi derivate non astratte sono necessarie per fornire la propria implementazione per le funzioni di accesso eseguendo l'override dell'evento. Poiché una funzione di accesso per una dichiarazione di evento astratta non fornisce alcuna implementazione effettiva, non fornisce event_accessor_declarations.
Una dichiarazione di evento che include i abstract
modificatori e override
specifica che l'evento è astratto ed esegue l'override di un evento di base. Anche le funzioni di accesso di questo evento sono astratte.
Le dichiarazioni di evento astratte sono consentite solo nelle classi astratte (§15.2.2.2).
Le funzioni di accesso di un evento virtuale ereditato possono essere sottoposte a override in una classe derivata includendo una dichiarazione di evento che specifica un override
modificatore. Questa operazione è nota come dichiarazione di evento di override. Una dichiarazione di evento di override non dichiara un nuovo evento. Ma è semplicemente specializzata nelle implementazioni delle funzioni di accesso di un evento virtuale esistente.
Una dichiarazione di evento di override specifica gli stessi modificatori di accessibilità e nome dell'evento sottoposto a override, deve essere presente una conversione di identità tra il tipo dell'override e l'evento sottoposto a override e le funzioni di accesso add e remove devono essere specificate all'interno della dichiarazione.
Una dichiarazione di evento di override può includere il sealed
modificatore. L'uso del this
modificatore impedisce a una classe derivata di eseguire ulteriormente l'override dell'evento. Anche le funzioni di accesso di un evento sealed sono sealed.
Si tratta di un errore in fase di compilazione per una dichiarazione di evento di override per includere un new
modificatore.
Ad eccezione delle differenze nella sintassi di dichiarazione e chiamata, le funzioni di accesso virtuali, sealed, override e astratte si comportano esattamente come metodi virtuali, sealed, override e astratti. In particolare, le regole descritte in §15.6.4, §15.6.5, §15.6.6 e §15.6.7 si applicano come se le funzioni di accesso fossero metodi di un modulo corrispondente. Ogni funzione di accesso corrisponde a un metodo con un singolo parametro value del tipo di evento, un void
tipo restituito e gli stessi modificatori dell'evento contenitore.
Indicizzatori 15.9
15.9.1 Generale
Un indicizzatore è un membro che consente l'indicizzazione di un oggetto nello stesso modo di una matrice. Gli indicizzatori vengono dichiarati usando indexer_declarations:
indexer_declaration
: attributes? indexer_modifier* indexer_declarator indexer_body
| attributes? indexer_modifier* ref_kind indexer_declarator ref_indexer_body
;
indexer_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| unsafe_modifier // unsafe code support
;
indexer_declarator
: type 'this' '[' parameter_list ']'
| type interface_type '.' 'this' '[' parameter_list ']'
;
indexer_body
: '{' accessor_declarations '}'
| '=>' expression ';'
;
ref_indexer_body
: '{' ref_get_accessor_declaration '}'
| '=>' 'ref' variable_reference ';'
;
unsafe_modifier (§23.2) è disponibile solo nel codice non sicuro (§23).
Esistono due tipi di indexer_declaration:
- Il primo dichiara un indicizzatore non con valori di riferimento. Il valore ha un tipo. Questo tipo di indicizzatore può essere leggibile e/o scrivibile.
- Il secondo dichiara un indicizzatore con valori di riferimento. Il valore è un variable_reference (§9.5
readonly
Questo tipo di indicizzatore è leggibile solo.
Un indexer_declaration può includere un set di attributi (§22) e uno dei tipi consentiti di accessibilità dichiarata (§15.3.6), il new
(§15.3.5), (§15.5) virtual
6.4), override
(§15.6.5), sealed
(§15.6.6), abstract
(§15.6.7) e extern
(§15.6.8) modificatori.
Le dichiarazioni dell'indicizzatore sono soggette alle stesse regole delle dichiarazioni di metodo (§15.6) per quanto riguarda le combinazioni valide di modificatori, con l'unica eccezione che il modificatore non è consentito in una dichiarazione dell'indicizzatore static
.
Il tipo di una dichiarazione dell'indicizzatore specifica il tipo di elemento dell'indicizzatore introdotto dalla dichiarazione.
Nota: poiché gli indicizzatori sono progettati per essere usati in contesti di tipo elemento matrice, il tipo di elemento come definito per una matrice viene usato anche con un indicizzatore. nota finale
A meno che l'indicizzatore non sia un'implementazione esplicita del membro dell'interfaccia, il tipo è seguito dalla parola chiave this
. Per un'implementazione esplicita del membro dell'interfaccia, il tipo è seguito da un interface_type, un ".
" e la parola chiave this
. A differenza di altri membri, gli indicizzatori non hanno nomi definiti dall'utente.
Il parameter_list specifica i parametri dell'indicizzatore. L'elenco di parametri di un indicizzatore corrisponde a quello di un metodo (§15.6.2), ad eccezione del fatto che è necessario specificare almeno un parametro e che i this
modificatori di parametri , ref
e out
non sono consentiti.
Il tipo di un indicizzatore e ognuno dei tipi a cui si fa riferimento nella parameter_list deve essere accessibile almeno quanto l'indicizzatore stesso (§7.5.5).
Un indexer_body può essere costituito da un corpo dell'istruzione (§15.7.1) o da un corpo dell'espressione (§15.6.1). In un corpo dell'istruzione, accessor_declarations, che deve essere racchiuso in token "{
" e "}
", dichiara le funzioni di accesso (§15.7.3) dell'indicizzatore. Le funzioni di accesso specificano le istruzioni eseguibili associate alla lettura e alla scrittura di elementi dell'indicizzatore.
In un indexer_body un corpo di espressione costituito da "=>
" seguito da un'espressione E
e un punto e virgola è esattamente equivalente al corpo { get { return E; } }
dell'istruzione e può quindi essere usato solo per specificare indicizzatori di sola lettura in cui il risultato della funzione di accesso get viene fornito da una singola espressione.
Un ref_indexer_body può essere costituito da un corpo dell'istruzione o da un corpo dell'espressione. In un corpo di un'istruzione un get_accessor_declaration dichiara la funzione di accesso get (§15.7.3) dell'indicizzatore. La funzione di accesso specifica le istruzioni eseguibili associate alla lettura dell'indicizzatore.
In un ref_indexer_body un corpo di =>
espressione costituito da seguito da ref
, un variable_referenceV
e un punto e virgola equivale esattamente al corpo { get { return ref V; } }
dell'istruzione .
Nota: anche se la sintassi per l'accesso a un elemento indicizzatore è identica a quella per un elemento di matrice, un elemento indicizzatore non viene classificato come variabile. Pertanto, non è possibile passare un elemento indicizzatore come
in
argomento ,out
oref
a meno che l'indicizzatore non sia ref-valued e restituisce quindi un riferimento (§9,7). nota finale
Il parameter_list di un indicizzatore definisce la firma (§7.6) dell'indicizzatore. In particolare, la firma di un indicizzatore è costituita dal numero e dai tipi dei relativi parametri. Il tipo di elemento e i nomi dei parametri non fanno parte della firma di un indicizzatore.
La firma di un indicizzatore deve essere diversa dalle firme di tutti gli altri indicizzatori dichiarati nella stessa classe.
Quando una dichiarazione dell'indicizzatore include un extern
modificatore, l'indicizzatore viene detto come indicizzatore esterno. Poiché una dichiarazione dell'indicizzatore esterno non fornisce alcuna implementazione effettiva, ognuna delle accessor_body nel relativo accessor_declarations deve essere un punto e virgola.
Esempio: l'esempio seguente dichiara una
BitArray
classe che implementa un indicizzatore per l'accesso ai singoli bit nella matrice di bit.class BitArray { int[] bits; int length; public BitArray(int length) { if (length < 0) { throw new ArgumentException(); } bits = new int[((length - 1) >> 5) + 1]; this.length = length; } public int Length => length; public bool this[int index] { get { if (index < 0 || index >= length) { throw new IndexOutOfRangeException(); } return (bits[index >> 5] & 1 << index) != 0; } set { if (index < 0 || index >= length) { throw new IndexOutOfRangeException(); } if (value) { bits[index >> 5] |= 1 << index; } else { bits[index >> 5] &= ~(1 << index); } } } }
Un'istanza della
BitArray
classe utilizza sostanzialmente meno memoria di un oggetto corrispondentebool[]
(poiché ogni valore del primo occupa un solo bit anziché quello dibyte
quest'ultimo), ma consente le stesse operazioni di un oggettobool[]
.La classe seguente
CountPrimes
usa unBitArray
algoritmo e l'algoritmo classico "sieve" per calcolare il numero di primi tra 2 e un determinato valore massimo:class CountPrimes { static int Count(int max) { BitArray flags = new BitArray(max + 1); int count = 0; for (int i = 2; i <= max; i++) { if (!flags[i]) { for (int j = i * 2; j <= max; j += i) { flags[j] = true; } count++; } } return count; } static void Main(string[] args) { int max = int.Parse(args[0]); int count = Count(max); Console.WriteLine($"Found {count} primes between 2 and {max}"); } }
Si noti che la sintassi per l'accesso agli elementi di
BitArray
è esattamente la stessa di per un oggettobool[]
.L'esempio seguente mostra una classe griglia 26×10 con un indicizzatore con due parametri. Il primo parametro deve essere una lettera maiuscola o minuscola nell'intervallo A-Z e la seconda deve essere un numero intero compreso nell'intervallo da 0 a 9.
class Grid { const int NumRows = 26; const int NumCols = 10; int[,] cells = new int[NumRows, NumCols]; public int this[char row, int col] { get { row = Char.ToUpper(row); if (row < 'A' || row > 'Z') { throw new ArgumentOutOfRangeException("row"); } if (col < 0 || col >= NumCols) { throw new ArgumentOutOfRangeException ("col"); } return cells[row - 'A', col]; } set { row = Char.ToUpper(row); if (row < 'A' || row > 'Z') { throw new ArgumentOutOfRangeException ("row"); } if (col < 0 || col >= NumCols) { throw new ArgumentOutOfRangeException ("col"); } cells[row - 'A', col] = value; } } }
esempio finale
15.9.2 Differenze tra indicizzatore e proprietà
Gli indicizzatori e le proprietà sono molto simili nel concetto, ma differiscono nei modi seguenti:
- Una proprietà viene identificata dal nome, mentre un indicizzatore è identificato dalla firma.
- È possibile accedere a una proprietà tramite un simple_name (§12.8.4) o un member_access (§12.8.7), mentre un elemento indicizzatore è accessibile tramite un element_access (§12.8.12.3).
- Una proprietà può essere un membro statico, mentre un indicizzatore è sempre un membro dell'istanza.
- Una funzione di accesso get di una proprietà corrisponde a un metodo senza parametri, mentre una funzione di accesso get di un indicizzatore corrisponde a un metodo con lo stesso elenco di parametri dell'indicizzatore.
- Una funzione di accesso set di una proprietà corrisponde a un metodo con un singolo parametro denominato
value
, mentre una funzione di accesso set di un indicizzatore corrisponde a un metodo con lo stesso elenco di parametri dell'indicizzatore, più un parametro aggiuntivo denominatovalue
. - Si tratta di un errore in fase di compilazione per una funzione di accesso dell'indicizzatore per dichiarare una variabile locale o una costante locale con lo stesso nome di un parametro dell'indicizzatore.
- In una dichiarazione di proprietà di override, si accede alla proprietà ereditata usando la sintassi
base.P
, doveP
è il nome della proprietà. In una dichiarazione dell'indicizzatore di override, l'indicizzatore ereditato è accessibile usando la sintassibase[E]
, doveE
è un elenco delimitato da virgole di espressioni. - Non esiste alcun concetto di "indicizzatore implementato automaticamente". Si tratta di un errore per avere un indicizzatore non astratto non esterno con punto e virgola accessor_bodys.
A parte queste differenze, tutte le regole definite in §15.7.3, §15.7.5 e §15.7.6 si applicano alle funzioni di accesso dell'indicizzatore e alle funzioni di accesso alle proprietà.
Questa sostituzione di proprietà/proprietà con indicizzatori/indicizzatori durante la lettura di §15.7.3, §15.7.5 e §15.7.6 si applica anche ai termini definiti. In particolare, la proprietà di lettura/scrittura diventa indicizzatore di lettura/scrittura, la proprietà di sola lettura diventa indicizzatore di sola lettura e la proprietà di sola scrittura diventa indicizzatore di sola scrittura.
Operatori 15.10
15.10.1 Generale
Un operatore è un membro che definisce il significato di un operatore di espressione che può essere applicato alle istanze della classe . Gli operatori vengono dichiarati usando operator_declarations:
operator_declaration
: attributes? operator_modifier+ operator_declarator operator_body
;
operator_modifier
: 'public'
| 'static'
| 'extern'
| unsafe_modifier // unsafe code support
;
operator_declarator
: unary_operator_declarator
| binary_operator_declarator
| conversion_operator_declarator
;
unary_operator_declarator
: type 'operator' overloadable_unary_operator '(' fixed_parameter ')'
;
logical_negation_operator
: '!'
;
overloadable_unary_operator
: '+' | '-' | logical_negation_operator | '~' | '++' | '--' | 'true' | 'false'
;
binary_operator_declarator
: type 'operator' overloadable_binary_operator
'(' fixed_parameter ',' fixed_parameter ')'
;
overloadable_binary_operator
: '+' | '-' | '*' | '/' | '%' | '&' | '|' | '^' | '<<'
| right_shift | '==' | '!=' | '>' | '<' | '>=' | '<='
;
conversion_operator_declarator
: 'implicit' 'operator' type '(' fixed_parameter ')'
| 'explicit' 'operator' type '(' fixed_parameter ')'
;
operator_body
: block
| '=>' expression ';'
| ';'
;
unsafe_modifier (§23.2) è disponibile solo nel codice non sicuro (§23).
Nota: gli operatori prefix logical negation (§12.9.4) e postfix null-forgiving operators (§12.8.9), mentre rappresentati dallo stesso token lessicale (!
) sono distinti. Quest'ultimo non è un operatore di overload.
nota finale
Esistono tre categorie di operatori di overload: operatori Unari (§15.10.2), operatori binari (§15.10.3) e operatori di conversione (§15.10.4).
Il operator_body è un punto e virgola, un corpo del blocco (§15.6.1) o un corpo dell'espressione (§15.6.1). Un corpo del blocco è costituito da un blocco, che specifica le istruzioni da eseguire quando viene richiamato l'operatore. Il blocco deve essere conforme alle regole per i metodi di restituzione dei valori descritti in §15.6.11. Un corpo dell'espressione =>
è costituito da un'espressione e da un punto e virgola e indica una singola espressione da eseguire quando viene richiamato l'operatore.
Per extern
gli operatori, il operator_body è costituito semplicemente da un punto e virgola. Per tutti gli altri operatori, il operator_body è un corpo del blocco o un corpo dell'espressione.
Le regole seguenti si applicano a tutte le dichiarazioni di operatore:
- Una dichiarazione di operatore include sia un
public
modificatore che unstatic
modificatore. - I parametri di un operatore non devono avere modificatori diversi da
in
. - La firma di un operatore (§15.10.2, §15.10.3, §15.10.4) è diversa dalle firme di tutti gli altri operatori dichiarati nella stessa classe.
- Tutti i tipi a cui viene fatto riferimento in una dichiarazione di operatore devono essere accessibili almeno quanto l'operatore stesso (§7.5.5).
- Si tratta di un errore per il quale lo stesso modificatore viene visualizzato più volte in una dichiarazione di operatore.
Ogni categoria di operatori impone restrizioni aggiuntive, come descritto nelle sottoclause seguenti.
Analogamente ad altri membri, gli operatori dichiarati in una classe base vengono ereditati dalle classi derivate. Poiché le dichiarazioni di operatore richiedono sempre la classe o lo struct in cui l'operatore viene dichiarato di partecipare alla firma dell'operatore, non è possibile che un operatore dichiarato in una classe derivata nasconda un operatore dichiarato in una classe base. Pertanto, il new
modificatore non è mai necessario, e pertanto non è mai consentito, in una dichiarazione di operatore.
Altre informazioni sugli operatori unari e binari sono disponibili in §12.4.
Altre informazioni sugli operatori di conversione sono disponibili in §10.5.
15.10.2 Operatori unari
Le regole seguenti si applicano alle dichiarazioni di operatore unario, dove T
indica il tipo di istanza della classe o dello struct che contiene la dichiarazione dell'operatore:
- Un operatore unario
+
,-
(!
solo negazione logica) o~
accetta un singolo parametro di tipoT
oT?
può restituire qualsiasi tipo. - Un operatore unario
++
o--
accetta un singolo parametro di tipoT
oT?
e restituisce lo stesso tipo o un tipo derivato da esso. - Un operatore unario
true
o accetta un singolo parametro di tipofalse
oT
e deve restituire il tipoT?
bool
.
La firma di un operatore unario è costituita dal token dell'operatore (+
, , -
!
~
, ++
--
, true
o false
) e dal tipo del singolo parametro. Il tipo restituito non fa parte della firma di un operatore unario, né è il nome del parametro.
Gli true
operatori unari e false
richiedono una dichiarazione a livello di coppia. Se una classe dichiara uno di questi operatori senza dichiarare l'altro, si verifica un errore in fase di compilazione. Gli true
operatori e false
sono descritti più avanti in §12.24.
Esempio: l'esempio seguente illustra un'implementazione e l'utilizzo successivo di operator++ per una classe vector integer:
public class IntVector { public IntVector(int length) {...} public int Length { get { ... } } // Read-only property public int this[int index] { get { ... } set { ... } } // Read-write indexer public static IntVector operator++(IntVector iv) { IntVector temp = new IntVector(iv.Length); for (int i = 0; i < iv.Length; i++) { temp[i] = iv[i] + 1; } return temp; } } class Test { static void Main() { IntVector iv1 = new IntVector(4); // Vector of 4 x 0 IntVector iv2; iv2 = iv1++; // iv2 contains 4 x 0, iv1 contains 4 x 1 iv2 = ++iv1; // iv2 contains 4 x 2, iv1 contains 4 x 2 } }
Si noti che il metodo operatore restituisce il valore prodotto aggiungendo 1 all'operando, proprio come gli operatori di incremento e decremento postfissi (§12.8.16) e gli operatori di incremento e decremento del prefisso (§12.9.6). A differenza di C++, questo metodo non deve modificare direttamente il valore del relativo operando perché viola la semantica standard dell'operatore di incremento del prefisso (§12.8.16).
esempio finale
Operatori binari 15.10.3
Le regole seguenti si applicano alle dichiarazioni dell'operatore binario, dove T
indica il tipo di istanza della classe o dello struct che contiene la dichiarazione dell'operatore:
- Un operatore binario non shift accetta due parametri, almeno uno dei quali deve avere tipo
T
oT?
e può restituire qualsiasi tipo. - Un operatore o binario
<<
(>>
) accetta due parametri, il primo dei quali deve avere il tipo o T? e il secondo di cui deve avere il tipoT
oint
e puòint?
restituire qualsiasi tipo.
La firma di un operatore binario è costituita dal token dell'operatore (+
, -
, *
/
%
&
|
^
<<
>>
==
!=
>
<
>=
, o <=
) e dai tipi dei due parametri. Il tipo restituito e i nomi dei parametri non fanno parte della firma di un operatore binario.
Alcuni operatori binari richiedono una dichiarazione a livello di coppia. Per ogni dichiarazione di uno degli operatori di una coppia, è presente una dichiarazione corrispondente dell'altro operatore della coppia. Due dichiarazioni di operatore corrispondono se esistono conversioni di identità tra i tipi restituiti e i tipi di parametro corrispondenti. Gli operatori seguenti richiedono una dichiarazione a livello di coppia:
- Operatore
==
e operatore!=
- Operatore
>
e operatore<
- Operatore
>=
e operatore<=
15.10.4 Operatori di conversione
Una dichiarazione di operatore di conversione introduce una conversione definita dall'utente (§10.5), che aumenta le conversioni implicite ed esplicite predefinite.
Una dichiarazione dell'operatore di conversione che include la implicit
parola chiave introduce una conversione implicita definita dall'utente. Le conversioni implicite possono verificarsi in diverse situazioni, tra cui chiamate ai membri della funzione, espressioni di cast e assegnazioni. Questo è descritto più avanti in §10.2.
Una dichiarazione di operatore di conversione che include la explicit
parola chiave introduce una conversione esplicita definita dall'utente. Le conversioni esplicite possono verificarsi nelle espressioni cast e sono descritte più avanti in §10.3.
Un operatore di conversione converte da un tipo di origine, indicato dal tipo di parametro dell'operatore di conversione, a un tipo di destinazione, indicato dal tipo restituito dell'operatore di conversione.
Per un tipo di origine e un tipo S
di T
destinazione specificati, se S
o T
sono tipi valore nullable, consentire S₀
e T₀
fare riferimento ai relativi tipi sottostanti; in caso contrario, S₀
e T₀
sono uguali rispettivamente a S
e T
. Una classe o uno struct è autorizzato a dichiarare una conversione da un tipo di origine a un tipo S
di T
destinazione solo se sono soddisfatte tutte le condizioni seguenti:
S₀
eT₀
sono tipi diversi.S₀
OppureT₀
è il tipo di istanza della classe o dello struct che contiene la dichiarazione dell'operatore.Né
S₀
néT₀
è un interface_type.Escluse le conversioni definite dall'utente, una conversione non esiste da
S
aT
o daT
aS
.
Ai fini di queste regole, tutti i parametri di tipo associati a S
o T
sono considerati tipi univoci che non hanno alcuna relazione di ereditarietà con altri tipi e tutti i vincoli per tali parametri di tipo vengono ignorati.
Esempio: nell'esempio seguente:
class C<T> {...} class D<T> : C<T> { public static implicit operator C<int>(D<T> value) {...} // Ok public static implicit operator C<string>(D<T> value) {...} // Ok public static implicit operator C<T>(D<T> value) {...} // Error }
Le prime due dichiarazioni di operatore sono consentite perché
T
eint
string
, rispettivamente sono considerati tipi univoci senza alcuna relazione. Tuttavia, il terzo operatore è un errore perchéC<T>
è la classe base diD<T>
.esempio finale
Dalla seconda regola, segue che un operatore di conversione deve convertire in o dal tipo di classe o struct in cui viene dichiarato l'operatore.
Esempio: è possibile che un tipo
C
di classe o struct definisci una conversione daC
e versoint
, ma non daint
C
int
a .bool
esempio finale
Non è possibile ridefinire direttamente una conversione predefinita. Pertanto, gli operatori di conversione non possono eseguire la conversione da o in object
perché le conversioni implicite ed esplicite esistono già tra object
e tutti gli altri tipi. Analogamente, né l'origine né i tipi di destinazione di una conversione possono essere un tipo di base dell'altro, poiché una conversione sarebbe già presente. Tuttavia, è possibile dichiarare operatori su tipi generici che, per determinati argomenti di tipo, specificano le conversioni già esistenti come conversioni predefinite.
Esempio:
struct Convertible<T> { public static implicit operator Convertible<T>(T value) {...} public static explicit operator T(Convertible<T> value) {...} }
quando il tipo
object
viene specificato come argomento di tipo perT
, il secondo operatore dichiara una conversione già esistente (implicito e pertanto esiste anche una conversione esplicita da qualsiasi tipo a oggetto type).esempio finale
Nei casi in cui esiste una conversione predefinita tra due tipi, tutte le conversioni definite dall'utente tra tali tipi vengono ignorate. In particolare:
- Se esiste una conversione implicita predefinita (§10.2) dal tipo al tipo
S
T
, tutte le conversioni definite dall'utente (implicite o esplicite) daS
aT
vengono ignorate. - Se esiste una conversione esplicita predefinita (§10.3) dal tipo al tipo
S
T
, tutte le conversioni esplicite definite dall'utente daS
aT
vengono ignorate. Inoltre:- Se o
S
T
è un tipo di interfaccia, le conversioni implicite definite dall'utente daS
aT
vengono ignorate. - In caso contrario, le conversioni implicite definite dall'utente da
S
aT
vengono comunque considerate.
- Se o
Per tutti i tipi ma object
, gli operatori dichiarati dal Convertible<T>
tipo precedente non sono in conflitto con conversioni predefinite.
Esempio:
void F(int i, Convertible<int> n) { i = n; // Error i = (int)n; // User-defined explicit conversion n = i; // User-defined implicit conversion n = (Convertible<int>)i; // User-defined implicit conversion }
Tuttavia, per il tipo
object
, le conversioni predefinite nascondono le conversioni definite dall'utente in tutti i casi, ma una:void F(object o, Convertible<object> n) { o = n; // Pre-defined boxing conversion o = (object)n; // Pre-defined boxing conversion n = o; // User-defined implicit conversion n = (Convertible<object>)o; // Pre-defined unboxing conversion }
esempio finale
Le conversioni definite dall'utente non possono essere convertite da o in interface_types. In particolare, questa restrizione garantisce che non si verifichino trasformazioni definite dall'utente durante la conversione in un interface_type e che una conversione in un interface_type abbia esito positivo solo se l'oggetto object
convertito implementa effettivamente il interface_type specificato.
La firma di un operatore di conversione è costituita dal tipo di origine e dal tipo di destinazione. Si tratta dell'unico tipo di membro per il quale il tipo restituito partecipa alla firma. La classificazione implicita o esplicita di un operatore di conversione non fa parte della firma dell'operatore. Pertanto, una classe o uno struct non può dichiarare un operatore di conversione implicito ed esplicito con gli stessi tipi di origine e di destinazione.
Nota: in generale, le conversioni implicite definite dall'utente devono essere progettate per non generare mai eccezioni e non perdere mai informazioni. Se una conversione definita dall'utente può generare eccezioni (ad esempio, perché l'argomento di origine non è compreso nell'intervallo) o la perdita di informazioni (ad esempio l'eliminazione di bit di ordine elevato), tale conversione deve essere definita come conversione esplicita. nota finale
Esempio: nel codice seguente
public struct Digit { byte value; public Digit(byte value) { if (value < 0 || value > 9) { throw new ArgumentException(); } this.value = value; } public static implicit operator byte(Digit d) => d.value; public static explicit operator Digit(byte b) => new Digit(b); }
la conversione da
Digit
abyte
è implicita perché non genera mai eccezioni o perde informazioni, ma la conversione dabyte
aDigit
è esplicita perchéDigit
può rappresentare solo un subset dei valori possibili di un oggettobyte
.esempio finale
15.11 Costruttori di istanze
15.11.1 Generale
Un costruttore di istanza è un membro che implementa le azioni necessarie per inizializzare un'istanza di una classe, I costruttori di istanza vengono dichiarati usando constructor_declarations:
constructor_declaration
: attributes? constructor_modifier* constructor_declarator constructor_body
;
constructor_modifier
: 'public'
| 'protected'
| 'internal'
| 'private'
| 'extern'
| unsafe_modifier // unsafe code support
;
constructor_declarator
: identifier '(' parameter_list? ')' constructor_initializer?
;
constructor_initializer
: ':' 'base' '(' argument_list? ')'
| ':' 'this' '(' argument_list? ')'
;
constructor_body
: block
| '=>' expression ';'
| ';'
;
unsafe_modifier (§23.2) è disponibile solo nel codice non sicuro (§23).
Un constructor_declaration può includere un set di attributi (§22), uno dei tipi consentiti di accessibilità dichiarata (§15.3.6) e un extern
modificatore (§15.6.8). Una dichiarazione del costruttore non può includere più volte lo stesso modificatore.
L'identificatore di un constructor_declarator denomina la classe in cui viene dichiarato il costruttore dell'istanza. Se viene specificato un altro nome, si verifica un errore in fase di compilazione.
Il parameter_list facoltativo di un costruttore di istanza è soggetto alle stesse regole del parameter_list di un metodo (§15.6). Poiché il this
modificatore per i parametri si applica solo ai metodi di estensione (§15.6.10), nessun parametro nel parameter_list di un costruttore deve contenere il this
modificatore. L'elenco dei parametri definisce la firma (§7.6) di un costruttore di istanza e regola il processo in base al quale la risoluzione dell'overload (§12.6.4) seleziona un costruttore di istanza specifico in una chiamata.
Ognuno dei tipi a cui si fa riferimento nella parameter_list di un costruttore di istanza deve essere accessibile almeno quanto il costruttore stesso (§7.5.5).
Il constructor_initializer facoltativo specifica un altro costruttore di istanza da richiamare prima di eseguire le istruzioni specificate nella constructor_body di questo costruttore di istanza. Questo è descritto più avanti in §15.11.2.
Quando una dichiarazione del costruttore include un extern
modificatore, si dice che il costruttore sia un costruttore esterno. Poiché una dichiarazione del costruttore esterno non fornisce alcuna implementazione effettiva, il relativo constructor_body è costituito da un punto e virgola. Per tutti gli altri costruttori, il constructor_body è costituito da uno dei due
- un blocco, che specifica le istruzioni per inizializzare una nuova istanza della classe oppure
- un corpo dell'espressione
=>
, costituito da un'espressionee da un punto e virgola, e indica una singola espressione per inizializzare una nuova istanza della classe .
Un constructor_body che è un blocco o un corpo dell'espressione corrisponde esattamente al blocco di un metodo di istanza con un void
tipo restituito (§15.6.11).
I costruttori di istanza non vengono ereditati. Pertanto, una classe non dispone di costruttori di istanza diversi da quelli effettivamente dichiarati nella classe, con l'eccezione che se una classe non contiene dichiarazioni di costruttore di istanza, viene fornito automaticamente un costruttore di istanza predefinito (§15.11.5).
I costruttori di istanza vengono richiamati da object_creation_expression(§12.8.17.2) e da constructor_initializers.
Inizializzatori del costruttore 15.11.2
Tutti i costruttori di istanza (ad eccezione di quelli per la classe object
) includono in modo implicito una chiamata di un altro costruttore di istanza immediatamente prima del constructor_body. Il costruttore da richiamare in modo implicito è determinato dal constructor_initializer:
- Un inizializzatore del costruttore di istanza del form
base(
argument_list)
(dove argument_list è facoltativo) fa sì che venga richiamato un costruttore di istanza dalla classe base diretta. Tale costruttore viene selezionato utilizzando argument_list e le regole di risoluzione dell'overload di §12.6.4. Il set di costruttori di istanze candidate è costituito da tutti i costruttori di istanza accessibili della classe base diretta. Se questo set è vuoto o se non è possibile identificare un singolo costruttore di istanza migliore, si verifica un errore in fase di compilazione. - Un inizializzatore del costruttore di istanza del modulo
this(
argument_list)
(dove argument_list è facoltativo) richiama un altro costruttore di istanza dalla stessa classe. Il costruttore viene selezionato utilizzando argument_list e le regole di risoluzione dell'overload di §12.6.4. Il set di costruttori di istanze candidate è costituito da tutti i costruttori di istanza dichiarati nella classe stessa. Se il set risultante di costruttori di istanze applicabili è vuoto o se non è possibile identificare un singolo costruttore di istanza migliore, si verifica un errore in fase di compilazione. Se una dichiarazione del costruttore di istanza richiama se stessa tramite una catena di uno o più inizializzatori del costruttore, si verifica un errore in fase di compilazione.
Se un costruttore di istanza non dispone di un inizializzatore di costruttore, viene fornito in modo implicito un inizializzatore del costruttore del modulo base()
.
Nota: pertanto, una dichiarazione del costruttore di istanza del modulo
C(...) {...}
è esattamente equivalente a
C(...) : base() {...}
nota finale
L'ambito dei parametri forniti dalla parameter_list di una dichiarazione del costruttore di istanza include l'inizializzatore del costruttore di tale dichiarazione. Pertanto, un inizializzatore del costruttore è autorizzato ad accedere ai parametri del costruttore.
Esempio:
class A { public A(int x, int y) {} } class B: A { public B(int x, int y) : base(x + y, x - y) {} }
esempio finale
Un inizializzatore del costruttore di istanza non può accedere all'istanza creata. Di conseguenza, si tratta di un errore in fase di compilazione per fare riferimento a questo oggetto in un'espressione di argomento dell'inizializzatore del costruttore, poiché si tratta di un errore in fase di compilazione per un'espressione di argomento per fare riferimento a qualsiasi membro dell'istanza tramite un simple_name.
15.11.3 Inizializzatori di variabili di istanza
Quando un costruttore di istanza non ha un inizializzatore di costruttore o ha un inizializzatore di costruttore del modulo base(...)
, tale costruttore esegue in modo implicito le inizializzazioni specificate dal variable_initializerdei campi dell'istanza dichiarati nella relativa classe. Corrisponde a una sequenza di assegnazioni eseguite immediatamente dopo l'immissione al costruttore e prima della chiamata implicita del costruttore della classe base diretta. Gli inizializzatori di variabile vengono eseguiti nell'ordine testuale in cui vengono visualizzati nella dichiarazione di classe (§15.5.6).
15.11.4 Esecuzione del costruttore
Gli inizializzatori di variabili vengono trasformati in istruzioni di assegnazione e queste istruzioni di assegnazione vengono eseguite prima della chiamata del costruttore dell'istanza della classe base. Questo ordinamento garantisce che tutti i campi dell'istanza vengano inizializzati dagli inizializzatori di variabile prima dell'esecuzione di qualsiasi istruzione con accesso a tale istanza.
Esempio: dato quanto segue:
class A { public A() { PrintFields(); } public virtual void PrintFields() {} } class B: A { int x = 1; int y; public B() { y = -1; } public override void PrintFields() => Console.WriteLine($"x = {x}, y = {y}"); }
quando viene usato new
B()
per creare un'istanza diB
, viene generato l'output seguente:x = 1, y = 0
Il valore di
x
è 1 perché l'inizializzatore di variabile viene eseguito prima che venga richiamato il costruttore dell'istanza della classe base. Tuttavia, il valore diy
è 0 (il valore predefinito di unint
oggetto ) perché l'assegnazione ay
non viene eseguita fino a quando non viene restituito il costruttore della classe base. È utile considerare gli inizializzatori di variabili di istanza e gli inizializzatori del costruttore come istruzioni inserite automaticamente prima del constructor_body. L'esempio:class A { int x = 1, y = -1, count; public A() { count = 0; } public A(int n) { count = n; } } class B : A { double sqrt2 = Math.Sqrt(2.0); ArrayList items = new ArrayList(100); int max; public B(): this(100) { items.Add("default"); } public B(int n) : base(n - 1) { max = n; } }
contiene diversi inizializzatori variabili; contiene anche inizializzatori di costruttori di entrambe le forme (
base
ethis
). L'esempio corrisponde al codice riportato di seguito, dove ogni commento indica un'istruzione inserita automaticamente (la sintassi usata per le chiamate al costruttore inserite automaticamente non è valida, ma serve solo per illustrare il meccanismo).class A { int x, y, count; public A() { x = 1; // Variable initializer y = -1; // Variable initializer object(); // Invoke object() constructor count = 0; } public A(int n) { x = 1; // Variable initializer y = -1; // Variable initializer object(); // Invoke object() constructor count = n; } } class B : A { double sqrt2; ArrayList items; int max; public B() : this(100) { B(100); // Invoke B(int) constructor items.Add("default"); } public B(int n) : base(n - 1) { sqrt2 = Math.Sqrt(2.0); // Variable initializer items = new ArrayList(100); // Variable initializer A(n - 1); // Invoke A(int) constructor max = n; } }
esempio finale
15.11.5 Costruttori predefiniti
Se una classe non contiene dichiarazioni del costruttore di istanza, viene fornito automaticamente un costruttore di istanza predefinito. Tale costruttore predefinito richiama semplicemente un costruttore della classe base diretta, come se avesse un inizializzatore del costruttore del form base()
. Se la classe è astratta, l'accessibilità dichiarata per il costruttore predefinito è protetta. In caso contrario, l'accessibilità dichiarata per il costruttore predefinito è pubblica.
Nota: pertanto, il costruttore predefinito è sempre del modulo
protected C(): base() {}
or
public C(): base() {}
dove
C
è il nome della classe.nota finale
Se la risoluzione dell'overload non è in grado di determinare un candidato migliore univoco per l'inizializzatore del costruttore della classe base, si verifica un errore in fase di compilazione.
Esempio: nel codice seguente
class Message { object sender; string text; }
Viene fornito un costruttore predefinito perché la classe non contiene dichiarazioni del costruttore di istanza. Di conseguenza, l'esempio è esattamente equivalente a
class Message { object sender; string text; public Message() : base() {} }
esempio finale
15.12 Costruttori statici
Un costruttore statico è un membro che implementa le azioni necessarie per inizializzare una classe chiusa. I costruttori statici vengono dichiarati usando static_constructor_declarations:
static_constructor_declaration
: attributes? static_constructor_modifiers identifier '(' ')'
static_constructor_body
;
static_constructor_modifiers
: 'static'
| 'static' 'extern' unsafe_modifier?
| 'static' unsafe_modifier 'extern'?
| 'extern' 'static' unsafe_modifier?
| 'extern' unsafe_modifier 'static'
| unsafe_modifier 'static' 'extern'?
| unsafe_modifier 'extern' 'static'
;
static_constructor_body
: block
| '=>' expression ';'
| ';'
;
unsafe_modifier (§23.2) è disponibile solo nel codice non sicuro (§23).
Un static_constructor_declaration può includere un set di attributi (§22) e un extern
modificatore (§15.6.8).
L'identificatore di un static_constructor_declaration denomina la classe in cui viene dichiarato il costruttore statico. Se viene specificato un altro nome, si verifica un errore in fase di compilazione.
Quando una dichiarazione di costruttore statico include un extern
modificatore, il costruttore statico viene detto come un costruttore statico esterno. Poiché una dichiarazione di costruttore statico esterno non fornisce alcuna implementazione effettiva, il relativo static_constructor_body è costituito da un punto e virgola. Per tutte le altre dichiarazioni del costruttore statico, il static_constructor_body è costituito da uno dei due
- un blocco, che specifica le istruzioni da eseguire per inizializzare la classe; o
- un corpo dell'espressione
=>
, costituito da seguito da un'espressione e da un punto e virgola, e indica una singola espressione da eseguire per inizializzare la classe.
Un static_constructor_body che è un blocco o un corpo dell'espressione corrisponde esattamente al method_body di un metodo statico con un void
tipo restituito (§15.6.11).
I costruttori statici non vengono ereditati e non possono essere chiamati direttamente.
Il costruttore statico per una classe chiusa viene eseguito al massimo una volta in un determinato dominio applicazione. L'esecuzione di un costruttore statico viene attivata dal primo degli eventi seguenti che si verificano all'interno di un dominio applicazione:
- Viene creata un'istanza della classe .
- Viene fatto riferimento a uno dei membri statici della classe .
Se una classe contiene il Main
metodo (§7.1) in cui inizia l'esecuzione, il costruttore statico per tale classe viene eseguito prima che venga chiamato il Main
metodo .
Per inizializzare un nuovo tipo di classe chiuso, viene creato un nuovo set di campi statici (§15.5.2) per quel particolare tipo chiuso. Ognuno dei campi statici viene inizializzato sul valore predefinito (§15.5.5). Successivamente, gli inizializzatori di campo statici (§15.5.6.2) vengono eseguiti per tali campi statici. Infine, viene eseguito il costruttore statico.
Esempio: esempio
class Test { static void Main() { A.F(); B.F(); } } class A { static A() { Console.WriteLine("Init A"); } public static void F() { Console.WriteLine("A.F"); } } class B { static B() { Console.WriteLine("Init B"); } public static void F() { Console.WriteLine("B.F"); } }
deve produrre l'output:
Init A A.F Init B B.F
poiché l'esecuzione del
A
costruttore statico di viene attivata dalla chiamata aA.F
e l'esecuzione delB
costruttore statico di viene attivata dalla chiamata aB.F
.esempio finale
È possibile costruire dipendenze circolari che consentono di osservare i campi statici con inizializzatori variabili nello stato del valore predefinito.
Esempio: esempio
class A { public static int X; static A() { X = B.Y + 1; } } class B { public static int Y = A.X + 1; static B() {} static void Main() { Console.WriteLine($"X = {A.X}, Y = {B.Y}"); } }
produce l'output
X = 1, Y = 2
Per eseguire il
Main
metodo , il sistema esegue prima l'inizializzatore perB.Y
, prima del costruttore statico della classeB
.Y
L'inizializzatore di fa sìA
che il costruttore vengastatic
eseguito perché viene fatto riferimento al valore diA.X
. Il costruttore statico diA
a sua volta procede per calcolare il valore diX
e in questo modo recupera il valore predefinito diY
, che è zero.A.X
viene quindi inizializzato su 1. Il processo di esecuzioneA
degli inizializzatori di campo statici e del costruttore statico viene quindi completato, restituendo al calcolo del valore iniziale diY
, il cui risultato diventa 2.esempio finale
Poiché il costruttore statico viene eseguito esattamente una volta per ogni tipo di classe costruito chiuso, è una posizione comoda per applicare controlli di runtime sul parametro di tipo che non possono essere controllati in fase di compilazione tramite vincoli (§15.2.5).
Esempio: il tipo seguente usa un costruttore statico per imporre che l'argomento di tipo sia un'enumerazione:
class Gen<T> where T : struct { static Gen() { if (!typeof(T).IsEnum) { throw new ArgumentException("T must be an enum"); } } }
esempio finale
15.13 Finalizzatori
Nota: in una versione precedente di questa specifica, il cosiddetto "finalizzatore" è stato definito "distruttore". L'esperienza ha dimostrato che il termine "distruttore" ha causato confusione e spesso ha causato aspettative errate, soprattutto per i programmatori che conoscono C++. In C++, un distruttore viene chiamato in modo determinato, mentre in C#, un finalizzatore non è. Per ottenere un comportamento determinato da C#, è necessario usare
Dispose
. nota finale
Un finalizzatore è un membro che implementa le azioni necessarie per finalizzare un'istanza di una classe. Un finalizzatore viene dichiarato usando un finalizer_declaration:
finalizer_declaration
: attributes? '~' identifier '(' ')' finalizer_body
| attributes? 'extern' unsafe_modifier? '~' identifier '(' ')'
finalizer_body
| attributes? unsafe_modifier 'extern'? '~' identifier '(' ')'
finalizer_body
;
finalizer_body
: block
| '=>' expression ';'
| ';'
;
unsafe_modifier (§23.2) è disponibile solo nel codice non sicuro (§23).
Un finalizer_declaration può includere un set di attributi (§22).
L'identificatore di un finalizer_declarator denomina la classe in cui viene dichiarato il finalizzatore. Se viene specificato un altro nome, si verifica un errore in fase di compilazione.
Quando una dichiarazione di finalizzatore include un extern
modificatore, il finalizzatore viene detto come un finalizzatore esterno. Poiché una dichiarazione di finalizzatore esterno non fornisce alcuna implementazione effettiva, il relativo finalizer_body è costituito da un punto e virgola. Per tutti gli altri finalizzatori, il finalizer_body è costituito da uno dei due
- un blocco, che specifica le istruzioni da eseguire per finalizzare un'istanza della classe .
- o un corpo dell'espressione
=>
, costituito da seguito da un'espressione e da un punto e virgola, e indica una singola espressione da eseguire per finalizzare un'istanza della classe .
Un finalizer_body che è un blocco o un corpo dell'espressione corrisponde esattamente alla method_body di un metodo di istanza con un void
tipo restituito (§15.6.11).
I finalizzatori non vengono ereditati. Pertanto, una classe non dispone di finalizzatori diversi da quello che può essere dichiarato in tale classe.
Nota: poiché un finalizzatore è necessario per non avere parametri, non può essere sottoposto a overload, quindi una classe può avere, al massimo, un finalizzatore. nota finale
I finalizzatori vengono richiamati automaticamente e non possono essere richiamati in modo esplicito. Un'istanza diventa idonea per la finalizzazione quando non è più possibile che il codice usi tale istanza. L'esecuzione del finalizzatore per l'istanza può verificarsi in qualsiasi momento dopo che l'istanza diventa idonea per la finalizzazione (§7.9). Quando un'istanza viene finalizzata, i finalizzatori nella catena di ereditarietà dell'istanza vengono chiamati, in ordine, dalla maggior parte derivata alla meno derivata. Un finalizzatore può essere eseguito su qualsiasi thread. Per ulteriori informazioni sulle regole che regolano quando e come viene eseguito un finalizzatore, vedere §7.9.
Esempio: output dell'esempio
class A { ~A() { Console.WriteLine("A's finalizer"); } } class B : A { ~B() { Console.WriteLine("B's finalizer"); } } class Test { static void Main() { B b = new B(); b = null; GC.Collect(); GC.WaitForPendingFinalizers(); } }
is
B's finalizer A's finalizer
poiché i finalizzatori in una catena di ereditarietà vengono chiamati in ordine, dalla maggior parte derivata alla meno derivata.
esempio finale
I finalizzatori vengono implementati eseguendo l'override del metodo Finalize
virtuale in System.Object
. I programmi C# non possono eseguire direttamente l'override di questo metodo o chiamarlo (o eseguirne l'override).
Esempio: ad esempio, il programma
class A { override protected void Finalize() {} // Error public void F() { this.Finalize(); // Error } }
contiene due errori.
esempio finale
Un compilatore si comporta come se questo metodo e i suoi override non esistessero affatto.
Esempio: di conseguenza, questo programma:
class A { void Finalize() {} // Permitted }
è valido e il metodo visualizzato nasconde
System.Object
il metodo .Finalize
esempio finale
Per una descrizione del comportamento quando viene generata un'eccezione da un finalizzatore, vedere §21.4.
15.14 Iteratori
15.14.1 Generale
Un membro della funzione (§12.6) implementato usando un blocco iteratore (§13.3) viene chiamato iteratore.
Un blocco iteratore può essere utilizzato come corpo di un membro della funzione, purché il tipo restituito del membro della funzione corrispondente sia una delle interfacce dell'enumeratore (§15.14.2) o una delle interfacce enumerabili (§15.14.3). Può verificarsi come method_body, operator_body o accessor_body, mentre gli eventi, i costruttori di istanza, i costruttori statici e il finalizzatore non devono essere implementati come iteratori.
Quando un membro della funzione viene implementato usando un blocco iteratore, si tratta di un errore in fase di compilazione per l'elenco di parametri del membro della funzione per specificare qualsiasi in
parametro , out
o ref
o di un parametro di un ref struct
tipo.
Interfacce enumeratori 15.14.2
Le interfacce dell'enumeratore sono l'interfaccia System.Collections.IEnumerator
non generica e tutte le istanze dell'interfaccia System.Collections.Generic.IEnumerator<T>
generica . Per motivi di brevità, in questa sottochiave e le relative interfacce di pari livello vengono rispettivamente a cui si fa riferimento come IEnumerator
e IEnumerator<T>
.
Interfacce enumerabili 15.14.3
Le interfacce enumerabili sono l'interfaccia System.Collections.IEnumerable
non generica e tutte le istanze dell'interfaccia System.Collections.Generic.IEnumerable<T>
generica . Per motivi di brevità, in questa sottochiave e le relative interfacce di pari livello vengono rispettivamente a cui si fa riferimento come IEnumerable
e IEnumerable<T>
.
15.14.4 Tipo di rendimento
Un iteratore produce una sequenza di valori, tutti dello stesso tipo. Questo tipo è denominato tipo di rendimento dell'iteratore.
- Tipo di risultato di un iteratore che restituisce
IEnumerator
oIEnumerable
èobject
. - Tipo di risultato di un iteratore che restituisce
IEnumerator<T>
oIEnumerable<T>
èT
.
15.14.5 Oggetti enumeratore
15.14.5.1 Generale
Quando un membro della funzione che restituisce un tipo di interfaccia dell'enumeratore viene implementato usando un blocco iteratore, richiamare il membro della funzione non esegue immediatamente il codice nel blocco iteratore. Viene invece creato e restituito un oggetto enumeratore. Questo oggetto incapsula il codice specificato nel blocco iteratore e l'esecuzione del codice nel blocco iteratore si verifica quando viene richiamato il metodo dell'oggetto MoveNext
enumeratore. Un oggetto enumeratore presenta le caratteristiche seguenti:
- Implementa
IEnumerator
eIEnumerator<T>
, doveT
è il tipo di rendimento dell'iteratore. - Implementa
System.IDisposable
. - Viene inizializzato con una copia dei valori dell'argomento (se presenti) e il valore dell'istanza passato al membro della funzione.
- Ha quattro stati potenziali, prima, in esecuzione, sospesi e dopo e inizialmente è nello stato precedente.
Un oggetto enumeratore è in genere un'istanza di una classe enumeratore generata dal compilatore che incapsula il codice nel blocco iteratore e implementa le interfacce dell'enumeratore, ma sono possibili altri metodi di implementazione. Se una classe enumeratore viene generata dal compilatore, tale classe verrà annidata, direttamente o indirettamente, nella classe contenente il membro della funzione, avrà accessibilità privata e avrà un nome riservato per l'uso del compilatore (§6.4.3).
Un oggetto enumeratore può implementare più interfacce rispetto a quelle specificate in precedenza.
Le sottoclause seguenti descrivono il comportamento richiesto dei MoveNext
membri , Current
e Dispose
delle implementazioni dell'interfaccia IEnumerator
e IEnumerator<T>
fornite da un oggetto enumeratore.
Gli oggetti enumeratore non supportano il IEnumerator.Reset
metodo . Il richiamo di questo metodo determina la creazione di un oggetto System.NotSupportedException
.
15.14.5.2 Metodo MoveNext
Il MoveNext
metodo di un oggetto enumeratore incapsula il codice di un blocco iteratore. Il richiamo del MoveNext
metodo esegue il codice nel blocco iteratore e imposta la Current
proprietà dell'oggetto enumeratore in base alle esigenze. L'azione precisa eseguita da MoveNext
dipende dallo stato dell'oggetto enumeratore quando MoveNext
viene richiamato:
- Se lo stato dell'oggetto enumeratore è precedente, richiamando
MoveNext
:- Modifica lo stato in esecuzione.
- Inizializza i parametri (incluso
this
) del blocco iteratore sui valori dell'argomento e sul valore dell'istanza salvati quando l'oggetto enumeratore è stato inizializzato. - Esegue il blocco iteratore dall'inizio fino a quando l'esecuzione non viene interrotta (come descritto di seguito).
- Se lo stato dell'oggetto enumeratore è in esecuzione, il risultato della chiamata
MoveNext
non è specificato. - Se lo stato dell'oggetto enumeratore viene sospeso, richiamando MoveNext:
- Modifica lo stato in esecuzione.
- Ripristina i valori di tutte le variabili e i parametri locali (incluso
this
) sui valori salvati durante l'ultima sospensione dell'esecuzione del blocco iteratore.Nota: il contenuto di tutti gli oggetti a cui si fa riferimento da queste variabili può essere cambiato dopo la chiamata precedente a
MoveNext
. nota finale - Riprende l'esecuzione del blocco iteratore immediatamente dopo l'istruzione return yield che ha causato la sospensione dell'esecuzione e continua fino a quando l'esecuzione non viene interrotta (come descritto di seguito).
- Se lo stato dell'oggetto enumeratore è successivo, la chiamata
MoveNext
restituisce false.
Quando MoveNext
esegue il blocco iteratore, l'esecuzione può essere interrotta in quattro modi: da un'istruzione yield return
, da un'istruzione yield break
, incontrando la fine del blocco iteratore e da un'eccezione generata e propagata all'esterno del blocco iteratore.
- Quando viene rilevata un'istruzione
yield return
(§9.4.4.20):- L'espressione specificata nell'istruzione viene valutata, convertita in modo implicito nel tipo yield e assegnata alla
Current
proprietà dell'oggetto enumeratore. - L'esecuzione del corpo dell'iteratore viene sospesa. I valori di tutte le variabili e i parametri locali (incluso
this
) vengono salvati, così come il percorso di questayield return
istruzione. Se l'istruzioneyield return
si trova all'interno di uno o piùtry
blocchi, i blocchi finally associati non vengono eseguiti in questo momento. - Lo stato dell'oggetto enumeratore viene modificato in sospeso.
- Il
MoveNext
metodo tornatrue
al chiamante, a indicare che l'iterazione è stata eseguita correttamente al valore successivo.
- L'espressione specificata nell'istruzione viene valutata, convertita in modo implicito nel tipo yield e assegnata alla
- Quando viene rilevata un'istruzione
yield break
(§9.4.4.20):- Se l'istruzione si trova all'interno
yield break
di uno o piùtry
blocchi, vengono eseguiti i blocchi associatifinally
. - Lo stato dell'oggetto enumeratore viene modificato in dopo.
- Il
MoveNext
metodo tornafalse
al chiamante, a indicare che l'iterazione è stata completata.
- Se l'istruzione si trova all'interno
- Quando viene rilevata la fine del corpo dell'iteratore:
- Lo stato dell'oggetto enumeratore viene modificato in dopo.
- Il
MoveNext
metodo tornafalse
al chiamante, a indicare che l'iterazione è stata completata.
- Quando viene generata e propagata un'eccezione all'esterno del blocco iteratore:
- I blocchi appropriati
finally
nel corpo dell'iteratore saranno stati eseguiti dalla propagazione dell'eccezione. - Lo stato dell'oggetto enumeratore viene modificato in dopo.
- La propagazione dell'eccezione continua al chiamante del
MoveNext
metodo .
- I blocchi appropriati
15.14.5.3 Proprietà corrente
La proprietà di Current
un oggetto enumeratore è interessata dalle yield return
istruzioni nel blocco iteratore.
Quando un oggetto enumeratore si trova nello stato sospesoMoveNext
Quando un oggetto enumeratore si trova negli stati precedenti, in esecuzione o dopo , il risultato dell'accesso Current
non è specificato.
Per un iteratore con un tipo yield diverso da object
, il risultato dell'accesso Current
tramite l'implementazione dell'oggetto IEnumerable
enumeratore corrisponde all'accesso Current
tramite l'implementazione dell'oggetto IEnumerator<T>
enumeratore e il cast del risultato a object
.
15.14.5.4 Il metodo Dispose
Il Dispose
metodo viene usato per pulire l'iterazione portando l'oggetto enumeratore allo stato dopo .
- Se lo stato dell'oggetto enumeratore è precedente, richiamare lo stato su
Dispose
. - Se lo stato dell'oggetto enumeratore è in esecuzione, il risultato della chiamata
Dispose
non è specificato. - Se lo stato dell'oggetto enumeratore è sospeso, richiamando
Dispose
:- Modifica lo stato in esecuzione.
- Esegue tutti i blocchi finally come se l'ultima istruzione eseguita
yield return
fosse un'istruzioneyield break
. Se in questo modo viene generata e propagata un'eccezione dal corpo dell'iteratore, lo stato dell'oggetto enumeratore viene impostato su after e l'eccezione viene propagata al chiamante delDispose
metodo. - Imposta lo stato su dopo.
- Se lo stato dell'oggetto enumeratore è successivo, la chiamata
Dispose
non ha alcun effetto.
15.14.6 Oggetti enumerabili
15.14.6.1 Generale
Quando un membro della funzione che restituisce un tipo di interfaccia enumerabile viene implementato usando un blocco iteratore, richiamare il membro della funzione non esegue immediatamente il codice nel blocco iteratore. Viene invece creato e restituito un oggetto enumerabile. Il metodo dell'oggetto GetEnumerator
enumerabile restituisce un oggetto enumeratore che incapsula il codice specificato nel blocco iteratore e l'esecuzione del codice nel blocco iteratore si verifica quando viene richiamato il metodo dell'oggetto MoveNext
enumeratore. Un oggetto enumerabile presenta le caratteristiche seguenti:
- Implementa
IEnumerable
eIEnumerable<T>
, doveT
è il tipo di rendimento dell'iteratore. - Viene inizializzato con una copia dei valori dell'argomento (se presenti) e il valore dell'istanza passato al membro della funzione.
Un oggetto enumerabile è in genere un'istanza di una classe enumerabile generata dal compilatore che incapsula il codice nel blocco iteratore e implementa le interfacce enumerabili, ma sono possibili altri metodi di implementazione. Se una classe enumerabile viene generata dal compilatore, tale classe verrà annidata, direttamente o indirettamente, nella classe contenente il membro della funzione, avrà accessibilità privata e avrà un nome riservato per l'uso del compilatore (§6.4.3).
Un oggetto enumerabile può implementare più interfacce di quelle specificate in precedenza.
Nota: ad esempio, un oggetto enumerabile può anche implementare
IEnumerator
eIEnumerator<T>
, consentendo di fungere sia da enumerabile che da enumeratore. In genere, tale implementazione restituirà la propria istanza (per salvare le allocazioni) dalla prima chiamata aGetEnumerator
. Le chiamate successive di , se presenti, restituiscono una nuova istanza diGetEnumerator
classe, in genere della stessa classe, in modo che le chiamate a istanze dell'enumeratore diverse non influiscano l'una sull'altra. Non può restituire la stessa istanza anche se l'enumeratore precedente ha già enumerato oltre la fine della sequenza, poiché tutte le chiamate future a un enumeratore esaurito devono generare eccezioni. nota finale
15.14.6.2 Metodo GetEnumerator
Un oggetto enumerabile fornisce un'implementazione dei GetEnumerator
metodi delle IEnumerable
interfacce e IEnumerable<T>
. I due GetEnumerator
metodi condividono un'implementazione comune che acquisisce e restituisce un oggetto enumeratore disponibile. L'oggetto enumeratore viene inizializzato con i valori dell'argomento e il valore dell'istanza salvati quando l'oggetto enumerabile è stato inizializzato, ma in caso contrario l'oggetto enumeratore funziona come descritto in §15.14.5.
15.15 Funzioni asincrone
15.15.1 Generale
Un metodo (§15.6) o una funzione anonima (§12.19) con il async
modificatore è detta funzione asincrona. In generale, il termine asincrono viene usato per descrivere qualsiasi tipo di funzione con il async
modificatore.
Si tratta di un errore in fase di compilazione per l'elenco di parametri di una funzione asincrona per specificare qualsiasi in
parametro , out
o ref
o qualsiasi parametro di un ref struct
tipo.
Il return_type di un metodo asincrono deve essere void
o un tipo di attività. Per un metodo asincrono che produce un valore di risultato, un tipo di attività deve essere generico. Per un metodo asincrono che non produce un valore di risultato, un tipo di attività non deve essere generico. Tali tipi sono indicati rispettivamente in questa specifica come «TaskType»<T>
e «TaskType»
. Il tipo di System.Threading.Tasks.Task
libreria Standard e i tipi costruiti da System.Threading.Tasks.Task<TResult>
sono tipi di attività, nonché una classe, uno struct o un tipo di interfaccia associati a un tipo di generatore di attività tramite l'attributo System.Runtime.CompilerServices.AsyncMethodBuilderAttribute
. Tali tipi sono indicati in questa specifica come «TaskBuilderType»<T>
e «TaskBuilderType»
. Un tipo di attività può avere al massimo un parametro di tipo e non può essere annidato in un tipo generico.
Si dice che un metodo asincrono che restituisce un tipo di attività sia la restituzione di attività.
I tipi di attività possono variare nella definizione esatta, ma dal punto di vista della lingua, un tipo di attività si trova in uno degli stati incompleti, riusciti o con errori. Un'attività con errori registra un'eccezione pertinente. Un oggetto ha avuto esito positivo«TaskType»<T>
registra un risultato di tipo T
. I tipi di attività sono awaitable e le attività possono quindi essere gli operandi delle espressioni await (§12.9.8).
Esempio: il tipo di
MyTask<T>
attività è associato al tipoMyTaskMethodBuilder<T>
di generatore di attività e al tipoAwaiter<T>
awaiter :using System.Runtime.CompilerServices; [AsyncMethodBuilder(typeof(MyTaskMethodBuilder<>))] class MyTask<T> { public Awaiter<T> GetAwaiter() { ... } } class Awaiter<T> : INotifyCompletion { public void OnCompleted(Action completion) { ... } public bool IsCompleted { get; } public T GetResult() { ... } }
esempio finale
Un tipo di generatore di attività è un tipo di classe o struct che corrisponde a un tipo di attività specifico (§15.15.2). Il tipo di generatore di attività deve corrispondere esattamente all'accessibilità dichiarata del tipo di attività corrispondente.
Nota: se il tipo di attività è dichiarato
internal
, il tipo di generatore corrispondente deve essere dichiaratointernal
e definito nello stesso assembly. Se il tipo di attività è annidato all'interno di un altro tipo, anche il tipo di buider dell'attività deve essere annidato nello stesso tipo. nota finale
Una funzione asincrona ha la possibilità di sospendere la valutazione tramite espressioni await (§12.9.8) nel corpo. La valutazione può essere ripresa successivamente al momento della sospensione dell'espressione await tramite un delegato di ripresa. Il delegato di ripresa è di tipo System.Action
e, quando viene richiamato, la valutazione della chiamata della funzione asincrona riprenderà dall'espressione await in cui è stata interrotta. Il chiamante corrente di una chiamata di funzione asincrona è il chiamante originale se la chiamata alla funzione non è mai stata sospesa o il chiamante più recente del delegato di ripresa in caso contrario.
15.15.2 Modello generatore di tipi di attività
Un tipo di generatore di attività può avere al massimo un parametro di tipo e non può essere annidato in un tipo generico. Un tipo di generatore di attività deve avere i membri seguenti (per i tipi di generatore di attività non generici, SetResult
senza parametri) con accessibilità dichiarata public
:
class «TaskBuilderType»<T>
{
public static «TaskBuilderType»<T> Create();
public void Start<TStateMachine>(ref TStateMachine stateMachine)
where TStateMachine : IAsyncStateMachine;
public void SetStateMachine(IAsyncStateMachine stateMachine);
public void SetException(Exception exception);
public void SetResult(T result);
public void AwaitOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : INotifyCompletion
where TStateMachine : IAsyncStateMachine;
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine;
public «TaskType»<T> Task { get; }
}
Un compilatore dovrà generare codice che utilizza il «TaskBuilderType» per implementare la semantica della sospensione e della ripresa della valutazione della funzione asincrona. Un compilatore userà «TaskBuilderType» come indicato di seguito:
-
«TaskBuilderType».Create()
viene richiamato per creare un'istanza di «TaskBuilderType», denominatabuilder
in questo elenco. -
builder.Start(ref stateMachine)
viene richiamato per associare il generatore a un'istanza della macchina a stati generata dal compilatore,stateMachine
.- Il generatore deve chiamare
stateMachine.MoveNext()
inStart()
o dopoStart()
che è tornato per far avanzare la macchina a stati.
- Il generatore deve chiamare
- Al
Start()
termine, ilasync
metodo richiamabuilder.Task
affinché l'attività restituisca dal metodo asincrono. - Ogni chiamata a farà avanzare la macchina a
stateMachine.MoveNext()
stati. - Se la macchina a stati viene completata correttamente,
builder.SetResult()
viene chiamata con il valore restituito del metodo, se presente. - In caso contrario,
e
se viene generata un'eccezione nella macchina a stati,builder.SetException(e)
viene chiamato . - Se la macchina a stati raggiunge un'espressione
await expr
,expr.GetAwaiter()
viene richiamata. - Se il awaiter implementa
ICriticalNotifyCompletion
eIsCompleted
è false, la macchina abuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine)
stati richiama .-
AwaitUnsafeOnCompleted()
deve chiamareawaiter.UnsafeOnCompleted(action)
con un oggettoAction
che chiamastateMachine.MoveNext()
al termine dell'awaiter.
-
- In caso contrario, la macchina a
builder.AwaitOnCompleted(ref awaiter, ref stateMachine)
stati richiama .-
AwaitOnCompleted()
deve chiamareawaiter.OnCompleted(action)
con un oggettoAction
che chiamastateMachine.MoveNext()
al termine dell'awaiter.
-
-
SetStateMachine(IAsyncStateMachine)
può essere chiamato dall'implementazione generataIAsyncStateMachine
dal compilatore per identificare l'istanza del generatore associata a un'istanza della macchina a stati, in particolare nei casi in cui la macchina a stati viene implementata come tipo valore.- Se il generatore chiama
stateMachine.SetStateMachine(stateMachine)
,stateMachine
chiameràbuilder.SetStateMachine(stateMachine)
sull'istanza del generatore associata astateMachine
.
- Se il generatore chiama
Nota: sia per
SetResult(T result)
che«TaskType»<T> Task { get; }
per , il parametro e l'argomento devono essere convertibili rispettivamente in identity convertibile inT
. Ciò consente a un generatore di tipi di attività di supportare tipi come le tuple, in cui due tipi che non sono uguali sono convertibili in identità. nota finale
15.15.3 Valutazione di una funzione asincrona che restituisce un'attività
La chiamata di una funzione asincrona che restituisce un'attività fa sì che venga generata un'istanza del tipo di attività restituito. Questa operazione è denominata attività restituita della funzione asincrona. L'attività è inizialmente in uno stato incompleto .
Il corpo della funzione asincrona viene quindi valutato fino a quando non viene sospeso (raggiungendo un'espressione await) o termina, in corrispondenza del quale il controllo del punto viene restituito al chiamante, insieme all'attività restituita.
Quando il corpo della funzione asincrona termina, l'attività restituita viene spostata dallo stato incompleto:
- Se il corpo della funzione termina come risultato del raggiungimento di un'istruzione return o della fine del corpo, qualsiasi valore del risultato viene registrato nell'attività restituita, che viene inserito in uno stato completato .
- Se il corpo della funzione termina a causa di un errore
OperationCanceledException
, l'eccezione viene registrata nell'attività restituita che viene inserita nello stato annullato . - Se il corpo della funzione termina come risultato di qualsiasi altra eccezione non rilevata (§13.10.6), l'eccezione viene registrata nell'attività restituita che viene inserita in uno stato di errore.
15.15.4 Valutazione di una funzione asincrona che restituisce void
Se il tipo restituito della funzione asincrona è void
, la valutazione è diversa da quella precedente nel modo seguente: Poiché non viene restituita alcuna attività, la funzione comunica invece il completamento e le eccezioni al contesto di sincronizzazione del thread corrente. La definizione esatta del contesto di sincronizzazione dipende dall'implementazione, ma è una rappresentazione di "dove" è in esecuzione il thread corrente. Il contesto di sincronizzazione riceve una notifica quando viene eseguita la valutazione di una void
funzione asincrona, viene completata correttamente o viene generata un'eccezione non rilevata.
In questo modo il contesto può tenere traccia del numero void
di funzioni asincrone in esecuzione e decidere come propagare le eccezioni in uscita.
ECMA C# draft specification