Partilhar via


15 Aulas

15.1 Generalidades

Uma classe é uma estrutura de dados que pode conter membros de dados (constantes e campos), membros de função (métodos, propriedades, eventos, indexadores, operadores, construtores de instância, finalizadores e construtores estáticos) e tipos aninhados. Os tipos de classe suportam herança, um mecanismo pelo qual uma classe derivada pode estender e especializar uma classe base.

15.2 Declarações de classe

15.2.1 Generalidades

Um class_declaration é um type_declaration (§14.7) que declara uma nova classe.

class_declaration
    : attributes? class_modifier* 'partial'? 'class' identifier
        type_parameter_list? class_base? type_parameter_constraints_clause*
        class_body ';'?
    ;

Um class_declaration consiste num conjunto opcional de atributos (§22), seguido por um conjunto opcional de class_modifiers (§15.2.2), seguido por um modificador opcional partial (§15.2.7), seguido pela palavra-chave class e um identificador que nomeia a classe, seguido por um type_parameter_list opcional (§15.2.3), seguido por uma especificação class_base opcional (§15.2.4), seguido de um conjunto opcional de type_parameter_constraints_clause s (§15.2.5), seguido de um class_body (§15.2.6), opcionalmente seguido de ponto e vírgula.

Uma declaração de classificação só pode fornecer um type_parameter_constraints_clauses se fornecer também um type_parameter_list.

Uma declaração de classe que fornece um type_parameter_list é uma declaração de classe genérica. Além disso, qualquer classe aninhada dentro de uma declaração de classe genérica ou uma declaração struct genérica é ela própria uma declaração de classe genérica, uma vez que os argumentos de tipo para o tipo que contém devem ser fornecidos para criar um tipo construído (§8.4).

15.2.2 Modificadores de classe

15.2.2.1 Generalidades

Um class_declaration pode, opcionalmente, incluir uma sequência de modificadores de classe:

class_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'abstract'
    | 'sealed'
    | 'static'
    | unsafe_modifier   // unsafe code support
    ;

unsafe_modifier (§23.2) só está disponível em código não seguro (§23).

É um erro em tempo de compilação para o mesmo modificador aparecer várias vezes em uma declaração de classe.

O new modificador é permitido em classes aninhadas. Especifica que a classe oculta um membro herdado com o mesmo nome, conforme descrito no §15.3.5. É um erro em tempo de compilação para o new modificador aparecer em uma declaração de classe que não é uma declaração de classe aninhada.

Os publicmodificadores , protected, internale private controlam a acessibilidade da classe. Dependendo do contexto em que a declaração de classe ocorre, alguns desses modificadores podem não ser permitidos (§7.5.2).

Quando uma declaração de tipo parcial (§15.2.7) incluir uma especificação de acessibilidade (através dos modificadores , publicprotected, internal, e private ), essa especificação deve ser acordada com todas as outras partes que incluam uma especificação de acessibilidade. Se nenhuma parte de um tipo parcial incluir uma especificação de acessibilidade, é dada ao tipo a acessibilidade por defeito adequada (§7.5.2).

O abstract, sealede static modificadores são discutidos nas subcláusulas a seguir.

15.2.2.2 Classes abstratas

O abstract modificador é usado para indicar que uma classe está incompleta e que se destina a ser usada apenas como uma classe base. Uma classe abstrata difere de uma classe não abstrata das seguintes maneiras:

  • Uma classe abstrata não pode ser instanciada diretamente, e é um erro em tempo de compilação usar o new operador em uma classe abstrata. Embora seja possível ter variáveis e valores cujos tipos de tempo de compilação são abstratos, tais variáveis e valores serão necessariamente null ou conterão referências a instâncias de classes não abstratas derivadas dos tipos abstratos.
  • Uma classe abstrata é permitida (mas não obrigatória) para conter membros abstratos.
  • Uma classe abstrata não pode ser selada.

Quando uma classe não abstrata é derivada de uma classe abstrata, a classe não abstrata deve incluir implementações reais de todos os membros abstratos herdados, substituindo assim esses membros abstratos.

Exemplo: No código a seguir

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

A classe A Abstract introduz um método Fabstrato. Class B introduz um método Gadicional, mas como não fornece uma implementação de , F também deve ser declarado Babstrato. A classe C substitui F e fornece uma implementação real. Uma vez que não há membros abstratos no C, C é permitido (mas não obrigatório) ser não-abstrato.

Exemplo final

Se uma ou mais partes de uma declaração de tipo parcial (§15.2.7) de uma classe incluírem o abstract modificador, a classe será abstrata. Caso contrário, a classe não é abstrata.

15.2.2.3 Classes seladas

O sealed modificador é usado para impedir a derivação de uma classe. Um erro em tempo de compilação ocorre se uma classe selada é especificada como a classe base de outra classe.

Uma classe selada não pode ser também uma classe abstrata.

Nota: O sealed modificador é usado principalmente para evitar derivação não intencional, mas também permite determinadas otimizações em tempo de execução. Em particular, como uma classe selada é conhecida por nunca ter classes derivadas, é possível transformar invocações de membros de funções virtuais em instâncias de classe lacradas em invocações não virtuais. Nota final

Se uma ou mais partes de uma declaração de tipo parcial (ponto 15.2.7) de uma classe incluírem o sealed modificador, a classe é selada. Caso contrário, a classe será deslacrada.

15.2.2.4 Classes estáticas

15.2.2.4.1 Generalidades

O static modificador é usado para marcar a classe que está sendo declarada como uma classe estática. Uma classe estática não deve ser instanciada, não deve ser utilizada como tipo e deve conter apenas elementos estáticos. Apenas uma classe estática pode conter declarações de métodos de extensão (§15.6.10).

Uma declaração de classe estática está sujeita às seguintes restrições:

  • Uma classe estática não deve incluir um sealed ou abstract modificador. (No entanto, como uma classe estática não pode ser instanciada ou derivada, ela se comporta como se fosse selada e abstrata.)
  • Uma classe estática não deve incluir uma especificação class_base (§15.2.4) e não pode especificar explicitamente uma classe de base ou uma lista de interfaces implementadas. Uma classe estática herda implicitamente do tipo object.
  • Uma classe estática deve conter apenas membros estáticos (§15.3.8).

    Nota: Todas as constantes e tipos aninhados são classificados como membros estáticos. Nota final

  • Uma classe estática não deve ter membros com protected, private protectedou protected internal acessibilidade declarada.

É um erro em tempo de compilação violar qualquer uma dessas restrições.

Uma classe estática não tem construtores de instância. Não é possível declarar um construtor de instância em uma classe estática, e nenhum construtor de instância padrão (§15.11.5) é fornecido para uma classe estática.

Os membros de uma classe estática não são automaticamente estáticos, e as declarações de membro devem incluir explicitamente um static modificador (exceto para constantes e tipos aninhados). Quando uma classe é aninhada dentro de uma classe externa estática, a classe aninhada não é uma classe estática, a menos que inclua explicitamente um static modificador.

Se uma ou mais partes de uma declaração de tipo parcial (§15.2.7) de uma classe incluírem o static modificador, a classe é estática. Caso contrário, a classe não é estática.

15.2.2.4.2 Referenciando tipos de classe estática

Um namespace_or_type_name (§7.8) pode fazer referência a uma classe estática se:

  • A namespace_or_type_name é a T namespace_or_type_name da formaT.I, ou
  • O namespace_or_type-nome é o T de uma typeof_expression (§12.8.18) do formulário typeof(T).

Um primary_expression (§12.8) pode fazer referência a uma classe estática se

  • O primary_expression é o E member_access(§12.8.7) do formulário E.I.

Em qualquer outro contexto, é um erro em tempo de compilação fazer referência a uma classe estática.

Nota: Por exemplo, é um erro para uma classe estática ser usada como uma classe base, um tipo constituinte (§15.3.7) de um membro, um argumento de tipo genérico ou uma restrição de parâmetro de tipo. Da mesma forma, uma classe estática não pode ser usada em um tipo de matriz, uma nova expressão, uma expressão de conversão, uma expressão is, uma expressão como, uma sizeof expressão ou uma expressão de valor padrão. Nota final

15.2.3 Parâmetros de tipo

Um parâmetro type é um identificador simples que indica um espaço reservado para um argumento type fornecido para criar um tipo construído. Por constrast, um argumento type (§8.4.2) é o tipo que é substituído pelo parâmetro type quando um tipo construído é criado.

type_parameter_list
    : '<' type_parameters '>'
    ;

type_parameters
    : attributes? type_parameter
    | type_parameters ',' attributes? type_parameter
    ;

type_parameter é definido no §8.5.

Cada parâmetro de tipo em uma declaração de classe define um nome no espaço de declaração (§7.3) dessa classe. Assim, não pode ter o mesmo nome que outro parâmetro de tipo dessa classe ou um membro declarado nessa classe. Um parâmetro type não pode ter o mesmo nome que o próprio tipo.

Duas declarações de tipo genéricas parciais (no mesmo programa) contribuem para o mesmo tipo genérico não acoplado se tiverem o mesmo nome totalmente qualificado (o que inclui um generic_dimension_specifier (§12.8.18) para o número de parâmetros de tipo) (§7.8.3). Duas dessas declarações de tipo parciais devem especificar o mesmo nome para cada parâmetro de tipo, por ordem.

15.2.4 Especificação de base de classe

15.2.4.1 Generalidades

Uma declaração de classe pode incluir uma especificação class_base , que define a classe base direta da classe e as interfaces (§18) diretamente implementadas pela classe.

class_base
    : ':' class_type
    | ':' interface_type_list
    | ':' class_type ',' interface_type_list
    ;

interface_type_list
    : interface_type (',' interface_type)*
    ;

15.2.4.2 Classes de base

Quando um class_type é incluído no class_base, ele especifica a classe base direta da classe que está sendo declarada. Se uma declaração de classe não parcial não tiver class_base, ou se o class_base listar apenas tipos de interface, a classe base direta será considerada object. Quando uma declaração de classe parcial incluir uma especificação de classe de base, essa especificação de classe de base deve fazer referência ao mesmo tipo que todas as outras partes desse tipo parcial que incluam uma especificação de classe de base. Se nenhuma parte de uma classe parcial incluir uma especificação de classe base, a classe base será object. Uma classe herda membros da sua classe base direta, conforme descrito no §15.3.4.

Exemplo: No código a seguir

class A {}
class B : A {}

Diz-se que a classe A é a classe base direta de B, e B diz-se que é derivada de A. Uma vez que A não especifica explicitamente uma classe base direta, sua classe base direta é implicitamente object.

Exemplo final

Para um tipo de classe construída, incluindo um tipo aninhado declarado numa declaração de tipo genérica (§15.3.9.7), se uma classe base for especificada na declaração de classe genérica, a classe base do tipo construído é obtida substituindo, para cada type_parameter na declaração de classe base, a type_argument correspondente do tipo construído.

Exemplo: Dadas as declarações de classe genéricas

class B<U,V> {...}
class G<T> : B<string,T[]> {...}

A classe base do tipo G<int> construído seria B<string,int[]>.

Exemplo final

A classe base especificada numa declaração de classe pode ser um tipo de classe construída (§8.4). Uma classe base não pode ser um parâmetro de tipo por si só (§8.5), embora possa envolver os parâmetros de tipo que estão no escopo.

Exemplo:

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

Exemplo final

A classe de base direta de um tipo de classe deve ser, pelo menos, tão acessível como o próprio tipo de classe (ponto 7.5.5). Por exemplo, é um erro em tempo de compilação para uma classe pública derivar de uma classe privada ou interna.

A classe de base direta de um tipo de classe não deve ser de nenhum dos seguintes tipos: System.Array, System.Delegate, System.Enum, System.ValueType ou o dynamic tipo. Além disso, uma declaração de classe genérica não pode ser utilizada System.Attribute como classe de base direta ou indireta (§22.2.1).

Ao determinar o significado da especificação A de classe base direta de uma classeB, a classe de base direta de é temporariamente assumida como , o Bque garante que o significado de uma especificação de classe base object não possa depender recursivamente de si mesma.

Exemplo: O seguinte

class X<T>
{
    public class Y{}
}

class Z : X<Z.Y> {}

está em erro, uma vez que na especificação da classe base a classe X<Z.Y> base direta de Z é considerada como sendo object, e, portanto, (pelas regras do §7.8) Z não é considerada como tendo um membro Y.

Exemplo final

As classes base de uma classe são a classe base direta e suas classes base. Em outras palavras, o conjunto de classes base é o fechamento transitivo da relação direta de classe base.

Exemplo: No seguinte:

class A {...}
class B<T> : A {...}
class C<T> : B<IComparable<T>> {...}
class D<T> : C<T[]> {...}

As classes base de D<int> são C<int[]>, B<IComparable<int[]>>, A, e object.

Exemplo final

Com exceção da classe object, cada classe tem exatamente uma classe base direta. A object classe não tem classe base direta e é a classe base final de todas as outras classes.

É um erro em tempo de compilação para uma classe depender de si mesma. Para efeitos desta regra, uma classe depende diretamente da sua classe base direta (se existir) e depende diretamente da classe de inclusão mais próxima na qual está aninhada (se existir). Dada esta definição, o conjunto completo de classes das quais uma classe depende é o fechamento transitivo da relação diretamente dependente .

Exemplo: O exemplo

class A : A {}

é errôneo porque a classe depende de si mesma. Da mesma forma, o exemplo

class A : B {}
class B : C {}
class C : A {}

está em erro porque as classes dependem circularmente de si mesmas. Por fim, o exemplo

class A : B.C {}
class B : A
{
    public class C {}
}

resulta em um erro em tempo de compilação porque A depende de B.C (sua classe base direta), que depende de B (sua classe imediatamente fechada), que depende circularmente de A.

Exemplo final

Uma classe não depende das classes que estão aninhadas dentro dela.

Exemplo: No código a seguir

class A
{
    class B : A {}
}

B depende de A (porque A é tanto a sua classe base direta como a sua classe imediatamente anexa), mas A não depende de B (uma vez que B não é nem uma classe base nem uma classe anexa de A). Assim, o exemplo é válido.

Exemplo final

Não é possível derivar de uma classe selada.

Exemplo: No código a seguir

sealed class A {}
class B : A {} // Error, cannot derive from a sealed class

A classe B está em erro porque tenta derivar da classe Aselada.

Exemplo final

15.2.4.3 Implementações de interface

Uma especificação class_base pode incluir uma lista de tipos de interface, caso em que se diz que a classe implementa os tipos de interface fornecidos. Para um tipo de classe construído, incluindo um tipo aninhado declarado numa declaração de tipo genérica (§15.3.9.7), cada tipo de interface implementado é obtido substituindo, para cada type_parameter na interface dada, a type_argument correspondente do tipo construído.

O conjunto de interfaces para um tipo declarado em várias partes (§15.2.7) é a união das interfaces especificadas em cada parte. Uma interface específica só pode ser nomeada uma vez em cada parte, mas várias partes podem nomear a(s) mesma(s) interface(s) base. Deve haver apenas uma implementação de cada membro de uma dada interface.

Exemplo: No seguinte:

partial class C : IA, IB {...}
partial class C : IC {...}
partial class C : IA, IB {...}

O conjunto de interfaces base para a classe C é IA, IBe IC.

Exemplo final

Normalmente, cada parte fornece uma implementação da(s) interface(s) declarada(s) nessa parte; no entanto, isso não é um requisito. Uma parte pode fornecer a implementação para uma interface declarada em uma parte diferente.

Exemplo:

partial class X
{
    int IComparable.CompareTo(object o) {...}
}

partial class X : IComparable
{
    ...
}

Exemplo final

As interfaces de base especificadas numa declaração de classe podem ser construídas como tipos de interface (§8.4, §18.2). Uma interface base não pode ser um parâmetro de tipo por si só, embora possa envolver os parâmetros de tipo que estão no escopo.

Exemplo: O código a seguir ilustra como uma classe pode implementar e estender tipos construídos:

class C<U, V> {}
interface I1<V> {}
class D : C<string, int>, I1<string> {}
class E<T> : C<int, T>, I1<T> {}

Exemplo final

As implementações de interface são discutidas mais detalhadamente no §18.6.

15.2.5 Restrições de parâmetros de tipo

As declarações genéricas de tipo e método podem, opcionalmente, especificar restrições de parâmetros de tipo incluindo type_parameter_constraints_clauses.

type_parameter_constraints_clauses
    : type_parameter_constraints_clause
    | type_parameter_constraints_clauses type_parameter_constraints_clause
    ;

type_parameter_constraints_clause
    : 'where' type_parameter ':' type_parameter_constraints
    ;

type_parameter_constraints
    : primary_constraint (',' secondary_constraints)? (',' constructor_constraint)?
    | secondary_constraints (',' constructor_constraint)?
    | constructor_constraint
    ;

primary_constraint
    : class_type nullable_type_annotation?
    | 'class' nullable_type_annotation?
    | 'struct'
    | 'notnull'
    | 'unmanaged'
    ;

secondary_constraint
    : interface_type nullable_type_annotation?
    | type_parameter nullable_type_annotation?
    ;

secondary_constraints
    : secondary_constraint (',' secondary_constraint)*
    ;

constructor_constraint
    : 'new' '(' ')'
    ;

Cada type_parameter_constraints_clause consiste no token where, seguido pelo nome de um parâmetro type, seguido por dois pontos e a lista de restrições para esse parâmetro type. Pode haver no máximo uma where cláusula para cada parâmetro de tipo, e as where cláusulas podem ser listadas em qualquer ordem. Como o get e set tokens em um acessador de propriedade, o where token não é uma palavra-chave.

A lista de restrições dada em uma where cláusula pode incluir qualquer um dos seguintes componentes, nesta ordem: uma única restrição primária, uma ou mais restrições secundárias e a restrição do construtor, new().

Uma restrição primária pode ser um tipo de classe, a restriçãoclass tipo de referência, astruct de tipo de valor, anotnull não nula ou a restriçãounmanagedtipo não gerenciado. O tipo de classe e a restrição de tipo de referência podem incluir o nullable_type_annotation.

Uma restrição secundária pode ser um interface_type ou type_parameter, opcionalmente seguido por um nullable_type_annotation. A presença do nullable_type_annotatione* indica que o argumento type pode ser o tipo de referência anulável que corresponde a um tipo de referência não anulável que satisfaz a restrição.

A restrição de tipo de referência especifica que um argumento de tipo usado para o parâmetro de tipo deve ser um tipo de referência. Todos os tipos de classe, tipos de interface, tipos delegados, tipos de matriz e parâmetros de tipo conhecidos por serem um tipo de referência (conforme definido abaixo) satisfazem essa restrição.

O tipo de classe, a restrição de tipo de referência e as restrições secundárias podem incluir a anotação de tipo anulável. A presença ou ausência dessa anotação no parâmetro type indica as expectativas de anulabilidade para o argumento type:

  • Se a restrição não incluir a anotação de tipo anulável, espera-se que o argumento type seja um tipo de referência não anulável. Um compilador pode emitir um aviso se o argumento type for um tipo de referência anulável.
  • Se a restrição incluir a anotação de tipo anulável, a restrição será satisfeita por um tipo de referência não anulável e um tipo de referência anulável.

A anulabilidade do argumento type não precisa corresponder à anulabilidade do parâmetro type. Um compilador pode emitir um aviso se a anulabilidade do parâmetro type não corresponder à anulabilidade do argumento type.

Nota: Para especificar que um argumento type é um tipo de referência anulável, não adicione a anotação de tipo anulável como uma restrição (use T : class ou T : BaseClass), mas use T? toda a declaração genérica para indicar o tipo de referência anulável correspondente para o argumento type. Nota final

A anotação de tipo anulável, ?, não pode ser usada em um argumento de tipo sem restrições.

Para um parâmetro T type quando o argumento type é um tipo C?de referência anulável, as instâncias de T? são interpretadas como C?, não C??.

Exemplo: Os exemplos a seguir mostram como a anulabilidade de um argumento type afeta a anulabilidade de uma declaração de seu parâmetro type:

public class C
{
}

public static class  Extensions
{
    public static void M<T>(this T? arg) where T : notnull
    {

    }
}

public class Test
{
    public void M()
    {
        C? mightBeNull = new C();
        C notNull = new C();

        int number = 5;
        int? missing = null;

        mightBeNull.M(); // arg is C?
        notNull.M(); //  arg is C?
        number.M(); // arg is int?
        missing.M(); // arg is int?
    }
}

Quando o argumento type é um tipo não anulável, a ? anotação de tipo indica que o parâmetro é o tipo anulável correspondente. Quando o argumento type já é um tipo de referência anulável, o parâmetro é esse mesmo tipo anulável.

Exemplo final

A restrição não nula especifica que um argumento type usado para o parâmetro type deve ser um tipo de valor não anulável ou um tipo de referência não anulável. Um argumento de tipo que não é um tipo de valor não anulável ou um tipo de referência não anulável é permitido, mas um compilador pode produzir um aviso de diagnóstico.

A restrição de tipo de valor especifica que um argumento de tipo usado para o parâmetro type deve ser um tipo de valor não anulável. Todos os tipos struct não anuláveis, tipos de enum e parâmetros de tipo com a restrição de tipo de valor satisfazem essa restrição. Observe que, embora classificado como um tipo de valor, um tipo de valor anulável (§8.3.12) não satisfaz a restrição de tipo de valor. Um parâmetro de tipo com a restrição de tipo de valor também não deve ter o constructor_constraint, embora possa ser usado como um argumento de tipo para outro parâmetro de tipo com um constructor_constraint.

Nota: O System.Nullable<T> tipo especifica a restrição de tipo de valor não anulável para T. Assim, tipos recursivamente construídos das formas T?? e Nullable<Nullable<T>> são proibidos. Nota final

Porque unmanaged não é uma palavra-chave, na primary_constraint a restrição não gerida é sempre sintaticamente ambígua com class_type. Por razões de compatibilidade, se uma pesquisa de nome (§12.8.4) do nome unmanaged for bem-sucedida, é tratada como um class_type. Caso contrário, é tratada como a restrição não gerenciada.

A restrição de tipo não gerenciado especifica que um argumento de tipo usado para o parâmetro type deve ser um tipo não gerenciado não anulável (§8.8).

Os tipos de ponteiro nunca podem ser argumentos de tipo e não satisfazem nenhuma restrição de tipo, mesmo não gerenciados, apesar de serem tipos não gerenciados.

Se uma restrição for um tipo de classe, um tipo de interface ou um parâmetro de tipo, esse tipo especifica um "tipo base" mínimo que cada argumento de tipo usado para esse parâmetro de tipo deve suportar. Sempre que um tipo construído ou método genérico é usado, o argumento type é verificado em relação às restrições no parâmetro type em tempo de compilação. O argumento de tipo fornecido deve satisfazer as condições descritas no ponto 8.4.5.

Uma restrição class_type deve satisfazer as seguintes regras:

  • O tipo deve ser um tipo de classe.
  • O tipo não deve ser sealed.
  • O tipo não deve ser um dos seguintes: System.Array ou System.ValueType.
  • O tipo não deve ser object.
  • No máximo, uma restrição para um determinado parâmetro de tipo pode ser um tipo de classe.

Um tipo especificado como restrição de interface_type deve satisfazer as seguintes regras:

  • O tipo deve ser um tipo de interface.
  • Um tipo não pode ser especificado mais do que uma vez numa determinada where cláusula.

Em ambos os casos, a restrição pode envolver qualquer um dos parâmetros de tipo da declaração de tipo ou método associada como parte de um tipo construído, e pode envolver o tipo que está sendo declarado.

Qualquer classe ou tipo de interface especificado como restrição de parâmetro de tipo deve ser pelo menos tão acessível (ponto 7.5.5) como o tipo genérico ou método declarado.

Um tipo especificado como restrição de type_parameter deve satisfazer as seguintes regras:

  • O tipo deve ser um parâmetro de tipo.
  • Um tipo não pode ser especificado mais do que uma vez numa determinada where cláusula.

Além disso, não deve haver ciclos no gráfico de dependência dos parâmetros de tipo, em que a dependência é uma relação transitiva definida por:

  • Se um parâmetro T type é usado como uma restrição para o parâmetro S type, entãoS depende de.T
  • Se um parâmetro S de tipo depende de um parâmetro T de tipo e T depende de um parâmetro U de tipo, então Sdepende deU.

Dada esta relação, é um erro em tempo de compilação para um parâmetro de tipo depender de si mesmo (direta ou indiretamente).

As eventuais restrições devem ser coerentes entre os parâmetros do tipo dependente. Se o parâmetro S type depender do parâmetro T type, então:

  • T não deve ter a restrição do tipo de valor. Caso contrário, T é efetivamente selado assim S seria forçado a ser do mesmo tipo que T, eliminando a necessidade de dois parâmetros de tipo.
  • Se S tiver a restrição de tipo de valor, então T não terá uma restrição de class_type .
  • Se S tiver uma A e T tiver uma B, então haverá uma conversão de identidade ou conversão de referência implícita de A para ou uma conversão de referência implícita de B para BA.
  • Se S também depende do parâmetro U de tipo e U tem uma A e T tem uma B, então deve haver uma conversão de identidade ou conversão de referência implícita de A para B ou uma conversão de referência implícita de B para A.

É válido para S ter a restrição de tipo de valor e T para ter a restrição de tipo de referência. Efetivamente, isso limita T os tipos System.Object, System.ValueType, System.Enum, e qualquer tipo de interface.

Se a where cláusula para um parâmetro type incluir uma restrição do construtor (que tem a forma new()), é possível usar o new operador para criar instâncias do tipo (§12.8.17.2). Qualquer argumento de tipo usado para um parâmetro de tipo com uma restrição de construtor deve ser um tipo de valor, uma classe não abstrata com um construtor público sem parâmetros ou um parâmetro de tipo com a restrição de tipo de valor ou restrição de construtor.

É um erro em tempo de compilação para type_parameter_constraints ter um primary_constraint de struct ou unmanaged também ter um constructor_constraint.

Exemplo: Seguem-se exemplos de restrições:

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()
{
    ...
}

O exemplo a seguir está em erro porque causa uma circularidade no gráfico de dependência dos parâmetros de tipo:

class Circular<S,T>
    where S: T
    where T: S // Error, circularity in dependency graph
{
    ...
}

Os exemplos a seguir ilustram situações inválidas adicionais:

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
{
    ...
}

Exemplo final

O apagamento dinâmico de um tipo C é do tipo Cₓ construído da seguinte forma:

  • Se C é um tipo Outer.Inner aninhado, então Cₓ é um tipo Outerₓ.Innerₓaninhado.
  • Se CCₓé um tipo G<A¹, ..., Aⁿ> construído com argumentos A¹, ..., Aⁿ de tipo, então Cₓ é o tipo G<A¹ₓ, ..., Aⁿₓ>construído .
  • Se C é um tipo E[] de matriz, então Cₓ é o tipo Eₓ[]de matriz .
  • Se C é dinâmico, então Cₓ é object.
  • Caso contrário, Cₓ é C.

A classe de base efetiva de um parâmetro T de tipo é definida da seguinte forma:

Seja R um conjunto de tipos tais que:

  • Para cada restrição de que é um parâmetro de T tipo, R contém sua classe base efetiva.
  • Para cada restrição de T que é um tipo struct, R contém System.ValueType.
  • Para cada restrição de que é um tipo de T enumeração, R contém System.Enum.
  • Para cada restrição de que é um tipo delegado T , R contém sua eliminação dinâmica.
  • Para cada restrição de que é um tipo de T matriz, R contém System.Array.
  • Para cada restrição de que é um tipo de T classe, R contém seu apagamento dinâmico.

Então

  • Se T tiver a restrição de tipo de valor, sua classe base efetiva será System.ValueType.
  • Caso contrário, se R estiver vazio, a classe base efetiva será object.
  • Caso contrário, a classe base efetiva de é o tipo mais englobado T (§10.5.3) do conjunto R. Se o conjunto não tiver nenhum tipo englobado, a classe base efetiva de T é object. As regras de coerência garantem a existência do tipo mais abrangente.

Se o parâmetro type for um parâmetro de tipo de método cujas restrições são herdadas do método base, a classe base efetiva é calculada após a substituição de tipo.

Essas regras garantem que a classe base efetiva seja sempre uma class_type.

O conjunto de interface efetivo de um parâmetro T de tipo é definido da seguinte forma:

  • Se T não tiver secondary_constraints, seu conjunto de interface efetivo estará vazio.
  • Se T tiver interface_type restrições, mas não type_parameter restrições, seu conjunto de interface eficaz é o conjunto de rasuras dinâmicas de suas restrições interface_type .
  • Se T não tem restrições de interface_type , mas tem type_parameter restrições, seu conjunto de interface eficaz é a união dos conjuntos de interface eficazes de suas restrições de type_parameter .
  • Se T tem restrições de interface_type e restrições de type_parameter , seu conjunto de interface eficaz é a união do conjunto de rasuras dinâmicas de suas restrições de interface_type e os conjuntos de interface eficazes de suas restrições de type_parameter .

Um parâmetro type é conhecido por ser um tipo de referência se tiver a restrição de tipo de referência ou se sua classe base efetiva não object for ou System.ValueType. Um parâmetro type é conhecido por ser um tipo de referência não anulável se for conhecido por ser um tipo de referência e tiver a restrição de tipo de referência não anulável.

Os valores de um tipo restrito de parâmetro podem ser usados para acessar os membros da instância implicados pelas restrições.

Exemplo: No seguinte:

interface IPrintable
{
    void Print();
}

class Printer<T> where T : IPrintable
{
    void PrintOne(T x) => x.Print();
}

os métodos de pode ser invocado IPrintable diretamente em x porque T é restrito a sempre implementar IPrintable.

Exemplo final

Quando uma declaração genérica de tipo parcial incluir restrições, estas devem ser acordadas com todas as outras partes que incluam restrições. Especificamente, cada parte que inclui restrições deve ter restrições para o mesmo conjunto de parâmetros de tipo, e para cada parâmetro de tipo, os conjuntos de restrições primárias, secundárias e do construtor devem ser equivalentes. Dois conjuntos de restrições são equivalentes se contiverem os mesmos membros. Se nenhuma parte de um tipo genérico parcial especificar restrições de parâmetro de tipo, os parâmetros de tipo serão considerados sem restrições.

Exemplo:

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>
{
    ...
}

está correto porque as partes que incluem restrições (as duas primeiras) especificam efetivamente o mesmo conjunto de restrições primárias, secundárias e de construtor para o mesmo conjunto de parâmetros de tipo, respectivamente.

Exemplo final

15.2.6 Corpo de classe

O class_body de uma classe define os membros dessa classe.

class_body
    : '{' class_member_declaration* '}'
    ;

15.2.7 Declarações parciais

O modificador partial é usado ao definir uma classe, struct ou tipo de interface em várias partes. O partial modificador é uma palavra-chave contextual (§6.4.4) e só tem um significado especial imediatamente antes de uma das palavras-chave class, structou interface.

Cada parte de uma declaração de tipo parcial deve incluir um partial modificador e deve ser declarada no mesmo espaço de nomes ou contendo o tipo que as outras partes. O partial modificador indica que partes adicionais da declaração de tipo podem existir em outro lugar, mas a existência dessas partes adicionais não é um requisito, é válido para a única declaração de um tipo incluir o partial modificador. É válido para apenas uma declaração de um tipo parcial para incluir a classe base ou interfaces implementadas. No entanto, todas as declarações de uma classe base ou interfaces implementadas devem corresponder, incluindo a anulabilidade de quaisquer argumentos de tipo especificados.

Todas as partes de um tipo parcial devem ser compiladas de modo a poderem ser fundidas em tempo de compilação. Tipos parciais especificamente não permitem que tipos já compilados sejam estendidos.

Os tipos aninhados podem ser declarados em várias partes usando o partial modificador. Normalmente, o tipo que contém é declarado usando partial também, e cada parte do tipo aninhado é declarada em uma parte diferente do tipo que contém.

Exemplo: A classe parcial a seguir é implementada em duas partes, que residem em unidades de compilação diferentes. A primeira parte é gerada por uma ferramenta de mapeamento de banco de dados, enquanto a segunda parte é criada manualmente:

public partial class Customer
{
    private int id;
    private string name;
    private string address;
    private List<Order> orders;

    public Customer()
    {
        ...
    }
}

// File: Customer2.cs
public partial class Customer
{
    public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted);

    public bool HasOutstandingOrders() => orders.Count > 0;
}

Quando as duas partes acima são compiladas juntas, o código resultante se comporta como se a classe tivesse sido escrita como uma única unidade, da seguinte maneira:

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

Exemplo final

O tratamento dos atributos especificados nos parâmetros de tipo ou tipo de diferentes partes de uma declaração parcial é discutido no §22.3.

15.3 Membros da turma

15.3.1 Generalidades

Os membros de uma classe consistem nos membros introduzidos por seus class_member_declarations e os membros herdados da classe base direta.

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
    ;

Os membros de uma classe são divididos nas seguintes categorias:

  • Constantes, que representam valores constantes associados à classe (§15.4).
  • Campos, que são as variáveis da classe (§15.5).
  • Métodos, que implementam os cálculos e ações que podem ser realizadas pela classe (§15.6).
  • Propriedades, que definem as características nomeadas e as ações associadas à leitura e escrita dessas características (§15.7).
  • Eventos, que definem as notificações que podem ser geradas pela classe (§15.8).
  • Indexadores, que permitem que instâncias da classe sejam indexadas da mesma forma (sintaticamente) que matrizes (§15.9).
  • Operadores, que definem os operadores de expressão que podem ser aplicados a instâncias da classe (§15.10).
  • Construtores de instância, que implementam as ações necessárias para inicializar instâncias da classe (§15.11)
  • Finalizadores, que implementam as ações a serem executadas antes que as instâncias da classe sejam permanentemente descartadas (§15.13).
  • Construtores estáticos, que implementam as ações necessárias para inicializar a própria classe (§15.12).
  • Tipos, que representam os tipos que são locais para a classe (§14.7).

Um class_declaration cria um novo espaço de declaração (§7.3), e os type_parameters e os class_member_declarationcontidos imediatamente pelo class_declaration introduzem novos membros nesse espaço de declaração. As seguintes regras aplicam-se a class_member_declarations:

  • Os construtores de instância, finalizadores e construtores estáticos devem ter o mesmo nome que a classe imediatamente anexada. Todos os outros membros devem ter nomes diferentes do nome da classe imediatamente anexa.

  • O nome de um parâmetro de tipo no type_parameter_list de uma declaração de classe deve diferir dos nomes de todos os outros parâmetros de tipo na mesma type_parameter_list e deve diferir do nome da classe e dos nomes de todos os membros da classe.

  • O nome de um tipo deve diferir dos nomes de todos os membros não tipoiros declarados na mesma classe. Se duas ou mais declarações de tipo partilharem o mesmo nome totalmente qualificado, as declarações devem ter o partial modificador (§15.2.7) e estas declarações combinam-se para definir um único tipo.

Nota: Como o nome totalmente qualificado de uma declaração de tipo codifica o número de parâmetros de tipo, dois tipos distintos podem compartilhar o mesmo nome, desde que tenham um número diferente de parâmetros de tipo. Nota final

  • O nome de uma constante, campo, propriedade ou evento deve diferir dos nomes de todos os outros membros declarados na mesma classe.

  • O nome de um método deve diferir dos nomes de todos os outros métodos não declarados na mesma classe. Além disso, a assinatura (§7.6) de um método deve diferir das assinaturas de todos os outros métodos declarados na mesma classe, e dois métodos declarados na mesma classe não devem ter assinaturas que diferem apenas por in, oute ref.

  • A assinatura de um construtor de instância deve diferir das assinaturas de todos os outros construtores de instância declarados na mesma classe, e dois construtores declarados na mesma classe não devem ter assinaturas que diferem apenas por ref e out.

  • A assinatura de um indexador deve diferir das assinaturas de todos os outros indexadores declarados na mesma classe.

  • A assinatura de um operador deve diferir da assinatura de todos os outros operadores declarados da mesma classe.

Os membros herdados de uma classe (§15.3.4) não fazem parte do espaço de declaração de uma classe.

Nota: Assim, uma classe derivada tem permissão para declarar um membro com o mesmo nome ou assinatura que um membro herdado (o que, na verdade, oculta o membro herdado). Nota final

O conjunto de membros de um tipo declarado em várias partes (§15.2.7) é a união dos membros declarados em cada parte. Os corpos de todas as partes da declaração de tipo partilham o mesmo espaço de declaração (§7.3), e o âmbito de cada membro (§7.7) estende-se aos corpos de todas as partes. O domínio de acessibilidade de qualquer membro inclui sempre todas as partes do tipo anexo; Um membro privado declarado numa parte é livremente acessível a partir de outra. É um erro em tempo de compilação declarar o mesmo membro em mais de uma parte do tipo, a menos que esse membro seja um tipo com o partial modificador.

Exemplo:

partial class A
{
    int x;                   // Error, cannot declare x more than once

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

partial class A
{
    int x;                   // Error, cannot declare x more than once

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

Exemplo final

A ordem de inicialização do campo pode ser significativa dentro do código C#, e algumas garantias são fornecidas, conforme definido no §15.5.6.1. Caso contrário, a ordenação dos membros dentro de um tipo raramente é significativa, mas pode ser significativa ao interagir com outras linguagens e ambientes. Nestes casos, a ordenação dos membros dentro de um tipo declarado em várias partes é indefinida.

15.3.2 O tipo de instância

Cada declaração de classe tem um tipo de instância associado. Para uma declaração de classe genérica, o tipo de instância é formado pela criação de um tipo construído (§8.4) a partir da declaração de tipo, com cada um dos argumentos de tipo fornecidos sendo o parâmetro type correspondente. Como o tipo de instância usa os parâmetros de tipo, ele só pode ser usado onde os parâmetros de tipo estão no escopo; ou seja, dentro da declaração de classe. O tipo de instância é o tipo de código for escrito dentro da declaração de this classe. Para classes não genéricas, o tipo de instância é simplesmente a classe declarada.

Exemplo: A seguir mostra várias declarações de classe junto com seus tipos de instância:

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

Exemplo final

15.3.3 Membros de tipos construídos

Os membros não herdados de um tipo construído obtêm-se substituindo, para cada type_parameter na declaração de membro, os type_argument correspondentes do tipo construído. O processo de substituição baseia-se no significado semântico das declarações de tipo, e não é simplesmente uma substituição textual.

Exemplo: Dada a declaração de classe 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) {...}
}

O tipo Gen<int[],IComparable<string>> construído tem os seguintes membros:

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

O tipo do membro na declaração a de classe genérica é "matriz bidimensional de Gen", então o tipo do membro T no tipo construído acima é "matriz bidimensional de matriz unidimensional de a", ou int.int[,][]

Exemplo final

Dentro dos membros da função de instância, o tipo de é o tipo de this instância (§15.3.2) da declaração que contém.

Todos os membros de uma classe genérica podem usar parâmetros de tipo de qualquer classe anexa, diretamente ou como parte de um tipo construído. Quando um determinado tipo construído fechado (§8.4.3) é utilizado em tempo de execução, cada utilização de um parâmetro de tipo é substituída pelo argumento de tipo fornecido ao tipo construído.

Exemplo:

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

Exemplo final

15.3.4 Herança

Uma classe herda os membros de sua classe base direta. Herança significa que uma classe contém implicitamente todos os membros de sua classe base direta, exceto os construtores de instância, finalizadores e construtores estáticos da classe base. Alguns aspetos importantes da herança são:

  • A herança é transitiva. Se C é derivado de B, e B é derivado de A, então C herda os membros declarados em B , bem como os membros declarados em A.

  • Uma classe derivada estende sua classe base direta. Uma classe derivada pode adicionar novos membros àqueles que herda, mas não pode remover a definição de um membro herdado.

  • Construtores de instância, finalizadores e construtores estáticos não são herdados, mas todos os outros membros são, independentemente de sua acessibilidade declarada (§7.5). No entanto, dependendo de sua acessibilidade declarada, os membros herdados podem não estar acessíveis em uma classe derivada.

  • Uma classe derivada pode ocultar (§7.7.2.3) membros herdados declarando novos membros com o mesmo nome ou assinatura. No entanto, ocultar um membro herdado não remove esse membro — apenas torna esse membro inacessível diretamente através da classe derivada.

  • Uma instância de uma classe contém um conjunto de todos os campos de instância declarados na classe e suas classes base, e uma conversão implícita (§10.2.8) existe de um tipo de classe derivado para qualquer um de seus tipos de classe base. Assim, uma referência a uma instância de alguma classe derivada pode ser tratada como uma referência a uma instância de qualquer uma de suas classes base.

  • Uma classe pode declarar métodos virtuais, propriedades, indexadores e eventos, e classes derivadas podem substituir a implementação desses membros da função. Isso permite que as classes exibam um comportamento polimórfico em que as ações executadas por uma invocação de membro de função variam dependendo do tipo de tempo de execução da instância através da qual esse membro da função é invocado.

Os membros herdados de um tipo de classe construído são os membros do tipo de classe base imediata (§15.2.4.2), que é encontrado substituindo os argumentos de tipo do tipo construído para cada ocorrência dos parâmetros de tipo correspondentes no base_class_specification. Estes membros, por sua vez, são transformados substituindo, para cada type_parameter na declaração de membro, o type_argument correspondente do base_class_specification.

Exemplo:

class B<U>
{
    public U F(long index) {...}
}

class D<T> : B<T[]>
{
    public T G(string s) {...}
}

No código acima, o tipo D<int> construído tem um público intG(string s) membro não herdado obtido substituindo o argumento int type pelo parâmetro Ttype . D<int> também tem um membro herdado da declaração Bde classe. Este membro herdado é determinado determinando primeiro o tipo B<int[]> de classe base substituindo D<int>int por na especificação Tde B<T[]> classe base. Então, como um argumento de tipo para B, int[] é substituído por U em public U F(long index), produzindo o membro public int[] F(long index)herdado.

Exemplo final

15.3.5 O novo modificador

Um class_member_declaration pode declarar um membro com o mesmo nome ou assinatura que um membro herdado. Quando isso ocorre, diz-se que o membro da classe derivada oculta o membro da classe base. Ver §7.7.2.3 para uma especificação precisa de quando um membro oculta um membro herdado.

Um membro M herdado é considerado disponível se M estiver acessível e não houver nenhum outro membro acessível herdado N que já oculte M. Ocultar implicitamente um membro herdado não é considerado um erro, mas um compilador deve emitir um aviso, a menos que a declaração do membro da classe derivada inclua um modificador de new para indicar explicitamente que o membro derivado se destina a ocultar o membro base. Se uma ou mais partes de uma declaração parcial (§15.2.7) de um tipo aninhado incluírem o new modificador, nenhum aviso será emitido se o tipo aninhado ocultar um membro herdado disponível.

Se um new modificador for incluído em uma declaração que não oculte um membro herdado disponível, um aviso para esse efeito será emitido.

15.3.6 Modificadores de acesso

Um private Exceto para as protected internal combinações e private protected , é um erro em tempo de compilação especificar mais de um modificador de acesso. Quando um class_member_declaration não inclui nenhum modificador de acesso, private é assumido.

15.3.7 Tipos de componentes

Os tipos que são usados na declaração de um membro são chamados de tipos constituintes desse membro. Os tipos de constituintes possíveis são o tipo de uma constante, campo, propriedade, evento ou indexador, o tipo de retorno de um método ou operador e os tipos de parâmetro de um construtor de método, indexador, operador ou instância. Os tipos de componentes de um membro devem ser pelo menos tão acessíveis como o próprio membro (§7.5.5).

15.3.8 Membros estáticos e de instância

Os membros de uma classe são membros estáticos ou membros da instância.

Nota: De um modo geral, é útil pensar em membros estáticos como pertencentes a classes e membros de instância como pertencentes a objetos (instâncias de classes). Nota final

Quando uma declaração de campo, método, propriedade, evento, operador ou construtor inclui um static modificador, ele declara um membro estático. Além disso, uma declaração constante ou de tipo declara implicitamente um membro estático. Os membros estáticos têm as seguintes características:

  • Quando um membro M estático é referenciado num member_access (§12.8.7) do formulário E.M, E deve indicar um tipo que tenha um membro M. É um erro em tempo de compilação para E denotar uma instância.
  • Um campo estático em uma classe não genérica identifica exatamente um local de armazenamento. Não importa quantas instâncias de uma classe não genérica sejam criadas, há apenas uma cópia de um campo estático. Cada tipo de construção fechada distinta (§8.4.3) tem o seu próprio conjunto de campos estáticos, independentemente do número de ocorrências do tipo fechado construído.
  • Um membro de função estática (método, propriedade, evento, operador ou construtor) não opera em uma instância específica, e é um erro em tempo de compilação referir-se a isso em tal membro de função.

Quando um campo, método, propriedade, evento, indexador, construtor ou declaração do finalizador não inclui um modificador estático, ele declara um membro da instância. (Um membro da instância às vezes é chamado de membro não estático.) Os membros da instância têm as seguintes características:

  • Quando um membro M da instância é referenciado em um member_access (§12.8.7) do formulário E.M, E deve indicar uma instância de um tipo que tenha um membro M. É um erro de tempo de vinculação para E denotar um tipo.
  • Cada instância de uma classe contém um conjunto separado de todos os campos de instância da classe.
  • Um membro da função de instância (método, propriedade, indexador, construtor de instância ou finalizador) opera em uma determinada instância da classe, e essa instância pode ser acessada como this (§12.8.14).

Exemplo: O exemplo a seguir ilustra as regras para acessar membros estáticos e de instância:

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

O F método mostra que, em um membro da função de instância, um simple_name (§12.8.4) pode ser usado para acessar membros da instância e membros estáticos. O G método mostra que, em um membro de função estática, é um erro em tempo de compilação acessar um membro da instância por meio de um simple_name. O Main método mostra que, em um member_access (§12.8.7), os membros da instância devem ser acessados por meio de instâncias, e os membros estáticos devem ser acessados por meio de tipos.

Exemplo final

15.3.9 Tipos aninhados

15.3.9.1 Generalidades

Um tipo declarado dentro de uma classe ou struct é chamado de tipo aninhado. Um tipo que é declarado dentro de uma unidade de compilação ou namespace é chamado de tipo não aninhado.

Exemplo: No exemplo a seguir:

class A
{
    class B
    {
        static void F()
        {
            Console.WriteLine("A.B.F");
        }
    }
}

class B é um tipo aninhado porque é declarado dentro da classe A, e class A é um tipo não aninhado porque é declarado dentro de uma unidade de compilação.

Exemplo final

15.3.9.2 Nome totalmente qualificado

O nome totalmente qualificado (§7.8.3) para uma declaração de tipo aninhada é S.N onde S é o nome totalmente qualificado da declaração de tipo em que o tipo N é declarado e N é o nome não qualificado (§7.8.2) da declaração de tipo aninhada (incluindo qualquer generic_dimension_specifier (§12.8.18)).

15.3.9.3 Acessibilidade declarada

Os tipos não aninhados podem ter publicinternal ou declarado acessibilidade e ter internal acessibilidade declarada por padrão. Os tipos aninhados também podem ter essas formas de acessibilidade declarada, além de uma ou mais formas adicionais de acessibilidade declarada, dependendo se o tipo que contém é uma classe ou estrutura:

  • Um tipo aninhado que é declarado em uma classe pode ter qualquer um dos tipos permitidos de acessibilidade declarada e, como outros membros da classe, assume como padrão a private acessibilidade declarada.
  • Um tipo aninhado que é declarado em uma struct pode ter qualquer uma das três formas de acessibilidade declarada (public, , ou internal) e, como outros membros struct, assume como padrão a privateprivateacessibilidade declarada.

Exemplo: O exemplo

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 uma classe Nodeaninhada privada .

Exemplo final

15.3.9.4 Ocultação

Um tipo aninhado pode ocultar (§7.7.2.2) um membro base. O new modificador (§15.3.5) é permitido em declarações de tipo aninhadas para que a ocultação possa ser expressa explicitamente.

Exemplo: O exemplo

class Base
{
    public static void M()
    {
        Console.WriteLine("Base.M");
    }
}

class Derived: Base
{
    public new class M
    {
        public static void F()
        {
            Console.WriteLine("Derived.M.F");
        }
    }
}

class Test
{
    static void Main()
    {
        Derived.M.F();
    }
}

Mostra uma classe M aninhada que oculta o método M definido em Base.

Exemplo final

15.3.9.5 Este acesso

Um tipo aninhado e o tipo que o contém não têm uma relação especial no que diz respeito à this_access (§12.8.14). Especificamente, this dentro de um tipo aninhado não pode ser usado para se referir a membros de instância do tipo que contém. Nos casos em que um tipo aninhado precisa de acesso aos membros da instância de seu tipo de contenção, o acesso pode ser fornecido fornecendo o this para a instância do tipo de contenção como um argumento de construtor para o tipo aninhado.

Exemplo: O exemplo a seguir

class C
{
    int i = 123;
    public void F()
    {
        Nested n = new Nested(this);
        n.G();
    }

    public class Nested
    {
        C this_c;

        public Nested(C c)
        {
            this_c = c;
        }

        public void G()
        {
            Console.WriteLine(this_c.i);
        }
    }
}

class Test
{
    static void Main()
    {
        C c = new C();
        c.F();
    }
}

mostra esta técnica. Uma instância de cria uma instância de , e passa sua própria para Co construtor de , a fim de fornecer acesso subsequente aos Nestedmembros da NestedCinstância de .

Exemplo final

15.3.9.6 Acesso a membros privados e protegidos do tipo que contém

Um tipo aninhado tem acesso a todos os membros que são acessíveis ao seu tipo que o contém, incluindo os membros do tipo que contém que têm private e protected declararam acessibilidade.

Exemplo: O exemplo

class C
{
    private static void F() => Console.WriteLine("C.F");

    public class Nested
    {
        public static void G() => F();
    }
}

class Test
{
    static void Main() => C.Nested.G();
}

Mostra uma classe C que contém uma classe Nestedaninhada. Dentro Nesteddo , o método G chama o método F estático definido em C, e F tem acessibilidade declarada privada.

Exemplo final

Um tipo aninhado também pode acessar membros protegidos definidos em um tipo base de seu tipo de contenção.

Exemplo: No código a seguir

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

A classe Derived.Nested aninhada acessa o método F protegido definido na Derivedclasse Basebase do , chamando por meio de uma instância de Derived.

Exemplo final

15.3.9.7 Tipos aninhados em classes genéricas

Uma declaração de classe genérica pode conter declarações de tipo aninhadas. Os parâmetros de tipo da classe anexa podem ser usados dentro dos tipos aninhados. Uma declaração de tipo aninhada pode conter parâmetros de tipo adicionais que se aplicam somente ao tipo aninhado.

Cada declaração de tipo contida em uma declaração de classe genérica é implicitamente uma declaração de tipo genérica. Ao escrever uma referência a um tipo aninhado dentro de um tipo genérico, o tipo construído que contém, incluindo os seus argumentos de tipo, deve ser nomeado. No entanto, a partir da classe exterior, o tipo aninhado pode ser utilizado sem qualificação; O tipo de instância da classe Outer pode ser usado implicitamente ao construir o tipo aninhado.

Exemplo: A seguir mostramos três maneiras corretas diferentes de se referir a um tipo construído criado a partir de Inner; as duas primeiras são 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
    }
}

Exemplo final

Embora seja um estilo de programação ruim, um parâmetro type em um tipo aninhado pode ocultar um membro ou parâmetro type declarado no tipo externo.

Exemplo:

class Outer<T>
{
    class Inner<T>                                  // Valid, hides Outer's T
    {
        public T t;                                 // Refers to Inner's T
    }
}

Exemplo final

15.3.10 Nomes de membros reservados

15.3.10.1 Generalidades

Para facilitar a implementação em tempo de execução C# subjacente, para cada declaração de membro de origem que seja uma propriedade, evento ou indexador, a implementação deve reservar duas assinaturas de método com base no tipo de declaração de membro, seu nome e seu tipo (§15.3.10.2, §15.3.10.3, §15.3.10.4). É um erro em tempo de compilação para um programa declarar um membro cuja assinatura corresponde a uma assinatura reservada por um membro declarado no mesmo escopo, mesmo que a implementação de tempo de execução subjacente não faça uso dessas reservas.

Os nomes reservados não introduzem declarações, pelo que não participam na pesquisa de membros. No entanto, as assinaturas do método reservado associado a uma declaração participam na herança (§15.3.4) e podem ser ocultadas com o new modificador (§15.3.5).

Nota: A reserva destes nomes serve três propósitos:

  1. Para permitir que a implementação subjacente use um identificador comum como um nome de método para obter ou definir o acesso ao recurso de linguagem C#.
  2. Para permitir que outros idiomas interoperem usando um identificador comum como um nome de método para obter ou definir o acesso ao recurso de linguagem C#.
  3. Para ajudar a garantir que a fonte aceita por um compilador em conformidade seja aceita por outro, tornando as especificidades dos nomes de membros reservados consistentes em todas as implementações C#.

Nota final

A declaração de um finalizador (§15.13) também faz com que uma assinatura seja reservada (§15.3.10.5).

Certos nomes são reservados para utilização como nomes de métodos de operadores (§15.3.10.6).

15.3.10.2 Nomes de membros reservados para propriedades

Para uma propriedade P (§15.7) do tipo T, as seguintes assinaturas são reservadas:

T get_P();
void set_P(T value);

Ambas as assinaturas são reservadas, mesmo que a propriedade seja somente leitura ou somente gravação.

Exemplo: No código a seguir

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

Uma classe A define uma propriedade Psomente leitura , reservando assinaturas para get_P e set_P métodos. A class B deriva e A oculta ambas as assinaturas reservadas. O exemplo produz a saída:

123
123
456

Exemplo final

15.3.10.3 Nomes de membros reservados para eventos

Para um evento E (§15.8) do tipo Tdelegado, as seguintes assinaturas são reservadas:

void add_E(T handler);
void remove_E(T handler);

15.3.10.4 Nomes de membros reservados aos indexadores

Para um indexador (§15.9) do tipo T com lista Lde parâmetros, as seguintes assinaturas são reservadas:

T get_Item(L);
void set_Item(L, T value);

Ambas as assinaturas são reservadas, mesmo que o indexador seja somente leitura ou somente gravação.

Além disso, o nome Item do membro é reservado.

15.3.10.5 Nomes dos membros reservados aos finalizadores

Para uma classe que contenha um finalizador (§15.13), reserva-se a seguinte assinatura:

void Finalize();

15.3.10.6 Nomes de métodos reservados aos operadores

Os seguintes nomes de método são reservados. Enquanto muitos têm operadores correspondentes nesta especificação, alguns são reservados para uso por versões futuras, enquanto alguns são reservados para interoperabilidade com outros idiomas.

Nome do método Operador C#
op_Addition + (binário)
op_AdditionAssignment (reservado)
op_AddressOf (reservado)
op_Assign (reservado)
op_BitwiseAnd & (binário)
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 -- (prefixo e postfix)
op_Division /
op_DivisionAssignment (reservado)
op_Equality ==
op_ExclusiveOr ^
op_ExclusiveOrAssignment (reservado)
op_Explicit coerção explícita (estreitamento)
op_False false
op_GreaterThan >
op_GreaterThanOrEqual >=
op_Implicit coerção implícita (alargamento)
op_Increment ++ (prefixo e postfix)
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 * (binário)
op_OnesComplement ~
op_PointerDereference (reservado)
op_PointerToMemberSelection (reservado)
op_RightShift >>
op_RightShiftAssignment (reservado)
op_SignedRightShift (reservado)
op_Subtraction - (binário)
op_SubtractionAssignment (reservado)
op_True true
op_UnaryNegation - (unário)
op_UnaryPlus + (unário)
op_UnsignedRightShift (reservado para uso futuro)
op_UnsignedRightShiftAssignment (reservado)

15.4 Constantes

Uma constante é um membro de classe que representa um valor constante: um valor que pode ser calculado em tempo de compilação. Um constant_declaration introduz uma ou mais constantes de um determinado tipo.

constant_declaration
    : attributes? constant_modifier* 'const' type constant_declarators ';'
    ;

constant_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    ;

Um constant_declaration pode incluir um conjunto de atributos (§22), um new modificador (§15.3.5) e qualquer um dos tipos permitidos de acessibilidade declarada (§15.3.6). Os atributos e modificadores aplicam-se a todos os membros declarados pelo constant_declaration. Embora as constantes sejam consideradas membros estáticos, um constant_declaration não requer nem permite um static modificador. É um erro para o mesmo modificador aparecer várias vezes em uma declaração constante.

O tipo de constant_declaration especifica o tipo de membros introduzidos pela declaração. O tipo é seguido por uma lista de constant_declarators (§13.6.3), cada um dos quais introduz um novo membro. Um constant_declarator consiste em um identificador que nomeia o membro, seguido por um token "=", seguido por um constant_expression (§12.23) que dá o valor do membro.

O tipo especificado numa declaração constante é sbytebyte, , short, , ushort, intuintlongulongcharfloatdoubledecimalboolstringenum_type ou reference_type. Cada constant_expression deve produzir um valor do tipo alvo ou de um tipo que possa ser convertido para o tipo alvo através de uma conversão implícita (§10.2).

O tipo de constante deve ser pelo menos tão acessível como a própria constante (ponto 7.5.5).

O valor de uma constante é obtido numa expressão utilizando um simple_name (§12.8.4) ou um member_access (§12.8.7).

Uma constante pode participar de uma constant_expression. Assim, uma constante pode ser usada em qualquer construção que exija um constant_expression.

Nota: Exemplos de tais construções incluem case rótulos, goto case instruções, enum declarações de membro, atributos e outras declarações constantes. Nota final

Nota: Conforme descrito no §12.23, um constant_expression é uma expressão que pode ser totalmente avaliada em tempo de compilação. Uma vez que a única maneira de criar um valor não nulo de um null Nota final

Quando um nome simbólico para um valor constante é desejado, mas quando o tipo desse valor não é permitido em uma declaração constante, ou quando o valor não pode ser calculado em tempo de compilação por um constant_expression, um campo somente leitura (§15.5.3) pode ser usado em vez disso.

Nota: A semântica de versionamento de const e readonly difere (§15.5.3.3). Nota final

Uma declaração constante que declara várias constantes é equivalente a várias declarações de constantes únicas com os mesmos atributos, modificadores e tipo.

Exemplo:

class A
{
    public const double X = 1.0, Y = 2.0, Z = 3.0;
}

é equivalente a

class A
{
    public const double X = 1.0;
    public const double Y = 2.0;
    public const double Z = 3.0;
}

Exemplo final

As constantes podem depender de outras constantes dentro do mesmo programa, desde que as dependências não sejam de natureza circular.

Exemplo: No código a seguir

class A
{
    public const int X = B.Z + 1;
    public const int Y = 10;
}

class B
{
    public const int Z = A.Y + 1;
}

Um compilador deve primeiro avaliar A.Y, depois avaliar B.Ze, finalmente, avaliar A.X, produzindo os valores 10, 11e 12.

Exemplo final

Declarações constantes podem depender de constantes de outros programas, mas tais dependências só são possíveis em uma direção.

Exemplo: Referindo-se ao exemplo acima, se A e B fossem declarados em programas separados, seria possível A.X depender de B.Z, mas B.Z não poderia então depender simultaneamente de A.Y. Exemplo final

15.5 Campos

15.5.1 Generalidades

Um campo é um membro que representa uma variável associada a um objeto ou classe. Um field_declaration introduz um ou mais campos de um determinado tipo.

field_declaration
    : attributes? field_modifier* type variable_declarators ';'
    ;

field_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'static'
    | 'readonly'
    | 'volatile'
    | unsafe_modifier   // unsafe code support
    ;

variable_declarators
    : variable_declarator (',' variable_declarator)*
    ;

variable_declarator
    : identifier ('=' variable_initializer)?
    ;

unsafe_modifier (§23.2) só está disponível em código não seguro (§23).

Um field_declaration pode incluir um conjunto de atributos (§22), um new modificador (§15.3.5), uma combinação válida dos quatro modificadores de acesso (§15.3.6) e um static modificador (§15.5.2). Além disso, um field_declaration pode incluir um readonly modificador (§15.5.3) ou um volatile modificador (§15.5.4), mas não ambos. Os atributos e modificadores aplicam-se a todos os membros declarados pelo field_declaration. É um erro para o mesmo modificador aparecer várias vezes em um field_declaration.

O tipo de field_declaration especifica o tipo de membros introduzidos pela declaração. O tipo é seguido por uma lista de variable_declarators, cada um dos quais introduz um novo membro. Um variable_declarator consiste em um identificador que nomeia esse membro, opcionalmente seguido por um token "=" e um variable_initializer (§15.5.6) que dá o valor inicial desse membro.

O tipo de campo deve ser pelo menos tão acessível como o próprio campo (ponto 7.5.5).

O valor de um campo é obtido numa expressão utilizando um simple_name (§12.8.4), um member_access (§12.8.7) ou um base_access (§12.8.15). O valor de um campo não somente leitura é modificado usando uma atribuição (§12.21). O valor de um campo não somente leitura pode ser obtido e modificado usando operadores de incremento e decréscimo postfix (§12.8.16) e operadores de incremento e decréscimo de prefixo (§12.9.6).

Uma declaração de campo que declara vários campos é equivalente a várias declarações de campos únicos com os mesmos atributos, modificadores e tipo.

Exemplo:

class A
{
    public static int X = 1, Y, Z = 100;
}

é equivalente a

class A
{
    public static int X = 1;
    public static int Y;
    public static int Z = 100;
}

Exemplo final

15.5.2 Campos estáticos e de instância

Quando uma declaração de campo inclui um static modificador, os campos introduzidos pela declaração são campos estáticos. Quando nenhum static modificador está presente, os campos introduzidos pela declaração são campos de instância. Campos estáticos e campos de instância são dois dos vários tipos de variáveis (§9) suportados pelo C# e, às vezes, são referidos como variáveis estáticas e variáveis de instância, respectivamente.

Conforme explicado no §15.3.8, cada instância de uma classe contém um conjunto completo dos campos de instância da classe, enquanto há apenas um conjunto de campos estáticos para cada classe não genérica ou tipo construído fechado, independentemente do número de instâncias da classe ou do tipo construído fechado.

15.5.3 Campos somente leitura

15.5.3.1 Generalidades

Quando um field_declaration inclui um modificador, os campos introduzidos pela declaração são readonly somente leitura. Atribuições diretas para campos somente leitura só podem ocorrer como parte dessa declaração ou em um construtor de instância ou construtor estático na mesma classe. (Um campo somente leitura pode ser atribuído várias vezes nesses contextos.) Especificamente, as atribuições diretas a um campo somente leitura são permitidas somente nos seguintes contextos:

  • No variable_declarator que introduz o campo (incluindo um variable_initializer na declaração).
  • Para um campo de instância, nos construtores de instância da classe que contém a declaração de campo; para um campo estático, no construtor estático da classe que contém a declaração de campo. Estes são também os únicos contextos em que é válido passar um campo somente leitura como um parâmetro de saída ou referência.

Tentar atribuir a um campo somente leitura ou passá-lo como um parâmetro de saída ou referência em qualquer outro contexto é um erro em tempo de compilação.

15.5.3.2 Usando campos estáticos somente leitura para constantes

Um campo estático somente leitura é útil quando um nome simbólico para um valor constante é desejado, mas quando o tipo do valor não é permitido em uma declaração const ou quando o valor não pode ser calculado em tempo de compilação.

Exemplo: No código a seguir

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

Os Blackmembros , White, Red, Green, e Blue não podem ser declarados como membros const porque seus valores não podem ser calculados em tempo de compilação. No entanto, declará-las static readonly tem o mesmo efeito.

Exemplo final

15.5.3.3 Controle de versão de constantes e campos estáticos somente leitura

Constantes e campos somente leitura têm semânticas de versionamento binário diferentes. Quando uma expressão faz referência a uma constante, o valor da constante é obtido em tempo de compilação, mas quando uma expressão faz referência a um campo somente leitura, o valor do campo não é obtido até o tempo de execução.

Exemplo: Considere um aplicativo que consiste em dois programas separados:

namespace Program1
{
    public class Utils
    {
        public static readonly int x = 1;
    }
}

e

namespace Program2
{
    class Test
    {
        static void Main()
        {
            Console.WriteLine(Program1.Utils.X);
        }
    }
}

Os Program1 namespaces e Program2 denotam dois programas que são compilados separadamente. Como Program1.Utils.X é declarado como um static readonly campo, a saída do Console.WriteLine valor pela instrução não é conhecida em tempo de compilação, mas é obtida em tempo de execução. Assim, se o valor de X for alterado e Program1 for recompilado, a Console.WriteLine instrução produzirá o novo valor, mesmo Program2 que não seja recompilado. No entanto, se tivesse X sido uma constante, o valor de X teria sido obtido no momento Program2 em que foi compilado, e permaneceria inalterado por mudanças até Program1Program2 ser recompilado.

Exemplo final

15.5.4 Campos voláteis

Quando um field_declaration inclui um modificador, os campos introduzidos por essa declaração são volatile. Para campos não voláteis, técnicas de otimização que reordenam instruções podem levar a resultados inesperados e imprevisíveis em programas multi-threaded que acessam campos sem sincronização, como o fornecido pelo lock_statement (§13.13). Essas otimizações podem ser executadas pelo compilador, pelo sistema de tempo de execução ou pelo hardware. Para campos voláteis, essas otimizações de reordenação são restritas:

  • Uma leitura de um campo volátil é chamada de leitura volátil. Uma leitura volátil tem "adquirir semântica"; ou seja, é garantido que ocorra antes de quaisquer referências à memória que ocorram depois dela na sequência de instruções.
  • Uma escrita de um campo volátil é chamada de escrita volátil. Uma escrita volátil tem "semântica de lançamento"; ou seja, é garantido que acontecerá após quaisquer referências de memória anteriores à instrução de escrita na sequência de instruções.

Essas restrições garantem que todos os threads observarão gravações voláteis executadas por qualquer outro thread na ordem em que foram executadas. Uma implementação em conformidade não é necessária para fornecer uma única ordenação total de gravações voláteis como visto de todos os threads de execução. O tipo de campo volátil deve ser um dos seguintes:

  • Uma reference_type.
  • Um type_parameter que se sabe ser um tipo de referência (§15.2.5).
  • O tipo byte, sbyte, short, ushort, , int, uint, char, floatbool, System.IntPtr, ou System.UIntPtr.
  • Um enum_type com um enum_base tipo de byte, sbyte, short, ushort, intou uint.

Exemplo: O exemplo

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

produz a saída:

result = 143

Neste exemplo, o método Main inicia um novo thread que executa o método Thread2. Este método armazena um valor em um campo não volátil chamado result, em seguida, armazena true no campo finishedvolátil. O thread principal aguarda que o campo finished seja definido como truee, em seguida, lê o campo result. Uma vez finished declarado volatile, o fio condutor deve ler o valor 143 do campo result. Se o campo finished não tivesse sido declarado volatile, então seria permitido que o repositório result ficasse visível para o thread principal após o store parafinished, e, portanto, para o thread principal ler o valor 0 do campo result. A declaração finished como campo volatile evita tais incoerências.

Exemplo final

15.5.5 Inicialização de campo

O valor inicial de um campo, seja ele um campo estático ou um campo de instância, é o valor padrão (§9.3) do tipo do campo. Não é possível observar o valor de um campo antes que essa inicialização padrão tenha ocorrido e, portanto, um campo nunca é "não inicializado".

Exemplo: O exemplo

class Test
{
    static bool b;
    int i;

    static void Main()
    {
        Test t = new Test();
        Console.WriteLine($"b = {b}, i = {t.i}");
    }
}

produz a saída

b = False, i = 0

porque b e i são ambos inicializados automaticamente para valores padrão.

Exemplo final

15.5.6 Inicializadores variáveis

15.5.6.1 Generalidades

As declarações de campo podem incluir variable_initializers. Para campos estáticos, inicializadores de variáveis correspondem a instruções de atribuição que são executadas durante a inicialização da classe. Por exemplo, campos, inicializadores de variáveis correspondem a instruções de atribuição que são executadas quando uma instância da classe é criada.

Exemplo: O exemplo

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

produz a saída

x = 1.4142135623730951, i = 100, s = Hello

porque uma atribuição a x ocorre quando inicializadores de campo estáticos são executados e atribuições a i e s ocorrem quando os inicializadores de campo de instância são executados.

Exemplo final

A inicialização do valor padrão descrita no §15.5.5 ocorre para todos os campos, incluindo campos que têm inicializadores variáveis. Assim, quando uma classe é inicializada, todos os campos estáticos nessa classe são primeiro inicializados com seus valores padrão e, em seguida, os inicializadores de campo estático são executados em ordem textual. Da mesma forma, quando uma instância de uma classe é criada, todos os campos de instância nessa instância são primeiro inicializados com seus valores padrão e, em seguida, os inicializadores do campo de instância são executados em ordem textual. Quando há declarações de campo em várias declarações de tipo parcial para o mesmo tipo, a ordem das partes não é especificada. No entanto, dentro de cada parte, os inicializadores de campo são executados em ordem.

É possível que campos estáticos com inicializadores variáveis sejam observados em seu estado de valor padrão.

Exemplo: No entanto, isso é fortemente desencorajado por uma questão de estilo. O exemplo

class Test
{
    static int a = b + 1;
    static int b = a + 1;

    static void Main()
    {
        Console.WriteLine($"a = {a}, b = {b}");
    }
}

apresenta este comportamento. Apesar das definições circulares de a e b, o programa é válido. Isso resulta na saída

a = 1, b = 2

porque os campos a estáticos e b são inicializados para 0 (o valor padrão para int) antes de seus inicializadores serem executados. Quando o inicializador é a executado, o valor de b é zero e, portanto a , é inicializado como 1. Quando o inicializador para b é executado, o valor de a já 1é , e assim b é inicializado como 2.

Exemplo final

15.5.6.2 Inicialização de campo estático

Os inicializadores da variável de campo estático de uma classe correspondem a uma sequência de atribuições que são executadas na ordem textual em que aparecem na declaração de classe (§15.5.6.1). Dentro de uma classe parcial, o significado de "ordem textual" é especificado pelo §15.5.6.1. Se existir um construtor estático (§15.12) na classe, a execução dos inicializadores de campo estático ocorre imediatamente antes de executar esse construtor estático. Caso contrário, os inicializadores de campo estático são executados em um momento dependente da implementação antes do primeiro uso de um campo estático dessa classe.

Exemplo: O exemplo

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

pode produzir a saída:

Init A
Init B
1 1

ou a saída:

Init B
Init A
1 1

Como a execução do inicializador do e Xdo inicializador do pode ocorrer em qualquer ordem, eles só são restritos a ocorrer antes das Yreferências a esses campos. No entanto, no exemplo:

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

A produção deve ser:

Init B
Init A
1 1

porque as regras para quando os construtores estáticos são executados (conforme definido no §15.12) fornecem que Bo construtor estático do (e, portanto, Bos inicializadores de campo estático) deve ser executado antes Ado construtor estático e dos inicializadores de campo.

Exemplo final

15.5.6.3 Inicialização do campo de instância

Os inicializadores da variável de campo de instância de uma classe correspondem a uma sequência de atribuições que são executadas imediatamente após a entrada em qualquer um dos construtores de instância (§15.11.3) dessa classe. Dentro de uma classe parcial, o significado de "ordem textual" é especificado pelo §15.5.6.1. Os inicializadores de variáveis são executados na ordem textual em que aparecem na declaração de classe (§15.5.6.1). O processo de criação e inicialização da instância de classe é descrito mais detalhadamente no §15.11.

Um inicializador de variável para um campo de instância não pode fazer referência à instância que está sendo criada. Assim, é um erro em tempo de compilação fazer referência this em um inicializador de variável, como é um erro em tempo de compilação para um inicializador de variável fazer referência a qualquer membro de instância por meio de um simple_name.

Exemplo: No código a seguir

class A
{
    int x = 1;
    int y = x + 1;     // Error, reference to instance member of this
}

O inicializador da variável para y resulta em um erro em tempo de compilação porque faz referência a um membro da instância que está sendo criada.

Exemplo final

15.6 Métodos

15.6.1 Generalidades

Um método é um membro que implementa um cálculo ou ação que pode ser executada por um objeto ou classe. Os métodos são declarados utilizando method_declarations:

method_declaration
    : attributes? method_modifiers return_type method_header method_body
    | attributes? ref_method_modifiers ref_kind ref_return_type method_header
      ref_method_body
    ;

method_modifiers
    : method_modifier* 'partial'?
    ;

ref_kind
    : 'ref'
    | 'ref' 'readonly'
    ;

ref_method_modifiers
    : ref_method_modifier*
    ;

method_header
    : member_name '(' parameter_list? ')'
    | member_name type_parameter_list '(' parameter_list? ')'
      type_parameter_constraints_clause*
    ;

method_modifier
    : ref_method_modifier
    | 'async'
    ;

ref_method_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'static'
    | 'virtual'
    | 'sealed'
    | 'override'
    | 'abstract'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;

return_type
    : ref_return_type
    | 'void'
    ;

ref_return_type
    : type
    ;

member_name
    : identifier
    | interface_type '.' identifier
    ;

method_body
    : block
    | '=>' null_conditional_invocation_expression ';'
    | '=>' expression ';'
    | ';'
    ;

ref_method_body
    : block
    | '=>' 'ref' variable_reference ';'
    | ';'
    ;

Notas gramaticais:

  • unsafe_modifier (§23.2) só está disponível em código não seguro (§23).
  • Ao reconhecer um method_body se forem aplicáveis tanto a alternativa null_conditional_invocation_expressioncomo a expressão, optar-se-á pela primeira.

Nota: A sobreposição e prioridade entre as alternativas aqui é apenas por conveniência descritiva, as regras gramaticais poderiam ser elaboradas para remover a sobreposição. ANTLR, e outros sistemas gramaticais, adotam a mesma conveniência e, portanto , method_body tem a semântica especificada automaticamente. Nota final

Um method_declaration pode incluir um conjunto de atributos (§22) e um dos tipos permitidos de acessibilidade declarada (§15.3.6), os new modificadores (§15.3.5), static (§15.6.3), (virtual), override (§15.6.5), sealed (abstract), (§15.6.7), extern (§15.6.8) e async (§15.15).

Uma declaração tem uma combinação válida de modificadores se todos os itens a seguir forem verdadeiros:

  • A declaração inclui uma combinação válida de modificadores de acesso (§15.3.6).
  • A declaração não inclui o mesmo modificador várias vezes.
  • A declaração inclui, no máximo, um dos seguintes modificadores: static, virtual, e override.
  • A declaração inclui, no máximo, um dos seguintes modificadores: new e override.
  • Se a declaração incluir o abstract modificador, a declaração não incluirá nenhum dos seguintes modificadores: static, virtual, sealed, ou extern.
  • Se a declaração incluir o private modificador, a declaração não incluirá nenhum dos seguintes modificadores: virtual, override, ou abstract.
  • Se a declaração incluir o sealed modificador, a declaração também incluirá o override modificador.
  • Se a declaração incluir o partial modificador, ela não incluirá nenhum dos seguintes modificadores: new, public, protected, internal, private, virtualsealed, , override, , abstractou extern.

Os métodos são classificados de acordo com o que eles retornam:

  • Se ref estiver presente, o método é returns-by-ref e retorna uma referência variável, que é opcionalmente somente leitura;
  • Caso contrário, se return_type for void, o método retorna sem valor e não retorna um valor;
  • Caso contrário, o método é returns-by-value e retorna um valor.

A return_type de uma declaração de método returns-by-value ou returns-no-value especifica o tipo do resultado, se houver, retornado pelo método. Apenas um método de retorno sem valor pode incluir o partial modificador (§15.6.9). Se a declaração incluir o async modificador, então return_type deve ser void ou o método retorna por valor e o tipo de retorno é um tipo de tarefa (§15.15.1).

O ref_return_type de uma declaração de método returns-by-ref especifica o tipo da variável referenciada pelo variable_reference retornado pelo método.

Um método genérico é um método cuja declaração inclui um type_parameter_list. Isso especifica os parâmetros de tipo para o método. Os type_parameter_constraints_clauseopcionais especificam as restrições para os parâmetros de tipo.

Uma method_declaration genérica para uma implementação explícita de membro da interface não deve ter nenhum type_parameter_constraints_clauses, a declaração herda quaisquer restrições das restrições no método de interface.

Da mesma forma, uma declaração de método com o override modificador não deve ter nenhum type_parameter_constraints_clauses e as restrições dos parâmetros de tipo do método são herdadas do método virtual que está sendo substituído.

O member_name especifica o nome do método. A menos que o método seja uma implementação explícita de membro da interface (§18.6.2), o member_name é simplesmente um identificador.

Para uma implementação explícita de membro da interface, o member_name consiste em um interface_type seguido por um "." e um identificador. Neste caso, a declaração não deve incluir modificadores para além de (eventualmente) extern ou async.

O parameter_list opcional especifica os parâmetros do método (§15.6.2).

O return_type ou ref_return_type, bem como cada um dos tipos referidos na parameter_list de um método, devem ser, pelo menos, tão acessíveis como o próprio método (ponto 7.5.5).

A method_body de um método returns-by-value ou returns-no-value é um ponto-e-vírgula, um corpo de bloco ou um corpo de expressão. Um corpo de bloco consiste em um bloco, que especifica as instruções a serem executadas quando o método é invocado. Um corpo de =>expressão consiste em , seguido por um null_conditional_invocation_expression ou expressão e um ponto-e-vírgula, e denota uma única expressão a ser executada quando o método é invocado.

Para métodos abstratos e externos, o method_body consiste simplesmente em um ponto-e-vírgula. Para métodos parciais, o method_body pode consistir em um ponto-e-vírgula, um corpo de bloco ou um corpo de expressão. Para todos os outros métodos, o method_body é um corpo de bloco ou um corpo de expressão.

Se o method_body consistir num ponto e vírgula, a declaração não deve incluir o async modificador.

A ref_method_body de um método returns-by-ref é um ponto-e-vírgula, um corpo de bloco ou um corpo de expressão. Um corpo de bloco consiste em um bloco, que especifica as instruções a serem executadas quando o método é invocado. Um corpo de expressão consiste em , seguido por =>ref, um variable_reference e um ponto-e-vírgula, e denota uma única variable_reference para avaliar quando o método é invocado.

Para métodos abstratos e externos, o ref_method_body consiste simplesmente em um ponto-e-vírgula, para todos os outros métodos, o ref_method_body é um corpo de bloco ou um corpo de expressão.

O nome, o número de parâmetros de tipo e a lista de parâmetros de um método definem a assinatura (§7.6) do método. Especificamente, a assinatura de um método consiste em seu nome, o número de seus parâmetros de tipo, e o número, parameter_mode_modifiers (§15.6.2.1), e tipos de seus parâmetros. O tipo de retorno não faz parte da assinatura de um método, nem os nomes dos parâmetros, os nomes dos parâmetros de tipo ou as restrições. Quando um tipo de parâmetro faz referência a um parâmetro de tipo do método, a posição ordinal do parâmetro type (não o nome do parâmetro type) é usada para equivalência de tipo.

O nome de um método deve diferir dos nomes de todos os outros métodos não declarados na mesma classe. Além disso, a assinatura de um método deve diferir das assinaturas de todos os outros métodos declarados na mesma classe, e dois métodos declarados na mesma classe não devem ter assinaturas que difiram apenas por in, oute ref.

Os type_parameters do método estão no escopo em todo o method_declaration e podem ser usados para formar tipos em todo esse escopo em return_type ou ref_return_type, method_body ou ref_method_body e type_parameter_constraints_clauses, mas não em atributos.

Todos os parâmetros e parâmetros-tipo devem ter nomes diferentes.

15.6.2 Parâmetros do método

15.6.2.1 Generalidades

Os parâmetros de um método, se houver, são declarados pelo parameter_list do 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
    ;

A lista de parâmetros consiste em um ou mais parâmetros separados por vírgula, dos quais apenas o último pode ser um parameter_array.

Um fixed_parameter consiste em um conjunto opcional de atributos (§22), um opcionalinout, , ref, ou this modificador, um tipo, um identificador e um default_argument opcional. Cada fixed_parameter declara um parâmetro do tipo dado com o nome próprio. O this modificador designa o método como um método de extensão e só é permitido no primeiro parâmetro de um método estático em uma classe estática não genérica e não aninhada. Se o parâmetro for um struct tipo ou um parâmetro de tipo restrito a um struct, o this modificador pode ser combinado com o ref ou in modificador, mas não com o out modificador. Os métodos de extensão são descritos em mais pormenor no ponto 15.6.10. Um fixed_parameter com um default_argument é conhecido como um parâmetro opcional, enquanto um fixed_parameter sem um default_argument é um parâmetro necessário. Um parâmetro obrigatório não deve aparecer depois de um parâmetro opcional numa parameter_list.

Um parâmetro com um refmodificador ou outthis um modificador não pode ter um default_argument. Um parâmetro de entrada pode ter um default_argument. A expressão num default_argument deve ser uma das seguintes:

  • um constant_expression
  • uma expressão do formulário new S() onde S é um tipo de valor
  • uma expressão do formulário default(S) onde S é um tipo de valor

A expressão deve ser implicitamente convertível por uma conversão de identidade ou anulável para o tipo do parâmetro.

Se parâmetros opcionais ocorrerem em uma declaração de método parcial de implementação (§15.6.9), uma implementação explícita de membro da interface (§18.6.2), uma declaração de indexador de parâmetro único (§15.9), ou em uma declaração de operador (§15.10.1), um compilador deve dar um aviso, uma vez que esses membros nunca podem ser invocados de uma forma que permita que os argumentos sejam omitidos.

Um parameter_array consiste em um conjunto opcional de atributos (§22), um params modificador, um array_type e um identificador. Uma matriz de parâmetros declara um único parâmetro do tipo de matriz determinado com o nome fornecido. A array_type de uma matriz de parâmetros deve ser um tipo de matriz unidimensional (§17.2). Em uma invocação de método, uma matriz de parâmetros permite que um único argumento do tipo de matriz determinado seja especificado ou permite que zero ou mais argumentos do tipo de elemento de matriz sejam especificados. As matrizes de parâmetros são descritas mais pormenorizadamente no §15.6.2.4.

Um parameter_array pode ocorrer após um parâmetro opcional, mas não pode ter um valor padrão – a omissão de argumentos para um parameter_array resultaria, em vez disso, na criação de uma matriz vazia.

Exemplo: O seguinte ilustra 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
) { }

No parameter_list for M, i é um parâmetro obrigatórioref, d é um parâmetro de valor obrigatório, b, so , e t são parâmetros de valor opcionais e a é uma matriz de parâmetros.

Exemplo final

Uma declaração de método cria um espaço de declaração separado (§7.3) para parâmetros e parâmetros de tipo. Os nomes são introduzidos neste espaço de declaração pela lista de parâmetros de tipo e pela lista de parâmetros do método. O corpo do método, se houver, é considerado aninhado dentro deste espaço de declaração. É um erro para dois membros de um espaço de declaração de método ter o mesmo nome.

Uma invocação de método (§12.8.10.2) cria uma cópia, específica para essa invocação, dos parâmetros e variáveis locais do método, e a lista de argumentos da invocação atribui valores ou referências de variáveis aos parâmetros recém-criados. No bloco de um método, os parâmetros podem ser referenciados pelos seus identificadores em expressões simple_name (§12.8.4).

Existem os seguintes tipos de parâmetros:

Nota: Conforme descrito no §7.6, o in, oute ref os modificadores fazem parte da assinatura de um método, mas o params modificador não. Nota final

15.6.2.2 Parâmetros de valor

Um parâmetro declarado sem modificadores é um parâmetro de valor. Um parâmetro value é uma variável local que obtém seu valor inicial do argumento correspondente fornecido na chamada do método.

Para regras de atribuição definitiva, ver §9.2.5.

O argumento correspondente numa invocação de método deve ser uma expressão implicitamente convertível (§10.2) para o tipo de parâmetro.

Um método é permitido para atribuir novos valores a um parâmetro value. Tais atribuições afetam apenas o local de armazenamento local representado pelo parâmetro value — elas não têm efeito sobre o argumento real dado na chamada do método.

15.6.2.3 Parâmetros de subreferência

15.6.2.3.1 Generalidades

Os parâmetros de entrada, saída e referência são parâmetros por referência. Um parâmetro por referência é uma variável de referência local (§9.7), o referente inicial é obtido a partir do argumento correspondente fornecido na invocação do método.

Nota: O referente de um parâmetro por referência pode ser alterado usando o operador ref assignment (= ref).

Quando um parâmetro é um parâmetro por referência, o argumento correspondente numa invocação de método deve consistir na palavra-chave correspondente, in, , ou ref, seguida de um out (§9.5) do mesmo tipo que o parâmetro. No entanto, quando o parâmetro é um in parâmetro, o argumento pode ser uma expressão para a qual existe uma conversão implícita (§10.2) dessa expressão de argumento para o tipo do parâmetro correspondente.

Os parâmetros de sub-referência não são permitidos em funções declaradas como iterador (§15.14) ou função assíncrona (§15.15).

Em um método que usa vários parâmetros por referência, é possível que vários nomes representem o mesmo local de armazenamento.

15.6.2.3.2 Parâmetros de entrada

Um parâmetro declarado com um in modificador é um parâmetro de entrada. O argumento correspondente a um parâmetro de entrada é uma variável existente no ponto da chamada do método ou uma variável criada pela implementação (§12.6.2.3) na chamada do método. Para regras de atribuição definitiva, ver §9.2.8.

É um erro em tempo de compilação para modificar o valor de um parâmetro de entrada.

Nota: O principal objetivo dos parâmetros de entrada é a eficiência. Quando o tipo de um parâmetro de método é uma estrutura grande (em termos de requisitos de memória), é útil ser capaz de evitar copiar todo o valor do argumento ao chamar o método. Os parâmetros de entrada permitem que os métodos se refiram a valores existentes na memória, ao mesmo tempo em que fornecem proteção contra alterações indesejadas nesses valores. Nota final

15.6.2.3.3. Parâmetros de referência

Um parâmetro declarado com um ref modificador é um parâmetro de referência. Para regras de atribuição definitiva, ver §9.2.6.

Exemplo: O exemplo

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

produz a saída

i = 2, j = 1

Para a invocação de Swap in Main, x representa i e y representa j. Assim, a invocação tem o efeito de trocar os valores de i e j.

Exemplo final

Exemplo: No código a seguir

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

a invocação de F in G passa uma referência para s ambos e ab. Assim, para essa invocação, os nomes s, ae b todos se referem ao mesmo local de armazenamento, e as três atribuições modificam o campo sde instância .

Exemplo final

Para um struct tipo, dentro de um método de instância, acessador de instância (§12.2.1) ou construtor de instância com um inicializador de construtor, a this palavra-chave se comporta exatamente como um parâmetro de referência do tipo struct (§12.8.14).

15.6.2.3.4 Parâmetros de saída

Um parâmetro declarado com um out modificador é um parâmetro de saída. Para regras de atribuição definitiva, ver §9.2.7.

Um método declarado como método parcial (§15.6.9) não deve ter parâmetros de saída.

Nota: Os parâmetros de saída são normalmente usados em métodos que produzem vários valores de retorno. Nota final

Exemplo:

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

O exemplo produz a saída:

c:\Windows\System\
hello.txt

Observe que as dir variáveis e name podem ser desatribuídas antes de serem passadas para SplitPath, e que elas são consideradas definitivamente atribuídas após a chamada.

Exemplo final

15.6.2.4 Matrizes de parâmetros

Um parâmetro declarado com um params modificador é uma matriz de parâmetros. Se uma lista de parâmetros incluir uma matriz de parâmetros, será o último parâmetro da lista e será do tipo de matriz unidimensional.

Exemplo: Os tipos string[] e string[][] pode ser usado como o tipo de uma matriz de parâmetros, mas o tipo string[,] não pode. Exemplo final

Nota: Não é possível combinar o params modificador com os modificadores in, outou ref. Nota final

Uma matriz de parâmetros permite que os argumentos sejam especificados de uma das duas maneiras em uma chamada de método:

  • O argumento dado para uma matriz de parâmetros pode ser uma única expressão que é implicitamente conversível (§10.2) para o tipo de matriz de parâmetros. Neste caso, a matriz de parâmetros age precisamente como um parâmetro de valor.
  • Como alternativa, a invocação pode especificar zero ou mais argumentos para a matriz de parâmetros, onde cada argumento é uma expressão que é implicitamente conversível (§10.2) para o tipo de elemento da matriz de parâmetros. Nesse caso, a invocação cria uma instância do tipo de matriz de parâmetros com um comprimento correspondente ao número de argumentos, inicializa os elementos da instância de matriz com os valores de argumento fornecidos e usa a instância de matriz recém-criada como o argumento real.

Exceto para permitir um número variável de argumentos em uma invocação, uma matriz de parâmetros é precisamente equivalente a um parâmetro de valor (§15.6.2.2) do mesmo tipo.

Exemplo: O exemplo

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

produz a saída

Array contains 3 elements: 1 2 3
Array contains 4 elements: 10 20 30 40
Array contains 0 elements:

A primeira invocação de simplesmente passa a matriz F como um parâmetro de arr valor. A segunda invocação de F cria automaticamente um elemento de quatro com int[] os valores de elemento fornecidos e passa essa instância de matriz como um parâmetro de valor. Da mesma forma, a terceira invocação de cria um elemento F zero e passa essa instância como um parâmetro de int[] valor. A segunda e terceira invocações equivalem precisamente à escrita:

F(new int[] {10, 20, 30, 40});
F(new int[] {});

Exemplo final

Ao executar a resolução de sobrecarga, um método com uma matriz de parâmetros pode ser aplicável, tanto na sua forma normal como na sua forma expandida (§12.6.4.2). A forma expandida de um método só está disponível se a forma normal do método não for aplicável e apenas se um método aplicável com a mesma assinatura que o formulário expandido não for já declarado no mesmo tipo.

Exemplo: O exemplo

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

produz a saída

F()
F(object[])
F(object,object)
F(object[])
F(object[])

No exemplo, duas das possíveis formas expandidas do método com uma matriz de parâmetros já estão incluídas na classe como métodos regulares. Essas formas expandidas não são, portanto, consideradas ao executar a resolução de sobrecarga, e a primeira e terceira invocações de método, portanto, selecionam os métodos regulares. Quando uma classe declara um método com uma matriz de parâmetros, não é incomum incluir também alguns dos formulários expandidos como métodos regulares. Ao fazer isso, é possível evitar a alocação de uma instância de matriz que ocorre quando uma forma expandida de um método com uma matriz de parâmetros é invocada.

Exemplo final

Uma matriz é um tipo de referência, portanto, o valor passado para uma matriz de parâmetros pode ser null.

Exemplo: O exemplo:

class Test
{
    static void F(params string[] array) =>
        Console.WriteLine(array == null);

    static void Main()
    {
        F(null);
        F((string) null);
    }
}

produz a saída:

True
False

A segunda invocação produz False como é equivalente e passa uma F(new string[] { null }) matriz contendo uma única referência nula.

Exemplo final

Quando o tipo de uma matriz de parâmetros é object[], surge uma ambiguidade potencial entre a forma normal do método e a forma expandida para um único object parâmetro. A razão para a ambiguidade é que um object[] é implicitamente conversível em tipo object. No entanto, a ambiguidade não apresenta qualquer problema, uma vez que pode ser resolvida através da inserção de um elenco, se necessário.

Exemplo: O exemplo

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

produz a saída

System.Int32 System.String System.Double
System.Object[]
System.Object[]
System.Int32 System.String System.Double

Na primeira e última invocações de F, a forma normal de F é aplicável porque existe uma conversão implícita do tipo de argumento para o tipo de parâmetro (ambos são do tipo object[]). Assim, a resolução de sobrecarga seleciona a forma normal de , e o argumento é passado como um parâmetro de Fvalor regular. Na segunda e terceira invocações, a forma normal de não é aplicável porque não existe conversão implícita do tipo de argumento para o tipo de F parâmetro (o tipo object não pode ser implicitamente convertido em tipo object[]). No entanto, a forma expandida de é aplicável, por isso é selecionado por resolução de F sobrecarga. Como resultado, um elemento object[] único é criado pela invocação, e o único elemento da matriz é inicializado com o valor de argumento dado (que por si só é uma referência a um object[]).

Exemplo final

15.6.3 Métodos estáticos e de instância

Quando uma declaração de método inclui um static modificador, esse método é dito ser um método estático. Quando nenhum static modificador está presente, o método é dito ser um método de instância.

Um método estático não opera em uma instância específica, e é um erro em tempo de compilação para this se referir em um método estático.

Um método de instância opera em uma determinada instância de uma classe, e essa instância pode ser acessada como this (§12.8.14).

As diferenças entre membros estáticos e de instância são discutidas mais detalhadamente no §15.3.8.

15.6.4 Métodos virtuais

Quando uma declaração de método de instância inclui um modificador virtual, esse método é dito ser um método virtual. Quando nenhum modificador virtual está presente, o método é dito ser um método não-virtual.

A implementação de um método não virtual é invariável: A implementação é a mesma se o método é invocado em uma instância da classe na qual é declarado ou uma instância de uma classe derivada. Em contraste, a implementação de um método virtual pode ser substituída por classes derivadas. O processo de substituição da implementação de um método virtual herdado é conhecido como a substituição desse método (§15.6.5).

Em uma chamada de método virtual, o tipo de tempo de execução da instância para a qual essa invocação ocorre determina a implementação real do método a ser invocada. Em uma invocação de método não virtual, o tipo de tempo de compilação da instância é o fator determinante. Em termos precisos, quando um método nomeado N é invocado com uma lista A de argumentos em uma instância com um tipo C de tempo de compilação e um tipo R de tempo de execução (onde R é ou C uma classe derivada de ), a invocação é processada Cda seguinte maneira:

  • Em tempo de ligação, a resolução de sobrecarga é aplicada a C, Ne A, para selecionar um método M específico do conjunto de métodos declarados e herdados por C. Isto é descrito no §12.8.10.2.
  • Em seguida, em tempo de execução:
    • Se M for um método não virtual, M é invocado.
    • Caso contrário, M é um método virtual, e a implementação mais derivada de M com respeito a R é invocada.

Para cada método virtual declarado ou herdado por uma classe, existe uma implementação mais derivada do método em relação a essa classe. A implementação mais derivada de um método M virtual em relação a uma classe R é determinada da seguinte forma:

  • Se R contém a declaração virtual de introdução de M, então esta é a implementação mais derivada de M com relação a R.
  • Caso contrário, se R contiver uma substituição de , então esta é a implementação mais derivada Mde M com relação a R.
  • Caso contrário, a implementação mais derivada de M com relação a R é a mesma que a implementação mais derivada de M com relação à classe de base direta de R.

Exemplo: O exemplo a seguir ilustra as diferenças entre métodos virtuais e não virtuais:

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

No exemplo, A introduz um método F não virtual e um método Gvirtual . A classe B introduz um novo método Fnão virtual, ocultando assim o método herdado F, e também substitui o método Gherdado. O exemplo produz a saída:

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

Observe que a instrução a.G() invoca B.G, não A.G. Isso ocorre porque o tipo de tempo de execução da instância (que é B), não o tipo de tempo de compilação da instância (que é A), determina a implementação real do método a ser invocada.

Exemplo final

Como os métodos têm permissão para ocultar métodos herdados, é possível que uma classe contenha vários métodos virtuais com a mesma assinatura. Isso não apresenta um problema de ambiguidade, uma vez que todos, exceto o método mais derivado, estão ocultos.

Exemplo: No código a seguir

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

as C classes e D contêm dois métodos virtuais com a mesma assinatura: o introduzido por A e o introduzido por C. O método introduzido por C oculta o método herdado de A. Assim, a declaração de substituição substitui D o método introduzido pela C, e não é possível D substituir o método introduzido pela A. O exemplo produz a saída:

B.F
B.F
D.F
D.F

Observe que é possível invocar o método virtual oculto acessando uma instância de através de D um tipo menos derivado no qual o método não está oculto.

Exemplo final

15.6.5 Métodos de substituição

Quando uma declaração de método de instância inclui um override modificador, o método é dito ser um método de substituição. Um método de substituição substitui um método virtual herdado com a mesma assinatura. Enquanto uma declaração de método virtual introduz um novo método, uma declaração de método de substituição especializa um método virtual herdado existente, fornecendo uma nova implementação desse método.

O método substituído por uma declaração de substituição é conhecido como método base substituído Para um método M de substituição declarado em uma classeC, o método de base substituído é determinado examinando cada classe base de C, começando com a classe base direta de e continuando com cada classe base direta sucessiva, até que em um determinado tipo de C classe base pelo menos um método acessível esteja localizado que tenha a mesma assinatura M que após a substituição de argumentos de tipo. Para efeitos de localização do método de base substituído, um método é considerado acessível se for public, se for protected, se for protected internal, ou se for ou internal e private protected declarado no mesmo programa que C.

Um erro em tempo de compilação ocorre a menos que todos os itens a seguir sejam verdadeiros para uma declaração de substituição:

  • Um método de base substituído pode ser localizado como descrito acima.
  • Há exatamente um desses métodos de base substituídos. Esta restrição só tem efeito se o tipo de classe base for um tipo construído em que a substituição de argumentos de tipo torna a assinatura de dois métodos a mesma.
  • O método base substituído é um método virtual, abstrato ou de substituição. Em outras palavras, o método base substituído não pode ser estático ou não virtual.
  • O método de base substituído não é um método selado.
  • Há uma conversão de identidade entre o tipo de retorno do método base substituído e o método de substituição.
  • A declaração de substituição e o método base substituído têm a mesma acessibilidade declarada. Em outras palavras, uma declaração de substituição não pode alterar a acessibilidade do método virtual. No entanto, se o método de base substituído for protegido internamente e for declarado em um assembly diferente do assembly que contém a declaração de substituição, a acessibilidade declarada da declaração de substituição será protegida.
  • A declaração de substituição não especifica nenhum type_parameter_constraints_clauses. Em vez disso, as restrições são herdadas do método base substituído. As restrições que são parâmetros de tipo no método substituído podem ser substituídas por argumentos de tipo na restrição herdada. Isso pode levar a restrições que não são válidas quando especificadas explicitamente, como tipos de valor ou tipos selados.

Exemplo: O seguinte demonstra como as regras de substituição funcionam para classes 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>
}

Exemplo final

Uma declaração de substituição pode acessar o método base substituído usando um base_access (§12.8.15).

Exemplo: No código a seguir

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

a base.PrintFields() invocação em B invoca o método PrintFields declarado em A. Um base_access desativa o mecanismo de invocação virtual e simplesmente trata o método base como um não-métodovirtual . Se a invocação em B tivesse sido escrita ((A)this).PrintFields(), ela invocaria recursivamente o método declarado PrintFields em B, não o declarado em A, uma vez que PrintFields é virtual e o tipo de tempo de execução de ((A)this) é B.

Exemplo final

Somente incluindo um override modificador é que um método pode substituir outro método. Em todos os outros casos, um método com a mesma assinatura de um método herdado simplesmente oculta o método herdado.

Exemplo: No código a seguir

class A
{
    public virtual void F() {}
}

class B : A
{
    public virtual void F() {} // Warning, hiding inherited F()
}

o F método in B não inclui um override modificador e, portanto, não substitui o F método em A. Em vez disso, o F método in B oculta o método em A, e um aviso é relatado porque a declaração não inclui um novo modificador.

Exemplo final

Exemplo: No código a seguir

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
}

O F método In B oculta o método virtual F herdado do A. Uma vez que o novo F in B tem acesso privado, o seu âmbito inclui apenas o corpo de classe de B e não se estende a C. Portanto, a declaração de F in C é permitida para substituir o F herdado de A.

Exemplo final

15.6.6 Métodos selados

Quando uma declaração de método de instância inclui um sealed modificador, esse método é dito ser um método selado. Um método selado substitui um método virtual herdado com a mesma assinatura. O modificador deve igualmente ser marcado com um override método selado. O uso do sealed modificador impede que uma classe derivada substitua ainda mais o método.

Exemplo: O exemplo

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

A classe B fornece dois métodos de substituição: um F método que tem o sealed modificador e um G método que não. B's uso do sealed modificador impede C de mais substituição F.

Exemplo final

15.6.7 Métodos abstratos

Quando uma declaração de método de instância inclui um abstract modificador, esse método é dito ser um método abstrato. Embora um método abstrato seja implicitamente também um método virtual, ele não pode ter o modificador virtual.

Uma declaração de método abstrato introduz um novo método virtual, mas não fornece uma implementação desse método. Em vez disso, classes derivadas não abstratas são obrigadas a fornecer sua própria implementação substituindo esse método. Como um método abstrato não fornece nenhuma implementação real, o corpo do método de um método abstrato consiste simplesmente em um ponto-e-vírgula.

As declarações de método abstrato só são permitidas em classes abstratas (§15.2.2.2).

Exemplo: No código a seguir

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

A Shape classe define a noção abstrata de um objeto de forma geométrica que pode pintar a si mesmo. O Paint método é abstrato porque não há nenhuma implementação padrão significativa. As Ellipse classes e Box são implementações concretas Shape . Como essas classes não são abstratas, elas são necessárias para substituir o Paint método e fornecer uma implementação real.

Exemplo final

É um erro em tempo de compilação para um base_access (§12.8.15) fazer referência a um método abstrato.

Exemplo: No código a seguir

abstract class A
{
    public abstract void F();
}

class B : A
{
    // Error, base.F is abstract
    public override void F() => base.F();
}

Um erro em tempo de compilação é relatado para a invocação porque faz referência a base.F() um método abstrato.

Exemplo final

Uma declaração de método abstrato tem permissão para substituir um método virtual. Isso permite que uma classe abstrata force a reimplementação do método em classes derivadas e torna a implementação original do método indisponível.

Exemplo: No código a seguir

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

class A declara um método virtual, class B substitui esse método por um método abstrato e class C substitui o método abstrato para fornecer sua própria implementação.

Exemplo final

15.6.8 Métodos externos

Quando uma declaração de método inclui um extern modificador, o método é dito ser um método externo. Os métodos externos são implementados externamente, normalmente usando uma linguagem diferente de C#. Como uma declaração de método externo não fornece nenhuma implementação real, o corpo do método de um método externo consiste simplesmente em um ponto-e-vírgula. Um método externo não deve ser genérico.

O mecanismo pelo qual se consegue a ligação a um método externo é definido pela implementação.

Exemplo: O exemplo a seguir demonstra o uso do extern modificador e do 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);
}

Exemplo final

15.6.9 Métodos parciais

Quando uma declaração de método inclui um partial modificador, esse método é dito ser um método parcial. Os métodos parciais só podem ser declarados como membros de tipos parciais (§15.2.7) e estão sujeitos a uma série de restrições.

Os métodos parciais podem ser definidos numa parte de uma declaração de tipo e aplicados noutra. A implementação é opcional; Se nenhuma parte implementar o método parcial, a declaração de método parcial e todas as chamadas para ela serão removidas da declaração de tipo resultante da combinação das partes.

Os métodos parciais não devem definir modificadores de acesso; são implicitamente privados. O seu tipo de retorno deve ser void, e os seus parâmetros não devem ser parâmetros de saída. O identificador parcial é reconhecido como uma palavra-chave contextual (§6.4.4) numa declaração de método apenas se aparecer imediatamente antes da void palavra-chave. Um método parcial não pode implementar explicitamente métodos de interface.

Existem dois tipos de declarações de método parcial: Se o corpo da declaração de método for um ponto-e-vírgula, a declaração é considerada uma declaração de método parcial definidora. Se o corpo não for ponto e vírgula, diz-se que a declaração é uma declaração de método parcial de execução. Nas partes de uma declaração de tipo, pode haver apenas uma declaração de método parcial definidora com uma determinada assinatura, e pode haver apenas uma declaração de método parcial de implementação com uma determinada assinatura. Se for fornecida uma declaração de método parcial de execução, deve existir uma declaração de método parcial definidora correspondente, e as declarações devem corresponder conforme especificado no seguinte:

  • As declarações devem ter os mesmos modificadores (embora não necessariamente pela mesma ordem), nome do método, número de parâmetros de tipo e número de parâmetros.
  • Os parâmetros correspondentes nas declarações devem ter os mesmos modificadores (embora não necessariamente na mesma ordem) e os mesmos tipos, ou tipos conversíveis de identidade (diferenças de módulos nos nomes dos parâmetros de tipo).
  • Os parâmetros de tipo correspondentes nas declarações devem ter as mesmas restrições (diferenças de modulo nos nomes dos parâmetros de tipo).

Uma declaração de método parcial de implementação pode aparecer na mesma parte que a declaração de método parcial de definição correspondente.

Apenas um método parcial definidor participa na resolução de sobrecarga. Assim, independentemente de ser ou não dada uma declaração de execução, as expressões de invocação podem resolver invocações do método parcial. Como um método parcial sempre retorna void, essas expressões de invocação sempre serão instruções de expressão. Além disso, como um método parcial é implicitamente private, tais declarações sempre ocorrerão dentro de uma das partes da declaração de tipo dentro da qual o método parcial é declarado.

Nota: A definição de correspondência definindo e implementando declarações parciais de método não requer nomes de parâmetros para corresponder. Isto pode produzir um comportamento surpreendente, embora bem definido, quando são utilizados argumentos nomeados (§12.6.2.1). Por exemplo, dada a definição da declaração de método parcial para M em um arquivo e a declaração de método parcial de implementação em outro arquivo:

// 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) {}
}

é inválida , pois a invocação usa o nome do argumento da implementação e não a declaração de método parcial definidora.

Nota final

Se nenhuma parte de uma declaração de tipo parcial contiver uma declaração de execução para um determinado método parcial, qualquer declaração de expressão que a invoque será simplesmente removida da declaração de tipo combinada. Assim, a expressão de invocação, incluindo quaisquer subexpressões, não tem efeito em tempo de execução. O método parcial em si também é removido e não será um membro da declaração de tipo combinado.

Se existir uma declaração de execução para um determinado método parcial, as invocações dos métodos parciais são mantidas. O método parcial dá origem a uma declaração de método semelhante à declaração de método parcial de execução, com exceção dos seguintes casos:

  • O partial modificador não está incluído.

  • Os atributos na declaração de método resultante são os atributos combinados da declaração de método parcial de definição e implementação em ordem não especificada. As duplicatas não são removidas.

  • Os atributos nos parâmetros da declaração de método resultante são os atributos combinados dos parâmetros correspondentes da declaração de método parcial de definição e implementação em ordem não especificada. As duplicatas não são removidas.

Se for fornecida uma declaração definidora, mas não uma declaração de execução, para um método Mparcial, aplicam-se as seguintes restrições:

  • É um erro em tempo de compilação criar um delegado a partir de M (§12.8.17.6).

  • É um erro em tempo de compilação referir-se dentro M de uma função anônima que é convertida em um tipo de árvore de expressão (§8.6).

  • As expressões que ocorrem como parte de uma invocação de não afetam o estado de atribuição definido (M), o que pode potencialmente levar a erros em tempo de compilação.

  • M não pode ser o ponto de entrada de um pedido (§7.1).

Os métodos parciais são úteis para permitir que uma parte de uma declaração de tipo personalize o comportamento de outra parte, por exemplo, uma que é gerada por uma ferramenta. Considere a seguinte declaração de classe 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();
}

Se essa classe for compilada sem quaisquer outras partes, as declarações de método parcial definidoras e suas invocações serão removidas, e a declaração de classe combinada resultante será equivalente ao seguinte:

class Customer
{
    string name;

    public string Name
    {
        get => name;
        set => name = value;
    }
}

Suponhamos que outra parte é dada, no entanto, que fornece declarações de implementação dos métodos parciais:

partial class Customer
{
    partial void OnNameChanging(string newName) =>
        Console.WriteLine($"Changing {name} to {newName}");

    partial void OnNameChanged() =>
        Console.WriteLine($"Changed to {name}");
}

Em seguida, a declaração de classe combinada resultante será equivalente ao seguinte:

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

15.6.10 Métodos de extensão

Quando o primeiro parâmetro de um método inclui o this modificador, esse método é dito ser um método de extensão. Os métodos de extensão só devem ser declarados em classes estáticas não genéricas e não aninhadas. O primeiro parâmetro de um método de extensão é restrito, da seguinte forma:

  • Pode ser apenas um parâmetro de entrada se tiver um tipo de valor
  • Pode ser apenas um parâmetro de referência se tiver um tipo de valor ou tiver um tipo genérico limitado a estruturar
  • Não deve ser um tipo de ponteiro.

Exemplo: A seguir está um exemplo de uma classe estática que declara dois métodos de extensão:

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

Exemplo final

Um método de extensão é um método estático regular. Além disso, quando sua classe estática anexa estiver no escopo, um método de extensão pode ser invocado usando a sintaxe de invocação do método de instância (§12.8.10.3), usando a expressão receiver como o primeiro argumento.

Exemplo: O programa a seguir usa os métodos de extensão declarados acima:

static class Program
{
    static void Main()
    {
        string[] strings = { "1", "22", "333", "4444" };
        foreach (string s in strings.Slice(1, 2))
        {
            Console.WriteLine(s.ToInt32());
        }
    }
}

O Slice método está disponível no string[], e o ToInt32 método está disponível no string, porque eles foram declarados como métodos de extensão. O significado do programa é o mesmo que o seguinte, usando chamadas de método estático comum:

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

Exemplo final

15.6.11 Corpo do método

O corpo do método de uma declaração de método consiste em um corpo de bloco, um corpo de expressão ou um ponto-e-vírgula.

As declarações de método abstrato e externo não fornecem uma implementação de método, portanto, seus corpos de método consistem simplesmente em ponto-e-vírgula. Para qualquer outro método, o corpo do método é um bloco (§13.3) que contém as instruções a serem executadas quando esse método é invocado.

O tipo de retorno efetivo de um método é void se o tipo de retorno for void, ou se o método for assíncrono e o tipo de retorno for «TaskType» (§15.15.1). Caso contrário, o tipo de retorno efetivo de um método não assíncrono é seu tipo de retorno, e o tipo de retorno efetivo de um método assíncrono com tipo «TaskType»<T>de retorno (§15.15.1) é T.

Quando o tipo de retorno efetivo de um método é void e o método tem um corpo de bloco, return as instruções (§13.10.5) no bloco não devem especificar uma expressão. Se a execução do bloco de um método void for concluída normalmente (ou seja, o controle flui do final do corpo do método), esse método simplesmente retornará ao seu chamador.

Quando o tipo de retorno efetivo de um método é void e o método tem um corpo de expressão, a expressão E deve ser um statement_expression, e o corpo é exatamente equivalente a um corpo de bloco da forma { E; }.

Para um método de retorno por valor (§15.6.1), cada declaração de retorno no corpo desse método deve especificar uma expressão que seja implicitamente conversível para o tipo de retorno efetivo.

Para um método returns-by-ref (§15.6.1), cada declaração de retorno no corpo desse método deve especificar uma expressão cujo tipo é o do tipo de retorno efetivo e tem um contexto ref-safe do contexto do chamador (§9.7.2).

Para os métodos returns-by-value e returns-by-ref, o endpoint do corpo do método não deve ser acessível. Por outras palavras, não é permitido que o controlo flua para fora da extremidade do corpo do método.

Exemplo: No código a seguir

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

O método de retorno F de valor resulta em um erro de tempo de compilação porque o controle pode fluir do final do corpo do método. Os G métodos e H estão corretos porque todos os caminhos de execução possíveis terminam em uma instrução return que especifica um valor de retorno. O I método está correto, porque seu corpo é equivalente a um bloco com apenas uma única instrução de retorno nele.

Exemplo final

15.7 Propriedades

15.7.1 Generalidades

Uma propriedade é um membro que fornece acesso a uma característica de um objeto ou classe. Exemplos de propriedades incluem o comprimento de uma cadeia de caracteres, o tamanho de uma fonte, a legenda de uma janela e o nome de um cliente. As propriedades são uma extensão natural de campos — ambos são membros nomeados com tipos associados, e a sintaxe para acessar campos e propriedades é a mesma. No entanto, ao contrário dos campos, as propriedades não denotam locais de armazenamento. Em vez disso, as propriedades têm acessadores que especificam as instruções a serem executadas quando seus valores são lidos ou gravados. As propriedades fornecem assim um mecanismo para associar ações com a leitura e escrita das características de um objeto ou classe; além disso, permitem calcular essas características.

As propriedades são declaradas usando property_declarations:

property_declaration
    : attributes? property_modifier* type member_name property_body
    | attributes? property_modifier* ref_kind type member_name ref_property_body
    ;    

property_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'static'
    | 'virtual'
    | 'sealed'
    | 'override'
    | 'abstract'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;
    
property_body
    : '{' accessor_declarations '}' property_initializer?
    | '=>' expression ';'
    ;

property_initializer
    : '=' variable_initializer ';'
    ;

ref_property_body
    : '{' ref_get_accessor_declaration '}'
    | '=>' 'ref' variable_reference ';'
    ;

unsafe_modifier (§23.2) só está disponível em código não seguro (§23).

Existem dois tipos de property_declaration:

  • O primeiro declara um imóvel não avaliado por ref. Seu valor tem tipo de tipo. Este tipo de propriedade pode ser legível e/ou gravável.
  • O segundo declara um imóvel avaliado por ref. O seu valor é um variable_reference (§9.5), que pode ser readonly, para uma variável do tipo tipo. Este tipo de propriedade é apenas legível.

Um property_declaration pode incluir um conjunto de atributos (§22) e qualquer um dos tipos permitidos de acessibilidade declarada (§15.3.6), os new modificadores (§15.3.5), static (§15.7.2), virtual (§15.6.4, §15.7.6), override (§15.6.5, §15.7.6), sealed (§15.6.6), abstract (§15.6.7, §15.7.6) e extern (§15.6.8).

As declarações de propriedade estão sujeitas às mesmas regras que as declarações de método (§15.6) no que diz respeito a combinações válidas de modificadores.

O member_name (§15.6.1) especifica o nome da propriedade. A menos que a propriedade seja uma implementação explícita de membro da interface, o member_name é simplesmente um identificador. Para uma implementação explícita de membro da interface (§18.6.2), o member_name consiste em um interface_type seguido por um "." e um identificador.

O tipo de propriedade deve ser pelo menos tão acessível como a própria propriedade (§7.5.5).

Um property_body pode consistir em um corpo de declaração ou um corpo de expressão. Em um corpo de declaração, accessor_declarations, que devem ser incluídos nos tokens "{" e "}", declaram os acessadores (§15.7.3) da propriedade. Os acessadores especificam as instruções executáveis associadas à leitura e gravação da propriedade.

Em um property_body um corpo de => expressão que consiste em seguido por uma expressãoE e um ponto-e-vírgula é exatamente equivalente ao corpo { get { return E; } }da instrução e, portanto, só pode ser usado para especificar propriedades somente leitura onde o resultado do acessor get é dado por uma única expressão.

Uma property_initializer só pode ser dada para uma propriedade implementada automaticamente (§15.7.4), e causa a inicialização do campo subjacente de tais propriedades com o valor dado pela expressão.

Um ref_property_body pode consistir num corpo de declaração ou num corpo de expressão. Em um corpo de declaração, um get_accessor_declaration declara o acessador (§15.7.3) da propriedade. O acessador especifica as instruções executáveis associadas à leitura da propriedade.

Em um ref_property_body um corpo de expressão que consiste em seguido por =>ref , um variable_referenceV e um ponto-e-vírgula é exatamente equivalente ao corpo { get { return ref V; } }da declaração.

Nota: Embora a sintaxe para acessar uma propriedade seja a mesma de um campo, uma propriedade não é classificada como uma variável. Assim, não é possível passar uma propriedade como um in, outou ref argumento, a menos que a propriedade seja ref-value e, portanto, retorne uma referência variável (§9.7). Nota final

Quando uma declaração de propriedade inclui um extern modificador, diz-se que a propriedade é uma propriedade externa. Como uma declaração de propriedade externa não fornece nenhuma implementação real, cada um dos accessor_bodys em seu accessor_declarations deve ser um ponto-e-vírgula.

15.7.2 Propriedades estáticas e de instância

Quando uma declaração de propriedade inclui um static modificador, diz-se que a propriedade é uma propriedade estática. Quando nenhum static modificador está presente, a propriedade é considerada uma propriedade de instância.

Uma propriedade estática não está associada a uma instância específica e é um erro em tempo de compilação para this se referir nos acessadores de uma propriedade estática.

Uma propriedade de instância está associada a uma determinada instância de uma classe, e essa instância pode ser acessada como this (§12.8.14) nos acessadores dessa propriedade.

As diferenças entre membros estáticos e de instância são discutidas mais detalhadamente no §15.3.8.

15.7.3 Acessadores

Nota: Esta cláusula aplica-se tanto às propriedades (§15.7) como aos indexadores (§15.9). A cláusula é escrita em termos de propriedades, ao ler para indexadores substituir indexador/indexadores por propriedade/propriedades e consultar a lista de diferenças entre propriedades e indexadores dada no §15.9.2. Nota final

Os accessor_declarations de uma propriedade especificam as instruções executáveis associadas à escrita e/ou leitura dessa propriedade.

accessor_declarations
    : get_accessor_declaration set_accessor_declaration?
    | set_accessor_declaration get_accessor_declaration?
    ;

get_accessor_declaration
    : attributes? accessor_modifier? 'get' accessor_body
    ;

set_accessor_declaration
    : attributes? accessor_modifier? 'set' accessor_body
    ;

accessor_modifier
    : 'protected'
    | 'internal'
    | 'private'
    | 'protected' 'internal'
    | 'internal' 'protected'
    | 'protected' 'private'
    | 'private' 'protected'
    ;

accessor_body
    : block
    | '=>' expression ';'
    | ';' 
    ;

ref_get_accessor_declaration
    : attributes? accessor_modifier? 'get' ref_accessor_body
    ;
    
ref_accessor_body
    : block
    | '=>' 'ref' variable_reference ';'
    | ';'
    ;

Os accessor_declarations consistem em um get_accessor_declaration, um set_accessor_declaration ou ambos. Cada declaração de acessador consiste em atributos opcionais, um accessor_modifier opcional, o token ou get, seguido por um set.

Para uma propriedade com valor ref, o ref_get_accessor_declaration consiste em atributos opcionais, um accessor_modifier opcional, o token get, seguido por um ref_accessor_body.

A utilização de accessor_modifiers rege-se pelas seguintes restrições:

  • Um accessor_modifier não pode ser utilizado numa interface ou numa implementação explícita de membro da interface.
  • Para uma propriedade ou indexador que não tenha modificador override , um accessor_modifier só é permitido se a propriedade ou indexador tiver um acessador get e set e, em seguida, é permitido somente em um desses acessadores.
  • Para uma propriedade ou indexador que inclua um override modificador, um acessador deve corresponder ao accessor_modifier, se houver, do acessador que está sendo substituído.
  • O accessor_modifier deve declarar uma acessibilidade estritamente mais restritiva do que a acessibilidade declarada da propriedade ou do próprio indexador. Para ser mais preciso:
    • Se a propriedade ou indexador tiver uma acessibilidade declarada de , a acessibilidade declarada por accessor_modifier pode ser , public, private protected, protected internal, ou internal.protectedprivate
    • Se a propriedade ou indexador tiver uma acessibilidade declarada de , a acessibilidade declarada por accessor_modifier pode ser , protected internal, private protected, protected private, ou internal.protectedprivate
    • Se a propriedade ou indexador tiver uma acessibilidade declarada de ou , a acessibilidade declarada internal por protected deve ser ou private protected .private
    • Se a propriedade ou indexador tiver uma acessibilidade declarada de , a acessibilidade declarada private protectedpor accessor_modifier deve ser private.
    • Se a propriedade ou indexador tiver uma acessibilidade declarada de private, nenhuma accessor_modifier poderá ser usada.

Para abstract propriedades extern sem valor de referência, qualquer accessor_body para cada acessador especificado é simplesmente um ponto-e-vírgula. Uma propriedade não-abstrata, não-externa, mas não um indexador, também pode ter o accessor_body para todos os acessadores especificados ser um ponto-e-vírgula, caso em que é uma propriedade implementada automaticamente (§15.7.4). Uma propriedade implementada automaticamente deve ter pelo menos um acessório get. Para os acessadores de qualquer outra propriedade não abstrata e não externa, a accessor_body é:

  • um bloco que especifica as instruções a serem executadas quando o acessador correspondente é invocado;
  • um corpo de => expressão, que consiste em seguido por uma expressão e um ponto-e-vírgula, e denota uma única expressão a ser executada quando o acessador correspondente é invocado.

Para abstract propriedades com extern valor ref, o ref_accessor_body é simplesmente um ponto-e-vírgula. Para o acessador de qualquer outra propriedade não abstrata e não externa, o ref_accessor_body é:

  • um bloco que especifica as instruções a serem executadas quando o acessador get é invocado;
  • um corpo de expressão, que consiste em seguido por =>ref, um variable_reference e um ponto-e-vírgula. A referência da variável é avaliada quando o acessador get é invocado.

Um acessador get para uma propriedade sem valor de referência corresponde a um método sem parâmetros com um valor de retorno do tipo de propriedade. Exceto como o destino de uma atribuição, quando tal propriedade é referenciada em uma expressão, seu acessador get é invocado para calcular o valor da propriedade (§12.2.2).

O corpo de um acessador obtido para uma propriedade sem valor de referência deve estar em conformidade com as regras para métodos de retorno de valor descritas no §15.6.11. Em particular, todas as return declarações no corpo de um acessador get devem especificar uma expressão que seja implicitamente conversível para o tipo de propriedade. Além disso, o ponto final de um acessor de acesso não deve ser acessível.

Um acessador get para uma propriedade com valor ref corresponde a um método sem parâmetros com um valor de retorno de um variable_reference a uma variável do tipo de propriedade. Quando essa propriedade é referenciada em uma expressão, seu acessor get é invocado para calcular o valor variable_reference da propriedade. Essa referência de variável, como qualquer outra, é então usada para ler ou, para variable_referencenão somente leitura, escrever a variável referenciada conforme exigido pelo contexto.

Exemplo: O exemplo a seguir ilustra uma propriedade com valor de referência como o destino de uma atribuição:

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

Exemplo final

O corpo de um acessor obtido para uma propriedade com valor de referência deve estar em conformidade com as regras para os métodos de avaliação de referência descritas no §15.6.11.

Um acessador de conjunto corresponde a um método com um único parâmetro de valor do tipo de propriedade e um void tipo de retorno. O parâmetro implícito de um acessador de conjunto é sempre chamado value. Quando uma propriedade é referenciada como o destino de uma atribuição (§12.21), ou como o operando de ou ++ (–-, §12.9.6), o acessador do conjunto é invocado com um argumento que fornece o novo valor (§12.21.2). O corpo de um conjunto de acessos deve estar em conformidade com as regras para void os métodos descritos no §15.6.11. Em particular, as instruções de retorno no corpo do acessador definido não têm permissão para especificar uma expressão. Como um acessador de conjunto tem implicitamente um parâmetro chamado value, é um erro em tempo de compilação para uma variável local ou declaração constante em um acessador de conjunto ter esse nome.

Com base na presença ou ausência dos acessadores get e set, uma propriedade é classificada da seguinte forma:

  • Uma propriedade que inclui um acessor get e um acessor set é considerada uma propriedade de leitura-gravação.
  • Diz-se que uma propriedade que tem apenas um acessor get é uma propriedade somente leitura. É um erro em tempo de compilação para uma propriedade somente leitura ser o destino de uma atribuição.
  • Uma propriedade que tem apenas um acessador definido é considerada uma propriedade somente gravação. Exceto como o destino de uma atribuição, é um erro em tempo de compilação fazer referência a uma propriedade somente gravação em uma expressão.

Nota: Os operadores pré e postfix ++ e -- os operadores de atribuição composta não podem ser aplicados a propriedades somente gravação, uma vez que esses operadores leem o valor antigo de seu operando antes de escrever o novo. Nota final

Exemplo: No código a seguir

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

o Button controle declara uma propriedade pública Caption . O acessador get da propriedade Caption retorna o string armazenado no campo privado caption . O acessador definido verifica se o novo valor é diferente do valor atual e, em caso afirmativo, armazena o novo valor e repinta o controle. As propriedades geralmente seguem o padrão mostrado acima: O acessador get simplesmente retorna um valor armazenado em um private campo, e o acessador definido modifica esse private campo e, em seguida, executa quaisquer ações adicionais necessárias para atualizar totalmente o estado do objeto. Dada a Button classe acima, o seguinte é um exemplo de uso do Caption imóvel:

Button okButton = new Button();
okButton.Caption = "OK"; // Invokes set accessor
string s = okButton.Caption; // Invokes get accessor

Aqui, o acessador de conjunto é invocado atribuindo um valor à propriedade e o acessador get é invocado fazendo referência à propriedade em uma expressão.

Exemplo final

Os acessadores get e set de uma propriedade não são membros distintos, e não é possível declarar os acessadores de uma propriedade separadamente.

Exemplo: O exemplo

class A
{
    private string name;

    // Error, duplicate member name
    public string Name
    { 
        get => name;
    }

    // Error, duplicate member name
    public string Name
    { 
        set => name = value;
    }
}

não declara uma única propriedade de leitura-gravação. Em vez disso, ele declara duas propriedades com o mesmo nome, uma somente leitura e uma somente gravação. Como dois membros declarados na mesma classe não podem ter o mesmo nome, o exemplo faz com que ocorra um erro em tempo de compilação.

Exemplo final

Quando uma classe derivada declara uma propriedade com o mesmo nome de uma propriedade herdada, a propriedade derivada oculta a propriedade herdada em relação à leitura e à escrita.

Exemplo: No código a seguir

class A
{
    public int P
    {
        set {...}
    }
}

class B : A
{
    public new int P
    {
        get {...}
    }
}

A P propriedade em B esconde a P propriedade em A relação à leitura e escrita. Assim, nos depoimentos

B b = new B();
b.P = 1;       // Error, B.P is read-only
((A)b).P = 1;  // Ok, reference to A.P

A atribuição a b.P faz com que um erro em tempo de compilação seja relatado, uma vez que a propriedade somente P leitura em B oculta a propriedade somente P gravação em A. Observe, no entanto, que um elenco pode ser usado para acessar a propriedade oculta P .

Exemplo final

Ao contrário dos campos públicos, as propriedades fornecem uma separação entre o estado interno de um objeto e sua interface pública.

Exemplo: considere o código a seguir, que usa um Point struct para representar um local:

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

Aqui, a Label classe usa dois int campos x e y, para armazenar sua localização. O local é exposto publicamente como uma X propriedade e Y como uma Location propriedade do tipo Point. Se, em uma versão futura do Label, se tornar mais conveniente armazenar o local internamente Point , a alteração pode ser feita sem afetar a interface pública da classe:

class Label
{
    private Point location;
    private string caption;

    public Label(int x, int y, string caption)
    {
        this.location = new Point(x, y);
        this.caption = caption;
    }

    public int X => location.X;
    public int Y => location.Y;
    public Point Location => location;
    public string Caption => caption;
}

Se tivessem x sido y campospublic readonly, teria sido impossível fazer tal mudança na Label classe.

Exemplo final

Nota: Expor o estado através de propriedades não é necessariamente menos eficiente do que expor campos diretamente. Em particular, quando uma propriedade não é virtual e contém apenas uma pequena quantidade de código, o ambiente de execução pode substituir chamadas para acessadores com o código real dos acessadores. Este processo é conhecido como inlining, e torna o acesso à propriedade tão eficiente quanto o acesso ao campo, mas preserva a maior flexibilidade das propriedades. Nota final

Exemplo: Como invocar um acessor get é conceitualmente equivalente a ler o valor de um campo, é considerado um estilo de programação ruim para obter acessadores ter efeitos colaterais observáveis. No exemplo

class Counter
{
    private int next;

    public int Next => next++;
}

O valor da Next propriedade depende do número de vezes que a propriedade foi acessada anteriormente. Assim, acessar a propriedade produz um efeito colateral observável, e a propriedade deve ser implementada como um método.

A convenção "sem efeitos colaterais" para obter acessadores não significa que obter acessadores deva sempre ser escrito simplesmente para retornar valores armazenados em campos. De fato, os acessadores de get geralmente calculam o valor de uma propriedade acessando vários campos ou invocando métodos. No entanto, um acessor get projetado corretamente não executa nenhuma ação que cause alterações observáveis no estado do objeto.

Exemplo final

As propriedades podem ser usadas para atrasar a inicialização de um recurso até o momento em que ele é referenciado pela primeira vez.

Exemplo:

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

A Console classe contém três propriedades, In, Out, e Error, que representam os dispositivos padrão de entrada, saída e erro, respectivamente. Ao expor esses membros como propriedades, a classe pode atrasar Console sua inicialização até que eles sejam realmente usados. Por exemplo, ao fazer a primeira referência ao Out imóvel, como em

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

O subjacente TextWriter para o dispositivo de saída é criado. No entanto, se o aplicativo não fizer referência às In propriedades e Error , nenhum objeto será criado para esses dispositivos.

Exemplo final

15.7.4 Propriedades implementadas automaticamente

Uma propriedade implementada automaticamente (ou auto-propriedade, para abreviar), é uma propriedade não-abstrata, não-externa, não-ref-valorada com ponto-e-vírgula apenas accessor_bodys. As propriedades automáticas devem ter um acessor get e podem, opcionalmente, ter um acessador definido.

Quando uma propriedade é especificada como uma propriedade implementada automaticamente, um campo de suporte oculto fica automaticamente disponível para a propriedade e os acessadores são implementados para ler e gravar nesse campo de suporte. O campo de suporte oculto é inacessível, ele pode ser lido e escrito apenas através dos acessadores de propriedade implementados automaticamente, mesmo dentro do tipo que contém. Se a propriedade automática não tiver um acessador definido, o campo de apoio é considerado readonly (§15.5.3). Assim como um readonly campo, uma propriedade automática somente leitura também pode ser atribuída no corpo de um construtor da classe inclusa. Essa atribuição é atribuída diretamente ao campo de suporte somente leitura da propriedade.

Uma propriedade automática pode, opcionalmente, ter um property_initializer, que é aplicado diretamente ao campo de apoio como um variable_initializer (§17.7).

Exemplo:

public class Point
{
    public int X { get; set; } // Automatically implemented
    public int Y { get; set; } // Automatically implemented
}

é equivalente à seguinte declaração:

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

Exemplo final

Exemplo: No seguinte

public class ReadOnlyPoint
{
    public int X { get; }
    public int Y { get; }

    public ReadOnlyPoint(int x, int y)
    {
        X = x;
        Y = y;
    }
}

é equivalente à seguinte declaração:

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

As atribuições para o campo somente leitura são válidas, porque ocorrem dentro do construtor.

Exemplo final

Embora o campo de suporte esteja oculto, esse campo pode ter atributos direcionados ao campo aplicados diretamente a ele por meio do property_declaration da propriedade implementada automaticamente (§15.7.1).

Exemplo: O código a seguir

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

resulta na aplicação do atributo NonSerialized field-targeted ao campo de suporte gerado pelo compilador, como se o código tivesse sido escrito da seguinte forma:

[Serializable]
public class Foo
{
    [NonSerialized]
    private string _mySecretBackingField;
    public string MySecret
    {
        get { return _mySecretBackingField; }
        set { _mySecretBackingField = value; }
    }
}

Exemplo final

15.7.5 Acessibilidade

Se um acessador tiver um accessor_modifier, o domínio de acessibilidade (§7.5.3) do acessador é determinado usando a acessibilidade declarada do accessor_modifier. Se um acessador não tiver um accessor_modifier, o domínio de acessibilidade do acessador é determinado a partir da acessibilidade declarada da propriedade ou indexador.

A presença de um accessor_modifier nunca afeta a pesquisa de membros (§12.5) ou a resolução de sobrecarga (§12.6.4). Os modificadores na propriedade ou indexador sempre determinam a qual propriedade ou indexador está vinculado, independentemente do contexto do acesso.

Uma vez selecionada uma determinada propriedade sem valor de referência ou indexador sem valor de referência, os domínios de acessibilidade dos acessadores específicos envolvidos são usados para determinar se esse uso é válido:

  • Se o uso for como um valor (§12.2.2), o acessor get deve existir e ser acessível.
  • Se o uso for como alvo de uma atribuição simples (§12.21.2), o acessador definido deve existir e ser acessível.
  • Se o uso for como o alvo da atribuição composta (§12.21.4), ou como o alvo dos ++ operadores ou -- (§12.8.16, §12.9.6), tanto os acessadores get quanto o acessor set devem existir e ser acessíveis.

Exemplo: No exemplo a seguir, a propriedade A.Text é ocultada pela propriedade B.Text, mesmo em contextos onde apenas o acessador definido é chamado. Por outro lado, a propriedade B.Count não é acessível à classe M, portanto, a propriedade A.Count acessível é usada em vez disso.

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

Exemplo final

Uma vez selecionada uma determinada propriedade ou indexador com valor ref; se o uso é como um valor, o destino de uma atribuição simples ou o destino de uma atribuição composta; O domínio de acessibilidade do acessador Get Involved é usado para determinar se esse uso é válido.

Um acessor que é usado para implementar uma interface não deve ter um accessor_modifier. Se apenas um acessador for usado para implementar uma interface, o outro acessador pode ser declarado com uma accessor_modifier:

Exemplo:

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

Exemplo final

15.7.6 Acessadores virtuais, lacrados, substituídos e abstratos

Nota: Esta cláusula aplica-se tanto às propriedades (§15.7) como aos indexadores (§15.9). A cláusula é escrita em termos de propriedades, ao ler para indexadores substituir indexador/indexadores por propriedade/propriedades e consultar a lista de diferenças entre propriedades e indexadores dada no §15.9.2. Nota final

Uma declaração de propriedade virtual especifica que os acessadores da propriedade são virtuais. O virtual modificador aplica-se a todos os acessores não privados de uma propriedade. Quando um acessador de uma propriedade virtual tem o privateaccessor_modifier, o acessador privado implicitamente não é virtual.

Uma declaração de propriedade abstrata especifica que os acessadores da propriedade são virtuais, mas não fornece uma implementação real dos acessadores. Em vez disso, classes derivadas não abstratas são necessárias para fornecer sua própria implementação para os acessadores, substituindo a propriedade. Como um acessador para uma declaração de propriedade abstrata não fornece nenhuma implementação real, seu accessor_body consiste simplesmente em um ponto-e-vírgula. Uma propriedade abstrata não deve ter um private acessor.

Uma declaração de propriedade que inclui os abstract modificadores e override especifica que a propriedade é abstrata e substitui uma propriedade base. Os acessórios de tal propriedade também são abstratos.

As declarações abstratas de propriedade só são permitidas em classes abstratas (§15.2.2.2). Os acessadores de uma propriedade virtual herdada podem ser substituídos em uma classe derivada incluindo uma declaração de propriedade que especifica uma override diretiva. Isso é conhecido como uma declaração de propriedade prevalecente. Uma declaração de propriedade prevalecente não declara uma nova propriedade. Em vez disso, ele simplesmente especializa as implementações dos acessadores de uma propriedade virtual existente.

A declaração de substituição e a propriedade base substituída devem ter a mesma acessibilidade declarada. Por outras palavras, uma declaração de substituição não deve alterar a acessibilidade da propriedade base. No entanto, se a propriedade de base substituída for protegida internamente e for declarada em um assembly diferente do assembly que contém a declaração de substituição, a acessibilidade declarada da declaração de substituição será protegida. Se a propriedade herdada tiver apenas um único acessador (ou seja, se a propriedade herdada for somente leitura ou gravação), a propriedade prevalecente deve incluir apenas esse acessador. Se a propriedade herdada incluir ambos os acessadores (ou seja, se a propriedade herdada for leitura-gravação), a propriedade predominante poderá incluir um único acessador ou ambos os acessadores. Deve haver uma conversão de identidade entre o tipo de bens prevalecentes e herdados.

Uma declaração de propriedade prevalecente pode incluir o sealed modificador. O uso desse modificador impede que uma classe derivada substitua ainda mais a propriedade. Os acessórios de uma propriedade lacrada também são lacrados.

Exceto para diferenças na sintaxe de declaração e invocação, os acessadores virtuais, selados, de substituição e abstratos se comportam exatamente como métodos virtuais, selados, de substituição e abstratos. Especificamente, as regras descritas nos §15.6.4, §15.6.5, §15.6.6 e §15.6.7 aplicam-se como se os acessadores fossem métodos de uma forma correspondente:

  • Um acessador get corresponde a um método sem parâmetros com um valor de retorno do tipo de propriedade e os mesmos modificadores que a propriedade que contém.
  • Um acessador de conjunto corresponde a um método com um único parâmetro de valor do tipo de propriedade, um tipo de retorno vazio e os mesmos modificadores que a propriedade de contenção.

Exemplo: No código a seguir

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 é uma propriedade virtual somente leitura, Y é uma propriedade virtual de leitura-gravação e Z é uma propriedade abstrata de leitura-gravação. Por Z ser abstrata, a classe A que contém também deve ser declarada abstrata.

Uma classe que deriva de é mostrada A abaixo:

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

Aqui, as declarações de , Xe Y são declarações de Zpropriedade prevalecentes. Cada declaração de propriedade corresponde exatamente aos modificadores de acessibilidade, tipo e nome da propriedade herdada correspondente. O acessador get de X e o acessador definido de Y usar a palavra-chave base para acessar os acessadores herdados. A declaração de substitui ambos os acessadores abstratos — assim, não há membros de Z função pendentes abstract no B, e B é permitido ser uma classe não abstrata.

Exemplo final

Quando uma propriedade é declarada como uma substituição, todos os acessores substituídos devem ser acessíveis ao código de substituição. Além disso, a acessibilidade declarada tanto da propriedade ou do próprio indexador, como dos acessórios, deve corresponder à do membro substituído e dos acessadores.

Exemplo:

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

Exemplo final

15.8 Eventos

15.8.1 Generalidades

Um evento é um membro que permite que um objeto ou classe forneça notificações. Os clientes podem anexar código executável para eventos fornecendo manipuladores de eventos.

Os eventos são declarados usando event_declarations:

event_declaration
    : attributes? event_modifier* 'event' type variable_declarators ';'
    | attributes? event_modifier* 'event' type member_name
        '{' event_accessor_declarations '}'
    ;

event_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'static'
    | 'virtual'
    | 'sealed'
    | 'override'
    | 'abstract'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;

event_accessor_declarations
    : add_accessor_declaration remove_accessor_declaration
    | remove_accessor_declaration add_accessor_declaration
    ;

add_accessor_declaration
    : attributes? 'add' block
    ;

remove_accessor_declaration
    : attributes? 'remove' block
    ;

unsafe_modifier (§23.2) só está disponível em código não seguro (§23).

Um event_declaration pode incluir um conjunto de atributos (§22) e qualquer um dos tipos permitidos de acessibilidade declarada (§15.3.6), os modificadores (new), (static, §15.8.4), (virtual, §15.8.5), (override, §15.8.5), (abstract, §15.8.5) e (extern).

As declarações de eventos estão sujeitas às mesmas regras que as declarações de método (§15.6) no que diz respeito a combinações válidas de modificadores.

O tipo de declaração de evento deve ser um delegate_type (§8.2.8), e essa delegate_type deve ser pelo menos tão acessível quanto o próprio evento (§7.5.5).

Uma declaração de evento pode incluir event_accessor_declarations. No entanto, se isso não acontecer, para eventos não externos e não abstratos, um compilador deve fornecê-los automaticamente (§15.8.2); Para eventos extern, os acessadores são fornecidos externamente.

Uma declaração de evento que omite event_accessor_declarations define um ou mais eventos — um para cada um dos variable_declarators. Os atributos e modificadores aplicam-se a todos os membros declarados por tal event_declaration.

É um erro em tempo de compilação para um event_declaration incluir o abstract modificador e event_accessor_declarations.

Quando uma declaração de evento inclui um extern modificador, o evento é dito ser um evento externo. Como uma declaração de evento externo não fornece nenhuma implementação real, é um erro incluir o extern modificador e event_accessor_declarations.

É um erro em tempo de compilação para um variable_declarator de uma declaração de evento com um abstract modificador ou external para incluir um variable_initializer.

Um evento pode ser usado como o operando esquerdo dos += operadores e -= . Esses operadores são usados, respectivamente, para anexar manipuladores de eventos ou para remover manipuladores de eventos de um evento, e os modificadores de acesso do evento controlam os contextos nos quais tais operações são permitidas.

As únicas operações permitidas em um evento por código que está fora do tipo no qual esse evento é declarado são += e -=. Portanto, embora esse código possa adicionar e remover manipuladores para um evento, ele não pode obter ou modificar diretamente a lista subjacente de manipuladores de eventos.

Em uma operação da forma x += y ou , quando x –= y é um evento, o resultado da operação tem tipo x (void) (em oposição a ter o tipo de , com o valor de após a atribuição, como para outros e xx operadores definidos em tipos não-evento+=-=). Isso impede que o código externo examine indiretamente o delegado subjacente de um evento.

Exemplo: O exemplo a seguir mostra como manipuladores de eventos são anexados a instâncias da Button classe:

public delegate void EventHandler(object sender, EventArgs e);

public class Button : Control
{
    public event EventHandler Click;
}

public class LoginDialog : Form
{
    Button okButton;
    Button cancelButton;

    public LoginDialog()
    {
        okButton = new Button(...);
        okButton.Click += new EventHandler(OkButtonClick);
        cancelButton = new Button(...);
        cancelButton.Click += new EventHandler(CancelButtonClick);
    }

    void OkButtonClick(object sender, EventArgs e)
    {
        // Handle okButton.Click event
    }

    void CancelButtonClick(object sender, EventArgs e)
    {
        // Handle cancelButton.Click event
    }
}

Aqui, o construtor de LoginDialog instância cria duas Button instâncias e anexa manipuladores de eventos aos Click eventos.

Exemplo final

15.8.2 Eventos de campo

Dentro do texto do programa da classe ou struct que contém a declaração de um evento, certos eventos podem ser usados como campos. Para ser usado desta forma, um evento não deve ser abstrato ou externo, e não deve incluir explicitamente event_accessor_declarations. Tal evento pode ser usado em qualquer contexto que permita um campo. O campo contém um delegado (§20), que se refere à lista de manipuladores de eventos que foram adicionados ao evento. Se nenhum manipulador de eventos tiver sido adicionado, o campo conterá null.

Exemplo: No código a seguir

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 é usado como um campo dentro da Button classe. Como o exemplo demonstra, o campo pode ser examinado, modificado e usado em expressões de invocação delegada. O OnClick método na Button classe "levanta" o Click evento. A noção de levantar um evento é precisamente equivalente a invocar o delegado representado pelo evento – assim, não há construções de linguagem especiais para levantar eventos. Observe que a invocação de delegado é precedida por uma verificação que garante que o delegado não seja nulo e que a verificação seja feita em uma cópia local para garantir a segurança do thread.

Fora da declaração da Button classe, o Click membro só pode ser usado no lado esquerdo do e += operadores–=, como em

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

que acrescenta um delegado à lista de invocação do Click evento, e

Click –= new EventHandler(...);

que remove um delegado da lista de invocação do Click evento.

Exemplo final

Ao compilar um evento semelhante a um campo, um compilador deve criar automaticamente armazenamento para manter o delegado e deve criar acessadores para o evento que adicionam ou removem manipuladores de eventos ao campo delegado. As operações de adição e remoção são seguras para threads e podem (mas não são obrigadas) ser feitas mantendo o bloqueio (§13.13) no objeto que contém um evento de instância, ou o System.Type objeto (§12.8.18) para um evento estático.

Nota: Assim, uma declaração de evento de instância do formulário:

class X
{
    public event D Ev;
}

deve ser compilado em 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 da classe X, as referências a Ev no lado esquerdo dos += operadores e –= fazem com que os acessadores add e remove sejam invocados. Todas as outras referências são Ev compiladas para fazer referência ao campo __Ev oculto (§12.8.7). O nome "__Ev" é arbitrário, o campo oculto pode ter qualquer nome ou nenhum nome.

Nota final

15.8.3 Acessadores de eventos

Nota: As declarações de evento normalmente omitem event_accessor_declarations, como no Button exemplo acima. Por exemplo, eles podem ser incluídos se o custo de armazenamento de um campo por evento não for aceitável. Nesses casos, uma classe pode incluir event_accessor_declarations e usar um mecanismo privado para armazenar a lista de manipuladores de eventos. Nota final

Os event_accessor_declarations de um evento especificam as instruções executáveis associadas à adição e remoção de manipuladores de eventos.

As declarações de acesso consistem num add_accessor_declaration e num remove_accessor_declaration. Cada declaração de acessador consiste no token adicionar ou remover seguido por um bloco. O bloco associado a um add_accessor_declaration especifica as instruções a serem executadas quando um manipulador de eventos é adicionado e o bloco associado a um remove_accessor_declaration especifica as instruções a serem executadas quando um manipulador de eventos é removido.

Cada add_accessor_declaration e remove_accessor_declaration corresponde a um método com um único parâmetro de valor do tipo de evento e um void tipo de retorno. O parâmetro implícito de um acessador de eventos é chamado value. Quando um evento é usado em uma atribuição de evento, o acessador de evento apropriado é usado. Especificamente, se o operador de atribuição for += então o acessador de adição será usado, e se o operador de atribuição for –= então o acessador de remoção será usado. Em ambos os casos, o operando direito do operador de atribuição é usado como argumento para o acessador de eventos. O bloco de um add_accessor_declaration ou de um remove_accessor_declaration deve estar em conformidade com as regras para void os métodos descritas no §15.6.9. Em particular, return as declarações contidas nesse bloco não podem especificar uma expressão.

Como um acessador de evento tem implicitamente um parâmetro chamado value, é um erro em tempo de compilação para uma variável local ou constante declarada em um acessador de evento ter esse nome.

Exemplo: No código a seguir


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

A Control classe implementa um mecanismo de armazenamento interno para eventos. O AddEventHandler método associa um valor de delegado a uma chave, o GetEventHandler método retorna o delegado atualmente associado a uma chave e o RemoveEventHandler método remove um delegado como um manipulador de eventos para o evento especificado. Presumivelmente, o mecanismo de armazenamento subjacente foi projetado de modo que não haja custo para associar um valor de delegado nulo a uma chave e, portanto, os eventos não manipulados não consomem armazenamento.

Exemplo final

15.8.4 Eventos estáticos e de instância

Quando uma declaração de evento inclui um static modificador, o evento é dito ser um evento estático. Quando nenhum static modificador está presente, o evento é dito como um evento de instância.

Um evento estático não está associado a uma instância específica e é um erro em tempo de compilação para this se referir nos acessadores de um evento estático.

Um evento de instância é associado a uma determinada instância de uma classe, e essa instância pode ser acessada como this (§12.8.14) nos acessadores desse evento.

As diferenças entre membros estáticos e de instância são discutidas mais detalhadamente no §15.3.8.

15.8.5 Acessadores virtuais, lacrados, de substituição e abstratos

Uma declaração de evento virtual especifica que os acessadores desse evento são virtuais. O virtual modificador aplica-se a ambos os acessadores de um evento.

Uma declaração de evento abstrata especifica que os acessadores do evento são virtuais, mas não fornece uma implementação real dos acessadores. Em vez disso, classes derivadas não abstratas são necessárias para fornecer sua própria implementação para os acessadores substituindo o evento. Uma vez que um acessor para uma declaração de evento abstrato não fornece nenhuma implementação real, ele não deve fornecer event_accessor_declarations.

Uma declaração de evento que inclui os abstract modificadores e override especifica que o evento é abstrato e substitui um evento base. Os acessadores de tal evento também são abstratos.

As declarações de eventos abstratos só são permitidas em classes abstratas (§15.2.2.2).

Os acessadores de um evento virtual herdado podem ser substituídos em uma classe derivada incluindo uma declaração de evento que especifica um override modificador. Isso é conhecido como uma declaração de evento prevalecente. Uma declaração de evento substitutivo não declara um novo evento. Em vez disso, ele simplesmente especializa as implementações dos acessadores de um evento virtual existente.

Uma declaração de evento prevalecente deve especificar exatamente os mesmos modificadores de acessibilidade e o nome que o evento anulado, deve haver uma conversão de identidade entre o tipo de evento anulatório e o evento anulado, e ambos os acessores de adição e remoção devem ser especificados na declaração.

Uma declaração de evento de substituição pode incluir o sealed modificador. O uso do this modificador impede que uma classe derivada substitua ainda mais o evento. Os acessórios de um evento selado também são lacrados.

É um erro em tempo de compilação para uma declaração de evento de substituição incluir um new modificador.

Exceto para diferenças na sintaxe de declaração e invocação, os acessadores virtuais, selados, de substituição e abstratos se comportam exatamente como métodos virtuais, selados, de substituição e abstratos. Especificamente, as regras descritas nos §15.6.4, §15.6.5, §15.6.6 e §15.6.7 aplicam-se como se os acessadores fossem métodos de uma forma correspondente. Cada acessador corresponde a um método com um único parâmetro de valor do tipo de evento, um void tipo de retorno e os mesmos modificadores que o evento que contém.

15.9 Indexadores

15.9.1 Generalidades

Um indexador é um membro que permite que um objeto seja indexado da mesma forma que uma matriz. Os indexadores são declarados usando indexer_declarations:

indexer_declaration
    : attributes? indexer_modifier* indexer_declarator indexer_body
    | attributes? indexer_modifier* ref_kind indexer_declarator ref_indexer_body
    ;

indexer_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'virtual'
    | 'sealed'
    | 'override'
    | 'abstract'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;

indexer_declarator
    : type 'this' '[' parameter_list ']'
    | type interface_type '.' 'this' '[' parameter_list ']'
    ;

indexer_body
    : '{' accessor_declarations '}' 
    | '=>' expression ';'
    ;  

ref_indexer_body
    : '{' ref_get_accessor_declaration '}'
    | '=>' 'ref' variable_reference ';'
    ;

unsafe_modifier (§23.2) só está disponível em código não seguro (§23).

Existem dois tipos de indexer_declaration:

  • O primeiro declara um indexador sem valor de referência. Seu valor tem tipo de tipo. Este tipo de indexador pode ser legível e/ou gravável.
  • O segundo declara um indexador com valor de referência. O seu valor é um variable_reference (§9.5), que pode ser readonly, para uma variável do tipo tipo. Este tipo de indexador é apenas legível.

Um indexer_declaration pode incluir um conjunto de atributos (§22) e qualquer um dos tipos permitidos de acessibilidade declarada (§15.3.6), os new modificadores (§15.3.5), virtual (§15.6.4), override (§15.6.5), sealed (abstract), (§15.6.7) e extern (§15.6.8).

As declarações de indexador estão sujeitas às mesmas regras que as declarações de método (§15.6) no que diz respeito a combinações válidas de modificadores, com a única exceção sendo que o static modificador não é permitido em uma declaração de indexador.

O tipo de uma declaração de indexador especifica o tipo de elemento do indexador introduzido pela declaração.

Nota: Como os indexadores são projetados para serem usados em contextos semelhantes a elementos de matriz, o termo tipo de elemento conforme definido para uma matriz também é usado com um indexador. Nota final

A menos que o indexador seja uma implementação explícita de membro da interface, o tipo é seguido pela palavra-chave this. Para uma implementação explícita de membro da interface, o tipo é seguido por um interface_type, um "." e a palavra-chave this. Ao contrário de outros membros, os indexadores não têm nomes definidos pelo usuário.

O parameter_list especifica os parâmetros do indexador. A lista de parâmetros de um indexador corresponde à de um método (§15.6.2), exceto que pelo menos um parâmetro deve ser especificado e que o this, refe out modificadores de parâmetros não são permitidos.

O tipo de indexador e cada um dos tipos referenciados no parameter_list devem ser pelo menos tão acessíveis como o próprio indexador (ponto 7.5.5).

Um indexer_body pode consistir num corpo de declarações (§15.7.1) ou num corpo de expressão (§15.6.1). Em um corpo de instrução, accessor_declarations, que devem ser incluídos em tokens "" e "{", declaram os acessadores (}) do indexador. Os acessadores especificam as instruções executáveis associadas aos elementos indexadores de leitura e gravação.

Em um indexer_body um corpo de expressão que consiste em "=>" seguido por uma expressão E e um ponto-e-vírgula é exatamente equivalente ao corpo { get { return E; } }da instrução e, portanto, só pode ser usado para especificar indexadores somente leitura onde o resultado do acessor get é dado por uma única expressão.

Um ref_indexer_body pode consistir em um corpo de declaração ou um corpo de expressão. Em um corpo de instrução, um get_accessor_declaration declara o acessador get (§15.7.3) do indexador. O acessador especifica as instruções executáveis associadas à leitura do indexador.

Em um ref_indexer_body um corpo de expressão que consiste em seguido por =>ref , um variable_referenceV e um ponto-e-vírgula é exatamente equivalente ao corpo { get { return ref V; } }da declaração.

Nota: Embora a sintaxe para acessar um elemento indexador seja a mesma de um elemento de matriz, um elemento indexador não é classificado como uma variável. Assim, não é possível passar um elemento indexador como um in, outou ref argumento, a menos que o indexador seja ref-value e, portanto, retorne uma referência (§9.7). Nota final

O parameter_list de um indexador define a assinatura (§7.6) do indexador. Especificamente, a assinatura de um indexador consiste no número e tipos de seus parâmetros. O tipo de elemento e os nomes dos parâmetros não fazem parte da assinatura de um indexador.

A assinatura de um indexador deve diferir das assinaturas de todos os outros indexadores declarados na mesma classe.

Quando uma declaração de indexador inclui um extern modificador, diz-se que o indexador é um indexador externo. Como uma declaração de indexador externo não fornece nenhuma implementação real, cada um dos accessor_bodys em seu accessor_declarations deve ser um ponto-e-vírgula.

Exemplo: O exemplo abaixo declara uma BitArray classe que implementa um indexador para acessar os bits individuais na 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);
            }
        }
    }
}

Uma instância da BitArray classe consome substancialmente menos memória do que uma correspondente bool[] (uma vez que cada valor do primeiro ocupa apenas um bit em vez do do segundo byte), mas permite as mesmas operações que um bool[].

A classe a seguir CountPrimes usa um BitArray e o algoritmo clássico de "peneira" para calcular o número de primos entre 2 e um determinado máximo:

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

Observe que a sintaxe para acessar elementos do é BitArray exatamente a mesma que para um bool[].

O exemplo a seguir mostra uma classe de grade 26×10 que tem um indexador com dois parâmetros. O primeiro parâmetro deve ser uma letra maiúscula ou minúscula no intervalo de A a Z, e o segundo deve ser um inteiro no intervalo de 0 a 9.

class Grid
{
    const int NumRows = 26;
    const int NumCols = 10;
    int[,] cells = new int[NumRows, NumCols];

    public int this[char row, int col]
    {
        get
        {
            row = Char.ToUpper(row);
            if (row < 'A' || row > 'Z')
            {
                throw new ArgumentOutOfRangeException("row");
            }
            if (col < 0 || col >= NumCols)
            {
                throw new ArgumentOutOfRangeException ("col");
            }
            return cells[row - 'A', col];
        }
        set
        {
            row = Char.ToUpper(row);
            if (row < 'A' || row > 'Z')
            {
                throw new ArgumentOutOfRangeException ("row");
            }
            if (col < 0 || col >= NumCols)
            {
                throw new ArgumentOutOfRangeException ("col");
            }
            cells[row - 'A', col] = value;
        }
    }
}

Exemplo final

15.9.2 Indexador e diferenças de propriedade

Indexadores e propriedades são muito semelhantes em conceito, mas diferem das seguintes maneiras:

  • Uma propriedade é identificada pelo seu nome, enquanto um indexador é identificado pela sua assinatura.
  • Uma propriedade é acedida através de um simple_name (§12.8.4) ou de um member_access (§12.8.7), enquanto um elemento indexador é acedido através de um element_access (§12.8.12.3).
  • Uma propriedade pode ser um membro estático, enquanto um indexador é sempre um membro de instância.
  • Um acessador get de uma propriedade corresponde a um método sem parâmetros, enquanto um acessor get de um indexador corresponde a um método com a mesma lista de parâmetros que o indexador.
  • Um acessador de conjunto de uma propriedade corresponde a um método com um único parâmetro chamado value, enquanto um acessador conjunto de um indexador corresponde a um método com a mesma lista de parâmetros que o indexador, mais um parâmetro adicional chamado value.
  • É um erro em tempo de compilação para um acessador indexador declarar uma variável local ou constante local com o mesmo nome de um parâmetro indexador.
  • Em uma declaração de propriedade de substituição, a propriedade herdada é acessada usando a sintaxe base.P, onde P é o nome da propriedade. Em uma declaração de indexador de substituição, o indexador herdado é acessado usando a sintaxe base[E], onde E é uma lista de expressões separadas por vírgula.
  • Não existe o conceito de um "indexador implementado automaticamente". É um erro ter um indexador não-abstrato, não-externo com ponto-e-vírgula accessor_bodys.

Além dessas diferenças, todas as regras definidas nos §15.7.3, §15.7.5 e §15.7.6 aplicam-se aos acessadores indexadores, bem como aos acessadores de propriedade.

Esta substituição de propriedade/propriedades por indexadores/indexadores ao ler os §15.7.3, §15.7.5 e §15.7.6 também se aplica a termos definidos. Especificamente, a propriedade leitura-gravação torna-se indexador de leitura-gravação, a propriedade somente leitura torna-se indexador somente leitura e a propriedade somente gravação torna-se indexador somente gravação.

15.10 Operadores

15.10.1 Generalidades

Um operador é um membro que define o significado de um operador de expressão que pode ser aplicado a instâncias da classe. Os operadores são declarados utilizando 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) só está disponível em código não seguro (§23).

Nota: O prefixo negação lógica (§12.9.4) e os operadores de perdão nulo do postfix (§12.8.9), embora representados pelo mesmo token lexical (!), são distintos. Este último não é um operador sobrecarregado. Nota final

Existem três categorias de operadores sobrecarregados: operadores unários (§15.10.2), operadores binários (§15.10.3) e operadores de conversão (§15.10.4).

O operator_body é um ponto e vírgula, um corpo de bloco (§15.6.1) ou um corpo de expressão (§15.6.1). Um corpo de bloco consiste em um bloco, que especifica as instruções a serem executadas quando o operador é invocado. O bloco deve estar em conformidade com as regras relativas aos métodos de retorno de valor descritas no ponto 15.6.11. Um corpo de => expressão consiste em seguido por uma expressão e um ponto-e-vírgula, e denota uma única expressão a ser executada quando o operador é invocado.

Para extern os operadores, o operator_body consiste simplesmente em um ponto-e-vírgula. Para todos os outros operadores, o operator_body é um corpo de bloco ou um corpo de expressão.

Aplicam-se as seguintes regras a todas as declarações de operador:

  • A declaração do operador deve incluir um modificador e um publicstatic modificador.
  • O(s) parâmetro(s) de um operador não deve(m) ter modificadores para além inde .
  • A assinatura de um operador (§15.10.2, §15.10.3, §15.10.4) deve diferir das assinaturas de todos os outros operadores declarados na mesma classe.
  • Todos os tipos referidos numa declaração do operador devem ser pelo menos tão acessíveis como o próprio operador (ponto 7.5.5).
  • É um erro para o mesmo modificador aparecer várias vezes em uma declaração de operador.

Cada categoria de operador impõe restrições adicionais, conforme descrito nas subcláusulas seguintes.

Como outros membros, os operadores declarados em uma classe base são herdados por classes derivadas. Como as declarações de operador sempre exigem que a classe ou estrutura na qual o operador é declarado participe da assinatura do operador, não é possível para um operador declarado em uma classe derivada ocultar um operador declarado em uma classe base. Assim, o new modificador nunca é exigido e, portanto, nunca permitido, em uma declaração de operador.

Informações adicionais sobre operadores unários e binários podem ser encontradas no §12.4.

Para mais informações sobre os operadores de conversão, consultar o ponto 10.5.

15.10.2 Operadores unários

As seguintes regras aplicam-se às declarações de operador unárias, onde T indica o tipo de instância da classe ou struct que contém a declaração do operador:

  • Um operador unário +, -, ! (apenas negação lógica), deve ~ tomar um único parâmetro de tipo T ou T? e pode retornar qualquer tipo.
  • Um unário ++ ou -- operador deve utilizar um único parâmetro de tipo T ou T? e devolver esse mesmo tipo ou um tipo dele derivado.
  • Um unário true ou false operador deve tomar um único parâmetro de tipo T ou T? e deve retornar o tipo bool.

A assinatura de um operador unário consiste no token do operador (+, -, !, , ~++, --true, ou false) e no tipo do parâmetro único. O tipo de retorno não faz parte da assinatura de um operador unário, nem o nome do parâmetro.

Os true operadores e false unary exigem declaração de pares. Um erro em tempo de compilação ocorre se uma classe declara um desses operadores sem também declarar o outro. Os true operadores e false são descritos mais detalhadamente no §12.24.

Exemplo: O exemplo a seguir mostra uma implementação e o uso subsequente de operator++ para uma classe de vetor inteiro:

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 como o método do operador retorna o valor produzido adicionando 1 ao operando, assim como os operadores de incremento e decréscimo postfix (§12.8.16) e os operadores de incremento e decréscimo de prefixo (§12.9.6). Ao contrário do C++, este método não deve modificar o valor de seu operando diretamente, pois isso violaria a semântica padrão do operador de incremento postfix (§12.8.16).

Exemplo final

15.10.3 Operadores binários

As regras a seguir se aplicam a declarações de operador binário, onde T denota o tipo de instância da classe ou struct que contém a declaração de operador:

  • Um operador binário sem turno deve ter dois parâmetros, dos quais pelo menos um deve ter o tipo T ou T?, e pode devolver qualquer tipo.
  • Um binário << ou >> operador (§12.11) deve tomar dois parâmetros, o primeiro dos quais deve ter tipo T ou T? e o segundo deve ter tipo int ou int?, e pode retornar qualquer tipo.

A assinatura de um operador binário consiste no token do operador (+, -, *, /, %, &|^<<>>==!=><>=ou <=) e os tipos dos dois parâmetros. O tipo de retorno e os nomes dos parâmetros não fazem parte da assinatura de um operador binário.

Certos operadores binários requerem declaração em pares. Para cada declaração de um operador de um par, deve existir uma declaração correspondente do outro operador do par. Duas declarações de operador correspondem se existirem conversões de identidade entre seus tipos de retorno e seus tipos de parâmetros correspondentes. Os operadores a seguir exigem declaração em pares:

  • operador == e operador !=
  • operador > e operador <
  • operador >= e operador <=

15.10.4 Operadores de conversão

Uma declaração de operador de conversão introduz uma conversão definida pelo utilizador (§10.5), que aumenta as conversões implícitas e explícitas predefinidas.

Uma declaração de operador de conversão que inclui a implicit palavra-chave introduz uma conversão implícita definida pelo usuário. As conversões implícitas podem ocorrer em várias situações, incluindo invocações de membros de funções, expressões de elenco e atribuições. Isto é descrito mais pormenorizadamente no §10.2.

Uma declaração de operador de conversão que inclui a explicit palavra-chave introduz uma conversão explícita definida pelo usuário. Conversões explícitas podem ocorrer em expressões de elenco e são descritas mais detalhadamente em §10.3.

Um operador de conversão converte de um tipo de fonte, indicado pelo tipo de parâmetro do operador de conversão, para um tipo de destino, indicado pelo tipo de retorno do operador de conversão.

Para um determinado tipo S de origem e tipo Tde destino, se S ou T são tipos de valor anuláveis, deixe S₀ e T₀ faça referência aos seus tipos subjacentes, caso contrário, S₀ e T₀ são iguais a S e T respectivamente. Uma classe ou struct tem permissão para declarar uma conversão de um tipo S de origem para um tipo T de destino somente se todos os itens a seguir forem verdadeiros:

  • S₀ e T₀ são tipos diferentes.

  • Ou S₀T₀ é o tipo de instância da classe ou struct que contém a declaração do operador.

  • Nem S₀T₀ é uma interface_type.

  • Excluindo conversões definidas pelo usuário, não existe uma conversão de S para T ou de T para S.

Para os fins dessas regras, quaisquer parâmetros de tipo associados ou ST considerados tipos exclusivos que não têm relação de herança com outros tipos, e quaisquer restrições sobre esses parâmetros de tipo são ignoradas.

Exemplo: No seguinte:

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
}

As duas primeiras declarações de operador são permitidas porque T e intstring , respectivamente, são consideradas tipos únicos sem relação. No entanto, o terceiro operador é um erro porque C<T> é a classe base de D<T>.

Exemplo final

Da segunda regra decorre que um operador de conversão deve converter para ou do tipo de classe ou estrutura em que é declarado.

Exemplo: É possível que uma classe ou tipo C struct defina uma conversão de C para int e de int para C, mas não de int para bool. Exemplo final

Não é possível redefinir diretamente uma conversão predefinida. Assim, os operadores de conversão não podem converter de ou para object porque já existem conversões implícitas e explícitas entre object e todos os outros tipos. Da mesma forma, nem os tipos de origem nem de destino de uma conversão podem ser um tipo base da outra, uma vez que uma conversão já existiria. No entanto, é possível declarar operadores em tipos genéricos que, para argumentos de tipo específicos, especificam conversões que já existem como conversões predefinidas.

Exemplo:

struct Convertible<T>
{
    public static implicit operator Convertible<T>(T value) {...}
    public static explicit operator T(Convertible<T> value) {...}
}

Quando type object é especificado como um argumento type para T, o segundo operador declara uma conversão que já existe (uma conversão implícita e, portanto, também explícita, existe de qualquer tipo para objeto type).

Exemplo final

Nos casos em que existe uma conversão predefinida entre dois tipos, todas as conversões definidas pelo usuário entre esses tipos são ignoradas. Especificamente:

  • Se existir uma conversão implícita predefinida (§10.2) de tipo S para tipo T, todas as conversões definidas pelo utilizador (implícitas ou explícitas) de S para T são ignoradas.
  • Se existir uma conversão explícita predefinida (§10.3) de tipo S para tipo T, todas as conversões explícitas definidas pelo utilizador de S para T serão ignoradas. Além disso:
    • Se um ou ST for um tipo de interface, as conversões implícitas definidas pelo usuário de S para T serão ignoradas.
    • Caso contrário, as conversões implícitas definidas pelo usuário de S para T ainda são consideradas.

Para todos os tipos, exceto object, os operadores declarados Convertible<T> pelo tipo acima não entram em conflito com conversões predefinidas.

Exemplo:

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
}

No entanto, para o tipo object, as conversões predefinidas ocultam as conversões definidas pelo usuário em todos os casos, exceto um:

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
}

Exemplo final

Conversões definidas pelo usuário não podem converter de ou para interface_types. Em particular, essa restrição garante que nenhuma transformação definida pelo usuário ocorra ao converter em um interface_type e que uma conversão em um interface_type seja bem-sucedida somente se o object que está sendo convertido realmente implementar o interface_type especificado.

A assinatura de um operador de conversão consiste no tipo de origem e no tipo de destino. (Esta é a única forma de membro para a qual o tipo de retorno participa da assinatura.) A classificação implícita ou explícita de um operador de conversão não faz parte da assinatura do operador. Assim, uma classe ou struct não pode declarar um operador de conversão implícito e explícito com os mesmos tipos de origem e destino.

Nota: Em geral, as conversões implícitas definidas pelo usuário devem ser projetadas para nunca lançar exceções e nunca perder informações. Se uma conversão definida pelo usuário pode dar origem a exceções (por exemplo, porque o argumento de origem está fora do intervalo) ou perda de informações (como descartar bits de ordem alta), essa conversão deve ser definida como uma conversão explícita. Nota final

Exemplo: No código a seguir

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

A conversão de Digit para byte é implícita porque nunca lança exceções ou perde informações, mas a conversão de byte para Digit é explícita, uma vez que Digit só pode representar um subconjunto dos valores possíveis de um byte.

Exemplo final

15.11 Construtores de instância

15.11.1 Generalidades

Um construtor de instância é um membro que implementa as ações necessárias para inicializar uma instância de uma classe. Os construtores de instância são declarados usando constructor_declarations:

constructor_declaration
    : attributes? constructor_modifier* constructor_declarator constructor_body
    ;

constructor_modifier
    : 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;

constructor_declarator
    : identifier '(' parameter_list? ')' constructor_initializer?
    ;

constructor_initializer
    : ':' 'base' '(' argument_list? ')'
    | ':' 'this' '(' argument_list? ')'
    ;

constructor_body
    : block
    | '=>' expression ';'
    | ';'
    ;

unsafe_modifier (§23.2) só está disponível em código não seguro (§23).

Um constructor_declaration pode incluir um conjunto de atributos (§22), qualquer um dos tipos permitidos de acessibilidade declarada (§15.3.6) e um extern modificador (§15.6.8). Uma declaração de construtor não tem permissão para incluir o mesmo modificador várias vezes.

O identificador de um constructor_declarator deve nomear a classe na qual o construtor da instância é declarado. Se qualquer outro nome for especificado, ocorrerá um erro em tempo de compilação.

O parameter_list opcional de um construtor de instância está sujeito às mesmas regras que o parameter_list de um método (§15.6). Como o this modificador para parâmetros só se aplica a métodos de extensão (§15.6.10), nenhum parâmetro no parameter_list de um construtor deve conter o this modificador. A lista de parâmetros define a assinatura (§7.6) de um construtor de instância e rege o processo pelo qual a resolução de sobrecarga (§12.6.4) seleciona um construtor de instância específico em uma invocação.

Cada um dos tipos referenciados no parameter_list de um construtor de instância deve ser pelo menos tão acessível quanto o próprio construtor (§7.5.5).

O constructor_initializer opcional especifica outro construtor de instância a ser invocado antes de executar as instruções fornecidas no constructor_body deste construtor de instância. Isto é descrito mais pormenorizadamente no §15.11.2.

Quando uma declaração de construtor inclui um extern modificador, o construtor é dito ser um construtor externo. Como uma declaração de construtor externo não fornece nenhuma implementação real, seu constructor_body consiste em um ponto-e-vírgula. Para todos os outros construtores, o constructor_body consiste em

  • um bloco, que especifica as instruções para inicializar uma nova instância da classe;
  • um corpo de => expressão, que consiste em seguido por uma expressão e um ponto-e-vírgula, e denota uma única expressão para inicializar uma nova instância da classe.

Um constructor_body que é um bloco ou corpo de expressão corresponde exatamente ao bloco de um método de instância com um void tipo de retorno (§15.6.11).

Os construtores de instância não são herdados. Assim, uma classe não tem construtores de instância além daqueles realmente declarados na classe, com a exceção de que, se uma classe não contiver declarações de construtor de instância, um construtor de instância padrão será fornecido automaticamente (§15.11.5).

Os construtores de instância são invocados por object_creation_expressions (§12.8.17.2) e através de constructor_initializers.

15.11.2 Inicializadores do construtor

Todos os construtores de instância (exceto aqueles para classe object) incluem implicitamente uma invocação de outro construtor de instância imediatamente antes do constructor_body. O construtor a invocar implicitamente é determinado pelo constructor_initializer:

  • Um construtor de instância inicializador do formulário base(argument_list) (onde argument_list é opcional) faz com que um construtor de instância da classe base direta seja invocado. Esse construtor é selecionado usando argument_list e as regras de resolução de sobrecarga do §12.6.4. O conjunto de construtores de instância candidatos consiste em todos os construtores de instância acessíveis da classe base direta. Se esse conjunto estiver vazio ou se um único construtor de melhor instância não puder ser identificado, ocorrerá um erro em tempo de compilação.
  • Um construtor de instância inicializador do formulário this(argument_list) (onde argument_list é opcional) invoca outro construtor de instância da mesma classe. O construtor é selecionado usando argument_list e as regras de resolução de sobrecarga do §12.6.4. O conjunto de construtores de instância candidato consiste em todos os construtores de instância declarados na própria classe. Se o conjunto resultante de construtores de instância aplicáveis estiver vazio ou se um único construtor de melhor instância não puder ser identificado, ocorrerá um erro em tempo de compilação. Se uma declaração de construtor de instância invoca a si mesma através de uma cadeia de um ou mais inicializadores de construtor, ocorre um erro em tempo de compilação.

Se um construtor de instância não tem nenhum inicializador de construtor, um inicializador de construtor do formulário base() é implicitamente fornecido.

Nota: Assim, uma declaração do construtor de instância do formulário

C(...) {...}

é exatamente equivalente a

C(...) : base() {...}

Nota final

O escopo dos parâmetros fornecidos pelo parameter_list de uma declaração de construtor de instância inclui o inicializador do construtor dessa declaração. Assim, um inicializador do construtor tem permissão para acessar os parâmetros do construtor.

Exemplo:

class A
{
    public A(int x, int y) {}
}

class B: A
{
    public B(int x, int y) : base(x + y, x - y) {}
}

Exemplo final

Um inicializador do construtor de instância não pode acessar a instância que está sendo criada. Portanto, é um erro em tempo de compilação fazer referência a isso em uma expressão de argumento do inicializador do construtor, pois é um erro em tempo de compilação para uma expressão de argumento fazer referência a qualquer membro da instância por meio de um simple_name.

15.11.3 Inicializadores de variáveis de instância

Quando um construtor de instância não tem um inicializador de construtor, ou tem um inicializador de construtor do formulário base(...), esse construtor executa implicitamente as inicializações especificadas pelos variable_initializers dos campos de instância declarados em sua classe. Isso corresponde a uma sequência de atribuições que são executadas imediatamente após a entrada no construtor e antes da invocação implícita do construtor de classe base direta. Os inicializadores de variáveis são executados na ordem textual em que aparecem na declaração de classe (§15.5.6).

15.11.4 Execução do construtor

Os inicializadores de variáveis são transformados em instruções de atribuição, e essas instruções de atribuição são executadas antes da invocação do construtor de instância de classe base. Essa ordenação garante que todos os campos de instância sejam inicializados por seus inicializadores variáveis antes que quaisquer instruções que tenham acesso a essa instância sejam executadas.

Exemplo: Dado o seguinte:

class A
{
    public A()
    {
        PrintFields();
    }

    public virtual void PrintFields() {}
}
class B: A
{
    int x = 1;
    int y;

    public B()
    {
        y = -1;
    }

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

Quando New B() é usado para criar uma instância de B, a seguinte saída é produzida:

x = 1, y = 0

O valor de é 1 porque o inicializador da variável é executado antes que o construtor da instância da x classe base seja invocado. No entanto, o valor de é 0 (o valor padrão de um y) porque a atribuição a int não é executada até que o construtor de y classe base retorne. É útil pensar em inicializadores de variáveis de instância e inicializadores de construtores como instruções que são inseridas automaticamente antes do constructor_body. O exemplo

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

contém vários inicializadores variáveis; ele também contém inicializadores construtores de ambos os formulários (base e this). O exemplo corresponde ao código mostrado abaixo, onde cada comentário indica uma instrução inserida automaticamente (a sintaxe usada para as invocações do construtor inseridas automaticamente não é válida, mas serve apenas para ilustrar o 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;
    }
}

Exemplo final

15.11.5 Construtores padrão

Se uma classe não contiver declarações de construtor de instância, um construtor de instância padrão será fornecido automaticamente. Esse construtor padrão simplesmente invoca um construtor da classe base direta, como se tivesse um inicializador de construtor do formulário base(). Se a classe for abstrata, a acessibilidade declarada para o construtor padrão será protegida. Caso contrário, a acessibilidade declarada para o construtor padrão é pública.

Nota: Assim, o construtor padrão é sempre da forma

protected C(): base() {}

ou

public C(): base() {}

onde C é o nome da classe.

Nota final

Se a resolução de sobrecarga não conseguir determinar um melhor candidato exclusivo para o inicializador do construtor de classe base, ocorrerá um erro em tempo de compilação.

Exemplo: No código a seguir

class Message
{
    object sender;
    string text;
}

Um construtor padrão é fornecido porque a classe não contém declarações de construtor de instância. Assim, o exemplo é precisamente equivalente a

class Message
{
    object sender;
    string text;

    public Message() : base() {}
}

Exemplo final

15.12 Construtores estáticos

Um construtor estático é um membro que implementa as ações necessárias para inicializar uma classe fechada. Os construtores estáticos são declarados usando static_constructor_declarations:

static_constructor_declaration
    : attributes? static_constructor_modifiers identifier '(' ')'
        static_constructor_body
    ;

static_constructor_modifiers
    : 'static'
    | 'static' 'extern' unsafe_modifier?
    | 'static' unsafe_modifier 'extern'?
    | 'extern' 'static' unsafe_modifier?
    | 'extern' unsafe_modifier 'static'
    | unsafe_modifier 'static' 'extern'?
    | unsafe_modifier 'extern' 'static'
    ;

static_constructor_body
    : block
    | '=>' expression ';'
    | ';'
    ;

unsafe_modifier (§23.2) só está disponível em código não seguro (§23).

Um static_constructor_declaration pode incluir um conjunto de atributos (§22) e um extern modificador (§15.6.8).

O identificador de um static_constructor_declaration deve nomear a classe na qual o construtor estático é declarado. Se qualquer outro nome for especificado, ocorrerá um erro em tempo de compilação.

Quando uma declaração de construtor estático inclui um extern modificador, o construtor estático é dito ser um construtor estático externo. Como uma declaração de construtor estático externo não fornece nenhuma implementação real, seu static_constructor_body consiste em um ponto-e-vírgula. Para todas as outras declarações de construtor estático, o static_constructor_body consiste em

  • um bloco, que especifica as instruções a serem executadas para inicializar a classe;
  • Um corpo de => expressão, que consiste em seguido por uma expressão e um ponto-e-vírgula, e denota uma única expressão a ser executada para inicializar a classe.

Um static_constructor_body que é um bloco ou corpo de expressão corresponde exatamente ao method_body de um método estático com um void tipo de retorno (§15.6.11).

Os construtores estáticos não são herdados e não podem ser chamados diretamente.

O construtor estático para uma classe fechada é executado no máximo uma vez em um determinado domínio de aplicativo. A execução de um construtor estático é acionada pelo primeiro dos seguintes eventos a ocorrer dentro de um domínio de aplicativo:

  • Uma instância da classe é criada.
  • Qualquer um dos membros estáticos da classe são referenciados.

Se uma classe contém o método (Main) no qual a execução começa, o construtor estático dessa classe é executado antes que o Main método seja chamado.

Para inicializar um novo tipo de classe fechada, primeiro é criado um novo conjunto de campos estáticos (§15.5.2) para esse tipo fechado específico. Cada um dos campos estáticos é inicializado com o seu valor predefinido (§15.5.5). Em seguida, os inicializadores de campo estático (§15.5.6.2) são executados para esses campos estáticos. Finalmente, o construtor estático é executado.

Exemplo: O exemplo

class Test
{
    static void Main()
    {
        A.F();
        B.F();
    }
}

class A
{
    static A()
    {
        Console.WriteLine("Init A");
    }

    public static void F()
    {
        Console.WriteLine("A.F");
    }
}

class B
{
    static B()
    {
        Console.WriteLine("Init B");
    }

    public static void F()
    {
        Console.WriteLine("B.F");
    }
}

deve produzir a saída:

Init A
A.F
Init B
B.F

porque a execução do Aconstrutor estático de 's é acionada pela chamada para A.F, e a execução do construtor estático de 's é acionada Bpela chamada para B.F.

Exemplo final

É possível construir dependências circulares que permitem que campos estáticos com inicializadores variáveis sejam observados em seu estado de valor padrão.

Exemplo: O exemplo

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

produz a saída

X = 1, Y = 2

Para executar o Main método, o sistema primeiro executa o inicializador para B.Y, antes do construtor estático da classe B. YO inicializador do faz com que Ao construtor de 's static seja executado porque o valor de A.X é referenciado. O construtor estático de A , por sua vez, prossegue para calcular o valor de X, e, ao fazer isso, obtém o valor padrão de Y, que é zero. A.X é, portanto, inicializado para 1. O processo de execução Ados inicializadores de campo estático e do construtor estático então é concluído, retornando ao cálculo do valor inicial de Y, cujo resultado se torna 2.

Exemplo final

Como o construtor estático é executado exatamente uma vez para cada tipo de classe construída fechada, é um local conveniente para impor verificações em tempo de execução no parâmetro type que não pode ser verificado em tempo de compilação por meio de restrições (§15.2.5).

Exemplo: O tipo a seguir usa um construtor estático para impor que o argumento type é um enum:

class Gen<T> where T : struct
{
    static Gen()
    {
        if (!typeof(T).IsEnum)
        {
            throw new ArgumentException("T must be an enum");
        }
    }
}

Exemplo final

15.13 Finalizadores

Nota: Em uma versão anterior desta especificação, o que agora é referido como um "finalizador" era chamado de "destruidor". A experiência tem mostrado que o termo "destruidor" causou confusão e muitas vezes resultou em expectativas incorretas, especialmente para programadores que conhecem C++. Em C++, um destruidor é chamado de maneira determinada, enquanto que, em C#, um finalizador não é. Para obter um comportamento determinado do C#, deve-se usar Disposeo . Nota final

Um finalizador é um membro que implementa as ações necessárias para finalizar uma instância de uma classe. Um finalizador é declarado usando um 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) só está disponível em código não seguro (§23).

Um finalizer_declaration pode incluir um conjunto de atributos (§22).

O identificador de um finalizer_declarator designa a classe em que o finalizador é declarado. Se qualquer outro nome for especificado, ocorrerá um erro em tempo de compilação.

Quando uma declaração de finalizador inclui um extern modificador, diz-se que o finalizador é um finalizador externo. Como uma declaração de finalizador externo não fornece nenhuma implementação real, seu finalizer_body consiste em um ponto-e-vírgula. Para todos os outros finalizadores, o finalizer_body consiste em

  • um bloco, que especifica as instruções a serem executadas para finalizar uma instância da classe.
  • ou um corpo de => expressão, que consiste em seguido por uma expressão e um ponto-e-vírgula, e denota uma única expressão a ser executada para finalizar uma instância da classe.

Um finalizer_body que é um bloco ou corpo de expressão corresponde exatamente ao method_body de um método de instância com um void tipo de retorno (§15.6.11).

Os finalizadores não são herdados. Assim, uma classe não tem finalizadores além daquele que pode ser declarado nessa classe.

Nota: Como um finalizador é obrigado a não ter parâmetros, ele não pode ser sobrecarregado, então uma classe pode ter, no máximo, um finalizador. Nota final

Os finalizadores são invocados automaticamente e não podem ser invocados explicitamente. Uma instância torna-se elegível para finalização quando não é mais possível para qualquer código usar essa instância. A execução do finalizador para a instância pode ocorrer a qualquer momento após a instância se tornar elegível para finalização (§7.9). Quando uma instância é finalizada, os finalizadores na cadeia de herança dessa instância são chamados, em ordem, do mais derivado para o menos derivado. Um finalizador pode ser executado em qualquer thread. Para uma discussão mais aprofundada das regras que regem quando e como um finalizador é executado, consulte §7.9.

Exemplo: A saída do exemplo

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

é

B's finalizer
A's finalizer

uma vez que os finalizadores em uma cadeia de herança são chamados em ordem, do mais derivado para o menos derivado.

Exemplo final

Os finalizadores são implementados substituindo o método Finalize virtual em System.Object. Os programas C# não têm permissão para substituir esse método ou chamá-lo (ou substituí-lo) diretamente.

Exemplo: Por exemplo, o programa

class A
{
    override protected void Finalize() {}  // Error
    public void F()
    {
        this.Finalize();                   // Error
    }
}

contém dois erros.

Exemplo final

Um compilador deve comportar-se como se este método, e as suas substituições, não existissem.

Exemplo: Assim, este programa:

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

é válido e o método mostrado oculta System.Objecto método de .Finalize

Exemplo final

Para uma discussão sobre o comportamento quando uma exceção é lançada de um finalizador, consulte §21.4.

15.14 Iteradores

15.14.1 Generalidades

Um membro da função (§12.6) implementado usando um bloco iterador (§13.3) é chamado de iterador.

Um bloco iterador pode ser usado como o corpo de um membro da função, desde que o tipo de retorno do membro da função correspondente seja uma das interfaces do enumerador (§15.14.2) ou uma das interfaces enumeráveis (§15.14.3). Pode ocorrer como um method_body, operator_body ou accessor_body, enquanto eventos, construtores de instância, construtores estáticos e finalizador não devem ser implementados como iteradores.

Quando um membro da função é implementado usando um bloco iterador, é um erro em tempo de compilação para a lista de parâmetros do membro da função especificar qualquer in, outou ref parâmetros, ou um parâmetro de um ref struct tipo.

15.14.2 Interfaces do enumerador

As interfaces do enumerador são a interface System.Collections.IEnumerator não genérica e todas as instanciações da interface System.Collections.Generic.IEnumerator<T>genérica. Por uma questão de brevidade, nesta subcláusula e seus irmãos essas interfaces são referenciadas como IEnumerator e IEnumerator<T>, respectivamente.

15.14.3 Interfaces enumeráveis

As interfaces enumeráveis são a interface System.Collections.IEnumerable não genérica e todas as instanciações da interface System.Collections.Generic.IEnumerable<T>genérica. Por uma questão de brevidade, nesta subcláusula e seus irmãos essas interfaces são referenciadas como IEnumerable e IEnumerable<T>, respectivamente.

15.14.4 Tipo de rendimento

Um iterador produz uma sequência de valores, todos do mesmo tipo. Este tipo é chamado de tipo de rendimento do iterador.

  • O tipo de rendimento de um iterador que retorna IEnumerator ou IEnumerable é object.
  • O tipo de rendimento de um iterador que retorna IEnumerator<T> ou IEnumerable<T> é T.

15.14.5 Objetos enumeradores

15.14.5.1 Generalidades

Quando um membro da função que retorna um tipo de interface do enumerador é implementado usando um bloco iterador, invocar o membro da função não executa imediatamente o código no bloco iterador. Em vez disso, um objeto enumerador é criado e retornado. Este objeto encapsula o código especificado no bloco iterador e a execução do código no bloco iterador ocorre quando o método do objeto MoveNext enumerador é invocado. Um objeto enumerador tem as seguintes características:

  • Ele implementa IEnumerator e IEnumerator<T>, onde T é o tipo de rendimento do iterador.
  • Implementa System.IDisposable.
  • Ele é inicializado com uma cópia dos valores de argumento (se houver) e valor de instância passado para o membro da função.
  • Tem quatro estados potenciais, antes, em execução, suspenso e depois, e está inicialmente no estado anterior.

Um objeto enumerador é normalmente uma instância de uma classe de enumerador gerada pelo compilador que encapsula o código no bloco iterador e implementa as interfaces do enumerador, mas outros métodos de implementação são possíveis. Se uma classe de enumerador for gerada pelo compilador, essa classe será aninhada, direta ou indiretamente, na classe que contém o membro da função, terá acessibilidade privada e terá um nome reservado para uso do compilador (§6.4.3).

Um objeto enumerador pode implementar mais interfaces do que as especificadas acima.

As subcláusulas a seguir descrevem o comportamento necessário do MoveNext, Currente Dispose membros das implementações e IEnumeratorIEnumerator<T> interface fornecidas por um objeto enumerador.

Objetos enumerador não suportam o IEnumerator.Reset método. Invocar este método faz com que um System.NotSupportedException seja lançado.

15.14.5.2 O método MoveNext

O MoveNext método de um objeto enumerador encapsula o código de um bloco iterador. Invocar o método executa o MoveNext código no bloco iterador e define a Current propriedade do objeto enumerador conforme apropriado. A ação precisa executada por MoveNext depende do estado do objeto enumerador quando MoveNext é invocado:

  • Se o estado do objeto enumerador for anterior, invocando MoveNext:
    • Altera o estado para execução.
    • Inicializa os parâmetros (incluindo this) do bloco iterador para os valores de argumento e valor de instância salvos quando o objeto enumerador foi inicializado.
    • Executa o bloco iterador desde o início até que a execução seja interrompida (conforme descrito abaixo).
  • Se o estado do objeto enumerador estiver em execução, o resultado da invocação MoveNext não será especificado.
  • Se o estado do objeto enumerador estiver suspenso, invocando MoveNext:
    • Altera o estado para execução.
    • Restaura os valores de todas as variáveis e parâmetros locais (incluindo this) para os valores salvos quando a execução do bloco iterador foi suspensa pela última vez.

      Nota: O conteúdo de quaisquer objetos referenciados por essas variáveis pode ter sido alterado desde a chamada anterior para MoveNext. Nota final

    • Retoma a execução do bloco iterador imediatamente após a declaração de retorno de rendimento que causou a suspensão da execução e continua até que a execução seja interrompida (conforme descrito abaixo).
  • Se o estado do objeto enumerador for depois, invocar MoveNext retornará false.

Quando MoveNext executa o bloco iterador, a execução pode ser interrompida de quatro maneiras: por uma yield return instrução, por uma yield break instrução, encontrando o final do bloco iterador e por uma exceção sendo lançada e propagada para fora do bloco iterador.

  • Quando uma yield return declaração é encontrada (§9.4.4.20):
    • A expressão dada na instrução é avaliada, implicitamente convertida para o tipo de rendimento e atribuída à Current propriedade do objeto enumerador.
    • A execução do corpo iterador é suspensa. Os valores de todas as variáveis e parâmetros locais (incluindo this) são salvos, assim como o local desta yield return instrução. Se a instrução estiver dentro de yield return um ou mais try blocos, os blocos finais associados não serão executados no momento.
    • O estado do objeto enumerador é alterado para suspenso.
    • O MoveNext método retorna true ao seu chamador, indicando que a iteração avançou com êxito para o próximo valor.
  • Quando uma yield break declaração é encontrada (§9.4.4.20):
    • Se a instrução estiver dentro de yield break um ou mais try blocos, os blocos associados finally serão executados.
    • O estado do objeto enumerador é alterado para depois.
    • O MoveNext método retorna false ao seu chamador, indicando que a iteração está concluída.
  • Quando o final do corpo do iterador é encontrado:
    • O estado do objeto enumerador é alterado para depois.
    • O MoveNext método retorna false ao seu chamador, indicando que a iteração está concluída.
  • Quando uma exceção é lançada e propagada para fora do bloco iterador:
    • Os blocos apropriados finally no corpo do iterador terão sido executados pela propagação da exceção.
    • O estado do objeto enumerador é alterado para depois.
    • A propagação da exceção continua para o chamador do MoveNext método.

15.14.5.3 A propriedade Current

A propriedade de Current um objeto enumerador é afetada por yield return instruções no bloco iterador.

Quando um objeto enumerador está no estado suspenso , o valor de Current é o valor definido pela chamada anterior como MoveNext. Quando um objeto enumerador está nos estados antes, em execução ou depois , o resultado do acesso Current não é especificado.

Para um iterador com um tipo de rendimento diferente de object, o resultado do acesso Current através da implementação do objeto enumerador corresponde ao acesso IEnumerable através da implementação do objeto CurrentIEnumerator<T> enumerador e à conversão do resultado para object.

15.14.5.4 Método de eliminação

O Dispose método é usado para limpar a iteração trazendo o objeto enumerador para o estado after .

  • Se o estado do objeto enumerador for antes, invocar Dispose alterará o estado para depois.
  • Se o estado do objeto enumerador estiver em execução, o resultado da invocação Dispose não será especificado.
  • Se o estado do objeto enumerador estiver suspenso, invocando Dispose:
    • Altera o estado para execução.
    • Executa quaisquer blocos finais como se a última instrução executada yield return fosse uma yield break instrução. Se isso fizer com que uma exceção seja lançada e propagada para fora do corpo do iterador, o estado do objeto enumerador será definido como depois e a exceção será propagada para o chamador do Dispose método.
    • Muda o estado para depois.
  • Se o estado do objeto enumerador for posterior, a Dispose invocação não terá nenhum efeito.

15.14.6 Objetos enumeráveis

15.14.6.1 Generalidades

Quando um membro da função que retorna um tipo de interface enumerável é implementado usando um bloco iterador, invocar o membro da função não executa imediatamente o código no bloco iterador. Em vez disso, um objeto enumerável é criado e retornado. O método do GetEnumerator objeto enumerável retorna um objeto enumerador que encapsula o código especificado no bloco iterador, e a execução do código no bloco iterador ocorre quando o método do objeto MoveNext enumerador é invocado. Um objeto enumerável tem as seguintes características:

  • Ele implementa IEnumerable e IEnumerable<T>, onde T é o tipo de rendimento do iterador.
  • Ele é inicializado com uma cópia dos valores de argumento (se houver) e valor de instância passado para o membro da função.

Um objeto enumerável é normalmente uma instância de uma classe enumerável gerada pelo compilador que encapsula o código no bloco iterador e implementa as interfaces enumeráveis, mas outros métodos de implementação são possíveis. Se uma classe enumerável for gerada pelo compilador, essa classe será aninhada, direta ou indiretamente, na classe que contém o membro da função, terá acessibilidade privada e terá um nome reservado para uso do compilador (§6.4.3).

Um objeto enumerável pode implementar mais interfaces do que as especificadas acima.

Nota: Por exemplo, um objeto enumerável também pode implementar IEnumerator e IEnumerator<T>, permitindo que ele sirva como enumerável e enumerador. Normalmente, essa implementação retornaria sua própria instância (para salvar alocações) da primeira chamada para GetEnumerator. Invocações subsequentes de , se houver, retornariam uma nova instância de GetEnumeratorclasse, normalmente da mesma classe, para que as chamadas para instâncias de enumerador diferentes não afetassem umas às outras. Ele não pode retornar a mesma instância, mesmo que o enumerador anterior já tenha enumerado após o final da sequência, uma vez que todas as chamadas futuras para um enumerador esgotado devem lançar exceções. Nota final

15.14.6.2 O método GetEnumerator

Um objeto enumerável fornece uma implementação dos GetEnumerator métodos das IEnumerable interfaces e IEnumerable<T> . Os dois GetEnumerator métodos compartilham uma implementação comum que adquire e retorna um objeto enumerador disponível. O objeto enumerador é inicializado com os valores de argumento e o valor da instância salvos quando o objeto enumerável foi inicializado, mas, caso contrário, o objeto enumerador funciona conforme descrito em §15.14.5.

15.15 Funções assíncronas

15.15.1 Generalidades

Um método (§15.6) ou função anónima (§12.19) com o modificador é chamado de async assíncrona. Em geral, o termo assíncrono é usado para descrever qualquer tipo de função que tenha o async modificador.

É um erro em tempo de compilação para a lista de parâmetros de uma função assíncrona especificar qualquer in, outou ref parâmetros, ou qualquer parâmetro de um ref struct tipo.

O return_type de um método assíncrono deve ser um void ou um tipo de tarefa. Para um método assíncrono que produz um valor de resultado, um tipo de tarefa deve ser genérico. Para um método assíncrono que não produz um valor de resultado, um tipo de tarefa não deve ser genérico. Estes tipos são referidos na presente especificação como «TaskType»<T> e «TaskType», respetivamente. O tipo System.Threading.Tasks.Task de biblioteca padrão e os tipos construídos a partir de são tipos de tarefa, bem como um tipo de System.Threading.Tasks.Task<TResult> classe, struct ou interface que está associado a um tipo de construtor de tarefas através do atributo System.Runtime.CompilerServices.AsyncMethodBuilderAttribute. Esses tipos são referidos na presente especificação como «TaskBuilderType»<T> e «TaskBuilderType». Um tipo de tarefa pode ter no máximo um parâmetro type e não pode ser aninhado em um tipo genérico.

Um método assíncrono que retorna um tipo de tarefa é dito como retornando tarefa.

Os tipos de tarefa podem variar em sua definição exata, mas do ponto de vista da linguagem, um tipo de tarefa está em um dos estados incompleto, bem-sucedido ou com falha. Uma tarefa com defeito registra uma exceção pertinente. Um bem-sucedido«TaskType»<T> registra um resultado do tipo T. Os tipos de tarefas são aguardados e, portanto, as tarefas podem ser os operandos de expressões de espera (§12.9.8).

Exemplo: O tipo MyTask<T> de tarefa está associado ao tipo MyTaskMethodBuilder<T> de construtor de tarefas e ao tipo Awaiter<T>de garçom :

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() { ... }
}

Exemplo final

Um tipo de construtor de tarefas é um tipo de classe ou estrutura que corresponde a um tipo de tarefa específico (§15.15.2). O tipo de construtor de tarefas deve corresponder exatamente à acessibilidade declarada do tipo de tarefa correspondente.

Nota: Se o tipo de tarefa for declarado internal, o tipo de construtor correspondente também deve ser declarado internal e definido no mesmo assembly. Se o tipo de tarefa estiver aninhado dentro de outro tipo, o tipo de buider de tarefa também deverá ser aninhado nesse mesmo tipo. Nota final

Uma função assíncrona tem a capacidade de suspender a avaliação por meio de expressões de espera (§12.9.8) em seu corpo. A avaliação poderá ser retomada posteriormente no momento da suspensão aguardando expressão por meio de um delegado de retomada. O delegado de retomada é do tipo System.Action, e quando é invocado, a avaliação da invocação da função assíncrona será retomada a partir da expressão await de onde parou. O chamador atual de uma invocação de função assíncrona é o chamador original se a invocação da função nunca tiver sido suspensa ou o chamador mais recente do delegado de retomada de outra forma.

15.15.2 Padrão do construtor do tipo de tarefa

Um tipo de construtor de tarefas pode ter no máximo um parâmetro de tipo e não pode ser aninhado em um tipo genérico. Um tipo de construtor de tarefas deve ter os seguintes membros (para tipos de construtores de tarefas não genéricos, SetResult não tem parâmetros) com acessibilidade 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; }
}

Um compilador deve gerar código que usa o «TaskBuilderType» para implementar a semântica de suspender e retomar a avaliação da função assíncrona. Um compilador deve usar o «TaskBuilderType» da seguinte forma:

  • «TaskBuilderType».Create() é invocado para criar uma instância do «TaskBuilderType», nomeado builder nesta lista.
  • builder.Start(ref stateMachine) é invocado para associar o construtor a uma instância de máquina de estado gerada pelo compilador, stateMachine.
    • O construtor deve ligar stateMachine.MoveNext() ou Start() depois Start() de ter retornado para avançar a máquina do estado.
  • Após Start() retornos, o async método invoca builder.Task para que a tarefa retorne do método assíncrono.
  • Cada chamada para stateMachine.MoveNext() irá avançar a máquina do estado.
  • Se a máquina de estado for concluída com êxito, builder.SetResult() será chamada, com o valor de retorno do método, se houver.
  • Caso contrário, se uma exceção, e é lançada na máquina de estado, builder.SetException(e) é chamado.
  • Se a máquina de estado atingir uma await expr expressão, expr.GetAwaiter() será invocada.
  • Se o garçom implementa ICriticalNotifyCompletion e IsCompleted é falso, a máquina de builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine)estado invoca .
    • AwaitUnsafeOnCompleted() deve ligar awaiter.UnsafeOnCompleted(action) com um Action que ligue stateMachine.MoveNext() quando o garçom terminar.
  • Caso contrário, a máquina de estado invoca builder.AwaitOnCompleted(ref awaiter, ref stateMachine).
    • AwaitOnCompleted() deve ligar awaiter.OnCompleted(action) com um Action que ligue stateMachine.MoveNext() quando o garçom terminar.
  • SetStateMachine(IAsyncStateMachine) pode ser chamado pela implementação gerada IAsyncStateMachine pelo compilador para identificar a instância do construtor associada a uma instância de máquina de estado, particularmente para casos em que a máquina de estado é implementada como um tipo de valor.
    • Se o construtor chamar , o chamará stateMachine.SetStateMachine(stateMachine) a instância do stateMachinebuilder.SetStateMachine(stateMachine).stateMachine

Nota: Para ambos e SetResult(T result)«TaskType»<T> Task { get; } , o parâmetro e o argumento, respectivamente, devem ser conversíveis em T. Isso permite que um construtor de tipo de tarefa ofereça suporte a tipos como tuplas, onde dois tipos que não são iguais são conversíveis de identidade. Nota final

15.15.3 Avaliação de uma função assíncrona de retorno de tarefa

A invocação de uma função assíncrona de retorno de tarefa faz com que uma instância do tipo de tarefa retornado seja gerada. Isso é chamado de tarefa de retorno da função assíncrona. A tarefa está inicialmente em um estado incompleto .

O corpo da função assíncrona é então avaliado até que seja suspenso (alcançando uma expressão de espera) ou terminado, momento em que o controle é retornado ao chamador, juntamente com a tarefa de retorno.

Quando o corpo da função assíncrona termina, a tarefa de retorno é movida para fora do estado incompleto:

  • Se o corpo da função termina como resultado de alcançar uma instrução de retorno ou o final do corpo, qualquer valor de resultado é registrado na tarefa de retorno, que é colocada em um estado bem-sucedido .
  • Se o corpo da função termina devido a um não capturado OperationCanceledException, a exceção é registrada na tarefa de retorno que é colocada no estado cancelado .
  • Se o corpo da função terminar como resultado de qualquer outra exceção não detetada (§13.10.6), a exceção é registrada na tarefa de retorno, que é colocada em um estado defeituoso .

15.15.4 Avaliação de uma função assíncrona de retorno de vazio

Se o tipo de retorno da função assíncrona for void, a avaliação difere da acima da seguinte maneira: Como nenhuma tarefa é retornada, a função comunica a conclusão e as exceções ao contexto de sincronização do thread atual. A definição exata do contexto de sincronização depende da implementação, mas é uma representação de "onde" o thread atual está sendo executado. O contexto de sincronização é notificado quando a avaliação de uma voidfunção assíncrona de retorno é iniciada, é concluída com êxito ou faz com que uma exceção não detetada seja lançada.

Isso permite que o contexto acompanhe quantas voidfunções assíncronas de retorno estão sendo executadas sob ele e decida como propagar exceções que saem delas.