Partekatu honen bidez:


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 ';'?
    ;

Una class_declaration consta de un conjunto opcional de atributos (Sección 22), seguido de un conjunto opcional de class_modifier (Sección 15.2. 2), seguido de un modificador opcional partial (Sección 15.2.7), seguido de la palabra clave class y un identificador que nombra la clase, seguido de una lista opcional type_parameter_list (Sección 15. 2.3), seguida de una especificación opcional class_base (Sección 15.2.4), seguida de un conjunto opcional de type_parameter_constraints_clauses (Sección 15.2.5), seguida de un class_body (Sección 15.2.6), opcionalmente seguido de un punto y coma.

Una declaración de clase no proporcionará type_parameter_constraints_clausea menos que también proporcione un 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 es permitido en clases anidadas. Especifica que la clase oculta un miembro heredado con el mismo nombre, como se describe en Sección 15.3.5. Es un error de compilación que el modificador new aparezca en una declaración de clase que no sea 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 modificadores abstract, sealed, y static se discuten en las siguientes subcláusulas.

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 es un error de tiempo de compilación utilizar el operador new 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.
  • Una clase abstracta no puede ser sellada.

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á sellada.

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 static no debe incluir un modificador sealed o abstract. (Sin embargo, dado que una clase static no puede instanciarse ni derivarse de ella, se comporta como si fuera a la vez 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.

Es un error de tiempo de compilación 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 namespace_or_type-name es el T en un typeof_expression (§12.8.18) del formato typeof(T).

Se permite que una expresión_primaria (§12.8) haga referencia a una clase estática si

  • El primary_expression es el E en un member_access (§12.8.7) del formato 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 array, una nueva expresión, una expresión de conversión, una expresión 'is', una expresión 'as', una expresión sizeof o una expresión de valor predeterminado. 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
    : '<' decorated_type_parameter (',' decorated_type_parameter)* '>'
    ;

decorated_type_parameter
    : 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. Dado que A 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 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.EnumSystem.ValueTypeo 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 directa entre clases base.

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 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 de las que depende una clase es el cierre transitivo de la relación de dependencia directa.

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 de compilación porque A depende de B.C (su clase base directa), que depende de B (su clase inmediatamente envolvente), 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 de A (porque A es tanto su clase base directa como su clase que lo envuelve inmediatamente), pero A no depende de B (puesto que B no es ni clase base ni clase que lo envuelve 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 tiene un error porque intenta derivar de la clase sellada A.

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 tipos y métodos genéricos pueden especificar opcionalmente restricciones en los parámetros de tipo a través de type_parameter_constraints_clauses.

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 tokens get y set en un descriptor de acceso de propiedad, el token where 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 de tipo de referencia, la restricción de tipo de valor, la restricción de no nulo o la restricción de tipo no administrado. El tipo de clase y la restricción de tipo de referencia pueden incluir el nullable_type_annotation.

Una restricción secundaria puede ser un interface_type o type_parameter, seguido opcionalmente de un nullable_type_annotation. La presencia del nullable_type_annotation indica que se permite que el argumento de tipo sea el tipo de referencia anulable que corresponde a un tipo de referencia no anulable 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 anulable. 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 anulable, se espera que el argumento de tipo sea un tipo de referencia no anulable. 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 anulable, la restricción se satisface tanto con un tipo de referencia no anulable como con un tipo de referencia anulable.

La nulabilidad del argumento de tipo no debe coincidir con la nulabilidad del parámetro de tipo. Un compilador puede emitir una advertencia si la anulabilidad del parámetro de tipo no coincide con la anulabilidad 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 de un tipo no anulable, la anotación de tipo ? indica que el parámetro es del tipo anulable 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 sea un tipo de valor que no admite valores NULL o un tipo de referencia que no admite valores NULL, pero el compilador puede producir una advertencia de diagnóstico.

Dado que notnull no es una palabra clave, en primary_constraint la restricción not null siempre es ambigua sintácticamente con class_type. Por motivos de compatibilidad, si una búsqueda de nombres (§12.8.4) del nombre notnull se realiza correctamente, se tratará como .class_type De lo contrario, se tratará como la restricción not null.

Ejemplo: La siguiente clase muestra el uso de varios argumentos de tipo con restricciones diferentes, lo que indica advertencias que puede emitir un compilador.

#nullable enable
public class C { }
public class A<T> where T : notnull { }
public class B1<T> where T : C { }
public class B2<T> where T : C? { }
class Test
{
    static void M()
    {
        // nonnull constraint allows nonnullable struct type argument
        A<int> x1;
        // possible warning: nonnull constraint prohibits nullable struct type argument
        A<int?> x2;
        // nonnull constraint allows nonnullable class type argument
        A<C> x3;
        // possible warning: nonnull constraint prohibits nullable class type argument
        A<C?> x4;
        // nonnullable base class requirement allows nonnullable class type argument
        B1<C> x5;
        // possible warning: nonnullable base class requirement prohibits nullable class type argument
        B1<C?> x6;
        // nullable base class requirement allows nonnullable class type argument
        B2<C> x7;
        // nullable base class requirement allows nullable class type argument
        B2<C?> x8;
    }
}

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 no anulable. 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

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).

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 En caso contrario, se trata como la restricción no gestionada.

Los tipos de puntero nunca pueden ser argumentos de tipo y no satisfacen ninguna restricción de tipo, ni siquiera las 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 será sealed.
  • El tipo no debe ser uno de los siguientes tipos: System.Array o System.ValueType.
  • El tipo no será object.
  • 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 un parámetro de tipo T se utiliza como restricción para el parámetro de tipo S, entonces S depende de 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, SdependeU 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 de tipo de clase y A tiene una restricción de tipo de clase T, entonces habrá una conversión de identidad o una conversión de referencia implícita de a B o una conversión de referencia implícita de A a B.
  • Si S también depende del parámetro de tipo U y U tiene una restricción de tipo de clase y A tiene una restricción de tipo de clase T, habrá una conversión de identidad o una conversión de referencia implícita de a B o una conversión de referencia implícita de A a B.

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.

Es un error en tiempo de compilación que type_parameter_constraints tenga un primary_constraint de struct o unmanaged y también 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, entonces Cₓ es un tipo Outerₓ.Innerₓ anidado.
  • Si CCₓ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:

Sea R un conjunto de tipos tal que:

  • Para cada restricción de T que sea un parámetro de 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 T que sea un tipo de 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 que sea del tipo T matriz, R contiene System.Array.
  • Para cada restricción con T que sea de tipo 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 efectivo de interfaz de un parámetro T de tipo se define de la siguiente manera:

  • Si T no tiene secondary_constraints, su conjunto de interfaces efectivo está vacío.
  • Si T tiene restricciones de tipo_interfaz pero no restricciones de parámetro_tipo, su conjunto de interfaces efectivo es el conjunto de eliminaciones dinámicas de sus restricciones de tipo_interfaz.
  • Si T no tiene restricciones interface_type pero tiene type_parameter, su conjunto de interfaces efectivas es la unión de los conjuntos de interfaces efectivas de sus type_parameter restricciones.
  • Si T tiene tanto restricciones de tipo_interfaz 1 como restricciones de type_parameter, su conjunto de interfaz efectiva es la unión del conjunto de borrados dinámicos de sus restricciones de interface_type y los conjuntos de interfaz efectiva de sus restricciones de interface_type.

Un parámetro de tipo se reconoce como un tipo de referencia si tiene la restricción de tipo de referencia o si su clase base efectiva no es ni object. 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á obligado a implementar siempre IPrintable.

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 de tipo parcial

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 tiene un significado especial inmediatamente antes de las palabras clave class, structy interface. (Un tipo parcial puede contener declaraciones de método parcial (§15.6.9).

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 coincidirán, 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 pueden declararse en múltiples partes utilizando el modificador partial. 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 está generada por máquina mediante una herramienta de mapeo 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 manejo de los atributos especificados en el tipo o los parámetros de tipo de diferentes partes de una declaración de tipo parcial se discute en §22.3.

15.3 Miembros de la clase

15.3.1 General

Los miembros de una clase consisten en 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 contenedora inmediata. Todos los demás miembros tendrán nombres que difieren del nombre de la clase inmediatamente envolvente.

  • 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 que lo encierra; un miembro privado declarado en una parte es libremente accesible desde otra parte. Es un error de compilación declarar el mismo elemento en más de una parte del tipo, a menos que ese elemento tenga el partial modificador.

Ejemplo:

partial class A
{
    int x;                   // Error, cannot declare x more than once
    partial void M();        // Ok, defining partial method declaration

    partial class Inner      // Ok, Inner is a partial type
    {
        int y;
    }
}

partial class A
{
    int x;                   // Error, cannot declare x more than once
    partial void M() { }     // Ok, implementing partial method declaration

    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 this para el código escrito dentro de 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, para cada parámetro_de_tipo en la declaración del miembro, el argumento_de_tipo 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 en la declaración de la clase genérica Gen es "matriz bidimensional de T", por lo que el tipo del miembro a en el tipo construido anterior es "matriz bidimensional de matriz unidimensional de int" o int[,][].

ejemplo final

Dentro de los miembros de función de instancia, el tipo de this es el tipo de 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 instanciado son los miembros del tipo de clase base inmediato (§15.2.4.2), que se determina sustituyendo los argumentos de tipo del tipo instanciado por cada aparición de los parámetros de tipo correspondientes en el base_class_specification. Estos miembros, a su vez, se transforman sustituyendo cada type_parameter en las declaraciones de miembros, con el type_argument correspondiente de la 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 público no heredado intG(string s) obtenido sustituyendo el argumento de tipo int por el parámetro de tipo T. D<int> también tiene un miembro heredado de la clase B. 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 existe otro miembro heredado accesible N que ya oculte M. Ocultar implícitamente un miembro heredado no se considera un error, pero un compilador emitirá una advertencia a menos que la declaración del miembro de la clase derivada incluya un modificador new para indicar explícitamente que el miembro derivado pretende 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 en consecuencia.

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 combinaciones protected internal y private protected, es un error 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 y de instancia

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 de compilación que E denote 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 vinculación que E denote 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 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 completamente cualificado (Sección 7.8.3) para una declaración de tipo anidada es S.N donde S es el nombre completamente cualificado de la declaración de tipo en la que se declara el tipo N y N es el nombre no cualificado (Sección 7.8.2) de la declaración de tipo anidada (incluyendo cualquier generic_dimension_specifier (Sección 12.8.18).

15.3.9.3 Accesibilidad declarada

Los tipos no anidados pueden tener public o internal como accesibilidad declarada y tener internal como accesibilidad declarada por defecto. 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 que se declara en una clase puede tener cualquiera de los tipos permitidos de accesibilidad declarada y, al igual que otros miembros de la clase, tiene por defecto private accesibilidad declarada.
  • Un tipo anidado declarado en una estructura puede tener cualquiera de tres formas de accesibilidad declarada (public, internal o private) y, al igual que otros miembros de la estructura, su accesibilidad declarada predeterminada es private.

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 anidada privada Node.

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 Nested y pasa su propio `this` al constructor de Nested para proporcionar acceso posterior a los miembros de instancia de C.

ejemplo final

15.3.9.6 Acceso a miembros privados y protegidos del tipo contenedor

Un tipo anidado tiene acceso a todos los miembros accesibles para su tipo contenedor, incluidos los miembros del tipo contenedor que tienen accesibilidad declarada por private y protected.

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 anidada Nested. Dentro de Nested, el método G llama al método estático F definido en C, y F tiene accesibilidad declarada privada.

ejemplo final

Un tipo anidado también puede acceder a los miembros protegidos definidos en un tipo base del tipo que lo contiene.

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 accede al método F protegido definido en la clase base de Derived, Base, llamándolo 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 pueden ser utilizados 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, dentro de la clase externa, el tipo anidado se puede usar sin cualificación; se puede utilizar implícitamente el tipo de instancia de la clase externa 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 P de solo lectura, reservando así firmas para los métodos get_P 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) de tipo delegado T, 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, el nombre de miembro Item está reservado.

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 modificador (new) 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 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 deberá ser sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, string, un enum_type o un reference_type. Cada constant_expression producirá un valor del tipo de destino o de un tipo que se pueda convertir al tipo de destino mediante una conversión implícita (§10.2).

El tipo de una constante debe ser al menos tan accesible como la propia constante (§7.5.5).

El valor de una constante se obtiene en una expresión mediante un simple_name (§12.8.4) o un member_access (§12.8.7).

Una constante puede participar en una constant_expression. Por lo tanto, se puede usar una constante en cualquier construcción que requiera un constant_expression.

Nota: Algunos ejemplos de estas construcciones incluyen case etiquetas, goto case instrucciones, enum declaraciones de miembro, atributos y otras declaraciones constantes. nota final

Nota: Como se describe en §12.23, un constant_expression es una expresión que se puede evaluar completamente en tiempo de compilación. Puesto que la única manera de crear un valor no NULL de un reference_type distinto string de es aplicar 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 el tipo de ese valor no está permitido en una declaración const, o cuando una expresión constante no puede calcular el valor en tiempo de compilación, se puede usar un campo de solo lectura (§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.

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;
}

un compilador debe primero evaluar A.Y, luego evaluar B.Z, y finalmente evaluar A.X, produciendo los valores 10, 11, y 12.

ejemplo final

Las declaraciones constantes pueden depender de constantes de otros programas, pero estas dependencias solo son posibles en una dirección.

Ejemplo: Refiriéndose al ejemplo anterior, si A y B se declararan en programas independientes, sería posible que A.X dependiera de B.Z, pero B.Z no podrí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 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 que no es de solo lectura se modifica mediante una asignación (§12.21). El valor de un campo que no es de solo lectura se puede obtener y modificar mediante operadores de incremento y decremento postfijo (§12.8.16) y operadores de incremento y decremento 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 lectura única

15.5.3.1 General

Cuando una declaración de campo 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 miembros Black, White, Red, Green y Blue no se pueden declarar como constantes 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 estáticos de solo lectura

Las constantes y los campos readonly tienen una semántica de versionado binario 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 espacios de nombres Program1 y Program2 designan dos programas que se compilan por separado. Dado que Program1.Utils.X se declara como campo static readonly, la salida del valor por la declaración Console.WriteLine 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, si X fuera una constante, el valor de X se habría obtenido en el momento en que se compiló Program2 y no se vería afectado por los cambios en Program1 hasta que Program2 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). Estas optimizaciones pueden ser realizadas por el compilador, 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 "semántica de adquisición"; es decir, se garantiza que ocurra antes de cualquier referencia a la memoria que ocurra 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 liberació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.

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 tipo_de_referencia.
  • Un type_parameter que se sabe que es un tipo de referencia (§15.2.5).
  • El tipo byte, sbyte, short, ushort, int, uint, char, float, bool, System.IntPtr, o System.UIntPtr.
  • Un enum_type que tiene un tipo enum_base de byte, sbyte, short, ushort, int o 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 en true, y a continuación lee el campo result. Dado que finished se ha declarado volatile, el hilo principal leerá el valor 143 del campo result. Si el campo finished no hubiera sido declarado volatile, entonces sería permisible que el almacenamiento a result fuera visible para el hilo principal después del almacenamiento a finished, y por lo tanto que el subproceso principal leyera el valor 0 del campo result. Declarar finished como campo volatile 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. Para los campos de instancia, 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 a x cuando los inicializadores de campos estáticos se ejecutan y se producen asignaciones a i y s cuando se ejecutan los inicializadores de campos 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. 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 a ya es 1, por lo que b se inicializa a 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 inicializador de X y del inicializador de Y podrían ocurrir en cualquier orden; solo están restringidos a ocurrir 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 la ejecución de constructores estáticos (como se define en §15.12) establecen que el constructor estático de B, y por lo tanto, los inicializadores de campo estáticos de B, se ejecutarán antes que el constructor estático y los inicializadores de campo de A.

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 referirse a this en un inicializador de variable, ya que es un error en tiempo de compilación que un inicializador de variable haga referencia a cualquier miembro de una 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'
    | 'readonly'        // direct struct members only
    | 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).
  • Al reconocer una method_body, si son aplicables tanto la invocación condicional nula como la expresión, se elegirá la primera de estas.

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), virtual (§15.6.4), override (§15.6.5), sealed (§15.6.6), abstract (§15.6.7), extern (§15.6.8) y async (§15.14). Además, una method_declaration que contiene directamente un struct_declaration puede incluir el readonly modificador (§16.4.12).

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 es returns-by-value y devuelve un valor.

El return_type de una declaración de método de retorno por valor o sin valor de retorno especifica el tipo de resultado devuelto por el método, si lo hay. Solo un método que no devuelve valor puede incluir el modificador partial (§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.14.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 . Las cláusulas opcionales type_parameter_constraints_clause especifican las restricciones para los parámetros de tipo.

Un genérico declaración_de_método para la implementación explícita de un miembro de la interfaz no tendrá ningún type_parameter_constraints_clauses; la declaración hereda cualquier restricción de las restricciones del método de la 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 bloque de código 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 consiste en =>, seguido de una null_conditional_invocation_expression o expression, y un punto y coma, y denota una única expresión a realizar cuando se invoca el método.

Para métodos abstractos y externos, el method_body consiste simplemente en un 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 que devuelve por referencia puede ser 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 parámetros_de_tipo del método están en el ámbito a lo largo de toda la declaración_del_método y se pueden utilizar para formar tipos durante ese ámbito en tipo_de_retorno o tipo_de_retorno_ref, cuerpo_del_método o cuerpo_del_método_ref, y cláusulas_de_restricciones_de_parámetros_de_tipo, pero 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 tipo struct o un parámetro de tipo restringido a un struct, el modificador this se puede combinar con el modificador ref o con el modificador in, pero no con el modificador out. 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:

  • un 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 aparecen en una declaración de método parcial de implementación (Sección 15.6.9), una implementación explícita de miembro de interfaz (Sección 18.6.2), una declaración de indexador de un solo parámetro (Sección 15.9), o en una declaración de operador (Sección 15.10.1) un compilador debe dar una advertencia, ya que estos miembros nunca pueden ser invocados de una manera que permita omitir los 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 la parameter_list para M, i es un parámetro obligatorio ref, d es un parámetro de valor obligatorio, b, s, o y t son parámetros de valor opcionales y a es una matriz de parámetros.

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. Es un error que dos miembros en 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, los parámetros pueden ser referenciados por sus identificadores en expresiones simple_name (§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.15) ni en funciones asincrónicas (§15.14).

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 tanto para a como para 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 struct, dentro de un método de instancia, accesor de instancia (§12.2.1) o constructor de instancia con un inicializador de constructor, this palabra clave se comporta exactamente como un parámetro de referencia del struct (§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

Observe que las variables dir y name pueden desasignarse antes de pasarlas a SplitPath, y que se consideran definitivamente asignadas tras 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 int[] de cuatro elementos con los valores de los elementos dados 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. La razón de la ambigüedad es que un object[] es en sí mismo implícitamente convertible al tipo object. Sin embargo, la ambigüedad no representa ningún problema, ya que puede resolverse 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 F es aplicable, por lo que se selecciona mediante resolución de sobrecarga. Como resultado, se crea un object[] de un solo elemento mediante la invocación, y el único elemento del array se inicializa con el valor del argumento especificado (que en sí mismo es una referencia a un 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 opera sobre una instancia específica, y es un error de compilación referirse a 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 de 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 de argumentos A en una instancia con un tipo C en tiempo de compilación y un tipo R 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 C, N y A, para seleccionar un método M específico del conjunto de métodos declarados en C y heredados por él. 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 introduce un nuevo método no virtual F, ocultando así el método heredado F y también anula el método heredado . 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 por C oculta 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 mediante el acceso a una instancia de D a través de un tipo menos derivado en el cual 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 base invalidado por una declaración de invalidación se conoce como el método base invalidado. Para un método M de sobrecarga 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 public, si es protected, si es protected internal, o si es internal o private protected y se declara en el mismo programa que C.

Se produce un error en tiempo de compilación a menos que se cumplan todas las siguientes condiciones para una declaración de invalidación:

  • Un método base invalidado se puede encontrar como se describió anteriormente.
  • Hay exactamente uno de estos métodos base invalidados. Esta restricción solo tiene efecto si el tipo de clase base es un tipo construido donde la sustitución de argumentos de tipo hace que la firma de dos métodos sea la misma.
  • El método base invalidado es un método virtual, abstracto o de invalidación. En otras palabras, el método base invalidado no puede ser estático ni no virtual.
  • El método base invalidado no es un método sellado.
  • Hay una conversión de identidad entre el tipo de valor devuelto del método base invalidado y el método override.
  • La declaración de invalidación y el método base invalidado tienen la misma accesibilidad declarada. Es decir, una declaración de invalidación no puede cambiar la accesibilidad del método virtual. Sin embargo, si el método base invalidado está protegido internamente y se declara en un ensamblado diferente al ensamblado que contiene la declaración de invalidación, se protegerá la accesibilidad declarada de la declaración de invalidación.
  • La declaración de invalidación no especifica ningún type_parameter_constraints_clauses. En su lugar, las restricciones se heredan del método base invalidado. Las restricciones que son parámetros de tipo en el método invalidado pueden reemplazarse por argumentos de tipo en la restricción heredada. Esto puede provocar restricciones que no son válidas cuando se especifican explícitamente, como tipos de valor o tipos sellados.

Ejemplo: a continuación se muestra cómo funcionan las reglas de invalidación para clases genéricas:

abstract class C<T>
{
    public virtual T F() {...}
    public virtual C<T> G() {...}
    public virtual void H(C<T> x) {...}
}

class D : C<string>
{
    public override string F() {...}            // Ok
    public override C<string> G() {...}         // Ok
    public override void H(C<T> x) {...}        // Error, should be C<string>
}

class E<T,U> : C<U>
{
    public override U F() {...}                 // Ok
    public override C<U> G() {...}              // Ok
    public override void H(C<T> x) {...}        // Error, should be C<U>
}

ejemplo final

Una declaración de invalidación puede tener acceso al método base invalidado mediante un base_access (§12.8.15).

Ejemplo: en el código siguiente

class A
{
    int x;

    public virtual void PrintFields() => Console.WriteLine($"x = {x}");
}

class B : A
{
    int y;

    public override void PrintFields()
    {
        base.PrintFields();
        Console.WriteLine($"y = {y}");
    }
}

la base.PrintFields() invocación de B invoca el método PrintFields declarado en A. Un base_access desactiva el mecanismo de invocación virtual y simplemente trata el método base como un método no-virtual. Si se hubiera escrito la invocación B , invocaría recursivamente el ((A)this).PrintFields() método declarado en PrintFields, no el declarado en B, ya que A es virtual y el tipo en tiempo de ejecución de PrintFields es ((A)this).B

ejemplo final

Solo al incluir un override modificador, un método puede sobrescribir 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 método F en B no incluye un modificador override y, por tanto, no sobrescribe el método F 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 modificador override. 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. El uso del modificador B por parte de sealed impide que C anule F.

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 clases Ellipse y Box son implementaciones concretas de 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 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 mediante el cual se consigue la vinculación a un método externo está 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 partial 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 habrá una declaración de método parcial que tenga una firma específica, y solo habrá una declaración de método parcial de implementación que tenga una firma específica. 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.

Únicamente un método parcial definitorio participa en la resolución de sobrecarga. 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 utiliza el nombre del argumento de la implementación y no el de la declaración de método parcial definida.

nota final

Si ninguna parte de una declaración de tipo parcial contiene una declaración de implementación para un determinado método parcial, cualquier instrucción de expresión que lo invoque se elimina simplemente 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 método parcial en sí también se elimina y no formará parte 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.5).

  • Es un error en tiempo de compilación referirse a M dentro de una función anónima que se transforma 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. Considere 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, sin embargo, que se da otra parte 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, cuando su clase estática envolvente está en el ámbito, un método de extensión puede invocarse utilizando la sintaxis de invocación de métodos de instancia (Sección 12.8.10.3), utilizando la expresión del receptor 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 de un método en una declaración de método consta de un bloque, una expresión o un punto y coma.

Las declaraciones de métodos abstractos y externos no proporcionan una implementación de método, por lo que sus cuerpos de método están simplemente constituidos por 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 void si el tipo de valor devuelto es void, o si el método es asincrónico y el tipo de valor devuelto es «TaskType» (§15.14.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.14.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 de devolución por valor (§15.6.1), cada instrucción return en el cuerpo del método debe especificar una expresión que se pueda convertir implícitamente al tipo de retorno efectivo.

Para un método de devolución por referencia (§15.6.1), cada instrucción return en el cuerpo de ese método especificará una expresión cuyo tipo sea el del tipo return efectivo y tenga un ref-safe-context de caller-context (§9.7.2).

Para los métodos returns-by-value y returns-by-ref, el punto final del cuerpo del método no debe ser alcanzable. En otras palabras, no se permite que el control fluya más allá 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 F que devuelve valores produce un error en tiempo de compilación porque el control puede salir al final del cuerpo del método. Los métodos G y H son correctos porque todas las rutas de acceso de ejecución posibles terminan en una instrucción de retorno que especifica un valor devuelto. El I método es correcto, porque su cuerpo es equivalente a un bloque con una sola sentencia de retorno 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 accesores 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'
    | 'readonly'        // direct struct members only
    | 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).

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). Además, una property_declaration que contiene directamente un struct_declaration puede incluir el readonly modificador (§16.4.11).

  • La primera declara una propiedad cuyo valor no es de referencia. Su valor es del tipo type. Este tipo de propiedad puede ser legible y/o escribible.
  • El segundo declara una propiedad con valores ref. Su valor es una variable_reference (§9.5), que puede ser readonly, a una variable de tipo tipo. Este tipo de propiedad solo es legible.

Un declaración_de_propiedad 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 consistir en un cuerpo de instrucción o un cuerpo de expresión. En un cuerpo de instrucción, accessor_declarations, que deberá estar encerrado entre los 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 => una expresiónE y un punto y coma es exactamente equivalente al cuerpo de la instrucción { get { return E; } }, y por lo tanto, solo se puede usar para especificar propiedades de solo lectura en las que el resultado del accesor get se da por 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 consistir en un cuerpo de instrucción o un cuerpo de expresión. En un bloque de instrucciones, un get_accessor_declaration declara el accesor 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 cuerpo de expresión consistente en => seguido de ref, una variable_referenceV y un punto y coma es exactamente equivalente al cuerpo de declaración { get { return ref V; } }.

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_bodyen sus accessor_declarations deberá 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 de tiempo de compilación referirse a this en los accesores 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.

Accesores 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 indexadores, debe sustituir 'property/properties' por 'indexador/indexadores' y consultar la lista de diferencias entre las propiedades y los indexadores proporcionada 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'
    | 'readonly'        // direct struct members only
    ;

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 modificador_de_acceso opcional, el token o get, seguido de un cuerpo_de_acceso.

Para una propiedad valorada como ref, la ref_get_accessor_declaration consta de atributos opcionales, un modificador_del_accesor opcional, el token get, seguido de un ref_accessor_body.

El uso de accessor_modifierse rige por las restricciones siguientes:

  • Un accessor_modifier no se usará en una interfaz ni en la implementación explícita de un miembro de interfaz.
  • El accessor_modifierreadonly solo se permite en una property_declaration o indexer_declaration que contiene directamente un struct_declaration (§16.4.11, §16.4.13).
  • 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 accesor debe coincidir con el accessor_modifier, si existe, del accesor que se va a sobrescribir.
  • 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 las propiedades de valor no ref indicadas en abstract y extern, cualquier accessor_body para cada accesor especificado es simplemente un punto y coma. Una propiedad no abstracta, no externa, pero no un indexador, también puede tener el `accessor_body` para todos los descriptores de acceso especificados como un punto y coma, en cuyo caso es una propiedad implementada automáticamente (§15.7.4). Una propiedad implementada automáticamente deberá tener al menos un descriptor de acceso get. Para los accesores de cualquier otra propiedad no abstracta, no externa, la accessor_body es:

  • bloque que especifica las instrucciones que se van a ejecutar cuando se invoca el accesor correspondiente; o
  • un cuerpo de expresión, que consta de => seguido de una expresión y un punto y coma, y denota una única expresión que se ejecutará cuando se invoque el descriptor de acceso correspondiente.

Para las propiedades con valor ref de abstract y extern, el ref_accessor_body es simplemente un punto y coma. Para el descriptor de acceso de cualquier otra propiedad no abstracta ni externa, el ref_accessor_body es:

  • un bloque que especifica las instrucciones que se van a ejecutar cuando se invoque el descriptor de acceso get; o
  • un cuerpo de expresión, que consta de => seguido por ref, una referencia de variable 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 no revalorizada corresponde a un método sin parámetros con un valor de retorno del tipo de la 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á alcanzar el extremo de un accesor '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. Que referencia variable, como cualquier otro, se utiliza para leer o, para los que no son de sólo lectura referencia_variables, escribir la variable referenciada 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 accesor de establecimiento corresponde a un método con un único parámetro de valor del tipo de propiedad y un void tipo de retorno. 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, –-), 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 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 que una variable local o una declaración constante en un descriptor de acceso set tenga ese nombre.

En función de la presencia o ausencia de los descriptores de acceso get y set, una propiedad se clasifica como sigue:

  • Se dice que una propiedad que incluye un accesor get y un accesor set es una propiedad de lectura y escritura.
  • Una propiedad que solo tiene un descriptor de acceso get se dice que 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 un write-only property. Excepto como destino de una asignación, es un error de compilación hacer referencia a una propiedad de escritura exclusiva en una expresión.

Nota: Los operadores de prefijo y postfijo ++ y -- y los operadores de 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 accesores get y set de una propiedad no son miembros distintos y no es posible declarar los accesores 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 ninguna propiedad de lectura-escritura. En su lugar, declara dos propiedades con el mismo nombre, una de solo lectura y otra de solo 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 propiedad P en B oculta la propiedad P en A con respecto tanto a la lectura como a la escritura. Por lo tanto, en las declaraciones

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 provoca que se informe de un error en tiempo de compilación, ya que la propiedad de solo lectura P en B oculta la propiedad de solo escritura P en A. Sin embargo, tenga en cuenta que un casteo 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 xy, para almacenar su ubicación. La ubicación se expone públicamente tanto como una propiedad X y Y como una propiedad Location 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 y y hubieran sido en cambio campos de public readonly, habría sido imposible realizar tal cambio en la clase Label.

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 accesores 'get' no significa que los accesores 'get' deban siempre escribirse de manera sencilla 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 referenciar por primera vez la propiedad Out, como en

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

se crea la base 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 autopropiedad para abreviar) es una propiedad no abstracta, no externa, sin valor ref con solo punto y coma accessor_bodys. 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 accesores de propiedad implementados automáticamente, incluso dentro del tipo que lo contiene. Si la propiedad automática no tiene ningún accesor 'set', el campo subyacente se considera readonly (§15.5.3). Al igual que un readonly campo, una propiedad automática de solo lectura también puede asignarse dentro de un constructor de la clase que lo contiene. Una asignación de este tipo 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 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, es posible que ese campo tenga atributos de destino de campo aplicados directamente a él a través de la propiedad implementada automáticamente property_declaration (§15.7.1).

Ejemplo: el código siguiente

[Serializable]
public class Foo
{
    [field: NonSerialized]
    public string MySecret { get; set; }
}

resulta en que el atributo de campo NonSerialized 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 consulta de miembros (§12.5) ni 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 del operador ++ o del operador -- (§12.8.16, §12.9.6), los accesores get y set deben existir y ser 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 que se ha seleccionado una propiedad con valores ref o un indexador con valores ref concretos (si el uso es como un valor, el destino de una asignación simple o el destino de una asignación compuesta), se usa el dominio de accesibilidad del descriptor de acceso get implicado 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 Los descriptores de acceso virtuales, sellado, de reemplazo 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 indexadores, debe sustituir 'property/properties' por 'indexador/indexadores' y consultar la lista de diferencias entre las propiedades y los indexadores proporcionada en §15.9.2. nota final

Una declaración de propiedad virtual especifica que los descriptores de acceso de la propiedad son virtuales. El modificador virtual se aplica a todos los descriptores de acceso no privados de una propiedad. Cuando un descriptor de acceso de una propiedad virtual tiene el privateaccessor_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 requiere que las clases derivadas no abstractas proporcionen su propia implementación para los accesores sobrescribiendo 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 accesor.

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 accesores de una propiedad virtual heredada se pueden invalidar en una clase derivada mediante la inclusión de una declaración de propiedad que especifique una directiva override. Esto se conoce como una declaración de propiedad de sobrescritura. Una declaración de propiedad de sobrescritura no declara una nueva propiedad. En su lugar, simplemente se enfoca en las implementaciones de los accesores 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 solo tiene un único descriptor de acceso (es decir, si la propiedad heredada es de solo lectura o de solo escritura), la propiedad sustituida solo incluirá ese descriptor de acceso. Si la propiedad heredada incluye los dos métodos de acceso (es decir, si la propiedad heredada es de lectura y escritura), la propiedad que reemplaza puede incluir un único método de acceso o ambos métodos de acceso. Habrá una conversión de identidad entre el tipo de invalidación y la propiedad heredada.

Una declaración de propiedad sobrecargada puede incluir el modificador sealed. El uso de este modificador impide que una clase derivada invalide aún más la propiedad . Los accesores de una propiedad sellada también están sellados.

Excepto por las diferencias en la sintaxis de declaración e invocación, los descriptores de acceso virtuales, sellados, sobrescritos y abstractos se comportan exactamente como los métodos virtuales, sellados, sobrescritos y abstractos. 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 de retorno del mismo tipo que la propiedad y los mismos modificadores que la propiedad que lo contiene.
  • 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 de X y el descriptor de acceso set de Y utilizan la palabra clave base para acceder a los descriptores de acceso heredados. La declaración de Z sobrescribe ambos descriptores de acceso abstractos; por lo tanto, no hay miembros de función pendientes abstract en B y 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'
    | 'readonly'        // direct struct members only
    | 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 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. Además, una event_declaration contenida directamente por un struct_declaration puede incluir el readonly modificador (§16.4.12).

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 externos ni abstractos, un compilador los proporcionará automáticamente (§15.8.2); para eventos de extern, 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.

Es un error en tiempo de compilación que una event_declaration incluya tanto el modificador abstract como event_accessor_declaration.

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 externa no proporciona una implementación real, es un error que incluya tanto el modificador extern como la event_accessor_declaration.

Se produce un error en tiempo de compilación al tener un variable_declarator de una declaración de evento con un modificador abstract o external que incluya un variable_initializer.

Un evento se puede usar como operando izquierdo de los operadores += y -=. Estos operadores se utilizan, respectivamente, para adjuntar controladores de eventos a un evento o para quitarlos, y los modificadores de acceso del evento determinan en qué contextos se permiten dichas 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 de la forma x += y o x –= y, cuando x es un evento el resultado de la operación tiene el tipo void (Sección 12.21.5) (en lugar de tener el tipo x, con el valor de x después de la asignación, como para otros operadores += y -= definidos sobre tipos que no son 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 de tipo campo

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 ser utilizado de esta manera, un evento no debe ser abstracto o externo, y no debe incluir explícitamente event_accessor_declaration. 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 delegados. 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 presente que la invocación del delegado está precedida de una comprobación que garantiza que el delegado no sea null y que esta comprobación se realice en una copia local para garantizar la seguridad de hilos.

Fuera de la declaración de la clase Button, el miembro Click solo puede utilizarse en el lado izquierdo de los operadores += y –=, como en

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

que añade un delegado a la lista de invocación del evento Click, y

Click –= new EventHandler(...);

que quita un delegado de la lista de invocación del evento Click.

ejemplo final

Al compilar un evento de tipo campo, un compilador creará automáticamente un almacenamiento para contener el delegado, y creará los descriptores de acceso para el evento que añadan o eliminen manejadores de eventos al campo delegado. Las operaciones de adición y eliminación son seguras para los hilos, y pueden (pero no es necesario) realizarse mientras se mantiene el bloqueo (§13.13) en el objeto contenedor para un evento de instancia, o en el objeto (System.Type) 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 utiliza un evento en una asignación de evento, se utiliza el descriptor de acceso de evento apropiado. 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 accesor 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 de tiempo de compilación hacer referencia a 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 Los descriptores de acceso virtuales, sellado, de reemplazo y abstractos

Una declaración de evento virtual especifica que los accesores de ese evento son virtuales. El virtual modificador se aplica a ambos accesores 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 requiere que las clases derivadas no abstractas proporcionen su propia implementación para los accesores sobrescribiendo el evento. Dado que un accesor para una declaración de evento abstracto no proporciona ninguna implementación real, no debe 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 accesores de tal evento también son abstractos.

Las declaraciones de eventos abstractos solo se permiten en clases abstractas (§15.2.2.2).

Los accesores de un evento virtual heredado pueden ser anulados en una clase derivada al incluir una declaración de evento que especifique un override modificador. Esto se conoce como una declaración de evento de sobrescritura. Una declaración de evento de sobrescritura no declara un nuevo evento. En su lugar, simplemente se enfoca en las implementaciones de los accesores 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 que invalida y el evento invalidado, y se especificarán los accesores add y remove dentro de la declaración.

Una declaración de evento de sobrescritura puede incluir el modificador sealed. El uso del this modificador impide que una clase derivada invalide aún más el evento. Los accesores de un evento sellada también están sellados.

Es un error de compilación que una declaración de evento de sobrescritura incluya un modificador new.

Excepto por las diferencias en la sintaxis de declaración e invocación, los descriptores de acceso virtuales, sellados, sobrescritos y abstractos se comportan exactamente como los métodos virtuales, sellados, sobrescritos y abstractos. 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'
    | 'readonly'        // direct struct members only
    | 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).

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), virtual (§15.6.4), override (§15.6.5), sealed (§15.6.6), abstract (§15.6.7) y extern (§15.6.8) modificadores. Además, una indexer_declaration que contiene directamente un struct_declaration puede incluir el readonly modificador (§16.4.12).

  • La primera declara un indizador con valores no referenciados. Su valor es del tipo type. Este tipo de indexador puede ser legible y/o escribible.
  • El segundo declara un indexador ref-valued. Su valor es una variable_reference (§9.5), que puede ser readonly, a una variable de tipo 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), 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 que imitan los elementos de un array, el término tipo de elemento definido para un array también se utiliza 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 deberá estar encerrado entre los 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 de la instrucción { get { return E; } }, y, por tanto, solo se puede usar para especificar indizadores de solo lectura donde el resultado del descriptor de acceso get se determina mediante una sola expresión.

Un ref_indexer_body puede constar de un bloque de instrucciones o de un cuerpo de expresión. En el cuerpo de una instrucción get_accessor_declaration declara el descriptor de acceso get (Sección 15.7.3) del indizador. 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 de ref, un variable_referenceV y un punto y coma es exactamente equivalente al cuerpo de la instrucción { get { return ref V; } }.

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_bodyen sus accessor_declarations deberá 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 que un descriptor de acceso del indizador declare una variable local o constante local con el mismo nombre que un parámetro del indizador.
  • En una declaración de propiedad de sobrescritura, se accede a la propiedad heredada usando la sintaxis base.P, donde P es el nombre de la propiedad. En una declaración de sobrecarga de indexador, se obtiene acceso al indexador heredado mediante la sintaxis base[E], donde E es una lista separada por comas de expresiones.
  • No existe el 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 se aplica también a los términos definidos. En concreto, la propiedad de lectura y escritura se convierte en indexador de lectura y escritura, 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 o bien un punto y coma, o bien un cuerpo de bloque (§15.6.1) o bien 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 consiste en => seguido por una expresión y un punto y coma, y denota una sola expresión que se ejecuta 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á tanto un public como un static modificador.
  • 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 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 consiguiente, el new modificador nunca es necesario y, por consiguiente, 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 operador unario +, -, ! (solo negación lógica) o ~ tomará un único parámetro de tipo T o T? y puede devolver cualquier tipo.
  • Un operador unario ++ o -- tomará un único parámetro de tipo T o T? y devolverá ese mismo tipo o un tipo derivado de él.
  • Un operador unario true o false tomará un único parámetro de tipo T o T? y devolverá el tipo bool.

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 operador binario << o >> (Sección 12.11) debe tomar dos parámetros, el primero de los cuales debe ser de tipo T o T? y el segundo de tipo int u int?, y puede devolver cualquier tipo.

La firma de un operador binario consta del token de operador (+, -, *, /, %, &, |, ^, <<, >>, ==, !=, >, <, >=, o <=) y de 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 de retorno y sus correspondientes tipos de parámetros. 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 implicit introduce una conversión implícita definida por el 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 explicit introduce una conversión explícita definida por el 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 S y un tipo objetivo T determinado, si S o T son tipos anulables, S₀ y T₀ deben referirse a sus tipos subyacentes; si no, 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 ST de destino solo si se cumplen todas las siguientes condiciones:

  • S₀ y T₀ son tipos diferentes.

  • S₀ O T₀ es el tipo de instancia de la clase o estructura que contiene la declaración del operador.

  • Ni S₀ ni T₀ son 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 intstring, 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).ST
  • 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 ST 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, excepto object, los operadores declarados por el tipo Convertible<T> anterior no entran en conflicto con las 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 desde o hacia 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. (Esta es la única forma de miembro para la que el tipo de retorno es parte de 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 Digitbyte 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 instancias

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 el modificador this para parámetros solo se aplica a los métodos de extensión (§15.6.10), ningún parámetro en la parameter_list de un constructor deberá contener el modificador this. 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 consiste en => seguido de una expresión y un punto y coma, y denota una única expresión para inicializar una nueva instancia de la clase.

Un constructor_body que es un bloque o un cuerpo de expresión corresponde exactamente al bloque de un método de instancia con un tipo de retorno void (§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_expressions (§12.8.17.2) y a través de constructor_initializers.

15.11.2 Inicializadores de constructor

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 de la forma

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 de una clase no externo no tiene inicializador de constructor, o tiene un inicializador de constructor de la forma base(...), ese constructor realiza implícitamente las inicializaciones especificadas por los variable_initializer de 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).

Los constructores de instancia de extern no necesitan ejecutar inicializadores de variables.

15.11.4 Ejecución del constructor

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. Esta ordenación garantiza que todos los campos de instancia sean inicializados por sus inicializadores de variable antes de que se ejecute cualquier instrucción que tenga 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 por defecto de un int) porque la asignación a y no se ejecuta hasta después de que retorne el compilador de la clase base. Es útil pensar en inicializadores de variables de instancia y inicializadores de constructores como instrucciones que se insertan automáticamente antes del constructor_body. 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 la forma

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 cuerpo de expresión corresponde exactamente al void de un método estático con un tipo de retorno (§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 alguno 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 creará un nuevo conjunto de campos estáticos (§15.5.2) para ese tipo cerrado determinado. Cada uno de los campos estáticos se inicializará en su valor predeterminado (§15.5.5). A continuación:

  • Si no hay ningún constructor estático o un constructor estático no extern, haga lo siguiente:
    • los inicializadores de campo estáticos (§15.5.6.2) se ejecutarán para esos campos estáticos;
    • A continuación, se ejecutará el constructor estático no externo, si existe.
  • De lo contrario, se ejecutará un constructor estático externo si existe. No se requiere que los constructores estáticos extern ejecuten los inicializadores de variables estáticas.

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. Yhace que Ase static ejecute el constructor porque se hace referencia A.X al valor de. 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, se completa el proceso de ejecución de los inicializadores de campo estáticos y del constructor estático de A, volviendo al cálculo del valor inicial de Y, cuyo resultado 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 simple que se ejecuta con el fin de finalizar una instancia de la clase.

Un finalizer_body que es un bloque de código o cuerpo de expresión corresponde exactamente al method_body de un método de instancia con un void tipo de retorno (§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();
    }
}

es

B's finalizer
A's finalizer

dado que se llama a los finalizadores de una cadena de herencia en orden, desde el más derivado al menos derivado.

ejemplo final

Los finalizadores se implementan reemplazando el método Finalize virtual en System.Object. Los programas de C# no pueden sobrescribir este método ni llamarlo (o sus versiones sobrescritas) 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 comportará como si este método, y sus modificaciones, no existieran.

Ejemplo: Por lo tanto, este programa:

class A
{
    void Finalize() {}  // Permitted
}

es válido y el método mostrado oculta el método System.Object de Finalize.

ejemplo final

Para obtener una explicación del comportamiento cuando se produce una excepción desde un finalizador, consulte §21.4.

15.14 Funciones asincrónicas

15.14.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 de compilación que la lista de parámetros de una función asincrónica especifique cualquier parámetro in, out o ref, o cualquier parámetro de tipo ref struct.

El return_type de un método asincrónico debe ser void, un tipo de tarea o un tipo de iterador asincrónico (§15.15). Para un método asincrónico que genera un valor de resultado, un tipo de tarea o un tipo de iterador asincrónico (§15.15.3) 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 de System.Threading.Tasks.Task<TResult> y System.Threading.Tasks.ValueTask<T> 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 que devuelve un tipo de tarea es 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 fallida registra una excepción pertinente. Un succeeded«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> 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.14.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 constructor de tareas también debe estar anidado en ese mismo tipo. nota final

Una función asíncrona tiene la capacidad de suspender la evaluación mediante expresiones await (§12.9.8) en su cuerpo. La evaluación se puede reanudar posteriormente en el punto de la expresión await de suspensión mediante un delegado de reanudación. El delegado de reanudación es de tipo System.Action, y cuando se invoca, la evaluación de la invocación de la función asincrónica se reanudará desde la expresión await en el punto donde se dejó. El llamador de la llamada actual de una invocación de función asincrónica es el llamador original si la invocación de función nunca se ha suspendido, o el llamador más reciente del delegado de la reanudación, de lo contrario.

15.14.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; }
}

Un compilador deberá generar código que utilice el "TaskBuilderType" para implementar la semántica de suspender y reanudar la evaluación de la función asincrónica. Un compilador utilizará el "TaskBuilderType" como sigue:

  • «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 constructor deberá llamar a stateMachine.MoveNext() ya sea en Start() o después de que Start() haya completado su retorno para avanzar en la máquina de estados.
  • Después de que Start() devuelva, el método async invoca builder.Task para que la tarea regrese del método asincrónico.
  • Con cada llamada a stateMachine.MoveNext(), se avanzará en la máquina de estados.
  • Si la máquina de estado se completa correctamente, se llama a builder.SetResult() con el valor devuelto del método, si existe.
  • De lo contrario, si se produce una excepción en la máquina de estado e, se llama a builder.SetException(e).
  • 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() debería llamar a awaiter.UnsafeOnCompleted(action) con un Action que llame a stateMachine.MoveNext() cuando se complete el awaiter.
  • De lo contrario, la máquina de estados invoca builder.AwaitOnCompleted(ref awaiter, ref stateMachine).
    • AwaitOnCompleted() debería llamar a awaiter.OnCompleted(action) con un Action que llame a stateMachine.MoveNext() cuando se complete el awaiter.
  • SetStateMachine(IAsyncStateMachine) puede ser llamada por la implementación generada por el compilador IAsyncStateMachine para determinar la instancia del generador asociada a una instancia de la máquina de estado, especialmente en los casos en que la máquina de estado se implementa como un tipo de valor.
    • Si el generador llama a stateMachine.SetStateMachine(stateMachine), el stateMachine llamará a builder.SetStateMachine(stateMachine) en la instancia del generador asociada constateMachine.

Nota: Tanto para SetResult(T result) como para «TaskType»<T> Task { get; }, el parámetro y el argumento respectivamente deben ser convertibles de identidad 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.14.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 (al alcanzar una expresión await) o finaliza, momento en el cual el control se devuelve al llamador, junto con la tarea de retorno.

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 al alcanzar una instrucción de retorno o llegar al final del cuerpo, cualquier valor de resultado se registra en la tarea de retorno, que se coloca en un estado completado con éxito.
  • Si el cuerpo de la función termina debido a un uncaught OperationCanceledException, la excepción se registra en la tarea de retorno que se pone 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.14.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 es notificado cuando la evaluación de un void-que devuelve la función asíncrona comienza, finaliza con éxito o provoca el lanzamiento de una excepción no capturada.

Esto permite que el contexto mantenga un seguimiento de cuántas funciones asincrónicas que retornan void están ejecutándose bajo él, y decida cómo propagar las excepciones que surgen de ellas.

15.15 Iteradores sincrónicos y asincrónicos

15.15.1 Información general

Un miembro de función (§12.6) o función local (§13.6.4) implementado mediante un bloque de iterador (§13.3) se denomina iterador. Un bloque de iterador se puede usar como el cuerpo de un miembro de una función siempre que el tipo de retorno del miembro de la función correspondiente sea una de las interfaces del enumerador (§15.15.2) o una de las interfaces enumerables (§15.15.3).

Una función asincrónica (§15.14) implementada mediante un bloque de iterador (§13.3) se denomina iterador asincrónico. Un bloque de iterador asincrónico se puede usar como el cuerpo de un miembro de función siempre que el tipo de valor devuelto del miembro de función correspondiente sea las interfaces asincrónicas del enumerador (§15.15.2) o las interfaces enumerables asincrónicas (§15.15.3).

Un bloque de iterador puede producirse como un 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 sincrónicos o asincrónicos.

Cuando se implementa un miembro de función o una función local mediante un bloque de iterador, es un error de tiempo de compilación que la lista de parámetros del miembro de función especifique cualquier parámetro de in, out o ref, o un parámetro de tipo ref struct.

Interfaces del enumerador 15.15.2

Las interfaces del enumerador son la interfaz System.Collections.IEnumerator no genérica y todas las instancias de las interfaces System.Collections.Generic.IEnumerator<T>genéricas .

Las interfaces del enumerador asincrónico son todas las instancias de la interfaz System.Collections.Generic.IAsyncEnumerator<T>genérica .

Por motivos de brevedad, en esta subclausa y sus elementos del mismo nivel se hace referencia a estas interfaces como IEnumerator, IEnumerator<T>y IAsyncEnumerator<T>, respectivamente.

15.15.3 Interfaces enumerables

Las interfaces enumerables son la interfaz System.Collections.IEnumerable no genérica y todas las instancias de las interfaces System.Collections.Generic.IEnumerable<T>genéricas .

Las interfaces enumerables asincrónicas son todas las instancias de la interfaz System.Collections.Generic.IAsyncEnumerable<T>genérica .

Por motivos de brevedad, en esta subclausa y sus elementos del mismo nivel se hace referencia a estas interfaces como IEnumerable, IEnumerable<T>y IAsyncEnumerable<T>, respectivamente.

15.15.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.
  • El tipo de rendimiento de un iterador que devuelve un IEnumerator<T>, IAsyncEnumerator<T>, IEnumerable<T>o IAsyncEnumerable<T> es T.

15.15.5 Objetos enumeradores

15.15.5.1 General

Cuando se implementa un miembro de función o una función local que devuelve un tipo de interfaz de enumerador mediante un bloque de iterador, invocar la 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 MoveNext o MoveNextAsync del objeto enumerador. Un objeto enumerador tiene las siguientes características:

  • Implementa System.IDisposable, IEnumerator y IEnumerator<T>, o System.IAsyncDisposable y IAsyncEnumerator<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.
  • Tiene cuatro estados potenciales, antes, en ejecución, suspendido y después, y se encuentra inicialmente en el estado antes.

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 siguientes subclases describen el comportamiento necesario del miembro para avanzar en el enumerador, recuperar el valor actual del enumerador y eliminar los recursos usados por el enumerador. Estos se definen en los siguientes miembros para enumeradores sincrónicos y asincrónicos, respectivamente:

  • Para avanzar en el enumerador: MoveNext y MoveNextAsync.
  • Para recuperar el valor actual: Current.
  • Para eliminar los recursos: Dispose y DisposeAsync.

Los objetos de enumerador no admiten el IEnumerator.Reset método . La invocación de este método hace que se lance una System.NotSupportedException.

Los bloques de iterador sincrónicos y asincrónicos difieren en que los miembros del iterador asincrónico devuelven tipos de tareas y se pueden esperar.

15.15.5.2 Avance del enumerador

Los métodos MoveNext y MoveNextAsync de un objeto enumerador encapsulan el código de un bloque de iteración. La invocación del MoveNext método o MoveNextAsync ejecuta código en el bloque iterador y establece la Current propiedad del objeto enumerador según corresponda.

MoveNext devuelve un bool valor cuyo significado se describe a continuación. MoveNextAsync devuelve un ValueTask<bool> (§15.14.3). El valor de resultado de la tarea devuelta de MoveNextAsync tiene el mismo significado que el valor de resultado de MoveNext. En la descripción siguiente, las acciones descritas para MoveNext se aplican a MoveNextAsync con la siguiente diferencia: Donde se indica que MoveNext devuelve true, false o MoveNextAsync, MoveNextAsync establece su tarea en el estado true y establece el valor de resultado de la tarea en el valor correspondiente de false o .

La acción precisa realizada por MoveNext o MoveNextAsync depende del estado del objeto enumerador cuando 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 está ejecutándose, el resultado de invocar MoveNext no se especifica.
  • Si el estado del objeto enumerador es suspendido, al invocar a 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 del iterador inmediatamente después de la instrucción yield return que causó la suspensión de la ejecución y continúa hasta que esta sea interrumpida (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.
    • La ejecución del cuerpo del iterador ha sido suspendida. 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 método MoveNext devuelve false al llamador, 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 método MoveNext devuelve false al llamador, 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 hacia quien llama al método MoveNext.

15.15.5.3 Recuperar el valor actual

La propiedad Current de un objeto enumerador está afectada por las instrucciones yield return en el bloque iterador.

Nota: La Current propiedad es una propiedad sincrónica para objetos de iterador sincrónicos y asincrónicos. nota final

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 que no sea object, el resultado de acceder a Current mediante la implementación del objeto enumerador en IEnumerable equivale a acceder a Current mediante la implementación del objeto enumerador en IEnumerator<T> y convertir el resultado a object.

15.15.5.4 Eliminación de recursos

El Dispose método o DisposeAsync se usa para limpiar la iteración mediante la incorporación del objeto enumerador al estado posterior .

  • Si el estado del objeto enumerador es anterior, la invocación Dispose cambia el estado a después.
  • Si el estado del objeto enumerador está ejecutándose, el resultado de invocar Dispose no se especifica.
  • Si el estado del objeto enumerador es suspendido, 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 provoca que se lance una excepción y se propague fuera del cuerpo del iterador, el estado del objeto enumerador se define como después y la excepción se propaga al llamador 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.15.6 Objetos enumerables

15.15.6.1 General

Cuando un miembro de función o una función local que 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 de iterador. En su lugar, se crea y devuelve un objeto enumerable.

El método o GetEnumerator del GetAsyncEnumerator 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 o MoveNext del MoveNextAsync objeto enumerador. Un objeto enumerable tiene las siguientes características:

  • Implementa IEnumerable y IEnumerable<T> o IAsyncEnumerable<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.15.6.2 El método GetEnumerator o GetAsyncEnumerator

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.15.5.

Un objeto enumerable asincrónico proporciona una implementación del GetAsyncEnumerator método de la IAsyncEnumerable<T> interfaz . Este método devuelve un objeto enumerador asincrónico 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.15.5.