Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
15.1 Generalidades
Uma classe é uma estrutura de dados que pode conter membros de dados (constantes e campos), membros de função (métodos, propriedades, eventos, indexadores, operadores, construtores de instância, finalizadores e construtores estáticos) e tipos aninhados. Os tipos de classe suportam herança, um mecanismo pelo qual uma classe derivada pode estender e especializar uma classe base.
15.2 Declarações de classe
15.2.1 Generalidades
Um class_declaration é um type_declaration (§14.7) que declara uma nova classe.
class_declaration
: attributes? class_modifier* 'partial'? 'class' identifier
type_parameter_list? class_base? type_parameter_constraints_clause*
class_body ';'?
;
Um class_declaration consiste num conjunto opcional de atributos (§22), seguido por um conjunto opcional de class_modifiers (§15.2.2), seguido por um modificador opcional partial
(§15.2.7), seguido pela palavra-chave class
e um identificador que nomeia a classe, seguido por uma type_parameter_list opcional (§15.2.3), seguido por uma especificação class_base opcional (§15.2.4), seguido de um conjunto opcional de type_parameter_constraints_clauses (§15.2.5), seguido de um class_body (§15.2.6), opcionalmente seguido de ponto e vírgula.
Uma declaração de classe não deve fornecer type_parameter_constraints_clausea menos que forneça também um type_parameter_list.
Uma declaração de classe que fornece um type_parameter_list é uma declaração de classe genérica. Adicionalmente, qualquer classe aninhada dentro de uma declaração de classe genérica ou uma declaração de estrutura genérica é, por si só, uma declaração de classe genérica, uma vez que os argumentos de tipo para o tipo envolvente devem ser fornecidos para criar um tipo construído (§8.4).
15.2.2 Modificadores de classe
15.2.2.1 Generalidades
Um class_declaration pode, opcionalmente, incluir uma sequência de modificadores de classe:
class_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'abstract'
| 'sealed'
| 'static'
| unsafe_modifier // unsafe code support
;
unsafe_modifier (§23.2) só está disponível em código não seguro (§23).
É um erro em tempo de compilação para o mesmo modificador aparecer várias vezes em uma declaração de classe.
O modificador new
é permitido em classes aninhadas. Especifica que a classe oculta um membro herdado com o mesmo nome, conforme descrito no §15.3.5. É um erro em tempo de compilação o modificador new
aparecer numa declaração de classe quando esta 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) incluir uma especificação de acessibilidade (através dos modificadores , public
protected
, internal
, e private
), essa especificação deve ser acordada com todas as outras partes que incluam uma especificação de acessibilidade. Se nenhuma parte de um tipo parcial incluir uma especificação de acessibilidade, é dada ao tipo a acessibilidade por defeito adequada (§7.5.2).
O abstract
, sealed
e static
modificadores são discutidos nas subcláusulas a seguir.
15.2.2.2 Classes abstratas
O abstract
modificador é usado para indicar que uma classe está incompleta e que se destina a ser usada apenas como uma classe base. Uma classe abstrata difere de uma classe não abstrata das seguintes maneiras:
- Uma classe abstrata não pode ser instanciada diretamente, e é um erro em tempo de compilação usar o
new
operador em uma classe abstrata. Embora seja possível ter variáveis e valores cujos tipos de tempo de compilação são abstratos, tais variáveis e valores serão necessariamentenull
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 seguinte código
abstract class A { public abstract void F(); } abstract class B : A { public void G() {} } class C : B { public override void F() { // Actual implementation of F } }
A classe
A
Abstract introduz um métodoF
abstrato. ClassB
introduz um métodoG
adicional, mas como não fornece uma implementação de ,F
também deve ser declaradoB
abstrato. A classeC
substituiF
e fornece uma implementação real. Uma vez que não há membros abstratos noC
,C
é permitido (mas não obrigatório) ser não-abstrato.Exemplo final
Se uma ou mais partes de uma declaração de tipo parcial (§15.2.7) de uma classe incluírem o abstract
modificador, a classe será abstrata. Caso contrário, a classe não é abstrata.
15.2.2.3 Classes seladas
O sealed
modificador é usado para impedir a derivação de uma classe. Um erro em tempo de compilação ocorre se uma classe selada é especificada como a classe base de outra classe.
Uma classe selada não pode ser também uma classe abstrata.
Nota: O
sealed
modificador é usado principalmente para evitar derivação não intencional, mas também permite determinadas otimizações em tempo de execução. Em particular, como uma classe selada é conhecida por nunca ter classes derivadas, é possível transformar invocações de membros de funções virtuais em instâncias de classe lacradas em invocações não virtuais. Nota final
Se uma ou mais partes de uma declaração de tipo parcial (ponto 15.2.7) de uma classe incluírem o sealed
modificador, a classe é selada. Caso contrário, a classe é deslacrada.
15.2.2.4 Classes estáticas
15.2.2.4.1 Generalidades
O static
modificador é usado para marcar a classe que está sendo declarada como uma classe estática. Uma classe estática não deve ser instanciada, não deve ser utilizada como tipo e deve conter apenas elementos estáticos. Apenas uma classe estática pode conter declarações de métodos de extensão (§15.6.10).
Uma declaração de classe estática está sujeita às seguintes restrições:
- Uma classe estática não deve incluir um
sealed
ouabstract
modificador. (No entanto, como uma classe estática não pode ser instanciada ou derivada, ela se comporta como se fosse selada e abstrata.) - Uma classe estática não deve incluir uma especificação class_base (§15.2.4) e não pode especificar explicitamente uma classe de base ou uma lista de interfaces implementadas. Uma classe estática herda implicitamente do tipo
object
. - Uma classe estática deve conter apenas membros estáticos (§15.3.8).
Nota: Todas as constantes e tipos aninhados são classificados como membros estáticos. Nota final
- Uma classe estática não deve ter membros com
protected
,private protected
ouprotected internal
acessibilidade declarada.
É um erro em tempo de compilação violar qualquer uma dessas restrições.
Uma classe estática não tem construtores de instância. Não é possível declarar um construtor de instância em uma classe estática, e nenhum construtor de instância padrão (§15.11.5) é fornecido para uma classe estática.
Os membros de uma classe estática não são automaticamente estáticos, e as declarações de membro devem incluir explicitamente um static
modificador (exceto para constantes e tipos aninhados). Quando uma classe é aninhada dentro de uma classe externa estática, a classe aninhada não é uma classe estática, a menos que inclua explicitamente um static
modificador.
Se uma ou mais partes de uma declaração de tipo parcial (§15.2.7) de uma classe incluírem o static
modificador, a classe é estática. Caso contrário, a classe não é estática.
15.2.2.4.2 Referenciando tipos de classe estática
Um namespace_or_type_name (§7.8) pode fazer referência a uma classe estática se:
- A namespace_or_type_name é o
T
em um namespace_or_type_name da formaT.I
, ou - O namespace_or_type-name é o
T
em uma typeof_expression (§12.8.18) do formatotypeof(T)
.
Um primary_expression (§12.8) pode fazer referência a uma classe estática se
- O primary_expression é o
E
num acesso_de_membro (§12.8.7) da formaE.I
.
Em qualquer outro contexto, é um erro em tempo de compilação fazer referência a uma classe estática.
Nota: Por exemplo, é um erro para uma classe estática ser usada como uma classe base, um tipo constituinte (§15.3.7) de um membro, um argumento de tipo genérico ou uma restrição de parâmetro de tipo. Da mesma forma, uma classe estática não pode ser usada num tipo de array, numa expressão new, numa expressão de conversão, numa expressão is, numa expressão as, numa expressão
sizeof
, ou numa expressão de valor padrão. Nota final
15.2.3 Parâmetros de tipo
Um parâmetro type é um identificador simples que indica um espaço reservado para um argumento type fornecido para criar um tipo construído. Por 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
: '<' decorated_type_parameter (',' decorated_type_parameter)* '>'
;
decorated_type_parameter
: attributes? type_parameter
;
type_parameter é definido no §8.5.
Cada parâmetro de tipo em uma declaração de classe define um nome no espaço de declaração (§7.3) dessa classe. Assim, não pode ter o mesmo nome que outro parâmetro de tipo dessa classe ou um membro declarado nessa classe. Um parâmetro type não pode ter o mesmo nome que o próprio tipo.
Duas declarações de tipo genéricas parciais (no mesmo programa) contribuem para o mesmo tipo genérico não acoplado se tiverem o mesmo nome totalmente qualificado (o que inclui um generic_dimension_specifier (§12.8.18) para o número de parâmetros de tipo) (§7.8.3). Duas dessas declarações de tipo parciais devem especificar o mesmo nome para cada parâmetro de tipo, por ordem.
15.2.4 Especificação de base de classe
15.2.4.1 Generalidades
Uma declaração de classe pode incluir uma especificação class_base , que define a classe base direta da classe e as interfaces (§18) diretamente implementadas pela classe.
class_base
: ':' class_type
| ':' interface_type_list
| ':' class_type ',' interface_type_list
;
interface_type_list
: interface_type (',' interface_type)*
;
15.2.4.2 Classes de base
Quando um class_type é incluído no class_base, ele especifica a classe base direta da classe que está sendo declarada. Se uma declaração de classe não parcial não tiver class_base, ou se o class_base listar apenas tipos de interface, a classe base direta será considerada object
. Quando uma declaração de classe parcial incluir uma especificação de classe de base, essa especificação de classe de base deve fazer referência ao mesmo tipo que todas as outras partes desse tipo parcial que incluam uma especificação de classe de base. Se nenhuma parte de uma classe parcial incluir uma especificação de classe base, a classe base será object
. Uma classe herda membros da sua classe base direta, conforme descrito no §15.3.4.
Exemplo: No seguinte código
class A {} class B : A {}
Diz-se que a classe
A
é a classe base direta deB
, eB
diz-se que é derivada deA
. Uma vez queA
não especifica explicitamente uma classe base direta, sua classe base direta é implicitamenteobject
.Exemplo final
Para um tipo de classe construída, incluindo um tipo aninhado declarado numa declaração de tipo genérica (§15.3.9.7), se uma classe base for especificada na declaração de classe genérica, a classe base do tipo construído é obtida substituindo, para cada type_parameter na declaração de classe base, a type_argument correspondente do tipo construído.
Exemplo: Dadas as declarações de classe genéricas
class B<U,V> {...} class G<T> : B<string,T[]> {...}
A classe base do tipo
G<int>
construído seriaB<string,int[]>
.Exemplo final
A classe base especificada numa declaração de classe pode ser um tipo de classe construída (§8.4). Uma classe base não pode ser um parâmetro de tipo por si só (§8.5), embora possa envolver os parâmetros de tipo que estão no escopo.
Exemplo:
class Base<T> {} // Valid, non-constructed class with constructed base class class Extend1 : Base<int> {} // Error, type parameter used as base class class Extend2<V> : V {} // Valid, type parameter used as type argument for base class class Extend3<V> : Base<V> {}
Exemplo final
A classe de base direta de um tipo de classe deve ser, pelo menos, tão acessível como o próprio tipo de classe (ponto 7.5.5). Por exemplo, é um erro em tempo de compilação para uma classe pública derivar de uma classe privada ou interna.
A classe de base direta de um tipo de classe não deve ser de nenhum dos seguintes tipos: System.Array
, System.Delegate
, System.Enum
, System.ValueType
ou o dynamic
tipo. Além disso, uma declaração de classe genérica não pode ser utilizada System.Attribute
como classe de base direta ou indireta (§22.2.1).
Ao determinar o significado da especificação da classe base direta A
de uma classe B
, a classe base direta de B
é temporariamente assumida como object
, o 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á em erro, uma vez que na especificação da classe base a classe
X<Z.Y>
base direta deZ
é considerada como sendoobject
, e, portanto, (pelas regras do §7.8)Z
não é considerada como tendo um membroY
.Exemplo final
As classes base de uma classe são a classe base direta e suas classes base. Em outras palavras, o conjunto de classes base é o encerramento transitivo da relação de classe base direta.
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 final
Com exceção da classe object
, cada classe tem exatamente uma classe base direta. A object
classe não tem classe base direta e é a classe base final de todas as outras classes.
É um erro de tempo de compilação para uma classe depender de si mesma. Para efeitos desta regra, uma classe depende diretamente da sua classe base direta (se existir) e depende diretamente da classe de inclusão mais próxima na qual está aninhada (se existir). Dada esta definição, o conjunto completo de classes das quais uma classe depende é o fechamento transitivo da relação diretamente dependente .
Exemplo: O exemplo
class A : A {}
é errôneo porque a classe depende de si mesma. Da mesma forma, o exemplo
class A : B {} class B : C {} class C : A {}
está em erro porque as classes dependem circularmente de si mesmas. Por fim, o exemplo
class A : B.C {} class B : A { public class C {} }
resulta num erro em tempo de compilação porque A depende de
B.C
(sua classe base direta), que depende deB
(sua classe imediatamente envolvente), que depende circularmente deA
.Exemplo final
Uma classe não depende das classes que estão aninhadas dentro dela.
Exemplo: No seguinte código
class A { class B : A {} }
B
depende deA
(porqueA
é tanto a sua classe base direta como a sua classe imediatamente anexa), masA
não depende deB
(uma vez queB
não é nem uma classe base nem uma classe anexa deA
). Assim, o exemplo é válido.Exemplo final
Não é possível derivar de uma classe selada.
Exemplo: No seguinte código
sealed class A {} class B : A {} // Error, cannot derive from a sealed class
A classe
B
está em erro porque tenta derivar da classeA
selada.Exemplo final
15.2.4.3 Implementações de interface
Uma especificação class_base pode incluir uma lista de tipos de interface, caso em que se diz que a classe implementa os tipos de interface fornecidos. Para um tipo de classe construído, incluindo um tipo aninhado declarado numa declaração de tipo genérica (§15.3.9.7), cada tipo de interface implementado é obtido substituindo, para cada type_parameter na interface dada, a type_argument correspondente do tipo construído.
O conjunto de interfaces para um tipo declarado em várias partes (§15.2.7) é a união das interfaces especificadas em cada parte. Uma determinada interface só pode ser nomeada uma vez em cada parte, mas várias partes podem nomear as mesmas interfaces de base. Deve haver apenas uma implementação de cada membro de uma dada interface.
Exemplo: No seguinte:
partial class C : IA, IB {...} partial class C : IC {...} partial class C : IA, IB {...}
O conjunto de interfaces base para a classe
C
éIA
,IB
eIC
.Exemplo final
Normalmente, cada parte fornece uma implementação das interfaces declaradas nessa parte, no entanto, isto não é um requisito. Uma parte pode fornecer a implementação para uma interface declarada em uma parte diferente.
Exemplo:
partial class X { int IComparable.CompareTo(object o) {...} } partial class X : IComparable { ... }
Exemplo final
As interfaces de base especificadas numa declaração de classe podem ser construídas como tipos de interface (§8.4, §18.2). Uma interface base não pode ser um parâmetro de tipo por si só, embora possa envolver os parâmetros de tipo que estão no escopo.
Exemplo: O código a seguir ilustra como uma classe pode implementar e estender tipos construídos:
class C<U, V> {} interface I1<V> {} class D : C<string, int>, I1<string> {} class E<T> : C<int, T>, I1<T> {}
Exemplo final
As implementações de interface são discutidas mais detalhadamente no §18.6.
15.2.5 Restrições de parâmetros de tipo
As declarações genéricas de tipo e método podem, opcionalmente, especificar restrições de parâmetros de tipo incluindo type_parameter_constraints_clauses.
type_parameter_constraints_clause
: 'where' type_parameter ':' type_parameter_constraints
;
type_parameter_constraints
: primary_constraint (',' secondary_constraints)? (',' constructor_constraint)?
| secondary_constraints (',' constructor_constraint)?
| constructor_constraint
;
primary_constraint
: class_type nullable_type_annotation?
| 'class' nullable_type_annotation?
| 'struct'
| 'notnull'
| 'unmanaged'
;
secondary_constraint
: interface_type nullable_type_annotation?
| type_parameter nullable_type_annotation?
;
secondary_constraints
: secondary_constraint (',' secondary_constraint)*
;
constructor_constraint
: 'new' '(' ')'
;
Cada type_parameter_constraints_clause consiste no token where
, seguido pelo nome de um parâmetro type, seguido por dois pontos e a lista de restrições para esse parâmetro type. Pode haver no máximo uma where
cláusula para cada parâmetro de tipo, e as where
cláusulas podem ser listadas em qualquer ordem. Como os get
e set
tokens em um acessador de propriedade, o where
token não é uma palavra-chave.
A lista de restrições dada em uma where
cláusula pode incluir qualquer um dos seguintes componentes, nesta ordem: uma única restrição primária, uma ou mais restrições secundárias e a restrição do construtor, new()
.
Uma restrição primária pode ser um tipo de classe, a restrição de tipo de referência, a restrição de tipo de valor, a restrição não nula ou a restrição 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, seguida opcionalmente por uma nullable_type_annotation. A presença do nullable_type_annotation indica que o argumento type pode ser o tipo de referência anulável que corresponde a um tipo de referência não anulável que satisfaz a restrição.
A restrição de tipo de referência especifica que um argumento de tipo usado para o parâmetro de tipo deve ser um tipo de referência. Todos os tipos de classe, tipos de interface, tipos delegados, tipos de matriz e parâmetros de tipo conhecidos por serem um tipo de referência (conforme definido abaixo) satisfazem essa restrição.
O tipo de classe, a restrição de tipos de referência e as restrições secundárias podem incluir a anotação de tipos anuláveis. A presença ou ausência dessa anotação no parâmetro type indica as expectativas de anulabilidade para o argumento type:
- Se a restrição não incluir a anotação de tipo nullable, espera-se que o argumento de tipo seja um tipo de referência não anulável. Um compilador pode emitir um aviso se o argumento type for um tipo de referência anulável.
- Se a restrição incluir a anotação de tipo anulável, a restrição será satisfeita por um tipo de referência não anulável e um tipo de referência anulável.
A anulabilidade do argumento type não precisa corresponder à anulabilidade do parâmetro type. Um compilador pode emitir um aviso se a anulabilidade do parâmetro type não corresponder à anulabilidade do argumento type.
Nota: Para especificar que um argumento type é um tipo de referência anulável, não adicione a anotação de tipo anulável como uma restrição (use
T : class
ouT : BaseClass
), mas useT?
toda a declaração genérica para indicar o tipo de referência anulável correspondente para o argumento type. Nota final
A anotação de tipo anulável, ?
, não pode ser usada em um argumento de tipo sem restrições.
Para um parâmetro T
quando o argumento de tipo é um tipo de referência anulável C?
, as instâncias de T?
são interpretadas como C?
, não C??
.
Exemplo: Os exemplos a seguir mostram como a anulabilidade de um argumento type afeta a anulabilidade de uma declaração de seu parâmetro type:
public class C { } public static class Extensions { public static void M<T>(this T? arg) where T : notnull { } } public class Test { public void M() { C? mightBeNull = new C(); C notNull = new C(); int number = 5; int? missing = null; mightBeNull.M(); // arg is C? notNull.M(); // arg is C? number.M(); // arg is int? missing.M(); // arg is int? } }
Quando o argumento type é um tipo não anulável, a
?
anotação de tipo indica que o parâmetro é o tipo anulável correspondente. Quando o argumento type já é um tipo de referência anulável, o parâmetro é esse mesmo tipo anulável.Exemplo final
A restrição não nula especifica que um argumento type usado para o parâmetro type deve ser um tipo de valor não anulável ou um tipo de referência não anulável. Um argumento de tipo que não é um tipo de valor não anulável ou um tipo de referência não anulável é permitido, mas um compilador pode produzir um aviso de diagnóstico.
Porque notnull
não é uma palavra-chave, em primary_constraint a restrição não nula é sempre sintaticamente ambígua com class_type. Por razões de compatibilidade, se uma pesquisa de nomes (§12.8.4) do nome notnull
for bem-sucedida, será tratada como um class_type
. Caso contrário, será tratada como a restrição não nula.
Exemplo: A classe a seguir demonstra o uso de vários argumentos de tipo contra restrições diferentes, indicando avisos que podem ser emitidos por um compilador.
#nullable enable public class C { } public class A<T> where T : notnull { } public class B1<T> where T : C { } public class B2<T> where T : C? { } class Test { static void M() { // nonnull constraint allows nonnullable struct type argument A<int> x1; // possible warning: nonnull constraint prohibits nullable struct type argument A<int?> x2; // nonnull constraint allows nonnullable class type argument A<C> x3; // possible warning: nonnull constraint prohibits nullable class type argument A<C?> x4; // nonnullable base class requirement allows nonnullable class type argument B1<C> x5; // possible warning: nonnullable base class requirement prohibits nullable class type argument B1<C?> x6; // nullable base class requirement allows nonnullable class type argument B2<C> x7; // nullable base class requirement allows nullable class type argument B2<C?> x8; } }
A restrição de tipo de valor especifica que um argumento de tipo usado para o parâmetro type deve ser um tipo de valor não anulável. Todos os tipos struct não anuláveis, tipos de enum e parâmetros de tipo com a restrição de tipo de valor satisfazem essa restrição. Observe que, embora classificado como um tipo de valor, um tipo de valor anulável (§8.3.12) não satisfaz a restrição de tipo de valor. Um parâmetro de tipo com a restrição de tipo de valor também não deve ter o constructor_constraint, embora possa ser usado como um argumento de tipo para outro parâmetro de tipo com um constructor_constraint.
Nota: O
System.Nullable<T>
tipo especifica a restrição de tipo de valor não anulável paraT
. Assim, tipos recursivamente construídos das formasT??
eNullable<Nullable<T>>
são proibidos. Nota final
A restrição de tipo não gerenciado especifica que um argumento de tipo usado para o parâmetro type deve ser um tipo não gerenciado não anulável (§8.8).
Porque unmanaged
não é uma palavra-chave, na primary_constraint a restrição não gerida é sempre sintaticamente ambígua com class_type. Por razões de compatibilidade, se uma pesquisa de nome (§12.8.4) do nome unmanaged
for bem-sucedida, é tratada como um class_type
. Caso contrário, é tratada como a restrição não gerenciada.
Os tipos de ponteiro nunca podem ser argumentos de tipo e não satisfazem nenhuma restrição de tipo, mesmo os tipos não gerenciados, apesar de serem tipos não gerenciados.
Se uma restrição for um tipo de classe, um tipo de interface ou um parâmetro de tipo, esse tipo especifica um "tipo base" mínimo que cada argumento de tipo usado para esse parâmetro de tipo deve suportar. Sempre que um tipo construído ou método genérico é usado, o argumento type é verificado em relação às restrições no parâmetro type em tempo de compilação. O argumento de tipo fornecido deve satisfazer as condições descritas no ponto 8.4.5.
Uma restrição class_type deve satisfazer as seguintes regras:
- O tipo deve ser um tipo de classe.
- O tipo não deve ser
sealed
. - O tipo não deve ser um dos seguintes:
System.Array
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 restrição de interface_type deve satisfazer as seguintes regras:
- O tipo deverá ser um tipo de interface.
- Um tipo não pode ser especificado mais do que uma vez numa determinada
where
cláusula.
Em ambos os casos, a restrição pode envolver qualquer um dos parâmetros de tipo da declaração de tipo ou método associada como parte de um tipo construído, e pode envolver o tipo que está sendo declarado.
Qualquer classe ou tipo de interface especificado como restrição de parâmetro de tipo deve ser pelo menos tão acessível (ponto 7.5.5) como o tipo genérico ou método declarado.
Um tipo especificado como restrição de type_parameter deve satisfazer as seguintes regras:
- O tipo deve ser um parâmetro de tipo.
- Um tipo não pode ser especificado mais do que uma vez numa determinada
where
cláusula.
Além disso, não deve haver ciclos no gráfico de dependência dos parâmetros de tipo, em que a dependência é uma relação transitiva definida por:
- Se um parâmetro de tipo
T
for usado como uma restrição para o parâmetro de tipoS
, entãoS
depende deT
. - 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 esta relação, é um erro em tempo de compilação para um parâmetro de tipo depender de si mesmo (direta ou indiretamente).
As eventuais restrições devem ser coerentes entre os parâmetros do tipo dependente. Se o parâmetro de tipo S
depender do parâmetro de tipo T
, então:
-
T
não deve ter a restrição do tipo de valor. Caso contrário,T
é efetivamente selado assimS
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, entãoT
não terá uma restrição de class_type . - Se
S
tiver uma restrição class_typeA
eT
tiver uma restrição class_typeB
, então haverá uma conversão de identidade ou uma conversão de referência implícita deA
paraB
ou uma conversão de referência implícita deB
paraA
. - Se
S
também depende do parâmetro de tipoU
eU
tem uma restrição de class_typeA
eT
tem uma restrição de class_typeB
, então deve haver uma conversão de identidade ou uma 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 cláusula where
de um parâmetro de tipo incluir uma restrição do construtor (na forma new()
), é possível usar o operador new
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 compilação quando type_parameter_constraints possui um primary_constraint de struct
ou unmanaged
e também possui um constructor_constraint.
Exemplo: Seguem-se exemplos de restrições:
interface IPrintable { void Print(); } interface IComparable<T> { int CompareTo(T value); } interface IKeyProvider<T> { T GetKey(); } class Printer<T> where T : IPrintable {...} class SortedList<T> where T : IComparable<T> {...} class Dictionary<K,V> where K : IComparable<K> where V : IPrintable, IKeyProvider<K>, new() { ... }
O exemplo a seguir está em erro porque causa uma circularidade no gráfico de dependência dos parâmetros de tipo:
class Circular<S,T> where S: T where T: S // Error, circularity in dependency graph { ... }
Os exemplos a seguir ilustram situações inválidas adicionais:
class Sealed<S,T> where S : T where T : struct // Error, `T` is sealed { ... } class A {...} class B {...} class Incompat<S,T> where S : A, T where T : B // Error, incompatible class-type constraints { ... } class StructWithClass<S,T,U> where S : struct, T where T : U where U : A // Error, A incompatible with struct { ... }
Exemplo final
A dynamic erasure de um tipo C
é um tipo Cₓ
construído da seguinte forma:
- Se
C
é um tipo aninhadoOuter.Inner
, entãoCₓ
é um tipo aninhadoOuterₓ.Innerₓ
. - Se
C
Cₓ
é um tipoG<A¹, ..., Aⁿ>
construído com argumentosA¹, ..., Aⁿ
de tipo, entãoCₓ
é o tipoG<A¹ₓ, ..., Aⁿₓ>
construído . - Se
C
é um tipoE[]
de matriz, entãoCₓ
é o tipoEₓ[]
de matriz . - Se
C
é dinâmico, entãoCₓ
éobject
. - Caso contrário,
Cₓ
éC
.
A classe de base efetiva de um parâmetro T
de tipo é definida da seguinte forma:
Seja R
um conjunto de tipos tais que:
- Para cada restrição de
T
que seja um parâmetro de tipo,R
contém a sua classe base efetiva. - Para cada restrição de
T
que é um tipo struct,R
contémSystem.ValueType
. - Para cada restrição de
T
que é um tipo de enumeração,R
contémSystem.Enum
. - Para cada restrição de
T
que é um tipo de delegado,R
contém a sua eliminação dinâmica. - Para cada restrição que é do tipo de matriz
T
,R
contémSystem.Array
. - Para cada restrição de
T
que seja um tipo de classe,R
contém a sua eliminação dinâmica.
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 coerência garantem a existência do tipo mais abrangente.
Se o parâmetro type for um parâmetro de tipo de método cujas restrições são herdadas do método base, a classe base efetiva é calculada após a substituição de tipo.
Essas regras garantem que a classe base efetiva seja sempre uma class_type.
O conjunto de interfaces efetivas de um parâmetro de tipo T
é definido da seguinte forma:
- Se
T
não tiver secondary_constraints, o seu conjunto de interface efetivo ficará vazio. - Se
T
tiver restrições de interface_type, mas não tiver restrições de type_parameter, o seu conjunto de interface eficaz é o conjunto de rasuras dinâmicas das suas restrições de interface_type. - Se
T
não tem restrições de interface_type mas tem restrições de type_parameter, o seu conjunto de interfaces eficaz é a união dos conjuntos de interfaces eficazes das suas restrições de type_parameter. - Se
T
tiver tanto as restrições de interface_type como de type_parameter, o seu conjunto de interfaces efetivo é a união das rasuras dinâmicas das suas restrições de interface_type e dos conjuntos de interfaces efetivos das suas restrições de 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 se a sua classe base efetiva não for object
ou System.ValueType
. Um parâmetro type é conhecido por ser um tipo de referência não anulável se for conhecido por ser um tipo de referência e tiver a restrição de tipo de referência não anulável.
Os valores de um tipo restrito de parâmetro podem ser usados para acessar os membros da instância implicados pelas restrições.
Exemplo: No seguinte:
interface IPrintable { void Print(); } class Printer<T> where T : IPrintable { void PrintOne(T x) => x.Print(); }
os métodos de
IPrintable
podem ser invocados diretamente sobrex
porqueT
é obrigado a implementar sempreIPrintable
.Exemplo final
Quando uma declaração genérica de tipo parcial incluir restrições, estas devem ser acordadas com todas as outras partes que incluam restrições. Especificamente, cada parte que inclui restrições deve ter restrições para o mesmo conjunto de parâmetros de tipo, e para cada parâmetro de tipo, os conjuntos de restrições primárias, secundárias e do construtor devem ser equivalentes. Dois conjuntos de restrições são equivalentes se contiverem os mesmos membros. Se nenhuma parte de um tipo genérico parcial especificar restrições de parâmetro de tipo, os parâmetros de tipo serão considerados sem restrições.
Exemplo:
partial class Map<K,V> where K : IComparable<K> where V : IKeyProvider<K>, new() { ... } partial class Map<K,V> where V : IKeyProvider<K>, new() where K : IComparable<K> { ... } partial class Map<K,V> { ... }
está correto porque as partes que incluem restrições (as duas primeiras) especificam efetivamente o mesmo conjunto de restrições primárias, secundárias e de construtor para o mesmo conjunto de parâmetros de tipo, respectivamente.
Exemplo final
15.2.6 Corpo da classe
O class_body de uma classe define os membros dessa classe.
class_body
: '{' class_member_declaration* '}'
;
15.2.7 Declarações de tipo parcial
O modificador partial
é usado ao definir uma classe, struct ou tipo de interface em várias partes. O modificador partial
é uma palavra-chave contextual (§6.4.4) e tem um significado especial imediatamente antes das palavras-chave class
, struct
e interface
. (Um tipo parcial pode conter declarações parciais de método (§15.6.9).
Cada parte de uma declaração de tipo parcial deve incluir um modificador partial
e deve ser declarada no mesmo namespace ou tipo contido que as outras partes. O partial
modificador indica que partes adicionais da declaração de tipo podem existir em outro lugar, mas a existência dessas partes adicionais não é um requisito, é válido para a única declaração de um tipo incluir o partial
modificador. Apenas uma declaração de um tipo parcial pode incluir a classe base ou as interfaces implementadas. No entanto, todas as declarações de uma classe base ou interfaces implementadas devem corresponder, incluindo a anulabilidade de quaisquer argumentos de tipo especificados.
Todas as partes de um tipo parcial devem ser compiladas juntas, de modo que as partes possam ser unificadas durante a compilação. Tipos parciais especificamente não permitem que tipos já compilados sejam estendidos.
Os tipos aninhados podem ser declarados em múltiplas partes usando o modificador partial
. Normalmente, o tipo contido é declarado também usando partial
, e cada parte do tipo aninhado é declarada numa parte diferente do tipo contido.
Exemplo: A classe parcial a seguir é implementada em duas partes, que residem em unidades de compilação diferentes. A primeira parte é gerada por uma ferramenta de mapeamento de banco de dados, enquanto a segunda parte é criada manualmente:
public partial class Customer { private int id; private string name; private string address; private List<Order> orders; public Customer() { ... } } // File: Customer2.cs public partial class Customer { public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted); public bool HasOutstandingOrders() => orders.Count > 0; }
Quando as duas partes acima são compiladas juntas, o código resultante se comporta como se a classe tivesse sido escrita como uma única unidade, da seguinte maneira:
public class Customer { private int id; private string name; private string address; private List<Order> orders; public Customer() { ... } public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted); public bool HasOutstandingOrders() => orders.Count > 0; }
Exemplo final
O tratamento dos atributos especificados no tipo ou nos parâmetros de tipo das diferentes partes de uma declaração de tipo parcial é discutido no §22.3.
15.3 Membros da turma
15.3.1 Generalidades
Os membros de uma classe consistem nos membros introduzidos pelas suas declarações_de_membros_da_classe e nos membros herdados da classe base direta.
class_member_declaration
: constant_declaration
| field_declaration
| method_declaration
| property_declaration
| event_declaration
| indexer_declaration
| operator_declaration
| constructor_declaration
| finalizer_declaration
| static_constructor_declaration
| type_declaration
;
Os membros de uma classe são divididos nas seguintes categorias:
- Constantes, que representam valores constantes associados à classe (§15.4).
- Campos, que são as variáveis da classe (§15.5).
- Métodos, que implementam os cálculos e ações que podem ser realizadas pela classe (§15.6).
- Propriedades, que definem as características nomeadas e as ações associadas à leitura e escrita dessas características (§15.7).
- Eventos, que definem as notificações que podem ser geradas pela classe (§15.8).
- Indexadores, que permitem que instâncias da classe sejam indexadas da mesma forma (sintaticamente) que matrizes (§15.9).
- Operadores, que definem os operadores de expressão que podem ser aplicados a instâncias da classe (§15.10).
- Construtores de instância, que implementam as ações necessárias para inicializar instâncias da classe (§15.11)
- Finalizadores, que implementam as ações a serem executadas antes que as instâncias da classe sejam permanentemente descartadas (§15.13).
- Construtores estáticos, que implementam as ações necessárias para inicializar a própria classe (§15.12).
- Tipos, que representam os tipos que são locais para a classe (§14.7).
Um class_declaration cria um novo espaço de declaração (§7.3), e os type_parameters e as class_member_declarations imediatamente contidos pelo class_declaration introduzem novos membros nesse espaço de declaração. As seguintes regras aplicam-se a class_member_declarations:
Os construtores de instância, finalizadores e construtores estáticos devem ter o mesmo nome que a classe imediatamente anexada. Todos os outros membros devem ter nomes diferentes do nome da classe imediatamente anexa.
O nome de um parâmetro de tipo no type_parameter_list de uma declaração de classe deve diferir dos nomes de todos os outros parâmetros de tipo na mesma type_parameter_list e deve diferir do nome da classe e dos nomes de todos os membros da classe.
O nome de um tipo deve diferir dos nomes de todos os membros não tipo declarados na mesma classe. Se duas ou mais declarações de tipo partilharem o mesmo nome totalmente qualificado, as declarações devem ter o
partial
modificador (§15.2.7) e estas declarações combinam-se para definir um único tipo.
Nota: Como o nome totalmente qualificado de uma declaração de tipo codifica o número de parâmetros de tipo, dois tipos distintos podem compartilhar o mesmo nome, desde que tenham um número diferente de parâmetros de tipo. Nota final
O nome de uma constante, campo, propriedade ou evento deve diferir dos nomes de todos os outros membros declarados na mesma classe.
O nome de um método deve diferir dos nomes de todos os outros métodos não declarados na mesma classe. Além disso, a assinatura (§7.6) de um método deve diferir das assinaturas de todos os outros métodos declarados na mesma classe, e dois métodos declarados na mesma classe não devem ter assinaturas que diferem apenas por
in
,out
eref
.A assinatura de um construtor de instância deve diferir das assinaturas de todos os outros construtores de instância declarados na mesma classe, e dois construtores declarados na mesma classe não devem ter assinaturas que diferem apenas por
ref
eout
.A assinatura de um indexador deve diferir das assinaturas de todos os outros indexadores declarados na mesma classe.
A assinatura de um operador deve diferir da assinatura de todos os outros operadores declarados da mesma classe.
Os membros herdados de uma classe (§15.3.4) não fazem parte do espaço de declaração de uma classe.
Nota: Assim, uma classe derivada tem permissão para declarar um membro com o mesmo nome ou assinatura que um membro herdado (o que, na verdade, oculta o membro herdado). Nota final
O conjunto de membros de um tipo declarado em várias partes (§15.2.7) é a união dos membros declarados em cada parte. Os corpos de todas as partes da declaração de tipo partilham o mesmo espaço de declaração (§7.3), e o âmbito de cada membro (§7.7) estende-se aos corpos de todas as partes. O domínio de acessibilidade de qualquer membro inclui sempre todas as partes do tipo adjacente; um membro privado declarado numa 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 tenha o modificador partial
.
Exemplo:
partial class A { int x; // Error, cannot declare x more than once partial void M(); // Ok, defining partial method declaration partial class Inner // Ok, Inner is a partial type { int y; } } partial class A { int x; // Error, cannot declare x more than once partial void M() { } // Ok, implementing partial method declaration partial class Inner // Ok, Inner is a partial type { int z; } }
Exemplo final
A ordem de inicialização do campo pode ser significativa dentro do código C#, e algumas garantias são fornecidas, conforme definido no §15.5.6.1. Caso contrário, a ordenação dos membros dentro de um tipo raramente é significativa, mas pode ser significativa ao interagir com outras linguagens e ambientes. Nestes casos, a ordenação dos membros dentro de um tipo declarado em várias partes é indefinida.
15.3.2 O tipo de instância
Cada declaração de classe tem um tipo de instância associado. Para uma declaração de classe genérica, o tipo de instância é formado pela criação de um tipo construído (§8.4) a partir da declaração de tipo, com cada um dos argumentos de tipo fornecidos sendo o parâmetro type correspondente. Como o tipo de instância usa os parâmetros de tipo, ele só pode ser usado onde os parâmetros de tipo estão no escopo; ou seja, dentro da declaração de classe. O tipo de instância é o tipo de this
para código escrito dentro da declaração da classe. Para classes não genéricas, o tipo de instância é simplesmente a classe declarada.
Exemplo: A seguir mostra várias declarações de classe junto com seus tipos de instância:
class A<T> // instance type: A<T> { class B {} // instance type: A<T>.B class C<U> {} // instance type: A<T>.C<U> } class D {} // instance type: D
Exemplo final
15.3.3 Membros de tipos construídos
Os membros não herdados de um tipo construído são obtidos substituindo, para cada type_parameter na declaração de membro, o correspondente type_argument do tipo construído. O processo de substituição baseia-se no significado semântico das declarações de tipo, e não é simplesmente uma substituição textual.
Exemplo: Dada a declaração de classe genérica
class Gen<T,U> { public T[,] a; public void G(int i, T t, Gen<U,T> gt) {...} public U Prop { get {...} set {...} } public int H(double d) {...} }
O tipo
Gen<int[],IComparable<string>>
construído tem os seguintes membros:public int[,][] a; public void G(int i, int[] t, Gen<IComparable<string>,int[]> gt) {...} public IComparable<string> Prop { get {...} set {...} } public int H(double d) {...}
O tipo do membro
a
na declaração de classe genéricaGen
é "matriz bidimensional deT
", por isso o tipo do membroa
no tipo construído acima é "matriz bidimensional de matriz unidimensional deint
", ouint[,][]
.Exemplo final
Nos membros de função de instância, o tipo de this
é o tipo de instância (§15.3.2) da declaração que contém.
Todos os membros de uma classe genérica podem usar parâmetros de tipo de qualquer classe anexa, diretamente ou como parte de um tipo construído. Quando um determinado tipo construído fechado (§8.4.3) é utilizado em tempo de execução, cada utilização de um parâmetro de tipo é substituída pelo argumento de tipo fornecido ao tipo construído.
Exemplo:
class C<V> { public V f1; public C<V> f2; public C(V x) { this.f1 = x; this.f2 = this; } } class Application { static void Main() { C<int> x1 = new C<int>(1); Console.WriteLine(x1.f1); // Prints 1 C<double> x2 = new C<double>(3.1415); Console.WriteLine(x2.f1); // Prints 3.1415 } }
Exemplo final
15.3.4 Herança
Uma classe herda os membros de sua classe base direta. Herança significa que uma classe contém implicitamente todos os membros da sua classe base direta, exceto os construtores de instância, os finalizadores e os construtores estáticos da classe base. Alguns aspetos importantes da herança são:
A herança é transitiva. Se
C
é derivado deB
, eB
é derivado deA
, entãoC
herda os membros declarados emB
, bem como os membros declarados emA
.Uma classe derivada estende sua classe base direta. Uma classe derivada pode adicionar novos membros àqueles que herda, mas não pode remover a definição de um membro herdado.
Construtores de instância, finalizadores e construtores estáticos não são herdados, mas todos os outros membros são, independentemente de sua acessibilidade declarada (§7.5). No entanto, dependendo de sua acessibilidade declarada, os membros herdados podem não estar acessíveis em uma classe derivada.
Uma classe derivada pode ocultar (§7.7.2.3) membros herdados declarando novos membros com o mesmo nome ou assinatura. No entanto, ocultar um membro herdado não remove esse membro — apenas torna esse membro inacessível diretamente através da classe derivada.
Uma instância de uma classe contém um conjunto de todos os campos de instância declarados na classe e suas classes base, e uma conversão implícita (§10.2.8) existe de um tipo de classe derivado para qualquer um de seus tipos de classe base. Assim, uma referência a uma instância de alguma classe derivada pode ser tratada como uma referência a uma instância de qualquer uma de suas classes base.
Uma classe pode declarar métodos virtuais, propriedades, indexadores e eventos, e classes derivadas podem substituir a implementação desses membros da função. Isso permite que as classes exibam um comportamento polimórfico em que as ações executadas por uma invocação de membro de função variam dependendo do tipo de tempo de execução da instância através da qual esse membro da função é invocado.
Os membros herdados de um tipo de classe construído são os membros do tipo de classe base imediata (§15.2.4.2), que é encontrado substituindo os argumentos de tipo do tipo construído em cada ocorrência dos parâmetros de tipo correspondentes na base_class_specification. Estes membros, por sua vez, são transformados substituindo, para cada type_parameter na declaração de membro, o type_argument correspondente da 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úblicoint
G(string s)
não herdado, obtido ao substituir o argumento de tipoint
pelo parâmetro de tipoT
.D<int>
também tem um membro herdado da declaração da classeB
. Este membro herdado é determinado primeiro ao determinar o tipoB<int[]>
da classe base, substituindoD<int>
porint
na especificação da classe baseT
. 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 final
15.3.5 O novo modificador
Um class_member_declaration pode declarar um membro com o mesmo nome ou assinatura que um membro herdado. Quando isso ocorre, diz-se que o membro da classe derivada oculta o membro da classe base. Ver §7.7.2.3 para uma especificação precisa de quando um membro oculta um membro herdado.
Um membro M
herdado é considerado disponível se M
estiver acessível e não houver nenhum outro membro acessível herdado N que já oculte M
. Ocultar implicitamente um membro herdado não é considerado um erro, mas um compilador deve emitir um aviso, a menos que a declaração do membro da classe derivada inclua um modificador de new
para indicar explicitamente que o membro derivado se destina a ocultar o membro base. Se uma ou mais partes de uma declaração parcial (§15.2.7) de um tipo aninhado incluírem o new
modificador, nenhum aviso será emitido se o tipo aninhado ocultar um membro herdado disponível.
Se um new
modificador for incluído numa declaração que não esconda um membro herdado disponível, será emitido um aviso a esse respeito.
15.3.6 Modificadores de acesso
Uma declaração_de_membro_de_classe 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 combinações protected internal
e private protected
, é um erro em tempo de compilação especificar mais de um modificador de acesso. Quando um class_member_declaration não inclui nenhum modificador de acesso, private
é assumido.
15.3.7 Tipos de componentes
Os tipos que são usados na declaração de um membro são chamados de tipos constituintes desse membro. Os tipos de constituintes possíveis são o tipo de uma constante, campo, propriedade, evento ou indexador, o tipo de retorno de um método ou operador e os tipos de parâmetro de um construtor de método, indexador, operador ou instância. Os tipos de componentes de um membro devem ser pelo menos tão acessíveis como o próprio membro (§7.5.5).
15.3.8 Membros estáticos e de instância
Os membros de uma classe são membros estáticos ou membros da instância.
Nota: De um modo geral, é útil pensar em membros estáticos como pertencentes a classes e membros de instância como pertencentes a objetos (instâncias de classes). Nota final
Quando uma declaração de campo, método, propriedade, evento, operador ou construtor inclui um static
modificador, ele declara um membro estático. Além disso, uma declaração constante ou de tipo declara implicitamente um membro estático. Os membros estáticos têm as seguintes características:
- Quando um membro
M
estático é referenciado num member_access (§12.8.7) do formulárioE.M
,E
deve indicar um tipo que tenha um membroM
. É um erro em tempo de compilação quandoE
denota uma instância. - Um campo estático em uma classe não genérica identifica exatamente um local de armazenamento. Não importa quantas instâncias de uma classe não genérica sejam criadas, há apenas uma cópia de um campo estático. Cada tipo de construção fechada distinta (§8.4.3) tem o seu próprio conjunto de campos estáticos, independentemente do número de ocorrências do tipo fechado construído.
- Um membro de função estática (método, propriedade, evento, operador ou construtor) não opera sobre uma instância específica, e é um erro em tempo de compilação referir-se a "this" em tal membro de função.
Quando um campo, método, propriedade, evento, indexador, construtor ou declaração do finalizador não inclui um modificador estático, ele declara um membro da instância. (Um membro da instância às vezes é chamado de membro não estático.) Os membros da instância têm as seguintes características:
- Quando um membro
M
da instância é referenciado em um member_access (§12.8.7) do formulárioE.M
,E
deve indicar uma instância de um tipo que tenha um membroM
. É um erro de tempo de ligação se E denotar um tipo. - Cada instância de uma classe contém um conjunto separado de todos os campos de instância da classe.
- Um membro da função de instância (método, propriedade, indexador, construtor de instância ou finalizador) opera em uma determinada instância da classe, e essa instância pode ser acessada como
this
(§12.8.14).
Exemplo: O exemplo a seguir ilustra as regras para acessar membros estáticos e de instância:
class Test { int x; static int y; void F() { x = 1; // Ok, same as this.x = 1 y = 1; // Ok, same as Test.y = 1 } static void G() { x = 1; // Error, cannot access this.x y = 1; // Ok, same as Test.y = 1 } static void Main() { Test t = new Test(); t.x = 1; // Ok t.y = 1; // Error, cannot access static member through instance Test.x = 1; // Error, cannot access instance member through type Test.y = 1; // Ok } }
O
F
método mostra que, num 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, num membro de função estática, é um erro durante a compilação acessar um membro de instância através 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 final
15.3.9 Tipos Aninhados
15.3.9.1 Generalidades
Um tipo declarado dentro de uma classe ou struct é chamado de tipo aninhado. Um tipo que é declarado dentro de uma unidade de compilação ou namespace é chamado de tipo não aninhado.
Exemplo: No exemplo a seguir:
class A { class B { static void F() { Console.WriteLine("A.B.F"); } } }
class
B
é um tipo aninhado porque é declarado dentro da classeA
, e classA
é um tipo não aninhado porque é declarado dentro de uma unidade de compilação.Exemplo final
15.3.9.2 Nome totalmente qualificado
O nome totalmente qualificado (§7.8.3) para uma declaração de tipo aninhada é S.N
em que S
é o nome totalmente qualificado da declaração de tipo na qual o tipo N
é declarado e N
é o nome não qualificado (§7.8.2) da declaração de tipo aninhada (incluindo qualquer generic_dimension_specifier (§12.8.18)).
15.3.9.3 Acessibilidade declarada
Os tipos não aninhados podem ter public
ou internal
acessibilidade declarada e têm internal
acessibilidade declarada por padrão. Tipos aninhados podem ter estas formas de acessibilidade declarada também, além de uma ou mais formas adicionais de acessibilidade declarada, dependendo se o tipo que os contém é uma classe ou uma estrutura.
- Um tipo aninhado que é declarado numa classe pode ter qualquer um dos tipos permitidos de acessibilidade declarada e, tal como outros membros da classe, por defeito terá
private
acessibilidade declarada. - Um tipo aninhado que é declarado numa estrutura pode ter qualquer uma das três formas de acessibilidade declarada (
public
,internal
ouprivate
) e, como outros membros da estrutura, assume como padrão a acessibilidade declaradaprivate
.
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 privada aninhada
Node
.Exemplo final
15.3.9.4 Ocultação
Um tipo aninhado pode ocultar (§7.7.2.2) um membro base. O modificador new
(§15.3.5) é permitido em declarações de tipos aninhados, de forma 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 final
15.3.9.5 este acesso
Um tipo aninhado e o tipo que o contém não têm uma relação especial 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 os contém. Em casos em que um tipo aninhado precisa acessar os membros de instância do seu tipo contido, o acesso pode ser fornecido passando a this
para a instância do tipo contido como um argumento do construtor para o tipo aninhado.
Exemplo: O exemplo a seguir
class C { int i = 123; public void F() { Nested n = new Nested(this); n.G(); } public class Nested { C this_c; public Nested(C c) { this_c = c; } public void G() { Console.WriteLine(this_c.i); } } } class Test { static void Main() { C c = new C(); c.F(); } }
mostra esta técnica. Uma instância de
C
cria uma instância deNested
e passa a sua própria referência "this" para o construtor deNested
, a fim de proporcionar acesso subsequente aos membros da instância deC
.Exemplo final
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 tipo que os contém, incluindo membros desse tipo que têm private
e protected
declarados como acessibilidade.
Exemplo: O exemplo
class C { private static void F() => Console.WriteLine("C.F"); public class Nested { public static void G() => F(); } } class Test { static void Main() => C.Nested.G(); }
Mostra uma classe
C
que contém uma classeNested
aninhada. Dentro deNested
, o métodoG
chama o método estáticoF
definido emC
, eF
tem acessibilidade privada declarada.Exemplo final
Um tipo aninhado também pode aceder a membros protegidos definidos num tipo base do seu tipo contenente.
Exemplo: No seguinte código
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 aninhada
Derived.Nested
acede ao método protegidoF
, definido na classe base deDerived
,Base
, chamando através de uma instância deDerived
.Exemplo final
15.3.9.7 Tipos aninhados em classes genéricas
Uma declaração de classe genérica pode conter declarações de tipos aninhados. Os parâmetros de tipo da classe anexa podem ser usados dentro dos tipos aninhados. Uma declaração de tipo aninhada pode conter parâmetros de tipo adicionais que se aplicam somente ao tipo aninhado.
Cada declaração de tipo contida em uma declaração de classe genérica é implicitamente uma declaração de tipo genérica. Ao escrever uma referência a um tipo aninhado dentro de um tipo genérico, o tipo construído que o contém, incluindo os seus argumentos de tipo, deve ser nomeado. No entanto, a partir da classe externa, o tipo aninhado pode ser utilizado sem qualificação; o tipo de instância da classe externa pode ser usado implicitamente ao construir o tipo aninhado.
Exemplo: A seguir mostramos três maneiras corretas diferentes de se referir a um tipo construído criado a partir de
Inner
; as duas primeiras são equivalentes:class Outer<T> { class Inner<U> { public static void F(T t, U u) {...} } static void F(T t) { Outer<T>.Inner<string>.F(t, "abc"); // These two statements have Inner<string>.F(t, "abc"); // the same effect Outer<int>.Inner<string>.F(3, "abc"); // This type is different Outer.Inner<string>.F(t, "abc"); // Error, Outer needs type arg } }
Exemplo final
Apesar de ser uma má prática de programação, um parâmetro de tipo em tipos aninhados 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 final
15.3.10 Nomes de membros reservados
15.3.10.1 Generalidades
Para facilitar a implementação em tempo de execução subjacente 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 do membro, no seu nome e no 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 subjacente em tempo de execução não faça uso dessas reservas.
Os nomes reservados não introduzem declarações, pelo que não participam na pesquisa de membros. No entanto, as assinaturas dos métodos reservados associadas a uma declaração participam na herança (§15.3.4) e podem ser ocultadas com o new
modificador (§15.3.5).
Nota: A reserva destes nomes serve três propósitos:
- Para permitir que a implementação subjacente use um identificador comum como um nome de método para obter ou definir o acesso ao recurso de linguagem C#.
- Para permitir que outros idiomas interoperem usando um identificador comum como um nome de método para obter ou definir o acesso ao recurso de linguagem C#.
- Para ajudar a garantir que a fonte aceita por um compilador em conformidade seja aceita por outro, tornando as especificidades dos nomes de membros reservados consistentes em todas as implementações C#.
Nota final
A declaração de um finalizador (§15.13) também faz com que uma assinatura seja reservada (§15.3.10.5).
Certos nomes são reservados para utilização como nomes de métodos de operadores (§15.3.10.6).
15.3.10.2 Nomes de membros reservados para propriedades
Para uma propriedade P
(§15.7) do tipo T
, as seguintes assinaturas são reservadas:
T get_P();
void set_P(T value);
Ambas as assinaturas são reservadas, mesmo que a propriedade seja apenas leitura ou apenas escrita.
Exemplo: No seguinte código
class A { public int P { get => 123; } } class B : A { public new int get_P() => 456; public new void set_P(int value) { } } class Test { static void Main() { B b = new B(); A a = b; Console.WriteLine(a.P); Console.WriteLine(b.P); Console.WriteLine(b.get_P()); } }
Uma classe
A
define uma propriedade somente leituraP
, reservando assinaturas para os métodosget_P
eset_P
.A
classB
deriva deA
e oculta ambas as assinaturas reservadas. O exemplo produz a saída:123 123 456
Exemplo final
Nomes de membros reservados para eventos
Para um evento E
(§15.8) do tipo delegado T
, as seguintes assinaturas são reservadas:
void add_E(T handler);
void remove_E(T handler);
15.3.10.4 Nomes de membros reservados aos indexadores
Para um indexador (§15.9) do tipo T
com lista 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 quando o indexador for 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 contenha um finalizador (§15.13), reserva-se a seguinte assinatura:
void Finalize();
15.3.10.6 Nomes de métodos reservados aos operadores
Os seguintes nomes de método são reservados. Enquanto muitos têm operadores correspondentes nesta especificação, alguns são reservados para uso por versões futuras, enquanto alguns são reservados para interoperabilidade com outros idiomas.
Nome do método | Operador C# |
---|---|
op_Addition |
+ (binário) |
op_AdditionAssignment |
(reservado) |
op_AddressOf |
(reservado) |
op_Assign |
(reservado) |
op_BitwiseAnd |
& (binário) |
op_BitwiseAndAssignment |
(reservado) |
op_BitwiseOr |
\| |
op_BitwiseOrAssignment |
(reservado) |
op_CheckedAddition |
(reservado para uso futuro) |
op_CheckedDecrement |
(reservado para uso futuro) |
op_CheckedDivision |
(reservado para uso futuro) |
op_CheckedExplicit |
(reservado para uso futuro) |
op_CheckedIncrement |
(reservado para uso futuro) |
op_CheckedMultiply |
(reservado para uso futuro) |
op_CheckedSubtraction |
(reservado para uso futuro) |
op_CheckedUnaryNegation |
(reservado para uso futuro) |
op_Comma |
(reservado) |
op_Decrement |
-- (prefixo e posfixo) |
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 de alargamento |
op_Increment |
++ (prefixo e posfixo) |
op_Inequality |
!= |
op_LeftShift |
<< |
op_LeftShiftAssignment |
(reservado) |
op_LessThan |
< |
op_LessThanOrEqual |
<= |
op_LogicalAnd |
(reservado) |
op_LogicalNot |
! |
op_LogicalOr |
(reservado) |
op_MemberSelection |
(reservado) |
op_Modulus |
% |
op_ModulusAssignment |
(reservado) |
op_MultiplicationAssignment |
(reservado) |
op_Multiply |
* (binário) |
op_OnesComplement |
~ |
op_PointerDereference |
(reservado) |
op_PointerToMemberSelection |
(reservado) |
op_RightShift |
>> |
op_RightShiftAssignment |
(reservado) |
op_SignedRightShift |
(reservado) |
op_Subtraction |
- (binário) |
op_SubtractionAssignment |
(reservado) |
op_True |
true |
op_UnaryNegation |
- (unário) |
op_UnaryPlus |
+ (unário) |
op_UnsignedRightShift |
(reservado para uso futuro) |
op_UnsignedRightShiftAssignment |
(reservado) |
15.4 Constantes
Uma constante é um membro de classe que representa um valor constante: um valor que pode ser calculado em tempo de compilação. Um constant_declaration introduz uma ou mais constantes de um determinado tipo.
constant_declaration
: attributes? constant_modifier* 'const' type constant_declarators ';'
;
constant_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
;
Um constant_declaration pode incluir um conjunto de atributos (§22), um new
modificador (§15.3.5) e qualquer um dos tipos permitidos de acessibilidade declarada (§15.3.6). Os atributos e modificadores aplicam-se a todos os membros declarados pelo constant_declaration. Embora as constantes sejam consideradas membros estáticos, um constant_declaration não requer nem permite um static
modificador. É um erro para o mesmo modificador aparecer várias vezes em uma declaração constante.
O tipo de uma 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 dá o valor do membro.
O tipo especificado numa declaração constante deverá ser sbyte
, byte
, short
, ushort
, int
, uint
, long
, ulong
, char
, float
, double
, decimal
, bool
, string
, um enum_type, ou um reference_type. Cada constant_expression deve produzir um valor do tipo alvo ou de um tipo que possa ser convertido para o tipo alvo através de uma conversão implícita (§10.2).
O tipo de constante deve ser pelo menos tão acessível como a própria constante (ponto 7.5.5).
O valor de uma constante é obtido numa expressão utilizando um simple_name (§12.8.4) ou um member_access (§12.8.7).
Uma constante pode ela própria participar de uma constant_expression. Assim, uma constante pode ser usada em qualquer construção que exija um constant_expression.
Nota: Exemplos de tais construções incluem
case
rótulos,goto case
instruções,enum
declarações de membro, atributos e outras declarações constantes. Nota final
Nota: Conforme descrito no §12.23, um constant_expression é uma expressão que pode ser totalmente avaliada em tempo de compilação. Uma vez que a única maneira de criar um valor não nulo de um reference_type diferente de
string
é aplicar o operadornew
, e uma vez que o operadornew
não é permitido numa constant_expression, o único valor possível para constantes de reference_types, além destring
, énull
. Nota final
Quando um nome simbólico para um valor constante é desejado, mas quando o tipo desse valor não é permitido em uma declaração constante, ou quando o valor não pode ser calculado em tempo de compilação por um constant_expression, um campo somente leitura (§15.5.3) pode ser usado em vez disso.
Nota: A semântica de versionamento de
const
ereadonly
difere (§15.5.3.3). Nota final
Uma declaração constante que declara várias constantes é equivalente a várias declarações de constantes únicas com os mesmos atributos, modificadores e tipo.
Exemplo:
class A { public const double X = 1.0, Y = 2.0, Z = 3.0; }
é equivalente a
class A { public const double X = 1.0; public const double Y = 2.0; public const double Z = 3.0; }
Exemplo final
As constantes podem depender de outras constantes dentro do mesmo programa, desde que as dependências não sejam de natureza circular.
Exemplo: No seguinte código
class A { public const int X = B.Z + 1; public const int Y = 10; } class B { public const int Z = A.Y + 1; }
Um compilador deve primeiro avaliar
A.Y
, depois avaliarB.Z
e, finalmente, avaliarA.X
, produzindo os valores10
,11
e12
.Exemplo final
Declarações constantes podem depender de constantes de outros programas, mas tais dependências só são possíveis em uma direção.
Exemplo: Referindo-se ao exemplo acima, se
A
eB
fossem declarados em programas separados, seria possívelA.X
depender deB.Z
, masB.Z
não poderia então depender simultaneamente deA.Y
. Exemplo final
15.5 Campos
15.5.1 Generalidades
Um campo é um membro que representa uma variável associada a um objeto ou classe. Um field_declaration introduz um ou mais campos de um determinado tipo.
field_declaration
: attributes? field_modifier* type variable_declarators ';'
;
field_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'readonly'
| 'volatile'
| unsafe_modifier // unsafe code support
;
variable_declarators
: variable_declarator (',' variable_declarator)*
;
variable_declarator
: identifier ('=' variable_initializer)?
;
unsafe_modifier (§23.2) só está disponível em código não seguro (§23).
Um field_declaration pode incluir um conjunto de atributos (§22), um new
modificador (§15.3.5), uma combinação válida dos quatro modificadores de acesso (§15.3.6) e um static
modificador (§15.5.2). Além disso, um field_declaration pode incluir um readonly
modificador (§15.5.3) ou um volatile
modificador (§15.5.4), mas não ambos. Os atributos e modificadores aplicam-se a todos os membros declarados pelo field_declaration. É um erro para o mesmo modificador aparecer várias vezes em um field_declaration.
O tipo de uma declaração_de_campo especifica o tipo dos membros que a declaração introduz. O tipo é seguido por uma lista de variable_declarators, cada um dos quais introduz um novo membro. Um variable_declarator consiste em um identificador que nomeia esse membro, opcionalmente seguido por um token "=
" e um variable_initializer (§15.5.6) que dá o valor inicial desse membro.
O tipo de campo deve ser pelo menos tão acessível como o próprio campo (ponto 7.5.5).
O valor de um campo é obtido numa expressão utilizando um simple_name (§12.8.4), um member_access (§12.8.7) ou um base_access (§12.8.15). O valor de um campo que não é somente de leitura é modificado usando uma atribuição (§12.21). O valor de um campo que não seja só de leitura pode ser tanto obtido quanto modificado usando operadores de incremento e decréscimo de sufixo (§12.8.16) e de prefixo (§12.9.6).
Uma declaração de campo que declara vários campos é equivalente a várias declarações de campos únicos com os mesmos atributos, modificadores e tipo.
Exemplo:
class A { public static int X = 1, Y, Z = 100; }
é equivalente a
class A { public static int X = 1; public static int Y; public static int Z = 100; }
Exemplo final
15.5.2 Campos estáticos e de instância
Quando uma declaração de campo inclui um static
modificador, os campos introduzidos pela declaração são campos estáticos. Quando nenhum static
modificador está presente, os campos introduzidos pela declaração são campos de instância. Campos estáticos e campos de instância são dois dos vários tipos de variáveis (§9) suportados pelo C# e, às vezes, são referidos como variáveis estáticas e variáveis de instância, respectivamente.
Conforme explicado no §15.3.8, cada instância de uma classe contém um conjunto completo dos campos de instância da classe, enquanto há apenas um conjunto de campos estáticos para cada classe não genérica ou tipo construído fechado, independentemente do número de instâncias da classe ou do tipo construído fechado.
15.5.3 Campos somente leitura
15.5.3.1 Generalidades
Quando uma field_declaration inclui um readonly
modificador, os campos introduzidos pela declaração são campos somente leitura. Atribuições diretas para campos somente leitura só podem ocorrer como parte dessa declaração ou em um construtor de instância ou construtor estático na mesma classe. (Um campo somente leitura pode ser atribuído várias vezes nesses contextos.) Especificamente, as atribuições diretas a um campo somente leitura são permitidas somente nos seguintes contextos:
- No declarador_de_variável que introduz o campo (incluindo um inicializador_de_variável na declaração).
- Para um campo de instância, nos construtores de instância da classe que contém a declaração de campo; para um campo estático, no construtor estático da classe que contém a declaração de campo. Estes são também os únicos contextos em que é válido passar um campo somente leitura como um parâmetro de saída ou referência.
Tentar atribuir a um campo somente leitura ou passá-lo como um parâmetro de saída ou referência em qualquer outro contexto é um erro em tempo de compilação.
15.5.3.2 Usando campos estáticos apenas 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 seguinte código
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 membros
Black
,White
,Red
,Green
eBlue
não podem ser declarados como membros const porque os seus valores não podem ser calculados em tempo de compilação. No entanto, declará-lasstatic readonly
tem o mesmo efeito.Exemplo final
15.5.3.3 Versionamento de constantes e campos estáticos readonly
Constantes e campos somente leitura têm semânticas de versionamento binário diferentes. Quando uma expressão faz referência a uma constante, o valor da constante é obtido em tempo de compilação, mas quando uma expressão faz referência a um campo somente leitura, o valor do campo não é obtido até o tempo de execução.
Exemplo: Considere um aplicativo que consiste em dois programas separados:
namespace Program1 { public class Utils { public static readonly int x = 1; } }
e ainda
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 um campostatic readonly
, a saída do valor pela instruçãoConsole.WriteLine
não é conhecida durante a compilação, mas é obtida durante a execução. Assim, se o valor deX
for alterado eProgram1
for recompilado, aConsole.WriteLine
instrução produzirá o novo valor, mesmoProgram2
que não seja recompilado. No entanto, pelo facto deX
ser uma constante, o valor deX
teria sido obtido no momento em queProgram2
foi compilado, e permaneceria inalterado por mudanças emProgram1
atéProgram2
ser recompilado.Exemplo final
15.5.4 Campos voláteis
Quando uma field_declaration inclui um volatile
, os campos introduzidos por essa declaração são campos voláteis. Para campos não voláteis, técnicas de otimização que reordenam instruções podem levar a resultados inesperados e imprevisíveis em programas multi-threaded que acessam campos sem sincronização, como o fornecido pelo lock_statement (§13.13). Essas otimizações podem ser executadas pelo compilador, pelo sistema de tempo de execução ou pelo hardware. Para campos voláteis, essas otimizações de reordenação são restritas:
- Uma leitura de um campo volátil é chamada de leitura volátil. Uma leitura volátil tem "semântica de aquisição"; ou seja, é garantida que ocorra antes de quaisquer referências à memória que ocorram depois dela na sequência de instruções.
- Uma escrita de um campo volátil é chamada de escrita volátil. Uma escrita volátil tem "semântica de liberação"; ou seja, garante-se que ocorre após quaisquer referências de memória anteriores à instrução de escrita na sequência de instruções.
Essas restrições garantem que todos os threads observarão gravações voláteis executadas por qualquer outro thread na ordem em que foram executadas. Uma implementação em conformidade não é obrigada a fornecer uma ordenação total única de escritas voláteis como visto a partir de todos os threads de execução. O tipo de campo volátil deve ser um dos seguintes:
- A tipo_de_referência.
- Um type_parameter conhecido por ser um tipo de referência (§15.2.5).
- O tipo
byte
,sbyte
,short
,ushort
,int
,uint
,char
,float
,bool
,System.IntPtr
, ouSystem.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 o resultado:
result = 143
Neste exemplo, o método
Main
inicia um novo thread que executa o métodoThread2
. Este 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
e, em seguida, lê o camporesult
. Uma vezfinished
declaradovolatile
, o fio condutor deve ler o valor143
do camporesult
. Se o campofinished
não tivesse sido declaradovolatile
, então seria permitido que o armazenamentoresult
fosse visível para o thread de execução principal após o armazenamento emfinished
, e, portanto, que o thread de execução principal lesse o valor 0 do camporesult
. A declaraçãofinished
como campovolatile
evita tais incoerências.Exemplo final
15.5.5 Inicialização de campo
O valor inicial de um campo, seja ele um campo estático ou um campo de instância, é o valor padrão (§9.3) do tipo do campo. Não é possível observar o valor de um campo antes que essa inicialização padrão tenha ocorrido e, portanto, um campo nunca é "não inicializado".
Exemplo: O exemplo
class Test { static bool b; int i; static void Main() { Test t = new Test(); Console.WriteLine($"b = {b}, i = {t.i}"); } }
produz a saída
b = False, i = 0
porque
b
ei
são ambos inicializados automaticamente para valores padrão.Exemplo final
15.5.6 Inicializadores variáveis
15.5.6.1 Generalidades
As declarações de campo podem incluir variable_initializers. Para campos estáticos, inicializadores de variáveis correspondem a instruções de atribuição que são executadas durante a inicialização da classe. Por exemplo, campos, inicializadores de variáveis correspondem a instruções de atribuição que são executadas quando uma instância da classe é criada.
Exemplo: O exemplo
class Test { static double x = Math.Sqrt(2.0); int i = 100; string s = "Hello"; static void Main() { Test a = new Test(); Console.WriteLine($"x = {x}, i = {a.i}, s = {a.s}"); } }
produz a saída
x = 1.4142135623730951, i = 100, s = Hello
porque uma atribuição a
x
ocorre quando inicializadores de campo estáticos são executados e atribuições ai
es
ocorrem quando os inicializadores de campo de instância são executados.Exemplo final
A inicialização do valor padrão descrita no §15.5.5 ocorre para todos os campos, incluindo campos que têm inicializadores variáveis. Assim, quando uma classe é inicializada, todos os campos estáticos nessa classe são primeiro inicializados com seus valores padrão e, em seguida, os inicializadores de campo estático são executados em ordem textual. Da mesma forma, quando uma instância de uma classe é criada, todos os campos de instância nessa instância são primeiro inicializados com seus valores padrão e, em seguida, os inicializadores do campo de instância são executados em ordem textual. Quando há declarações de campo em várias declarações de tipo parcial para o mesmo tipo, a ordem das partes não é especificada. No entanto, dentro de cada parte, os inicializadores de campo são executados por ordem.
É possível que campos estáticos com inicializadores variáveis sejam observados em seu estado de valor padrão.
Exemplo: No entanto, isso é fortemente desencorajado por uma questão de estilo. O exemplo
class Test { static int a = b + 1; static int b = a + 1; static void Main() { Console.WriteLine($"a = {a}, b = {b}"); } }
exibe esse comportamento Apesar das definições circulares de
a
eb
, o programa é válido. Isso resulta no resultadoa = 1, b = 2
porque os campos
a
estáticos eb
são inicializados para0
(o valor padrão paraint
) antes de seus inicializadores serem executados. Quando o inicializador paraa
é executado, o valor deb
é zero e, portanto,a
é inicializado para1
. Quando o inicializador parab
é executado, o valor de a já é1
, e assimb
é inicializado para2
.Exemplo final
15.5.6.2 Inicialização de campo estático
Os inicializadores da variável de campo estático de uma classe correspondem a uma sequência de atribuições que são executadas na ordem textual em que aparecem na declaração de classe (§15.5.6.1). Dentro de uma classe parcial, o significado de "ordem textual" é especificado pelo §15.5.6.1. Se existir um construtor estático (§15.12) na classe, a execução dos inicializadores de campo estático ocorre imediatamente antes de executar esse construtor estático. Caso contrário, os inicializadores de campo estático são executados em um momento dependente da implementação antes do primeiro uso de um campo estático dessa classe.
Exemplo: O exemplo
class Test { static void Main() { Console.WriteLine($"{B.Y} {A.X}"); } public static int F(string s) { Console.WriteLine(s); return 1; } } class A { public static int X = Test.F("Init A"); } class B { public static int Y = Test.F("Init B"); }
pode produzir a seguinte saída:
Init A Init B 1 1
ou a saída:
Init B Init A 1 1
Como a execução do inicializador de
X
e do inicializador deY
pode ocorrer em qualquer ordem, eles são apenas obrigados 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 produção deve ser:
Init B Init A 1 1
porque as regras para quando os construtores estáticos são executados (conforme definido no §15.12) estabelecem que o construtor estático de
B
(e, portanto, os inicializadores de campo estático deB
) deve ser executado antes do construtor estático e dos inicializadores de campo deA
.Exemplo final
15.5.6.3 Inicialização de campo de instância
Os inicializadores da variável de campo de instância de uma classe correspondem a uma sequência de atribuições que são executadas imediatamente após a entrada em qualquer um dos construtores de instância (§15.11.3) dessa classe. Dentro de uma classe parcial, o significado de "ordem textual" é especificado pelo §15.5.6.1. Os inicializadores de variáveis são executados na ordem textual em que aparecem na declaração de classe (§15.5.6.1). O processo de criação e inicialização da instância de classe é descrito mais detalhadamente no §15.11.
Um inicializador de variável para um campo de instância não pode fazer referência à instância que está sendo criada. Assim, é um erro em tempo de compilação fazer referência a this
em um inicializador de variável, tal como é um erro em tempo de compilação para um inicializador de variável fazer referência a qualquer membro de instância através de um simple_name.
Exemplo: No seguinte código
class A { int x = 1; int y = x + 1; // Error, reference to instance member of this }
O inicializador da variável para
y
resulta num erro em tempo de compilação porque faz referência a um membro da instância que está a ser criada.Exemplo final
15.6 Métodos
15.6.1 Generalidades
Um método é um membro que implementa um cálculo ou ação que pode ser executada por um objeto ou classe. Os métodos são declarados utilizando method_declarations:
method_declaration
: attributes? method_modifiers return_type method_header method_body
| attributes? ref_method_modifiers ref_kind ref_return_type method_header
ref_method_body
;
method_modifiers
: method_modifier* 'partial'?
;
ref_kind
: 'ref'
| 'ref' 'readonly'
;
ref_method_modifiers
: ref_method_modifier*
;
method_header
: member_name '(' parameter_list? ')'
| member_name type_parameter_list '(' parameter_list? ')'
type_parameter_constraints_clause*
;
method_modifier
: ref_method_modifier
| 'async'
;
ref_method_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| 'readonly' // direct struct members only
| unsafe_modifier // unsafe code support
;
return_type
: ref_return_type
| 'void'
;
ref_return_type
: type
;
member_name
: identifier
| interface_type '.' identifier
;
method_body
: block
| '=>' null_conditional_invocation_expression ';'
| '=>' expression ';'
| ';'
;
ref_method_body
: block
| '=>' 'ref' variable_reference ';'
| ';'
;
Notas gramaticais:
- unsafe_modifier (§23.2) só está disponível em código não seguro (§23).
- Ao reconhecer um method_body, se forem aplicáveis tanto a alternativa null_conditional_invocation_expression como a expressão, optar-se-á pela primeira.
Nota: A sobreposição e prioridade entre as alternativas aqui é apenas por conveniência descritiva, as regras gramaticais poderiam ser elaboradas para remover a sobreposição. ANTLR, e outros sistemas gramaticais, adotam a mesma conveniência e, portanto , method_body tem a semântica especificada automaticamente. Nota final
Um method_declaration pode incluir um conjunto de atributos (§22) e um dos tipos permitidos de acessibilidade declarada (§15.3.6), new
(§15.3.5), static
(§15.6.3), virtual
(§15.6.4), override
(§15.6.5), sealed
(§15.6.6), abstract
(§15.6.7), extern
(§15.6.8) e async
(§15.14). Além disso, um method_declaration contido diretamente por um struct_declaration pode incluir o readonly
modificador (§16.4.12).
Uma declaração tem uma combinação válida de modificadores se todos os itens a seguir forem verdadeiros:
- A declaração inclui uma combinação válida de modificadores de acesso (§15.3.6).
- A declaração não inclui o mesmo modificador várias vezes.
- A declaração inclui, no máximo, um dos seguintes modificadores:
static
,virtual
, eoverride
. - A declaração inclui, no máximo, um dos seguintes modificadores:
new
eoverride
. - Se a declaração incluir o
abstract
modificador, a declaração 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
,private
,virtual
sealed
, ,override
, ,abstract
ouextern
.
Os métodos são classificados de acordo com o que, se houver, devolvem.
- Se
ref
estiver presente, o método é returns-by-ref e retorna uma referência variável, que é opcionalmente somente leitura; - Caso contrário, se return_type for
void
, o método é returns-no-value e não retorna um valor; - Caso contrário, o método é returns-by-value e retorna um valor.
A return_type de uma declaração de método returns-by-value ou returns-no-value especifica o tipo do resultado, se houver, retornado pelo método. Apenas um método de retorno sem valor pode incluir o partial
modificador (§15.6.9). Se a declaração incluir o async
modificador, então return_type deve ser void
ou o método retorna por valor e o tipo de retorno é um tipo de tarefa (§15.14.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. As type_parameter_constraints_clause opcionais especificam as restrições para os parâmetros de tipo.
Uma method_declaration genérica para uma implementação explícita de membro da interface não deve ter nenhuma type_parameter_constraints_clause; a declaração herda quaisquer restrições do método de interface.
Da mesma forma, uma declaração de método com o override
modificador não deve ter qualquer type_parameter_constraints_clause e as restrições dos parâmetros de tipo do método são herdadas do método virtual que está a ser substituído.
O member_name especifica o nome do método. A menos que o método seja uma implementação explícita de membro da interface (§18.6.2), o member_name é simplesmente um identificador.
Para uma implementação explícita de membro da interface, o member_name consiste em um interface_type seguido por um ".
" e um identificador. Neste caso, a declaração não deve incluir modificadores para além de (eventualmente) extern
ou async
.
O parameter_list opcional especifica os parâmetros do método (§15.6.2).
O return_type ou ref_return_type, bem como cada um dos tipos referidos na parameter_list de um método, devem ser, pelo menos, tão acessíveis como o próprio método (§7.5.5).
A method_body de um método returns-by-value ou returns-no-value é representada por 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 um null_conditional_invocation_expression ou expressão, seguido por 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 apenas em um ponto e vírgula. Para métodos parciais, o method_body pode consistir de um ponto e vírgula, um corpo em 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 corpo_do_método consistir em um ponto e vírgula, a declaração não deverá incluir o modificador async
.
O ref_method_body de um método que retorna por referência é 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
, uma variable_reference e um ponto e vírgula; denota uma única variable_reference a ser avaliada quando o método for invocado.
Para métodos abstract e extern, o ref_method_body consiste simplesmente de um ponto e vírgula; para todos os outros métodos, o ref_method_body é ou 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 no seu nome, no número dos seus parâmetros de tipo, e no número, modificadores de modo de parâmetro (§15.6.2.1), e tipos dos seus parâmetros. O tipo de retorno não faz parte da assinatura de um método, nem os nomes dos parâmetros, os nomes dos parâmetros de tipo ou as restrições. Quando um tipo de parâmetro faz referência a um parâmetro de tipo do método, a posição ordinal do parâmetro type (não o nome do parâmetro type) é usada para equivalência de tipo.
O nome de um método deve diferir dos nomes de todos os outros métodos não declarados na mesma classe. Além disso, a assinatura de um método deve diferir das assinaturas de todos os outros métodos declarados na mesma classe, e dois métodos declarados na mesma classe não devem ter assinaturas que difiram apenas por in
, out
e ref
.
Os type_parameters do método estão abrangidos ao longo de toda a method_declaration e podem ser utilizados para formar tipos durante esse âmbito em return_type ou ref_return_type, method_body ou ref_method_body, e type_parameter_constraints_clauses, mas não nos atributos.
Todos os parâmetros e parâmetros-tipo devem ter nomes diferentes.
15.6.2 Parâmetros do método
15.6.2.1 Generalidades
Os parâmetros de um método, se houver, são declarados pelo parameter_list do método.
parameter_list
: fixed_parameters
| fixed_parameters ',' parameter_array
| parameter_array
;
fixed_parameters
: fixed_parameter (',' fixed_parameter)*
;
fixed_parameter
: attributes? parameter_modifier? type identifier default_argument?
;
default_argument
: '=' expression
;
parameter_modifier
: parameter_mode_modifier
| 'this'
;
parameter_mode_modifier
: 'ref'
| 'out'
| 'in'
;
parameter_array
: attributes? 'params' array_type identifier
;
A lista de parâmetros consiste em um ou mais parâmetros separados por vírgula, dos quais apenas o último pode ser um parameter_array.
Um fixed_parameter consiste em um conjunto opcional de atributos (§22); um modificador opcional in
, out
, ref
ou this
; um tipo; um identificador; e um argumento padrão opcional. Cada fixed_parameter declara um parâmetro do tipo dado com o nome próprio. O modificador this
determina o método como um método de extensão e é permitido apenas no primeiro parâmetro de um método estático em uma classe estática não genérica e não aninhada. Se o parâmetro for um struct
tipo ou um parâmetro de tipo restrito a um struct
, o this
modificador pode ser combinado com o ref
ou in
modificador, mas não com o out
modificador. Os métodos de extensão são descritos em mais pormenor no ponto 15.6.10. Um fixed_parameter com um default_argument é conhecido como um parâmetro opcional, enquanto um fixed_parameter sem um default_argument é um parâmetro necessário. Um parâmetro obrigatório não deve aparecer depois de um parâmetro opcional numa parameter_list.
Um parâmetro com um ref
, out
ou this
modificador não pode ter um default_argument. Um parâmetro de entrada pode ter um default_argument. A expressão num default_argument deve ser uma das seguintes:
- uma expressão_constante
- uma expressão do formulário
new S()
ondeS
é um tipo de valor - uma expressão do formulário
default(S)
ondeS
é um tipo de valor
A expressão deve ser implicitamente convertida por uma conversão de identidade ou anulável para o tipo do parâmetro.
Se parâmetros opcionais ocorrerem em uma declaração de método parcial de implementação (§15.6.9), uma implementação explícita de membro da interface (§18.6.2), uma declaração de indexador de parâmetro único (§15.9), ou em uma declaração de operador (§15.10.1), um compilador deve dar um aviso, uma vez que esses membros nunca podem ser invocados de uma forma que permita que os argumentos sejam omitidos.
Um parameter_array consiste em um conjunto opcional de atributos (§22), um params
modificador, um array_type e um identificador. Uma matriz de parâmetros declara um único parâmetro do tipo de matriz determinado com o nome fornecido. A array_type de uma matriz de parâmetros deve ser um tipo de matriz unidimensional (§17.2). Em uma invocação de método, uma matriz de parâmetros permite que um único argumento do tipo de matriz determinado seja especificado ou permite que zero ou mais argumentos do tipo de elemento de matriz sejam especificados. As matrizes de parâmetros são descritas mais pormenorizadamente no §15.6.2.4.
Um parameter_array pode ocorrer após um parâmetro opcional, mas não pode ter um valor padrão – a omissão de argumentos para um parameter_array resultaria, em vez disso, na criação de uma matriz vazia.
Exemplo: O seguinte ilustra diferentes tipos de parâmetros:
void M<T>( ref int i, decimal d, bool b = false, bool? n = false, string s = "Hello", object o = null, T t = default(T), params int[] a ) { }
Na 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 final
Uma declaração de método cria um espaço de declaração separado (§7.3) para parâmetros e parâmetros de tipo. Os nomes são introduzidos neste espaço de declaração pela lista de parâmetros de tipo e pela lista de parâmetros do método. O corpo do método, se houver, é considerado encaixado neste 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 pelos seus identificadores em expressões de 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 (ponto 15.6.2.3.3).
- Matrizes de parâmetros (§15.6.2.4).
Nota: Conforme descrito no §7.6, os modificadores
in
,out
eref
fazem parte da assinatura de um método, mas o modificadorparams
não. Nota final
15.6.2.2 Parâmetros de valor
Um parâmetro declarado sem modificadores é um parâmetro de valor. Um parâmetro value é uma variável local que obtém seu valor inicial do argumento correspondente fornecido na chamada do método.
Para regras de atribuição definitiva, ver §9.2.5.
O argumento correspondente numa invocação de método deve ser uma expressão implicitamente convertível (§10.2) para o tipo de parâmetro.
Permite-se a um método atribuir novos valores a um parâmetro de valor. Tais atribuições afetam apenas o local de armazenamento local representado pelo parâmetro value — elas não têm efeito sobre o argumento real dado na chamada do método.
15.6.2.3 Parâmetros de subreferência
15.6.2.3.1 Generalidades
Os parâmetros de entrada, saída e referência são parâmetros por referência. Um parâmetro por referência é uma variável de referência local (§9.7), o referente inicial é obtido a partir do argumento correspondente fornecido na invocação do método.
Nota: O referente de um parâmetro por referência pode ser alterado usando o operador ref assignment (
= ref
).
Quando um parâmetro é um parâmetro por referência, o argumento correspondente numa invocação de método deve consistir na palavra-chave correspondente, in
, ref
, ou out
, seguida de uma referência_de_variável (§9.5) do mesmo tipo que o parâmetro. No entanto, quando o parâmetro é um in
parâmetro, o argumento pode ser uma expressão para a qual existe uma conversão implícita (§10.2) dessa expressão de argumento para o tipo do parâmetro correspondente.
Os parâmetros de sub-referência não são permitidos em funções declaradas como iterador (§15.15) ou função assíncrona (§15.14).
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 in
modificador é um parâmetro de entrada. O argumento correspondente a um parâmetro de entrada é uma variável existente no ponto da chamada do método ou uma variável criada pela implementação (§12.6.2.3) na chamada do método. Para regras de atribuição definitiva, ver §9.2.8.
É um erro em tempo de compilação para modificar o valor de um parâmetro de entrada.
Nota: O principal objetivo dos parâmetros de entrada é a eficiência. Quando o tipo de um parâmetro de método é uma estrutura grande (em termos de requisitos de memória), é útil ser capaz de evitar copiar todo o valor do argumento ao chamar o método. Os parâmetros de entrada permitem que os métodos se refiram a valores existentes na memória, ao mesmo tempo em que fornecem proteção contra alterações indesejadas nesses valores. Nota final
15.6.2.3.3. Parâmetros de referência
Um parâmetro declarado com um ref
modificador é um parâmetro de referência. Para regras de atribuição definitiva, ver §9.2.6.
Exemplo: O exemplo
class Test { static void Swap(ref int x, ref int y) { int temp = x; x = y; y = temp; } static void Main() { int i = 1, j = 2; Swap(ref i, ref j); Console.WriteLine($"i = {i}, j = {j}"); } }
produz a saída
i = 2, j = 1
Para a invocação de
Swap
inMain
,x
representai
ey
representaj
. Assim, a invocação tem o efeito de trocar os valores dei
ej
.Exemplo final
Exemplo: No seguinte código
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
emG
passa uma referência paras
em ambosa
eb
. Assim, para essa invocação, os nomess
,a
eb
todos se referem ao mesmo local de armazenamento, e as três atribuições modificam o campos
de instância .Exemplo final
Para um struct
tipo, dentro de um método de instância, um acessador de instância (§12.2.1) ou um construtor de instância com um inicializador de construtor, a palavra-chave this
se comporta exatamente como um parâmetro de referência do tipo struct (§12.8.14).
15.6.2.3.4 Parâmetros de saída
Um parâmetro declarado com um out
modificador é um parâmetro de saída. Para regras de atribuição definitiva, ver §9.2.7.
Um método declarado como método parcial (§15.6.9) não deve ter parâmetros de saída.
Nota: Os parâmetros de saída são normalmente usados em métodos que produzem vários valores de retorno. Nota final
Exemplo:
class Test { static void SplitPath(string path, out string dir, out string name) { int i = path.Length; while (i > 0) { char ch = path[i - 1]; if (ch == '\\' || ch == '/' || ch == ':') { break; } i--; } dir = path.Substring(0, i); name = path.Substring(i); } static void Main() { string dir, name; SplitPath(@"c:\Windows\System\hello.txt", out dir, out name); Console.WriteLine(dir); Console.WriteLine(name); } }
O exemplo produz a saída:
c:\Windows\System\ hello.txt
Observe que as
dir
variáveis ename
podem ser desatribuídas antes de serem passadas paraSplitPath
, e que elas são consideradas definitivamente atribuídas após a chamada.Exemplo final
15.6.2.4 Matrizes de parâmetros
Um parâmetro declarado com um params
modificador é uma matriz de parâmetros. Se uma lista de parâmetros incluir uma matriz de parâmetros, será o último parâmetro da lista e será do tipo de matriz unidimensional.
Exemplo: Os tipos
string[]
estring[][]
pode ser usado como o tipo de uma matriz de parâmetros, mas o tipostring[,]
não pode. Exemplo final
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 uma das duas maneiras em uma chamada de método:
- O argumento dado para uma matriz de parâmetros pode ser uma única expressão que é implicitamente conversível (§10.2) para o tipo de matriz de parâmetros. Neste caso, a matriz de parâmetros age precisamente como um parâmetro de valor.
- Como alternativa, a invocação pode especificar zero ou mais argumentos para a matriz de parâmetros, onde cada argumento é uma expressão que é implicitamente conversível (§10.2) para o tipo de elemento da matriz de parâmetros. Nesse caso, a invocação cria uma instância do tipo de matriz de parâmetros com um comprimento correspondente ao número de argumentos, inicializa os elementos da instância de matriz com os valores de argumento fornecidos e usa a instância de matriz recém-criada como o argumento real.
Exceto para permitir um número variável de argumentos em uma invocação, uma matriz de parâmetros é precisamente equivalente a um parâmetro de valor (§15.6.2.2) do mesmo tipo.
Exemplo: O exemplo
class Test { static void F(params int[] args) { Console.Write($"Array contains {args.Length} elements:"); foreach (int i in args) { Console.Write($" {i}"); } Console.WriteLine(); } static void Main() { int[] arr = {1, 2, 3}; F(arr); F(10, 20, 30, 40); F(); } }
produz a saída
Array contains 3 elements: 1 2 3 Array contains 4 elements: 10 20 30 40 Array contains 0 elements:
A primeira invocação de
F
simplesmente passa a matrizarr
como um parâmetro de valor. A segunda invocação de F cria automaticamente um array de quatro elementos com os valores de elementos fornecidos e passa essa instância de array como parâmetro de valor. Da mesma forma, a terceira invocação deF
cria umint[]
de elemento zero e passa essa instância como um parâmetro de valor. A segunda e terceira invocações equivalem precisamente à escrita:F(new int[] {10, 20, 30, 40}); F(new int[] {});
Exemplo final
Ao executar a resolução de sobrecarga, um método com uma matriz de parâmetros pode ser aplicável, tanto na sua forma normal como na sua forma expandida (§12.6.4.2). A forma expandida de um método só está disponível se a forma normal do método não for aplicável e apenas se um método aplicável com a mesma assinatura que o formulário expandido não for já declarado no mesmo tipo.
Exemplo: O exemplo
class Test { static void F(params object[] a) => Console.WriteLine("F(object[])"); static void F() => Console.WriteLine("F()"); static void F(object a0, object a1) => Console.WriteLine("F(object,object)"); static void Main() { F(); F(1); F(1, 2); F(1, 2, 3); F(1, 2, 3, 4); } }
produz a saída
F() F(object[]) F(object,object) F(object[]) F(object[])
No exemplo, duas das possíveis formas expandidas do método com uma matriz de parâmetros já estão incluídas na classe como métodos regulares. Essas formas expandidas não são, portanto, consideradas ao executar a resolução de sobrecarga, e a primeira e terceira invocações de método, portanto, selecionam os métodos regulares. Quando uma classe declara um método com uma matriz de parâmetros, não é incomum incluir também alguns dos formulários expandidos como métodos regulares. Ao fazer isso, é possível evitar a alocação de uma instância de matriz que ocorre quando uma forma expandida de um método com uma matriz de parâmetros é invocada.
Exemplo final
Uma matriz é um tipo de referência, portanto, o valor passado para uma matriz de parâmetros pode ser
null
.Exemplo: O exemplo:
class Test { static void F(params string[] array) => Console.WriteLine(array == null); static void Main() { F(null); F((string) null); } }
produz o resultado:
True False
A segunda invocação produz
False
porque é equivalente aF(new string[] { null })
e passa uma matriz contendo uma única referência nula.Exemplo final
Quando o tipo de uma matriz de parâmetros é object[]
, surge uma ambiguidade potencial entre a forma normal do método e a forma expandida para um único object
parâmetro. A razão para a ambiguidade é que um object[]
é implicitamente conversível em tipo object
. No entanto, a ambiguidade não apresenta qualquer problema, uma vez que pode ser resolvida através da inserção de um elenco, se necessário.
Exemplo: O exemplo
class Test { static void F(params object[] args) { foreach (object o in args) { Console.Write(o.GetType().FullName); Console.Write(" "); } Console.WriteLine(); } static void Main() { object[] a = {1, "Hello", 123.456}; object o = a; F(a); F((object)a); F(o); F((object[])o); } }
produz a saída
System.Int32 System.String System.Double System.Object[] System.Object[] System.Int32 System.String System.Double
Na primeira e última invocações de
F
, a forma normal 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 deF
, e o argumento é passado como um parâmetro de valor regular. Na segunda e terceira invocações, a forma normal deF
não é aplicável porque não existe conversão implícita do tipo de argumento para o tipo de parâmetroF
(o tipoobject[]
não pode ser implicitamente convertido para o tipo ). No entanto, a forma expandida deF
é aplicável, por isso é selecionada por resolução de sobrecarga. Como resultado, um elementoobject[]
único é criado pela invocação, e o único elemento da matriz é inicializado com o valor de argumento dado (que por si só é uma referência a umobject[]
).Exemplo final
15.6.3 Métodos estáticos e de instância
Quando uma declaração de método inclui um static
modificador, esse método é dito ser um método estático. Quando nenhum static
modificador está presente, o método é dito ser um método de instância.
Um método estático não opera em uma instância específica, e é um erro em tempo de compilação referir-se a this
dentro de um método estático.
Um método de instância opera em uma determinada instância de uma classe, e essa instância pode ser acessada como this
(§12.8.14).
As diferenças entre membros estáticos e de instância são discutidas mais detalhadamente no §15.3.8.
15.6.4 Métodos virtuais
Quando uma declaração de método de instância inclui um modificador virtual, esse método é dito ser um método virtual. Quando nenhum modificador virtual está presente, o método é dito ser um método não-virtual.
A implementação de um método não virtual é invariável: A implementação é a mesma se o método é invocado em uma instância da classe na qual é declarado ou uma instância de uma classe derivada. Em contraste, a implementação de um método virtual pode ser substituída por classes derivadas. O processo de substituir a implementação de um método virtual herdado é conhecido como a substituição desse método (§15.6.5).
Em uma chamada de método virtual, o tipo de tempo de execução da instância para a qual essa invocação ocorre determina a implementação real do método a ser invocada. Em uma invocação de método não virtual, o tipo de tempo de compilação da instância é o fator determinante. Em termos precisos, quando um método nomeado N
é invocado com uma lista de argumentos A
em uma instância com um tipo de tempo de compilação C
e um tipo de tempo de execução R
(onde R
é C
ou uma classe derivada de C
), a invocação é processada da seguinte maneira:
- Em tempo de ligação, a resolução de sobrecarga é aplicada a
C
,N
eA
, para selecionar um métodoM
específico do conjunto de métodos declarados e herdados porC
. Isto é descrito no §12.8.10.2. - Então, 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
com respeito 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
contém a declaração virtual de introdução deM
, então esta é a implementação mais derivada deM
com relação aR
. - Caso contrário, se
R
contiver uma substituição deM
, então esta é a implementação mais derivada deM
com relação aR
. - Caso contrário, a implementação mais derivada de
M
com relação aR
é a mesma que a implementação mais derivada deM
com relação à classe de 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
introduz um métodoF
não virtual e um métodoG
virtual. A classeB
introduz um novo métodoF
não virtual, ocultando assim o método 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
), não o tipo de tempo de compilação da instância (que éA
), determina a implementação real do método a ser invocada.Exemplo final
Como os métodos têm permissão para ocultar métodos herdados, é possível que uma classe contenha vários métodos virtuais com a mesma assinatura. Isso não apresenta um problema de ambiguidade, uma vez que todos, exceto o método mais derivado, estão ocultos.
Exemplo: No seguinte código
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 eD
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 emD
substitui o método introduzido pelaC
, e não é possível paraD
substituir o método introduzido pelaA
. O exemplo produz a saída:B.F B.F D.F D.F
Note que é possível invocar o método virtual oculto ao acessar uma instância de
D
por meio de um tipo menos derivado no qual o método não está oculto.Exemplo final
15.6.5 Métodos de sobreposição
Quando uma declaração de método de instância inclui um override
modificador, o método é dito ser um método de substituição. Um método de substituição substitui um método virtual herdado com a mesma assinatura. Enquanto uma declaração de método virtual introduz um novo método, uma declaração de método de substituição especializa um método virtual herdado existente, fornecendo uma nova implementação desse método.
O método substituído por uma declaração de sobreposição é conhecido como o método base substituído. Para um método de sobreposição M
declarado numa classe C
, o método base substituído é determinado examinando cada classe base de C
, começando com a classe base direta de C
e continuando com cada classe base direta sucessiva, até que, num determinado tipo de classe base, seja localizado pelo menos um método acessível que possua a mesma assinatura que M
após a substituição dos argumentos de tipo. Para efeitos de localização do método de base substituído, um método é considerado acessível se for public
, se for protected
, se for protected internal
, ou se for 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 tal método base substituído. Esta restrição só tem efeito se o tipo de classe base for um tipo construído em que a substituição de argumentos de tipo torna a assinatura de dois métodos a mesma.
- A função base substituída é uma função virtual, abstrata ou sobreposta. Em outras palavras, o método base substituído não pode ser estático ou não virtual.
- O método base substituído não é um método selado.
- Existe uma conversão de identidade entre o tipo de retorno do método base substituído e o método de substituição.
- A declaração de substituição e o método base substituído têm a mesma acessibilidade declarada. Em outras palavras, uma declaração de substituição não pode alterar a acessibilidade do método virtual. No entanto, se o método de base substituído for protegido internamente e for declarado em um assembly diferente do assembly que contém a declaração de substituição, a acessibilidade declarada da declaração de substituição será protegida.
- A declaração de substituição não especifica qualquer type_parameter_constraints_clause. Em vez disso, as restrições são herdadas do método base substituído. As restrições que são parâmetros de tipo no método substituído podem ser substituídas por argumentos de tipo na restrição herdada. Isso pode levar a restrições que não são válidas quando especificadas explicitamente, como tipos de valor ou tipos selados.
Exemplo: O seguinte demonstra como as regras de substituição funcionam para classes genéricas:
abstract class C<T> { public virtual T F() {...} public virtual C<T> G() {...} public virtual void H(C<T> x) {...} } class D : C<string> { public override string F() {...} // Ok public override C<string> G() {...} // Ok public override void H(C<T> x) {...} // Error, should be C<string> } class E<T,U> : C<U> { public override U F() {...} // Ok public override C<U> G() {...} // Ok public override void H(C<T> x) {...} // Error, should be C<U> }
Exemplo final
Uma declaração de substituição pode acessar o método base substituído usando um base_access (§12.8.15).
Exemplo: No seguinte código
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 desativa o mecanismo de invocação virtual e simplesmente trata o método base como não sendo um métodovirtual
. Se a invocação emB
tivesse sido escrita((A)this).PrintFields()
, ela invocaria recursivamente o método declaradoPrintFields
emB
, não o declarado emA
, uma vez quePrintFields
é virtual e o tipo de tempo de execução de((A)this)
éB
.Exemplo final
Somente incluindo um override
modificador é que um método pode substituir outro método. Em todos os outros casos, um método com a mesma assinatura de um método herdado simplesmente oculta o método herdado.
Exemplo: No seguinte código
class A { public virtual void F() {} } class B : A { public virtual void F() {} // Warning, hiding inherited F() }
o método
F
emB
não inclui um modificadoroverride
e, portanto, não substitui o métodoF
emA
. Em vez disso, oF
método emB
oculta o método emA
, e um aviso é gerado porque a declaração não inclui um novo modificador.Exemplo final
Exemplo: No seguinte código
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 método
F
emB
oculta o método virtualF
herdado deA
. Uma vez que o novoF
inB
tem acesso privado, o seu âmbito inclui apenas o corpo de classe deB
e não se estende aC
. Portanto, a declaração deF
inC
é permitida para substituir oF
herdado deA
.Exemplo final
15.6.6 Métodos selados
Quando uma declaração de método de instância inclui um sealed
modificador, esse método é dito ser um método selado. Um método selado substitui um método virtual herdado com a mesma assinatura. Um método selado também deve ser marcado com o modificador override
. O uso do sealed
modificador impede que uma classe derivada substitua ainda mais o método.
Exemplo: O exemplo
class A { public virtual void F() => Console.WriteLine("A.F"); public virtual void G() => Console.WriteLine("A.G"); } class B : A { public sealed override void F() => Console.WriteLine("B.F"); public override void G() => Console.WriteLine("B.G"); } class C : B { public override void G() => Console.WriteLine("C.G"); }
A classe
B
fornece dois métodos de substituição: um métodoF
que tem o modificadorsealed
e um métodoG
que não. O uso do modificadorB
porsealed
impede queC
substitua ainda maisF
.Exemplo final
15.6.7 Métodos abstratos
Quando uma declaração de método de instância inclui um abstract
modificador, esse método é dito ser um método abstrato. Embora um método abstrato seja implicitamente também um método virtual, ele não pode ter o modificador virtual
.
Uma declaração de método abstrato introduz um novo método virtual, mas não fornece uma implementação desse método. Em vez disso, classes derivadas não abstratas são obrigadas a fornecer sua própria implementação substituindo esse método. Como um método abstrato não fornece uma implementação real, o corpo do método de um método abstrato consiste apenas em um ponto e vírgula.
As declarações de método abstrato só são permitidas em classes abstratas (§15.2.2.2).
Exemplo: No seguinte código
public abstract class Shape { public abstract void Paint(Graphics g, Rectangle r); } public class Ellipse : Shape { public override void Paint(Graphics g, Rectangle r) => g.DrawEllipse(r); } public class Box : Shape { public override void Paint(Graphics g, Rectangle r) => g.DrawRect(r); }
A
Shape
classe define a noção abstrata de um objeto de forma geométrica que pode pintar a si mesmo. OPaint
método é abstrato porque não há nenhuma implementação padrão significativa. As classesEllipse
eBox
são implementações concretas deShape
. 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 final
É um erro em tempo de compilação que um base_access (§12.8.15) faça referência a um método abstrato.
Exemplo: No seguinte código
abstract class A { public abstract void F(); } class B : A { // Error, base.F is abstract public override void F() => base.F(); }
Um erro em tempo de compilação é reportado para a invocação de
base.F()
porque faz referência a um método abstrato.Exemplo final
Uma declaração de método abstrato tem permissão para substituir um método virtual. Isso permite que uma classe abstrata force a reimplementação do método em classes derivadas e torna a implementação original do método indisponível.
Exemplo: No seguinte código
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 final
15.6.8 Métodos externos
Quando uma declaração de método inclui um extern
modificador, o método é dito ser um método externo. Os métodos externos são implementados externamente, normalmente usando uma linguagem diferente de C#. Como uma declaração de método externo não fornece 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 final
15.6.9 Métodos parciais
Quando uma declaração de método inclui um partial
modificador, esse método é dito ser um método parcial. Os métodos parciais só podem ser declarados como membros de tipos parciais (§15.2.7) e estão sujeitos a uma série de restrições.
Os métodos parciais podem ser definidos numa parte de uma declaração de tipo e aplicados noutra. A implementação é opcional; Se nenhuma parte implementar o método parcial, a declaração de método parcial e todas as chamadas para ela serão removidas da declaração de tipo resultante da combinação das partes.
Os métodos parciais não devem definir modificadores de acesso; são implicitamente privados. O seu tipo de retorno deve ser void
, e os seus parâmetros não devem ser parâmetros de saída. O identificador partial
é reconhecido como uma palavra-chave contextual (§6.4.4) em uma declaração de método somente se aparecer imediatamente antes da palavra-chave void
. Um método parcial não pode implementar explicitamente métodos de interface.
Existem dois tipos de declarações de método parcial: Se o corpo da declaração do método for um ponto-e-vírgula, a declaração é considerada uma declaração parcial de método definidora. Se o corpo for diferente de um ponto e vírgula, a declaração é considerada uma declaração de método parcial de implementação. Nas partes de uma declaração de tipo, deve existir apenas uma declaração de método parcial definidora com uma determinada assinatura, e deve haver, no máximo, apenas uma declaração de método parcial de execução com uma determinada assinatura. Se for fornecida uma declaração de método parcial de execução, deve existir uma declaração de método parcial definidora correspondente, e as declarações devem corresponder conforme especificado no seguinte:
- As declarações devem ter os mesmos modificadores (embora não necessariamente pela mesma ordem), nome do método, número de parâmetros de tipo e número de parâmetros.
- Os parâmetros correspondentes nas declarações devem ter os mesmos modificadores (embora não necessariamente na mesma ordem) e os mesmos tipos, ou tipos conversíveis de identidade (diferenças de módulos nos nomes dos parâmetros de tipo).
- Os parâmetros de tipo correspondentes nas declarações devem ter as mesmas restrições (diferenças de modulo nos nomes dos parâmetros de tipo).
Uma declaração de método parcial de implementação pode aparecer na mesma parte que a declaração de método parcial de definição correspondente.
Apenas um método parcial definido participa na resolução de sobrecarga. Assim, independentemente de ser ou não dada uma declaração de implementação, as expressões de invocação podem resultar em invocações do método parcial. Como um método parcial sempre retorna void
, essas expressões de invocação sempre serão instruções de expressão. Além disso, como um método parcial é implicitamente private
, tais declarações sempre ocorrerão dentro de uma das partes da declaração de tipo dentro da qual o método parcial é declarado.
Nota: A definição de como corresponder e implementar declarações de métodos parciais não requer que os nomes dos parâmetros correspondam. Isto pode produzir um comportamento surpreendente, embora bem definido, quando são utilizados argumentos nomeados (§12.6.2.1). Por exemplo, dada a definição da declaração de método parcial para
M
em um arquivo e a declaração de método parcial de implementação em outro arquivo:// File P1.cs: partial class P { static partial void M(int x); } // File P2.cs: partial class P { static void Caller() => M(y: 0); static partial void M(int y) {} }
é inválida , pois a invocação usa o nome do argumento da implementação e não a declaração de método parcial definidora.
Nota final
Se nenhuma parte de uma declaração de tipo parcial contiver uma declaração de execução para um determinado método parcial, qualquer declaração de expressão que a invoque será simplesmente removida da declaração de tipo combinada. Assim, a expressão de invocação, incluindo quaisquer subexpressões, não tem efeito em tempo de execução. O método parcial em si também é removido e não será um membro da declaração de tipo combinado.
Se existir uma declaração de execução para um determinado método parcial, as invocações dos métodos parciais são mantidas. O método parcial dá origem a uma declaração de método semelhante à declaração de método parcial de execução, com exceção dos seguintes casos:
O
partial
modificador não está incluído.Os atributos na declaração de método resultante são os atributos combinados da declaração de método parcial de definição e implementação em ordem não especificada. As duplicatas não são removidas.
Os atributos nos parâmetros da declaração de método resultante são os atributos combinados dos parâmetros correspondentes da declaração de método parcial de definição e implementação em ordem não especificada. As duplicatas não são removidas.
Se for fornecida uma declaração definidora, mas não uma declaração de execução, para um método M
parcial, aplicam-se as seguintes restrições:
É um erro de tempo de compilação criar um delegado de
M
(§12.8.17.5).É um erro em tempo de compilação referir-se a
M
dentro de uma função anónima que é convertida em um tipo de árvore de expressões (§8.6).As expressões que ocorrem como parte de uma invocação de
M
não afetam o estado de atribuição definido, o que pode potencialmente levar a erros em tempo de compilação .M
não pode ser o ponto de entrada de um pedido (§7.1).
Os métodos parciais são úteis para permitir que uma parte de uma declaração de tipo personalize o comportamento de outra parte, por exemplo, uma que é gerada por uma ferramenta. Considere a seguinte declaração de classe parcial:
partial class Customer
{
string name;
public string Name
{
get => name;
set
{
OnNameChanging(value);
name = value;
OnNameChanged();
}
}
partial void OnNameChanging(string newName);
partial void OnNameChanged();
}
Se essa classe for compilada sem quaisquer outras partes, as declarações de método parcial definidoras e suas invocações serão removidas, e a declaração de classe combinada resultante será equivalente ao seguinte:
class Customer
{
string name;
public string Name
{
get => name;
set => name = value;
}
}
Assumindo que, no entanto, é fornecida outra parte que fornece as declarações de implementação dos métodos parciais:
partial class Customer
{
partial void OnNameChanging(string newName) =>
Console.WriteLine($"Changing {name} to {newName}");
partial void OnNameChanged() =>
Console.WriteLine($"Changed to {name}");
}
Em seguida, a declaração de classe combinada resultante será equivalente ao seguinte:
class Customer
{
string name;
public string Name
{
get => name;
set
{
OnNameChanging(value);
name = value;
OnNameChanged();
}
}
void OnNameChanging(string newName) =>
Console.WriteLine($"Changing {name} to {newName}");
void OnNameChanged() =>
Console.WriteLine($"Changed to {name}");
}
15.6.10 Métodos de extensão
Quando o primeiro parâmetro de um método inclui o this
modificador, esse método é dito ser um método de extensão. Os métodos de extensão só devem ser declarados em classes estáticas, não aninhadas e não genéricas. O primeiro parâmetro de um método de extensão é restrito, da seguinte forma:
- Pode ser apenas um parâmetro de entrada se tiver um tipo de valor
- Pode ser um parâmetro de referência apenas se tiver um tipo de valor ou um tipo genérico limitado a estrutura.
- Não pode ser um tipo de ponteiro.
Exemplo: A seguir está um exemplo de uma classe estática que declara dois métodos de extensão:
public static class Extensions { public static int ToInt32(this string s) => Int32.Parse(s); public static T[] Slice<T>(this T[] source, int index, int count) { if (index < 0 || count < 0 || source.Length - index < count) { throw new ArgumentException(); } T[] result = new T[count]; Array.Copy(source, index, result, 0, count); return result; } }
Exemplo final
Um método de extensão é um método estático regular. Além disso, quando a sua classe estática envolvente está no escopo, um método de extensão pode ser invocado usando a sintaxe de invocação de método de instância (§12.8.10.3), usando a expressão do receptor 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 normais.static class Program { static void Main() { string[] strings = { "1", "22", "333", "4444" }; foreach (string s in Extensions.Slice(strings, 1, 2)) { Console.WriteLine(Extensions.ToInt32(s)); } } }
Exemplo final
15.6.11 Corpo do método
O corpo de um método numa declaração de método consiste em um corpo de bloco, um corpo de expressão ou um ponto e vírgula.
Declarações de métodos abstratos e externos não fornecem uma implementação do método, portanto, os seus corpos de métodos simplesmente consistem de 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.14.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.14.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 controlo flui a partir do final do corpo do método), esse método simplesmente retorna a quem o chamou.
Quando o tipo de retorno efetivo de um método é void
e o método tem um corpo de expressão, a expressão E
deve ser um statement_expression, e o corpo é exatamente equivalente a um corpo de bloco da forma { E; }
.
Para um método de retorno por valor (§15.6.1), cada declaração de retorno no corpo desse método deve especificar uma expressão que seja implicitamente conversível para o tipo de retorno efetivo.
Para um método returns-by-ref (§15.6.1), cada declaração de retorno no corpo desse método deve especificar uma expressão cujo tipo é o do tipo de retorno efetivo e tem um contexto ref-safe do contexto do chamador (§9.7.2).
Para os métodos returns-by-value e returns-by-ref, o endpoint do corpo do método não deve ser acessível. Por outras palavras, não é permitido que o controlo flua além do final do corpo do método.
Exemplo: No seguinte código
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
F
que retorna um valor resulta em um erro de compilação porque o controlo pode sair do final do corpo do método. OsG
métodos eH
estão corretos porque todos os caminhos de execução possíveis terminam em uma instrução return que especifica um valor de retorno. O métodoI
está correto, porque o seu corpo é equivalente a um bloco com apenas uma única declaração de retorno.Exemplo final
15.7 Propriedades
15.7.1 Generalidades
Uma propriedade é um membro que fornece acesso a uma característica de um objeto ou classe. Exemplos de propriedades incluem o comprimento de uma cadeia de caracteres, o tamanho de uma fonte, a legenda de uma janela e o nome de um cliente. As propriedades são uma extensão natural de campos — ambos são membros nomeados com tipos associados, e a sintaxe para acessar campos e propriedades é a mesma. No entanto, ao contrário dos campos, as propriedades não denotam locais de armazenamento. Em vez disso, as propriedades têm acessadores que especificam as instruções a serem executadas quando seus valores são lidos ou gravados. As propriedades fornecem assim um mecanismo para associar ações com a leitura e escrita das características de um objeto ou classe; além disso, permitem calcular essas características.
As propriedades são declaradas usando property_declarations:
property_declaration
: attributes? property_modifier* type member_name property_body
| attributes? property_modifier* ref_kind type member_name ref_property_body
;
property_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| 'readonly' // direct struct members only
| unsafe_modifier // unsafe code support
;
property_body
: '{' accessor_declarations '}' property_initializer?
| '=>' expression ';'
;
property_initializer
: '=' variable_initializer ';'
;
ref_property_body
: '{' ref_get_accessor_declaration '}'
| '=>' 'ref' variable_reference ';'
;
unsafe_modifier (§23.2) só está disponível em código não seguro (§23).
Um property_declaration pode incluir um conjunto de atributos (§22) e qualquer um dos tipos permitidos de acessibilidade declarada (§15.3.6), new
(§15.3.5), static
(§15.7.2), virtual
(§15.6.4, §15.7.6), override
(§15.6.5, §15.7.6), sealed
(§15.6.6), abstract
(§15.6.7, §15.7.6) e extern
(§15.6.8). Além disso, um property_declaration contido diretamente por um struct_declaration pode incluir o readonly
modificador (§16.4.11).
- O primeiro declara uma propriedade que não tem valor de referência. O valor tem o tipo type. Este tipo de propriedade pode ser legível e/ou gravável.
- O segundo declara uma propriedade com valor de referência. O seu valor é uma variable_reference (§9.5), que pode ser
readonly
, para uma variável do tipo type. Este tipo de propriedade é apenas legível.
Um property_declaration pode incluir um conjunto de atributos (§22) e qualquer um dos tipos permitidos de acessibilidade declarada (§15.3.6), os new
modificadores (§15.3.5), static
(§15.7.2), virtual
(§15.6.4, §15.7.6), override
(§15.6.5, §15.7.6), sealed
(§15.6.6), abstract
(§15.6.7, §15.7.6) e extern
(§15.6.8).
As declarações de propriedade estão sujeitas às mesmas regras que as declarações de método (§15.6) no que diz respeito a combinações válidas de modificadores.
O member_name (§15.6.1) especifica o nome da propriedade. A menos que a propriedade seja uma implementação explícita de membro da interface, o member_name é simplesmente um identificador. Para uma implementação explícita de membro da interface (§18.6.2), o member_name consiste em um interface_type seguido por um ".
" e um identificador.
O tipo de propriedade deve ser pelo menos tão acessível como a própria propriedade (§7.5.5).
Um property_body pode consistir em um corpo de declaração ou um corpo de expressão. Num corpo de declaração, accessor_declarations, que deve ser encerrado em tokens “{
” e “}
”, declaram os acessores (§15.7.3) da propriedade. Os métodos de acesso especificam as instruções executáveis associadas à leitura e gravação da propriedade.
Em um property_body, um corpo de expressão consistindo em =>
seguido por uma expressãoE
e um ponto-e-vírgula é exatamente equivalente ao corpo da instrução { get { return E; } }
, e, portanto, só pode ser usado para especificar propriedades de leitura onde o resultado do acessor de leitura é dado por uma única expressão.
Uma property_initializer só pode ser dada para uma propriedade implementada automaticamente (§15.7.4), e causa a inicialização do campo subjacente de tais propriedades com o valor dado pela expressão.
Um ref_property_body pode consistir num corpo de declaração ou num corpo de expressão. Em um corpo de declaração, um get_accessor_declaration declara o acessor get (§15.7.3) da propriedade. O acessador especifica as instruções executáveis associadas à leitura da propriedade.
Em um ref_property_body, o corpo de expressão que consiste em =>
seguido por ref
, um variable_referenceV
e um ponto-e-vírgula é exatamente equivalente ao corpo da instrução { get { return ref V; } }
.
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
,out
ouref
argumento, a menos que a propriedade seja ref-value e, portanto, retorne uma referência variável (§9.7). Nota final
Quando uma declaração de propriedade inclui um extern
modificador, diz-se que a propriedade é uma propriedade externa. Como uma declaração de propriedade externa não fornece uma implementação real, cada um dos accessor_bodys nas suas accessor_declarations deve ser um ponto e vírgula.
15.7.2 Propriedades estáticas e de instância
Quando uma declaração de propriedade inclui um static
modificador, diz-se que a propriedade é uma propriedade estática. Quando nenhum static
modificador está presente, a propriedade é considerada uma propriedade de instância.
Uma propriedade estática não está associada a uma instância específica, e é um erro em tempo de compilação referir-se a this
nos acessadores de uma propriedade estática.
Uma propriedade de instância está associada a uma determinada instância de uma classe, e essa instância pode ser acessada como this
(§12.8.14) nos acessadores dessa propriedade.
As diferenças entre membros estáticos e de instância são discutidas mais detalhadamente no §15.3.8.
15.7.3 Acessadores
Nota: Esta cláusula aplica-se tanto às propriedades (§15.7) como aos indexadores (§15.9). A cláusula é redigida em termos de propriedades; ao ler no contexto de indexadores, substitua indexador/indexadores por propriedade/propriedades e consulte a lista de diferenças entre propriedades e indexadores apresentada no §15.9.2. Nota final
Os accessor_declarations de uma propriedade especificam as instruções executáveis associadas à escrita e/ou leitura dessa propriedade.
accessor_declarations
: get_accessor_declaration set_accessor_declaration?
| set_accessor_declaration get_accessor_declaration?
;
get_accessor_declaration
: attributes? accessor_modifier? 'get' accessor_body
;
set_accessor_declaration
: attributes? accessor_modifier? 'set' accessor_body
;
accessor_modifier
: 'protected'
| 'internal'
| 'private'
| 'protected' 'internal'
| 'internal' 'protected'
| 'protected' 'private'
| 'private' 'protected'
| 'readonly' // direct struct members only
;
accessor_body
: block
| '=>' expression ';'
| ';'
;
ref_get_accessor_declaration
: attributes? accessor_modifier? 'get' ref_accessor_body
;
ref_accessor_body
: block
| '=>' 'ref' variable_reference ';'
| ';'
;
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 modificador_de_acessador opcional, o token
Para uma propriedade de valor ref, a ref_get_accessor_declaration é composta por atributos opcionais, um accessor_modifier opcional, o token get
, seguido por um ref_accessor_body.
A utilização de accessor_modifiers rege-se pelas seguintes restrições:
- Um accessor_modifier não pode ser utilizado numa interface ou numa implementação explícita de membro da interface.
- O accessor_modifier
readonly
só é permitido em property_declaration ou indexer_declaration que esteja contido diretamente por um struct_declaration (§16.4.11, §16.4.13). - Para uma propriedade ou indexador que não tenha modificador
override
, um accessor_modifier só é permitido se a propriedade ou o indexador tiverem acessadores get e set, e então é permitido somente em um desses acessadores. - Para uma propriedade ou indexador que inclua um
override
modificador, um acessor deve corresponder ao accessor_modifier, se houver, do acessor que está a ser substituído. - O accessor_modifier deve declarar uma acessibilidade estritamente mais restritiva do que a acessibilidade declarada da propriedade ou do próprio indexador. Para ser mais preciso:
- Se a propriedade ou indexador tiver uma acessibilidade declarada de
public
, a acessibilidade declarada por accessor_modifier pode serprivate protected
,protected internal
,internal
,protected
ouprivate
. - Se a propriedade ou indexador tiver uma acessibilidade declarada de
protected internal
, a acessibilidade declarada por accessor_modifier pode serprivate protected
,protected private
,internal
,protected
ouprivate
. - Se a propriedade ou o indexador tiver uma acessibilidade declarada de `
internal
` ou `protected
`, a acessibilidade declarada pelo `modificador_de_acesso` deve ser `private protected
` ou `private
`. - Se a propriedade ou indexador tiver uma acessibilidade declarada de
private protected
, a acessibilidade declarada por accessor_modifier deve serprivate
. - Se a propriedade ou indexador tiver uma acessibilidade declarada de
private
, nenhuma accessor_modifier poderá ser usada.
- Se a propriedade ou indexador tiver uma acessibilidade declarada de
Para abstract
e extern
propriedades com valor não-referenciado, qualquer accessor_body para cada acessório especificado é simplesmente um ponto-e-vírgula. Uma propriedade que não seja abstrata, nem externa, e também não um indexador, pode ter o corpo do acessor para todos os acessores especificados como um ponto-e-vírgula; nesse caso, trata-se de uma propriedade de implementação automática (§15.7.4). Uma propriedade implementada automaticamente deve ter pelo menos um acessório get. Para os acessores de qualquer outra propriedade não abstrata, não externa, o accessor_body é:
- um bloco que especifica as instruções a serem executadas quando o acessador correspondente é invocado;
- um corpo de expressão, que consiste em
=>
seguido por uma expressão e um ponto e vírgula, e denota uma única expressão a ser executada quando o acessador correspondente é invocado.
Para as propriedades com valores de referência abstract
e extern
, 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 é, ou:
- um bloco que especifica as instruções a serem executadas quando o acessador get é invocado;
- um corpo de expressão, que consiste em
=>
seguido deref
, uma referência a variável e um ponto-e-vírgula. A referência da variável é avaliada quando o acessador get é invocado.
Um acessador get para uma propriedade sem valor de referência corresponde a um método sem parâmetros com um valor de retorno do tipo de propriedade. Exceto como o destino de uma atribuição, quando tal propriedade é referenciada em uma expressão, seu acessador get é invocado para calcular o valor da propriedade (§12.2.2).
O corpo de um acessador 'get' para uma propriedade não valor-ref deve estar em conformidade com as regras para métodos que retornam valores descritas no §15.6.11. Em particular, todas as return
declarações no corpo de um acessador get devem especificar uma expressão que seja implicitamente conversível para o tipo de propriedade. Além disso, o ponto final de um acessor de leitura não deve estar acessível.
Um acessador get para uma propriedade de valor ref corresponde a um método sem parâmetros com um valor de retorno de uma referência_de_variável para uma variável do tipo da propriedade. Quando essa propriedade é referenciada em uma expressão, seu acessor get é invocado para calcular o valor variable_reference da propriedade. Essa referência de variável, como qualquer outra, é então usada para ler ou, para variáveis que não são apenas de leitura, escrever a variável referenciada conforme exigido pelo contexto.
Exemplo: O exemplo a seguir ilustra uma propriedade com valor de referência como o destino de uma atribuição:
class Program { static int field; static ref int Property => ref field; static void Main() { field = 10; Console.WriteLine(Property); // Prints 10 Property = 20; // This invokes the get accessor, then assigns // via the resulting variable reference Console.WriteLine(field); // Prints 20 } }
Exemplo final
O corpo de um acessor de obtenção para uma propriedade com valor de referência deve estar em conformidade com as regras para métodos com valor de referência descritas no §15.6.11.
Um acessador de conjunto corresponde a um método com um único parâmetro de valor do tipo da propriedade e um tipo de retorno void
. O parâmetro implícito de um assessor de definição é sempre chamado de 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 de definição é invocado com um argumento que fornece o novo valor (§12.21.2). O corpo de um acessor de conjunto deve estar em conformidade com as regras para métodos descritos no void
. Em particular, as instruções de retorno no corpo do acessor de definição não têm permissão para especificar uma expressão. Como um acessador de conjunto tem implicitamente um parâmetro chamado value
, é um erro em tempo de compilação para uma variável local ou declaração constante em um acessador de conjunto ter esse nome.
Com base na presença ou ausência dos acessores get e set, uma propriedade é classificada da seguinte forma:
- Uma propriedade que inclui um acessor get e um acessor set é denominada uma propriedade de leitura-gravação.
- Uma propriedade que possui apenas um método de obtenção é denominada uma propriedade de leitura apenas. É um erro durante a compilação que uma propriedade só de leitura seja o destino de uma atribuição.
- Uma propriedade que tem apenas um acessor de definição é dita ser uma propriedade apenas de escrita. Exceto como o alvo de uma atribuição, é um erro em tempo de compilação referir-se a uma propriedade apenas para gravação em uma expressão.
Nota: Os operadores de pré-fixo e pós-fixo
++
e--
os operadores de atribuição composta não podem ser aplicados a propriedades apenas para escrita, uma vez que esses operadores leem o valor antigo de seu operando antes de escrever o novo. Nota final
Exemplo: No seguinte código
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 acessor 'get' da propriedade 'Caption' retorna ostring
armazenado no campo privadocaption
. O acessador definido verifica se o novo valor é diferente do valor atual e, em caso afirmativo, armazena o novo valor e repinta o controle. As propriedades geralmente seguem o padrão mostrado acima: O acessador get simplesmente retorna um valor armazenado em umprivate
campo, e o acessador definido modifica esseprivate
campo e, em seguida, executa quaisquer 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 de conjunto é invocado atribuindo um valor à propriedade e o acessador get é invocado fazendo referência à propriedade em uma expressão.
Exemplo final
Os acessores get e set de uma propriedade não são membros distintos, e não é possível declarar os acessores 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-escrita. Em vez disso, declara duas propriedades com o mesmo nome, uma apenas para leitura e outra apenas para escrita. Como dois membros declarados na mesma classe não podem ter o mesmo nome, o exemplo faz com que ocorra um erro em tempo de compilação.
Exemplo final
Quando uma classe derivada declara uma propriedade com o mesmo nome de uma propriedade herdada, a propriedade derivada oculta a propriedade herdada em relação à leitura e à escrita.
Exemplo: No seguinte código
class A { public int P { set {...} } } class B : A { public new int P { get {...} } }
A
P
propriedade emB
esconde aP
propriedade emA
relativamente à leitura e à escrita. Assim, nas declaraçõesB 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
causa um erro a ser relatado durante a compilação, uma vez que a propriedade de somente leitura emP
oculta a propriedade de somente gravação emB
. No entanto, note que um "cast" pode ser utilizado para acessar a propriedade ocultaP
.Exemplo final
Ao contrário dos campos públicos, as propriedades fornecem uma separação entre o estado interno de um objeto e sua interface pública.
Exemplo: considere o código a seguir, que usa um
Point
struct para representar um local:class Label { private int x, y; private string caption; public Label(int x, int y, string caption) { this.x = x; this.y = y; this.caption = caption; } public int X => x; public int Y => y; public Point Location => new Point(x, y); public string Caption => caption; }
Aqui, a
Label
classe usa doisint
camposx
ey
, para armazenar sua localização. O local é exposto publicamente tanto como umaX
e umaY
propriedade como também umaLocation
propriedade do tipoPoint
. Se, numa versão futura doLabel
, se tornar mais conveniente armazenar o local como umPoint
internamente, a alteração pode ser feita sem afetar a interface pública da classe.class Label { private Point location; private string caption; public Label(int x, int y, string caption) { this.location = new Point(x, y); this.caption = caption; } public int X => location.X; public int Y => location.Y; public Point Location => location; public string Caption => caption; }
Se
x
ey
tivessem, em vez disso, sido campospublic readonly
, teria sido impossível fazer tal mudança na classeLabel
.Exemplo final
Nota: Expor o estado através de propriedades não é necessariamente menos eficiente do que expor campos diretamente. Em particular, quando uma propriedade não é virtual e contém apenas uma pequena quantidade de código, o ambiente de execução pode substituir chamadas para acessadores com o código real dos acessadores. Este processo é conhecido como inlining, e torna o acesso à propriedade tão eficiente quanto o acesso ao campo, mas preserva a maior flexibilidade das propriedades. Nota final
Exemplo: Como invocar um acessor de obtenção é conceitualmente equivalente a ler o valor de um campo, é considerado um estilo de programação ruim que os acessadores de obtenção 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, o acesso à propriedade produz um efeito colateral observável e, em vez disso, a propriedade deve ser implementada como um método.A convenção "sem efeitos colaterais" para acessores de leitura não significa que eles devam sempre ser escritos simplesmente para retornar valores armazenados em campos de dados. De fato, os acessadores de get geralmente calculam o valor de uma propriedade acessando vários campos ou invocando métodos. No entanto, um acessor get projetado corretamente não executa nenhuma ação que cause alterações observáveis no estado do objeto.
Exemplo final
As propriedades podem ser usadas para atrasar a inicialização de um recurso até o momento em que ele é referenciado pela primeira vez.
Exemplo:
public class Console { private static TextReader reader; private static TextWriter writer; private static TextWriter error; public static TextReader In { get { if (reader == null) { reader = new StreamReader(Console.OpenStandardInput()); } return reader; } } public static TextWriter Out { get { if (writer == null) { writer = new StreamWriter(Console.OpenStandardOutput()); } return writer; } } public static TextWriter Error { get { if (error == null) { error = new StreamWriter(Console.OpenStandardError()); } return error; } } ... }
A
Console
classe contém três propriedades,In
,Out
, eError
, que representam os dispositivos padrão de entrada, saída e erro, respectivamente. Ao expor esses membros como propriedades, a classeConsole
pode atrasar a sua inicialização até que eles sejam realmente usados. Por exemplo, ao fazer a primeira referência àOut
propriedade, como emConsole.Out.WriteLine("hello, world");
A base subjacente
TextWriter
para o dispositivo de saída é criada. No entanto, se o aplicativo não fizer referência àsIn
propriedades eError
, nenhum objeto será criado para esses dispositivos.Exemplo final
15.7.4 Propriedades implementadas automaticamente
Uma propriedade implementada automaticamente (ou auto-propriedade, para abreviar), é uma propriedade não-abstrata, não-externa, que não tem valor de referência, cujos accessor_bodys contêm apenas ponto-e-vírgula. As propriedades automáticas devem ter um acessor de obtenção e podem, opcionalmente, ter um acessor de definição.
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; pode ser lido e escrito apenas através dos acessores de propriedade criados automaticamente, mesmo dentro do tipo que o contém. Se a propriedade automática não tiver um acessador de conjunto, o campo subjacente é considerado readonly
(§15.5.3). Tal como um campo readonly
, uma propriedade automática de leitura também pode ser atribuída no corpo de um construtor da classe envolvente. Tal atribuição atribui diretamente ao campo de suporte somente leitura da propriedade.
Uma propriedade automática pode, opcionalmente, ter um property_initializer, que é aplicado diretamente ao campo subjacente como um variable_initializer (§17.7).
Exemplo:
public class Point { public int X { get; set; } // Automatically implemented public int Y { get; set; } // Automatically implemented }
é equivalente à seguinte declaração:
public class Point { private int x; private int y; public int X { get { return x; } set { x = value; } } public int Y { get { return y; } set { y = value; } } }
Exemplo final
Exemplo: No seguinte
public class ReadOnlyPoint { public int X { get; } public int Y { get; } public ReadOnlyPoint(int x, int y) { X = x; Y = y; } }
é equivalente à seguinte declaração:
public class ReadOnlyPoint { private readonly int __x; private readonly int __y; public int X { get { return __x; } } public int Y { get { return __y; } } public ReadOnlyPoint(int x, int y) { __x = x; __y = y; } }
As atribuições ao campo de leitura somente são válidas, porque ocorrem dentro do construtor.
Exemplo final
Embora o campo de suporte esteja oculto, esse campo pode ter atributos direcionados ao campo aplicados diretamente a ele por meio da property_declaration da propriedade implementada automaticamente (§15.7.1).
Exemplo: O código a seguir
[Serializable] public class Foo { [field: NonSerialized] public string MySecret { get; set; } }
resulta na aplicação do atributo direcionado ao campo
NonSerialized
ao campo de suporte gerado pelo compilador, como se o código tivesse sido escrito da seguinte forma:[Serializable] public class Foo { [NonSerialized] private string _mySecretBackingField; public string MySecret { get { return _mySecretBackingField; } set { _mySecretBackingField = value; } } }
Exemplo final
15.7.5 Acessibilidade
Se um acessador tiver um accessor_modifier, o domínio de acessibilidade (§7.5.3) do acessador é determinado usando a acessibilidade declarada do accessor_modifier. Se um acessador não tiver um accessor_modifier, o domínio de acessibilidade do acessador é determinado a partir da acessibilidade declarada da propriedade ou indexador.
A presença de um accessor_modifier nunca afeta a pesquisa de membros (§12.5) ou a resolução de sobrecarga (§12.6.4). Os modificadores na propriedade ou indexador sempre determinam a que propriedade ou indexador estão vinculados, independentemente do contexto do acesso.
Uma vez selecionada uma determinada propriedade sem valor de referência ou indexador sem valor de referência, os domínios de acessibilidade dos acessadores específicos envolvidos são usados para determinar se esse uso é válido:
- Se o uso for como um valor (§12.2.2), o acessor get deverá existir e ser acessível.
- Se o uso for como alvo de uma atribuição simples (§12.21.2), o acessador definido deve existir e ser acessível.
- Se o uso for como o alvo da atribuição composta (§12.21.4), ou como o alvo dos operadores
++
ou--
(§12.8.16, §12.9.6), tanto os acessores get como o acessor set devem existir e ser acessíveis.
Exemplo: No exemplo a seguir, a propriedade
A.Text
é ocultada pela propriedadeB.Text
, mesmo em contextos onde apenas o acessador definido é chamado. Por outro lado, a propriedadeB.Count
não é acessível à classeM
, portanto, a propriedadeA.Count
acessível é usada em vez disso.class A { public string Text { get => "hello"; set { } } public int Count { get => 5; set { } } } class B : A { private string text = "goodbye"; private int count = 0; public new string Text { get => text; protected set => text = value; } protected new int Count { get => count; set => count = value; } } class M { static void Main() { B b = new B(); b.Count = 12; // Calls A.Count set accessor int i = b.Count; // Calls A.Count get accessor b.Text = "howdy"; // Error, B.Text set accessor not accessible string s = b.Text; // Calls B.Text get accessor } }
Exemplo final
Depois que uma determinada propriedade ou indexador com valor de referência tiver sido selecionado — seja 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 acessor get envolvido é utilizado para determinar a validade desse uso.
Um acessor que é usado para implementar uma interface não deve ter um accessor_modifier. Se apenas um acessador for usado para implementar uma interface, o outro acessador pode ser declarado com uma accessor_modifier:
Exemplo:
public interface I { string Prop { get; } } public class C : I { public string Prop { get => "April"; // Must not have a modifier here internal set {...} // Ok, because I.Prop has no set accessor } }
Exemplo final
15.7.6 Acessadores virtuais, selados, sobrepostos e abstratos
Nota: Esta cláusula aplica-se tanto às propriedades (§15.7) como aos indexadores (§15.9). A cláusula é redigida em termos de propriedades; ao ler no contexto de indexadores, substitua indexador/indexadores por propriedade/propriedades e consulte a lista de diferenças entre propriedades e indexadores apresentada no §15.9.2. Nota final
Uma declaração de propriedade virtual especifica que os acessadores da propriedade são virtuais. O modificador virtual
aplica-se a todos os acessores não privados de uma propriedade. Quando um acessador de uma propriedade virtual tem o private
accessor_modifier, o acessador privado implicitamente não é virtual.
Uma declaração de propriedade abstrata especifica que os acessadores da propriedade são virtuais, mas não fornece uma implementação real dos acessadores. Em vez disso, as classes derivadas que não são abstratas são necessárias para fornecer a sua própria implementação para os acessadores, sobrepondo a propriedade. Como um acessor para uma declaração de propriedade abstrata não fornece nenhuma implementação real, o seu accessor_body consiste simplesmente em um ponto e vírgula. Uma propriedade abstrata não deve ter um private
acessor.
Uma declaração de propriedade que inclui os abstract
modificadores e override
especifica que a propriedade é abstrata e substitui uma propriedade base. Os acessores de tal propriedade são também abstratos.
As declarações abstratas de propriedade só são permitidas em classes abstratas (§15.2.2.2). Os acessores de uma propriedade virtual herdada podem ser substituídos em uma classe derivada, incluindo uma declaração de propriedade que especifique uma diretiva override
. Isso é conhecido como uma declaração de propriedade sobrescrevente. Uma declaração de propriedade prevalecente não declara uma nova propriedade. Em vez disso, ele simplesmente especializa as implementações dos acessadores de uma propriedade virtual existente.
A declaração de substituição e a propriedade base que é 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 interna e for declarada num assembly diferente do assembly que contém a declaração de substituição, então a acessibilidade declarada da declaração de substituição deverá ser protegida. Se a propriedade herdada tiver apenas um único acessor (ou seja, se a propriedade herdada for somente de leitura ou somente de escrita), a propriedade que a supera deve incluir apenas esse acessor. Se a propriedade herdada incluir ambos os acessadores (ou seja, se a propriedade herdada for de leitura e gravação), a propriedade que substitui poderá incluir apenas um acessador ou ambos os acessadores. Deve haver uma conversão de identidade entre o tipo da propriedade que está a substituir e a propriedade herdada.
Uma declaração de propriedade sobrescritora pode incluir o modificador sealed
. O uso desse modificador impede que uma classe derivada substitua ainda mais a propriedade. Os acessores de uma propriedade selada também são selados.
Exceto pelas diferenças na sintaxe de declaração e invocação, os acessores virtuais, sealed, override e abstract comportam-se exatamente como os métodos virtuais, sealed, override e abstract. Especificamente, as regras descritas nos §15.6.4, §15.6.5, §15.6.6 e §15.6.7 aplicam-se como se os acessadores fossem métodos de uma forma correspondente:
- Um acessador get corresponde a um método sem parâmetros com um valor de retorno do tipo de propriedade e os mesmos modificadores que a propriedade que o contém.
- Um acessor de definição corresponde a um método com um único parâmetro de valor do tipo da propriedade, um tipo de retorno void e os mesmos modificadores que a propriedade que o contém.
Exemplo: No seguinte código
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 de leitura apenas,Y
é uma propriedade virtual de leitura e escrita, eZ
é uma propriedade abstrata de leitura e escrita. PorZ
ser abstrata, a classe A que contém também deve ser declarada abstrata.Uma classe que deriva de
A
é mostrada 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
X
,Y
eZ
são declarações de propriedade que prevalecem. Cada declaração de propriedade corresponde exatamente aos modificadores de acessibilidade, tipo e nome da propriedade herdada correspondente. O acessador get deX
e o acessador set deY
usam a palavra-chave 'base' para aceder aos acessadores herdados. A declaração deZ
substitui ambos os acessadores abstratos — assim, não há membros de função pendentesabstract
emB
, eB
é permitido que seja uma classe não abstrata.Exemplo final
Quando uma propriedade é definida como uma substituição, quaisquer acessores substituídos devem ser acessíveis para o código que os substitui. Além disso, a acessibilidade declarada tanto da própria propriedade como do indexador, assim como dos acessores, deve corresponder à do membro anulado e dos seus acessores.
Exemplo:
public class B { public virtual int P { get {...} protected set {...} } } public class D: B { public override int P { get {...} // Must not have a modifier here protected set {...} // Must specify protected here } }
Exemplo final
15.8 Eventos
15.8.1 Generalidades
Um evento é um membro que permite que um objeto ou classe forneça notificações. Os clientes podem anexar código executável para eventos fornecendo manipuladores de eventos.
Os eventos são declarados usando event_declarations:
event_declaration
: attributes? event_modifier* 'event' type variable_declarators ';'
| attributes? event_modifier* 'event' type member_name
'{' event_accessor_declarations '}'
;
event_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| 'readonly' // direct struct members only
| unsafe_modifier // unsafe code support
;
event_accessor_declarations
: add_accessor_declaration remove_accessor_declaration
| remove_accessor_declaration add_accessor_declaration
;
add_accessor_declaration
: attributes? 'add' block
;
remove_accessor_declaration
: attributes? 'remove' block
;
unsafe_modifier (§23.2) só está disponível em código não seguro (§23).
Uma event_declaration pode incluir um conjunto de atributos (§22) e qualquer um dos tipos permitidos de acessibilidade declarada (§15.3.6), modificadores new
(§15.3.5), static
(§15.6.3, §15.8.4), virtual
(§15.6.4, §15.8.5), override
(§15.6.5, §15.8.5), sealed
(§15.6.6), abstract
(§15.6.7, §15.8.5) e extern
(§15.6.8). Além disso, uma event_declaration contida diretamente por um struct_declaration pode incluir o readonly
modificador (§16.4.12).
As declarações de eventos estão sujeitas às mesmas regras que as declarações de método (§15.6) no que diz respeito a combinações válidas de modificadores.
O tipo de declaração de evento deve ser um delegate_type (§8.2.8), e essa delegate_type deve ser pelo menos tão acessível quanto o próprio evento (§7.5.5).
Uma declaração de evento pode incluir event_accessor_declarations. No entanto, se isso não acontecer, para eventos não externos e não abstratos, um compilador deve fornecê-los automaticamente (§15.8.2); Para eventos extern
, os acessadores são fornecidos externamente.
Uma declaração de evento que omite as event_accessor_declaration define um ou mais eventos — um para cada declarador_de_variável. Os atributos e modificadores aplicam-se a todos os membros declarados por tal event_declaration.
É um erro de tempo de compilação para uma event_declaration incluir tanto o modificador abstract
quanto as event_accessor_declarations.
Quando uma declaração de evento inclui um extern
modificador, o evento é dito ser um evento externo. Como uma declaração de evento externo não fornece nenhuma implementação real, é um erro incluir tanto o modificador extern
quanto as declarações de acessores de evento event_accessor_declaration.
É um erro de tempo de compilação para um variable_declarator de uma declaração de evento com um modificador abstract
ou external
incluir um variable_initializer.
Um evento pode ser usado como operando esquerdo dos operadores +=
e -=
. Esses operadores são usados, respectivamente, para anexar manipuladores de eventos ou para remover manipuladores de eventos de um evento, e os modificadores de acesso do evento controlam os contextos nos quais tais operações são permitidas.
As únicas operações permitidas em um evento por código que está fora do tipo no qual esse evento é declarado são +=
e -=
. Portanto, embora esse código possa adicionar e remover manipuladores para um evento, ele não pode obter ou modificar diretamente a lista subjacente de manipuladores de eventos.
Em uma operação da forma x += y
ou x –= y
, quando x
é um evento, o resultado da operação tem tipo void
(§12.21.5) (em oposição a ter o tipo de x
, com o valor de x
após a atribuição, como para outros operadores +=
e -=
definidos em tipos não-evento). Isso impede que o código externo examine indiretamente o delegado subjacente de um evento.
Exemplo: O exemplo a seguir mostra como manipuladores de eventos são anexados a instâncias da
Button
classe:public delegate void EventHandler(object sender, EventArgs e); public class Button : Control { public event EventHandler Click; } public class LoginDialog : Form { Button okButton; Button cancelButton; public LoginDialog() { okButton = new Button(...); okButton.Click += new EventHandler(OkButtonClick); cancelButton = new Button(...); cancelButton.Click += new EventHandler(CancelButtonClick); } void OkButtonClick(object sender, EventArgs e) { // Handle okButton.Click event } void CancelButtonClick(object sender, EventArgs e) { // Handle cancelButton.Click event } }
Aqui, o construtor de
LoginDialog
instância cria duasButton
instâncias e anexa manipuladores de eventos aosClick
eventos.Exemplo final
Eventos semelhantes a campo
Dentro do texto do programa da classe ou struct que contém a declaração de um evento, certos eventos podem ser usados como campos. Para ser utilizado desta forma, um evento não deve ser abstrato ou externo e não deve incluir explicitamente event_accessor_declarations. Tal evento pode ser usado em qualquer contexto que permita um campo. O campo contém um delegado (§20), que se refere à lista de manipuladores de eventos que foram adicionados ao evento. Se nenhum manipulador de eventos tiver sido adicionado, o campo conterá null
.
Exemplo: No seguinte código
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. O métodoOnClick
na classeButton
"aciona" o eventoClick
. A noção de gerar um evento é precisamente equivalente a invocar o delegado representado pelo evento — portanto, não existem construções especiais de linguagem para gerar eventos. Note que a invocação do delegado é precedida por uma verificação que garante que o delegado não é nulo e que essa verificação é feita numa cópia local para garantir a segurança de thread.Fora da declaração da classe
Button
, o membroClick
só pode ser usado no lado esquerdo dos operadores+=
e–=
, 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 evento
Click
.Exemplo final
Ao compilar um evento semelhante a um campo, um compilador deve criar automaticamente armazenamento para manter o delegado e deve criar acessadores para o evento que adicionam ou removem manipuladores de eventos ao campo delegado. As operações de adição e remoção são seguras para execução simultânea e podem (mas não é obrigatório) ser realizadas mantendo o bloqueio sobre o objeto que contém o evento de instância (§13.13), ou o objeto System.Type
(§12.8.18) para um evento estático.
Nota: Assim, uma declaração de evento de instância da forma:
class X { public event D Ev; }
deverá ser compilado para algo que seja 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+=
e–=
fazem com que os acessores add e remove sejam invocados. Todas as outras referências aEv
são compiladas para fazer referência ao campo oculto__Ev
(§12.8.7). O nome "__Ev
" é arbitrário, o campo oculto pode ter qualquer nome ou nenhum nome.Nota final
15.8.3 Acessores de eventos
Nota: As declarações de evento normalmente omitem event_accessor_declarations, como no
Button
exemplo acima. Por exemplo, eles podem ser incluídos se o custo de armazenamento de um campo por evento não for aceitável. Nesses casos, uma classe pode incluir event_accessor_declarations e usar um mecanismo privado para armazenar a lista de manipuladores de eventos. Nota final
Os event_accessor_declarations de um evento especificam as instruções executáveis associadas à adição e remoção de manipuladores de eventos.
As declarações de acesso consistem num add_accessor_declaration e num remove_accessor_declaration. Cada declaração de acessor consiste no token add ou remove seguido por um bloco. O bloco associado a um add_accessor_declaration especifica as instruções a serem executadas quando um manipulador de eventos é adicionado e o bloco associado a um remove_accessor_declaration especifica as instruções a serem executadas quando um manipulador de eventos é removido.
Cada add_accessor_declaration e remove_accessor_declaration corresponde a um método com um único parâmetro de valor do tipo de evento e um void
tipo de retorno. O parâmetro implícito de um acessador de eventos é chamado value
. Quando um evento é utilizado numa atribuição de evento, o acessador de evento apropriado é usado. Especificamente, se o operador de atribuição for +=
então o acessador de adição será usado, e se o operador de atribuição for –=
então o acessador de remoção será usado. Em ambos os casos, o operando direito do operador de atribuição é utilizado como argumento para o acessador de eventos. O bloco de um add_accessor_declaration ou de um remove_accessor_declaration deve estar em conformidade com as regras para métodos descritas no void
. Em particular, return
as declarações contidas nesse bloco não podem especificar uma expressão.
Como um acessador de evento tem implicitamente um parâmetro chamado value
, é um erro em tempo de compilação para uma variável local ou constante declarada em um acessador de evento ter esse nome.
Exemplo: No seguinte código
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 de 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 modo que não haja custo para associar um valor de delegado nulo a uma chave e, portanto, os eventos não manipulados não consomem armazenamento.Exemplo final
15.8.4 Eventos estáticos e de instância
Quando uma declaração de evento inclui um static
modificador, o evento é dito ser um evento estático. Quando nenhum static
modificador está presente, o evento é dito um evento de instância.
Um evento estático não está associado a uma instância específica, e ocorre um erro de compilação ao referir-se a 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 detalhadamente no §15.3.8.
15.8.5 Acessadores virtuais, lacrados, de substituição e abstratos
Uma declaração de evento virtual especifica que os acessadores desse evento são virtuais. O modificador virtual
aplica-se a ambos os acessores 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 obrigadas a fornecer a sua própria implementação para os métodos de acesso ao substituir o evento. Uma vez que um acessor para uma declaração de evento abstrato não fornece nenhuma implementação real, ele não deve fornecer event_accessor_declarations.
Uma declaração de evento que inclui os abstract
modificadores e override
especifica que o evento é abstrato e substitui um evento base. Os métodos de acesso de tal evento também são abstratos.
As declarações de eventos abstratos só são permitidas em classes abstratas (§15.2.2.2).
Os acessores de um evento virtual herdado podem ser substituídos numa classe derivada ao incluir uma declaração de evento que especifica um modificador override
. Isto é conhecido como uma declaração de evento de sobreposição. Uma declaração de evento de substituição não declara um novo evento. Em vez disso, simplesmente especializa as implementações dos acessores de um evento virtual existente.
Uma declaração de evento que sobrepõe deve especificar exatamente os mesmos modificadores de acessibilidade e nome do evento sobreposto, deve haver uma conversão de identidade entre o tipo de evento que sobrepõe e o tipo do evento sobreposto, e tanto os métodos de acesso add quanto remove devem ser especificados na declaração.
Uma declaração de evento de substituição pode incluir o modificador sealed
. O uso do this
modificador impede que uma classe derivada substitua ainda mais o evento. Os acessores de um evento selado são também selados.
Um erro de tempo de compilação ocorre quando uma declaração de evento de sobreposição inclui um modificador new
.
Exceto pelas diferenças na sintaxe de declaração e invocação, os acessores virtuais, sealed, override e abstract comportam-se exatamente como os métodos virtuais, sealed, override e abstract. Especificamente, as regras descritas nos §15.6.4, §15.6.5, §15.6.6 e §15.6.7 aplicam-se como se os acessadores fossem métodos de uma forma correspondente. Cada acessador corresponde a um método com um único parâmetro de valor do tipo de evento, um void
tipo de retorno e os mesmos modificadores que o evento que contém.
15.9 Indexadores
15.9.1 Generalidades
Um indexador é um membro que permite que um objeto seja indexado da mesma forma que uma matriz. Os indexadores são declarados usando indexer_declarations:
indexer_declaration
: attributes? indexer_modifier* indexer_declarator indexer_body
| attributes? indexer_modifier* ref_kind indexer_declarator ref_indexer_body
;
indexer_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'virtual'
| 'sealed'
| 'override'
| 'abstract'
| 'extern'
| 'readonly' // direct struct members only
| unsafe_modifier // unsafe code support
;
indexer_declarator
: type 'this' '[' parameter_list ']'
| type interface_type '.' 'this' '[' parameter_list ']'
;
indexer_body
: '{' accessor_declarations '}'
| '=>' expression ';'
;
ref_indexer_body
: '{' ref_get_accessor_declaration '}'
| '=>' 'ref' variable_reference ';'
;
unsafe_modifier (§23.2) só está disponível em código não seguro (§23).
Uma indexer_declaration pode incluir um conjunto de atributos (§22) e qualquer um dos tipos permitidos de acessibilidade declarada (§15.3.6), juntamente com os modificadores descritos nas seções: new
(§15.3.5), virtual
(§15.6.4), override
(§15.6.5), sealed
(§15.6.6), abstract
(§15.6.7) e extern
(§15.6.8). Além disso, um indexer_declaration contido diretamente por um struct_declaration pode incluir o readonly
modificador (§16.4.12).
- O primeiro declara um indexador sem valor de referência. O valor tem o tipo type. Este tipo de indexador pode ser legível e/ou gravável.
- O segundo declara um indexador com valor de referência. O seu valor é uma variable_reference (§9.5), que pode ser
readonly
, para uma variável do tipo type. Este tipo de indexador é apenas legível.
Um indexer_declaration pode incluir um conjunto de atributos (§22) e qualquer um dos tipos permitidos de acessibilidade declarada (§15.3.6), o new
(§15.3.5), o virtual
(§15.6.4), o override
(§15.6.5), o sealed
(§15.6.6), o abstract
(§15.6.7) e o extern
(§15.6.8) modificadores.
As declarações de indexador estão sujeitas às mesmas regras que as declarações de método (§15.6) no que diz respeito a combinações válidas de modificadores, com a única exceção sendo que o static
modificador não é permitido em uma declaração de indexador.
O tipo de uma declaração de indexador especifica o tipo de elemento do indexador introduzido pela declaração.
Nota: Como os indexadores são projetados para serem usados em contextos semelhantes a elementos de matriz, o termo tipo de elemento conforme definido para uma matriz também é usado com um indexador. Nota final
A menos que o indexador seja uma implementação explícita de membro da interface, o tipo é seguido pela palavra-chave this
. Para uma implementação explícita de membro da interface, o tipo é seguido por um interface_type, um ".
" e a palavra-chave this
. Ao contrário de outros membros, os indexadores não têm nomes definidos pelo usuário.
O parameter_list especifica os parâmetros do indexador. A lista de parâmetros de um indexador corresponde à de um método (§15.6.2), exceto que pelo menos um parâmetro deve ser especificado e que o this
, ref
e out
modificadores de parâmetros não são permitidos.
O tipo de indexador e cada um dos tipos referenciados no parameter_list devem ser pelo menos tão acessíveis como o próprio indexador (ponto 7.5.5).
Um indexer_body pode consistir num corpo de declarações (§15.7.1) ou num corpo de expressão (§15.6.1). Em um corpo de instrução, accessor_declarations, que devem ser encerrados em tokens "{
" e "}
", declaram os acessadores (§15.7.3) do indexador. Os acessadores especificam as instruções executáveis associadas aos elementos indexadores de leitura e gravação.
Em um indexer_body, um corpo de expressão que consiste em "=>
" seguido por uma expressão E
e um ponto-e-vírgula é exatamente equivalente ao corpo da instrução { get { return E; } }
, e, portanto, só pode ser usado para especificar indexadores de somente leitura onde o resultado do acessor de obtenção é dado por uma única expressão.
Um ref_indexer_body pode consistir em um corpo de declaração ou um corpo de expressão. Em um corpo de instrução, um get_accessor_declaration declara o acessador get (§15.7.3) do indexador. O acessador especifica as instruções executáveis associadas à leitura do indexador.
Em um ref_indexer_body, um corpo de expressão que consiste em =>
seguido por ref
, um variable_referenceV
e um ponto-e-vírgula é exatamente equivalente ao corpo da declaração { get { return ref V; } }
.
Nota: Embora a sintaxe para acessar um elemento indexador seja a mesma de um elemento de matriz, um elemento indexador não é classificado como uma variável. Assim, não é possível passar um elemento indexador como um
in
,out
ouref
argumento, a menos que o indexador seja ref-value e, portanto, retorne uma referência (§9.7). Nota final
O parameter_list de um indexador define a assinatura (§7.6) do indexador. Especificamente, a assinatura de um indexador consiste no número e tipos de seus parâmetros. O tipo de elemento e os nomes dos parâmetros não fazem parte da assinatura de um indexador.
A assinatura de um indexador deve diferir das assinaturas de todos os outros indexadores declarados na mesma classe.
Quando uma declaração de indexador inclui um extern
modificador, diz-se que o indexador é um indexador externo. Como uma declaração de indexador externo não fornece implementação real, cada um dos corpos de acessor em suas declarações de acessor deverá ser um ponto e vírgula.
Exemplo: O exemplo abaixo declara uma
BitArray
classe que implementa um indexador para acessar os bits individuais na matriz de bits.class BitArray { int[] bits; int length; public BitArray(int length) { if (length < 0) { throw new ArgumentException(); } bits = new int[((length - 1) >> 5) + 1]; this.length = length; } public int Length => length; public bool this[int index] { get { if (index < 0 || index >= length) { throw new IndexOutOfRangeException(); } return (bits[index >> 5] & 1 << index) != 0; } set { if (index < 0 || index >= length) { throw new IndexOutOfRangeException(); } if (value) { bits[index >> 5] |= 1 << index; } else { bits[index >> 5] &= ~(1 << index); } } } }
Uma instância da
BitArray
classe consome substancialmente menos memória do que uma correspondentebool[]
(uma vez que cada valor do primeiro ocupa apenas um bit em vez do do segundobyte
), mas permite as mesmas operações que umbool[]
.A seguinte classe
CountPrimes
usa umBitArray
e o algoritmo clássico de "peneira" para calcular a quantidade de números 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}"); } }
Note que a sintaxe para acessar elementos do
BitArray
é exatamente a mesma que para umbool[]
.O exemplo a seguir mostra uma classe de grade 26×10 que tem um indexador com dois parâmetros. O primeiro parâmetro deve ser uma letra maiúscula ou minúscula no intervalo de A a Z, e o segundo deve ser um inteiro no intervalo de 0 a 9.
class Grid { const int NumRows = 26; const int NumCols = 10; int[,] cells = new int[NumRows, NumCols]; public int this[char row, int col] { get { row = Char.ToUpper(row); if (row < 'A' || row > 'Z') { throw new ArgumentOutOfRangeException("row"); } if (col < 0 || col >= NumCols) { throw new ArgumentOutOfRangeException ("col"); } return cells[row - 'A', col]; } set { row = Char.ToUpper(row); if (row < 'A' || row > 'Z') { throw new ArgumentOutOfRangeException ("row"); } if (col < 0 || col >= NumCols) { throw new ArgumentOutOfRangeException ("col"); } cells[row - 'A', col] = value; } } }
Exemplo final
15.9.2 Indexadores e Diferenças de Propriedades
Indexadores e propriedades são muito semelhantes em conceito, mas diferem das seguintes maneiras:
- Uma propriedade é identificada pelo seu nome, enquanto um indexador é identificado pela sua assinatura.
- Uma propriedade é acedida através de um simple_name (§12.8.4) ou de um member_access (§12.8.7), enquanto um elemento indexador é acedido através de um element_access (§12.8.12.3).
- Uma propriedade pode ser um membro estático, enquanto um indexador é sempre um membro de instância.
- Um acessador get de uma propriedade corresponde a um método sem parâmetros, enquanto um acessor get de um indexador corresponde a um método com a mesma lista de parâmetros que o indexador.
- Um acessador de conjunto de uma propriedade corresponde a um método com um único parâmetro chamado
value
, enquanto um acessador conjunto de um indexador corresponde a um método com a mesma lista de parâmetros que o indexador, mais um parâmetro adicional chamadovalue
. - É um erro em tempo de compilação que um acessador de indexador declare uma variável local ou constante local com o mesmo nome de um parâmetro do indexador.
- Em uma declaração de propriedade sobreposta, 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]
, ondeE
é uma lista de expressões separadas por vírgula. - Não existe o conceito de um "indexador implementado automaticamente". É um erro ter um indexador não abstrato, não externo com corpos de acessador em ponto e vírgula accessor_body.
Além dessas diferenças, todas as regras definidas nos §15.7.3, §15.7.5 e §15.7.6 aplicam-se aos acessadores indexadores, bem como aos acessadores de propriedade.
Esta substituição de propriedade/propriedades por indexador/indexadores ao ler §15.7.3, §15.7.5 e §15.7.6 também se aplica a termos definidos. Especificamente, propriedade de leitura-gravação torna-se indexador de leitura-gravação, propriedade de somente leitura torna-se indexador de somente leitura, e propriedade de somente gravação torna-se indexador de somente gravação.
15.10 Operadores
15.10.1 Generalidades
Um operador é um membro que define o significado de um operador de expressão que pode ser aplicado a instâncias da classe. Os operadores são declarados utilizando operator_declarations:
operator_declaration
: attributes? operator_modifier+ operator_declarator operator_body
;
operator_modifier
: 'public'
| 'static'
| 'extern'
| unsafe_modifier // unsafe code support
;
operator_declarator
: unary_operator_declarator
| binary_operator_declarator
| conversion_operator_declarator
;
unary_operator_declarator
: type 'operator' overloadable_unary_operator '(' fixed_parameter ')'
;
logical_negation_operator
: '!'
;
overloadable_unary_operator
: '+' | '-' | logical_negation_operator | '~' | '++' | '--' | 'true' | 'false'
;
binary_operator_declarator
: type 'operator' overloadable_binary_operator
'(' fixed_parameter ',' fixed_parameter ')'
;
overloadable_binary_operator
: '+' | '-' | '*' | '/' | '%' | '&' | '|' | '^' | '<<'
| right_shift | '==' | '!=' | '>' | '<' | '>=' | '<='
;
conversion_operator_declarator
: 'implicit' 'operator' type '(' fixed_parameter ')'
| 'explicit' 'operator' type '(' fixed_parameter ')'
;
operator_body
: block
| '=>' expression ';'
| ';'
;
unsafe_modifier (§23.2) só está disponível em código não seguro (§23).
Nota: A negação lógica prefixada (§12.9.4) e os operadores de correção de nulidade pós-fixo (§12.8.9), embora representados pelo mesmo token lexical (!
), são distintos. Este último não é um operador sobrecarregável.
Nota final
Existem três categorias de operadores sobrecarregados: operadores unários (§15.10.2), operadores binários (§15.10.3) e operadores de conversão (§15.10.4).
O operator_body é ou um ponto e vírgula, ou um corpo de bloco (§15.6.1), ou um corpo de expressão (§15.6.1). Um corpo de bloco consiste em um bloco, que especifica as instruções a serem executadas quando o operador é invocado. O bloco deve estar em conformidade com as regras relativas aos métodos de retorno de valor descritas no ponto 15.6.11. Um corpo de expressão consiste em =>
, seguido por uma expressão e um ponto-e-vírgula, e representa uma única expressão a ser executada quando o operador é invocado.
Para os extern
operadores, o corpo_do_operador consiste simplesmente de um ponto e vírgula. Para todos os outros operadores, o operator_body é um corpo de bloco ou um corpo de expressão.
Aplicam-se as seguintes regras a todas as declarações de operador:
- A declaração do operador deve incluir tanto um modificador
public
como um modificadorstatic
. - Os parâmetros de um operador não devem ter modificadores além de
in
. - A assinatura de um operador (§15.10.2, §15.10.3, §15.10.4) deve diferir das assinaturas de todos os outros operadores declarados na mesma classe.
- Todos os tipos referidos numa declaração do operador devem ser pelo menos tão acessíveis como o próprio operador (ponto 7.5.5).
- É um erro para o mesmo modificador aparecer várias vezes em uma declaração de operador.
Cada categoria de operador impõe restrições adicionais, conforme descrito nas subcláusulas seguintes.
Como outros membros, os operadores declarados em uma classe base são herdados por classes derivadas. Como as declarações de operador sempre exigem que a classe ou estrutura na qual o operador é declarado participe da assinatura do operador, não é possível para um operador declarado em uma classe derivada ocultar um operador declarado em uma classe base. Assim, o new
modificador nunca é exigido e, portanto, nunca permitido, em uma declaração de operador.
Informações adicionais sobre operadores unários e binários podem ser encontradas no §12.4.
Para mais informações sobre os operadores de conversão, consultar o ponto 10.5.
15.10.2 Operadores unários
As seguintes regras aplicam-se às declarações de operador unárias, onde T
indica o tipo de instância da classe ou struct que contém a declaração do operador:
- Um operador unário
+
,-
,!
(apenas negação lógica) ou~
deve ter um único parâmetro de tipoT
ouT?
e pode retornar qualquer tipo. - Um operador unário
++
ou--
deve aceitar um único parâmetro de tipoT
ouT?
e devolver o mesmo tipo ou um tipo derivado dele. - O operador unário
true
oufalse
deve receber 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 operadores unários true
e false
exigem declaração em pares. Um erro em tempo de compilação ocorre se uma classe declara um desses operadores sem também declarar o outro. Os true
operadores e false
são descritos mais detalhadamente no §12.24.
Exemplo: O exemplo a seguir mostra uma implementação e o uso subsequente de operator++ para uma classe de vetor inteiro:
public class IntVector { public IntVector(int length) {...} public int Length { get { ... } } // Read-only property public int this[int index] { get { ... } set { ... } } // Read-write indexer public static IntVector operator++(IntVector iv) { IntVector temp = new IntVector(iv.Length); for (int i = 0; i < iv.Length; i++) { temp[i] = iv[i] + 1; } return temp; } } class Test { static void Main() { IntVector iv1 = new IntVector(4); // Vector of 4 x 0 IntVector iv2; iv2 = iv1++; // iv2 contains 4 x 0, iv1 contains 4 x 1 iv2 = ++iv1; // iv2 contains 4 x 2, iv1 contains 4 x 2 } }
Observe como o método do operador retorna o valor produzido adicionando 1 ao operando, assim como os operadores de incremento e decréscimo postfix (§12.8.16) e os operadores de incremento e decréscimo de prefixo (§12.9.6). Ao contrário do C++, este método não deve modificar o valor de seu operando diretamente, pois isso violaria a semântica padrão do operador de incremento postfix (§12.8.16).
Exemplo final
15.10.3 Operadores binários
As regras a seguir se aplicam a declarações de operador binário, onde T
denota o tipo de instância da classe ou struct que contém a declaração de operador:
- Um operador binário sem turno deve ter dois parâmetros, dos quais pelo menos um deve ter o tipo
T
ouT?
, e pode devolver qualquer tipo. - Um operador binário
<<
ou>>
(§12.11) deve ter dois parâmetros, o primeiro dos quais deve ter o tipoT
ouT?
e o segundo deve ter o tipoint
ouint?
, e pode retornar qualquer tipo.
A assinatura de um operador binário consiste no token do operador (+
, -
, *
, /
, %
, &
, |
, ^
, <<
, >>
, ==
, !=
, >
, <
, >=
, ou <=
) e os tipos dos dois parâmetros. O tipo de retorno e os nomes dos parâmetros não fazem parte da assinatura de um operador binário.
Certos operadores binários requerem declaração em pares. Para cada declaração de um operador de um par, deve existir uma declaração correspondente do outro operador do par. Duas declarações de operador correspondem se existirem conversões de identidade entre os seus tipos de retorno e os tipos dos parâmetros correspondentes. Os operadores a seguir exigem declaração em pares:
- operador
==
e operador!=
- operador
>
e operador<
- operador
>=
e operador<=
15.10.4 Operadores de conversão
Uma declaração de operador de conversão introduz uma conversão definida pelo utilizador (§10.5), que aumenta as conversões implícitas e explícitas predefinidas.
Uma declaração de operador de conversão que inclui a implicit
palavra-chave introduz uma conversão implícita definida pelo usuário. As conversões implícitas podem ocorrer em várias situações, incluindo invocações de membros de funções, expressões de tipo e atribuições. Isto é descrito mais pormenorizadamente no §10.2.
Uma declaração de operador de conversão que inclui a explicit
palavra-chave introduz uma conversão explícita definida pelo usuário. Conversões explícitas podem ocorrer em expressões de cast e são descritas mais detalhadamente em §10.3.
Um operador de conversão converte de um tipo de fonte, indicado pelo tipo de parâmetro do operador de conversão, para um tipo de destino, indicado pelo tipo de retorno do operador de conversão.
Para um determinado tipo S
de origem e tipo T
de destino, se S
ou T
são tipos de valor anuláveis, deixe S₀
e T₀
faça referência aos seus tipos subjacentes, caso contrário, S₀
e T₀
são iguais a S
e T
respectivamente. Uma classe ou struct tem permissão para declarar uma conversão de um tipo S
de origem para um tipo T
de destino somente se todos os itens a seguir forem verdadeiros:
S₀
eT₀
são tipos diferentes.Ou
S₀
ouT₀
é o tipo de instância da classe ou da struct que contém a declaração do operador.Nem
S₀
nemT₀
é uma interface_type.Excluindo conversões definidas pelo usuário, não existe uma conversão de
S
paraT
ou deT
paraS
.
Para os fins destas regras, quaisquer parâmetros de tipo associados com S
ou T
são considerados tipos exclusivos que não têm relação de herança com outros tipos, e quaisquer restrições sobre esses parâmetros de tipo são ignoradas.
Exemplo: No seguinte:
class C<T> {...} class D<T> : C<T> { public static implicit operator C<int>(D<T> value) {...} // Ok public static implicit operator C<string>(D<T> value) {...} // Ok public static implicit operator C<T>(D<T> value) {...} // Error }
As duas primeiras declarações de operador são permitidas porque
T
eint
string
, respectivamente, são consideradas tipos únicos sem relação. No entanto, o terceiro operador é um erro porqueC<T>
é a classe base deD<T>
.Exemplo final
A partir da segunda regra, segue-se que um operador de conversão deve converter para ou a partir do tipo de classe ou struct no qual o operador é declarado.
Exemplo: É possível que uma classe ou tipo
C
struct defina uma conversão deC
paraint
e deint
paraC
, mas não deint
parabool
. Exemplo final
Não é possível redefinir diretamente uma conversão predefinida. Assim, os operadores de conversão não podem converter de ou para object
porque já existem conversões implícitas e explícitas entre object
e todos os outros tipos. Da mesma forma, nem os tipos de origem nem de destino de uma conversão podem ser um tipo base da outra, uma vez que uma conversão já existiria. No entanto, é possível declarar operadores em tipos genéricos que, para argumentos de tipo específicos, especificam conversões que já existem como conversões predefinidas.
Exemplo:
struct Convertible<T> { public static implicit operator Convertible<T>(T value) {...} public static explicit operator T(Convertible<T> value) {...} }
Quando o tipo
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, consequentemente, também uma explícita de qualquer tipo para tipo objeto).Exemplo final
Nos casos em que existe uma conversão predefinida entre dois tipos, todas as conversões definidas pelo usuário entre esses tipos são ignoradas. Especificamente:
- Se existir uma conversão implícita predefinida (§10.2) de tipo
S
para tipoT
, todas as conversões definidas pelo utilizador (implícitas ou explícitas) deS
paraT
sã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 utilizador deS
paraT
serão ignoradas. Além disso:- Se qualquer um entre
S
ouT
for um tipo de interface, as conversões implícitas definidas pelo utilizador deS
paraT
serão ignoradas. - Caso contrário, as conversões implícitas definidas pelo usuário de
S
paraT
ainda são consideradas.
- Se qualquer um entre
Para todos os tipos, exceto object
, os operadores declarados Convertible<T>
pelo tipo acima não entram em conflito com conversões predefinidas.
Exemplo:
void F(int i, Convertible<int> n) { i = n; // Error i = (int)n; // User-defined explicit conversion n = i; // User-defined implicit conversion n = (Convertible<int>)i; // User-defined implicit conversion }
No entanto, para o tipo
object
, as conversões predefinidas ocultam as conversões definidas pelo usuário em todos os casos, exceto um:void F(object o, Convertible<object> n) { o = n; // Pre-defined boxing conversion o = (object)n; // Pre-defined boxing conversion n = o; // User-defined implicit conversion n = (Convertible<object>)o; // Pre-defined unboxing conversion }
Exemplo final
As conversões definidas pelo usuário não são permitidas para converter de ou para interface_types. Em particular, essa restrição garante que nenhuma transformação definida pelo usuário ocorra ao converter em um interface_type e que uma conversão em um interface_type seja bem-sucedida somente se o object
que está sendo convertido realmente implementar o interface_type especificado.
A assinatura de um operador de conversão consiste no tipo de origem e no tipo de destino. (Esta é a única forma de membro para a qual o tipo de retorno participa da assinatura.) A classificação implícita ou explícita de um operador de conversão não faz parte da assinatura do operador. Assim, uma classe ou struct não pode declarar um operador de conversão implícito e explícito com os mesmos tipos de origem e destino.
Nota: Em geral, as conversões implícitas definidas pelo usuário devem ser projetadas para nunca lançar exceções e nunca perder informações. Se uma conversão definida pelo usuário pode dar origem a exceções (por exemplo, porque o argumento de origem está fora do intervalo) ou perda de informações (como descartar bits de ordem alta), essa conversão deve ser definida como uma conversão explícita. Nota final
Exemplo: No seguinte código
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
parabyte
é implícita porque nunca lança exceções ou perde informações, mas a conversão debyte
paraDigit
é explícita, uma vez queDigit
só pode representar um subconjunto dos valores possíveis de umbyte
.Exemplo final
15.11 Construtores de instância
15.11.1 Generalidades
Um construtor de instância é um membro que implementa as ações necessárias para inicializar uma instância de uma classe. Os construtores de instância são declarados usando constructor_declarations:
constructor_declaration
: attributes? constructor_modifier* constructor_declarator constructor_body
;
constructor_modifier
: 'public'
| 'protected'
| 'internal'
| 'private'
| 'extern'
| unsafe_modifier // unsafe code support
;
constructor_declarator
: identifier '(' parameter_list? ')' constructor_initializer?
;
constructor_initializer
: ':' 'base' '(' argument_list? ')'
| ':' 'this' '(' argument_list? ')'
;
constructor_body
: block
| '=>' expression ';'
| ';'
;
unsafe_modifier (§23.2) só está disponível em código não seguro (§23).
Um constructor_declaration pode incluir um conjunto de atributos (§22), qualquer um dos tipos permitidos de acessibilidade declarada (§15.3.6) e um extern
modificador (§15.6.8). Uma declaração de construtor não tem permissão para incluir o mesmo modificador várias vezes.
O identificador de um constructor_declarator deve nomear a classe na qual o construtor da instância é declarado. Se qualquer outro nome for especificado, ocorrerá um erro em tempo de compilação.
O parameter_list opcional de um construtor de instância está sujeito às mesmas regras que o parameter_list de um método (§15.6). Como o this
modificador para parâmetros só se aplica a métodos de extensão (§15.6.10), nenhum parâmetro no parameter_list de um construtor deve conter o this
modificador. A lista de parâmetros define a assinatura (§7.6) de um construtor de instância e rege o processo pelo qual a resolução de sobrecarga (§12.6.4) seleciona um construtor de instância específico em uma invocação.
Cada um dos tipos referenciados no parameter_list de um construtor de instância deve ser pelo menos tão acessível quanto o próprio construtor (§7.5.5).
O constructor_initializer opcional especifica outro construtor de instância a ser invocado antes de executar as instruções fornecidas no constructor_body deste construtor de instância. Isto é descrito mais pormenorizadamente no §15.11.2.
Quando uma declaração de construtor inclui um extern
modificador, o construtor é dito ser um construtor externo. Porque uma declaração de construtor externo não oferece implementação real, o seu corpo_do_construtor consiste de um ponto e vírgula. Para todos os outros construtores, o constructor_body consiste em
- um bloco, que especifica as instruções para inicializar uma nova instância da classe;
- um corpo de expressão, que consiste em
=>
seguido por uma expressão e um ponto-e-vírgula, e denota uma única expressão para inicializar uma nova instância da classe.
Um constructor_body que é um corpo de bloco ou de expressão corresponde precisamente 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 de construtor
Todos os construtores de instância (exceto aqueles para classe object
) incluem implicitamente uma invocação de outro construtor de instância imediatamente antes do constructor_body. O construtor a invocar implicitamente é determinado pelo constructor_initializer:
- Um construtor de instância inicializador do formulário
base(
argument_list)
(onde argument_list é opcional) faz com que um construtor de instância da classe base direta seja invocado. Esse construtor é selecionado usando argument_list e as regras de resolução de sobrecarga do §12.6.4. O conjunto de construtores de instância candidatos consiste em todos os construtores de instância acessíveis da classe base direta. Se esse conjunto estiver vazio ou se um único construtor de melhor instância não puder ser identificado, ocorrerá um erro em tempo de compilação. - Um construtor de instância inicializador do formulário
this(
argument_list)
(onde argument_list é opcional) invoca outro construtor de instância da mesma classe. O construtor é selecionado usando argument_list e as regras de resolução de sobrecarga do §12.6.4. O conjunto de construtores de instância 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 através de uma cadeia de um ou mais inicializadores de construtor, ocorre um erro de tempo de compilação.
Se um construtor de instância não tem nenhum inicializador de construtor, um inicializador de construtor da forma base()
é implicitamente fornecido.
Nota: Assim, uma declaração de construtor de instância da forma
C(...) {...}
é exatamente equivalente a
C(...) : base() {...}
Nota final
O escopo dos parâmetros fornecidos pelo parameter_list de uma declaração de construtor de instância inclui o inicializador do construtor dessa declaração. Assim, um inicializador do construtor pode acessar os parâmetros do construtor.
Exemplo:
class A { public A(int x, int y) {} } class B: A { public B(int x, int y) : base(x + y, x - y) {} }
Exemplo final
Um inicializador do construtor de instância não pode acessar a instância que está sendo criada. Portanto, é um erro em tempo de compilação fazer referência a isso em uma expressão de argumento do inicializador do construtor, pois é um erro em tempo de compilação para uma expressão de argumento fazer referência a qualquer membro da instância por meio de um simple_name.
15.11.3 Inicializadores de variáveis de instância
Quando um construtor de instância não-externo não tem um inicializador de construtor, ou tem um inicializador de construtor do formulário base(...)
, esse construtor executa implicitamente as inicializações especificadas pelos variable_initializers dos campos de instância declarados em sua classe. Esta frase corresponde a uma sequência de atribuições que são executadas imediatamente aquando a entrada no construtor e antes da invocação implícita do construtor da 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).
Os inicializadores de variáveis não precisam ser executados por construtores de instância externa.
15.11.4 Execução do Construtor
Os inicializadores de variáveis são transformados em instruções de atribuição, e essas instruções de atribuição são executadas antes da invocação do construtor de instância de classe base. Essa ordenação garante que todos os campos de instância sejam inicializados por seus inicializadores variáveis antes que quaisquer instruções que tenham acesso a essa instância sejam executadas.
Exemplo: Dado o seguinte
class A { public A() { PrintFields(); } public virtual void PrintFields() {} } class B: A { int x = 1; int y; public B() { y = -1; } public override void PrintFields() => Console.WriteLine($"x = {x}, y = {y}"); }
Quando New
B()
é usado para criar uma instância deB
, a seguinte saída é produzida:x = 1, y = 0
O valor de
x
é 1 porque o inicializador da variável é executado antes que o construtor da instância da 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 basey
retorne. É útil pensar em inicializadores de variáveis de instância e inicializadores de construtores como instruções que são inseridas automaticamente antes do constructor_body. O 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 construtores de ambos os tipos (
base
ethis
). O exemplo corresponde ao código mostrado abaixo, onde cada comentário indica uma instrução inserida automaticamente (a sintaxe usada para as invocações do construtor inseridas automaticamente não é válida, mas serve apenas para ilustrar o mecanismo).class A { int x, y, count; public A() { x = 1; // Variable initializer y = -1; // Variable initializer object(); // Invoke object() constructor count = 0; } public A(int n) { x = 1; // Variable initializer y = -1; // Variable initializer object(); // Invoke object() constructor count = n; } } class B : A { double sqrt2; ArrayList items; int max; public B() : this(100) { B(100); // Invoke B(int) constructor items.Add("default"); } public B(int n) : base(n - 1) { sqrt2 = Math.Sqrt(2.0); // Variable initializer items = new ArrayList(100); // Variable initializer A(n - 1); // Invoke A(int) constructor max = n; } }
Exemplo final
15.11.5 Construtores padrão
Se uma classe não contiver declarações de construtor de instância, um construtor de instância padrão será fornecido automaticamente. Esse construtor padrão simplesmente invoca um construtor da classe base direta, como se tivesse um inicializador de construtor do formulário base()
. Se a classe for abstrata, a acessibilidade declarada para o construtor padrão será protegida. Caso contrário, a acessibilidade declarada para o construtor padrão é pública.
Nota: Assim, o construtor padrão é sempre da forma
protected C(): base() {}
ou
public C(): base() {}
onde
C
é o nome da classe.Nota final
Se a resolução de sobrecarga for incapaz de determinar um candidato único e melhor para o inicializador do construtor da classe base, ocorre um erro de tempo de compilação.
Exemplo: No seguinte código
class Message { object sender; string text; }
Um construtor padrão é fornecido porque a classe não contém declarações de construtor de instância. Assim, o exemplo é precisamente equivalente a
class Message { object sender; string text; public Message() : base() {} }
Exemplo final
15.12 Construtores estáticos
Um construtor estático é um membro que implementa as ações necessárias para inicializar uma classe fechada. Os construtores estáticos são declarados usando static_constructor_declarations:
static_constructor_declaration
: attributes? static_constructor_modifiers identifier '(' ')'
static_constructor_body
;
static_constructor_modifiers
: 'static'
| 'static' 'extern' unsafe_modifier?
| 'static' unsafe_modifier 'extern'?
| 'extern' 'static' unsafe_modifier?
| 'extern' unsafe_modifier 'static'
| unsafe_modifier 'static' 'extern'?
| unsafe_modifier 'extern' 'static'
;
static_constructor_body
: block
| '=>' expression ';'
| ';'
;
unsafe_modifier (§23.2) só está disponível em código não seguro (§23).
Um static_constructor_declaration pode incluir um conjunto de atributos (§22) e um extern
modificador (§15.6.8).
O identificador de um static_constructor_declaration deve nomear a classe na qual o construtor estático é declarado. Se qualquer outro nome for especificado, ocorrerá um erro em tempo de compilação.
Quando uma declaração de construtor estático inclui um extern
modificador, o construtor estático é dito ser um construtor estático externo. O motivo pelo qual uma declaração de construtor estático externo não fornece uma implementação real é porque o seu corpo_construtor_estático consiste em um ponto e vírgula. Para todas as outras declarações de construtor estático, o static_constructor_body consiste em
- um bloco, que especifica as instruções a serem executadas para inicializar a classe;
- Um corpo de expressão
=>
, que consiste de uma expressão seguida por um ponto e vírgula, e denota uma única expressão a ser executada para inicializar a classe.
Um static_constructor_body que é um bloco ou corpo de expressão corresponde exatamente ao method_body de um método estático com um void
tipo de retorno (§15.6.11).
Construtores estáticos não são herdados e não podem ser chamados diretamente.
O construtor estático para uma classe fechada é executado no máximo uma vez em um determinado domínio de aplicativo. A execução de um construtor estático é acionada pelo primeiro dos seguintes eventos a ocorrer dentro de um domínio de aplicativo:
- Uma instância da classe é criada.
- Qualquer um dos membros estáticos da classe é referenciado.
Se uma classe contém o Main
método (§7.1) em que a execução começa, o construtor estático dessa classe é executado antes que o Main
método seja chamado.
Para inicializar um novo tipo de classe fechada, primeiro deve ser criado um novo conjunto de campos estáticos (§15.5.2) para esse tipo fechado específico. Cada um dos campos estáticos deve ser inicializado com o seu valor predefinido (§15.5.5). Na sequência:
- Se não houver nenhum construtor estático ou um construtor estático não-externo, então:
- os inicializadores de campos estáticos (§15.5.6.2) devem ser executados para esses campos estáticos;
- Em seguida, deve ser executado o construtor estático não externo, se houver.
- Caso contrário, se houver um construtor estático externo, ele deve ser executado. Os inicializadores de variáveis estáticas não precisam ser executados por construtores estáticos externos.
Exemplo: O exemplo
class Test { static void Main() { A.F(); B.F(); } } class A { static A() { Console.WriteLine("Init A"); } public static void F() { Console.WriteLine("A.F"); } } class B { static B() { Console.WriteLine("Init B"); } public static void F() { Console.WriteLine("B.F"); } }
deve produzir a saída:
Init A A.F Init B B.F
porque a execução do construtor estático de
A
é acionada pela chamada paraA.F
, e a execução do construtor estático deB
é acionada pela chamada paraB.F
.Exemplo final
É possível construir dependências circulares que permitem que campos estáticos com inicializadores variáveis sejam observados em seu estado de valor padrão.
Exemplo: O exemplo
class A { public static int X; static A() { X = B.Y + 1; } } class B { public static int Y = A.X + 1; static B() {} static void Main() { Console.WriteLine($"X = {A.X}, Y = {B.Y}"); } }
produz a saída
X = 1, Y = 2
Para executar o
Main
método, o sistema primeiro executa o inicializador paraB.Y
, antes do construtor estático da classeB
.Y
faz com que o construtor doA
'sstatic
seja executado porque o valor deA.X
é referenciado. O construtor estático deA
, por sua vez, prossegue para calcular o valor deX
, e, ao fazer isso, obtém o valor padrão deY
, que é zero.A.X
é, portanto, inicializado para 1. O processo de execuçãoA
dos inicializadores de campo estático e do construtor estático então é concluído, retornando ao cálculo do valor inicial deY
, cujo resultado se torna 2.Exemplo final
Como o construtor estático é executado exatamente uma vez para cada tipo de classe construída fechada, é um local conveniente para impor verificações em tempo de execução no parâmetro type que não pode ser verificado em tempo de compilação por meio de restrições (§15.2.5).
Exemplo: O tipo a seguir usa um construtor estático para impor que o argumento type é um enum:
class Gen<T> where T : struct { static Gen() { if (!typeof(T).IsEnum) { throw new ArgumentException("T must be an enum"); } } }
Exemplo final
15.13 Finalizadores
Nota: Em uma versão anterior desta especificação, o que agora é referido como um "finalizador" era chamado de "destruidor". A experiência tem mostrado que o termo "destruidor" causou confusão e muitas vezes resultou em expectativas incorretas, especialmente para programadores que conhecem C++. Em C++, um destruidor é chamado de maneira determinada, enquanto que, em C#, um finalizador não é. Para obter um comportamento previsível do C#, deve 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) só está disponível em código não seguro (§23).
Um finalizer_declaration pode incluir um conjunto de atributos (§22).
O identificador de um finalizer_declarator designa a classe em que o finalizador é declarado. Se qualquer outro nome for especificado, ocorrerá um erro em tempo de compilação.
Quando uma declaração de finalizador inclui um extern
modificador, diz-se que o finalizador é um finalizador externo. Como uma declaração de finalizador externo não fornece uma implementação real, o seu finalizer_body consiste de 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 de=>
seguido por uma expressão e um ponto-e-vírgula, e denota uma expressão única a ser executada para finalizar uma instância da classe.
Um finalizer_body que é um bloco ou corpo de expressão corresponde exatamente ao method_body de um método de instância com um void
tipo de retorno (§15.6.11).
Finalizadores não são herdados. Assim, uma classe não tem finalizadores além dos que podem ser declarados nessa própria classe.
Nota: Como um finalizador é obrigado a não ter parâmetros, ele não pode ser sobrecarregado, então uma classe pode ter, no máximo, um finalizador. Nota final
Os finalizadores são invocados automaticamente e não podem ser invocados explicitamente. Uma instância torna-se elegível para finalização quando não é mais possível para qualquer código usar essa instância. A execução do finalizador para a instância pode ocorrer a qualquer momento após a instância se tornar elegível para finalização (§7.9). Quando uma instância é finalizada, os finalizadores na cadeia de herança dessa instância são chamados, em ordem, do mais derivado ao menos derivado. Um finalizador pode ser executado em qualquer thread. Para uma discussão mais aprofundada das regras que regem quando e como um finalizador é executado, consulte §7.9.
Exemplo: A saída do exemplo
class A { ~A() { Console.WriteLine("A's finalizer"); } } class B : A { ~B() { Console.WriteLine("B's finalizer"); } } class Test { static void Main() { B b = new B(); b = null; GC.Collect(); GC.WaitForPendingFinalizers(); } }
é
B's finalizer A's finalizer
Sendo que os finalizadores numa cadeia de herança são chamados na ordem, do mais derivado para o menos derivado.
Exemplo final
Os finalizadores são implementados substituindo o método Finalize
virtual no System.Object
. Os programas C# não estão autorizados a redefinir este método nem a chamá-lo (ou suas redefinições) diretamente.
Exemplo: Por exemplo, o programa
class A { override protected void Finalize() {} // Error public void F() { this.Finalize(); // Error } }
contém dois erros.
Exemplo final
Um compilador deve comportar-se como se este método, e as suas substituições, não existissem.
Exemplo: Assim, este programa:
class A { void Finalize() {} // Permitted }
é válido e o método mostrado esconde o método de
System.Object
Finalize
.Exemplo final
Para uma discussão sobre o comportamento quando uma exceção é lançada por um finalizador, consulte §21.4.
15.14 Funções assíncronas
15.14.1 Generalidades
Um método (§15.6) ou função anónima (§12.19) com o modificador async
é 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 em tempo de compilação para a lista de parâmetros de uma função assíncrona especificar qualquer in
, out
, ou parâmetros de ref
, ou qualquer parâmetro de tipo ref struct
.
O return_type de um método assíncrono deve ser void
um tipo de tarefa ou um tipo de iterador assíncrono (§15.15). Para um método assíncrono que produza um valor de resultado, um tipo de tarefa ou um tipo de iterador assíncrono (§15.15.3) deve ser genérico. Para um método assíncrono que não produz um valor de resultado, um tipo de tarefa não deve ser genérico. Estes tipos são referidos na presente especificação como «TaskType»<T>
e «TaskType»
, respetivamente. O tipo de biblioteca padrão System.Threading.Tasks.Task
e os tipos construídos a partir de System.Threading.Tasks.Task<TResult>
e System.Threading.Tasks.ValueTask<T>
são tipos de tarefa, bem como um tipo de classe, struct ou interface que está associado a um tipo de construtor de tarefas através do atributo System.Runtime.CompilerServices.AsyncMethodBuilderAttribute
. Esses tipos são referidos na presente especificação como «TaskBuilderType»<T>
e «TaskBuilderType»
. Um tipo de tarefa pode ter no máximo um parâmetro type e não pode ser aninhado em um tipo genérico.
Um método assíncrono que devolve um tipo de tarefa é dito ser task-returning.
Os tipos de tarefa podem variar em sua definição exata, mas do ponto de vista da linguagem, um tipo de tarefa está em um dos estados incompleto, bem-sucedido ou com falha. Uma tarefa com defeito registra uma exceção pertinente. Um sucedido«TaskType»<T>
regista um resultado do tipo T
. Os tipos de tarefas são aguardáveis, e as tarefas podem, portanto, 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 final
Um tipo de construtor de tarefas é um tipo de classe ou estrutura que corresponde a um tipo de tarefa específico (§15.14.2). O tipo de construtor de tarefas deve corresponder exatamente à acessibilidade declarada do tipo de tarefa correspondente.
Nota: Se o tipo de tarefa for declarado
internal
, o tipo de construtor correspondente também deve ser declaradointernal
e definido no mesmo assembly. Se o tipo de tarefa estiver aninhado dentro de outro tipo, o tipo de construtor de tarefas também deve estar aninhado nesse mesmo tipo. Nota final
Uma função assíncrona tem a capacidade de suspender a avaliação por meio de expressões await (§12.9.8) no seu corpo. A avaliação pode ser retomada posteriormente no ponto da expressão de espera suspensa por meio de um delegado de retomada. O delegado de continuação é do tipo System.Action
, e, quando invocado, a avaliação da invocação da função assíncrona retoma a partir da expressão await onde foi interrompida. O chamador atual de uma invocação de função assíncrona é o chamador original se a invocação da função nunca tiver sido suspensa ou o chamador mais recente do delegado de retomada de outra forma.
15.14.2 Padrão de construção 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 construtores de tarefas não genéricos, SetResult
não tem parâmetros) com acessibilidade declarada public
:
class «TaskBuilderType»<T>
{
public static «TaskBuilderType»<T> Create();
public void Start<TStateMachine>(ref TStateMachine stateMachine)
where TStateMachine : IAsyncStateMachine;
public void SetStateMachine(IAsyncStateMachine stateMachine);
public void SetException(Exception exception);
public void SetResult(T result);
public void AwaitOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : INotifyCompletion
where TStateMachine : IAsyncStateMachine;
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine;
public «TaskType»<T> Task { get; }
}
Um compilador deve gerar código que usa o «TaskBuilderType» para implementar a semântica de suspender e retomar a avaliação da função assíncrona. Um compilador deve usar o «TaskBuilderType» da seguinte forma:
-
«TaskBuilderType».Create()
é invocado para criar uma instância do «TaskBuilderType», 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()
dentro deStart()
ou apósStart()
ter retornado para avançar a máquina de estados.
- O construtor deve chamar
- Após
Start()
voltar, o métodoasync
invocabuilder.Task
para que a tarefa retorne do método assíncrono. - Cada chamada para
stateMachine.MoveNext()
irá avançar a máquina de estados. - Se a máquina de estados for concluída com êxito,
builder.SetResult()
será chamada, com o valor de retorno do método, se houver algum. - Caso contrário, se uma exceção
e
for lançada na máquina de estados,builder.SetException(e)
será chamado. - Se a máquina de estado atingir uma
await expr
expressão,expr.GetAwaiter()
será invocada. - Se o esperador implementa
ICriticalNotifyCompletion
eIsCompleted
é falso, a máquina de estado invocabuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine)
.-
AwaitUnsafeOnCompleted()
deve chamarawaiter.UnsafeOnCompleted(action)
com umAction
que chamastateMachine.MoveNext()
quando o awaiter concluir.
-
- Caso contrário, a máquina de estado invoca
builder.AwaitOnCompleted(ref awaiter, ref stateMachine)
.-
AwaitOnCompleted()
deve chamarawaiter.OnCompleted(action)
com umAction
que chamastateMachine.MoveNext()
quando o awaiter concluir.
-
-
SetStateMachine(IAsyncStateMachine)
pode ser chamado pela implementaçãoIAsyncStateMachine
gerada pelo compilador para identificar a instância do construtor associada a uma instância de máquina de estado, particularmente para casos em que a máquina de estado é implementada como um tipo de valor.- Se o construtor chamar
stateMachine.SetStateMachine(stateMachine)
, ostateMachine
chamarábuilder.SetStateMachine(stateMachine)
na instância do construtor associadastateMachine
.
- Se o construtor chamar
Nota: Tanto para
SetResult(T result)
como para«TaskType»<T> Task { get; }
, o parâmetro e o argumento, respectivamente, devem ser conversíveis por identidade emT
. Isto permite que um construtor de tipo de tarefa suporte tipos como tuplas, onde dois tipos que não são iguais são conversíveis por identidade. Nota final
15.14.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 (ao alcançar uma expressão await) ou termine, momento em que o controlo é devolvido ao chamador, juntamente com a tarefa de retorno.
Quando o corpo da função assíncrona termina, a tarefa de retorno sai do estado incompleto.
- Se o corpo da função termina ao atingir uma instrução de retorno ou o final do corpo, qualquer valor de resultado é guardado na tarefa de retorno, que é colocada em um estado de sucesso.
- Se o corpo da função termina devido a uma exceção não capturada
OperationCanceledException
, a exceção é registada na tarefa de retorno, que é colocada no estado cancelado. - Se o corpo da função terminar como resultado de qualquer outra exceção não detetada (§13.10.6), a exceção é registada na tarefa de retorno, que é colocada num estado de falha.
15.14.4 Avaliação de uma função assíncrona de retorno de vazio
Se o tipo de retorno da função assíncrona for void
, a avaliação difere da acima da seguinte maneira: Como nenhuma tarefa é retornada, a função comunica a conclusão e as exceções ao contexto de sincronização do thread atual. A definição exata do contexto de sincronização depende da implementação, mas é uma representação de "onde" o thread atual está sendo executado. O contexto de sincronização é notificado quando a avaliação de uma função assíncrona que retorna void
é iniciada, é concluída com êxito, ou causa o lançamento de uma exceção não detetada.
Isto permite que o contexto acompanhe quantas funções assíncronas que retornam void
estão em execução sob ele, e decida como propagar as exceções que emanam delas.
15.15 Iteradores síncronos e assíncronos
15.15.1 Generalidades
Um membro da função (§12.6) ou função local (§13.6.4) implementado usando um bloco iterador (§13.3) é chamado de iterador. Um bloco iterador pode ser usado como o corpo de um membro da função, desde que o tipo de retorno do membro da função correspondente seja uma das interfaces do enumerador (§15.15.2) ou uma das interfaces enumeráveis (§15.15.3).
Uma função assíncrona (§15.14) implementada usando um bloco iterador (§13.3) é chamada de iterador assíncrono. Um bloco iterador assíncrono pode ser usado como o corpo de um membro da função, desde que o tipo de retorno do membro da função correspondente seja as interfaces do enumerador assíncrono (§15.15.2) ou as interfaces enumeráveis assíncronas (§15.15.3).
Um bloco iterador 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 síncronos ou assíncronos.
Quando um membro de função ou uma função local é implementado usando um bloco iterador, é um erro em tempo de compilação que a lista de parâmetros especifique qualquer in
, out
ou ref
parâmetro, ou um parâmetro de tipo ref struct
.
15.15.2 Interfaces do enumerador
As interfaces do enumerador são a interface System.Collections.IEnumerator
não genérica e todas as instanciações das interfaces System.Collections.Generic.IEnumerator<T>
genéricas.
As interfaces do enumerador assíncrono são todas as instanciações da interface System.Collections.Generic.IAsyncEnumerator<T>
genérica.
Por uma questão de brevidade, nesta subcláusula e seus irmãos essas interfaces são referenciadas como IEnumerator
, IEnumerator<T>
, e IAsyncEnumerator<T>
, respectivamente.
15.15.3 Interfaces enumeráveis
As interfaces enumeráveis são a interface System.Collections.IEnumerable
não genérica e todas as instanciações das interfaces System.Collections.Generic.IEnumerable<T>
genéricas.
As interfaces enumeráveis assíncronas são todas as instanciações da interface System.Collections.Generic.IAsyncEnumerable<T>
genérica.
Por uma questão de brevidade, nesta subcláusula e seus irmãos essas interfaces são referenciadas como IEnumerable
, IEnumerable<T>
, e IAsyncEnumerable<T>
, respectivamente.
15.15.4 Tipo de rendimento
Um iterador produz uma sequência de valores, todos do mesmo tipo. Este tipo é chamado de tipo de rendimento do iterador.
- O tipo de rendimento de um iterador que retorna
IEnumerator
ouIEnumerable
éobject
. - O tipo de rendimento de um iterador que retorna um
IEnumerator<T>
,IAsyncEnumerator<T>
,IEnumerable<T>
, ouIAsyncEnumerable<T>
éT
.
15.15.5 Objetos do enumerador
15.15.5.1 Generalidades
Quando um membro da função ou função local que retorna um tipo de interface do enumerador é implementado usando um bloco iterador, invocar a função não executa imediatamente o código no bloco iterador. Em vez disso, um objeto enumerador é criado e retornado. Este objeto encapsula o código especificado no bloco do iterador, e a execução do código nesse bloco ocorre quando o método MoveNext
ou MoveNextAsync
do objeto enumerador é invocado. Um objeto enumerador tem as seguintes características:
- Ele implementa
System.IDisposable
,IEnumerator
eIEnumerator<T>
, ouSystem.IAsyncDisposable
eIAsyncEnumerator<T>
, ondeT
é o tipo de resultado do iterador. - Ele é inicializado com uma cópia dos valores de argumento (se houver) e valor de instância passado para o membro da função.
- Tem quatro estados potenciais, antes, em execução, suspenso e depois, e está inicialmente no estado anterior.
Um objeto enumerador é normalmente uma instância de uma classe de enumerador gerada pelo compilador que encapsula o código no bloco iterador e implementa as interfaces do enumerador, mas outros métodos de implementação são possíveis. Se uma classe de enumerador for gerada pelo compilador, essa classe será aninhada, direta ou indiretamente, na classe que contém o membro da função, terá acessibilidade privada e terá um nome reservado para uso do compilador (§6.4.3).
Um objeto enumerador pode implementar mais interfaces do que as especificadas acima.
As subcláusulas a seguir descrevem o comportamento necessário do membro para avançar o enumerador, recuperar o valor atual do enumerador e descartar os recursos usados pelo enumerador. Eles são definidos nos seguintes membros para enumeradores síncronos e assíncronos, respectivamente:
- Para avançar o recenseador:
MoveNext
eMoveNextAsync
. - Para recuperar o valor atual:
Current
. - Para dispor de recursos:
Dispose
eDisposeAsync
.
Objetos enumerador não suportam o IEnumerator.Reset
método. Invocar este método causa a ocorrência de uma exceção System.NotSupportedException
.
Os blocos de iterador síncrono e assíncrono diferem na medida em que os membros do iterador assíncrono retornam tipos de tarefas e podem ser aguardados.
15.15.5.2 Adiantar o recenseador
Os métodos MoveNext
e MoveNextAsync
de um objeto enumerador encapsulam o código de um bloco iterador. Invocar o método MoveNext
ou MoveNextAsync
executa código no bloco do iterador e define apropriadamente a propriedade Current
do objeto enumerador.
MoveNext
Devolve um bool
valor cujo significado é descrito abaixo.
MoveNextAsync
retorna um ValueTask<bool>
(§15.14.3). Valor de resultado da tarefa retornado de MoveNextAsync
tem o mesmo significado que o valor de resultado do MoveNext
. Na descrição a seguir, as ações descritas para MoveNext
aplicam-se a MoveNextAsync
com a seguinte diferença: Onde declarado que MoveNext
retorna true
ou false
, MoveNextAsync
define a sua tarefa para o estado concluído e define o valor do resultado da tarefa como o valor correspondente true
ou false
.
A ação precisa executada por MoveNext
ou MoveNextAsync
depende do estado do objeto enumerador quando invocado:
- Se o estado do objeto enumerador for antes, invocando
MoveNext
:- Altera o estado para running.
- Inicializa os parâmetros (incluindo
this
) do bloco iterador para os valores de argumento e valor de instância salvos quando o objeto enumerador foi inicializado. - Executa o bloco iterador desde o início até que a execução seja interrompida (conforme descrito abaixo).
- Se o estado do objeto enumerador estiver em execução, o resultado da invocação
MoveNext
não será especificado. - Se o estado do objeto enumerador estiver suspenso, invocando MoveNext:
- Altera o estado para 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 iterador foi suspensa pela última vez.Nota: O conteúdo de quaisquer objetos referenciados por essas variáveis pode ter sido alterado desde a chamada anterior para
MoveNext
. Nota final - Retoma a execução do bloco de 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 depois, ao invocar
MoveNext
, este retornará false.
Quando MoveNext
executa o bloco iterador, a execução pode ser interrompida de quatro maneiras: por uma yield return
instrução, por uma yield break
instrução, ao encontrar o final do bloco iterador e por uma exceção que é lançada e propagada para fora do bloco iterador.
- Quando uma
yield return
declaração é encontrada (§9.4.4.20):- A expressão dada na instrução é avaliada, implicitamente convertida para o tipo de rendimento e atribuída à
Current
propriedade do objeto enumerador. - A execução do corpo do iterador foi suspensa. Os valores de todas as variáveis e parâmetros locais (incluindo
this
) são salvos, assim como o local destayield return
instrução. Se a instrução estiver dentro de um ou mais blocosyield return
try
, os blocos finais associados não serão executados no momento. - O estado do objeto enumerador é alterado para suspenso.
- O
MoveNext
método retornatrue
ao seu chamador, indicando que a iteração avançou com êxito para o próximo valor.
- A expressão dada na instrução é avaliada, implicitamente convertida para o tipo de rendimento e atribuída à
- Quando uma
yield break
declaração é encontrada (§9.4.4.20):- Se a instrução
yield break
estiver dentro de um ou mais blocostry
, os blocosfinally
associados serão executados. - O estado do objeto enumerador é alterado para depois.
- O
MoveNext
método retornafalse
ao seu chamador, indicando que a iteração está concluída.
- Se a instrução
- Quando a parte final do corpo do iterador é encontrada:
- O estado do objeto enumerador é alterado para depois.
- O
MoveNext
método retornafalse
ao seu chamador, indicando que a iteração está concluída.
- Quando uma exceção é lançada e propagada para fora do bloco iterador:
- Os blocos apropriados
finally
serão executados dentro do corpo do iterador pela propagação de exceções. - O estado do objeto enumerador é alterado para depois.
- A propagação da exceção continua para o chamador do
MoveNext
método.
- Os blocos apropriados
15.15.5.3 Recuperar o valor atual
A propriedade Current
de um objeto enumerador é afetada por instruções yield return
no bloco do iterador.
Nota: A
Current
propriedade é uma propriedade síncrona para objetos iteradores síncronos e assíncronos. Nota final
Quando um objeto enumerador está no estado suspenso , o valor de Current
é o valor definido pela chamada anterior como MoveNext
. Quando um objeto enumerador está nos estados antes, em execução ou depois , o resultado do acesso Current
não é especificado.
Para um iterador com um tipo de rendimento diferente de object
, o resultado de aceder a Current
através da implementação de IEnumerable
do objeto enumerador corresponde a aceder a Current
através da implementação de IEnumerator<T>
do objeto enumerador e a converter o resultado para object
.
15.15.5.4 Dispor dos recursos
O Dispose
método or DisposeAsync
é usado para limpar a iteração trazendo o objeto enumerador para o estado after .
- Se o estado do objeto enumerador for antes, invocar
Dispose
alterará o estado para depois. - Se o estado do objeto enumerador estiver em execução, o resultado da invocação
Dispose
não será especificado. - Se o estado do objeto enumerador estiver suspenso, invocando
Dispose
:- Altera o estado para running.
- Executa quaisquer blocos finais 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 depois 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
Dispose
invocação não terá nenhum efeito.
15.15.6 Objetos enumeráveis
15.15.6.1 Generalidades
Quando um membro da função ou função local que retorna um tipo de interface enumerável é implementado usando um bloco iterador, invocar o membro da função não executa imediatamente o código no bloco iterador. Em vez disso, um objeto enumerável é criado e retornado.
O método GetEnumerator
ou GetAsyncEnumerator
do 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 MoveNext
ou MoveNextAsync
do objeto enumerador é invocado. Um objeto enumerável tem as seguintes características:
- Ele implementa
IEnumerable
eIEnumerable<T>
ouIAsyncEnumerable<T>
, ondeT
é o tipo de rendimento do iterador. - Ele é inicializado com uma cópia dos valores de argumento (se houver) e valor de instância passado para o membro da função.
Um objeto enumerável é normalmente uma instância de uma classe enumerável gerada pelo compilador que encapsula o código no bloco iterador e implementa as interfaces enumeráveis, mas outros métodos de implementação são possíveis. Se uma classe enumerável for gerada pelo compilador, essa classe será aninhada, direta ou indiretamente, na classe que contém o membro da função, terá acessibilidade privada e terá um nome reservado para uso do compilador (§6.4.3).
Um objeto enumerável pode implementar mais interfaces do que as especificadas acima.
Nota: Por exemplo, um objeto enumerável também pode implementar
IEnumerator
eIEnumerator<T>
, permitindo que ele sirva como enumerável e enumerador. Normalmente, esta implementação irá devolver a sua própria instância (para poupar transferências de memória) na primeira chamada paraGetEnumerator
. Invocações subsequentes deGetEnumerator
, se houver, retornariam uma nova instância de classe, normalmente da mesma classe, para que as chamadas para diferentes instâncias de enumerador não afetem umas às outras. Ele não pode retornar a mesma instância, mesmo que o enumerador anterior já tenha enumerado após o final da sequência, uma vez que todas as chamadas futuras para um enumerador esgotado devem lançar exceções. Nota final
15.15.6.2 O método GetEnumerator ou GetAsyncEnumerator
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.15.5.
Um objeto enumerável assíncrono fornece uma implementação do GetAsyncEnumerator
método da IAsyncEnumerable<T>
interface. Esse método retorna um objeto enumerador assíncrono 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.15.5.
ECMA C# draft specification