Compartilhar via


15 Aulas

15.1 Geral

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 dão suporte à 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 Geral

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 em um 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 de class_base opcional (§15.2.4), seguido por um conjunto opcional de type_parameter_constraints_clause s (§15.2.5), seguido por um class_body (§15.2.6), opcionalmente seguido por um ponto-e-vírgula.

Uma declaração de categoria só deve fornecer um type_parameter_constraints_clausese fornecer também um type_parameter_list.

Uma declaração de classe que fornece uma 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 de struct genérica é em si uma declaração de classe genérica, uma vez que os argumentos de tipo para o tipo que o contém devem ser fornecidos para criar um tipo construído (§8.4).

15.2.2 Modificadores de classe

15.2.2.1 Geral

Opcionalmente, um class_declaration pode 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) está disponível apenas em código não seguro (§23).

É um erro de 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. Ele especifica que a classe oculta um membro herdado com o mesmo nome, conforme descrito em §15.3.5. É um erro de 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, internal, e 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) inclui uma especificação de acessibilidade (por meio dos publicmodificadores , protected, internal, e private ), essa especificação deve concordar com todas as outras partes que incluem uma especificação de acessibilidade. Se nenhuma parte de um tipo parcial incluir uma especificação de acessibilidade, o tipo receberá a acessibilidade padrão apropriada (§7.5.2).

Os abstractmodificadores , sealed, e static são discutidos nas subcláusulas a seguir.

15.2.2.2 Aulas abstratas

O abstract modificador é usado para indicar que uma classe está incompleta e que deve 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 de 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, essas variáveis e valores necessariamente serão 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 abstrata introduz um método Fabstrato . Class B introduz um método Gadicional , mas como não fornece uma implementação de , B também deve ser declarado Fabstrato. A classe C substitui F e fornece uma implementação real. Como não há membros abstratos em C, C é permitido (mas não obrigatório) ser não abstrato.

exemplo de fim

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 de tempo de compilação ocorrerá se uma classe selada for especificada como a classe base de outra classe.

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

Observação: o sealed modificador é usado principalmente para evitar derivações não intencionais, mas também permite determinadas otimizações de tempo de execução. Em particular, como uma classe selada é conhecida por nunca ter nenhuma classe derivada, é possível transformar invocações de membro de função virtual em instâncias de classe seladas em invocações não virtuais. nota final

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

15.2.2.4 Classes estáticas

15.2.2.4.1 Geral

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 usada como um tipo e deve conter apenas membros estáticos. Somente 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 modificador or abstract . (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 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 protected, ou protected internal acessibilidade declarada.

É um erro de 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 será 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) tem permissão para fazer referência a uma classe estática se

  • O namespace_or_type_name é o T em um namespace_or_type_name da forma T.I, ou
  • O nome namespace_or_type é o T em um typeof_expression (§12.8.18) do formulário typeof(T).

Um primary_expression (§12.8) tem permissão para fazer referência a uma classe estática se

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

Observação: por exemplo, é um erro 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 as, uma sizeof expressão ou uma expressão de valor padrão. nota final

15.2.3 Parâmetros de tipo

Um parâmetro de tipo é um identificador simples que denota um espaço reservado para um argumento de tipo fornecido para criar um tipo construído. Por contraste, um argumento de tipo (§8.4.2) é o tipo que é substituído pelo parâmetro de tipo 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. Portanto, ele não pode ter o mesmo nome que outro parâmetro de tipo dessa classe ou um membro declarado nessa classe. Um parâmetro de tipo não pode ter o mesmo nome que o próprio tipo.

Duas declarações de tipo genérico parciais (no mesmo programa) contribuem para o mesmo tipo genérico não associado se tiverem o mesmo nome totalmente qualificado (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 parciais de tipo devem especificar o mesmo nome para cada parâmetro de tipo, em ordem.

15.2.4 Especificação básica da classe

15.2.4.1 Geral

Uma declaração de classe pode incluir uma especificação class_base , que define a classe base direta da classe e as interfaces (§18) implementadas diretamente 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 básicas

Quando uma class_type é incluída no class_base, ela 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 a class_base listar apenas os tipos de interface, a classe base direta será considerada object. Quando uma declaração de classe parcial inclui uma especificação de classe base, essa especificação de classe base deve fazer referência ao mesmo tipo que todas as outras partes desse tipo parcial que incluem uma especificação de classe base. Se nenhuma parte de uma classe parcial incluir uma especificação de classe base, a classe base será object. Uma classe herda membros de sua classe base direta, conforme descrito em §15.3.4.

Exemplo: no código a seguir

class A {}
class B : A {}

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

exemplo de fim

Para um tipo de classe construída, incluindo um tipo aninhado declarado em uma declaração de tipo genérico (§15.3.9.7), se uma classe base for especificada na declaração de classe genérica, a classe base do tipo construído será obtida substituindo, por cada type_parameter na declaração de classe base, o 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 de fim

A classe base especificada em uma 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 conta própria (§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 de fim

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

A classe base direta de um tipo de classe não deve ser 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 deve ser usada System.Attribute como uma classe base direta ou indireta (§22.2.1).

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

Exemplo: o seguinte

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

class Z : X<Z.Y> {}

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

exemplo de fim

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[]>>, Ae object.

exemplo de fim

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 de tempo de compilação para uma classe depender de si mesma. Para a finalidade desta regra, uma classe depende diretamente de sua classe base direta (se houver) e depende diretamente da classe delimitadora mais próxima na qual ela está aninhada (se houver). Dada essa definição, o conjunto completo de classes das quais uma classe depende é o fechamento transitivo do relacionamento diretamente depende .

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á errada porque as classes dependem circularmente de si mesmas. Finalmente, o exemplo

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

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

exemplo de fim

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 é sua classe base direta e sua classe imediatamente delimitadora), mas A não depende de B (uma vez que B não é uma classe base nem uma classe delimitadora de A). Assim, o exemplo é válido.

exemplo de fim

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á com erro porque tenta derivar da classe Aselada.

exemplo de fim

15.2.4.3 Implementações de interface

Uma especificação class_base pode incluir uma lista de tipos de interface, caso em que a classe implementa os tipos de interface fornecidos. Para um tipo de classe construída, incluindo um tipo aninhado declarado em uma declaração de tipo genérico (§15.3.9.7), cada tipo de interface implementado é obtido substituindo, por cada type_parameter na interface fornecida, o 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 as mesmas interfaces base. Deve haver apenas uma implementação de cada membro de uma determinada 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, IB, e IC.

exemplo de fim

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 de uma interface declarada em uma parte diferente.

Exemplo:

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

partial class X : IComparable
{
    ...
}

exemplo de fim

As interfaces base especificadas em uma declaração de classe podem ser tipos de interface construídos (§8.4, §18.2). Uma interface base não pode ser um parâmetro de tipo por conta própria, 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 de fim

As implementações de interface são discutidas mais adiante em §18.6.

15.2.5 Restrições de parâmetro de tipo

Opcionalmente, as declarações genéricas de tipo e método podem especificar restrições de parâmetro 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 de tipo, seguido por dois-pontos e a lista de restrições para esse parâmetro de tipo. 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 os get tokens and set em um acessador de propriedade, o where token não é uma palavra-chave.

A lista de restrições fornecida 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ção classde tipo de referência, a restrição structde tipo de valor, a restriçãonotnull não nula ou a restrição unmanaged de tipo 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 de tipo 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 de delegado, tipos de matriz e parâmetros de tipo conhecidos como um tipo de referência (conforme definido abaixo) atendem a 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 nulidade para o argumento type:

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

A nulidade do argumento de tipo não precisa corresponder à nulidade do parâmetro de tipo. O compilador poderá emitir um aviso se a nulidade do parâmetro de tipo não corresponder à nulidade do argumento de tipo.

Observação: para especificar que um argumento de tipo é 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? em toda a declaração genérica para indicar o tipo de referência anulável correspondente para o argumento de tipo. nota final

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

Para um parâmetro T de tipo quando o argumento de tipo é 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 nulidade de um argumento de tipo afeta a nulidade de uma declaração de seu parâmetro de tipo:

public class C
{
}

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

    }
}

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

        int number = 5;
        int? missing = null;

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

Quando o argumento de tipo é 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 é o mesmo tipo anulável.

exemplo de fim

A restrição not null especifica que um argumento de tipo 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 o 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 de tipo deve ser um tipo de valor não anulável. Todos os tipos de struct não anuláveis, tipos de enumeração e parâmetros de tipo com a restrição de tipo de valor atendem a essa restrição. Observe que, embora classificado como um tipo de valor, um tipo de valor anulável (§8.3.12) não atende à 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.

Observação: 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

Como unmanaged não é uma palavra-chave, em primary_constraint a restrição não gerenciada é sempre sintaticamente ambígua com class_type. Por motivos de compatibilidade, se uma pesquisa de nome (§12.8.4) do nome unmanaged for bem-sucedida, ela será tratada como um class_type. Caso contrário, ele será tratado 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 de tipo 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 atendem a 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 especificará um "tipo base" mínimo que cada argumento de tipo usado para esse parâmetro de tipo deve dar suporte. Sempre que um tipo construído ou método genérico é usado, o argumento de tipo é verificado em relação às restrições no parâmetro de tipo em tempo de compilação. O argumento de tipo fornecido deve satisfazer as condições descritas em §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 tipos: 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 uma restrição interface_type deve satisfazer as seguintes regras:

  • O tipo deve ser um tipo de interface.
  • Um tipo não deve ser especificado mais de uma vez em uma 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 associado como parte de um tipo construído e pode envolver o tipo que está sendo declarado.

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

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

  • O tipo deve ser um parâmetro de tipo.
  • Um tipo não deve ser especificado mais de uma vez em uma 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 de tipo for usado como uma restrição para o parâmetro S de tipo, entãoS depende.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 S depende deU.

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

Quaisquer restrições devem ser consistentes entre os parâmetros de tipo dependente. Se o parâmetro S de tipo depender do parâmetro T de tipo, então:

  • T não deve ter a restrição de tipo de valor. Caso contrário, é efetivamente selado, portantoS, T 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, não T deverá ter uma restrição class_type .
  • Se S tiver uma restrição A class_type e T tiver uma restrição B class_type, 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.
  • Se S também depender do parâmetro U de tipo e U tiver uma restrição A class_type e T tiver uma restrição B class_type, 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 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 de um parâmetro de tipo incluir uma restrição de construtor (que tem o formato 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 de tempo de compilação para type_parameter_constraints ter um primary_constraint ou unmanaged struct também ter um constructor_constraint.

Exemplo: Veja a seguir 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á errado 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 de fim

A eliminação dinâmica de um tipo C é Cₓ construída da seguinte maneira:

  • Se C for um tipo Outer.Inner aninhado, então Cₓ é um tipo Outerₓ.Innerₓaninhado.
  • Se C Cₓé 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 for 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 base efetiva de um parâmetro T de tipo é definida da seguinte maneira:

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 T de que é um tipo de enumeração, R contém System.Enum.
  • Para cada restrição T de que é um tipo delegado, R contém seu apagamento dinâmico.
  • 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 T é o tipo mais abrangente (§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 consistência garantem que o tipo mais englobado exista.

Se o parâmetro de tipo for um parâmetro de tipo de método cujas restrições são herdadas do método base, a classe base efetiva será 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 maneira:

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

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

Os valores de um tipo de parâmetro de tipo restrito podem ser usados para acessar os membros da instância implícitos 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 IPrintable podem ser invocados diretamente em x porque T é restrito a sempre implementar IPrintable.

exemplo de fim

Quando uma declaração de tipo genérica parcial inclui restrições, as restrições devem concordar com todas as outras partes que incluem 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 de 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 irrestritos.

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 de fim

15.2.6 Corpo da 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 um tipo de classe, struct ou 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, struct, ou interface.

Cada parte de uma declaração de tipo parcial deve incluir um partial modificador e deve ser declarada no mesmo namespace ou contendo 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 de tais 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 nulidade de quaisquer argumentos de tipo especificados.

Todas as partes de um tipo parcial devem ser compiladas em conjunto, de modo a que possam 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 de conteúdo também é declarado usando partial e cada parte do tipo aninhado é declarada em uma parte diferente do tipo de contenção.

Exemplo: a classe parcial a seguir é implementada em duas partes, que residem em unidades de compilação diferentes. A primeira parte é gerada por máquina 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 de fim

A manipulação de atributos especificados no tipo ou nos parâmetros de tipo de diferentes partes de uma declaração parcial é discutida no §22.3.

15.3 Membros da classe

15.3.1 Geral

Os membros de uma classe consistem nos membros introduzidos por seus class_member_declaratione 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 executados pela classe (§15.6).
  • Propriedades, que definem características nomeadas e as ações associadas à leitura e gravação dessas características (§15.7).
  • Eventos, que definem notificações que podem ser geradas pela classe (§15.8).
  • Indexadores, que permitem que instâncias da classe sejam indexadas da mesma maneira (sintaticamente) que as 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 descartadas permanentemente (§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_parametere os class_member_declarations imediatamente contidos pelo class_declaration introduzem novos membros nesse espaço de declaração. As seguintes regras se aplicam a class_member_declarations:

  • Construtores de instância, finalizadores e construtores estáticos devem ter o mesmo nome que a classe delimitadora imediata. 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 ser diferente dos nomes de todos os membros não pertencentes ao tipo declarados na mesma classe. Se duas ou mais declarações de tipo compartilharem o mesmo nome totalmente qualificado, as declarações deverão ter o partial modificador (§15.2.7) e essas declarações se combinam para definir um único tipo.

Observação: 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 ser diferente dos nomes de todos os outros membros declarados na mesma classe.

  • O nome de um método deve ser diferente dos nomes de todos os outros não-métodos 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 difiram apenas por in, out, e ref.

  • A assinatura de um construtor de instância deve ser diferente 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 ser diferente das assinaturas de todos os outros indexadores declarados na mesma classe.

  • A assinatura de um operador deve ser diferente da assinatura de todos os outros operadores declarados na mesma categoria.

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 de 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 compartilham o mesmo espaço de declaração (§7.3) e o escopo de cada membro (§7.7) se estende aos corpos de todas as partes. O domínio de acessibilidade de qualquer membro sempre inclui todas as partes do tipo delimitador; Um membro privado declarado em uma parte é livremente acessível a partir de outra parte. É 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 de fim

A ordem de inicialização do campo pode ser significativa no código C# e algumas garantias são fornecidas, conforme definido em §15.5.6.1. Caso contrário, a ordenação de membros dentro de um tipo raramente é significativa, mas pode ser significativa ao fazer interface com outras linguagens e ambientes. Nesses casos, a ordenação de 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 de tipo 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: o 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 de fim

15.3.3 Membros de tipos construídos

Os membros não herdados de um tipo construído são obtidos substituindo, por cada type_parameter na declaração de membro, o type_argument correspondente do tipo construído. O processo de substituição é baseado no significado semântico das declarações de tipo e não é simplesmente 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 Gen de classe genérica é "matriz bidimensional de T", portanto, o tipo do membro a no tipo construído acima é "matriz bidimensional de matriz unidimensional de int", ou int[,][].a

exemplo de fim

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

Todos os membros de uma classe genérica podem usar parâmetros de tipo de qualquer classe delimitadora, diretamente ou como parte de um tipo construído. Quando um tipo construído fechado específico (§8.4.3) é usado em tempo de execução, cada uso de um parâmetro de tipo é substituído 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 de fim

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 aspectos 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 Acomo

  • Uma classe derivada estende sua classe base direta. Uma classe derivada pode adicionar novos membros aos que ela herda, mas ela 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 o torna inacessível diretamente por meio 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 existe uma conversão implícita (§10.2.8) 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 as 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 por meio da qual esse membro de 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 por cada ocorrência dos parâmetros de tipo correspondentes no base_class_specification. Esses membros, por sua vez, são transformados substituindo-se, por 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 membro público G(string s) int 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 . Esse membro herdado é determinado primeiro determinando o tipo B<int[]> de classe base de D<int> substituindo por T na especificação B<T[]>da classe int 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 de fim

15.3.5 O novo modificador

Um class_member_declaration tem permissão para declarar um membro com o mesmo nome ou assinatura de um membro herdado. Quando isso ocorre, diz-se que o membro da classe derivada oculta o membro da classe base. Consulte §7.7.2.3 para obter 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á esteja oculto.M Ocultar implicitamente um membro herdado não é considerado um erro, mas faz com que o compilador emita um aviso, a menos que a declaração do membro da classe derivada inclua um new modificador 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 class_member_declaration pode ter qualquer um dos tipos permitidos de acessibilidade declarada (§7.5.2): public, protected internal, protected, private protected, , internalou private. Exceto para as protected internal combinações e private protected , é um erro de 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 constituintes

Os tipos 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 método, indexador, operador ou construtor de instância. Os tipos constituintes de um membro devem ser pelo menos tão acessíveis quanto 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 de 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 em um member_access (§12.8.7) do formulário E.M, E deve denotar um tipo que tem um membro M. É um erro de 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 construído fechado distinto (§8.4.3) tem seu próprio conjunto de campos estáticos, independentemente do número de instâncias do tipo construído fechado.
  • 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 de tempo de compilação referir-se a isso em tal membro de função.

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

  • Quando um membro M de instância é referenciado em um member_access (§12.8.7) do formulário E.M, E deve denotar uma instância de um tipo que tem um membro M. É um erro de tempo de associaçã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 de 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 de função de instância, um simple_name (§12.8.4) pode ser usado para acessar membros de 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 de fim

15.3.9 Tipos aninhados

15.3.9.1 Geral

Um tipo declarado em uma classe ou struct é chamado de tipo aninhado. Um tipo declarado em 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 de class A, e class A é um tipo não aninhado porque é declarado dentro de uma unidade de compilação.

exemplo de fim

15.3.9.2 Nome totalmente qualificado

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

15.3.9.3 Acessibilidade declarada

Os tipos não aninhados podem ter public ou internal declarar 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 struct:

  • Um tipo aninhado declarado em uma classe pode ter qualquer um dos tipos permitidos de acessibilidade declarada e, como outros membros da classe, o private padrão é a acessibilidade declarada.
  • Um tipo aninhado declarado em um struct pode ter qualquer uma das três formas de acessibilidade declarada (public, internal, ou private) e, como outros membros do struct, o private padrão é a acessibilidade 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 de fim

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 de fim

15.3.9.5 Este acesso

Um tipo aninhado e seu tipo que o contém não têm uma relação especial em relação a 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 o contém. Nos casos em que um tipo aninhado precisa de acesso aos membros da instância de seu tipo contém, o acesso pode ser fornecido fornecendo o this para a instância do tipo contendo 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 essa técnica. Uma instância de C cria uma instância de Nested, e passa seu próprio construtor this to para Nested, a fim de fornecer acesso subsequente aos Cmembros da instância de .

exemplo de fim

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

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

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 de fim

Um tipo aninhado também pode acessar membros protegidos definidos em um tipo base de seu tipo contém.

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 de fim

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 delimitadora podem ser usados dentro dos tipos aninhados. Uma declaração de tipo aninhado 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érico. Ao escrever uma referência a um tipo aninhado em um tipo genérico, o tipo construído que o contém, incluindo seus argumentos de tipo, deve ser nomeado. No entanto, de dentro da classe externa, o tipo aninhado pode ser usado sem qualificação; O tipo de instância da classe externa pode ser usado implicitamente ao construir o tipo aninhado.

Exemplo: O seguinte mostra 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 de fim

Embora seja um estilo de programação incorreto, um parâmetro de tipo em um tipo aninhado pode ocultar um membro ou parâmetro de tipo 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 de fim

15.3.10 Nomes de membros reservados

15.3.10.1 Geral

Para facilitar a implementação subjacente do tempo de execução do C#, 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 da declaração de membro, seu nome e seu tipo (§15.3.10.2, §15.3.10.3, §15.3.10.4). É um erro de 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, portanto, não participam da pesquisa de membros. No entanto, as assinaturas de método reservado associadas a uma declaração participam da herança (§15.3.4) e podem ser ocultadas com o new modificador (§15.3.5).

Nota: A reserva desses nomes serve a 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 acesso ao recurso de linguagem C#.
  2. Para permitir que outras linguagens interoperem usando um identificador comum como um nome de método para obter ou definir 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 do C#.

nota final

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

Determinados nomes são reservados para uso como nomes de método de operador (§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 deriva B A e oculta essas duas assinaturas reservadas. O exemplo produz a saída:

123
123
456

exemplo de fim

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 para 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 de membros reservados para finalizadores

Para uma classe que contém um finalizador (§15.13), a seguinte assinatura é reservada:

void Finalize();

15.3.10.6 Nomes de métodos reservados para operadores

Os nomes de método a seguir são reservados. Embora muitos tenham operadores correspondentes nesta especificação, alguns são reservados para uso por versões futuras, enquanto outros são reservados para interoperabilidade com outras linguagens.

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 sufixo)
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 (ampliação)
op_Increment ++ (prefixo e sufixo)
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 se aplicam 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 o mesmo modificador aparecer várias vezes em uma declaração constante.

O tipo de um constant_declaration especifica o tipo dos 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 fornece o valor do membro.

O tipo especificado em uma declaração constante deve ser sbyte, byte, short, ushortuintlongulongcharfloatdoubleboolintstringdecimal, , enum_type ou reference_type. Cada constant_expression deve produzir um valor do tipo de destino ou de um tipo que pode ser convertido no tipo de destino por uma conversão implícita (§10.2).

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

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

Uma constante pode participar de um 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

Observação: conforme descrito em §12.23, um constant_expression é uma expressão que pode ser totalmente avaliada em tempo de compilação. Como a única maneira de criar um valor não nulo de um reference_type diferente é aplicar o new operador, e como o new operador não é permitido em um constant_expression, o único valor possível para constantes de reference_types diferente de string é null.string 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.

Observação: a semântica de controle de const versão de e difere readonly (§15.5.3.3). nota final

Uma declaração de 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 de fim

As constantes podem depender de outras constantes dentro do mesmo programa, desde que as dependências não sejam de natureza circular. O compilador organiza automaticamente a avaliação das declarações constantes na ordem apropriada.

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

O compilador primeiro avalia A.Y, depois avalia B.Z, e finalmente avalia A.X, produzindo os valores 10, 11, e 12.

exemplo de fim

Declarações constantes podem depender de constantes de outros programas, mas essas 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 depender simultaneamente de A.Y. exemplo de fim

15.5 Campos

15.5.1 Geral

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) está disponível apenas 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 se aplicam a todos os membros declarados pelo field_declaration. É um erro o mesmo modificador aparecer várias vezes em um field_declaration.

O tipo de um field_declaration especifica o tipo dos membros introduzidos pela declaração. O tipo é seguido por uma lista de variable_declarators, cada uma das 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 fornece o valor inicial desse membro.

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

O valor de um campo é obtido em uma expressão usando 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 decremento de sufixo (§12.8.16) e operadores de incremento e decremento 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 de fim

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 chamados de variáveis estáticas e variáveis de instância, respectivamente.

Conforme explicado em §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 tipo construído fechado.

15.5.3 Campos somente leitura

15.5.3.1 Geral

Quando um field_declaration inclui um readonly modificador, os campos introduzidos pela declaração são campos somente leitura. As atribuições diretas a 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. Esses também são 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, Greene 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á-los static readonly tem o mesmo efeito.

exemplo de fim

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 controle de versão binárias 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 valor pela Console.WriteLine instrução não é conhecida em tempo de compilação, mas é obtida em tempo de execução. Portanto, se o valor de X for alterado e Program1 recompilado, a Console.WriteLine instrução produzirá o novo valor mesmo que Program2 não seja recompilado. No entanto, se tivesse X sido uma constante, o valor de teria sido obtido no momento Program2 em que foi compilado e não seria afetado X por mudanças em Program1 até Program2 que seja recompilado.

exemplo de fim

15.5.4 Campos voláteis

Quando um field_declaration inclui um volatile modificador, os campos introduzidos por essa declaração são campos voláteis. Para campos não voláteis, as técnicas de otimização que reordenam instruções podem levar a resultados inesperados e imprevisíveis em programas multithread que acessam campos sem sincronização, como a fornecida 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 qualquer referência à memória que ocorra depois dela na sequência de instruções.
  • Uma gravação de um campo volátil é chamada de gravação volátil. Uma gravação volátil tem "semântica de lançamento"; ou seja, é garantido que isso aconteça após qualquer referência de memória antes da instrução de gravação na sequência de instruções.

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

  • Um reference_type.
  • Um type_parameter que é conhecido por ser um tipo de referência (§15.2.5).
  • O tipo byte, sbyte, short, ushort, intuintfloatchar, , bool, ou . System.IntPtrSystem.UIntPtr
  • Um enum_type com um enum_base tipo de byte, sbyte, short, ushort, int, ou 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. Esse 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 true, em seguida, lê o campo result. Uma vez que finished foi declarado volatile, o thread principal deve ler o valor 143 do campo result. Se o campo finished não tivesse sido declarado volatile, seria permitido que o armazenamento result ficasse visível para o thread principal após o armazenamento para finished, e, portanto, que o thread principal lesse o valor 0 do campo result. Declarar finished como um volatile campo evita essa inconsistência.

exemplo de fim

15.5.5 Inicialização de campo

O valor inicial de um campo, seja 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

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

exemplo de fim

15.5.6 Inicializadores de variáveis

15.5.6.1 Geral

As declarações de campo podem incluir variable_initializers. Para campos estáticos, os inicializadores de variáveis correspondem a instruções de atribuição que são executadas durante a inicialização da classe. Para campos de instância, os 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ático são executados e atribuições a i e s ocorrem quando os inicializadores de campo de instância são executados.

exemplo de fim

A inicialização do valor padrão descrita em §15.5.5 ocorre para todos os campos, incluindo campos que têm inicializadores de variáveis. Assim, quando uma classe é inicializada, todos os campos estáticos nessa classe são inicializados primeiro 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 inicializados primeiro com seus valores padrão e, em seguida, os inicializadores de campo de instância são executados em ordem textual. Quando há declarações de campo em várias declarações de tipo parciais 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 de 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}");
    }
}

exibe esse 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 que seus inicializadores sejam executados. Quando o inicializador for a executado, o valor de b é zero e, portanto a , é inicializado como 1. Quando o inicializador for b executado, o valor de a já 1é , e assim b é inicializado para 2.

exemplo de fim

15.5.6.2 Inicialização de campo estático

Os inicializadores de 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 por §15.5.6.1. Se um construtor estático (§15.12) existir na classe, a execução dos inicializadores de campo estático ocorrerá imediatamente antes da execução desse construtor estático. Caso contrário, os inicializadores de campo estático serã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 Yde e do Xinicializador pode ocorrer em qualquer ordem; eles só são restritos a ocorrer antes das referê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 saída deve ser:

Init B
Init A
1 1

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

exemplo de fim

15.5.6.3 Inicialização do campo de instância

Os inicializadores de 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 por §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 em §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. Portanto, é um erro de tempo de compilação fazer referência this em um inicializador de variável, pois é um erro de tempo de compilação para um inicializador de variável fazer referência a qualquer membro da 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 de variável FOR y resulta em um erro de tempo de compilação porque faz referência a um membro da instância que está sendo criada.

exemplo de fim

15.6 Métodos

15.6.1 Geral

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

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

method_modifiers
    : method_modifier* 'partial'?
    ;

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

ref_method_modifiers
    : ref_method_modifier*
    ;

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

method_modifier
    : ref_method_modifier
    | 'async'
    ;

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

return_type
    : ref_return_type
    | 'void'
    ;

ref_return_type
    : type
    ;

member_name
    : identifier
    | interface_type '.' identifier
    ;

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

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

Notas gramaticais:

  • unsafe_modifier (§23.2) está disponível apenas em código não seguro (§23).
  • Ao reconhecer um method_body se forem aplicáveis as alternativas null_conditional_invocation_expression e de expressão , deve ser escolhida a primeira.

Nota: A sobreposição e a prioridade entre as alternativas aqui são apenas para conveniência descritiva; as regras gramaticais podem ser elaboradas para remover a sobreposição. 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 (§15.6.4), override (§15.6.5), sealed (§15.6.6), (§15.6.7abstract), extern (§15.6.8) e async (§15.15).

Uma declaração terá 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, ela 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, ,externvirtualsealedoverrideabstractprivate

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

  • Se ref estiver presente, o método será retornado por ref e retornará uma referência de variável, que é opcionalmente somente leitura;
  • Caso contrário, se return_type for void, o método não retornará nenhum valor e não retornará um valor;
  • Caso contrário, o método será retornado por valor e retornará um valor.

O 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. Somente um método returns-no-value pode incluir o partial modificador (§15.6.9). Se a declaração incluir o async modificador, return_type deverá 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_clauses opcionais especificam as restrições para os parâmetros de tipo.

Uma method_declaration genérica para uma implementação explícita de membro de interface não deve ter nenhum type_parameter_constraints_clauses; a declaração herda todas as 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 de 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 outros 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, e cada um dos tipos referenciados no parameter_list de um método, devem ser pelo menos tão acessíveis quanto o próprio método (ponto 7.5.5).

A method_body de um método retorna por valor ou retorna sem valor é 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 em um 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 um único 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_modifier s (§15.6.2.1) e os tipos deseus 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 de tipo (não o nome do parâmetro de tipo) é usada para equivalência de tipo.

O nome de um método deve ser diferente dos nomes de todos os outros não-métodos 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, out, e 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 de tipo devem ter nomes diferentes.

15.6.2 Parâmetros do método

15.6.2.1 Geral

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írgulas, dos quais apenas o último pode ser um parameter_array.

Um fixed_parameter consiste em um conjunto opcional de atributos (§22); um modificador , out, ref, ou this opcionalin; um tipo; um identificador; e um default_argument opcional. Cada fixed_parameter declara um parâmetro do tipo fornecido com o nome fornecido. 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 poderá ser combinado com o ref modificador or in , mas não com o out modificador. Os métodos de extensão são descritos em mais detalhes em §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 obrigatório. Um parâmetro obrigatório não deve aparecer após um parâmetro opcional em um parameter_list.

Um parâmetro com um refmodificador , out or this não pode ter um default_argument. Um parâmetro de entrada pode ter um default_argument. A expressão constante de um default_argument é uma das seguintes:

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

A expressão deve ser implicitamente conversível por uma identidade ou conversão 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 de 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), o compilador deverá fornecer um aviso, pois 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 fornecido 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 fornecido 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 detalhadamente em §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 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 para M, i é um parâmetro obrigatórioref, d é um parâmetro de valor obrigatório, b, s, o e t são parâmetros de valor opcionais e a é uma matriz de parâmetros.

exemplo de fim

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 nesse 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 desse espaço de declaração. É um erro que dois membros de um espaço de declaração de método tenham 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. Dentro do bloco de um método, os parâmetros podem ser referenciados por seus identificadores em expressões simple_name (§12.8.4).

Existem os seguintes tipos de parâmetros:

Observação: conforme descrito no §7.6, os inmodificadores , oute ref 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 invocação do método.

Para regras de atribuição definida, consulte §9.2.5.

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

Um método tem permissão para atribuir novos valores a um parâmetro de valor. Essas atribuições afetam apenas o local de armazenamento local representado pelo parâmetro value — elas não têm efeito sobre o argumento real fornecido na invocação do método.

15.6.2.3 Parâmetros por referência

15.6.2.3.1 Geral

Os parâmetros de entrada, saída e referência são parâmetros de referência. Um parâmetro por referência é uma variável de referência local (§9.7); o referente inicial é obtido 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 em uma invocação de método deve consistir na palavra-chave correspondente, in, ref, ou out, seguida por um variable_reference (§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.

Parâmetros por referência não são permitidos em funções declaradas como um 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 invocação do método ou uma criada pela implementação (§12.6.2.3) na invocação do método. Para regras de atribuição definida, consulte §9.2.8.

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

Observação: o objetivo principal dos parâmetros de entrada é a eficiência. Quando o tipo de um parâmetro de método é um struct grande (em termos de requisitos de memória), é útil 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 definida, consulte §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 em Main, x representa i e y representa j. Assim, a invocação tem o efeito de trocar os valores de i e j.

exemplo de fim

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 a s For Both a e b. Assim, para essa chamada, os nomes s, , e b todos se referem ao mesmo local de armazenamento, e as três atribuições modificam o campo sde ainstância.

exemplo de fim

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 definida, consulte §9.2.7.

Um método declarado como um 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 de fim

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, este deve ser o último parâmetro da lista e deve ser do tipo matriz unidimensional.

Exemplo: Os tipos string[] e string[][] podem ser usados como o tipo de uma matriz de parâmetros, mas o tipo string[,] não. exemplo de fim

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

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

  • O argumento fornecido 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. Nesse caso, a matriz de parâmetros atua precisamente como um parâmetro de valor.
  • Como alternativa, a invocação pode especificar zero ou mais argumentos para a matriz de parâmetros, em que cada argumento é uma expressão 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 da matriz com os valores de argumento fornecidos e usa a instância da matriz recém-criada como o argumento real.

Exceto por 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 F simplesmente passa a matriz arr como um parâmetro de 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 F cria um elemento int[] zero e passa essa instância como um parâmetro de valor. A segunda e a terceira invocações são precisamente equivalentes à escrita:

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

exemplo de fim

Ao executar a resolução de sobrecarga, um método com uma matriz de parâmetros pode ser aplicável, em sua forma normal ou em sua forma expandida (§12.6.4.2). A forma expandida de um método só estará disponível se a forma normal do método não for aplicável e somente se um método aplicável com a mesma assinatura que a forma expandida ainda não estiver 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 as invocações de primeiro e terceiro método selecionam os métodos regulares. Quando uma classe declara um método com uma matriz de parâmetros, não é incomum incluir também algumas das formas expandidas como métodos regulares. Ao fazer isso, é possível evitar a alocação de uma instância de array que ocorre quando uma forma expandida de um método com um array de parâmetros é invocada.

exemplo de fim

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 F(new string[] { null }) passa uma matriz contendo uma única referência nula.

exemplo de fim

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 an object[] é implicitamente conversível em tipo object. A ambigüidade não apresenta nenhum problema, no entanto, uma vez que pode ser resolvida inserindo um molde, 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 na ú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 F não é aplicável porque não existe nenhuma conversão implícita do tipo de argumento para o tipo de parâmetro (o tipo object não pode ser convertido implicitamente em tipo object[]). No entanto, a forma expandida de é aplicável, portanto, é selecionada pela resolução de F sobrecarga. Como resultado, um elemento object[] único é criado pela invocação e o elemento único da matriz é inicializado com o valor do argumento fornecido (que por sua vez é uma referência a um object[]).

exemplo de fim

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 é considerado um método estático. Quando nenhum static modificador está presente, o método é considerado um método de instância.

Um método estático não opera em uma instância específica e é um erro de tempo de compilação para se referir this 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 adiante em §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 é considerado um método virtual. Quando nenhum modificador virtual está presente, o método é considerado 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 ele é declarado ou em 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 substituição desse método (§15.6.5).

Em uma invocação 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 chamado 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:

  • No tempo de associação, a resolução de sobrecarga é aplicada a C, , e A, para selecionar um método M específico do conjunto de métodos declarados e herdados por CN. Isso é descrito em §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 em relação 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 contiver a declaração virtual de introdução de M, então esta é a implementação mais derivada de M em relação a R.
  • Caso contrário, se R contém uma substituição de M, então esta é a implementação mais derivada de M em relação a R.
  • Caso contrário, a implementação mais derivada de M em relação a R é a mesma que a implementação mais derivada de M em relação à classe 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 apresenta 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 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), e 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 de fim

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 and 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 por C, e não é possível D substituir o método introduzido por 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 por meio de D um tipo menos derivado no qual o método não está oculto.

exemplo de fim

15.6.5 Substituir métodos

Quando uma declaração de método de instância inclui um override modificador, o método é considerado 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 classe C, o método 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 seja localizado com a mesma assinatura M que após a substituição de argumentos de tipo. Para fins de localização do método base substituído, um método é considerado acessível se for public, se for protected, se for protected internal, ou se for internal ou private protected e 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 base substituído pode ser localizado conforme descrito acima.
  • Existe exatamente um desses métodos básicos substituídos. Essa restrição terá efeito somente 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 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 deverá 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 de fim

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 desabilita 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 PrintFields método declarado 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 de fim

Somente incluindo um override modificador 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 em 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 em B oculta o método em A, e um aviso é relatado porque a declaração não inclui um novo modificador.

exemplo de fim

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 em B oculta o método virtual F herdado de A. Como o novo F in B tem acesso privado, seu escopo inclui apenas o corpo da classe e B não se estende a C. Portanto, a declaração de F in C tem permissão para substituir o F herdado de A.

exemplo de fim

15.6.6 Métodos selados

Quando uma declaração de método de instância inclui um sealed modificador, esse método é considerado um método selado. Um método selado substitui um método virtual herdado com a mesma assinatura. Um método selado também deve ser marcado com o override modificador. 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 tem. BO uso do sealed modificador impede C a substituição Fadicional de .

exemplo de fim

15.6.7 Métodos abstratos

Quando uma declaração de método de instância inclui um abstract modificador, esse método é considerado 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 abstrata 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 necessárias para 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.

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 se pintar. O Paint método é abstrato porque não há 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 de fim

É um erro de 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 de tempo de compilação é relatado para a base.F() invocação porque faz referência a um método abstrato.

exemplo de fim

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 de fim

15.6.8 Métodos externos

Quando uma declaração de método inclui um extern modificador, o método é considerado 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 a ligação a um método externo é alcançada é 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 de fim

15.6.9 Métodos parciais

Quando uma declaração de método inclui um partial modificador, esse método é considerado 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 várias restrições.

Os métodos parciais podem ser definidos em uma parte de uma declaração de tipo e implementados em outra. 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; eles são implicitamente privados. Seu tipo de retorno deve ser void, e seus parâmetros não devem ser parâmetros de saída. A parcial do identificador é reconhecida como uma palavra-chave contextual (§6.4.4) em uma declaração de método somente se aparecer imediatamente antes da void palavra-chave. Um método parcial não pode implementar explicitamente métodos de interface.

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

  • As declarações devem ter os mesmos modificadores (embora não necessariamente na 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ódulo em nomes de parâmetros de tipo).
  • Os parâmetros de tipo correspondentes nas declarações devem ter as mesmas restrições (diferenças de módulo 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.

Somente um método parcial de definição participa da resolução de sobrecarga. Assim, independentemente de uma declaração de implementação ser fornecida ou não, as expressões de chamada podem ser resolvidas para 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, essas instruçõ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, definição e implementação de declarações parciais de método não requer que os nomes dos parâmetros correspondam. Isso pode produzir um comportamento surpreendente, embora bem definido, quando argumentos nomeados (§12.6.2.1) são usados. Por exemplo, dada a definição da declaração parcial do método para M em um arquivo e a implementação da declaração parcial do método 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álido , pois a invocação usa o nome do argumento da declaração de método parcial de implementação e não a definição.

nota final

Se nenhuma parte de uma declaração de tipo parcial contiver uma declaração de implementação para um determinado método parcial, qualquer instrução de expressão que a invoque será simplesmente removida da declaração de tipo combinada. Portanto, 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á membro da declaração de tipo combinada.

Se existir uma declaração de implementação para um determinado método parcial, as invocações dos métodos parciais serão mantidas. O método parcial dá origem a uma declaração de método semelhante à declaração de método parcial de implementação, exceto para o seguinte:

  • 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 apresentada 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 de M (§12.8.17.6).

  • É um erro de tempo de compilação para se referir a M 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 M não afetam o estado de atribuição definido (§9.4), o que pode levar a erros de tempo de compilação.

  • M não pode ser o ponto de entrada para um aplicativo (§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 nenhuma outra parte, as declarações de método parcial de definição 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;
    }
}

Suponha que outra parte seja fornecida, 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 é considerado 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 maneira:

  • Só pode ser um parâmetro de entrada se tiver um tipo de valor
  • Ele só pode ser um parâmetro de referência se tiver um tipo de valor ou tiver um tipo genérico restrito a struct
  • Não deve ser um tipo de ponteiro.

Exemplo: Veja a seguir 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 de fim

Um método de extensão é um método estático regular. Além disso, quando sua classe estática delimitadora está 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 receptora 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 comuns:

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 de fim

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 um 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 uma expressão body, 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 retornos por valor (§15.6.1), cada instruçã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 instrução return no corpo desse método deve especificar uma expressão cujo tipo é o do tipo de retorno efetivo e tem um ref-safe-context de caller-context (§9.7.2).

Para os métodos returns-by-value e returns-by-ref, o ponto final do corpo do método não deve ser alcançável. Em outras palavras, o controle não pode fluir do final 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 para fora do final do corpo do método. Os G métodos and 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 return.

exemplo de fim

15.7 Propriedades

15.7.1 Geral

Uma propriedade é um membro que fornece acesso a uma característica de um objeto ou de uma 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 dos campos — ambos são membros nomeados com tipos associados e a sintaxe para acessar campos e propriedades é a mesma. No entanto, diferentemente 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 os valores forem lidos ou gravados. As propriedades, portanto, fornecem um mecanismo para associar ações à leitura e gravação das características de um objeto ou classe; além disso, permitem que tais características sejam calculadas.

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) está disponível apenas em código não seguro (§23).

Existem dois tipos de property_declaration:

  • O primeiro declara uma propriedade sem valor de referência. Seu valor tem tipo type. Esse tipo de propriedade pode ser legível e/ou gravável.
  • O segundo declara uma propriedade com valor de referência. Seu valor é um variable_reference (§9.5), que pode ser readonly, para uma variável do tipo tipo. Esse tipo de propriedade só é 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) em relação 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 um bem deve ser pelo menos tão acessível quanto o próprio bem (§7.5.5).

Um property_body pode consistir em um corpo de declaração ou em um corpo de expressão. Em um corpo de instrução, accessor_declarations, que deve ser colocado entre tokens "{" e "}", declare 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ã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 propriedades somente leitura em que o resultado do acessador get é fornecido por uma única expressão.

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

Um ref_property_body pode consistir em um corpo de declaração ou em um corpo de expressão. Em um corpo de instrução, um get_accessor_declaration declara o acessador get (§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_reference V e um ponto-e-vírgula é exatamente equivalente ao corpo { get { return ref V; } }da instruçã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 inargumento , out, ou ref a menos que a propriedade seja valorada por referência e, portanto, retorne uma referência de variável (§9.7). nota final

Quando uma declaração de propriedade inclui um extern modificador, a propriedade é considerada uma propriedade externa. Como uma declaração de propriedade externa não fornece nenhuma implementação real, cada um dos accessor_bodyem 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, a propriedade é considerada 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 de tempo de compilação a ser consultado this nos acessadores de uma propriedade estática.

Uma propriedade de instância é 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 adiante em §15.3.8.

15.7.3 Acessadores

Observação: essa cláusula se aplica a propriedades (§15.7) e indexadores (§15.9). A cláusula é escrita em termos de propriedades, ao ler para indexadores, substitua indexador/indexadores por propriedade/propriedades e consulte a lista de diferenças entre propriedades e indexadores fornecida em §15.9.2. nota final

Os accessor_declarations de uma propriedade especificam as instruções executáveis associadas à gravação 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 get ou set, seguido por um accessor_body.

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

O uso de accessor_modifiers é regido pelas seguintes restrições:

  • Um accessor_modifier não deve ser usado em uma interface ou em uma implementação explícita de membro da interface.
  • Para uma propriedade ou indexador que não tem modificador override , um accessor_modifier só será permitido se a propriedade ou o indexador tiver um acessador get e set e, em seguida, será permitido apenas em um desses acessadores.
  • Para uma propriedade ou indexador que inclui 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 o indexador tiver uma acessibilidade declarada de , a acessibilidade declarada por accessor_modifier poderá ser private protected, protected internal, internal, protected, ou private. public
    • Se a propriedade ou o indexador tiver uma acessibilidade declarada de , a acessibilidade declarada por accessor_modifier poderá ser private protected, protected private, internal, protected, ou private. protected internal
    • Se a propriedade ou o indexador tiver uma acessibilidade declarada de ou , a acessibilidade declarada internal por accessor_modifier deverá ser ou private protected private.protected
    • Se a propriedade ou o indexador tiver uma acessibilidade declarada de , a acessibilidade declarada private protectedpor accessor_modifier será private.
    • Se a propriedade ou o indexador tiver uma acessibilidade declarada de private, nenhum accessor_modifier poderá ser usado.

Para abstract e extern propriedades não valorizadas por ref, qualquer accessor_body para cada acessador especificado é simplesmente um ponto-e-vírgula. Uma propriedade não abstrata e 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 acessador get. Para os acessadores de qualquer outra propriedade não abstrata e não externa, o accessor_body é:

  • um bloco que especifica as instruções a serem executadas quando o acessador correspondente é invocado; 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 quando o acessador correspondente é invocado.

Para abstract e extern ref-valued properties, 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; ou
  • um corpo de => expressão, que consiste em seguido por ref, um variable_reference e um ponto e vírgula. A referência de variável é avaliada quando o acessador get é invocado.

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

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

Um acessador get para uma propriedade com valor de ref corresponde a um método sem parâmetros com um valor retornado de um variable_reference a uma variável do tipo de propriedade. Quando essa propriedade é referenciada em uma expressão, seu acessador get é invocado para calcular o valor variable_reference da propriedade. Essa referência de variável, como qualquer outra, é usada para ler ou, para variable_references não somente leitura, gravar 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 de fim

O corpo de um acessador get para uma propriedade com valor de referência deve estar em conformidade com as regras para métodos com valor de referência descritas em §15.6.11.

Um acessador set 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 set é sempre nomeado value. Quando uma propriedade é referenciada como o destino de uma atribuição (§12.21) ou como o operando de ++ ou –- (§12.8.16, §12.9.6), o acessador set é invocado com um argumento que fornece o novo valor (§12.21.2). O corpo de um acessador de conjunto deve estar em conformidade com as regras para void métodos descritos em §15.6.11. Em particular, as instruções return no corpo do acessador set não têm permissão para especificar uma expressão. Como um acessador set tem implicitamente um parâmetro chamado value, é um erro de tempo de compilação para uma variável local ou declaração constante em um acessador set ter esse nome.

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

  • Uma propriedade que inclui um acessador get e um acessador set é considerada uma propriedade de leitura/gravação.
  • Uma propriedade que tem apenas um acessador get é considerada uma propriedade somente leitura. É um erro de tempo de compilação que uma propriedade somente leitura seja o destino de uma atribuição.
  • Uma propriedade que tem apenas um acessador definido é considerada uma propriedade somente gravação. Exceto como 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 e -- operadores de pré e pós-fixos ++ e de atribuição composta não podem ser aplicados a propriedades somente gravação, pois esses operadores leem o valor antigo de seu operando antes de gravar 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 campo armazenado no caption privado. O acessador set verifica se o novo valor é diferente do valor atual e, em caso afirmativo, armazena o novo valor e redesenha 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 set modifica esse private campo e, em seguida, executa todas as ações adicionais necessárias para atualizar totalmente o estado do objeto. Dada a Button classe acima, o seguinte é um exemplo de uso da Caption propriedade:

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

Aqui, o acessador set é invocado atribuindo um valor à propriedade e o acessador get é invocado referenciando a propriedade em uma expressão.

exemplo de fim

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 outra 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 de fim

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 à gravação.

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, nas falas

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 de tempo de compilação seja relatado, pois a propriedade somente P leitura em B oculta a propriedade somente P gravação em A. Observe, no entanto, que uma conversão pode ser usada para acessar a propriedade oculta P .

exemplo de fim

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, for mais conveniente armazenar o local como internamente Point , a alteração poderá 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 e x y em vez disso fossem public readonly campos, teria sido impossível fazer tal mudança na Label classe.

exemplo de fim

Nota: Expor o estado por meio 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 pelo código real dos acessadores. Esse 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 acessador get é conceitualmente equivalente à leitura do valor de um campo, é considerado um estilo de programação ruim que os acessadores get tenham 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 acessadores get não significa que os acessadores get sempre devam ser gravados simplesmente para retornar valores armazenados em campos. De fato, os acessadores get geralmente calculam o valor de uma propriedade acessando vários campos ou invocando métodos. No entanto, um acessador get projetado corretamente não executa nenhuma ação que cause alterações observáveis no estado do objeto.

exemplo de fim

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 Console classe pode atrasar sua inicialização até que eles sejam realmente usados. Por exemplo, ao fazer referência Out à propriedade pela primeira vez, 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 and Error , nenhum objeto será criado para esses dispositivos.

exemplo de fim

15.7.4 Propriedades implementadas automaticamente

Uma propriedade implementada automaticamente (ou propriedade automática, para abreviar) é uma propriedade não abstrata, não externa e não valorizada por ref com accessor_bodys somente ponto-e-vírgula. As propriedades automáticas devem ter um acessador get e, opcionalmente, podem 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 gravado somente por meio dos acessadores de propriedade implementados automaticamente, mesmo dentro do tipo que o contém. Se a propriedade auto não tiver um acessador definido, o campo de suporte será 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 delimitadora. Essa atribuição é atribuída diretamente ao campo de suporte somente leitura da propriedade.

Opcionalmente, uma propriedade auto pode ter um property_initializer, que é aplicado diretamente ao campo de suporte 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 de fim

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 ao campo somente leitura são válidas, pois ocorrem dentro do construtor.

exemplo de fim

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 no atributo NonSerialized field-targeted sendo aplicado ao campo de suporte gerado pelo compilador, como se o código tivesse sido escrito da seguinte maneira:

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

exemplo de fim

15.7.5 Acessibilidade

Se um acessador tiver um accessor_modifier, o domínio de acessibilidade (§7.5.3) do acessador será determinado usando a acessibilidade declarada do accessor_modifier. Se um acessador não tiver um accessor_modifier, o domínio de acessibilidade do acessador será determinado a partir da acessibilidade declarada da propriedade ou do 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 no indexador sempre determinam a qual propriedade ou indexador está associado, independentemente do contexto do acesso.

Depois que uma determinada propriedade sem valor de ref ou um indexador sem valor de ref tiver sido selecionado, os domínios de acessibilidade dos acessadores específicos envolvidos serão usados para determinar se esse uso é válido:

  • Se o uso for como um valor (§12.2.2), o acessador get deverá existir e ser acessível.
  • Se o uso for como o destino de uma atribuição simples (§12.21.2), o acessador set deverá existir e estar acessível.
  • Se o uso for como o destino da atribuição composta (§12.21.4) ou como o destino dos ++ operadores or -- (§12.8.16, §12.9.6), os acessadores get e o acessador set deverão existir e ser acessíveis.

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

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 de fim

Depois que uma propriedade específica com valor de referência ou um indexador com valor de referência tiver sido selecionado; 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 envolvido é usado para determinar se esse uso é válido.

Um acessador 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 poderá ser declarado com um 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 de fim

15.7.6 Acessadores virtuais, selados, de substituição e abstratos

Observação: essa cláusula se aplica a propriedades (§15.7) e indexadores (§15.9). A cláusula é escrita em termos de propriedades, ao ler para indexadores, substitua indexador/indexadores por propriedade/propriedades e consulte a lista de diferenças entre propriedades e indexadores fornecida em §15.9.2. nota final

Uma declaração de propriedade virtual especifica que os acessadores da propriedade são virtuais. O virtual modificador se aplica a todos os acessadores não privados de uma propriedade. Quando um acessador de uma propriedade virtual tem o private accessor_modifier, o acessador privado não é implicitamente 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, as 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 acessador.

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

Declarações de propriedade abstratas 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 de substituição. Uma declaração de propriedade de substituição 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. Em outras palavras, uma declaração de substituição não deve alterar a acessibilidade da propriedade base. No entanto, se a propriedade 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 deverá ser protegida. Se a propriedade herdada tiver apenas um único acessador (ou seja, se a propriedade herdada for somente leitura ou somente gravação), a propriedade de substituição deverá incluir apenas esse acessador. Se a propriedade herdada incluir ambos os acessadores (ou seja, se a propriedade herdada for leitura/gravação), a propriedade de substituição poderá incluir um único acessador ou ambos os acessadores. Deve haver uma conversão de identidade entre o tipo de propriedade superior e a herança.

Uma declaração de propriedade de substituição pode incluir o sealed modificador. O uso desse modificador impede que uma classe derivada substitua ainda mais a propriedade. Os acessadores de uma propriedade selada também são selados.

Exceto pelas diferenças na sintaxe de declaração e invocação, os acessadores virtual, sealed, override e abstract se comportam exatamente como métodos virtuais, sealed, override e abstract. Especificamente, as regras descritas em §15.6.4, §15.6.5, §15.6.6 e §15.6.7 se aplicam 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 retornado do tipo de propriedade e os mesmos modificadores que a propriedade que o contém.
  • Um acessador set corresponde a um método com um único parâmetro de valor do tipo de propriedade, um tipo de retorno void e os mesmos modificadores que a propriedade que o contém.

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 a 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 , Y, e Z são declarações de Xpropriedade de substituição. Cada declaração de propriedade corresponde exatamente aos modificadores de acessibilidade, ao tipo e ao nome da propriedade herdada correspondente. O acessador get de X e o acessador set de Y usam a palavra-chave base para acessar os acessadores herdados. A declaração de Z substitui ambos os acessadores abstratos — portanto, não há membros de função pendentes abstract em B, e B é permitido ser uma classe não abstrata.

exemplo de fim

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

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 de fim

15.8 Eventos

15.8.1 Geral

Um evento é um membro que permite que um objeto ou uma 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) está disponível apenas 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 new modificadores (§15.3.5), static (§15.6.3, §15.8.4), virtual (§15.6.4, §15.8.5), override (§15.6.5, §15.8.5), sealed (§15.6.6), (§15.6.7abstract, §15.8.5) e extern (§15.6.8).

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

O tipo de uma declaração de evento deve ser um delegate_type (§8.2.8) e esse 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, o compilador deverá fornecê-los automaticamente (§15.8.2); para extern eventos, 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 se aplicam a todos os membros declarados por esse event_declaration.

É um erro de tempo de compilação para um event_declaration incluir o modificador e event_accessor_declaration abstracts.

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

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

Um evento pode ser usado como o operando esquerdo dos += operadores and -= . Esses operadores são usados, respectivamente, para anexar manipuladores de eventos ou removê-los de um evento, e os modificadores de acesso do evento controlam os contextos nos quais essas 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 do formulário x += y ou , quando x é um evento, o resultado da operação tem o tipo void (§12.21.5) (em vez de ter o tipo de x, com o valor de x após a atribuição, como para outros operadores e -= += definidos em tipos que não x –= ysão de evento). Isso impede que o código externo examine indiretamente o delegado subjacente de um evento.

Exemplo: o exemplo a seguir mostra como os 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 de fim

15.8.2 Eventos semelhantes a campos

Dentro do texto do programa da classe ou struct que contém a declaração de um evento, determinados eventos podem ser usados como campos. Para ser usado dessa maneira, um evento não deve ser abstrato ou externo e não deve incluir explicitamente event_accessor_declarations. Esse 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 "gera" o Click evento. A noção de gerar um evento é precisamente equivalente a invocar o delegado representado pelo evento — assim, não há constructos de linguagem especial para gerar eventos. Observe que a invocação do 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 dos += operadores and –= , 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 de fim

Ao compilar um evento semelhante a um campo, o compilador cria automaticamente o armazenamento para manter o delegado e cria acessadores para o evento que adicionam ou removem manipuladores de eventos para o campo delegado. As operações de adição e remoção são thread-safe e podem (mas não são obrigadas) ser feitas enquanto mantém 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.

Observação: portanto, uma declaração de evento de instância do formato:

class X
{
    public event D Ev;
}

deve ser compilado para 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 and –= fazem com que os acessadores add e remove sejam invocados. Todas as outras referências a Ev são 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

Observação: 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 do acessador consistem em um add_accessor_declaration e um remove_accessor_declaration. Cada declaração de acessador consiste na adição ou remoção de token seguida 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 evento é denominado 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 += , o acessador de adição será usado e, se o operador de atribuição for –= , o acessador de remoção será usado. Em ambos os casos, o operando direito do operador de atribuição é usado como o argumento para o acessador de eventos. O bloco de um add_accessor_declaration ou remove_accessor_declaration deve estar em conformidade com as regras para void métodos descritos no ponto 15.6.9. Em particular, return as instruções em tal bloco não têm permissão para especificar uma expressão.

Como um acessador de evento tem implicitamente um parâmetro chamado value, é um erro de tempo de compilação que uma variável local ou constante declarada em um acessador de evento tenha 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 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 forma que não haja custo para associar um valor delegado nulo a uma chave e, portanto, eventos sem tratamento não consumam armazenamento.

exemplo de fim

15.8.4 Eventos estáticos e de instância

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

Um evento estático não está associado a uma instância específica e é um erro de tempo de compilação a ser consultado this 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 adiante em §15.3.8.

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

Uma declaração de evento virtual especifica que os acessadores desse evento são virtuais. O virtual modificador se aplica 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, as classes derivadas não abstratas são necessárias para fornecer sua própria implementação para os acessadores substituindo o evento. Como um acessador 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 and override especifica que o evento é abstrato e substitui um evento base. Os acessadores de tal evento também são abstratos.

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 de substituição. Uma declaração de evento de substituição 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 de substituição deve especificar exatamente os mesmos modificadores de acessibilidade e nome que o evento substituído, deve haver uma conversão de identidade entre o tipo do evento de substituição e o evento substituído e os acessadores 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 acessadores de um evento lacrado 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 pelas diferenças na sintaxe de declaração e invocação, os acessadores virtual, sealed, override e abstract se comportam exatamente como métodos virtuais, sealed, override e abstract. Especificamente, as regras descritas em §15.6.4, §15.6.5, §15.6.6 e §15.6.7 se aplicam 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 o contém.

15.9 Indexadores

15.9.1 Geral

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) está disponível apenas em código não seguro (§23).

Existem dois tipos de indexer_declaration:

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

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 (§15.6.6), (§15.6.7abstract) e extern (§15.6.8).

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

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

Observação: 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 de 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 os thismodificadores , refe out de parâmetro não são permitidos.

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

Um indexer_body pode consistir em um corpo de instrução (§15.7.1) ou um corpo de expressão (§15.6.1). Em um corpo de instrução, accessor_declarations, que deve ser colocado entre tokens "{" e "}", declare os acessadores (§15.7.3) do indexador. Os acessadores especificam as instruções executáveis associadas à leitura e gravação de elementos do indexador.

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 em que o resultado do acessador get é fornecido 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_reference V e um ponto-e-vírgula é exatamente equivalente ao corpo { get { return ref V; } }da instrução .

Observação: embora a sintaxe para acessar um elemento de indexador seja a mesma de um elemento de matriz, um elemento de indexador não é classificado como uma variável. Portanto, não é possível passar um elemento indexador como um inargumento , out, or ref , a menos que o indexador tenha valor de referência 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 nos 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 ser diferente das assinaturas de todos os outros indexadores declarados na mesma classe.

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

Exemplo: o exemplo a seguir 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[] (já que cada valor da primeira ocupa apenas um bit em vez do último 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 .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 número 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 de fim

15.9.2 Diferenças entre indexador e propriedade

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

  • Uma propriedade é identificada por seu nome, enquanto um indexador é identificado por sua assinatura.
  • Uma propriedade é acessada por meio de um simple_name (§12.8.4) ou um member_access (§12.8.7), enquanto um elemento indexador é acessado por meio 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 acessador get de um indexador corresponde a um método com a mesma lista de parâmetros que o indexador.
  • Um acessador set de uma propriedade corresponde a um método com um único parâmetro chamado value, enquanto um acessador set de um indexador corresponde a um método com a mesma lista de parâmetros que o indexador, além de um parâmetro adicional chamado value.
  • É um erro de tempo de compilação para um acessador de indexador declarar uma variável local ou constante local com o mesmo nome de um parâmetro de 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], em que E é uma lista de expressões separadas por vírgulas.
  • Não existe o conceito de um "indexador implementado automaticamente". É um erro ter um indexador não abstrato e não externo com ponto-e-vírgula accessor_bodys.

Além dessas diferenças, todas as regras definidas em §15.7.3, §15.7.5 e §15.7.6 se aplicam a acessadores de indexador, bem como a acessadores de propriedade.

Essa substituição de propriedade/propriedades por indexador/indexadores ao ler §15.7.3, §15.7.5 e §15.7.6 também se aplica a termos definidos. Especificamente, a propriedade de 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 Geral

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 usando operator_declarations:

operator_declaration
    : attributes? operator_modifier+ operator_declarator operator_body
    ;

operator_modifier
    : 'public'
    | 'static'
    | 'extern'
    | unsafe_modifier   // unsafe code support
    ;

operator_declarator
    : unary_operator_declarator
    | binary_operator_declarator
    | conversion_operator_declarator
    ;

unary_operator_declarator
    : type 'operator' overloadable_unary_operator '(' fixed_parameter ')'
    ;

logical_negation_operator
    : '!'
    ;

overloadable_unary_operator
    : '+' | '-' | logical_negation_operator | '~' | '++' | '--' | 'true' | 'false'
    ;

binary_operator_declarator
    : type 'operator' overloadable_binary_operator
        '(' fixed_parameter ',' fixed_parameter ')'
    ;

overloadable_binary_operator
    : '+'  | '-'  | '*'  | '/'  | '%'  | '&' | '|' | '^'  | '<<' 
    | right_shift | '==' | '!=' | '>' | '<' | '>=' | '<='
    ;

conversion_operator_declarator
    : 'implicit' 'operator' type '(' fixed_parameter ')'
    | 'explicit' 'operator' type '(' fixed_parameter ')'
    ;

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

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

Nota: O prefixo negação lógica (§12.9.4) e os operadores pós-sufixo que perdoam nulos (§12.8.9), embora representados pelo mesmo token lexical (!), são distintos. Este último não é um operador sobrecarregável. nota final

Há três categorias de operadores sobrepujáveis: 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 para métodos de retorno de valor descritos em §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 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.

As seguintes regras se aplicam a todas as declarações do operador:

  • A declaração do operador deve incluir um public modificador e um static modificador.
  • O(s) parâmetro(s) de um operador não deve(m) ter modificadores além de in.
  • A assinatura de um operador (§15.10.2, §15.10.3, §15.10.4) deve ser diferente das assinaturas de todos os outros operadores declarados na mesma classe.
  • Todos os tipos referenciados em uma declaração do operador devem ser pelo menos tão acessíveis quanto o próprio operador (§7.5.5).
  • É um erro que o mesmo modificador apareça várias vezes em uma declaração de operador.

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

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 struct na qual o operador é declarado participe da assinatura do operador, não é possível que um operador declarado em uma classe derivada oculte um operador declarado em uma classe base. Assim, o new modificador nunca é necessário e, portanto, nunca é permitido em uma declaração de operador.

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

Informações adicionais sobre operadores de conversão podem ser encontradas em §10.5.

15.10.2 Operadores unários

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

  • Um unário +, -, ! (somente negação lógica) ou ~ operador deve usar um único parâmetro do tipo T ou T? e pode retornar qualquer tipo.
  • Um unário ++ ou -- operador deve pegar um único parâmetro do tipo T ou T? e deve retornar esse mesmo tipo ou um tipo derivado dele.
  • Um unário true ou false operador deve usar um único parâmetro do 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 unários e false unários exigem declaração em pares. Um erro de tempo de compilação ocorrerá se uma classe declarar um desses operadores sem também declarar o outro. Os true operadores e false são descritos mais detalhadamente em §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 pela adição de 1 ao operando, assim como os operadores de incremento e decremento do sufixo (§12.8.16) e os operadores de incremento e decremento de prefixo (§12.9.6). Ao contrário do C++, esse método não deve modificar o valor de seu operando diretamente, pois isso violaria a semântica padrão do operador de incremento de sufixo (§12.8.16).

exemplo de fim

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 não shift deve ter dois parâmetros, pelo menos um dos quais deve ter tipo T ou T?, e pode retornar qualquer tipo.
  • Um binário << ou >> operador (§12.11) deve ter 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 nos 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 qualquer operador de um par, deve haver uma declaração de concordância do outro operador do par. Duas declarações de operador correspondem se houver conversões de identidade entre seus tipos de retorno e seus tipos de parâmetro correspondentes. Os seguintes operadores 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 usuário (§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 uma variedade de situações, incluindo invocações de membros da função, expressões de conversão e atribuições. Isso é descrito mais adiante 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 conversão e são descritas mais detalhadamente em §10.3.

Um operador de conversão converte de um tipo de origem, 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 forem tipos de valor anuláveis, deixe S₀ e T₀ se refira a 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.

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

  • Nem S₀ T₀ é um 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, todos os parâmetros de tipo associados S ou T considerados tipos exclusivos que não têm relação de herança com outros tipos e quaisquer restrições nesses parâmetros de tipo são ignorados.

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 int e , stringrespectivamente, são considerados tipos exclusivos sem relação. No entanto, o terceiro operador é um erro porque C<T> é a classe base de D<T>.

exemplo de fim

Da segunda regra, segue-se que um operador de conversão deve converter de ou para a classe ou tipo de struct no qual o operador é declarado.

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

Não é possível redefinir diretamente uma conversão predefinida. Assim, os operadores de conversão não têm permissão para 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 do outro, pois 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 de tipo para T, o segundo operador declara uma conversão que já existe (existe uma conversão implícita e, portanto, também explícita, de qualquer tipo para objeto de tipo).

exemplo de fim

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 usuário (implícitas ou explícitas) de S para T serã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 usuário de S para T serão ignoradas. Além disso:
    • Se ou S T 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 serã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 de fim

As 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. (Essa é 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. Portanto, 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.

Observação: em geral, as conversões implícitas definidas pelo usuário devem ser projetadas para nunca gerar exceções e nunca perder informações. Se uma conversão definida pelo usuário puder 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 alta ordem), essa conversão deverá 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 TO byte é implícita porque nunca lança exceções ou perde informações, mas a conversão de byte TO Digit é explícita, pois Digit só pode representar um subconjunto dos valores possíveis de um byte.

exemplo de fim

15.11 Construtores de instância

15.11.1 Geral

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) está disponível apenas 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 de instância é declarado. Se qualquer outro nome for especificado, ocorrerá um erro em tempo de compilação.

A parameter_list opcional de um construtor de instância está sujeita às mesmas regras que a parameter_list de um método (§15.6). Como o this modificador para parâmetros se aplica apenas 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 desse construtor de instância. Isso é descrito mais adiante em §15.11.2.

Quando uma declaração de construtor inclui um extern modificador, o construtor é considerado um construtor externo. Como uma declaração de construtor externo não fornece nenhuma implementação real, sua 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; 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 para inicializar uma nova instância da classe.

Um constructor_body que é um corpo de bloco ou 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 por meio 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 ser invocado implicitamente é determinado pelo constructor_initializer:

  • Um inicializador de construtor de instância do argument_list) de formuláriobase( (em que 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 de §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 inicializador de construtor de instância do formulário this(argument_list) (em que 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 de §12.6.4. O conjunto de construtores de instância candidatos 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 invocar a si mesma por meio de uma cadeia de um ou mais inicializadores de construtor, ocorrerá um erro em tempo de compilação.

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

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

C(...) {...}

é exatamente equivalente a

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

nota final

O escopo dos parâmetros fornecidos pela parameter_list de uma declaração de construtor de instância inclui o inicializador de construtor dessa declaração. Assim, um inicializador de 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 de fim

Um inicializador de construtor de instância não pode acessar a instância que está sendo criada. Portanto, é um erro de tempo de compilação fazer referência a isso em uma expressão de argumento do inicializador do construtor, pois é um erro de tempo de compilação para uma expressão de argumento fazer referência a qualquer membro de 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 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 de variáveis antes que qualquer instrução que tenha acesso a essa instância seja executada.

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 de variável é executado antes que o construtor de instância de x classe base seja invocado. No entanto, o valor de y é 0 (o valor padrão de um int) porque a atribuição a y não é executada até que o construtor da classe base retorne. É útil pensar em inicializadores de variáveis de instância e inicializadores de construtor 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 de variáveis; Ele também contém inicializadores de construtor de ambas as formas (base e this). O exemplo corresponde ao código mostrado abaixo, em que 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 de fim

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 será pública.

Nota: Assim, o construtor padrão é sempre do formato

protected C(): base() {}

ou

public C(): base() {}

onde C é o nome da classe.

nota final

Se a resolução de sobrecarga não puder 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 de fim

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) está disponível apenas 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 é considerado um construtor estático externo. Como uma declaração de construtor estático externo não fornece nenhuma implementação real, sua 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; 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 inicializar a classe.

Um static_constructor_body que é um corpo de bloco ou 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 de uma classe fechada é executado no máximo uma vez em um determinado domínio de aplicativo. A execução de um construtor estático é disparada pelo primeiro dos seguintes eventos a ocorrer em um domínio de aplicativo:

  • Uma instância da classe é criada.
  • Qualquer um dos membros estáticos da classe é referenciado.

Se uma classe contiver o Main método (§7.1) no qual a execução começa, o construtor estático dessa classe será executado antes que o Main método seja chamado.

Para inicializar um novo tipo de classe fechada, primeiro um novo conjunto de campos estáticos (§15.5.2) para esse tipo fechado específico é criado. Cada um dos campos estáticos é inicializado com seu valor padrão (§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

Como a execução do construtor estático de . é acionada Apela chamada para A.F, e a execução do construtor B.Festático de B.

exemplo de fim

É 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 do static seja executado porque o valor de A.X é referenciado. O construtor estático de A , por sua vez, continua a calcular o valor de X, e, ao fazer isso, busca o valor padrão de Y, que é zero. A.X é, portanto, inicializado como 1. O processo de execução Ados inicializadores de campo estático e do construtor estático da é concluído, retornando ao cálculo do valor inicial de Y, cujo resultado se torna 2.

exemplo de fim

Como o construtor estático é executado exatamente uma vez para cada tipo de classe construída fechada, ele é um local conveniente para impor verificações de tempo de execução no parâmetro de tipo 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 de tipo seja uma enumeração:

class Gen<T> where T : struct
{
    static Gen()
    {
        if (!typeof(T).IsEnum)
        {
            throw new ArgumentException("T must be an enum");
        }
    }
}

exemplo de fim

15.13 Finalizadores

Nota: Em uma versão anterior desta especificação, o que agora é chamado de "finalizador" era chamado de "destruidor". A experiência mostrou que o termo "destruidor" causava confusão e muitas vezes resultava em expectativas incorretas, especialmente para programadores que conheciam C++. Em C++, um destruidor é chamado de maneira determinada, enquanto, em C#, um finalizador não é. Para obter um comportamento determinado do C#, deve-se usar Dispose. 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) está disponível apenas em código não seguro (§23).

Um finalizer_declaration pode incluir um conjunto de atributos (§22).

O identificador de um finalizer_declarator deve nomear a classe na qual 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, o finalizador é considerado um finalizador externo. Como uma declaração de finalizador externo não fornece nenhuma implementação real, sua 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 corpo de bloco ou 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 deve não ter parâmetros, ele não pode ser sobrecarregado, portanto, 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 se torna elegível para finalização quando não é mais possível que nenhum código use 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, da mais derivada para a menos derivada. Um finalizador pode ser executado em qualquer thread. Para uma discussão mais aprofundada sobre as 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 de fim

Os finalizadores são implementados substituindo o método Finalize virtual no 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 de fim

O compilador se comporta como se esse método e as substituições dele 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 de fim

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 Geral

Um membro de função (§12.6) implementado usando um bloco de iterador (§13.3) é chamado de iterador.

Um bloco de iterador pode ser usado como o corpo de um membro de função, desde que o tipo de retorno do membro de 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 de função é implementado usando um bloco de iterador, é um erro de tempo de compilação para a lista de parâmetros do membro de função especificar qualquer inparâmetro , out, ou ref 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 em 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 em 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. Esse 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 Geral

Quando um membro de função que retorna um tipo de interface de enumerador é implementado usando um bloco de iterador, invocar o membro de função não executa imediatamente o código no bloco de iterador. Em vez disso, um objeto enumerador é criado e retornado. Esse 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 MoveNext objeto enumerador é invocado. Um objeto enumerador tem as seguintes características:

  • Ele implementa IEnumerator e IEnumerator<T>, onde T é o tipo de rendimento do iterador.
  • Ele implementa System.IDisposable.
  • Ele é inicializado com uma cópia dos valores do argumento (se houver) e do valor da instância passado para o membro da função.
  • Ele 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 do iterador e implementa as interfaces do enumerador, mas outros métodos de implementação são possíveis. Se uma classe enumerador for gerada pelo compilador, essa classe será aninhada, direta ou indiretamente, na classe que contém o membro da função, ela 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 dos MoveNextmembros , Currente Dispose das implementações de IEnumerator interface e IEnumerator<T> fornecidas por um objeto enumerador.

Os objetos enumeradores não dão suporte ao IEnumerator.Reset método. Invocar esse método faz com que a 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. A invocação do MoveNext método executa o código no bloco do 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 running.
    • Inicializa os parâmetros (incluindo this) do bloco iterador para os valores de argumento e o valor da instância salvos quando o objeto enumerador foi inicializado.
    • Executa o bloco do 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 for suspenso, invocar MoveNext:
    • Altera o estado para running.
    • Restaura os valores de todas as variáveis e parâmetros locais (incluindo this) para os valores salvos quando a execução do bloco do iterador foi suspensa pela última vez.

      Observação: o conteúdo de todos os objetos referenciados por essas variáveis pode ter sido alterado desde a chamada anterior para MoveNext. nota final

    • Retoma a execução do bloco do iterador imediatamente após a instrução yield return 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 posterior, a MoveNext invocação retornará false.

Quando MoveNext executa o bloco do 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 do iterador e por uma exceção sendo lançada e propagada para fora do bloco do iterador.

  • Quando uma yield return instrução é encontrada (§9.4.4.20):
    • A expressão fornecida na instrução é avaliada, convertida implicitamente no tipo yield e atribuída à Current propriedade do objeto enumerador.
    • A execução do corpo do iterador é suspensa. Os valores de todas as variáveis e parâmetros locais (incluindo this) são salvos, assim como o local dessa yield return instrução. Se a yield return instrução estiver dentro de um ou mais try blocos, os blocos finally associados não serão executados neste momento.
    • O estado do objeto enumerador é alterado para suspenso.
    • O MoveNext método retorna true ao chamador, indicando que a iteração avançou com êxito para o próximo valor.
  • Quando uma yield break instrução é encontrada (§9.4.4.20):
    • Se a yield break instrução estiver dentro de 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 chamador, indicando que a iteração foi 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 chamador, indicando que a iteração foi concluída.
  • Quando uma exceção é lançada e propagada para fora do bloco do iterador:
    • Os blocos apropriados finally no corpo do iterador terão sido executados pela propagação de exceção.
    • O estado do objeto enumerador é alterado para depois.
    • A propagação de 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 para MoveNext. Quando um objeto enumerador está nos estados before, running ou after , o resultado do acesso Current não é especificado.

Para um iterador com um tipo yield diferente de object, o resultado do acesso Current por meio da implementação do IEnumerable objeto enumerador corresponde ao acesso Current por meio da implementação do IEnumerator<T> objeto enumerador e à conversão do resultado para object.

15.14.5.4 O método Descartar

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 before, a invocação Dispose alterará o estado para after.
  • 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 for suspenso, invocar Dispose:
    • Altera o estado para running.
    • Executa todos os blocos finally 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 after e a exceção será propagada para o chamador do Dispose método.
    • Altera o estado para depois.
  • Se o estado do objeto enumerador for posterior, a invocação Dispose não terá efeito.

15.14.6 Objetos enumeráveis

15.14.6.1 Geral

Quando um membro de função que retorna um tipo de interface enumerável é implementado usando um bloco de iterador, invocar o membro de função não executa imediatamente o código no bloco de 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 MoveNext objeto 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 do argumento (se houver) e do valor da 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 do 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. As invocações subsequentes de GetEnumerator, se houver, retornariam uma nova instância de classe, normalmente da mesma classe, de modo que as chamadas para diferentes instâncias do enumerador não afetariam 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, pois 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 Geral

Um método (§15.6) ou função anônima (§12.19) com o async modificador é chamado de função assíncrona. Em geral, o termo assíncrono é usado para descrever qualquer tipo de função que tenha o async modificador.

É um erro de tempo de compilação para a lista de parâmetros de uma função assíncrona especificar qualquer inparâmetro , out, ou ref 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. Esses tipos são referidos nesta especificação como «TaskType»<T> e «TaskType», respectivamente. O tipo System.Threading.Tasks.Task de biblioteca padrão e os tipos construídos a partir são tipos de tarefa, bem como um tipo de System.Threading.Tasks.Task<TResult> classe, struct ou interface associado a um tipo de construtor de tarefas por meio do atributo System.Runtime.CompilerServices.AsyncMethodBuilderAttribute. Esses tipos são referidos nesta especificação como «TaskBuilderType»<T> e «TaskBuilderType». Um tipo de tarefa pode ter no máximo um parâmetro de tipo e não pode ser aninhado em um tipo genérico.

Um método assíncrono que retorna um tipo de tarefa é chamado de retorno de 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 falha registra uma exceção pertinente. A succeeded «TaskType»<T> registra um resultado do tipo T. Os tipos de tarefa são aguardáveis 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 awaiter:

using System.Runtime.CompilerServices; 
[AsyncMethodBuilder(typeof(MyTaskMethodBuilder<>))]
class MyTask<T>
{
    public Awaiter<T> GetAwaiter() { ... }
}

class Awaiter<T> : INotifyCompletion
{
    public void OnCompleted(Action completion) { ... }
    public bool IsCompleted { get; }
    public T GetResult() { ... }
}

exemplo de fim

Um tipo de construtor de tarefas é um tipo de classe ou struct que corresponde a um tipo de tarefa específico (§15.15.2). O tipo de construtor de tarefas deve corresponder exatamente à acessibilidade declarada de seu tipo de tarefa correspondente.

Observação: se o tipo de tarefa for declarado internal, o tipo de construtor correspondente também deverá ser declarado internal e definido no mesmo assembly. Se o tipo de tarefa estiver aninhado dentro de outro tipo, o tipo de compartimento de tarefas 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 pode ser retomada posteriormente no ponto da expressão de espera de suspensão por meio de um delegado de retomada. O delegado de retomada é do tipo System.Actione, quando for invocado, a avaliação da invocação da função assíncrona será retomada 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 caso contrário.

15.15.2 Padrão do construtor de 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 construtor 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; }
}

O compilador gera código que usa o «TaskBuilderType» para implementar a semântica de suspender e retomar a avaliação da função assíncrona. O compilador usa o «TaskBuilderType» da seguinte maneira:

  • «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 chamar stateMachine.MoveNext() ou Start() depois Start() de ter retornado para avançar a máquina de estado.
  • Após Start() retornar, o async método invoca builder.Task para que a tarefa retorne do método assíncrono.
  • Cada chamada para stateMachine.MoveNext() avançará a máquina de estado.
  • Se a máquina de estado for concluída com êxito, builder.SetResult() será chamada, com o valor retornado do método, se houver.
  • Caso contrário, se uma exceção e for lançada na máquina de estado, builder.SetException(e) será chamada.
  • Se a máquina de estado atingir uma await expr expressão, expr.GetAwaiter() será invocada.
  • Se o awaiter implementar ICriticalNotifyCompletion e IsCompleted for false, a máquina de estado invocará builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine).
    • AwaitUnsafeOnCompleted() deve chamar awaiter.UnsafeOnCompleted(action) com um Action que chama stateMachine.MoveNext() quando o aguardador é concluído.
  • Caso contrário, a máquina de estado invocará builder.AwaitOnCompleted(ref awaiter, ref stateMachine).
    • AwaitOnCompleted() deve chamar awaiter.OnCompleted(action) com um Action que chama stateMachine.MoveNext() quando o aguardador é concluído.
  • 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, especialmente para casos em que a máquina de estado é implementada como um tipo de valor.
    • Se o construtor chamar , o chamará builder.SetStateMachine(stateMachine) a instância do construtor associada a stateMachine.stateMachine stateMachine.SetStateMachine(stateMachine)

Nota: Para ambos e «TaskType»<T> Task { get; }SetResult(T result) , o parâmetro e o argumento, respectivamente, devem ser conversíveis em identidade para T. Isso permite que um construtor de tipo de tarefa dê suporte a tipos como tuplas, em que dois tipos que não são iguais são conversíveis em 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 (atingindo uma expressão de espera) ou encerrado, momento em que o controle é retornado ao chamador, juntamente com a tarefa de retorno.

Quando o corpo da função assíncrona é encerrado, a tarefa de retorno é movida para fora do estado incompleto:

  • Se o corpo da função for encerrado como resultado de atingir uma instrução return ou o final do corpo, qualquer valor de resultado será registrado na tarefa return, que é colocada em um estado bem-sucedido .
  • Se o corpo da função for encerrado devido a um uncaught OperationCanceledException, a exceção será registrada na tarefa de retorno que é colocada no estado cancelado .
  • Se o corpo da função for encerrado como resultado de qualquer outra exceção não capturada (§13.10.6), a exceção será 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 será diferente da anterior 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 -returning começa, é concluída com êxito ou faz com que uma exceção não capturada seja lançada.

Isso permite que o contexto acompanhe quantas voidfunções assíncronas -returning estão sendo executadas nele e decida como propagar exceções que saem delas.