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 public
modificadores , 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 public
modificadores , 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 abstract
modificadores , 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ãonull
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étodoF
abstrato . ClassB
introduz um métodoG
adicional , mas como não fornece uma implementação de ,B
também deve ser declaradoF
abstrato. A classeC
substituiF
e fornece uma implementação real. Como não há membros abstratos emC
,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 orabstract
. (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
, ouprotected 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 formaT.I
, ou - O nome namespace_or_type é o
T
em um typeof_expression (§12.8.18) do formuláriotypeof(T)
.
Um primary_expression (§12.8) tem permissão para fazer referência a uma classe estática se
- O primary_expression é o
E
em um member_access (§12.8.7) do formulárioE.I
.
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 , eB
é derivadaB
deA
. ComoA
não especifica explicitamente uma classe base direta, sua classe base direta é implicitamenteobject
.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 seriaB<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 object
que 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 deZ
é consideradaobject
, e, portanto, (pelas regras do §7.8)Z
não é considerada como tendo um membroY
.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ãoC<int[]>
,B<IComparable<int[]>>
,A
eobject
.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 deB
(sua classe imediatamente delimitadora), que depende circularmente deA
.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 deA
(porqueA
é sua classe base direta e sua classe imediatamente delimitadora), masA
não depende deB
(uma vez queB
não é uma classe base nem uma classe delimitadora deA
). 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 classeA
selada.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
, eIC
.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 class
de tipo de referência, a restrição struct
de 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
ouT : BaseClass
), mas useT?
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 paraT
. Assim, tipos recursivamente construídos das formasT??
eNullable<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
ouSystem.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âmetroS
de tipo, entãoS
depende.T
- Se um parâmetro
S
de tipo depende de um parâmetroT
de tipo eT
depende de um parâmetroU
de tipo, entãoS
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 queT
, eliminando a necessidade de dois parâmetros de tipo.- Se
S
tiver a restrição de tipo de valor, nãoT
deverá ter uma restrição class_type . - Se
S
tiver uma restriçãoA
class_type eT
tiver uma restriçãoB
class_type, haverá uma conversão de identidade ou conversão de referência implícita deA
paraB
ou uma conversão de referência implícita deB
paraA
. - Se
S
também depender do parâmetroU
de tipo eU
tiver uma restriçãoA
class_type eT
tiver uma restriçãoB
class_type, haverá uma conversão de identidade ou conversão de referência implícita deA
paraB
ou uma conversão de referência implícita deB
paraA
.
É 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 tipoOuter.Inner
aninhado, entãoCₓ
é um tipoOuterₓ.Innerₓ
aninhado. - Se
C
Cₓ
é um tipoG<A¹, ..., Aⁿ>
construído com argumentosA¹, ..., Aⁿ
de tipo, entãoCₓ
é o tipoG<A¹ₓ, ..., Aⁿₓ>
construído . - Se
C
for um tipoE[]
de matriz, entãoCₓ
é o tipoEₓ[]
de matriz. - Se
C
é dinâmico, entãoCₓ
é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émSystem.ValueType
. - Para cada restrição
T
de que é um tipo de enumeração,R
contémSystem.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émSystem.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 conjuntoR
. Se o conjunto não tiver nenhum tipo englobado, a classe base efetiva deT
é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 emx
porqueT
é restrito a sempre implementarIPrintable
.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
, eref
.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
eout
.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 deT
", portanto, o tipo do membroa
no tipo construído acima é "matriz bidimensional de matriz unidimensional deint
", ouint[,][]
.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 deB
, eB
é derivado deA
, entãoC
herda os membros declarados emB
A
comoUma 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úblicoG(string s)
int
não herdado obtido substituindo o argumentoint
type pelo parâmetroT
type .D<int>
também tem um membro herdado da declaraçãoB
de classe . Esse membro herdado é determinado primeiro determinando o tipoB<int[]>
de classe base deD<int>
substituindo porT
na especificaçãoB<T[]>
da classeint
base . Então, como um argumento de tipo paraB
,int[]
é substituído porU
empublic U F(long index)
, produzindo o membropublic 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
, , internal
ou 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árioE.M
,E
deve denotar um tipo que tem um membroM
. É um erro de tempo de compilação paraE
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árioE.M
,E
deve denotar uma instância de um tipo que tem um membroM
. É 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. OG
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. OMain
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 classA
, e classA
é 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
, ouprivate
) e, como outros membros do struct, oprivate
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
Node
aninhada 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étodoM
definido emBase
.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 deNested
, e passa seu próprio construtor this to paraNested
, a fim de fornecer acesso subsequente aosC
membros 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 classeNested
aninhada. DentroNested
do , o métodoG
chama o métodoF
estático definido emC
, eF
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étodoF
protegido definido naDerived
classeBase
base do , , chamando por meio de uma instância deDerived
.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:
- 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#.
- 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#.
- 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 propriedadeP
somente leitura , reservando assinaturas paraget_P
eset_P
métodos.A
derivaB
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 T
delegado, 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 L
de 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
, ushort
uint
long
ulong
char
float
double
bool
int
string
decimal
, , 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 onew
operador não é permitido em um constant_expression, o único valor possível para constantes de reference_types diferente destring
é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 diferereadonly
(§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 avaliaB.Z
, e finalmente avaliaA.X
, produzindo os valores10
,11
, e12
.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
eB
fossem declarados em programas separados, seria possívelA.X
depender deB.Z
, masB.Z
não poderia depender simultaneamente deA.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
Black
membros ,White
,Red
,Green
eBlue
não podem ser declarados como membros const porque seus valores não podem ser calculados em tempo de compilação. No entanto, declará-losstatic 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 eProgram2
denotam dois programas que são compilados separadamente. ComoProgram1.Utils.X
é declarado como umstatic readonly
campo, a saída do valor pelaConsole.WriteLine
instrução não é conhecida em tempo de compilação, mas é obtida em tempo de execução. Portanto, se o valor deX
for alterado eProgram1
recompilado, aConsole.WriteLine
instrução produzirá o novo valor mesmo queProgram2
não seja recompilado. No entanto, se tivesseX
sido uma constante, o valor de teria sido obtido no momentoProgram2
em que foi compilado e não seria afetadoX
por mudanças emProgram1
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
,int
uint
float
char
, ,bool
, ou .System.IntPtr
System.UIntPtr
- Um enum_type com um enum_base tipo de
byte
,sbyte
,short
,ushort
,int
, ouuint
.
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étodoThread2
. Esse método armazena um valor em um campo não volátil chamadoresult
, em seguida, armazenatrue
no campofinished
volátil . O thread principal aguarda que o campofinished
seja definido comotrue
, em seguida, lê o camporesult
. Uma vez quefinished
foi declaradovolatile
, o thread principal deve ler o valor143
do camporesult
. Se o campofinished
não tivesse sido declaradovolatile
, seria permitido que o armazenamentoresult
ficasse visível para o thread principal após o armazenamento parafinished
, e, portanto, que o thread principal lesse o valor 0 do camporesult
. Declararfinished
como umvolatile
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
ei
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 ai
es
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
eb
, o programa é válido. Isso resulta na saídaa = 1, b = 2
porque os campos
a
estáticos eb
são inicializados para0
(o valor padrão paraint
) antes que seus inicializadores sejam executados. Quando o inicializador fora
executado, o valor deb
é zero e, portantoa
, é inicializado como1
. Quando o inicializador forb
executado, o valor de a já1
é , e assimb
é inicializado para2
.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
Y
de e doX
inicializador 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
B
o construtor estático do (e, portanto,B
os inicializadores de campo estático do ) deve ser executado antesA
do 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
, eoverride
. - A declaração inclui, no máximo, um dos seguintes modificadores:
new
eoverride
. - Se a declaração incluir o
abstract
modificador, ela não incluirá nenhum dos seguintes modificadores:static
,virtual
,sealed
, ouextern
. - Se a declaração incluir o
private
modificador, a declaração não incluirá nenhum dos seguintes modificadores:virtual
,override
, ouabstract
. - Se a declaração incluir o
sealed
modificador, a declaração também incluirá ooverride
modificador. - Se a declaração incluir o
partial
modificador, ela não incluirá nenhum dos seguintes modificadores:new
,public
,protected
,internal
, ,extern
virtual
sealed
override
abstract
private
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 ref
modificador , 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 queS
é um tipo de valor - uma expressão do formulário
default(S)
em queS
é 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
et
são parâmetros de valor opcionais ea
é 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:
- Parâmetros de valor (§15.6.2.2).
- Parâmetros de entrada (§15.6.2.3.2).
- Parâmetros de saída (§15.6.2.3.4).
- Parâmetros de referência (§15.6.2.3.3).
- Matrizes de parâmetros (§15.6.2.4).
Observação: conforme descrito no §7.6, os
in
modificadores ,out
eref
fazem parte da assinatura de um método, mas oparams
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
emMain
,x
representai
ey
representaj
. Assim, a invocação tem o efeito de trocar os valores dei
ej
.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
InG
passa uma referência as
For Botha
eb
. Assim, para essa chamada, os nomess
, , eb
todos se referem ao mesmo local de armazenamento, e as três atribuições modificam o campos
dea
instâ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 ename
podem ser desatribuídas antes de serem passadas paraSplitPath
, 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[]
estring[][]
podem ser usados como o tipo de uma matriz de parâmetros, mas o tipostring[,]
não. exemplo de fim
Nota: Não é possível combinar o
params
modificador com os modificadoresin
,out
, ouref
. 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 matrizarr
como um parâmetro de valor. A segunda invocação de F cria automaticamente um elemento de quatro comint[]
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 deF
cria um elementoint[]
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 eF(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 deF
é aplicável porque existe uma conversão implícita do tipo de argumento para o tipo de parâmetro (ambos são do tipoobject[]
). Assim, a resolução de sobrecarga seleciona a forma normal de , e o argumento é passado como um parâmetro deF
valor regular. Na segunda e terceira invocações, a forma normal deF
não é aplicável porque não existe nenhuma conversão implícita do tipo de argumento para o tipo de parâmetro (o tipoobject
não pode ser convertido implicitamente em tipoobject[]
). No entanto, a forma expandida de é aplicável, portanto, é selecionada pela resolução deF
sobrecarga. Como resultado, um elementoobject[]
ú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 umobject[]
).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 C
da seguinte maneira:
- No tempo de associação, a resolução de sobrecarga é aplicada a
C
, , eA
, para selecionar um métodoM
específico do conjunto de métodos declarados e herdados porC
N
. 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 deM
em relação aR
é invocada.
- Se
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 deM
, então esta é a implementação mais derivada deM
em relação aR
. - Caso contrário, se
R
contém uma substituição deM
, então esta é a implementação mais derivada deM
em relação aR
. - Caso contrário, a implementação mais derivada de
M
em relação aR
é a mesma que a implementação mais derivada deM
em relação à classe base direta deR
.
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étodoF
não virtual e um métodoG
virtual . A classeB
introduz um novo métodoF
não virtual , ocultando assim o herdadoF
, e também substitui o métodoG
herdado . O exemplo produz a saída:A.F B.F B.G B.G
Observe que a instrução
a.G()
invocaB.G
, nãoA.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 andD
contêm dois métodos virtuais com a mesma assinatura: o introduzido porA
e o introduzido porC
. O método introduzido porC
oculta o método herdado deA
. Assim, a declaração de substituição substituiD
o método introduzido porC
, e não é possívelD
substituir o método introduzido porA
. 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 emB
invoca o método PrintFields declarado emA
. 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 emB
tivesse sido escrita((A)this).PrintFields()
, ela invocaria recursivamente oPrintFields
método declarado emB
, não o declarado emA
, uma vez quePrintFields
é 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 emB
não inclui umoverride
modificador e, portanto, não substitui oF
método emA
. Em vez disso, oF
método emB
oculta o método emA
, 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 emB
oculta o método virtualF
herdado deA
. Como o novoF
inB
tem acesso privado, seu escopo inclui apenas o corpo da classe eB
não se estende aC
. Portanto, a declaração deF
inC
tem permissão para substituir oF
herdado deA
.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: umF
método que tem osealed
modificador e umG
método que não tem.B
O uso dosealed
modificador impedeC
a substituiçãoF
adicional 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. OPaint
método é abstrato porque não há implementação padrão significativa. AsEllipse
classes eBox
são implementações concretasShape
. Como essas classes não são abstratas, elas são necessárias para substituir oPaint
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, classB
substitui esse método por um método abstrato e classC
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 doDllImport
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 M
parcial, 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 nostring[]
, e oToInt32
método está disponível nostring
, 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. OsG
métodos andH
estão corretos porque todos os caminhos de execução possíveis terminam em uma instrução return que especifica um valor de retorno. OI
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
in
argumento ,out
, ouref
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
, ouprivate
.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
, ouprivate
.protected internal
- Se a propriedade ou o indexador tiver uma acessibilidade declarada de ou , a acessibilidade declarada
internal
por accessor_modifier deverá ser ouprivate protected
private
.protected
- Se a propriedade ou o indexador tiver uma acessibilidade declarada de , a acessibilidade declarada
private protected
por accessor_modifier seráprivate
. - Se a propriedade ou o indexador tiver uma acessibilidade declarada de
private
, nenhum accessor_modifier poderá ser usado.
- Se a propriedade ou o indexador tiver uma acessibilidade declarada de , a acessibilidade declarada por accessor_modifier poderá ser
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 porref
, 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úblicaCaption
. O acessador get da propriedade Caption retorna ostring
campo armazenado nocaption
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 umprivate
campo e o acessador set modifica esseprivate
campo e, em seguida, executa todas as ações adicionais necessárias para atualizar totalmente o estado do objeto. Dada aButton
classe acima, o seguinte é um exemplo de uso daCaption
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 emB
esconde aP
propriedade emA
relação à leitura e à escrita. Assim, nas falasB 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 somenteP
leitura emB
oculta a propriedade somenteP
gravação emA
. Observe, no entanto, que uma conversão pode ser usada para acessar a propriedade ocultaP
.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 doisint
camposx
ey
, para armazenar sua localização. O local é exposto publicamente como umaX
propriedade eY
como umaLocation
propriedade do tipoPoint
. Se, em uma versão futura doLabel
, for mais conveniente armazenar o local como internamentePoint
, 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 fossempublic readonly
campos, teria sido impossível fazer tal mudança naLabel
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
, eError
, que representam os dispositivos padrão de entrada, saída e erro, respectivamente. Ao expor esses membros como propriedades, aConsole
classe pode atrasar sua inicialização até que eles sejam realmente usados. Por exemplo, ao fazer referênciaOut
à propriedade pela primeira vez, como emConsole.Out.WriteLine("hello, world");
O subjacente
TextWriter
para o dispositivo de saída é criado. No entanto, se o aplicativo não fizer referência àsIn
propriedades andError
, 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 propriedadeB.Text
, mesmo em contextos em que apenas o acessador set é chamado. Por outro lado, a propriedadeB.Count
não é acessível à classeM
, portanto, a propriedadeA.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 eZ
é uma propriedade abstrata de leitura/gravação. PorZ
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
, eZ
são declarações deX
propriedade 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 deX
e o acessador set deY
usam a palavra-chave base para acessar os acessadores herdados. A declaração deZ
substitui ambos os acessadores abstratos — portanto, não há membros de função pendentesabstract
emB
, eB
é 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 abstract
s.
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 extern
s.
É 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 –= y
sã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 duasButton
instâncias e anexa manipuladores de eventos aosClick
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 daButton
classe. Como o exemplo demonstra, o campo pode ser examinado, modificado e usado em expressões de invocação delegada. OOnClick
método naButton
classe "gera" oClick
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, oClick
membro só pode ser usado no lado esquerdo dos+=
operadores and–=
, como emb.Click += new EventHandler(...);
que acrescenta um delegado à lista de invocação do
Click
evento eClick –= 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 aEv
no lado esquerdo dos+=
operadores and–=
fazem com que os acessadores add e remove sejam invocados. Todas as outras referências aEv
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. OAddEventHandler
método associa um valor delegado a uma chave, oGetEventHandler
método retorna o delegado atualmente associado a uma chave e oRemoveEventHandler
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 this
modificadores , ref
e 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
in
argumento ,out
, orref
, 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 correspondentebool[]
(já que cada valor da primeira ocupa apenas um bit em vez do últimobyte
), mas permite as mesmas operações que umbool[]
.A classe a seguir
CountPrimes
usa umBitArray
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 chamadovalue
. - É 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
, ondeP
é o nome da propriedade. Em uma declaração de indexador de substituição, o indexador herdado é acessado usando a sintaxebase[E]
, em queE
é 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 umstatic
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 tipoT
ouT?
e pode retornar qualquer tipo. - Um unário
++
ou--
operador deve pegar um único parâmetro do tipoT
ouT?
e deve retornar esse mesmo tipo ou um tipo derivado dele. - Um unário
true
oufalse
operador deve usar um único parâmetro do tipoT
ouT?
e deve retornar o tipobool
.
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
ouT?
, e pode retornar qualquer tipo. - Um binário
<<
ou>>
operador (§12.11) deve ter dois parâmetros, o primeiro dos quais deve ter tipoT
ou T? e o segundo deve ter tipoint
ouint?
, 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 T
de 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₀
eT₀
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
paraT
ou deT
paraS
.
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
eint
e ,string
respectivamente, são considerados tipos exclusivos sem relação. No entanto, o terceiro operador é um erro porqueC<T>
é a classe base deD<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 deC
paraint
e deint
paraC
, mas não deint
parabool
. 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 paraT
, 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 tipoT
, todas as conversões definidas pelo usuário (implícitas ou explícitas) deS
paraT
serão ignoradas. - Se existir uma conversão explícita predefinida (§10.3) de tipo
S
para tipoT
, todas as conversões explícitas definidas pelo usuário deS
paraT
serão ignoradas. Além disso:- Se ou
S
T
for um tipo de interface, as conversões implícitas definidas pelo usuário deS
paraT
serão ignoradas. - Caso contrário, as conversões implícitas definidas pelo usuário de
S
paraT
ainda serão consideradas.
- Se ou
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
TObyte
é implícita porque nunca lança exceções ou perde informações, mas a conversão debyte
TODigit
é explícita, poisDigit
só pode representar um subconjunto dos valores possíveis de umbyte
.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 deB
, 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 dey
é 0 (o valor padrão de umint
) porque a atribuição ay
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 exemploclass 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
ethis
). 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
A
pela chamada paraA.F
, e a execução do construtorB.F
estático deB
.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 paraB.Y
, antes do construtor estático da classeB
.Y
O inicializador do faz com queA
o construtor dostatic
seja executado porque o valor deA.X
é referenciado. O construtor estático deA
, por sua vez, continua a calcular o valor deX
, e, ao fazer isso, busca o valor padrão deY
, que é zero.A.X
é, portanto, inicializado como 1. O processo de execuçãoA
dos inicializadores de campo estático e do construtor estático da é concluído, retornando ao cálculo do valor inicial deY
, 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.Object
o método deFinalize
.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 in
parâ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
ouIEnumerable
éobject
. - O tipo de rendimento de um iterador que retorna
IEnumerator<T>
ouIEnumerable<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
eIEnumerator<T>
, ondeT
é 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 MoveNext
membros , Current
e 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 dessayield return
instrução. Se ayield return
instrução estiver dentro de um ou maistry
blocos, os blocos finally associados não serão executados neste momento. - O estado do objeto enumerador é alterado para suspenso.
- O
MoveNext
método retornatrue
ao chamador, indicando que a iteração avançou com êxito para o próximo valor.
- A expressão fornecida na instrução é avaliada, convertida implicitamente no tipo yield e atribuída à
- Quando uma
yield break
instrução é encontrada (§9.4.4.20):- Se a
yield break
instrução estiver dentro de um ou maistry
blocos, os blocos associadosfinally
serão executados. - O estado do objeto enumerador é alterado para depois.
- O
MoveNext
método retornafalse
ao chamador, indicando que a iteração foi concluída.
- Se a
- Quando o final do corpo do iterador é encontrado:
- O estado do objeto enumerador é alterado para depois.
- O
MoveNext
método retornafalse
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.
- Os blocos apropriados
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 umayield 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 doDispose
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
eIEnumerable<T>
, ondeT
é 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
eIEnumerator<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 paraGetEnumerator
. As invocações subsequentes deGetEnumerator
, 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 in
parâ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 tipoMyTaskMethodBuilder<T>
de construtor de tarefas e ao tipoAwaiter<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 declaradointernal
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.Action
e, 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», nomeadobuilder
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()
ouStart()
depoisStart()
de ter retornado para avançar a máquina de estado.
- O construtor deve chamar
- Após
Start()
retornar, oasync
método invocabuilder.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
eIsCompleted
for false, a máquina de estado invocarábuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine)
.AwaitUnsafeOnCompleted()
deve chamarawaiter.UnsafeOnCompleted(action)
com umAction
que chamastateMachine.MoveNext()
quando o aguardador é concluído.
- Caso contrário, a máquina de estado invocará
builder.AwaitOnCompleted(ref awaiter, ref stateMachine)
.AwaitOnCompleted()
deve chamarawaiter.OnCompleted(action)
com umAction
que chamastateMachine.MoveNext()
quando o aguardador é concluído.
SetStateMachine(IAsyncStateMachine)
pode ser chamado pela implementação geradaIAsyncStateMachine
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 astateMachine
.stateMachine
stateMachine.SetStateMachine(stateMachine)
- Se o construtor chamar , o chamará
Nota: Para ambos e
«TaskType»<T> Task { get; }
SetResult(T result)
, o parâmetro e o argumento, respectivamente, devem ser conversíveis em identidade paraT
. 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 void
funçã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 void
funções assíncronas -returning estão sendo executadas nele e decida como propagar exceções que saem delas.
ECMA C# draft specification