15 clases
15.1 General
Una clase es una estructura de datos que puede contener miembros de datos (constantes y campos), miembros de función (métodos, propiedades, eventos, indexadores, operadores, constructores de instancia, finalizadores y constructores estáticos) y tipos anidados. Los tipos de clase admiten la herencia, un mecanismo por el que una clase derivada puede extender y especializar una clase base.
15.2 Declaraciones de clase
15.2.1 General
Un class_declaration es un type_declaration (§14.7) que declara una nueva clase.
class_declaration
: attributes? class_modifier* 'partial'? 'class' identifier
type_parameter_list? class_base? type_parameter_constraints_clause*
class_body ';'?
;
Un class_declaration consta de un conjunto opcional de atributos (§22), seguido de un conjunto opcional de class_modifiers (§15.2.2), seguido de un modificador opcional partial
(§15.2.7), seguido de la palabra clave class
y un identificador que nombra la clase , seguido de un type_parameter_list opcional (§15.2.3), seguido de una especificación de class_base opcional (§15.2.4)), seguido de un conjunto opcional de type_parameter_constraints_clause s (§15.2.5), seguido de un class_body (§15.2.6), opcionalmente seguido de unpunto y coma.
Una declaración de clase no proporcionará una type_parameter_constraints_clausea menos que también proporcione una type_parameter_list.
Una declaración de clase que proporciona un type_parameter_list es una declaración de clase genérica. Además, cualquier clase anidada dentro de una declaración de clase genérica o una declaración de estructura genérica es una declaración de clase genérica, ya que se proporcionarán argumentos de tipo para el tipo contenedor para crear un tipo construido (§8.4).
15.2.2 Modificadores de clase
15.2.2.1 General
Un class_declaration puede incluir opcionalmente una secuencia de modificadores de clase:
class_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'abstract'
| 'sealed'
| 'static'
| unsafe_modifier // unsafe code support
;
unsafe_modifier (§23.2) solo está disponible en código no seguro (§23).
Es un error en tiempo de compilación para que el mismo modificador aparezca varias veces en una declaración de clase.
El new
modificador se permite en clases anidadas. Especifica que la clase oculta un miembro heredado por el mismo nombre, como se describe en §15.3.5. Es un error en tiempo de compilación que el new
modificador aparece en una declaración de clase que no es una declaración de clase anidada.
Los public
modificadores , protected
, internal
y private
controlan la accesibilidad de la clase . En función del contexto en el que se produzca la declaración de clase, es posible que algunos de estos modificadores no se permitan (§7.5.2).
Cuando una declaración de tipo parcial (§15.2.7) incluye una especificación de accesibilidad (a través de los public
modificadores , protected
, internal
y private
), esa especificación estará de acuerdo con todas las demás partes que incluyan una especificación de accesibilidad. Si ninguna parte de un tipo parcial incluye una especificación de accesibilidad, el tipo recibe la accesibilidad predeterminada adecuada (§7.5.2).
Los abstract
modificadores , sealed
y static
se describen en las subclases siguientes.
15.2.2.2 Clases abstractas
El abstract
modificador se usa para indicar que una clase está incompleta y que está pensada para usarse solo como una clase base. Una clase abstracta difiere de una clase no abstracta de las maneras siguientes:
- No se puede crear una instancia de una clase abstracta directamente y se trata de un error en tiempo de compilación para usar el
new
operador en una clase abstracta. Aunque es posible tener variables y valores cuyos tipos en tiempo de compilación sean abstractos, estas variables y valores necesariamente seránnull
o contendrán referencias a instancias de clases no abstractas derivadas de los tipos abstractos. - Se permite que una clase abstracta (pero no necesaria) contenga miembros abstractos.
- No se puede sellarse una clase abstracta.
Cuando una clase no abstracta se deriva de una clase abstracta, la clase no abstracta incluirá implementaciones reales de todos los miembros abstractos heredados, lo que invalida esos miembros abstractos.
Ejemplo: en el código siguiente
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 clase
A
abstracta presenta un métodoF
abstracto . La claseB
presenta un métodoG
adicional, pero dado que no proporciona una implementación deF
,B
también se declarará abstracta. La claseC
invalidaF
y proporciona una implementación real. Dado que no hay miembros abstractos enC
,C
se permite (pero no es necesario) que no sean abstractos.ejemplo final
Si una o varias partes de una declaración de tipo parcial (§15.2.7) de una clase incluyen el abstract
modificador, la clase es abstracta. De lo contrario, la clase no es abstracta.
15.2.2.3 Clases selladas
El sealed
modificador se usa para evitar la derivación de una clase . Se produce un error en tiempo de compilación si se especifica una clase sellada como la clase base de otra clase.
Una clase sellada tampoco puede ser una clase abstracta.
Nota: El
sealed
modificador se usa principalmente para evitar la derivación no deseada, pero también habilita determinadas optimizaciones en tiempo de ejecución. En concreto, dado que se sabe que una clase sellada nunca tiene ninguna clase derivada, es posible transformar las invocaciones de miembro de función virtual en instancias de clase selladas en invocaciones no virtuales. nota final
Si una o varias partes de una declaración de tipo parcial (§15.2.7) de una clase incluyen el sealed
modificador, la clase está sellada. De lo contrario, la clase no está seseada.
15.2.2.4 Clases estáticas
15.2.2.4.1 General
El static
modificador se usa para marcar la clase que se declara como una clase estática. No se creará una instancia de una clase estática, no se usará como tipo y solo contendrá miembros estáticos. Solo una clase estática puede contener declaraciones de métodos de extensión (§15.6.10).
Una declaración de clase estática está sujeta a las restricciones siguientes:
- Una clase estática no incluirá un
sealed
modificador oabstract
. (Sin embargo, dado que una clase estática no se puede crear una instancia o derivar de , se comporta como si fuera sellada y abstracta). - Una clase estática no incluirá una especificación de class_base (§15.2.4) y no podrá especificar explícitamente una clase base ni una lista de interfaces implementadas. Una clase estática hereda implícitamente del tipo
object
. - Una clase estática solo contendrá miembros estáticos (§15.3.8).
Nota: Todas las constantes y los tipos anidados se clasifican como miembros estáticos. nota final
- Una clase estática no tendrá miembros con
protected
,private protected
oprotected internal
accesibilidad declarada.
Se trata de un error en tiempo de compilación para infringir cualquiera de estas restricciones.
Una clase estática no tiene constructores de instancia. No es posible declarar un constructor de instancia en una clase estática y no se proporciona ningún constructor de instancia predeterminado (§15.11.5) para una clase estática.
Los miembros de una clase estática no son estáticos automáticamente y las declaraciones de miembro incluirán explícitamente un static
modificador (excepto las constantes y los tipos anidados). Cuando una clase está anidada dentro de una clase externa estática, la clase anidada no es una clase estática a menos que incluya explícitamente un static
modificador.
Si una o varias partes de una declaración de tipo parcial (§15.2.7) de una clase incluyen el static
modificador, la clase es estática. De lo contrario, la clase no es estática.
15.2.2.4.2 Hacer referencia a tipos de clase estática
Se permite que un namespace_or_type_name (§7.8) haga referencia a una clase estática si
- El namespace_or_type_name es en
T
un namespace_or_type_name del formatoT.I
, o - El nombre namespace_or_type es en
T
un typeof_expression (§12.8.18) del formatotypeof(T)
.
Se permite que un primary_expression (§12.8) haga referencia a una clase estática si
- El primary_expression es en
E
un member_access (§12.8.7) del formularioE.I
.
En cualquier otro contexto, es un error en tiempo de compilación para hacer referencia a una clase estática.
Nota: Por ejemplo, es un error para que una clase estática se use como clase base, un tipo constituyente (§15.3.7) de un miembro, un argumento de tipo genérico o una restricción de parámetro de tipo. Del mismo modo, una clase estática no se puede usar en un tipo de matriz, una nueva expresión, una expresión de conversión, una expresión es , una expresión como expresión, una
sizeof
expresión o una expresión de valor predeterminada. nota final
15.2.3 Parámetros de tipo
Un parámetro de tipo es un identificador simple que denota un marcador de posición para un argumento de tipo proporcionado para crear un tipo construido. Por constrast, un argumento de tipo (§8.4.2) es el tipo que se sustituye por el parámetro de tipo cuando se crea un tipo construido.
type_parameter_list
: '<' type_parameters '>'
;
type_parameters
: attributes? type_parameter
| type_parameters ',' attributes? type_parameter
;
type_parameter se define en §8.5.
Cada parámetro de tipo de una declaración de clase define un nombre en el espacio de declaración (§7.3) de esa clase. Por lo tanto, no puede tener el mismo nombre que otro parámetro de tipo de esa clase o un miembro declarado en esa clase. Un parámetro de tipo no puede tener el mismo nombre que el propio tipo.
Dos declaraciones de tipos genéricos parciales (en el mismo programa) contribuyen al mismo tipo genérico no enlazado si tienen el mismo nombre completo (que incluye un generic_dimension_specifier (§12.8.18) para el número de parámetros de tipo) (§7.8.3). Dos declaraciones de tipo parcial especificarán el mismo nombre para cada parámetro de tipo, en orden.
15.2.4 Especificación base de clase
15.2.4.1 General
Una declaración de clase puede incluir una especificación de class_base , que define la clase base directa de la clase y las interfaces (§18) implementadas directamente por la clase .
class_base
: ':' class_type
| ':' interface_type_list
| ':' class_type ',' interface_type_list
;
interface_type_list
: interface_type (',' interface_type)*
;
15.2.4.2 Clases base
Cuando se incluye un class_type en el class_base, especifica la clase base directa de la clase que se declara. Si una declaración de clase no parcial no tiene class_base o si el class_base enumera solo los tipos de interfaz, se supone que la clase base directa es object
. Cuando una declaración de clase parcial incluye una especificación de clase base, esa especificación de clase base hará referencia al mismo tipo que todas las demás partes de ese tipo parcial que incluyan una especificación de clase base. Si ninguna parte de una clase parcial incluye una especificación de clase base, la clase base es object
. Una clase hereda los miembros de su clase base directa, como se describe en §15.3.4.
Ejemplo: en el código siguiente
class A {} class B : A {}
Se dice que la clase
A
es la clase base directa deB
yB
se dice que se deriva deA
. PuestoA
que no especifica explícitamente una clase base directa, su clase base directa es implícitamenteobject
.ejemplo final
Para un tipo de clase construido, incluido un tipo anidado declarado dentro de una declaración de tipo genérico (§15.3.9.7), si se especifica una clase base en la declaración de clase genérica, la clase base del tipo construido se obtiene sustituyendo, por cada type_parameter de la declaración de clase base, el type_argument correspondiente del tipo construido.
Ejemplo: Dadas las declaraciones de clase genéricas
class B<U,V> {...} class G<T> : B<string,T[]> {...}
la clase base del tipo
G<int>
construido seríaB<string,int[]>
.ejemplo final
La clase base especificada en una declaración de clase puede ser un tipo de clase construido (§8.4). Una clase base no puede ser un parámetro de tipo propio (§8.5), aunque puede implicar los parámetros de tipo que están en el ámbito.
Ejemplo:
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> {}
ejemplo final
La clase base directa de un tipo de clase debe ser al menos tan accesible como el propio tipo de clase (§7.5.5). Por ejemplo, es un error en tiempo de compilación para que una clase pública derive de una clase privada o interna.
La clase base directa de un tipo de clase no debe ser ninguno de los siguientes tipos: System.Array
, System.Delegate
, System.ValueType
System.Enum
o el dynamic
tipo . Además, una declaración de clase genérica no se usará System.Attribute
como una clase base directa o indirecta (§22.2.1).
Al determinar el significado de la especificación A
de clase base directa de una clase B
, se supone temporalmente que la clase base directa de B
es object
, lo que garantiza que el significado de una especificación de clase base no puede depender recursivamente de sí mismo.
Ejemplo: lo siguiente
class X<T> { public class Y{} } class Z : X<Z.Y> {}
se produce un error porque, en la especificación
X<Z.Y>
de clase base, la clase base directa deZ
se consideraobject
, y por lo tanto (por las reglas de §7.8)Z
no se considera que tiene un miembroY
.ejemplo final
Las clases base de una clase son la clase base directa y sus clases base. En otras palabras, el conjunto de clases base es el cierre transitivo de la relación de clase base directa.
Ejemplo: En lo siguiente:
class A {...} class B<T> : A {...} class C<T> : B<IComparable<T>> {...} class D<T> : C<T[]> {...}
las clases base de
D<int>
sonC<int[]>
,B<IComparable<int[]>>
,A
yobject
.ejemplo final
Excepto para la clase object
, cada clase tiene exactamente una clase base directa. La object
clase no tiene ninguna clase base directa y es la clase base definitiva de todas las demás clases.
Es un error en tiempo de compilación para que una clase dependa de sí misma. Para esta regla, una clase depende directamente de su clase base directa (si existe) y depende directamente de la clase envolvente más cercana dentro de la que esté anidada (si existe). Dada esta definición, el conjunto completo de clases en las que depende una clase es el cierre transitivo del elemento directamente depende de la relación.
Ejemplo: El ejemplo
class A : A {}
es erróneo porque la clase depende de sí misma. Del mismo modo, el ejemplo
class A : B {} class B : C {} class C : A {}
está en error porque las clases dependen circularmente de sí mismas. Por último, el ejemplo
class A : B.C {} class B : A { public class C {} }
produce un error en tiempo de compilación porque A depende
B.C
de (su clase base directa), que dependeB
de (su clase envolvente inmediatamente), que depende circularmente deA
.ejemplo final
Una clase no depende de las clases anidadas dentro de ella.
Ejemplo: en el código siguiente
class A { class B : A {} }
B
dependeA
de (porqueA
es su clase base directa y su clase envolvente inmediatamente), peroA
no dependeB
de (yaB
que no es una clase base ni una clase envolvente deA
). Por lo tanto, el ejemplo es válido.ejemplo final
No es posible derivar de una clase sellada.
Ejemplo: en el código siguiente
sealed class A {} class B : A {} // Error, cannot derive from a sealed class
La clase
B
está en error porque intenta derivar de la claseA
sealed .ejemplo final
15.2.4.3 Implementaciones de interfaz
Una especificación class_base puede incluir una lista de tipos de interfaz, en cuyo caso se dice que la clase implementa los tipos de interfaz especificados. Para un tipo de clase construido, incluido un tipo anidado declarado dentro de una declaración de tipo genérico (§15.3.9.7), cada tipo de interfaz implementado se obtiene sustituyendo, por cada type_parameter en la interfaz especificada, el type_argument correspondiente del tipo construido.
El conjunto de interfaces de un tipo declarado en varias partes (§15.2.7) es la unión de las interfaces especificadas en cada parte. Una interfaz determinada solo se puede denominar una vez en cada parte, pero varias partes pueden asignar un nombre a las mismas interfaces base. Solo habrá una implementación de cada miembro de cualquier interfaz determinada.
Ejemplo: En lo siguiente:
partial class C : IA, IB {...} partial class C : IC {...} partial class C : IA, IB {...}
el conjunto de interfaces base para la clase
C
esIA
,IB
yIC
.ejemplo final
Normalmente, cada parte proporciona una implementación de las interfaces declaradas en esa parte; sin embargo, esto no es un requisito. Una parte puede proporcionar la implementación de una interfaz declarada en otra parte.
Ejemplo:
partial class X { int IComparable.CompareTo(object o) {...} } partial class X : IComparable { ... }
ejemplo final
Las interfaces base especificadas en una declaración de clase se pueden construir tipos de interfaz (§8.4, §18.2). Una interfaz base no puede ser un parámetro de tipo propio, aunque puede implicar los parámetros de tipo que están en el ámbito.
Ejemplo: el código siguiente muestra cómo una clase puede implementar y extender tipos construidos:
class C<U, V> {} interface I1<V> {} class D : C<string, int>, I1<string> {} class E<T> : C<int, T>, I1<T> {}
ejemplo final
Las implementaciones de interfaz se describen más adelante en §18.6.
15.2.5 Restricciones de parámetro de tipo
Las declaraciones de tipo y método genéricos pueden especificar opcionalmente restricciones de parámetro de tipo mediante la inclusión de 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' '(' ')'
;
Cada type_parameter_constraints_clause consta del token where
, seguido del nombre de un parámetro de tipo, seguido de dos puntos y la lista de restricciones para ese parámetro de tipo. Puede haber como máximo una where
cláusula para cada parámetro de tipo y las where
cláusulas se pueden enumerar en cualquier orden. Al igual que los get
tokens y set
en un descriptor de acceso de propiedad, el where
token no es una palabra clave.
La lista de restricciones dadas en una where
cláusula puede incluir cualquiera de los siguientes componentes, en este orden: una única restricción principal, una o varias restricciones secundarias y la restricción constructor, new()
.
Una restricción principal puede ser un tipo de clase, la restricción class
de tipo de referencia , la restricción struct
de tipo de valor , la restricción notnull
not null o la restricción unmanaged
de tipo no administrado . El tipo de clase y la restricción de tipo de referencia pueden incluir el nullable_type_annotation.
Una restricción secundaria puede ser un interface_type o type_parameter, seguido opcionalmente de un nullable_type_annotation. La presencia del nullable_type_annotatione* indica que el argumento type puede ser el tipo de referencia que acepta valores NULL que corresponde a un tipo de referencia que no acepta valores NULL que satisface la restricción.
La restricción de tipo de referencia especifica que un argumento de tipo utilizado para el parámetro de tipo debe ser un tipo de referencia. Todos los tipos de clase, tipos de interfaz, tipos delegados, tipos de matriz y parámetros de tipo conocidos como un tipo de referencia (como se define a continuación) cumplen esta restricción.
El tipo de clase, la restricción de tipo de referencia y las restricciones secundarias pueden incluir la anotación de tipo que acepta valores NULL. La presencia o ausencia de esta anotación en el parámetro de tipo indica las expectativas de nulabilidad para el argumento de tipo:
- Si la restricción no incluye la anotación de tipo que acepta valores NULL, se espera que el argumento type sea un tipo de referencia que no acepta valores NULL. Un compilador puede emitir una advertencia si el argumento type es un tipo de referencia que acepta valores NULL.
- Si la restricción incluye la anotación de tipo que acepta valores NULL, la restricción se satisface mediante un tipo de referencia que no acepta valores NULL y un tipo de referencia que acepta valores NULL.
La nulabilidad del argumento de tipo no debe coincidir con la nulabilidad del parámetro de tipo. El compilador puede emitir una advertencia si la nulabilidad del parámetro de tipo no coincide con la nulabilidad del argumento de tipo.
Nota: Para especificar que un argumento de tipo es un tipo de referencia que acepta valores NULL, no agregue la anotación de tipo que acepta valores NULL como una restricción (use
T : class
oT : BaseClass
), pero useT?
a lo largo de la declaración genérica para indicar el tipo de referencia que acepta valores NULL correspondiente para el argumento de tipo. nota final
La anotación de tipo que acepta valores NULL, ?
, no se puede usar en un argumento de tipo sin restricciones.
Para un parámetro T
de tipo cuando el argumento de tipo es un tipo C?
de referencia que acepta valores NULL, las instancias de T?
se interpretan como C?
, no C??
.
Ejemplo: en los ejemplos siguientes se muestra cómo la nulabilidad de un argumento de tipo afecta a la nulabilidad de una declaración de su parámetro de 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? } }
Cuando el argumento de tipo es un tipo que no acepta valores NULL, la
?
anotación de tipo indica que el parámetro es el tipo que acepta valores NULL correspondiente. Cuando el argumento de tipo ya es un tipo de referencia que acepta valores NULL, el parámetro es ese mismo tipo que acepta valores NULL.ejemplo final
La restricción not null especifica que un argumento de tipo usado para el parámetro de tipo debe ser un tipo de valor que no acepta valores NULL o un tipo de referencia que no acepta valores NULL. Se permite un argumento de tipo que no es un tipo de valor que acepta valores NULL o un tipo de referencia que no acepta valores NULL, pero el compilador puede generar una advertencia de diagnóstico.
La restricción de tipo de valor especifica que un argumento de tipo utilizado para el parámetro de tipo debe ser un tipo de valor que no acepta valores NULL. Todos los tipos de estructura que no aceptan valores NULL, los tipos de enumeración y los parámetros de tipo que tienen la restricción de tipo de valor cumplen esta restricción. Tenga en cuenta que, aunque se clasifica como un tipo de valor, un tipo de valor que acepta valores NULL (§8.3.12) no satisface la restricción de tipo de valor. Un parámetro de tipo que tenga la restricción de tipo de valor no también tendrá el constructor_constraint, aunque se puede usar como argumento de tipo para otro parámetro de tipo con un constructor_constraint.
Nota: El
System.Nullable<T>
tipo especifica la restricción de tipo de valor que no acepta valores NULL paraT
. Por lo tanto, los tipos construidos recursivamente de las formasT??
yNullable<Nullable<T>>
están prohibidos. nota final
Dado que unmanaged
no es una palabra clave, en primary_constraint la restricción no administrada siempre es ambigua sintácticamente con class_type. Por motivos de compatibilidad, si una búsqueda de nombres (§12.8.4) del nombre unmanaged
se realiza correctamente, se trata como .class_type
De lo contrario, se trata como la restricción no administrada.
La restricción de tipo no administrado especifica que un argumento de tipo usado para el parámetro de tipo debe ser un tipo no administrado que no acepta valores NULL (§8.8).
Los tipos de puntero nunca pueden ser argumentos de tipo y no satisfacen restricciones de tipo, incluso no administradas, a pesar de ser tipos no administrados.
Si una restricción es un tipo de clase, un tipo de interfaz o un parámetro de tipo, ese tipo especifica un "tipo base" mínimo que todos los argumentos de tipo utilizados para ese parámetro de tipo admitirán. Cada vez que se usa un tipo construido o un método genérico, el argumento type se comprueba con las restricciones del parámetro de tipo en tiempo de compilación. El argumento type proporcionado cumplirá las condiciones descritas en §8.4.5.
Una restricción class_type cumplirá las siguientes normas:
- El tipo debe ser un tipo de clase.
- El tipo no
sealed
será . - El tipo no debe ser uno de los siguientes tipos:
System.Array
oSystem.ValueType
. - El tipo no
object
será . - Como máximo, una restricción para un parámetro de tipo determinado puede ser un tipo de clase.
Un tipo especificado como restricción interface_type cumplirá las siguientes reglas:
- El tipo debe ser un tipo de interfaz.
- Un tipo no se especificará más de una vez en una cláusula determinada
where
.
En cualquier caso, la restricción puede implicar cualquiera de los parámetros de tipo de la declaración de método o tipo asociado como parte de un tipo construido, y puede implicar el tipo que se declara.
Cualquier clase o tipo de interfaz especificado como restricción de parámetro de tipo debe ser al menos tan accesible (§7.5.5) como tipo genérico o método que se declara.
Un tipo especificado como restricción type_parameter cumplirá las siguientes reglas:
- El tipo debe ser un parámetro de tipo.
- Un tipo no se especificará más de una vez en una cláusula determinada
where
.
Además, no habrá ciclos en el gráfico de dependencias de parámetros de tipo, donde dependency es una relación transitiva definida por:
- Si se usa un parámetro
T
de tipo como restricción para el parámetroS
de tipo,S
depende.T
- Si un parámetro
S
de tipo depende de un parámetroT
de tipo yT
depende de un parámetroU
de tipo,S
dependeU
de .
Dada esta relación, es un error en tiempo de compilación para que un parámetro de tipo dependa de sí mismo (directa o indirectamente).
Las restricciones deben ser coherentes entre los parámetros de tipo dependiente. Si el parámetro S
de tipo depende del parámetro T
de tipo, haga lo siguiente:
T
no tendrá la restricción de tipo de valor. De lo contrario,T
se sella eficazmente, por lo queS
se forzaría a ser el mismo tipo queT
, lo que elimina la necesidad de dos parámetros de tipo.- Si
S
tiene la restricción de tipo de valor,T
no tendrá una restricción class_type . - Si
S
tiene una restricciónA
class_type yT
tiene una restricciónB
class_type, habrá una conversión de identidad o una conversión de referencia implícita deA
a oB
una conversión de referencia implícita deB
aA
. - Si
S
también depende del parámetroU
de tipo yU
tiene una restricciónA
class_type yT
tiene una restricciónB
class_type, habrá una conversión de identidad o una conversión de referencia implícita deA
aB
o una conversión de referencia implícita deB
aA
.
Es válido para S
tener la restricción de tipo de valor y T
tener la restricción de tipo de referencia. De hecho, esto limita T
a los tipos System.Object
, System.ValueType
, System.Enum
y cualquier tipo de interfaz.
Si la where
cláusula de un parámetro de tipo incluye una restricción de constructor (que tiene el formato new()
), es posible usar el new
operador para crear instancias del tipo (§12.8.17.2). Cualquier argumento de tipo usado para un parámetro de tipo con una restricción de constructor debe ser un tipo de valor, una clase no abstracta que tenga un constructor sin parámetros público o un parámetro de tipo que tenga la restricción de tipo de valor o la restricción de constructor.
Se trata de un error en tiempo de compilación para type_parameter_constraints tener un primary_constraint de struct
o unmanaged
también tener un constructor_constraint.
Ejemplo: a continuación se muestran ejemplos de restricciones:
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() { ... }
En el ejemplo siguiente se produce un error porque provoca una circularidad en el gráfico de dependencias de los parámetros de tipo:
class Circular<S,T> where S: T where T: S // Error, circularity in dependency graph { ... }
En los ejemplos siguientes se muestran situaciones no válidas adicionales:
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 { ... }
ejemplo final
La eliminación dinámica de un tipo C
se Cₓ
construye de la siguiente manera:
- Si
C
es un tipoOuter.Inner
anidado,Cₓ
es un tipoOuterₓ.Innerₓ
anidado . - Si
C
Cₓ
es un tipoG<A¹, ..., Aⁿ>
construido con argumentosA¹, ..., Aⁿ
de tipo,Cₓ
es el tipoG<A¹ₓ, ..., Aⁿₓ>
construido . - Si
C
es un tipoE[]
de matriz,Cₓ
es el tipoEₓ[]
de matriz . - Si
C
es dinámico,Cₓ
esobject
. - En caso contrario,
Cₓ
esC
.
La clase base efectiva de un parámetro T
de tipo se define de la siguiente manera:
Vamos R
a ser un conjunto de tipos de tal manera que:
- Para cada restricción de que es un parámetro de
T
tipo,R
contiene su clase base efectiva. - Para cada restricción de
T
que es un tipo de estructura,R
contieneSystem.ValueType
. - Para cada restricción de que es un tipo de
T
enumeración,R
contieneSystem.Enum
. - Para cada restricción de
T
que es un tipo delegado,R
contiene su borrado dinámico. - Para cada restricción de que es un tipo de
T
matriz,R
contieneSystem.Array
. - Para cada restricción de que es un tipo de
T
clase,R
contiene su borrado dinámico.
Entonces
- Si
T
tiene la restricción de tipo de valor, su clase base efectiva esSystem.ValueType
. - De lo contrario, si
R
está vacío, la clase base efectiva esobject
. - De lo contrario, la clase base efectiva de
T
es el tipo más abarcado (§10.5.3) del conjuntoR
. Si el conjunto no tiene ningún tipo abarcado, la clase base efectiva deT
esobject
. Las reglas de coherencia garantizan que exista el tipo más abarcado.
Si el parámetro de tipo es un parámetro de tipo de método cuyas restricciones se heredan del método base, la clase base efectiva se calcula después de la sustitución de tipos.
Estas reglas garantizan que la clase base efectiva siempre sea una class_type.
El conjunto de interfaz efectivo de un parámetro T
de tipo se define de la siguiente manera:
- Si
T
no tiene secondary_constraints, su conjunto de interfaz efectivo está vacío. - Si
T
tiene restricciones interface_type pero no type_parameter restricciones, su conjunto de interfaces efectivo es el conjunto de borrados dinámicos de sus restricciones de interface_type . - Si
T
no tiene restricciones interface_type , pero tiene restricciones type_parameter , su conjunto de interfaz efectivo es la unión de los conjuntos de interfaces eficaces de sus restricciones de type_parameter . - Si
T
tiene restricciones interface_type y restricciones de type_parameter , su conjunto de interfaces efectivo es la unión del conjunto de borrados dinámicos de sus restricciones de interface_type y los conjuntos de interfaces efectivos de sus restricciones de type_parameter .
Se sabe que un parámetro de tipo es un tipo de referencia si tiene la restricción de tipo de referencia o su clase base efectiva no object
es o System.ValueType
. Se sabe que un parámetro de tipo es un tipo de referencia que no acepta valores NULL si se sabe que es un tipo de referencia y tiene la restricción de tipo de referencia que no acepta valores NULL.
Los valores de un tipo de parámetro de tipo restringido se pueden usar para tener acceso a los miembros de instancia implícitos en las restricciones.
Ejemplo: En lo siguiente:
interface IPrintable { void Print(); } class Printer<T> where T : IPrintable { void PrintOne(T x) => x.Print(); }
los métodos de
IPrintable
se pueden invocar directamente enx
porqueT
está restringido para implementarIPrintable
siempre .ejemplo final
Cuando una declaración de tipo genérico parcial incluye restricciones, las restricciones estarán de acuerdo con todas las demás partes que incluyan restricciones. En concreto, cada parte que incluya restricciones tendrá restricciones para el mismo conjunto de parámetros de tipo y, para cada parámetro de tipo, los conjuntos de restricciones principal, secundaria y constructor serán equivalentes. Dos conjuntos de restricciones son equivalentes si contienen los mismos miembros. Si ninguna parte de un tipo genérico parcial especifica restricciones de parámetro de tipo, los parámetros de tipo se consideran sin restricciones.
Ejemplo:
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> { ... }
es correcto porque las partes que incluyen restricciones (las dos primeras) especifican eficazmente el mismo conjunto de restricciones principal, secundaria y constructor para el mismo conjunto de parámetros de tipo, respectivamente.
ejemplo final
15.2.6 Cuerpo de clase
El class_body de una clase define los miembros de esa clase.
class_body
: '{' class_member_declaration* '}'
;
15.2.7 Declaraciones parciales
El modificador partial
se usa al definir una clase, estructura o tipo de interfaz en varias partes. El partial
modificador es una palabra clave contextual (§6.4.4) y solo tiene un significado especial inmediatamente antes de una de las palabras clave class
, struct
o interface
.
Cada parte de una declaración de tipo parcial incluirá un partial
modificador y se declarará en el mismo espacio de nombres o tipo contenedor que las demás partes. El partial
modificador indica que pueden existir partes adicionales de la declaración de tipo en otro lugar, pero la existencia de dichas partes adicionales no es un requisito; es válida para que la única declaración de un tipo incluya el partial
modificador. Es válido para que solo una declaración de un tipo parcial incluya la clase base o las interfaces implementadas. Sin embargo, todas las declaraciones de una clase base o interfaces implementadas deben coincidir, incluida la nulabilidad de cualquier argumento de tipo especificado.
Todas las partes de un tipo parcial se compilarán conjuntamente de modo que las partes se puedan combinar en tiempo de compilación. Los tipos parciales específicamente no permiten ampliar los tipos ya compilados.
Los tipos anidados se pueden declarar en varias partes mediante el partial
modificador . Normalmente, el tipo contenedor también se declara utilizando partial
y cada parte del tipo anidado se declara en una parte diferente del tipo contenedor.
Ejemplo: La siguiente clase parcial se implementa en dos partes, que residen en unidades de compilación diferentes. La primera parte es la máquina generada por una herramienta de asignación de base de datos, mientras que la segunda parte se crea 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; }
Cuando las dos partes anteriores se compilan juntas, el código resultante se comporta como si la clase se hubiera escrito como una sola unidad, como se indica a continuación:
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; }
ejemplo final
El control de los atributos especificados en los parámetros de tipo o tipo de diferentes partes de una declaración parcial se describe en §22.3.
15.3 Miembros de la clase
15.3.1 General
Los miembros de una clase constan de los miembros introducidos por sus class_member_declarations y los miembros heredados de la clase base directa.
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
;
Los miembros de una clase se dividen en las siguientes categorías:
- Constantes, que representan valores constantes asociados a la clase (§15.4).
- Campos, que son las variables de la clase (§15.5).
- Métodos, que implementan los cálculos y las acciones que puede realizar la clase (§15.6).
- Propiedades, que definen características con nombre y las acciones asociadas a la lectura y escritura de esas características (§15.7).
- Eventos, que definen las notificaciones que puede generar la clase (§15.8).
- Indizadores, que permiten que las instancias de la clase se indexan de la misma manera (sintácticamente) que las matrices (§15.9).
- Operadores, que definen los operadores de expresión que se pueden aplicar a instancias de la clase (§15.10).
- Constructores de instancia, que implementan las acciones necesarias para inicializar instancias de la clase (§15.11)
- Finalizadores, que implementan las acciones que se van a realizar antes de que las instancias de la clase se descarten permanentemente (§15.13).
- Constructores estáticos, que implementan las acciones necesarias para inicializar la propia clase (§15.12).
- Tipos, que representan los tipos que son locales para la clase (§14.7).
Un class_declaration crea un nuevo espacio de declaración (§7.3) y los type_parametery los class_member_declarationque contiene inmediatamente el class_declaration introducen nuevos miembros en este espacio de declaración. Las reglas siguientes se aplican a class_member_declarations:
Los constructores de instancia, los finalizadores y los constructores estáticos tendrán el mismo nombre que la clase envolvente inmediatamente. Todos los demás miembros tendrán nombres que difieren del nombre de la clase envolvente inmediatamente.
El nombre de un parámetro de tipo en la type_parameter_list de una declaración de clase diferirá de los nombres de todos los demás parámetros de tipo de la misma type_parameter_list y diferirá del nombre de la clase y los nombres de todos los miembros de la clase.
El nombre de un tipo diferirá de los nombres de todos los miembros no de tipo declarados en la misma clase. Si dos o más declaraciones de tipo comparten el mismo nombre completo, las declaraciones tendrán el
partial
modificador (§15.2.7) y estas declaraciones se combinarán para definir un único tipo.
Nota: Dado que el nombre completo de una declaración de tipo codifica el número de parámetros de tipo, dos tipos distintos pueden compartir el mismo nombre siempre que tengan un número diferente de parámetros de tipo. nota final
El nombre de una constante, campo, propiedad o evento será diferente de los nombres de todos los demás miembros declarados en la misma clase.
El nombre de un método diferirá de los nombres de todos los demás no métodos declarados en la misma clase. Además, la firma (§7.6) de un método diferirá de las firmas de todos los demás métodos declarados en la misma clase y dos métodos declarados en la misma clase no tendrán firmas que difieren únicamente por
in
,out
yref
.La firma de un constructor de instancia diferirá de las firmas de todos los demás constructores de instancia declarados en la misma clase y dos constructores declarados en la misma clase no tendrán firmas que difieren únicamente por
ref
yout
.La firma de un indexador diferirá de las firmas de todos los demás indizadores declarados en la misma clase.
La firma de un operador diferirá de las firmas de todos los demás operadores declarados en la misma clase.
Los miembros heredados de una clase (§15.3.4) no forman parte del espacio de declaración de una clase.
Nota: Por lo tanto, se permite que una clase derivada declare un miembro con el mismo nombre o firma que un miembro heredado (que en efecto oculta el miembro heredado). nota final
El conjunto de miembros de un tipo declarado en varias partes (§15.2.7) es la unión de los miembros declarados en cada parte. Los cuerpos de todas las partes de la declaración de tipo comparten el mismo espacio de declaración (§7.3) y el ámbito de cada miembro (§7.7) se extiende a los cuerpos de todas las partes. El dominio de accesibilidad de cualquier miembro siempre incluye todas las partes del tipo envolvente; Un miembro privado declarado en una parte es libremente accesible desde otra parte. Se trata de un error en tiempo de compilación para declarar el mismo miembro en más de una parte del tipo, a menos que ese miembro sea un tipo que tenga el partial
modificador .
Ejemplo:
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; } }
ejemplo final
El orden de inicialización de campo puede ser significativo en el código de C# y se proporcionan algunas garantías, como se define en §15.5.6.1. De lo contrario, el orden de los miembros dentro de un tipo rara vez es significativo, pero puede ser significativo al interactuar con otros lenguajes y entornos. En estos casos, el orden de los miembros dentro de un tipo declarado en varias partes no está definido.
15.3.2 El tipo de instancia
Cada declaración de clase tiene un tipo de instancia asociado. Para una declaración de clase genérica, el tipo de instancia se forma mediante la creación de un tipo construido (§8.4) a partir de la declaración de tipo, con cada uno de los argumentos de tipo proporcionados que son el parámetro de tipo correspondiente. Puesto que el tipo de instancia usa los parámetros de tipo, solo se puede usar donde los parámetros de tipo están en el ámbito; es decir, dentro de la declaración de clase. El tipo de instancia es el tipo de para el código escrito dentro de this
la declaración de clase. Para las clases no genéricas, el tipo de instancia es simplemente la clase declarada.
Ejemplo: a continuación se muestran varias declaraciones de clase junto con sus tipos de instancia:
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
ejemplo final
15.3.3 Miembros de tipos construidos
Los miembros no heredados de un tipo construido se obtienen sustituyendo, por cada type_parameter de la declaración miembro, el type_argument correspondiente del tipo construido. El proceso de sustitución se basa en el significado semántico de las declaraciones de tipo y no es simplemente la sustitución textual.
Ejemplo: Dada la declaración de clase genérica
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) {...} }
el tipo
Gen<int[],IComparable<string>>
construido tiene los siguientes miembros: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) {...}
El tipo del miembro
a
de la declaraciónGen
de clase genérica es "matriz bidimensional deT
", por lo que el tipo del miembroa
del tipo construido anterior es "matriz bidimensional de matriz unidimensional deint
" oint[,][]
.ejemplo final
Dentro de los miembros de la función de instancia, el tipo de es el tipo de this
instancia (§15.3.2) de la declaración contenedora.
Todos los miembros de una clase genérica pueden usar parámetros de tipo de cualquier clase envolvente, ya sea directamente o como parte de un tipo construido. Cuando se usa un tipo construido cerrado determinado (§8.4.3) en tiempo de ejecución, cada uso de un parámetro de tipo se reemplaza por el argumento type proporcionado al tipo construido.
Ejemplo:
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 } }
ejemplo final
15.3.4 Herencia
Una clase hereda los miembros de su clase base directa. La herencia significa que una clase contiene implícitamente todos los miembros de su clase base directa, excepto los constructores de instancia, los finalizadores y los constructores estáticos de la clase base. Algunos aspectos importantes de la herencia son:
La herencia es transitiva. Si
C
se deriva deB
yB
se deriva deA
,C
hereda los miembros declarados enB
, así como los miembros declarados enA
.Una clase derivada extiende su clase base directa. Una clase derivada puede agregar nuevos miembros a aquellos de los que hereda, pero no puede quitar la definición de un miembro heredado.
Los constructores de instancia, los finalizadores y los constructores estáticos no se heredan, pero todos los demás miembros son, independientemente de su accesibilidad declarada (§7.5). Sin embargo, dependiendo de su accesibilidad declarada, es posible que los miembros heredados no sean accesibles en una clase derivada.
Una clase derivada puede ocultar (§7.7.2.3) miembros heredados declarando nuevos miembros con el mismo nombre o firma. Sin embargo, ocultar un miembro heredado no quita ese miembro; simplemente hace que ese miembro sea inaccesible directamente a través de la clase derivada.
Una instancia de una clase contiene un conjunto de todos los campos de instancia declarados en la clase y sus clases base, y existe una conversión implícita (§10.2.8) desde un tipo de clase derivada a cualquiera de sus tipos de clase base. Por lo tanto, una referencia a una instancia de alguna clase derivada se puede tratar como referencia a una instancia de cualquiera de sus clases base.
Una clase puede declarar métodos virtuales, propiedades, indizadores y eventos, y las clases derivadas pueden invalidar la implementación de estos miembros de función. Esto permite que las clases muestren un comportamiento polimórfico en el que las acciones realizadas por una invocación de miembro de función varían en función del tipo en tiempo de ejecución de la instancia a través de la cual se invoca ese miembro de función.
Los miembros heredados de un tipo de clase construido son los miembros del tipo de clase base inmediato (§15.2.4.2), que se encuentra sustituyendo los argumentos de tipo del tipo construido por cada aparición de los parámetros de tipo correspondientes en el base_class_specification. Estos miembros, a su vez, se transforman sustituyendo, por cada type_parameter de la declaración de miembro, el type_argument correspondiente del base_class_specification.
Ejemplo:
class B<U> { public U F(long index) {...} } class D<T> : B<T[]> { public T G(string s) {...} }
En el código anterior, el tipo
D<int>
construido tiene un miembro no heredado públicoG(string s)
int
obtenido sustituyendo el argumentoint
type del parámetroT
de tipo .D<int>
también tiene un miembro heredado de la declaraciónB
de clase . Este miembro heredado viene determinado por determinar primero el tipoB<int[]>
de clase base deD<int>
sustituyendoint
porT
en la especificaciónB<T[]>
de clase base . A continuación, como argumento de tipo aB
,int[]
se sustituye porU
enpublic U F(long index)
, lo que produce el miembropublic int[] F(long index)
heredado .ejemplo final
15.3.5 El nuevo modificador
Un class_member_declaration puede declarar un miembro con el mismo nombre o firma que un miembro heredado. Cuando esto ocurre, se dice que el miembro de clase derivada oculta el miembro de clase base. Consulte §7.7.2.3 para obtener una especificación precisa de cuándo un miembro oculta un miembro heredado.
Se considera que un miembro M
heredado está disponible si M
es accesible y no hay ningún otro miembro accesible heredado N que ya oculta M
. Ocultar implícitamente un miembro heredado no se considera un error, pero hace que el compilador emita una advertencia a menos que la declaración del miembro de clase derivada incluya un new
modificador para indicar explícitamente que el miembro derivado está pensado para ocultar el miembro base. Si una o varias partes de una declaración parcial (§15.2.7) de un tipo anidado incluyen el new
modificador, no se emite ninguna advertencia si el tipo anidado oculta un miembro heredado disponible.
Si se incluye un new
modificador en una declaración que no oculta un miembro heredado disponible, se emite una advertencia a ese efecto.
15.3.6 Modificadores de acceso
Un class_member_declaration puede tener cualquiera de los tipos permitidos de accesibilidad declarada (§7.5.2): public
, protected internal
, , protected
private protected
, , internal
o private
. A excepción de las protected internal
combinaciones y private protected
, es un error en tiempo de compilación especificar más de un modificador de acceso. Cuando un class_member_declaration no incluye ningún modificador de acceso, private
se supone.
15.3.7 Tipos constituyentes
Los tipos que se usan en la declaración de un miembro se denominan tipos constituyentes de ese miembro. Los tipos constituyentes posibles son el tipo de una constante, campo, propiedad, evento o indexador, el tipo de valor devuelto de un método o operador, y los tipos de parámetro de un método, indexador, operador o constructor de instancia. Los tipos constituyentes de un miembro serán al menos tan accesibles como ese miembro (§7.5.5).
15.3.8 Miembros estáticos e instancias
Los miembros de una clase son miembros estáticos o miembros de instancia.
Nota: Por lo general, resulta útil pensar en miembros estáticos como pertenecientes a clases y miembros de instancia como pertenecientes a objetos (instancias de clases). nota final
Cuando una declaración de campo, método, propiedad, evento, operador o constructor incluye un static
modificador, declara un miembro estático. Además, una declaración constante o de tipo declara implícitamente un miembro estático. Los miembros estáticos tienen las siguientes características:
- Cuando se hace referencia a un miembro
M
estático en un member_access (§12.8.7) del formularioE.M
,E
denotará un tipo que tenga un miembroM
. Es un error en tiempo de compilación paraE
indicar una instancia. - Un campo estático de una clase no genérica identifica exactamente una ubicación de almacenamiento. Independientemente de cuántas instancias de una clase no genérica se creen, solo hay una copia de un campo estático. Cada tipo construido cerrado distinto (§8.4.3) tiene su propio conjunto de campos estáticos, independientemente del número de instancias del tipo construido cerrado.
- Un miembro de función estática (método, propiedad, evento, operador o constructor) no funciona en una instancia específica y es un error en tiempo de compilación para hacer referencia a esto en dicho miembro de función.
Cuando una declaración de campo, método, propiedad, evento, indexador, constructor o finalizador no incluye un modificador estático, declara un miembro de instancia. (A veces, un miembro de instancia se denomina miembro no estático). Los miembros de instancia tienen las siguientes características:
- Cuando se hace referencia a un miembro
M
de instancia en un member_access (§12.8.7) del formularioE.M
,E
indicará una instancia de un tipo que tiene un miembroM
. Es un error en tiempo de enlace para que E denota un tipo. - Cada instancia de una clase contiene un conjunto independiente de todos los campos de instancia de la clase.
- Un miembro de función de instancia (método, propiedad, indexador, constructor de instancia o finalizador) funciona en una instancia determinada de la clase y se puede acceder a esta instancia como
this
(§12.8.14).
Ejemplo: en el ejemplo siguiente se muestran las reglas para acceder a miembros estáticos e de instancia:
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 } }
El
F
método muestra que, en un miembro de función de instancia, se puede usar un simple_name (§12.8.4) para tener acceso a los miembros de instancia y a los miembros estáticos. ElG
método muestra que, en un miembro de función estática, se trata de un error en tiempo de compilación para acceder a un miembro de instancia a través de un simple_name. ElMain
método muestra que, en un member_access (§12.8.7), se tendrá acceso a los miembros de instancia a través de instancias y se tendrá acceso a los miembros estáticos a través de tipos.ejemplo final
15.3.9 Tipos anidados
15.3.9.1 General
Un tipo declarado dentro de una clase o estructura se denomina tipo anidado. Un tipo que se declara dentro de una unidad de compilación o un espacio de nombres se denomina tipo no anidado.
Ejemplo: en el ejemplo siguiente:
class A { class B { static void F() { Console.WriteLine("A.B.F"); } } }
la clase
B
es un tipo anidado porque se declara dentro de la claseA
y la claseA
es un tipo no anidado porque se declara dentro de una unidad de compilación.ejemplo final
15.3.9.2 Nombre completo
El nombre completo (§7.8.3) para una declaración S.N
de tipo anidado, donde S
es el nombre completo de la declaración de tipo en la que se declara el tipo N
y N
es el nombre no completo (§7.8.2) de la declaración de tipo anidado (incluido cualquier generic_dimension_specifier (§12.8.18)).
15.3.9.3 Accesibilidad declarada
Los tipos no anidados pueden tener public
o internal
declarar la accesibilidad y han internal
declarado la accesibilidad de forma predeterminada. Los tipos anidados también pueden tener estas formas de accesibilidad declaradas, además de una o varias formas adicionales de accesibilidad declarada, dependiendo de si el tipo contenedor es una clase o estructura:
- Un tipo anidado declarado en una clase puede tener cualquiera de los tipos permitidos de accesibilidad declarada y, al igual que otros miembros de clase, el valor predeterminado es
private
la accesibilidad declarada. - Un tipo anidado declarado en un struct puede tener cualquiera de tres formas de accesibilidad declarada (
public
,internal
oprivate
) y, al igual que otros miembros de estructura, el valor predeterminado esprivate
la accesibilidad declarada.
Ejemplo: El ejemplo
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 {...} } }
declara una clase
Node
anidada privada .ejemplo final
15.3.9.4 Ocultar
Un tipo anidado puede ocultar (§7.7.2.2) un miembro base. El new
modificador (§15.3.5) se permite en declaraciones de tipo anidado para que la ocultación se pueda expresar explícitamente.
Ejemplo: El ejemplo
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(); } }
muestra una clase
M
anidada que oculta el métodoM
definido enBase
.ejemplo final
15.3.9.5 este acceso
Un tipo anidado y su tipo contenedor no tienen una relación especial con respecto a this_access (§12.8.14). En concreto, this
dentro de un tipo anidado no se puede usar para hacer referencia a los miembros de instancia del tipo contenedor. En los casos en los que un tipo anidado necesita acceso a los miembros de instancia de su tipo contenedor, se puede proporcionar acceso proporcionando para this
la instancia del tipo contenedor como argumento de constructor para el tipo anidado.
Ejemplo: El ejemplo siguiente
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(); } }
muestra esta técnica. Una instancia de
C
crea una instancia deNested
y pasa su propio constructor aNested
para proporcionar acceso posterior a losC
miembros de instancia de .ejemplo final
15.3.9.6 Acceso a miembros privados y protegidos del tipo contenedor
Un tipo anidado tiene acceso a todos los miembros a los que se puede acceder a su tipo contenedor, incluidos los miembros del tipo contenedor que tienen private
y protected
declaran accesibilidad.
Ejemplo: El ejemplo
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(); }
muestra una clase
C
que contiene una claseNested
anidada . DentroNested
de , el métodoG
llama al métodoF
estático definido enC
yF
tiene accesibilidad declarada privada.ejemplo final
Un tipo anidado también puede tener acceso a los miembros protegidos definidos en un tipo base de su tipo contenedor.
Ejemplo: en el código siguiente
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 clase
Derived.Nested
anidada tiene acceso al métodoF
protegido definido enDerived
la clase base de ,Base
llamando a a través de una instancia deDerived
.ejemplo final
15.3.9.7 Tipos anidados en clases genéricas
Una declaración de clase genérica puede contener declaraciones de tipo anidadas. Los parámetros de tipo de la clase envolvente se pueden usar dentro de los tipos anidados. Una declaración de tipo anidado puede contener parámetros de tipo adicionales que solo se aplican al tipo anidado.
Cada declaración de tipo contenida dentro de una declaración de clase genérica es implícitamente una declaración de tipo genérico. Al escribir una referencia a un tipo anidado dentro de un tipo genérico, se denominará el tipo contenedor construido, incluidos sus argumentos de tipo. Sin embargo, desde dentro de la clase externa, se puede usar el tipo anidado sin cualificación; el tipo de instancia de la clase externa se puede usar implícitamente al construir el tipo anidado.
Ejemplo: a continuación se muestran tres formas correctas diferentes de hacer referencia a un tipo construido creado a partir de
Inner
; los dos primeros son equivalentes: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 } }
ejemplo final
Aunque es un estilo de programación incorrecto, un parámetro de tipo en un tipo anidado puede ocultar un miembro o parámetro de tipo declarado en el tipo externo.
Ejemplo:
class Outer<T> { class Inner<T> // Valid, hides Outer's T { public T t; // Refers to Inner's T } }
ejemplo final
15.3.10 Nombres de miembros reservados
15.3.10.1 General
Para facilitar la implementación en tiempo de ejecución subyacente de C#, para cada declaración de miembro de origen que sea una propiedad, evento o indexador, la implementación reservará dos firmas de método basadas en el tipo de declaración de miembro, su nombre y su tipo (§15.3.10.2, §15.3.10.3, §15.3.10.4). Se trata de un error en tiempo de compilación para que un programa declare un miembro cuya firma coincide con una firma reservada por un miembro declarado en el mismo ámbito, incluso si la implementación en tiempo de ejecución subyacente no hace uso de estas reservas.
Los nombres reservados no introducen declaraciones, por lo que no participan en la búsqueda de miembros. Sin embargo, las firmas de método reservado asociadas de una declaración participan en la herencia (§15.3.4) y se pueden ocultar con el new
modificador (§15.3.5).
Nota: La reserva de estos nombres tiene tres propósitos:
- Para permitir que la implementación subyacente use un identificador normal como nombre de método para obtener o establecer el acceso a la característica del lenguaje C#.
- Para permitir que otros lenguajes interoperan mediante un identificador normal como un nombre de método para obtener o establecer el acceso a la característica de lenguaje C#.
- Para ayudar a garantizar que el origen aceptado por un compilador conforme sea aceptado por otro, al hacer que los detalles de los nombres de miembro reservados sean coherentes en todas las implementaciones de C#.
nota final
La declaración de un finalizador (§15.13) también hace que se reserve una firma (§15.3.10.5).
Algunos nombres están reservados para su uso como nombres de método de operador (§15.3.10.6).
15.3.10.2 Nombres de miembros reservados para las propiedades
Para una propiedad P
(§15.7) de tipo T
, se reservan las siguientes firmas:
T get_P();
void set_P(T value);
Ambas firmas están reservadas, incluso si la propiedad es de solo lectura o de solo escritura.
Ejemplo: en el código siguiente
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 clase
A
define una propiedadP
de solo lectura , reservando así firmas paraget_P
los métodos yset_P
.A
La claseB
se deriva deA
y oculta ambas firmas reservadas. En el ejemplo se genera la salida:123 123 456
ejemplo final
15.3.10.3 Nombres de miembros reservados para eventos
Para un evento E
(§15.8) del tipo T
delegado , se reservan las siguientes firmas:
void add_E(T handler);
void remove_E(T handler);
15.3.10.4 Nombres de miembros reservados para indexadores
Para un indexador (§15.9) de tipo T
con la lista L
de parámetros, se reservan las siguientes firmas:
T get_Item(L);
void set_Item(L, T value);
Ambas firmas están reservadas, incluso si el indexador es de solo lectura o de solo escritura.
Además, se reserva el nombre Item
del miembro.
15.3.10.5 Nombres de miembros reservados para los finalizadores
Para una clase que contiene un finalizador (§15.13), se reserva la siguiente firma:
void Finalize();
15.3.10.6 Nombres de método reservados para operadores
Los siguientes nombres de método están reservados. Aunque muchos tienen operadores correspondientes en esta especificación, algunos están reservados para su uso en versiones futuras, mientras que algunos están reservados para la interoperabilidad con otros lenguajes.
Nombre de método | Operador de C# |
---|---|
op_Addition |
+ (binario) |
op_AdditionAssignment |
(reservado) |
op_AddressOf |
(reservado) |
op_Assign |
(reservado) |
op_BitwiseAnd |
& (binario) |
op_BitwiseAndAssignment |
(reservado) |
op_BitwiseOr |
\| |
op_BitwiseOrAssignment |
(reservado) |
op_CheckedAddition |
(reservado para uso futuro) |
op_CheckedDecrement |
(reservado para uso futuro) |
op_CheckedDivision |
(reservado para uso futuro) |
op_CheckedExplicit |
(reservado para uso futuro) |
op_CheckedIncrement |
(reservado para uso futuro) |
op_CheckedMultiply |
(reservado para uso futuro) |
op_CheckedSubtraction |
(reservado para uso futuro) |
op_CheckedUnaryNegation |
(reservado para uso futuro) |
op_Comma |
(reservado) |
op_Decrement |
-- (prefijo y postfijo) |
op_Division |
/ |
op_DivisionAssignment |
(reservado) |
op_Equality |
== |
op_ExclusiveOr |
^ |
op_ExclusiveOrAssignment |
(reservado) |
op_Explicit |
coerción explícita (restricción) |
op_False |
false |
op_GreaterThan |
> |
op_GreaterThanOrEqual |
>= |
op_Implicit |
coerción implícita (ampliación) |
op_Increment |
++ (prefijo y postfijo) |
op_Inequality |
!= |
op_LeftShift |
<< |
op_LeftShiftAssignment |
(reservado) |
op_LessThan |
< |
op_LessThanOrEqual |
<= |
op_LogicalAnd |
(reservado) |
op_LogicalNot |
! |
op_LogicalOr |
(reservado) |
op_MemberSelection |
(reservado) |
op_Modulus |
% |
op_ModulusAssignment |
(reservado) |
op_MultiplicationAssignment |
(reservado) |
op_Multiply |
* (binario) |
op_OnesComplement |
~ |
op_PointerDereference |
(reservado) |
op_PointerToMemberSelection |
(reservado) |
op_RightShift |
>> |
op_RightShiftAssignment |
(reservado) |
op_SignedRightShift |
(reservado) |
op_Subtraction |
- (binario) |
op_SubtractionAssignment |
(reservado) |
op_True |
true |
op_UnaryNegation |
- (unario) |
op_UnaryPlus |
+ (unario) |
op_UnsignedRightShift |
(reservado para uso futuro) |
op_UnsignedRightShiftAssignment |
(reservado) |
15.4 Constantes
Una constante es un miembro de clase que representa un valor constante: un valor que se puede calcular en tiempo de compilación. Un constant_declaration introduce una o varias constantes de un tipo determinado.
constant_declaration
: attributes? constant_modifier* 'const' type constant_declarators ';'
;
constant_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
;
Un constant_declaration puede incluir un conjunto de atributos (§22), un new
modificador (§15.3.5) y cualquiera de los tipos permitidos de accesibilidad declarada (§15.3.6). Los atributos y modificadores se aplican a todos los miembros declarados por el constant_declaration. Aunque las constantes se consideran miembros estáticos, un constant_declaration no requiere ni permite un static
modificador. Es un error para que el mismo modificador aparezca varias veces en una declaración constante.
El tipo de un constant_declaration especifica el tipo de los miembros introducidos por la declaración. El tipo va seguido de una lista de constant_declarator s (§13.6.3), cada uno de los cuales presentaun nuevo miembro. Un constant_declarator consta de un identificador que asigna un nombre al miembro, seguido de un token "=
", seguido de un constant_expression (§12.23) que proporciona el valor del miembro.
El tipo especificado en una declaración constante será sbyte
, byte
, short
int
ushort
long
uint
float
double
ulong
string
char
bool
decimal
enum_type o un reference_type. Cada constant_expression producirá un valor del tipo de destino o de un tipo que se pueda convertir al tipo de destino mediante una conversión implícita (§10.2).
El tipo de una constante debe ser al menos tan accesible como la propia constante (§7.5.5).
El valor de una constante se obtiene en una expresión mediante un simple_name (§12.8.4) o un member_access (§12.8.7).
Una constante puede participar en una constant_expression. Por lo tanto, se puede usar una constante en cualquier construcción que requiera un constant_expression.
Nota: Algunos ejemplos de estas construcciones incluyen
case
etiquetas,goto case
instrucciones,enum
declaraciones de miembro, atributos y otras declaraciones constantes. nota final
Nota: Como se describe en §12.23, un constant_expression es una expresión que se puede evaluar completamente en tiempo de compilación. Puesto que la única manera de crear un valor no NULL de un reference_type distinto
string
de es aplicar elnew
operador y, dado que elnew
operador no está permitido en un constant_expression, el único valor posible para constantes de reference_types distintosstring
de esnull
. nota final
Cuando se desea un nombre simbólico para un valor constante, pero cuando no se permite el tipo de ese valor en una declaración constante, o cuando un constant_expression no puede calcular el valor en tiempo de compilación, se puede usar un campo readonly (§15.5.3).
Nota: La semántica de control de versiones de
const
yreadonly
difiere (§15.5.3.3). nota final
Una declaración constante que declara varias constantes es equivalente a varias declaraciones de constantes únicas con los mismos atributos, modificadores y tipo.
Ejemplo:
class A { public const double X = 1.0, Y = 2.0, Z = 3.0; }
es equivalente a
class A { public const double X = 1.0; public const double Y = 2.0; public const double Z = 3.0; }
ejemplo final
Las constantes pueden depender de otras constantes dentro del mismo programa siempre que las dependencias no sean de naturaleza circular. El compilador organiza automáticamente para evaluar las declaraciones constantes en el orden adecuado.
Ejemplo: en el código siguiente
class A { public const int X = B.Z + 1; public const int Y = 10; } class B { public const int Z = A.Y + 1; }
El compilador evalúa
A.Y
primero , después evalúaB.Z
y, por último, evalúaA.X
, lo que genera los valores10
,11
y12
.ejemplo final
Las declaraciones constantes pueden depender de constantes de otros programas, pero estas dependencias solo son posibles en una dirección.
Ejemplo: Hacer referencia al ejemplo anterior, si
A
yB
se declararon en programas independientes, sería posibleA.X
dependerB.Z
de , peroB.Z
no podía depender simultáneamente deA.Y
. ejemplo final
15.5 Campos
15.5.1 General
Un campo es un miembro que representa una variable asociada a un objeto o clase. Un field_declaration introduce uno o varios campos de un tipo determinado.
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) solo está disponible en código no seguro (§23).
Un field_declaration puede incluir un conjunto de atributos (§22), un new
modificador (§15.3.5), una combinación válida de los cuatro modificadores de acceso (§15.3.6) y un static
modificador (§15.5.2). Además, un field_declaration puede incluir un readonly
modificador (§15.5.3) o un volatile
modificador (§15.5.4), pero no ambos. Los atributos y modificadores se aplican a todos los miembros declarados por el field_declaration. Es un error para que el mismo modificador aparezca varias veces en un field_declaration.
El tipo de un field_declaration especifica el tipo de los miembros introducidos por la declaración. El tipo va seguido de una lista de variable_declarators, cada uno de los cuales presenta un nuevo miembro. Un variable_declarator consta de un identificador que asigna nombres a ese miembro, seguido opcionalmente de un token "=
" y un variable_initializer (§15.5.6) que proporciona el valor inicial de ese miembro.
El tipo de un campo debe ser al menos tan accesible como el propio campo (§7.5.5).
El valor de un campo se obtiene en una expresión mediante un simple_name (§12.8.4), un member_access (§12.8.7) o un base_access (§12.8.15). El valor de un campo no de solo lectura se modifica mediante una asignación (§12.21). El valor de un campo no de solo lectura se puede obtener y modificar mediante operadores de incremento y decremento postfijo (§12.8.16) y operadores de incremento y decremento de prefijo (§12.9.6).
Una declaración de campo que declara varios campos es equivalente a varias declaraciones de campos únicos con los mismos atributos, modificadores y tipo.
Ejemplo:
class A { public static int X = 1, Y, Z = 100; }
es equivalente a
class A { public static int X = 1; public static int Y; public static int Z = 100; }
ejemplo final
15.5.2 Campos estáticos e instancias
Cuando una declaración de campo incluye un static
modificador, los campos introducidos por la declaración son campos estáticos. Cuando no hay ningún static
modificador presente, los campos introducidos por la declaración son campos de instancia. Los campos estáticos y los campos de instancia son dos de los distintos tipos de variables (§9) admitidos por C#, y en ocasiones se conocen como variables estáticas y variables de instancia, respectivamente.
Como se explica en §15.3.8, cada instancia de una clase contiene un conjunto completo de los campos de instancia de la clase, mientras que solo hay un conjunto de campos estáticos para cada clase no genérica o tipo construido cerrado, independientemente del número de instancias de la clase o del tipo construido cerrado.
15.5.3 Campos de solo lectura
15.5.3.1 General
Cuando un field_declaration incluye un readonly
modificador, los campos introducidos por la declaración son campos de solo lectura. Las asignaciones directas a campos de solo lectura pueden producirse como parte de esa declaración o en un constructor de instancia o en un constructor estático en la misma clase. (Un campo de solo lectura se puede asignar a varias veces en estos contextos). En concreto, solo se permiten asignaciones directas a un campo de solo lectura en los contextos siguientes:
- En el variable_declarator que introduce el campo (incluyendo un variable_initializer en la declaración).
- Para un campo de instancia, en los constructores de instancia de la clase que contiene la declaración de campo; para un campo estático, en el constructor estático de la clase que contiene la declaración de campo. Estos también son los únicos contextos en los que es válido pasar un campo de solo lectura como parámetro de salida o referencia.
Intentar asignar a un campo de solo lectura o pasarlo como parámetro de salida o referencia en cualquier otro contexto es un error en tiempo de compilación.
15.5.3.2 Uso de campos de lectura estáticos para constantes
Un campo estático de solo lectura es útil cuando se desea un nombre simbólico para un valor constante, pero cuando no se permite el tipo del valor en una declaración const o cuando el valor no se puede calcular en tiempo de compilación.
Ejemplo: en el código siguiente
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; } }
los
Black
miembros ,White
,Red
,Green
yBlue
no se pueden declarar como miembros const porque sus valores no se pueden calcular en tiempo de compilación. Sin embargo, declararlosstatic readonly
en su lugar tiene mucho el mismo efecto.ejemplo final
15.5.3.3 Control de versiones de constantes y campos de lectura estáticos
Las constantes y los campos readonly tienen una semántica de control de versiones binarias diferente. Cuando una expresión hace referencia a una constante, el valor de la constante se obtiene en tiempo de compilación, pero cuando una expresión hace referencia a un campo readonly, el valor del campo no se obtiene hasta tiempo de ejecución.
Ejemplo: Considere una aplicación que consta de dos programas independientes:
namespace Program1 { public class Utils { public static readonly int x = 1; } }
y
namespace Program2 { class Test { static void Main() { Console.WriteLine(Program1.Utils.X); } } }
Los
Program1
espacios de nombres yProgram2
indican dos programas que se compilan por separado. Dado queProgram1.Utils.X
se declara como unstatic readonly
campo, la salida del valor por laConsole.WriteLine
instrucción no se conoce en tiempo de compilación, sino que se obtiene en tiempo de ejecución. Por lo tanto, si se cambia el valor deX
yProgram1
se vuelve a compilar, laConsole.WriteLine
instrucción generará el nuevo valor incluso siProgram2
no se vuelve a compilar. Sin embargo, habíaX
sido una constante, el valor deX
se habría obtenido en el momentoProgram2
de la compilación y no se vería afectado por los cambios enProgram1
hastaProgram2
que se vuelva a compilar.ejemplo final
15.5.4 Campos volátiles
Cuando un field_declaration incluye un volatile
modificador, los campos introducidos por esa declaración son campos volátiles. En el caso de los campos no volátiles, las técnicas de optimización que reordenan las instrucciones pueden dar lugar a resultados inesperados e impredecibles en programas multiproceso que acceden a campos sin sincronización, como los proporcionados por el lock_statement (§13.13). El compilador puede realizar estas optimizaciones, por el sistema en tiempo de ejecución o por hardware. En el caso de los campos volátiles, estas optimizaciones de reordenación están restringidas:
- Una lectura de un campo volátil se denomina lectura volátil. Una lectura volátil tiene "adquirir semántica"; es decir, se garantiza que se produzcan antes de cualquier referencia a la memoria que se produzca después de ella en la secuencia de instrucciones.
- Una escritura de un campo volátil se denomina escritura volátil. Una escritura volátil tiene "semántica de versión"; es decir, se garantiza que se produzca después de cualquier referencia de memoria antes de la instrucción de escritura en la secuencia de instrucciones.
Estas restricciones aseguran que todos los subprocesos observarán las operaciones de escritura volátiles realizadas por cualquier otro subproceso en el orden en que se realizaron. No se requiere una implementación conforme para proporcionar una única ordenación total de escrituras volátiles, como se ve en todos los subprocesos de ejecución. El tipo de un campo volátil será uno de los siguientes:
- Un reference_type.
- Un type_parameter que se sabe que es un tipo de referencia (§15.2.5).
- El tipo
byte
,sbyte
, ,ushort
short
,int
,uint
char
float
,bool
,System.IntPtr
o .System.UIntPtr
- Un enum_type tener un tipo de enum_base de
byte
,sbyte
,short
,ushort
,int
ouint
.
Ejemplo: El ejemplo
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; } } } }
genera el resultado:
result = 143
En este ejemplo, el método
Main
inicia un nuevo subproceso que ejecuta el métodoThread2
. Este método almacena un valor en un campo no volátil denominadoresult
y, a continuación, almacenatrue
en el campofinished
volátil . El subproceso principal espera a que el campofinished
se establezcatrue
en y, a continuación, lee el camporesult
. Puestofinished
que se ha declaradovolatile
, el subproceso principal leerá el valor143
del camporesult
. Si no se hubiera declarado el campofinished
, se permitiría que el almacénresult
fuera visible para el subproceso principal después del almacén enfinished
y, por lo tanto, para que el subproceso principal lea el valor 0 del camporesult
.volatile
finished
Declarar como unvolatile
campo evita cualquier incoherencia de este tipo.ejemplo final
Inicialización de campo 15.5.5
El valor inicial de un campo, ya sea un campo estático o un campo de instancia, es el valor predeterminado (§9.3) del tipo del campo. No es posible observar el valor de un campo antes de que se haya producido esta inicialización predeterminada y, por tanto, un campo nunca se "inicializa".
Ejemplo: El ejemplo
class Test { static bool b; int i; static void Main() { Test t = new Test(); Console.WriteLine($"b = {b}, i = {t.i}"); } }
genera el resultado
b = False, i = 0
porque
b
yi
se inicializan automáticamente en valores predeterminados.ejemplo final
Inicializadores de variables 15.5.6
15.5.6.1 General
Las declaraciones de campo pueden incluir variable_initializers. En el caso de los campos estáticos, los inicializadores de variables corresponden a instrucciones de asignación que se ejecutan durante la inicialización de clase. Por ejemplo, los inicializadores de variables corresponden a instrucciones de asignación que se ejecutan cuando se crea una instancia de la clase.
Ejemplo: El ejemplo
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}"); } }
genera el resultado
x = 1.4142135623730951, i = 100, s = Hello
porque se produce una asignación cuando
x
los inicializadores de campos estáticos ejecutan y se asignan ai
ys
se producen cuando se ejecutan los inicializadores de campo de instancia.ejemplo final
La inicialización del valor predeterminado descrita en §15.5.5 se produce para todos los campos, incluidos los campos que tienen inicializadores variables. Por lo tanto, cuando se inicializa una clase, todos los campos estáticos de esa clase se inicializan primero en sus valores predeterminados y, a continuación, los inicializadores de campo estáticos se ejecutan en orden textual. Del mismo modo, cuando se crea una instancia de una clase, todos los campos de instancia de esa instancia se inicializan primero en sus valores predeterminados y, a continuación, los inicializadores de campo de instancia se ejecutan en orden textual. Cuando hay declaraciones de campo en varias declaraciones de tipo parcial para el mismo tipo, no se especifica el orden de las partes. Sin embargo, dentro de cada parte, los inicializadores de campo se ejecutan en orden.
Es posible que los campos estáticos con inicializadores de variables se observen en su estado de valor predeterminado.
Ejemplo: Sin embargo, esto es muy desaconsejado como cuestión de estilo. En el ejemplo
class Test { static int a = b + 1; static int b = a + 1; static void Main() { Console.WriteLine($"a = {a}, b = {b}"); } }
muestra este comportamiento. A pesar de las definiciones circulares de
a
yb
, el programa es válido. Da como resultado la salidaa = 1, b = 2
porque los campos
a
estáticos yb
se inicializan en0
(el valor predeterminado paraint
) antes de que se ejecuten sus inicializadores. Cuando se ejecuta el inicializador paraa
, el valor deb
es cero y, por tantoa
, se inicializa en1
. Cuando se ejecuta el inicializador parab
, el valor de ya1
es y, por tantob
, se inicializa en2
.ejemplo final
15.5.6.2 Inicialización de campos estáticos
Los inicializadores de variables de campo estáticos de una clase corresponden a una secuencia de asignaciones que se ejecutan en el orden textual en el que aparecen en la declaración de clase (§15.5.6.1). Dentro de una clase parcial, el significado de "orden textual" se especifica mediante §15.5.6.1. Si existe un constructor estático (§15.12) en la clase , la ejecución de los inicializadores de campo estáticos se produce inmediatamente antes de ejecutar ese constructor estático. De lo contrario, los inicializadores de campo estáticos se ejecutan en un tiempo dependiente de la implementación antes del primer uso de un campo estático de esa clase.
Ejemplo: El ejemplo
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"); }
puede producir cualquiera de los resultados:
Init A Init B 1 1
o la salida:
Init B Init A 1 1
dado que la ejecución del
X
inicializador yY
el inicializador de se pueden producir en cualquier orden; solo se restringen para que se produzcan antes de las referencias a esos campos. Sin embargo, en el ejemplo: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"); }
la salida será:
Init B Init A 1 1
dado que las reglas para cuando se ejecutan constructores estáticos (como se define en §15.12) proporcionan que
B
el constructor estático (y, por lo tantoB
, los inicializadores de campo estáticos) se ejecutarán antesA
de los inicializadores de campo y constructor estáticos.ejemplo final
15.5.6.3 Inicialización del campo de instancia
Los inicializadores de variables de campo de instancia de una clase corresponden a una secuencia de asignaciones que se ejecutan inmediatamente después de la entrada a cualquiera de los constructores de instancia (§15.11.3) de esa clase. Dentro de una clase parcial, el significado de "orden textual" se especifica mediante §15.5.6.1. Los inicializadores de variables se ejecutan en el orden textual en el que aparecen en la declaración de clase (§15.5.6.1). El proceso de creación e inicialización de la instancia de clase se describe más adelante en §15.11.
Un inicializador de variable para un campo de instancia no puede hacer referencia a la instancia que se está creando. Por lo tanto, es un error en tiempo de compilación al que se hace referencia this
en un inicializador de variable, ya que es un error en tiempo de compilación para que un inicializador de variable haga referencia a cualquier miembro de instancia a través de un simple_name.
Ejemplo: en el código siguiente
class A { int x = 1; int y = x + 1; // Error, reference to instance member of this }
el inicializador de variable para
y
produce un error en tiempo de compilación porque hace referencia a un miembro de la instancia que se está creando.ejemplo final
Métodos 15.6
15.6.1 General
Un método es un miembro que implementa un cálculo o una acción que puede realizar un objeto o una clase. Los métodos se declaran mediante 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 ';'
| ';'
;
Notas gramaticales:
- unsafe_modifier (§23.2) solo está disponible en código no seguro (§23).
- cuando se reconozca una method_body si se aplican las alternativas de null_conditional_invocation_expression y expresión , se elegirá la primera.
Nota: La superposición de, y la prioridad entre, las alternativas aquí son únicamente para comodidad descriptiva; las reglas gramaticales se pueden elaborar para quitar la superposición. ANTLR y otros sistemas gramaticales adoptan la misma comodidad y, por tanto , method_body tiene automáticamente la semántica especificada. nota final
Un method_declaration puede incluir un conjunto de atributos (§22) y uno de los tipos permitidos de accesibilidad declarada (§15.3.6), (new
§15.3.5), static
(§15.6.3), (§15.6.3), virtual
(§15 .6.4), (§15.6.5), sealed
(§15.6.6), abstract
(§15.6.7), extern
override
(§15.6.8) y (§15.15) modificadores async
.
Una declaración tiene una combinación válida de modificadores si se cumplen todas las siguientes condiciones:
- La declaración incluye una combinación válida de modificadores de acceso (§15.3.6).
- La declaración no incluye el mismo modificador varias veces.
- La declaración incluye como máximo uno de los modificadores siguientes:
static
,virtual
yoverride
. - La declaración incluye como máximo uno de los modificadores siguientes:
new
yoverride
. - Si la declaración incluye el
abstract
modificador, la declaración no incluye ninguno de los siguientes modificadores:static
,virtual
,sealed
oextern
. - Si la declaración incluye el
private
modificador, la declaración no incluye ninguno de los siguientes modificadores:virtual
,override
oabstract
. - Si la declaración incluye el
sealed
modificador, la declaración también incluye eloverride
modificador . - Si la declaración incluye el
partial
modificador, no incluye ninguno de los siguientes modificadores:new
,public
,protected
,internal
,private
,virtual
,sealed
, , ,override
oabstract
extern
.
Los métodos se clasifican de acuerdo con lo que, si hay algo, devuelven:
- Si
ref
está presente, el método es returns-by-ref y devuelve una referencia de variable, que es opcionalmente de solo lectura; - De lo contrario, si return_type es
void
, el método es returns-no-value y no devuelve un valor; - De lo contrario, el método devuelve por valor y devuelve un valor.
El return_type de una declaración de método returns-by-value o returns-no-value especifica el tipo del resultado, si existe, devuelto por el método . Solo un método returns-no-value puede incluir el partial
modificador (§15.6.9). Si la declaración incluye el async
modificador, return_type debe ser void
o el método devuelve por valor y el tipo de valor devuelto es un tipo de tarea (§15.15.1).
El ref_return_type de una declaración de método returns-by-ref especifica el tipo de la variable a la que hace referencia el variable_reference devuelto por el método .
Un método genérico es un método cuya declaración incluye un type_parameter_list. Especifica los parámetros de tipo para el método . Los type_parameter_constraints_clauseopcionales especifican las restricciones para los parámetros de tipo.
Un method_declaration genérico para una implementación explícita del miembro de interfaz no tendrá ningún type_parameter_constraints_clauses; la declaración hereda las restricciones de las restricciones del método de interfaz.
Del mismo modo, una declaración de método con el override
modificador no tendrá ningún type_parameter_constraints_clausey las restricciones de los parámetros de tipo del método se heredan del método virtual que se está reemplazando.
El member_name especifica el nombre del método. A menos que el método sea una implementación explícita de miembro de interfaz (§18.6.2), el member_name es simplemente un identificador.
Para una implementación explícita de miembro de interfaz, el member_name consta de un interface_type seguido de un ".
" e identificador. En este caso, la declaración no incluirá modificadores distintos de (posiblemente) extern
o async
.
El parameter_list opcional especifica los parámetros del método (§15.6.2).
El return_type o ref_return_type, y cada uno de los tipos a los que se hace referencia en el parameter_list de un método, será al menos tan accesible como el propio método (§7.5.5).
El method_body de un método returns-by-value o returns-no-value es un punto y coma, un cuerpo de bloque o un cuerpo de expresión. Un cuerpo de bloque consta de un bloque, que especifica las instrucciones que se van a ejecutar cuando se invoca el método . Un cuerpo de expresión consta de , seguido de =>
un null_conditional_invocation_expression o expresión, y un punto y coma, y denota una expresión única que se va a realizar cuando se invoca el método.
Para los métodos abstractos y extern, el method_body consiste simplemente en punto y coma. Para los métodos parciales, el method_body puede constar de un punto y coma, un cuerpo de bloque o un cuerpo de expresión. Para todos los demás métodos, el method_body es un cuerpo de bloque o un cuerpo de expresión.
Si el method_body consta de punto y coma, la declaración no incluirá el async
modificador .
El ref_method_body de un método returns-by-ref es un punto y coma, un cuerpo de bloque o un cuerpo de expresión. Un cuerpo de bloque consta de un bloque, que especifica las instrucciones que se van a ejecutar cuando se invoca el método . Un cuerpo de expresión consta de , seguido de =>
ref
, un variable_reference y un punto y coma, y denota un único variable_reference para evaluar cuándo se invoca el método.
Para los métodos abstractos y extern, el ref_method_body consiste simplemente en punto y coma; para todos los demás métodos, el ref_method_body es un cuerpo de bloque o un cuerpo de expresión.
El nombre, el número de parámetros de tipo y la lista de parámetros de un método definen la firma (§7.6) del método. En concreto, la firma de un método consta de su nombre, el número de sus parámetros de tipo y el número, parameter_mode_modifiers (§15.6.2.1) y los tipos de sus parámetros. El tipo de valor devuelto no forma parte de la firma de un método, ni son los nombres de los parámetros, los nombres de los parámetros de tipo o las restricciones. Cuando un tipo de parámetro hace referencia a un parámetro de tipo del método, se usa la posición ordinal del parámetro de tipo (no el nombre del parámetro de tipo) para la equivalencia de tipos.
El nombre de un método diferirá de los nombres de todos los demás no métodos declarados en la misma clase. Además, la firma de un método diferirá de las firmas de todos los demás métodos declarados en la misma clase y dos métodos declarados en la misma clase no tendrán firmas que difieren únicamente de in
, out
y ref
.
Los type_parameterdel método están en el ámbito en toda la method_declaration y se pueden usar para formar tipos a lo largo de ese ámbito en return_type o ref_return_type, method_body o ref_method_body, y type_parameter_constraints_clausepero no en atributos.
Todos los parámetros y parámetros de tipo tendrán nombres diferentes.
15.6.2 Parámetros del método
15.6.2.1 General
Los parámetros de un método, si los hay, se declaran mediante el parameter_list del método.
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
;
La lista de parámetros consta de uno o varios parámetros separados por comas de los que solo la última puede ser una parameter_array.
Un fixed_parameter consta de un conjunto opcional de atributos (§22); un modificador opcional in
, out
, ref
o this
; un tipo; un identificador; y un default_argument opcional. Cada fixed_parameter declara un parámetro del tipo especificado con el nombre especificado. El this
modificador designa el método como un método de extensión y solo se permite en el primer parámetro de un método estático en una clase estática no genérica y no anidada. Si el parámetro es un struct
tipo o un parámetro de tipo restringido a , struct
el this
modificador se puede combinar con el ref
modificador o in
, pero no con el out
modificador . Los métodos de extensión se describen más adelante en §15.6.10. Un fixed_parameter con un default_argument se conoce como parámetro opcional, mientras que un fixed_parameter sin un default_argument es un parámetro obligatorio. Un parámetro necesario no aparecerá después de un parámetro opcional en un parameter_list.
Un parámetro con un ref
modificador , out
o this
no puede tener un default_argument. Un parámetro de entrada puede tener un default_argument. La expresión de un default_argument será una de las siguientes:
- a constant_expression
- expresión del formulario
new S()
dondeS
es un tipo de valor - expresión del formulario
default(S)
dondeS
es un tipo de valor
La expresión se podrá convertir implícitamente mediante una identidad o una conversión que acepta valores NULL al tipo del parámetro .
Si los parámetros opcionales se producen en una declaración de método parcial de implementación (§15.6.9), una implementación explícita del miembro de interfaz (§18.6.2), una declaración de indexador de parámetro único (§15.9) o en una declaración de operador (§15.10.1), el compilador debe dar una advertencia, ya que estos miembros nunca se pueden invocar de una manera que permita omitir argumentos.
Un parameter_array consta de un conjunto opcional de atributos (§22), un params
modificador, un array_type y un identificador. Una matriz de parámetros declara un único parámetro del tipo de matriz especificado con el nombre especificado. El array_type de una matriz de parámetros será un tipo de matriz unidimensional (§17.2). En una invocación de método, una matriz de parámetros permite especificar un único argumento del tipo de matriz especificado, o permite especificar cero o más argumentos del tipo de elemento de matriz. Las matrices de parámetros se describen más adelante en §15.6.2.4.
Una parameter_array puede producirse después de un parámetro opcional, pero no puede tener un valor predeterminado: la omisión de argumentos de un parameter_array daría lugar a la creación de una matriz vacía.
Ejemplo: a continuación se muestran diferentes tipos de parámetros:
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 ) { }
En el parameter_list para , es un parámetro obligatorio,
d
es un parámetro de valor obligatorioref
,b
,s
o
y son parámetros de valor opcionales yt
a
es una matriz de parámetros.i
M
ejemplo final
Una declaración de método crea un espacio de declaración independiente (§7.3) para parámetros y parámetros de tipo. Los nombres se introducen en este espacio de declaración por la lista de parámetros de tipo y la lista de parámetros del método . El cuerpo del método, si existe, se considera anidado dentro de este espacio de declaración. Se trata de un error para que dos miembros de un espacio de declaración de método tengan el mismo nombre.
Una invocación de método (§12.8.10.2) crea una copia, específica de esa invocación, de los parámetros y variables locales del método, y la lista de argumentos de la invocación asigna valores o referencias de variables a los parámetros recién creados. Dentro del bloque de un método, sus identificadores pueden hacer referencia a los parámetros en simple_name expresiones (§12.8.4).
Existen los siguientes tipos de parámetros:
- Parámetros de valor (§15.6.2.2).
- Parámetros de entrada (§15.6.2.3.2).
- Parámetros de salida (§15.6.2.3.4).
- Parámetros de referencia (§15.6.2.3.3).
- Matrices de parámetros (§15.6.2.4).
Nota: Como se describe en §7.6, los
in
modificadores ,out
yref
forman parte de la firma de un método, pero elparams
modificador no lo es. nota final
15.6.2.2 Parámetros de valor
Un parámetro declarado sin modificadores es un parámetro de valor. Un parámetro value es una variable local que obtiene su valor inicial del argumento correspondiente proporcionado en la invocación del método.
Para conocer las reglas de asignación definitiva, consulte §9.2.5.
El argumento correspondiente de una invocación de método debe ser una expresión que se puede convertir implícitamente (§10.2) en el tipo de parámetro.
Se permite que un método asigne nuevos valores a un parámetro value. Estas asignaciones solo afectan a la ubicación de almacenamiento local representada por el parámetro value; no tienen ningún efecto en el argumento real proporcionado en la invocación del método.
15.6.2.3 Parámetros por referencia
15.6.2.3.1 General
Los parámetros de entrada, salida y referencia son parámetros por referencia. Un parámetro por referencia es una variable de referencia local (§9.7); el referencia inicial se obtiene del argumento correspondiente proporcionado en la invocación del método.
Nota: El referente de un parámetro por referencia se puede cambiar mediante el operador de asignación de referencia (
= ref
).
Cuando un parámetro es un parámetro por referencia, el argumento correspondiente de una invocación de método constará de la palabra clave correspondiente, in
, ref
o out
, seguida de un variable_reference (§9.5) del mismo tipo que el parámetro. Sin embargo, cuando el parámetro es un in
parámetro, el argumento puede ser una expresión para la que existe una conversión implícita (§10.2) desde esa expresión de argumento al tipo del parámetro correspondiente.
Los parámetros por referencia no se permiten en funciones declaradas como iteradores (§15.14) ni en funciones asincrónicas (§15.15).
En un método que toma varios parámetros por referencia, es posible que varios nombres representen la misma ubicación de almacenamiento.
15.6.2.3.2 Parámetros de entrada
Un parámetro declarado con un in
modificador es un parámetro de entrada. El argumento correspondiente a un parámetro de entrada es una variable existente en el punto de la invocación del método o una creada por la implementación (§12.6.2.3) en la invocación del método. Para conocer las reglas de asignación definitiva, consulte §9.2.8.
Se trata de un error en tiempo de compilación para modificar el valor de un parámetro de entrada.
Nota: El propósito principal de los parámetros de entrada es la eficacia. Cuando el tipo de un parámetro de método es una estructura grande (en términos de requisitos de memoria), resulta útil poder evitar copiar el valor completo del argumento al llamar al método . Los parámetros de entrada permiten a los métodos hacer referencia a valores existentes en la memoria, al tiempo que proporcionan protección contra cambios no deseados en esos valores. nota final
15.6.2.3.3 Parámetros de referencia
Un parámetro declarado con un ref
modificador es un parámetro de referencia. Para conocer las reglas de asignación definitiva, consulte §9.2.6.
Ejemplo: El ejemplo
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}"); } }
genera el resultado
i = 2, j = 1
Para la invocación de
Swap
enMain
,x
representai
yy
representaj
. Por lo tanto, la invocación tiene el efecto de intercambiar los valores dei
yj
.ejemplo final
Ejemplo: en el código siguiente
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 invocación de
F
enG
pasa una referencia as
para ya
b
. Por lo tanto, para esa invocación, los nombress
,a
yb
todos hacen referencia a la misma ubicación de almacenamiento, y todas las tres asignaciones modifican el campos
de instancia .ejemplo final
Para un tipo, dentro de un struct
método de instancia, descriptor de acceso de instancia (§12.2.1) o constructor de instancia con un inicializador de constructor, la this
palabra clave se comporta exactamente como un parámetro de referencia del tipo de estructura (§12.8.14).
15.6.2.3.4 Parámetros de salida
Un parámetro declarado con un out
modificador es un parámetro de salida. Para conocer las reglas de asignación definitiva, consulte §9.2.7.
Un método declarado como método parcial (§15.6.9) no tendrá parámetros de salida.
Nota: Los parámetros de salida se suelen usar en métodos que generan varios valores devueltos. nota final
Ejemplo:
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); } }
En el ejemplo se genera la salida:
c:\Windows\System\ hello.txt
Tenga en cuenta que las
dir
variables yname
se pueden anular la asignación antes de pasarlas aSplitPath
y que se consideran asignadas definitivamente después de la llamada.ejemplo final
15.6.2.4 Matrices de parámetros
Un parámetro declarado con un params
modificador es una matriz de parámetros. Si una lista de parámetros incluye una matriz de parámetros, será el último parámetro de la lista y será de un tipo de matriz unidimensional.
Ejemplo: los tipos
string[]
ystring[][]
se pueden usar como el tipo de una matriz de parámetros, pero el tipostring[,]
no puede. ejemplo final
Nota: No es posible combinar el
params
modificador con los modificadoresin
,out
oref
. nota final
Una matriz de parámetros permite especificar argumentos de una de estas dos maneras en una invocación de método:
- El argumento proporcionado para una matriz de parámetros puede ser una expresión única que se puede convertir implícitamente (§10.2) en el tipo de matriz de parámetros. En este caso, la matriz de parámetros actúa exactamente como un parámetro de valor.
- Como alternativa, la invocación puede especificar cero o más argumentos para la matriz de parámetros, donde cada argumento es una expresión que se puede convertir implícitamente (§10.2) en el tipo de elemento de la matriz de parámetros. En este caso, la invocación crea una instancia del tipo de matriz de parámetros con una longitud correspondiente al número de argumentos, inicializa los elementos de la instancia de matriz con los valores de argumento especificados y usa la instancia de matriz recién creada como argumento real.
Excepto para permitir un número variable de argumentos en una invocación, una matriz de parámetros es exactamente equivalente a un parámetro de valor (§15.6.2.2) del mismo tipo.
Ejemplo: El ejemplo
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(); } }
genera el resultado
Array contains 3 elements: 1 2 3 Array contains 4 elements: 10 20 30 40 Array contains 0 elements:
La primera invocación de
F
simplemente pasa la matrizarr
como parámetro de valor. La segunda invocación de F crea automáticamente un cuatro elementosint[]
con los valores de elemento especificados y pasa esa instancia de matriz como parámetro de valor. Del mismo modo, la tercera invocación deF
crea un elementoint[]
cero y pasa esa instancia como parámetro de valor. Las invocaciones segunda y tercera son exactamente equivalentes a escribir:F(new int[] {10, 20, 30, 40}); F(new int[] {});
ejemplo final
Al realizar la resolución de sobrecarga, un método con una matriz de parámetros puede ser aplicable, ya sea en su forma normal o en su forma expandida (§12.6.4.2). La forma expandida de un método solo está disponible si la forma normal del método no es aplicable y solo si un método aplicable con la misma firma que el formulario expandido aún no está declarado en el mismo tipo.
Ejemplo: El ejemplo
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); } }
genera el resultado
F() F(object[]) F(object,object) F(object[]) F(object[])
En el ejemplo, dos de las formas expandidas posibles del método con una matriz de parámetros ya se incluyen en la clase como métodos normales. Por lo tanto, estos formularios expandidos no se consideran al realizar la resolución de sobrecargas y, por tanto, las invocaciones de primer y tercer método seleccionan los métodos normales. Cuando una clase declara un método con una matriz de parámetros, no es raro incluir también algunos de los formularios expandidos como métodos normales. Al hacerlo, es posible evitar la asignación de una instancia de matriz que se produce cuando se invoca una forma expandida de un método con una matriz de parámetros.
ejemplo final
Una matriz es un tipo de referencia, por lo que el valor pasado para una matriz de parámetros puede ser
null
.Ejemplo: Ejemplo:
class Test { static void F(params string[] array) => Console.WriteLine(array == null); static void Main() { F(null); F((string) null); } }
genera el resultado:
True False
La segunda invocación genera
False
, ya que es equivalente aF(new string[] { null })
y pasa una matriz que contiene una sola referencia nula.ejemplo final
Cuando el tipo de una matriz de parámetros es object[]
, surge una posible ambigüedad entre la forma normal del método y el formulario expandido para un único object
parámetro. El motivo de la ambigüedad es que un object[]
objeto se convierte implícitamente en el tipo object
. Sin embargo, la ambigüedad no presenta ningún problema, ya que se puede resolver insertando una conversión si es necesario.
Ejemplo: El ejemplo
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); } }
genera el resultado
System.Int32 System.String System.Double System.Object[] System.Object[] System.Int32 System.String System.Double
En la primera y última invocación de
F
, la forma normal deF
es aplicable porque existe una conversión implícita del tipo de argumento al tipo de parámetro (ambos son de tipoobject[]
). Por lo tanto, la resolución de sobrecarga selecciona la forma normal deF
y el argumento se pasa como un parámetro de valor normal. En las invocaciones segunda y tercera, la forma normal deF
no es aplicable porque no existe ninguna conversión implícita del tipo de argumento al tipo de parámetro (el tipoobject
no se puede convertir implícitamente al tipoobject[]
). Sin embargo, la forma expandida de es aplicable, por lo que se selecciona mediante resolución deF
sobrecarga. Como resultado, la invocación crea un elementoobject[]
uno y el único elemento de la matriz se inicializa con el valor de argumento especificado (que es una referencia a ).object[]
ejemplo final
15.6.3 Métodos estáticos e de instancia
Cuando una declaración de método incluye un static
modificador, ese método se dice que es un método estático. Cuando no hay ningún static
modificador presente, se dice que el método es un método de instancia.
Un método estático no funciona en una instancia específica y es un error en tiempo de compilación al que hacer referencia this
en un método estático.
Un método de instancia funciona en una instancia determinada de una clase y se puede acceder a esa instancia como this
(§12.8.14).
Las diferencias entre los miembros estáticos e de instancia se describen más adelante en §15.3.8.
15.6.4 Métodos virtuales
Cuando una declaración de método de instancia incluye un modificador virtual, se dice que ese método es un método virtual. Cuando no hay ningún modificador virtual, se dice que el método es un método no virtual.
La implementación de un método no virtual es invariable: la implementación es la misma si el método se invoca en una instancia de la clase en la que se declara o una instancia de una clase derivada. Por el contrario, la implementación de un método virtual se puede sustituir por clases derivadas. El proceso de supersedir la implementación de un método virtual heredado se conoce como invalidar ese método (§15.6.5).
En una invocación de método virtual, el tipo en tiempo de ejecución de la instancia para la que se realiza esa invocación determina la implementación del método real que se va a invocar. En una invocación de método no virtual, el tipo en tiempo de compilación de la instancia es el factor determinante. En términos precisos, cuando se invoca un método denominado N
con una lista A
de argumentos en una instancia con un tipo en tiempo de compilación y un tipo R
C
en tiempo de ejecución (donde R
es C
o una clase derivada de C
), la invocación se procesa de la siguiente manera:
- En tiempo de enlace, la resolución de sobrecarga se aplica a , y , para seleccionar un método
M
específico del conjunto de métodos declarados en y heredados porC
.A
N
C
Esto se describe en §12.8.10.2. - A continuación, en tiempo de ejecución:
- Si
M
es un método no virtual,M
se invoca. - De lo contrario,
M
es un método virtual y se invoca la implementación más derivada deM
con respecto aR
.
- Si
Para cada método virtual declarado en o heredado por una clase, existe una implementación más derivada del método con respecto a esa clase. La implementación más derivada de un método M
virtual con respecto a una clase R
se determina de la siguiente manera:
- Si
R
contiene la declaración virtual de introducción deM
, esta es la implementación más derivada deM
con respecto aR
. - De lo contrario, si
R
contiene una invalidación deM
, esta es la implementación más derivada deM
con respecto aR
. - De lo contrario, la implementación más derivada de
M
con respecto aR
es la misma que la implementación más derivada deM
con respecto a la clase base directa deR
.
Ejemplo: en el ejemplo siguiente se muestran las diferencias entre los métodos virtuales y no virtuales:
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(); } }
En el ejemplo,
A
presenta un métodoF
no virtual y un métodoG
virtual . La claseB
presenta un nuevo métodoF
que no es virtual , ocultando así el heredado y también invalida el métodoG
heredadoF
. En el ejemplo se genera la salida:A.F B.F B.G B.G
Observe que la instrucción
a.G()
invocaB.G
, noA.G
. Esto se debe a que el tipo en tiempo de ejecución de la instancia (que esB
), no el tipo en tiempo de compilación de la instancia (que esA
), determina la implementación del método real que se va a invocar.ejemplo final
Dado que los métodos pueden ocultar métodos heredados, es posible que una clase contenga varios métodos virtuales con la misma firma. Esto no presenta un problema de ambigüedad, ya que todos los métodos más derivados están ocultos.
Ejemplo: en el código siguiente
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(); } }
las
C
clases yD
contienen dos métodos virtuales con la misma firma: la introducida porA
y la introducida porC
. El método introducido ocultaC
el método heredado deA
. Por lo tanto, la declaración de invalidación deD
invalida el método introducido porC
y no es posibleD
invalidar el método introducido porA
. En el ejemplo se genera la salida:B.F B.F D.F D.F
Tenga en cuenta que es posible invocar el método virtual oculto accediendo a una instancia de a través de
D
un tipo menos derivado en el que el método no está oculto.ejemplo final
15.6.5 Métodos de invalidación
Cuando una declaración de método de instancia incluye un override
modificador, se dice que el método es un método de invalidación. Un método de invalidación invalida un método virtual heredado con la misma firma. Mientras que una declaración de método virtual introduce un nuevo método, una declaración de método de invalidación especializa un método virtual heredado existente proporcionando una nueva implementación de ese método.
El método invalidado por una declaración de invalidación se conoce como método base invalidado Para un método M
de invalidación declarado en una clase C
, el método base invalidado se determina examinando cada clase base de C
, empezando por la clase base directa de C
y continuando con cada clase base directa sucesiva, hasta que en un tipo de clase base determinado se encuentra al menos un método accesible que tiene la misma firma que M
después de la sustitución de argumentos de tipo. Para buscar el método base invalidado, se considera accesible un método si es , si es public
, si es , si es protected
protected internal
, o si es internal
o private protected
y se declara en el mismo programa que C
.
Se produce un error en tiempo de compilación a menos que se cumplan todas las siguientes condiciones para una declaración de invalidación:
- Un método base invalidado se puede encontrar como se describió anteriormente.
- Hay exactamente uno de estos métodos base invalidados. Esta restricción solo tiene efecto si el tipo de clase base es un tipo construido donde la sustitución de argumentos de tipo hace que la firma de dos métodos sea la misma.
- El método base invalidado es un método virtual, abstracto o de invalidación. En otras palabras, el método base invalidado no puede ser estático ni no virtual.
- El método base invalidado no es un método sellado.
- Hay una conversión de identidad entre el tipo de valor devuelto del método base invalidado y el método override.
- La declaración de invalidación y el método base invalidado tienen la misma accesibilidad declarada. Es decir, una declaración de invalidación no puede cambiar la accesibilidad del método virtual. Sin embargo, si el método base invalidado está protegido internamente y se declara en un ensamblado diferente al ensamblado que contiene la declaración de invalidación, se protegerá la accesibilidad declarada de la declaración de invalidación.
- La declaración de invalidación no especifica ningún type_parameter_constraints_clauses. En su lugar, las restricciones se heredan del método base invalidado. Las restricciones que son parámetros de tipo en el método invalidado pueden reemplazarse por argumentos de tipo en la restricción heredada. Esto puede provocar restricciones que no son válidas cuando se especifican explícitamente, como tipos de valor o tipos sellados.
Ejemplo: a continuación se muestra cómo funcionan las reglas de invalidación para clases genéricas:
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> }
ejemplo final
Una declaración de invalidación puede tener acceso al método base invalidado mediante un base_access (§12.8.15).
Ejemplo: en el código siguiente
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()
invocación deB
invoca el método PrintFields declarado enA
. Un base_access deshabilita el mecanismo de invocación virtual y simplemente trata el método base como un método que novirtual
es . Si se hubiera escrito la invocaciónB
, invocaría recursivamente elPrintFields
método declarado enB
, no el declarado enA
, ya quePrintFields
es virtual y el tipo en tiempo de ejecución de((A)this)
esB
.((A)this).PrintFields()
ejemplo final
Solo al incluir un override
modificador puede invalidar otro método. En todos los demás casos, un método con la misma firma que un método heredado simplemente oculta el método heredado.
Ejemplo: en el código siguiente
class A { public virtual void F() {} } class B : A { public virtual void F() {} // Warning, hiding inherited F() }
El
F
método de noB
incluye unoverride
modificador y, por tanto, no invalida elF
método enA
. En su lugar, elF
método deB
oculta el método enA
y se notifica una advertencia porque la declaración no incluye un nuevo modificador.ejemplo final
Ejemplo: en el código siguiente
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 }
el
F
método deB
oculta el método virtualF
heredado deA
. Puesto que el nuevoF
enB
tiene acceso privado, su ámbito solo incluye el cuerpo de clase deB
y no se extiende aC
. Por lo tanto, se permite que la declaración deF
enC
invalide elF
heredado deA
.ejemplo final
15.6.6 Métodos sellados
Cuando una declaración de método de instancia incluye un sealed
modificador, ese método se dice que es un método sellado. Un método sellado invalida un método virtual heredado con la misma firma. Un método sellado también se marcará con el override
modificador . El uso del sealed
modificador impide que una clase derivada invalide aún más el método.
Ejemplo: El ejemplo
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 clase
B
proporciona dos métodos de invalidación: unF
método que tiene elsealed
modificador y unG
método que no lo hace.B
El uso delsealed
modificador impideC
que se invalideF
aún más .ejemplo final
15.6.7 Métodos abstractos
Cuando una declaración de método de instancia incluye un abstract
modificador, ese método se dice que es un método abstracto. Aunque un método abstracto también es implícitamente un método virtual, no puede tener el modificador virtual
.
Una declaración de método abstracto introduce un nuevo método virtual, pero no proporciona una implementación de ese método. En su lugar, las clases derivadas no abstractas son necesarias para proporcionar su propia implementación reemplazando ese método. Dado que un método abstracto no proporciona ninguna implementación real, el cuerpo del método de un método abstracto simplemente consta de un punto y coma.
Las declaraciones de método abstracto solo se permiten en clases abstractas (§15.2.2.2).
Ejemplo: en el código siguiente
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
clase define la noción abstracta de un objeto de forma geométrica que puede pintarse a sí mismo. ElPaint
método es abstracto porque no hay ninguna implementación predeterminada significativa. LasEllipse
clases yBox
son implementaciones concretasShape
. Dado que estas clases no son abstractas, son necesarias para invalidar elPaint
método y proporcionar una implementación real.ejemplo final
Es un error en tiempo de compilación para que un base_access (§12.8.15) haga referencia a un método abstracto.
Ejemplo: en el código siguiente
abstract class A { public abstract void F(); } class B : A { // Error, base.F is abstract public override void F() => base.F(); }
Se notifica un error en tiempo de compilación para la
base.F()
invocación porque hace referencia a un método abstracto.ejemplo final
Se permite una declaración de método abstracto para invalidar un método virtual. Esto permite a una clase abstracta forzar la nueva implementación del método en clases derivadas y hace que la implementación original del método no esté disponible.
Ejemplo: en el código siguiente
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 clase
A
declara un método virtual, la claseB
invalida este método con un método abstracto y la claseC
invalida el método abstracto para proporcionar su propia implementación.ejemplo final
15.6.8 Métodos externos
Cuando una declaración de método incluye un extern
modificador, se dice que el método es un método externo. Los métodos externos se implementan externamente, normalmente mediante un lenguaje distinto de C#. Dado que una declaración de método externo no proporciona ninguna implementación real, el cuerpo del método de un método externo simplemente consta de un punto y coma. Un método externo no será genérico.
El mecanismo por el que se logra la vinculación a un método externo es definido por la implementación.
Ejemplo: en el ejemplo siguiente se muestra el uso del
extern
modificador y elDllImport
atributo :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); }
ejemplo final
15.6.9 Métodos parciales
Cuando una declaración de método incluye un partial
modificador, se dice que ese método es un método parcial. Los métodos parciales solo se pueden declarar como miembros de tipos parciales (§15.2.7) y están sujetos a una serie de restricciones.
Los métodos parciales se pueden definir en una parte de una declaración de tipo e implementarse en otro. La implementación es opcional; si ninguna parte implementa el método parcial, la declaración de método parcial y todas las llamadas a ella se quitan de la declaración de tipo resultante de la combinación de las partes.
Los métodos parciales no definirán modificadores de acceso; son implícitamente privados. Su tipo de valor devuelto será void
y sus parámetros no serán parámetros de salida. El identificador parcial se reconoce como una palabra clave contextual (§6.4.4) en una declaración de método solo si aparece inmediatamente antes de la void
palabra clave . Un método parcial no puede implementar explícitamente métodos de interfaz.
Hay dos tipos de declaraciones de método parcial: si el cuerpo de la declaración del método es un punto y coma, se dice que la declaración es una declaración de método parcial que define. Si el cuerpo es distinto de un punto y coma, se dice que la declaración es una declaración de método parcial de implementación. En las partes de una declaración de tipo, solo puede haber una declaración de método parcial que defina con una firma determinada y solo puede haber una declaración de método parcial de implementación con una firma determinada. Si se proporciona una declaración de método parcial de implementación, existirá una declaración de método parcial correspondiente y las declaraciones coincidirán con las especificadas en lo siguiente:
- Las declaraciones tendrán los mismos modificadores (aunque no necesariamente en el mismo orden), el nombre del método, el número de parámetros de tipo y el número de parámetros.
- Los parámetros correspondientes de las declaraciones tendrán los mismos modificadores (aunque no necesariamente en el mismo orden) y los mismos tipos, o tipos convertibles de identidad (diferencias de módulo en los nombres de parámetro de tipo).
- Los parámetros de tipo correspondientes en las declaraciones tendrán las mismas restricciones (diferencias de módulo en los nombres de parámetro de tipo).
Una declaración de método parcial de implementación puede aparecer en la misma parte que la declaración de método parcial de definición correspondiente.
Solo un método parcial que define participa en la resolución de sobrecargas. Por lo tanto, si se proporciona o no una declaración de implementación, las expresiones de invocación pueden resolverse en invocaciones del método parcial. Dado que un método parcial siempre devuelve void
, estas expresiones de invocación siempre serán instrucciones expression. Además, dado que un método parcial es implícitamente private
, estas instrucciones siempre se producirán dentro de una de las partes de la declaración de tipo en la que se declara el método parcial.
Nota: La definición de la coincidencia de definiciones e implementación de declaraciones de método parcial no requiere que los nombres de parámetro coincidan. Esto puede producir un comportamiento sorprendente, aunque bien definido, cuando se usan argumentos con nombre (§12.6.2.1). Por ejemplo, dada la declaración de método parcial de definición para
M
en un archivo y la declaración de método parcial de implementación en otro archivo:// 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) {} }
no es válido porque la invocación usa el nombre del argumento de la implementación y no la declaración de método parcial de definición.
nota final
Si ninguna parte de una declaración de tipo parcial contiene una declaración de implementación para un método parcial determinado, cualquier instrucción de expresión que invoque simplemente se quita de la declaración de tipo combinado. Por lo tanto, la expresión de invocación, incluidas las subexpresiones, no tiene ningún efecto en tiempo de ejecución. El propio método parcial también se quita y no será miembro de la declaración de tipo combinado.
Si existe una declaración de implementación para un método parcial determinado, se conservan las invocaciones de los métodos parciales. El método parcial da lugar a una declaración de método similar a la declaración de método parcial de implementación, excepto para lo siguiente:
El
partial
modificador no está incluido.Los atributos de la declaración del método resultante son los atributos combinados de la definición y la declaración de método parcial de implementación en orden no especificado. No se quitan los duplicados.
Los atributos de los parámetros de la declaración del método resultante son los atributos combinados de los parámetros correspondientes de la definición y la declaración de método parcial de implementación en orden no especificado. No se quitan los duplicados.
Si se proporciona una declaración de definición pero no una declaración de implementación para un método M
parcial, se aplican las restricciones siguientes:
Se trata de un error en tiempo de compilación para crear un delegado a partir de
M
(§12.8.17.6).Es un error en tiempo de compilación al que se hace referencia
M
dentro de una función anónima que se convierte en un tipo de árbol de expresión (§8.6).Las expresiones que se producen como parte de una invocación de
M
no afectan al estado de asignación definitiva (§9.4), lo que puede provocar errores en tiempo de compilación.M
no puede ser el punto de entrada de una aplicación (§7.1).
Los métodos parciales son útiles para permitir que una parte de una declaración de tipo personalice el comportamiento de otra parte, por ejemplo, uno generado por una herramienta. Tenga en cuenta la siguiente declaración de clase parcial:
partial class Customer
{
string name;
public string Name
{
get => name;
set
{
OnNameChanging(value);
name = value;
OnNameChanged();
}
}
partial void OnNameChanging(string newName);
partial void OnNameChanged();
}
Si esta clase se compila sin ninguna otra parte, se quitarán las declaraciones de método parcial de definición y sus invocaciones, y la declaración de clase combinada resultante será equivalente a lo siguiente:
class Customer
{
string name;
public string Name
{
get => name;
set => name = value;
}
}
Supongamos que se proporciona otra parte, sin embargo, que proporciona declaraciones de implementación de los métodos parciales:
partial class Customer
{
partial void OnNameChanging(string newName) =>
Console.WriteLine($"Changing {name} to {newName}");
partial void OnNameChanged() =>
Console.WriteLine($"Changed to {name}");
}
A continuación, la declaración de clase combinada resultante será equivalente a lo siguiente:
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}");
}
Métodos de extensión 15.6.10
Cuando el primer parámetro de un método incluye el this
modificador , ese método se dice que es un método de extensión. Los métodos de extensión solo se declararán en clases estáticas no genéricas y no anidadas. El primer parámetro de un método de extensión está restringido, como se indica a continuación:
- Solo puede ser un parámetro de entrada si tiene un tipo de valor.
- Solo puede ser un parámetro de referencia si tiene un tipo de valor o tiene un tipo genérico restringido a struct.
- No será un tipo de puntero.
Ejemplo: a continuación se muestra un ejemplo de una clase estática que declara dos métodos de extensión:
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; } }
ejemplo final
Un método de extensión es un método estático normal. Además, donde su clase estática envolvente está en el ámbito, se puede invocar un método de extensión mediante la sintaxis de invocación del método de instancia (§12.8.10.3), mediante la expresión receptora como primer argumento.
Ejemplo: El siguiente programa usa los métodos de extensión declarados anteriormente:
static class Program { static void Main() { string[] strings = { "1", "22", "333", "4444" }; foreach (string s in strings.Slice(1, 2)) { Console.WriteLine(s.ToInt32()); } } }
El
Slice
método está disponible enstring[]
y elToInt32
método está disponible enstring
, porque se han declarado como métodos de extensión. El significado del programa es el mismo que el siguiente, mediante llamadas de método estático normal: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)); } } }
ejemplo final
15.6.11 Cuerpo del método
El cuerpo del método de una declaración de método consta de un cuerpo de bloque, un cuerpo de expresión o un punto y coma.
Las declaraciones de método abstracto y externo no proporcionan una implementación de método, por lo que sus cuerpos de método simplemente constan de un punto y coma. Para cualquier otro método, el cuerpo del método es un bloque (§13.3) que contiene las instrucciones que se van a ejecutar cuando se invoca ese método.
El tipo de valor devuelto efectivo de un método es si el tipo de valor devuelto es void
void
, o si el método es asincrónico y el tipo de valor devuelto es «TaskType»
(§15.15.1). De lo contrario, el tipo de valor devuelto efectivo de un método no asincrónico es su tipo de valor devuelto y el tipo de valor devuelto efectivo de un método asincrónico con tipo «TaskType»<T>
de valor devuelto (§15.15.1) es T
.
Cuando el tipo de valor devuelto efectivo de un método es void
y el método tiene un cuerpo de bloque, return
las instrucciones (§13.10.5) del bloque no especificarán una expresión. Si la ejecución del bloque de un método void se completa normalmente (es decir, el control fluye fuera del final del cuerpo del método), ese método simplemente vuelve a su llamador.
Cuando el tipo de valor devuelto efectivo de un método es void
y el método tiene un cuerpo de expresión, la expresión E
será un statement_expression y el cuerpo es exactamente equivalente a un cuerpo de bloque del formulario { E; }
.
Para un método return-by-value (§15.6.1), cada instrucción return del cuerpo del método especificará una expresión que se puede convertir implícitamente en el tipo de valor devuelto efectivo.
Para un método return-by-ref (§15.6.1), cada instrucción return del cuerpo del método especificará una expresión cuyo tipo es el del tipo de valor devuelto efectivo y tiene un contexto ref-safe-context de llamador-context (§9.7.2).
Para los métodos returns-by-value y returns-by-ref, el punto de conexión del cuerpo del método no será accesible. En otras palabras, no se permite que el control fluya fuera del final del cuerpo del método.
Ejemplo: en el código siguiente
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; }
El método que devuelve
F
valores da como resultado un error en tiempo de compilación porque el control puede fluir fuera del final del cuerpo del método. LosG
métodos yH
son correctos porque todas las rutas de acceso de ejecución posibles terminan en una instrucción return que especifica un valor devuelto. ElI
método es correcto, porque su cuerpo es equivalente a un bloque con una sola instrucción return en él.ejemplo final
15.7 Propiedades
15.7.1 General
Una propiedad es un miembro que proporciona acceso a una característica de un objeto o una clase. Algunos ejemplos de propiedades incluyen la longitud de una cadena, el tamaño de una fuente, el título de una ventana y el nombre de un cliente. Las propiedades son una extensión natural de campos: ambos se denominan miembros con tipos asociados y la sintaxis para acceder a campos y propiedades es la misma. Sin embargo, a diferencia de los campos, las propiedades no denotan ubicaciones de almacenamiento. Las propiedades tienen descriptores de acceso que especifican las instrucciones que se ejecutan cuando se leen o escriben sus valores. Por lo tanto, las propiedades proporcionan un mecanismo para asociar acciones con la lectura y escritura de las características de un objeto o clase; además, permiten calcular estas características.
Las propiedades se declaran mediante 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) solo está disponible en código no seguro (§23).
Hay dos tipos de property_declaration:
- La primera declara una propiedad no con valores ref. Su valor tiene tipo de tipo. Este tipo de propiedad puede ser legible o grabable.
- El segundo declara una propiedad con valores ref. Su valor es un variable_reference (§9.5), que puede ser
readonly
, a una variable de tipo . Este tipo de propiedad solo es legible.
Un property_declaration puede incluir un conjunto de atributos (§22) y cualquiera de los tipos permitidos de accesibilidad declarada (§15.3.6), (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) y extern
(§15.6.8) modificadores.
Las declaraciones de propiedad están sujetas a las mismas reglas que las declaraciones de método (§15.6) con respecto a combinaciones válidas de modificadores.
El member_name (§15.6.1) especifica el nombre de la propiedad. A menos que la propiedad sea una implementación explícita de miembro de interfaz, el member_name es simplemente un identificador. Para una implementación explícita de miembro de interfaz (§18.6.2), el member_name consta de un interface_type seguido de ".
" y un identificador.
El tipo de una propiedad debe ser al menos tan accesible como la propiedad en sí (§7.5.5).
Un property_body puede constar de un cuerpo de instrucción o de un cuerpo de expresión. En un cuerpo de instrucción, accessor_declarations, que se incluirá en tokens "" y "{
}
", declare los descriptores de acceso (§15.7.3) de la propiedad. Los descriptores de acceso especifican las instrucciones ejecutables asociadas a la lectura y escritura de la propiedad .
En un property_body un cuerpo de expresión que consta de seguido de =>
una expresión E
y un punto y coma es exactamente equivalente al cuerpo { get { return E; } }
de la instrucción y, por tanto, solo se puede usar para especificar propiedades de solo lectura en las que el resultado del descriptor de acceso get recibe una sola expresión.
Una property_initializer solo se puede dar para una propiedad implementada automáticamente (§15.7.4) y provoca la inicialización del campo subyacente de estas propiedades con el valor proporcionado por la expresión.
Un ref_property_body puede constar de un cuerpo de instrucción o de un cuerpo de expresión. En un cuerpo de instrucción, un get_accessor_declaration declara el descriptor de acceso get (§15.7.3) de la propiedad . El descriptor de acceso especifica las instrucciones ejecutables asociadas con la lectura de la propiedad .
En un ref_property_body un cuerpo de expresión que consta de seguido ref
de =>
, un variable_reference V
y un punto y coma es exactamente equivalente al cuerpo { get { return ref V; } }
de la instrucción .
Nota: Aunque la sintaxis para acceder a una propiedad es la misma que para un campo, una propiedad no se clasifica como una variable. Por lo tanto, no es posible pasar una propiedad como argumento
in
,out
oref
a menos que la propiedad tenga valores ref y, por tanto, devuelva una referencia de variable (§9.7). nota final
Cuando una declaración de propiedad incluye un extern
modificador, se dice que la propiedad es una propiedad externa. Dado que una declaración de propiedad externa no proporciona ninguna implementación real, cada uno de los accessor_bodyde su accessor_declarations será un punto y coma.
15.7.2 Propiedades estáticas e instancias
Cuando una declaración de propiedad incluye un static
modificador, se dice que la propiedad es una propiedad estática. Cuando no hay ningún static
modificador presente, se dice que la propiedad es una propiedad de instancia.
Una propiedad estática no está asociada a una instancia específica y es un error en tiempo de compilación al que se hace referencia this
en los descriptores de acceso de una propiedad estática.
Una propiedad de instancia está asociada a una instancia determinada de una clase y se puede tener acceso a esa instancia como this
(§12.8.14) en los descriptores de acceso de esa propiedad.
Las diferencias entre los miembros estáticos e de instancia se describen más adelante en §15.3.8.
Descriptores de acceso 15.7.3
Nota: Esta cláusula se aplica a ambas propiedades (§15.7) e indizadores (§15.9). La cláusula se escribe en términos de propiedades, al leer para indizadores sustituyen indexadores/indexadores para property/properties y consultan la lista de diferencias entre las propiedades y los indexadores proporcionados en §15.9.2. nota final
El accessor_declarations de una propiedad especifica las instrucciones ejecutables asociadas a la escritura o lectura de esa propiedad.
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 ';'
| ';'
;
El accessor_declarations consta de un get_accessor_declaration, un set_accessor_declaration o ambos. Cada declaración de descriptor de acceso consta de atributos opcionales, un accessor_modifier opcional, el token get
o set
, seguido de un accessor_body.
Para una propiedad con valores ref, el ref_get_accessor_declaration consta de atributos opcionales, un accessor_modifier opcional, el token get
, seguido de un ref_accessor_body.
El uso de accessor_modifierse rige por las restricciones siguientes:
- Los accessor_modifier no se usarán en una interfaz ni en una implementación explícita del miembro de interfaz.
- Para una propiedad o indexador que no tiene ningún
override
modificador, solo se permite un accessor_modifier si la propiedad o el indexador tiene un descriptor de acceso get y set y, a continuación, solo se permite en uno de esos descriptores de acceso. - Para una propiedad o indexador que incluya un
override
modificador, un descriptor de acceso coincidirá con el accessor_modifier, si existe, del descriptor de acceso que se va a invalidar. - El accessor_modifier declarará una accesibilidad estrictamente más restrictiva que la accesibilidad declarada de la propiedad o el propio indexador. Para ser precisos:
- Si la propiedad o el indexador tiene una accesibilidad declarada de
public
, la accesibilidad declarada por accessor_modifier puede serprivate protected
,protected internal
,internal
,protected
oprivate
. - Si la propiedad o el indexador tiene una accesibilidad declarada de
protected internal
, la accesibilidad declarada por accessor_modifier puede serprivate protected
,protected private
,internal
,protected
oprivate
. - Si la propiedad o indexador tiene una accesibilidad declarada de
internal
oprotected
, la accesibilidad declarada por accessor_modifier seráprivate protected
oprivate
. - Si la propiedad o indexador tiene una accesibilidad declarada de
private protected
, la accesibilidad declarada por accessor_modifier seráprivate
. - Si la propiedad o el indexador tiene una accesibilidad declarada de
private
, no se puede usar ningún accessor_modifier .
- Si la propiedad o el indexador tiene una accesibilidad declarada de
Para abstract
las propiedades con valores no ref y extern
no ref, cualquier accessor_body para cada descriptor de acceso especificado es simplemente un punto y coma. Una propiedad no abstracta, no extern, pero no un indexador, también puede tener la accessor_body para todos los descriptores de acceso especificados como punto y coma, en cuyo caso es una propiedad implementada automáticamente (§15.7.4). Una propiedad implementada automáticamente tendrá al menos un descriptor de acceso get. Para los descriptores de acceso de cualquier otra propiedad no abstracta, no extern, la accessor_body es:
- bloque que especifica las instrucciones que se van a ejecutar cuando se invoca el descriptor de acceso correspondiente; o
- un cuerpo de expresión, que consta de seguido de
=>
una expresión y un punto y coma, y denota una expresión única que se va a ejecutar cuando se invoca el descriptor de acceso correspondiente.
Para abstract
las propiedades con valores ref y extern
ref, el ref_accessor_body es simplemente un punto y coma. Para el descriptor de acceso de cualquier otra propiedad no abstracta, no extern, el ref_accessor_body es:
- bloque que especifica las instrucciones que se van a ejecutar cuando se invoca el descriptor de acceso get; o
- un cuerpo de expresión, que consta de seguido
ref
de=>
, un variable_reference y un punto y coma. La referencia de variable se evalúa cuando se invoca el descriptor de acceso get.
Un descriptor de acceso get para una propiedad sin valores ref corresponde a un método sin parámetros con un valor devuelto del tipo de propiedad. Excepto como destino de una asignación, cuando se hace referencia a dicha propiedad en una expresión, se invoca su descriptor de acceso get para calcular el valor de la propiedad (§12.2.2).
El cuerpo de un descriptor de acceso get para una propiedad con valores no ref se ajustará a las reglas para los métodos que devuelven valores descritos en §15.6.11. En concreto, todas las return
instrucciones del cuerpo de un descriptor de acceso get especificarán una expresión que se puede convertir implícitamente en el tipo de propiedad. Además, no se podrá acceder al punto de conexión de un descriptor de acceso get.
Un descriptor de acceso get para una propiedad con valores ref corresponde a un método sin parámetros con un valor devuelto de un variable_reference a una variable del tipo de propiedad. Cuando se hace referencia a dicha propiedad en una expresión, se invoca su descriptor de acceso get para calcular el valor variable_reference de la propiedad. Esa referencia de variable, como cualquier otra, se usa para leer o, para variable_reference s, escribir la variable a la que se hace referencia según lo requiera el contexto.
Ejemplo: en el ejemplo siguiente se muestra una propiedad con valores ref como destino de una asignación:
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 } }
ejemplo final
El cuerpo de un descriptor de acceso get para una propiedad con valores ref se ajustará a las reglas para los métodos con valores ref descritos en §15.6.11.
Un descriptor de acceso set corresponde a un método con un único parámetro de valor del tipo de propiedad y un void
tipo de valor devuelto. El parámetro implícito de un descriptor de acceso set siempre se denomina value
. Cuando se hace referencia a una propiedad como destino de una asignación (§12.21), o como operando de ++
o –-
(§12.8.16, §12.9.6), se invoca el descriptor de acceso set con un argumento que proporciona el nuevo valor (§12.21.2). El cuerpo de un descriptor de acceso set se ajustará a las reglas de void
los métodos descritos en §15.6.11. En concreto, las instrucciones return del cuerpo del descriptor de acceso set no pueden especificar una expresión. Dado que un descriptor de acceso set tiene implícitamente un parámetro denominado value
, es un error en tiempo de compilación para una variable local o declaración constante en un descriptor de acceso set para tener ese nombre.
En función de la presencia o ausencia de los descriptores de acceso get y set, una propiedad se clasifica de la siguiente manera:
- Se dice que una propiedad que incluye un descriptor de acceso get y un descriptor de acceso set es una propiedad de lectura y escritura.
- Se dice que una propiedad que solo tiene un descriptor de acceso get es una propiedad de solo lectura. Es un error en tiempo de compilación para que una propiedad de solo lectura sea el destino de una asignación.
- Se dice que una propiedad que solo tiene un descriptor de acceso set es una propiedad de solo escritura. Excepto como destino de una asignación, es un error en tiempo de compilación para hacer referencia a una propiedad de solo escritura en una expresión.
Nota: Los operadores y operadores de prefijo y postfijo
++
y--
asignación compuesta no se pueden aplicar a las propiedades de solo escritura, ya que estos operadores leen el valor antiguo de su operando antes de escribir el nuevo. nota final
Ejemplo: en el código siguiente
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 } }
el
Button
control declara una propiedad públicaCaption
. El descriptor de acceso get de la propiedad Caption devuelve elstring
almacenado en el campo privadocaption
. El descriptor de acceso set comprueba si el nuevo valor es diferente del valor actual y, si es así, almacena el nuevo valor y vuelve a dibujar el control. Las propiedades suelen seguir el patrón mostrado anteriormente: el descriptor de acceso get simplemente devuelve un valor almacenado en unprivate
campo y el descriptor de acceso set modifica eseprivate
campo y, a continuación, realiza las acciones adicionales necesarias para actualizar completamente el estado del objeto. Dada laButton
clase anterior, a continuación se muestra un ejemplo de uso de laCaption
propiedad :Button okButton = new Button(); okButton.Caption = "OK"; // Invokes set accessor string s = okButton.Caption; // Invokes get accessor
Aquí se invoca el descriptor de acceso set mediante la asignación de un valor a la propiedad y el descriptor de acceso get se invoca haciendo referencia a la propiedad en una expresión.
ejemplo final
Los descriptores de acceso get y set de una propiedad no son miembros distintos y no es posible declarar los descriptores de acceso de una propiedad por separado.
Ejemplo: El ejemplo
class A { private string name; // Error, duplicate member name public string Name { get => name; } // Error, duplicate member name public string Name { set => name = value; } }
no declara una sola propiedad de lectura y escritura. En su lugar, declara dos propiedades con el mismo nombre, una de solo lectura y una sola escritura. Dado que dos miembros declarados en la misma clase no pueden tener el mismo nombre, el ejemplo hace que se produzca un error en tiempo de compilación.
ejemplo final
Cuando una clase derivada declara una propiedad por el mismo nombre que una propiedad heredada, la propiedad derivada oculta la propiedad heredada con respecto a la lectura y escritura.
Ejemplo: en el código siguiente
class A { public int P { set {...} } } class B : A { public new int P { get {...} } }
la
P
propiedad deB
oculta laP
propiedad enA
con respecto tanto a la lectura como a la escritura. Por lo tanto, en las instruccionesB b = new B(); b.P = 1; // Error, B.P is read-only ((A)b).P = 1; // Ok, reference to A.P
la asignación a
b.P
hace que se notifique un error en tiempo de compilación, ya que la propiedad de soloP
lectura ocultaB
la propiedad de soloP
escritura enA
. Sin embargo, tenga en cuenta que una conversión se puede usar para acceder a la propiedad ocultaP
.ejemplo final
A diferencia de los campos públicos, las propiedades proporcionan una separación entre el estado interno de un objeto y su interfaz pública.
Ejemplo: Considere el código siguiente, que usa una
Point
estructura para representar una ubicación: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; }
En este caso, la
Label
clase usa dosint
campos yx
y
, para almacenar su ubicación. La ubicación se expone públicamente como unaX
Y
propiedad y como unaLocation
propiedad de tipoPoint
. Si, en una versión futura deLabel
, resulta más conveniente almacenar la ubicación como internamentePoint
, el cambio se puede realizar sin afectar a la interfaz pública de la clase :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; }
Si
x
eny
su lugar hubiera sidopublic readonly
campos, habría sido imposible realizar este cambio en laLabel
clase .ejemplo final
Nota: Exponer el estado a través de propiedades no es necesariamente menos eficaz que exponer campos directamente. En concreto, cuando una propiedad no es virtual y contiene solo una pequeña cantidad de código, el entorno de ejecución podría reemplazar las llamadas a descriptores de acceso por el código real de los descriptores de acceso. Este proceso se conoce como inserción y hace que el acceso a propiedades sea tan eficaz como acceso a campos, pero conserva la mayor flexibilidad de las propiedades. nota final
Ejemplo: Dado que invocar un descriptor de acceso get es conceptualmente equivalente a leer el valor de un campo, se considera un estilo de programación incorrecto para que los descriptores de acceso get tengan efectos secundarios observables. En el ejemplo
class Counter { private int next; public int Next => next++; }
El valor de la
Next
propiedad depende del número de veces que se ha accedido a la propiedad anteriormente. Por lo tanto, el acceso a la propiedad produce un efecto secundario observable y la propiedad debe implementarse como un método en su lugar.La convención "sin efectos secundarios" para los descriptores de acceso get no significa que los descriptores de acceso get siempre deben escribirse simplemente para devolver valores almacenados en campos. De hecho, los descriptores de acceso get suelen calcular el valor de una propiedad accediendo a varios campos o invocando métodos. Sin embargo, un descriptor de acceso get diseñado correctamente no realiza ninguna acción que provoque cambios observables en el estado del objeto.
ejemplo final
Las propiedades se pueden usar para retrasar la inicialización de un recurso hasta el momento en que se hace referencia por primera vez.
Ejemplo:
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
clase contiene tres propiedades,In
,Out
yError
, que representan los dispositivos estándar de entrada, salida y error, respectivamente. Al exponer estos miembros como propiedades, laConsole
clase puede retrasar su inicialización hasta que se usen realmente. Por ejemplo, al hacer referencia primero a laOut
propiedad , como enConsole.Out.WriteLine("hello, world");
se crea el subyacente
TextWriter
para el dispositivo de salida. Sin embargo, si la aplicación no hace referencia a lasIn
propiedades yError
, no se crea ningún objeto para esos dispositivos.ejemplo final
15.7.4 Propiedades implementadas automáticamente
Una propiedad implementada automáticamente (o propiedad automática para short) es una propiedad no abstracta, no extern, sin valor ref con punto y coma accessor_body s. Las propiedades automáticas tendrán un descriptor de acceso get y, opcionalmente, pueden tener un descriptor de acceso set.
Cuando se especifica una propiedad como una propiedad implementada automáticamente, un campo de respaldo oculto está disponible automáticamente para la propiedad y los descriptores de acceso se implementan para leer y escribir en ese campo de respaldo. El campo de respaldo oculto no es accesible, solo se puede leer y escribir a través de los descriptores de acceso de propiedad implementados automáticamente, incluso dentro del tipo contenedor. Si la propiedad automática no tiene ningún descriptor de acceso set, el campo de respaldo se considera readonly
(§15.5.3). Al igual que un readonly
campo, también se puede asignar una propiedad automática de solo lectura en el cuerpo de un constructor de la clase envolvente. Esta asignación se asigna directamente al campo de respaldo de solo lectura de la propiedad .
Una propiedad automática puede tener opcionalmente un property_initializer, que se aplica directamente al campo de respaldo como un variable_initializer (§17.7).
Ejemplo:
public class Point { public int X { get; set; } // Automatically implemented public int Y { get; set; } // Automatically implemented }
es equivalente a la siguiente declaración:
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; } } }
ejemplo final
Ejemplo: en lo siguiente
public class ReadOnlyPoint { public int X { get; } public int Y { get; } public ReadOnlyPoint(int x, int y) { X = x; Y = y; } }
es equivalente a la siguiente declaración:
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; } }
Las asignaciones al campo de solo lectura son válidas, ya que se producen dentro del constructor.
ejemplo final
Aunque el campo de respaldo está oculto, ese campo puede tener atributos de destino de campo aplicados directamente a él a través del property_declaration de la propiedad implementada automáticamente (§15.7.1).
Ejemplo: el código siguiente
[Serializable] public class Foo { [field: NonSerialized] public string MySecret { get; set; } }
da como resultado el atributo
NonSerialized
de destino de campo que se aplica al campo de respaldo generado por el compilador, como si el código se hubiera escrito de la siguiente manera:[Serializable] public class Foo { [NonSerialized] private string _mySecretBackingField; public string MySecret { get { return _mySecretBackingField; } set { _mySecretBackingField = value; } } }
ejemplo final
15.7.5 Accesibilidad
Si un descriptor de acceso tiene un accessor_modifier, el dominio de accesibilidad (§7.5.3) del descriptor de acceso se determina mediante la accesibilidad declarada de la accessor_modifier. Si un descriptor de acceso no tiene un accessor_modifier, el dominio de accesibilidad del descriptor de acceso se determina a partir de la accesibilidad declarada de la propiedad o indexador.
La presencia de un accessor_modifier nunca afecta a la búsqueda de miembros (§12.5) o a la resolución de sobrecarga (§12.6.4). Los modificadores de la propiedad o indexador siempre determinan a qué propiedad o indizador está enlazado, independientemente del contexto del acceso.
Una vez seleccionada una propiedad no con valores ref concretos o un indexador con valores no ref, los dominios de accesibilidad de los descriptores de acceso específicos implicados se usan para determinar si ese uso es válido:
- Si el uso es como un valor (§12.2.2), el descriptor de acceso get existirá y será accesible.
- Si el uso es como destino de una asignación simple (§12.21.2), el descriptor de acceso set existirá y será accesible.
- Si el uso es como destino de la asignación compuesta (§12.21.4), o como destino de los
++
operadores o--
(§12.8.16, §12.9.6), los descriptores de acceso get y el descriptor de acceso set existirán y serán accesibles.
Ejemplo: en el ejemplo siguiente, la propiedad
A.Text
está oculta por la propiedadB.Text
, incluso en contextos en los que solo se llama al descriptor de acceso set. Por el contrario, la propiedadB.Count
no es accesible para la claseM
, por lo que la propiedadA.Count
accesible se usa en su lugar.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 } }
ejemplo final
Una vez seleccionada una propiedad con valores ref concretos o un indexador con valores ref; si el uso es como un valor, el destino de una asignación simple o el destino de una asignación compuesta; El dominio de accesibilidad del descriptor de acceso get implicado se usa para determinar si ese uso es válido.
Un descriptor de acceso que se usa para implementar una interfaz no tendrá un accessor_modifier. Si solo se usa un descriptor de acceso para implementar una interfaz, el otro descriptor de acceso se puede declarar con un accessor_modifier:
Ejemplo:
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 } }
ejemplo final
15.7.6 Descriptores de acceso virtuales, sellados, invalidaciones y abstractos
Nota: Esta cláusula se aplica a ambas propiedades (§15.7) e indizadores (§15.9). La cláusula se escribe en términos de propiedades, al leer para indizadores sustituyen indexadores/indexadores para property/properties y consultan la lista de diferencias entre las propiedades y los indexadores proporcionados en §15.9.2. nota final
Una declaración de propiedad virtual especifica que los descriptores de acceso de la propiedad son virtuales. El virtual
modificador se aplica a todos los descriptores de acceso no privados de una propiedad. Cuando un descriptor de acceso de una propiedad virtual tiene el private
accessor_modifier, el descriptor de acceso privado no es virtual implícitamente.
Una declaración de propiedad abstracta especifica que los descriptores de acceso de la propiedad son virtuales, pero no proporciona una implementación real de los descriptores de acceso. En su lugar, se requieren clases derivadas no abstractas para proporcionar su propia implementación para los descriptores de acceso reemplazando la propiedad . Dado que un descriptor de acceso para una declaración de propiedad abstracta no proporciona ninguna implementación real, su accessor_body simplemente consta de un punto y coma. Una propiedad abstracta no tendrá un private
descriptor de acceso.
Una declaración de propiedad que incluye los abstract
modificadores y override
especifica que la propiedad es abstracta e invalida una propiedad base. Los descriptores de acceso de dicha propiedad también son abstractos.
Las declaraciones de propiedades abstractas solo se permiten en clases abstractas (§15.2.2.2). Los descriptores de acceso de una propiedad virtual heredada se pueden invalidar en una clase derivada mediante la inclusión de una declaración de propiedad que especifica una override
directiva. Esto se conoce como una declaración de propiedad de invalidación. Una declaración de propiedad de invalidación no declara una nueva propiedad. En su lugar, simplemente especializa las implementaciones de los descriptores de acceso de una propiedad virtual existente.
La declaración de invalidación y la propiedad base invalidada son necesarias para tener la misma accesibilidad declarada. Es decir, una declaración de invalidación no cambiará la accesibilidad de la propiedad base. Sin embargo, si la propiedad base invalidada está protegida internamente y se declara en un ensamblado diferente al ensamblado que contiene la declaración de invalidación, se protegerá la accesibilidad declarada de la declaración de invalidación. Si la propiedad heredada tiene solo un descriptor de acceso único (es decir, si la propiedad heredada es de solo lectura o de solo escritura), la propiedad de invalidación solo incluirá ese descriptor de acceso. Si la propiedad heredada incluye ambos descriptores de acceso (es decir, si la propiedad heredada es de lectura y escritura), la propiedad de invalidación puede incluir un único descriptor de acceso o ambos descriptores de acceso. Habrá una conversión de identidad entre el tipo de invalidación y la propiedad heredada.
Una declaración de propiedad de invalidación puede incluir el sealed
modificador . El uso de este modificador impide que una clase derivada invalide aún más la propiedad . Los descriptores de acceso de una propiedad sellada también están sellados.
Excepto las diferencias en la sintaxis de declaración e invocación, los descriptores de acceso virtual, sellado, invalidado y abstracto se comportan exactamente como los métodos virtual, sellado, invalidación y abstracto. En concreto, las reglas descritas en §15.6.4, §15.6.5, §15.6.6 y §15.6.7 se aplican como si los descriptores de acceso fueran métodos de un formulario correspondiente:
- Un descriptor de acceso get corresponde a un método sin parámetros con un valor devuelto del tipo de propiedad y los mismos modificadores que la propiedad contenedora.
- Un descriptor de acceso set corresponde a un método con un único parámetro de valor del tipo de propiedad, un tipo de valor devuelto void y los mismos modificadores que la propiedad contenedora.
Ejemplo: en el código siguiente
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
es una propiedad de solo lectura virtual,Y
es una propiedad de lectura y escritura virtual yZ
es una propiedad abstracta de lectura y escritura. Dado queZ
es abstracta, la clase contenedora A también se declarará abstracta.A continuación se muestra una clase que deriva de
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; } }
Aquí, las declaraciones de
X
,Y
yZ
reemplazan las declaraciones de propiedad. Cada declaración de propiedad coincide exactamente con los modificadores de accesibilidad, el tipo y el nombre de la propiedad heredada correspondiente. Descriptor de acceso get deX
y el descriptor de acceso set deY
usar la palabra clave base para tener acceso a los descriptores de acceso heredados. La declaración de invalida ambos descriptores deZ
acceso abstractos; por lo tanto, no hay miembros de función pendientesabstract
enB
yB
se permite que sea una clase no abstracta.ejemplo final
Cuando una propiedad se declara como invalidación, los descriptores de acceso invalidados serán accesibles para el código de invalidación. Además, la accesibilidad declarada tanto de la propiedad como del indizador, y de los descriptores de acceso, coincidirá con la del miembro y los descriptores de acceso invalidados.
Ejemplo:
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 } }
ejemplo final
15.8 Eventos
15.8.1 General
Un evento es un miembro que permite que una clase u objeto proporcionen notificaciones. Los clientes pueden adjuntar código ejecutable para eventos proporcionando controladores de eventos.
Los eventos se declaran mediante 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) solo está disponible en código no seguro (§23).
Un event_declaration puede incluir un conjunto de atributos (§22) y cualquiera de los tipos permitidos de accesibilidad declarada (§15.3.6), (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) y extern
(§15.6.8) modificadores.
Las declaraciones de eventos están sujetas a las mismas reglas que las declaraciones de método (§15.6) con respecto a combinaciones válidas de modificadores.
El tipo de una declaración de evento será un delegate_type (§8.2.8) y que delegate_type serán al menos tan accesibles como el propio evento (§7.5.5).
Una declaración de evento puede incluir event_accessor_declarations. Sin embargo, si no es así, para eventos no extern, no abstractos, el compilador los proporcionará automáticamente (§15.8.2); para extern
los eventos, los descriptores de acceso se proporcionan externamente.
Una declaración de evento que omite event_accessor_declarations define uno o varios eventos, uno para cada uno de los variable_declarators. Los atributos y modificadores se aplican a todos los miembros declarados por este event_declaration.
Se trata de un error en tiempo de compilación para que un event_declaration incluya tanto el abstract
modificador como el event_accessor_declarations.
Cuando una declaración de evento incluye un extern
modificador, se dice que el evento es un evento externo. Dado que una declaración de evento externo no proporciona ninguna implementación real, se trata de un error para que incluya tanto el extern
modificador como event_accessor_declarations.
Se trata de un error en tiempo de compilación para un variable_declarator de una declaración de evento con un abstract
modificador o external
para incluir un variable_initializer.
Un evento se puede usar como operando izquierdo de los +=
operadores y -=
. Estos operadores se usan, respectivamente, para adjuntar controladores de eventos a o para quitar controladores de eventos de un evento, y los modificadores de acceso del control de eventos controlan los contextos en los que se permiten estas operaciones.
Las únicas operaciones que se permiten en un evento por código que está fuera del tipo en el que se declara ese evento, son +=
y -=
. Por lo tanto, aunque este código puede agregar y quitar controladores para un evento, no puede obtener ni modificar directamente la lista subyacente de controladores de eventos.
En una operación del formulario x += y
o , cuando x
es un evento, el resultado de la operación tiene el tipo void
(§12.21.5) (en lugar de tener el tipo de x
, con el valor de después de x
la asignación, como para otros +=
operadores y -=
definidos en tipos x –= y
de no eventos). Esto impide que el código externo examine indirectamente el delegado subyacente de un evento.
Ejemplo: en el ejemplo siguiente se muestra cómo se adjuntan los controladores de eventos a instancias de la
Button
clase :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 } }
Aquí, el
LoginDialog
constructor de instancia crea dosButton
instancias y adjunta controladores de eventos a losClick
eventos.ejemplo final
15.8.2 Eventos similares a campos
Dentro del texto del programa de la clase o estructura que contiene la declaración de un evento, se pueden usar determinados eventos como campos. Para su uso de esta manera, un evento no será abstracto ni extern, y no incluirá explícitamente event_accessor_declarations. Este evento se puede usar en cualquier contexto que permita un campo. El campo contiene un delegado (§20), que hace referencia a la lista de controladores de eventos que se han agregado al evento. Si no se ha agregado ningún controlador de eventos, el campo contiene null
.
Ejemplo: en el código siguiente
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
se usa como campo dentro de laButton
clase . Como se muestra en el ejemplo, el campo se puede examinar, modificar y usar en expresiones de invocación de delegado. ElOnClick
método de laButton
clase "genera" elClick
evento. La noción de generar un evento es equivalente exactamente a invocar el delegado representado por el evento; por lo tanto, no hay ninguna construcción especial de lenguaje para generar eventos. Tenga en cuenta que la invocación del delegado está precedida de una comprobación que garantiza que el delegado no sea NULL y que la comprobación se realice en una copia local para garantizar la seguridad de los subprocesos.Fuera de la declaración de la
Button
clase , elClick
miembro solo se puede usar en el lado izquierdo de los+=
operadores y–=
, como enb.Click += new EventHandler(...);
que anexa un delegado a la lista de invocación del
Click
evento yClick –= new EventHandler(...);
que quita un delegado de la lista de invocación del
Click
evento.ejemplo final
Al compilar un evento similar a un campo, el compilador crea automáticamente el almacenamiento para almacenar el delegado y crea descriptores de acceso para el evento que agregan o quitan controladores de eventos al campo delegado. Las operaciones de adición y eliminación son seguras para subprocesos y pueden realizarse (pero no necesarias) al mantener el bloqueo (§13.13) en el objeto contenedor para un evento de instancia o el System.Type
objeto (§12.8.18) para un evento estático.
Nota: Por lo tanto, una declaración de evento de instancia del formulario:
class X { public event D Ev; }
se compilará en algo 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 */ } } }
Dentro de la clase
X
, las referencias aEv
en el lado izquierdo de los+=
operadores y–=
hacen que se invoquen los descriptores de acceso add y remove. Todas las demás referencias aEv
se compilan para hacer referencia al campo__Ev
oculto en su lugar (§12.8.7). El nombre "__Ev
" es arbitrario; el campo oculto podría tener cualquier nombre o ningún nombre en absoluto.nota final
15.8.3 Descriptores de acceso de eventos
Nota: Las declaraciones de eventos normalmente omiten event_accessor_declarations, como en el
Button
ejemplo anterior. Por ejemplo, podrían incluirse si el costo de almacenamiento de un campo por evento no es aceptable. En tales casos, una clase puede incluir event_accessor_declarations y usar un mecanismo privado para almacenar la lista de controladores de eventos. nota final
El event_accessor_declarations de un evento especifica las instrucciones ejecutables asociadas a agregar y quitar controladores de eventos.
Las declaraciones de descriptor de acceso constan de un add_accessor_declaration y un remove_accessor_declaration. Cada declaración de descriptor de acceso consta de la adición o eliminación del token seguida de un bloque. El bloque asociado a un add_accessor_declaration especifica las instrucciones que se van a ejecutar cuando se agrega un controlador de eventos y el bloque asociado a un remove_accessor_declaration especifica las instrucciones que se van a ejecutar cuando se quita un controlador de eventos.
Cada add_accessor_declaration y remove_accessor_declaration corresponde a un método con un único parámetro de valor del tipo de evento y un void
tipo de valor devuelto. El parámetro implícito de un descriptor de acceso de eventos se denomina value
. Cuando se usa un evento en una asignación de eventos, se usa el descriptor de acceso de evento adecuado. En concreto, si el operador de asignación es +=
, se usa el descriptor de acceso add y, si el operador de asignación es –=
el descriptor de acceso remove. En cualquier caso, el operando derecho del operador de asignación se usa como argumento para el descriptor de acceso de eventos. El bloque de un add_accessor_declaration o un remove_accessor_declaration se ajustará a las reglas para void
los métodos descritos en §15.6.9. En concreto, return
las instrucciones de este bloque no pueden especificar una expresión.
Dado que un descriptor de acceso de eventos tiene implícitamente un parámetro denominado value
, es un error en tiempo de compilación para una variable local o constante declarada en un descriptor de acceso de eventos para tener ese nombre.
Ejemplo: en el código siguiente
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
clase implementa un mecanismo de almacenamiento interno para eventos. ElAddEventHandler
método asocia un valor delegado a una clave, elGetEventHandler
método devuelve el delegado asociado actualmente a una clave y elRemoveEventHandler
método quita un delegado como controlador de eventos para el evento especificado. Presumiblemente, el mecanismo de almacenamiento subyacente está diseñado de modo que no haya ningún costo para asociar un valor delegado NULO con una clave y, por tanto, los eventos no controlados no consumen almacenamiento.ejemplo final
15.8.4 Eventos estáticos e de instancia
Cuando una declaración de evento incluye un static
modificador, se dice que el evento es un evento estático. Cuando no hay ningún static
modificador presente, se dice que el evento es un evento de instancia.
Un evento estático no está asociado a una instancia específica y es un error en tiempo de compilación al que hacer referencia this
en los descriptores de acceso de un evento estático.
Se asocia un evento de instancia a una instancia determinada de una clase y se puede tener acceso a esta instancia como this
(§12.8.14) en los descriptores de acceso de ese evento.
Las diferencias entre los miembros estáticos e de instancia se describen más adelante en §15.3.8.
15.8.5 Descriptores de acceso virtuales, sellados, invalidaciones y abstractos
Una declaración de evento virtual especifica que los descriptores de acceso de ese evento son virtuales. El virtual
modificador se aplica a ambos descriptores de acceso de un evento.
Una declaración de evento abstracta especifica que los descriptores de acceso del evento son virtuales, pero no proporciona una implementación real de los descriptores de acceso. En su lugar, se requieren clases derivadas no abstractas para proporcionar su propia implementación para los descriptores de acceso invalidando el evento. Dado que un descriptor de acceso para una declaración de evento abstracto no proporciona ninguna implementación real, no proporcionará event_accessor_declarations.
Una declaración de evento que incluye los abstract
modificadores y override
especifica que el evento es abstracto e invalida un evento base. Los descriptores de acceso de este evento también son abstractos.
Las declaraciones de eventos abstractos solo se permiten en clases abstractas (§15.2.2.2).
Los descriptores de acceso de un evento virtual heredado se pueden invalidar en una clase derivada mediante la inclusión de una declaración de evento que especifica un override
modificador. Esto se conoce como una declaración de evento de invalidación. Una declaración de evento de invalidación no declara un nuevo evento. En su lugar, simplemente especializa las implementaciones de los descriptores de acceso de un evento virtual existente.
Una declaración de evento de invalidación especificará exactamente los mismos modificadores de accesibilidad y el mismo nombre que el evento invalidado, habrá una conversión de identidad entre el tipo del evento invalidado y el evento invalidado, y se especificarán los descriptores de acceso add y remove dentro de la declaración.
Una declaración de evento de invalidación puede incluir el sealed
modificador . El uso del this
modificador impide que una clase derivada invalide aún más el evento. Los descriptores de acceso de un evento sellado también están sellados.
Se trata de un error en tiempo de compilación para que una declaración de evento de invalidación incluya un new
modificador.
Excepto las diferencias en la sintaxis de declaración e invocación, los descriptores de acceso virtual, sellado, invalidado y abstracto se comportan exactamente como los métodos virtual, sellado, invalidación y abstracto. En concreto, las reglas descritas en §15.6.4, §15.6.5, §15.6.6 y §15.6.7 se aplican como si los descriptores de acceso fueran métodos de un formulario correspondiente. Cada descriptor de acceso corresponde a un método con un único parámetro de valor del tipo de evento, un void
tipo de valor devuelto y los mismos modificadores que el evento contenedor.
Indexadores 15.9
15.9.1 General
Un indexador es un miembro que permite que un objeto se indexe de la misma manera que una matriz. Los indexadores se declaran mediante 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) solo está disponible en código no seguro (§23).
Hay dos tipos de indexer_declaration:
- La primera declara un indizador sin valores ref. Su valor tiene tipo de tipo. Este tipo de indexador puede ser legible o grabable.
- El segundo declara un indexador con valores ref. Su valor es un variable_reference (§9.5), que puede ser
readonly
, a una variable de tipo . Este tipo de indexador solo es legible.
Un indexer_declaration puede incluir un conjunto de atributos (§22) y cualquiera de los tipos permitidos de accesibilidad declarada (§15.3.6), (new
§15.3.5), (§15.3.5), virtual
(§15 .6.4), override
(§15.6.5), sealed
(§15.6.6), abstract
(§15.6.7) y extern
(§15.6.8) modificadores.
Las declaraciones del indexador están sujetas a las mismas reglas que las declaraciones de método (§15.6) con respecto a combinaciones válidas de modificadores, con la única excepción de que el static
modificador no está permitido en una declaración de indexador.
El tipo de una declaración del indexador especifica el tipo de elemento del indexador introducido por la declaración.
Nota: Como los indexadores están diseñados para usarse en contextos similares a elementos de matriz, el tipo de elemento term tal como se define para una matriz también se usa con un indexador. nota final
A menos que el indexador sea una implementación explícita de miembro de interfaz, el tipo va seguido de la palabra clave this
. Para una implementación de miembro de interfaz explícita, el tipo va seguido de un interface_type, un ".
" y la palabra clave this
. A diferencia de otros miembros, los indexadores no tienen nombres definidos por el usuario.
El parameter_list especifica los parámetros del indexador. La lista de parámetros de un indexador corresponde a la de un método (§15.6.2), excepto que se especificará al menos un parámetro y que no se permiten los this
modificadores de parámetro , ref
y out
.
El tipo de indizador y cada uno de los tipos a los que se hace referencia en el parameter_list será al menos tan accesible como el propio indexador (§7.5.5).
Un indexer_body puede constar de un cuerpo de instrucción (§15.7.1) o de un cuerpo de expresión (§15.6.1). En un cuerpo de instrucción, accessor_declarations, que se incluirá en tokens "" y "{
}
", declare los descriptores de acceso (§15.7.3) del indexador. Los descriptores de acceso especifican las instrucciones ejecutables asociadas con la lectura y escritura de elementos del indexador.
En un indexer_body un cuerpo de expresión que consta de "=>
" seguido de una expresión E
y un punto y coma es exactamente equivalente al cuerpo { get { return E; } }
de la instrucción y, por tanto, solo se puede usar para especificar indizadores de solo lectura en los que el resultado del descriptor de acceso get recibe una sola expresión.
Un ref_indexer_body puede constar de un cuerpo de instrucción o de un cuerpo de expresión. En un cuerpo de instrucción, un get_accessor_declaration declara el descriptor de acceso get (§15.7.3) del indexador. El descriptor de acceso especifica las instrucciones ejecutables asociadas con la lectura del indexador.
En un ref_indexer_body un cuerpo de expresión que consta de seguido ref
de =>
, un variable_reference V
y un punto y coma es exactamente equivalente al cuerpo { get { return ref V; } }
de la instrucción .
Nota: Aunque la sintaxis para acceder a un elemento indexador es la misma que para un elemento de matriz, un elemento indexador no se clasifica como una variable. Por lo tanto, no es posible pasar un elemento indexador como argumento
in
,out
oref
a menos que el indexador tenga valores ref y, por tanto, devuelva una referencia (§9.7). nota final
El parameter_list de un indexador define la firma (§7.6) del indexador. En concreto, la firma de un indexador consta del número y los tipos de sus parámetros. El tipo de elemento y los nombres de los parámetros no forman parte de la firma de un indexador.
La firma de un indexador diferirá de las firmas de todos los demás indizadores declarados en la misma clase.
Cuando una declaración de indexador incluye un extern
modificador, se dice que el indexador es un indexador externo. Dado que una declaración de indexador externo no proporciona ninguna implementación real, cada uno de los accessor_bodyde su accessor_declarations será un punto y coma.
Ejemplo: En el ejemplo siguiente se declara una
BitArray
clase que implementa un indexador para acceder a los bits individuales de la matriz de bits.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); } } } }
Una instancia de la
BitArray
clase consume considerablemente menos memoria que una correspondientebool[]
(ya que cada valor del primero ocupa solo un bit en lugar del otrobyte
), pero permite las mismas operaciones que .bool[]
La siguiente
CountPrimes
clase usa unBitArray
y el algoritmo clásico "sieve" para calcular el número de primos entre 2 y un máximo determinado: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}"); } }
Tenga en cuenta que la sintaxis para acceder a los elementos de
BitArray
es exactamente la misma que para .bool[]
En el ejemplo siguiente se muestra una clase de cuadrícula 26×10 que tiene un indexador con dos parámetros. El primer parámetro es necesario para ser una letra mayúscula o minúscula en el rango A-Z, y la segunda debe ser un entero en el intervalo 0–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; } } }
ejemplo final
15.9.2 Diferencias de indexador y propiedad
Los indizadores y las propiedades son muy similares en concepto, pero difieren de las siguientes maneras:
- Una propiedad se identifica por su nombre, mientras que un indexador se identifica mediante su firma.
- Se accede a una propiedad a través de un simple_name (§12.8.4) o un member_access (§12.8.7), mientras que se accede a un elemento indexador a través de un element_access (§12.8.12.3).
- Una propiedad puede ser un miembro estático, mientras que un indexador siempre es un miembro de instancia.
- Un descriptor de acceso get de una propiedad corresponde a un método sin parámetros, mientras que un descriptor de acceso get de un indexador corresponde a un método con la misma lista de parámetros que el indexador.
- Un descriptor de acceso set de una propiedad corresponde a un método con un único parámetro denominado
value
, mientras que un descriptor de acceso set de un indexador corresponde a un método con la misma lista de parámetros que el indexador, además de un parámetro adicional denominadovalue
. - Es un error en tiempo de compilación para que un descriptor de acceso del indexador declare una variable local o una constante local con el mismo nombre que un parámetro de indexador.
- En una declaración de propiedad de invalidación, se obtiene acceso a la propiedad heredada mediante la sintaxis
base.P
, dondeP
es el nombre de propiedad. En una declaración de indexador de invalidación, se obtiene acceso al indexador heredado mediante la sintaxisbase[E]
, dondeE
es una lista separada por comas de expresiones. - No existe ningún concepto de "indexador implementado automáticamente". Es un error tener un indizador no abstracto y no externo con punto y coma accessor_bodys.
Además de estas diferencias, todas las reglas definidas en §15.7.3, §15.7.5 y §15.7.6 se aplican a los descriptores de acceso del indexador, así como a los descriptores de acceso de propiedad.
Esta sustitución de property/properties por indexador/indexadores al leer §15.7.3, §15.7.5 y §15.7.6 también se aplica a los términos definidos. En concreto, la propiedad read-write se convierte en indexador de solo lectura, la propiedad de solo lectura se convierte en indexador de solo lectura y la propiedad de solo escritura se convierte en indexador de solo escritura.
15.10 Operadores
15.10.1 General
Un operador es un miembro que define el significado de un operador de expresión que se puede aplicar a instancias de la clase . Los operadores se declaran mediante 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) solo está disponible en código no seguro (§23).
Nota: Los operadores de negación lógica de prefijo (§12.9.4) y postfix null-forgiving (§12.8.9), mientras que se representan mediante el mismo token léxico (!
), son distintos. Este último no es un operador sobrecargable. nota final
Hay tres categorías de operadores sobrecargables: operadores unarios (§15.10.2), operadores binarios (§15.10.3) y operadores de conversión (§15.10.4).
El operator_body es un punto y coma, un cuerpo de bloque (§15.6.1) o un cuerpo de expresión (§15.6.1). Un cuerpo de bloque consta de un bloque, que especifica las instrucciones que se van a ejecutar cuando se invoca el operador. El bloque se ajustará a las reglas para los métodos de devolución de valores descritos en §15.6.11. Un cuerpo de expresión consta de seguido de =>
una expresión y un punto y coma, y denota una expresión única que se va a realizar cuando se invoca al operador.
Para extern
los operadores, el operator_body consta simplemente de punto y coma. Para todos los demás operadores, el operator_body es un cuerpo de bloque o un cuerpo de expresión.
Las reglas siguientes se aplican a todas las declaraciones de operador:
- Una declaración de operador incluirá un
public
modificador y .static
- Los parámetros de un operador no tendrán modificadores distintos de
in
. - La firma de un operador (§15.10.2, §15.10.3, §15.10.4) diferirá de las firmas de todos los demás operadores declarados en la misma clase.
- Todos los tipos a los que se hace referencia en una declaración de operador deben ser al menos tan accesibles como el propio operador (§7.5.5).
- Es un error para que el mismo modificador aparezca varias veces en una declaración de operador.
Cada categoría de operador impone restricciones adicionales, como se describe en las subclases siguientes.
Al igual que otros miembros, los operadores declarados en una clase base se heredan mediante clases derivadas. Dado que las declaraciones de operador siempre requieren la clase o estructura en la que el operador se declara para participar en la firma del operador, no es posible que un operador declarado en una clase derivada oculte un operador declarado en una clase base. Por lo tanto, el new
modificador nunca es necesario y, por tanto, nunca se permite, en una declaración de operador.
Puede encontrar información adicional sobre operadores unarios y binarios en §12.4.
Puede encontrar información adicional sobre los operadores de conversión en §10.5.
15.10.2 Operadores unarios
Las reglas siguientes se aplican a las declaraciones de operador unario, donde T
denota el tipo de instancia de la clase o estructura que contiene la declaración del operador:
- Un unario
+
,-
,!
(solo negación lógica), o~
el operador tomará un único parámetro de tipoT
oT?
y puede devolver cualquier tipo. - Un operador o unario
++
tomará un único parámetro de tipoT
oT?
y devolverá ese mismo tipo o un tipo derivado de--
él. - Un operador o unario
true
tomará un único parámetro de tipoT
oT?
y devolverá el tipobool
.false
La firma de un operador unario consta del token de operador (+
, -
, ++
~
--
!
o true
false
) y el tipo del parámetro único. El tipo de valor devuelto no forma parte de la firma de un operador unario, ni es el nombre del parámetro .
Los true
operadores unarios y false
requieren una declaración en pares. Se produce un error en tiempo de compilación si una clase declara uno de estos operadores sin declarar el otro. Los true
operadores y false
se describen más adelante en §12.24.
Ejemplo: en el ejemplo siguiente se muestra una implementación y el uso posterior de operator++ para una clase de vector entero:
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 } }
Observe cómo el método de operador devuelve el valor generado agregando 1 al operando, al igual que los operadores de incremento y decremento postfijo (§12.8.16) y los operadores de incremento y decremento de prefijo (§12.9.6). A diferencia de C++, este método no debe modificar el valor de su operando directamente, ya que esto infringiría la semántica estándar del operador de incremento de postfijo (§12.8.16).
ejemplo final
15.10.3 Operadores binarios
Las reglas siguientes se aplican a las declaraciones de operador binario, donde T
denota el tipo de instancia de la clase o estructura que contiene la declaración del operador:
- Un operador binario sin desplazamiento tomará dos parámetros, al menos uno de los cuales tendrá el tipo
T
oT?
, y puede devolver cualquier tipo. - Un binario
<<
o>>
operador (§12.11) tomará dos parámetros, el primero de los cuales tendrá el tipoT
o T? y el segundo de los cuales tendrá el tipoint
oint?
, y puede devolver cualquier tipo.
La firma de un operador binario consta del token de operador (+
, -
, *
, /
, %
, |
&
^
>
!=
<
>>
==
<<
o >=
<=
) y los tipos de los dos parámetros. El tipo de valor devuelto y los nombres de los parámetros no forman parte de la firma de un operador binario.
Algunos operadores binarios requieren una declaración en pares. Para cada declaración de cualquiera de los operadores de un par, habrá una declaración coincidente del otro operador del par. Dos declaraciones de operador coinciden si existen conversiones de identidad entre sus tipos devueltos y sus tipos de parámetros correspondientes. Los operadores siguientes requieren una declaración en pares:
- operador y operador
==
!=
- operador y operador
>
<
- operador y operador
>=
<=
15.10.4 Operadores de conversión
Una declaración de operador de conversión introduce una conversión definida por el usuario (§10.5), que aumenta las conversiones implícitas y explícitas predefinidas.
Una declaración del operador de conversión que incluye la palabra clave introduce una conversión implícita definida por el implicit
usuario. Las conversiones implícitas pueden producirse en diversas situaciones, incluidas las invocaciones de miembro de función, las expresiones de conversión y las asignaciones. Esto se describe más adelante en §10.2.
Una declaración del operador de conversión que incluye la palabra clave presenta una conversión explícita definida por el explicit
usuario. Las conversiones explícitas pueden producirse en expresiones de conversión y se describen más adelante en §10.3.
Un operador de conversión convierte de un tipo de origen, indicado por el tipo de parámetro del operador de conversión, a un tipo de destino, indicado por el tipo de valor devuelto del operador de conversión.
Para un tipo de origen y un tipo T
S
de destino determinado , si S
o T
son tipos de valor que aceptan valores NULL, let S₀
y T₀
hacen referencia a sus tipos subyacentes; de lo contrario, S₀
y T₀
son iguales a S
y T
respectivamente. Una clase o estructura puede declarar una conversión de un tipo de origen a un tipo S
T
de destino solo si se cumplen todas las siguientes condiciones:
S₀
yT₀
son tipos diferentes.T₀
OS₀
es el tipo de instancia de la clase o estructura que contiene la declaración del operador.Ni ni
S₀
T₀
es un interface_type.Excluyendo las conversiones definidas por el usuario, una conversión no existe de
S
aT
o deT
aS
.
Para los fines de estas reglas, los parámetros de tipo asociados a S
o T
se consideran tipos únicos que no tienen ninguna relación de herencia con otros tipos, y se omiten las restricciones en esos parámetros de tipo.
Ejemplo: En lo siguiente:
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 }
Se permiten las dos primeras declaraciones de operador porque
T
yint
string
, respectivamente, se consideran tipos únicos sin relación. Sin embargo, el tercer operador es un error porqueC<T>
es la clase base deD<T>
.ejemplo final
A partir de la segunda regla, se desprende que un operador de conversión se convertirá en o desde el tipo de clase o estructura en el que se declara el operador.
Ejemplo: Es posible que un tipo
C
de clase o estructura defina una conversión deC
aint
y deint
aC
, pero no deint
abool
. ejemplo final
No es posible volver a definir directamente una conversión predefinida. Por lo tanto, no se permite que los operadores de conversión se conviertan de o a object
porque ya existen conversiones implícitas y explícitas entre object
y todos los demás tipos. Del mismo modo, ni el origen ni los tipos de destino de una conversión pueden ser un tipo base del otro, ya que ya existiría una conversión. Sin embargo, es posible declarar operadores en tipos genéricos que, para argumentos de tipo concretos, especifique conversiones que ya existen como conversiones predefinidas.
Ejemplo:
struct Convertible<T> { public static implicit operator Convertible<T>(T value) {...} public static explicit operator T(Convertible<T> value) {...} }
cuando el tipo
object
se especifica como un argumento de tipo paraT
, el segundo operador declara una conversión que ya existe (una implícita y, por lo tanto, también existe una conversión explícita de cualquier tipo a objeto de tipo).ejemplo final
En los casos en los que existe una conversión predefinida entre dos tipos, se omiten las conversiones definidas por el usuario entre esos tipos. Específicamente:
- Si existe una conversión implícita predefinida (§10.2) de tipo
S
a tipoT
, se omiten todas las conversiones definidas por el usuario (implícitas o explícitas).S
T
- Si existe una conversión explícita predefinida (§10.3) de tipo
S
a tipoT
, se omiten las conversiones explícitas definidas por el usuario deS
aT
. Además:- Si o
S
T
es un tipo de interfaz, se omiten las conversiones implícitas definidas por el usuario deS
aT
. - De lo contrario, las conversiones implícitas definidas por el usuario de
S
aT
se siguen considerando.
- Si o
Para todos los tipos, pero object
, los operadores declarados por el Convertible<T>
tipo anterior no entran en conflicto con conversiones predefinidas.
Ejemplo:
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 }
Sin embargo, para el tipo
object
, las conversiones predefinidas ocultan las conversiones definidas por el usuario en todos los casos, pero 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 }
ejemplo final
Las conversiones definidas por el usuario no pueden convertir de o a interface_types. En concreto, esta restricción garantiza que no se produzcan transformaciones definidas por el usuario al convertir en un interface_type y que una conversión a un interface_type se realice correctamente solo si el object
que se convierte realmente implementa el interface_type especificado.
La firma de un operador de conversión consta del tipo de origen y del tipo de destino. (Este es el único formulario de miembro para el que participa el tipo de valor devuelto en la firma). La clasificación implícita o explícita de un operador de conversión no forma parte de la firma del operador. Por lo tanto, una clase o estructura no puede declarar un operador de conversión implícito y explícito con los mismos tipos de origen y destino.
Nota: En general, las conversiones implícitas definidas por el usuario deben diseñarse para no iniciar excepciones y nunca perder información. Si una conversión definida por el usuario puede dar lugar a excepciones (por ejemplo, porque el argumento de origen está fuera del intervalo) o la pérdida de información (como descartar bits de orden alto), esa conversión debe definirse como una conversión explícita. nota final
Ejemplo: en el código siguiente
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 conversión de a
Digit
byte
es implícita porque nunca produce excepciones o pierde información, pero la conversión debyte
aDigit
es explícita, ya queDigit
solo puede representar un subconjunto de los valores posibles de .byte
ejemplo final
15.11 Constructores de instancia
15.11.1 General
Un constructor de instancia es un miembro que implementa las acciones necesarias para inicializar una instancia de una clase. Los constructores de instancia se declaran mediante 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) solo está disponible en código no seguro (§23).
Un constructor_declaration puede incluir un conjunto de atributos (§22), cualquiera de los tipos permitidos de accesibilidad declarada (§15.3.6) y un extern
modificador (§15.6.8). No se permite que una declaración de constructor incluya el mismo modificador varias veces.
El identificador de un constructor_declarator denominará la clase en la que se declara el constructor de instancia. Si se especifica otro nombre, se produce un error en tiempo de compilación.
El parameter_list opcional de un constructor de instancia está sujeto a las mismas reglas que la parameter_list de un método (§15.6). Como modificador this
para parámetros solo se aplica a los métodos de extensión (§15.6.10), ningún parámetro de la parameter_list de un constructor contendrá el this
modificador . La lista de parámetros define la firma (§7.6) de un constructor de instancia y rige el proceso por el que la resolución de sobrecarga (§12.6.4) selecciona un constructor de instancia determinado en una invocación.
Cada uno de los tipos a los que se hace referencia en el parameter_list de un constructor de instancia debe ser al menos tan accesible como el propio constructor (§7.5.5).
El constructor_initializer opcional especifica otro constructor de instancia que se va a invocar antes de ejecutar las instrucciones dadas en el constructor_body de este constructor de instancia. Esto se describe más adelante en §15.11.2.
Cuando una declaración de constructor incluye un extern
modificador, se dice que el constructor es un constructor externo. Dado que una declaración de constructor externo no proporciona ninguna implementación real, su constructor_body consta de un punto y coma. Para todos los demás constructores, el constructor_body consta de cualquiera de los dos
- bloque , que especifica las instrucciones para inicializar una nueva instancia de la clase ; o
- un cuerpo de expresión, que consta de seguido de
=>
una expresión y un punto y coma, y denota una expresión única para inicializar una nueva instancia de la clase.
Un constructor_body que es un bloque o cuerpo de expresión corresponde exactamente al bloque de un método de instancia con un void
tipo de valor devuelto (§15.6.11).
Los constructores de instancia no se heredan. Por lo tanto, una clase no tiene constructores de instancia distintos de los declarados realmente en la clase, con la excepción de que si una clase no contiene declaraciones de constructor de instancia, se proporciona automáticamente un constructor de instancia predeterminado (§15.11.5).
Los constructores de instancia se invocan mediante object_creation_expression s (§12.8.17.2) y a través de constructor_initializers.
Inicializadores del constructor 15.11.2
Todos los constructores de instancia (excepto los de la clase object
) incluyen implícitamente una invocación de otro constructor de instancia inmediatamente antes del constructor_body. El constructor que se va a invocar implícitamente viene determinado por el constructor_initializer:
- Un inicializador de constructor de instancia del formulario
base(
argument_list)
(donde argument_list es opcional) hace que se invoque un constructor de instancia de la clase base directa. Ese constructor se selecciona mediante argument_list y las reglas de resolución de sobrecarga de §12.6.4. El conjunto de constructores de instancias candidatas consta de todos los constructores de instancia accesibles de la clase base directa. Si este conjunto está vacío o si no se puede identificar un único constructor de instancia mejor, se produce un error en tiempo de compilación. - Un inicializador de constructor de instancia del formulario
this(
argument_list)
(donde argument_list es opcional) invoca otro constructor de instancia de la misma clase. El constructor se selecciona mediante argument_list y las reglas de resolución de sobrecarga de §12.6.4. El conjunto de constructores de instancia candidatos consta de todos los constructores de instancia declarados en la propia clase. Si el conjunto resultante de constructores de instancia aplicables está vacío o si no se puede identificar un único constructor de instancia mejor, se produce un error en tiempo de compilación. Si una declaración de constructor de instancia se invoca a sí misma a través de una cadena de uno o varios inicializadores de constructor, se produce un error en tiempo de compilación.
Si un constructor de instancia no tiene inicializador de constructor, se proporciona implícitamente un inicializador de constructor del formulario base()
.
Nota: Por lo tanto, una declaración de constructor de instancia del formulario
C(...) {...}
es exactamente equivalente a
C(...) : base() {...}
nota final
El ámbito de los parámetros proporcionados por el parameter_list de una declaración de constructor de instancia incluye el inicializador de constructor de esa declaración. Por lo tanto, se permite que un inicializador de constructor acceda a los parámetros del constructor.
Ejemplo:
class A { public A(int x, int y) {} } class B: A { public B(int x, int y) : base(x + y, x - y) {} }
ejemplo final
Un inicializador de constructor de instancia no puede tener acceso a la instancia que se está creando. Por lo tanto, es un error en tiempo de compilación para hacer referencia a esto en una expresión de argumento del inicializador del constructor, ya que es un error en tiempo de compilación para que una expresión de argumento haga referencia a cualquier miembro de instancia a través de un simple_name.
Inicializadores de variables de instancia 15.11.3
Cuando un constructor de instancia no tiene inicializador de constructor o tiene un inicializador de constructor del formulario base(...)
, ese constructor realiza implícitamente las inicializaciones especificadas por los variable_initializerde los campos de instancia declarados en su clase. Esto corresponde a una secuencia de asignaciones que se ejecutan inmediatamente después de la entrada al constructor y antes de la invocación implícita del constructor de clase base directa. Los inicializadores de variables se ejecutan en el orden textual en el que aparecen en la declaración de clase (§15.5.6).
Ejecución del constructor 15.11.4
Los inicializadores de variables se transforman en instrucciones de asignación y estas instrucciones de asignación se ejecutan antes de la invocación del constructor de instancia de clase base. Este orden garantiza que todos los campos de instancia se inicialicen mediante sus inicializadores variables antes de que se ejecuten las instrucciones que tengan acceso a esa instancia.
Ejemplo: dado lo siguiente:
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}"); }
cuando se usa new
B()
para crear una instancia deB
, se genera la siguiente salida:x = 1, y = 0
El valor de
x
es 1 porque el inicializador de variable se ejecuta antes de invocar el constructor de instancia de clase base. Sin embargo, el valor dey
es 0 (el valor predeterminado de )int
porque la asignación ay
no se ejecuta hasta después de que el constructor de clase base devuelva. Es útil pensar en inicializadores de variables de instancia y inicializadores de constructores como instrucciones que se insertan automáticamente antes del constructor_body. En el ejemploclass 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 varios inicializadores de variables; también contiene inicializadores de constructor de ambos formularios (
base
ythis
). El ejemplo corresponde al código que se muestra a continuación, donde cada comentario indica una instrucción insertada automáticamente (la sintaxis usada para las invocaciones de constructor insertadas automáticamente no es válida, pero simplemente sirve para ilustrar el mecanismo).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; } }
ejemplo final
15.11.5 Constructores predeterminados
Si una clase no contiene declaraciones de constructor de instancia, se proporciona automáticamente un constructor de instancia predeterminado. Ese constructor predeterminado simplemente invoca un constructor de la clase base directa, como si tuviera un inicializador de constructor del formulario base()
. Si la clase es abstracta, la accesibilidad declarada para el constructor predeterminado está protegida. De lo contrario, la accesibilidad declarada para el constructor predeterminado es pública.
Nota: Por lo tanto, el constructor predeterminado siempre tiene el formato
protected C(): base() {}
o
public C(): base() {}
donde
C
es el nombre de la clase .nota final
Si la resolución de sobrecarga no puede determinar un mejor candidato único para el inicializador de constructor de clase base, se produce un error en tiempo de compilación.
Ejemplo: en el código siguiente
class Message { object sender; string text; }
Se proporciona un constructor predeterminado porque la clase no contiene declaraciones de constructor de instancia. Por lo tanto, el ejemplo es exactamente equivalente a
class Message { object sender; string text; public Message() : base() {} }
ejemplo final
15.12 Constructores estáticos
Un constructor estático es un miembro que implementa las acciones necesarias para inicializar una clase cerrada. Los constructores estáticos se declaran mediante 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) solo está disponible en código no seguro (§23).
Un static_constructor_declaration puede incluir un conjunto de atributos (§22) y un extern
modificador (§15.6.8).
El identificador de un static_constructor_declaration denominará la clase en la que se declara el constructor estático. Si se especifica otro nombre, se produce un error en tiempo de compilación.
Cuando una declaración de constructor estático incluye un extern
modificador, se dice que el constructor estático es un constructor estático externo. Dado que una declaración de constructor estático externo no proporciona ninguna implementación real, su static_constructor_body consta de un punto y coma. Para todas las demás declaraciones de constructores estáticos, el static_constructor_body consta de cualquiera de las dos
- un bloque, que especifica las instrucciones que se van a ejecutar para inicializar la clase; o
- un cuerpo de expresión, que consta de seguido de
=>
una expresión y un punto y coma, y denota una expresión única que se va a ejecutar para inicializar la clase.
Un static_constructor_body que es un cuerpo de bloque o expresión corresponde exactamente al method_body de un método estático con un void
tipo de valor devuelto (§15.6.11).
Los constructores estáticos no se heredan y no se pueden llamar directamente.
El constructor estático de una clase cerrada se ejecuta como máximo una vez en un dominio de aplicación determinado. La ejecución de un constructor estático se desencadena mediante el primero de los siguientes eventos que se producirán dentro de un dominio de aplicación:
- Se crea una instancia de la clase .
- Se hace referencia a cualquiera de los miembros estáticos de la clase .
Si una clase contiene el Main
método (§7.1) en el que comienza la ejecución, el constructor estático para esa clase se ejecuta antes de llamar al Main
método .
Para inicializar un nuevo tipo de clase cerrada, primero se crea un nuevo conjunto de campos estáticos (§15.5.2) para ese tipo cerrado determinado. Cada uno de los campos estáticos se inicializa en su valor predeterminado (§15.5.5). A continuación, los inicializadores de campo estáticos (§15.5.6.2) se ejecutan para esos campos estáticos. Por último, se ejecuta el constructor estático.
Ejemplo: El ejemplo
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"); } }
debe generar la salida:
Init A A.F Init B B.F
como la ejecución del
A
constructor estático se desencadena mediante la llamada aA.F
y la ejecución delB
constructor estático se desencadena mediante la llamada aB.F
.ejemplo final
Es posible construir dependencias circulares que permitan observar campos estáticos con inicializadores variables en su estado de valor predeterminado.
Ejemplo: El ejemplo
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}"); } }
genera el resultado
X = 1, Y = 2
Para ejecutar el
Main
método, el sistema ejecuta primero el inicializador paraB.Y
, antes del constructor estático de la claseB
.Y
El inicializador haceA
que se ejecute el constructor porquestatic
se hace referencia al valor deA.X
. El constructor estático deA
a su vez continúa calculando el valor deX
y, al hacerlo, captura el valor predeterminado deY
, que es cero.A.X
Por lo tanto, se inicializa en 1. A continuación, el proceso de ejecuciónA
de inicializadores de campo estáticos y constructor estático se completa, volviendo al cálculo del valor inicial deY
, el resultado de que se convierte en 2.ejemplo final
Dado que el constructor estático se ejecuta exactamente una vez para cada tipo de clase construido cerrado, es un lugar conveniente para aplicar comprobaciones en tiempo de ejecución en el parámetro de tipo que no se pueden comprobar en tiempo de compilación a través de restricciones (§15.2.5).
Ejemplo: el siguiente tipo usa un constructor estático para exigir que el argumento type sea una enumeración:
class Gen<T> where T : struct { static Gen() { if (!typeof(T).IsEnum) { throw new ArgumentException("T must be an enum"); } } }
ejemplo final
15.13 Finalizadores
Nota: En una versión anterior de esta especificación, lo que ahora se conoce como "finalizador" se denominaba "destructor". La experiencia ha demostrado que el término "destructor" causó confusión y a menudo dio lugar a expectativas incorrectas, especialmente a los programadores que conozcan C++. En C++, se llama a un destructor de forma determinada, mientras que, en C#, no es un finalizador. Para obtener un comportamiento determinado de C#, se debe usar
Dispose
. nota final
Un finalizador es un miembro que implementa las acciones necesarias para finalizar una instancia de una clase. Un finalizador se declara mediante 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) solo está disponible en código no seguro (§23).
Un finalizer_declaration puede incluir un conjunto de atributos (§22).
El identificador de un finalizer_declarator denominará la clase en la que se declara el finalizador. Si se especifica otro nombre, se produce un error en tiempo de compilación.
Cuando una declaración de finalizador incluye un extern
modificador, se dice que el finalizador es un finalizador externo. Dado que una declaración de finalizador externo no proporciona ninguna implementación real, su finalizer_body consta de un punto y coma. Para todos los demás finalizadores, el finalizer_body consta de cualquiera de los dos
- bloque , que especifica las instrucciones que se van a ejecutar para finalizar una instancia de la clase .
- o un cuerpo de expresión, que consta de seguido de
=>
una expresión y un punto y coma, y denota una expresión única que se va a ejecutar para finalizar una instancia de la clase.
Un finalizer_body que es un cuerpo de bloque o expresión corresponde exactamente al method_body de un método de instancia con un void
tipo de valor devuelto (§15.6.11).
Los finalizadores no se heredan. Por lo tanto, una clase no tiene finalizadores distintos de los que se pueden declarar en esa clase.
Nota: Dado que se requiere un finalizador para no tener parámetros, no se puede sobrecargar, por lo que una clase puede tener, como máximo, un finalizador. nota final
Los finalizadores se invocan automáticamente y no se pueden invocar explícitamente. Una instancia se convierte en apta para la finalización cuando ya no es posible que ningún código use esa instancia. La ejecución del finalizador de la instancia puede producirse en cualquier momento después de que la instancia sea apta para la finalización (§7.9). Cuando se finaliza una instancia, se llama a los finalizadores de la cadena de herencia de esa instancia, en orden, de la mayoría de los derivados a los menos derivados. Un finalizador se puede ejecutar en cualquier subproceso. Para obtener más información sobre las reglas que rigen cuándo y cómo se ejecuta un finalizador, consulte §7.9.
Ejemplo: salida del ejemplo
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
dado que se llama a los finalizadores de una cadena de herencia en orden, desde la mayoría de los derivados hasta los menos derivados.
ejemplo final
Los finalizadores se implementan reemplazando el método Finalize
virtual en System.Object
. Los programas de C# no pueden invalidar este método ni llamarlo (o invalidaciones de él) directamente.
Ejemplo: por ejemplo, el programa
class A { override protected void Finalize() {} // Error public void F() { this.Finalize(); // Error } }
contiene dos errores.
ejemplo final
El compilador se comporta como si este método, y las invalidaciones de este método, no existen en absoluto.
Ejemplo: Por lo tanto, este programa:
class A { void Finalize() {} // Permitted }
es válido y el método mostrado oculta
System.Object
elFinalize
método .ejemplo final
Para obtener una explicación del comportamiento cuando se produce una excepción desde un finalizador, consulte §21.4.
15.14 Iteradores
15.14.1 General
Un miembro de función (§12.6) implementado mediante un bloque de iterador (§13.3) se denomina iterador.
Un bloque de iterador se puede usar como cuerpo de un miembro de función siempre que el tipo de valor devuelto del miembro de función correspondiente sea una de las interfaces del enumerador (§15.14.2) o una de las interfaces enumerables (§15.14.3). Puede producirse como method_body, operator_body o accessor_body, mientras que los eventos, constructores de instancia, constructores estáticos y finalizador no se implementarán como iteradores.
Cuando un miembro de función se implementa mediante un bloque de iterador, se trata de un error en tiempo de compilación para la lista de parámetros del miembro de función para especificar cualquier in
parámetro , out
o ref
, o un parámetro de un ref struct
tipo.
15.14.2 Interfaces del enumerador
Las interfaces del enumerador son la interfaz System.Collections.IEnumerator
no genérica y todas las instancias de la interfaz System.Collections.Generic.IEnumerator<T>
genérica . Por motivos de brevedad, en esta subclausa y sus elementos del mismo nivel se hace referencia a estas interfaces como IEnumerator
y IEnumerator<T>
, respectivamente.
15.14.3 Interfaces enumerables
Las interfaces enumerables son la interfaz System.Collections.IEnumerable
no genérica y todas las instancias de la interfaz System.Collections.Generic.IEnumerable<T>
genérica . Por motivos de brevedad, en esta subclausa y sus elementos del mismo nivel se hace referencia a estas interfaces como IEnumerable
y IEnumerable<T>
, respectivamente.
15.14.4 Tipo de rendimiento
Un iterador genera una secuencia de valores, todo el mismo tipo. Este tipo se denomina tipo de rendimiento del iterador.
- Tipo de rendimiento de un iterador que devuelve
IEnumerator
oIEnumerable
esobject
. - Tipo de rendimiento de un iterador que devuelve
IEnumerator<T>
oIEnumerable<T>
esT
.
15.14.5 Objetos enumeradores
15.14.5.1 General
Cuando un miembro de función que devuelve un tipo de interfaz de enumerador se implementa mediante un bloque de iterador, invocar al miembro de función no ejecuta inmediatamente el código en el bloque iterador. En su lugar, se crea y devuelve un objeto enumerador. Este objeto encapsula el código especificado en el bloque iterador y la ejecución del código en el bloque iterador se produce cuando se invoca el método del MoveNext
objeto enumerador. Un objeto enumerador tiene las siguientes características:
- Implementa
IEnumerator
yIEnumerator<T>
, dondeT
es el tipo de rendimiento del iterador. - Implementa
System.IDisposable
. - Se inicializa con una copia de los valores de argumento (si los hay) y el valor de instancia pasados al miembro de función.
- Tiene cuatro estados potenciales, antes, en ejecución, suspendidos y posteriores, y se encuentra inicialmente en el estado anterior.
Normalmente, un objeto enumerador es una instancia de una clase de enumerador generada por el compilador que encapsula el código en el bloque iterador e implementa las interfaces del enumerador, pero son posibles otros métodos de implementación. Si el compilador genera una clase de enumerador, esa clase se anidará, directa o indirectamente, en la clase que contiene el miembro de función, tendrá accesibilidad privada y tendrá un nombre reservado para el uso del compilador (§6.4.3).
Un objeto enumerador puede implementar más interfaces de las especificadas anteriormente.
Las subclases siguientes describen el comportamiento necesario de los MoveNext
miembros , Current
y Dispose
de las implementaciones de la IEnumerator
interfaz y IEnumerator<T>
proporcionadas por un objeto enumerador.
Los objetos de enumerador no admiten el IEnumerator.Reset
método . La invocación de este método hace que se produzca una System.NotSupportedException
excepción .
15.14.5.2 El método MoveNext
El MoveNext
método de un objeto enumerador encapsula el código de un bloque de iterador. Al invocar el método se MoveNext
ejecuta código en el bloque iterador y se establece la Current
propiedad del objeto enumerador según corresponda. La acción precisa realizada por MoveNext
depende del estado del objeto enumerador cuando MoveNext
se invoca:
- Si el estado del objeto enumerador es anterior, invocando
MoveNext
:- Cambia el estado a en ejecución.
- Inicializa los parámetros (incluidos
this
) del bloque iterador en los valores de argumento y el valor de instancia guardados cuando se inicializó el objeto enumerador. - Ejecuta el bloque de iterador desde el principio hasta que se interrumpe la ejecución (como se describe a continuación).
- Si el estado del objeto enumerador se está ejecutando, el resultado de invocar
MoveNext
no se especifica. - Si se suspende el estado del objeto enumerador, invocando MoveNext:
- Cambia el estado a en ejecución.
- Restaura los valores de todas las variables y parámetros locales (incluidos
this
) a los valores guardados cuando la ejecución del bloque iterador se suspendió por última vez.Nota: El contenido de los objetos a los que hacen referencia estas variables puede haber cambiado desde la llamada anterior a
MoveNext
. nota final - Reanuda la ejecución del bloque iterador inmediatamente después de la instrucción yield return que provocó la suspensión de la ejecución y continúa hasta que se interrumpe la ejecución (como se describe a continuación).
- Si el estado del objeto enumerador es posterior, la invocación
MoveNext
devuelve false.
Cuando MoveNext
ejecuta el bloque de iterador, la ejecución se puede interrumpir de cuatro maneras: mediante una yield return
instrucción , mediante una yield break
instrucción , al encontrar el final del bloque iterador y mediante una excepción que se inicia y propaga fuera del bloque de iterador.
- Cuando se encuentra una
yield return
instrucción (§9.4.4.20):- La expresión dada en la instrucción se evalúa, se convierte implícitamente en el tipo de rendimiento y se asigna a la
Current
propiedad del objeto enumerador. - Se suspende la ejecución del cuerpo del iterador. Los valores de todas las variables y parámetros locales (incluidos
this
) se guardan, como es la ubicación de estayield return
instrucción. Si layield return
instrucción está dentro de uno o variostry
bloques, los bloques finally asociados no se ejecutan en este momento. - El estado del objeto enumerador se cambia a suspendido.
- El
MoveNext
método vuelvetrue
a su llamador, lo que indica que la iteración ha avanzado correctamente al siguiente valor.
- La expresión dada en la instrucción se evalúa, se convierte implícitamente en el tipo de rendimiento y se asigna a la
- Cuando se encuentra una
yield break
instrucción (§9.4.4.20):- Si la
yield break
instrucción está dentro de uno o variostry
bloques, se ejecutan los bloques asociadosfinally
. - El estado del objeto enumerador se cambia a después.
- El
MoveNext
método vuelvefalse
a su autor de la llamada, lo que indica que la iteración está completa.
- Si la
- Cuando se encuentra el final del cuerpo del iterador:
- El estado del objeto enumerador se cambia a después.
- El
MoveNext
método vuelvefalse
a su autor de la llamada, lo que indica que la iteración está completa.
- Cuando se produce una excepción y se propaga fuera del bloque de iterador:
- Los bloques adecuados
finally
en el cuerpo del iterador se ejecutarán mediante la propagación de excepciones. - El estado del objeto enumerador se cambia a después.
- La propagación de excepciones continúa con el autor de la llamada del
MoveNext
método .
- Los bloques adecuados
15.14.5.3 La propiedad Current
Las instrucciones del bloque iterador afectan a yield return
la propiedad de Current
un objeto enumerador.
Cuando un objeto enumerador está en estado suspendido , el valor de Current
es el valor establecido por la llamada anterior a MoveNext
. Cuando un objeto enumerador está en los estados antes, en ejecución o después , no se especifica el resultado del acceso Current
.
Para un iterador con un tipo de rendimiento distinto object
de , el resultado de acceder Current
a través de la implementación del IEnumerable
objeto enumerador corresponde al acceso Current
a través de la implementación del IEnumerator<T>
objeto enumerador y convertir el resultado en object
.
15.14.5.4 El método Dispose
El Dispose
método se usa para limpiar la iteración mediante la incorporación del objeto enumerador al estado después .
- Si el estado del objeto enumerador es anterior, la invocación
Dispose
cambia el estado a después. - Si el estado del objeto enumerador se está ejecutando, el resultado de invocar
Dispose
no se especifica. - Si se suspende el estado del objeto enumerador, invocando
Dispose
:- Cambia el estado a en ejecución.
- Ejecuta los bloques finally como si la última instrucción ejecutada
yield return
fuera unayield break
instrucción . Si esto hace que se produzca una excepción y se propague fuera del cuerpo del iterador, el estado del objeto enumerador se establece en después y la excepción se propaga al autor de la llamada delDispose
método. - Cambia el estado a después.
- Si el estado del objeto enumerador es posterior, la invocación
Dispose
no tiene ningún efecto.
15.14.6 Objetos enumerables
15.14.6.1 General
Cuando un miembro de función devuelve un tipo de interfaz enumerable se implementa mediante un bloque de iterador, invocar al miembro de función no ejecuta inmediatamente el código en el bloque iterador. En su lugar, se crea y devuelve un objeto enumerable. El método del GetEnumerator
objeto enumerable devuelve un objeto enumerador que encapsula el código especificado en el bloque iterador y la ejecución del código en el bloque iterador se produce cuando se invoca el método del MoveNext
objeto enumerador. Un objeto enumerable tiene las siguientes características:
- Implementa
IEnumerable
yIEnumerable<T>
, dondeT
es el tipo de rendimiento del iterador. - Se inicializa con una copia de los valores de argumento (si los hay) y el valor de instancia pasados al miembro de función.
Normalmente, un objeto enumerable es una instancia de una clase enumerable generada por el compilador que encapsula el código en el bloque iterador e implementa las interfaces enumerables, pero son posibles otros métodos de implementación. Si el compilador genera una clase enumerable, esa clase se anidará, directa o indirectamente, en la clase que contiene el miembro de función, tendrá accesibilidad privada y tendrá un nombre reservado para el uso del compilador (§6.4.3).
Un objeto enumerable puede implementar más interfaces de las especificadas anteriormente.
Nota: Por ejemplo, un objeto enumerable también puede implementar
IEnumerator
yIEnumerator<T>
, lo que permite que actúe como enumerable y como enumerador. Normalmente, esta implementación devolvería su propia instancia (para guardar asignaciones) de la primera llamada aGetEnumerator
. Las invocaciones posteriores deGetEnumerator
, si las hubiera, devolverían una nueva instancia de clase, normalmente de la misma clase, de modo que las llamadas a instancias diferentes del enumerador no afectarán entre sí. No puede devolver la misma instancia aunque el enumerador anterior ya haya enumerado más allá del final de la secuencia, ya que todas las llamadas futuras a un enumerador agotado deben producir excepciones. nota final
15.14.6.2 El método GetEnumerator
Un objeto enumerable proporciona una implementación de los GetEnumerator
métodos de las IEnumerable
interfaces y IEnumerable<T>
. Los dos GetEnumerator
métodos comparten una implementación común que adquiere y devuelve un objeto enumerador disponible. El objeto enumerador se inicializa con los valores de argumento y el valor de instancia guardados cuando se inicializó el objeto enumerable, pero de lo contrario, el objeto enumerador funciona como se describe en §15.14.5.
15.15 Funciones asincrónicas
15.15.1 General
Un método (§15.6) o una función anónima (§12.19) con el async
modificador se denomina función asincrónica. En general, el término asincrónico se usa para describir cualquier tipo de función que tenga el async
modificador .
Es un error en tiempo de compilación para la lista de parámetros de una función asincrónica especificar cualquier in
parámetro , out
o ref
, o cualquier parámetro de un ref struct
tipo.
El return_type de un método asincrónico debe ser void
o un tipo de tarea. Para un método asincrónico que genera un valor de resultado, un tipo de tarea será genérico. Para un método asincrónico que no genera un valor de resultado, un tipo de tarea no será genérico. Estos tipos se conocen en esta especificación como «TaskType»<T>
y «TaskType»
, respectivamente. El tipo System.Threading.Tasks.Task
de biblioteca estándar y los tipos construidos a partir System.Threading.Tasks.Task<TResult>
de son tipos de tareas, así como un tipo de clase, estructura o interfaz asociado a un tipo de generador de tareas mediante el atributo System.Runtime.CompilerServices.AsyncMethodBuilderAttribute
. Estos tipos se conocen en esta especificación como «TaskBuilderType»<T>
y «TaskBuilderType»
. Un tipo de tarea puede tener como máximo un parámetro de tipo y no se puede anidar en un tipo genérico.
Se dice que un método asincrónico devuelve un tipo de tarea que devuelve tareas.
Los tipos de tareas pueden variar en su definición exacta, pero desde el punto de vista del lenguaje, un tipo de tarea se encuentra en uno de los estados incompletos, correctos o con errores. Una tarea con errores registra una excepción pertinente. Un objeto correcto «TaskType»<T>
registra un resultado de tipo T
. Los tipos de tareas son esperables y, por tanto, las tareas pueden ser los operandos de expresiones await (§12.9.8).
Ejemplo: el tipo
MyTask<T>
de tarea está asociado al tipoMyTaskMethodBuilder<T>
del generador de tareas y 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() { ... } }
ejemplo final
Un tipo de generador de tareas es un tipo de clase o estructura que corresponde a un tipo de tarea específico (§15.15.2). El tipo del generador de tareas coincidirá exactamente con la accesibilidad declarada de su tipo de tarea correspondiente.
Nota: Si el tipo de tarea se declara
internal
, el tipo de generador correspondiente también debe declararseinternal
y definirse en el mismo ensamblado. Si el tipo de tarea está anidado dentro de otro tipo, el tipo del buider de tareas también debe anidarse en ese mismo tipo. nota final
Una función asincrónica tiene la capacidad de suspender la evaluación mediante expresiones await (§12.9.8) en su cuerpo. La evaluación se puede reanudar posteriormente en el punto de la expresión await de suspensión mediante un delegado de reanudación. El delegado de reanudación es de tipo System.Action
y, cuando se invoca, la evaluación de la invocación de función asincrónica se reanudará desde la expresión await donde se dejó. El autor de la llamada actual de una invocación de función asincrónica es el autor de la llamada original si la invocación de función nunca se ha suspendido o el autor de llamada más reciente del delegado de reanudación de lo contrario.
15.15.2 Patrón del generador de tipos de tareas
Un tipo de generador de tareas puede tener como máximo un parámetro de tipo y no se puede anidar en un tipo genérico. Un tipo de generador de tareas tendrá los siguientes miembros (para los tipos de generador de tareas no genéricos, SetResult
no tiene parámetros) con accesibilidad declarada 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; }
}
El compilador genera código que usa «TaskBuilderType» para implementar la semántica de suspender y reanudar la evaluación de la función asincrónica. El compilador usa «TaskBuilderType» como se indica a continuación:
«TaskBuilderType».Create()
se invoca para crear una instancia de "TaskBuilderType", denominadabuilder
en esta lista.builder.Start(ref stateMachine)
se invoca para asociar el generador a una instancia de máquina de estado generada por el compilador,stateMachine
.- El generador llamará
stateMachine.MoveNext()
a enStart()
o después deStart()
haber devuelto para avanzar en la máquina de estado.
- El generador llamará
- Después
Start()
de la devolución, elasync
método invocabuilder.Task
para que la tarea vuelva del método asincrónico. - Cada llamada a
stateMachine.MoveNext()
avanzará en la máquina de estado. - Si la máquina de estado se completa correctamente,
builder.SetResult()
se llama a , con el valor devuelto del método , si existe. - De lo contrario, si se produce una excepción
e
en la máquina de estado,builder.SetException(e)
se llama a . - Si la máquina de estado alcanza una
await expr
expresión,expr.GetAwaiter()
se invoca. - Si el awaiter implementa
ICriticalNotifyCompletion
yIsCompleted
es false, la máquina de estado invocabuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine)
.AwaitUnsafeOnCompleted()
debe llamar aawaiter.UnsafeOnCompleted(action)
con unAction
que llamastateMachine.MoveNext()
cuando se completa el awaiter.
- De lo contrario, el equipo de estado invoca
builder.AwaitOnCompleted(ref awaiter, ref stateMachine)
.AwaitOnCompleted()
debe llamar aawaiter.OnCompleted(action)
con unAction
que llamastateMachine.MoveNext()
cuando se completa el awaiter.
SetStateMachine(IAsyncStateMachine)
la implementación generada porIAsyncStateMachine
el compilador puede llamarla para identificar la instancia del generador asociada a una instancia de máquina de estado, especialmente en los casos en los que la máquina de estado se implementa como un tipo de valor.- Si el generador llama a
stateMachine.SetStateMachine(stateMachine)
, llamarábuilder.SetStateMachine(stateMachine)
stateMachine
a en la instancia del generador asociada astateMachine
.
- Si el generador llama a
Nota: Para y
SetResult(T result)
«TaskType»<T> Task { get; }
, el parámetro y el argumento respectivamente deben ser identidad convertibles aT
. Esto permite que un generador de tipos de tareas admita tipos como tuplas, donde dos tipos que no son iguales son convertibles de identidad. nota final
15.15.3 Evaluación de una función asincrónica que devuelve tareas
La invocación de una función asincrónica que devuelve tareas hace que se genere una instancia del tipo de tarea devuelto. Esto se denomina tarea de devolución de la función asincrónica. La tarea está inicialmente en un estado incompleto .
A continuación, el cuerpo de la función asincrónica se evalúa hasta que se suspende (llegando a una expresión await) o finaliza, en el que se devuelve el control de punto al autor de la llamada, junto con la tarea de devolución.
Cuando finaliza el cuerpo de la función asincrónica, la tarea de retorno se mueve fuera del estado incompleto:
- Si el cuerpo de la función finaliza como resultado de alcanzar una instrucción return o el final del cuerpo, cualquier valor de resultado se registra en la tarea de devolución, que se coloca en un estado correcto .
- Si el cuerpo de la función finaliza debido a una excepción no detectada
OperationCanceledException
, la excepción se registra en la tarea de retorno que se coloca en el estado cancelado . - Si el cuerpo de la función finaliza como resultado de cualquier otra excepción no detectada (§13.10.6), la excepción se registra en la tarea de devolución que se coloca en un estado defectuoso .
15.15.4 Evaluación de una función asincrónica que devuelve void
Si el tipo de valor devuelto de la función asincrónica es void
, la evaluación difiere de la anterior de la siguiente manera: Dado que no se devuelve ninguna tarea, la función comunica en su lugar la finalización y las excepciones al contexto de sincronización del subproceso actual. La definición exacta del contexto de sincronización depende de la implementación, pero es una representación de "donde" se ejecuta el subproceso actual. El contexto de sincronización se notifica cuando se inicia la evaluación de una void
función asincrónica que devuelve , se completa correctamente o se produce una excepción no detectada.
Esto permite que el contexto realice un seguimiento del número void
de funciones asincrónicas que devuelven en él y decida cómo propagar excepciones procedentes de ellas.
ECMA C# draft specification