Compartir a través de


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 publicmodificadores , protected, internaly 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 publicmodificadores , protected, internaly 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 abstractmodificadores , sealedy 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án null 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étodo Fabstracto . La clase B presenta un método Gadicional, pero dado que no proporciona una implementación de F, B también se declarará abstracta. La clase C invalida F y proporciona una implementación real. Dado que no hay miembros abstractos en C, 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 o abstract . (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 protectedo protected 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 formato T.I, o
  • El nombre namespace_or_type es en T un typeof_expression (§12.8.18) del formato typeof(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 formulario E.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 de By B se dice que se deriva de A. Puesto A que no especifica explícitamente una clase base directa, su clase base directa es implícitamente object.

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ía B<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.Enumo 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 de Z se considera object, y por lo tanto (por las reglas de §7.8) Z no se considera que tiene un miembro Y.

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> son C<int[]>, B<IComparable<int[]>>, Ay object.

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 depende B de (su clase envolvente inmediatamente), que depende circularmente de A.

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 depende A de (porque A es su clase base directa y su clase envolvente inmediatamente), pero A no depende B de (ya B que no es una clase base ni una clase envolvente de A). 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 clase Asealed .

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 es IA, IBy IC.

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 classde tipo de referencia , la restricción structde tipo de valor , la restricción notnullnot null o la restricción unmanagedde 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 o T : BaseClass), pero use T? 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 para T. Por lo tanto, los tipos construidos recursivamente de las formas T?? y Nullable<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 sealedserá .
  • El tipo no debe ser uno de los siguientes tipos: System.Array o System.ValueType.
  • El tipo no objectserá .
  • 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ámetro S de tipo,S depende.T
  • Si un parámetro S de tipo depende de un parámetro T de tipo y T depende de un parámetro U 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 que S se forzaría a ser el mismo tipo que T, 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ón A class_type y T tiene una restricción B class_type, habrá una conversión de identidad o una conversión de referencia implícita de A a o B una conversión de referencia implícita de B a A.
  • Si S también depende del parámetro U de tipo y U tiene una restricción A class_type y T tiene una restricción B class_type, habrá una conversión de identidad o una conversión de referencia implícita de A a B o una conversión de referencia implícita de B a A.

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.Enumy 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 tipo Outer.Inner anidado, Cₓ es un tipo Outerₓ.Innerₓanidado .
  • Si C Cₓes un tipo G<A¹, ..., Aⁿ> construido con argumentos A¹, ..., Aⁿ de tipo, Cₓ es el tipo G<A¹ₓ, ..., Aⁿₓ>construido .
  • Si C es un tipo E[] de matriz, Cₓ es el tipo Eₓ[]de matriz .
  • Si C es dinámico, Cₓ es object.
  • En caso contrario, Cₓ es C.

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 contiene System.ValueType.
  • Para cada restricción de que es un tipo de T enumeración, R contiene System.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 contiene System.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 es System.ValueType.
  • De lo contrario, si R está vacío, la clase base efectiva es object.
  • De lo contrario, la clase base efectiva de T es el tipo más abarcado (§10.5.3) del conjunto R. Si el conjunto no tiene ningún tipo abarcado, la clase base efectiva de T es object. 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 en x porque T está restringido para implementar IPrintablesiempre .

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, structo 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, outy ref.

  • 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 y out.

  • 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ón Gen de clase genérica es "matriz bidimensional de T", por lo que el tipo del miembro a del tipo construido anterior es "matriz bidimensional de matriz unidimensional de int" o int[,][].

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 de By B se deriva de A, C hereda los miembros declarados en B , así como los miembros declarados en A.

  • 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úblico G(string s) int obtenido sustituyendo el argumento int type del parámetro Tde tipo . D<int> también tiene un miembro heredado de la declaración Bde clase . Este miembro heredado viene determinado por determinar primero el tipo B<int[]> de clase base de D<int> sustituyendo int por T en la especificación B<T[]>de clase base . A continuación, como argumento de tipo a B, int[] se sustituye por U en public U F(long index), lo que produce el miembro public 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, , protectedprivate protected, , internalo 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 formulario E.M, E denotará un tipo que tenga un miembro M. Es un error en tiempo de compilación para E 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 formulario E.M, E indicará una instancia de un tipo que tiene un miembro M. 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. El G 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. El Main 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 clase Ay la clase A 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, internalo private) y, al igual que otros miembros de estructura, el valor predeterminado es private 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 Nodeanidada 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étodo M definido en Base.

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 de Nestedy pasa su propio constructor a Nestedpara proporcionar acceso posterior a los Cmiembros 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 clase Nestedanidada . Dentro Nestedde , el método G llama al método F estático definido en Cy F 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étodo F protegido definido en Derivedla clase base de , Basellamando a a través de una instancia de Derived.

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:

  1. 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#.
  2. 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#.
  3. 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 propiedad Pde solo lectura , reservando así firmas para get_P los métodos y set_P . A La clase B se deriva de A 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 Tdelegado , 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 Lde 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, shortintushortlonguintfloatdoubleulongstringcharbooldecimalenum_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 el new operador y, dado que el new operador no está permitido en un constant_expression, el único valor posible para constantes de reference_types distintos string de es null. 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 y readonly 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.Yprimero , después evalúa B.Zy, por último, evalúa A.X, lo que genera los valores 10, 11y 12.

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 y B se declararon en programas independientes, sería posible A.X depender B.Zde , pero B.Z no podía depender simultáneamente de A.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 Blackmiembros , White, Red, Greeny Blue no se pueden declarar como miembros const porque sus valores no se pueden calcular en tiempo de compilación. Sin embargo, declararlos static 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 y Program2 indican dos programas que se compilan por separado. Dado que Program1.Utils.X se declara como un static readonly campo, la salida del valor por la Console.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 de X y Program1 se vuelve a compilar, la Console.WriteLine instrucción generará el nuevo valor incluso si Program2 no se vuelve a compilar. Sin embargo, había X sido una constante, el valor de X se habría obtenido en el momento Program2 de la compilación y no se vería afectado por los cambios en Program1 hasta Program2 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, , ushortshort, int, uintcharfloat, bool, System.IntPtro .System.UIntPtr
  • Un enum_type tener un tipo de enum_base de byte, sbyte, short, ushort, into uint.

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étodo Thread2. Este método almacena un valor en un campo no volátil denominado resulty, a continuación, almacena true en el campo finishedvolátil . El subproceso principal espera a que el campo finished se establezca trueen y, a continuación, lee el campo result. Puesto finished que se ha declarado volatile, el subproceso principal leerá el valor 143 del campo result. Si no se hubiera declarado el campo finished , se permitiría que el almacén result fuera visible para el subproceso principal después del almacén en finishedy, por lo tanto, para que el subproceso principal lea el valor 0 del campo result.volatile finished Declarar como un volatile 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 y i 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 a i y s 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 y b, el programa es válido. Da como resultado la salida

a = 1, b = 2

porque los campos a estáticos y b se inicializan en 0 (el valor predeterminado para int) antes de que se ejecuten sus inicializadores. Cuando se ejecuta el inicializador para a , el valor de b es cero y, por tanto a , se inicializa en 1. Cuando se ejecuta el inicializador para b , el valor de ya 1es y, por tanto b , se inicializa en 2.

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 Xinicializador y Yel 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 Bel constructor estático (y, por lo tanto B, los inicializadores de campo estáticos) se ejecutarán antes Ade 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, virtualy override.
  • La declaración incluye como máximo uno de los modificadores siguientes: new y override.
  • Si la declaración incluye el abstract modificador, la declaración no incluye ninguno de los siguientes modificadores: static, virtual, sealedo extern.
  • Si la declaración incluye el private modificador, la declaración no incluye ninguno de los siguientes modificadores: virtual, overrideo abstract.
  • Si la declaración incluye el sealed modificador, la declaración también incluye el override modificador .
  • Si la declaración incluye el partial modificador, no incluye ninguno de los siguientes modificadores: new, public, protected, internal, private, virtual, sealed, , , overrideo abstractextern.

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, outy 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, refo 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 , structel 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 refmodificador , 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() donde S es un tipo de valor
  • expresión del formulario default(S) donde S 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, so y son parámetros de valor opcionales y t 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:

Nota: Como se describe en §7.6, los inmodificadores , outy ref forman parte de la firma de un método, pero el params 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, refo 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 en Main, x representa i y y representa j. Por lo tanto, la invocación tiene el efecto de intercambiar los valores de i y j.

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 en G pasa una referencia a s para y a b. Por lo tanto, para esa invocación, los nombres s, ay b todos hacen referencia a la misma ubicación de almacenamiento, y todas las tres asignaciones modifican el campo sde 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 y name se pueden anular la asignación antes de pasarlas a SplitPathy 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[] y string[][] se pueden usar como el tipo de una matriz de parámetros, pero el tipo string[,] no puede. ejemplo final

Nota: No es posible combinar el params modificador con los modificadores in, outo ref. 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 matriz arr como parámetro de valor. La segunda invocación de F crea automáticamente un cuatro elementos int[] con los valores de elemento especificados y pasa esa instancia de matriz como parámetro de valor. Del mismo modo, la tercera invocación de F crea un elemento int[] 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 a F(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 de F es aplicable porque existe una conversión implícita del tipo de argumento al tipo de parámetro (ambos son de tipo object[]). Por lo tanto, la resolución de sobrecarga selecciona la forma normal de Fy el argumento se pasa como un parámetro de valor normal. En las invocaciones segunda y tercera, la forma normal de F no es aplicable porque no existe ninguna conversión implícita del tipo de argumento al tipo de parámetro (el tipo object no se puede convertir implícitamente al tipo object[]). Sin embargo, la forma expandida de es aplicable, por lo que se selecciona mediante resolución de F sobrecarga. Como resultado, la invocación crea un elemento object[] 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 por C.ANC 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 de M con respecto a R .

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 de M, esta es la implementación más derivada de M con respecto a R.
  • De lo contrario, si R contiene una invalidación de M, esta es la implementación más derivada de M con respecto a R.
  • De lo contrario, la implementación más derivada de M con respecto a R es la misma que la implementación más derivada de M con respecto a la clase base directa de R.

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étodo F no virtual y un método Gvirtual . La clase B presenta un nuevo método Fque no es virtual , ocultando así el heredado y también invalida el método Gheredado F. En el ejemplo se genera la salida:

A.F
B.F
B.G
B.G

Observe que la instrucción a.G() invoca B.G, no A.G. Esto se debe a que el tipo en tiempo de ejecución de la instancia (que es B), no el tipo en tiempo de compilación de la instancia (que es A), 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 y D contienen dos métodos virtuales con la misma firma: la introducida por A y la introducida por C. El método introducido oculta C el método heredado de A. Por lo tanto, la declaración de invalidación de D invalida el método introducido por Cy no es posible D invalidar el método introducido por A. 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 protectedprotected 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 de B invoca el método PrintFields declarado en A. 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ón B , invocaría recursivamente el PrintFields método declarado en B, no el declarado en A, ya que PrintFields es virtual y el tipo en tiempo de ejecución de ((A)this) es B.((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 no B incluye un override modificador y, por tanto, no invalida el F método en A. En su lugar, el F método de B oculta el método en Ay 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 de B oculta el método virtual F heredado de A. Puesto que el nuevo F en B tiene acceso privado, su ámbito solo incluye el cuerpo de clase de B y no se extiende a C. Por lo tanto, se permite que la declaración de F en C invalide el F heredado de A.

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: un F método que tiene el sealed modificador y un G método que no lo hace. BEl uso del sealed modificador impide C que se invalide Faú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. El Paint método es abstracto porque no hay ninguna implementación predeterminada significativa. Las Ellipse clases y Box son implementaciones concretas Shape . Dado que estas clases no son abstractas, son necesarias para invalidar el Paint 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 clase B invalida este método con un método abstracto y la clase C 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 el DllImport 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á voidy 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 Mparcial, 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 en string[]y el ToInt32 método está disponible en string, 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. Los G métodos y H son correctos porque todas las rutas de acceso de ejecución posibles terminan en una instrucción return que especifica un valor devuelto. El I 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 refde => , 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, outo ref 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 ser private protected, protected internal, internal, protectedo private.
    • Si la propiedad o el indexador tiene una accesibilidad declarada de protected internal, la accesibilidad declarada por accessor_modifier puede ser private protected, protected private, internal, protectedo private.
    • Si la propiedad o indexador tiene una accesibilidad declarada de internal o protected, la accesibilidad declarada por accessor_modifier será private protected o private.
    • 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 .

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 refde => , 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ública Caption . El descriptor de acceso get de la propiedad Caption devuelve el string almacenado en el campo privado caption . 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 un private campo y el descriptor de acceso set modifica ese private campo y, a continuación, realiza las acciones adicionales necesarias para actualizar completamente el estado del objeto. Dada la Button clase anterior, a continuación se muestra un ejemplo de uso de la Caption 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 de B oculta la P propiedad en A con respecto tanto a la lectura como a la escritura. Por lo tanto, en las instrucciones

B 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 solo P lectura oculta B la propiedad de solo P escritura en A. Sin embargo, tenga en cuenta que una conversión se puede usar para acceder a la propiedad oculta P .

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 dos int campos y x y, para almacenar su ubicación. La ubicación se expone públicamente como una X Y propiedad y como una Location propiedad de tipo Point. Si, en una versión futura de Label, resulta más conveniente almacenar la ubicación como internamente Point , 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 en y su lugar hubiera sido public readonly campos, habría sido imposible realizar este cambio en la Label 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, Outy Error, que representan los dispositivos estándar de entrada, salida y error, respectivamente. Al exponer estos miembros como propiedades, la Console clase puede retrasar su inicialización hasta que se usen realmente. Por ejemplo, al hacer referencia primero a la Out propiedad , como en

Console.Out.WriteLine("hello, world");

se crea el subyacente TextWriter para el dispositivo de salida. Sin embargo, si la aplicación no hace referencia a las In propiedades y Error , 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 propiedad B.Text, incluso en contextos en los que solo se llama al descriptor de acceso set. Por el contrario, la propiedad B.Count no es accesible para la clase M, por lo que la propiedad A.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 y Z es una propiedad abstracta de lectura y escritura. Dado que Z 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, Yy Z 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 de X y el descriptor de acceso set de Y usar la palabra clave base para tener acceso a los descriptores de acceso heredados. La declaración de invalida ambos descriptores de Z acceso abstractos; por lo tanto, no hay miembros de función pendientes abstract en By B 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 –= yde 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 dos Button instancias y adjunta controladores de eventos a los Click 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 la Button clase . Como se muestra en el ejemplo, el campo se puede examinar, modificar y usar en expresiones de invocación de delegado. El OnClick método de la Button clase "genera" el Click 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 , el Click miembro solo se puede usar en el lado izquierdo de los += operadores y –= , como en

b.Click += new EventHandler(...);

que anexa un delegado a la lista de invocación del Click evento y

Click –= 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 a Ev en el lado izquierdo de los += operadores y –= hacen que se invoquen los descriptores de acceso add y remove. Todas las demás referencias a Ev 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. El AddEventHandler método asocia un valor delegado a una clave, el GetEventHandler método devuelve el delegado asociado actualmente a una clave y el RemoveEventHandler 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 thismodificadores de parámetro , refy 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 refde => , 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, outo ref 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 correspondiente bool[] (ya que cada valor del primero ocupa solo un bit en lugar del otro byte), pero permite las mismas operaciones que .bool[]

La siguiente CountPrimes clase usa un BitArray 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 denominado value.
  • 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, donde P es el nombre de propiedad. En una declaración de indexador de invalidación, se obtiene acceso al indexador heredado mediante la sintaxis base[E], donde E 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 tipo T o T? y puede devolver cualquier tipo.
  • Un operador o unario ++ tomará un único parámetro de tipo T o T? y devolverá ese mismo tipo o un tipo derivado de -- él.
  • Un operador o unario true tomará un único parámetro de tipo T o T? y devolverá el tipo bool.false

La firma de un operador unario consta del token de operador (+, -, ++~--!o truefalse) 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 o T?, y puede devolver cualquier tipo.
  • Un binario << o >> operador (§12.11) tomará dos parámetros, el primero de los cuales tendrá el tipo T o T? y el segundo de los cuales tendrá el tipo int o int?, 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 TS 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₀ y T₀ son tipos diferentes.

  • T₀ O S₀ 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 a T o de T a S.

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 y int string, respectivamente, se consideran tipos únicos sin relación. Sin embargo, el tercer operador es un error porque C<T> es la clase base de D<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 de C a int y de int a C, pero no de int a bool. 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 para T, 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 tipo T, 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 tipo T, se omiten las conversiones explícitas definidas por el usuario de S a T . Además:
    • Si o S T es un tipo de interfaz, se omiten las conversiones implícitas definidas por el usuario de S a T .
    • De lo contrario, las conversiones implícitas definidas por el usuario de S a T se siguen considerando.

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 de byte a Digit es explícita, ya que Digit 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 de B, 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 de y es 0 (el valor predeterminado de ) intporque la asignación a y 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 ejemplo

class A
{
    int x = 1, y = -1, count;

    public A()
    {
        count = 0;
    }

    public A(int n)
    {
        count = n;
    }
}

class B : A
{
    double sqrt2 = Math.Sqrt(2.0);
    ArrayList items = new ArrayList(100);
    int max;

    public B(): this(100)
    {
        items.Add("default");
    }

    public B(int n) : base(n - 1)
    {
        max = n;
    }
}

contiene varios inicializadores de variables; también contiene inicializadores de constructor de ambos formularios (base y this). 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 Aconstructor estático se desencadena mediante la llamada a A.Fy la ejecución del Bconstructor estático se desencadena mediante la llamada a B.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 para B.Y, antes del constructor estático de la clase B. YEl inicializador hace Aque se ejecute el constructor porque static se hace referencia al valor de A.X . El constructor estático de A a su vez continúa calculando el valor de Xy, al hacerlo, captura el valor predeterminado de Y, que es cero. A.X Por lo tanto, se inicializa en 1. A continuación, el proceso de ejecución Ade inicializadores de campo estáticos y constructor estático se completa, volviendo al cálculo del valor inicial de Y, 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.Objectel Finalize 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 inparámetro , outo 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 o IEnumerable es object.
  • Tipo de rendimiento de un iterador que devuelve IEnumerator<T> o IEnumerable<T> es T.

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 y IEnumerator<T>, donde T 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 MoveNextmiembros , Currenty 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 esta yield return instrucción. Si la yield return instrucción está dentro de uno o varios try bloques, los bloques finally asociados no se ejecutan en este momento.
    • El estado del objeto enumerador se cambia a suspendido.
    • El MoveNext método vuelve true a su llamador, lo que indica que la iteración ha avanzado correctamente al siguiente valor.
  • Cuando se encuentra una yield break instrucción (§9.4.4.20):
    • Si la yield break instrucción está dentro de uno o varios try bloques, se ejecutan los bloques asociados finally .
    • El estado del objeto enumerador se cambia a después.
    • El MoveNext método vuelve false a su autor de la llamada, lo que indica que la iteración está completa.
  • Cuando se encuentra el final del cuerpo del iterador:
    • El estado del objeto enumerador se cambia a después.
    • El MoveNext método vuelve false 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 .

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 objectde , 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 una yield 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 del Dispose 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 y IEnumerable<T>, donde T 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 y IEnumerator<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 a GetEnumerator. Las invocaciones posteriores de GetEnumerator, 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 inparámetro , outo 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 tipo MyTaskMethodBuilder<T> del generador de tareas y al tipo Awaiter<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 declararse internal 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.Actiony, 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", denominada builder 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 en Start() o después de Start() haber devuelto para avanzar en la máquina de estado.
  • Después Start() de la devolución, el async método invoca builder.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 y IsCompleted es false, la máquina de estado invoca builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine).
    • AwaitUnsafeOnCompleted() debe llamar a awaiter.UnsafeOnCompleted(action) con un Action que llama stateMachine.MoveNext() cuando se completa el awaiter.
  • De lo contrario, el equipo de estado invoca builder.AwaitOnCompleted(ref awaiter, ref stateMachine).
    • AwaitOnCompleted() debe llamar a awaiter.OnCompleted(action) con un Action que llama stateMachine.MoveNext() cuando se completa el awaiter.
  • SetStateMachine(IAsyncStateMachine) la implementación generada por IAsyncStateMachine 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 a stateMachine.

Nota: Para y SetResult(T result) «TaskType»<T> Task { get; }, el parámetro y el argumento respectivamente deben ser identidad convertibles a T. 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 voidfunció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 voidde funciones asincrónicas que devuelven en él y decida cómo propagar excepciones procedentes de ellas.